diff --git a/MixpanelDemo/MixpanelDemo/ActionCompleteViewController.swift b/MixpanelDemo/MixpanelDemo/ActionCompleteViewController.swift index 238d34d9..c300fc9e 100644 --- a/MixpanelDemo/MixpanelDemo/ActionCompleteViewController.swift +++ b/MixpanelDemo/MixpanelDemo/ActionCompleteViewController.swift @@ -9,35 +9,35 @@ import UIKit class ActionCompleteViewController: UIViewController { - @IBOutlet weak var popupView: UIView! - @IBOutlet weak var actionLabel: UILabel! - @IBOutlet weak var descLabel: UILabel! - var actionStr: String? - var descStr: String? + @IBOutlet weak var popupView: UIView! + @IBOutlet weak var actionLabel: UILabel! + @IBOutlet weak var descLabel: UILabel! + var actionStr: String? + var descStr: String? - override func viewDidLoad() { - super.viewDidLoad() + override func viewDidLoad() { + super.viewDidLoad() - popupView.clipsToBounds = true - popupView.layer.cornerRadius = 6 + popupView.clipsToBounds = true + popupView.layer.cornerRadius = 6 - let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - view.addGestureRecognizer(tap) + let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + view.addGestureRecognizer(tap) - actionLabel.text = actionStr - descLabel.text = descStr + actionLabel.text = actionStr + descLabel.text = descStr + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + self.dismiss(animated: true, completion: nil) } + } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - DispatchQueue.main.asyncAfter(deadline: .now() + 5) { - self.dismiss(animated: true, completion: nil) - } - } - - @objc func handleTap(gesture: UITapGestureRecognizer) { - dismiss(animated: true, completion: nil) - } + @objc func handleTap(gesture: UITapGestureRecognizer) { + dismiss(animated: true, completion: nil) + } } diff --git a/MixpanelDemo/MixpanelDemo/AppDelegate.swift b/MixpanelDemo/MixpanelDemo/AppDelegate.swift index a2906a53..22f63c64 100644 --- a/MixpanelDemo/MixpanelDemo/AppDelegate.swift +++ b/MixpanelDemo/MixpanelDemo/AppDelegate.swift @@ -6,22 +6,23 @@ // Copyright © 2016 Mixpanel. All rights reserved. // -import UIKit import Mixpanel +import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - - var window: UIWindow? - - func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { - var ADD_YOUR_MIXPANEL_TOKEN_BELOW_🛠🛠🛠🛠🛠🛠: String - let mixpanelOptions = MixpanelOptions(token: "MIXPANEL_TOKEN", trackAutomaticEvents: true) - Mixpanel.initialize(options: mixpanelOptions) - Mixpanel.mainInstance().loggingEnabled = true - return true - } + var window: UIWindow? + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + var ADD_YOUR_MIXPANEL_TOKEN_BELOW_🛠🛠🛠🛠🛠🛠: String + let mixpanelOptions = MixpanelOptions(token: "MIXPANEL_TOKEN", trackAutomaticEvents: true) + Mixpanel.initialize(options: mixpanelOptions) + Mixpanel.mainInstance().loggingEnabled = true + + return true + } } - diff --git a/MixpanelDemo/MixpanelDemo/GDPRViewController.swift b/MixpanelDemo/MixpanelDemo/GDPRViewController.swift index 35e0cf74..7a004ec9 100644 --- a/MixpanelDemo/MixpanelDemo/GDPRViewController.swift +++ b/MixpanelDemo/MixpanelDemo/GDPRViewController.swift @@ -6,80 +6,90 @@ // Copyright © 2018 Mixpanel. All rights reserved. // -import UIKit import Mixpanel +import UIKit class GDPRViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { - - @IBOutlet weak var tableView: UITableView! - var tableViewItems = ["Opt Out", - "Check Opted Out Flag", - "Opt In", - "Opt In w DistinctId", - "Opt In w DistinctId & Properties", - "Init with default opt-out", - "Init with default opt-in" - ] - - override func viewDidLoad() { - super.viewDidLoad() - tableView.delegate = self - tableView.dataSource = self - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell")! as UITableViewCell - cell.textLabel?.text = tableViewItems[indexPath.item] - cell.textLabel?.textColor = #colorLiteral(red: 0.200000003, green: 0.200000003, blue: 0.200000003, alpha: 1) - return cell - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - let actionStr = tableViewItems[indexPath.item] - var descStr = "" - - switch indexPath.item { - case 0: - Mixpanel.mainInstance().optOutTracking() - descStr = "Opted out" - case 1: - descStr = "Opt-out flag is \(Mixpanel.mainInstance().hasOptedOutTracking())" - case 2: - Mixpanel.mainInstance().optInTracking() - descStr = "Opted In" - case 3: - Mixpanel.mainInstance().optInTracking(distinctId: "aDistinctIdForOptIn") - descStr = "Opt In with distinctId 'aDistinctIdForOptIn'" - case 4: - let p: Properties = ["a": 1, - "b": 2.3, - "c": ["4", 5] as [Any], - "d": URL(string:"https://mixpanel.com")!, - "e": NSNull(), - "f": Date()] - Mixpanel.mainInstance().optInTracking(distinctId: "aDistinctIdForOptIn", properties: p) - descStr = "Opt In with distinctId 'aDistinctIdForOptIn' and \(p)" - case 5: - Mixpanel.initialize(token: "testtoken", trackAutomaticEvents: true, optOutTrackingByDefault: true) - descStr = "Init Mixpanel with default opt-out(sample only), to make it work, place it in your startup stage of your app" - case 6: - Mixpanel.initialize(token: "testtoken", trackAutomaticEvents: true, optOutTrackingByDefault: false) - descStr = "Init Mixpanel with default opt-in(sample only), to make it work, place it in your startup stage of your app" - default: - break - } - - let vc = storyboard!.instantiateViewController(withIdentifier: "ActionCompleteViewController") as! ActionCompleteViewController - vc.actionStr = actionStr - vc.descStr = descStr - vc.modalTransitionStyle = UIModalTransitionStyle.crossDissolve - vc.modalPresentationStyle = UIModalPresentationStyle.overFullScreen - present(vc, animated: true, completion: nil) - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return tableViewItems.count + + @IBOutlet weak var tableView: UITableView! + var tableViewItems = [ + "Opt Out", + "Check Opted Out Flag", + "Opt In", + "Opt In w DistinctId", + "Opt In w DistinctId & Properties", + "Init with default opt-out", + "Init with default opt-in", + ] + + override func viewDidLoad() { + super.viewDidLoad() + tableView.delegate = self + tableView.dataSource = self + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell")! as UITableViewCell + cell.textLabel?.text = tableViewItems[indexPath.item] + cell.textLabel?.textColor = #colorLiteral( + red: 0.200000003, green: 0.200000003, blue: 0.200000003, alpha: 1) + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let actionStr = tableViewItems[indexPath.item] + var descStr = "" + + switch indexPath.item { + case 0: + Mixpanel.mainInstance().optOutTracking() + descStr = "Opted out" + case 1: + descStr = "Opt-out flag is \(Mixpanel.mainInstance().hasOptedOutTracking())" + case 2: + Mixpanel.mainInstance().optInTracking() + descStr = "Opted In" + case 3: + Mixpanel.mainInstance().optInTracking(distinctId: "aDistinctIdForOptIn") + descStr = "Opt In with distinctId 'aDistinctIdForOptIn'" + case 4: + let p: Properties = [ + "a": 1, + "b": 2.3, + "c": ["4", 5] as [Any], + "d": URL(string: "https://mixpanel.com")!, + "e": NSNull(), + "f": Date(), + ] + Mixpanel.mainInstance().optInTracking(distinctId: "aDistinctIdForOptIn", properties: p) + descStr = "Opt In with distinctId 'aDistinctIdForOptIn' and \(p)" + case 5: + Mixpanel.initialize( + token: "testtoken", trackAutomaticEvents: true, optOutTrackingByDefault: true) + descStr = + "Init Mixpanel with default opt-out(sample only), to make it work, place it in your startup stage of your app" + case 6: + Mixpanel.initialize( + token: "testtoken", trackAutomaticEvents: true, optOutTrackingByDefault: false) + descStr = + "Init Mixpanel with default opt-in(sample only), to make it work, place it in your startup stage of your app" + default: + break } + + let vc = + storyboard!.instantiateViewController(withIdentifier: "ActionCompleteViewController") + as! ActionCompleteViewController + vc.actionStr = actionStr + vc.descStr = descStr + vc.modalTransitionStyle = UIModalTransitionStyle.crossDissolve + vc.modalPresentationStyle = UIModalPresentationStyle.overFullScreen + present(vc, animated: true, completion: nil) + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return tableViewItems.count + } } diff --git a/MixpanelDemo/MixpanelDemo/GroupsViewController.swift b/MixpanelDemo/MixpanelDemo/GroupsViewController.swift index 0036b0c6..50ab4670 100644 --- a/MixpanelDemo/MixpanelDemo/GroupsViewController.swift +++ b/MixpanelDemo/MixpanelDemo/GroupsViewController.swift @@ -6,118 +6,131 @@ // Copyright © 2018 Mixpanel. All rights reserved. // -import UIKit import Mixpanel +import UIKit class GroupsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { - @IBOutlet weak var tableView: UITableView! - var tableViewItems = ["Set Properties", - "Set One Property", - "Set Properties Once", - "Unset Property", - "Remove Property", - "Union Properties", - "Delete Group", - "Set Group", - "Set One Group", - "Add Group", - "Remove Group", - "Track with Groups"] + @IBOutlet weak var tableView: UITableView! + var tableViewItems = [ + "Set Properties", + "Set One Property", + "Set Properties Once", + "Unset Property", + "Remove Property", + "Union Properties", + "Delete Group", + "Set Group", + "Set One Group", + "Add Group", + "Remove Group", + "Track with Groups", + ] - override func viewDidLoad() { - super.viewDidLoad() - tableView.delegate = self - tableView.dataSource = self + override func viewDidLoad() { + super.viewDidLoad() + tableView.delegate = self + tableView.dataSource = self + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell")! as UITableViewCell + cell.textLabel?.text = tableViewItems[indexPath.item] + cell.textLabel?.textColor = #colorLiteral( + red: 0.200000003, green: 0.200000003, blue: 0.200000003, alpha: 1) + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let actionStr = tableViewItems[indexPath.item] + var descStr = "" + + let groupKey = "Cool Property" + let groupID = 12345 + + switch indexPath.item { + case 0: + let p: Properties = [ + "a": 1, + "b": 2.3, + "c": ["4", 5] as [Any], + "d": URL(string: "https://mixpanel.com")!, + "e": NSNull(), + "f": Date(), + ] + Mixpanel.mainInstance().getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) + descStr = "Properties: \(p)" + case 1: + Mixpanel.mainInstance().getGroup(groupKey: groupKey, groupID: groupID).set( + property: "g", to: "yo") + descStr = "Property key: g, value: yo" + case 2: + let p = ["h": "just once"] + Mixpanel.mainInstance().getGroup(groupKey: groupKey, groupID: groupID).setOnce(properties: p) + descStr = "Properties: \(p)" + case 3: + let p = "b" + Mixpanel.mainInstance().getGroup(groupKey: groupKey, groupID: groupID).unset(property: p) + descStr = "Unset Property: \(p)" + case 4: + Mixpanel.mainInstance().getGroup(groupKey: groupKey, groupID: groupID).remove( + key: "c", value: 5) + descStr = "Remove Property: [\"c\" : 5]" + case 5: + let p = ["c": [5, 4]] + Mixpanel.mainInstance().getGroup(groupKey: groupKey, groupID: groupID).union( + key: "c", values: p["c"]!) + descStr = "Properties: \(p)" + case 6: + Mixpanel.mainInstance().getGroup(groupKey: groupKey, groupID: groupID).deleteGroup() + descStr = "Deleted Group" + case 7: + let groupIDs = [groupID, 301] + Mixpanel.mainInstance().setGroup(groupKey: groupKey, groupIDs: groupIDs) + descStr = "Set Group \(groupKey) to \(groupIDs)" + case 8: + Mixpanel.mainInstance().setGroup(groupKey: groupKey, groupID: groupID) + descStr = "Set Group \(groupKey) to \(groupID)" + case 9: + let newID = "iris_test3" + Mixpanel.mainInstance().addGroup(groupKey: groupKey, groupID: newID) + descStr = "Add Group \(groupKey), ID \(newID)" + case 10: + Mixpanel.mainInstance().removeGroup(groupKey: groupKey, groupID: groupID) + descStr = "Remove Group \(groupKey), ID \(groupID)" + case 11: + let p: Properties = [ + "a": 1, + "b": 2.3, + "c": ["4", 5] as [Any], + "d": URL(string: "https://mixpanel.com")!, + "e": NSNull(), + "f": Date(), + "Cool Property": "foo", + ] + let groups: Properties = ["Cool Property": "actual group value"] + Mixpanel.mainInstance().trackWithGroups( + event: "tracked with groups", properties: p, groups: groups) + descStr = "Track with groups: properties \(p), groups \(groups)" + default: + break } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell")! as UITableViewCell - cell.textLabel?.text = tableViewItems[indexPath.item] - cell.textLabel?.textColor = #colorLiteral(red: 0.200000003, green: 0.200000003, blue: 0.200000003, alpha: 1) - return cell - } + let vc = + storyboard!.instantiateViewController(withIdentifier: "ActionCompleteViewController") + as! ActionCompleteViewController + vc.actionStr = actionStr + vc.descStr = descStr + vc.modalTransitionStyle = UIModalTransitionStyle.crossDissolve + vc.modalPresentationStyle = UIModalPresentationStyle.overFullScreen + present(vc, animated: true, completion: nil) - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) + } - let actionStr = tableViewItems[indexPath.item] - var descStr = "" - - let groupKey = "Cool Property" - let groupID = 12345 - - switch indexPath.item { - case 0: - let p: Properties = ["a": 1, - "b": 2.3, - "c": ["4", 5] as [Any], - "d": URL(string:"https://mixpanel.com")!, - "e": NSNull(), - "f": Date()] - Mixpanel.mainInstance().getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) - descStr = "Properties: \(p)" - case 1: - Mixpanel.mainInstance().getGroup(groupKey: groupKey, groupID: groupID).set(property: "g", to: "yo") - descStr = "Property key: g, value: yo" - case 2: - let p = ["h": "just once"] - Mixpanel.mainInstance().getGroup(groupKey: groupKey, groupID: groupID).setOnce(properties: p) - descStr = "Properties: \(p)" - case 3: - let p = "b" - Mixpanel.mainInstance().getGroup(groupKey: groupKey, groupID: groupID).unset(property: p) - descStr = "Unset Property: \(p)" - case 4: - Mixpanel.mainInstance().getGroup(groupKey: groupKey, groupID: groupID).remove(key: "c", value: 5) - descStr = "Remove Property: [\"c\" : 5]" - case 5: - let p = ["c": [5, 4]] - Mixpanel.mainInstance().getGroup(groupKey: groupKey, groupID: groupID).union(key: "c", values: p["c"]!) - descStr = "Properties: \(p)" - case 6: - Mixpanel.mainInstance().getGroup(groupKey: groupKey, groupID: groupID).deleteGroup() - descStr = "Deleted Group" - case 7: - let groupIDs = [groupID, 301] - Mixpanel.mainInstance().setGroup(groupKey: groupKey, groupIDs: groupIDs) - descStr = "Set Group \(groupKey) to \(groupIDs)" - case 8: - Mixpanel.mainInstance().setGroup(groupKey: groupKey, groupID: groupID) - descStr = "Set Group \(groupKey) to \(groupID)" - case 9: - let newID = "iris_test3" - Mixpanel.mainInstance().addGroup(groupKey: groupKey, groupID: newID) - descStr = "Add Group \(groupKey), ID \(newID)" - case 10: - Mixpanel.mainInstance().removeGroup(groupKey: groupKey, groupID: groupID) - descStr = "Remove Group \(groupKey), ID \(groupID)" - case 11: - let p: Properties = ["a": 1, - "b": 2.3, - "c": ["4", 5] as [Any], - "d": URL(string:"https://mixpanel.com")!, - "e": NSNull(), - "f": Date(), - "Cool Property": "foo"] - let groups: Properties = ["Cool Property": "actual group value"] - Mixpanel.mainInstance().trackWithGroups(event: "tracked with groups", properties: p, groups: groups) - descStr = "Track with groups: properties \(p), groups \(groups)" - default: - break - } - - let vc = storyboard!.instantiateViewController(withIdentifier: "ActionCompleteViewController") as! ActionCompleteViewController - vc.actionStr = actionStr - vc.descStr = descStr - vc.modalTransitionStyle = UIModalTransitionStyle.crossDissolve - vc.modalPresentationStyle = UIModalPresentationStyle.overFullScreen - present(vc, animated: true, completion: nil) - - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return tableViewItems.count - } + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return tableViewItems.count + } } diff --git a/MixpanelDemo/MixpanelDemo/LoginViewController.swift b/MixpanelDemo/MixpanelDemo/LoginViewController.swift index 7bcd4f86..6881b158 100644 --- a/MixpanelDemo/MixpanelDemo/LoginViewController.swift +++ b/MixpanelDemo/MixpanelDemo/LoginViewController.swift @@ -1,48 +1,47 @@ -import UIKit import Mixpanel +import UIKit class LoginViewController: UIViewController { - - let delegate = UIApplication.shared.delegate as! AppDelegate - - @IBOutlet weak var projectTokenTextField: UITextField! - - @IBOutlet weak var distinctIdTextField: UITextField! - - @IBOutlet weak var nameTextField: UITextField! - - @IBOutlet weak var startButton: UIButton! - - override func viewDidLoad() { - super.viewDidLoad() - let token = Mixpanel.mainInstance().apiToken - projectTokenTextField.text = token - distinctIdTextField.text = "demo_user" - nameTextField.text = "Demo User" - } - - open func goToMainView() { - if let vc = storyboard?.instantiateViewController(withIdentifier: "mainNav") { - self.view.window?.rootViewController = vc - } else { - NSLog("Unable to find view controller with name \"mainView\"") - } - } - - - @IBAction func start(_ sender: Any) { - Mixpanel.mainInstance().identify(distinctId: distinctIdTextField.text ?? "demo_user") - Mixpanel.mainInstance().people.set(property: "$name", to: nameTextField.text ?? "") - Mixpanel.mainInstance().track(event: "Logged in") - Mixpanel.mainInstance().flush() - - goToMainView() - } + let delegate = UIApplication.shared.delegate as! AppDelegate - @IBAction func rateDevX(_ sender: Any) { - if let url = URL(string: "https://www.mixpanel.com/devnps") { - UIApplication.shared.open(url) - } + @IBOutlet weak var projectTokenTextField: UITextField! + + @IBOutlet weak var distinctIdTextField: UITextField! + + @IBOutlet weak var nameTextField: UITextField! + + @IBOutlet weak var startButton: UIButton! + + override func viewDidLoad() { + super.viewDidLoad() + let token = Mixpanel.mainInstance().apiToken + projectTokenTextField.text = token + + distinctIdTextField.text = "demo_user" + nameTextField.text = "Demo User" + } + + open func goToMainView() { + if let vc = storyboard?.instantiateViewController(withIdentifier: "mainNav") { + self.view.window?.rootViewController = vc + } else { + NSLog("Unable to find view controller with name \"mainView\"") } + } + + @IBAction func start(_ sender: Any) { + Mixpanel.mainInstance().identify(distinctId: distinctIdTextField.text ?? "demo_user") + Mixpanel.mainInstance().people.set(property: "$name", to: nameTextField.text ?? "") + Mixpanel.mainInstance().track(event: "Logged in") + Mixpanel.mainInstance().flush() + + goToMainView() + } + + @IBAction func rateDevX(_ sender: Any) { + if let url = URL(string: "https://www.mixpanel.com/devnps") { + UIApplication.shared.open(url) + } + } } diff --git a/MixpanelDemo/MixpanelDemo/PeopleViewController.swift b/MixpanelDemo/MixpanelDemo/PeopleViewController.swift index 7921bfd6..f594058f 100644 --- a/MixpanelDemo/MixpanelDemo/PeopleViewController.swift +++ b/MixpanelDemo/MixpanelDemo/PeopleViewController.swift @@ -6,118 +6,125 @@ // Copyright © 2016 Mixpanel. All rights reserved. // -import UIKit import Mixpanel +import UIKit class PeopleViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { - @IBOutlet weak var tableView: UITableView! - var tableViewItems = ["Set Properties", - "Set One Property", - "Set Properties Once", - "Unset Properties", - "Incremet Properties", - "Increment Property", - "Append Properties", - "Union Properties", - "Track Charge w/o Properties", - "Track Charge w Properties", - "Clear Charges", - "Delete User", - "Identify"] + @IBOutlet weak var tableView: UITableView! + var tableViewItems = [ + "Set Properties", + "Set One Property", + "Set Properties Once", + "Unset Properties", + "Incremet Properties", + "Increment Property", + "Append Properties", + "Union Properties", + "Track Charge w/o Properties", + "Track Charge w Properties", + "Clear Charges", + "Delete User", + "Identify", + ] - override func viewDidLoad() { - super.viewDidLoad() - tableView.delegate = self - tableView.dataSource = self + override func viewDidLoad() { + super.viewDidLoad() + tableView.delegate = self + tableView.dataSource = self + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell")! as UITableViewCell + cell.textLabel?.text = tableViewItems[indexPath.item] + cell.textLabel?.textColor = #colorLiteral( + red: 0.200000003, green: 0.200000003, blue: 0.200000003, alpha: 1) + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let actionStr = tableViewItems[indexPath.item] + var descStr = "" + + switch indexPath.item { + case 0: + let p: Properties = [ + "a": 1, + "b": 2.3, + "c": ["4", 5] as [Any], + "d": URL(string: "https://mixpanel.com")!, + "e": NSNull(), + "f": Date(), + ] + Mixpanel.mainInstance().people.set(properties: p) + descStr = "Properties: \(p)" + case 1: + Mixpanel.mainInstance().people.set(property: "g", to: "yo") + descStr = "Property key: g, value: yo" + case 2: + let p = ["h": "just once"] + Mixpanel.mainInstance().people.setOnce(properties: p) + descStr = "Properties: \(p)" + case 3: + let p = ["b", "h"] + Mixpanel.mainInstance().people.unset(properties: p) + descStr = "Unset Properties: \(p)" + case 4: + let p = ["a": 1.2, "b": 3] + Mixpanel.mainInstance().people.increment(properties: p) + descStr = "Properties: \(p)" + case 5: + Mixpanel.mainInstance().people.increment(property: "b", by: 2.3) + descStr = "Property key: b, value increment: 2.3" + case 6: + let p = ["c": "hello", "d": "goodbye"] + Mixpanel.mainInstance().people.append(properties: p) + descStr = "Properties: \(p)" + case 7: + let p = ["c": ["goodbye", "hi"], "d": ["hello"]] + Mixpanel.mainInstance().people.union(properties: p) + descStr = "Properties: \(p)" + case 8: + Mixpanel.mainInstance().people.trackCharge(amount: 20.5) + descStr = "Amount: 20.5" + case 9: + let p = ["sandwich": 1] + Mixpanel.mainInstance().people.trackCharge(amount: 12.8, properties: p) + descStr = "Amount: 12.8, Properties: \(p)" + case 10: + Mixpanel.mainInstance().people.clearCharges() + descStr = "Cleared Charges" + case 11: + Mixpanel.mainInstance().people.deleteUser() + descStr = "Deleted User" + case 12: + // Mixpanel People requires that you explicitly set a distinct ID for the current user. In this case, + // we're using the automatically generated distinct ID from event tracking, based on the device's MAC address. + // It is strongly recommended that you use the same distinct IDs for Mixpanel Engagement and Mixpanel People. + // Note that the call to Mixpanel People identify: can come after properties have been set. We queue them until + // identify: is called and flush them at that time. That way, you can set properties before a user is logged in + // and identify them once you know their user ID. + Mixpanel.mainInstance().identify(distinctId: "testDistinctId111") + descStr = "Identified User" + default: + break } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell")! as UITableViewCell - cell.textLabel?.text = tableViewItems[indexPath.item] - cell.textLabel?.textColor = #colorLiteral(red: 0.200000003, green: 0.200000003, blue: 0.200000003, alpha: 1) - return cell - } + let vc = + storyboard!.instantiateViewController(withIdentifier: "ActionCompleteViewController") + as! ActionCompleteViewController + vc.actionStr = actionStr + vc.descStr = descStr + vc.modalTransitionStyle = UIModalTransitionStyle.crossDissolve + vc.modalPresentationStyle = UIModalPresentationStyle.overFullScreen + present(vc, animated: true, completion: nil) - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) + } - let actionStr = tableViewItems[indexPath.item] - var descStr = "" - - switch indexPath.item { - case 0: - let p: Properties = ["a": 1, - "b": 2.3, - "c": ["4", 5] as [Any], - "d": URL(string:"https://mixpanel.com")!, - "e": NSNull(), - "f": Date()] - Mixpanel.mainInstance().people.set(properties: p) - descStr = "Properties: \(p)" - case 1: - Mixpanel.mainInstance().people.set(property: "g", to: "yo") - descStr = "Property key: g, value: yo" - case 2: - let p = ["h": "just once"] - Mixpanel.mainInstance().people.setOnce(properties: p) - descStr = "Properties: \(p)" - case 3: - let p = ["b", "h"] - Mixpanel.mainInstance().people.unset(properties: p) - descStr = "Unset Properties: \(p)" - case 4: - let p = ["a": 1.2, "b": 3] - Mixpanel.mainInstance().people.increment(properties: p) - descStr = "Properties: \(p)" - case 5: - Mixpanel.mainInstance().people.increment(property: "b", by: 2.3) - descStr = "Property key: b, value increment: 2.3" - case 6: - let p = ["c": "hello", "d": "goodbye"] - Mixpanel.mainInstance().people.append(properties: p) - descStr = "Properties: \(p)" - case 7: - let p = ["c": ["goodbye", "hi"], "d": ["hello"]] - Mixpanel.mainInstance().people.union(properties: p) - descStr = "Properties: \(p)" - case 8: - Mixpanel.mainInstance().people.trackCharge(amount: 20.5) - descStr = "Amount: 20.5" - case 9: - let p = ["sandwich": 1] - Mixpanel.mainInstance().people.trackCharge(amount: 12.8, properties: p) - descStr = "Amount: 12.8, Properties: \(p)" - case 10: - Mixpanel.mainInstance().people.clearCharges() - descStr = "Cleared Charges" - case 11: - Mixpanel.mainInstance().people.deleteUser() - descStr = "Deleted User" - case 12: - // Mixpanel People requires that you explicitly set a distinct ID for the current user. In this case, - // we're using the automatically generated distinct ID from event tracking, based on the device's MAC address. - // It is strongly recommended that you use the same distinct IDs for Mixpanel Engagement and Mixpanel People. - // Note that the call to Mixpanel People identify: can come after properties have been set. We queue them until - // identify: is called and flush them at that time. That way, you can set properties before a user is logged in - // and identify them once you know their user ID. - Mixpanel.mainInstance().identify(distinctId: "testDistinctId111") - descStr = "Identified User" - default: - break - } - - let vc = storyboard!.instantiateViewController(withIdentifier: "ActionCompleteViewController") as! ActionCompleteViewController - vc.actionStr = actionStr - vc.descStr = descStr - vc.modalTransitionStyle = UIModalTransitionStyle.crossDissolve - vc.modalPresentationStyle = UIModalPresentationStyle.overFullScreen - present(vc, animated: true, completion: nil) - - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return tableViewItems.count - } + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return tableViewItems.count + } } diff --git a/MixpanelDemo/MixpanelDemo/TrackingViewController.swift b/MixpanelDemo/MixpanelDemo/TrackingViewController.swift index d40e9581..81de8f6e 100644 --- a/MixpanelDemo/MixpanelDemo/TrackingViewController.swift +++ b/MixpanelDemo/MixpanelDemo/TrackingViewController.swift @@ -6,105 +6,112 @@ // Copyright © 2016 Mixpanel. All rights reserved. // -import UIKit import Mixpanel +import UIKit class TrackingViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { - @IBOutlet weak var tableView: UITableView! - var tableViewItems = ["Track w/o Properties", - "Track w Properties", - "Time Event 5secs", - "Clear Timed Events", - "Get Current SuperProperties", - "Clear SuperProperties", - "Register SuperProperties", - "Register SuperProperties Once", - "Register SP Once w Default Value", - "Unregister SuperProperty"] + @IBOutlet weak var tableView: UITableView! + var tableViewItems = [ + "Track w/o Properties", + "Track w Properties", + "Time Event 5secs", + "Clear Timed Events", + "Get Current SuperProperties", + "Clear SuperProperties", + "Register SuperProperties", + "Register SuperProperties Once", + "Register SP Once w Default Value", + "Unregister SuperProperty", + ] - override func viewDidLoad() { - super.viewDidLoad() - - tableView.delegate = self - tableView.dataSource = self + override func viewDidLoad() { + super.viewDidLoad() + + tableView.delegate = self + tableView.dataSource = self + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell")! as UITableViewCell + cell.textLabel?.text = tableViewItems[indexPath.item] + cell.textLabel?.textColor = #colorLiteral( + red: 0.200000003, green: 0.200000003, blue: 0.200000003, alpha: 1) + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let actionStr = self.tableViewItems[indexPath.item] + var descStr = "" + + switch indexPath.item { + case 0: + let ev = "Track Event!" + Mixpanel.mainInstance().track(event: ev) + descStr = "Event: \"\(ev)\"" + case 1: + let ev = "Track Event With Properties!" + let p = ["Cool Property": "Property Value"] + Mixpanel.mainInstance().track(event: ev, properties: p) + descStr = "Event: \"\(ev)\"\n Properties: \(p)" + case 2: + let ev = "Timed Event" + Mixpanel.mainInstance().time(event: ev) + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + Mixpanel.mainInstance().track(event: ev) + } + descStr = "Timed Event: \"\(ev)\"" + case 3: + Mixpanel.mainInstance().clearTimedEvents() + descStr = "Timed Events Cleared" + case 4: + descStr = "Super Properties:\n" + descStr += "\(Mixpanel.mainInstance().currentSuperProperties())" + case 5: + Mixpanel.mainInstance().clearSuperProperties() + descStr = "Cleared Super Properties" + case 6: + let p: Properties = [ + "Super Property 1": 1, + "Super Property 2": "p2", + "Super Property 3": Date(), + "Super Property 4": ["a": "b"], + "Super Property 5": [3, "a", Date()] as [Any], + "Super Property 6": + URL(string: "https://mixpanel.com")!, + "Super Property 7": NSNull(), + ] + Mixpanel.mainInstance().registerSuperProperties(p) + descStr = "Properties: \(p)" + case 7: + let p = ["Super Property 1": 2.3] + Mixpanel.mainInstance().registerSuperPropertiesOnce(p) + descStr = "Properties: \(p)" + case 8: + let p = ["Super Property 1": 1.2] + Mixpanel.mainInstance().registerSuperPropertiesOnce(p, defaultValue: 2.3) + descStr = "Properties: \(p) with Default Value: 2.3" + case 9: + let p = "Super Property 2" + Mixpanel.mainInstance().unregisterSuperProperty(p) + descStr = "Properties: \(p)" + default: + break } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell")! as UITableViewCell - cell.textLabel?.text = tableViewItems[indexPath.item] - cell.textLabel?.textColor = #colorLiteral(red: 0.200000003, green: 0.200000003, blue: 0.200000003, alpha: 1) - return cell - } + let vc = + storyboard!.instantiateViewController(withIdentifier: "ActionCompleteViewController") + as! ActionCompleteViewController + vc.actionStr = actionStr + vc.descStr = descStr + vc.modalTransitionStyle = UIModalTransitionStyle.crossDissolve + vc.modalPresentationStyle = UIModalPresentationStyle.overFullScreen + present(vc, animated: true, completion: nil) + } - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - let actionStr = self.tableViewItems[indexPath.item] - var descStr = "" - - switch indexPath.item { - case 0: - let ev = "Track Event!" - Mixpanel.mainInstance().track(event: ev) - descStr = "Event: \"\(ev)\"" - case 1: - let ev = "Track Event With Properties!" - let p = ["Cool Property": "Property Value"] - Mixpanel.mainInstance().track(event: ev, properties: p) - descStr = "Event: \"\(ev)\"\n Properties: \(p)" - case 2: - let ev = "Timed Event" - Mixpanel.mainInstance().time(event: ev) - DispatchQueue.main.asyncAfter(deadline: .now() + 5) { - Mixpanel.mainInstance().track(event: ev) - } - descStr = "Timed Event: \"\(ev)\"" - case 3: - Mixpanel.mainInstance().clearTimedEvents() - descStr = "Timed Events Cleared" - case 4: - descStr = "Super Properties:\n" - descStr += "\(Mixpanel.mainInstance().currentSuperProperties())" - case 5: - Mixpanel.mainInstance().clearSuperProperties() - descStr = "Cleared Super Properties" - case 6: - let p: Properties = ["Super Property 1": 1, - "Super Property 2": "p2", - "Super Property 3": Date(), - "Super Property 4": ["a":"b"], - "Super Property 5": [3, "a", Date()] as [Any], - "Super Property 6": - URL(string: "https://mixpanel.com")!, - "Super Property 7": NSNull()] - Mixpanel.mainInstance().registerSuperProperties(p) - descStr = "Properties: \(p)" - case 7: - let p = ["Super Property 1": 2.3] - Mixpanel.mainInstance().registerSuperPropertiesOnce(p) - descStr = "Properties: \(p)" - case 8: - let p = ["Super Property 1": 1.2] - Mixpanel.mainInstance().registerSuperPropertiesOnce(p, defaultValue: 2.3) - descStr = "Properties: \(p) with Default Value: 2.3" - case 9: - let p = "Super Property 2" - Mixpanel.mainInstance().unregisterSuperProperty(p) - descStr = "Properties: \(p)" - default: - break - } - - let vc = storyboard!.instantiateViewController(withIdentifier: "ActionCompleteViewController") as! ActionCompleteViewController - vc.actionStr = actionStr - vc.descStr = descStr - vc.modalTransitionStyle = UIModalTransitionStyle.crossDissolve - vc.modalPresentationStyle = UIModalPresentationStyle.overFullScreen - present(vc, animated: true, completion: nil) - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return tableViewItems.count - } + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return tableViewItems.count + } } diff --git a/MixpanelDemo/MixpanelDemo/UtilityViewController.swift b/MixpanelDemo/MixpanelDemo/UtilityViewController.swift index 54ba638e..f5c8e860 100644 --- a/MixpanelDemo/MixpanelDemo/UtilityViewController.swift +++ b/MixpanelDemo/MixpanelDemo/UtilityViewController.swift @@ -6,111 +6,122 @@ // Copyright © 2016 Mixpanel. All rights reserved. // -import UIKit import Mixpanel import StoreKit +import UIKit -class UtilityViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, SKProductsRequestDelegate, SKPaymentTransactionObserver { +class UtilityViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, + SKProductsRequestDelegate, SKPaymentTransactionObserver +{ - @IBOutlet weak var tableView: UITableView! - var tableViewItems = ["Create Alias", - "Reset", - "Archive", - "Flush", - "In-App Purchase"] + @IBOutlet weak var tableView: UITableView! + var tableViewItems = [ + "Create Alias", + "Reset", + "Archive", + "Flush", + "In-App Purchase", + ] - override func viewDidLoad() { - super.viewDidLoad() - tableView.delegate = self - tableView.dataSource = self + override func viewDidLoad() { + super.viewDidLoad() + tableView.delegate = self + tableView.dataSource = self + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell")! as UITableViewCell + cell.textLabel?.text = tableViewItems[indexPath.item] + cell.textLabel?.textColor = #colorLiteral( + red: 0.200000003, green: 0.200000003, blue: 0.200000003, alpha: 1) + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let actionStr = tableViewItems[indexPath.item] + var descStr = "" + + switch indexPath.item { + case 0: + Mixpanel.mainInstance().createAlias( + "New Alias", distinctId: Mixpanel.mainInstance().distinctId) + descStr = "Alias: New Alias, from: \(Mixpanel.mainInstance().distinctId)" + case 1: + Mixpanel.mainInstance().reset() + descStr = "Reset Instance" + case 2: + Mixpanel.mainInstance().archive() + descStr = "Archived Data" + case 3: + Mixpanel.mainInstance().flush() + descStr = "Flushed Data" + case 4: + IAPFlow() + default: + break } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell")! as UITableViewCell - cell.textLabel?.text = tableViewItems[indexPath.item] - cell.textLabel?.textColor = #colorLiteral(red: 0.200000003, green: 0.200000003, blue: 0.200000003, alpha: 1) - return cell + let vc = + storyboard!.instantiateViewController(withIdentifier: "ActionCompleteViewController") + as! ActionCompleteViewController + vc.actionStr = actionStr + vc.descStr = descStr + vc.modalTransitionStyle = UIModalTransitionStyle.crossDissolve + vc.modalPresentationStyle = UIModalPresentationStyle.overFullScreen + present(vc, animated: true, completion: nil) + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return tableViewItems.count + } + + func IAPFlow() { + let productIdentifiers = NSSet( + objects: + "com.iaptutorial.fun", + "com.mixpanel.swiftsdkdemo.fun" + ) + let productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers as! Set) + productsRequest.delegate = self + productsRequest.start() + } + + func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { + if response.products.count > 0 { + if let firstProduct = response.products.first { + let payment = SKPayment(product: firstProduct) + SKPaymentQueue.default().add(self) + SKPaymentQueue.default().add(payment) + } } + } - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) + func paymentQueue( + _ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction] + ) { + for transaction: AnyObject in transactions { + if let trans = transaction as? SKPaymentTransaction { + switch trans.transactionState { + case .purchased: + SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction) + print("IAP purchased") + break - let actionStr = tableViewItems[indexPath.item] - var descStr = "" + case .failed: + SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction) + print("IAP failed") + break + case .restored: + SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction) + print("IAP restored") + break - switch indexPath.item { - case 0: - Mixpanel.mainInstance().createAlias("New Alias", distinctId: Mixpanel.mainInstance().distinctId) - descStr = "Alias: New Alias, from: \(Mixpanel.mainInstance().distinctId)" - case 1: - Mixpanel.mainInstance().reset() - descStr = "Reset Instance" - case 2: - Mixpanel.mainInstance().archive() - descStr = "Archived Data" - case 3: - Mixpanel.mainInstance().flush() - descStr = "Flushed Data" - case 4: - IAPFlow() - default: - break - } - - let vc = storyboard!.instantiateViewController(withIdentifier: "ActionCompleteViewController") as! ActionCompleteViewController - vc.actionStr = actionStr - vc.descStr = descStr - vc.modalTransitionStyle = UIModalTransitionStyle.crossDissolve - vc.modalPresentationStyle = UIModalPresentationStyle.overFullScreen - present(vc, animated: true, completion: nil) - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return tableViewItems.count - } - - func IAPFlow() { - let productIdentifiers = NSSet(objects: - "com.iaptutorial.fun", - "com.mixpanel.swiftsdkdemo.fun" - ) - let productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers as! Set) - productsRequest.delegate = self - productsRequest.start() - } - - func productsRequest (_ request:SKProductsRequest, didReceive response:SKProductsResponse) { - if (response.products.count > 0) { - if let firstProduct = response.products.first { - let payment = SKPayment(product: firstProduct) - SKPaymentQueue.default().add(self) - SKPaymentQueue.default().add(payment) - } - } - } - - func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { - for transaction:AnyObject in transactions { - if let trans = transaction as? SKPaymentTransaction { - switch trans.transactionState { - case .purchased: - SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction) - print("IAP purchased") - break - - case .failed: - SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction) - print("IAP failed") - break - case .restored: - SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction) - print("IAP restored") - break - - default: break - } - } + default: break } + } } + } } diff --git a/MixpanelDemo/MixpanelDemoMac/AppDelegate.swift b/MixpanelDemo/MixpanelDemoMac/AppDelegate.swift index ae033a94..5d0fd45f 100644 --- a/MixpanelDemo/MixpanelDemoMac/AppDelegate.swift +++ b/MixpanelDemo/MixpanelDemoMac/AppDelegate.swift @@ -12,24 +12,19 @@ import Mixpanel @main class AppDelegate: NSObject, NSApplicationDelegate { - + func applicationDidFinishLaunching(_ aNotification: Notification) { + // Insert code here to initialize your application + var ADD_YOUR_MIXPANEL_TOKEN_BELOW_🛠🛠🛠🛠🛠🛠: String - func applicationDidFinishLaunching(_ aNotification: Notification) { - // Insert code here to initialize your application - - var ADD_YOUR_MIXPANEL_TOKEN_BELOW_🛠🛠🛠🛠🛠🛠: String - - Mixpanel.initialize(token: "MIXPANEL_TOKEN") - Mixpanel.mainInstance().loggingEnabled = true - Mixpanel.mainInstance().track(event: "Tracked Event") - - } + Mixpanel.initialize(token: "MIXPANEL_TOKEN") + Mixpanel.mainInstance().loggingEnabled = true + Mixpanel.mainInstance().track(event: "Tracked Event") - func applicationWillTerminate(_ aNotification: Notification) { - // Insert code here to tear down your application - } + } + func applicationWillTerminate(_ aNotification: Notification) { + // Insert code here to tear down your application + } } - diff --git a/MixpanelDemo/MixpanelDemoMac/ViewController.swift b/MixpanelDemo/MixpanelDemoMac/ViewController.swift index 797ccc3b..0826466b 100644 --- a/MixpanelDemo/MixpanelDemoMac/ViewController.swift +++ b/MixpanelDemo/MixpanelDemoMac/ViewController.swift @@ -9,21 +9,18 @@ import Cocoa import Mixpanel - class ViewController: NSViewController { - override func viewDidLoad() { - super.viewDidLoad() + override func viewDidLoad() { + super.viewDidLoad() - // Do any additional setup after loading the view. + // Do any additional setup after loading the view. + } + + override var representedObject: Any? { + didSet { + // Update the view, if already loaded. } - - override var representedObject: Any? { - didSet { - // Update the view, if already loaded. - } - } - + } } - diff --git a/MixpanelDemo/MixpanelDemoMacTests/MixpanelBaseTests.swift b/MixpanelDemo/MixpanelDemoMacTests/MixpanelBaseTests.swift index 83df4324..b999d368 100644 --- a/MixpanelDemo/MixpanelDemoMacTests/MixpanelBaseTests.swift +++ b/MixpanelDemo/MixpanelDemoMacTests/MixpanelBaseTests.swift @@ -6,157 +6,158 @@ // Copyright © 2016 Mixpanel. All rights reserved. // -import XCTest import SQLite3 +import XCTest @testable import Mixpanel @testable import MixpanelDemoMac class MixpanelBaseTests: XCTestCase, MixpanelDelegate { - var mixpanelWillFlush: Bool! - static var requestCount = 0 + var mixpanelWillFlush: Bool! + static var requestCount = 0 - override func setUp() { - NSLog("starting test setup...") - super.setUp() - mixpanelWillFlush = false + override func setUp() { + NSLog("starting test setup...") + super.setUp() + mixpanelWillFlush = false - NSLog("finished test setup") + NSLog("finished test setup") + } + + override func tearDown() { + super.tearDown() + } + + func removeDBfile(apiToken: String) { + do { + let fileManager = FileManager.default + + // Check if file exists + if fileManager.fileExists(atPath: dbFilePath(apiToken)) { + // Delete file + try fileManager.removeItem(atPath: dbFilePath(apiToken)) + } else { + print( + "Unable to delete the test db file at \(dbFilePath(apiToken)), the file does not exist") + } + } catch let error as NSError { + print("An error took place: \(error)") } - - override func tearDown() { - super.tearDown() - } - - func removeDBfile(apiToken: String) { - do { - let fileManager = FileManager.default - - // Check if file exists - if fileManager.fileExists(atPath: dbFilePath(apiToken)) { - // Delete file - try fileManager.removeItem(atPath: dbFilePath(apiToken)) - } else { - print("Unable to delete the test db file at \(dbFilePath(apiToken)), the file does not exist") - } - } - catch let error as NSError { - print("An error took place: \(error)") - } - } - - func removeDBfile(_ mixpanel: MixpanelInstance) { - mixpanel.mixpanelPersistence.closeDB() - removeDBfile(apiToken: mixpanel.apiToken) - } - - func dbFilePath(_ token: String? = nil) -> String { - let manager = FileManager.default - #if os(iOS) - let url = manager.urls(for: .libraryDirectory, in: .userDomainMask).last - #else - let url = manager.urls(for: .cachesDirectory, in: .userDomainMask).last - #endif // os(iOS) - guard let apiToken = token else { - return "" - } - - guard let urlUnwrapped = url?.appendingPathComponent("\(token ?? apiToken)_MPDB.sqlite").path else { - return "" - } - return urlUnwrapped + } + + func removeDBfile(_ mixpanel: MixpanelInstance) { + mixpanel.mixpanelPersistence.closeDB() + removeDBfile(apiToken: mixpanel.apiToken) + } + + func dbFilePath(_ token: String? = nil) -> String { + let manager = FileManager.default + #if os(iOS) + let url = manager.urls(for: .libraryDirectory, in: .userDomainMask).last + #else + let url = manager.urls(for: .cachesDirectory, in: .userDomainMask).last + #endif // os(iOS) + guard let apiToken = token else { + return "" } - - func mixpanelWillFlush(_ mixpanel: MixpanelInstance) -> Bool { - return mixpanelWillFlush + guard let urlUnwrapped = url?.appendingPathComponent("\(token ?? apiToken)_MPDB.sqlite").path + else { + return "" + } + return urlUnwrapped + } + + func mixpanelWillFlush(_ mixpanel: MixpanelInstance) -> Bool { + return mixpanelWillFlush + } + + func waitForTrackingQueue(_ mixpanel: MixpanelInstance) { + mixpanel.trackingQueue.sync { + mixpanel.networkQueue.sync { + return + } } - func waitForTrackingQueue(_ mixpanel: MixpanelInstance) { - mixpanel.trackingQueue.sync() { - mixpanel.networkQueue.sync() { - return - } - } - - mixpanel.trackingQueue.sync() { - mixpanel.networkQueue.sync() { - return - } - } + mixpanel.trackingQueue.sync { + mixpanel.networkQueue.sync { + return + } } - - func randomId() -> String - { - return String(format: "%08x%08x", arc4random(), arc4random()) - } - - func waitForAsyncTasks() { - var hasCompletedTask = false - DispatchQueue.main.async { - hasCompletedTask = true - } + } - let loopUntil = Date(timeIntervalSinceNow: 10) - while !hasCompletedTask && loopUntil.timeIntervalSinceNow > 0 { - RunLoop.current.run(mode: RunLoop.Mode.default, before: loopUntil) - } - } - - func eventQueue(token: String) -> Queue { - return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .events) + func randomId() -> String { + return String(format: "%08x%08x", arc4random(), arc4random()) + } + + func waitForAsyncTasks() { + var hasCompletedTask = false + DispatchQueue.main.async { + hasCompletedTask = true } - func peopleQueue(token: String) -> Queue { - return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .people) - } - - func unIdentifiedPeopleQueue(token: String) -> Queue { - return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .people, flag: PersistenceConstant.unIdentifiedFlag) - } - - func groupQueue(token: String) -> Queue { - return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .groups) - } - - func flushAndWaitForTrackingQueue(_ mixpanel: MixpanelInstance) { - mixpanel.flush() - waitForTrackingQueue(mixpanel) - mixpanel.flush() - waitForTrackingQueue(mixpanel) + let loopUntil = Date(timeIntervalSinceNow: 10) + while !hasCompletedTask && loopUntil.timeIntervalSinceNow > 0 { + RunLoop.current.run(mode: RunLoop.Mode.default, before: loopUntil) } + } - func assertDefaultPeopleProperties(_ properties: InternalProperties) { - XCTAssertNotNil(properties["$ios_device_model"], "missing $ios_device_model property") - XCTAssertNotNil(properties["$ios_lib_version"], "missing $ios_lib_version property") - XCTAssertNotNil(properties["$ios_version"], "missing $ios_version property") - XCTAssertNotNil(properties["$ios_app_version"], "missing $ios_app_version property") - XCTAssertNotNil(properties["$ios_app_release"], "missing $ios_app_release property") - } + func eventQueue(token: String) -> Queue { + return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .events) + } - func compareDate(dateString: String, dateDate: Date) { - let dateFormatter: ISO8601DateFormatter = ISO8601DateFormatter() - let date = dateFormatter.string(from: dateDate) - XCTAssertEqual(String(date.prefix(19)), String(dateString.prefix(19))) - } - - func allPropertyTypes() -> Properties { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss zzz" - let date = dateFormatter.date(from: "2012-09-28 19:14:36 PDT") - let nested = ["p1": ["p2": ["p3": ["bottom"]]]] - let opt: String? = nil - return ["string": "yello", - "number": 3, - "date": date!, - "dictionary": ["k": "v", "opt": opt as Any], - "array": ["1", opt as Any], - "null": NSNull(), - "nested": nested, - "url": URL(string: "https://mixpanel.com/")!, - "float": 1.3, - "optional": opt, - ] - } + func peopleQueue(token: String) -> Queue { + return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .people) + } + + func unIdentifiedPeopleQueue(token: String) -> Queue { + return MixpanelPersistence.init(token: token).loadEntitiesInBatch( + type: .people, flag: PersistenceConstant.unIdentifiedFlag) + } + + func groupQueue(token: String) -> Queue { + return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .groups) + } + + func flushAndWaitForTrackingQueue(_ mixpanel: MixpanelInstance) { + mixpanel.flush() + waitForTrackingQueue(mixpanel) + mixpanel.flush() + waitForTrackingQueue(mixpanel) + } + + func assertDefaultPeopleProperties(_ properties: InternalProperties) { + XCTAssertNotNil(properties["$ios_device_model"], "missing $ios_device_model property") + XCTAssertNotNil(properties["$ios_lib_version"], "missing $ios_lib_version property") + XCTAssertNotNil(properties["$ios_version"], "missing $ios_version property") + XCTAssertNotNil(properties["$ios_app_version"], "missing $ios_app_version property") + XCTAssertNotNil(properties["$ios_app_release"], "missing $ios_app_release property") + } + + func compareDate(dateString: String, dateDate: Date) { + let dateFormatter: ISO8601DateFormatter = ISO8601DateFormatter() + let date = dateFormatter.string(from: dateDate) + XCTAssertEqual(String(date.prefix(19)), String(dateString.prefix(19))) + } + + func allPropertyTypes() -> Properties { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss zzz" + let date = dateFormatter.date(from: "2012-09-28 19:14:36 PDT") + let nested = ["p1": ["p2": ["p3": ["bottom"]]]] + let opt: String? = nil + return [ + "string": "yello", + "number": 3, + "date": date!, + "dictionary": ["k": "v", "opt": opt as Any], + "array": ["1", opt as Any], + "null": NSNull(), + "nested": nested, + "url": URL(string: "https://mixpanel.com/")!, + "float": 1.3, + "optional": opt, + ] + } } diff --git a/MixpanelDemo/MixpanelDemoMacTests/MixpanelDemoTests.swift b/MixpanelDemo/MixpanelDemoMacTests/MixpanelDemoTests.swift index 4bb048a6..922b419f 100644 --- a/MixpanelDemo/MixpanelDemoMacTests/MixpanelDemoTests.swift +++ b/MixpanelDemo/MixpanelDemoMacTests/MixpanelDemoTests.swift @@ -10,957 +10,1075 @@ import XCTest @testable import Mixpanel @testable import MixpanelDemoMac + private let devicePrefix = "$device:" class MixpanelDemoTests: MixpanelBaseTests { - func test5XXResponse() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.serverURL = kFakeServerUrl - testMixpanel.track(event: "Fake Event") - flushAndWaitForTrackingQueue(testMixpanel) - // Failure count should be 3 - let waitTime = - testMixpanel.flushInstance.flushRequest.networkRequestsAllowedAfterTime - Date().timeIntervalSince1970 - print("Delta wait time is \(waitTime)") - XCTAssert(waitTime >= 110, "Network backoff time is less than 2 minutes.") - XCTAssert(testMixpanel.flushInstance.flushRequest.networkConsecutiveFailures == 2, - "Network failures did not equal 2") + func test5XXResponse() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.serverURL = kFakeServerUrl + testMixpanel.track(event: "Fake Event") + flushAndWaitForTrackingQueue(testMixpanel) + // Failure count should be 3 + let waitTime = + testMixpanel.flushInstance.flushRequest.networkRequestsAllowedAfterTime + - Date().timeIntervalSince1970 + print("Delta wait time is \(waitTime)") + XCTAssert(waitTime >= 110, "Network backoff time is less than 2 minutes.") + XCTAssert( + testMixpanel.flushInstance.flushRequest.networkConsecutiveFailures == 2, + "Network failures did not equal 2") - XCTAssert(eventQueue(token: testMixpanel.apiToken).count == 1, - "Removed an event from the queue that was not sent") - removeDBfile(testMixpanel) + XCTAssert( + eventQueue(token: testMixpanel.apiToken).count == 1, + "Removed an event from the queue that was not sent") + removeDBfile(testMixpanel) + } + + func testFlushEvents() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + for i in 0..<50 { + testMixpanel.track(event: "event \(i)") } + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).isEmpty, + "events should have been flushed") - func testFlushEvents() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - for i in 0..<50 { - testMixpanel.track(event: "event \(i)") - } - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, - "events should have been flushed") - - for i in 0..<60 { - testMixpanel.track(event: "event \(i)") - } - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, - "events should have been flushed") - removeDBfile(testMixpanel) + for i in 0..<60 { + testMixpanel.track(event: "event \(i)") } + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).isEmpty, + "events should have been flushed") + removeDBfile(testMixpanel) + } - - func testFlushPeople() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - for i in 0..<50 { - testMixpanel.people.set(property: "p1", to: "\(i)") - } - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "people should have been flushed") - for i in 0..<60 { - testMixpanel.people.set(property: "p1", to: "\(i)") - } - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "people should have been flushed") - removeDBfile(testMixpanel) + func testFlushPeople() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + for i in 0..<50 { + testMixpanel.people.set(property: "p1", to: "\(i)") } - - func testFlushGroups() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - let groupKey = "test_key" - let groupValue = "test_value" - for i in 0..<50 { - testMixpanel.getGroup(groupKey: groupKey, groupID: groupValue).set(property: "p1", to: "\(i)") - } - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(groupQueue(token: testMixpanel.apiToken).isEmpty, "groups should have been flushed") - for i in 0..<60 { - testMixpanel.getGroup(groupKey: groupKey, groupID: groupValue).set(property: "p1", to: "\(i)") - } - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "groups should have been flushed") - removeDBfile(testMixpanel) + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + peopleQueue(token: testMixpanel.apiToken).isEmpty, "people should have been flushed") + for i in 0..<60 { + testMixpanel.people.set(property: "p1", to: "\(i)") } + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + peopleQueue(token: testMixpanel.apiToken).isEmpty, "people should have been flushed") + removeDBfile(testMixpanel) + } - func testFlushNetworkFailure() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.serverURL = kFakeServerUrl - for i in 0..<50 { - testMixpanel.track(event: "event \(UInt(i))") - } - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 50, "50 events should be queued up") - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 50, - "events should still be in the queue if flush fails") - removeDBfile(testMixpanel) + func testFlushGroups() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + let groupKey = "test_key" + let groupValue = "test_value" + for i in 0..<50 { + testMixpanel.getGroup(groupKey: groupKey, groupID: groupValue).set(property: "p1", to: "\(i)") } - - func testFlushQueueContainsCorruptedEvent() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.trackingQueue.async { - testMixpanel.mixpanelPersistence.saveEntity(["event": "bad event1", "properties": ["BadProp": Double.nan]], type: .events) - testMixpanel.mixpanelPersistence.saveEntity(["event": "bad event2", "properties": ["BadProp": Float.nan]], type: .events) - testMixpanel.mixpanelPersistence.saveEntity(["event": "bad event3", "properties": ["BadProp": Double.infinity]], type: .events) - testMixpanel.mixpanelPersistence.saveEntity(["event": "bad event4", "properties": ["BadProp": Float.infinity]], type: .events) - } - for i in 0..<10 { - testMixpanel.track(event: "event \(UInt(i))") - } - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 0, "good events should still be flushed") - removeDBfile(testMixpanel) + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + groupQueue(token: testMixpanel.apiToken).isEmpty, "groups should have been flushed") + for i in 0..<60 { + testMixpanel.getGroup(groupKey: groupKey, groupID: groupValue).set(property: "p1", to: "\(i)") } - - 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) + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + peopleQueue(token: testMixpanel.apiToken).isEmpty, "groups should have been flushed") + removeDBfile(testMixpanel) + } + + func testFlushNetworkFailure() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.serverURL = kFakeServerUrl + for i in 0..<50 { + testMixpanel.track(event: "event \(UInt(i))") } + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 50, "50 events should be queued up") + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 50, + "events should still be in the queue if flush fails") + 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 testFlushQueueContainsCorruptedEvent() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.trackingQueue.async { + testMixpanel.mixpanelPersistence.saveEntity( + ["event": "bad event1", "properties": ["BadProp": Double.nan]], type: .events) + testMixpanel.mixpanelPersistence.saveEntity( + ["event": "bad event2", "properties": ["BadProp": Float.nan]], type: .events) + testMixpanel.mixpanelPersistence.saveEntity( + ["event": "bad event3", "properties": ["BadProp": Double.infinity]], type: .events) + testMixpanel.mixpanelPersistence.saveEntity( + ["event": "bad event4", "properties": ["BadProp": Float.infinity]], type: .events) } - - 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) + for i in 0..<10 { + testMixpanel.track(event: "event \(UInt(i))") } + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 0, "good events should still be flushed") + 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 testAddingEventsAfterFlush() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - for i in 0..<10 { - testMixpanel.track(event: "event \(UInt(i))") - } - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 10, "10 events should be queued up") - flushAndWaitForTrackingQueue(testMixpanel) - for i in 0..<5 { - testMixpanel.track(event: "event \(UInt(i))") - } - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 5, "5 more events should be queued up") - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "events should have been flushed") - 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 testIdentify() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - for _ in 0..<2 { - // run this twice to test reset works correctly wrt to distinct ids - let distinctId: String = "d1" - // try this for ODIN and nil - #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(testMixpanel.distinctId, - devicePrefix + testMixpanel.defaultDeviceId(), - "mixpanel identify failed to set default distinct id") - XCTAssertEqual(testMixpanel.anonymousId, - testMixpanel.defaultDeviceId(), - "mixpanel failed to set default anonymous id") - #endif - XCTAssertNil(testMixpanel.people.distinctId, - "mixpanel people distinct id should default to nil") - XCTAssertNil(testMixpanel.people.distinctId, - "mixpanel user id should default to nil") - testMixpanel.track(event: "e1") - waitForTrackingQueue(testMixpanel) - let eventsQueue = eventQueue(token: testMixpanel.apiToken) - XCTAssertTrue(eventsQueue.count == 1, - "events should be sent right away with default distinct id") - #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual((eventsQueue.last?["properties"] as? InternalProperties)?["distinct_id"] as? String, - devicePrefix + mixpanel.defaultDeviceId(), - "events should use default distinct id if none set") - #endif - XCTAssertEqual((eventsQueue.last?["properties"] as? InternalProperties)?["$lib_version"] as? String, - AutomaticProperties.libVersion(), - "events should has lib version in internal properties") - testMixpanel.people.set(property: "p1", to: "a") - waitForTrackingQueue(testMixpanel) - var peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) - var unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) - XCTAssertTrue(peopleQueue_value.isEmpty, - "people records should go to unidentified queue before identify:") - XCTAssertTrue(unidentifiedQueue.count == 1, - "unidentified people records not queued") - XCTAssertEqual(unidentifiedQueue.last?["$token"] as? String, - testMixpanel.apiToken, - "incorrect project token in people record") - testMixpanel.identify(distinctId: distinctId) - waitForTrackingQueue(testMixpanel) - let anonymousId = testMixpanel.anonymousId - peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) - unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) - XCTAssertEqual(testMixpanel.distinctId, distinctId, - "mixpanel identify failed to set distinct id") - XCTAssertEqual(testMixpanel.userId, distinctId, - "mixpanel identify failed to set user id") - XCTAssertEqual(testMixpanel.anonymousId, anonymousId, - "mixpanel identify shouldn't change anonymousId") - XCTAssertEqual(testMixpanel.people.distinctId, distinctId, - "mixpanel identify failed to set people distinct id") - XCTAssertTrue(unidentifiedQueue.isEmpty, - "identify: should move records from unidentified queue") - XCTAssertTrue(peopleQueue_value.count > 0, - "identify: should move records to main people queue") - XCTAssertEqual(peopleQueue_value.last?["$token"] as? String, - testMixpanel.apiToken, "incorrect project token in people record") - let p: InternalProperties = peopleQueue_value.last?["$set"] as! InternalProperties - XCTAssertEqual(p["p1"] as? String, "a", "custom people property not queued") - assertDefaultPeopleProperties(p) - peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) - - testMixpanel.people.set(property: "p1", to: "a") - waitForTrackingQueue(testMixpanel) - - peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) - unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) - XCTAssertEqual(peopleQueue_value.last?["$distinct_id"] as? String, - distinctId, "distinct id not set properly on unidentified people record") - XCTAssertTrue(unidentifiedQueue.isEmpty, - "once idenitfy: is called, unidentified queue should be skipped") - XCTAssertTrue(peopleQueue_value.count > 0 , - "once identify: is called, records should go straight to main queue") - testMixpanel.track(event: "e2") - waitForTrackingQueue(testMixpanel) - let newDistinctId = (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?["distinct_id"] as? String - XCTAssertEqual(newDistinctId, distinctId, - "events should use new distinct id after identify:") - testMixpanel.reset() - waitForTrackingQueue(testMixpanel) - } - 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 testIdentifyTrack() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let distinctIdBeforeIdentify: String? = testMixpanel.distinctId - let distinctId = "testIdentifyTrack" - - testMixpanel.identify(distinctId: distinctId) - waitForTrackingQueue(testMixpanel) - let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(e["event"] as? String, "$identify", "incorrect event name") - let p: InternalProperties = e["properties"] as! InternalProperties - XCTAssertEqual(p["distinct_id"] as? String, distinctId, "wrong distinct_id") - XCTAssertEqual(p["$anon_distinct_id"] as? String, distinctIdBeforeIdentify, "wrong $anon_distinct_id") - 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 testIdentifyResetTrack() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let originalDistinctId: String? = testMixpanel.distinctId - let distinctId = "testIdentifyTrack" - testMixpanel.reset() - waitForTrackingQueue(testMixpanel) - - for i in 1...3 { - let prevDistinctId: String? = testMixpanel.distinctId - let newDistinctId = distinctId + String(i) - testMixpanel.identify(distinctId: newDistinctId) - waitForTrackingQueue(testMixpanel) - waitForTrackingQueue(testMixpanel) - let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(e["event"] as? String, "$identify", "incorrect event name") - let p: InternalProperties = e["properties"] as! InternalProperties - XCTAssertEqual(p["distinct_id"] as? String, newDistinctId, "wrong distinct_id") - XCTAssertEqual(p["$anon_distinct_id"] as? String, prevDistinctId, "wrong $anon_distinct_id") - XCTAssertNotEqual(prevDistinctId, originalDistinctId, "After reset, UUID will be used - never the same"); - #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(prevDistinctId, originalDistinctId, "After reset, IFV will be used - always the same"); - #endif - testMixpanel.reset() - waitForTrackingQueue(testMixpanel) - } - removeDBfile(testMixpanel) + func testAddingEventsAfterFlush() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + for i in 0..<10 { + testMixpanel.track(event: "event \(UInt(i))") } - - func testPersistentIdentity() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let distinctId: String = "d1" - let alias: String = "a1" - testMixpanel.identify(distinctId: distinctId) - waitForTrackingQueue(testMixpanel) - testMixpanel.createAlias(alias, distinctId: testMixpanel.distinctId) - waitForTrackingQueue(testMixpanel) - var mixpanelIdentity = MixpanelPersistence.loadIdentity(apiToken: testMixpanel.apiToken) - XCTAssertTrue(distinctId == mixpanelIdentity.distinctID && distinctId == mixpanelIdentity.peopleDistinctID && distinctId == mixpanelIdentity.userId && alias == mixpanelIdentity.alias) - testMixpanel.archive() - waitForTrackingQueue(testMixpanel) - testMixpanel.unarchive() - waitForTrackingQueue(testMixpanel) - mixpanelIdentity = MixpanelPersistence.loadIdentity(apiToken: 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) - waitForTrackingQueue(testMixpanel) - mixpanelIdentity = MixpanelPersistence.loadIdentity(apiToken: testMixpanel.apiToken) - XCTAssertTrue("" == mixpanelIdentity.distinctID && nil == mixpanelIdentity.peopleDistinctID && nil == mixpanelIdentity.anonymousId && nil == mixpanelIdentity.userId && nil == mixpanelIdentity.alias) - removeDBfile(testMixpanel) + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 10, "10 events should be queued up") + flushAndWaitForTrackingQueue(testMixpanel) + for i in 0..<5 { + testMixpanel.track(event: "event \(UInt(i))") } + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 5, "5 more events should be queued up") + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).isEmpty, "events should have been flushed") + removeDBfile(testMixpanel) + } - func testHadPersistedDistinctId() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - XCTAssertNotNil(testMixpanel.distinctId) - let distinctId: String = "d1" - testMixpanel.anonymousId = nil - testMixpanel.userId = nil - testMixpanel.alias = nil - testMixpanel.distinctId = distinctId - testMixpanel.archive() - - XCTAssertEqual(testMixpanel.distinctId, distinctId) - - let userId: String = "u1" - testMixpanel.identify(distinctId: userId) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.anonymousId, distinctId) - XCTAssertEqual(testMixpanel.userId, userId) - XCTAssertEqual(testMixpanel.distinctId, userId) - XCTAssertTrue(testMixpanel.hadPersistedDistinctId!) - removeDBfile(testMixpanel) + func testIdentify() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + for _ in 0..<2 { + // run this twice to test reset works correctly wrt to distinct ids + let distinctId: String = "d1" + // try this for ODIN and nil + #if MIXPANEL_UNIQUE_DISTINCT_ID + XCTAssertEqual( + testMixpanel.distinctId, + devicePrefix + testMixpanel.defaultDeviceId(), + "mixpanel identify failed to set default distinct id") + XCTAssertEqual( + testMixpanel.anonymousId, + testMixpanel.defaultDeviceId(), + "mixpanel failed to set default anonymous id") + #endif + XCTAssertNil( + testMixpanel.people.distinctId, + "mixpanel people distinct id should default to nil") + XCTAssertNil( + testMixpanel.people.distinctId, + "mixpanel user id should default to nil") + testMixpanel.track(event: "e1") + waitForTrackingQueue(testMixpanel) + let eventsQueue = eventQueue(token: testMixpanel.apiToken) + XCTAssertTrue( + eventsQueue.count == 1, + "events should be sent right away with default distinct id") + #if MIXPANEL_UNIQUE_DISTINCT_ID + XCTAssertEqual( + (eventsQueue.last?["properties"] as? InternalProperties)?["distinct_id"] as? String, + devicePrefix + mixpanel.defaultDeviceId(), + "events should use default distinct id if none set") + #endif + XCTAssertEqual( + (eventsQueue.last?["properties"] as? InternalProperties)?["$lib_version"] as? String, + AutomaticProperties.libVersion(), + "events should has lib version in internal properties") + testMixpanel.people.set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + var peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + var unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) + XCTAssertTrue( + peopleQueue_value.isEmpty, + "people records should go to unidentified queue before identify:") + XCTAssertTrue( + unidentifiedQueue.count == 1, + "unidentified people records not queued") + XCTAssertEqual( + unidentifiedQueue.last?["$token"] as? String, + testMixpanel.apiToken, + "incorrect project token in people record") + testMixpanel.identify(distinctId: distinctId) + waitForTrackingQueue(testMixpanel) + let anonymousId = testMixpanel.anonymousId + peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) + XCTAssertEqual( + testMixpanel.distinctId, distinctId, + "mixpanel identify failed to set distinct id") + XCTAssertEqual( + testMixpanel.userId, distinctId, + "mixpanel identify failed to set user id") + XCTAssertEqual( + testMixpanel.anonymousId, anonymousId, + "mixpanel identify shouldn't change anonymousId") + XCTAssertEqual( + testMixpanel.people.distinctId, distinctId, + "mixpanel identify failed to set people distinct id") + XCTAssertTrue( + unidentifiedQueue.isEmpty, + "identify: should move records from unidentified queue") + XCTAssertTrue( + peopleQueue_value.count > 0, + "identify: should move records to main people queue") + XCTAssertEqual( + peopleQueue_value.last?["$token"] as? String, + testMixpanel.apiToken, "incorrect project token in people record") + let p: InternalProperties = peopleQueue_value.last?["$set"] as! InternalProperties + XCTAssertEqual(p["p1"] as? String, "a", "custom people property not queued") + assertDefaultPeopleProperties(p) + peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + + testMixpanel.people.set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + + peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) + XCTAssertEqual( + peopleQueue_value.last?["$distinct_id"] as? String, + distinctId, "distinct id not set properly on unidentified people record") + XCTAssertTrue( + unidentifiedQueue.isEmpty, + "once idenitfy: is called, unidentified queue should be skipped") + XCTAssertTrue( + peopleQueue_value.count > 0, + "once identify: is called, records should go straight to main queue") + testMixpanel.track(event: "e2") + waitForTrackingQueue(testMixpanel) + let newDistinctId = + (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?[ + "distinct_id"] as? String + XCTAssertEqual( + newDistinctId, distinctId, + "events should use new distinct id after identify:") + testMixpanel.reset() + waitForTrackingQueue(testMixpanel) } + removeDBfile(testMixpanel) + } - func testTrackWithDefaultProperties() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.track(event: "Something Happened") - waitForTrackingQueue(testMixpanel) - let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(e["event"] as? String, "Something Happened", "incorrect event name") - let p: InternalProperties = e["properties"] as! InternalProperties - XCTAssertNotNil(p["$app_build_number"], "$app_build_number not set") - XCTAssertNotNil(p["$app_version_string"], "$app_version_string not set") - XCTAssertNotNil(p["$lib_version"], "$lib_version not set") - XCTAssertNotNil(p["$model"], "$model not set") - XCTAssertNotNil(p["$os"], "$os not set") - XCTAssertNotNil(p["$os_version"], "$os_version not set") - XCTAssertNotNil(p["$screen_height"], "$screen_height not set") - XCTAssertNotNil(p["$screen_width"], "$screen_width not set") - XCTAssertNotNil(p["distinct_id"], "distinct_id not set") - XCTAssertNotNil(p["time"], "time not set") - XCTAssertEqual(p["$manufacturer"] as? String, "Apple", "incorrect $manufacturer") - XCTAssertEqual(p["mp_lib"] as? String, "swift", "incorrect mp_lib") - XCTAssertEqual(p["token"] as? String, testMixpanel.apiToken, "incorrect token") - removeDBfile(testMixpanel) + func testIdentifyTrack() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let distinctIdBeforeIdentify: String? = testMixpanel.distinctId + let distinctId = "testIdentifyTrack" + + testMixpanel.identify(distinctId: distinctId) + waitForTrackingQueue(testMixpanel) + let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(e["event"] as? String, "$identify", "incorrect event name") + let p: InternalProperties = e["properties"] as! InternalProperties + XCTAssertEqual(p["distinct_id"] as? String, distinctId, "wrong distinct_id") + XCTAssertEqual( + p["$anon_distinct_id"] as? String, distinctIdBeforeIdentify, "wrong $anon_distinct_id") + removeDBfile(testMixpanel) + } + + func testIdentifyResetTrack() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let originalDistinctId: String? = testMixpanel.distinctId + let distinctId = "testIdentifyTrack" + testMixpanel.reset() + waitForTrackingQueue(testMixpanel) + + for i in 1...3 { + let prevDistinctId: String? = testMixpanel.distinctId + let newDistinctId = distinctId + String(i) + testMixpanel.identify(distinctId: newDistinctId) + waitForTrackingQueue(testMixpanel) + waitForTrackingQueue(testMixpanel) + let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(e["event"] as? String, "$identify", "incorrect event name") + let p: InternalProperties = e["properties"] as! InternalProperties + XCTAssertEqual(p["distinct_id"] as? String, newDistinctId, "wrong distinct_id") + XCTAssertEqual(p["$anon_distinct_id"] as? String, prevDistinctId, "wrong $anon_distinct_id") + XCTAssertNotEqual( + prevDistinctId, originalDistinctId, "After reset, UUID will be used - never the same") + #if MIXPANEL_UNIQUE_DISTINCT_ID + XCTAssertEqual( + prevDistinctId, originalDistinctId, "After reset, IFV will be used - always the same") + #endif + testMixpanel.reset() + waitForTrackingQueue(testMixpanel) } + removeDBfile(testMixpanel) + } - func testTrackWithCustomProperties() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let now = Date() - let p: Properties = ["string": "yello", - "number": 3, - "date": now, - "$app_version": "override"] - testMixpanel.track(event: "Something Happened", properties: p) - waitForTrackingQueue(testMixpanel) - let props: InternalProperties = eventQueue(token: testMixpanel.apiToken).last?["properties"] as! InternalProperties - XCTAssertEqual(props["string"] as? String, "yello") - XCTAssertEqual(props["number"] as? Int, 3) - let dateValue = props["date"] as! String - compareDate(dateString: dateValue, dateDate: now) - XCTAssertEqual(props["$app_version"] as? String, "override", - "reserved property override failed") - removeDBfile(testMixpanel) + func testPersistentIdentity() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let distinctId: String = "d1" + let alias: String = "a1" + testMixpanel.identify(distinctId: distinctId) + waitForTrackingQueue(testMixpanel) + testMixpanel.createAlias(alias, distinctId: testMixpanel.distinctId) + waitForTrackingQueue(testMixpanel) + var mixpanelIdentity = MixpanelPersistence.loadIdentity(apiToken: testMixpanel.apiToken) + XCTAssertTrue( + distinctId == mixpanelIdentity.distinctID && distinctId == mixpanelIdentity.peopleDistinctID + && distinctId == mixpanelIdentity.userId && alias == mixpanelIdentity.alias) + testMixpanel.archive() + waitForTrackingQueue(testMixpanel) + testMixpanel.unarchive() + waitForTrackingQueue(testMixpanel) + mixpanelIdentity = MixpanelPersistence.loadIdentity(apiToken: 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) + waitForTrackingQueue(testMixpanel) + mixpanelIdentity = MixpanelPersistence.loadIdentity(apiToken: testMixpanel.apiToken) + XCTAssertTrue( + "" == mixpanelIdentity.distinctID && nil == mixpanelIdentity.peopleDistinctID + && nil == mixpanelIdentity.anonymousId && nil == mixpanelIdentity.userId + && nil == mixpanelIdentity.alias) + removeDBfile(testMixpanel) + } + + func testHadPersistedDistinctId() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + XCTAssertNotNil(testMixpanel.distinctId) + let distinctId: String = "d1" + testMixpanel.anonymousId = nil + testMixpanel.userId = nil + testMixpanel.alias = nil + testMixpanel.distinctId = distinctId + testMixpanel.archive() + + XCTAssertEqual(testMixpanel.distinctId, distinctId) + + let userId: String = "u1" + testMixpanel.identify(distinctId: userId) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.anonymousId, distinctId) + XCTAssertEqual(testMixpanel.userId, userId) + XCTAssertEqual(testMixpanel.distinctId, userId) + XCTAssertTrue(testMixpanel.hadPersistedDistinctId!) + removeDBfile(testMixpanel) + } + + func testTrackWithDefaultProperties() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.track(event: "Something Happened") + waitForTrackingQueue(testMixpanel) + let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(e["event"] as? String, "Something Happened", "incorrect event name") + let p: InternalProperties = e["properties"] as! InternalProperties + XCTAssertNotNil(p["$app_build_number"], "$app_build_number not set") + XCTAssertNotNil(p["$app_version_string"], "$app_version_string not set") + XCTAssertNotNil(p["$lib_version"], "$lib_version not set") + XCTAssertNotNil(p["$model"], "$model not set") + XCTAssertNotNil(p["$os"], "$os not set") + XCTAssertNotNil(p["$os_version"], "$os_version not set") + XCTAssertNotNil(p["$screen_height"], "$screen_height not set") + XCTAssertNotNil(p["$screen_width"], "$screen_width not set") + XCTAssertNotNil(p["distinct_id"], "distinct_id not set") + XCTAssertNotNil(p["time"], "time not set") + XCTAssertEqual(p["$manufacturer"] as? String, "Apple", "incorrect $manufacturer") + XCTAssertEqual(p["mp_lib"] as? String, "swift", "incorrect mp_lib") + XCTAssertEqual(p["token"] as? String, testMixpanel.apiToken, "incorrect token") + removeDBfile(testMixpanel) + } + + func testTrackWithCustomProperties() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let now = Date() + let p: Properties = [ + "string": "yello", + "number": 3, + "date": now, + "$app_version": "override", + ] + testMixpanel.track(event: "Something Happened", properties: p) + waitForTrackingQueue(testMixpanel) + let props: InternalProperties = + eventQueue(token: testMixpanel.apiToken).last?["properties"] as! InternalProperties + XCTAssertEqual(props["string"] as? String, "yello") + XCTAssertEqual(props["number"] as? Int, 3) + let dateValue = props["date"] as! String + compareDate(dateString: dateValue, dateDate: now) + XCTAssertEqual( + props["$app_version"] as? String, "override", + "reserved property override failed") + removeDBfile(testMixpanel) + } + + func testTrackWithOptionalProperties() { + let optNil: Double? = nil + let optDouble: Double? = 1.0 + let optArray: [Double?] = [nil, 1.0, 2.0] + let optDict: [String: Double?] = ["nil": nil, "double": 1.0] + let nested: [String: Any] = ["list": optArray, "dict": optDict] + let p: Properties = [ + "nil": optNil, + "double": optDouble, + "list": optArray, + "dict": optDict, + "nested": nested, + ] + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.track(event: "Optional Test", properties: p) + waitForTrackingQueue(testMixpanel) + let props: InternalProperties = + eventQueue(token: testMixpanel.apiToken).last?["properties"] as! InternalProperties + XCTAssertNil(props["nil"] as? Double) + XCTAssertEqual(props["double"] as? Double, 1.0) + XCTAssertEqual(props["list"] as? Array, [1.0, 2.0]) + XCTAssertEqual(props["dict"] as? Dictionary, ["nil": nil, "double": 1.0]) + let nestedProp = props["nested"] as? [String: Any] + XCTAssertEqual(nestedProp?["dict"] as? Dictionary, ["nil": nil, "double": 1.0]) + XCTAssertEqual(nestedProp?["list"] as? Array, [1.0, 2.0]) + removeDBfile(testMixpanel) + } + + func testTrackWithCustomDistinctIdAndToken() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let p: Properties = ["token": "t1", "distinct_id": "d1"] + testMixpanel.track(event: "e1", properties: p) + waitForTrackingQueue(testMixpanel) + let trackToken = + (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?[ + "token"] as? String + let trackDistinctId = + (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?[ + "distinct_id"] as? String + XCTAssertEqual(trackToken, "t1", "user-defined distinct id not used in track.") + XCTAssertEqual(trackDistinctId, "d1", "user-defined distinct id not used in track.") + removeDBfile(testMixpanel) + } + + func testTrackWithGroups() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + testMixpanel.trackWithGroups( + event: "Something Happened", properties: [groupKey: "some other value", "p1": "value"], + groups: [groupKey: groupID]) + waitForTrackingQueue(testMixpanel) + let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(e["event"] as? String, "Something Happened", "incorrect event name") + let p: InternalProperties = e["properties"] as! InternalProperties + XCTAssertNotNil(p["$app_build_number"], "$app_build_number not set") + XCTAssertNotNil(p["$app_version_string"], "$app_version_string not set") + XCTAssertNotNil(p["$lib_version"], "$lib_version not set") + XCTAssertNotNil(p["$model"], "$model not set") + XCTAssertNotNil(p["$os"], "$os not set") + XCTAssertNotNil(p["$os_version"], "$os_version not set") + XCTAssertNotNil(p["$screen_height"], "$screen_height not set") + XCTAssertNotNil(p["$screen_width"], "$screen_width not set") + XCTAssertNotNil(p["distinct_id"], "distinct_id not set") + XCTAssertNotNil(p["time"], "time not set") + XCTAssertEqual(p["$manufacturer"] as? String, "Apple", "incorrect $manufacturer") + XCTAssertEqual(p["mp_lib"] as? String, "swift", "incorrect mp_lib") + XCTAssertEqual(p["token"] as? String, testMixpanel.apiToken, "incorrect token") + XCTAssertEqual(p[groupKey] as? String, groupID, "incorrect group id") + XCTAssertEqual(p["p1"] as? String, "value", "incorrect group value") + removeDBfile(testMixpanel) + } + + func testRegisterSuperProperties() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + var p: Properties = ["p1": "a", "p2": 3, "p3": Date()] + testMixpanel.registerSuperProperties(p) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + NSDictionary(dictionary: testMixpanel.currentSuperProperties()), + NSDictionary(dictionary: p), + "register super properties failed") + p = ["p1": "b"] + testMixpanel.registerSuperProperties(p) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + testMixpanel.currentSuperProperties()["p1"] as? String, "b", + "register super properties failed to overwrite existing value") + p = ["p4": "a"] + testMixpanel.registerSuperPropertiesOnce(p) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + testMixpanel.currentSuperProperties()["p4"] as? String, "a", + "register super properties once failed first time") + p = ["p4": "b"] + testMixpanel.registerSuperPropertiesOnce(p) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + testMixpanel.currentSuperProperties()["p4"] as? String, "a", + "register super properties once failed second time") + p = ["p4": "c"] + testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "d") + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + testMixpanel.currentSuperProperties()["p4"] as? String, "a", + "register super properties once with default value failed when no match") + testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "a") + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + testMixpanel.currentSuperProperties()["p4"] as? String, "c", + "register super properties once with default value failed when match") + testMixpanel.unregisterSuperProperty("a") + waitForTrackingQueue(testMixpanel) + XCTAssertNil( + testMixpanel.currentSuperProperties()["a"], + "unregister super property failed") + // unregister non-existent super property should not throw + testMixpanel.unregisterSuperProperty("a") + testMixpanel.clearSuperProperties() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + testMixpanel.currentSuperProperties().isEmpty, + "clear super properties failed") + removeDBfile(testMixpanel) + } + + func testInvalidPropertiesTrack() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let p: Properties = ["data": [Data()]] + XCTExpectAssert("property type should not be allowed") { + testMixpanel.track(event: "e1", properties: p) } - - func testTrackWithOptionalProperties() { - let optNil: Double? = nil - let optDouble: Double? = 1.0 - let optArray: Array = [nil, 1.0, 2.0] - let optDict: Dictionary = ["nil": nil, "double": 1.0] - let nested: Dictionary = ["list": optArray, "dict": optDict] - let p: Properties = ["nil": optNil, - "double": optDouble, - "list": optArray, - "dict": optDict, - "nested": nested, - ] - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.track(event: "Optional Test", properties: p) - waitForTrackingQueue(testMixpanel) - let props: InternalProperties = eventQueue(token: testMixpanel.apiToken).last?["properties"] as! InternalProperties - XCTAssertNil(props["nil"] as? Double) - XCTAssertEqual(props["double"] as? Double, 1.0) - XCTAssertEqual(props["list"] as? Array, [1.0, 2.0]) - XCTAssertEqual(props["dict"] as? Dictionary, ["nil": nil, "double": 1.0]) - let nestedProp = props["nested"] as? Dictionary - XCTAssertEqual(nestedProp?["dict"] as? Dictionary, ["nil": nil, "double": 1.0]) - XCTAssertEqual(nestedProp?["list"] as? Array, [1.0, 2.0]) - removeDBfile(testMixpanel) + removeDBfile(testMixpanel) + } + + func testInvalidSuperProperties() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let p: Properties = ["data": [Data()]] + XCTExpectAssert("property type should not be allowed") { + testMixpanel.registerSuperProperties(p) } + removeDBfile(testMixpanel) + } - func testTrackWithCustomDistinctIdAndToken() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let p: Properties = ["token": "t1", "distinct_id": "d1"] - testMixpanel.track(event: "e1", properties: p) - waitForTrackingQueue(testMixpanel) - let trackToken = (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?["token"] as? String - let trackDistinctId = (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?["distinct_id"] as? String - XCTAssertEqual(trackToken, "t1", "user-defined distinct id not used in track.") - XCTAssertEqual(trackDistinctId, "d1", "user-defined distinct id not used in track.") - removeDBfile(testMixpanel) + func testInvalidSuperProperties2() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let p: Properties = ["data": [Data()]] + XCTExpectAssert("property type should not be allowed") { + testMixpanel.registerSuperPropertiesOnce(p) } + removeDBfile(testMixpanel) + } - func testTrackWithGroups() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - testMixpanel.trackWithGroups(event: "Something Happened", properties: [groupKey: "some other value", "p1": "value"], groups: [groupKey: groupID]) - waitForTrackingQueue(testMixpanel) - let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(e["event"] as? String, "Something Happened", "incorrect event name") - let p: InternalProperties = e["properties"] as! InternalProperties - XCTAssertNotNil(p["$app_build_number"], "$app_build_number not set") - XCTAssertNotNil(p["$app_version_string"], "$app_version_string not set") - XCTAssertNotNil(p["$lib_version"], "$lib_version not set") - XCTAssertNotNil(p["$model"], "$model not set") - XCTAssertNotNil(p["$os"], "$os not set") - XCTAssertNotNil(p["$os_version"], "$os_version not set") - XCTAssertNotNil(p["$screen_height"], "$screen_height not set") - XCTAssertNotNil(p["$screen_width"], "$screen_width not set") - XCTAssertNotNil(p["distinct_id"], "distinct_id not set") - XCTAssertNotNil(p["time"], "time not set") - XCTAssertEqual(p["$manufacturer"] as? String, "Apple", "incorrect $manufacturer") - XCTAssertEqual(p["mp_lib"] as? String, "swift", "incorrect mp_lib") - XCTAssertEqual(p["token"] as? String, testMixpanel.apiToken, "incorrect token") - XCTAssertEqual(p[groupKey] as? String, groupID, "incorrect group id") - XCTAssertEqual(p["p1"] as? String, "value", "incorrect group value") - removeDBfile(testMixpanel) + func testInvalidSuperProperties3() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let p: Properties = ["data": [Data()]] + XCTExpectAssert("property type should not be allowed") { + testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "v") } + removeDBfile(testMixpanel) + } - func testRegisterSuperProperties() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - var p: Properties = ["p1": "a", "p2": 3, "p3": Date()] - testMixpanel.registerSuperProperties(p) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(NSDictionary(dictionary: testMixpanel.currentSuperProperties()), - NSDictionary(dictionary: p), - "register super properties failed") - p = ["p1": "b"] - testMixpanel.registerSuperProperties(p) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()["p1"] as? String, "b", - "register super properties failed to overwrite existing value") - p = ["p4": "a"] - testMixpanel.registerSuperPropertiesOnce(p) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()["p4"] as? String, "a", - "register super properties once failed first time") - p = ["p4": "b"] - testMixpanel.registerSuperPropertiesOnce(p) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()["p4"] as? String, "a", - "register super properties once failed second time") - p = ["p4": "c"] - testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "d") - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()["p4"] as? String, "a", - "register super properties once with default value failed when no match") - testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "a") - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()["p4"] as? String, "c", - "register super properties once with default value failed when match") - testMixpanel.unregisterSuperProperty("a") - waitForTrackingQueue(testMixpanel) - XCTAssertNil(testMixpanel.currentSuperProperties()["a"], - "unregister super property failed") - // unregister non-existent super property should not throw - testMixpanel.unregisterSuperProperty("a") - testMixpanel.clearSuperProperties() - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(testMixpanel.currentSuperProperties().isEmpty, - "clear super properties failed") - removeDBfile(testMixpanel) - } + func testValidPropertiesTrack() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let p: Properties = allPropertyTypes() + testMixpanel.track(event: "e1", properties: p) + removeDBfile(testMixpanel) + } - func testInvalidPropertiesTrack() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let p: Properties = ["data": [Data()]] - XCTExpectAssert("property type should not be allowed") { - testMixpanel.track(event: "e1", properties: p) - } - removeDBfile(testMixpanel) - } + func testValidSuperProperties() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let p: Properties = allPropertyTypes() + testMixpanel.registerSuperProperties(p) + testMixpanel.registerSuperPropertiesOnce(p) + testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "v") + removeDBfile(testMixpanel) + } - func testInvalidSuperProperties() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let p: Properties = ["data": [Data()]] - XCTExpectAssert("property type should not be allowed") { - testMixpanel.registerSuperProperties(p) - } - removeDBfile(testMixpanel) - } + func testReset() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.track(event: "e1") + waitForTrackingQueue(testMixpanel) + let p: Properties = ["p1": "a"] + testMixpanel.registerSuperProperties(p) + testMixpanel.people.set(properties: p) + testMixpanel.archive() + testMixpanel.reset() + waitForTrackingQueue(testMixpanel) + #if MIXPANEL_UNIQUE_DISTINCT_ID + XCTAssertEqual( + testMixpanel.distinctId, + devicePrefix + testMixpanel.defaultDeviceId(), + "distinct id failed to reset") + #endif + XCTAssertNil(testMixpanel.people.distinctId, "people distinct id failed to reset") + XCTAssertTrue( + testMixpanel.currentSuperProperties().isEmpty, + "super properties failed to reset") + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "events queue failed to reset") + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "people queue failed to reset") + let testMixpanel2 = Mixpanel.initialize(token: randomId(), flushInterval: 60) + waitForTrackingQueue(testMixpanel2) + #if MIXPANEL_UNIQUE_DISTINCT_ID + XCTAssertEqual( + testMixpanel2.distinctId, devicePrefix + testMixpanel2.defaultDeviceId(), + "distinct id failed to reset after archive") + #endif + XCTAssertNil( + testMixpanel2.people.distinctId, + "people distinct id failed to reset after archive") + XCTAssertTrue( + testMixpanel2.currentSuperProperties().isEmpty, + "super properties failed to reset after archive") + XCTAssertTrue( + eventQueue(token: testMixpanel2.apiToken).isEmpty, + "events queue failed to reset after archive") + XCTAssertTrue( + peopleQueue(token: testMixpanel2.apiToken).isEmpty, + "people queue failed to reset after archive") + removeDBfile(testMixpanel) + removeDBfile(testMixpanel2) + } - func testInvalidSuperProperties2() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let p: Properties = ["data": [Data()]] - XCTExpectAssert("property type should not be allowed") { - testMixpanel.registerSuperPropertiesOnce(p) - } - removeDBfile(testMixpanel) - } + func testArchiveNSNumberBoolIntProperty() { + let testToken = randomId() + let testMixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) + testMixpanel.serverURL = kFakeServerUrl + let aBoolNumber: Bool = true + let aBoolNSNumber = NSNumber(value: aBoolNumber) - func testInvalidSuperProperties3() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let p: Properties = ["data": [Data()]] - XCTExpectAssert("property type should not be allowed") { - testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "v") - } - removeDBfile(testMixpanel) - } + let aIntNumber: Int = 1 + let aIntNSNumber = NSNumber(value: aIntNumber) - func testValidPropertiesTrack() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let p: Properties = allPropertyTypes() - testMixpanel.track(event: "e1", properties: p) - removeDBfile(testMixpanel) - } + testMixpanel.track(event: "e1", properties: ["p1": aBoolNSNumber, "p2": aIntNSNumber]) + testMixpanel.archive() + waitForTrackingQueue(testMixpanel) + testMixpanel.mixpanelPersistence.closeDB() + let testMixpanel2 = Mixpanel.initialize(token: testToken, flushInterval: 60) + testMixpanel2.serverURL = kFakeServerUrl + waitForTrackingQueue(testMixpanel2) + let properties: [String: Any] = + eventQueue(token: testMixpanel2.apiToken)[0]["properties"] as! [String: Any] - func testValidSuperProperties() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let p: Properties = allPropertyTypes() - testMixpanel.registerSuperProperties(p) - testMixpanel.registerSuperPropertiesOnce(p) - testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "v") - removeDBfile(testMixpanel) - } + XCTAssertTrue( + isBoolNumber(num: properties["p1"]! as! NSNumber), + "The bool value should be unarchived as bool") + XCTAssertFalse( + isBoolNumber(num: properties["p2"]! as! NSNumber), + "The int value should not be unarchived as bool") + removeDBfile(testMixpanel2) + } - func testReset() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - testMixpanel.track(event: "e1") - waitForTrackingQueue(testMixpanel) - let p: Properties = ["p1": "a"] - testMixpanel.registerSuperProperties(p) - testMixpanel.people.set(properties: p) - testMixpanel.archive() - testMixpanel.reset() - waitForTrackingQueue(testMixpanel) - #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(testMixpanel.distinctId, - devicePrefix + testMixpanel.defaultDeviceId(), - "distinct id failed to reset") - #endif - XCTAssertNil(testMixpanel.people.distinctId, "people distinct id failed to reset") - XCTAssertTrue(testMixpanel.currentSuperProperties().isEmpty, - "super properties failed to reset") - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "events queue failed to reset") - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "people queue failed to reset") - let testMixpanel2 = Mixpanel.initialize(token: randomId(), flushInterval: 60) - waitForTrackingQueue(testMixpanel2) - #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(testMixpanel2.distinctId, devicePrefix + testMixpanel2.defaultDeviceId(), - "distinct id failed to reset after archive") - #endif - XCTAssertNil(testMixpanel2.people.distinctId, - "people distinct id failed to reset after archive") - XCTAssertTrue(testMixpanel2.currentSuperProperties().isEmpty, - "super properties failed to reset after archive") - XCTAssertTrue(eventQueue(token: testMixpanel2.apiToken).isEmpty, - "events queue failed to reset after archive") - XCTAssertTrue(peopleQueue(token: testMixpanel2.apiToken).isEmpty, - "people queue failed to reset after archive") - removeDBfile(testMixpanel) - removeDBfile(testMixpanel2) - } + private func isBoolNumber(num: NSNumber) -> Bool { + let boolID = CFBooleanGetTypeID() // the type ID of CFBoolean + let numID = CFGetTypeID(num) // the type ID of num + return numID == boolID + } - func testArchiveNSNumberBoolIntProperty() { - let testToken = randomId() - let testMixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) - testMixpanel.serverURL = kFakeServerUrl - let aBoolNumber: Bool = true - let aBoolNSNumber = NSNumber(value: aBoolNumber) + func testArchive() { + let testToken = randomId() + let testMixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) + testMixpanel.serverURL = kFakeServerUrl + #if MIXPANEL_UNIQUE_DISTINCT_ID + XCTAssertEqual( + testMixpanel.distinctId, devicePrefix + testMixpanel.defaultDeviceId(), + "default distinct id archive failed") + #endif + XCTAssertTrue( + testMixpanel.currentSuperProperties().isEmpty, + "default super properties archive failed") + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).isEmpty, "default events queue archive failed") + XCTAssertNil(testMixpanel.people.distinctId, "default people distinct id archive failed") + XCTAssertTrue( + peopleQueue(token: testMixpanel.apiToken).isEmpty, "default people queue archive failed") + let p: Properties = ["p1": "a"] + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + testMixpanel.registerSuperProperties(p) + testMixpanel.track(event: "e1") + testMixpanel.track(event: "e2") + testMixpanel.track(event: "e3") + testMixpanel.track(event: "e4") + testMixpanel.track(event: "e5") + testMixpanel.track(event: "e6") + testMixpanel.track(event: "e7") + testMixpanel.track(event: "e8") + testMixpanel.track(event: "e9") + testMixpanel.people.set(properties: p) + waitForTrackingQueue(testMixpanel) + testMixpanel.timedEvents["e2"] = 5 + testMixpanel.archive() + testMixpanel.mixpanelPersistence.closeDB() + let testMixpanel2 = Mixpanel.initialize(token: testToken, flushInterval: 60) + testMixpanel2.serverURL = kFakeServerUrl + waitForTrackingQueue(testMixpanel2) + XCTAssertEqual(testMixpanel2.distinctId, "d1", "custom distinct archive failed") + XCTAssertTrue( + testMixpanel2.currentSuperProperties().count == 1, + "custom super properties archive failed") + let eventQueueValue = eventQueue(token: testMixpanel2.apiToken) - let aIntNumber: Int = 1 - let aIntNSNumber = NSNumber(value: aIntNumber) + XCTAssertEqual( + eventQueueValue[1]["event"] as? String, "e1", + "event was not successfully archived/unarchived") + XCTAssertEqual( + eventQueueValue[2]["event"] as? String, "e2", + "event was not successfully archived/unarchived or order is incorrect") + XCTAssertEqual( + eventQueueValue[3]["event"] as? String, "e3", + "event was not successfully archived/unarchived or order is incorrect") + XCTAssertEqual( + eventQueueValue[4]["event"] as? String, "e4", + "event was not successfully archived/unarchived or order is incorrect") + XCTAssertEqual( + eventQueueValue[5]["event"] as? String, "e5", + "event was not successfully archived/unarchived or order is incorrect") + XCTAssertEqual( + eventQueueValue[6]["event"] as? String, "e6", + "event was not successfully archived/unarchived or order is incorrect") + XCTAssertEqual( + eventQueueValue[7]["event"] as? String, "e7", + "event was not successfully archived/unarchived or order is incorrect") + XCTAssertEqual( + eventQueueValue[8]["event"] as? String, "e8", + "event was not successfully archived/unarchived or order is incorrect") + XCTAssertEqual( + eventQueueValue[9]["event"] as? String, "e9", + "event was not successfully archived/unarchived or order is incorrect") + XCTAssertEqual( + testMixpanel2.people.distinctId, "d1", + "custom people distinct id archive failed") + XCTAssertTrue( + peopleQueue(token: testMixpanel2.apiToken).count == 1, "pending people queue archive failed") + XCTAssertEqual( + testMixpanel2.timedEvents["e2"] as? Int, 5, + "timedEvents archive failed") + testMixpanel2.mixpanelPersistence.closeDB() + let testMixpanel3 = Mixpanel.initialize(token: testToken, flushInterval: 60) + testMixpanel3.serverURL = kFakeServerUrl + XCTAssertEqual(testMixpanel3.distinctId, "d1", "expecting d1 as distinct id as initialised") + XCTAssertTrue( + testMixpanel3.currentSuperProperties().count == 1, + "default super properties expected to have 1 item") + XCTAssertNotNil(eventQueue(token: testMixpanel3.apiToken), "default events queue is nil") + XCTAssertTrue( + eventQueue(token: testMixpanel3.apiToken).count == 10, + "default events queue expecting 10 items ($identify call added)") + XCTAssertNotNil( + testMixpanel3.people.distinctId, + "default people distinct id from no file failed") + XCTAssertNotNil( + peopleQueue(token: testMixpanel3.apiToken), "default people queue from no file is nil") + XCTAssertTrue( + peopleQueue(token: testMixpanel3.apiToken).count == 1, "default people queue expecting 1 item" + ) + XCTAssertTrue(testMixpanel3.timedEvents.count == 1, "timedEvents expecting 1 item") + testMixpanel3.mixpanelPersistence.closeDB() + removeDBfile(testMixpanel) + } - testMixpanel.track(event: "e1", properties: ["p1": aBoolNSNumber, "p2": aIntNSNumber]) - testMixpanel.archive() - waitForTrackingQueue(testMixpanel) - testMixpanel.mixpanelPersistence.closeDB() - let testMixpanel2 = Mixpanel.initialize(token: testToken, flushInterval: 60) - testMixpanel2.serverURL = kFakeServerUrl - waitForTrackingQueue(testMixpanel2) - let properties: [String: Any] = eventQueue(token: testMixpanel2.apiToken)[0]["properties"] as! [String: Any] + func testMixpanelDelegate() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.delegate = self + testMixpanel.identify(distinctId: "d1") + testMixpanel.track(event: "e1") + testMixpanel.people.set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 2, "delegate should have stopped flush") + XCTAssertTrue( + peopleQueue(token: testMixpanel.apiToken).count == 1, "delegate should have stopped flush") + removeDBfile(testMixpanel) + } - XCTAssertTrue(isBoolNumber(num: properties["p1"]! as! NSNumber), - "The bool value should be unarchived as bool") - XCTAssertFalse(isBoolNumber(num: properties["p2"]! as! NSNumber), - "The int value should not be unarchived as bool") - removeDBfile(testMixpanel2) - } + func testEventTiming() { + 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.") + testMixpanel.time(event: "400 Meters") + testMixpanel.track(event: "500 Meters") + waitForTrackingQueue(testMixpanel) + e = eventQueue(token: testMixpanel.apiToken).last! + p = e["properties"] as! InternalProperties + XCTAssertNil(p["$duration"], "The exact same event name is required for timing.") + testMixpanel.track(event: "400 Meters") + waitForTrackingQueue(testMixpanel) + e = eventQueue(token: testMixpanel.apiToken).last! + p = e["properties"] as! InternalProperties + XCTAssertNotNil(p["$duration"], "This event should be timed.") + testMixpanel.track(event: "400 Meters") + 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.") + testMixpanel.time(event: "Time Event A") + testMixpanel.time(event: "Time Event B") + testMixpanel.time(event: "Time Event C") + waitForTrackingQueue(testMixpanel) + var testTimedEvents = MixpanelPersistence.loadTimedEvents(apiToken: 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) + 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, + "clearTimedEvents should remove all key/value pairs") + removeDBfile(testMixpanel) + } - private func isBoolNumber(num: NSNumber) -> Bool - { - let boolID = CFBooleanGetTypeID() // the type ID of CFBoolean - let numID = CFGetTypeID(num) // the type ID of num - return numID == boolID - } - - func testArchive() { - let testToken = randomId() - let testMixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) - testMixpanel.serverURL = kFakeServerUrl - #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(testMixpanel.distinctId, devicePrefix + testMixpanel.defaultDeviceId(), - "default distinct id archive failed") - #endif - XCTAssertTrue(testMixpanel.currentSuperProperties().isEmpty, - "default super properties archive failed") - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "default events queue archive failed") - XCTAssertNil(testMixpanel.people.distinctId, "default people distinct id archive failed") - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "default people queue archive failed") - let p: Properties = ["p1": "a"] - testMixpanel.identify(distinctId: "d1") - waitForTrackingQueue(testMixpanel) - testMixpanel.registerSuperProperties(p) - testMixpanel.track(event: "e1") - testMixpanel.track(event: "e2") - testMixpanel.track(event: "e3") - testMixpanel.track(event: "e4") - testMixpanel.track(event: "e5") - testMixpanel.track(event: "e6") - testMixpanel.track(event: "e7") - testMixpanel.track(event: "e8") - testMixpanel.track(event: "e9") - testMixpanel.people.set(properties: p) - waitForTrackingQueue(testMixpanel) - testMixpanel.timedEvents["e2"] = 5 - testMixpanel.archive() - testMixpanel.mixpanelPersistence.closeDB() - let testMixpanel2 = Mixpanel.initialize(token: testToken, flushInterval: 60) - testMixpanel2.serverURL = kFakeServerUrl - waitForTrackingQueue(testMixpanel2) - XCTAssertEqual(testMixpanel2.distinctId, "d1", "custom distinct archive failed") - XCTAssertTrue(testMixpanel2.currentSuperProperties().count == 1, - "custom super properties archive failed") - let eventQueueValue = eventQueue(token: testMixpanel2.apiToken) - - XCTAssertEqual(eventQueueValue[1]["event"] as? String, "e1", - "event was not successfully archived/unarchived") - XCTAssertEqual(eventQueueValue[2]["event"] as? String, "e2", - "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(eventQueueValue[3]["event"] as? String, "e3", - "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(eventQueueValue[4]["event"] as? String, "e4", - "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(eventQueueValue[5]["event"] as? String, "e5", - "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(eventQueueValue[6]["event"] as? String, "e6", - "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(eventQueueValue[7]["event"] as? String, "e7", - "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(eventQueueValue[8]["event"] as? String, "e8", - "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(eventQueueValue[9]["event"] as? String, "e9", - "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(testMixpanel2.people.distinctId, "d1", - "custom people distinct id archive failed") - XCTAssertTrue(peopleQueue(token: testMixpanel2.apiToken).count == 1, "pending people queue archive failed") - XCTAssertEqual(testMixpanel2.timedEvents["e2"] as? Int, 5, - "timedEvents archive failed") - testMixpanel2.mixpanelPersistence.closeDB() - let testMixpanel3 = Mixpanel.initialize(token: testToken, flushInterval: 60) - testMixpanel3.serverURL = kFakeServerUrl - XCTAssertEqual(testMixpanel3.distinctId, "d1", "expecting d1 as distinct id as initialised") - XCTAssertTrue(testMixpanel3.currentSuperProperties().count == 1, - "default super properties expected to have 1 item") - XCTAssertNotNil(eventQueue(token: testMixpanel3.apiToken), "default events queue is nil") - XCTAssertTrue(eventQueue(token: testMixpanel3.apiToken).count == 10, "default events queue expecting 10 items ($identify call added)") - XCTAssertNotNil(testMixpanel3.people.distinctId, - "default people distinct id from no file failed") - XCTAssertNotNil(peopleQueue(token:testMixpanel3.apiToken), "default people queue from no file is nil") - XCTAssertTrue(peopleQueue(token:testMixpanel3.apiToken).count == 1, "default people queue expecting 1 item") - XCTAssertTrue(testMixpanel3.timedEvents.count == 1, "timedEvents expecting 1 item") - testMixpanel3.mixpanelPersistence.closeDB() - removeDBfile(testMixpanel) - } - - - func testMixpanelDelegate() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.delegate = self - testMixpanel.identify(distinctId: "d1") - testMixpanel.track(event: "e1") - testMixpanel.people.set(property: "p1", to: "a") - waitForTrackingQueue(testMixpanel) - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 2, "delegate should have stopped flush") - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).count == 1, "delegate should have stopped flush") - removeDBfile(testMixpanel) - } - - func testEventTiming() { - 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.") - testMixpanel.time(event: "400 Meters") - testMixpanel.track(event: "500 Meters") - waitForTrackingQueue(testMixpanel) - e = eventQueue(token: testMixpanel.apiToken).last! - p = e["properties"] as! InternalProperties - XCTAssertNil(p["$duration"], "The exact same event name is required for timing.") - testMixpanel.track(event: "400 Meters") - waitForTrackingQueue(testMixpanel) - e = eventQueue(token: testMixpanel.apiToken).last! - p = e["properties"] as! InternalProperties - XCTAssertNotNil(p["$duration"], "This event should be timed.") - testMixpanel.track(event: "400 Meters") - 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.") - testMixpanel.time(event: "Time Event A") - testMixpanel.time(event: "Time Event B") - testMixpanel.time(event: "Time Event C") - waitForTrackingQueue(testMixpanel) - var testTimedEvents = MixpanelPersistence.loadTimedEvents(apiToken: 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) - 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, "clearTimedEvents should remove all key/value pairs") - removeDBfile(testMixpanel) - } - - func testReadWriteLock() { - var array = [Int]() - let lock = ReadWriteLock(label: "test") - let queue = DispatchQueue(label: "concurrent", qos: .utility, attributes: .concurrent) - for _ in 0..<10 { - queue.async { - lock.write { - for i in 0..<100 { - array.append(i) - } - } - } - - queue.async { - lock.read { - XCTAssertTrue(array.count % 100 == 0, "supposed to happen after write") - } - } - } - } - - func testSetGroup() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - let groupKey = "test_key" - let groupValue = "test_value" - testMixpanel.setGroup(groupKey: groupKey, groupIDs: [groupValue]) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) - let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties - XCTAssertEqual(q[groupKey] as? [String], [groupValue], "group value people property not queued") - assertDefaultPeopleProperties(q) - removeDBfile(testMixpanel) - } - - func testAddGroup() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - let groupKey = "test_key" - let groupValue = "test_value" - - testMixpanel.addGroup(groupKey: groupKey, groupID: groupValue) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) - waitForTrackingQueue(testMixpanel) - let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties - XCTAssertEqual(q[groupKey] as? [String], [groupValue], "addGroup people update not queued") - assertDefaultPeopleProperties(q) - - testMixpanel.addGroup(groupKey: groupKey, groupID: groupValue) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) - waitForTrackingQueue(testMixpanel) - waitForTrackingQueue(testMixpanel) - let q2 = peopleQueue(token: testMixpanel.apiToken).last!["$union"] as! InternalProperties - XCTAssertEqual(q2[groupKey] as? [String], [groupValue], "addGroup people update not queued") - - let newVal = "new_group" - testMixpanel.addGroup(groupKey: groupKey, groupID: newVal) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue, newVal]) - waitForTrackingQueue(testMixpanel) - waitForTrackingQueue(testMixpanel) - let q3 = peopleQueue(token: testMixpanel.apiToken).last!["$union"] as! InternalProperties - XCTAssertEqual(q3[groupKey] as? [String], [newVal], "addGroup people update not queued") - removeDBfile(testMixpanel) - } - - func testRemoveGroup() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - let groupKey = "test_key" - let groupValue = "test_value" - let newVal = "new_group" - - testMixpanel.setGroup(groupKey: groupKey, groupIDs: [groupValue, newVal]) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue, newVal]) - - testMixpanel.removeGroup(groupKey: groupKey, groupID: groupValue) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [newVal]) - waitForTrackingQueue(testMixpanel) - let q2 = peopleQueue(token: testMixpanel.apiToken).last!["$remove"] as! InternalProperties - XCTAssertEqual(q2[groupKey] as? String, groupValue, "removeGroup people update not queued") - - testMixpanel.removeGroup(groupKey: groupKey, groupID: groupValue) - waitForTrackingQueue(testMixpanel) - XCTAssertNil(testMixpanel.currentSuperProperties()[groupKey]) - waitForTrackingQueue(testMixpanel) - let q3 = peopleQueue(token: testMixpanel.apiToken).last!["$unset"] as! [String] - XCTAssertEqual(q3, [groupKey], "removeGroup people update not queued") - removeDBfile(testMixpanel) - } - - func testMultipleInstancesWithSameToken() { - let testToken = randomId() - let concurentQueue = DispatchQueue(label: "multithread", attributes: .concurrent) - - var testMixpanel: MixpanelInstance? - for _ in 1...10 { - concurentQueue.async { - testMixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) - testMixpanel?.loggingEnabled = true - testMixpanel?.track(event: "test") - } + func testReadWriteLock() { + var array = [Int]() + let lock = ReadWriteLock(label: "test") + let queue = DispatchQueue(label: "concurrent", qos: .utility, attributes: .concurrent) + for _ in 0..<10 { + queue.async { + lock.write { + for i in 0..<100 { + array.append(i) } - - var testMixpanel2: MixpanelInstance? - for _ in 1...10 { - concurentQueue.async { - testMixpanel2 = Mixpanel.initialize(token: testToken, flushInterval: 60) - testMixpanel2?.loggingEnabled = true - testMixpanel2?.track(event: "test") - } } - sleep(5) + } + + queue.async { + lock.read { + XCTAssertTrue(array.count % 100 == 0, "supposed to happen after write") + } + } + } + } + + func testSetGroup() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + let groupKey = "test_key" + let groupValue = "test_value" + testMixpanel.setGroup(groupKey: groupKey, groupIDs: [groupValue]) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties + XCTAssertEqual(q[groupKey] as? [String], [groupValue], "group value people property not queued") + assertDefaultPeopleProperties(q) + removeDBfile(testMixpanel) + } + + func testAddGroup() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + let groupKey = "test_key" + let groupValue = "test_value" + + testMixpanel.addGroup(groupKey: groupKey, groupID: groupValue) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) + waitForTrackingQueue(testMixpanel) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties + XCTAssertEqual(q[groupKey] as? [String], [groupValue], "addGroup people update not queued") + assertDefaultPeopleProperties(q) + + testMixpanel.addGroup(groupKey: groupKey, groupID: groupValue) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) + waitForTrackingQueue(testMixpanel) + waitForTrackingQueue(testMixpanel) + let q2 = peopleQueue(token: testMixpanel.apiToken).last!["$union"] as! InternalProperties + XCTAssertEqual(q2[groupKey] as? [String], [groupValue], "addGroup people update not queued") + + let newVal = "new_group" + testMixpanel.addGroup(groupKey: groupKey, groupID: newVal) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue, newVal]) + waitForTrackingQueue(testMixpanel) + waitForTrackingQueue(testMixpanel) + let q3 = peopleQueue(token: testMixpanel.apiToken).last!["$union"] as! InternalProperties + XCTAssertEqual(q3[groupKey] as? [String], [newVal], "addGroup people update not queued") + removeDBfile(testMixpanel) + } + + func testRemoveGroup() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + let groupKey = "test_key" + let groupValue = "test_value" + let newVal = "new_group" + + testMixpanel.setGroup(groupKey: groupKey, groupIDs: [groupValue, newVal]) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue, newVal]) + + testMixpanel.removeGroup(groupKey: groupKey, groupID: groupValue) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [newVal]) + waitForTrackingQueue(testMixpanel) + let q2 = peopleQueue(token: testMixpanel.apiToken).last!["$remove"] as! InternalProperties + XCTAssertEqual(q2[groupKey] as? String, groupValue, "removeGroup people update not queued") + + testMixpanel.removeGroup(groupKey: groupKey, groupID: groupValue) + waitForTrackingQueue(testMixpanel) + XCTAssertNil(testMixpanel.currentSuperProperties()[groupKey]) + waitForTrackingQueue(testMixpanel) + let q3 = peopleQueue(token: testMixpanel.apiToken).last!["$unset"] as! [String] + XCTAssertEqual(q3, [groupKey], "removeGroup people update not queued") + removeDBfile(testMixpanel) + } + + func testMultipleInstancesWithSameToken() { + let testToken = randomId() + let concurentQueue = DispatchQueue(label: "multithread", attributes: .concurrent) + + var testMixpanel: MixpanelInstance? + for _ in 1...10 { + concurentQueue.async { testMixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) + testMixpanel?.loggingEnabled = true + testMixpanel?.track(event: "test") + } + } + + var testMixpanel2: MixpanelInstance? + for _ in 1...10 { + concurentQueue.async { testMixpanel2 = Mixpanel.initialize(token: testToken, flushInterval: 60) - XCTAssertTrue(testMixpanel === testMixpanel2, "instance with same token should be reused and no sqlite db locked error should be populated") + testMixpanel2?.loggingEnabled = true + testMixpanel2?.track(event: "test") + } } - - func testReadWriteMultiThreadShouldNotCrash() { - let concurentQueue = DispatchQueue(label: "multithread", attributes: .concurrent) - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + sleep(5) + testMixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) + testMixpanel2 = Mixpanel.initialize(token: testToken, flushInterval: 60) + XCTAssertTrue( + testMixpanel === testMixpanel2, + "instance with same token should be reused and no sqlite db locked error should be populated") + } - for n in 1...10 { - concurentQueue.async { - testMixpanel.track(event: "event\(n)") - } - concurentQueue.async { - testMixpanel.flush() - } - concurentQueue.async { - testMixpanel.archive() - } - concurentQueue.async { - testMixpanel.reset() - } - concurentQueue.async { - testMixpanel.createAlias("aaa11", distinctId: testMixpanel.distinctId) - testMixpanel.identify(distinctId: "test") - } - concurentQueue.async { - testMixpanel.registerSuperProperties(["Plan": "Mega"]) - } - concurentQueue.async { - let _ = testMixpanel.currentSuperProperties() - } - concurentQueue.async { - testMixpanel.people.set(property: "aaa", to: "SwiftSDK Cocoapods") - testMixpanel.getGroup(groupKey: "test", groupID: 123).set(properties: ["test": 123]) - testMixpanel.removeGroup(groupKey: "test", groupID: 123) - } - concurentQueue.async { - testMixpanel.track(event: "test") - testMixpanel.time(event: "test") - testMixpanel.clearTimedEvents() - } - } - removeDBfile(testMixpanel) + func testReadWriteMultiThreadShouldNotCrash() { + let concurentQueue = DispatchQueue(label: "multithread", attributes: .concurrent) + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + + for n in 1...10 { + concurentQueue.async { + testMixpanel.track(event: "event\(n)") + } + concurentQueue.async { + testMixpanel.flush() + } + concurentQueue.async { + testMixpanel.archive() + } + concurentQueue.async { + testMixpanel.reset() + } + concurentQueue.async { + testMixpanel.createAlias("aaa11", distinctId: testMixpanel.distinctId) + testMixpanel.identify(distinctId: "test") + } + concurentQueue.async { + testMixpanel.registerSuperProperties(["Plan": "Mega"]) + } + concurentQueue.async { + let _ = testMixpanel.currentSuperProperties() + } + concurentQueue.async { + testMixpanel.people.set(property: "aaa", to: "SwiftSDK Cocoapods") + testMixpanel.getGroup(groupKey: "test", groupID: 123).set(properties: ["test": 123]) + testMixpanel.removeGroup(groupKey: "test", groupID: 123) + } + concurentQueue.async { + testMixpanel.track(event: "test") + testMixpanel.time(event: "test") + testMixpanel.clearTimedEvents() + } } - - func testMPDB() { - let testToken = randomId() - let numRows = 50 - let halfRows = numRows/2 - let eventName = "Test Event" - func _inner() { - removeDBfile(apiToken: testToken) - let mpdb = MPDB.init(token: testToken) - mpdb.open() - for pType in PersistenceType.allCases { - let emptyArray: [InternalProperties] = mpdb.readRows(pType, numRows: numRows) - XCTAssertTrue(emptyArray.isEmpty, "Table should be empty") - for i in 0...numRows-1 { - let eventObj : InternalProperties = ["event": eventName, "properties": ["index": i]] - let eventData = JSONHandler.serializeJSONObject(eventObj)! - mpdb.insertRow(pType, data: eventData) - } - let dataArray : [InternalProperties] = mpdb.readRows(pType, numRows: halfRows) - XCTAssertEqual(dataArray.count, halfRows, "Should have read only half of the rows") - var ids: [Int32] = [] - for (n, entity) in dataArray.enumerated() { - guard let id = entity["id"] as? Int32 else { - continue - } - ids.append(id) - XCTAssertEqual(entity["event"] as! String, eventName, "Event name should be unchanged") - // index should be oldest events, 0 - 24 - XCTAssertEqual(entity["properties"] as! [String : Int], ["index": n], "Should read oldest events first") - } - - mpdb.deleteRows(pType, ids: [1, 2, 3]) - let dataArray2 : [InternalProperties] = mpdb.readRows(pType, numRows: numRows) - // even though we requested numRows, there should only be halfRows left - XCTAssertEqual(dataArray2.count, numRows - 3, "Should have deleted half the rows") - for (n, entity) in dataArray2.enumerated() { - XCTAssertEqual(entity["event"] as! String, eventName, "Event name should be unchanged") - // old events (0-24) should have been deleted so index should be recent events 25-49 - XCTAssertEqual(entity["properties"] as! [String : Int], ["index": n + halfRows], "Should have deleted oldest events first") - } - mpdb.close() - } + removeDBfile(testMixpanel) + } + + func testMPDB() { + let testToken = randomId() + let numRows = 50 + let halfRows = numRows / 2 + let eventName = "Test Event" + func _inner() { + removeDBfile(apiToken: testToken) + let mpdb = MPDB.init(token: testToken) + mpdb.open() + for pType in PersistenceType.allCases { + let emptyArray: [InternalProperties] = mpdb.readRows(pType, numRows: numRows) + XCTAssertTrue(emptyArray.isEmpty, "Table should be empty") + for i in 0...numRows - 1 { + let eventObj: InternalProperties = ["event": eventName, "properties": ["index": i]] + let eventData = JSONHandler.serializeJSONObject(eventObj)! + mpdb.insertRow(pType, data: eventData) } - removeDBfile(apiToken: testToken) + let dataArray: [InternalProperties] = mpdb.readRows(pType, numRows: halfRows) + XCTAssertEqual(dataArray.count, halfRows, "Should have read only half of the rows") + var ids: [Int32] = [] + for (n, entity) in dataArray.enumerated() { + guard let id = entity["id"] as? Int32 else { + continue + } + ids.append(id) + XCTAssertEqual(entity["event"] as! String, eventName, "Event name should be unchanged") + // index should be oldest events, 0 - 24 + XCTAssertEqual( + entity["properties"] as! [String: Int], ["index": n], "Should read oldest events first") + } + + mpdb.deleteRows(pType, ids: [1, 2, 3]) + let dataArray2: [InternalProperties] = mpdb.readRows(pType, numRows: numRows) + // even though we requested numRows, there should only be halfRows left + XCTAssertEqual(dataArray2.count, numRows - 3, "Should have deleted half the rows") + for (n, entity) in dataArray2.enumerated() { + XCTAssertEqual(entity["event"] as! String, eventName, "Event name should be unchanged") + // old events (0-24) should have been deleted so index should be recent events 25-49 + XCTAssertEqual( + entity["properties"] as! [String: Int], ["index": n + halfRows], + "Should have deleted oldest events first") + } + mpdb.close() + } } - + removeDBfile(apiToken: testToken) + } + } - - diff --git a/MixpanelDemo/MixpanelDemoMacTests/MixpanelGroupTests.swift b/MixpanelDemo/MixpanelDemoMacTests/MixpanelGroupTests.swift index 33cbf479..593a1d3b 100644 --- a/MixpanelDemo/MixpanelDemoMacTests/MixpanelGroupTests.swift +++ b/MixpanelDemo/MixpanelDemoMacTests/MixpanelGroupTests.swift @@ -13,130 +13,131 @@ import XCTest class MixpanelGroupTests: MixpanelBaseTests { - func testGroupSet() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - let p: Properties = ["p1": "a"] - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) - waitForTrackingQueue(testMixpanel) - let msg = groupQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(msg["$group_key"] as! String, groupKey) - XCTAssertEqual(msg["$group_id"] as! String, groupID) - let q = msg["$set"] as! InternalProperties - XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") - removeDBfile(testMixpanel) - } + func testGroupSet() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + let p: Properties = ["p1": "a"] + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(msg["$group_key"] as! String, groupKey) + XCTAssertEqual(msg["$group_id"] as! String, groupID) + let q = msg["$set"] as! InternalProperties + XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") + removeDBfile(testMixpanel) + } - func testGroupSetIntegerID() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let groupKey = "test_key" - let groupID = 3 - let p: Properties = ["p1": "a"] - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) - waitForTrackingQueue(testMixpanel) - let msg = groupQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(msg["$group_key"] as! String, groupKey) - XCTAssertEqual(msg["$group_id"] as! Int, groupID) - let q = msg["$set"] as! InternalProperties - XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") - removeDBfile(testMixpanel) - } + func testGroupSetIntegerID() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let groupKey = "test_key" + let groupID = 3 + let p: Properties = ["p1": "a"] + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(msg["$group_key"] as! String, groupKey) + XCTAssertEqual(msg["$group_id"] as! Int, groupID) + let q = msg["$set"] as! InternalProperties + XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") + removeDBfile(testMixpanel) + } - func testGroupSetOnce() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - let p: Properties = ["p1": "a"] - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).setOnce(properties: p) - waitForTrackingQueue(testMixpanel) - let msg = groupQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(msg["$group_key"] as! String, groupKey) - XCTAssertEqual(msg["$group_id"] as! String, groupID) - let q = msg["$set_once"] as! InternalProperties - XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") - removeDBfile(testMixpanel) - } + func testGroupSetOnce() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + let p: Properties = ["p1": "a"] + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).setOnce(properties: p) + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(msg["$group_key"] as! String, groupKey) + XCTAssertEqual(msg["$group_id"] as! String, groupID) + let q = msg["$set_once"] as! InternalProperties + XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") + removeDBfile(testMixpanel) + } - func testGroupSetTo() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "p1", to: "a") - waitForTrackingQueue(testMixpanel) - let msg = groupQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(msg["$group_key"] as! String, groupKey) - XCTAssertEqual(msg["$group_id"] as! String, groupID) - let p = msg["$set"] as! InternalProperties - XCTAssertEqual(p["p1"] as? String, "a", "custom group property not queued") - removeDBfile(testMixpanel) - } + func testGroupSetTo() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(msg["$group_key"] as! String, groupKey) + XCTAssertEqual(msg["$group_id"] as! String, groupID) + let p = msg["$set"] as! InternalProperties + XCTAssertEqual(p["p1"] as? String, "a", "custom group property not queued") + removeDBfile(testMixpanel) + } - func testGroupUnset() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).unset(property: "p1") - waitForTrackingQueue(testMixpanel) - let msg = groupQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(msg["$group_key"] as! String, groupKey) - XCTAssertEqual(msg["$group_id"] as! String, groupID) - XCTAssertEqual(msg["$unset"] as! [String], ["p1"], "group property unset not queued") - removeDBfile(testMixpanel) - } + func testGroupUnset() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).unset(property: "p1") + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(msg["$group_key"] as! String, groupKey) + XCTAssertEqual(msg["$group_id"] as! String, groupID) + XCTAssertEqual(msg["$unset"] as! [String], ["p1"], "group property unset not queued") + removeDBfile(testMixpanel) + } - func testGroupRemove() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).remove(key: "p1", value: "a") - waitForTrackingQueue(testMixpanel) - let msg = groupQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(msg["$group_key"] as! String, groupKey) - XCTAssertEqual(msg["$group_id"] as! String, groupID) - XCTAssertEqual(msg["$remove"] as? [String: String], ["p1": "a"], "group property remove not queued") - removeDBfile(testMixpanel) - } + func testGroupRemove() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).remove(key: "p1", value: "a") + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(msg["$group_key"] as! String, groupKey) + XCTAssertEqual(msg["$group_id"] as! String, groupID) + XCTAssertEqual( + msg["$remove"] as? [String: String], ["p1": "a"], "group property remove not queued") + removeDBfile(testMixpanel) + } - func testGroupUnion() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).union(key: "p1", values: ["a"]) - waitForTrackingQueue(testMixpanel) - let msg = groupQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(msg["$group_key"] as! String, groupKey) - XCTAssertEqual(msg["$group_id"] as! String, groupID) - XCTAssertEqual(msg["$union"] as? [String: [String]], ["p1": ["a"]], "group property union not queued") - removeDBfile(testMixpanel) - } + func testGroupUnion() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).union(key: "p1", values: ["a"]) + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(msg["$group_key"] as! String, groupKey) + XCTAssertEqual(msg["$group_id"] as! String, groupID) + XCTAssertEqual( + msg["$union"] as? [String: [String]], ["p1": ["a"]], "group property union not queued") + removeDBfile(testMixpanel) + } - - func testGroupAssertPropertyTypes() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - let p: Properties = ["URL": [Data()]] - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) - } - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "p1", to: [Data()]) - } - removeDBfile(testMixpanel) + func testGroupAssertPropertyTypes() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + let p: Properties = ["URL": [Data()]] + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) } - - func testDeleteGroup() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).deleteGroup() - waitForTrackingQueue(testMixpanel) - let msg = groupQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(msg["$group_key"] as! String, groupKey) - XCTAssertEqual(msg["$group_id"] as! String, groupID) - let p: InternalProperties = msg["$delete"] as! InternalProperties - XCTAssertTrue(p.isEmpty, "incorrect group properties: \(p)") - removeDBfile(testMixpanel) + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "p1", to: [Data()]) } + removeDBfile(testMixpanel) + } + + func testDeleteGroup() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).deleteGroup() + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(msg["$group_key"] as! String, groupKey) + XCTAssertEqual(msg["$group_id"] as! String, groupID) + let p: InternalProperties = msg["$delete"] as! InternalProperties + XCTAssertTrue(p.isEmpty, "incorrect group properties: \(p)") + removeDBfile(testMixpanel) + } } diff --git a/MixpanelDemo/MixpanelDemoMacTests/MixpanelOptOutTests.swift b/MixpanelDemo/MixpanelDemoMacTests/MixpanelOptOutTests.swift index 09801cef..52ea54be 100644 --- a/MixpanelDemo/MixpanelDemoMacTests/MixpanelOptOutTests.swift +++ b/MixpanelDemo/MixpanelDemoMacTests/MixpanelOptOutTests.swift @@ -7,264 +7,310 @@ // import XCTest + @testable import Mixpanel class MixpanelOptOutTests: MixpanelBaseTests { - - func testHasOptOutTrackingFlagBeingSetProperlyAfterInitializedWithOptedOutYES() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(testMixpanel.hasOptedOutTracking(), "When initialize with opted out flag set to YES, the current user should have opted out tracking") - testMixpanel.reset() - removeDBfile(testMixpanel) + + func testHasOptOutTrackingFlagBeingSetProperlyAfterInitializedWithOptedOutYES() { + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + testMixpanel.hasOptedOutTracking(), + "When initialize with opted out flag set to YES, the current user should have opted out tracking" + ) + testMixpanel.reset() + removeDBfile(testMixpanel) + } + + func testOptInWillAddOptInEvent() { + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optInTracking() + XCTAssertFalse( + testMixpanel.hasOptedOutTracking(), "The current user should have opted in tracking") + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 1, + "When opted in, event queue should have one even(opt in) being queued") + + if eventQueue(token: testMixpanel.apiToken).count > 0 { + let event = eventQueue(token: testMixpanel.apiToken).first + XCTAssertEqual( + (event!["event"] as? String), "$opt_in", + "When opted in, a track '$opt_in' should have been queued") + } else { + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 1, + "When opted in, event queue should have one even(opt in) being queued") + } + removeDBfile(testMixpanel) + } + + func testOptInTrackingForDistinctId() { + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optInTracking(distinctId: "testDistinctId") + waitForTrackingQueue(testMixpanel) + XCTAssertFalse( + testMixpanel.hasOptedOutTracking(), "The current user should have opted in tracking") + waitForTrackingQueue(testMixpanel) + let event = eventQueue(token: testMixpanel.apiToken).first + XCTAssertEqual( + (event!["event"] as? String), "$opt_in", + "When opted in, a track '$opt_in' should have been queued") + XCTAssertEqual( + testMixpanel.distinctId, "testDistinctId", "mixpanel identify failed to set distinct id") + XCTAssertEqual( + testMixpanel.people.distinctId, "testDistinctId", + "mixpanel identify failed to set people distinct id") + XCTAssertTrue( + unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 0, + "identify: should move records from unidentified queue") + removeDBfile(testMixpanel) + } + + func testOptInTrackingForDistinctIdAndWithEventProperties() { + let now = Date() + let testProperties: Properties = [ + "string": "yello", + "number": 3, + "date": now, + "$app_version": "override", + ] + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optInTracking(distinctId: "testDistinctId", properties: testProperties) + waitForTrackingQueue(testMixpanel) + waitForTrackingQueue(testMixpanel) + let eventQueueValue = eventQueue(token: testMixpanel.apiToken) + + let props = eventQueueValue.first!["properties"] as? InternalProperties + XCTAssertEqual(props!["string"] as? String, "yello") + XCTAssertEqual(props!["number"] as? NSNumber, 3) + compareDate(dateString: props!["date"] as! String, dateDate: now) + XCTAssertEqual( + props!["$app_version"] as? String, "override", "reserved property override failed") + + if eventQueueValue.count > 0 { + let event = eventQueueValue.first + XCTAssertEqual( + (event!["event"] as? String), "$opt_in", + "When opted in, a track '$opt_in' should have been queued") + } else { + XCTAssertTrue( + eventQueueValue.count == 1, + "When opted in, event queue should have one even(opt in) being queued") } - func testOptInWillAddOptInEvent() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - testMixpanel.optInTracking() - XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "The current user should have opted in tracking") - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "When opted in, event queue should have one even(opt in) being queued") + XCTAssertEqual( + testMixpanel.distinctId, "testDistinctId", "mixpanel identify failed to set distinct id") + XCTAssertEqual( + testMixpanel.people.distinctId, "testDistinctId", + "mixpanel identify failed to set people distinct id") + XCTAssertTrue( + unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 0, + "identify: should move records from unidentified queue") + removeDBfile(testMixpanel) + } - if eventQueue(token: testMixpanel.apiToken).count > 0 { - let event = eventQueue(token: testMixpanel.apiToken).first - XCTAssertEqual((event!["event"] as? String), "$opt_in", "When opted in, a track '$opt_in' should have been queued") - } - else { - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "When opted in, event queue should have one even(opt in) being queued") - } - removeDBfile(testMixpanel) + func testHasOptOutTrackingFlagBeingSetProperlyForMultipleInstances() { + let mixpanel1 = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + waitForTrackingQueue(mixpanel1) + XCTAssertTrue( + mixpanel1.hasOptedOutTracking(), + "When initialize with opted out flag set to YES, the current user should have opted out tracking" + ) + removeDBfile(mixpanel1) + + let mixpanel2 = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: false) + XCTAssertFalse( + mixpanel2.hasOptedOutTracking(), + "When initialize with opted out flag set to NO, the current user should have opted in tracking" + ) + removeDBfile(mixpanel2) + } + + func testHasOptOutTrackingFlagBeingSetProperlyAfterInitializedWithOptedOutNO() { + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: false) + XCTAssertFalse( + testMixpanel.hasOptedOutTracking(), + "When initialize with opted out flag set to NO, the current user should have opted out tracking" + ) + removeDBfile(testMixpanel) + } + + func testHasOptOutTrackingFlagBeingSetProperlyByDefault() { + let testMixpanel = Mixpanel.initialize(token: randomId()) + XCTAssertFalse( + testMixpanel.hasOptedOutTracking(), + "By default, the current user should not opted out tracking") + removeDBfile(testMixpanel) + } + + func testHasOptOutTrackingFlagBeingSetProperlyForOptOut() { + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + waitForTrackingQueue(testMixpanel) + testMixpanel.optOutTracking() + XCTAssertTrue( + testMixpanel.hasOptedOutTracking(), + "When optOutTracking is called, the current user should have opted out tracking") + removeDBfile(testMixpanel) + } + + func testHasOptOutTrackingFlagBeingSetProperlyForOptIn() { + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + waitForTrackingQueue(testMixpanel) + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + testMixpanel.hasOptedOutTracking(), + "When optOutTracking is called, the current user should have opted out tracking") + testMixpanel.optInTracking() + waitForTrackingQueue(testMixpanel) + XCTAssertFalse( + testMixpanel.hasOptedOutTracking(), + "When optOutTracking is called, the current user should have opted in tracking") + removeDBfile(testMixpanel) + } + + func testOptOutTrackingWillNotGenerateEventQueue() { + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + waitForTrackingQueue(testMixpanel) + testMixpanel.optOutTracking() + for i in 0..<50 { + testMixpanel.track(event: "event \(i)") } + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 0, + "When opted out, events should not be queued") + removeDBfile(testMixpanel) + } - func testOptInTrackingForDistinctId() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - testMixpanel.optInTracking(distinctId: "testDistinctId") - waitForTrackingQueue(testMixpanel) - XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "The current user should have opted in tracking") - waitForTrackingQueue(testMixpanel) - let event = eventQueue(token: testMixpanel.apiToken).first - XCTAssertEqual((event!["event"] as? String), "$opt_in", "When opted in, a track '$opt_in' should have been queued") - XCTAssertEqual(testMixpanel.distinctId, "testDistinctId", "mixpanel identify failed to set distinct id") - XCTAssertEqual(testMixpanel.people.distinctId, "testDistinctId", "mixpanel identify failed to set people distinct id") - XCTAssertTrue(unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 0, "identify: should move records from unidentified queue") - removeDBfile(testMixpanel) + func testOptOutTrackingWillNotGeneratePeopleQueue() { + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + for i in 0..<50 { + testMixpanel.people.set(property: "p1", to: "\(i)") } + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + peopleQueue(token: testMixpanel.apiToken).count == 0, + "When opted out, events should not be queued") + removeDBfile(testMixpanel) + } - func testOptInTrackingForDistinctIdAndWithEventProperties() - { - let now = Date() - let testProperties: Properties = ["string": "yello", - "number": 3, - "date": now, - "$app_version": "override"] - let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - testMixpanel.optInTracking(distinctId: "testDistinctId", properties: testProperties) - waitForTrackingQueue(testMixpanel) - waitForTrackingQueue(testMixpanel) - let eventQueueValue = eventQueue(token: testMixpanel.apiToken) - - let props = eventQueueValue.first!["properties"] as? InternalProperties - XCTAssertEqual(props!["string"] as? String, "yello") - XCTAssertEqual(props!["number"] as? NSNumber, 3) - compareDate(dateString: props!["date"] as! String, dateDate: now) - XCTAssertEqual(props!["$app_version"] as? String, "override", "reserved property override failed") + func testOptOutTrackingWillSkipAlias() { + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + testMixpanel.createAlias("testAlias", distinctId: "aDistinctId") + XCTAssertNotEqual(testMixpanel.alias, "testAlias", "When opted out, alias should not be set") + removeDBfile(testMixpanel) + } - if eventQueueValue.count > 0 { - let event = eventQueueValue.first - XCTAssertEqual((event!["event"] as? String), "$opt_in", "When opted in, a track '$opt_in' should have been queued") - } - else { - XCTAssertTrue(eventQueueValue.count == 1, "When opted in, event queue should have one even(opt in) being queued") - } + func testEventBeingTrackedBeforeOptOutShouldNotBeCleared() { + let testMixpanel = Mixpanel.initialize(token: randomId()) + testMixpanel.track(event: "a normal event") + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "events should be queued") + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 1, + "When opted out, any events tracked before opted out should not be cleared") + removeDBfile(testMixpanel) + } - XCTAssertEqual(testMixpanel.distinctId, "testDistinctId", "mixpanel identify failed to set distinct id") - XCTAssertEqual(testMixpanel.people.distinctId, "testDistinctId", "mixpanel identify failed to set people distinct id") - XCTAssertTrue(unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 0, "identify: should move records from unidentified queue") - removeDBfile(testMixpanel) + func testOptOutTrackingRegisterSuperProperties() { + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + waitForTrackingQueue(testMixpanel) + let properties: Properties = ["p1": "a", "p2": 3, "p3": Date()] + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) + waitForTrackingQueue(testMixpanel) + testMixpanel.registerSuperProperties(properties) + waitForTrackingQueue(testMixpanel) + XCTAssertNotEqual( + NSDictionary(dictionary: testMixpanel.currentSuperProperties()), + NSDictionary(dictionary: properties), + "When opted out, register super properties should not be successful") + removeDBfile(testMixpanel) + } + + func testOptOutTrackingRegisterSuperPropertiesOnce() { + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + waitForTrackingQueue(testMixpanel) + let properties: Properties = ["p1": "a", "p2": 3, "p3": Date()] + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) + testMixpanel.registerSuperPropertiesOnce(properties) + waitForTrackingQueue(testMixpanel) + XCTAssertNotEqual( + NSDictionary(dictionary: testMixpanel.currentSuperProperties()), + NSDictionary(dictionary: properties), + "When opted out, register super properties once should not be successful") + removeDBfile(testMixpanel) + } + + func testOptOutWilSkipTimeEvent() { + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) + testMixpanel.time(event: "400 Meters") + testMixpanel.track(event: "400 Meters") + 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) + waitForTrackingQueue(testMixpanel) + testMixpanel.optInTracking() + waitForTrackingQueue(testMixpanel) + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + for i in 0..<1 { + testMixpanel.people.set(property: "p1", to: "\(i)") } + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + peopleQueue(token: testMixpanel.apiToken).count == 1, + "When opted in, people queue should have been queued") - func testHasOptOutTrackingFlagBeingSetProperlyForMultipleInstances() - { - let mixpanel1 = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - waitForTrackingQueue(mixpanel1) - XCTAssertTrue(mixpanel1.hasOptedOutTracking(), "When initialize with opted out flag set to YES, the current user should have opted out tracking") - removeDBfile(mixpanel1) + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) - let mixpanel2 = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: false) - XCTAssertFalse(mixpanel2.hasOptedOutTracking(), "When initialize with opted out flag set to NO, the current user should have opted in tracking") - removeDBfile(mixpanel2) + testMixpanel.flush() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + peopleQueue(token: testMixpanel.apiToken).count == 1, + "When opted out, people queue should not be flushed") + removeDBfile(testMixpanel) + } + + func testOptOutWillSkipFlushEvent() { + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optInTracking() + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + for i in 0..<1 { + testMixpanel.track(event: "event \(i)") } + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 3, + "When opted in, events should have been queued") - func testHasOptOutTrackingFlagBeingSetProperlyAfterInitializedWithOptedOutNO() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: false) - XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "When initialize with opted out flag set to NO, the current user should have opted out tracking") - removeDBfile(testMixpanel) - } + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) - func testHasOptOutTrackingFlagBeingSetProperlyByDefault() - { - let testMixpanel = Mixpanel.initialize(token: randomId()) - XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "By default, the current user should not opted out tracking") - removeDBfile(testMixpanel) - } + testMixpanel.flush() + waitForTrackingQueue(testMixpanel) - func testHasOptOutTrackingFlagBeingSetProperlyForOptOut() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - waitForTrackingQueue(testMixpanel) - testMixpanel.optOutTracking() - XCTAssertTrue(testMixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted out tracking") - removeDBfile(testMixpanel) - } - - func testHasOptOutTrackingFlagBeingSetProperlyForOptIn() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - waitForTrackingQueue(testMixpanel) - testMixpanel.optOutTracking() - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(testMixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted out tracking") - testMixpanel.optInTracking() - waitForTrackingQueue(testMixpanel) - XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted in tracking") - removeDBfile(testMixpanel) - } - - func testOptOutTrackingWillNotGenerateEventQueue() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - waitForTrackingQueue(testMixpanel) - testMixpanel.optOutTracking() - for i in 0..<50 { - testMixpanel.track(event: "event \(i)") - } - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 0, "When opted out, events should not be queued") - removeDBfile(testMixpanel) - } - - func testOptOutTrackingWillNotGeneratePeopleQueue() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - testMixpanel.optOutTracking() - for i in 0..<50 { - testMixpanel.people.set(property: "p1", to: "\(i)") - } - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).count == 0, "When opted out, events should not be queued") - removeDBfile(testMixpanel) - } - - func testOptOutTrackingWillSkipAlias() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - testMixpanel.optOutTracking() - testMixpanel.createAlias("testAlias", distinctId: "aDistinctId") - XCTAssertNotEqual(testMixpanel.alias, "testAlias", "When opted out, alias should not be set") - removeDBfile(testMixpanel) - } - - func testEventBeingTrackedBeforeOptOutShouldNotBeCleared() - { - let testMixpanel = Mixpanel.initialize(token: randomId()) - testMixpanel.track(event: "a normal event") - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "events should be queued") - testMixpanel.optOutTracking() - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "When opted out, any events tracked before opted out should not be cleared") - removeDBfile(testMixpanel) - } - - func testOptOutTrackingRegisterSuperProperties() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - waitForTrackingQueue(testMixpanel) - let properties: Properties = ["p1": "a", "p2": 3, "p3": Date()] - testMixpanel.optOutTracking() - waitForTrackingQueue(testMixpanel) - waitForTrackingQueue(testMixpanel) - testMixpanel.registerSuperProperties(properties) - waitForTrackingQueue(testMixpanel) - XCTAssertNotEqual(NSDictionary(dictionary: testMixpanel.currentSuperProperties()), - NSDictionary(dictionary: properties), - "When opted out, register super properties should not be successful") - removeDBfile(testMixpanel) - } - - func testOptOutTrackingRegisterSuperPropertiesOnce() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - waitForTrackingQueue(testMixpanel) - let properties: Properties = ["p1": "a", "p2": 3, "p3": Date()] - testMixpanel.optOutTracking() - waitForTrackingQueue(testMixpanel) - testMixpanel.registerSuperPropertiesOnce(properties) - waitForTrackingQueue(testMixpanel) - XCTAssertNotEqual(NSDictionary(dictionary: testMixpanel.currentSuperProperties()), - NSDictionary(dictionary: properties), - "When opted out, register super properties once should not be successful") - removeDBfile(testMixpanel) - } - - func testOptOutWilSkipTimeEvent() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - testMixpanel.optOutTracking() - waitForTrackingQueue(testMixpanel) - testMixpanel.time(event: "400 Meters") - testMixpanel.track(event: "400 Meters") - 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) - waitForTrackingQueue(testMixpanel) - testMixpanel.optInTracking() - waitForTrackingQueue(testMixpanel) - testMixpanel.identify(distinctId: "d1") - waitForTrackingQueue(testMixpanel) - for i in 0..<1 { - testMixpanel.people.set(property: "p1", to: "\(i)") - } - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).count == 1, "When opted in, people queue should have been queued") - - testMixpanel.optOutTracking() - waitForTrackingQueue(testMixpanel) - - testMixpanel.flush() - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).count == 1, "When opted out, people queue should not be flushed") - removeDBfile(testMixpanel) - } - - func testOptOutWillSkipFlushEvent() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - testMixpanel.optInTracking() - testMixpanel.identify(distinctId: "d1") - waitForTrackingQueue(testMixpanel) - for i in 0..<1 { - testMixpanel.track(event: "event \(i)") - } - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 3, "When opted in, events should have been queued") - - testMixpanel.optOutTracking() - waitForTrackingQueue(testMixpanel) - - testMixpanel.flush() - waitForTrackingQueue(testMixpanel) - - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 3, "When opted out, events should not be flushed") - removeDBfile(testMixpanel) - } + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 3, + "When opted out, events should not be flushed") + removeDBfile(testMixpanel) + } } diff --git a/MixpanelDemo/MixpanelDemoMacTests/MixpanelPeopleTests.swift b/MixpanelDemo/MixpanelDemoMacTests/MixpanelPeopleTests.swift index f0874672..9b308a48 100644 --- a/MixpanelDemo/MixpanelDemoMacTests/MixpanelPeopleTests.swift +++ b/MixpanelDemo/MixpanelDemoMacTests/MixpanelPeopleTests.swift @@ -13,197 +13,215 @@ import XCTest class MixpanelPeopleTests: MixpanelBaseTests { - func testPeopleSet() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - waitForTrackingQueue(testMixpanel) - let p: Properties = ["p1": "a"] - testMixpanel.people.set(properties: p) - waitForTrackingQueue(testMixpanel) - let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties - XCTAssertEqual(q["p1"] as? String, "a", "custom people property not queued") - assertDefaultPeopleProperties(q) - removeDBfile(testMixpanel) - } + func testPeopleSet() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + let p: Properties = ["p1": "a"] + testMixpanel.people.set(properties: p) + waitForTrackingQueue(testMixpanel) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties + XCTAssertEqual(q["p1"] as? String, "a", "custom people property not queued") + assertDefaultPeopleProperties(q) + removeDBfile(testMixpanel) + } - func testPeopleSetOnce() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - let p: Properties = ["p1": "a"] - testMixpanel.people.setOnce(properties: p) - waitForTrackingQueue(testMixpanel) - let q = peopleQueue(token: testMixpanel.apiToken).last!["$set_once"] as! InternalProperties - XCTAssertEqual(q["p1"] as? String, "a", "custom people property not queued") - assertDefaultPeopleProperties(q) - removeDBfile(testMixpanel) - } + func testPeopleSetOnce() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + let p: Properties = ["p1": "a"] + testMixpanel.people.setOnce(properties: p) + waitForTrackingQueue(testMixpanel) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set_once"] as! InternalProperties + XCTAssertEqual(q["p1"] as? String, "a", "custom people property not queued") + assertDefaultPeopleProperties(q) + removeDBfile(testMixpanel) + } - func testPeopleSetReservedProperty() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - let p: Properties = ["$ios_app_version": "override"] - testMixpanel.people.set(properties: p) - waitForTrackingQueue(testMixpanel) - let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties - XCTAssertEqual(q["$ios_app_version"] as? String, - "override", - "reserved property override failed") - assertDefaultPeopleProperties(q) - removeDBfile(testMixpanel) - } + func testPeopleSetReservedProperty() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + let p: Properties = ["$ios_app_version": "override"] + testMixpanel.people.set(properties: p) + waitForTrackingQueue(testMixpanel) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties + XCTAssertEqual( + q["$ios_app_version"] as? String, + "override", + "reserved property override failed") + assertDefaultPeopleProperties(q) + removeDBfile(testMixpanel) + } - func testPeopleSetTo() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - testMixpanel.people.set(property: "p1", to: "a") - waitForTrackingQueue(testMixpanel) - let p: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties - XCTAssertEqual(p["p1"] as? String, "a", "custom people property not queued") - assertDefaultPeopleProperties(p) - removeDBfile(testMixpanel) - } + func testPeopleSetTo() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + let p: InternalProperties = + peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties + XCTAssertEqual(p["p1"] as? String, "a", "custom people property not queued") + assertDefaultPeopleProperties(p) + removeDBfile(testMixpanel) + } - func testDropUnidentifiedPeopleRecords() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - for i in 0..<505 { - testMixpanel.people.set(property: "i", to: i) - } - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 505) - var r: InternalProperties = unIdentifiedPeopleQueue(token: testMixpanel.apiToken).first! - XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 0) - r = unIdentifiedPeopleQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 504) - removeDBfile(testMixpanel) + func testDropUnidentifiedPeopleRecords() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + for i in 0..<505 { + testMixpanel.people.set(property: "i", to: i) } + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 505) + var r: InternalProperties = unIdentifiedPeopleQueue(token: testMixpanel.apiToken).first! + XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 0) + r = unIdentifiedPeopleQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 504) + removeDBfile(testMixpanel) + } - - func testPeopleAssertPropertyTypes() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - var p: Properties = ["URL": [Data()]] - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.people.set(properties: p) - } - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.people.set(property: "p1", to: [Data()]) - } - p = ["p1": "a"] - // increment should require a number - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.people.increment(properties: p) - } - removeDBfile(testMixpanel) + func testPeopleAssertPropertyTypes() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + var p: Properties = ["URL": [Data()]] + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.people.set(properties: p) } - - func testPeopleIncrement() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - let p: Properties = ["p1": 3] - testMixpanel.people.increment(properties: p) - waitForTrackingQueue(testMixpanel) - let q = peopleQueue(token: testMixpanel.apiToken).last!["$add"] as! InternalProperties - XCTAssertTrue(q.count == 1, "incorrect people properties: \(p)") - XCTAssertEqual(q["p1"] as? Int, 3, "custom people property not queued") - removeDBfile(testMixpanel) + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.people.set(property: "p1", to: [Data()]) } - - func testPeopleIncrementBy() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - testMixpanel.people.increment(property: "p1", by: 3) - waitForTrackingQueue(testMixpanel) - let p: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last!["$add"] as! InternalProperties - XCTAssertTrue(p.count == 1, "incorrect people properties: \(p)") - XCTAssertEqual(p["p1"] as? Double, 3, "custom people property not queued") - removeDBfile(testMixpanel) + p = ["p1": "a"] + // increment should require a number + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.people.increment(properties: p) } + removeDBfile(testMixpanel) + } - func testPeopleDeleteUser() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - testMixpanel.people.deleteUser() - waitForTrackingQueue(testMixpanel) - let p: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last!["$delete"] as! InternalProperties - XCTAssertTrue(p.isEmpty, "incorrect people properties: \(p)") - removeDBfile(testMixpanel) - } + func testPeopleIncrement() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + let p: Properties = ["p1": 3] + testMixpanel.people.increment(properties: p) + waitForTrackingQueue(testMixpanel) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$add"] as! InternalProperties + XCTAssertTrue(q.count == 1, "incorrect people properties: \(p)") + XCTAssertEqual(q["p1"] as? Int, 3, "custom people property not queued") + removeDBfile(testMixpanel) + } + func testPeopleIncrementBy() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.increment(property: "p1", by: 3) + waitForTrackingQueue(testMixpanel) + let p: InternalProperties = + peopleQueue(token: testMixpanel.apiToken).last!["$add"] as! InternalProperties + XCTAssertTrue(p.count == 1, "incorrect people properties: \(p)") + XCTAssertEqual(p["p1"] as? Double, 3, "custom people property not queued") + removeDBfile(testMixpanel) + } - func testPeopleTrackChargeDecimal() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - testMixpanel.people.trackCharge(amount: 25.34) - waitForTrackingQueue(testMixpanel) - let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! - let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double - let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] - XCTAssertEqual(prop, 25.34) - XCTAssertNotNil(prop2) - removeDBfile(testMixpanel) - } + func testPeopleDeleteUser() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.deleteUser() + waitForTrackingQueue(testMixpanel) + let p: InternalProperties = + peopleQueue(token: testMixpanel.apiToken).last!["$delete"] as! InternalProperties + XCTAssertTrue(p.isEmpty, "incorrect people properties: \(p)") + removeDBfile(testMixpanel) + } - func testPeopleTrackChargeZero() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - waitForTrackingQueue(testMixpanel) - testMixpanel.people.trackCharge(amount: 0) - waitForTrackingQueue(testMixpanel) - let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! - let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double - let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] - XCTAssertEqual(prop, 0) - XCTAssertNotNil(prop2) - removeDBfile(testMixpanel) - } - - func testPeopleTrackChargeWithTime() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - let p: Properties = allPropertyTypes() - testMixpanel.people.trackCharge(amount: 25, properties: ["$time": p["date"]!]) - waitForTrackingQueue(testMixpanel) - let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! - let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double - let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] as? String - XCTAssertEqual(prop, 25) - compareDate(dateString: prop2!, dateDate: p["date"] as! Date) - removeDBfile(testMixpanel) - } + func testPeopleTrackChargeDecimal() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.trackCharge(amount: 25.34) + waitForTrackingQueue(testMixpanel) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! + let prop = + ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] + as? Double + let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?[ + "$time"] + XCTAssertEqual(prop, 25.34) + XCTAssertNotNil(prop2) + removeDBfile(testMixpanel) + } - func testPeopleTrackChargeWithProperties() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - testMixpanel.people.trackCharge(amount: 25, properties: ["p1": "a"]) - waitForTrackingQueue(testMixpanel) - let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! - let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double - let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["p1"] - XCTAssertEqual(prop, 25) - XCTAssertEqual(prop2 as? String, "a") - removeDBfile(testMixpanel) - } + func testPeopleTrackChargeZero() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + testMixpanel.people.trackCharge(amount: 0) + waitForTrackingQueue(testMixpanel) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! + let prop = + ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] + as? Double + let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?[ + "$time"] + XCTAssertEqual(prop, 0) + XCTAssertNotNil(prop2) + removeDBfile(testMixpanel) + } - func testPeopleTrackCharge() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - testMixpanel.people.trackCharge(amount: 25) - waitForTrackingQueue(testMixpanel) - let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! - let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double - let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] - XCTAssertEqual(prop, 25) - XCTAssertNotNil(prop2) - removeDBfile(testMixpanel) - } + func testPeopleTrackChargeWithTime() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + let p: Properties = allPropertyTypes() + testMixpanel.people.trackCharge(amount: 25, properties: ["$time": p["date"]!]) + waitForTrackingQueue(testMixpanel) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! + let prop = + ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] + as? Double + let prop2 = + ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] + as? String + XCTAssertEqual(prop, 25) + compareDate(dateString: prop2!, dateDate: p["date"] as! Date) + removeDBfile(testMixpanel) + } - func testPeopleClearCharges() { - let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - testMixpanel.people.clearCharges() - waitForTrackingQueue(testMixpanel) - let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! - let transactions = (r["$set"] as? InternalProperties)?["$transactions"] as? [MixpanelType] - XCTAssertEqual(transactions?.count, 0) - removeDBfile(testMixpanel) - } + func testPeopleTrackChargeWithProperties() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.trackCharge(amount: 25, properties: ["p1": "a"]) + waitForTrackingQueue(testMixpanel) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! + let prop = + ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] + as? Double + let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?[ + "p1"] + XCTAssertEqual(prop, 25) + XCTAssertEqual(prop2 as? String, "a") + removeDBfile(testMixpanel) + } + + func testPeopleTrackCharge() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.trackCharge(amount: 25) + waitForTrackingQueue(testMixpanel) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! + let prop = + ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] + as? Double + let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?[ + "$time"] + XCTAssertEqual(prop, 25) + XCTAssertNotNil(prop2) + removeDBfile(testMixpanel) + } + + func testPeopleClearCharges() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.clearCharges() + waitForTrackingQueue(testMixpanel) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! + let transactions = (r["$set"] as? InternalProperties)?["$transactions"] as? [MixpanelType] + XCTAssertEqual(transactions?.count, 0) + removeDBfile(testMixpanel) + } } diff --git a/MixpanelDemo/MixpanelDemoMacTests/TestConstants.swift b/MixpanelDemo/MixpanelDemoMacTests/TestConstants.swift index 0a7f938f..5342f2df 100644 --- a/MixpanelDemo/MixpanelDemoMacTests/TestConstants.swift +++ b/MixpanelDemo/MixpanelDemoMacTests/TestConstants.swift @@ -14,20 +14,22 @@ let kFakeServerUrl = "https://34a272abf23d.com" extension XCTestCase { - func XCTExpectAssert(_ expectedMessage: String, file: StaticString = #file, line: UInt = #line, block: () -> ()) { - let exp = expectation(description: expectedMessage) + func XCTExpectAssert( + _ expectedMessage: String, file: StaticString = #file, line: UInt = #line, block: () -> Void + ) { + let exp = expectation(description: expectedMessage) - Assertions.assertClosure = { - (condition, message, file, line) in - if !condition { - exp.fulfill() - } - } - - // Call code. - block() - waitForExpectations(timeout: 0.5, handler: nil) - Assertions.assertClosure = Assertions.swiftAssertClosure + Assertions.assertClosure = { + (condition, message, file, line) in + if !condition { + exp.fulfill() + } } + // Call code. + block() + waitForExpectations(timeout: 0.5, handler: nil) + Assertions.assertClosure = Assertions.swiftAssertClosure + } + } diff --git a/MixpanelDemo/MixpanelDemoMacUITests/MixpanelDemoMacUITests.swift b/MixpanelDemo/MixpanelDemoMacUITests/MixpanelDemoMacUITests.swift index 40c07ab8..28f35b33 100644 --- a/MixpanelDemo/MixpanelDemoMacUITests/MixpanelDemoMacUITests.swift +++ b/MixpanelDemo/MixpanelDemoMacUITests/MixpanelDemoMacUITests.swift @@ -10,34 +10,34 @@ import XCTest class MixpanelDemoMacUITests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use recording to get started writing UI tests. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } } + } } diff --git a/MixpanelDemo/MixpanelDemoTV/AppDelegate.swift b/MixpanelDemo/MixpanelDemoTV/AppDelegate.swift index e93d2cbc..69c2e693 100644 --- a/MixpanelDemo/MixpanelDemoTV/AppDelegate.swift +++ b/MixpanelDemo/MixpanelDemoTV/AppDelegate.swift @@ -6,55 +6,55 @@ // Copyright © 2019 Mixpanel. All rights reserved. // -import UIKit import Mixpanel +import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? + var window: UIWindow? + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + // Override point for customization after application launch. + print("didFinishLaunchingWithOptions") - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - print("didFinishLaunchingWithOptions") - - var ADD_YOUR_MIXPANEL_TOKEN_BELOW_🛠🛠🛠🛠🛠🛠: String - Mixpanel.initialize(token: "MIXPANEL_TOKEN") - Mixpanel.mainInstance().loggingEnabled = true - Mixpanel.mainInstance().registerSuperProperties(["super apple tv properties": 1]); - Mixpanel.mainInstance().track(event: "apple tv track") - - return true - } + var ADD_YOUR_MIXPANEL_TOKEN_BELOW_🛠🛠🛠🛠🛠🛠: String + Mixpanel.initialize(token: "MIXPANEL_TOKEN") + Mixpanel.mainInstance().loggingEnabled = true + Mixpanel.mainInstance().registerSuperProperties(["super apple tv properties": 1]) + Mixpanel.mainInstance().track(event: "apple tv track") - func applicationWillResignActive(_ application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. - print("applicationWillResignActive") - } + return true + } - func applicationDidEnterBackground(_ application: UIApplication) { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - print("applicationDidEnterBackground") - } + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. + print("applicationWillResignActive") + } - func applicationWillEnterForeground(_ application: UIApplication) { - // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. - print("applicationWillEnterForeground") - } + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + print("applicationDidEnterBackground") + } - func applicationDidBecomeActive(_ application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - print("applicationDidBecomeActive") - } + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + print("applicationWillEnterForeground") + } - func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - print("applicationWillTerminate") - } + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + print("applicationDidBecomeActive") + } + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + print("applicationWillTerminate") + } } - diff --git a/MixpanelDemo/MixpanelDemoTV/ViewController.swift b/MixpanelDemo/MixpanelDemoTV/ViewController.swift index e5a8ef37..51770eb4 100644 --- a/MixpanelDemo/MixpanelDemoTV/ViewController.swift +++ b/MixpanelDemo/MixpanelDemoTV/ViewController.swift @@ -6,31 +6,31 @@ // Copyright © 2019 Mixpanel. All rights reserved. // -import UIKit import Mixpanel +import UIKit class ViewController: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view, typically from a nib. - } + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view, typically from a nib. + } - @IBAction func timeEventClicked(_ sender: Any) { - Mixpanel.mainInstance().time(event: "time something") - DispatchQueue.main.asyncAfter(deadline: .now() + 5) { - Mixpanel.mainInstance().track(event: "time something") - } - } - - @IBAction func TrackEventClicked(_ sender: Any) { - Mixpanel.mainInstance().track(event: "Player Create", properties: ["gender": "Male", "weapon": "Pistol"]) - } - - @IBAction func peopleClicked(_ sender: Any) { - let mixpanel = Mixpanel.mainInstance() - mixpanel.people.set(properties: ["gender": "Male", "weapon": "Pistol"]) - mixpanel.identify(distinctId: mixpanel.distinctId) + @IBAction func timeEventClicked(_ sender: Any) { + Mixpanel.mainInstance().time(event: "time something") + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + Mixpanel.mainInstance().track(event: "time something") } + } + + @IBAction func TrackEventClicked(_ sender: Any) { + Mixpanel.mainInstance().track( + event: "Player Create", properties: ["gender": "Male", "weapon": "Pistol"]) + } + + @IBAction func peopleClicked(_ sender: Any) { + let mixpanel = Mixpanel.mainInstance() + mixpanel.people.set(properties: ["gender": "Male", "weapon": "Pistol"]) + mixpanel.identify(distinctId: mixpanel.distinctId) + } } - diff --git a/MixpanelDemo/MixpanelDemoTVTests/MixpanelDemoTVTests.swift b/MixpanelDemo/MixpanelDemoTVTests/MixpanelDemoTVTests.swift index a0c5a99d..2d33daa7 100644 --- a/MixpanelDemo/MixpanelDemoTVTests/MixpanelDemoTVTests.swift +++ b/MixpanelDemo/MixpanelDemoTVTests/MixpanelDemoTVTests.swift @@ -7,28 +7,29 @@ // import XCTest + @testable import MixpanelDemoTV class MixpanelDemoTVTests: XCTestCase { - override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. - } + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. } + } } diff --git a/MixpanelDemo/MixpanelDemoTVUITests/MixpanelDemoTVUITests.swift b/MixpanelDemo/MixpanelDemoTVUITests/MixpanelDemoTVUITests.swift index 9524a551..08e14085 100644 --- a/MixpanelDemo/MixpanelDemoTVUITests/MixpanelDemoTVUITests.swift +++ b/MixpanelDemo/MixpanelDemoTVUITests/MixpanelDemoTVUITests.swift @@ -10,25 +10,25 @@ import XCTest class MixpanelDemoTVUITests: XCTestCase { - override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false - // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. - XCUIApplication().launch() + // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. + XCUIApplication().launch() - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } - func testExample() { - // Use recording to get started writing UI tests. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } + func testExample() { + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } } diff --git a/MixpanelDemo/MixpanelDemoTests/JSONHandlerTests.swift b/MixpanelDemo/MixpanelDemoTests/JSONHandlerTests.swift index 0fb62bab..c0cddaa5 100644 --- a/MixpanelDemo/MixpanelDemoTests/JSONHandlerTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/JSONHandlerTests.swift @@ -6,79 +6,84 @@ // Copyright © 2021 Mixpanel. All rights reserved. // import XCTest + @testable import Mixpanel @testable import MixpanelDemo class JSONHandlerTests: XCTestCase { - func testSerializeJSONObject() { - let nSNumberProp: NSNumber = NSNumber(value: 1) - let doubleProp: Double = 2.0 - let floatProp: Float = Float(3.5) - let stringProp: String = "string" - let intProp: Int = -4 - let uIntProp: UInt = 4 - let uInt64Prop: UInt64 = 5000000000 - let boolProp: Bool = true - let optArrayProp: Array = [nil, 1.0, 2.0] - let arrayProp: Array = [0.0, 1.0, 2.0] - let dictProp: Dictionary = ["nil": nil, "a": "a", "b": "b"] - let dateProp: Date = Date() - let urlProp: URL = URL(string: "https://www.mixpanel.com")! - let nilProp: String? = nil - let nestedDictProp: Dictionary> = ["nested": dictProp] - let nestedArraryProp: Array> = [optArrayProp] + func testSerializeJSONObject() { + let nSNumberProp: NSNumber = NSNumber(value: 1) + let doubleProp: Double = 2.0 + let floatProp: Float = Float(3.5) + let stringProp: String = "string" + let intProp: Int = -4 + let uIntProp: UInt = 4 + let uInt64Prop: UInt64 = 5_000_000_000 + let boolProp: Bool = true + let optArrayProp: [Double?] = [nil, 1.0, 2.0] + let arrayProp: [Double] = [0.0, 1.0, 2.0] + let dictProp: [String: String?] = ["nil": nil, "a": "a", "b": "b"] + let dateProp: Date = Date() + let urlProp: URL = URL(string: "https://www.mixpanel.com")! + let nilProp: String? = nil + let nestedDictProp: [String: [String: String?]] = ["nested": dictProp] + let nestedArraryProp: [[Double?]] = [optArrayProp] - let event: Dictionary = ["event": "test", - "properties": ["nSNumberProp": nSNumberProp, - "doubleProp": doubleProp, - "floatProp": floatProp, - "stringProp": stringProp, - "intProp": intProp, - "uIntProp": uIntProp, - "uInt64Prop": uInt64Prop, - "boolProp": boolProp, - "optArrayProp": optArrayProp, - "arrayProp": arrayProp, - "dictProp": dictProp, - "dateProp": dateProp, - "urlProp": urlProp, - "nilProp": nilProp as Any, - "nestedDictProp": nestedDictProp, - "nestedArraryProp": nestedArraryProp, - ]] + let event: [String: Any] = [ + "event": "test", + "properties": [ + "nSNumberProp": nSNumberProp, + "doubleProp": doubleProp, + "floatProp": floatProp, + "stringProp": stringProp, + "intProp": intProp, + "uIntProp": uIntProp, + "uInt64Prop": uInt64Prop, + "boolProp": boolProp, + "optArrayProp": optArrayProp, + "arrayProp": arrayProp, + "dictProp": dictProp, + "dateProp": dateProp, + "urlProp": urlProp, + "nilProp": nilProp as Any, + "nestedDictProp": nestedDictProp, + "nestedArraryProp": nestedArraryProp, + ], + ] - let serializedQueue = JSONHandler.serializeJSONObject([event]) - let deserializedQueue = try! JSONSerialization.jsonObject(with: serializedQueue!, options: []) as! Array> - XCTAssertEqual(deserializedQueue[0]["event"] as! String, "test") - let props = deserializedQueue[0]["properties"] as! [String : Any] - XCTAssertEqual(props["nSNumberProp"] as! NSNumber, nSNumberProp) - XCTAssertEqual(props["doubleProp"] as! Double, doubleProp) - XCTAssertEqual(props["floatProp"] as! Float, floatProp) - XCTAssertEqual(props["stringProp"] as! String, stringProp) - XCTAssertEqual(props["intProp"] as! Int, intProp) - XCTAssertEqual(props["uIntProp"] as! UInt, uIntProp) - XCTAssertEqual(props["uInt64Prop"] as! UInt64, uInt64Prop) - XCTAssertEqual(props["boolProp"] as! Bool, boolProp) - // nil should be dropped from Array properties - XCTAssertEqual(props["optArrayProp"] as! Array, [1.0, 2.0]) - XCTAssertEqual(props["arrayProp"] as! Array, arrayProp) - let deserializedDictProp = props["dictProp"] as! [String : Any] - // nil should be convereted to NSNull() inside Dictionary properties - XCTAssertEqual(deserializedDictProp["nil"] as! NSNull, NSNull()) - XCTAssertEqual(deserializedDictProp["a"] as! String, "a") - XCTAssertEqual(deserializedDictProp["b"] as! String, "b") - XCTAssertEqual(props["urlProp"] as! String, urlProp.absoluteString) - // nil properties themselves should also be converted to NSNull() - XCTAssertEqual(props["nilProp"] as! NSNull, NSNull()) - let deserializedNestedDictProp = props["nestedDictProp"] as! [String : [String : Any]] - let nestedDict = deserializedNestedDictProp["nested"]! - // the same nil logic from above should be applied to nested Collections as well - XCTAssertEqual(nestedDict["nil"] as! NSNull, NSNull()) - XCTAssertEqual(nestedDict["a"] as! String, "a") - XCTAssertEqual(nestedDict["b"] as! String, "b") - let deserializednestedArraryProp = props["nestedArraryProp"] as! [[Double?]] - XCTAssertEqual(deserializednestedArraryProp[0] as! Array, [1.0, 2.0]) - } + let serializedQueue = JSONHandler.serializeJSONObject([event]) + let deserializedQueue = + try! JSONSerialization.jsonObject(with: serializedQueue!, options: []) as! [[String: Any]] + XCTAssertEqual(deserializedQueue[0]["event"] as! String, "test") + let props = deserializedQueue[0]["properties"] as! [String: Any] + XCTAssertEqual(props["nSNumberProp"] as! NSNumber, nSNumberProp) + XCTAssertEqual(props["doubleProp"] as! Double, doubleProp) + XCTAssertEqual(props["floatProp"] as! Float, floatProp) + XCTAssertEqual(props["stringProp"] as! String, stringProp) + XCTAssertEqual(props["intProp"] as! Int, intProp) + XCTAssertEqual(props["uIntProp"] as! UInt, uIntProp) + XCTAssertEqual(props["uInt64Prop"] as! UInt64, uInt64Prop) + XCTAssertEqual(props["boolProp"] as! Bool, boolProp) + // nil should be dropped from Array properties + XCTAssertEqual(props["optArrayProp"] as! Array, [1.0, 2.0]) + XCTAssertEqual(props["arrayProp"] as! Array, arrayProp) + let deserializedDictProp = props["dictProp"] as! [String: Any] + // nil should be convereted to NSNull() inside Dictionary properties + XCTAssertEqual(deserializedDictProp["nil"] as! NSNull, NSNull()) + XCTAssertEqual(deserializedDictProp["a"] as! String, "a") + XCTAssertEqual(deserializedDictProp["b"] as! String, "b") + XCTAssertEqual(props["urlProp"] as! String, urlProp.absoluteString) + // nil properties themselves should also be converted to NSNull() + XCTAssertEqual(props["nilProp"] as! NSNull, NSNull()) + let deserializedNestedDictProp = props["nestedDictProp"] as! [String: [String: Any]] + let nestedDict = deserializedNestedDictProp["nested"]! + // the same nil logic from above should be applied to nested Collections as well + XCTAssertEqual(nestedDict["nil"] as! NSNull, NSNull()) + XCTAssertEqual(nestedDict["a"] as! String, "a") + XCTAssertEqual(nestedDict["b"] as! String, "b") + let deserializednestedArraryProp = props["nestedArraryProp"] as! [[Double?]] + XCTAssertEqual(deserializednestedArraryProp[0] as! Array, [1.0, 2.0]) + } } diff --git a/MixpanelDemo/MixpanelDemoTests/LoggerTests.swift b/MixpanelDemo/MixpanelDemoTests/LoggerTests.swift index b6f6a8b6..db2903ad 100644 --- a/MixpanelDemo/MixpanelDemoTests/LoggerTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/LoggerTests.swift @@ -8,70 +8,70 @@ import Foundation import XCTest + @testable import Mixpanel class MixpanelLoggerTests: XCTestCase { + func testEnableDebug() { + let counter = CounterLogging() + MixpanelLogger.addLogging(counter) + MixpanelLogger.enableLevel(.debug) - func testEnableDebug() { - let counter = CounterLogging() - MixpanelLogger.addLogging(counter) - MixpanelLogger.enableLevel(.debug) + MixpanelLogger.debug(message: "logged") + XCTAssertEqual(1, counter.count) + } - MixpanelLogger.debug(message: "logged") - XCTAssertEqual(1, counter.count) - } + func testEnableInfo() { + let counter = CounterLogging() + MixpanelLogger.addLogging(counter) + MixpanelLogger.enableLevel(.info) + MixpanelLogger.info(message: "logged") + XCTAssertEqual(1, counter.count) + } - func testEnableInfo() { - let counter = CounterLogging() - MixpanelLogger.addLogging(counter) - MixpanelLogger.enableLevel(.info) - MixpanelLogger.info(message: "logged") - XCTAssertEqual(1, counter.count) - } + func testEnableWarning() { + let counter = CounterLogging() + MixpanelLogger.addLogging(counter) + MixpanelLogger.enableLevel(.warning) + MixpanelLogger.warn(message: "logged") + XCTAssertEqual(1, counter.count) + } - func testEnableWarning() { - let counter = CounterLogging() - MixpanelLogger.addLogging(counter) - MixpanelLogger.enableLevel(.warning) - MixpanelLogger.warn(message: "logged") - XCTAssertEqual(1, counter.count) - } + func testEnableError() { + let counter = CounterLogging() + MixpanelLogger.addLogging(counter) + MixpanelLogger.enableLevel(.error) + MixpanelLogger.error(message: "logged") + XCTAssertEqual(1, counter.count) + } - func testEnableError() { - let counter = CounterLogging() - MixpanelLogger.addLogging(counter) - MixpanelLogger.enableLevel(.error) - MixpanelLogger.error(message: "logged") - XCTAssertEqual(1, counter.count) - } + func testDisabledLogging() { + let counter = CounterLogging() + MixpanelLogger.addLogging(counter) + MixpanelLogger.disableLevel(.debug) + MixpanelLogger.debug(message: "not logged") + XCTAssertEqual(0, counter.count) - func testDisabledLogging() { - let counter = CounterLogging() - MixpanelLogger.addLogging(counter) - MixpanelLogger.disableLevel(.debug) - MixpanelLogger.debug(message: "not logged") - XCTAssertEqual(0, counter.count) + MixpanelLogger.disableLevel(.error) + MixpanelLogger.error(message: "not logged") + XCTAssertEqual(0, counter.count) - MixpanelLogger.disableLevel(.error) - MixpanelLogger.error(message: "not logged") - XCTAssertEqual(0, counter.count) + MixpanelLogger.disableLevel(.info) + MixpanelLogger.info(message: "not logged") + XCTAssertEqual(0, counter.count) - MixpanelLogger.disableLevel(.info) - MixpanelLogger.info(message: "not logged") - XCTAssertEqual(0, counter.count) - - MixpanelLogger.disableLevel(.warning) - MixpanelLogger.warn(message: "not logged") - XCTAssertEqual(0, counter.count) - } + MixpanelLogger.disableLevel(.warning) + MixpanelLogger.warn(message: "not logged") + XCTAssertEqual(0, counter.count) + } } /// This is a stub that implements `MixpanelLogging` to be passed to our `MixpanelLogger` instance for testing class CounterLogging: MixpanelLogging { - var count = 0 + var count = 0 - func addMessage(message: MixpanelLogMessage) { - count = count + 1 - } + func addMessage(message: MixpanelLogMessage) { + count = count + 1 + } } diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelAutomaticEventsTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelAutomaticEventsTests.swift index 291d9c3b..dc22cfb4 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelAutomaticEventsTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelAutomaticEventsTests.swift @@ -13,118 +13,149 @@ import XCTest class MixpanelAutomaticEventsTests: MixpanelBaseTests { - func testSession() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.minimumSessionDuration = 0; - testMixpanel.identify(distinctId: "d1") - waitForTrackingQueue(testMixpanel) - testMixpanel.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), - with: Notification(name: Notification.Name(rawValue: "test"))) - waitForTrackingQueue(testMixpanel) - - let event = eventQueue(token: testMixpanel.apiToken).last - let people1 = peopleQueue(token: testMixpanel.apiToken)[1]["$add"] as! InternalProperties - let people2 = peopleQueue(token: testMixpanel.apiToken)[2]["$add"] as! InternalProperties - XCTAssertEqual((people1["$ae_total_app_sessions"] as? NSNumber)?.intValue, 1, "total app sessions should be added by 1") - XCTAssertNotNil((people2["$ae_total_app_session_length"], "should have session length in $add queue")) - XCTAssertNotNil(event, "Should have an event") - XCTAssertEqual(event?["event"] as? String, "$ae_session", "should be app session event") - XCTAssertNotNil((event?["properties"] as? [String: Any])?["$ae_session_length"], "should have session length") - removeDBfile(testMixpanel.apiToken) - } + func testSession() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.minimumSessionDuration = 0 + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + testMixpanel.automaticEvents.perform( + #selector(AutomaticEvents.appWillResignActive(_:)), + with: Notification(name: Notification.Name(rawValue: "test"))) + waitForTrackingQueue(testMixpanel) - func testKeepAutomaticEventsIfNetworkNotAvailable() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.minimumSessionDuration = 0; - testMixpanel.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), - with: Notification(name: Notification.Name(rawValue: "test"))) + let event = eventQueue(token: testMixpanel.apiToken).last + let people1 = peopleQueue(token: testMixpanel.apiToken)[1]["$add"] as! InternalProperties + let people2 = peopleQueue(token: testMixpanel.apiToken)[2]["$add"] as! InternalProperties + XCTAssertEqual( + (people1["$ae_total_app_sessions"] as? NSNumber)?.intValue, 1, + "total app sessions should be added by 1") + XCTAssertNotNil( + (people2["$ae_total_app_session_length"], "should have session length in $add queue")) + XCTAssertNotNil(event, "Should have an event") + XCTAssertEqual(event?["event"] as? String, "$ae_session", "should be app session event") + XCTAssertNotNil( + (event?["properties"] as? [String: Any])?["$ae_session_length"], "should have session length") + removeDBfile(testMixpanel.apiToken) + } - waitForTrackingQueue(testMixpanel) - let event = eventQueue(token: testMixpanel.apiToken).last - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 2, "automatic events should be accumulated if device is offline") - XCTAssertEqual(event?["event"] as? String, "$ae_session", "should be app session event") - removeDBfile(testMixpanel.apiToken) - } + func testKeepAutomaticEventsIfNetworkNotAvailable() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.minimumSessionDuration = 0 + testMixpanel.automaticEvents.perform( + #selector(AutomaticEvents.appWillResignActive(_:)), + with: Notification(name: Notification.Name(rawValue: "test"))) - func testDiscardAutomaticEventsIftrackAutomaticEventsEnabledIsFalse() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: false, flushInterval: 60) - testMixpanel.minimumSessionDuration = 0; - testMixpanel.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), - with: Notification(name: Notification.Name(rawValue: "test"))) - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 0, "automatic events should not be tracked") - removeDBfile(testMixpanel.apiToken) - } + waitForTrackingQueue(testMixpanel) + let event = eventQueue(token: testMixpanel.apiToken).last + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 2, + "automatic events should be accumulated if device is offline") + XCTAssertEqual(event?["event"] as? String, "$ae_session", "should be app session event") + removeDBfile(testMixpanel.apiToken) + } - func testFlushAutomaticEventsIftrackAutomaticEventsEnabledIsTrue() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.minimumSessionDuration = 0; - testMixpanel.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), - with: Notification(name: Notification.Name(rawValue: "test"))) - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 2, "automatic events should be tracked") - - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 0, "automatic events should be flushed") - removeDBfile(testMixpanel.apiToken) - } + func testDiscardAutomaticEventsIftrackAutomaticEventsEnabledIsFalse() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: false, flushInterval: 60) + testMixpanel.minimumSessionDuration = 0 + testMixpanel.automaticEvents.perform( + #selector(AutomaticEvents.appWillResignActive(_:)), + with: Notification(name: Notification.Name(rawValue: "test"))) + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 0, "automatic events should not be tracked") + removeDBfile(testMixpanel.apiToken) + } - func testUpdated() { - let defaults = UserDefaults(suiteName: "Mixpanel") - let infoDict = Bundle.main.infoDictionary - let appVersionValue = infoDict?["CFBundleShortVersionString"] - let savedVersionValue = defaults?.string(forKey: "MPAppVersion") - XCTAssertEqual(appVersionValue as? String, savedVersionValue, "Saved version and current version need to be the same") - } + func testFlushAutomaticEventsIftrackAutomaticEventsEnabledIsTrue() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.minimumSessionDuration = 0 + testMixpanel.automaticEvents.perform( + #selector(AutomaticEvents.appWillResignActive(_:)), + with: Notification(name: Notification.Name(rawValue: "test"))) + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 2, "automatic events should be tracked") - func testFirstAppShouldOnlyBeTrackedOnce() { - let testToken = randomId() - let mp = Mixpanel.initialize(token: testToken, trackAutomaticEvents: true) - mp.minimumSessionDuration = 0; - waitForTrackingQueue(mp) - XCTAssertEqual(eventQueue(token: mp.apiToken).count, 1, "First app open should be tracked again") - flushAndWaitForTrackingQueue(mp) - - let mp2 = Mixpanel.initialize(token: testToken, trackAutomaticEvents: true) - mp2.minimumSessionDuration = 0; - waitForTrackingQueue(mp2) - XCTAssertEqual(eventQueue(token: mp2.apiToken).count, 0, "First app open should not be tracked again") - } - - func testAutomaticEventsInMultipleInstances() { - // remove UserDefaults key and archive files to simulate first app open state - let defaults = UserDefaults(suiteName: "Mixpanel") - defaults?.removeObject(forKey: "MPFirstOpen") + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 0, "automatic events should be flushed") + removeDBfile(testMixpanel.apiToken) + } - let mp = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true) - mp.minimumSessionDuration = 0; - waitForTrackingQueue(mp) - let mp2 = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true) - mp2.minimumSessionDuration = 0; - waitForTrackingQueue(mp2) + func testUpdated() { + let defaults = UserDefaults(suiteName: "Mixpanel") + let infoDict = Bundle.main.infoDictionary + let appVersionValue = infoDict?["CFBundleShortVersionString"] + let savedVersionValue = defaults?.string(forKey: "MPAppVersion") + XCTAssertEqual( + appVersionValue as? String, savedVersionValue, + "Saved version and current version need to be the same") + } - XCTAssertEqual(eventQueue(token: mp.apiToken).count, 1, "there should be only 1 event") - let appOpenEvent = eventQueue(token: mp.apiToken).last - XCTAssertEqual(appOpenEvent?["event"] as? String, "$ae_first_open", "should be first app open event") + func testFirstAppShouldOnlyBeTrackedOnce() { + let testToken = randomId() + let mp = Mixpanel.initialize(token: testToken, trackAutomaticEvents: true) + mp.minimumSessionDuration = 0 + waitForTrackingQueue(mp) + XCTAssertEqual( + eventQueue(token: mp.apiToken).count, 1, "First app open should be tracked again") + flushAndWaitForTrackingQueue(mp) - XCTAssertEqual(eventQueue(token: mp2.apiToken).count, 1, "there should be only 1 event") - let otherAppOpenEvent = eventQueue(token: mp2.apiToken).last - XCTAssertEqual(otherAppOpenEvent?["event"] as? String, "$ae_first_open", "should be first app open event") + let mp2 = Mixpanel.initialize(token: testToken, trackAutomaticEvents: true) + mp2.minimumSessionDuration = 0 + waitForTrackingQueue(mp2) + XCTAssertEqual( + eventQueue(token: mp2.apiToken).count, 0, "First app open should not be tracked again") + } - mp.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), - with: Notification(name: Notification.Name(rawValue: "test"))) - mp2.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), - with: Notification(name: Notification.Name(rawValue: "test"))) - mp.trackingQueue.sync { } - mp2.trackingQueue.sync { } - let appSessionEvent = eventQueue(token: mp.apiToken).last - XCTAssertNotNil(appSessionEvent, "Should have an event") - XCTAssertEqual(appSessionEvent?["event"] as? String, "$ae_session", "should be app session event") - XCTAssertNotNil((appSessionEvent?["properties"] as? [String: Any])?["$ae_session_length"], "should have session length") - let otherAppSessionEvent = eventQueue(token: mp2.apiToken).last - XCTAssertEqual(otherAppSessionEvent?["event"] as? String, "$ae_session", "should be app session event") - XCTAssertNotNil((otherAppSessionEvent?["properties"] as? [String: Any])?["$ae_session_length"], "should have session length") - removeDBfile(mp.apiToken) - removeDBfile(mp2.apiToken) - } + func testAutomaticEventsInMultipleInstances() { + // remove UserDefaults key and archive files to simulate first app open state + let defaults = UserDefaults(suiteName: "Mixpanel") + defaults?.removeObject(forKey: "MPFirstOpen") + + let mp = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true) + mp.minimumSessionDuration = 0 + waitForTrackingQueue(mp) + let mp2 = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true) + mp2.minimumSessionDuration = 0 + waitForTrackingQueue(mp2) + + XCTAssertEqual(eventQueue(token: mp.apiToken).count, 1, "there should be only 1 event") + let appOpenEvent = eventQueue(token: mp.apiToken).last + XCTAssertEqual( + appOpenEvent?["event"] as? String, "$ae_first_open", "should be first app open event") + + XCTAssertEqual(eventQueue(token: mp2.apiToken).count, 1, "there should be only 1 event") + let otherAppOpenEvent = eventQueue(token: mp2.apiToken).last + XCTAssertEqual( + otherAppOpenEvent?["event"] as? String, "$ae_first_open", "should be first app open event") + + mp.automaticEvents.perform( + #selector(AutomaticEvents.appWillResignActive(_:)), + with: Notification(name: Notification.Name(rawValue: "test"))) + mp2.automaticEvents.perform( + #selector(AutomaticEvents.appWillResignActive(_:)), + with: Notification(name: Notification.Name(rawValue: "test"))) + mp.trackingQueue.sync {} + mp2.trackingQueue.sync {} + let appSessionEvent = eventQueue(token: mp.apiToken).last + XCTAssertNotNil(appSessionEvent, "Should have an event") + XCTAssertEqual( + appSessionEvent?["event"] as? String, "$ae_session", "should be app session event") + XCTAssertNotNil( + (appSessionEvent?["properties"] as? [String: Any])?["$ae_session_length"], + "should have session length") + let otherAppSessionEvent = eventQueue(token: mp2.apiToken).last + XCTAssertEqual( + otherAppSessionEvent?["event"] as? String, "$ae_session", "should be app session event") + XCTAssertNotNil( + (otherAppSessionEvent?["properties"] as? [String: Any])?["$ae_session_length"], + "should have session length") + removeDBfile(mp.apiToken) + removeDBfile(mp2.apiToken) + } } diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelBaseTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelBaseTests.swift index ff6cf19c..ef55c19b 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelBaseTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelBaseTests.swift @@ -6,153 +6,153 @@ // Copyright © 2016 Mixpanel. All rights reserved. // -import XCTest import SQLite3 +import XCTest @testable import Mixpanel class MixpanelBaseTests: XCTestCase, MixpanelDelegate { - var mixpanelWillFlush: Bool! - static var requestCount = 0 + var mixpanelWillFlush: Bool! + static var requestCount = 0 - override func setUp() { - NSLog("starting test setup...") - super.setUp() - mixpanelWillFlush = false - let defaults = UserDefaults(suiteName: "Mixpanel") - defaults?.removeObject(forKey: "MPFirstOpen") + override func setUp() { + NSLog("starting test setup...") + super.setUp() + mixpanelWillFlush = false + let defaults = UserDefaults(suiteName: "Mixpanel") + defaults?.removeObject(forKey: "MPFirstOpen") - NSLog("finished test setup") + NSLog("finished test setup") + } + + override func tearDown() { + super.tearDown() + } + + func removeDBfile(_ token: String? = nil) { + do { + let fileManager = FileManager.default + + // Check if file exists + if fileManager.fileExists(atPath: dbFilePath(token)) { + // Delete file + try fileManager.removeItem(atPath: dbFilePath(token)) + } else { + print("Unable to delete the test db file at \(dbFilePath(token)), the file does not exist") + } + + } catch let error as NSError { + print("An error took place: \(error)") + } + } + + func dbFilePath(_ token: String? = nil) -> String { + let manager = FileManager.default + #if os(iOS) + let url = manager.urls(for: .libraryDirectory, in: .userDomainMask).last + #else + let url = manager.urls(for: .cachesDirectory, in: .userDomainMask).last + #endif // os(iOS) + guard let apiToken = token else { + return "" } - override func tearDown() { - super.tearDown() + guard let urlUnwrapped = url?.appendingPathComponent("\(token ?? apiToken)_MPDB.sqlite").path + else { + return "" } - - func removeDBfile(_ token: String? = nil) { - do { - let fileManager = FileManager.default - - // Check if file exists - if fileManager.fileExists(atPath: dbFilePath(token)) { - // Delete file - try fileManager.removeItem(atPath: dbFilePath(token)) - } else { - print("Unable to delete the test db file at \(dbFilePath(token)), the file does not exist") - } - - } - catch let error as NSError { - print("An error took place: \(error)") - } + return urlUnwrapped + } + + func mixpanelWillFlush(_ mixpanel: MixpanelInstance) -> Bool { + return mixpanelWillFlush + } + + func waitForTrackingQueue(_ mixpanel: MixpanelInstance) { + mixpanel.trackingQueue.sync { + mixpanel.networkQueue.sync { + return + } } - - func dbFilePath(_ token: String? = nil) -> String { - let manager = FileManager.default - #if os(iOS) - let url = manager.urls(for: .libraryDirectory, in: .userDomainMask).last - #else - let url = manager.urls(for: .cachesDirectory, in: .userDomainMask).last - #endif // os(iOS) - guard let apiToken = token else { - return "" - } - - guard let urlUnwrapped = url?.appendingPathComponent("\(token ?? apiToken)_MPDB.sqlite").path else { - return "" - } - return urlUnwrapped + mixpanel.trackingQueue.sync { + mixpanel.networkQueue.sync { + return + } + } + } + + func randomId() -> String { + return String(format: "%08x%08x", arc4random(), arc4random()) + } + + func waitForAsyncTasks() { + var hasCompletedTask = false + DispatchQueue.main.async { + hasCompletedTask = true } - - func mixpanelWillFlush(_ mixpanel: MixpanelInstance) -> Bool { - return mixpanelWillFlush + let loopUntil = Date(timeIntervalSinceNow: 10) + while !hasCompletedTask && loopUntil.timeIntervalSinceNow > 0 { + RunLoop.current.run(mode: RunLoop.Mode.default, before: loopUntil) } + } - func waitForTrackingQueue(_ mixpanel: MixpanelInstance) { - mixpanel.trackingQueue.sync() { - mixpanel.networkQueue.sync() { - return - } - } - mixpanel.trackingQueue.sync() { - mixpanel.networkQueue.sync() { - return - } - } - } - - func randomId() -> String - { - return String(format: "%08x%08x", arc4random(), arc4random()) - } - - func waitForAsyncTasks() { - var hasCompletedTask = false - DispatchQueue.main.async { - hasCompletedTask = true - } + func eventQueue(token: String) -> Queue { + return MixpanelPersistence.init(instanceName: token).loadEntitiesInBatch(type: .events) + } - let loopUntil = Date(timeIntervalSinceNow: 10) - while !hasCompletedTask && loopUntil.timeIntervalSinceNow > 0 { - RunLoop.current.run(mode: RunLoop.Mode.default, before: loopUntil) - } - } - - func eventQueue(token: String) -> Queue { - return MixpanelPersistence.init(instanceName: token).loadEntitiesInBatch(type: .events) - } + func peopleQueue(token: String) -> Queue { + return MixpanelPersistence.init(instanceName: token).loadEntitiesInBatch(type: .people) + } - func peopleQueue(token: String) -> Queue { - return MixpanelPersistence.init(instanceName: token).loadEntitiesInBatch(type: .people) - } - - func unIdentifiedPeopleQueue(token: String) -> Queue { - return MixpanelPersistence.init(instanceName: token).loadEntitiesInBatch(type: .people, flag: PersistenceConstant.unIdentifiedFlag) - } - - func groupQueue(token: String) -> Queue { - return MixpanelPersistence.init(instanceName: token).loadEntitiesInBatch(type: .groups) - } - - func flushAndWaitForTrackingQueue(_ mixpanel: MixpanelInstance) { - mixpanel.flush() - waitForTrackingQueue(mixpanel) - mixpanel.flush() - waitForTrackingQueue(mixpanel) - } + func unIdentifiedPeopleQueue(token: String) -> Queue { + return MixpanelPersistence.init(instanceName: token).loadEntitiesInBatch( + type: .people, flag: PersistenceConstant.unIdentifiedFlag) + } - func assertDefaultPeopleProperties(_ properties: InternalProperties) { - XCTAssertNotNil(properties["$ios_device_model"], "missing $ios_device_model property") - XCTAssertNotNil(properties["$ios_lib_version"], "missing $ios_lib_version property") - XCTAssertNotNil(properties["$ios_version"], "missing $ios_version property") - XCTAssertNotNil(properties["$ios_app_version"], "missing $ios_app_version property") - XCTAssertNotNil(properties["$ios_app_release"], "missing $ios_app_release property") - } + func groupQueue(token: String) -> Queue { + return MixpanelPersistence.init(instanceName: token).loadEntitiesInBatch(type: .groups) + } - func compareDate(dateString: String, dateDate: Date) { - let dateFormatter: ISO8601DateFormatter = ISO8601DateFormatter() - let date = dateFormatter.string(from: dateDate) - XCTAssertEqual(String(date.prefix(19)), String(dateString.prefix(19))) - } - - func allPropertyTypes() -> Properties { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss zzz" - let date = dateFormatter.date(from: "2012-09-28 19:14:36 PDT") - let nested = ["p1": ["p2": ["p3": ["bottom"]]]] - let opt: String? = nil - return ["string": "yello", - "number": 3, - "date": date!, - "dictionary": ["k": "v", "opt": opt as Any], - "array": ["1", opt as Any], - "null": NSNull(), - "nested": nested, - "url": URL(string: "https://mixpanel.com/")!, - "float": 1.3, - "optional": opt, - ] - } + func flushAndWaitForTrackingQueue(_ mixpanel: MixpanelInstance) { + mixpanel.flush() + waitForTrackingQueue(mixpanel) + mixpanel.flush() + waitForTrackingQueue(mixpanel) + } + + func assertDefaultPeopleProperties(_ properties: InternalProperties) { + XCTAssertNotNil(properties["$ios_device_model"], "missing $ios_device_model property") + XCTAssertNotNil(properties["$ios_lib_version"], "missing $ios_lib_version property") + XCTAssertNotNil(properties["$ios_version"], "missing $ios_version property") + XCTAssertNotNil(properties["$ios_app_version"], "missing $ios_app_version property") + XCTAssertNotNil(properties["$ios_app_release"], "missing $ios_app_release property") + } + + func compareDate(dateString: String, dateDate: Date) { + let dateFormatter: ISO8601DateFormatter = ISO8601DateFormatter() + let date = dateFormatter.string(from: dateDate) + XCTAssertEqual(String(date.prefix(19)), String(dateString.prefix(19))) + } + + func allPropertyTypes() -> Properties { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss zzz" + let date = dateFormatter.date(from: "2012-09-28 19:14:36 PDT") + let nested = ["p1": ["p2": ["p3": ["bottom"]]]] + let opt: String? = nil + return [ + "string": "yello", + "number": 3, + "date": date!, + "dictionary": ["k": "v", "opt": opt as Any], + "array": ["1", opt as Any], + "null": NSNull(), + "nested": nested, + "url": URL(string: "https://mixpanel.com/")!, + "float": 1.3, + "optional": opt, + ] + } } diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift index 5b3d7efc..faf0b5ba 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift @@ -6,7 +6,6 @@ // Copyright © 2016 Mixpanel. All rights reserved. // - import XCTest @testable import Mixpanel @@ -15,1247 +14,1470 @@ import XCTest private let devicePrefix = "$device:" class MixpanelDemoTests: MixpanelBaseTests { - func test5XXResponse() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.serverURL = kFakeServerUrl - testMixpanel.track(event: "Fake Event") - flushAndWaitForTrackingQueue(testMixpanel) - // Failure count should be 3 - let waitTime = - testMixpanel.flushInstance.flushRequest.networkRequestsAllowedAfterTime - Date().timeIntervalSince1970 - print("Delta wait time is \(waitTime)") - XCTAssert(waitTime >= 110, "Network backoff time is less than 2 minutes.") - XCTAssert(testMixpanel.flushInstance.flushRequest.networkConsecutiveFailures == 2, - "Network failures did not equal 2") - - XCTAssert(eventQueue(token: testMixpanel.apiToken).count == 2, - "Removed an event from the queue that was not sent") - removeDBfile(testMixpanel.apiToken) + func test5XXResponse() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.serverURL = kFakeServerUrl + testMixpanel.track(event: "Fake Event") + flushAndWaitForTrackingQueue(testMixpanel) + // Failure count should be 3 + let waitTime = + testMixpanel.flushInstance.flushRequest.networkRequestsAllowedAfterTime + - Date().timeIntervalSince1970 + print("Delta wait time is \(waitTime)") + XCTAssert(waitTime >= 110, "Network backoff time is less than 2 minutes.") + XCTAssert( + testMixpanel.flushInstance.flushRequest.networkConsecutiveFailures == 2, + "Network failures did not equal 2") + + XCTAssert( + eventQueue(token: testMixpanel.apiToken).count == 2, + "Removed an event from the queue that was not sent") + removeDBfile(testMixpanel.apiToken) + } + + func testFlushEvents() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + for i in 0..<50 { + testMixpanel.track(event: "event \(i)") + } + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).isEmpty, + "events should have been flushed") + + for i in 0..<60 { + testMixpanel.track(event: "event \(i)") + } + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).isEmpty, + "events should have been flushed") + removeDBfile(testMixpanel.apiToken) + } + + func testFlushProperties() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + XCTAssertTrue(testMixpanel.flushBatchSize == 50, "the default flush batch size is set to 50") + XCTAssertTrue(testMixpanel.flushInterval == 60, "flush interval is set correctly") + testMixpanel.flushBatchSize = 10 + XCTAssertTrue(testMixpanel.flushBatchSize == 10, "flush batch size is set correctly") + testMixpanel.flushBatchSize = 60 + XCTAssertTrue(testMixpanel.flushBatchSize == 50, "flush batch size is max at 50") + testMixpanel.flushInterval = 30 + XCTAssertTrue(testMixpanel.flushInterval == 30, "flush interval is set correctly") + } + + func testFlushPeople() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + for i in 0..<50 { + testMixpanel.people.set(property: "p1", to: "\(i)") + } + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + peopleQueue(token: testMixpanel.apiToken).isEmpty, "people should have been flushed") + for i in 0..<60 { + testMixpanel.people.set(property: "p1", to: "\(i)") + } + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + peopleQueue(token: testMixpanel.apiToken).isEmpty, "people should have been flushed") + removeDBfile(testMixpanel.apiToken) + } + + func testFlushGroups() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + let groupKey = "test_key" + let groupValue = "test_value" + for i in 0..<50 { + testMixpanel.getGroup(groupKey: groupKey, groupID: groupValue).set(property: "p1", to: "\(i)") + } + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + groupQueue(token: testMixpanel.apiToken).isEmpty, "groups should have been flushed") + for i in 0..<60 { + testMixpanel.getGroup(groupKey: groupKey, groupID: groupValue).set(property: "p1", to: "\(i)") + } + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + peopleQueue(token: testMixpanel.apiToken).isEmpty, "groups should have been flushed") + removeDBfile(testMixpanel.apiToken) + } + + func testFlushNetworkFailure() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.serverURL = kFakeServerUrl + for i in 0..<50 { + testMixpanel.track(event: "event \(UInt(i))") + } + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 51, "51 events should be queued up") + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 51, + "events should still be in the queue if flush fails") + removeDBfile(testMixpanel.apiToken) + } + + func testFlushQueueContainsCorruptedEvent() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.trackingQueue.async { + testMixpanel.mixpanelPersistence.saveEntity( + ["event": "bad event1", "properties": ["BadProp": Double.nan]], type: .events) + testMixpanel.mixpanelPersistence.saveEntity( + ["event": "bad event2", "properties": ["BadProp": Float.nan]], type: .events) + testMixpanel.mixpanelPersistence.saveEntity( + ["event": "bad event3", "properties": ["BadProp": Double.infinity]], type: .events) + testMixpanel.mixpanelPersistence.saveEntity( + ["event": "bad event4", "properties": ["BadProp": Float.infinity]], type: .events) } - func testFlushEvents() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - for i in 0..<50 { - testMixpanel.track(event: "event \(i)") - } - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, - "events should have been flushed") - - for i in 0..<60 { - testMixpanel.track(event: "event \(i)") - } - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, - "events should have been flushed") - removeDBfile(testMixpanel.apiToken) + for i in 0..<10 { + testMixpanel.track(event: "event \(UInt(i))") } - - func testFlushProperties() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - XCTAssertTrue(testMixpanel.flushBatchSize == 50, "the default flush batch size is set to 50") - XCTAssertTrue(testMixpanel.flushInterval == 60, "flush interval is set correctly") - testMixpanel.flushBatchSize = 10 - XCTAssertTrue(testMixpanel.flushBatchSize == 10, "flush batch size is set correctly") - testMixpanel.flushBatchSize = 60 - XCTAssertTrue(testMixpanel.flushBatchSize == 50, "flush batch size is max at 50") - testMixpanel.flushInterval = 30 - XCTAssertTrue(testMixpanel.flushInterval == 30, "flush interval is set correctly") + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 0, "good events should still be flushed") + removeDBfile(testMixpanel.apiToken) + } + + func testAddEventContainsInvalidJsonObjectDoubleNaN() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.track(event: "bad event", properties: ["BadProp": Double.nan]) + } + removeDBfile(testMixpanel.apiToken) + } + + func testAddEventContainsInvalidJsonObjectFloatNaN() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.track(event: "bad event", properties: ["BadProp": Float.nan]) + } + removeDBfile(testMixpanel.apiToken) + } + + func testAddEventContainsInvalidJsonObjectDoubleInfinity() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.track(event: "bad event", properties: ["BadProp": Double.infinity]) + } + removeDBfile(testMixpanel.apiToken) + } + + func testAddEventContainsInvalidJsonObjectFloatInfinity() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.track(event: "bad event", properties: ["BadProp": Float.infinity]) + } + removeDBfile(testMixpanel.apiToken) + } + + func testAddingEventsAfterFlush() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + for i in 0..<10 { + testMixpanel.track(event: "event \(UInt(i))") + } + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 11, "11 events should be queued up") + flushAndWaitForTrackingQueue(testMixpanel) + for i in 0..<5 { + testMixpanel.track(event: "event \(UInt(i))") + } + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 5, "5 more events should be queued up") + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).isEmpty, "events should have been flushed") + removeDBfile(testMixpanel.apiToken) + } + + // Mock implementation of MixpanelFlags to track loadFlags calls + class MockMixpanelFlags: MixpanelFlags { + var delegate: MixpanelFlagDelegate? + var loadFlagsCallCount = 0 + + func loadFlags() { + loadFlagsCallCount += 1 } - func testFlushPeople() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - for i in 0..<50 { - testMixpanel.people.set(property: "p1", to: "\(i)") - } - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "people should have been flushed") - for i in 0..<60 { - testMixpanel.people.set(property: "p1", to: "\(i)") - } - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "people should have been flushed") - removeDBfile(testMixpanel.apiToken) + func areFlagsReady() -> Bool { + return true } - func testFlushGroups() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - let groupKey = "test_key" - let groupValue = "test_value" - for i in 0..<50 { - testMixpanel.getGroup(groupKey: groupKey, groupID: groupValue).set(property: "p1", to: "\(i)") - } - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(groupQueue(token: testMixpanel.apiToken).isEmpty, "groups should have been flushed") - for i in 0..<60 { - testMixpanel.getGroup(groupKey: groupKey, groupID: groupValue).set(property: "p1", to: "\(i)") - } - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "groups should have been flushed") - removeDBfile(testMixpanel.apiToken) + func getVariantSync(_ flagName: String, fallback: MixpanelFlagVariant) -> MixpanelFlagVariant { + return fallback } - func testFlushNetworkFailure() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.serverURL = kFakeServerUrl - for i in 0..<50 { - testMixpanel.track(event: "event \(UInt(i))") - } - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 51, "51 events should be queued up") - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 51, - "events should still be in the queue if flush fails") - removeDBfile(testMixpanel.apiToken) + func getVariant( + _ flagName: String, fallback: MixpanelFlagVariant, + completion: @escaping (MixpanelFlagVariant) -> Void + ) { + completion(fallback) } - func testFlushQueueContainsCorruptedEvent() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.trackingQueue.async { - testMixpanel.mixpanelPersistence.saveEntity(["event": "bad event1", "properties": ["BadProp": Double.nan]], type: .events) - testMixpanel.mixpanelPersistence.saveEntity(["event": "bad event2", "properties": ["BadProp": Float.nan]], type: .events) - testMixpanel.mixpanelPersistence.saveEntity(["event": "bad event3", "properties": ["BadProp": Double.infinity]], type: .events) - testMixpanel.mixpanelPersistence.saveEntity(["event": "bad event4", "properties": ["BadProp": Float.infinity]], type: .events) - } - - for i in 0..<10 { - testMixpanel.track(event: "event \(UInt(i))") - } - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 0, "good events should still be flushed") - removeDBfile(testMixpanel.apiToken) + func getVariantValueSync(_ flagName: String, fallbackValue: Any?) -> Any? { + return fallbackValue } - func testAddEventContainsInvalidJsonObjectDoubleNaN() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.track(event: "bad event", properties: ["BadProp": Double.nan]) - } - removeDBfile(testMixpanel.apiToken) + func getVariantValue( + _ flagName: String, fallbackValue: Any?, completion: @escaping (Any?) -> Void + ) { + completion(fallbackValue) } - func testAddEventContainsInvalidJsonObjectFloatNaN() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.track(event: "bad event", properties: ["BadProp": Float.nan]) - } - removeDBfile(testMixpanel.apiToken) + func isEnabledSync(_ flagName: String, fallbackValue: Bool) -> Bool { + return fallbackValue } - func testAddEventContainsInvalidJsonObjectDoubleInfinity() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.track(event: "bad event", properties: ["BadProp": Double.infinity]) + func isEnabled(_ flagName: String, fallbackValue: Bool, completion: @escaping (Bool) -> Void) { + completion(fallbackValue) + } + } + + func testIdentify() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + + // Inject our mock flags object + let mockFlags = MockMixpanelFlags() + testMixpanel.flags = mockFlags + + for _ in 0..<2 { + // run this twice to test reset works correctly wrt to distinct ids + let distinctId: String = "d1" + // try this for ODIN and nil + #if MIXPANEL_UNIQUE_DISTINCT_ID + XCTAssertEqual( + testMixpanel.distinctId, + devicePrefix + testMixpanel.defaultDeviceId(), + "mixpanel identify failed to set default distinct id") + XCTAssertEqual( + testMixpanel.anonymousId, + testMixpanel.defaultDeviceId(), + "mixpanel failed to set default anonymous id") + #endif + XCTAssertNil( + testMixpanel.people.distinctId, + "mixpanel people distinct id should default to nil") + XCTAssertNil( + testMixpanel.people.distinctId, + "mixpanel user id should default to nil") + testMixpanel.track(event: "e1") + waitForTrackingQueue(testMixpanel) + let eventsQueue = eventQueue(token: testMixpanel.apiToken) + XCTAssertTrue( + eventsQueue.count == 2 || eventsQueue.count == 1, // first app open should not be tracked for the second run, + "events should be sent right away with default distinct id") + #if MIXPANEL_UNIQUE_DISTINCT_ID + XCTAssertEqual( + (eventsQueue.last?["properties"] as? InternalProperties)?["distinct_id"] as? String, + devicePrefix + mixpanel.defaultDeviceId(), + "events should use default distinct id if none set") + #endif + XCTAssertEqual( + (eventsQueue.last?["properties"] as? InternalProperties)?["$lib_version"] as? String, + AutomaticProperties.libVersion(), + "events should has lib version in internal properties") + testMixpanel.people.set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + var peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + var unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) + XCTAssertTrue( + peopleQueue_value.isEmpty, + "people records should go to unidentified queue before identify:") + XCTAssertTrue( + unidentifiedQueue.count == 2 || eventsQueue.count == 1, // first app open should not be tracked for the second run, + "unidentified people records not queued") + XCTAssertEqual( + unidentifiedQueue.last?["$token"] as? String, + testMixpanel.apiToken, + "incorrect project token in people record") + // Record the loadFlags call count before identify + let loadFlagsCallCountBefore = mockFlags.loadFlagsCallCount + + testMixpanel.identify(distinctId: distinctId) + waitForTrackingQueue(testMixpanel) + + // Assert that loadFlags was called when distinctId changed + XCTAssertEqual( + mockFlags.loadFlagsCallCount, loadFlagsCallCountBefore + 1, + "loadFlags should be called when distinctId changes during identify") + + let anonymousId = testMixpanel.anonymousId + peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) + XCTAssertEqual( + peopleQueue_value.last?["$distinct_id"] as? String, + distinctId, "distinct id not set properly on unidentified people record") + XCTAssertEqual( + testMixpanel.distinctId, distinctId, + "mixpanel identify failed to set distinct id") + XCTAssertEqual( + testMixpanel.userId, distinctId, + "mixpanel identify failed to set user id") + XCTAssertEqual( + testMixpanel.anonymousId, anonymousId, + "mixpanel identify shouldn't change anonymousId") + XCTAssertEqual( + testMixpanel.people.distinctId, distinctId, + "mixpanel identify failed to set people distinct id") + XCTAssertTrue( + unidentifiedQueue.isEmpty, + "identify: should move records from unidentified queue") + XCTAssertTrue( + peopleQueue_value.count > 0, + "identify: should move records to main people queue") + XCTAssertEqual( + peopleQueue_value.last?["$token"] as? String, + testMixpanel.apiToken, "incorrect project token in people record") + let p: InternalProperties = peopleQueue_value.last?["$set"] as! InternalProperties + XCTAssertEqual(p["p1"] as? String, "a", "custom people property not queued") + assertDefaultPeopleProperties(p) + peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + + testMixpanel.people.set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + + peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) + XCTAssertEqual( + peopleQueue_value.last?["$distinct_id"] as? String, + distinctId, "distinct id not set properly on unidentified people record") + XCTAssertTrue( + unidentifiedQueue.isEmpty, + "once idenitfy: is called, unidentified queue should be skipped") + XCTAssertTrue( + peopleQueue_value.count > 0, + "once identify: is called, records should go straight to main queue") + testMixpanel.track(event: "e2") + waitForTrackingQueue(testMixpanel) + let newDistinctId = + (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?[ + "distinct_id"] as? String + XCTAssertEqual( + newDistinctId, distinctId, + "events should use new distinct id after identify:") + + // Test that calling identify with the same distinctId does NOT trigger loadFlags + let loadFlagsCountBeforeSameId = mockFlags.loadFlagsCallCount + testMixpanel.identify(distinctId: distinctId) // Same distinctId + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + mockFlags.loadFlagsCallCount, loadFlagsCountBeforeSameId, + "loadFlags should NOT be called when distinctId doesn't change") + + testMixpanel.reset() + waitForTrackingQueue(testMixpanel) + } + removeDBfile(testMixpanel.apiToken) + } + + func testIdentifyTrack() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let distinctIdBeforeIdentify: String? = testMixpanel.distinctId + let distinctId = "testIdentifyTrack" + + testMixpanel.identify(distinctId: distinctId) + waitForTrackingQueue(testMixpanel) + let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(e["event"] as? String, "$identify", "incorrect event name") + let p: InternalProperties = e["properties"] as! InternalProperties + XCTAssertEqual(p["distinct_id"] as? String, distinctId, "wrong distinct_id") + XCTAssertEqual( + p["$anon_distinct_id"] as? String, distinctIdBeforeIdentify, "wrong $anon_distinct_id") + removeDBfile(testMixpanel.apiToken) + } + + func testIdentifyResetTrack() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let originalDistinctId: String? = testMixpanel.distinctId + let distinctId = "testIdentifyTrack" + testMixpanel.reset() + waitForTrackingQueue(testMixpanel) + + for i in 1...3 { + let prevDistinctId: String? = testMixpanel.distinctId + let newDistinctId = distinctId + String(i) + testMixpanel.identify(distinctId: newDistinctId) + waitForTrackingQueue(testMixpanel) + + let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(e["event"] as? String, "$identify", "incorrect event name") + let p: InternalProperties = e["properties"] as! InternalProperties + XCTAssertEqual(p["distinct_id"] as? String, newDistinctId, "wrong distinct_id") + XCTAssertEqual(p["$anon_distinct_id"] as? String, prevDistinctId, "wrong $anon_distinct_id") + XCTAssertNotEqual( + prevDistinctId, originalDistinctId, "After reset, UUID will be used - never the same") + #if MIXPANEL_UNIQUE_DISTINCT_ID + XCTAssertEqual( + prevDistinctId, originalDistinctId, "After reset, IFV will be used - always the same") + #endif + testMixpanel.reset() + waitForTrackingQueue(testMixpanel) + } + removeDBfile(testMixpanel.apiToken) + } + + func testCreateAlias() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.people.set(properties: ["p1": "a"]) + waitForTrackingQueue(testMixpanel) + var unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) + // the user profile update has been queued up in unidentifiedQueue until identify is called + XCTAssertTrue(!unidentifiedQueue.isEmpty) + + let distinctId = testMixpanel.distinctId + let alias: String = "a1" + testMixpanel.createAlias(alias, distinctId: testMixpanel.distinctId) + waitForTrackingQueue(testMixpanel) + + let mixpanelIdentity = MixpanelPersistence.loadIdentity(instanceName: testMixpanel.apiToken) + XCTAssertTrue( + distinctId == mixpanelIdentity.distinctID && distinctId == mixpanelIdentity.peopleDistinctID + && distinctId == mixpanelIdentity.userId && alias == mixpanelIdentity.alias) + removeDBfile(testMixpanel.apiToken) + unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) + // unidentifiedQueue has been flushed + XCTAssertTrue(unidentifiedQueue.isEmpty) + + let testMixpanel2 = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel2.people.set(properties: ["p1": "a"]) + waitForTrackingQueue(testMixpanel2) + + let distinctId2 = testMixpanel2.distinctId + testMixpanel2.createAlias(alias, distinctId: testMixpanel.distinctId, andIdentify: false) + waitForTrackingQueue(testMixpanel2) + + let unidentifiedQueue2 = unIdentifiedPeopleQueue(token: testMixpanel2.apiToken) + // The user profile updates should still be held in unidentifiedQueue cause no identify is called + XCTAssertTrue(!unidentifiedQueue2.isEmpty) + let mixpanelIdentity2 = MixpanelPersistence.loadIdentity(instanceName: testMixpanel2.apiToken) + XCTAssertTrue( + distinctId2 == mixpanelIdentity2.distinctID && nil == mixpanelIdentity2.peopleDistinctID + && nil == mixpanelIdentity2.userId && alias == mixpanelIdentity2.alias) + removeDBfile(testMixpanel2.apiToken) + } + + func testPersistentIdentity() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let distinctId: String = "d1" + let alias: String = "a1" + testMixpanel.identify(distinctId: distinctId) + waitForTrackingQueue(testMixpanel) + testMixpanel.createAlias(alias, distinctId: testMixpanel.distinctId) + waitForTrackingQueue(testMixpanel) + var mixpanelIdentity = MixpanelPersistence.loadIdentity(instanceName: testMixpanel.apiToken) + XCTAssertTrue( + distinctId == mixpanelIdentity.distinctID && distinctId == mixpanelIdentity.peopleDistinctID + && distinctId == mixpanelIdentity.userId && alias == mixpanelIdentity.alias) + testMixpanel.archive() + waitForTrackingQueue(testMixpanel) + testMixpanel.unarchive() + waitForTrackingQueue(testMixpanel) + 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(instanceName: testMixpanel.apiToken) + waitForTrackingQueue(testMixpanel) + mixpanelIdentity = MixpanelPersistence.loadIdentity(instanceName: testMixpanel.apiToken) + XCTAssertTrue( + "" == mixpanelIdentity.distinctID && nil == mixpanelIdentity.peopleDistinctID + && nil == mixpanelIdentity.anonymousId && nil == mixpanelIdentity.userId + && nil == mixpanelIdentity.alias) + removeDBfile(testMixpanel.apiToken) + } + + func testUseUniqueDistinctId() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let testMixpanel2 = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + XCTAssertNotEqual( + testMixpanel.distinctId, testMixpanel2.distinctId, + "by default, distinctId should not be unique to the device") + + let testMixpanel3 = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60, useUniqueDistinctId: false) + let testMixpanel4 = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60, useUniqueDistinctId: false) + XCTAssertNotEqual( + testMixpanel3.distinctId, testMixpanel4.distinctId, + "distinctId should not be unique to the device if useUniqueDistinctId is set to false") + + let testMixpanel5 = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60, useUniqueDistinctId: true) + let testMixpanel6 = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60, useUniqueDistinctId: true) + XCTAssertEqual( + testMixpanel5.distinctId, testMixpanel6.distinctId, + "distinctId should be unique to the device if useUniqueDistinctId is set to true") + } + + func testHadPersistedDistinctId() { + let testToken = randomId() + let testMixpanel = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: true, flushInterval: 60) + XCTAssertNotNil(testMixpanel.distinctId) + let distinctId = testMixpanel.distinctId + let testMixpanel2 = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: true, flushInterval: 60) + XCTAssertEqual( + testMixpanel2.distinctId, distinctId, + "mixpanel anonymous distinct id should not be changed for each init") + + let userId: String = "u1" + testMixpanel.identify(distinctId: userId) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(devicePrefix + testMixpanel.anonymousId!, distinctId) + XCTAssertEqual(testMixpanel.userId, userId) + XCTAssertEqual(testMixpanel.distinctId, userId) + XCTAssertTrue(testMixpanel.hadPersistedDistinctId!) + removeDBfile(testMixpanel.apiToken) + } + + func testTrackWithDefaultProperties() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.track(event: "Something Happened") + waitForTrackingQueue(testMixpanel) + let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(e["event"] as? String, "Something Happened", "incorrect event name") + let p: InternalProperties = e["properties"] as! InternalProperties + XCTAssertNotNil(p["$app_build_number"], "$app_build_number not set") + XCTAssertNotNil(p["$app_version_string"], "$app_version_string not set") + XCTAssertNotNil(p["$lib_version"], "$lib_version not set") + XCTAssertNotNil(p["$model"], "$model not set") + XCTAssertNotNil(p["$os"], "$os not set") + XCTAssertNotNil(p["$os_version"], "$os_version not set") + XCTAssertNotNil(p["$screen_height"], "$screen_height not set") + XCTAssertNotNil(p["$screen_width"], "$screen_width not set") + XCTAssertNotNil(p["distinct_id"], "distinct_id not set") + XCTAssertNotNil(p["time"], "time not set") + XCTAssertEqual(p["$manufacturer"] as? String, "Apple", "incorrect $manufacturer") + XCTAssertEqual(p["mp_lib"] as? String, "swift", "incorrect mp_lib") + XCTAssertEqual(p["token"] as? String, testMixpanel.apiToken, "incorrect token") + removeDBfile(testMixpanel.apiToken) + } + + func testTrackWithCustomProperties() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let now = Date() + let p: Properties = [ + "string": "yello", + "number": 3, + "date": now, + "$app_version": "override", + ] + testMixpanel.track(event: "Something Happened", properties: p) + waitForTrackingQueue(testMixpanel) + let props: InternalProperties = + eventQueue(token: testMixpanel.apiToken).last?["properties"] as! InternalProperties + XCTAssertEqual(props["string"] as? String, "yello") + XCTAssertEqual(props["number"] as? Int, 3) + let dateValue = props["date"] as! String + compareDate(dateString: dateValue, dateDate: now) + XCTAssertEqual( + props["$app_version"] as? String, "override", + "reserved property override failed") + removeDBfile(testMixpanel.apiToken) + } + + func testTrackWithOptionalProperties() { + let optNil: Double? = nil + let optDouble: Double? = 1.0 + let optArray: [Double?] = [nil, 1.0, 2.0] + let optDict: [String: Double?] = ["nil": nil, "double": 1.0] + let nested: [String: Any] = ["list": optArray, "dict": optDict] + let p: Properties = [ + "nil": optNil, + "double": optDouble, + "list": optArray, + "dict": optDict, + "nested": nested, + ] + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.track(event: "Optional Test", properties: p) + waitForTrackingQueue(testMixpanel) + let props: InternalProperties = + eventQueue(token: testMixpanel.apiToken).last?["properties"] as! InternalProperties + XCTAssertNil(props["nil"] as? Double) + XCTAssertEqual(props["double"] as? Double, 1.0) + XCTAssertEqual(props["list"] as? Array, [1.0, 2.0]) + XCTAssertEqual(props["dict"] as? Dictionary, ["nil": nil, "double": 1.0]) + let nestedProp = props["nested"] as? [String: Any] + XCTAssertEqual(nestedProp?["dict"] as? Dictionary, ["nil": nil, "double": 1.0]) + XCTAssertEqual(nestedProp?["list"] as? Array, [1.0, 2.0]) + removeDBfile(testMixpanel.apiToken) + } + + func testTrackWithCustomDistinctIdAndToken() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let p: Properties = ["token": "t1", "distinct_id": "d1"] + testMixpanel.track(event: "e1", properties: p) + waitForTrackingQueue(testMixpanel) + let trackToken = + (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?[ + "token"] as? String + let trackDistinctId = + (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?[ + "distinct_id"] as? String + XCTAssertEqual(trackToken, "t1", "user-defined distinct id not used in track.") + XCTAssertEqual(trackDistinctId, "d1", "user-defined distinct id not used in track.") + removeDBfile(testMixpanel.apiToken) + } + + func testTrackWithGroups() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + testMixpanel.trackWithGroups( + event: "Something Happened", properties: [groupKey: "some other value", "p1": "value"], + groups: [groupKey: groupID]) + waitForTrackingQueue(testMixpanel) + let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(e["event"] as? String, "Something Happened", "incorrect event name") + let p: InternalProperties = e["properties"] as! InternalProperties + XCTAssertNotNil(p["$app_build_number"], "$app_build_number not set") + XCTAssertNotNil(p["$app_version_string"], "$app_version_string not set") + XCTAssertNotNil(p["$lib_version"], "$lib_version not set") + XCTAssertNotNil(p["$model"], "$model not set") + XCTAssertNotNil(p["$os"], "$os not set") + XCTAssertNotNil(p["$os_version"], "$os_version not set") + XCTAssertNotNil(p["$screen_height"], "$screen_height not set") + XCTAssertNotNil(p["$screen_width"], "$screen_width not set") + XCTAssertNotNil(p["distinct_id"], "distinct_id not set") + XCTAssertNotNil(p["time"], "time not set") + XCTAssertEqual(p["$manufacturer"] as? String, "Apple", "incorrect $manufacturer") + XCTAssertEqual(p["mp_lib"] as? String, "swift", "incorrect mp_lib") + XCTAssertEqual(p["token"] as? String, testMixpanel.apiToken, "incorrect token") + XCTAssertEqual(p[groupKey] as? String, groupID, "incorrect group id") + XCTAssertEqual(p["p1"] as? String, "value", "incorrect group value") + removeDBfile(testMixpanel.apiToken) + } + + func testRegisterSuperProperties() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + var p: Properties = ["p1": "a", "p2": 3, "p3": Date()] + testMixpanel.registerSuperProperties(p) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + NSDictionary(dictionary: testMixpanel.currentSuperProperties()), + NSDictionary(dictionary: p), + "register super properties failed") + p = ["p1": "b"] + testMixpanel.registerSuperProperties(p) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + testMixpanel.currentSuperProperties()["p1"] as? String, "b", + "register super properties failed to overwrite existing value") + p = ["p4": "a"] + testMixpanel.registerSuperPropertiesOnce(p) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + testMixpanel.currentSuperProperties()["p4"] as? String, "a", + "register super properties once failed first time") + p = ["p4": "b"] + testMixpanel.registerSuperPropertiesOnce(p) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + testMixpanel.currentSuperProperties()["p4"] as? String, "a", + "register super properties once failed second time") + p = ["p4": "c"] + testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "d") + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + testMixpanel.currentSuperProperties()["p4"] as? String, "a", + "register super properties once with default value failed when no match") + testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "a") + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + testMixpanel.currentSuperProperties()["p4"] as? String, "c", + "register super properties once with default value failed when match") + testMixpanel.unregisterSuperProperty("a") + waitForTrackingQueue(testMixpanel) + XCTAssertNil( + testMixpanel.currentSuperProperties()["a"], + "unregister super property failed") + // unregister non-existent super property should not throw + testMixpanel.unregisterSuperProperty("a") + testMixpanel.clearSuperProperties() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + testMixpanel.currentSuperProperties().isEmpty, + "clear super properties failed") + removeDBfile(testMixpanel.apiToken) + } + + func testSettingSuperPropertiesWhenInit() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60, + superProperties: ["mp_lib": "flutter"]) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + testMixpanel.currentSuperProperties()["mp_lib"] as? String, "flutter", + "register super properties in init failed") + testMixpanel.track(event: "e1") + waitForTrackingQueue(testMixpanel) + let e = eventQueue(token: testMixpanel.apiToken).last! + let p = e["properties"] as! InternalProperties + XCTAssertNotNil(p["mp_lib"], "flutter") + } + + func testInvalidPropertiesTrack() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let p: Properties = ["data": [Data()]] + XCTExpectAssert("property type should not be allowed") { + testMixpanel.track(event: "e1", properties: p) + } + removeDBfile(testMixpanel.apiToken) + } + + func testInvalidSuperProperties() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let p: Properties = ["data": [Data()]] + XCTExpectAssert("property type should not be allowed") { + testMixpanel.registerSuperProperties(p) + } + removeDBfile(testMixpanel.apiToken) + } + + func testInvalidSuperProperties2() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let p: Properties = ["data": [Data()]] + XCTExpectAssert("property type should not be allowed") { + testMixpanel.registerSuperPropertiesOnce(p) + } + removeDBfile(testMixpanel.apiToken) + } + + func testInvalidSuperProperties3() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let p: Properties = ["data": [Data()]] + XCTExpectAssert("property type should not be allowed") { + testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "v") + } + removeDBfile(testMixpanel.apiToken) + } + + func testValidPropertiesTrack() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let p: Properties = allPropertyTypes() + testMixpanel.track(event: "e1", properties: p) + removeDBfile(testMixpanel.apiToken) + } + + func testValidSuperProperties() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let p: Properties = allPropertyTypes() + testMixpanel.registerSuperProperties(p) + testMixpanel.registerSuperPropertiesOnce(p) + testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "v") + removeDBfile(testMixpanel.apiToken) + } + + func testReset() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.track(event: "e1") + waitForTrackingQueue(testMixpanel) + let p: Properties = ["p1": "a"] + testMixpanel.registerSuperProperties(p) + testMixpanel.people.set(properties: p) + testMixpanel.archive() + testMixpanel.reset() + waitForTrackingQueue(testMixpanel) + + #if MIXPANEL_UNIQUE_DISTINCT_ID + XCTAssertEqual( + testMixpanel.distinctId, + devicePrefix + testMixpanel.defaultDeviceId(), + "distinct id failed to reset") + #endif + XCTAssertNil(testMixpanel.people.distinctId, "people distinct id failed to reset") + XCTAssertTrue( + testMixpanel.currentSuperProperties().isEmpty, + "super properties failed to reset") + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "events queue failed to reset") + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "people queue failed to reset") + let testMixpanel2 = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + waitForTrackingQueue(testMixpanel2) + #if MIXPANEL_UNIQUE_DISTINCT_ID + XCTAssertEqual( + testMixpanel2.distinctId, devicePrefix + testMixpanel2.defaultDeviceId(), + "distinct id failed to reset after archive") + #endif + XCTAssertNil( + testMixpanel2.people.distinctId, + "people distinct id failed to reset after archive") + XCTAssertTrue( + testMixpanel2.currentSuperProperties().isEmpty, + "super properties failed to reset after archive") + XCTAssertTrue( + eventQueue(token: testMixpanel2.apiToken).count == 1, + "events queue failed to reset after archive") + XCTAssertTrue( + peopleQueue(token: testMixpanel2.apiToken).isEmpty, + "people queue failed to reset after archive") + removeDBfile(testMixpanel.apiToken) + removeDBfile(testMixpanel2.apiToken) + } + + func testArchiveNSNumberBoolIntProperty() { + let testToken = randomId() + let testMixpanel = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.serverURL = kFakeServerUrl + let aBoolNumber: Bool = true + let aBoolNSNumber = NSNumber(value: aBoolNumber) + + let aIntNumber: Int = 1 + let aIntNSNumber = NSNumber(value: aIntNumber) + + testMixpanel.track(event: "e1", properties: ["p1": aBoolNSNumber, "p2": aIntNSNumber]) + testMixpanel.archive() + waitForTrackingQueue(testMixpanel) + + let testMixpanel2 = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: true, flushInterval: 60) + testMixpanel2.serverURL = kFakeServerUrl + waitForTrackingQueue(testMixpanel2) + + let properties: [String: Any] = + eventQueue(token: testMixpanel2.apiToken)[1]["properties"] as! [String: Any] + + XCTAssertTrue( + isBoolNumber(num: properties["p1"]! as! NSNumber), + "The bool value should be unarchived as bool") + XCTAssertFalse( + isBoolNumber(num: properties["p2"]! as! NSNumber), + "The int value should not be unarchived as bool") + removeDBfile(testToken) + } + + private func isBoolNumber(num: NSNumber) -> Bool { + let boolID = CFBooleanGetTypeID() // the type ID of CFBoolean + let numID = CFGetTypeID(num) // the type ID of num + return numID == boolID + } + + func testArchive() { + let testToken = randomId() + let testMixpanel = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: false, flushInterval: 60) + testMixpanel.serverURL = kFakeServerUrl + #if MIXPANEL_UNIQUE_DISTINCT_ID + XCTAssertEqual( + testMixpanel.distinctId, devicePrefix + testMixpanel.defaultDeviceId(), + "default distinct id archive failed") + #endif + XCTAssertTrue( + testMixpanel.currentSuperProperties().isEmpty, + "default super properties archive failed") + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).isEmpty, "default events queue archive failed") + XCTAssertNil(testMixpanel.people.distinctId, "default people distinct id archive failed") + XCTAssertTrue( + peopleQueue(token: testMixpanel.apiToken).isEmpty, "default people queue archive failed") + let p: Properties = ["p1": "a"] + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + testMixpanel.registerSuperProperties(p) + testMixpanel.track(event: "e1") + testMixpanel.track(event: "e2") + testMixpanel.track(event: "e3") + testMixpanel.track(event: "e4") + testMixpanel.track(event: "e5") + testMixpanel.track(event: "e6") + testMixpanel.track(event: "e7") + testMixpanel.track(event: "e8") + testMixpanel.track(event: "e9") + testMixpanel.people.set(properties: p) + waitForTrackingQueue(testMixpanel) + testMixpanel.timedEvents["e2"] = 5 + testMixpanel.archive() + let testMixpanel2 = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: false, flushInterval: 60) + testMixpanel2.serverURL = kFakeServerUrl + waitForTrackingQueue(testMixpanel2) + XCTAssertEqual(testMixpanel2.distinctId, "d1", "custom distinct archive failed") + XCTAssertTrue( + testMixpanel2.currentSuperProperties().count == 1, + "custom super properties archive failed") + let eventQueueValue = eventQueue(token: testMixpanel2.apiToken) + + XCTAssertEqual( + eventQueueValue[1]["event"] as? String, "e1", + "event was not successfully archived/unarchived") + XCTAssertEqual( + eventQueueValue[2]["event"] as? String, "e2", + "event was not successfully archived/unarchived or order is incorrect") + XCTAssertEqual( + eventQueueValue[3]["event"] as? String, "e3", + "event was not successfully archived/unarchived or order is incorrect") + XCTAssertEqual( + eventQueueValue[4]["event"] as? String, "e4", + "event was not successfully archived/unarchived or order is incorrect") + XCTAssertEqual( + eventQueueValue[5]["event"] as? String, "e5", + "event was not successfully archived/unarchived or order is incorrect") + XCTAssertEqual( + eventQueueValue[6]["event"] as? String, "e6", + "event was not successfully archived/unarchived or order is incorrect") + XCTAssertEqual( + eventQueueValue[7]["event"] as? String, "e7", + "event was not successfully archived/unarchived or order is incorrect") + XCTAssertEqual( + eventQueueValue[8]["event"] as? String, "e8", + "event was not successfully archived/unarchived or order is incorrect") + XCTAssertEqual( + eventQueueValue[9]["event"] as? String, "e9", + "event was not successfully archived/unarchived or order is incorrect") + XCTAssertEqual( + testMixpanel2.people.distinctId, "d1", + "custom people distinct id archive failed") + XCTAssertTrue( + peopleQueue(token: testMixpanel2.apiToken).count >= 1, "pending people queue archive failed") + XCTAssertEqual( + testMixpanel2.timedEvents["e2"] as? Int, 5, + "timedEvents archive failed") + let testMixpanel3 = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: false, flushInterval: 60) + testMixpanel3.serverURL = kFakeServerUrl + XCTAssertEqual(testMixpanel3.distinctId, "d1", "expecting d1 as distinct id as initialised") + XCTAssertTrue( + testMixpanel3.currentSuperProperties().count == 1, + "default super properties expected to have 1 item") + XCTAssertNotNil(eventQueue(token: testMixpanel3.apiToken), "default events queue is nil") + XCTAssertTrue( + eventQueue(token: testMixpanel3.apiToken).count == 10, + "default events queue expecting 10 items ($identify call added)") + XCTAssertNotNil( + testMixpanel3.people.distinctId, + "default people distinct id from no file failed") + XCTAssertNotNil( + peopleQueue(token: testMixpanel3.apiToken), "default people queue from no file is nil") + XCTAssertTrue( + peopleQueue(token: testMixpanel3.apiToken).count >= 1, "default people queue expecting 1 item" + ) + XCTAssertTrue(testMixpanel3.timedEvents.count == 1, "timedEvents expecting 1 item") + removeDBfile(testToken) + } + + func testMixpanelDelegate() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.delegate = self + testMixpanel.identify(distinctId: "d1") + testMixpanel.track(event: "e1") + testMixpanel.people.set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 3, "delegate should have stopped flush") + XCTAssertTrue( + peopleQueue(token: testMixpanel.apiToken).count == 2, "delegate should have stopped flush") + removeDBfile(testMixpanel.apiToken) + } + + func testEventTiming() { + 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.") + testMixpanel.time(event: "400 Meters") + testMixpanel.track(event: "500 Meters") + waitForTrackingQueue(testMixpanel) + e = eventQueue(token: testMixpanel.apiToken).last! + p = e["properties"] as! InternalProperties + XCTAssertNil(p["$duration"], "The exact same event name is required for timing.") + testMixpanel.track(event: "400 Meters") + waitForTrackingQueue(testMixpanel) + e = eventQueue(token: testMixpanel.apiToken).last! + p = e["properties"] as! InternalProperties + XCTAssertNotNil(p["$duration"], "This event should be timed.") + testMixpanel.track(event: "400 Meters") + 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.") + testMixpanel.time(event: "Time Event A") + testMixpanel.time(event: "Time Event B") + testMixpanel.time(event: "Time Event C") + waitForTrackingQueue(testMixpanel) + 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(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(instanceName: testMixpanel.apiToken).count == 0, + "clearTimedEvents should remove all key/value pairs") + removeDBfile(testMixpanel.apiToken) + } + + func testReadWriteLock() { + var array = [Int]() + let lock = ReadWriteLock(label: "test") + let queue = DispatchQueue(label: "concurrent", qos: .utility, attributes: .concurrent) + for _ in 0..<10 { + queue.async { + lock.write { + for i in 0..<100 { + array.append(i) + } } - removeDBfile(testMixpanel.apiToken) + } + + queue.async { + lock.read { + XCTAssertTrue(array.count % 100 == 0, "supposed to happen after write") + } + } + } + } + + func testSetGroup() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + let groupKey = "test_key" + let groupValue = "test_value" + testMixpanel.setGroup(groupKey: groupKey, groupIDs: [groupValue]) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties + XCTAssertEqual(q[groupKey] as? [String], [groupValue], "group value people property not queued") + assertDefaultPeopleProperties(q) + removeDBfile(testMixpanel.apiToken) + } + + func testAddGroup() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + let groupKey = "test_key" + let groupValue = "test_value" + + testMixpanel.addGroup(groupKey: groupKey, groupID: groupValue) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) + waitForTrackingQueue(testMixpanel) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties + XCTAssertEqual(q[groupKey] as? [String], [groupValue], "addGroup people update not queued") + assertDefaultPeopleProperties(q) + testMixpanel.addGroup(groupKey: groupKey, groupID: groupValue) + + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) + waitForTrackingQueue(testMixpanel) + let q2 = peopleQueue(token: testMixpanel.apiToken).last!["$union"] as! InternalProperties + XCTAssertEqual(q2[groupKey] as? [String], [groupValue], "addGroup people update not queued") + + let newVal = "new_group" + testMixpanel.addGroup(groupKey: groupKey, groupID: newVal) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue, newVal]) + waitForTrackingQueue(testMixpanel) + waitForTrackingQueue(testMixpanel) + let q3 = peopleQueue(token: testMixpanel.apiToken).last!["$union"] as! InternalProperties + XCTAssertEqual(q3[groupKey] as? [String], [newVal], "addGroup people update not queued") + removeDBfile(testMixpanel.apiToken) + } + + func testRemoveGroup() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + let groupKey = "test_key" + let groupValue = "test_value" + let newVal = "new_group" + + testMixpanel.setGroup(groupKey: groupKey, groupIDs: [groupValue, newVal]) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual( + testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue, newVal]) + + testMixpanel.removeGroup(groupKey: groupKey, groupID: groupValue) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [newVal]) + waitForTrackingQueue(testMixpanel) + let q2 = peopleQueue(token: testMixpanel.apiToken).last!["$remove"] as! InternalProperties + XCTAssertEqual(q2[groupKey] as? String, groupValue, "removeGroup people update not queued") + + testMixpanel.removeGroup(groupKey: groupKey, groupID: groupValue) + waitForTrackingQueue(testMixpanel) + XCTAssertNil(testMixpanel.currentSuperProperties()[groupKey]) + waitForTrackingQueue(testMixpanel) + let q3 = peopleQueue(token: testMixpanel.apiToken).last!["$unset"] as! [String] + XCTAssertEqual(q3, [groupKey], "removeGroup people update not queued") + removeDBfile(testMixpanel.apiToken) + } + + func testMultipleInstancesWithSameToken() { + let testToken = randomId() + let concurentQueue = DispatchQueue(label: "multithread", attributes: .concurrent) + + var testMixpanel: MixpanelInstance? + for _ in 1...10 { + concurentQueue.async { + testMixpanel = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: true, flushInterval: 60) + testMixpanel?.loggingEnabled = true + testMixpanel?.track(event: "test") + } } - func testAddEventContainsInvalidJsonObjectFloatInfinity() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.track(event: "bad event", properties: ["BadProp": Float.infinity]) - } - removeDBfile(testMixpanel.apiToken) + var testMixpanel2: MixpanelInstance? + for _ in 1...10 { + concurentQueue.async { + testMixpanel2 = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: true, flushInterval: 60) + testMixpanel2?.loggingEnabled = true + testMixpanel2?.track(event: "test") + } + } + sleep(5) + testMixpanel = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: true, flushInterval: 60) + testMixpanel2 = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: true, flushInterval: 60) + XCTAssertTrue( + testMixpanel === testMixpanel2, + "instance with same token should be reused and no sqlite db locked error should be populated") + } + + func testMultipleInstancesWithSameTokenButDifferentInstanceNameShouldNotCrash() { + let testToken = randomId() + let concurentQueue = DispatchQueue(label: "multithread", attributes: .concurrent) + + for i in 1...10 { + concurentQueue.async { + let testMixpanel = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: true, flushInterval: 60, + instanceName: "instance\(i)") + testMixpanel.loggingEnabled = true + testMixpanel.track(event: "test") + } } - func testAddingEventsAfterFlush() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - for i in 0..<10 { - testMixpanel.track(event: "event \(UInt(i))") - } - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 11, "11 events should be queued up") - flushAndWaitForTrackingQueue(testMixpanel) - for i in 0..<5 { - testMixpanel.track(event: "event \(UInt(i))") - } - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 5, "5 more events should be queued up") - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "events should have been flushed") - removeDBfile(testMixpanel.apiToken) + sleep(5) + XCTAssertTrue(true, "no sqlite db locked error should be populated") + for j in 1...10 { + removeDBfile("instance\(j)") } + } - // Mock implementation of MixpanelFlags to track loadFlags calls - class MockMixpanelFlags: MixpanelFlags { - var delegate: MixpanelFlagDelegate? - var loadFlagsCallCount = 0 - - func loadFlags() { - loadFlagsCallCount += 1 - } - - func areFlagsReady() -> Bool { - return true - } - - func getVariantSync(_ flagName: String, fallback: MixpanelFlagVariant) -> MixpanelFlagVariant { - return fallback - } - - func getVariant(_ flagName: String, fallback: MixpanelFlagVariant, completion: @escaping (MixpanelFlagVariant) -> Void) { - completion(fallback) - } - - func getVariantValueSync(_ flagName: String, fallbackValue: Any?) -> Any? { - return fallbackValue - } - - func getVariantValue(_ flagName: String, fallbackValue: Any?, completion: @escaping (Any?) -> Void) { - completion(fallbackValue) - } - - func isEnabledSync(_ flagName: String, fallbackValue: Bool) -> Bool { - return fallbackValue - } - - func isEnabled(_ flagName: String, fallbackValue: Bool, completion: @escaping (Bool) -> Void) { - completion(fallbackValue) - } - } - - func testIdentify() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - - // Inject our mock flags object - let mockFlags = MockMixpanelFlags() - testMixpanel.flags = mockFlags - - for _ in 0..<2 { - // run this twice to test reset works correctly wrt to distinct ids - let distinctId: String = "d1" - // try this for ODIN and nil - #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(testMixpanel.distinctId, - devicePrefix + testMixpanel.defaultDeviceId(), - "mixpanel identify failed to set default distinct id") - XCTAssertEqual(testMixpanel.anonymousId, - testMixpanel.defaultDeviceId(), - "mixpanel failed to set default anonymous id") - #endif - XCTAssertNil(testMixpanel.people.distinctId, - "mixpanel people distinct id should default to nil") - XCTAssertNil(testMixpanel.people.distinctId, - "mixpanel user id should default to nil") - testMixpanel.track(event: "e1") - waitForTrackingQueue(testMixpanel) - let eventsQueue = eventQueue(token: testMixpanel.apiToken) - XCTAssertTrue(eventsQueue.count == 2 || eventsQueue.count == 1, // first app open should not be tracked for the second run, - "events should be sent right away with default distinct id") - #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual((eventsQueue.last?["properties"] as? InternalProperties)?["distinct_id"] as? String, - devicePrefix + mixpanel.defaultDeviceId(), - "events should use default distinct id if none set") - #endif - XCTAssertEqual((eventsQueue.last?["properties"] as? InternalProperties)?["$lib_version"] as? String, - AutomaticProperties.libVersion(), - "events should has lib version in internal properties") - testMixpanel.people.set(property: "p1", to: "a") - waitForTrackingQueue(testMixpanel) - var peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) - var unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) - XCTAssertTrue(peopleQueue_value.isEmpty, - "people records should go to unidentified queue before identify:") - XCTAssertTrue(unidentifiedQueue.count == 2 || eventsQueue.count == 1, // first app open should not be tracked for the second run, - "unidentified people records not queued") - XCTAssertEqual(unidentifiedQueue.last?["$token"] as? String, - testMixpanel.apiToken, - "incorrect project token in people record") - // Record the loadFlags call count before identify - let loadFlagsCallCountBefore = mockFlags.loadFlagsCallCount - - testMixpanel.identify(distinctId: distinctId) - waitForTrackingQueue(testMixpanel) - - // Assert that loadFlags was called when distinctId changed - XCTAssertEqual(mockFlags.loadFlagsCallCount, loadFlagsCallCountBefore + 1, - "loadFlags should be called when distinctId changes during identify") - - let anonymousId = testMixpanel.anonymousId - peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) - unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) - XCTAssertEqual(peopleQueue_value.last?["$distinct_id"] as? String, - distinctId, "distinct id not set properly on unidentified people record") - XCTAssertEqual(testMixpanel.distinctId, distinctId, - "mixpanel identify failed to set distinct id") - XCTAssertEqual(testMixpanel.userId, distinctId, - "mixpanel identify failed to set user id") - XCTAssertEqual(testMixpanel.anonymousId, anonymousId, - "mixpanel identify shouldn't change anonymousId") - XCTAssertEqual(testMixpanel.people.distinctId, distinctId, - "mixpanel identify failed to set people distinct id") - XCTAssertTrue(unidentifiedQueue.isEmpty, - "identify: should move records from unidentified queue") - XCTAssertTrue(peopleQueue_value.count > 0, - "identify: should move records to main people queue") - XCTAssertEqual(peopleQueue_value.last?["$token"] as? String, - testMixpanel.apiToken, "incorrect project token in people record") - let p: InternalProperties = peopleQueue_value.last?["$set"] as! InternalProperties - XCTAssertEqual(p["p1"] as? String, "a", "custom people property not queued") - assertDefaultPeopleProperties(p) - peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + func testMultipleInstancesWithSameTokenButDifferentInstanceName() { + let testToken = randomId() + let instanceName1 = randomId() + let instanceName2 = randomId() + let instance1 = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: true, flushInterval: 60, instanceName: instanceName1) + let instance2 = Mixpanel.initialize( + token: testToken, trackAutomaticEvents: true, flushInterval: 60, instanceName: instanceName2) - testMixpanel.people.set(property: "p1", to: "a") - waitForTrackingQueue(testMixpanel) + XCTAssertNotEqual(instance1.distinctId, instance2.distinctId) + instance1.identify(distinctId: "user1") + instance1.track(event: "test") + waitForTrackingQueue(instance1) + waitForTrackingQueue(instance2) - peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) - unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) - XCTAssertEqual(peopleQueue_value.last?["$distinct_id"] as? String, - distinctId, "distinct id not set properly on unidentified people record") - XCTAssertTrue(unidentifiedQueue.isEmpty, - "once idenitfy: is called, unidentified queue should be skipped") - XCTAssertTrue(peopleQueue_value.count > 0 , - "once identify: is called, records should go straight to main queue") - testMixpanel.track(event: "e2") - waitForTrackingQueue(testMixpanel) - let newDistinctId = (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?["distinct_id"] as? String - XCTAssertEqual(newDistinctId, distinctId, - "events should use new distinct id after identify:") - - // Test that calling identify with the same distinctId does NOT trigger loadFlags - let loadFlagsCountBeforeSameId = mockFlags.loadFlagsCallCount - testMixpanel.identify(distinctId: distinctId) // Same distinctId - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(mockFlags.loadFlagsCallCount, loadFlagsCountBeforeSameId, - "loadFlags should NOT be called when distinctId doesn't change") - - testMixpanel.reset() - waitForTrackingQueue(testMixpanel) - } - removeDBfile(testMixpanel.apiToken) - } + XCTAssertEqual(instance1.distinctId, "user1") + XCTAssertEqual(instance1.userId, "user1") + let events = eventQueue(token: instanceName1) + let properties = events.last?["properties"] as? InternalProperties + XCTAssertEqual(properties?["distinct_id"] as? String, "user1") - func testIdentifyTrack() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let distinctIdBeforeIdentify: String? = testMixpanel.distinctId - let distinctId = "testIdentifyTrack" + instance1.people.set(property: "p1", to: "a") + waitForTrackingQueue(instance1) + waitForTrackingQueue(instance2) - testMixpanel.identify(distinctId: distinctId) - waitForTrackingQueue(testMixpanel) - let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(e["event"] as? String, "$identify", "incorrect event name") - let p: InternalProperties = e["properties"] as! InternalProperties - XCTAssertEqual(p["distinct_id"] as? String, distinctId, "wrong distinct_id") - XCTAssertEqual(p["$anon_distinct_id"] as? String, distinctIdBeforeIdentify, "wrong $anon_distinct_id") - removeDBfile(testMixpanel.apiToken) - } + let peopleQueue_value = peopleQueue(token: instanceName1) + let setValue = peopleQueue_value.last!["$set"] as! InternalProperties + XCTAssertEqual(setValue["p1"] as? String, "a", "custom people property not queued") - func testIdentifyResetTrack() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let originalDistinctId: String? = testMixpanel.distinctId - let distinctId = "testIdentifyTrack" + XCTAssertEqual( + peopleQueue_value.last?["$distinct_id"] as? String, + "user1", "distinct id not set properly on the people record") + + instance2.identify(distinctId: "user2") + instance2.track(event: "test2") + waitForTrackingQueue(instance1) + waitForTrackingQueue(instance2) + XCTAssertEqual(instance2.distinctId, "user2") + XCTAssertEqual(instance2.userId, "user2") + let events2 = eventQueue(token: instanceName2) + let properties2 = events2.last?["properties"] as? InternalProperties + // event property should have the current distinct id + XCTAssertEqual(properties2?["distinct_id"] as? String, "user2") + + instance2.people.set(property: "p2", to: "b") + waitForTrackingQueue(instance1) + waitForTrackingQueue(instance2) + + let peopleQueue2_value = peopleQueue(token: instanceName2) + XCTAssertEqual( + peopleQueue2_value.last?["$distinct_id"] as? String, + "user2", "distinct id not set properly on the people record") + + let setValue2 = peopleQueue2_value.last!["$set"] as! InternalProperties + XCTAssertEqual(setValue2["p2"] as? String, "b", "custom people property not queued") + + removeDBfile(instanceName1) + removeDBfile(instanceName2) + } + + func testReadWriteMultiThreadShouldNotCrash() { + let concurentQueue = DispatchQueue(label: "multithread", attributes: .concurrent) + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + + for n in 1...10 { + concurentQueue.async { + testMixpanel.track(event: "event\(n)") + } + concurentQueue.async { + testMixpanel.flush() + } + concurentQueue.async { + testMixpanel.archive() + } + concurentQueue.async { testMixpanel.reset() - waitForTrackingQueue(testMixpanel) - - for i in 1...3 { - let prevDistinctId: String? = testMixpanel.distinctId - let newDistinctId = distinctId + String(i) - testMixpanel.identify(distinctId: newDistinctId) - waitForTrackingQueue(testMixpanel) - - let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(e["event"] as? String, "$identify", "incorrect event name") - let p: InternalProperties = e["properties"] as! InternalProperties - XCTAssertEqual(p["distinct_id"] as? String, newDistinctId, "wrong distinct_id") - XCTAssertEqual(p["$anon_distinct_id"] as? String, prevDistinctId, "wrong $anon_distinct_id") - XCTAssertNotEqual(prevDistinctId, originalDistinctId, "After reset, UUID will be used - never the same"); - #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(prevDistinctId, originalDistinctId, "After reset, IFV will be used - always the same"); - #endif - testMixpanel.reset() - waitForTrackingQueue(testMixpanel) - } - removeDBfile(testMixpanel.apiToken) - } - - func testCreateAlias() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.people.set(properties: ["p1": "a"]) - waitForTrackingQueue(testMixpanel) - var unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) - // the user profile update has been queued up in unidentifiedQueue until identify is called - XCTAssertTrue(!unidentifiedQueue.isEmpty) - - let distinctId = testMixpanel.distinctId - let alias: String = "a1" - testMixpanel.createAlias(alias, distinctId: testMixpanel.distinctId) - waitForTrackingQueue(testMixpanel) - - let mixpanelIdentity = MixpanelPersistence.loadIdentity(instanceName: testMixpanel.apiToken) - XCTAssertTrue(distinctId == mixpanelIdentity.distinctID && distinctId == mixpanelIdentity.peopleDistinctID && distinctId == mixpanelIdentity.userId && alias == mixpanelIdentity.alias) - removeDBfile(testMixpanel.apiToken) - unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) - // unidentifiedQueue has been flushed - XCTAssertTrue(unidentifiedQueue.isEmpty) - - let testMixpanel2 = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel2.people.set(properties: ["p1": "a"]) - waitForTrackingQueue(testMixpanel2) - - let distinctId2 = testMixpanel2.distinctId - testMixpanel2.createAlias(alias, distinctId: testMixpanel.distinctId, andIdentify: false) - waitForTrackingQueue(testMixpanel2) - - let unidentifiedQueue2 = unIdentifiedPeopleQueue(token: testMixpanel2.apiToken) - // The user profile updates should still be held in unidentifiedQueue cause no identify is called - XCTAssertTrue(!unidentifiedQueue2.isEmpty) - let mixpanelIdentity2 = MixpanelPersistence.loadIdentity(instanceName: testMixpanel2.apiToken) - XCTAssertTrue(distinctId2 == mixpanelIdentity2.distinctID && nil == mixpanelIdentity2.peopleDistinctID && nil == mixpanelIdentity2.userId && alias == mixpanelIdentity2.alias) - removeDBfile(testMixpanel2.apiToken) - } - - func testPersistentIdentity() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let distinctId: String = "d1" - let alias: String = "a1" - testMixpanel.identify(distinctId: distinctId) - waitForTrackingQueue(testMixpanel) - testMixpanel.createAlias(alias, distinctId: testMixpanel.distinctId) - waitForTrackingQueue(testMixpanel) - var mixpanelIdentity = MixpanelPersistence.loadIdentity(instanceName: testMixpanel.apiToken) - XCTAssertTrue(distinctId == mixpanelIdentity.distinctID && distinctId == mixpanelIdentity.peopleDistinctID && distinctId == mixpanelIdentity.userId && alias == mixpanelIdentity.alias) - testMixpanel.archive() - waitForTrackingQueue(testMixpanel) - testMixpanel.unarchive() - waitForTrackingQueue(testMixpanel) - 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(instanceName: testMixpanel.apiToken) - waitForTrackingQueue(testMixpanel) - mixpanelIdentity = MixpanelPersistence.loadIdentity(instanceName: testMixpanel.apiToken) - XCTAssertTrue("" == mixpanelIdentity.distinctID && nil == mixpanelIdentity.peopleDistinctID && nil == mixpanelIdentity.anonymousId && nil == mixpanelIdentity.userId && nil == mixpanelIdentity.alias) - removeDBfile(testMixpanel.apiToken) - } - - func testUseUniqueDistinctId() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let testMixpanel2 = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - XCTAssertNotEqual(testMixpanel.distinctId, testMixpanel2.distinctId, "by default, distinctId should not be unique to the device") - - let testMixpanel3 = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60, useUniqueDistinctId: false) - let testMixpanel4 = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60, useUniqueDistinctId: false) - XCTAssertNotEqual(testMixpanel3.distinctId, testMixpanel4.distinctId, "distinctId should not be unique to the device if useUniqueDistinctId is set to false") - - let testMixpanel5 = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60, useUniqueDistinctId: true) - let testMixpanel6 = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60, useUniqueDistinctId: true) - XCTAssertEqual(testMixpanel5.distinctId, testMixpanel6.distinctId, "distinctId should be unique to the device if useUniqueDistinctId is set to true") - } - - func testHadPersistedDistinctId() { - let testToken = randomId() - let testMixpanel = Mixpanel.initialize(token: testToken, trackAutomaticEvents: true, flushInterval: 60) - XCTAssertNotNil(testMixpanel.distinctId) - let distinctId = testMixpanel.distinctId - let testMixpanel2 = Mixpanel.initialize(token: testToken, trackAutomaticEvents: true, flushInterval: 60) - XCTAssertEqual(testMixpanel2.distinctId, distinctId, "mixpanel anonymous distinct id should not be changed for each init") - - let userId: String = "u1" - testMixpanel.identify(distinctId: userId) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(devicePrefix + testMixpanel.anonymousId!, distinctId) - XCTAssertEqual(testMixpanel.userId, userId) - XCTAssertEqual(testMixpanel.distinctId, userId) - XCTAssertTrue(testMixpanel.hadPersistedDistinctId!) - removeDBfile(testMixpanel.apiToken) - } - - func testTrackWithDefaultProperties() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.track(event: "Something Happened") - waitForTrackingQueue(testMixpanel) - let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(e["event"] as? String, "Something Happened", "incorrect event name") - let p: InternalProperties = e["properties"] as! InternalProperties - XCTAssertNotNil(p["$app_build_number"], "$app_build_number not set") - XCTAssertNotNil(p["$app_version_string"], "$app_version_string not set") - XCTAssertNotNil(p["$lib_version"], "$lib_version not set") - XCTAssertNotNil(p["$model"], "$model not set") - XCTAssertNotNil(p["$os"], "$os not set") - XCTAssertNotNil(p["$os_version"], "$os_version not set") - XCTAssertNotNil(p["$screen_height"], "$screen_height not set") - XCTAssertNotNil(p["$screen_width"], "$screen_width not set") - XCTAssertNotNil(p["distinct_id"], "distinct_id not set") - XCTAssertNotNil(p["time"], "time not set") - XCTAssertEqual(p["$manufacturer"] as? String, "Apple", "incorrect $manufacturer") - XCTAssertEqual(p["mp_lib"] as? String, "swift", "incorrect mp_lib") - XCTAssertEqual(p["token"] as? String, testMixpanel.apiToken, "incorrect token") - removeDBfile(testMixpanel.apiToken) - } - - func testTrackWithCustomProperties() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let now = Date() - let p: Properties = ["string": "yello", - "number": 3, - "date": now, - "$app_version": "override"] - testMixpanel.track(event: "Something Happened", properties: p) - waitForTrackingQueue(testMixpanel) - let props: InternalProperties = eventQueue(token: testMixpanel.apiToken).last?["properties"] as! InternalProperties - XCTAssertEqual(props["string"] as? String, "yello") - XCTAssertEqual(props["number"] as? Int, 3) - let dateValue = props["date"] as! String - compareDate(dateString: dateValue, dateDate: now) - XCTAssertEqual(props["$app_version"] as? String, "override", - "reserved property override failed") - removeDBfile(testMixpanel.apiToken) - } - - func testTrackWithOptionalProperties() { - let optNil: Double? = nil - let optDouble: Double? = 1.0 - let optArray: Array = [nil, 1.0, 2.0] - let optDict: Dictionary = ["nil": nil, "double": 1.0] - let nested: Dictionary = ["list": optArray, "dict": optDict] - let p: Properties = ["nil": optNil, - "double": optDouble, - "list": optArray, - "dict": optDict, - "nested": nested, - ] - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.track(event: "Optional Test", properties: p) - waitForTrackingQueue(testMixpanel) - let props: InternalProperties = eventQueue(token: testMixpanel.apiToken).last?["properties"] as! InternalProperties - XCTAssertNil(props["nil"] as? Double) - XCTAssertEqual(props["double"] as? Double, 1.0) - XCTAssertEqual(props["list"] as? Array, [1.0, 2.0]) - XCTAssertEqual(props["dict"] as? Dictionary, ["nil": nil, "double": 1.0]) - let nestedProp = props["nested"] as? Dictionary - XCTAssertEqual(nestedProp?["dict"] as? Dictionary, ["nil": nil, "double": 1.0]) - XCTAssertEqual(nestedProp?["list"] as? Array, [1.0, 2.0]) - removeDBfile(testMixpanel.apiToken) - } - - func testTrackWithCustomDistinctIdAndToken() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let p: Properties = ["token": "t1", "distinct_id": "d1"] - testMixpanel.track(event: "e1", properties: p) - waitForTrackingQueue(testMixpanel) - let trackToken = (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?["token"] as? String - let trackDistinctId = (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?["distinct_id"] as? String - XCTAssertEqual(trackToken, "t1", "user-defined distinct id not used in track.") - XCTAssertEqual(trackDistinctId, "d1", "user-defined distinct id not used in track.") - removeDBfile(testMixpanel.apiToken) - } - - func testTrackWithGroups() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - testMixpanel.trackWithGroups(event: "Something Happened", properties: [groupKey: "some other value", "p1": "value"], groups: [groupKey: groupID]) - waitForTrackingQueue(testMixpanel) - let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(e["event"] as? String, "Something Happened", "incorrect event name") - let p: InternalProperties = e["properties"] as! InternalProperties - XCTAssertNotNil(p["$app_build_number"], "$app_build_number not set") - XCTAssertNotNil(p["$app_version_string"], "$app_version_string not set") - XCTAssertNotNil(p["$lib_version"], "$lib_version not set") - XCTAssertNotNil(p["$model"], "$model not set") - XCTAssertNotNil(p["$os"], "$os not set") - XCTAssertNotNil(p["$os_version"], "$os_version not set") - XCTAssertNotNil(p["$screen_height"], "$screen_height not set") - XCTAssertNotNil(p["$screen_width"], "$screen_width not set") - XCTAssertNotNil(p["distinct_id"], "distinct_id not set") - XCTAssertNotNil(p["time"], "time not set") - XCTAssertEqual(p["$manufacturer"] as? String, "Apple", "incorrect $manufacturer") - XCTAssertEqual(p["mp_lib"] as? String, "swift", "incorrect mp_lib") - XCTAssertEqual(p["token"] as? String, testMixpanel.apiToken, "incorrect token") - XCTAssertEqual(p[groupKey] as? String, groupID, "incorrect group id") - XCTAssertEqual(p["p1"] as? String, "value", "incorrect group value") - removeDBfile(testMixpanel.apiToken) - } - - func testRegisterSuperProperties() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - var p: Properties = ["p1": "a", "p2": 3, "p3": Date()] - testMixpanel.registerSuperProperties(p) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(NSDictionary(dictionary: testMixpanel.currentSuperProperties()), - NSDictionary(dictionary: p), - "register super properties failed") - p = ["p1": "b"] - testMixpanel.registerSuperProperties(p) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()["p1"] as? String, "b", - "register super properties failed to overwrite existing value") - p = ["p4": "a"] - testMixpanel.registerSuperPropertiesOnce(p) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()["p4"] as? String, "a", - "register super properties once failed first time") - p = ["p4": "b"] - testMixpanel.registerSuperPropertiesOnce(p) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()["p4"] as? String, "a", - "register super properties once failed second time") - p = ["p4": "c"] - testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "d") - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()["p4"] as? String, "a", - "register super properties once with default value failed when no match") - testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "a") - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()["p4"] as? String, "c", - "register super properties once with default value failed when match") - testMixpanel.unregisterSuperProperty("a") - waitForTrackingQueue(testMixpanel) - XCTAssertNil(testMixpanel.currentSuperProperties()["a"], - "unregister super property failed") - // unregister non-existent super property should not throw - testMixpanel.unregisterSuperProperty("a") - testMixpanel.clearSuperProperties() - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(testMixpanel.currentSuperProperties().isEmpty, - "clear super properties failed") - removeDBfile(testMixpanel.apiToken) - } - - func testSettingSuperPropertiesWhenInit() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60, superProperties: ["mp_lib": "flutter"]) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()["mp_lib"] as? String, "flutter", - "register super properties in init failed") - testMixpanel.track(event: "e1") - waitForTrackingQueue(testMixpanel) - let e = eventQueue(token: testMixpanel.apiToken).last! - let p = e["properties"] as! InternalProperties - XCTAssertNotNil(p["mp_lib"], "flutter") - } - - func testInvalidPropertiesTrack() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let p: Properties = ["data": [Data()]] - XCTExpectAssert("property type should not be allowed") { - testMixpanel.track(event: "e1", properties: p) - } - removeDBfile(testMixpanel.apiToken) - } - - func testInvalidSuperProperties() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let p: Properties = ["data": [Data()]] - XCTExpectAssert("property type should not be allowed") { - testMixpanel.registerSuperProperties(p) - } - removeDBfile(testMixpanel.apiToken) - } - - func testInvalidSuperProperties2() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let p: Properties = ["data": [Data()]] - XCTExpectAssert("property type should not be allowed") { - testMixpanel.registerSuperPropertiesOnce(p) - } - removeDBfile(testMixpanel.apiToken) - } - - func testInvalidSuperProperties3() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let p: Properties = ["data": [Data()]] - XCTExpectAssert("property type should not be allowed") { - testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "v") - } - removeDBfile(testMixpanel.apiToken) - } - - func testValidPropertiesTrack() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let p: Properties = allPropertyTypes() - testMixpanel.track(event: "e1", properties: p) - removeDBfile(testMixpanel.apiToken) - } - - func testValidSuperProperties() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let p: Properties = allPropertyTypes() - testMixpanel.registerSuperProperties(p) - testMixpanel.registerSuperPropertiesOnce(p) - testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "v") - removeDBfile(testMixpanel.apiToken) - } - - func testReset() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - testMixpanel.track(event: "e1") - waitForTrackingQueue(testMixpanel) - let p: Properties = ["p1": "a"] - testMixpanel.registerSuperProperties(p) - testMixpanel.people.set(properties: p) - testMixpanel.archive() - testMixpanel.reset() - waitForTrackingQueue(testMixpanel) - - #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(testMixpanel.distinctId, - devicePrefix + testMixpanel.defaultDeviceId(), - "distinct id failed to reset") - #endif - XCTAssertNil(testMixpanel.people.distinctId, "people distinct id failed to reset") - XCTAssertTrue(testMixpanel.currentSuperProperties().isEmpty, - "super properties failed to reset") - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "events queue failed to reset") - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "people queue failed to reset") - let testMixpanel2 = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - waitForTrackingQueue(testMixpanel2) - #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(testMixpanel2.distinctId, devicePrefix + testMixpanel2.defaultDeviceId(), - "distinct id failed to reset after archive") - #endif - XCTAssertNil(testMixpanel2.people.distinctId, - "people distinct id failed to reset after archive") - XCTAssertTrue(testMixpanel2.currentSuperProperties().isEmpty, - "super properties failed to reset after archive") - XCTAssertTrue(eventQueue(token: testMixpanel2.apiToken).count == 1, - "events queue failed to reset after archive") - XCTAssertTrue(peopleQueue(token: testMixpanel2.apiToken).isEmpty, - "people queue failed to reset after archive") - removeDBfile(testMixpanel.apiToken) - removeDBfile(testMixpanel2.apiToken) - } - - func testArchiveNSNumberBoolIntProperty() { - let testToken = randomId() - let testMixpanel = Mixpanel.initialize(token: testToken, trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.serverURL = kFakeServerUrl - let aBoolNumber: Bool = true - let aBoolNSNumber = NSNumber(value: aBoolNumber) - - let aIntNumber: Int = 1 - let aIntNSNumber = NSNumber(value: aIntNumber) - - testMixpanel.track(event: "e1", properties: ["p1": aBoolNSNumber, "p2": aIntNSNumber]) - testMixpanel.archive() - waitForTrackingQueue(testMixpanel) - - let testMixpanel2 = Mixpanel.initialize(token: testToken, trackAutomaticEvents: true, flushInterval: 60) - testMixpanel2.serverURL = kFakeServerUrl - waitForTrackingQueue(testMixpanel2) - - let properties: [String: Any] = eventQueue(token: testMixpanel2.apiToken)[1]["properties"] as! [String: Any] - - XCTAssertTrue(isBoolNumber(num: properties["p1"]! as! NSNumber), - "The bool value should be unarchived as bool") - XCTAssertFalse(isBoolNumber(num: properties["p2"]! as! NSNumber), - "The int value should not be unarchived as bool") - removeDBfile(testToken) - } - - private func isBoolNumber(num: NSNumber) -> Bool - { - let boolID = CFBooleanGetTypeID() // the type ID of CFBoolean - let numID = CFGetTypeID(num) // the type ID of num - return numID == boolID - } - - func testArchive() { - let testToken = randomId() - let testMixpanel = Mixpanel.initialize(token: testToken, trackAutomaticEvents: false, flushInterval: 60) - testMixpanel.serverURL = kFakeServerUrl - #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(testMixpanel.distinctId, devicePrefix + testMixpanel.defaultDeviceId(), - "default distinct id archive failed") - #endif - XCTAssertTrue(testMixpanel.currentSuperProperties().isEmpty, - "default super properties archive failed") - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "default events queue archive failed") - XCTAssertNil(testMixpanel.people.distinctId, "default people distinct id archive failed") - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "default people queue archive failed") - let p: Properties = ["p1": "a"] - testMixpanel.identify(distinctId: "d1") - waitForTrackingQueue(testMixpanel) - testMixpanel.registerSuperProperties(p) - testMixpanel.track(event: "e1") - testMixpanel.track(event: "e2") - testMixpanel.track(event: "e3") - testMixpanel.track(event: "e4") - testMixpanel.track(event: "e5") - testMixpanel.track(event: "e6") - testMixpanel.track(event: "e7") - testMixpanel.track(event: "e8") - testMixpanel.track(event: "e9") - testMixpanel.people.set(properties: p) - waitForTrackingQueue(testMixpanel) - testMixpanel.timedEvents["e2"] = 5 - testMixpanel.archive() - let testMixpanel2 = Mixpanel.initialize(token: testToken, trackAutomaticEvents: false, flushInterval: 60) - testMixpanel2.serverURL = kFakeServerUrl - waitForTrackingQueue(testMixpanel2) - XCTAssertEqual(testMixpanel2.distinctId, "d1", "custom distinct archive failed") - XCTAssertTrue(testMixpanel2.currentSuperProperties().count == 1, - "custom super properties archive failed") - let eventQueueValue = eventQueue(token: testMixpanel2.apiToken) - - XCTAssertEqual(eventQueueValue[1]["event"] as? String, "e1", - "event was not successfully archived/unarchived") - XCTAssertEqual(eventQueueValue[2]["event"] as? String, "e2", - "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(eventQueueValue[3]["event"] as? String, "e3", - "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(eventQueueValue[4]["event"] as? String, "e4", - "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(eventQueueValue[5]["event"] as? String, "e5", - "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(eventQueueValue[6]["event"] as? String, "e6", - "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(eventQueueValue[7]["event"] as? String, "e7", - "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(eventQueueValue[8]["event"] as? String, "e8", - "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(eventQueueValue[9]["event"] as? String, "e9", - "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(testMixpanel2.people.distinctId, "d1", - "custom people distinct id archive failed") - XCTAssertTrue(peopleQueue(token: testMixpanel2.apiToken).count >= 1, "pending people queue archive failed") - XCTAssertEqual(testMixpanel2.timedEvents["e2"] as? Int, 5, - "timedEvents archive failed") - let testMixpanel3 = Mixpanel.initialize(token: testToken, trackAutomaticEvents: false, flushInterval: 60) - testMixpanel3.serverURL = kFakeServerUrl - XCTAssertEqual(testMixpanel3.distinctId, "d1", "expecting d1 as distinct id as initialised") - XCTAssertTrue(testMixpanel3.currentSuperProperties().count == 1, - "default super properties expected to have 1 item") - XCTAssertNotNil(eventQueue(token: testMixpanel3.apiToken), "default events queue is nil") - XCTAssertTrue(eventQueue(token: testMixpanel3.apiToken).count == 10, "default events queue expecting 10 items ($identify call added)") - XCTAssertNotNil(testMixpanel3.people.distinctId, - "default people distinct id from no file failed") - XCTAssertNotNil(peopleQueue(token:testMixpanel3.apiToken), "default people queue from no file is nil") - XCTAssertTrue(peopleQueue(token:testMixpanel3.apiToken).count >= 1, "default people queue expecting 1 item") - XCTAssertTrue(testMixpanel3.timedEvents.count == 1, "timedEvents expecting 1 item") - removeDBfile(testToken) - } - - - func testMixpanelDelegate() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.delegate = self - testMixpanel.identify(distinctId: "d1") - testMixpanel.track(event: "e1") - testMixpanel.people.set(property: "p1", to: "a") - waitForTrackingQueue(testMixpanel) - flushAndWaitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 3, "delegate should have stopped flush") - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).count == 2, "delegate should have stopped flush") - removeDBfile(testMixpanel.apiToken) - } - - func testEventTiming() { - 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.") - testMixpanel.time(event: "400 Meters") - testMixpanel.track(event: "500 Meters") - waitForTrackingQueue(testMixpanel) - e = eventQueue(token: testMixpanel.apiToken).last! - p = e["properties"] as! InternalProperties - XCTAssertNil(p["$duration"], "The exact same event name is required for timing.") - testMixpanel.track(event: "400 Meters") - waitForTrackingQueue(testMixpanel) - e = eventQueue(token: testMixpanel.apiToken).last! - p = e["properties"] as! InternalProperties - XCTAssertNotNil(p["$duration"], "This event should be timed.") - testMixpanel.track(event: "400 Meters") - 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.") - testMixpanel.time(event: "Time Event A") - testMixpanel.time(event: "Time Event B") - testMixpanel.time(event: "Time Event C") - waitForTrackingQueue(testMixpanel) - 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(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") + } + concurentQueue.async { + testMixpanel.createAlias("aaa11", distinctId: testMixpanel.distinctId) + testMixpanel.identify(distinctId: "test") + } + concurentQueue.async { + testMixpanel.registerSuperProperties(["Plan": "Mega"]) + } + concurentQueue.async { + let _ = testMixpanel.currentSuperProperties() + } + concurentQueue.async { + testMixpanel.people.set(property: "aaa", to: "SwiftSDK Cocoapods") + testMixpanel.getGroup(groupKey: "test", groupID: 123).set(properties: ["test": 123]) + testMixpanel.removeGroup(groupKey: "test", groupID: 123) + } + concurentQueue.async { + testMixpanel.track(event: "test") + testMixpanel.time(event: "test") testMixpanel.clearTimedEvents() - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(MixpanelPersistence.loadTimedEvents(instanceName: testMixpanel.apiToken).count == 0, "clearTimedEvents should remove all key/value pairs") - removeDBfile(testMixpanel.apiToken) + } } + removeDBfile(testMixpanel.apiToken) + } - func testReadWriteLock() { - var array = [Int]() - let lock = ReadWriteLock(label: "test") - let queue = DispatchQueue(label: "concurrent", qos: .utility, attributes: .concurrent) - for _ in 0..<10 { - queue.async { - lock.write { - for i in 0..<100 { - array.append(i) - } - } - } + func testMPDB() { + // we test with this crazy string because the "token" here can be the instanceName + // which can be any string the user likes, MPDB should strip the non-alphanumeric characters to prevent SQL errors + let mpdb = MPDB.init(token: "Re@lly Bad.T0ken+\(randomId())--*🤪/0️⃣\\_? ") + mpdb.open() - queue.async { - lock.read { - XCTAssertTrue(array.count % 100 == 0, "supposed to happen after write") - } - } + let numRows = 50 + let halfRows = numRows / 2 + let eventName = "Test Event" + + for pType in PersistenceType.allCases { + let emptyArray: [InternalProperties] = mpdb.readRows(pType, numRows: numRows) + XCTAssertTrue(emptyArray.isEmpty, "Table should be empty") + for i in 0...numRows - 1 { + let eventObj: InternalProperties = ["event": eventName, "properties": ["index": i]] + let eventData = JSONHandler.serializeJSONObject(eventObj)! + mpdb.insertRow(pType, data: eventData) + } + let dataArray: [InternalProperties] = mpdb.readRows(pType, numRows: halfRows) + XCTAssertEqual(dataArray.count, halfRows, "Should have read only half of the rows") + var ids: [Int32] = [] + for (n, entity) in dataArray.enumerated() { + guard let id = entity["id"] as? Int32 else { + continue } + ids.append(id) + XCTAssertEqual(entity["event"] as! String, eventName, "Event name should be unchanged") + // index should be oldest events, 0 - 24 + XCTAssertEqual( + entity["properties"] as! [String: Int], ["index": n], "Should read oldest events first") + } + + mpdb.deleteRows(pType, ids: ids) + let dataArray2: [InternalProperties] = mpdb.readRows(pType, numRows: numRows) + // even though we requested numRows, there should only be halfRows left + XCTAssertEqual(dataArray2.count, halfRows, "Should have deleted half the rows") + for (n, entity) in dataArray2.enumerated() { + XCTAssertEqual(entity["event"] as! String, eventName, "Event name should be unchanged") + // old events (0-24) should have been deleted so index should be recent events 25-49 + XCTAssertEqual( + entity["properties"] as! [String: Int], ["index": n + halfRows], + "Should have deleted oldest events first") + } } - func testSetGroup() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - let groupKey = "test_key" - let groupValue = "test_value" - testMixpanel.setGroup(groupKey: groupKey, groupIDs: [groupValue]) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) - let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties - XCTAssertEqual(q[groupKey] as? [String], [groupValue], "group value people property not queued") - assertDefaultPeopleProperties(q) - removeDBfile(testMixpanel.apiToken) + mpdb.close() + removeDBfile(mpdb.apiToken) + } + + func testMigration() { + let token = "testToken" + // clean up + removeDBfile(token) + // copy the legacy archived file for the migration test + let legacyFiles = [ + "mixpanel-testToken-events", "mixpanel-testToken-properties", "mixpanel-testToken-groups", + "mixpanel-testToken-people", "mixpanel-testToken-optOutStatus", + ] + prepareForMigrationFiles(legacyFiles) + // initialize mixpanel will do the migration automatically if found legacy archive files. + let testMixpanel = Mixpanel.initialize( + token: token, trackAutomaticEvents: true, flushInterval: 60) + let fileManager = FileManager.default + let libraryUrls = fileManager.urls( + for: .libraryDirectory, + in: .userDomainMask) + XCTAssertFalse( + fileManager.fileExists( + atPath: (libraryUrls.first?.appendingPathComponent("mixpanel-testToken-events"))!.path), + "after migration, the legacy archive files should be removed") + XCTAssertFalse( + fileManager.fileExists( + atPath: (libraryUrls.first?.appendingPathComponent("mixpanel-testToken-properties"))!.path), + "after migration, the legacy archive files should be removed") + XCTAssertFalse( + fileManager.fileExists( + atPath: (libraryUrls.first?.appendingPathComponent("mixpanel-testToken-groups"))!.path), + "after migration, the legacy archive files should be removed") + XCTAssertFalse( + fileManager.fileExists( + atPath: (libraryUrls.first?.appendingPathComponent("mixpanel-testToken-people"))!.path), + "after migration, the legacy archive files should be removed") + + let events = eventQueue(token: testMixpanel.apiToken) + XCTAssertEqual(events.count, 306) + + XCTAssertEqual(events[0]["event"] as? String, "$identify") + XCTAssertEqual(events[1]["event"] as? String, "Logged in") + XCTAssertEqual(events[2]["event"] as? String, "$ae_first_open") + XCTAssertEqual(events[3]["event"] as? String, "Tracked event 1") + let properties = events.last?["properties"] as? InternalProperties + XCTAssertEqual(properties?["Cool Property"] as? [Int], [12345, 301]) + XCTAssertEqual(properties?["Super Property 2"] as? String, "p2") + + let people = peopleQueue(token: testMixpanel.apiToken) + XCTAssertEqual(people.count, 6) + XCTAssertEqual(people[0]["$distinct_id"] as? String, "demo_user") + XCTAssertEqual(people[0]["$token"] as? String, "testToken") + let appendProperties = people[5]["$append"] as! InternalProperties + XCTAssertEqual(appendProperties["d"] as? String, "goodbye") + + let group = groupQueue(token: testMixpanel.apiToken) + XCTAssertEqual(group.count, 2) + XCTAssertEqual(group[0]["$group_key"] as? String, "Cool Property") + let setProperties = group[0]["$set"] as! InternalProperties + XCTAssertEqual(setProperties["g"] as? String, "yo") + let setProperties2 = group[1]["$set"] as! InternalProperties + XCTAssertEqual(setProperties2["a"] as? Int, 1) + XCTAssertTrue(MixpanelPersistence.loadOptOutStatusFlag(instanceName: token)!) + + //timedEvents + let testTimedEvents = MixpanelPersistence.loadTimedEvents(instanceName: token) + XCTAssertEqual(testTimedEvents.count, 3) + XCTAssertNotNil(testTimedEvents["Time Event A"]) + XCTAssertNotNil(testTimedEvents["Time Event B"]) + XCTAssertNotNil(testTimedEvents["Time Event C"]) + let identity = MixpanelPersistence.loadIdentity(instanceName: token) + XCTAssertEqual(identity.distinctID, "demo_user") + XCTAssertEqual(identity.peopleDistinctID, "demo_user") + XCTAssertNotNil(identity.anonymousId) + XCTAssertEqual(identity.userId, "demo_user") + XCTAssertEqual(identity.alias, "New Alias") + XCTAssertEqual(identity.hadPersistedDistinctId, false) + + let superProperties = MixpanelPersistence.loadSuperProperties(instanceName: token) + XCTAssertEqual(superProperties.count, 7) + XCTAssertEqual(superProperties["Super Property 1"] as? Int, 1) + XCTAssertEqual(superProperties["Super Property 7"] as? NSNull, NSNull()) + removeDBfile("testToken") + } + + func prepareForMigrationFiles(_ fileNames: [String]) { + for fileName in fileNames { + let fileManager = FileManager.default + let filepath = Bundle(for: type(of: self)).url(forResource: fileName, withExtension: nil)! + let libraryUrls = fileManager.urls( + for: .libraryDirectory, + in: .userDomainMask) + let destURL = libraryUrls.first?.appendingPathComponent(fileName) + do { + try FileManager.default.copyItem(at: filepath, to: destURL!) + } catch let error { + print(error) + } } + } - func testAddGroup() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - let groupKey = "test_key" - let groupValue = "test_value" + func testGzipCompressionInit() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: false, useGzipCompression: true) + XCTAssertTrue(testMixpanel.useGzipCompression == true, "the init of GzipCompression failed") + } - testMixpanel.addGroup(groupKey: groupKey, groupID: groupValue) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) - waitForTrackingQueue(testMixpanel) - let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties - XCTAssertEqual(q[groupKey] as? [String], [groupValue], "addGroup people update not queued") - assertDefaultPeopleProperties(q) - testMixpanel.addGroup(groupKey: groupKey, groupID: groupValue) - - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) - waitForTrackingQueue(testMixpanel) - let q2 = peopleQueue(token: testMixpanel.apiToken).last!["$union"] as! InternalProperties - XCTAssertEqual(q2[groupKey] as? [String], [groupValue], "addGroup people update not queued") - - let newVal = "new_group" - testMixpanel.addGroup(groupKey: groupKey, groupID: newVal) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue, newVal]) - waitForTrackingQueue(testMixpanel) - waitForTrackingQueue(testMixpanel) - let q3 = peopleQueue(token: testMixpanel.apiToken).last!["$union"] as! InternalProperties - XCTAssertEqual(q3[groupKey] as? [String], [newVal], "addGroup people update not queued") - removeDBfile(testMixpanel.apiToken) - } - - func testRemoveGroup() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - let groupKey = "test_key" - let groupValue = "test_value" - let newVal = "new_group" - - testMixpanel.setGroup(groupKey: groupKey, groupIDs: [groupValue, newVal]) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue, newVal]) - - testMixpanel.removeGroup(groupKey: groupKey, groupID: groupValue) - waitForTrackingQueue(testMixpanel) - XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [newVal]) - waitForTrackingQueue(testMixpanel) - let q2 = peopleQueue(token: testMixpanel.apiToken).last!["$remove"] as! InternalProperties - XCTAssertEqual(q2[groupKey] as? String, groupValue, "removeGroup people update not queued") - - testMixpanel.removeGroup(groupKey: groupKey, groupID: groupValue) - waitForTrackingQueue(testMixpanel) - XCTAssertNil(testMixpanel.currentSuperProperties()[groupKey]) - waitForTrackingQueue(testMixpanel) - let q3 = peopleQueue(token: testMixpanel.apiToken).last!["$unset"] as! [String] - XCTAssertEqual(q3, [groupKey], "removeGroup people update not queued") - removeDBfile(testMixpanel.apiToken) - } - - func testMultipleInstancesWithSameToken() { - let testToken = randomId() - let concurentQueue = DispatchQueue(label: "multithread", attributes: .concurrent) - - var testMixpanel: MixpanelInstance? - for _ in 1...10 { - concurentQueue.async { - testMixpanel = Mixpanel.initialize(token: testToken, trackAutomaticEvents: true, flushInterval: 60) - testMixpanel?.loggingEnabled = true - testMixpanel?.track(event: "test") - } - } - - var testMixpanel2: MixpanelInstance? - for _ in 1...10 { - concurentQueue.async { - testMixpanel2 = Mixpanel.initialize(token: testToken, trackAutomaticEvents: true, flushInterval: 60) - testMixpanel2?.loggingEnabled = true - testMixpanel2?.track(event: "test") - } - } - sleep(5) - testMixpanel = Mixpanel.initialize(token: testToken, trackAutomaticEvents: true, flushInterval: 60) - testMixpanel2 = Mixpanel.initialize(token: testToken, trackAutomaticEvents: true, flushInterval: 60) - XCTAssertTrue(testMixpanel === testMixpanel2, "instance with same token should be reused and no sqlite db locked error should be populated") - } - - func testMultipleInstancesWithSameTokenButDifferentInstanceNameShouldNotCrash() { - let testToken = randomId() - let concurentQueue = DispatchQueue(label: "multithread", attributes: .concurrent) - - - for i in 1...10 { - concurentQueue.async { - let testMixpanel = Mixpanel.initialize(token: testToken, trackAutomaticEvents: true, flushInterval: 60, instanceName: "instance\(i)") - testMixpanel.loggingEnabled = true - testMixpanel.track(event: "test") - } - } - - sleep(5) - XCTAssertTrue(true, "no sqlite db locked error should be populated") - for j in 1...10 { - removeDBfile("instance\(j)") - } - } - - func testMultipleInstancesWithSameTokenButDifferentInstanceName() { - let testToken = randomId() - let instanceName1 = randomId() - let instanceName2 = randomId() - let instance1 = Mixpanel.initialize(token: testToken, trackAutomaticEvents: true, flushInterval: 60, instanceName: instanceName1) - let instance2 = Mixpanel.initialize(token: testToken, trackAutomaticEvents: true, flushInterval: 60, instanceName: instanceName2) - - XCTAssertNotEqual(instance1.distinctId, instance2.distinctId) - instance1.identify(distinctId: "user1") - instance1.track(event: "test") - waitForTrackingQueue(instance1) - waitForTrackingQueue(instance2) - - XCTAssertEqual(instance1.distinctId, "user1") - XCTAssertEqual(instance1.userId, "user1") - let events = eventQueue(token: instanceName1) - let properties = events.last?["properties"] as? InternalProperties - XCTAssertEqual(properties?["distinct_id"] as? String, "user1") - - instance1.people.set(property: "p1", to: "a") - waitForTrackingQueue(instance1) - waitForTrackingQueue(instance2) - - let peopleQueue_value = peopleQueue(token: instanceName1) - let setValue = peopleQueue_value.last!["$set"] as! InternalProperties - XCTAssertEqual(setValue["p1"] as? String, "a", "custom people property not queued") - - XCTAssertEqual(peopleQueue_value.last?["$distinct_id"] as? String, - "user1", "distinct id not set properly on the people record") - - instance2.identify(distinctId: "user2") - instance2.track(event: "test2") - waitForTrackingQueue(instance1) - waitForTrackingQueue(instance2) - XCTAssertEqual(instance2.distinctId, "user2") - XCTAssertEqual(instance2.userId, "user2") - let events2 = eventQueue(token: instanceName2) - let properties2 = events2.last?["properties"] as? InternalProperties - // event property should have the current distinct id - XCTAssertEqual(properties2?["distinct_id"] as? String, "user2") - - instance2.people.set(property: "p2", to: "b") - waitForTrackingQueue(instance1) - waitForTrackingQueue(instance2) - - let peopleQueue2_value = peopleQueue(token: instanceName2) - XCTAssertEqual(peopleQueue2_value.last?["$distinct_id"] as? String, - "user2", "distinct id not set properly on the people record") - - let setValue2 = peopleQueue2_value.last!["$set"] as! InternalProperties - XCTAssertEqual(setValue2["p2"] as? String, "b", "custom people property not queued") - - removeDBfile(instanceName1) - removeDBfile(instanceName2) - } - - func testReadWriteMultiThreadShouldNotCrash() { - let concurentQueue = DispatchQueue(label: "multithread", attributes: .concurrent) - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - - for n in 1...10 { - concurentQueue.async { - testMixpanel.track(event: "event\(n)") - } - concurentQueue.async { - testMixpanel.flush() - } - concurentQueue.async { - testMixpanel.archive() - } - concurentQueue.async { - testMixpanel.reset() - } - concurentQueue.async { - testMixpanel.createAlias("aaa11", distinctId: testMixpanel.distinctId) - testMixpanel.identify(distinctId: "test") - } - concurentQueue.async { - testMixpanel.registerSuperProperties(["Plan": "Mega"]) - } - concurentQueue.async { - let _ = testMixpanel.currentSuperProperties() - } - concurentQueue.async { - testMixpanel.people.set(property: "aaa", to: "SwiftSDK Cocoapods") - testMixpanel.getGroup(groupKey: "test", groupID: 123).set(properties: ["test": 123]) - testMixpanel.removeGroup(groupKey: "test", groupID: 123) - } - concurentQueue.async { - testMixpanel.track(event: "test") - testMixpanel.time(event: "test") - testMixpanel.clearTimedEvents() - } - } - removeDBfile(testMixpanel.apiToken) - } - - func testMPDB() { - // we test with this crazy string because the "token" here can be the instanceName - // which can be any string the user likes, MPDB should strip the non-alphanumeric characters to prevent SQL errors - let mpdb = MPDB.init(token: "Re@lly Bad.T0ken+\(randomId())--*🤪/0️⃣\\_? ") - mpdb.open() - - let numRows = 50 - let halfRows = numRows/2 - let eventName = "Test Event" - - for pType in PersistenceType.allCases { - let emptyArray: [InternalProperties] = mpdb.readRows(pType, numRows: numRows) - XCTAssertTrue(emptyArray.isEmpty, "Table should be empty") - for i in 0...numRows-1 { - let eventObj : InternalProperties = ["event": eventName, "properties": ["index": i]] - let eventData = JSONHandler.serializeJSONObject(eventObj)! - mpdb.insertRow(pType, data: eventData) - } - let dataArray : [InternalProperties] = mpdb.readRows(pType, numRows: halfRows) - XCTAssertEqual(dataArray.count, halfRows, "Should have read only half of the rows") - var ids: [Int32] = [] - for (n, entity) in dataArray.enumerated() { - guard let id = entity["id"] as? Int32 else { - continue - } - ids.append(id) - XCTAssertEqual(entity["event"] as! String, eventName, "Event name should be unchanged") - // index should be oldest events, 0 - 24 - XCTAssertEqual(entity["properties"] as! [String : Int], ["index": n], "Should read oldest events first") - } - - mpdb.deleteRows(pType, ids: ids) - let dataArray2 : [InternalProperties] = mpdb.readRows(pType, numRows: numRows) - // even though we requested numRows, there should only be halfRows left - XCTAssertEqual(dataArray2.count, halfRows, "Should have deleted half the rows") - for (n, entity) in dataArray2.enumerated() { - XCTAssertEqual(entity["event"] as! String, eventName, "Event name should be unchanged") - // old events (0-24) should have been deleted so index should be recent events 25-49 - XCTAssertEqual(entity["properties"] as! [String : Int], ["index": n + halfRows], "Should have deleted oldest events first") - } - } - - mpdb.close() - removeDBfile(mpdb.apiToken) - } - - func testMigration() { - let token = "testToken" - // clean up - removeDBfile(token) - // copy the legacy archived file for the migration test - let legacyFiles = ["mixpanel-testToken-events", "mixpanel-testToken-properties", "mixpanel-testToken-groups", "mixpanel-testToken-people", "mixpanel-testToken-optOutStatus"] - prepareForMigrationFiles(legacyFiles) - // initialize mixpanel will do the migration automatically if found legacy archive files. - let testMixpanel = Mixpanel.initialize(token: token, trackAutomaticEvents: true, flushInterval: 60) - let fileManager = FileManager.default - let libraryUrls = fileManager.urls(for: .libraryDirectory, - in: .userDomainMask) - XCTAssertFalse(fileManager.fileExists(atPath: (libraryUrls.first?.appendingPathComponent("mixpanel-testToken-events"))!.path), "after migration, the legacy archive files should be removed") - XCTAssertFalse(fileManager.fileExists(atPath: (libraryUrls.first?.appendingPathComponent("mixpanel-testToken-properties"))!.path), "after migration, the legacy archive files should be removed") - XCTAssertFalse(fileManager.fileExists(atPath: (libraryUrls.first?.appendingPathComponent("mixpanel-testToken-groups"))!.path), "after migration, the legacy archive files should be removed") - XCTAssertFalse(fileManager.fileExists(atPath: (libraryUrls.first?.appendingPathComponent("mixpanel-testToken-people"))!.path), "after migration, the legacy archive files should be removed") - - let events = eventQueue(token: testMixpanel.apiToken) - XCTAssertEqual(events.count, 306) - - XCTAssertEqual(events[0]["event"] as? String, "$identify") - XCTAssertEqual(events[1]["event"] as? String, "Logged in") - XCTAssertEqual(events[2]["event"] as? String, "$ae_first_open") - XCTAssertEqual(events[3]["event"] as? String, "Tracked event 1") - let properties = events.last?["properties"] as? InternalProperties - XCTAssertEqual(properties?["Cool Property"] as? [Int], [12345,301]) - XCTAssertEqual(properties?["Super Property 2"] as? String, "p2") - - let people = peopleQueue(token: testMixpanel.apiToken) - XCTAssertEqual(people.count, 6) - XCTAssertEqual(people[0]["$distinct_id"] as? String, "demo_user") - XCTAssertEqual(people[0]["$token"] as? String, "testToken") - let appendProperties = people[5]["$append"] as! InternalProperties - XCTAssertEqual(appendProperties["d"] as? String, "goodbye") - - let group = groupQueue(token: testMixpanel.apiToken) - XCTAssertEqual(group.count, 2) - XCTAssertEqual(group[0]["$group_key"] as? String, "Cool Property") - let setProperties = group[0]["$set"] as! InternalProperties - XCTAssertEqual(setProperties["g"] as? String, "yo") - let setProperties2 = group[1]["$set"] as! InternalProperties - XCTAssertEqual(setProperties2["a"] as? Int, 1) - XCTAssertTrue(MixpanelPersistence.loadOptOutStatusFlag(instanceName: token)!) - - //timedEvents - let testTimedEvents = MixpanelPersistence.loadTimedEvents(instanceName: token) - XCTAssertEqual(testTimedEvents.count, 3) - XCTAssertNotNil(testTimedEvents["Time Event A"]) - XCTAssertNotNil(testTimedEvents["Time Event B"]) - XCTAssertNotNil(testTimedEvents["Time Event C"]) - let identity = MixpanelPersistence.loadIdentity(instanceName: token) - XCTAssertEqual(identity.distinctID, "demo_user") - XCTAssertEqual(identity.peopleDistinctID, "demo_user") - XCTAssertNotNil(identity.anonymousId) - XCTAssertEqual(identity.userId, "demo_user") - XCTAssertEqual(identity.alias, "New Alias") - XCTAssertEqual(identity.hadPersistedDistinctId, false) - - let superProperties = MixpanelPersistence.loadSuperProperties(instanceName: token) - XCTAssertEqual(superProperties.count, 7) - XCTAssertEqual(superProperties["Super Property 1"] as? Int, 1) - XCTAssertEqual(superProperties["Super Property 7"] as? NSNull, NSNull()) - removeDBfile("testToken") - } - - func prepareForMigrationFiles(_ fileNames: [String]) { - for fileName in fileNames { - let fileManager = FileManager.default - let filepath = Bundle(for: type(of: self)).url(forResource: fileName, withExtension: nil)! - let libraryUrls = fileManager.urls(for: .libraryDirectory, - in: .userDomainMask) - let destURL = libraryUrls.first?.appendingPathComponent(fileName) - do { - try FileManager.default.copyItem(at: filepath, to: destURL!) - } catch let error { - print(error) - } - } - } - - func testGzipCompressionInit() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: false, useGzipCompression: true) - XCTAssertTrue(testMixpanel.useGzipCompression == true, "the init of GzipCompression failed") - } - - func testGzipCompressionDefault() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: false) - XCTAssertTrue(testMixpanel.useGzipCompression == false, "the default gzip option disabled failed") - } + func testGzipCompressionDefault() { + let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: false) + XCTAssertTrue( + testMixpanel.useGzipCompression == false, "the default gzip option disabled failed") + } } diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelFeatureFlagTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelFeatureFlagTests.swift index 8b68a43f..47f0e8c8 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelFeatureFlagTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelFeatureFlagTests.swift @@ -7,834 +7,872 @@ // import XCTest + @testable import Mixpanel // MARK: - Mocks and Helpers (Largely Unchanged) class MockFeatureFlagDelegate: MixpanelFlagDelegate { - - var options: MixpanelOptions - var distinctId: String - var trackedEvents: [(event: String?, properties: Properties?)] = [] - var trackExpectation: XCTestExpectation? - var getOptionsCallCount = 0 - var getDistinctIdCallCount = 0 - init(options: MixpanelOptions = MixpanelOptions(token: "test", featureFlagsEnabled: true), distinctId: String = "test_distinct_id") { - self.options = options - self.distinctId = distinctId - } + var options: MixpanelOptions + var distinctId: String + var trackedEvents: [(event: String?, properties: Properties?)] = [] + var trackExpectation: XCTestExpectation? + var getOptionsCallCount = 0 + var getDistinctIdCallCount = 0 - func getOptions() -> MixpanelOptions { - getOptionsCallCount += 1 - return options - } + init( + options: MixpanelOptions = MixpanelOptions(token: "test", featureFlagsEnabled: true), + distinctId: String = "test_distinct_id" + ) { + self.options = options + self.distinctId = distinctId + } - func getDistinctId() -> String { - getDistinctIdCallCount += 1 - return distinctId - } + func getOptions() -> MixpanelOptions { + getOptionsCallCount += 1 + return options + } - func track(event: String?, properties: Properties?) { - print("MOCK Delegate: Track called - Event: \(event ?? "nil"), Props: \(properties ?? [:])") - trackedEvents.append((event: event, properties: properties)) - trackExpectation?.fulfill() - } + func getDistinctId() -> String { + getDistinctIdCallCount += 1 + return distinctId + } + + func track(event: String?, properties: Properties?) { + print("MOCK Delegate: Track called - Event: \(event ?? "nil"), Props: \(properties ?? [:])") + trackedEvents.append((event: event, properties: properties)) + trackExpectation?.fulfill() + } } // AssertEqual helper (Unchanged from previous working version) func AssertEqual(_ value1: Any?, _ value2: Any?, file: StaticString = #file, line: UInt = #line) { - // ... (Use the version that fixed the Any?? issues) ... - switch (value1, value2) { - case (nil, nil): - break // Equal - case (let v1 as Bool, let v2 as Bool): - XCTAssertEqual(v1, v2, file: file, line: line) - case (let v1 as String, let v2 as String): - XCTAssertEqual(v1, v2, file: file, line: line) - case (let v1 as Int, let v2 as Int): - XCTAssertEqual(v1, v2, file: file, line: line) - case (let v1 as Double, let v2 as Double): - // Handle potential precision issues if necessary - XCTAssertEqual(v1, v2, accuracy: 0.00001, file: file, line: line) - case (let v1 as [Any?], let v2 as [Any?]): - XCTAssertEqual(v1.count, v2.count, "Array counts differ", file: file, line: line) - for (index, item1) in v1.enumerated() { - guard index < v2.count else { - XCTFail("Index \(index) out of bounds for second array", file: file, line: line) - return - } - AssertEqual(item1, v2[index], file: file, line: line) - } - case (let v1 as [String: Any?], let v2 as [String: Any?]): - XCTAssertEqual(v1.count, v2.count, "Dictionary counts differ (\(v1.keys.sorted()) vs \(v2.keys.sorted()))", file: file, line: line) - for (key, item1) in v1 { - guard v2.keys.contains(key) else { - XCTFail("Key '\(key)' missing in second dictionary", file: file, line: line) - continue - } - let item2DoubleOptional = v2[key] - AssertEqual(item1, item2DoubleOptional ?? nil, file: file, line: line) - } - default: - if let n1 = value1 as? NSNumber, let n2 = value2 as? NSNumber { - XCTAssertEqual(n1, n2, "NSNumber values differ: \(n1) vs \(n2)", file: file, line: line) - } else { - XCTFail("Values are not equal or of comparable types: \(String(describing: value1)) vs \(String(describing: value2))", file: file, line: line) - } + // ... (Use the version that fixed the Any?? issues) ... + switch (value1, value2) { + case (nil, nil): + break // Equal + case (let v1 as Bool, let v2 as Bool): + XCTAssertEqual(v1, v2, file: file, line: line) + case (let v1 as String, let v2 as String): + XCTAssertEqual(v1, v2, file: file, line: line) + case (let v1 as Int, let v2 as Int): + XCTAssertEqual(v1, v2, file: file, line: line) + case (let v1 as Double, let v2 as Double): + // Handle potential precision issues if necessary + XCTAssertEqual(v1, v2, accuracy: 0.00001, file: file, line: line) + case (let v1 as [Any?], let v2 as [Any?]): + XCTAssertEqual(v1.count, v2.count, "Array counts differ", file: file, line: line) + for (index, item1) in v1.enumerated() { + guard index < v2.count else { + XCTFail("Index \(index) out of bounds for second array", file: file, line: line) + return + } + AssertEqual(item1, v2[index], file: file, line: line) } + case (let v1 as [String: Any?], let v2 as [String: Any?]): + XCTAssertEqual( + v1.count, v2.count, "Dictionary counts differ (\(v1.keys.sorted()) vs \(v2.keys.sorted()))", + file: file, line: line) + for (key, item1) in v1 { + guard v2.keys.contains(key) else { + XCTFail("Key '\(key)' missing in second dictionary", file: file, line: line) + continue + } + let item2DoubleOptional = v2[key] + AssertEqual(item1, item2DoubleOptional ?? nil, file: file, line: line) + } + default: + if let n1 = value1 as? NSNumber, let n2 = value2 as? NSNumber { + XCTAssertEqual(n1, n2, "NSNumber values differ: \(n1) vs \(n2)", file: file, line: line) + } else { + XCTFail( + "Values are not equal or of comparable types: \(String(describing: value1)) vs \(String(describing: value2))", + file: file, line: line) + } + } } - // MARK: - Refactored FeatureFlagManager Tests class FeatureFlagManagerTests: XCTestCase { - var mockDelegate: MockFeatureFlagDelegate! - var manager: FeatureFlagManager! - // Sample flag data for simulating fetch results - let sampleFlags: [String: MixpanelFlagVariant] = [ - "feature_bool_true": MixpanelFlagVariant(key: "v_true", value: true), - "feature_bool_false": MixpanelFlagVariant(key: "v_false", value: false), - "feature_string": MixpanelFlagVariant(key: "v_str", value: "test_string"), - "feature_int": MixpanelFlagVariant(key: "v_int", value: 101), - "feature_double": MixpanelFlagVariant(key: "v_double", value: 99.9), - "feature_null": MixpanelFlagVariant(key: "v_null", value: nil) - ] - let defaultFallback = MixpanelFlagVariant(value: nil) // Default fallback for convenience + var mockDelegate: MockFeatureFlagDelegate! + var manager: FeatureFlagManager! + // Sample flag data for simulating fetch results + let sampleFlags: [String: MixpanelFlagVariant] = [ + "feature_bool_true": MixpanelFlagVariant(key: "v_true", value: true), + "feature_bool_false": MixpanelFlagVariant(key: "v_false", value: false), + "feature_string": MixpanelFlagVariant(key: "v_str", value: "test_string"), + "feature_int": MixpanelFlagVariant(key: "v_int", value: 101), + "feature_double": MixpanelFlagVariant(key: "v_double", value: 99.9), + "feature_null": MixpanelFlagVariant(key: "v_null", value: nil), + ] + let defaultFallback = MixpanelFlagVariant(value: nil) // Default fallback for convenience - override func setUpWithError() throws { - try super.setUpWithError() - mockDelegate = MockFeatureFlagDelegate() - // Ensure manager is initialized with the delegate - manager = FeatureFlagManager(serverURL: "https://test.com", delegate: mockDelegate) + override func setUpWithError() throws { + try super.setUpWithError() + mockDelegate = MockFeatureFlagDelegate() + // Ensure manager is initialized with the delegate + manager = FeatureFlagManager(serverURL: "https://test.com", delegate: mockDelegate) + } + + override func tearDownWithError() throws { + mockDelegate = nil + manager = nil + try super.tearDownWithError() + } + + // --- Simulation Helpers --- + // These now directly modify state and call the *internal* _completeFetch + // Requires _completeFetch to be accessible (e.g., internal or @testable import) + + private func simulateFetchSuccess(flags: [String: MixpanelFlagVariant]? = nil) { + let flagsToSet = flags ?? sampleFlags + // Set flags directly *before* calling completeFetch + manager.accessQueue.sync { + manager.flags = flagsToSet + // Important: Set isFetching = true *before* calling _completeFetch, + // as _completeFetch assumes a fetch was in progress. + manager.isFetching = true + } + // Call internal completion logic + manager._completeFetch(success: true) + } + + private func simulateFetchFailure() { + // Set isFetching = true before calling _completeFetch + manager.accessQueue.sync { + manager.isFetching = true + // Ensure flags are nil or unchanged on failure simulation if desired + manager.flags = nil // Or keep existing flags based on desired failure behavior + } + // Call internal completion logic + manager._completeFetch(success: false) + } + + // --- State and Configuration Tests --- + + func testAreFeaturesReady_InitialState() { + XCTAssertFalse(manager.areFlagsReady(), "Features should not be ready initially") + } + + func testAreFeaturesReady_AfterSuccessfulFetchSimulation() { + simulateFetchSuccess() + // Need to wait briefly for the main queue dispatch in _completeFetch to potentially run + let expectation = XCTestExpectation(description: "Wait for potential completion dispatch") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { expectation.fulfill() } + wait(for: [expectation], timeout: 0.5) + XCTAssertTrue( + manager.areFlagsReady(), "Features should be ready after successful fetch simulation") + } + + func testAreFeaturesReady_AfterFailedFetchSimulation() { + simulateFetchFailure() + // Need to wait briefly for the main queue dispatch in _completeFetch to potentially run + let expectation = XCTestExpectation(description: "Wait for potential completion dispatch") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { expectation.fulfill() } + wait(for: [expectation], timeout: 0.5) + XCTAssertFalse( + manager.areFlagsReady(), "Features should not be ready after failed fetch simulation") + } + + // --- Load Flags Tests --- + + func testLoadFlags_WhenDisabledInConfig() { + mockDelegate.options = MixpanelOptions(token: "test", featureFlagsEnabled: false) // Explicitly disable + manager.loadFlags() // Call public API + + // Wait to ensure no async fetch operations started changing state + let expectation = XCTestExpectation(description: "Wait briefly") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { expectation.fulfill() } + wait(for: [expectation], timeout: 0.5) + + XCTAssertFalse(manager.areFlagsReady(), "Flags should not become ready if disabled") + // We can't easily check if _fetchFlagsIfNeeded was *not* called without more testability hooks + } + + // Note: Testing that loadFlags *starts* a fetch is harder now without exposing internal state. + // We test the outcome via the async getFeature tests below. + + // --- Sync Flag Retrieval Tests --- + + func testGetVariantSync_FlagsReady_ExistingFlag() { + simulateFetchSuccess() // Flags loaded + let flagVariant = manager.getVariantSync("feature_string", fallback: defaultFallback) + AssertEqual(flagVariant.key, "v_str") + AssertEqual(flagVariant.value, "test_string") + // Tracking check happens later + } + + func testGetVariantSync_FlagsReady_MissingFlag_UsesFallback() { + simulateFetchSuccess() + let fallback = MixpanelFlagVariant(key: "fb_key", value: "fb_value") + let flagVariant = manager.getVariantSync("missing_feature", fallback: fallback) + AssertEqual(flagVariant.key, fallback.key) + AssertEqual(flagVariant.value, fallback.value) + XCTAssertEqual(mockDelegate.trackedEvents.count, 0, "Should not track for fallback") + } + + func testGetVariantSync_FlagsNotReady_UsesFallback() { + XCTAssertFalse(manager.areFlagsReady()) // Precondition + let fallback = MixpanelFlagVariant(key: "fb_key", value: 999) + let flagVariant = manager.getVariantSync("feature_bool_true", fallback: fallback) + AssertEqual(flagVariant.key, fallback.key) + AssertEqual(flagVariant.value, fallback.value) + XCTAssertEqual(mockDelegate.trackedEvents.count, 0, "Should not track if flags not ready") + } + + func testGetVariantValueSync_FlagsReady() { + simulateFetchSuccess() + let value = manager.getVariantValueSync("feature_int", fallbackValue: -1) + AssertEqual(value, 101) + } + + func testGetVariantValueSync_FlagsReady_MissingFlag() { + simulateFetchSuccess() + let value = manager.getVariantValueSync("missing_feature", fallbackValue: "default") + AssertEqual(value, "default") + } + + func testGetVariantValueSync_FlagsNotReady() { + XCTAssertFalse(manager.areFlagsReady()) + let value = manager.getVariantValueSync("feature_int", fallbackValue: -1) + AssertEqual(value, -1) + } + + func testIsFlagEnabledSync_FlagsReady_True() { + simulateFetchSuccess() + XCTAssertTrue(manager.isEnabledSync("feature_bool_true")) + } + + func testIsFlagEnabledSync_FlagsReady_False() { + simulateFetchSuccess() + XCTAssertFalse(manager.isEnabledSync("feature_bool_false")) + } + + func testIsFlagEnabledSync_FlagsReady_MissingFlag_UsesFallback() { + simulateFetchSuccess() + XCTAssertTrue(manager.isEnabledSync("missing", fallbackValue: true)) + XCTAssertFalse(manager.isEnabledSync("missing", fallbackValue: false)) + } + + func testIsFlagEnabledSync_FlagsReady_NonBoolValue_UsesFallback() { + simulateFetchSuccess() + XCTAssertTrue(manager.isEnabledSync("feature_string", fallbackValue: true)) // String value + XCTAssertFalse(manager.isEnabledSync("feature_int", fallbackValue: false)) // Int value + XCTAssertTrue(manager.isEnabledSync("feature_null", fallbackValue: true)) // Null value + } + + func testIsFlagEnabledSync_FlagsNotReady_UsesFallback() { + XCTAssertFalse(manager.areFlagsReady()) + XCTAssertTrue(manager.isEnabledSync("feature_bool_true", fallbackValue: true)) + XCTAssertFalse(manager.isEnabledSync("feature_bool_true", fallbackValue: false)) + } + + // --- Async Flag Retrieval Tests --- + + func testGetVariant_Async_FlagsReady_ExistingFlag_XCTWaiter() { + // Arrange + simulateFetchSuccess() // Ensure flags are ready + let expectation = XCTestExpectation(description: "Async getFeature ready - XCTWaiter Wait") + var receivedData: MixpanelFlagVariant? + var assertionError: String? + + // Act + manager.getVariant("feature_double", fallback: defaultFallback) { data in + // This completion should run on the main thread + if !Thread.isMainThread { + assertionError = "Completion not on main thread (\(Thread.current))" + } + receivedData = data + // Perform crucial checks inside completion + if receivedData == nil { assertionError = (assertionError ?? "") + "; Received data was nil" } + if receivedData?.key != "v_double" { + assertionError = (assertionError ?? "") + "; Received key mismatch" + } + // Add other essential checks if needed + expectation.fulfill() } - override func tearDownWithError() throws { - mockDelegate = nil - manager = nil - try super.tearDownWithError() + // Assert - Wait using an explicit XCTWaiter instance + let waiter = XCTWaiter() + let result = waiter.wait(for: [expectation], timeout: 2.0) // Increased timeout + + // Check waiter result and any errors captured in completion + if result != .completed { + XCTFail( + "XCTWaiter timed out waiting for expectation. Error captured: \(assertionError ?? "None")") + } else if let error = assertionError { + XCTFail("Assertions failed within completion block: \(error)") } - // --- Simulation Helpers --- - // These now directly modify state and call the *internal* _completeFetch - // Requires _completeFetch to be accessible (e.g., internal or @testable import) + // Final check on data after wait + // These might be redundant if checked thoroughly in completion, but good final check + XCTAssertNotNil(receivedData, "Received data should be non-nil after successful wait") + AssertEqual(receivedData?.key, "v_double") + AssertEqual(receivedData?.value, 99.9) + } - private func simulateFetchSuccess(flags: [String: MixpanelFlagVariant]? = nil) { - let flagsToSet = flags ?? sampleFlags - // Set flags directly *before* calling completeFetch - manager.accessQueue.sync { - manager.flags = flagsToSet - // Important: Set isFetching = true *before* calling _completeFetch, - // as _completeFetch assumes a fetch was in progress. - manager.isFetching = true + func testGetVariant_Async_FlagsReady_MissingFlag_UsesFallback() { + simulateFetchSuccess() // Flags loaded + let expectation = XCTestExpectation( + description: "Async getFeature (Flags Ready, Missing) completes") + let fallback = MixpanelFlagVariant(key: "fb_async", value: -1) + var receivedData: MixpanelFlagVariant? + + manager.getVariant("missing_feature", fallback: fallback) { data in + XCTAssertTrue(Thread.isMainThread, "Completion should be on main thread") + receivedData = data + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1.0) + + XCTAssertNotNil(receivedData) + AssertEqual(receivedData?.key, fallback.key) + AssertEqual(receivedData?.value, fallback.value) + // Check delegate tracking after wait (should not have tracked) + XCTAssertEqual(mockDelegate.trackedEvents.count, 0, "Should not track fallback") + } + + // Test fetch triggering and completion via getFeature when not ready + func testGetVariant_Async_FlagsNotReady_FetchSuccess() { + XCTAssertFalse(manager.areFlagsReady()) + let expectation = XCTestExpectation( + description: "Async getFeature (Flags Not Ready) triggers fetch and succeeds") + var receivedData: MixpanelFlagVariant? + + // Setup tracking expectation *before* calling getFeature + mockDelegate.trackExpectation = XCTestExpectation( + description: "Tracking call for fetch success") + + // Call getFeature - this should trigger the fetch logic internally + manager.getVariant("feature_int", fallback: defaultFallback) { data in + XCTAssertTrue(Thread.isMainThread, "Completion should be on main thread") + receivedData = data + expectation.fulfill() // Fulfill main expectation + } + + // Crucially, simulate the fetch success *after* getFeature was called. + // Add a slight delay to mimic network latency and allow fetch logic to start. + DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) { + print("Simulating fetch success...") + self.simulateFetchSuccess() // This sets flags and calls _completeFetch + } + + // Wait for BOTH the getFeature completion AND the tracking expectation + wait(for: [expectation, mockDelegate.trackExpectation!], timeout: 3.0) // Increased timeout + + XCTAssertNotNil(receivedData) + AssertEqual(receivedData?.key, "v_int") // Check correct flag data received + AssertEqual(receivedData?.value, 101) + XCTAssertTrue(manager.areFlagsReady(), "Flags should be ready after successful fetch") + XCTAssertEqual(mockDelegate.trackedEvents.count, 1, "Tracking event should have been recorded") + } + + func testGetVariant_Async_FlagsNotReady_FetchFailure() { + XCTAssertFalse(manager.areFlagsReady()) + let expectation = XCTestExpectation( + description: "Async getFeature (Flags Not Ready) triggers fetch and fails") + let fallback = MixpanelFlagVariant(key: "fb_fail", value: "failed_fetch") + var receivedData: MixpanelFlagVariant? + + // Call getFeature + manager.getVariant("feature_string", fallback: fallback) { data in + XCTAssertTrue(Thread.isMainThread, "Completion should be on main thread") + receivedData = data + expectation.fulfill() + } + + // Simulate fetch failure after a delay + DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) { + print("Simulating fetch failure...") + self.simulateFetchFailure() // This calls _completeFetch(success: false) + } + + wait(for: [expectation], timeout: 3.0) + + XCTAssertNotNil(receivedData) + AssertEqual(receivedData?.key, fallback.key) // Should receive fallback + AssertEqual(receivedData?.value, fallback.value) + XCTAssertFalse(manager.areFlagsReady(), "Flags should still not be ready after failed fetch") + XCTAssertEqual( + mockDelegate.trackedEvents.count, 0, "Should not track on fetch failure/fallback") + } + + // --- Tracking Tests --- + + func testTracking_CalledOncePerFeature() { + simulateFetchSuccess() // Flags ready + + mockDelegate.trackExpectation = XCTestExpectation( + description: "Track called once for feature_bool_true") + mockDelegate.trackExpectation?.expectedFulfillmentCount = 1 // Expect exactly one call + + // Call sync methods multiple times + _ = manager.getVariantSync("feature_bool_true", fallback: defaultFallback) + _ = manager.getVariantValueSync("feature_bool_true", fallbackValue: nil) + _ = manager.isEnabledSync("feature_bool_true") + + // Call async method + let asyncExpectation = XCTestExpectation( + description: "Async getFeature completes for tracking test") + manager.getVariant("feature_bool_true", fallback: defaultFallback) { _ in + asyncExpectation.fulfill() + } + + // Wait for async call AND the track expectation + wait(for: [asyncExpectation, mockDelegate.trackExpectation!], timeout: 2.0) + + // Verify track delegate method was called exactly once + let trueEvents = mockDelegate.trackedEvents.filter { + $0.properties?["Experiment name"] as? String == "feature_bool_true" + } + XCTAssertEqual(trueEvents.count, 1, "Track should only be called once for the same feature") + + // --- Call for a *different* feature --- + mockDelegate.trackExpectation = XCTestExpectation( + description: "Track called for feature_string") + _ = manager.getVariantSync("feature_string", fallback: defaultFallback) + wait(for: [mockDelegate.trackExpectation!], timeout: 1.0) + + let stringEvents = mockDelegate.trackedEvents.filter { + $0.properties?["Experiment name"] as? String == "feature_string" + } + XCTAssertEqual(stringEvents.count, 1, "Track should be called again for a different feature") + + // Verify total calls + XCTAssertEqual(mockDelegate.trackedEvents.count, 2, "Total track calls should be 2") + } + + func testTracking_SendsCorrectProperties() { + simulateFetchSuccess() + mockDelegate.trackExpectation = XCTestExpectation( + description: "Track called for properties check") + + _ = manager.getVariantSync("feature_int", fallback: defaultFallback) // Trigger tracking + + wait(for: [mockDelegate.trackExpectation!], timeout: 1.0) + + XCTAssertEqual(mockDelegate.trackedEvents.count, 1) + let tracked = mockDelegate.trackedEvents[0] + XCTAssertEqual(tracked.event, "$experiment_started") + XCTAssertNotNil(tracked.properties) + + let props = tracked.properties! + AssertEqual(props["Experiment name"] ?? nil, "feature_int") + AssertEqual(props["Variant name"] ?? nil, "v_int") + AssertEqual(props["$experiment_type"] ?? nil, "feature_flag") + } + + func testTracking_DoesNotTrackForFallback_Sync() { + simulateFetchSuccess() // Flags ready + _ = manager.getVariantSync( + "missing_feature", fallback: MixpanelFlagVariant(key: "fb", value: "v")) // Request missing flag + // Wait briefly to ensure no unexpected tracking call + let expectation = XCTestExpectation(description: "Wait briefly for no track") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { expectation.fulfill() } + wait(for: [expectation], timeout: 0.5) + XCTAssertEqual( + mockDelegate.trackedEvents.count, 0, + "Track should not be called when a fallback is used (sync)") + } + + func testTracking_DoesNotTrackForFallback_Async() { + simulateFetchSuccess() // Flags ready + let expectation = XCTestExpectation(description: "Async getFeature (Fallback) completes") + + manager.getVariant("missing_feature", fallback: MixpanelFlagVariant(key: "fb", value: "v")) { + _ in + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1.0) + // Check delegate tracking after wait + XCTAssertEqual( + mockDelegate.trackedEvents.count, 0, + "Track should not be called when a fallback is used (async)") + } + + // --- Concurrency Tests --- + + // Test concurrent fetch attempts (via getFeature when not ready) + func testConcurrentGetFeature_WhenNotReady_OnlyOneFetch() { + XCTAssertFalse(manager.areFlagsReady()) + + let numConcurrentCalls = 5 + var expectations: [XCTestExpectation] = [] + var completionResults: [MixpanelFlagVariant?] = Array(repeating: nil, count: numConcurrentCalls) + + // Expect tracking only ONCE for the actual feature if fetch succeeds + mockDelegate.trackExpectation = XCTestExpectation(description: "Track call (should be once)") + mockDelegate.trackExpectation?.expectedFulfillmentCount = 1 + + print("Starting \(numConcurrentCalls) concurrent getFeature calls...") + for i in 0.. FlagsResponse? = { data in + do { + return try JSONDecoder().decode(FlagsResponse.self, from: data) + } catch { + print("Error parsing flags JSON: \(error)") + return nil + } } - func testAreFeaturesReady_AfterSuccessfulFetchSimulation() { - simulateFetchSuccess() - // Need to wait briefly for the main queue dispatch in _completeFetch to potentially run - let expectation = XCTestExpectation(description: "Wait for potential completion dispatch") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { expectation.fulfill() } - wait(for: [expectation], timeout: 0.5) - XCTAssertTrue(manager.areFlagsReady(), "Features should be ready after successful fetch simulation") + // Create various test data scenarios + let validJSON = """ + { + "flags": { + "test_flag": { + "variant_key": "test_variant", + "variant_value": "test_value" + } + } + } + """.data(using: .utf8)! + + let emptyFlagsJSON = """ + { + "flags": {} + } + """.data(using: .utf8)! + + let nullFlagsJSON = """ + { + "flags": null + } + """.data(using: .utf8)! + + let malformedJSON = "not json".data(using: .utf8)! + + // Test valid JSON with flags + let validResult = parseResponse(validJSON) + XCTAssertNotNil(validResult, "Parser should handle valid JSON") + XCTAssertNotNil(validResult?.flags, "Flags should be non-nil") + XCTAssertEqual(validResult?.flags?.count, 1, "Should have one flag") + XCTAssertEqual(validResult?.flags?["test_flag"]?.key, "test_variant") + XCTAssertEqual(validResult?.flags?["test_flag"]?.value as? String, "test_value") + + // Test empty flags object + let emptyResult = parseResponse(emptyFlagsJSON) + XCTAssertNotNil(emptyResult, "Parser should handle empty flags object") + XCTAssertNotNil(emptyResult?.flags, "Flags should be non-nil") + XCTAssertEqual(emptyResult?.flags?.count, 0, "Flags should be empty") + + // Test null flags field + let nullResult = parseResponse(nullFlagsJSON) + XCTAssertNotNil(nullResult, "Parser should handle null flags") + XCTAssertNil(nullResult?.flags, "Flags should be nil when null in JSON") + + // Test malformed JSON + let malformedResult = parseResponse(malformedJSON) + XCTAssertNil(malformedResult, "Parser should return nil for malformed JSON") + + // Test with multiple flags + let multipleFlagsJSON = """ + { + "flags": { + "feature_a": { + "variant_key": "variant_a", + "variant_value": true + }, + "feature_b": { + "variant_key": "variant_b", + "variant_value": 42 + }, + "feature_c": { + "variant_key": "variant_c", + "variant_value": null + } + } + } + """.data(using: .utf8)! + + let multiResult = parseResponse(multipleFlagsJSON) + XCTAssertNotNil(multiResult, "Parser should handle multiple flags") + XCTAssertEqual(multiResult?.flags?.count, 3, "Should have three flags") + XCTAssertEqual(multiResult?.flags?["feature_a"]?.value as? Bool, true) + XCTAssertEqual(multiResult?.flags?["feature_b"]?.value as? Int, 42) + XCTAssertNil(multiResult?.flags?["feature_c"]?.value, "Null value should be preserved") + + // Test with missing required fields + let missingFieldJSON = """ + { + "not_flags": {} + } + """.data(using: .utf8)! + + let missingFieldResult = parseResponse(missingFieldJSON) + XCTAssertNotNil(missingFieldResult, "Parser should handle missing flags field") + XCTAssertNil(missingFieldResult?.flags, "Flags should be nil when field is missing") + } + + // --- Delegate Error Handling Tests --- + + func testDelegateNilHandling() { + // Set up with flags ready, but then remove delegate + simulateFetchSuccess() + manager.delegate = nil + + // Test all operations with nil delegate + + // Synchronous operations + let syncData = manager.getVariantSync("feature_bool_true", fallback: defaultFallback) + XCTAssertEqual(syncData.key, "v_true") + XCTAssertEqual(syncData.value as? Bool, true) + + // Async operations + let expectation = XCTestExpectation(description: "Async with nil delegate") + manager.getVariant("feature_int", fallback: defaultFallback) { data in + XCTAssertEqual(data.key, "v_int") + XCTAssertEqual(data.value as? Int, 101) + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + + // No tracking calls should succeed, but operations should still work + // This is "success" as the code doesn't crash when delegate is nil + } + + func testFetchWithNoDelegate() { + // Create manager with no delegate + let noDelegate = FeatureFlagManager(serverURL: "https://test.com", delegate: nil) + + // Try to load flags + noDelegate.loadFlags() + + // Verify no crash; attempt a flag fetch after a short delay + let expectation = XCTestExpectation(description: "Check after attempted fetch") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + XCTAssertFalse(noDelegate.areFlagsReady(), "Flags should not be ready without delegate") + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + } + + func testDelegateConfigDisabledHandling() { + // Set delegate options to disabled + mockDelegate.options = MixpanelOptions(token: "test", featureFlagsEnabled: false) + + // Try to load flags + manager.loadFlags() + + // Verify no fetch is triggered + let expectation = XCTestExpectation(description: "Check disabled options behavior") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + XCTAssertFalse( + self.manager.areFlagsReady(), "Flags should not be ready when options disabled") + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + } + + // --- AnyCodable Edge Cases --- + + func testAnyCodableWithComplexTypes() { + // Use reflection to test AnyCodable directly + + // Test with nested array + let nestedArrayJSON = """ + { + "variant_key": "complex_array", + "variant_value": [1, "string", true, [2, 3], {"key": "value"}] + } + """.data(using: .utf8)! + + do { + let decoder = JSONDecoder() + let flagData = try decoder.decode(MixpanelFlagVariant.self, from: nestedArrayJSON) + + XCTAssertEqual(flagData.key, "complex_array") + XCTAssertNotNil(flagData.value, "Value should not be nil") + + // Verify array structure + guard let array = flagData.value as? [Any?] else { + XCTFail("Value should be an array") + return + } + + XCTAssertEqual(array.count, 5, "Array should have 5 elements") + XCTAssertEqual(array[0] as? Int, 1) + XCTAssertEqual(array[1] as? String, "string") + XCTAssertEqual(array[2] as? Bool, true) + + // Nested array check + guard let nestedArray = array[3] as? [Any?] else { + XCTFail("Element 3 should be an array") + return + } + XCTAssertEqual(nestedArray.count, 2) + XCTAssertEqual(nestedArray[0] as? Int, 2) + XCTAssertEqual(nestedArray[1] as? Int, 3) + + // Nested dictionary check + guard let nestedDict = array[4] as? [String: Any?] else { + XCTFail("Element 4 should be a dictionary") + return + } + XCTAssertEqual(nestedDict.count, 1) + XCTAssertEqual(nestedDict["key"] as? String, "value") + + } catch { + XCTFail("Failed to decode nested array JSON: \(error)") } - func testAreFeaturesReady_AfterFailedFetchSimulation() { - simulateFetchFailure() - // Need to wait briefly for the main queue dispatch in _completeFetch to potentially run - let expectation = XCTestExpectation(description: "Wait for potential completion dispatch") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { expectation.fulfill() } - wait(for: [expectation], timeout: 0.5) - XCTAssertFalse(manager.areFlagsReady(), "Features should not be ready after failed fetch simulation") + // Test with deeply nested object + let nestedObjectJSON = """ + { + "variant_key": "complex_object", + "variant_value": { + "str": "value", + "num": 42, + "bool": true, + "null": null, + "array": [1, 2], + "nested": { + "deeper": { + "deepest": "bottom" + } + } + } + } + """.data(using: .utf8)! + + do { + let decoder = JSONDecoder() + let flagData = try decoder.decode(MixpanelFlagVariant.self, from: nestedObjectJSON) + + XCTAssertEqual(flagData.key, "complex_object") + XCTAssertNotNil(flagData.value, "Value should not be nil") + + // Verify dictionary structure + guard let dict = flagData.value as? [String: Any?] else { + XCTFail("Value should be a dictionary") + return + } + + XCTAssertEqual(dict.count, 6, "Dictionary should have 6 keys") + XCTAssertEqual(dict["str"] as? String, "value") + XCTAssertEqual(dict["num"] as? Int, 42) + XCTAssertEqual(dict["bool"] as? Bool, true) + XCTAssertTrue(dict.keys.contains("null"), "Key 'null' should exist") + if let nullEntry = dict["null"] { + // Key exists with a value of nil (as wanted) + XCTAssertNil(nullEntry, "Value for null key should be nil") + } else { + // Key doesn't exist (which would be wrong) + XCTFail("'null' key should exist in dictionary") + } + + // Check nested array + guard let array = dict["array"] as? [Any?] else { + XCTFail("Array key should contain an array") + return + } + XCTAssertEqual(array.count, 2) + + // Check deeply nested structure + guard let nested = dict["nested"] as? [String: Any?] else { + XCTFail("Nested key should contain dictionary") + return + } + + guard let deeper = nested["deeper"] as? [String: Any?] else { + XCTFail("Deeper key should contain dictionary") + return + } + + XCTAssertEqual(deeper["deepest"] as? String, "bottom") + + } catch { + XCTFail("Failed to decode nested object JSON: \(error)") + } + } + + func testAnyCodableWithInvalidTypes() { + // Test case where variant_value has an unsupported type + // Note: This is harder to test directly since JSON doesn't have many "invalid" types + // We can test error handling by constructing invalid JSON manually + + let unsupportedTypeJSON = """ + { + "variant_key": "invalid_type", + "variant_value": "infinity" + } + """.data(using: .utf8)! + + // This is a valid test since the string will decode properly + do { + let decoder = JSONDecoder() + let flagData = try decoder.decode(MixpanelFlagVariant.self, from: unsupportedTypeJSON) + XCTAssertEqual(flagData.key, "invalid_type") + XCTAssertEqual(flagData.value as? String, "infinity") + } catch { + XCTFail("Should not fail with simple string value: \(error)") } - // --- Load Flags Tests --- + // Test handling of missing variant_value + let missingValueJSON = """ + { + "variant_key": "missing_value" + } + """.data(using: .utf8)! - func testLoadFlags_WhenDisabledInConfig() { - mockDelegate.options = MixpanelOptions(token:"test", featureFlagsEnabled: false) // Explicitly disable - manager.loadFlags() // Call public API - - // Wait to ensure no async fetch operations started changing state - let expectation = XCTestExpectation(description: "Wait briefly") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { expectation.fulfill() } - wait(for: [expectation], timeout: 0.5) - - XCTAssertFalse(manager.areFlagsReady(), "Flags should not become ready if disabled") - // We can't easily check if _fetchFlagsIfNeeded was *not* called without more testability hooks + do { + let decoder = JSONDecoder() + let _ = try decoder.decode(MixpanelFlagVariant.self, from: missingValueJSON) + XCTFail("Decoding should fail with missing variant_value") + } catch { + // This is expected to fail, so the test passes + XCTAssertTrue(error is DecodingError, "Error should be a DecodingError") } + } - // Note: Testing that loadFlags *starts* a fetch is harder now without exposing internal state. - // We test the outcome via the async getFeature tests below. - - // --- Sync Flag Retrieval Tests --- - - func testGetVariantSync_FlagsReady_ExistingFlag() { - simulateFetchSuccess() // Flags loaded - let flagVariant = manager.getVariantSync("feature_string", fallback: defaultFallback) - AssertEqual(flagVariant.key, "v_str") - AssertEqual(flagVariant.value, "test_string") - // Tracking check happens later - } - - func testGetVariantSync_FlagsReady_MissingFlag_UsesFallback() { - simulateFetchSuccess() - let fallback = MixpanelFlagVariant(key: "fb_key", value: "fb_value") - let flagVariant = manager.getVariantSync("missing_feature", fallback: fallback) - AssertEqual(flagVariant.key, fallback.key) - AssertEqual(flagVariant.value, fallback.value) - XCTAssertEqual(mockDelegate.trackedEvents.count, 0, "Should not track for fallback") - } - - func testGetVariantSync_FlagsNotReady_UsesFallback() { - XCTAssertFalse(manager.areFlagsReady()) // Precondition - let fallback = MixpanelFlagVariant(key: "fb_key", value: 999) - let flagVariant = manager.getVariantSync("feature_bool_true", fallback: fallback) - AssertEqual(flagVariant.key, fallback.key) - AssertEqual(flagVariant.value, fallback.value) - XCTAssertEqual(mockDelegate.trackedEvents.count, 0, "Should not track if flags not ready") - } - - func testGetVariantValueSync_FlagsReady() { - simulateFetchSuccess() - let value = manager.getVariantValueSync("feature_int", fallbackValue: -1) - AssertEqual(value, 101) - } - - func testGetVariantValueSync_FlagsReady_MissingFlag() { - simulateFetchSuccess() - let value = manager.getVariantValueSync("missing_feature", fallbackValue: "default") - AssertEqual(value, "default") - } - - func testGetVariantValueSync_FlagsNotReady() { - XCTAssertFalse(manager.areFlagsReady()) - let value = manager.getVariantValueSync("feature_int", fallbackValue: -1) - AssertEqual(value, -1) - } - - func testIsFlagEnabledSync_FlagsReady_True() { - simulateFetchSuccess() - XCTAssertTrue(manager.isEnabledSync("feature_bool_true")) - } - - func testIsFlagEnabledSync_FlagsReady_False() { - simulateFetchSuccess() - XCTAssertFalse(manager.isEnabledSync("feature_bool_false")) - } - - func testIsFlagEnabledSync_FlagsReady_MissingFlag_UsesFallback() { - simulateFetchSuccess() - XCTAssertTrue(manager.isEnabledSync("missing", fallbackValue: true)) - XCTAssertFalse(manager.isEnabledSync("missing", fallbackValue: false)) - } - - func testIsFlagEnabledSync_FlagsReady_NonBoolValue_UsesFallback() { - simulateFetchSuccess() - XCTAssertTrue(manager.isEnabledSync("feature_string", fallbackValue: true)) // String value - XCTAssertFalse(manager.isEnabledSync("feature_int", fallbackValue: false)) // Int value - XCTAssertTrue(manager.isEnabledSync("feature_null", fallbackValue: true)) // Null value - } - - func testIsFlagEnabledSync_FlagsNotReady_UsesFallback() { - XCTAssertFalse(manager.areFlagsReady()) - XCTAssertTrue(manager.isEnabledSync("feature_bool_true", fallbackValue: true)) - XCTAssertFalse(manager.isEnabledSync("feature_bool_true", fallbackValue: false)) - } - - // --- Async Flag Retrieval Tests --- - - func testGetVariant_Async_FlagsReady_ExistingFlag_XCTWaiter() { - // Arrange - simulateFetchSuccess() // Ensure flags are ready - let expectation = XCTestExpectation(description: "Async getFeature ready - XCTWaiter Wait") - var receivedData: MixpanelFlagVariant? - var assertionError: String? - - // Act - manager.getVariant("feature_double", fallback: defaultFallback) { data in - // This completion should run on the main thread - if !Thread.isMainThread { assertionError = "Completion not on main thread (\(Thread.current))" } - receivedData = data - // Perform crucial checks inside completion - if receivedData == nil { assertionError = (assertionError ?? "") + "; Received data was nil" } - if receivedData?.key != "v_double" { assertionError = (assertionError ?? "") + "; Received key mismatch" } - // Add other essential checks if needed - expectation.fulfill() - } - - // Assert - Wait using an explicit XCTWaiter instance - let waiter = XCTWaiter() - let result = waiter.wait(for: [expectation], timeout: 2.0) // Increased timeout - - // Check waiter result and any errors captured in completion - if result != .completed { - XCTFail("XCTWaiter timed out waiting for expectation. Error captured: \(assertionError ?? "None")") - } else if let error = assertionError { - XCTFail("Assertions failed within completion block: \(error)") - } - - // Final check on data after wait - // These might be redundant if checked thoroughly in completion, but good final check - XCTAssertNotNil(receivedData, "Received data should be non-nil after successful wait") - AssertEqual(receivedData?.key, "v_double") - AssertEqual(receivedData?.value, 99.9) - } - - func testGetVariant_Async_FlagsReady_MissingFlag_UsesFallback() { - simulateFetchSuccess() // Flags loaded - let expectation = XCTestExpectation(description: "Async getFeature (Flags Ready, Missing) completes") - let fallback = MixpanelFlagVariant(key: "fb_async", value: -1) - var receivedData: MixpanelFlagVariant? - - manager.getVariant("missing_feature", fallback: fallback) { data in - XCTAssertTrue(Thread.isMainThread, "Completion should be on main thread") - receivedData = data - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1.0) - - XCTAssertNotNil(receivedData) - AssertEqual(receivedData?.key, fallback.key) - AssertEqual(receivedData?.value, fallback.value) - // Check delegate tracking after wait (should not have tracked) - XCTAssertEqual(mockDelegate.trackedEvents.count, 0, "Should not track fallback") - } - - // Test fetch triggering and completion via getFeature when not ready - func testGetVariant_Async_FlagsNotReady_FetchSuccess() { - XCTAssertFalse(manager.areFlagsReady()) - let expectation = XCTestExpectation(description: "Async getFeature (Flags Not Ready) triggers fetch and succeeds") - var receivedData: MixpanelFlagVariant? - - // Setup tracking expectation *before* calling getFeature - mockDelegate.trackExpectation = XCTestExpectation(description: "Tracking call for fetch success") - - // Call getFeature - this should trigger the fetch logic internally - manager.getVariant("feature_int", fallback: defaultFallback) { data in - XCTAssertTrue(Thread.isMainThread, "Completion should be on main thread") - receivedData = data - expectation.fulfill() // Fulfill main expectation - } - - // Crucially, simulate the fetch success *after* getFeature was called. - // Add a slight delay to mimic network latency and allow fetch logic to start. - DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) { - print("Simulating fetch success...") - self.simulateFetchSuccess() // This sets flags and calls _completeFetch - } - - // Wait for BOTH the getFeature completion AND the tracking expectation - wait(for: [expectation, mockDelegate.trackExpectation!], timeout: 3.0) // Increased timeout - - XCTAssertNotNil(receivedData) - AssertEqual(receivedData?.key, "v_int") // Check correct flag data received - AssertEqual(receivedData?.value, 101) - XCTAssertTrue(manager.areFlagsReady(), "Flags should be ready after successful fetch") - XCTAssertEqual(mockDelegate.trackedEvents.count, 1, "Tracking event should have been recorded") - } - - func testGetVariant_Async_FlagsNotReady_FetchFailure() { - XCTAssertFalse(manager.areFlagsReady()) - let expectation = XCTestExpectation(description: "Async getFeature (Flags Not Ready) triggers fetch and fails") - let fallback = MixpanelFlagVariant(key:"fb_fail", value: "failed_fetch") - var receivedData: MixpanelFlagVariant? - - // Call getFeature - manager.getVariant("feature_string", fallback: fallback) { data in - XCTAssertTrue(Thread.isMainThread, "Completion should be on main thread") - receivedData = data - expectation.fulfill() - } - - // Simulate fetch failure after a delay - DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) { - print("Simulating fetch failure...") - self.simulateFetchFailure() // This calls _completeFetch(success: false) - } - - wait(for: [expectation], timeout: 3.0) - - XCTAssertNotNil(receivedData) - AssertEqual(receivedData?.key, fallback.key) // Should receive fallback - AssertEqual(receivedData?.value, fallback.value) - XCTAssertFalse(manager.areFlagsReady(), "Flags should still not be ready after failed fetch") - XCTAssertEqual(mockDelegate.trackedEvents.count, 0, "Should not track on fetch failure/fallback") - } - - - // --- Tracking Tests --- - - func testTracking_CalledOncePerFeature() { - simulateFetchSuccess() // Flags ready - - mockDelegate.trackExpectation = XCTestExpectation(description: "Track called once for feature_bool_true") - mockDelegate.trackExpectation?.expectedFulfillmentCount = 1 // Expect exactly one call - - // Call sync methods multiple times - _ = manager.getVariantSync("feature_bool_true", fallback: defaultFallback) - _ = manager.getVariantValueSync("feature_bool_true", fallbackValue: nil) - _ = manager.isEnabledSync("feature_bool_true") - - // Call async method - let asyncExpectation = XCTestExpectation(description: "Async getFeature completes for tracking test") - manager.getVariant("feature_bool_true", fallback: defaultFallback) { _ in asyncExpectation.fulfill() } - - // Wait for async call AND the track expectation - wait(for: [asyncExpectation, mockDelegate.trackExpectation!], timeout: 2.0) - - // Verify track delegate method was called exactly once - let trueEvents = mockDelegate.trackedEvents.filter { $0.properties?["Experiment name"] as? String == "feature_bool_true" } - XCTAssertEqual(trueEvents.count, 1, "Track should only be called once for the same feature") - - // --- Call for a *different* feature --- - mockDelegate.trackExpectation = XCTestExpectation(description: "Track called for feature_string") - _ = manager.getVariantSync("feature_string", fallback: defaultFallback) - wait(for: [mockDelegate.trackExpectation!], timeout: 1.0) - - let stringEvents = mockDelegate.trackedEvents.filter { $0.properties?["Experiment name"] as? String == "feature_string" } - XCTAssertEqual(stringEvents.count, 1, "Track should be called again for a different feature") - - // Verify total calls - XCTAssertEqual(mockDelegate.trackedEvents.count, 2, "Total track calls should be 2") - } - - func testTracking_SendsCorrectProperties() { - simulateFetchSuccess() - mockDelegate.trackExpectation = XCTestExpectation(description: "Track called for properties check") - - _ = manager.getVariantSync("feature_int", fallback: defaultFallback) // Trigger tracking - - wait(for: [mockDelegate.trackExpectation!], timeout: 1.0) - - XCTAssertEqual(mockDelegate.trackedEvents.count, 1) - let tracked = mockDelegate.trackedEvents[0] - XCTAssertEqual(tracked.event, "$experiment_started") - XCTAssertNotNil(tracked.properties) - - let props = tracked.properties! - AssertEqual(props["Experiment name"] ?? nil, "feature_int") - AssertEqual(props["Variant name"] ?? nil, "v_int") - AssertEqual(props["$experiment_type"] ?? nil, "feature_flag") - } - - func testTracking_DoesNotTrackForFallback_Sync() { - simulateFetchSuccess() // Flags ready - _ = manager.getVariantSync("missing_feature", fallback: MixpanelFlagVariant(key:"fb", value:"v")) // Request missing flag - // Wait briefly to ensure no unexpected tracking call - let expectation = XCTestExpectation(description: "Wait briefly for no track") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { expectation.fulfill() } - wait(for: [expectation], timeout: 0.5) - XCTAssertEqual(mockDelegate.trackedEvents.count, 0, "Track should not be called when a fallback is used (sync)") - } - - func testTracking_DoesNotTrackForFallback_Async() { - simulateFetchSuccess() // Flags ready - let expectation = XCTestExpectation(description: "Async getFeature (Fallback) completes") - - manager.getVariant("missing_feature", fallback: MixpanelFlagVariant(key:"fb", value:"v")) { _ in - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1.0) - // Check delegate tracking after wait - XCTAssertEqual(mockDelegate.trackedEvents.count, 0, "Track should not be called when a fallback is used (async)") - } - - // --- Concurrency Tests --- - - // Test concurrent fetch attempts (via getFeature when not ready) - func testConcurrentGetFeature_WhenNotReady_OnlyOneFetch() { - XCTAssertFalse(manager.areFlagsReady()) - - let numConcurrentCalls = 5 - var expectations: [XCTestExpectation] = [] - var completionResults: [MixpanelFlagVariant?] = Array(repeating: nil, count: numConcurrentCalls) - - // Expect tracking only ONCE for the actual feature if fetch succeeds - mockDelegate.trackExpectation = XCTestExpectation(description: "Track call (should be once)") - mockDelegate.trackExpectation?.expectedFulfillmentCount = 1 - - print("Starting \(numConcurrentCalls) concurrent getFeature calls...") - for i in 0.. FlagsResponse? = { data in - do { - return try JSONDecoder().decode(FlagsResponse.self, from: data) - } catch { - print("Error parsing flags JSON: \(error)") - return nil - } - } - - // Create various test data scenarios - let validJSON = """ - { - "flags": { - "test_flag": { - "variant_key": "test_variant", - "variant_value": "test_value" - } - } - } - """.data(using: .utf8)! - - let emptyFlagsJSON = """ - { - "flags": {} - } - """.data(using: .utf8)! - - let nullFlagsJSON = """ - { - "flags": null - } - """.data(using: .utf8)! - - let malformedJSON = "not json".data(using: .utf8)! - - // Test valid JSON with flags - let validResult = parseResponse(validJSON) - XCTAssertNotNil(validResult, "Parser should handle valid JSON") - XCTAssertNotNil(validResult?.flags, "Flags should be non-nil") - XCTAssertEqual(validResult?.flags?.count, 1, "Should have one flag") - XCTAssertEqual(validResult?.flags?["test_flag"]?.key, "test_variant") - XCTAssertEqual(validResult?.flags?["test_flag"]?.value as? String, "test_value") - - // Test empty flags object - let emptyResult = parseResponse(emptyFlagsJSON) - XCTAssertNotNil(emptyResult, "Parser should handle empty flags object") - XCTAssertNotNil(emptyResult?.flags, "Flags should be non-nil") - XCTAssertEqual(emptyResult?.flags?.count, 0, "Flags should be empty") - - // Test null flags field - let nullResult = parseResponse(nullFlagsJSON) - XCTAssertNotNil(nullResult, "Parser should handle null flags") - XCTAssertNil(nullResult?.flags, "Flags should be nil when null in JSON") - - // Test malformed JSON - let malformedResult = parseResponse(malformedJSON) - XCTAssertNil(malformedResult, "Parser should return nil for malformed JSON") - - // Test with multiple flags - let multipleFlagsJSON = """ - { - "flags": { - "feature_a": { - "variant_key": "variant_a", - "variant_value": true - }, - "feature_b": { - "variant_key": "variant_b", - "variant_value": 42 - }, - "feature_c": { - "variant_key": "variant_c", - "variant_value": null - } - } - } - """.data(using: .utf8)! - - let multiResult = parseResponse(multipleFlagsJSON) - XCTAssertNotNil(multiResult, "Parser should handle multiple flags") - XCTAssertEqual(multiResult?.flags?.count, 3, "Should have three flags") - XCTAssertEqual(multiResult?.flags?["feature_a"]?.value as? Bool, true) - XCTAssertEqual(multiResult?.flags?["feature_b"]?.value as? Int, 42) - XCTAssertNil(multiResult?.flags?["feature_c"]?.value, "Null value should be preserved") - - // Test with missing required fields - let missingFieldJSON = """ - { - "not_flags": {} - } - """.data(using: .utf8)! - - let missingFieldResult = parseResponse(missingFieldJSON) - XCTAssertNotNil(missingFieldResult, "Parser should handle missing flags field") - XCTAssertNil(missingFieldResult?.flags, "Flags should be nil when field is missing") - } - - // --- Delegate Error Handling Tests --- - - func testDelegateNilHandling() { - // Set up with flags ready, but then remove delegate - simulateFetchSuccess() - manager.delegate = nil - - // Test all operations with nil delegate - - // Synchronous operations - let syncData = manager.getVariantSync("feature_bool_true", fallback: defaultFallback) - XCTAssertEqual(syncData.key, "v_true") - XCTAssertEqual(syncData.value as? Bool, true) - - // Async operations - let expectation = XCTestExpectation(description: "Async with nil delegate") - manager.getVariant("feature_int", fallback: defaultFallback) { data in - XCTAssertEqual(data.key, "v_int") - XCTAssertEqual(data.value as? Int, 101) - expectation.fulfill() - } - wait(for: [expectation], timeout: 1.0) - - // No tracking calls should succeed, but operations should still work - // This is "success" as the code doesn't crash when delegate is nil - } - - func testFetchWithNoDelegate() { - // Create manager with no delegate - let noDelegate = FeatureFlagManager(serverURL: "https://test.com", delegate: nil) - - // Try to load flags - noDelegate.loadFlags() - - // Verify no crash; attempt a flag fetch after a short delay - let expectation = XCTestExpectation(description: "Check after attempted fetch") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - XCTAssertFalse(noDelegate.areFlagsReady(), "Flags should not be ready without delegate") - expectation.fulfill() - } - wait(for: [expectation], timeout: 1.0) - } - - func testDelegateConfigDisabledHandling() { - // Set delegate options to disabled - mockDelegate.options = MixpanelOptions(token: "test", featureFlagsEnabled: false) - - // Try to load flags - manager.loadFlags() - - // Verify no fetch is triggered - let expectation = XCTestExpectation(description: "Check disabled options behavior") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - XCTAssertFalse(self.manager.areFlagsReady(), "Flags should not be ready when options disabled") - expectation.fulfill() - } - wait(for: [expectation], timeout: 1.0) - } - - // --- AnyCodable Edge Cases --- - - func testAnyCodableWithComplexTypes() { - // Use reflection to test AnyCodable directly - - // Test with nested array - let nestedArrayJSON = """ - { - "variant_key": "complex_array", - "variant_value": [1, "string", true, [2, 3], {"key": "value"}] - } - """.data(using: .utf8)! - - do { - let decoder = JSONDecoder() - let flagData = try decoder.decode(MixpanelFlagVariant.self, from: nestedArrayJSON) - - XCTAssertEqual(flagData.key, "complex_array") - XCTAssertNotNil(flagData.value, "Value should not be nil") - - // Verify array structure - guard let array = flagData.value as? [Any?] else { - XCTFail("Value should be an array") - return - } - - XCTAssertEqual(array.count, 5, "Array should have 5 elements") - XCTAssertEqual(array[0] as? Int, 1) - XCTAssertEqual(array[1] as? String, "string") - XCTAssertEqual(array[2] as? Bool, true) - - // Nested array check - guard let nestedArray = array[3] as? [Any?] else { - XCTFail("Element 3 should be an array") - return - } - XCTAssertEqual(nestedArray.count, 2) - XCTAssertEqual(nestedArray[0] as? Int, 2) - XCTAssertEqual(nestedArray[1] as? Int, 3) - - // Nested dictionary check - guard let nestedDict = array[4] as? [String: Any?] else { - XCTFail("Element 4 should be a dictionary") - return - } - XCTAssertEqual(nestedDict.count, 1) - XCTAssertEqual(nestedDict["key"] as? String, "value") - - } catch { - XCTFail("Failed to decode nested array JSON: \(error)") - } - - // Test with deeply nested object - let nestedObjectJSON = """ - { - "variant_key": "complex_object", - "variant_value": { - "str": "value", - "num": 42, - "bool": true, - "null": null, - "array": [1, 2], - "nested": { - "deeper": { - "deepest": "bottom" - } - } - } - } - """.data(using: .utf8)! - - do { - let decoder = JSONDecoder() - let flagData = try decoder.decode(MixpanelFlagVariant.self, from: nestedObjectJSON) - - XCTAssertEqual(flagData.key, "complex_object") - XCTAssertNotNil(flagData.value, "Value should not be nil") - - // Verify dictionary structure - guard let dict = flagData.value as? [String: Any?] else { - XCTFail("Value should be a dictionary") - return - } - - XCTAssertEqual(dict.count, 6, "Dictionary should have 6 keys") - XCTAssertEqual(dict["str"] as? String, "value") - XCTAssertEqual(dict["num"] as? Int, 42) - XCTAssertEqual(dict["bool"] as? Bool, true) - XCTAssertTrue(dict.keys.contains("null"), "Key 'null' should exist") - if let nullEntry = dict["null"] { - // Key exists with a value of nil (as wanted) - XCTAssertNil(nullEntry, "Value for null key should be nil") - } else { - // Key doesn't exist (which would be wrong) - XCTFail("'null' key should exist in dictionary") - } - - // Check nested array - guard let array = dict["array"] as? [Any?] else { - XCTFail("Array key should contain an array") - return - } - XCTAssertEqual(array.count, 2) - - // Check deeply nested structure - guard let nested = dict["nested"] as? [String: Any?] else { - XCTFail("Nested key should contain dictionary") - return - } - - guard let deeper = nested["deeper"] as? [String: Any?] else { - XCTFail("Deeper key should contain dictionary") - return - } - - XCTAssertEqual(deeper["deepest"] as? String, "bottom") - - } catch { - XCTFail("Failed to decode nested object JSON: \(error)") - } - } - - func testAnyCodableWithInvalidTypes() { - // Test case where variant_value has an unsupported type - // Note: This is harder to test directly since JSON doesn't have many "invalid" types - // We can test error handling by constructing invalid JSON manually - - let unsupportedTypeJSON = """ - { - "variant_key": "invalid_type", - "variant_value": "infinity" - } - """.data(using: .utf8)! - - // This is a valid test since the string will decode properly - do { - let decoder = JSONDecoder() - let flagData = try decoder.decode(MixpanelFlagVariant.self, from: unsupportedTypeJSON) - XCTAssertEqual(flagData.key, "invalid_type") - XCTAssertEqual(flagData.value as? String, "infinity") - } catch { - XCTFail("Should not fail with simple string value: \(error)") - } - - // Test handling of missing variant_value - let missingValueJSON = """ - { - "variant_key": "missing_value" - } - """.data(using: .utf8)! - - do { - let decoder = JSONDecoder() - let _ = try decoder.decode(MixpanelFlagVariant.self, from: missingValueJSON) - XCTFail("Decoding should fail with missing variant_value") - } catch { - // This is expected to fail, so the test passes - XCTAssertTrue(error is DecodingError, "Error should be a DecodingError") - } - } - -} // End Test Class +} // End Test Class diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelGroupTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelGroupTests.swift index fa62a345..380cf041 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelGroupTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelGroupTests.swift @@ -13,130 +13,140 @@ import XCTest class MixpanelGroupTests: MixpanelBaseTests { - func testGroupSet() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - let p: Properties = ["p1": "a"] - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) - waitForTrackingQueue(testMixpanel) - let msg = groupQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(msg["$group_key"] as! String, groupKey) - XCTAssertEqual(msg["$group_id"] as! String, groupID) - let q = msg["$set"] as! InternalProperties - XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") - removeDBfile(testMixpanel.apiToken) - } + func testGroupSet() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + let p: Properties = ["p1": "a"] + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(msg["$group_key"] as! String, groupKey) + XCTAssertEqual(msg["$group_id"] as! String, groupID) + let q = msg["$set"] as! InternalProperties + XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") + removeDBfile(testMixpanel.apiToken) + } - func testGroupSetIntegerID() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let groupKey = "test_key" - let groupID = 3 - let p: Properties = ["p1": "a"] - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) - waitForTrackingQueue(testMixpanel) - let msg = groupQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(msg["$group_key"] as! String, groupKey) - XCTAssertEqual(msg["$group_id"] as! Int, groupID) - let q = msg["$set"] as! InternalProperties - XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") - removeDBfile(testMixpanel.apiToken) - } + func testGroupSetIntegerID() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let groupKey = "test_key" + let groupID = 3 + let p: Properties = ["p1": "a"] + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(msg["$group_key"] as! String, groupKey) + XCTAssertEqual(msg["$group_id"] as! Int, groupID) + let q = msg["$set"] as! InternalProperties + XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") + removeDBfile(testMixpanel.apiToken) + } - func testGroupSetOnce() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - let p: Properties = ["p1": "a"] - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).setOnce(properties: p) - waitForTrackingQueue(testMixpanel) - let msg = groupQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(msg["$group_key"] as! String, groupKey) - XCTAssertEqual(msg["$group_id"] as! String, groupID) - let q = msg["$set_once"] as! InternalProperties - XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") - removeDBfile(testMixpanel.apiToken) - } + func testGroupSetOnce() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + let p: Properties = ["p1": "a"] + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).setOnce(properties: p) + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(msg["$group_key"] as! String, groupKey) + XCTAssertEqual(msg["$group_id"] as! String, groupID) + let q = msg["$set_once"] as! InternalProperties + XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") + removeDBfile(testMixpanel.apiToken) + } - func testGroupSetTo() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "p1", to: "a") - waitForTrackingQueue(testMixpanel) - let msg = groupQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(msg["$group_key"] as! String, groupKey) - XCTAssertEqual(msg["$group_id"] as! String, groupID) - let p = msg["$set"] as! InternalProperties - XCTAssertEqual(p["p1"] as? String, "a", "custom group property not queued") - removeDBfile(testMixpanel.apiToken) - } + func testGroupSetTo() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(msg["$group_key"] as! String, groupKey) + XCTAssertEqual(msg["$group_id"] as! String, groupID) + let p = msg["$set"] as! InternalProperties + XCTAssertEqual(p["p1"] as? String, "a", "custom group property not queued") + removeDBfile(testMixpanel.apiToken) + } - func testGroupUnset() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).unset(property: "p1") - waitForTrackingQueue(testMixpanel) - let msg = groupQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(msg["$group_key"] as! String, groupKey) - XCTAssertEqual(msg["$group_id"] as! String, groupID) - XCTAssertEqual(msg["$unset"] as! [String], ["p1"], "group property unset not queued") - removeDBfile(testMixpanel.apiToken) - } + func testGroupUnset() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).unset(property: "p1") + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(msg["$group_key"] as! String, groupKey) + XCTAssertEqual(msg["$group_id"] as! String, groupID) + XCTAssertEqual(msg["$unset"] as! [String], ["p1"], "group property unset not queued") + removeDBfile(testMixpanel.apiToken) + } - func testGroupRemove() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).remove(key: "p1", value: "a") - waitForTrackingQueue(testMixpanel) - let msg = groupQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(msg["$group_key"] as! String, groupKey) - XCTAssertEqual(msg["$group_id"] as! String, groupID) - XCTAssertEqual(msg["$remove"] as? [String: String], ["p1": "a"], "group property remove not queued") - removeDBfile(testMixpanel.apiToken) - } + func testGroupRemove() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).remove(key: "p1", value: "a") + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(msg["$group_key"] as! String, groupKey) + XCTAssertEqual(msg["$group_id"] as! String, groupID) + XCTAssertEqual( + msg["$remove"] as? [String: String], ["p1": "a"], "group property remove not queued") + removeDBfile(testMixpanel.apiToken) + } - func testGroupUnion() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).union(key: "p1", values: ["a"]) - waitForTrackingQueue(testMixpanel) - let msg = groupQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(msg["$group_key"] as! String, groupKey) - XCTAssertEqual(msg["$group_id"] as! String, groupID) - XCTAssertEqual(msg["$union"] as? [String: [String]], ["p1": ["a"]], "group property union not queued") - removeDBfile(testMixpanel.apiToken) - } + func testGroupUnion() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).union(key: "p1", values: ["a"]) + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(msg["$group_key"] as! String, groupKey) + XCTAssertEqual(msg["$group_id"] as! String, groupID) + XCTAssertEqual( + msg["$union"] as? [String: [String]], ["p1": ["a"]], "group property union not queued") + removeDBfile(testMixpanel.apiToken) + } - - func testGroupAssertPropertyTypes() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - let p: Properties = ["URL": [Data()]] - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) - } - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "p1", to: [Data()]) - } - removeDBfile(testMixpanel.apiToken) + func testGroupAssertPropertyTypes() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + let p: Properties = ["URL": [Data()]] + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) } - - func testDeleteGroup() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - let groupKey = "test_key" - let groupID = "test_id" - testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).deleteGroup() - waitForTrackingQueue(testMixpanel) - let msg = groupQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual(msg["$group_key"] as! String, groupKey) - XCTAssertEqual(msg["$group_id"] as! String, groupID) - let p: InternalProperties = msg["$delete"] as! InternalProperties - XCTAssertTrue(p.isEmpty, "incorrect group properties: \(p)") - removeDBfile(testMixpanel.apiToken) + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "p1", to: [Data()]) } + removeDBfile(testMixpanel.apiToken) + } + + func testDeleteGroup() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + let groupKey = "test_key" + let groupID = "test_id" + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).deleteGroup() + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual(msg["$group_key"] as! String, groupKey) + XCTAssertEqual(msg["$group_id"] as! String, groupID) + let p: InternalProperties = msg["$delete"] as! InternalProperties + XCTAssertTrue(p.isEmpty, "incorrect group properties: \(p)") + removeDBfile(testMixpanel.apiToken) + } } diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelOptOutTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelOptOutTests.swift index a4fe9554..67f666be 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelOptOutTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelOptOutTests.swift @@ -7,266 +7,332 @@ // import XCTest + @testable import Mixpanel class MixpanelOptOutTests: MixpanelBaseTests { - - func testHasOptOutTrackingFlagBeingSetProperlyAfterInitializedWithOptedOutYES() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(testMixpanel.hasOptedOutTracking(), "When initialize with opted out flag set to YES, the current user should have opted out tracking") - testMixpanel.reset() - removeDBfile(testMixpanel.apiToken) + + func testHasOptOutTrackingFlagBeingSetProperlyAfterInitializedWithOptedOutYES() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + testMixpanel.hasOptedOutTracking(), + "When initialize with opted out flag set to YES, the current user should have opted out tracking" + ) + testMixpanel.reset() + removeDBfile(testMixpanel.apiToken) + } + + func testOptInWillAddOptInEvent() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: false, optOutTrackingByDefault: true) + testMixpanel.optInTracking() + waitForTrackingQueue(testMixpanel) + XCTAssertFalse( + testMixpanel.hasOptedOutTracking(), "The current user should have opted in tracking") + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 1, + "When opted in, event queue should have one even(opt in) being queued") + + if eventQueue(token: testMixpanel.apiToken).count > 0 { + let event = eventQueue(token: testMixpanel.apiToken).first + XCTAssertEqual( + (event!["event"] as? String), "$opt_in", + "When opted in, a track '$opt_in' should have been queued") + } else { + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 1, + "When opted in, event queue should have one even(opt in) being queued") + } + removeDBfile(testMixpanel.apiToken) + } + + func testOptInTrackingForDistinctId() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: false, optOutTrackingByDefault: true) + testMixpanel.optInTracking(distinctId: "testDistinctId") + waitForTrackingQueue(testMixpanel) + XCTAssertFalse( + testMixpanel.hasOptedOutTracking(), "The current user should have opted in tracking") + waitForTrackingQueue(testMixpanel) + let event1 = eventQueue(token: testMixpanel.apiToken).first + let event2 = eventQueue(token: testMixpanel.apiToken).last + XCTAssertTrue( + (event1!["event"] as? String) == "$opt_in" || (event2!["event"] as? String) == "$opt_in", + "When opted in, a track '$opt_in' should have been queued") + XCTAssertEqual( + testMixpanel.distinctId, "testDistinctId", "mixpanel identify failed to set distinct id") + XCTAssertEqual( + testMixpanel.people.distinctId, "testDistinctId", + "mixpanel identify failed to set people distinct id") + XCTAssertTrue( + unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 0, + "identify: should move records from unidentified queue") + removeDBfile(testMixpanel.apiToken) + } + + func testOptInTrackingForDistinctIdAndWithEventProperties() { + let now = Date() + let testProperties: Properties = [ + "string": "yello", + "number": 3, + "date": now, + "$app_version": "override", + ] + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) + testMixpanel.optInTracking(distinctId: "testDistinctId", properties: testProperties) + waitForTrackingQueue(testMixpanel) + waitForTrackingQueue(testMixpanel) + let eventQueueValue = eventQueue(token: testMixpanel.apiToken) + let props = eventQueueValue[0]["properties"] as? InternalProperties + XCTAssertEqual(props!["string"] as? String, "yello") + XCTAssertEqual(props!["number"] as? NSNumber, 3) + compareDate(dateString: props!["date"] as! String, dateDate: now) + XCTAssertEqual( + props!["$app_version"] as? String, "override", "reserved property override failed") + + if eventQueueValue.count > 0 { + let event = eventQueueValue[0] + XCTAssertEqual( + (event["event"] as? String), "$opt_in", + "When opted in, a track '$opt_in' should have been queued") + } else { + XCTAssertTrue( + eventQueueValue.count == 1, + "When opted in, event queue should have one even(opt in) being queued") } - func testOptInWillAddOptInEvent() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: false, optOutTrackingByDefault: true) - testMixpanel.optInTracking() - waitForTrackingQueue(testMixpanel) - XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "The current user should have opted in tracking") - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "When opted in, event queue should have one even(opt in) being queued") + XCTAssertEqual( + testMixpanel.distinctId, "testDistinctId", "mixpanel identify failed to set distinct id") + XCTAssertEqual( + testMixpanel.people.distinctId, "testDistinctId", + "mixpanel identify failed to set people distinct id") + XCTAssertTrue( + unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 0, + "identify: should move records from unidentified queue") + removeDBfile(testMixpanel.apiToken) + } - if eventQueue(token: testMixpanel.apiToken).count > 0 { - let event = eventQueue(token: testMixpanel.apiToken).first - XCTAssertEqual((event!["event"] as? String), "$opt_in", "When opted in, a track '$opt_in' should have been queued") - } - else { - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "When opted in, event queue should have one even(opt in) being queued") - } - removeDBfile(testMixpanel.apiToken) + func testHasOptOutTrackingFlagBeingSetProperlyForMultipleInstances() { + let mixpanel1 = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) + waitForTrackingQueue(mixpanel1) + XCTAssertTrue( + mixpanel1.hasOptedOutTracking(), + "When initialize with opted out flag set to YES, the current user should have opted out tracking" + ) + removeDBfile(mixpanel1.apiToken) + + let mixpanel2 = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: false) + XCTAssertFalse( + mixpanel2.hasOptedOutTracking(), + "When initialize with opted out flag set to NO, the current user should have opted in tracking" + ) + removeDBfile(mixpanel2.apiToken) + } + + func testHasOptOutTrackingFlagBeingSetProperlyAfterInitializedWithOptedOutNO() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: false) + XCTAssertFalse( + testMixpanel.hasOptedOutTracking(), + "When initialize with opted out flag set to NO, the current user should have opted out tracking" + ) + removeDBfile(testMixpanel.apiToken) + } + + func testHasOptOutTrackingFlagBeingSetProperlyByDefault() { + let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true) + XCTAssertFalse( + testMixpanel.hasOptedOutTracking(), + "By default, the current user should not opted out tracking") + removeDBfile(testMixpanel.apiToken) + } + + func testHasOptOutTrackingFlagBeingSetProperlyForOptOut() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + testMixpanel.hasOptedOutTracking(), + "When optOutTracking is called, the current user should have opted out tracking") + removeDBfile(testMixpanel.apiToken) + } + + func testHasOptOutTrackingFlagBeingSetProperlyForOptIn() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + testMixpanel.hasOptedOutTracking(), + "When optOutTracking is called, the current user should have opted out tracking") + testMixpanel.optInTracking() + waitForTrackingQueue(testMixpanel) + XCTAssertFalse( + testMixpanel.hasOptedOutTracking(), + "When optOutTracking is called, the current user should have opted in tracking") + removeDBfile(testMixpanel.apiToken) + } + + func testOptOutTrackingWillNotGenerateEventQueue() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: false, optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) + for i in 0..<50 { + testMixpanel.track(event: "event \(i)") } + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 0, + "When opted out, events should not be queued") + removeDBfile(testMixpanel.apiToken) + } - func testOptInTrackingForDistinctId() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: false, optOutTrackingByDefault: true) - testMixpanel.optInTracking(distinctId: "testDistinctId") - waitForTrackingQueue(testMixpanel) - XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "The current user should have opted in tracking") - waitForTrackingQueue(testMixpanel) - let event1 = eventQueue(token: testMixpanel.apiToken).first - let event2 = eventQueue(token: testMixpanel.apiToken).last - XCTAssertTrue((event1!["event"] as? String) == "$opt_in" || (event2!["event"] as? String) == "$opt_in", "When opted in, a track '$opt_in' should have been queued") - XCTAssertEqual(testMixpanel.distinctId, "testDistinctId", "mixpanel identify failed to set distinct id") - XCTAssertEqual(testMixpanel.people.distinctId, "testDistinctId", "mixpanel identify failed to set people distinct id") - XCTAssertTrue(unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 0, "identify: should move records from unidentified queue") - removeDBfile(testMixpanel.apiToken) + func testOptOutTrackingWillNotGeneratePeopleQueue() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + for i in 0..<50 { + testMixpanel.people.set(property: "p1", to: "\(i)") } + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + peopleQueue(token: testMixpanel.apiToken).count == 0, + "When opted out, events should not be queued") + removeDBfile(testMixpanel.apiToken) + } - func testOptInTrackingForDistinctIdAndWithEventProperties() - { - let now = Date() - let testProperties: Properties = ["string": "yello", - "number": 3, - "date": now, - "$app_version": "override"] - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) - testMixpanel.optInTracking(distinctId: "testDistinctId", properties: testProperties) - waitForTrackingQueue(testMixpanel) - waitForTrackingQueue(testMixpanel) - let eventQueueValue = eventQueue(token: testMixpanel.apiToken) - let props = eventQueueValue[0]["properties"] as? InternalProperties - XCTAssertEqual(props!["string"] as? String, "yello") - XCTAssertEqual(props!["number"] as? NSNumber, 3) - compareDate(dateString: props!["date"] as! String, dateDate: now) - XCTAssertEqual(props!["$app_version"] as? String, "override", "reserved property override failed") + func testOptOutTrackingWillSkipAlias() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + testMixpanel.createAlias("testAlias", distinctId: "aDistinctId") + XCTAssertNotEqual(testMixpanel.alias, "testAlias", "When opted out, alias should not be set") + removeDBfile(testMixpanel.apiToken) + } - if eventQueueValue.count > 0 { - let event = eventQueueValue[0] - XCTAssertEqual((event["event"] as? String), "$opt_in", "When opted in, a track '$opt_in' should have been queued") - } - else { - XCTAssertTrue(eventQueueValue.count == 1, "When opted in, event queue should have one even(opt in) being queued") - } + func testEventBeingTrackedBeforeOptOutShouldNotBeCleared() { + let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true) + testMixpanel.track(event: "a normal event") + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 2, "events should be queued") + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 2, + "When opted out, any events tracked before opted out should not be cleared") + removeDBfile(testMixpanel.apiToken) + } - XCTAssertEqual(testMixpanel.distinctId, "testDistinctId", "mixpanel identify failed to set distinct id") - XCTAssertEqual(testMixpanel.people.distinctId, "testDistinctId", "mixpanel identify failed to set people distinct id") - XCTAssertTrue(unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 0, "identify: should move records from unidentified queue") - removeDBfile(testMixpanel.apiToken) + func testOptOutTrackingRegisterSuperProperties() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) + let properties: Properties = ["p1": "a", "p2": 3, "p3": Date()] + testMixpanel.optOutTracking() + testMixpanel.registerSuperProperties(properties) + waitForTrackingQueue(testMixpanel) + XCTAssertNotEqual( + NSDictionary(dictionary: testMixpanel.currentSuperProperties()), + NSDictionary(dictionary: properties), + "When opted out, register super properties should not be successful") + removeDBfile(testMixpanel.apiToken) + } + + func testOptOutTrackingRegisterSuperPropertiesOnce() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) + let properties: Properties = ["p1": "a", "p2": 3, "p3": Date()] + testMixpanel.optOutTracking() + testMixpanel.registerSuperPropertiesOnce(properties) + waitForTrackingQueue(testMixpanel) + XCTAssertNotEqual( + NSDictionary(dictionary: testMixpanel.currentSuperProperties()), + NSDictionary(dictionary: properties), + "When opted out, register super properties once should not be successful") + removeDBfile(testMixpanel.apiToken) + } + + func testOptOutWilSkipTimeEvent() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: false, optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) + testMixpanel.time(event: "400 Meters") + testMixpanel.track(event: "400 Meters") + 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( + token: randomId(), trackAutomaticEvents: false, flushInterval: 0, + optOutTrackingByDefault: true) + testMixpanel.optInTracking() + waitForTrackingQueue(testMixpanel) + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + for i in 0..<1 { + testMixpanel.people.set(property: "p1", to: "\(i)") } + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + peopleQueue(token: testMixpanel.apiToken).count == 1, + "When opted in, people queue should have been queued") - func testHasOptOutTrackingFlagBeingSetProperlyForMultipleInstances() - { - let mixpanel1 = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) - waitForTrackingQueue(mixpanel1) - XCTAssertTrue(mixpanel1.hasOptedOutTracking(), "When initialize with opted out flag set to YES, the current user should have opted out tracking") - removeDBfile(mixpanel1.apiToken) + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) - let mixpanel2 = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: false) - XCTAssertFalse(mixpanel2.hasOptedOutTracking(), "When initialize with opted out flag set to NO, the current user should have opted in tracking") - removeDBfile(mixpanel2.apiToken) + testMixpanel.flush() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + peopleQueue(token: testMixpanel.apiToken).count == 1, + "When opted out, people queue should not be flushed") + removeDBfile(testMixpanel.apiToken) + } + + func testOptOutByDefaultTrueSkipsFirstAppOpen() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 0, + "When opted out, first app open should not be tracked") + removeDBfile(testMixpanel.apiToken) + } + + func testOptOutWillSkipFlushEvent() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) + waitForTrackingQueue(testMixpanel) + testMixpanel.optInTracking() + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + for i in 0..<1 { + testMixpanel.track(event: "event \(i)") } + waitForTrackingQueue(testMixpanel) + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 2, + "When opted in, events should have been queued") - func testHasOptOutTrackingFlagBeingSetProperlyAfterInitializedWithOptedOutNO() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: false) - XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "When initialize with opted out flag set to NO, the current user should have opted out tracking") - removeDBfile(testMixpanel.apiToken) - } + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) - func testHasOptOutTrackingFlagBeingSetProperlyByDefault() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true) - XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "By default, the current user should not opted out tracking") - removeDBfile(testMixpanel.apiToken) - } + testMixpanel.flush() + waitForTrackingQueue(testMixpanel) - func testHasOptOutTrackingFlagBeingSetProperlyForOptOut() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) - testMixpanel.optOutTracking() - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(testMixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted out tracking") - removeDBfile(testMixpanel.apiToken) - } - - func testHasOptOutTrackingFlagBeingSetProperlyForOptIn() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) - testMixpanel.optOutTracking() - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(testMixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted out tracking") - testMixpanel.optInTracking() - waitForTrackingQueue(testMixpanel) - XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted in tracking") - removeDBfile(testMixpanel.apiToken) - } - - func testOptOutTrackingWillNotGenerateEventQueue() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: false, optOutTrackingByDefault: true) - testMixpanel.optOutTracking() - waitForTrackingQueue(testMixpanel) - for i in 0..<50 { - testMixpanel.track(event: "event \(i)") - } - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 0, "When opted out, events should not be queued") - removeDBfile(testMixpanel.apiToken) - } - - func testOptOutTrackingWillNotGeneratePeopleQueue() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) - testMixpanel.optOutTracking() - for i in 0..<50 { - testMixpanel.people.set(property: "p1", to: "\(i)") - } - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).count == 0, "When opted out, events should not be queued") - removeDBfile(testMixpanel.apiToken) - } - - func testOptOutTrackingWillSkipAlias() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) - testMixpanel.optOutTracking() - testMixpanel.createAlias("testAlias", distinctId: "aDistinctId") - XCTAssertNotEqual(testMixpanel.alias, "testAlias", "When opted out, alias should not be set") - removeDBfile(testMixpanel.apiToken) - } - - func testEventBeingTrackedBeforeOptOutShouldNotBeCleared() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true) - testMixpanel.track(event: "a normal event") - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 2, "events should be queued") - testMixpanel.optOutTracking() - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 2, "When opted out, any events tracked before opted out should not be cleared") - removeDBfile(testMixpanel.apiToken) - } - - func testOptOutTrackingRegisterSuperProperties() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) - let properties: Properties = ["p1": "a", "p2": 3, "p3": Date()] - testMixpanel.optOutTracking() - testMixpanel.registerSuperProperties(properties) - waitForTrackingQueue(testMixpanel) - XCTAssertNotEqual(NSDictionary(dictionary: testMixpanel.currentSuperProperties()), - NSDictionary(dictionary: properties), - "When opted out, register super properties should not be successful") - removeDBfile(testMixpanel.apiToken) - } - - func testOptOutTrackingRegisterSuperPropertiesOnce() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) - let properties: Properties = ["p1": "a", "p2": 3, "p3": Date()] - testMixpanel.optOutTracking() - testMixpanel.registerSuperPropertiesOnce(properties) - waitForTrackingQueue(testMixpanel) - XCTAssertNotEqual(NSDictionary(dictionary: testMixpanel.currentSuperProperties()), - NSDictionary(dictionary: properties), - "When opted out, register super properties once should not be successful") - removeDBfile(testMixpanel.apiToken) - } - - func testOptOutWilSkipTimeEvent() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: false, optOutTrackingByDefault: true) - testMixpanel.optOutTracking() - waitForTrackingQueue(testMixpanel) - testMixpanel.time(event: "400 Meters") - testMixpanel.track(event: "400 Meters") - 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(token: randomId(), trackAutomaticEvents: false, flushInterval: 0, optOutTrackingByDefault: true) - testMixpanel.optInTracking() - waitForTrackingQueue(testMixpanel) - testMixpanel.identify(distinctId: "d1") - waitForTrackingQueue(testMixpanel) - for i in 0..<1 { - testMixpanel.people.set(property: "p1", to: "\(i)") - } - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).count == 1, "When opted in, people queue should have been queued") - - testMixpanel.optOutTracking() - waitForTrackingQueue(testMixpanel) - - testMixpanel.flush() - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).count == 1, "When opted out, people queue should not be flushed") - removeDBfile(testMixpanel.apiToken) - } - - func testOptOutByDefaultTrueSkipsFirstAppOpen() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 0, "When opted out, first app open should not be tracked") - removeDBfile(testMixpanel.apiToken) - } - - func testOptOutWillSkipFlushEvent() - { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, optOutTrackingByDefault: true) - waitForTrackingQueue(testMixpanel) - testMixpanel.optInTracking() - testMixpanel.identify(distinctId: "d1") - waitForTrackingQueue(testMixpanel) - for i in 0..<1 { - testMixpanel.track(event: "event \(i)") - } - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 2, "When opted in, events should have been queued") - - testMixpanel.optOutTracking() - waitForTrackingQueue(testMixpanel) - - testMixpanel.flush() - waitForTrackingQueue(testMixpanel) - - XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 2, "When opted out, events should not be flushed") - removeDBfile(testMixpanel.apiToken) - } + XCTAssertTrue( + eventQueue(token: testMixpanel.apiToken).count == 2, + "When opted out, events should not be flushed") + removeDBfile(testMixpanel.apiToken) + } } diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelPeopleTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelPeopleTests.swift index b2e541f4..4d5560ab 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelPeopleTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelPeopleTests.swift @@ -13,197 +13,230 @@ import XCTest class MixpanelPeopleTests: MixpanelBaseTests { - func testPeopleSet() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - waitForTrackingQueue(testMixpanel) - let p: Properties = ["p1": "a"] - testMixpanel.people.set(properties: p) - waitForTrackingQueue(testMixpanel) - let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties - XCTAssertEqual(q["p1"] as? String, "a", "custom people property not queued") - assertDefaultPeopleProperties(q) - removeDBfile(testMixpanel.apiToken) - } + func testPeopleSet() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + let p: Properties = ["p1": "a"] + testMixpanel.people.set(properties: p) + waitForTrackingQueue(testMixpanel) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties + XCTAssertEqual(q["p1"] as? String, "a", "custom people property not queued") + assertDefaultPeopleProperties(q) + removeDBfile(testMixpanel.apiToken) + } - func testPeopleSetOnce() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - let p: Properties = ["p1": "a"] - testMixpanel.people.setOnce(properties: p) - waitForTrackingQueue(testMixpanel) - let q = peopleQueue(token: testMixpanel.apiToken).last!["$set_once"] as! InternalProperties - XCTAssertEqual(q["p1"] as? String, "a", "custom people property not queued") - assertDefaultPeopleProperties(q) - removeDBfile(testMixpanel.apiToken) - } + func testPeopleSetOnce() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + let p: Properties = ["p1": "a"] + testMixpanel.people.setOnce(properties: p) + waitForTrackingQueue(testMixpanel) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set_once"] as! InternalProperties + XCTAssertEqual(q["p1"] as? String, "a", "custom people property not queued") + assertDefaultPeopleProperties(q) + removeDBfile(testMixpanel.apiToken) + } - func testPeopleSetReservedProperty() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - let p: Properties = ["$ios_app_version": "override"] - testMixpanel.people.set(properties: p) - waitForTrackingQueue(testMixpanel) - let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties - XCTAssertEqual(q["$ios_app_version"] as? String, - "override", - "reserved property override failed") - assertDefaultPeopleProperties(q) - removeDBfile(testMixpanel.apiToken) - } + func testPeopleSetReservedProperty() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + let p: Properties = ["$ios_app_version": "override"] + testMixpanel.people.set(properties: p) + waitForTrackingQueue(testMixpanel) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties + XCTAssertEqual( + q["$ios_app_version"] as? String, + "override", + "reserved property override failed") + assertDefaultPeopleProperties(q) + removeDBfile(testMixpanel.apiToken) + } - func testPeopleSetTo() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - testMixpanel.people.set(property: "p1", to: "a") - waitForTrackingQueue(testMixpanel) - let p: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties - XCTAssertEqual(p["p1"] as? String, "a", "custom people property not queued") - assertDefaultPeopleProperties(p) - removeDBfile(testMixpanel.apiToken) - } + func testPeopleSetTo() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + let p: InternalProperties = + peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties + XCTAssertEqual(p["p1"] as? String, "a", "custom people property not queued") + assertDefaultPeopleProperties(p) + removeDBfile(testMixpanel.apiToken) + } - func testDropUnidentifiedPeopleRecords() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - for i in 0..<505 { - testMixpanel.people.set(property: "i", to: i) - } - waitForTrackingQueue(testMixpanel) - XCTAssertTrue(unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 506) - var r: InternalProperties = unIdentifiedPeopleQueue(token: testMixpanel.apiToken)[1] - XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 0) - r = unIdentifiedPeopleQueue(token: testMixpanel.apiToken).last! - XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 504) - removeDBfile(testMixpanel.apiToken) + func testDropUnidentifiedPeopleRecords() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + for i in 0..<505 { + testMixpanel.people.set(property: "i", to: i) } + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 506) + var r: InternalProperties = unIdentifiedPeopleQueue(token: testMixpanel.apiToken)[1] + XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 0) + r = unIdentifiedPeopleQueue(token: testMixpanel.apiToken).last! + XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 504) + removeDBfile(testMixpanel.apiToken) + } - - func testPeopleAssertPropertyTypes() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - var p: Properties = ["URL": [Data()]] - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.people.set(properties: p) - } - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.people.set(property: "p1", to: [Data()]) - } - p = ["p1": "a"] - // increment should require a number - XCTExpectAssert("unsupported property type was allowed") { - testMixpanel.people.increment(properties: p) - } - removeDBfile(testMixpanel.apiToken) + func testPeopleAssertPropertyTypes() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + var p: Properties = ["URL": [Data()]] + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.people.set(properties: p) } - - func testPeopleIncrement() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - let p: Properties = ["p1": 3] - testMixpanel.people.increment(properties: p) - waitForTrackingQueue(testMixpanel) - let q = peopleQueue(token: testMixpanel.apiToken).last!["$add"] as! InternalProperties - XCTAssertTrue(q.count == 1, "incorrect people properties: \(p)") - XCTAssertEqual(q["p1"] as? Int, 3, "custom people property not queued") - removeDBfile(testMixpanel.apiToken) + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.people.set(property: "p1", to: [Data()]) } - - func testPeopleIncrementBy() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - testMixpanel.people.increment(property: "p1", by: 3) - waitForTrackingQueue(testMixpanel) - let p: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last!["$add"] as! InternalProperties - XCTAssertTrue(p.count == 1, "incorrect people properties: \(p)") - XCTAssertEqual(p["p1"] as? Double, 3, "custom people property not queued") - removeDBfile(testMixpanel.apiToken) + p = ["p1": "a"] + // increment should require a number + XCTExpectAssert("unsupported property type was allowed") { + testMixpanel.people.increment(properties: p) } + removeDBfile(testMixpanel.apiToken) + } - func testPeopleDeleteUser() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - testMixpanel.people.deleteUser() - waitForTrackingQueue(testMixpanel) - let p: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last!["$delete"] as! InternalProperties - XCTAssertTrue(p.isEmpty, "incorrect people properties: \(p)") - removeDBfile(testMixpanel.apiToken) - } + func testPeopleIncrement() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + let p: Properties = ["p1": 3] + testMixpanel.people.increment(properties: p) + waitForTrackingQueue(testMixpanel) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$add"] as! InternalProperties + XCTAssertTrue(q.count == 1, "incorrect people properties: \(p)") + XCTAssertEqual(q["p1"] as? Int, 3, "custom people property not queued") + removeDBfile(testMixpanel.apiToken) + } + func testPeopleIncrementBy() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.increment(property: "p1", by: 3) + waitForTrackingQueue(testMixpanel) + let p: InternalProperties = + peopleQueue(token: testMixpanel.apiToken).last!["$add"] as! InternalProperties + XCTAssertTrue(p.count == 1, "incorrect people properties: \(p)") + XCTAssertEqual(p["p1"] as? Double, 3, "custom people property not queued") + removeDBfile(testMixpanel.apiToken) + } - func testPeopleTrackChargeDecimal() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - testMixpanel.people.trackCharge(amount: 25.34) - waitForTrackingQueue(testMixpanel) - let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! - let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double - let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] - XCTAssertEqual(prop, 25.34) - XCTAssertNotNil(prop2) - removeDBfile(testMixpanel.apiToken) - } + func testPeopleDeleteUser() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.deleteUser() + waitForTrackingQueue(testMixpanel) + let p: InternalProperties = + peopleQueue(token: testMixpanel.apiToken).last!["$delete"] as! InternalProperties + XCTAssertTrue(p.isEmpty, "incorrect people properties: \(p)") + removeDBfile(testMixpanel.apiToken) + } - func testPeopleTrackChargeZero() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - waitForTrackingQueue(testMixpanel) - testMixpanel.people.trackCharge(amount: 0) - waitForTrackingQueue(testMixpanel) - let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! - let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double - let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] - XCTAssertEqual(prop, 0) - XCTAssertNotNil(prop2) - removeDBfile(testMixpanel.apiToken) - } - - func testPeopleTrackChargeWithTime() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - let p: Properties = allPropertyTypes() - testMixpanel.people.trackCharge(amount: 25, properties: ["$time": p["date"]!]) - waitForTrackingQueue(testMixpanel) - let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! - let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double - let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] as? String - XCTAssertEqual(prop, 25) - compareDate(dateString: prop2!, dateDate: p["date"] as! Date) - removeDBfile(testMixpanel.apiToken) - } + func testPeopleTrackChargeDecimal() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.trackCharge(amount: 25.34) + waitForTrackingQueue(testMixpanel) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! + let prop = + ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] + as? Double + let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?[ + "$time"] + XCTAssertEqual(prop, 25.34) + XCTAssertNotNil(prop2) + removeDBfile(testMixpanel.apiToken) + } - func testPeopleTrackChargeWithProperties() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - testMixpanel.people.trackCharge(amount: 25, properties: ["p1": "a"]) - waitForTrackingQueue(testMixpanel) - let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! - let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double - let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["p1"] - XCTAssertEqual(prop, 25) - XCTAssertEqual(prop2 as? String, "a") - removeDBfile(testMixpanel.apiToken) - } + func testPeopleTrackChargeZero() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + testMixpanel.people.trackCharge(amount: 0) + waitForTrackingQueue(testMixpanel) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! + let prop = + ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] + as? Double + let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?[ + "$time"] + XCTAssertEqual(prop, 0) + XCTAssertNotNil(prop2) + removeDBfile(testMixpanel.apiToken) + } - func testPeopleTrackCharge() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - testMixpanel.people.trackCharge(amount: 25) - waitForTrackingQueue(testMixpanel) - let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! - let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double - let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] - XCTAssertEqual(prop, 25) - XCTAssertNotNil(prop2) - removeDBfile(testMixpanel.apiToken) - } + func testPeopleTrackChargeWithTime() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + let p: Properties = allPropertyTypes() + testMixpanel.people.trackCharge(amount: 25, properties: ["$time": p["date"]!]) + waitForTrackingQueue(testMixpanel) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! + let prop = + ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] + as? Double + let prop2 = + ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] + as? String + XCTAssertEqual(prop, 25) + compareDate(dateString: prop2!, dateDate: p["date"] as! Date) + removeDBfile(testMixpanel.apiToken) + } - func testPeopleClearCharges() { - let testMixpanel = Mixpanel.initialize(token: randomId(), trackAutomaticEvents: true, flushInterval: 60) - testMixpanel.identify(distinctId: "d1") - testMixpanel.people.clearCharges() - waitForTrackingQueue(testMixpanel) - let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! - let transactions = (r["$set"] as? InternalProperties)?["$transactions"] as? [MixpanelType] - XCTAssertEqual(transactions?.count, 0) - removeDBfile(testMixpanel.apiToken) - } + func testPeopleTrackChargeWithProperties() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.trackCharge(amount: 25, properties: ["p1": "a"]) + waitForTrackingQueue(testMixpanel) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! + let prop = + ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] + as? Double + let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?[ + "p1"] + XCTAssertEqual(prop, 25) + XCTAssertEqual(prop2 as? String, "a") + removeDBfile(testMixpanel.apiToken) + } + + func testPeopleTrackCharge() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.trackCharge(amount: 25) + waitForTrackingQueue(testMixpanel) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! + let prop = + ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] + as? Double + let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?[ + "$time"] + XCTAssertEqual(prop, 25) + XCTAssertNotNil(prop2) + removeDBfile(testMixpanel.apiToken) + } + + func testPeopleClearCharges() { + let testMixpanel = Mixpanel.initialize( + token: randomId(), trackAutomaticEvents: true, flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.clearCharges() + waitForTrackingQueue(testMixpanel) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! + let transactions = (r["$set"] as? InternalProperties)?["$transactions"] as? [MixpanelType] + XCTAssertEqual(transactions?.count, 0) + removeDBfile(testMixpanel.apiToken) + } } diff --git a/MixpanelDemo/MixpanelDemoTests/TestConstants.swift b/MixpanelDemo/MixpanelDemoTests/TestConstants.swift index c0e3b8b1..3d98d625 100644 --- a/MixpanelDemo/MixpanelDemoTests/TestConstants.swift +++ b/MixpanelDemo/MixpanelDemoTests/TestConstants.swift @@ -12,23 +12,24 @@ import XCTest let kFakeServerUrl = "https://34a272abf23d.com" - extension XCTestCase { - func XCTExpectAssert(_ expectedMessage: String, file: StaticString = #file, line: UInt = #line, block: () -> ()) { - let exp = expectation(description: expectedMessage) + func XCTExpectAssert( + _ expectedMessage: String, file: StaticString = #file, line: UInt = #line, block: () -> Void + ) { + let exp = expectation(description: expectedMessage) - Assertions.assertClosure = { - (condition, message, file, line) in - if !condition { - exp.fulfill() - } - } - - // Call code. - block() - waitForExpectations(timeout: 60, handler: nil) - Assertions.assertClosure = Assertions.swiftAssertClosure + Assertions.assertClosure = { + (condition, message, file, line) in + if !condition { + exp.fulfill() + } } + // Call code. + block() + waitForExpectations(timeout: 60, handler: nil) + Assertions.assertClosure = Assertions.swiftAssertClosure + } + } diff --git a/MixpanelDemo/MixpanelDemoWatch Extension/ExtensionDelegate.swift b/MixpanelDemo/MixpanelDemoWatch Extension/ExtensionDelegate.swift index 413614a3..ebb142cf 100644 --- a/MixpanelDemo/MixpanelDemoWatch Extension/ExtensionDelegate.swift +++ b/MixpanelDemo/MixpanelDemoWatch Extension/ExtensionDelegate.swift @@ -6,57 +6,58 @@ // Copyright © 2019 Mixpanel. All rights reserved. // -import WatchKit - import Mixpanel +import WatchKit class ExtensionDelegate: NSObject, WKExtensionDelegate { - func applicationDidFinishLaunching() { - var ADD_YOUR_MIXPANEL_TOKEN_BELOW_🛠🛠🛠🛠🛠🛠: String - Mixpanel.initialize(token: "MIXPANEL_TOKEN") - Mixpanel.mainInstance().loggingEnabled = true - Mixpanel.mainInstance().registerSuperProperties(["super watch properties": 1]); - // Perform any final initialization of your application. - } + func applicationDidFinishLaunching() { + var ADD_YOUR_MIXPANEL_TOKEN_BELOW_🛠🛠🛠🛠🛠🛠: String + Mixpanel.initialize(token: "MIXPANEL_TOKEN") + Mixpanel.mainInstance().loggingEnabled = true + Mixpanel.mainInstance().registerSuperProperties(["super watch properties": 1]) + // Perform any final initialization of your application. + } - func applicationDidBecomeActive() { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - } + func applicationDidBecomeActive() { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } - func applicationWillResignActive() { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, etc. - } + func applicationWillResignActive() { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, etc. + } - func handle(_ backgroundTasks: Set) { - // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one. - for task in backgroundTasks { - // Use a switch statement to check the task type - switch task { - case let backgroundTask as WKApplicationRefreshBackgroundTask: - // Be sure to complete the background task once you’re done. - backgroundTask.setTaskCompletedWithSnapshot(false) - case let snapshotTask as WKSnapshotRefreshBackgroundTask: - // Snapshot tasks have a unique completion call, make sure to set your expiration date - snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil) - case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask: - // Be sure to complete the connectivity task once you’re done. - connectivityTask.setTaskCompletedWithSnapshot(false) - case let urlSessionTask as WKURLSessionRefreshBackgroundTask: - // Be sure to complete the URL session task once you’re done. - urlSessionTask.setTaskCompletedWithSnapshot(false) - case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask: - // Be sure to complete the relevant-shortcut task once you're done. - relevantShortcutTask.setTaskCompletedWithSnapshot(false) - case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask: - // Be sure to complete the intent-did-run task once you're done. - intentDidRunTask.setTaskCompletedWithSnapshot(false) - default: - // make sure to complete unhandled task types - task.setTaskCompletedWithSnapshot(false) - } - } + func handle(_ backgroundTasks: Set) { + // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one. + for task in backgroundTasks { + // Use a switch statement to check the task type + switch task { + case let backgroundTask as WKApplicationRefreshBackgroundTask: + // Be sure to complete the background task once you’re done. + backgroundTask.setTaskCompletedWithSnapshot(false) + case let snapshotTask as WKSnapshotRefreshBackgroundTask: + // Snapshot tasks have a unique completion call, make sure to set your expiration date + snapshotTask.setTaskCompleted( + restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil + ) + case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask: + // Be sure to complete the connectivity task once you’re done. + connectivityTask.setTaskCompletedWithSnapshot(false) + case let urlSessionTask as WKURLSessionRefreshBackgroundTask: + // Be sure to complete the URL session task once you’re done. + urlSessionTask.setTaskCompletedWithSnapshot(false) + case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask: + // Be sure to complete the relevant-shortcut task once you're done. + relevantShortcutTask.setTaskCompletedWithSnapshot(false) + case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask: + // Be sure to complete the intent-did-run task once you're done. + intentDidRunTask.setTaskCompletedWithSnapshot(false) + default: + // make sure to complete unhandled task types + task.setTaskCompletedWithSnapshot(false) + } } + } } diff --git a/MixpanelDemo/MixpanelDemoWatch Extension/InterfaceController.swift b/MixpanelDemo/MixpanelDemoWatch Extension/InterfaceController.swift index 0ec3beef..f0da5510 100644 --- a/MixpanelDemo/MixpanelDemoWatch Extension/InterfaceController.swift +++ b/MixpanelDemo/MixpanelDemoWatch Extension/InterfaceController.swift @@ -6,51 +6,49 @@ // Copyright © 2019 Mixpanel. All rights reserved. // -import WatchKit import Foundation import Mixpanel +import WatchKit class InterfaceController: WKInterfaceController { - - - @IBOutlet weak var timeSomethingButton: WKInterfaceButton! - - var currentlyTiming = false - override func awake(withContext context: Any?) { - super.awake(withContext: context) - } - - override func willActivate() { - // This method is called when watch view controller is about to be visible to user - super.willActivate() - } - - @IBAction func trackButtonTapped() { - Mixpanel.mainInstance().track(event: "trackButtonTapped") - } - - - @IBAction func timeButtonTapped() { - if !currentlyTiming { - Mixpanel.mainInstance().time(event: "time something") - timeSomethingButton.setTitle("Finish Timing") - } else { - Mixpanel.mainInstance().track(event: "time something") - timeSomethingButton.setTitle("Time Something") - } - currentlyTiming = !currentlyTiming - } - - @IBAction func identifyButtonTapped() { - let watchName = WKInterfaceDevice.current().systemName - Mixpanel.mainInstance().people.set(properties: ["watch": watchName]) - Mixpanel.mainInstance().identify(distinctId: Mixpanel.mainInstance().distinctId) - } - - override func didDeactivate() { - // This method is called when watch view controller is no longer visible - super.didDeactivate() + @IBOutlet weak var timeSomethingButton: WKInterfaceButton! + + var currentlyTiming = false + + override func awake(withContext context: Any?) { + super.awake(withContext: context) + } + + override func willActivate() { + // This method is called when watch view controller is about to be visible to user + super.willActivate() + } + + @IBAction func trackButtonTapped() { + Mixpanel.mainInstance().track(event: "trackButtonTapped") + } + + @IBAction func timeButtonTapped() { + if !currentlyTiming { + Mixpanel.mainInstance().time(event: "time something") + timeSomethingButton.setTitle("Finish Timing") + } else { + Mixpanel.mainInstance().track(event: "time something") + timeSomethingButton.setTitle("Time Something") } + currentlyTiming = !currentlyTiming + } + + @IBAction func identifyButtonTapped() { + let watchName = WKInterfaceDevice.current().systemName + Mixpanel.mainInstance().people.set(properties: ["watch": watchName]) + Mixpanel.mainInstance().identify(distinctId: Mixpanel.mainInstance().distinctId) + } + + override func didDeactivate() { + // This method is called when watch view controller is no longer visible + super.didDeactivate() + } } diff --git a/Package.swift b/Package.swift index d5f1623d..844f6f94 100644 --- a/Package.swift +++ b/Package.swift @@ -3,23 +3,23 @@ import PackageDescription let package = Package( - name: "Mixpanel", - platforms: [ - .iOS(.v12), - .tvOS(.v11), - .macOS(.v10_13), - .watchOS(.v4) - ], - products: [ - .library(name: "Mixpanel", targets: ["Mixpanel"]) - ], - targets: [ - .target( - name: "Mixpanel", - path: "Sources", - resources: [ - .copy("Mixpanel/PrivacyInfo.xcprivacy") - ] - ) - ] + name: "Mixpanel", + platforms: [ + .iOS(.v12), + .tvOS(.v11), + .macOS(.v10_13), + .watchOS(.v4), + ], + products: [ + .library(name: "Mixpanel", targets: ["Mixpanel"]) + ], + targets: [ + .target( + name: "Mixpanel", + path: "Sources", + resources: [ + .copy("Mixpanel/PrivacyInfo.xcprivacy") + ] + ) + ] ) diff --git a/Sources/AutomaticEvents.swift b/Sources/AutomaticEvents.swift index 4413c749..440e5960 100644 --- a/Sources/AutomaticEvents.swift +++ b/Sources/AutomaticEvents.swift @@ -7,143 +7,156 @@ // protocol AEDelegate: AnyObject { - func track(event: String?, properties: Properties?) - func setOnce(properties: Properties) - func increment(property: String, by: Double) + func track(event: String?, properties: Properties?) + func setOnce(properties: Properties) + func increment(property: String, by: Double) } #if os(iOS) || os(tvOS) || os(visionOS) -import Foundation -import UIKit -import StoreKit + import Foundation + import UIKit + import StoreKit + + class AutomaticEvents: NSObject, SKPaymentTransactionObserver, SKProductsRequestDelegate { -class AutomaticEvents: NSObject, SKPaymentTransactionObserver, SKProductsRequestDelegate { - var _minimumSessionDuration: UInt64 = 10000 var minimumSessionDuration: UInt64 { - get { - return _minimumSessionDuration - } - set { - _minimumSessionDuration = newValue - } + get { + return _minimumSessionDuration + } + set { + _minimumSessionDuration = newValue + } } var _maximumSessionDuration: UInt64 = UINT64_MAX var maximumSessionDuration: UInt64 { - get { - return _maximumSessionDuration - } - set { - _maximumSessionDuration = newValue - } + get { + return _maximumSessionDuration + } + set { + _maximumSessionDuration = newValue + } } - + var awaitingTransactions = [String: SKPaymentTransaction]() let defaults = UserDefaults(suiteName: "Mixpanel") weak var delegate: AEDelegate? var sessionLength: TimeInterval = 0 var sessionStartTime: TimeInterval = Date().timeIntervalSince1970 var hasAddedObserver = false - - let awaitingTransactionsWriteLock = DispatchQueue(label: "com.mixpanel.awaiting_transactions_writeLock", qos: .userInitiated, autoreleaseFrequency: .workItem) - + + let awaitingTransactionsWriteLock = DispatchQueue( + label: "com.mixpanel.awaiting_transactions_writeLock", qos: .userInitiated, + autoreleaseFrequency: .workItem) + func initializeEvents(instanceName: String) { - let legacyFirstOpenKey = "MPFirstOpen" - let firstOpenKey = "MPFirstOpen-\(instanceName)" - // do not track `$ae_first_open` again if the legacy key exist, - // but we will start using the key with the mixpanel token in favour of multiple instances support - if let defaults = defaults, !defaults.bool(forKey: legacyFirstOpenKey) { - if !defaults.bool(forKey: firstOpenKey) { - defaults.set(true, forKey: firstOpenKey) - defaults.synchronize() - delegate?.track(event: "$ae_first_open", properties: ["$ae_first_app_open_date": Date()]) - delegate?.setOnce(properties: ["$ae_first_app_open_date": Date()]) - } + let legacyFirstOpenKey = "MPFirstOpen" + let firstOpenKey = "MPFirstOpen-\(instanceName)" + // do not track `$ae_first_open` again if the legacy key exist, + // but we will start using the key with the mixpanel token in favour of multiple instances support + if let defaults = defaults, !defaults.bool(forKey: legacyFirstOpenKey) { + if !defaults.bool(forKey: firstOpenKey) { + defaults.set(true, forKey: firstOpenKey) + defaults.synchronize() + delegate?.track(event: "$ae_first_open", properties: ["$ae_first_app_open_date": Date()]) + delegate?.setOnce(properties: ["$ae_first_app_open_date": Date()]) } - if let defaults = defaults, let infoDict = Bundle.main.infoDictionary { - let appVersionKey = "MPAppVersion" - let appVersionValue = infoDict["CFBundleShortVersionString"] - let savedVersionValue = defaults.string(forKey: appVersionKey) - if let appVersionValue = appVersionValue as? String, - let savedVersionValue = savedVersionValue, - appVersionValue.compare(savedVersionValue, options: .numeric, range: nil, locale: nil) == .orderedDescending { - delegate?.track(event: "$ae_updated", properties: ["$ae_updated_version": appVersionValue]) - defaults.set(appVersionValue, forKey: appVersionKey) - defaults.synchronize() - } else if savedVersionValue == nil { - defaults.set(appVersionValue, forKey: appVersionKey) - defaults.synchronize() - } + } + if let defaults = defaults, let infoDict = Bundle.main.infoDictionary { + let appVersionKey = "MPAppVersion" + let appVersionValue = infoDict["CFBundleShortVersionString"] + let savedVersionValue = defaults.string(forKey: appVersionKey) + if let appVersionValue = appVersionValue as? String, + let savedVersionValue = savedVersionValue, + appVersionValue.compare(savedVersionValue, options: .numeric, range: nil, locale: nil) + == .orderedDescending + { + delegate?.track( + event: "$ae_updated", properties: ["$ae_updated_version": appVersionValue]) + defaults.set(appVersionValue, forKey: appVersionKey) + defaults.synchronize() + } else if savedVersionValue == nil { + defaults.set(appVersionValue, forKey: appVersionKey) + defaults.synchronize() } - - NotificationCenter.default.addObserver(self, - selector: #selector(appWillResignActive(_:)), - name: UIApplication.willResignActiveNotification, - object: nil) - - NotificationCenter.default.addObserver(self, - selector: #selector(appDidBecomeActive(_:)), - name: UIApplication.didBecomeActiveNotification, - object: nil) - - SKPaymentQueue.default().add(self) + } + + NotificationCenter.default.addObserver( + self, + selector: #selector(appWillResignActive(_:)), + name: UIApplication.willResignActiveNotification, + object: nil) + + NotificationCenter.default.addObserver( + self, + selector: #selector(appDidBecomeActive(_:)), + name: UIApplication.didBecomeActiveNotification, + object: nil) + + SKPaymentQueue.default().add(self) } - + @objc func appWillResignActive(_ notification: Notification) { - sessionLength = roundOneDigit(num: Date().timeIntervalSince1970 - sessionStartTime) - if sessionLength >= Double(minimumSessionDuration / 1000) && - sessionLength <= Double(maximumSessionDuration / 1000) { - delegate?.track(event: "$ae_session", properties: ["$ae_session_length": sessionLength]) - delegate?.increment(property: "$ae_total_app_sessions", by: 1) - delegate?.increment(property: "$ae_total_app_session_length", by: sessionLength) - } + sessionLength = roundOneDigit(num: Date().timeIntervalSince1970 - sessionStartTime) + if sessionLength >= Double(minimumSessionDuration / 1000) + && sessionLength <= Double(maximumSessionDuration / 1000) + { + delegate?.track(event: "$ae_session", properties: ["$ae_session_length": sessionLength]) + delegate?.increment(property: "$ae_total_app_sessions", by: 1) + delegate?.increment(property: "$ae_total_app_session_length", by: sessionLength) + } } - + @objc func appDidBecomeActive(_ notification: Notification) { - sessionStartTime = Date().timeIntervalSince1970 + sessionStartTime = Date().timeIntervalSince1970 } - - func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { - var productsRequest = SKProductsRequest() - var productIdentifiers: Set = [] - awaitingTransactionsWriteLock.async { [self] in - for transaction: AnyObject in transactions { - if let trans = transaction as? SKPaymentTransaction { - switch trans.transactionState { - case .purchased: - productIdentifiers.insert(trans.payment.productIdentifier) - awaitingTransactions[trans.payment.productIdentifier] = trans - case .failed: break - case .restored: break - default: break - } - } - } - if !productIdentifiers.isEmpty { - productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers) - productsRequest.delegate = self - productsRequest.start() + + func paymentQueue( + _ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction] + ) { + var productsRequest = SKProductsRequest() + var productIdentifiers: Set = [] + awaitingTransactionsWriteLock.async { [self] in + for transaction: AnyObject in transactions { + if let trans = transaction as? SKPaymentTransaction { + switch trans.transactionState { + case .purchased: + productIdentifiers.insert(trans.payment.productIdentifier) + awaitingTransactions[trans.payment.productIdentifier] = trans + case .failed: break + case .restored: break + default: break } + } } - + if !productIdentifiers.isEmpty { + productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers) + productsRequest.delegate = self + productsRequest.start() + } + } } - + func roundOneDigit(num: TimeInterval) -> TimeInterval { - return round(num * 10.0) / 10.0 + return round(num * 10.0) / 10.0 } - + func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { - awaitingTransactionsWriteLock.async { [self] in - for product in response.products { - if let trans = awaitingTransactions[product.productIdentifier] { - delegate?.track(event: "$ae_iap", properties: ["$ae_iap_price": "\(product.price)", - "$ae_iap_quantity": trans.payment.quantity, - "$ae_iap_name": product.productIdentifier]) - awaitingTransactions.removeValue(forKey: product.productIdentifier) - } - } + awaitingTransactionsWriteLock.async { [self] in + for product in response.products { + if let trans = awaitingTransactions[product.productIdentifier] { + delegate?.track( + event: "$ae_iap", + properties: [ + "$ae_iap_price": "\(product.price)", + "$ae_iap_quantity": trans.payment.quantity, + "$ae_iap_name": product.productIdentifier, + ]) + awaitingTransactions.removeValue(forKey: product.productIdentifier) + } } + } } -} + } #endif diff --git a/Sources/AutomaticProperties.swift b/Sources/AutomaticProperties.swift index 5412ee83..c2ed37b5 100644 --- a/Sources/AutomaticProperties.swift +++ b/Sources/AutomaticProperties.swift @@ -9,146 +9,146 @@ import Foundation #if os(iOS) || os(tvOS) || os(visionOS) -import UIKit + import UIKit #elseif os(macOS) -import Cocoa + import Cocoa #elseif canImport(WatchKit) -import WatchKit + import WatchKit #endif class AutomaticProperties { - static let automaticPropertiesLock = ReadWriteLock(label: "automaticPropertiesLock") + static let automaticPropertiesLock = ReadWriteLock(label: "automaticPropertiesLock") - static var properties: InternalProperties = { - var p = InternalProperties() + static var properties: InternalProperties = { + var p = InternalProperties() - #if os(iOS) || os(tvOS) - var screenSize: CGSize? = nil - screenSize = UIScreen.main.bounds.size - if let screenSize = screenSize { - p["$screen_height"] = Int(screenSize.height) - p["$screen_width"] = Int(screenSize.width) - } - #if targetEnvironment(macCatalyst) - p["$os"] = "macOS" - p["$os_version"] = ProcessInfo.processInfo.operatingSystemVersionString - #else - if AutomaticProperties.isiOSAppOnMac() { - // iOS App Running on Apple Silicon Mac - p["$os"] = "macOS" - // unfortunately, there is no API that reports the correct macOS version - // for "Designed for iPad" apps running on macOS, so we omit it here rather than mis-report - } else { - p["$os"] = UIDevice.current.systemName - p["$os_version"] = UIDevice.current.systemVersion - } - #endif - #elseif os(macOS) - if let screenSize = NSScreen.main?.frame.size { - p["$screen_height"] = Int(screenSize.height) - p["$screen_width"] = Int(screenSize.width) - } - p["$os"] = "macOS" - p["$os_version"] = ProcessInfo.processInfo.operatingSystemVersionString - #elseif os(watchOS) - let watchDevice = WKInterfaceDevice.current() - p["$os"] = watchDevice.systemName - p["$os_version"] = watchDevice.systemVersion - p["$watch_model"] = AutomaticProperties.watchModel() - let screenSize = watchDevice.screenBounds.size - p["$screen_width"] = Int(screenSize.width) - p["$screen_height"] = Int(screenSize.height) - #elseif os(visionOS) - p["$os"] = "visionOS" - p["$os_version"] = UIDevice.current.systemVersion - #endif - - let infoDict = Bundle.main.infoDictionary ?? [:] - p["$app_build_number"] = infoDict["CFBundleVersion"] as? String ?? "Unknown" - p["$app_version_string"] = infoDict["CFBundleShortVersionString"] as? String ?? "Unknown" - - p["mp_lib"] = "swift" - p["$lib_version"] = AutomaticProperties.libVersion() - p["$manufacturer"] = "Apple" - p["$model"] = AutomaticProperties.deviceModel() - - return p - }() - - static var peopleProperties: InternalProperties = { - var p = InternalProperties() - let infoDict = Bundle.main.infoDictionary - if let infoDict = infoDict { - p["$ios_app_version"] = infoDict["CFBundleVersion"] - p["$ios_app_release"] = infoDict["CFBundleShortVersionString"] - } - p["$ios_device_model"] = AutomaticProperties.deviceModel() - #if !os(OSX) && !os(watchOS) && !os(visionOS) - p["$ios_version"] = UIDevice.current.systemVersion - #else - p["$ios_version"] = ProcessInfo.processInfo.operatingSystemVersionString - #endif - p["$ios_lib_version"] = AutomaticProperties.libVersion() - p["$swift_lib_version"] = AutomaticProperties.libVersion() - - return p - }() - - class func deviceModel() -> String { - var modelCode : String = "Unknown" + #if os(iOS) || os(tvOS) + var screenSize: CGSize? = nil + screenSize = UIScreen.main.bounds.size + if let screenSize = screenSize { + p["$screen_height"] = Int(screenSize.height) + p["$screen_width"] = Int(screenSize.width) + } + #if targetEnvironment(macCatalyst) + p["$os"] = "macOS" + p["$os_version"] = ProcessInfo.processInfo.operatingSystemVersionString + #else if AutomaticProperties.isiOSAppOnMac() { - // iOS App Running on Apple Silicon Mac - var size = 0 - sysctlbyname("hw.model", nil, &size, nil, 0) - var model = [CChar](repeating: 0, count: size) - sysctlbyname("hw.model", &model, &size, nil, 0) - modelCode = String(cString: model) + // iOS App Running on Apple Silicon Mac + p["$os"] = "macOS" + // unfortunately, there is no API that reports the correct macOS version + // for "Designed for iPad" apps running on macOS, so we omit it here rather than mis-report } else { - var systemInfo = utsname() - uname(&systemInfo) - let size = MemoryLayout.size - modelCode = withUnsafePointer(to: &systemInfo.machine) { - $0.withMemoryRebound(to: CChar.self, capacity: size) { - String(cString: UnsafePointer($0)) - } - } + p["$os"] = UIDevice.current.systemName + p["$os_version"] = UIDevice.current.systemVersion } - return modelCode - } - - #if os(watchOS) - class func watchModel() -> String { - let watchSize38mm = Int(136) - let watchSize40mm = Int(162) - let watchSize42mm = Int(156) - let watchSize44mm = Int(184) - - let screenWidth = Int(WKInterfaceDevice.current().screenBounds.size.width) - switch screenWidth { - case watchSize38mm: - return "Apple Watch 38mm" - case watchSize40mm: - return "Apple Watch 40mm" - case watchSize42mm: - return "Apple Watch 42mm" - case watchSize44mm: - return "Apple Watch 44mm" - default: - return "Apple Watch" - } - } + #endif + #elseif os(macOS) + if let screenSize = NSScreen.main?.frame.size { + p["$screen_height"] = Int(screenSize.height) + p["$screen_width"] = Int(screenSize.width) + } + p["$os"] = "macOS" + p["$os_version"] = ProcessInfo.processInfo.operatingSystemVersionString + #elseif os(watchOS) + let watchDevice = WKInterfaceDevice.current() + p["$os"] = watchDevice.systemName + p["$os_version"] = watchDevice.systemVersion + p["$watch_model"] = AutomaticProperties.watchModel() + let screenSize = watchDevice.screenBounds.size + p["$screen_width"] = Int(screenSize.width) + p["$screen_height"] = Int(screenSize.height) + #elseif os(visionOS) + p["$os"] = "visionOS" + p["$os_version"] = UIDevice.current.systemVersion #endif - - class func isiOSAppOnMac() -> Bool { - var isiOSAppOnMac = false - if #available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *) { - isiOSAppOnMac = ProcessInfo.processInfo.isiOSAppOnMac - } - return isiOSAppOnMac - } - class func libVersion() -> String { - return "5.1.0" + let infoDict = Bundle.main.infoDictionary ?? [:] + p["$app_build_number"] = infoDict["CFBundleVersion"] as? String ?? "Unknown" + p["$app_version_string"] = infoDict["CFBundleShortVersionString"] as? String ?? "Unknown" + + p["mp_lib"] = "swift" + p["$lib_version"] = AutomaticProperties.libVersion() + p["$manufacturer"] = "Apple" + p["$model"] = AutomaticProperties.deviceModel() + + return p + }() + + static var peopleProperties: InternalProperties = { + var p = InternalProperties() + let infoDict = Bundle.main.infoDictionary + if let infoDict = infoDict { + p["$ios_app_version"] = infoDict["CFBundleVersion"] + p["$ios_app_release"] = infoDict["CFBundleShortVersionString"] } + p["$ios_device_model"] = AutomaticProperties.deviceModel() + #if !os(OSX) && !os(watchOS) && !os(visionOS) + p["$ios_version"] = UIDevice.current.systemVersion + #else + p["$ios_version"] = ProcessInfo.processInfo.operatingSystemVersionString + #endif + p["$ios_lib_version"] = AutomaticProperties.libVersion() + p["$swift_lib_version"] = AutomaticProperties.libVersion() + + return p + }() + + class func deviceModel() -> String { + var modelCode: String = "Unknown" + if AutomaticProperties.isiOSAppOnMac() { + // iOS App Running on Apple Silicon Mac + var size = 0 + sysctlbyname("hw.model", nil, &size, nil, 0) + var model = [CChar](repeating: 0, count: size) + sysctlbyname("hw.model", &model, &size, nil, 0) + modelCode = String(cString: model) + } else { + var systemInfo = utsname() + uname(&systemInfo) + let size = MemoryLayout.size + modelCode = withUnsafePointer(to: &systemInfo.machine) { + $0.withMemoryRebound(to: CChar.self, capacity: size) { + String(cString: UnsafePointer($0)) + } + } + } + return modelCode + } + + #if os(watchOS) + class func watchModel() -> String { + let watchSize38mm = Int(136) + let watchSize40mm = Int(162) + let watchSize42mm = Int(156) + let watchSize44mm = Int(184) + + let screenWidth = Int(WKInterfaceDevice.current().screenBounds.size.width) + switch screenWidth { + case watchSize38mm: + return "Apple Watch 38mm" + case watchSize40mm: + return "Apple Watch 40mm" + case watchSize42mm: + return "Apple Watch 42mm" + case watchSize44mm: + return "Apple Watch 44mm" + default: + return "Apple Watch" + } + } + #endif + + class func isiOSAppOnMac() -> Bool { + var isiOSAppOnMac = false + if #available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *) { + isiOSAppOnMac = ProcessInfo.processInfo.isiOSAppOnMac + } + return isiOSAppOnMac + } + + class func libVersion() -> String { + return "5.1.0" + } } diff --git a/Sources/Constants.swift b/Sources/Constants.swift index 88796d92..1a262483 100644 --- a/Sources/Constants.swift +++ b/Sources/Constants.swift @@ -7,34 +7,35 @@ // import Foundation + #if !os(OSX) -import UIKit -#endif // !os(OSX) + import UIKit +#endif // !os(OSX) struct QueueConstants { - static var queueSize = 5000 + static var queueSize = 5000 } struct APIConstants { - static let maxBatchSize = 50 - static let flushSize = 1000 - static let minRetryBackoff = 60.0 - static let maxRetryBackoff = 600.0 - static let failuresTillBackoff = 2 + static let maxBatchSize = 50 + static let flushSize = 1000 + static let minRetryBackoff = 60.0 + static let maxRetryBackoff = 600.0 + static let failuresTillBackoff = 2 } struct BundleConstants { - static let ID = "com.mixpanel.Mixpanel" + static let ID = "com.mixpanel.Mixpanel" } struct GzipSettings { - static let gzipHeaderOffset = Int32(16) + static let gzipHeaderOffset = Int32(16) } #if !os(OSX) && !os(watchOS) && !os(visionOS) -extension UIDevice { + extension UIDevice { var iPhoneX: Bool { - return UIScreen.main.nativeBounds.height == 2436 + return UIScreen.main.nativeBounds.height == 2436 } -} -#endif // !os(OSX) + } +#endif // !os(OSX) diff --git a/Sources/Data+Compression.swift b/Sources/Data+Compression.swift index dd60f610..9036f956 100644 --- a/Sources/Data+Compression.swift +++ b/Sources/Data+Compression.swift @@ -9,89 +9,97 @@ import Foundation import zlib public enum GzipError: Swift.Error { - case stream - case data - case memory - case buffer - case version - case unknown(code: Int) + case stream + case data + case memory + case buffer + case version + case unknown(code: Int) - init(code: Int32) { - switch code { - case Z_STREAM_ERROR: - self = .stream - case Z_DATA_ERROR: - self = .data - case Z_MEM_ERROR: - self = .memory - case Z_BUF_ERROR: - self = .buffer - case Z_VERSION_ERROR: - self = .version - default: - self = .unknown(code: Int(code)) - } + init(code: Int32) { + switch code { + case Z_STREAM_ERROR: + self = .stream + case Z_DATA_ERROR: + self = .data + case Z_MEM_ERROR: + self = .memory + case Z_BUF_ERROR: + self = .buffer + case Z_VERSION_ERROR: + self = .version + default: + self = .unknown(code: Int(code)) } + } } extension Data { - /// Compresses the data using gzip compression. - /// Adapted from: https://github.com/1024jp/GzipSwift/blob/main/Sources/Gzip/Data%2BGzip.swift - /// - Parameter level: Compression level. - /// - Returns: The compressed data. - /// - Throws: `GzipError` if compression fails. - public func gzipCompressed(level: Int32 = Z_DEFAULT_COMPRESSION) throws -> Data { - guard !self.isEmpty else { - MixpanelLogger.warn(message: "Empty Data object cannot be compressed.") - return Data() - } - - let originalSize = self.count - - var stream = z_stream() - stream.next_in = UnsafeMutablePointer(mutating: (self as NSData).bytes.bindMemory(to: Bytef.self, capacity: self.count)) - stream.avail_in = uint(self.count) - - let windowBits = MAX_WBITS + GzipSettings.gzipHeaderOffset // Use gzip header instead of zlib header - let memLevel = MAX_MEM_LEVEL - let strategy = Z_DEFAULT_STRATEGY - - var status = deflateInit2_(&stream, level, Z_DEFLATED, windowBits, memLevel, strategy, ZLIB_VERSION, Int32(MemoryLayout.size)) - guard status == Z_OK else { - throw GzipError(code: status) - } - - var compressedData = Data(count: self.count / 2) - repeat { - if Int(stream.total_out) >= compressedData.count { - compressedData.count += self.count / 2 - } - let bufferPointer = compressedData.withUnsafeMutableBytes { $0.baseAddress?.assumingMemoryBound(to: Bytef.self) } - guard let bufferPointer = bufferPointer else { - throw GzipError(code: Z_BUF_ERROR) - } - stream.next_out = bufferPointer.advanced(by: Int(stream.total_out)) - stream.avail_out = uint(compressedData.count) - uint(stream.total_out) - - status = deflate(&stream, Z_FINISH) - } while stream.avail_out == 0 && status == Z_OK - - guard status == Z_STREAM_END else { - throw GzipError(code: status) - } - - deflateEnd(&stream) - compressedData.count = Int(stream.total_out) - - let compressedSize = compressedData.count - let compressionRatio = Double(compressedSize) / Double(originalSize) - let compressionPercentage = (1 - compressionRatio) * 100 - - let roundedCompressionRatio = floor(compressionRatio * 1000) / 1000 - let roundedCompressionPercentage = floor(compressionPercentage * 1000) / 1000 - - MixpanelLogger.info(message: "Payload gzipped: original size = \(originalSize) bytes, compressed size = \(compressedSize) bytes, compression ratio = \(roundedCompressionRatio), compression percentage = \(roundedCompressionPercentage)%") - - return compressedData + /// Compresses the data using gzip compression. + /// Adapted from: https://github.com/1024jp/GzipSwift/blob/main/Sources/Gzip/Data%2BGzip.swift + /// - Parameter level: Compression level. + /// - Returns: The compressed data. + /// - Throws: `GzipError` if compression fails. + public func gzipCompressed(level: Int32 = Z_DEFAULT_COMPRESSION) throws -> Data { + guard !self.isEmpty else { + MixpanelLogger.warn(message: "Empty Data object cannot be compressed.") + return Data() } + + let originalSize = self.count + + var stream = z_stream() + stream.next_in = UnsafeMutablePointer( + mutating: (self as NSData).bytes.bindMemory(to: Bytef.self, capacity: self.count)) + stream.avail_in = uint(self.count) + + let windowBits = MAX_WBITS + GzipSettings.gzipHeaderOffset // Use gzip header instead of zlib header + let memLevel = MAX_MEM_LEVEL + let strategy = Z_DEFAULT_STRATEGY + + var status = deflateInit2_( + &stream, level, Z_DEFLATED, windowBits, memLevel, strategy, ZLIB_VERSION, + Int32(MemoryLayout.size)) + guard status == Z_OK else { + throw GzipError(code: status) + } + + var compressedData = Data(count: self.count / 2) + repeat { + if Int(stream.total_out) >= compressedData.count { + compressedData.count += self.count / 2 + } + let bufferPointer = compressedData.withUnsafeMutableBytes { + $0.baseAddress?.assumingMemoryBound(to: Bytef.self) + } + guard let bufferPointer = bufferPointer else { + throw GzipError(code: Z_BUF_ERROR) + } + stream.next_out = bufferPointer.advanced(by: Int(stream.total_out)) + stream.avail_out = uint(compressedData.count) - uint(stream.total_out) + + status = deflate(&stream, Z_FINISH) + } while stream.avail_out == 0 && status == Z_OK + + guard status == Z_STREAM_END else { + throw GzipError(code: status) + } + + deflateEnd(&stream) + compressedData.count = Int(stream.total_out) + + let compressedSize = compressedData.count + let compressionRatio = Double(compressedSize) / Double(originalSize) + let compressionPercentage = (1 - compressionRatio) * 100 + + let roundedCompressionRatio = floor(compressionRatio * 1000) / 1000 + let roundedCompressionPercentage = floor(compressionPercentage * 1000) / 1000 + + MixpanelLogger.info( + message: + "Payload gzipped: original size = \(originalSize) bytes, compressed size = \(compressedSize) bytes, compression ratio = \(roundedCompressionRatio), compression percentage = \(roundedCompressionPercentage)%" + ) + + return compressedData + } } diff --git a/Sources/Error.swift b/Sources/Error.swift index 9708995b..fc8b1c30 100644 --- a/Sources/Error.swift +++ b/Sources/Error.swift @@ -9,34 +9,36 @@ import Foundation enum PropertyError: Error { - case invalidType(type: Any) + case invalidType(type: Any) } class Assertions { - static var assertClosure = swiftAssertClosure - static let swiftAssertClosure = { Swift.assert($0, $1, file: $2, line: $3) } + static var assertClosure = swiftAssertClosure + static let swiftAssertClosure = { Swift.assert($0, $1, file: $2, line: $3) } } -func MPAssert(_ condition: @autoclosure() -> Bool, - _ message: @autoclosure() -> String = "", - file: StaticString = #file, - line: UInt = #line) { - Assertions.assertClosure(condition(), message(), file, line) +func MPAssert( + _ condition: @autoclosure () -> Bool, + _ message: @autoclosure () -> String = "", + file: StaticString = #file, + line: UInt = #line +) { + Assertions.assertClosure(condition(), message(), file, line) } class ErrorHandler { - class func wrap(_ f: () throws -> ReturnType?) -> ReturnType? { - do { - return try f() - } catch let error { - logError(error) - return nil - } + class func wrap(_ f: () throws -> ReturnType?) -> ReturnType? { + do { + return try f() + } catch let error { + logError(error) + return nil } + } - class func logError(_ error: Error) { - let stackSymbols = Thread.callStackSymbols - MixpanelLogger.error(message: "Error: \(error) \n Stack Symbols: \(stackSymbols)") - } + class func logError(_ error: Error) { + let stackSymbols = Thread.callStackSymbols + MixpanelLogger.error(message: "Error: \(error) \n Stack Symbols: \(stackSymbols)") + } } diff --git a/Sources/FeatureFlags.swift b/Sources/FeatureFlags.swift index 68be3605..5887ddcc 100644 --- a/Sources/FeatureFlags.swift +++ b/Sources/FeatureFlags.swift @@ -3,487 +3,511 @@ import Foundation // Wrapper to help decode 'Any' types within Codable structures // (Keep AnyCodable as defined previously, it holds the necessary decoding logic) struct AnyCodable: Decodable { - let value: Any? + let value: Any? - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let intValue = try? container.decode(Int.self) { - value = intValue - } else if let doubleValue = try? container.decode(Double.self) { - value = doubleValue - } else if let stringValue = try? container.decode(String.self) { - value = stringValue - } else if let boolValue = try? container.decode(Bool.self) { - value = boolValue - } else if let arrayValue = try? container.decode([AnyCodable].self) { - value = arrayValue.map { $0.value } - } else if let dictValue = try? container.decode([String: AnyCodable].self) { - value = dictValue.mapValues { $0.value } - } else if container.decodeNil() { - value = nil - } else { - let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unsupported type in AnyCodable.") - throw DecodingError.dataCorrupted(context) - } + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let intValue = try? container.decode(Int.self) { + value = intValue + } else if let doubleValue = try? container.decode(Double.self) { + value = doubleValue + } else if let stringValue = try? container.decode(String.self) { + value = stringValue + } else if let boolValue = try? container.decode(Bool.self) { + value = boolValue + } else if let arrayValue = try? container.decode([AnyCodable].self) { + value = arrayValue.map { $0.value } + } else if let dictValue = try? container.decode([String: AnyCodable].self) { + value = dictValue.mapValues { $0.value } + } else if container.decodeNil() { + value = nil + } else { + let context = DecodingError.Context( + codingPath: decoder.codingPath, debugDescription: "Unsupported type in AnyCodable.") + throw DecodingError.dataCorrupted(context) } + } } - // Represents the variant associated with a feature flag public struct MixpanelFlagVariant: Decodable { - public let key: String // Corresponds to 'variant_key' from API - public let value: Any? // Corresponds to 'variant_value' from API + public let key: String // Corresponds to 'variant_key' from API + public let value: Any? // Corresponds to 'variant_value' from API - enum CodingKeys: String, CodingKey { - case key = "variant_key" - case value = "variant_value" - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - key = try container.decode(String.self, forKey: .key) - - // Directly decode the 'variant_value' using AnyCodable. - // If the key is missing, it throws. - // If the value is null, AnyCodable handles it. - // If the value is an unsupported type, AnyCodable throws. - let anyCodableValue = try container.decode(AnyCodable.self, forKey: .value) - value = anyCodableValue.value // Extract the underlying Any? value - } - - // Helper initializer with fallbacks, value defaults to key if nil - public init(key: String = "", value: Any? = nil) { - self.key = key - if let value = value { - self.value = value - } else { - self.value = key - } + enum CodingKeys: String, CodingKey { + case key = "variant_key" + case value = "variant_value" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + key = try container.decode(String.self, forKey: .key) + + // Directly decode the 'variant_value' using AnyCodable. + // If the key is missing, it throws. + // If the value is null, AnyCodable handles it. + // If the value is an unsupported type, AnyCodable throws. + let anyCodableValue = try container.decode(AnyCodable.self, forKey: .value) + value = anyCodableValue.value // Extract the underlying Any? value + } + + // Helper initializer with fallbacks, value defaults to key if nil + public init(key: String = "", value: Any? = nil) { + self.key = key + if let value = value { + self.value = value + } else { + self.value = key } + } } // Response structure for the /flags endpoint struct FlagsResponse: Decodable { - let flags: [String: MixpanelFlagVariant]? // Dictionary where key is flag name + let flags: [String: MixpanelFlagVariant]? // Dictionary where key is flag name } // --- FeatureFlagDelegate Protocol --- public protocol MixpanelFlagDelegate: AnyObject { - func getOptions() -> MixpanelOptions - func getDistinctId() -> String - func track(event: String?, properties: Properties?) + func getOptions() -> MixpanelOptions + func getDistinctId() -> String + func track(event: String?, properties: Properties?) } /// A protocol defining the public interface for a feature flagging system. public protocol MixpanelFlags { - - /// The delegate responsible for handling feature flag lifecycle events, - /// such as tracking. It is declared `weak` to prevent retain cycles. - var delegate: MixpanelFlagDelegate? { get set } - // --- Public Methods --- + /// The delegate responsible for handling feature flag lifecycle events, + /// such as tracking. It is declared `weak` to prevent retain cycles. + var delegate: MixpanelFlagDelegate? { get set } - /// Initiates the loading or refreshing of flags - func loadFlags() + // --- Public Methods --- - /// Synchronously checks if the flags have been successfully loaded - /// and are available for querying. - /// - /// - Returns: `true` if the flags are loaded and ready for use, `false` otherwise. - func areFlagsReady() -> Bool + /// Initiates the loading or refreshing of flags + func loadFlags() - // --- Sync Flag Retrieval --- + /// Synchronously checks if the flags have been successfully loaded + /// and are available for querying. + /// + /// - Returns: `true` if the flags are loaded and ready for use, `false` otherwise. + func areFlagsReady() -> Bool - /// Synchronously retrieves the complete `MixpanelFlagVariant` for a given flag name. - /// If the feature flag is found and flags are ready, its variant is returned. - /// Otherwise, the provided `fallback` `MixpanelFlagVariant` is returned. - /// This method will also trigger any necessary tracking logic for the accessed flag. - /// - /// - Parameters: - /// - flagName: The unique identifier for the feature flag. - /// - fallback: The `MixpanelFlagVariant` to return if the specified flag is not found - /// or if the flags are not yet loaded. - /// - Returns: The `MixpanelFlagVariant` associated with `flagName`, or the `fallback` variant. - func getVariantSync(_ flagName: String, fallback: MixpanelFlagVariant) -> MixpanelFlagVariant + // --- Sync Flag Retrieval --- - /// Asynchronously retrieves the complete `MixpanelFlagVariant` for a given flag name. - /// If flags are not ready, an attempt will be made to load them. - /// The `completion` handler is called with the `MixpanelFlagVariant` for the flag, - /// or the `fallback` variant if the flag is not found or loading fails. - /// This method will also trigger any necessary tracking logic for the accessed flag. - /// The completion handler is typically invoked on the main thread. - /// - /// - Parameters: - /// - flagName: The unique identifier for the feature flag. - /// - fallback: The `MixpanelFlagVariant` to use as a default if the specified flag - /// is not found or an error occurs during fetching. - /// - completion: A closure that is called with the resulting `MixpanelFlagVariant`. - /// This closure will be executed on the main dispatch queue. - func getVariant(_ flagName: String, fallback: MixpanelFlagVariant, completion: @escaping (MixpanelFlagVariant) -> Void) + /// Synchronously retrieves the complete `MixpanelFlagVariant` for a given flag name. + /// If the feature flag is found and flags are ready, its variant is returned. + /// Otherwise, the provided `fallback` `MixpanelFlagVariant` is returned. + /// This method will also trigger any necessary tracking logic for the accessed flag. + /// + /// - Parameters: + /// - flagName: The unique identifier for the feature flag. + /// - fallback: The `MixpanelFlagVariant` to return if the specified flag is not found + /// or if the flags are not yet loaded. + /// - Returns: The `MixpanelFlagVariant` associated with `flagName`, or the `fallback` variant. + func getVariantSync(_ flagName: String, fallback: MixpanelFlagVariant) -> MixpanelFlagVariant - /// Synchronously retrieves the underlying value of a feature flag. - /// This is a convenience method that extracts the `value` property from the `MixpanelFlagVariant` - /// obtained via `getVariantSync`. - /// - /// - Parameters: - /// - flagName: The unique identifier for the feature flag. - /// - fallbackValue: The default value to return if the flag is not found, - /// its variant doesn't contain a value, or flags are not ready. - /// - Returns: The value of the feature flag, or `fallbackValue`. The type is `Any?`. - func getVariantValueSync(_ flagName: String, fallbackValue: Any?) -> Any? + /// Asynchronously retrieves the complete `MixpanelFlagVariant` for a given flag name. + /// If flags are not ready, an attempt will be made to load them. + /// The `completion` handler is called with the `MixpanelFlagVariant` for the flag, + /// or the `fallback` variant if the flag is not found or loading fails. + /// This method will also trigger any necessary tracking logic for the accessed flag. + /// The completion handler is typically invoked on the main thread. + /// + /// - Parameters: + /// - flagName: The unique identifier for the feature flag. + /// - fallback: The `MixpanelFlagVariant` to use as a default if the specified flag + /// is not found or an error occurs during fetching. + /// - completion: A closure that is called with the resulting `MixpanelFlagVariant`. + /// This closure will be executed on the main dispatch queue. + func getVariant( + _ flagName: String, fallback: MixpanelFlagVariant, + completion: @escaping (MixpanelFlagVariant) -> Void) - /// Asynchronously retrieves the underlying value of a feature flag. - /// This is a convenience method that extracts the `value` property from the `MixpanelFlagVariant` - /// obtained via `getVariant`. If flags are not ready, an attempt will be made to load them. - /// The `completion` handler is called with the flag's value or the `fallbackValue`. - /// The completion handler is typically invoked on the main thread. - /// - /// - Parameters: - /// - flagName: The unique identifier for the feature flag. - /// - fallbackValue: The default value to use if the flag is not found, - /// fetching fails, or its variant doesn't contain a value. - /// - completion: A closure that is called with the resulting value (`Any?`). - /// This closure will be executed on the main dispatch queue. - func getVariantValue(_ flagName: String, fallbackValue: Any?, completion: @escaping (Any?) -> Void) + /// Synchronously retrieves the underlying value of a feature flag. + /// This is a convenience method that extracts the `value` property from the `MixpanelFlagVariant` + /// obtained via `getVariantSync`. + /// + /// - Parameters: + /// - flagName: The unique identifier for the feature flag. + /// - fallbackValue: The default value to return if the flag is not found, + /// its variant doesn't contain a value, or flags are not ready. + /// - Returns: The value of the feature flag, or `fallbackValue`. The type is `Any?`. + func getVariantValueSync(_ flagName: String, fallbackValue: Any?) -> Any? - /// Synchronously checks if a specific feature flag is considered "enabled". - /// This typically involves retrieving the flag's value and evaluating it as a boolean. - /// The exact logic for what constitutes "enabled" (e.g., `true`, non-nil, a specific string) - /// should be defined by the implementing class. - /// - /// - Parameters: - /// - flagName: The unique identifier for the feature flag. - /// - fallbackValue: The boolean value to return if the flag is not found, - /// cannot be evaluated as a boolean, or flags are not ready. Defaults to `false`. - /// - Returns: `true` if the flag is considered enabled, `false` otherwise (including if `fallbackValue` is used). - func isEnabledSync(_ flagName: String, fallbackValue: Bool) -> Bool + /// Asynchronously retrieves the underlying value of a feature flag. + /// This is a convenience method that extracts the `value` property from the `MixpanelFlagVariant` + /// obtained via `getVariant`. If flags are not ready, an attempt will be made to load them. + /// The `completion` handler is called with the flag's value or the `fallbackValue`. + /// The completion handler is typically invoked on the main thread. + /// + /// - Parameters: + /// - flagName: The unique identifier for the feature flag. + /// - fallbackValue: The default value to use if the flag is not found, + /// fetching fails, or its variant doesn't contain a value. + /// - completion: A closure that is called with the resulting value (`Any?`). + /// This closure will be executed on the main dispatch queue. + func getVariantValue( + _ flagName: String, fallbackValue: Any?, completion: @escaping (Any?) -> Void) - /// Asynchronously checks if a specific feature flag is considered "enabled". - /// This typically involves retrieving the flag's value and evaluating it as a boolean. - /// If flags are not ready, an attempt will be made to load them. - /// The `completion` handler is called with the boolean result. - /// The completion handler is typically invoked on the main thread. - /// - /// - Parameters: - /// - flagName: The unique identifier for the feature flag. - /// - fallbackValue: The boolean value to use if the flag is not found, fetching fails, - /// or it cannot be evaluated as a boolean. Defaults to `false`. - /// - completion: A closure that is called with the boolean result. - /// This closure will be executed on the main dispatch queue. - func isEnabled(_ flagName: String, fallbackValue: Bool, completion: @escaping (Bool) -> Void) + /// Synchronously checks if a specific feature flag is considered "enabled". + /// This typically involves retrieving the flag's value and evaluating it as a boolean. + /// The exact logic for what constitutes "enabled" (e.g., `true`, non-nil, a specific string) + /// should be defined by the implementing class. + /// + /// - Parameters: + /// - flagName: The unique identifier for the feature flag. + /// - fallbackValue: The boolean value to return if the flag is not found, + /// cannot be evaluated as a boolean, or flags are not ready. Defaults to `false`. + /// - Returns: `true` if the flag is considered enabled, `false` otherwise (including if `fallbackValue` is used). + func isEnabledSync(_ flagName: String, fallbackValue: Bool) -> Bool + + /// Asynchronously checks if a specific feature flag is considered "enabled". + /// This typically involves retrieving the flag's value and evaluating it as a boolean. + /// If flags are not ready, an attempt will be made to load them. + /// The `completion` handler is called with the boolean result. + /// The completion handler is typically invoked on the main thread. + /// + /// - Parameters: + /// - flagName: The unique identifier for the feature flag. + /// - fallbackValue: The boolean value to use if the flag is not found, fetching fails, + /// or it cannot be evaluated as a boolean. Defaults to `false`. + /// - completion: A closure that is called with the boolean result. + /// This closure will be executed on the main dispatch queue. + func isEnabled(_ flagName: String, fallbackValue: Bool, completion: @escaping (Bool) -> Void) } - // --- FeatureFlagManager Class --- class FeatureFlagManager: Network, MixpanelFlags { - - weak var delegate: MixpanelFlagDelegate? - - // *** Use a SERIAL queue for automatic state serialization *** - let accessQueue = DispatchQueue(label: "com.mixpanel.featureflagmanager.serialqueue") - - // Internal State - Protected by accessQueue - var flags: [String: MixpanelFlagVariant]? = nil - var isFetching: Bool = false - private var trackedFeatures: Set = Set() - private var fetchCompletionHandlers: [(Bool) -> Void] = [] - - // Configuration - private var currentOptions: MixpanelOptions? { delegate?.getOptions() } - private var flagsRoute = "/flags/" - - // Initializers - required init(serverURL: String) { - super.init(serverURL: serverURL) - } - - public init(serverURL: String, delegate: MixpanelFlagDelegate?) { - self.delegate = delegate - super.init(serverURL: serverURL) - } - - // --- Public Methods --- - - func loadFlags() { - // Dispatch fetch trigger to allow caller to continue - // Using the serial queue itself for this background task is fine - accessQueue.async { [weak self] in - self?._fetchFlagsIfNeeded(completion: nil) - } - } - - func areFlagsReady() -> Bool { - // Simple sync read - serial queue ensures this is safe - accessQueue.sync { flags != nil } - } - - // --- Sync Flag Retrieval --- - - func getVariantSync(_ flagName: String, fallback: MixpanelFlagVariant) -> MixpanelFlagVariant { - var flagVariant: MixpanelFlagVariant? - var tracked = false - // === Serial Queue: Single Sync Block for Read AND Track Update === - accessQueue.sync { - guard let currentFlags = self.flags else { return } - - if let variant = currentFlags[flagName] { - flagVariant = variant - - // Perform atomic check-and-set for tracking *within the same sync block* - if !self.trackedFeatures.contains(flagName) { - self.trackedFeatures.insert(flagName) - tracked = true - } - } - // If flag wasn't found, flagVariant remains nil - } - // === End Sync Block === - - // Now, process the results outside the lock - - if let foundVariant = flagVariant { - // If tracking was done *in this call*, call the delegate - if tracked { - self._performTrackingDelegateCall(flagName: flagName, variant: foundVariant) - } - return foundVariant - } else { - print("Info: Flag '\(flagName)' not found or flags not ready. Returning fallback.") - return fallback - } - } - - // --- Async Flag Retrieval --- - - func getVariant(_ flagName: String, fallback: MixpanelFlagVariant, completion: @escaping (MixpanelFlagVariant) -> Void) { - accessQueue.async { [weak self] in // Block A runs serially on accessQueue - guard let self = self else { return } - - var flagVariant: MixpanelFlagVariant? - var needsTrackingCheck = false - var flagsAreCurrentlyReady = false - - // === Access state DIRECTLY within the async block === - // No inner sync needed - we are already synchronized by the serial queue - flagsAreCurrentlyReady = (self.flags != nil) - if flagsAreCurrentlyReady, let currentFlags = self.flags { - if let variant = currentFlags[flagName] { - flagVariant = variant - // Also safe to access trackedFeatures directly here - needsTrackingCheck = !self.trackedFeatures.contains(flagName) - } - } - // === State access finished === - - if flagsAreCurrentlyReady { - let result = flagVariant ?? fallback - if flagVariant != nil, needsTrackingCheck { - // Perform atomic check-and-track. _trackFeatureIfNeeded uses its - // own sync block, which is safe to call from here (it's not nested). - self._trackFlagIfNeeded(flagName: flagName, variant: result) - } - DispatchQueue.main.async { completion(result) } - - } else { - // --- Flags were NOT ready --- - // Trigger fetch; fetch completion will handle calling the original completion handler - print("Flags not ready, attempting fetch for getFeature call...") - self._fetchFlagsIfNeeded { success in - // This completion runs *after* fetch completes (or fails) - let result: MixpanelFlagVariant - if success { - // Fetch succeeded, get the flag SYNCHRONOUSLY - result = self.getVariantSync(flagName, fallback: fallback) - } else { - print("Warning: Failed to fetch flags, returning fallback for \(flagName).") - result = fallback - } - // Call original completion (on main thread) - DispatchQueue.main.async { completion(result) } - } - return // Exit Block A early, fetch completion handles the callback. - - } - } // End accessQueue.async (Block A) - } - - func getVariantValueSync(_ flagName: String, fallbackValue: Any?) -> Any? { - return getVariantSync(flagName, fallback: MixpanelFlagVariant(value: fallbackValue)).value - } - - func getVariantValue(_ flagName: String, fallbackValue: Any?, completion: @escaping (Any?) -> Void) { - getVariant(flagName, fallback: MixpanelFlagVariant(value: fallbackValue)) { flagVariant in - completion(flagVariant.value) - } - } - - func isEnabledSync(_ flagName: String, fallbackValue: Bool = false) -> Bool { - let variantValue = getVariantValueSync(flagName, fallbackValue: fallbackValue) - return self._evaluateBooleanFlag(flagName: flagName, variantValue: variantValue, fallbackValue: fallbackValue) - } - - func isEnabled(_ flagName: String, fallbackValue: Bool = false, completion: @escaping (Bool) -> Void) { - getVariantValue(flagName, fallbackValue: fallbackValue) { [weak self] variantValue in - guard let self = self else { - completion(fallbackValue) - return - } - let result = self._evaluateBooleanFlag(flagName: flagName, variantValue: variantValue, fallbackValue: fallbackValue) - completion(result) - } - } - - // --- Fetching Logic (Simplified by Serial Queue) --- - - // Internal function to handle fetch logic and state checks - private func _fetchFlagsIfNeeded(completion: ((Bool) -> Void)?) { - - var shouldStartFetch = false - let optionsSnapshot = self.currentOptions // Read options directly (safe on accessQueue) - + weak var delegate: MixpanelFlagDelegate? - guard let options = optionsSnapshot, options.featureFlagsEnabled else { - print("Feature flags are disabled, not fetching.") - // Call completion immediately since we know the result and are on the queue. - completion?(false) - return // Exit method - } - - // Access/Modify isFetching and fetchCompletionHandlers directly (safe on accessQueue) - if !self.isFetching { - self.isFetching = true - shouldStartFetch = true - if let completion = completion { - self.fetchCompletionHandlers.append(completion) - } - } else { - print("Fetch already in progress, queueing completion handler.") - if let completion = completion { - self.fetchCompletionHandlers.append(completion) - } - } - // State modifications related to starting the fetch are complete - - if shouldStartFetch { - print("Starting flag fetch (dispatching network request)...") - // Perform network request OUTSIDE the serial accessQueue context - // to avoid blocking the queue during network latency. - // Dispatch the network request initiation to a global queue. - DispatchQueue.global(qos: .utility).async { [weak self] in - self?._performFetchRequest() - } - } - } - - - // Performs the actual network request construction and call - private func _performFetchRequest() { - // This method runs OUTSIDE the accessQueue - - guard let delegate = self.delegate, let options = self.currentOptions else { - print("Error: Delegate or options missing for fetch.") - self._completeFetch(success: false) - return - } - - let distinctId = delegate.getDistinctId() - print("Fetching flags for distinct ID: \(distinctId)") - - var context = options.featureFlagsContext - context["distinct_id"] = distinctId - let requestBodyDict = ["context": context] - - guard let requestBodyData = try? JSONSerialization.data(withJSONObject: requestBodyDict, options: []) else { - print("Error: Failed to serialize request body for flags.") - self._completeFetch(success: false); return - } - guard let authData = "\(options.token):".data(using: .utf8) else { - print("Error: Failed to create auth data."); self._completeFetch(success: false); return - } - let base64Auth = authData.base64EncodedString() - let headers = ["Authorization": "Basic \(base64Auth)", "Content-Type": "application/json"] - let responseParser: (Data) -> FlagsResponse? = { data in - do { return try JSONDecoder().decode(FlagsResponse.self, from: data) } - catch { print("Error parsing flags JSON: \(error)"); return nil } - } - let resource = Network.buildResource(path: flagsRoute, method: .post, requestBody: requestBodyData, headers: headers, parse: responseParser) - - // Make the API request - Network.apiRequest( - base: serverURL, - resource: resource, - failure: { [weak self] reason, data, response in // Completion handlers run on URLSession's queue - print("Error: Failed to fetch flags. Reason: \(reason)") - // Update state and call completions via _completeFetch on the serial queue - self?.accessQueue.async { // Dispatch completion handling to serial queue - self?._completeFetch(success: false) - } - }, - success: { [weak self] (flagsResponse, response) in // Completion handlers run on URLSession's queue - print("Successfully fetched flags.") - guard let self = self else { return } - // Update state and call completions via _completeFetch on the serial queue - self.accessQueue.async { [weak self] in - guard let self = self else { return } - // already on accessQueue – write directly - self.flags = flagsResponse.flags ?? [:] - print("Flags updated: \(self.flags ?? [:])") - self._completeFetch(success: true) // still on accessQueue - } - } - ) - } - - // Centralized fetch completion logic - MUST be called from within accessQueue - func _completeFetch(success: Bool) { - self.isFetching = false - let handlers = self.fetchCompletionHandlers - self.fetchCompletionHandlers.removeAll() + // *** Use a SERIAL queue for automatic state serialization *** + let accessQueue = DispatchQueue(label: "com.mixpanel.featureflagmanager.serialqueue") - DispatchQueue.main.async { - handlers.forEach { $0(success) } - } + // Internal State - Protected by accessQueue + var flags: [String: MixpanelFlagVariant]? = nil + var isFetching: Bool = false + private var trackedFeatures: Set = Set() + private var fetchCompletionHandlers: [(Bool) -> Void] = [] + + // Configuration + private var currentOptions: MixpanelOptions? { delegate?.getOptions() } + private var flagsRoute = "/flags/" + + // Initializers + required init(serverURL: String) { + super.init(serverURL: serverURL) + } + + public init(serverURL: String, delegate: MixpanelFlagDelegate?) { + self.delegate = delegate + super.init(serverURL: serverURL) + } + + // --- Public Methods --- + + func loadFlags() { + // Dispatch fetch trigger to allow caller to continue + // Using the serial queue itself for this background task is fine + accessQueue.async { [weak self] in + self?._fetchFlagsIfNeeded(completion: nil) } - - - // --- Tracking Logic --- - - // Performs the atomic check and triggers delegate call if needed - private func _trackFlagIfNeeded(flagName: String, variant: MixpanelFlagVariant) { - var shouldCallDelegate = false - - // We are already executing on the serial accessQueue, so this is safe. + } + + func areFlagsReady() -> Bool { + // Simple sync read - serial queue ensures this is safe + accessQueue.sync { flags != nil } + } + + // --- Sync Flag Retrieval --- + + func getVariantSync(_ flagName: String, fallback: MixpanelFlagVariant) -> MixpanelFlagVariant { + var flagVariant: MixpanelFlagVariant? + var tracked = false + // === Serial Queue: Single Sync Block for Read AND Track Update === + accessQueue.sync { + guard let currentFlags = self.flags else { return } + + if let variant = currentFlags[flagName] { + flagVariant = variant + + // Perform atomic check-and-set for tracking *within the same sync block* if !self.trackedFeatures.contains(flagName) { - self.trackedFeatures.insert(flagName) - shouldCallDelegate = true + self.trackedFeatures.insert(flagName) + tracked = true } - - // Call delegate *outside* this conceptual block if tracking occurred - // This prevents holding any potential implicit lock during delegate execution - if shouldCallDelegate { - self._performTrackingDelegateCall(flagName: flagName, variant: variant) + } + // If flag wasn't found, flagVariant remains nil + } + // === End Sync Block === + + // Now, process the results outside the lock + + if let foundVariant = flagVariant { + // If tracking was done *in this call*, call the delegate + if tracked { + self._performTrackingDelegateCall(flagName: flagName, variant: foundVariant) + } + return foundVariant + } else { + print("Info: Flag '\(flagName)' not found or flags not ready. Returning fallback.") + return fallback + } + } + + // --- Async Flag Retrieval --- + + func getVariant( + _ flagName: String, fallback: MixpanelFlagVariant, + completion: @escaping (MixpanelFlagVariant) -> Void + ) { + accessQueue.async { [weak self] in // Block A runs serially on accessQueue + guard let self = self else { return } + + var flagVariant: MixpanelFlagVariant? + var needsTrackingCheck = false + var flagsAreCurrentlyReady = false + + // === Access state DIRECTLY within the async block === + // No inner sync needed - we are already synchronized by the serial queue + flagsAreCurrentlyReady = (self.flags != nil) + if flagsAreCurrentlyReady, let currentFlags = self.flags { + if let variant = currentFlags[flagName] { + flagVariant = variant + // Also safe to access trackedFeatures directly here + needsTrackingCheck = !self.trackedFeatures.contains(flagName) } - } - - // Helper to just call the delegate (no locking) - private func _performTrackingDelegateCall(flagName: String, variant: MixpanelFlagVariant) { - guard let delegate = self.delegate else { return } - let properties: Properties = [ - "Experiment name": flagName, "Variant name": variant.key, "$experiment_type": "feature_flag" - ] - // Dispatch delegate call asynchronously to main thread for safety - DispatchQueue.main.async { - delegate.track(event: "$experiment_started", properties: properties) - print("Tracked $experiment_started for \(flagName) (dispatched to main)") + } + // === State access finished === + + if flagsAreCurrentlyReady { + let result = flagVariant ?? fallback + if flagVariant != nil, needsTrackingCheck { + // Perform atomic check-and-track. _trackFeatureIfNeeded uses its + // own sync block, which is safe to call from here (it's not nested). + self._trackFlagIfNeeded(flagName: flagName, variant: result) } + DispatchQueue.main.async { completion(result) } + + } else { + // --- Flags were NOT ready --- + // Trigger fetch; fetch completion will handle calling the original completion handler + print("Flags not ready, attempting fetch for getFeature call...") + self._fetchFlagsIfNeeded { success in + // This completion runs *after* fetch completes (or fails) + let result: MixpanelFlagVariant + if success { + // Fetch succeeded, get the flag SYNCHRONOUSLY + result = self.getVariantSync(flagName, fallback: fallback) + } else { + print("Warning: Failed to fetch flags, returning fallback for \(flagName).") + result = fallback + } + // Call original completion (on main thread) + DispatchQueue.main.async { completion(result) } + } + + return // Exit Block A early, fetch completion handles the callback. + + } + } // End accessQueue.async (Block A) + } + + func getVariantValueSync(_ flagName: String, fallbackValue: Any?) -> Any? { + return getVariantSync(flagName, fallback: MixpanelFlagVariant(value: fallbackValue)).value + } + + func getVariantValue( + _ flagName: String, fallbackValue: Any?, completion: @escaping (Any?) -> Void + ) { + getVariant(flagName, fallback: MixpanelFlagVariant(value: fallbackValue)) { flagVariant in + completion(flagVariant.value) } - - // --- Boolean Evaluation Helper --- - private func _evaluateBooleanFlag(flagName: String, variantValue: Any?, fallbackValue: Bool) -> Bool { - guard let val = variantValue else { return fallbackValue } - if let boolVal = val as? Bool { return boolVal } - else { print("Error: Flag '\(flagName)' is not Bool"); return fallbackValue } + } + + func isEnabledSync(_ flagName: String, fallbackValue: Bool = false) -> Bool { + let variantValue = getVariantValueSync(flagName, fallbackValue: fallbackValue) + return self._evaluateBooleanFlag( + flagName: flagName, variantValue: variantValue, fallbackValue: fallbackValue) + } + + func isEnabled( + _ flagName: String, fallbackValue: Bool = false, completion: @escaping (Bool) -> Void + ) { + getVariantValue(flagName, fallbackValue: fallbackValue) { [weak self] variantValue in + guard let self = self else { + completion(fallbackValue) + return + } + let result = self._evaluateBooleanFlag( + flagName: flagName, variantValue: variantValue, fallbackValue: fallbackValue) + completion(result) } + } + + // --- Fetching Logic (Simplified by Serial Queue) --- + + // Internal function to handle fetch logic and state checks + private func _fetchFlagsIfNeeded(completion: ((Bool) -> Void)?) { + + var shouldStartFetch = false + let optionsSnapshot = self.currentOptions // Read options directly (safe on accessQueue) + + guard let options = optionsSnapshot, options.featureFlagsEnabled else { + print("Feature flags are disabled, not fetching.") + // Call completion immediately since we know the result and are on the queue. + completion?(false) + return // Exit method + } + + // Access/Modify isFetching and fetchCompletionHandlers directly (safe on accessQueue) + if !self.isFetching { + self.isFetching = true + shouldStartFetch = true + if let completion = completion { + self.fetchCompletionHandlers.append(completion) + } + } else { + print("Fetch already in progress, queueing completion handler.") + if let completion = completion { + self.fetchCompletionHandlers.append(completion) + } + } + // State modifications related to starting the fetch are complete + + if shouldStartFetch { + print("Starting flag fetch (dispatching network request)...") + // Perform network request OUTSIDE the serial accessQueue context + // to avoid blocking the queue during network latency. + // Dispatch the network request initiation to a global queue. + DispatchQueue.global(qos: .utility).async { [weak self] in + self?._performFetchRequest() + } + } + } + + // Performs the actual network request construction and call + private func _performFetchRequest() { + // This method runs OUTSIDE the accessQueue + + guard let delegate = self.delegate, let options = self.currentOptions else { + print("Error: Delegate or options missing for fetch.") + self._completeFetch(success: false) + return + } + + let distinctId = delegate.getDistinctId() + print("Fetching flags for distinct ID: \(distinctId)") + + var context = options.featureFlagsContext + context["distinct_id"] = distinctId + let requestBodyDict = ["context": context] + + guard + let requestBodyData = try? JSONSerialization.data( + withJSONObject: requestBodyDict, options: []) + else { + print("Error: Failed to serialize request body for flags.") + self._completeFetch(success: false) + return + } + guard let authData = "\(options.token):".data(using: .utf8) else { + print("Error: Failed to create auth data.") + self._completeFetch(success: false) + return + } + let base64Auth = authData.base64EncodedString() + let headers = ["Authorization": "Basic \(base64Auth)", "Content-Type": "application/json"] + let responseParser: (Data) -> FlagsResponse? = { data in + do { return try JSONDecoder().decode(FlagsResponse.self, from: data) } catch { + print("Error parsing flags JSON: \(error)") + return nil + } + } + let resource = Network.buildResource( + path: flagsRoute, method: .post, requestBody: requestBodyData, headers: headers, + parse: responseParser) + + // Make the API request + Network.apiRequest( + base: serverURL, + resource: resource, + failure: { [weak self] reason, data, response in // Completion handlers run on URLSession's queue + print("Error: Failed to fetch flags. Reason: \(reason)") + // Update state and call completions via _completeFetch on the serial queue + self?.accessQueue.async { // Dispatch completion handling to serial queue + self?._completeFetch(success: false) + } + }, + success: { [weak self] (flagsResponse, response) in // Completion handlers run on URLSession's queue + print("Successfully fetched flags.") + guard let self = self else { return } + // Update state and call completions via _completeFetch on the serial queue + self.accessQueue.async { [weak self] in + guard let self = self else { return } + // already on accessQueue – write directly + self.flags = flagsResponse.flags ?? [:] + print("Flags updated: \(self.flags ?? [:])") + self._completeFetch(success: true) // still on accessQueue + } + } + ) + } + + // Centralized fetch completion logic - MUST be called from within accessQueue + func _completeFetch(success: Bool) { + self.isFetching = false + let handlers = self.fetchCompletionHandlers + self.fetchCompletionHandlers.removeAll() + + DispatchQueue.main.async { + handlers.forEach { $0(success) } + } + } + + // --- Tracking Logic --- + + // Performs the atomic check and triggers delegate call if needed + private func _trackFlagIfNeeded(flagName: String, variant: MixpanelFlagVariant) { + var shouldCallDelegate = false + + // We are already executing on the serial accessQueue, so this is safe. + if !self.trackedFeatures.contains(flagName) { + self.trackedFeatures.insert(flagName) + shouldCallDelegate = true + } + + // Call delegate *outside* this conceptual block if tracking occurred + // This prevents holding any potential implicit lock during delegate execution + if shouldCallDelegate { + self._performTrackingDelegateCall(flagName: flagName, variant: variant) + } + } + + // Helper to just call the delegate (no locking) + private func _performTrackingDelegateCall(flagName: String, variant: MixpanelFlagVariant) { + guard let delegate = self.delegate else { return } + let properties: Properties = [ + "Experiment name": flagName, "Variant name": variant.key, "$experiment_type": "feature_flag", + ] + // Dispatch delegate call asynchronously to main thread for safety + DispatchQueue.main.async { + delegate.track(event: "$experiment_started", properties: properties) + print("Tracked $experiment_started for \(flagName) (dispatched to main)") + } + } + + // --- Boolean Evaluation Helper --- + private func _evaluateBooleanFlag(flagName: String, variantValue: Any?, fallbackValue: Bool) + -> Bool + { + guard let val = variantValue else { return fallbackValue } + if let boolVal = val as? Bool { + return boolVal + } else { + print("Error: Flag '\(flagName)' is not Bool") + return fallbackValue + } + } } diff --git a/Sources/FileLogging.swift b/Sources/FileLogging.swift index 017c4af2..e1e3436e 100644 --- a/Sources/FileLogging.swift +++ b/Sources/FileLogging.swift @@ -10,30 +10,31 @@ import Foundation /// Logs all messages to a file class FileLogging: MixpanelLogging { - private let fileHandle: FileHandle + private let fileHandle: FileHandle - init(path: String) { - if let handle = FileHandle(forWritingAtPath: path) { - fileHandle = handle - } else { - fileHandle = FileHandle.standardError - } - - // Move to the end of the file so we can append messages - fileHandle.seekToEndOfFile() + init(path: String) { + if let handle = FileHandle(forWritingAtPath: path) { + fileHandle = handle + } else { + fileHandle = FileHandle.standardError } - deinit { - // Ensure we close the file handle to clear the resources - fileHandle.closeFile() - } + // Move to the end of the file so we can append messages + fileHandle.seekToEndOfFile() + } - func addMessage(message: MixpanelLogMessage) { - let string = "File: \(message.file) - Func: \(message.function) - " + - "Level: \(message.level.rawValue) - Message: \(message.text)" - if let data = string.data(using: String.Encoding.utf8) { - // Write the message as data to the file - fileHandle.write(data) - } + deinit { + // Ensure we close the file handle to clear the resources + fileHandle.closeFile() + } + + func addMessage(message: MixpanelLogMessage) { + let string = + "File: \(message.file) - Func: \(message.function) - " + + "Level: \(message.level.rawValue) - Message: \(message.text)" + if let data = string.data(using: String.Encoding.utf8) { + // Write the message as data to the file + fileHandle.write(data) } + } } diff --git a/Sources/Flush.swift b/Sources/Flush.swift index 389ad6ff..7c9a08a0 100644 --- a/Sources/Flush.swift +++ b/Sources/Flush.swift @@ -9,172 +9,184 @@ import Foundation protocol FlushDelegate: AnyObject { - func flush(performFullFlush: Bool, completion: (() -> Void)?) - func flushSuccess(type: FlushType, ids: [Int32]) - - #if os(iOS) + func flush(performFullFlush: Bool, completion: (() -> Void)?) + func flushSuccess(type: FlushType, ids: [Int32]) + + #if os(iOS) func updateNetworkActivityIndicator(_ on: Bool) - #endif // os(iOS) + #endif // os(iOS) } class Flush: AppLifecycle { - var timer: Timer? - weak var delegate: FlushDelegate? - var useIPAddressForGeoLocation = true - var flushRequest: FlushRequest - var flushOnBackground = true - var _flushInterval = 0.0 - var _flushBatchSize = APIConstants.maxBatchSize - private var _serverURL = BasePath.DefaultMixpanelAPI - private let flushRequestReadWriteLock: DispatchQueue + var timer: Timer? + weak var delegate: FlushDelegate? + var useIPAddressForGeoLocation = true + var flushRequest: FlushRequest + var flushOnBackground = true + var _flushInterval = 0.0 + var _flushBatchSize = APIConstants.maxBatchSize + private var _serverURL = BasePath.DefaultMixpanelAPI + private let flushRequestReadWriteLock: DispatchQueue - var useGzipCompression: Bool - - var serverURL: String { - get { - flushRequestReadWriteLock.sync { - return _serverURL - } - } - set { - flushRequestReadWriteLock.sync(flags: .barrier, execute: { - _serverURL = newValue - self.flushRequest.serverURL = newValue - }) - } - } - - var flushInterval: Double { - get { - flushRequestReadWriteLock.sync { - return _flushInterval - } - } - set { - flushRequestReadWriteLock.sync(flags: .barrier, execute: { - _flushInterval = newValue - }) + var useGzipCompression: Bool - delegate?.flush(performFullFlush: false, completion: nil) - startFlushTimer() - } + var serverURL: String { + get { + flushRequestReadWriteLock.sync { + return _serverURL + } } - - var flushBatchSize: Int { - get { - return _flushBatchSize - } - set { - _flushBatchSize = newValue - } + set { + flushRequestReadWriteLock.sync( + flags: .barrier, + execute: { + _serverURL = newValue + self.flushRequest.serverURL = newValue + }) } + } - required init(serverURL: String, useGzipCompression: Bool) { - self.flushRequest = FlushRequest(serverURL: serverURL) - self.useGzipCompression = useGzipCompression - _serverURL = serverURL - flushRequestReadWriteLock = DispatchQueue(label: "com.mixpanel.flush_interval.lock", qos: .utility, attributes: .concurrent, autoreleaseFrequency: .workItem) + var flushInterval: Double { + get { + flushRequestReadWriteLock.sync { + return _flushInterval + } } + set { + flushRequestReadWriteLock.sync( + flags: .barrier, + execute: { + _flushInterval = newValue + }) - func flushQueue(_ queue: Queue, type: FlushType, headers: [String: String], queryItems: [URLQueryItem]) { - if flushRequest.requestNotAllowed() { - return - } - flushQueueInBatches(queue, type: type, headers: headers, queryItems: queryItems) + delegate?.flush(performFullFlush: false, completion: nil) + startFlushTimer() } + } - func startFlushTimer() { - stopFlushTimer() - DispatchQueue.main.async { [weak self] in - guard let self = self else { - return - } - - if self.flushInterval > 0 { - self.timer?.invalidate() - self.timer = Timer.scheduledTimer(timeInterval: self.flushInterval, - target: self, - selector: #selector(self.flushSelector), - userInfo: nil, - repeats: true) - } - } + var flushBatchSize: Int { + get { + return _flushBatchSize } - - @objc func flushSelector() { - delegate?.flush(performFullFlush: false, completion: nil) + set { + _flushBatchSize = newValue } + } - func stopFlushTimer() { - if let timer = timer { - DispatchQueue.main.async { [weak self, timer] in - timer.invalidate() - self?.timer = nil - } - } - } + required init(serverURL: String, useGzipCompression: Bool) { + self.flushRequest = FlushRequest(serverURL: serverURL) + self.useGzipCompression = useGzipCompression + _serverURL = serverURL + flushRequestReadWriteLock = DispatchQueue( + label: "com.mixpanel.flush_interval.lock", qos: .utility, attributes: .concurrent, + autoreleaseFrequency: .workItem) + } - func flushQueueInBatches(_ queue: Queue, type: FlushType, headers: [String: String], queryItems: [URLQueryItem]) { - var mutableQueue = queue - while !mutableQueue.isEmpty { - let batchSize = min(mutableQueue.count, flushBatchSize) - let range = 0.. Queue { - var shadowQueue = queue - let range = 0.. lastIndex { - shadowQueue.removeSubrange(range) + flushQueueInBatches(queue, type: type, headers: headers, queryItems: queryItems) + } + + func startFlushTimer() { + stopFlushTimer() + DispatchQueue.main.async { [weak self] in + guard let self = self else { + return + } + + if self.flushInterval > 0 { + self.timer?.invalidate() + self.timer = Timer.scheduledTimer( + timeInterval: self.flushInterval, + target: self, + selector: #selector(self.flushSelector), + userInfo: nil, + repeats: true) + } + } + } + + @objc func flushSelector() { + delegate?.flush(performFullFlush: false, completion: nil) + } + + func stopFlushTimer() { + if let timer = timer { + DispatchQueue.main.async { [weak self, timer] in + timer.invalidate() + self?.timer = nil + } + } + } + + func flushQueueInBatches( + _ queue: Queue, type: FlushType, headers: [String: String], queryItems: [URLQueryItem] + ) { + var mutableQueue = queue + while !mutableQueue.isEmpty { + let batchSize = min(mutableQueue.count, flushBatchSize) + let range = 0.. Queue { + var shadowQueue = queue + let range = 0.. lastIndex { + shadowQueue.removeSubrange(range) + } else { + shadowQueue.removeAll() } + return shadowQueue + } - func applicationWillResignActive() { - stopFlushTimer() - } + // MARK: - Lifecycle + func applicationDidBecomeActive() { + startFlushTimer() + } + + func applicationWillResignActive() { + stopFlushTimer() + } } - diff --git a/Sources/FlushRequest.swift b/Sources/FlushRequest.swift index d428e67b..229aa2b7 100644 --- a/Sources/FlushRequest.swift +++ b/Sources/FlushRequest.swift @@ -9,110 +9,121 @@ import Foundation enum FlushType: String { - case events = "/track/" - case people = "/engage/" - case groups = "/groups/" + case events = "/track/" + case people = "/engage/" + case groups = "/groups/" } class FlushRequest: Network { - var networkRequestsAllowedAfterTime = 0.0 - var networkConsecutiveFailures = 0 + var networkRequestsAllowedAfterTime = 0.0 + var networkConsecutiveFailures = 0 - func sendRequest(_ requestData: String, - type: FlushType, - useIP: Bool, - headers: [String: String], - queryItems: [URLQueryItem] = [], - useGzipCompression: Bool) -> Bool { + func sendRequest( + _ requestData: String, + type: FlushType, + useIP: Bool, + headers: [String: String], + queryItems: [URLQueryItem] = [], + useGzipCompression: Bool + ) -> Bool { - let responseParser: (Data) -> Int? = { data in - let response = String(data: data, encoding: String.Encoding.utf8) - if let response = response { - return Int(response) ?? 0 - } - return nil + let responseParser: (Data) -> Int? = { data in + let response = String(data: data, encoding: String.Encoding.utf8) + if let response = response { + return Int(response) ?? 0 + } + return nil + } + + var resourceHeaders: [String: String] = ["Content-Type": "application/json"].merging(headers) { + (_, new) in new + } + var compressedData: Data? = nil + + if useGzipCompression && type == .events { + if let requestDataRaw = requestData.data(using: .utf8) { + do { + compressedData = try requestDataRaw.gzipCompressed() + resourceHeaders["Content-Encoding"] = "gzip" + } catch { + MixpanelLogger.error(message: "Failed to compress data with gzip: \(error)") } - - var resourceHeaders: [String: String] = ["Content-Type": "application/json"].merging(headers) {(_,new) in new } - var compressedData: Data? = nil - - if useGzipCompression && type == .events { - if let requestDataRaw = requestData.data(using: .utf8) { - do { - compressedData = try requestDataRaw.gzipCompressed() - resourceHeaders["Content-Encoding"] = "gzip" - } catch { - MixpanelLogger.error(message: "Failed to compress data with gzip: \(error)") - } - } + } + } + let ipString = useIP ? "1" : "0" + var resourceQueryItems: [URLQueryItem] = [URLQueryItem(name: "ip", value: ipString)] + resourceQueryItems.append(contentsOf: queryItems) + let resource = Network.buildResource( + path: type.rawValue, + method: .post, + requestBody: compressedData ?? requestData.data(using: .utf8), + queryItems: resourceQueryItems, + headers: resourceHeaders, + parse: responseParser) + var result = false + let semaphore = DispatchSemaphore(value: 0) + flushRequestHandler( + serverURL, + resource: resource, + completion: { success in + result = success + semaphore.signal() + }) + _ = semaphore.wait(timeout: .now() + 120.0) + return result + } + + private func flushRequestHandler( + _ base: String, + resource: Resource, + completion: @escaping (Bool) -> Void + ) { + + Network.apiRequest( + base: base, resource: resource, + failure: { (reason, _, response) in + self.networkConsecutiveFailures += 1 + self.updateRetryDelay(response) + MixpanelLogger.warn( + message: "API request to \(resource.path) has failed with reason \(reason)") + completion(false) + }, + success: { (result, response) in + self.networkConsecutiveFailures = 0 + self.updateRetryDelay(response) + if result == 0 { + MixpanelLogger.info(message: "\(base) api rejected some items") } - let ipString = useIP ? "1" : "0" - var resourceQueryItems: [URLQueryItem] = [URLQueryItem(name: "ip", value: ipString)] - resourceQueryItems.append(contentsOf: queryItems) - let resource = Network.buildResource(path: type.rawValue, - method: .post, - requestBody: compressedData ?? requestData.data(using: .utf8), - queryItems: resourceQueryItems, - headers: resourceHeaders, - parse: responseParser) - var result = false - let semaphore = DispatchSemaphore(value: 0) - flushRequestHandler(serverURL, - resource: resource, - completion: { success in - result = success - semaphore.signal() - }) - _ = semaphore.wait(timeout: .now() + 120.0) - return result + completion(true) + }) + } + + private func updateRetryDelay(_ response: URLResponse?) { + var retryTime = 0.0 + let retryHeader = (response as? HTTPURLResponse)?.allHeaderFields["Retry-After"] as? String + if let retryHeader = retryHeader, let retryHeaderParsed = (Double(retryHeader)) { + retryTime = retryHeaderParsed } - - private func flushRequestHandler(_ base: String, - resource: Resource, - completion: @escaping (Bool) -> Void) { - - Network.apiRequest(base: base, resource: resource, - failure: { (reason, _, response) in - self.networkConsecutiveFailures += 1 - self.updateRetryDelay(response) - MixpanelLogger.warn(message: "API request to \(resource.path) has failed with reason \(reason)") - completion(false) - }, success: { (result, response) in - self.networkConsecutiveFailures = 0 - self.updateRetryDelay(response) - if result == 0 { - MixpanelLogger.info(message: "\(base) api rejected some items") - } - completion(true) - }) + if networkConsecutiveFailures >= APIConstants.failuresTillBackoff { + retryTime = max( + retryTime, + retryBackOffTimeWithConsecutiveFailures(networkConsecutiveFailures)) } + let retryDate = Date(timeIntervalSinceNow: retryTime) + networkRequestsAllowedAfterTime = retryDate.timeIntervalSince1970 + } - private func updateRetryDelay(_ response: URLResponse?) { - var retryTime = 0.0 - let retryHeader = (response as? HTTPURLResponse)?.allHeaderFields["Retry-After"] as? String - if let retryHeader = retryHeader, let retryHeaderParsed = (Double(retryHeader)) { - retryTime = retryHeaderParsed - } + private func retryBackOffTimeWithConsecutiveFailures(_ failureCount: Int) -> TimeInterval { + let time = pow(2.0, Double(failureCount) - 1) * 60 + Double(arc4random_uniform(30)) + return min( + max(APIConstants.minRetryBackoff, time), + APIConstants.maxRetryBackoff) + } - if networkConsecutiveFailures >= APIConstants.failuresTillBackoff { - retryTime = max(retryTime, - retryBackOffTimeWithConsecutiveFailures(networkConsecutiveFailures)) - } - let retryDate = Date(timeIntervalSinceNow: retryTime) - networkRequestsAllowedAfterTime = retryDate.timeIntervalSince1970 - } - - private func retryBackOffTimeWithConsecutiveFailures(_ failureCount: Int) -> TimeInterval { - let time = pow(2.0, Double(failureCount) - 1) * 60 + Double(arc4random_uniform(30)) - return min(max(APIConstants.minRetryBackoff, time), - APIConstants.maxRetryBackoff) - } - - func requestNotAllowed() -> Bool { - return Date().timeIntervalSince1970 < networkRequestsAllowedAfterTime - } + func requestNotAllowed() -> Bool { + return Date().timeIntervalSince1970 < networkRequestsAllowedAfterTime + } } - diff --git a/Sources/Group.swift b/Sources/Group.swift index 170e9e5f..1fb7cd97 100644 --- a/Sources/Group.swift +++ b/Sources/Group.swift @@ -12,68 +12,70 @@ import Foundation /// the main Mixpanel instance. open class Group { - let apiToken: String - let serialQueue: DispatchQueue - let lock: ReadWriteLock - let groupKey: String - let groupID: MixpanelType - weak var delegate: FlushDelegate? - let metadata: SessionMetadata - let mixpanelPersistence: MixpanelPersistence - weak var mixpanelInstance: MixpanelInstance? + let apiToken: String + let serialQueue: DispatchQueue + let lock: ReadWriteLock + let groupKey: String + let groupID: MixpanelType + weak var delegate: FlushDelegate? + let metadata: SessionMetadata + let mixpanelPersistence: MixpanelPersistence + weak var mixpanelInstance: MixpanelInstance? - init(apiToken: String, - serialQueue: DispatchQueue, - lock: ReadWriteLock, - groupKey: String, - groupID: MixpanelType, - metadata: SessionMetadata, - mixpanelPersistence: MixpanelPersistence, - mixpanelInstance: MixpanelInstance) { - self.apiToken = apiToken - self.serialQueue = serialQueue - self.lock = lock - self.groupKey = groupKey - self.groupID = groupID - self.metadata = metadata - self.mixpanelPersistence = mixpanelPersistence - self.mixpanelInstance = mixpanelInstance + init( + apiToken: String, + serialQueue: DispatchQueue, + lock: ReadWriteLock, + groupKey: String, + groupID: MixpanelType, + metadata: SessionMetadata, + mixpanelPersistence: MixpanelPersistence, + mixpanelInstance: MixpanelInstance + ) { + self.apiToken = apiToken + self.serialQueue = serialQueue + self.lock = lock + self.groupKey = groupKey + self.groupID = groupID + self.metadata = metadata + self.mixpanelPersistence = mixpanelPersistence + self.mixpanelInstance = mixpanelInstance + } + + func addGroupRecordToQueueWithAction(_ action: String, properties: InternalProperties) { + if mixpanelInstance?.hasOptedOutTracking() ?? false { + return + } + let epochMilliseconds = round(Date().timeIntervalSince1970 * 1000) + + serialQueue.async { + var r = InternalProperties() + var p = InternalProperties() + r["$token"] = self.apiToken + r["$time"] = epochMilliseconds + if action == "$unset" { + // $unset takes an array of property names which is supplied to this method + // in the properties parameter under the key "$properties" + r[action] = properties["$properties"] + } else { + p += properties + r[action] = p + } + self.metadata.toDict(isEvent: false).forEach { (k, v) in r[k] = v } + + r["$group_key"] = self.groupKey + r["$group_id"] = self.groupID + self.mixpanelPersistence.saveEntity(r, type: .groups) } - func addGroupRecordToQueueWithAction(_ action: String, properties: InternalProperties) { - if mixpanelInstance?.hasOptedOutTracking() ?? false { - return - } - let epochMilliseconds = round(Date().timeIntervalSince1970 * 1000) - - serialQueue.async { - var r = InternalProperties() - var p = InternalProperties() - r["$token"] = self.apiToken - r["$time"] = epochMilliseconds - if action == "$unset" { - // $unset takes an array of property names which is supplied to this method - // in the properties parameter under the key "$properties" - r[action] = properties["$properties"] - } else { - p += properties - r[action] = p - } - self.metadata.toDict(isEvent: false).forEach { (k, v) in r[k] = v } - - r["$group_key"] = self.groupKey - r["$group_id"] = self.groupID - self.mixpanelPersistence.saveEntity(r, type: .groups) - } - - if MixpanelInstance.isiOSAppExtension() { - delegate?.flush(performFullFlush: false, completion: nil) - } + if MixpanelInstance.isiOSAppExtension() { + delegate?.flush(performFullFlush: false, completion: nil) } + } - // MARK: - Group + // MARK: - Group - /** + /** Sets properties on this group. Property keys must be String objects and the supported value types need to conform to MixpanelType. @@ -83,12 +85,12 @@ open class Group { - parameter properties: properties dictionary */ - open func set(properties: Properties) { - assertPropertyTypes(properties) - addGroupRecordToQueueWithAction("$set", properties: properties) - } + open func set(properties: Properties) { + assertPropertyTypes(properties) + addGroupRecordToQueueWithAction("$set", properties: properties) + } - /** + /** Convenience method for setting a single property in Mixpanel Groups. Property keys must be String objects and the supported value types need to conform to MixpanelType. @@ -97,11 +99,11 @@ open class Group { - parameter property: property name - parameter to: property value */ - open func set(property: String, to: MixpanelType) { - set(properties: [property: to]) - } + open func set(property: String, to: MixpanelType) { + set(properties: [property: to]) + } - /** + /** Sets properties on the current Mixpanel Group, but doesn't overwrite if there is an existing value. @@ -111,23 +113,23 @@ open class Group { - parameter properties: properties dictionary */ - open func setOnce(properties: Properties) { - assertPropertyTypes(properties) - addGroupRecordToQueueWithAction("$set_once", properties: properties) - } + open func setOnce(properties: Properties) { + assertPropertyTypes(properties) + addGroupRecordToQueueWithAction("$set_once", properties: properties) + } - /** + /** Remove a property and its value from a group's profile in Mixpanel Groups. For properties that don't exist there will be no effect. - parameter property: name of the property to unset */ - open func unset(property: String) { - addGroupRecordToQueueWithAction("$unset", properties: ["$properties": [property]]) - } + open func unset(property: String) { + addGroupRecordToQueueWithAction("$unset", properties: ["$properties": [property]]) + } - /** + /** Removes list properties. Property keys must be String objects and the supported value types need to conform to MixpanelType. @@ -135,29 +137,29 @@ open class Group { - parameter properties: mapping of list property names to values to remove */ - open func remove(key: String, value: MixpanelType) { - let properties = [key: value] - assertPropertyTypes(properties) - addGroupRecordToQueueWithAction("$remove", properties: properties) - } + open func remove(key: String, value: MixpanelType) { + let properties = [key: value] + assertPropertyTypes(properties) + addGroupRecordToQueueWithAction("$remove", properties: properties) + } - /** + /** Union list properties. Property values must be array objects. - parameter properties: mapping of list property names to lists to union */ - open func union(key: String, values: [MixpanelType]) { - let properties = [key: values] - addGroupRecordToQueueWithAction("$union", properties: properties) - } + open func union(key: String, values: [MixpanelType]) { + let properties = [key: values] + addGroupRecordToQueueWithAction("$union", properties: properties) + } - /** + /** Delete group's record from Mixpanel Groups. */ - open func deleteGroup() { - addGroupRecordToQueueWithAction("$delete", properties: [:]) - mixpanelInstance?.removeCachedGroup(groupKey: groupKey, groupID: groupID) - } + open func deleteGroup() { + addGroupRecordToQueueWithAction("$delete", properties: [:]) + mixpanelInstance?.removeCachedGroup(groupKey: groupKey, groupID: groupID) + } } diff --git a/Sources/JSONHandler.swift b/Sources/JSONHandler.swift index 9fee4c2e..ca0829df 100644 --- a/Sources/JSONHandler.swift +++ b/Sources/JSONHandler.swift @@ -10,119 +10,120 @@ import Foundation class JSONHandler { - typealias MPObjectToParse = Any + typealias MPObjectToParse = Any - class func encodeAPIData(_ obj: MPObjectToParse) -> String? { - let data: Data? = serializeJSONObject(obj) + class func encodeAPIData(_ obj: MPObjectToParse) -> String? { + let data: Data? = serializeJSONObject(obj) - guard let d = data else { - MixpanelLogger.warn(message: "couldn't serialize object") - return nil - } - - return String(decoding: d, as: UTF8.self) - } - - class func deserializeData(_ data: Data) -> MPObjectToParse? { - var object: MPObjectToParse? - do { - object = try JSONSerialization.jsonObject(with: data, options: []) - } catch { - MixpanelLogger.warn(message: "exception decoding object data") - } - return object + guard let d = data else { + MixpanelLogger.warn(message: "couldn't serialize object") + return nil } - class func serializeJSONObject(_ obj: MPObjectToParse) -> Data? { - let serializableJSONObject: MPObjectToParse - if let jsonObject = makeObjectSerializable(obj) as? [Any] { - serializableJSONObject = jsonObject.filter { - JSONSerialization.isValidJSONObject($0) - } - } else { - serializableJSONObject = makeObjectSerializable(obj) - } - - guard JSONSerialization.isValidJSONObject(serializableJSONObject) else { - MixpanelLogger.warn(message: "object isn't valid and can't be serialzed to JSON") - return nil - } - - var serializedObject: Data? - do { - serializedObject = try JSONSerialization - .data(withJSONObject: serializableJSONObject, options: []) - } catch { - MixpanelLogger.warn(message: "exception encoding api data") - } - return serializedObject + return String(decoding: d, as: UTF8.self) + } + + class func deserializeData(_ data: Data) -> MPObjectToParse? { + var object: MPObjectToParse? + do { + object = try JSONSerialization.jsonObject(with: data, options: []) + } catch { + MixpanelLogger.warn(message: "exception decoding object data") + } + return object + } + + class func serializeJSONObject(_ obj: MPObjectToParse) -> Data? { + let serializableJSONObject: MPObjectToParse + if let jsonObject = makeObjectSerializable(obj) as? [Any] { + serializableJSONObject = jsonObject.filter { + JSONSerialization.isValidJSONObject($0) + } + } else { + serializableJSONObject = makeObjectSerializable(obj) } - private class func makeObjectSerializable(_ obj: MPObjectToParse) -> MPObjectToParse { - switch obj { - case let obj as NSNumber: - if isBoolNumber(obj) { - return obj.boolValue - } else if isInvalidNumber(obj) { - return String(describing: obj) - } else { - return obj - } - - case let obj as Double where obj.isFinite && !obj.isNaN: - return obj - - case let obj as Float where obj.isFinite && !obj.isNaN: - return obj - - case is String, is Int, is UInt, is UInt64, is Bool: - return obj - - case let obj as [Any?]: - // nil values in Array properties are dropped - let nonNilEls: [Any] = obj.compactMap({ $0 }) - return nonNilEls.map { makeObjectSerializable($0) } - - case let obj as [Any]: - return obj.map { makeObjectSerializable($0) } - - case let obj as InternalProperties: - var serializedDict = InternalProperties() - _ = obj.map { e in - serializedDict[e.key] = - makeObjectSerializable(e.value) - } - return serializedDict - - case let obj as Date: - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" - dateFormatter.timeZone = TimeZone(abbreviation: "UTC") - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - return dateFormatter.string(from: obj) - - case let obj as URL: - return obj.absoluteString - - default: - let objString = String(describing: obj) - if objString == "nil" { - // all nil properties outside of Arrays are converted to NSNull() - return NSNull() - } else { - MixpanelLogger.info(message: "enforcing string on object") - return objString - } - } + guard JSONSerialization.isValidJSONObject(serializableJSONObject) else { + MixpanelLogger.warn(message: "object isn't valid and can't be serialzed to JSON") + return nil } - private class func isBoolNumber(_ num: NSNumber) -> Bool { - let boolID = CFBooleanGetTypeID() - let numID = CFGetTypeID(num) - return numID == boolID + var serializedObject: Data? + do { + serializedObject = + try JSONSerialization + .data(withJSONObject: serializableJSONObject, options: []) + } catch { + MixpanelLogger.warn(message: "exception encoding api data") } + return serializedObject + } - private class func isInvalidNumber(_ num: NSNumber) -> Bool { - return num.doubleValue.isInfinite || num.doubleValue.isNaN + private class func makeObjectSerializable(_ obj: MPObjectToParse) -> MPObjectToParse { + switch obj { + case let obj as NSNumber: + if isBoolNumber(obj) { + return obj.boolValue + } else if isInvalidNumber(obj) { + return String(describing: obj) + } else { + return obj + } + + case let obj as Double where obj.isFinite && !obj.isNaN: + return obj + + case let obj as Float where obj.isFinite && !obj.isNaN: + return obj + + case is String, is Int, is UInt, is UInt64, is Bool: + return obj + + case let obj as [Any?]: + // nil values in Array properties are dropped + let nonNilEls: [Any] = obj.compactMap({ $0 }) + return nonNilEls.map { makeObjectSerializable($0) } + + case let obj as [Any]: + return obj.map { makeObjectSerializable($0) } + + case let obj as InternalProperties: + var serializedDict = InternalProperties() + _ = obj.map { e in + serializedDict[e.key] = + makeObjectSerializable(e.value) + } + return serializedDict + + case let obj as Date: + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + dateFormatter.timeZone = TimeZone(abbreviation: "UTC") + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + return dateFormatter.string(from: obj) + + case let obj as URL: + return obj.absoluteString + + default: + let objString = String(describing: obj) + if objString == "nil" { + // all nil properties outside of Arrays are converted to NSNull() + return NSNull() + } else { + MixpanelLogger.info(message: "enforcing string on object") + return objString + } } + } + + private class func isBoolNumber(_ num: NSNumber) -> Bool { + let boolID = CFBooleanGetTypeID() + let numID = CFGetTypeID(num) + return numID == boolID + } + + private class func isInvalidNumber(_ num: NSNumber) -> Bool { + return num.doubleValue.isInfinite || num.doubleValue.isNaN + } } diff --git a/Sources/MPDB.swift b/Sources/MPDB.swift index 4424b566..f43c8161 100644 --- a/Sources/MPDB.swift +++ b/Sources/MPDB.swift @@ -10,283 +10,291 @@ import Foundation import SQLite3 class MPDB { - private var connection: OpaquePointer? - private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) - private let DB_FILE_NAME: String = "MPDB.sqlite" - - let apiToken: String - - init(token: String) { - // token can be instanceName which can be any string so we strip all non-alhpanumeric characters to prevent SQL errors - apiToken = String(token.unicodeScalars.filter({ CharacterSet.alphanumerics.contains($0) })) - open() - } - - deinit { - close() - } - - private func pathToDb() -> String? { - let manager = FileManager.default - #if os(iOS) - let url = manager.urls(for: .libraryDirectory, in: .userDomainMask).last - #else - let url = manager.urls(for: .cachesDirectory, in: .userDomainMask).last - #endif // os(iOS) + private var connection: OpaquePointer? + private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + private let DB_FILE_NAME: String = "MPDB.sqlite" - guard let urlUnwrapped = url?.appendingPathComponent(apiToken + "_" + DB_FILE_NAME).path else { - return nil - } - return urlUnwrapped + let apiToken: String + + init(token: String) { + // token can be instanceName which can be any string so we strip all non-alhpanumeric characters to prevent SQL errors + apiToken = String(token.unicodeScalars.filter({ CharacterSet.alphanumerics.contains($0) })) + open() + } + + deinit { + close() + } + + private func pathToDb() -> String? { + let manager = FileManager.default + #if os(iOS) + let url = manager.urls(for: .libraryDirectory, in: .userDomainMask).last + #else + let url = manager.urls(for: .cachesDirectory, in: .userDomainMask).last + #endif // os(iOS) + + guard let urlUnwrapped = url?.appendingPathComponent(apiToken + "_" + DB_FILE_NAME).path else { + return nil } - - private func tableNameFor(_ persistenceType: PersistenceType) -> String { - return "mixpanel_\(apiToken)_\(persistenceType)" + return urlUnwrapped + } + + private func tableNameFor(_ persistenceType: PersistenceType) -> String { + return "mixpanel_\(apiToken)_\(persistenceType)" + } + + private func reconnect() { + MixpanelLogger.warn(message: "No database connection found. Calling MPDB.open()") + open() + } + + func open() { + if apiToken.isEmpty { + MixpanelLogger.error(message: "Project token must not be empty. Database cannot be opened.") + return } - - private func reconnect() { - MixpanelLogger.warn(message: "No database connection found. Calling MPDB.open()") - open() - } - - func open() { - if apiToken.isEmpty { - MixpanelLogger.error(message: "Project token must not be empty. Database cannot be opened.") - return - } - if let dbPath = pathToDb() { - if sqlite3_open_v2(dbPath, &connection, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nil) != SQLITE_OK { - logSqlError(message: "Error opening or creating database at path: \(dbPath)") - close() - } else { - MixpanelLogger.info(message: "Successfully opened connection to database at path: \(dbPath)") - if let db = connection { - let pragmaString = "PRAGMA journal_mode=WAL;" - var pragmaStatement: OpaquePointer? - if sqlite3_prepare_v2(db, pragmaString, -1, &pragmaStatement, nil) == SQLITE_OK { - if sqlite3_step(pragmaStatement) == SQLITE_ROW { - let res = String(cString: sqlite3_column_text(pragmaStatement, 0)) - MixpanelLogger.info(message: "SQLite journal mode set to \(res)") - } else { - logSqlError(message: "Failed to enable journal_mode=WAL") - } - } else { - logSqlError(message: "PRAGMA journal_mode=WAL statement could not be prepared") - } - sqlite3_finalize(pragmaStatement) - } else { - reconnect() - } - createTablesAndIndexes() - } - } - } - - func close() { - sqlite3_close(connection) - connection = nil - MixpanelLogger.info(message: "Connection to database closed.") - } - - private func recreate() { + if let dbPath = pathToDb() { + if sqlite3_open_v2(dbPath, &connection, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nil) + != SQLITE_OK + { + logSqlError(message: "Error opening or creating database at path: \(dbPath)") close() - if let dbPath = pathToDb() { - do { - let manager = FileManager.default - if manager.fileExists(atPath: dbPath) { - try manager.removeItem(atPath: dbPath) - MixpanelLogger.info(message: "Deleted database file at path: \(dbPath)") - } - } catch let error { - MixpanelLogger.error(message: "Unable to remove database file at path: \(dbPath), error: \(error)") - } - } - reconnect() - } - - private func createTableFor(_ persistenceType: PersistenceType) { + } else { + MixpanelLogger.info( + message: "Successfully opened connection to database at path: \(dbPath)") if let db = connection { - let tableName = tableNameFor(persistenceType) - let createTableString = - "CREATE TABLE IF NOT EXISTS \(tableName)(id integer primary key autoincrement,data blob,time real,flag integer);" - var createTableStatement: OpaquePointer? - if sqlite3_prepare_v2(db, createTableString, -1, &createTableStatement, nil) == SQLITE_OK { - if sqlite3_step(createTableStatement) == SQLITE_DONE { - MixpanelLogger.info(message: "\(tableName) table created") - } else { - logSqlError(message: "\(tableName) table create failed") - } + let pragmaString = "PRAGMA journal_mode=WAL;" + var pragmaStatement: OpaquePointer? + if sqlite3_prepare_v2(db, pragmaString, -1, &pragmaStatement, nil) == SQLITE_OK { + if sqlite3_step(pragmaStatement) == SQLITE_ROW { + let res = String(cString: sqlite3_column_text(pragmaStatement, 0)) + MixpanelLogger.info(message: "SQLite journal mode set to \(res)") } else { - logSqlError(message: "CREATE statement for table \(tableName) could not be prepared") + logSqlError(message: "Failed to enable journal_mode=WAL") } - sqlite3_finalize(createTableStatement) + } else { + logSqlError(message: "PRAGMA journal_mode=WAL statement could not be prepared") + } + sqlite3_finalize(pragmaStatement) } else { - reconnect() + reconnect() } + createTablesAndIndexes() + } } - - private func createIndexFor(_ persistenceType: PersistenceType) { - if let db = connection { - let tableName = tableNameFor(persistenceType) - let indexName = "idx_\(persistenceType)_time" - let createIndexString = "CREATE INDEX IF NOT EXISTS \(indexName) ON \(tableName) (time);" - var createIndexStatement: OpaquePointer? - if sqlite3_prepare_v2(db, createIndexString, -1, &createIndexStatement, nil) == SQLITE_OK { - if sqlite3_step(createIndexStatement) == SQLITE_DONE { - MixpanelLogger.info(message: "\(indexName) index created") - } else { - logSqlError(message: "\(indexName) index creation failed") - } + } + + func close() { + sqlite3_close(connection) + connection = nil + MixpanelLogger.info(message: "Connection to database closed.") + } + + private func recreate() { + close() + if let dbPath = pathToDb() { + do { + let manager = FileManager.default + if manager.fileExists(atPath: dbPath) { + try manager.removeItem(atPath: dbPath) + MixpanelLogger.info(message: "Deleted database file at path: \(dbPath)") + } + } catch let error { + MixpanelLogger.error( + message: "Unable to remove database file at path: \(dbPath), error: \(error)") + } + } + reconnect() + } + + private func createTableFor(_ persistenceType: PersistenceType) { + if let db = connection { + let tableName = tableNameFor(persistenceType) + let createTableString = + "CREATE TABLE IF NOT EXISTS \(tableName)(id integer primary key autoincrement,data blob,time real,flag integer);" + var createTableStatement: OpaquePointer? + if sqlite3_prepare_v2(db, createTableString, -1, &createTableStatement, nil) == SQLITE_OK { + if sqlite3_step(createTableStatement) == SQLITE_DONE { + MixpanelLogger.info(message: "\(tableName) table created") + } else { + logSqlError(message: "\(tableName) table create failed") + } + } else { + logSqlError(message: "CREATE statement for table \(tableName) could not be prepared") + } + sqlite3_finalize(createTableStatement) + } else { + reconnect() + } + } + + private func createIndexFor(_ persistenceType: PersistenceType) { + if let db = connection { + let tableName = tableNameFor(persistenceType) + let indexName = "idx_\(persistenceType)_time" + let createIndexString = "CREATE INDEX IF NOT EXISTS \(indexName) ON \(tableName) (time);" + var createIndexStatement: OpaquePointer? + if sqlite3_prepare_v2(db, createIndexString, -1, &createIndexStatement, nil) == SQLITE_OK { + if sqlite3_step(createIndexStatement) == SQLITE_DONE { + MixpanelLogger.info(message: "\(indexName) index created") + } else { + logSqlError(message: "\(indexName) index creation failed") + } + } else { + logSqlError(message: "CREATE statement for index \(indexName) could not be prepared") + } + sqlite3_finalize(createIndexStatement) + } else { + reconnect() + } + } + + private func createTablesAndIndexes() { + createTableFor(PersistenceType.events) + createIndexFor(PersistenceType.events) + createTableFor(PersistenceType.people) + createIndexFor(PersistenceType.people) + createTableFor(PersistenceType.groups) + createIndexFor(PersistenceType.groups) + } + + func insertRow(_ persistenceType: PersistenceType, data: Data, flag: Bool = false) { + if let db = connection { + let tableName = tableNameFor(persistenceType) + let insertString = "INSERT INTO \(tableName) (data, flag, time) VALUES(?, ?, ?);" + var insertStatement: OpaquePointer? + data.withUnsafeBytes { rawBuffer in + if let pointer = rawBuffer.baseAddress { + if sqlite3_prepare_v2(db, insertString, -1, &insertStatement, nil) == SQLITE_OK { + sqlite3_bind_blob(insertStatement, 1, pointer, Int32(rawBuffer.count), SQLITE_TRANSIENT) + sqlite3_bind_int(insertStatement, 2, flag ? 1 : 0) + sqlite3_bind_double(insertStatement, 3, Date().timeIntervalSince1970) + if sqlite3_step(insertStatement) == SQLITE_DONE { + MixpanelLogger.info(message: "Successfully inserted row into table \(tableName)") } else { - logSqlError(message: "CREATE statement for index \(indexName) could not be prepared") + logSqlError(message: "Failed to insert row into table \(tableName)") + recreate() } - sqlite3_finalize(createIndexStatement) - } else { - reconnect() + } else { + logSqlError(message: "INSERT statement for table \(tableName) could not be prepared") + recreate() + } + sqlite3_finalize(insertStatement) } + } + } else { + reconnect() } - - private func createTablesAndIndexes() { - createTableFor(PersistenceType.events) - createIndexFor(PersistenceType.events) - createTableFor(PersistenceType.people) - createIndexFor(PersistenceType.people) - createTableFor(PersistenceType.groups) - createIndexFor(PersistenceType.groups) + } + + func deleteRows(_ persistenceType: PersistenceType, ids: [Int32] = [], isDeleteAll: Bool = false) + { + if let db = connection { + let tableName = tableNameFor(persistenceType) + let deleteString = + "DELETE FROM \(tableName)\(isDeleteAll ? "" : " WHERE id IN \(idsSqlString(ids))")" + var deleteStatement: OpaquePointer? + if sqlite3_prepare_v2(db, deleteString, -1, &deleteStatement, nil) == SQLITE_OK { + if sqlite3_step(deleteStatement) == SQLITE_DONE { + MixpanelLogger.info(message: "Successfully deleted rows from table \(tableName)") + } else { + logSqlError(message: "Failed to delete rows from table \(tableName)") + recreate() + } + } else { + logSqlError(message: "DELETE statement for table \(tableName) could not be prepared") + recreate() + } + sqlite3_finalize(deleteStatement) + } else { + reconnect() } - - func insertRow(_ persistenceType: PersistenceType, data: Data, flag: Bool = false) { - if let db = connection { - let tableName = tableNameFor(persistenceType) - let insertString = "INSERT INTO \(tableName) (data, flag, time) VALUES(?, ?, ?);" - var insertStatement: OpaquePointer? - data.withUnsafeBytes { rawBuffer in - if let pointer = rawBuffer.baseAddress { - if sqlite3_prepare_v2(db, insertString, -1, &insertStatement, nil) == SQLITE_OK { - sqlite3_bind_blob(insertStatement, 1, pointer, Int32(rawBuffer.count), SQLITE_TRANSIENT) - sqlite3_bind_int(insertStatement, 2, flag ? 1 : 0) - sqlite3_bind_double(insertStatement, 3, Date().timeIntervalSince1970) - if sqlite3_step(insertStatement) == SQLITE_DONE { - MixpanelLogger.info(message: "Successfully inserted row into table \(tableName)") - } else { - logSqlError(message: "Failed to insert row into table \(tableName)") - recreate() - } - } else { - logSqlError(message: "INSERT statement for table \(tableName) could not be prepared") - recreate() - } - sqlite3_finalize(insertStatement) - } + } + + private func idsSqlString(_ ids: [Int32] = []) -> String { + var sqlString = "(" + for id in ids { + sqlString += "\(id)," + } + sqlString = String(sqlString.dropLast()) + sqlString += ")" + return sqlString + } + + func updateRowsFlag(_ persistenceType: PersistenceType, newFlag: Bool) { + if let db = connection { + let tableName = tableNameFor(persistenceType) + let updateString = "UPDATE \(tableName) SET flag = \(newFlag) where flag = \(!newFlag)" + var updateStatement: OpaquePointer? + if sqlite3_prepare_v2(db, updateString, -1, &updateStatement, nil) == SQLITE_OK { + if sqlite3_step(updateStatement) == SQLITE_DONE { + MixpanelLogger.info(message: "Successfully updated rows from table \(tableName)") + } else { + logSqlError(message: "Failed to update rows from table \(tableName)") + recreate() + } + } else { + logSqlError(message: "UPDATE statement for table \(tableName) could not be prepared") + recreate() + } + sqlite3_finalize(updateStatement) + } else { + reconnect() + } + } + + func readRows(_ persistenceType: PersistenceType, numRows: Int, flag: Bool = false) + -> [InternalProperties] + { + var rows: [InternalProperties] = [] + if let db = connection { + let tableName = tableNameFor(persistenceType) + let selectString = """ + SELECT id, data FROM \(tableName) WHERE flag = \(flag ? 1 : 0) \ + ORDER BY time\(numRows == Int.max ? "" : " LIMIT \(numRows)") + """ + var selectStatement: OpaquePointer? + var rowsRead: Int = 0 + if sqlite3_prepare_v2(db, selectString, -1, &selectStatement, nil) == SQLITE_OK { + while sqlite3_step(selectStatement) == SQLITE_ROW { + if let blob = sqlite3_column_blob(selectStatement, 1) { + let blobLength = sqlite3_column_bytes(selectStatement, 1) + let data = Data(bytes: blob, count: Int(blobLength)) + let id = sqlite3_column_int(selectStatement, 0) + + if let jsonObject = JSONHandler.deserializeData(data) as? InternalProperties { + var entity = jsonObject + entity["id"] = id + rows.append(entity) } - } else { - reconnect() + rowsRead += 1 + } else { + logSqlError(message: "No blob found in data column for row in \(tableName)") + } } - } - - func deleteRows(_ persistenceType: PersistenceType, ids: [Int32] = [], isDeleteAll: Bool = false) { - if let db = connection { - let tableName = tableNameFor(persistenceType) - let deleteString = "DELETE FROM \(tableName)\(isDeleteAll ? "" : " WHERE id IN \(idsSqlString(ids))")" - var deleteStatement: OpaquePointer? - if sqlite3_prepare_v2(db, deleteString, -1, &deleteStatement, nil) == SQLITE_OK { - if sqlite3_step(deleteStatement) == SQLITE_DONE { - MixpanelLogger.info(message: "Successfully deleted rows from table \(tableName)") - } else { - logSqlError(message: "Failed to delete rows from table \(tableName)") - recreate() - } - } else { - logSqlError(message: "DELETE statement for table \(tableName) could not be prepared") - recreate() - } - sqlite3_finalize(deleteStatement) - } else { - reconnect() + if rowsRead > 0 { + MixpanelLogger.info(message: "Successfully read \(rowsRead) from table \(tableName)") } + } else { + logSqlError(message: "SELECT statement for table \(tableName) could not be prepared") + } + sqlite3_finalize(selectStatement) + } else { + reconnect() } - - private func idsSqlString(_ ids: [Int32] = []) -> String { - var sqlString = "(" - for id in ids { - sqlString += "\(id)," - } - sqlString = String(sqlString.dropLast()) - sqlString += ")" - return sqlString - } - - func updateRowsFlag(_ persistenceType: PersistenceType, newFlag: Bool) { - if let db = connection { - let tableName = tableNameFor(persistenceType) - let updateString = "UPDATE \(tableName) SET flag = \(newFlag) where flag = \(!newFlag)" - var updateStatement: OpaquePointer? - if sqlite3_prepare_v2(db, updateString, -1, &updateStatement, nil) == SQLITE_OK { - if sqlite3_step(updateStatement) == SQLITE_DONE { - MixpanelLogger.info(message: "Successfully updated rows from table \(tableName)") - } else { - logSqlError(message: "Failed to update rows from table \(tableName)") - recreate() - } - } else { - logSqlError(message: "UPDATE statement for table \(tableName) could not be prepared") - recreate() - } - sqlite3_finalize(updateStatement) - } else { - reconnect() - } - } - - func readRows(_ persistenceType: PersistenceType, numRows: Int, flag: Bool = false) -> [InternalProperties] { - var rows: [InternalProperties] = [] - if let db = connection { - let tableName = tableNameFor(persistenceType) - let selectString = """ - SELECT id, data FROM \(tableName) WHERE flag = \(flag ? 1 : 0) \ - ORDER BY time\(numRows == Int.max ? "" : " LIMIT \(numRows)") - """ - var selectStatement: OpaquePointer? - var rowsRead: Int = 0 - if sqlite3_prepare_v2(db, selectString, -1, &selectStatement, nil) == SQLITE_OK { - while sqlite3_step(selectStatement) == SQLITE_ROW { - if let blob = sqlite3_column_blob(selectStatement, 1) { - let blobLength = sqlite3_column_bytes(selectStatement, 1) - let data = Data(bytes: blob, count: Int(blobLength)) - let id = sqlite3_column_int(selectStatement, 0) - - if let jsonObject = JSONHandler.deserializeData(data) as? InternalProperties { - var entity = jsonObject - entity["id"] = id - rows.append(entity) - } - rowsRead += 1 - } else { - logSqlError(message: "No blob found in data column for row in \(tableName)") - } - } - if rowsRead > 0 { - MixpanelLogger.info(message: "Successfully read \(rowsRead) from table \(tableName)") - } - } else { - logSqlError(message: "SELECT statement for table \(tableName) could not be prepared") - } - sqlite3_finalize(selectStatement) - } else { - reconnect() - } - return rows - } - - private func logSqlError(message: String? = nil) { - if let db = connection { - if let msg = message { - MixpanelLogger.error(message: msg) - } - let sqlError = String(cString: sqlite3_errmsg(db)!) - MixpanelLogger.error(message: sqlError) - } else { - reconnect() - } + return rows + } + + private func logSqlError(message: String? = nil) { + if let db = connection { + if let msg = message { + MixpanelLogger.error(message: msg) + } + let sqlError = String(cString: sqlite3_errmsg(db)!) + MixpanelLogger.error(message: sqlError) + } else { + reconnect() } + } } diff --git a/Sources/Mixpanel.swift b/Sources/Mixpanel.swift index ba22af39..1493648b 100644 --- a/Sources/Mixpanel.swift +++ b/Sources/Mixpanel.swift @@ -7,26 +7,27 @@ // import Foundation + #if !os(OSX) -import UIKit -#endif // os(OSX) + import UIKit +#endif // os(OSX) /// The primary class for integrating Mixpanel with your app. open class Mixpanel { - - @discardableResult - open class func initialize(options: MixpanelOptions) -> MixpanelInstance { - return MixpanelManager.sharedInstance.initialize(options: options) - } - -#if !os(OSX) && !os(watchOS) + + @discardableResult + open class func initialize(options: MixpanelOptions) -> MixpanelInstance { + return MixpanelManager.sharedInstance.initialize(options: options) + } + + #if !os(OSX) && !os(watchOS) /** Initializes an instance of the API with the given project token. - + Returns a new Mixpanel instance API object. This allows you to create more than one instance of the API object, which is convenient if you'd like to send data to more than one Mixpanel project from a single app. - + - parameter token: your project token - parameter trackAutomaticEvents: Whether or not to collect common mobile events - parameter flushInterval: Optional. Interval to run background flushing @@ -37,41 +38,44 @@ open class Mixpanel { - parameter superProperties: Optional. Super properties dictionary to register during initialization - parameter serverURL: Optional. Mixpanel cluster URL - parameter useGzipCompression: Optional. Whether to use gzip compression for network requests. - + - important: If you have more than one Mixpanel instance, it is beneficial to initialize the instances with an instanceName. Then they can be reached by calling getInstance with name. - + - returns: returns a mixpanel instance if needed to keep throughout the project. You can always get the instance by calling getInstance(name) */ @discardableResult - open class func initialize(token apiToken: String, - trackAutomaticEvents: Bool, - flushInterval: Double = 60, - instanceName: String? = nil, - optOutTrackingByDefault: Bool = false, - useUniqueDistinctId: Bool = false, - superProperties: Properties? = nil, - serverURL: String? = nil, - useGzipCompression: Bool = false) -> MixpanelInstance { - return MixpanelManager.sharedInstance.initialize(token: apiToken, - flushInterval: flushInterval, - instanceName: ((instanceName != nil) ? instanceName! : apiToken), - trackAutomaticEvents: trackAutomaticEvents, - optOutTrackingByDefault: optOutTrackingByDefault, - useUniqueDistinctId: useUniqueDistinctId, - superProperties: superProperties, - serverURL: serverURL, - useGzipCompression: useGzipCompression) + open class func initialize( + token apiToken: String, + trackAutomaticEvents: Bool, + flushInterval: Double = 60, + instanceName: String? = nil, + optOutTrackingByDefault: Bool = false, + useUniqueDistinctId: Bool = false, + superProperties: Properties? = nil, + serverURL: String? = nil, + useGzipCompression: Bool = false + ) -> MixpanelInstance { + return MixpanelManager.sharedInstance.initialize( + token: apiToken, + flushInterval: flushInterval, + instanceName: ((instanceName != nil) ? instanceName! : apiToken), + trackAutomaticEvents: trackAutomaticEvents, + optOutTrackingByDefault: optOutTrackingByDefault, + useUniqueDistinctId: useUniqueDistinctId, + superProperties: superProperties, + serverURL: serverURL, + useGzipCompression: useGzipCompression) } - + /** Initializes an instance of the API with the given project token. - + Returns a new Mixpanel instance API object. This allows you to create more than one instance of the API object, which is convenient if you'd like to send data to more than one Mixpanel project from a single app. - + - parameter token: your project token - parameter trackAutomaticEvents: Whether or not to collect common mobile events - parameter flushInterval: Optional. Interval to run background flushing @@ -82,42 +86,45 @@ open class Mixpanel { - parameter superProperties: Optional. Super properties dictionary to register during initialization - parameter proxyServerConfig: Optional. Setup for proxy server. - parameter useGzipCompression: Optional. Whether to use gzip compression for network requests. - + - important: If you have more than one Mixpanel instance, it is beneficial to initialize the instances with an instanceName. Then they can be reached by calling getInstance with name. - + - returns: returns a mixpanel instance if needed to keep throughout the project. You can always get the instance by calling getInstance(name) */ - + @discardableResult - open class func initialize(token apiToken: String, - trackAutomaticEvents: Bool, - flushInterval: Double = 60, - instanceName: String? = nil, - optOutTrackingByDefault: Bool = false, - useUniqueDistinctId: Bool = false, - superProperties: Properties? = nil, - proxyServerConfig: ProxyServerConfig, - useGzipCompression: Bool = false) -> MixpanelInstance { - return MixpanelManager.sharedInstance.initialize(token: apiToken, - flushInterval: flushInterval, - instanceName: ((instanceName != nil) ? instanceName! : apiToken), - trackAutomaticEvents: trackAutomaticEvents, - optOutTrackingByDefault: optOutTrackingByDefault, - useUniqueDistinctId: useUniqueDistinctId, - superProperties: superProperties, - proxyServerConfig: proxyServerConfig, - useGzipCompression: useGzipCompression) + open class func initialize( + token apiToken: String, + trackAutomaticEvents: Bool, + flushInterval: Double = 60, + instanceName: String? = nil, + optOutTrackingByDefault: Bool = false, + useUniqueDistinctId: Bool = false, + superProperties: Properties? = nil, + proxyServerConfig: ProxyServerConfig, + useGzipCompression: Bool = false + ) -> MixpanelInstance { + return MixpanelManager.sharedInstance.initialize( + token: apiToken, + flushInterval: flushInterval, + instanceName: ((instanceName != nil) ? instanceName! : apiToken), + trackAutomaticEvents: trackAutomaticEvents, + optOutTrackingByDefault: optOutTrackingByDefault, + useUniqueDistinctId: useUniqueDistinctId, + superProperties: superProperties, + proxyServerConfig: proxyServerConfig, + useGzipCompression: useGzipCompression) } -#else + #else /** Initializes an instance of the API with the given project token (MAC OS ONLY). - + Returns a new Mixpanel instance API object. This allows you to create more than one instance of the API object, which is convenient if you'd like to send data to more than one Mixpanel project from a single app. - + - parameter token: your project token - parameter flushInterval: Optional. Interval to run background flushing - parameter instanceName: Optional. The name you want to uniquely identify the Mixpanel Instance. @@ -127,41 +134,44 @@ open class Mixpanel { - parameter superProperties: Optional. Super properties dictionary to register during initialization - parameter serverURL: Optional. Mixpanel cluster URL - parameter useGzipCompression: Optional. Whether to use gzip compression for network requests. - + - important: If you have more than one Mixpanel instance, it is beneficial to initialize the instances with an instanceName. Then they can be reached by calling getInstance with name. - + - returns: returns a mixpanel instance if needed to keep throughout the project. You can always get the instance by calling getInstance(name) */ - + @discardableResult - open class func initialize(token apiToken: String, - flushInterval: Double = 60, - instanceName: String? = nil, - optOutTrackingByDefault: Bool = false, - useUniqueDistinctId: Bool = false, - superProperties: Properties? = nil, - serverURL: String? = nil, - useGzipCompression: Bool = false) -> MixpanelInstance { - return MixpanelManager.sharedInstance.initialize(token: apiToken, - flushInterval: flushInterval, - instanceName: ((instanceName != nil) ? instanceName! : apiToken), - trackAutomaticEvents: false, - optOutTrackingByDefault: optOutTrackingByDefault, - useUniqueDistinctId: useUniqueDistinctId, - superProperties: superProperties, - serverURL: serverURL, - useGzipCompression: useGzipCompression) + open class func initialize( + token apiToken: String, + flushInterval: Double = 60, + instanceName: String? = nil, + optOutTrackingByDefault: Bool = false, + useUniqueDistinctId: Bool = false, + superProperties: Properties? = nil, + serverURL: String? = nil, + useGzipCompression: Bool = false + ) -> MixpanelInstance { + return MixpanelManager.sharedInstance.initialize( + token: apiToken, + flushInterval: flushInterval, + instanceName: ((instanceName != nil) ? instanceName! : apiToken), + trackAutomaticEvents: false, + optOutTrackingByDefault: optOutTrackingByDefault, + useUniqueDistinctId: useUniqueDistinctId, + superProperties: superProperties, + serverURL: serverURL, + useGzipCompression: useGzipCompression) } - + /** Initializes an instance of the API with the given project token (MAC OS ONLY). - + Returns a new Mixpanel instance API object. This allows you to create more than one instance of the API object, which is convenient if you'd like to send data to more than one Mixpanel project from a single app. - + - parameter token: your project token - parameter flushInterval: Optional. Interval to run background flushing - parameter instanceName: Optional. The name you want to uniquely identify the Mixpanel Instance. @@ -171,230 +181,244 @@ open class Mixpanel { - parameter superProperties: Optional. Super properties dictionary to register during initialization - parameter proxyServerConfig: Optional. Setup for proxy server. - parameter useGzipCompression: Optional. Whether to use gzip compression for network requests. - + - important: If you have more than one Mixpanel instance, it is beneficial to initialize the instances with an instanceName. Then they can be reached by calling getInstance with name. - + - returns: returns a mixpanel instance if needed to keep throughout the project. You can always get the instance by calling getInstance(name) */ - + @discardableResult - open class func initialize(token apiToken: String, - flushInterval: Double = 60, - instanceName: String? = nil, - optOutTrackingByDefault: Bool = false, - useUniqueDistinctId: Bool = false, - superProperties: Properties? = nil, - proxyServerConfig: ProxyServerConfig, - useGzipCompression: Bool = false) -> MixpanelInstance { - return MixpanelManager.sharedInstance.initialize(token: apiToken, - flushInterval: flushInterval, - instanceName: ((instanceName != nil) ? instanceName! : apiToken), - trackAutomaticEvents: false, - optOutTrackingByDefault: optOutTrackingByDefault, - useUniqueDistinctId: useUniqueDistinctId, - superProperties: superProperties, - proxyServerConfig: proxyServerConfig, - useGzipCompression: useGzipCompression) + open class func initialize( + token apiToken: String, + flushInterval: Double = 60, + instanceName: String? = nil, + optOutTrackingByDefault: Bool = false, + useUniqueDistinctId: Bool = false, + superProperties: Properties? = nil, + proxyServerConfig: ProxyServerConfig, + useGzipCompression: Bool = false + ) -> MixpanelInstance { + return MixpanelManager.sharedInstance.initialize( + token: apiToken, + flushInterval: flushInterval, + instanceName: ((instanceName != nil) ? instanceName! : apiToken), + trackAutomaticEvents: false, + optOutTrackingByDefault: optOutTrackingByDefault, + useUniqueDistinctId: useUniqueDistinctId, + superProperties: superProperties, + proxyServerConfig: proxyServerConfig, + useGzipCompression: useGzipCompression) } -#endif // os(OSX) - - /** + #endif // os(OSX) + + /** Gets the mixpanel instance with the given name - + - parameter name: the instance name - + - returns: returns the mixpanel instance */ - open class func getInstance(name: String) -> MixpanelInstance? { - return MixpanelManager.sharedInstance.getInstance(name: name) - } - - /** + open class func getInstance(name: String) -> MixpanelInstance? { + return MixpanelManager.sharedInstance.getInstance(name: name) + } + + /** Returns the main instance that was initialized. - + If not specified explicitly, the main instance is always the last instance added - + - returns: returns the main Mixpanel instance */ - open class func mainInstance() -> MixpanelInstance { - if let instance = MixpanelManager.sharedInstance.getMainInstance() { - return instance - } else { - #if !targetEnvironment(simulator) - assert(false, "You have to call initialize(token:trackAutomaticEvents:) before calling the main instance, " + - "or define a new main instance if removing the main one") - #endif - -#if !os(OSX) && !os(watchOS) - return Mixpanel.initialize(token: "", trackAutomaticEvents: true) -#else - return Mixpanel.initialize(token: "") -#endif - - } + open class func mainInstance() -> MixpanelInstance { + if let instance = MixpanelManager.sharedInstance.getMainInstance() { + return instance + } else { + #if !targetEnvironment(simulator) + assert( + false, + "You have to call initialize(token:trackAutomaticEvents:) before calling the main instance, " + + "or define a new main instance if removing the main one") + #endif + + #if !os(OSX) && !os(watchOS) + return Mixpanel.initialize(token: "", trackAutomaticEvents: true) + #else + return Mixpanel.initialize(token: "") + #endif + } - - /// Returns the main Mixpanel instance if it has been initialized. - /// - Returns: An optional MixpanelInstance, or nil if not yet initialized. - public class func safeMainInstance() -> MixpanelInstance? { - if let instance = MixpanelManager.sharedInstance.getMainInstance() { - return instance - } else { - MixpanelLogger.warn(message: "WARNING: Mixpanel main instance is NOT initialized. Call Mixpanel.initialize(...) first.") - return nil - } + } + + /// Returns the main Mixpanel instance if it has been initialized. + /// - Returns: An optional MixpanelInstance, or nil if not yet initialized. + public class func safeMainInstance() -> MixpanelInstance? { + if let instance = MixpanelManager.sharedInstance.getMainInstance() { + return instance + } else { + MixpanelLogger.warn( + message: + "WARNING: Mixpanel main instance is NOT initialized. Call Mixpanel.initialize(...) first." + ) + return nil } - - /** + } + + /** Sets the main instance based on the instance name - + - parameter name: the instance name */ - open class func setMainInstance(name: String) { - MixpanelManager.sharedInstance.setMainInstance(name: name) - } - - /** + open class func setMainInstance(name: String) { + MixpanelManager.sharedInstance.setMainInstance(name: name) + } + + /** Removes an unneeded Mixpanel instance based on its name - + - parameter name: the instance name */ - open class func removeInstance(name: String) { - MixpanelManager.sharedInstance.removeInstance(name: name) - } + open class func removeInstance(name: String) { + MixpanelManager.sharedInstance.removeInstance(name: name) + } } final class MixpanelManager { - static let sharedInstance = MixpanelManager() - private var instances: [String: MixpanelInstance] - private var mainInstance: MixpanelInstance? - private let readWriteLock: ReadWriteLock - private let instanceQueue: DispatchQueue - - init() { - instances = [String: MixpanelInstance]() - MixpanelLogger.addLogging(PrintLogging()) - readWriteLock = ReadWriteLock(label: "com.mixpanel.instance.manager.lock") - instanceQueue = DispatchQueue(label: "com.mixpanel.instance.manager.instance", qos: .utility, autoreleaseFrequency: .workItem) - } - - func initialize(options: MixpanelOptions) -> MixpanelInstance { - let instanceName = options.instanceName ?? options.token - return dequeueInstance(instanceName: instanceName) { - return MixpanelInstance(options: options) - } - } - - func initialize(token apiToken: String, - flushInterval: Double, - instanceName: String, - trackAutomaticEvents: Bool, - optOutTrackingByDefault: Bool = false, - useUniqueDistinctId: Bool = false, - superProperties: Properties? = nil, - serverURL: String? = nil, - useGzipCompression: Bool = false - ) -> MixpanelInstance { - return dequeueInstance(instanceName: instanceName) { - return MixpanelInstance(apiToken: apiToken, - flushInterval: flushInterval, - name: instanceName, - trackAutomaticEvents: trackAutomaticEvents, - optOutTrackingByDefault: optOutTrackingByDefault, - useUniqueDistinctId: useUniqueDistinctId, - superProperties: superProperties, - serverURL: serverURL, - useGzipCompression: useGzipCompression) - } - } - - func initialize(token apiToken: String, - flushInterval: Double, - instanceName: String, - trackAutomaticEvents: Bool, - optOutTrackingByDefault: Bool = false, - useUniqueDistinctId: Bool = false, - superProperties: Properties? = nil, - proxyServerConfig: ProxyServerConfig, - useGzipCompression: Bool = false - ) -> MixpanelInstance { - return dequeueInstance(instanceName: instanceName) { - return MixpanelInstance(apiToken: apiToken, - flushInterval: flushInterval, - name: instanceName, - trackAutomaticEvents: trackAutomaticEvents, - optOutTrackingByDefault: optOutTrackingByDefault, - useUniqueDistinctId: useUniqueDistinctId, - superProperties: superProperties, - proxyServerConfig: proxyServerConfig, - useGzipCompression: useGzipCompression) - } - } - - private func dequeueInstance(instanceName: String, instanceCreation: () -> MixpanelInstance) -> MixpanelInstance { - instanceQueue.sync { - var instance: MixpanelInstance? - if let instance = instances[instanceName] { - mainInstance = instance - return - } - - instance = instanceCreation() - readWriteLock.write { - instances[instanceName] = instance! - mainInstance = instance! - } - } - return mainInstance! - } - - func getInstance(name instanceName: String) -> MixpanelInstance? { - var instance: MixpanelInstance? - readWriteLock.read { - instance = instances[instanceName] - } - if instance == nil { - MixpanelLogger.warn(message: "no such instance: \(instanceName)") - return nil - } - return instance - } - - func getMainInstance() -> MixpanelInstance? { - return mainInstance - } - - func getAllInstances() -> [MixpanelInstance]? { - var allInstances: [MixpanelInstance]? - readWriteLock.read { - allInstances = Array(instances.values) - } - return allInstances - } - - func setMainInstance(name instanceName: String) { - var instance: MixpanelInstance? - readWriteLock.read { - instance = instances[instanceName] - } - if instance == nil { - return - } - mainInstance = instance - } - - func removeInstance(name instanceName: String) { - readWriteLock.write { - if instances[instanceName] === mainInstance { - mainInstance = nil - } - instances[instanceName] = nil - } - } - - -} + static let sharedInstance = MixpanelManager() + private var instances: [String: MixpanelInstance] + private var mainInstance: MixpanelInstance? + private let readWriteLock: ReadWriteLock + private let instanceQueue: DispatchQueue + init() { + instances = [String: MixpanelInstance]() + MixpanelLogger.addLogging(PrintLogging()) + readWriteLock = ReadWriteLock(label: "com.mixpanel.instance.manager.lock") + instanceQueue = DispatchQueue( + label: "com.mixpanel.instance.manager.instance", qos: .utility, + autoreleaseFrequency: .workItem) + } + + func initialize(options: MixpanelOptions) -> MixpanelInstance { + let instanceName = options.instanceName ?? options.token + return dequeueInstance(instanceName: instanceName) { + return MixpanelInstance(options: options) + } + } + + func initialize( + token apiToken: String, + flushInterval: Double, + instanceName: String, + trackAutomaticEvents: Bool, + optOutTrackingByDefault: Bool = false, + useUniqueDistinctId: Bool = false, + superProperties: Properties? = nil, + serverURL: String? = nil, + useGzipCompression: Bool = false + ) -> MixpanelInstance { + return dequeueInstance(instanceName: instanceName) { + return MixpanelInstance( + apiToken: apiToken, + flushInterval: flushInterval, + name: instanceName, + trackAutomaticEvents: trackAutomaticEvents, + optOutTrackingByDefault: optOutTrackingByDefault, + useUniqueDistinctId: useUniqueDistinctId, + superProperties: superProperties, + serverURL: serverURL, + useGzipCompression: useGzipCompression) + } + } + + func initialize( + token apiToken: String, + flushInterval: Double, + instanceName: String, + trackAutomaticEvents: Bool, + optOutTrackingByDefault: Bool = false, + useUniqueDistinctId: Bool = false, + superProperties: Properties? = nil, + proxyServerConfig: ProxyServerConfig, + useGzipCompression: Bool = false + ) -> MixpanelInstance { + return dequeueInstance(instanceName: instanceName) { + return MixpanelInstance( + apiToken: apiToken, + flushInterval: flushInterval, + name: instanceName, + trackAutomaticEvents: trackAutomaticEvents, + optOutTrackingByDefault: optOutTrackingByDefault, + useUniqueDistinctId: useUniqueDistinctId, + superProperties: superProperties, + proxyServerConfig: proxyServerConfig, + useGzipCompression: useGzipCompression) + } + } + + private func dequeueInstance(instanceName: String, instanceCreation: () -> MixpanelInstance) + -> MixpanelInstance + { + instanceQueue.sync { + var instance: MixpanelInstance? + if let instance = instances[instanceName] { + mainInstance = instance + return + } + + instance = instanceCreation() + readWriteLock.write { + instances[instanceName] = instance! + mainInstance = instance! + } + } + return mainInstance! + } + + func getInstance(name instanceName: String) -> MixpanelInstance? { + var instance: MixpanelInstance? + readWriteLock.read { + instance = instances[instanceName] + } + if instance == nil { + MixpanelLogger.warn(message: "no such instance: \(instanceName)") + return nil + } + return instance + } + + func getMainInstance() -> MixpanelInstance? { + return mainInstance + } + + func getAllInstances() -> [MixpanelInstance]? { + var allInstances: [MixpanelInstance]? + readWriteLock.read { + allInstances = Array(instances.values) + } + return allInstances + } + + func setMainInstance(name instanceName: String) { + var instance: MixpanelInstance? + readWriteLock.read { + instance = instances[instanceName] + } + if instance == nil { + return + } + mainInstance = instance + } + + func removeInstance(name instanceName: String) { + readWriteLock.write { + if instances[instanceName] === mainInstance { + mainInstance = nil + } + instances[instanceName] = nil + } + } + +} diff --git a/Sources/MixpanelInstance.swift b/Sources/MixpanelInstance.swift index b8854ccb..cbe28a55 100644 --- a/Sources/MixpanelInstance.swift +++ b/Sources/MixpanelInstance.swift @@ -7,47 +7,44 @@ // import Foundation + #if !os(OSX) -import UIKit + import UIKit #else -import Cocoa -#endif // os(OSX) + import Cocoa +#endif // os(OSX) #if os(iOS) -import SystemConfiguration + import SystemConfiguration #endif #if os(iOS) -import CoreTelephony -#endif // os(iOS) + import CoreTelephony +#endif // os(iOS) private let devicePrefix = "$device:" -/** - * Delegate protocol for updating the Proxy Server API's network behavior. - */ +/// Delegate protocol for updating the Proxy Server API's network behavior. public protocol MixpanelProxyServerDelegate: AnyObject { - /** + /** Asks the delegate to return API resource items like query params & headers for proxy Server. - + - parameter mixpanel: The mixpanel instance - + - returns: return ServerProxyResource to give custom headers and query params. */ - func mixpanelResourceForProxyServer(_ name: String) -> ServerProxyResource? + func mixpanelResourceForProxyServer(_ name: String) -> ServerProxyResource? } -/** - * Delegate protocol for controlling the Mixpanel API's network behavior. - */ +/// Delegate protocol for controlling the Mixpanel API's network behavior. public protocol MixpanelDelegate: AnyObject { - /** + /** Asks the delegate if data should be uploaded to the server. - + - parameter mixpanel: The mixpanel instance - + - returns: return true to upload now or false to defer until later */ - func mixpanelWillFlush(_ mixpanel: MixpanelInstance) -> Bool + func mixpanelWillFlush(_ mixpanel: MixpanelInstance) -> Bool } public typealias Properties = [String: MixpanelType] @@ -55,785 +52,817 @@ typealias InternalProperties = [String: Any] typealias Queue = [InternalProperties] protocol AppLifecycle { - func applicationDidBecomeActive() - func applicationWillResignActive() + func applicationDidBecomeActive() + func applicationWillResignActive() } public struct ProxyServerConfig { - public init?(serverUrl: String, delegate: MixpanelProxyServerDelegate? = nil) { - /// check if proxy server is not same as default mixpanel API - /// if same, then fail the initializer - /// this is to avoid case where client might inadvertently use headers intended for the proxy server - /// on Mixpanel's default server, leading to unexpected behavior. - guard serverUrl != BasePath.DefaultMixpanelAPI else { return nil } - self.serverUrl = serverUrl - self.delegate = delegate - } - - let serverUrl: String - let delegate: MixpanelProxyServerDelegate? + public init?(serverUrl: String, delegate: MixpanelProxyServerDelegate? = nil) { + /// check if proxy server is not same as default mixpanel API + /// if same, then fail the initializer + /// this is to avoid case where client might inadvertently use headers intended for the proxy server + /// on Mixpanel's default server, leading to unexpected behavior. + guard serverUrl != BasePath.DefaultMixpanelAPI else { return nil } + self.serverUrl = serverUrl + self.delegate = delegate + } + + let serverUrl: String + let delegate: MixpanelProxyServerDelegate? } /// The class that represents the Mixpanel Instance -open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDelegate, MixpanelFlagDelegate { +open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDelegate, + MixpanelFlagDelegate +{ - private let options: MixpanelOptions - - /// apiToken string that identifies the project to track data to - open var apiToken = "" - - /// The a MixpanelDelegate object that gives control over Mixpanel network activity. - open weak var delegate: MixpanelDelegate? - - /// distinctId string that uniquely identifies the current user. - open var distinctId = "" - - /// anonymousId string that uniquely identifies the device. - open var anonymousId: String? - - /// userId string that identify is called with. - open var userId: String? - - /// hadPersistedDistinctId is a boolean value which specifies that the stored distinct_id - /// already exists in persistence - open var hadPersistedDistinctId: Bool? - - /// alias string that uniquely identifies the current user. - open var alias: String? - - /// Accessor to the Mixpanel People API object. - open var people: People! - - /// Accessor the Mixpanel Feature Flags API object. - open var flags: MixpanelFlags! - - let mixpanelPersistence: MixpanelPersistence - - /// Accessor to the Mixpanel People API object. - var groups: [String: Group] = [:] - - /// Controls whether to show spinning network activity indicator when flushing - /// data to the Mixpanel servers. Defaults to true. - open var showNetworkActivityIndicator = true - - /// This allows enabling or disabling collecting common mobile events, - open var trackAutomaticEventsEnabled: Bool - - /// Flush timer's interval. - /// Setting a flush interval of 0 will turn off the flush timer and you need to call the flush() API manually - /// to upload queued data to the Mixpanel server. - open var flushInterval: Double { - get { - return flushInstance.flushInterval - } - set { - flushInstance.flushInterval = newValue - } - } - - /// Control whether the library should flush data to Mixpanel when the app - /// enters the background. Defaults to true. - open var flushOnBackground: Bool { - get { - return flushInstance.flushOnBackground - } - set { - flushInstance.flushOnBackground = newValue - } - } - - /// Controls whether to automatically send the client IP Address as part of - /// event tracking. With an IP address, the Mixpanel Dashboard will show you the users' city. - /// Defaults to true. - open var useIPAddressForGeoLocation: Bool { - get { - return flushInstance.useIPAddressForGeoLocation - } - set { - flushInstance.useIPAddressForGeoLocation = newValue - } - } - - /// The `flushBatchSize` property determines the number of events sent in a single network request to the Mixpanel server. - /// By configuring this value, you can optimize network usage and manage the frequency of communication between the client - /// and the server. The maximum size is 50; any value over 50 will default to 50. - open var flushBatchSize: Int { - get { - return flushInstance.flushBatchSize - } - set { - flushInstance.flushBatchSize = min(newValue, APIConstants.maxBatchSize) - } - } - - - /// The base URL used for Mixpanel API requests. - /// Useful if you need to proxy Mixpanel requests. Defaults to - /// https://api.mixpanel.com. - open var serverURL = BasePath.DefaultMixpanelAPI { - didSet { - flushInstance.serverURL = serverURL - } - } + private let options: MixpanelOptions - open var useGzipCompression: Bool = false { - didSet { - flushInstance.useGzipCompression = useGzipCompression - } - } - - /// The a MixpanelProxyServerDelegate object that gives config control over Proxy Server's network activity. - open weak var proxyServerDelegate: MixpanelProxyServerDelegate? = nil - - - open var debugDescription: String { - return "Mixpanel(\n" - + " Token: \(apiToken),\n" - + " Distinct Id: \(distinctId)\n" - + ")" + /// apiToken string that identifies the project to track data to + open var apiToken = "" + + /// The a MixpanelDelegate object that gives control over Mixpanel network activity. + open weak var delegate: MixpanelDelegate? + + /// distinctId string that uniquely identifies the current user. + open var distinctId = "" + + /// anonymousId string that uniquely identifies the device. + open var anonymousId: String? + + /// userId string that identify is called with. + open var userId: String? + + /// hadPersistedDistinctId is a boolean value which specifies that the stored distinct_id + /// already exists in persistence + open var hadPersistedDistinctId: Bool? + + /// alias string that uniquely identifies the current user. + open var alias: String? + + /// Accessor to the Mixpanel People API object. + open var people: People! + + /// Accessor the Mixpanel Feature Flags API object. + open var flags: MixpanelFlags! + + let mixpanelPersistence: MixpanelPersistence + + /// Accessor to the Mixpanel People API object. + var groups: [String: Group] = [:] + + /// Controls whether to show spinning network activity indicator when flushing + /// data to the Mixpanel servers. Defaults to true. + open var showNetworkActivityIndicator = true + + /// This allows enabling or disabling collecting common mobile events, + open var trackAutomaticEventsEnabled: Bool + + /// Flush timer's interval. + /// Setting a flush interval of 0 will turn off the flush timer and you need to call the flush() API manually + /// to upload queued data to the Mixpanel server. + open var flushInterval: Double { + get { + return flushInstance.flushInterval } - - /// This allows enabling or disabling of all Mixpanel logs at run time. - /// - Note: All logging is disabled by default. Usually, this is only required - /// if you are running in to issues with the SDK and you need support. - open var loggingEnabled: Bool = false { - didSet { - if loggingEnabled { - MixpanelLogger.enableLevel(.debug) - MixpanelLogger.enableLevel(.info) - MixpanelLogger.enableLevel(.warning) - MixpanelLogger.enableLevel(.error) - MixpanelLogger.info(message: "MixpanelLogging Enabled") - } else { - MixpanelLogger.info(message: "MixpanelLogging Disabled") - MixpanelLogger.disableLevel(.debug) - MixpanelLogger.disableLevel(.info) - MixpanelLogger.disableLevel(.warning) - MixpanelLogger.disableLevel(.error) - } -#if DEBUG - var trackProps: Properties = ["MixpanelLogging Enabled": loggingEnabled] - if (superProperties["mp_lib"] != nil) { - trackProps["mp_lib"] = self.superProperties["mp_lib"] as! String - } - if (superProperties["$lib_version"] != nil) { - trackProps["$lib_version"] = self.superProperties["$lib_version"] as! String - } -#endif - } + set { + flushInstance.flushInterval = newValue } - - /// A unique identifier for this MixpanelInstance - public let name: String - - /// The minimum session duration (ms) that is tracked in automatic events. - /// The default value is 10000 (10 seconds). -#if os(iOS) || os(tvOS) || os(visionOS) + } + + /// Control whether the library should flush data to Mixpanel when the app + /// enters the background. Defaults to true. + open var flushOnBackground: Bool { + get { + return flushInstance.flushOnBackground + } + set { + flushInstance.flushOnBackground = newValue + } + } + + /// Controls whether to automatically send the client IP Address as part of + /// event tracking. With an IP address, the Mixpanel Dashboard will show you the users' city. + /// Defaults to true. + open var useIPAddressForGeoLocation: Bool { + get { + return flushInstance.useIPAddressForGeoLocation + } + set { + flushInstance.useIPAddressForGeoLocation = newValue + } + } + + /// The `flushBatchSize` property determines the number of events sent in a single network request to the Mixpanel server. + /// By configuring this value, you can optimize network usage and manage the frequency of communication between the client + /// and the server. The maximum size is 50; any value over 50 will default to 50. + open var flushBatchSize: Int { + get { + return flushInstance.flushBatchSize + } + set { + flushInstance.flushBatchSize = min(newValue, APIConstants.maxBatchSize) + } + } + + /// The base URL used for Mixpanel API requests. + /// Useful if you need to proxy Mixpanel requests. Defaults to + /// https://api.mixpanel.com. + open var serverURL = BasePath.DefaultMixpanelAPI { + didSet { + flushInstance.serverURL = serverURL + } + } + + open var useGzipCompression: Bool = false { + didSet { + flushInstance.useGzipCompression = useGzipCompression + } + } + + /// The a MixpanelProxyServerDelegate object that gives config control over Proxy Server's network activity. + open weak var proxyServerDelegate: MixpanelProxyServerDelegate? = nil + + open var debugDescription: String { + return "Mixpanel(\n" + + " Token: \(apiToken),\n" + + " Distinct Id: \(distinctId)\n" + + ")" + } + + /// This allows enabling or disabling of all Mixpanel logs at run time. + /// - Note: All logging is disabled by default. Usually, this is only required + /// if you are running in to issues with the SDK and you need support. + open var loggingEnabled: Bool = false { + didSet { + if loggingEnabled { + MixpanelLogger.enableLevel(.debug) + MixpanelLogger.enableLevel(.info) + MixpanelLogger.enableLevel(.warning) + MixpanelLogger.enableLevel(.error) + MixpanelLogger.info(message: "MixpanelLogging Enabled") + } else { + MixpanelLogger.info(message: "MixpanelLogging Disabled") + MixpanelLogger.disableLevel(.debug) + MixpanelLogger.disableLevel(.info) + MixpanelLogger.disableLevel(.warning) + MixpanelLogger.disableLevel(.error) + } + #if DEBUG + var trackProps: Properties = ["MixpanelLogging Enabled": loggingEnabled] + if superProperties["mp_lib"] != nil { + trackProps["mp_lib"] = self.superProperties["mp_lib"] as! String + } + if superProperties["$lib_version"] != nil { + trackProps["$lib_version"] = self.superProperties["$lib_version"] as! String + } + #endif + } + } + + /// A unique identifier for this MixpanelInstance + public let name: String + + /// The minimum session duration (ms) that is tracked in automatic events. + /// The default value is 10000 (10 seconds). + #if os(iOS) || os(tvOS) || os(visionOS) open var minimumSessionDuration: UInt64 { - get { - return automaticEvents.minimumSessionDuration - } - set { - automaticEvents.minimumSessionDuration = newValue - } + get { + return automaticEvents.minimumSessionDuration + } + set { + automaticEvents.minimumSessionDuration = newValue + } } - + /// The maximum session duration (ms) that is tracked in automatic events. /// The default value is UINT64_MAX (no maximum session duration). open var maximumSessionDuration: UInt64 { - get { - return automaticEvents.maximumSessionDuration - } - set { - automaticEvents.maximumSessionDuration = newValue - } + get { + return automaticEvents.maximumSessionDuration + } + set { + automaticEvents.maximumSessionDuration = newValue + } } -#endif - var superProperties = InternalProperties() - var trackingQueue: DispatchQueue - var networkQueue: DispatchQueue - var optOutStatus: Bool? - var useUniqueDistinctId: Bool - var timedEvents = InternalProperties() - - let readWriteLock: ReadWriteLock -#if os(iOS) && !targetEnvironment(macCatalyst) + #endif + var superProperties = InternalProperties() + var trackingQueue: DispatchQueue + var networkQueue: DispatchQueue + var optOutStatus: Bool? + var useUniqueDistinctId: Bool + var timedEvents = InternalProperties() + + let readWriteLock: ReadWriteLock + #if os(iOS) && !targetEnvironment(macCatalyst) static let reachability = SCNetworkReachabilityCreateWithName(nil, "api.mixpanel.com") static let telephonyInfo = CTTelephonyNetworkInfo() -#endif -#if !os(OSX) && !os(watchOS) + #endif + #if !os(OSX) && !os(watchOS) var taskId = UIBackgroundTaskIdentifier.invalid -#endif // os(OSX) - let sessionMetadata: SessionMetadata - let flushInstance: Flush - let trackInstance: Track -#if os(iOS) || os(tvOS) || os(visionOS) + #endif // os(OSX) + let sessionMetadata: SessionMetadata + let flushInstance: Flush + let trackInstance: Track + #if os(iOS) || os(tvOS) || os(visionOS) let automaticEvents = AutomaticEvents() -#endif - private let registerSuperPropertiesNotificationName = Notification.Name("com.mixpanel.properties.register") - private let unregisterSuperPropertiesNotificationName = Notification.Name("com.mixpanel.properties.unregister") - - convenience init(options: MixpanelOptions) { - self.init(apiToken: options.token, - flushInterval: options.flushInterval, - name: options.instanceName ?? options.token, - trackAutomaticEvents: options.trackAutomaticEvents, - optOutTrackingByDefault: options.optOutTrackingByDefault, - useUniqueDistinctId: options.useUniqueDistinctId, - superProperties: options.superProperties, - serverURL: options.serverURL, - proxyServerDelegate: options.proxyServerConfig?.delegate, - useGzipCompression: options.useGzipCompression, - options: options) + #endif + private let registerSuperPropertiesNotificationName = Notification.Name( + "com.mixpanel.properties.register") + private let unregisterSuperPropertiesNotificationName = Notification.Name( + "com.mixpanel.properties.unregister") + + convenience init(options: MixpanelOptions) { + self.init( + apiToken: options.token, + flushInterval: options.flushInterval, + name: options.instanceName ?? options.token, + trackAutomaticEvents: options.trackAutomaticEvents, + optOutTrackingByDefault: options.optOutTrackingByDefault, + useUniqueDistinctId: options.useUniqueDistinctId, + superProperties: options.superProperties, + serverURL: options.serverURL, + proxyServerDelegate: options.proxyServerConfig?.delegate, + useGzipCompression: options.useGzipCompression, + options: options) + } + + convenience init( + apiToken: String?, + flushInterval: Double, + name: String, + trackAutomaticEvents: Bool, + optOutTrackingByDefault: Bool = false, + useUniqueDistinctId: Bool = false, + superProperties: Properties? = nil, + proxyServerConfig: ProxyServerConfig, + useGzipCompression: Bool = false + ) { + self.init( + apiToken: apiToken, + flushInterval: flushInterval, + name: name, + trackAutomaticEvents: trackAutomaticEvents, + optOutTrackingByDefault: optOutTrackingByDefault, + useUniqueDistinctId: useUniqueDistinctId, + superProperties: superProperties, + serverURL: proxyServerConfig.serverUrl, + proxyServerDelegate: proxyServerConfig.delegate, + useGzipCompression: useGzipCompression) + } + + convenience init( + apiToken: String?, + flushInterval: Double, + name: String, + trackAutomaticEvents: Bool, + optOutTrackingByDefault: Bool = false, + useUniqueDistinctId: Bool = false, + superProperties: Properties? = nil, + serverURL: String? = nil, + useGzipCompression: Bool = false + ) { + self.init( + apiToken: apiToken, + flushInterval: flushInterval, + name: name, + trackAutomaticEvents: trackAutomaticEvents, + optOutTrackingByDefault: optOutTrackingByDefault, + useUniqueDistinctId: useUniqueDistinctId, + superProperties: superProperties, + serverURL: serverURL, + proxyServerDelegate: nil, + useGzipCompression: useGzipCompression) + } + + private init( + apiToken: String?, + flushInterval: Double, + name: String, + trackAutomaticEvents: Bool, + optOutTrackingByDefault: Bool = false, + useUniqueDistinctId: Bool = false, + superProperties: Properties? = nil, + serverURL: String? = nil, + proxyServerDelegate: MixpanelProxyServerDelegate? = nil, + useGzipCompression: Bool = false, + options: MixpanelOptions? = nil + ) { + // Store the config if provided, otherwise create one with the current values + self.options = + options + ?? MixpanelOptions( + token: apiToken ?? "", + flushInterval: flushInterval, + instanceName: name, + trackAutomaticEvents: trackAutomaticEvents, + optOutTrackingByDefault: optOutTrackingByDefault, + useUniqueDistinctId: useUniqueDistinctId, + superProperties: superProperties, + serverURL: serverURL, + useGzipCompression: useGzipCompression + ) + + if let apiToken = apiToken, !apiToken.isEmpty { + self.apiToken = apiToken } - - convenience init( - apiToken: String?, - flushInterval: Double, - name: String, - trackAutomaticEvents: Bool, - optOutTrackingByDefault: Bool = false, - useUniqueDistinctId: Bool = false, - superProperties: Properties? = nil, - proxyServerConfig: ProxyServerConfig, - useGzipCompression: Bool = false - ) { - self.init(apiToken: apiToken, - flushInterval: flushInterval, - name: name, - trackAutomaticEvents: trackAutomaticEvents, - optOutTrackingByDefault: optOutTrackingByDefault, - useUniqueDistinctId: useUniqueDistinctId, - superProperties: superProperties, - serverURL: proxyServerConfig.serverUrl, - proxyServerDelegate: proxyServerConfig.delegate, - useGzipCompression: useGzipCompression) + trackAutomaticEventsEnabled = trackAutomaticEvents + if let serverURL = serverURL { + self.serverURL = serverURL } - - convenience init( - apiToken: String?, - flushInterval: Double, - name: String, - trackAutomaticEvents: Bool, - optOutTrackingByDefault: Bool = false, - useUniqueDistinctId: Bool = false, - superProperties: Properties? = nil, - serverURL: String? = nil, - useGzipCompression: Bool = false - ) { - self.init(apiToken: apiToken, - flushInterval: flushInterval, - name: name, - trackAutomaticEvents: trackAutomaticEvents, - optOutTrackingByDefault: optOutTrackingByDefault, - useUniqueDistinctId: useUniqueDistinctId, - superProperties: superProperties, - serverURL: serverURL, - proxyServerDelegate: nil, - useGzipCompression: useGzipCompression) + self.useGzipCompression = useGzipCompression + self.proxyServerDelegate = proxyServerDelegate + let label = "com.mixpanel.\(self.apiToken)" + trackingQueue = DispatchQueue( + label: "\(label).tracking)", qos: .utility, autoreleaseFrequency: .workItem) + networkQueue = DispatchQueue( + label: "\(label).network)", qos: .utility, autoreleaseFrequency: .workItem) + self.name = name + + mixpanelPersistence = MixpanelPersistence.init(instanceName: name) + mixpanelPersistence.migrate() + self.useUniqueDistinctId = useUniqueDistinctId + + readWriteLock = ReadWriteLock(label: "com.mixpanel.globallock") + flushInstance = Flush(serverURL: self.serverURL, useGzipCompression: useGzipCompression) + sessionMetadata = SessionMetadata(trackingQueue: trackingQueue) + trackInstance = Track( + apiToken: self.apiToken, + instanceName: self.name, + lock: self.readWriteLock, + metadata: sessionMetadata, mixpanelPersistence: mixpanelPersistence) + flags = FeatureFlagManager(serverURL: self.serverURL) + trackInstance.mixpanelInstance = self + flags.delegate = self + #if os(iOS) && !targetEnvironment(macCatalyst) + if let reachability = MixpanelInstance.reachability { + var context = SCNetworkReachabilityContext( + version: 0, info: nil, retain: nil, release: nil, copyDescription: nil) + func reachabilityCallback( + reachability: SCNetworkReachability, + flags: SCNetworkReachabilityFlags, + unsafePointer: UnsafeMutableRawPointer? + ) { + let wifi = + flags.contains(SCNetworkReachabilityFlags.reachable) + && !flags.contains(SCNetworkReachabilityFlags.isWWAN) + AutomaticProperties.automaticPropertiesLock.write { + AutomaticProperties.properties["$wifi"] = wifi + } + MixpanelLogger.info(message: "reachability changed, wifi=\(wifi)") + } + if SCNetworkReachabilitySetCallback(reachability, reachabilityCallback, &context) { + if !SCNetworkReachabilitySetDispatchQueue(reachability, trackingQueue) { + // cleanup callback if setting dispatch queue failed + SCNetworkReachabilitySetCallback(reachability, nil, nil) + } + } + } + #endif + flushInstance.delegate = self + distinctId = devicePrefix + defaultDeviceId() + people = People( + apiToken: self.apiToken, + serialQueue: trackingQueue, + lock: self.readWriteLock, + metadata: sessionMetadata, mixpanelPersistence: mixpanelPersistence) + people.mixpanelInstance = self + people.delegate = self + flushInstance.flushInterval = flushInterval + #if !os(watchOS) + setupListeners() + #endif + unarchive() + + // check whether we should opt out by default + // note: we don't override opt out persistence here since opt-out default state is often + // used as an initial state while GDPR information is being collected + if optOutTrackingByDefault && (hasOptedOutTracking() || optOutStatus == nil) { + optOutTracking() } - - - private init( - apiToken: String?, - flushInterval: Double, - name: String, - trackAutomaticEvents: Bool, - optOutTrackingByDefault: Bool = false, - useUniqueDistinctId: Bool = false, - superProperties: Properties? = nil, - serverURL: String? = nil, - proxyServerDelegate: MixpanelProxyServerDelegate? = nil, - useGzipCompression: Bool = false, - options: MixpanelOptions? = nil - ) { - // Store the config if provided, otherwise create one with the current values - self.options = options ?? MixpanelOptions( - token: apiToken ?? "", - flushInterval: flushInterval, - instanceName: name, - trackAutomaticEvents: trackAutomaticEvents, - optOutTrackingByDefault: optOutTrackingByDefault, - useUniqueDistinctId: useUniqueDistinctId, - superProperties: superProperties, - serverURL: serverURL, - useGzipCompression: useGzipCompression - ) - - if let apiToken = apiToken, !apiToken.isEmpty { - self.apiToken = apiToken - } - trackAutomaticEventsEnabled = trackAutomaticEvents - if let serverURL = serverURL { - self.serverURL = serverURL - } - self.useGzipCompression = useGzipCompression - self.proxyServerDelegate = proxyServerDelegate - let label = "com.mixpanel.\(self.apiToken)" - trackingQueue = DispatchQueue(label: "\(label).tracking)", qos: .utility, autoreleaseFrequency: .workItem) - networkQueue = DispatchQueue(label: "\(label).network)", qos: .utility, autoreleaseFrequency: .workItem) - self.name = name - - mixpanelPersistence = MixpanelPersistence.init(instanceName: name) - mixpanelPersistence.migrate() - self.useUniqueDistinctId = useUniqueDistinctId - - readWriteLock = ReadWriteLock(label: "com.mixpanel.globallock") - flushInstance = Flush(serverURL: self.serverURL, useGzipCompression: useGzipCompression) - sessionMetadata = SessionMetadata(trackingQueue: trackingQueue) - trackInstance = Track(apiToken: self.apiToken, - instanceName: self.name, - lock: self.readWriteLock, - metadata: sessionMetadata, mixpanelPersistence: mixpanelPersistence) - flags = FeatureFlagManager(serverURL: self.serverURL) - trackInstance.mixpanelInstance = self - flags.delegate = self -#if os(iOS) && !targetEnvironment(macCatalyst) - if let reachability = MixpanelInstance.reachability { - var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil) - func reachabilityCallback(reachability: SCNetworkReachability, - flags: SCNetworkReachabilityFlags, - unsafePointer: UnsafeMutableRawPointer?) { - let wifi = flags.contains(SCNetworkReachabilityFlags.reachable) && !flags.contains(SCNetworkReachabilityFlags.isWWAN) - AutomaticProperties.automaticPropertiesLock.write { - AutomaticProperties.properties["$wifi"] = wifi - } - MixpanelLogger.info(message: "reachability changed, wifi=\(wifi)") - } - if SCNetworkReachabilitySetCallback(reachability, reachabilityCallback, &context) { - if !SCNetworkReachabilitySetDispatchQueue(reachability, trackingQueue) { - // cleanup callback if setting dispatch queue failed - SCNetworkReachabilitySetCallback(reachability, nil, nil) - } - } - } -#endif - flushInstance.delegate = self - distinctId = devicePrefix + defaultDeviceId() - people = People(apiToken: self.apiToken, - serialQueue: trackingQueue, - lock: self.readWriteLock, - metadata: sessionMetadata, mixpanelPersistence: mixpanelPersistence) - people.mixpanelInstance = self - people.delegate = self - flushInstance.flushInterval = flushInterval -#if !os(watchOS) - setupListeners() -#endif - unarchive() - - // check whether we should opt out by default - // note: we don't override opt out persistence here since opt-out default state is often - // used as an initial state while GDPR information is being collected - if optOutTrackingByDefault && (hasOptedOutTracking() || optOutStatus == nil) { - optOutTracking() - } - - if let superProperties = superProperties { - registerSuperProperties(superProperties) - } - -#if os(iOS) || os(tvOS) || os(visionOS) - if !MixpanelInstance.isiOSAppExtension() && trackAutomaticEvents { - automaticEvents.delegate = self - automaticEvents.initializeEvents(instanceName: self.name) - } -#endif - flags.loadFlags() + + if let superProperties = superProperties { + registerSuperProperties(superProperties) } - - public func getOptions() -> MixpanelOptions { - return options - } - - public func getDistinctId() -> String { - return distinctId - } - -#if !os(OSX) && !os(watchOS) + + #if os(iOS) || os(tvOS) || os(visionOS) + if !MixpanelInstance.isiOSAppExtension() && trackAutomaticEvents { + automaticEvents.delegate = self + automaticEvents.initializeEvents(instanceName: self.name) + } + #endif + flags.loadFlags() + } + + public func getOptions() -> MixpanelOptions { + return options + } + + public func getDistinctId() -> String { + return distinctId + } + + #if !os(OSX) && !os(watchOS) private func setupListeners() { - let notificationCenter = NotificationCenter.default -#if os(iOS) && !targetEnvironment(macCatalyst) + let notificationCenter = NotificationCenter.default + #if os(iOS) && !targetEnvironment(macCatalyst) setCurrentRadio() - // Temporarily remove the ability to monitor the radio change due to a crash issue might relate to the api from Apple - // https://openradar.appspot.com/46873673 - // notificationCenter.addObserver(self, - // selector: #selector(setCurrentRadio), - // name: .CTRadioAccessTechnologyDidChange, - // object: nil) -#endif // os(iOS) - if !MixpanelInstance.isiOSAppExtension() { - notificationCenter.addObserver(self, - selector: #selector(applicationWillResignActive(_:)), - name: UIApplication.willResignActiveNotification, - object: nil) - notificationCenter.addObserver(self, - selector: #selector(applicationDidBecomeActive(_:)), - name: UIApplication.didBecomeActiveNotification, - object: nil) - notificationCenter.addObserver(self, - selector: #selector(applicationDidEnterBackground(_:)), - name: UIApplication.didEnterBackgroundNotification, - object: nil) - notificationCenter.addObserver(self, - selector: #selector(applicationWillEnterForeground(_:)), - name: UIApplication.willEnterForegroundNotification, - object: nil) - notificationCenter.addObserver( - self, - selector: #selector(handleSuperPropertiesRegistrationNotification(_:)), - name: registerSuperPropertiesNotificationName, - object: nil - ) - notificationCenter.addObserver( - self, - selector: #selector(handleSuperPropertiesRegistrationNotification(_:)), - name: unregisterSuperPropertiesNotificationName, - object: nil - ) - } + // Temporarily remove the ability to monitor the radio change due to a crash issue might relate to the api from Apple + // https://openradar.appspot.com/46873673 + // notificationCenter.addObserver(self, + // selector: #selector(setCurrentRadio), + // name: .CTRadioAccessTechnologyDidChange, + // object: nil) + #endif // os(iOS) + if !MixpanelInstance.isiOSAppExtension() { + notificationCenter.addObserver( + self, + selector: #selector(applicationWillResignActive(_:)), + name: UIApplication.willResignActiveNotification, + object: nil) + notificationCenter.addObserver( + self, + selector: #selector(applicationDidBecomeActive(_:)), + name: UIApplication.didBecomeActiveNotification, + object: nil) + notificationCenter.addObserver( + self, + selector: #selector(applicationDidEnterBackground(_:)), + name: UIApplication.didEnterBackgroundNotification, + object: nil) + notificationCenter.addObserver( + self, + selector: #selector(applicationWillEnterForeground(_:)), + name: UIApplication.willEnterForegroundNotification, + object: nil) + notificationCenter.addObserver( + self, + selector: #selector(handleSuperPropertiesRegistrationNotification(_:)), + name: registerSuperPropertiesNotificationName, + object: nil + ) + notificationCenter.addObserver( + self, + selector: #selector(handleSuperPropertiesRegistrationNotification(_:)), + name: unregisterSuperPropertiesNotificationName, + object: nil + ) + } } -#elseif os(OSX) + #elseif os(OSX) private func setupListeners() { - let notificationCenter = NotificationCenter.default - notificationCenter.addObserver(self, - selector: #selector(applicationWillResignActive(_:)), - name: NSApplication.willResignActiveNotification, - object: nil) - notificationCenter.addObserver(self, - selector: #selector(applicationDidBecomeActive(_:)), - name: NSApplication.didBecomeActiveNotification, - object: nil) + let notificationCenter = NotificationCenter.default + notificationCenter.addObserver( + self, + selector: #selector(applicationWillResignActive(_:)), + name: NSApplication.willResignActiveNotification, + object: nil) + notificationCenter.addObserver( + self, + selector: #selector(applicationDidBecomeActive(_:)), + name: NSApplication.didBecomeActiveNotification, + object: nil) } -#endif // os(OSX) - - deinit { - NotificationCenter.default.removeObserver(self) -#if os(iOS) && !os(watchOS) && !targetEnvironment(macCatalyst) - if let reachability = MixpanelInstance.reachability { - if !SCNetworkReachabilitySetCallback(reachability, nil, nil) { - MixpanelLogger.error(message: "\(self) error unsetting reachability callback") - } - if !SCNetworkReachabilitySetDispatchQueue(reachability, nil) { - MixpanelLogger.error(message: "\(self) error unsetting reachability dispatch queue") - } + #endif // os(OSX) + + deinit { + NotificationCenter.default.removeObserver(self) + #if os(iOS) && !os(watchOS) && !targetEnvironment(macCatalyst) + if let reachability = MixpanelInstance.reachability { + if !SCNetworkReachabilitySetCallback(reachability, nil, nil) { + MixpanelLogger.error(message: "\(self) error unsetting reachability callback") } -#endif - } - - static func isiOSAppExtension() -> Bool { - return Bundle.main.bundlePath.hasSuffix(".appex") - } - -#if !os(OSX) && !os(watchOS) + if !SCNetworkReachabilitySetDispatchQueue(reachability, nil) { + MixpanelLogger.error(message: "\(self) error unsetting reachability dispatch queue") + } + } + #endif + } + + static func isiOSAppExtension() -> Bool { + return Bundle.main.bundlePath.hasSuffix(".appex") + } + + #if !os(OSX) && !os(watchOS) static func sharedUIApplication() -> UIApplication? { - guard let sharedApplication = - UIApplication.perform(NSSelectorFromString("sharedApplication"))?.takeUnretainedValue() as? UIApplication else { - return nil - } - return sharedApplication + guard + let sharedApplication = + UIApplication.perform(NSSelectorFromString("sharedApplication"))?.takeUnretainedValue() + as? UIApplication + else { + return nil + } + return sharedApplication } -#endif // !os(OSX) - - @objc private func applicationDidBecomeActive(_ notification: Notification) { - flushInstance.applicationDidBecomeActive() - } - - @objc private func applicationWillResignActive(_ notification: Notification) { - flushInstance.applicationWillResignActive() -#if os(OSX) - if flushOnBackground { - flush() - } - -#endif - } - -#if !os(OSX) && !os(watchOS) + #endif // !os(OSX) + + @objc private func applicationDidBecomeActive(_ notification: Notification) { + flushInstance.applicationDidBecomeActive() + } + + @objc private func applicationWillResignActive(_ notification: Notification) { + flushInstance.applicationWillResignActive() + #if os(OSX) + if flushOnBackground { + flush() + } + + #endif + } + + #if !os(OSX) && !os(watchOS) @objc private func applicationDidEnterBackground(_ notification: Notification) { - guard let sharedApplication = MixpanelInstance.sharedUIApplication() else { - return - } - - if hasOptedOutTracking() { - return - } - - let completionHandler: () -> Void = { [weak self] in - guard let self = self else { return } - - if self.taskId != UIBackgroundTaskIdentifier.invalid { - sharedApplication.endBackgroundTask(self.taskId) - self.taskId = UIBackgroundTaskIdentifier.invalid - } - } - - taskId = sharedApplication.beginBackgroundTask(expirationHandler: completionHandler) - - // Ensure that any session replay ID is cleared when the app enters the background - unregisterSuperProperty("$mp_replay_id") - - if flushOnBackground { - flush(performFullFlush: true, completion: completionHandler) + guard let sharedApplication = MixpanelInstance.sharedUIApplication() else { + return + } + + if hasOptedOutTracking() { + return + } + + let completionHandler: () -> Void = { [weak self] in + guard let self = self else { return } + + if self.taskId != UIBackgroundTaskIdentifier.invalid { + sharedApplication.endBackgroundTask(self.taskId) + self.taskId = UIBackgroundTaskIdentifier.invalid } + } + + taskId = sharedApplication.beginBackgroundTask(expirationHandler: completionHandler) + + // Ensure that any session replay ID is cleared when the app enters the background + unregisterSuperProperty("$mp_replay_id") + + if flushOnBackground { + flush(performFullFlush: true, completion: completionHandler) + } } - + @objc private func applicationWillEnterForeground(_ notification: Notification) { - guard let sharedApplication = MixpanelInstance.sharedUIApplication() else { - return - } - sessionMetadata.applicationWillEnterForeground() - - if taskId != UIBackgroundTaskIdentifier.invalid { - sharedApplication.endBackgroundTask(taskId) - taskId = UIBackgroundTaskIdentifier.invalid -#if os(iOS) - self.updateNetworkActivityIndicator(false) -#endif // os(iOS) - } - + guard let sharedApplication = MixpanelInstance.sharedUIApplication() else { + return + } + sessionMetadata.applicationWillEnterForeground() + + if taskId != UIBackgroundTaskIdentifier.invalid { + sharedApplication.endBackgroundTask(taskId) + taskId = UIBackgroundTaskIdentifier.invalid + #if os(iOS) + self.updateNetworkActivityIndicator(false) + #endif // os(iOS) + } + } -#endif - - func addPrefixToDeviceId(deviceId: String?) -> String { - if let temp = deviceId { - return devicePrefix + temp - } - return "" + #endif + + func addPrefixToDeviceId(deviceId: String?) -> String { + if let temp = deviceId { + return devicePrefix + temp } - - func defaultDeviceId() -> String { - let distinctId: String? - if useUniqueDistinctId { - distinctId = uniqueIdentifierForDevice() - } else { -#if MIXPANEL_UNIQUE_DISTINCT_ID - distinctId = uniqueIdentifierForDevice() -#else - distinctId = nil -#endif - } - return distinctId ?? UUID().uuidString // use a random UUID by default - } - - func uniqueIdentifierForDevice() -> String? { - var distinctId: String? -#if os(OSX) - distinctId = MixpanelInstance.macOSIdentifier() -#elseif !os(watchOS) - if NSClassFromString("UIDevice") != nil { - distinctId = UIDevice.current.identifierForVendor?.uuidString - } else { - distinctId = nil - } -#else + return "" + } + + func defaultDeviceId() -> String { + let distinctId: String? + if useUniqueDistinctId { + distinctId = uniqueIdentifierForDevice() + } else { + #if MIXPANEL_UNIQUE_DISTINCT_ID + distinctId = uniqueIdentifierForDevice() + #else distinctId = nil -#endif - return distinctId + #endif } - -#if os(OSX) + return distinctId ?? UUID().uuidString // use a random UUID by default + } + + func uniqueIdentifierForDevice() -> String? { + var distinctId: String? + #if os(OSX) + distinctId = MixpanelInstance.macOSIdentifier() + #elseif !os(watchOS) + if NSClassFromString("UIDevice") != nil { + distinctId = UIDevice.current.identifierForVendor?.uuidString + } else { + distinctId = nil + } + #else + distinctId = nil + #endif + return distinctId + } + + #if os(OSX) static func macOSIdentifier() -> String? { - let platformExpert: io_service_t = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice")) - let serialNumberAsCFString = - IORegistryEntryCreateCFProperty(platformExpert, kIOPlatformSerialNumberKey as CFString, kCFAllocatorDefault, 0) - IOObjectRelease(platformExpert) - return (serialNumberAsCFString?.takeUnretainedValue() as? String) + let platformExpert: io_service_t = IOServiceGetMatchingService( + kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice")) + let serialNumberAsCFString = + IORegistryEntryCreateCFProperty( + platformExpert, kIOPlatformSerialNumberKey as CFString, kCFAllocatorDefault, 0) + IOObjectRelease(platformExpert) + return (serialNumberAsCFString?.takeUnretainedValue() as? String) } -#endif // os(OSX) - -#if os(iOS) + #endif // os(OSX) + + #if os(iOS) func updateNetworkActivityIndicator(_ on: Bool) { - if showNetworkActivityIndicator { - DispatchQueue.main.async { [on] in - MixpanelInstance.sharedUIApplication()?.isNetworkActivityIndicatorVisible = on - } + if showNetworkActivityIndicator { + DispatchQueue.main.async { [on] in + MixpanelInstance.sharedUIApplication()?.isNetworkActivityIndicatorVisible = on } + } } -#if os(iOS) && !targetEnvironment(macCatalyst) - @objc func setCurrentRadio() { + #if os(iOS) && !targetEnvironment(macCatalyst) + @objc func setCurrentRadio() { var radio = "" let prefix = "CTRadioAccessTechnology" if #available(iOS 12.0, *) { - if let radioDict = MixpanelInstance.telephonyInfo.serviceCurrentRadioAccessTechnology { - for (_, value) in radioDict where !value.isEmpty && value.hasPrefix(prefix) { - // the first should be the prefix, second the target - let components = value.components(separatedBy: prefix) - - // Something went wrong and we have more than prefix:target - guard components.count == 2 else { - continue - } - - // Safe to directly access by index since we confirmed count == 2 above - let radioValue = components[1] - - // Send to parent - radio += radio.isEmpty ? radioValue : ", \(radioValue)" - } - - radio = radio.isEmpty ? "None": radio + if let radioDict = MixpanelInstance.telephonyInfo.serviceCurrentRadioAccessTechnology { + for (_, value) in radioDict where !value.isEmpty && value.hasPrefix(prefix) { + // the first should be the prefix, second the target + let components = value.components(separatedBy: prefix) + + // Something went wrong and we have more than prefix:target + guard components.count == 2 else { + continue + } + + // Safe to directly access by index since we confirmed count == 2 above + let radioValue = components[1] + + // Send to parent + radio += radio.isEmpty ? radioValue : ", \(radioValue)" } + + radio = radio.isEmpty ? "None" : radio + } } else { - radio = MixpanelInstance.telephonyInfo.currentRadioAccessTechnology ?? "None" - if radio.hasPrefix(prefix) { - radio = (radio as NSString).substring(from: prefix.count) - } + radio = MixpanelInstance.telephonyInfo.currentRadioAccessTechnology ?? "None" + if radio.hasPrefix(prefix) { + radio = (radio as NSString).substring(from: prefix.count) + } } - + trackingQueue.async { - AutomaticProperties.automaticPropertiesLock.write { [weak self, radio] in - AutomaticProperties.properties["$radio"] = radio - - guard self != nil else { - return - } - - AutomaticProperties.properties["$carrier"] = "" - if #available(iOS 12.0, *) { - if let carrierName = MixpanelInstance.telephonyInfo.serviceSubscriberCellularProviders?.first?.value.carrierName { - AutomaticProperties.properties["$carrier"] = carrierName - } - } else { - if let carrierName = MixpanelInstance.telephonyInfo.subscriberCellularProvider?.carrierName { - AutomaticProperties.properties["$carrier"] = carrierName - } - } + AutomaticProperties.automaticPropertiesLock.write { [weak self, radio] in + AutomaticProperties.properties["$radio"] = radio + + guard self != nil else { + return } - } - } -#endif -#endif // os(iOS) - - @objc func handleSuperPropertiesRegistrationNotification(_ notification: Notification) { - guard let data = notification.userInfo else { return } - - if notification.name.rawValue == registerSuperPropertiesNotificationName.rawValue { - guard let properties = data as? Properties else { return } - registerSuperProperties(properties) - } else { - for (key, _) in data { - if let keyToUnregister = key as? String { - unregisterSuperProperty(keyToUnregister) - } + + AutomaticProperties.properties["$carrier"] = "" + if #available(iOS 12.0, *) { + if let carrierName = MixpanelInstance.telephonyInfo + .serviceSubscriberCellularProviders?.first?.value.carrierName + { + AutomaticProperties.properties["$carrier"] = carrierName + } + } else { + if let carrierName = MixpanelInstance.telephonyInfo.subscriberCellularProvider? + .carrierName + { + AutomaticProperties.properties["$carrier"] = carrierName + } } + } } + } + #endif + #endif // os(iOS) + + @objc func handleSuperPropertiesRegistrationNotification(_ notification: Notification) { + guard let data = notification.userInfo else { return } + + if notification.name.rawValue == registerSuperPropertiesNotificationName.rawValue { + guard let properties = data as? Properties else { return } + registerSuperProperties(properties) + } else { + for (key, _) in data { + if let keyToUnregister = key as? String { + unregisterSuperProperty(keyToUnregister) + } + } } + } } extension MixpanelInstance { - // MARK: - Identity - - /** + // MARK: - Identity + + /** Sets the distinct ID of the current user. - + Mixpanel uses a randomly generated persistent UUID as the default local distinct ID. - + If you want to use a unique persistent UUID, you can define the MIXPANEL_UNIQUE_DISTINCT_ID flag in your Active Compilation Conditions build settings. It then uses the IFV String (`UIDevice.current().identifierForVendor`) as the default local distinct ID. This ID will identify a user across all apps by the same vendor, but cannot be used to link the same user across apps from different vendors. If we are unable to get an IFV, we will fall back to generating a random persistent UUID. - + For tracking events, you do not need to call `identify:`. However, **Mixpanel User profiles always requires an explicit call to `identify:`.** If calls are made to `set:`, `increment` or other `People` methods prior to calling `identify:`, then they are queued up and flushed once `identify:` is called. - + If you'd like to use the default distinct ID for Mixpanel People as well (recommended), call `identify:` using the current distinct ID: `mixpanelInstance.identify(mixpanelInstance.distinctId)`. - + - parameter distinctId: string that uniquely identifies the current user - parameter usePeople: boolean that controls whether or not to set the people distinctId to the event distinctId. This should only be set to false if you wish to prevent people profile updates for that user. - parameter completion: an optional completion handler for when the identify has completed. */ - public func identify(distinctId: String, usePeople: Bool = true, completion: (() -> Void)? = nil) { - if hasOptedOutTracking() { - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } - return - } - if distinctId.isEmpty { - MixpanelLogger.error(message: "\(self) cannot identify blank distinct id") - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } - return - } - trackingQueue.async { [weak self, distinctId, usePeople] in - guard let self = self else { return } - - // If there's no anonymousId assigned yet, that means distinctId is stored in the storage. Assigning already stored - // distinctId as anonymousId on identify and also setting a flag to notify that it might be previously logged in user - if self.anonymousId == nil { - self.anonymousId = self.distinctId - self.hadPersistedDistinctId = true - } - - if self.userId == nil { - self.readWriteLock.write { - self.userId = distinctId - } - } - - if distinctId != self.distinctId { - let oldDistinctId = self.distinctId - self.readWriteLock.write { - self.alias = nil - self.distinctId = distinctId - self.userId = distinctId - } - self.flags.loadFlags() - self.track(event: "$identify", properties: ["$anon_distinct_id": oldDistinctId]) - } - - if usePeople { - self.readWriteLock.write { - self.people.distinctId = distinctId - } - self.mixpanelPersistence.identifyPeople(token: self.apiToken) - } else { - self.people.distinctId = nil - } - - MixpanelPersistence.saveIdentity(MixpanelIdentity.init( - distinctID: self.distinctId, - peopleDistinctID: self.people.distinctId, - anonymousId: self.anonymousId, - userId: self.userId, - alias: self.alias, - hadPersistedDistinctId: self.hadPersistedDistinctId), instanceName: self.name) - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } - } - - if MixpanelInstance.isiOSAppExtension() { - flush() - } + public func identify(distinctId: String, usePeople: Bool = true, completion: (() -> Void)? = nil) + { + if hasOptedOutTracking() { + if let completion = completion { + DispatchQueue.main.async(execute: completion) + } + return } - - /** + if distinctId.isEmpty { + MixpanelLogger.error(message: "\(self) cannot identify blank distinct id") + if let completion = completion { + DispatchQueue.main.async(execute: completion) + } + return + } + trackingQueue.async { [weak self, distinctId, usePeople] in + guard let self = self else { return } + + // If there's no anonymousId assigned yet, that means distinctId is stored in the storage. Assigning already stored + // distinctId as anonymousId on identify and also setting a flag to notify that it might be previously logged in user + if self.anonymousId == nil { + self.anonymousId = self.distinctId + self.hadPersistedDistinctId = true + } + + if self.userId == nil { + self.readWriteLock.write { + self.userId = distinctId + } + } + + if distinctId != self.distinctId { + let oldDistinctId = self.distinctId + self.readWriteLock.write { + self.alias = nil + self.distinctId = distinctId + self.userId = distinctId + } + self.flags.loadFlags() + self.track(event: "$identify", properties: ["$anon_distinct_id": oldDistinctId]) + } + + if usePeople { + self.readWriteLock.write { + self.people.distinctId = distinctId + } + self.mixpanelPersistence.identifyPeople(token: self.apiToken) + } else { + self.people.distinctId = nil + } + + MixpanelPersistence.saveIdentity( + MixpanelIdentity.init( + distinctID: self.distinctId, + peopleDistinctID: self.people.distinctId, + anonymousId: self.anonymousId, + userId: self.userId, + alias: self.alias, + hadPersistedDistinctId: self.hadPersistedDistinctId), instanceName: self.name) + if let completion = completion { + DispatchQueue.main.async(execute: completion) + } + } + + if MixpanelInstance.isiOSAppExtension() { + flush() + } + } + + /** The alias method creates an alias which Mixpanel will use to remap one id to another. Multiple aliases can point to the same identifier. - + Please note: With Mixpanel Identity Merge enabled, calling alias is no longer required but can be used to merge two IDs in scenarios where identify() would fail - - + + `mixpanelInstance.createAlias("New ID", distinctId: mixpanelInstance.distinctId)` - + You can add multiple id aliases to the existing id - + `mixpanelInstance.createAlias("Newer ID", distinctId: mixpanelInstance.distinctId)` - - + + - parameter alias: A unique identifier that you want to use as an identifier for this user. - parameter distinctId: The current user identifier. - parameter usePeople: boolean that controls whether or not to set the people distinctId to the event distinctId. @@ -842,428 +871,441 @@ extension MixpanelInstance { - parameter completion: an optional completion handler for when the createAlias has completed. This should only be set to false if you wish to prevent people profile updates for that user. */ - public func createAlias(_ alias: String, distinctId: String, usePeople: Bool = true, andIdentify: Bool = true, completion: (() -> Void)? = nil) { - if hasOptedOutTracking() { - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } - return + public func createAlias( + _ alias: String, distinctId: String, usePeople: Bool = true, andIdentify: Bool = true, + completion: (() -> Void)? = nil + ) { + if hasOptedOutTracking() { + if let completion = completion { + DispatchQueue.main.async(execute: completion) + } + return + } + + if distinctId.isEmpty { + MixpanelLogger.error(message: "\(self) cannot identify blank distinct id") + if let completion = completion { + DispatchQueue.main.async(execute: completion) + } + return + } + + if alias.isEmpty { + MixpanelLogger.error(message: "\(self) create alias called with empty alias") + if let completion = completion { + DispatchQueue.main.async(execute: completion) + } + return + } + + if alias != distinctId { + trackingQueue.async { [weak self, alias] in + guard let self = self else { + if let completion = completion { + DispatchQueue.main.async(execute: completion) + } + return } - - if distinctId.isEmpty { - MixpanelLogger.error(message: "\(self) cannot identify blank distinct id") - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } - return - } - - if alias.isEmpty { - MixpanelLogger.error(message: "\(self) create alias called with empty alias") - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } - return + self.readWriteLock.write { + self.alias = alias } - if alias != distinctId { - trackingQueue.async { [weak self, alias] in - guard let self = self else { - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } - return - } - self.readWriteLock.write { - self.alias = alias - } - - var distinctIdSnapshot: String? - var peopleDistinctIDSnapshot: String? - var anonymousIdSnapshot: String? - var userIdSnapshot: String? - var aliasSnapshot: String? - var hadPersistedDistinctIdSnapshot: Bool? - - self.readWriteLock.read { - distinctIdSnapshot = self.distinctId - peopleDistinctIDSnapshot = self.people.distinctId - anonymousIdSnapshot = self.anonymousId - userIdSnapshot = self.userId - aliasSnapshot = self.alias - hadPersistedDistinctIdSnapshot = self.hadPersistedDistinctId - } - - MixpanelPersistence.saveIdentity(MixpanelIdentity.init( - distinctID: distinctIdSnapshot!, - peopleDistinctID: peopleDistinctIDSnapshot, - anonymousId: anonymousIdSnapshot, - userId: userIdSnapshot, - alias: aliasSnapshot, - hadPersistedDistinctId: hadPersistedDistinctIdSnapshot), instanceName: self.name) - } - - let properties = ["distinct_id": distinctId, "alias": alias] - track(event: "$create_alias", properties: properties) - if andIdentify { - identify(distinctId: distinctId, usePeople: usePeople) - } - flush(completion: completion) - } else { - MixpanelLogger.error(message: "alias: \(alias) matches distinctId: \(distinctId) - skipping api call.") - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } + var distinctIdSnapshot: String? + var peopleDistinctIDSnapshot: String? + var anonymousIdSnapshot: String? + var userIdSnapshot: String? + var aliasSnapshot: String? + var hadPersistedDistinctIdSnapshot: Bool? + + self.readWriteLock.read { + distinctIdSnapshot = self.distinctId + peopleDistinctIDSnapshot = self.people.distinctId + anonymousIdSnapshot = self.anonymousId + userIdSnapshot = self.userId + aliasSnapshot = self.alias + hadPersistedDistinctIdSnapshot = self.hadPersistedDistinctId } + + MixpanelPersistence.saveIdentity( + MixpanelIdentity.init( + distinctID: distinctIdSnapshot!, + peopleDistinctID: peopleDistinctIDSnapshot, + anonymousId: anonymousIdSnapshot, + userId: userIdSnapshot, + alias: aliasSnapshot, + hadPersistedDistinctId: hadPersistedDistinctIdSnapshot), instanceName: self.name) + } + + let properties = ["distinct_id": distinctId, "alias": alias] + track(event: "$create_alias", properties: properties) + if andIdentify { + identify(distinctId: distinctId, usePeople: usePeople) + } + flush(completion: completion) + } else { + MixpanelLogger.error( + message: "alias: \(alias) matches distinctId: \(distinctId) - skipping api call.") + if let completion = completion { + DispatchQueue.main.async(execute: completion) + } } - - /** + } + + /** Clears all stored properties including the distinct Id. Useful if your app's user logs out. - + - parameter completion: an optional completion handler for when the reset has completed. */ - public func reset(completion: (() -> Void)? = nil) { - flush() - trackingQueue.async { [weak self] in - guard let self = self else { - return - } - - MixpanelPersistence.deleteMPUserDefaultsData(instanceName: self.name) - self.readWriteLock.write { - self.timedEvents = InternalProperties() - self.anonymousId = self.defaultDeviceId() - self.distinctId = self.addPrefixToDeviceId(deviceId: self.anonymousId) - self.hadPersistedDistinctId = true - self.userId = nil - self.superProperties = InternalProperties() - self.people.distinctId = nil - self.alias = nil - } - - self.mixpanelPersistence.resetEntities() - self.archive() - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } - } + public func reset(completion: (() -> Void)? = nil) { + flush() + trackingQueue.async { [weak self] in + guard let self = self else { + return + } + + MixpanelPersistence.deleteMPUserDefaultsData(instanceName: self.name) + self.readWriteLock.write { + self.timedEvents = InternalProperties() + self.anonymousId = self.defaultDeviceId() + self.distinctId = self.addPrefixToDeviceId(deviceId: self.anonymousId) + self.hadPersistedDistinctId = true + self.userId = nil + self.superProperties = InternalProperties() + self.people.distinctId = nil + self.alias = nil + } + + self.mixpanelPersistence.resetEntities() + self.archive() + if let completion = completion { + DispatchQueue.main.async(execute: completion) + } } + } } extension MixpanelInstance { - // MARK: - Persistence - - public func archive() { - self.readWriteLock.read { - MixpanelPersistence.saveTimedEvents(timedEvents: timedEvents, instanceName: self.name) - MixpanelPersistence.saveSuperProperties(superProperties: superProperties, instanceName: self.name) - MixpanelPersistence.saveIdentity(MixpanelIdentity.init( - distinctID: distinctId, - peopleDistinctID: people.distinctId, - anonymousId: anonymousId, - userId: userId, - alias: alias, - hadPersistedDistinctId: hadPersistedDistinctId), instanceName: self.name) - } - } - - func unarchive() { - let didCreateIdentity = self.readWriteLock.write { - optOutStatus = MixpanelPersistence.loadOptOutStatusFlag(instanceName: self.name) - superProperties = MixpanelPersistence.loadSuperProperties(instanceName: self.name) - timedEvents = MixpanelPersistence.loadTimedEvents(instanceName: self.name) - let mixpanelIdentity = MixpanelPersistence.loadIdentity(instanceName: self.name) - (distinctId, people.distinctId, anonymousId, userId, alias, hadPersistedDistinctId) = ( - mixpanelIdentity.distinctID, - mixpanelIdentity.peopleDistinctID, - mixpanelIdentity.anonymousId, - mixpanelIdentity.userId, - mixpanelIdentity.alias, - mixpanelIdentity.hadPersistedDistinctId - ) - if distinctId.isEmpty { - anonymousId = defaultDeviceId() - distinctId = addPrefixToDeviceId(deviceId: anonymousId) - hadPersistedDistinctId = true - userId = nil - return true - } else { - return false - } - } + // MARK: - Persistence - if didCreateIdentity { - self.readWriteLock.read { - MixpanelPersistence.saveIdentity(MixpanelIdentity.init( - distinctID: distinctId, - peopleDistinctID: people.distinctId, - anonymousId: anonymousId, - userId: userId, - alias: alias, - hadPersistedDistinctId: hadPersistedDistinctId), instanceName: self.name) - } - } + public func archive() { + self.readWriteLock.read { + MixpanelPersistence.saveTimedEvents(timedEvents: timedEvents, instanceName: self.name) + MixpanelPersistence.saveSuperProperties( + superProperties: superProperties, instanceName: self.name) + MixpanelPersistence.saveIdentity( + MixpanelIdentity.init( + distinctID: distinctId, + peopleDistinctID: people.distinctId, + anonymousId: anonymousId, + userId: userId, + alias: alias, + hadPersistedDistinctId: hadPersistedDistinctId), instanceName: self.name) } + } + + func unarchive() { + let didCreateIdentity = self.readWriteLock.write { + optOutStatus = MixpanelPersistence.loadOptOutStatusFlag(instanceName: self.name) + superProperties = MixpanelPersistence.loadSuperProperties(instanceName: self.name) + timedEvents = MixpanelPersistence.loadTimedEvents(instanceName: self.name) + let mixpanelIdentity = MixpanelPersistence.loadIdentity(instanceName: self.name) + (distinctId, people.distinctId, anonymousId, userId, alias, hadPersistedDistinctId) = ( + mixpanelIdentity.distinctID, + mixpanelIdentity.peopleDistinctID, + mixpanelIdentity.anonymousId, + mixpanelIdentity.userId, + mixpanelIdentity.alias, + mixpanelIdentity.hadPersistedDistinctId + ) + if distinctId.isEmpty { + anonymousId = defaultDeviceId() + distinctId = addPrefixToDeviceId(deviceId: anonymousId) + hadPersistedDistinctId = true + userId = nil + return true + } else { + return false + } + } + + if didCreateIdentity { + self.readWriteLock.read { + MixpanelPersistence.saveIdentity( + MixpanelIdentity.init( + distinctID: distinctId, + peopleDistinctID: people.distinctId, + anonymousId: anonymousId, + userId: userId, + alias: alias, + hadPersistedDistinctId: hadPersistedDistinctId), instanceName: self.name) + } + } + } } extension MixpanelInstance { - // MARK: - Flush - - /** + // MARK: - Flush + + /** Uploads queued data to the Mixpanel server. - + By default, queued data is flushed to the Mixpanel servers every minute (the default for `flushInterval`), and on background (since `flushOnBackground` is on by default). You only need to call this method manually if you want to force a flush at a particular moment. - + - parameter performFullFlush: A optional boolean value indicating whether a full flush should be performed. If `true`, a full flush will be triggered, sending all events to the server. Default to `false`, a partial flush will be executed for reducing memory footprint. - parameter completion: an optional completion handler for when the flush has completed. */ - public func flush(performFullFlush: Bool = false, completion: (() -> Void)? = nil) { - if hasOptedOutTracking() { - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } - return - } - trackingQueue.async { [weak self, completion] in - guard let self = self else { - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } - return - } - - if let shouldFlush = self.delegate?.mixpanelWillFlush(self), !shouldFlush { - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } - return - } - - // automatic events will NOT be flushed until one of the flags is non-nil - let eventQueue = self.mixpanelPersistence.loadEntitiesInBatch( - type: self.persistenceTypeFromFlushType(.events), - batchSize: performFullFlush ? Int.max : self.flushBatchSize, - excludeAutomaticEvents: !self.trackAutomaticEventsEnabled - ) - let peopleQueue = self.mixpanelPersistence.loadEntitiesInBatch( - type: self.persistenceTypeFromFlushType(.people), - batchSize: performFullFlush ? Int.max : self.flushBatchSize - ) - let groupsQueue = self.mixpanelPersistence.loadEntitiesInBatch( - type: self.persistenceTypeFromFlushType(.groups), - batchSize: performFullFlush ? Int.max : self.flushBatchSize - ) - - self.networkQueue.async { [weak self, completion] in - guard let self = self else { - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } - return - } - self.flushQueue(eventQueue, type: .events) - self.flushQueue(peopleQueue, type: .people) - self.flushQueue(groupsQueue, type: .groups) - - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } - } - } + public func flush(performFullFlush: Bool = false, completion: (() -> Void)? = nil) { + if hasOptedOutTracking() { + if let completion = completion { + DispatchQueue.main.async(execute: completion) + } + return } - - private func persistenceTypeFromFlushType(_ type: FlushType) -> PersistenceType { - switch type { - case .events: - return PersistenceType.events - case .people: - return PersistenceType.people - case .groups: - return PersistenceType.groups + trackingQueue.async { [weak self, completion] in + guard let self = self else { + if let completion = completion { + DispatchQueue.main.async(execute: completion) } - } - - func flushQueue(_ queue: Queue, type: FlushType) { - if hasOptedOutTracking() { - return + return + } + + if let shouldFlush = self.delegate?.mixpanelWillFlush(self), !shouldFlush { + if let completion = completion { + DispatchQueue.main.async(execute: completion) } - let proxyServerResource = proxyServerDelegate?.mixpanelResourceForProxyServer(name) - let headers: [String: String] = proxyServerResource?.headers ?? [:] - let queryItems = proxyServerResource?.queryItems ?? [] - - self.flushInstance.flushQueue(queue, type: type, headers: headers, queryItems: queryItems) - } - - func flushSuccess(type: FlushType, ids: [Int32]) { - trackingQueue.async { [weak self] in - guard let self = self else { - return - } - self.mixpanelPersistence.removeEntitiesInBatch(type: self.persistenceTypeFromFlushType(type), ids: ids) + return + } + + // automatic events will NOT be flushed until one of the flags is non-nil + let eventQueue = self.mixpanelPersistence.loadEntitiesInBatch( + type: self.persistenceTypeFromFlushType(.events), + batchSize: performFullFlush ? Int.max : self.flushBatchSize, + excludeAutomaticEvents: !self.trackAutomaticEventsEnabled + ) + let peopleQueue = self.mixpanelPersistence.loadEntitiesInBatch( + type: self.persistenceTypeFromFlushType(.people), + batchSize: performFullFlush ? Int.max : self.flushBatchSize + ) + let groupsQueue = self.mixpanelPersistence.loadEntitiesInBatch( + type: self.persistenceTypeFromFlushType(.groups), + batchSize: performFullFlush ? Int.max : self.flushBatchSize + ) + + self.networkQueue.async { [weak self, completion] in + guard let self = self else { + if let completion = completion { + DispatchQueue.main.async(execute: completion) + } + return } + self.flushQueue(eventQueue, type: .events) + self.flushQueue(peopleQueue, type: .people) + self.flushQueue(groupsQueue, type: .groups) + + if let completion = completion { + DispatchQueue.main.async(execute: completion) + } + } } - + } + + private func persistenceTypeFromFlushType(_ type: FlushType) -> PersistenceType { + switch type { + case .events: + return PersistenceType.events + case .people: + return PersistenceType.people + case .groups: + return PersistenceType.groups + } + } + + func flushQueue(_ queue: Queue, type: FlushType) { + if hasOptedOutTracking() { + return + } + let proxyServerResource = proxyServerDelegate?.mixpanelResourceForProxyServer(name) + let headers: [String: String] = proxyServerResource?.headers ?? [:] + let queryItems = proxyServerResource?.queryItems ?? [] + + self.flushInstance.flushQueue(queue, type: type, headers: headers, queryItems: queryItems) + } + + func flushSuccess(type: FlushType, ids: [Int32]) { + trackingQueue.async { [weak self] in + guard let self = self else { + return + } + self.mixpanelPersistence.removeEntitiesInBatch( + type: self.persistenceTypeFromFlushType(type), ids: ids) + } + } + } extension MixpanelInstance { - // MARK: - Track - - /** + // MARK: - Track + + /** 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 properties: properties dictionary */ - public func track(event: String?, properties: Properties? = nil) { - let epochInterval = Date().timeIntervalSince1970 - - trackingQueue.async { [weak self, event, properties, epochInterval] in - guard let self else { - return - } - if self.hasOptedOutTracking() { - return - } - var shadowTimedEvents = InternalProperties() - var shadowSuperProperties = InternalProperties() - - self.readWriteLock.read { - shadowTimedEvents = self.timedEvents - shadowSuperProperties = self.superProperties - } - - let mixpanelIdentity = MixpanelIdentity.init(distinctID: self.distinctId, - peopleDistinctID: nil, - anonymousId: self.anonymousId, - userId: self.userId, - alias: nil, - hadPersistedDistinctId: self.hadPersistedDistinctId) - let timedEventsSnapshot = self.trackInstance.track(event: event, - properties: properties, - timedEvents: shadowTimedEvents, - superProperties: shadowSuperProperties, - mixpanelIdentity: mixpanelIdentity, - epochInterval: epochInterval) - - self.readWriteLock.write { - self.timedEvents = timedEventsSnapshot - } - } - - if MixpanelInstance.isiOSAppExtension() { - flush() - } + public func track(event: String?, properties: Properties? = nil) { + let epochInterval = Date().timeIntervalSince1970 + + trackingQueue.async { [weak self, event, properties, epochInterval] in + guard let self else { + return + } + if self.hasOptedOutTracking() { + return + } + var shadowTimedEvents = InternalProperties() + var shadowSuperProperties = InternalProperties() + + self.readWriteLock.read { + shadowTimedEvents = self.timedEvents + shadowSuperProperties = self.superProperties + } + + let mixpanelIdentity = MixpanelIdentity.init( + distinctID: self.distinctId, + peopleDistinctID: nil, + anonymousId: self.anonymousId, + userId: self.userId, + alias: nil, + hadPersistedDistinctId: self.hadPersistedDistinctId) + let timedEventsSnapshot = self.trackInstance.track( + event: event, + properties: properties, + timedEvents: shadowTimedEvents, + superProperties: shadowSuperProperties, + mixpanelIdentity: mixpanelIdentity, + epochInterval: epochInterval) + + self.readWriteLock.write { + self.timedEvents = timedEventsSnapshot + } } - - /** + + if MixpanelInstance.isiOSAppExtension() { + flush() + } + } + + /** Tracks an event with properties and to specific groups. Properties and groups are optional and can be added only if needed. - + Properties will allow you to segment your events in your Mixpanel reports. Property and group 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 properties: properties dictionary - parameter groups: groups dictionary */ - public func trackWithGroups(event: String?, properties: Properties? = nil, groups: Properties?) { - if hasOptedOutTracking() { - return - } - - guard let properties = properties else { - self.track(event: event, properties: groups) - return - } - - guard let groups = groups else { - self.track(event: event, properties: properties) - return - } - - var mergedProperties = properties - for (groupKey, groupID) in groups { - mergedProperties[groupKey] = groupID - } - self.track(event: event, properties: mergedProperties) + public func trackWithGroups(event: String?, properties: Properties? = nil, groups: Properties?) { + if hasOptedOutTracking() { + return } - - public func getGroup(groupKey: String, groupID: MixpanelType) -> Group { - let key = makeMapKey(groupKey: groupKey, groupID: groupID) - - var groupsShadow: [String: Group] = [:] - - readWriteLock.read { - groupsShadow = groups - } - - guard let group = groupsShadow[key] else { - readWriteLock.write { - groups[key] = Group(apiToken: apiToken, - serialQueue: trackingQueue, - lock: self.readWriteLock, - groupKey: groupKey, - groupID: groupID, - metadata: sessionMetadata, - mixpanelPersistence: mixpanelPersistence, - mixpanelInstance: self) - groupsShadow = groups - } - return groupsShadow[key]! - } - - if !(group.groupKey == groupKey && group.groupID.equals(rhs: groupID)) { - // we somehow hit a collision on the map key, return a new group with the correct key and ID - MixpanelLogger.info(message: "groups dictionary key collision: \(key)") - let newGroup = Group(apiToken: apiToken, - serialQueue: trackingQueue, - lock: self.readWriteLock, - groupKey: groupKey, - groupID: groupID, - metadata: sessionMetadata, - mixpanelPersistence: mixpanelPersistence, - mixpanelInstance: self) - readWriteLock.write { - groups[key] = newGroup - } - return newGroup - } - - return group + + guard let properties = properties else { + self.track(event: event, properties: groups) + return } - - func removeCachedGroup(groupKey: String, groupID: MixpanelType) { - readWriteLock.write { - groups.removeValue(forKey: makeMapKey(groupKey: groupKey, groupID: groupID)) - } + + guard let groups = groups else { + self.track(event: event, properties: properties) + return } - - func makeMapKey(groupKey: String, groupID: MixpanelType) -> String { - return "\(groupKey)_\(groupID)" + + var mergedProperties = properties + for (groupKey, groupID) in groups { + mergedProperties[groupKey] = groupID } - - /** + self.track(event: event, properties: mergedProperties) + } + + public func getGroup(groupKey: String, groupID: MixpanelType) -> Group { + let key = makeMapKey(groupKey: groupKey, groupID: groupID) + + var groupsShadow: [String: Group] = [:] + + readWriteLock.read { + groupsShadow = groups + } + + guard let group = groupsShadow[key] else { + readWriteLock.write { + groups[key] = Group( + apiToken: apiToken, + serialQueue: trackingQueue, + lock: self.readWriteLock, + groupKey: groupKey, + groupID: groupID, + metadata: sessionMetadata, + mixpanelPersistence: mixpanelPersistence, + mixpanelInstance: self) + groupsShadow = groups + } + return groupsShadow[key]! + } + + if !(group.groupKey == groupKey && group.groupID.equals(rhs: groupID)) { + // we somehow hit a collision on the map key, return a new group with the correct key and ID + MixpanelLogger.info(message: "groups dictionary key collision: \(key)") + let newGroup = Group( + apiToken: apiToken, + serialQueue: trackingQueue, + lock: self.readWriteLock, + groupKey: groupKey, + groupID: groupID, + metadata: sessionMetadata, + mixpanelPersistence: mixpanelPersistence, + mixpanelInstance: self) + readWriteLock.write { + groups[key] = newGroup + } + return newGroup + } + + return group + } + + func removeCachedGroup(groupKey: String, groupID: MixpanelType) { + readWriteLock.write { + groups.removeValue(forKey: makeMapKey(groupKey: groupKey, groupID: groupID)) + } + } + + func makeMapKey(groupKey: String, groupID: MixpanelType) -> String { + return "\(groupKey)_\(groupID)" + } + + /** Starts a timer that will be stopped and added as a property when a corresponding event 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(event:"Image Upload") @@ -1272,148 +1314,154 @@ extension MixpanelInstance { // track the event mixpanelInstance.track("Image Upload") } - + - parameter event: the event name to be timed - + */ - public func time(event: String) { - let startTime = Date().timeIntervalSince1970 - trackingQueue.async { [weak self, startTime, event] in - guard let self = self else { return } - let timedEvents = self.trackInstance.time(event: event, timedEvents: self.timedEvents, startTime: startTime) - self.readWriteLock.write { - self.timedEvents = timedEvents - } - MixpanelPersistence.saveTimedEvents(timedEvents: timedEvents, instanceName: self.name) - } + public func time(event: String) { + let startTime = Date().timeIntervalSince1970 + trackingQueue.async { [weak self, startTime, event] in + guard let self = self else { return } + let timedEvents = self.trackInstance.time( + event: event, timedEvents: self.timedEvents, startTime: startTime) + self.readWriteLock.write { + self.timedEvents = timedEvents + } + MixpanelPersistence.saveTimedEvents(timedEvents: timedEvents, instanceName: self.name) } - - /** + } + + /** Retrieves the time elapsed for the named event since time(event:) was called. - + - 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() - self.readWriteLock.read { - timedEvents = self.timedEvents - } - - if let startTime = timedEvents[event] as? TimeInterval { - return Date().timeIntervalSince1970 - startTime - } - return 0 + public func eventElapsedTime(event: String) -> Double { + var timedEvents = InternalProperties() + self.readWriteLock.read { + timedEvents = self.timedEvents } - - /** + + if let startTime = timedEvents[event] as? TimeInterval { + return Date().timeIntervalSince1970 - startTime + } + return 0 + } + + /** Clears all current event timers. */ - public func clearTimedEvents() { - trackingQueue.async { [weak self] in - guard let self = self else { return } - self.readWriteLock.write { - self.timedEvents = InternalProperties() - } - MixpanelPersistence.saveTimedEvents(timedEvents: InternalProperties(), instanceName: self.name) - } + public func clearTimedEvents() { + trackingQueue.async { [weak self] in + guard let self = self else { return } + self.readWriteLock.write { + self.timedEvents = InternalProperties() + } + MixpanelPersistence.saveTimedEvents( + timedEvents: InternalProperties(), instanceName: self.name) } - - /** + } + + /** Clears the event timer for the named event. - + - parameter event: the name of the event to clear the timer for */ - public func clearTimedEvent(event: String) { - trackingQueue.async { [weak self, event] in - guard let self = self else { return } - - let updatedTimedEvents = self.trackInstance.clearTimedEvent(event: event, timedEvents: self.timedEvents) - MixpanelPersistence.saveTimedEvents(timedEvents: updatedTimedEvents, instanceName: self.name) - } + public func clearTimedEvent(event: String) { + trackingQueue.async { [weak self, event] in + guard let self = self else { return } + + let updatedTimedEvents = self.trackInstance.clearTimedEvent( + event: event, timedEvents: self.timedEvents) + MixpanelPersistence.saveTimedEvents(timedEvents: updatedTimedEvents, instanceName: self.name) } - - /** + } + + /** Returns the currently set super properties. - + - returns: the current super properties */ - public func currentSuperProperties() -> [String: Any] { - var properties = InternalProperties() - self.readWriteLock.read { - properties = superProperties - } - return properties + public func currentSuperProperties() -> [String: Any] { + var properties = InternalProperties() + self.readWriteLock.read { + properties = superProperties } - - /** + return properties + } + + /** Clears all currently set super properties. */ - public func clearSuperProperties() { - trackingQueue.async { [weak self] in - guard let self = self else { return } - self.superProperties = self.trackInstance.clearSuperProperties(self.superProperties) - MixpanelPersistence.saveSuperProperties(superProperties: self.superProperties, instanceName: self.name) - } + public func clearSuperProperties() { + trackingQueue.async { [weak self] in + guard let self = self else { return } + self.superProperties = self.trackInstance.clearSuperProperties(self.superProperties) + MixpanelPersistence.saveSuperProperties( + superProperties: self.superProperties, instanceName: self.name) } - - /** + } + + /** Registers super properties, overwriting ones that have already been set. - + Super properties, once registered, are automatically sent as properties for all event tracking calls. They save you having to maintain and add a common set of properties to your events. 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. - + - parameter properties: properties dictionary */ - public func registerSuperProperties(_ properties: Properties) { - trackingQueue.async { [weak self] in - guard let self = self else { return } - let updatedSuperProperties = self.trackInstance.registerSuperProperties(properties, - superProperties: self.superProperties) - self.readWriteLock.write { - self.superProperties = updatedSuperProperties - } - self.readWriteLock.read { - MixpanelPersistence.saveSuperProperties(superProperties: self.superProperties, instanceName: self.name) - } - } + public func registerSuperProperties(_ properties: Properties) { + trackingQueue.async { [weak self] in + guard let self = self else { return } + let updatedSuperProperties = self.trackInstance.registerSuperProperties( + properties, + superProperties: self.superProperties) + self.readWriteLock.write { + self.superProperties = updatedSuperProperties + } + self.readWriteLock.read { + MixpanelPersistence.saveSuperProperties( + superProperties: self.superProperties, instanceName: self.name) + } } - - - - - - /** + } + + /** Registers super properties without overwriting ones that have already been set, unless the existing value is equal to defaultValue. defaultValue is optional. - + 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. - + - parameter properties: properties dictionary - parameter defaultValue: Optional. overwrite existing properties that have this value */ - public func registerSuperPropertiesOnce(_ properties: Properties, - defaultValue: MixpanelType? = nil) { - trackingQueue.async { [weak self] in - guard let self = self else { return } - let updatedSuperProperties = self.trackInstance.registerSuperPropertiesOnce(properties, - superProperties: self.superProperties, - defaultValue: defaultValue) - self.readWriteLock.write { - self.superProperties = updatedSuperProperties - } - self.readWriteLock.read { - MixpanelPersistence.saveSuperProperties(superProperties: self.superProperties, instanceName: self.name) - } - } + public func registerSuperPropertiesOnce( + _ properties: Properties, + defaultValue: MixpanelType? = nil + ) { + trackingQueue.async { [weak self] in + guard let self = self else { return } + let updatedSuperProperties = self.trackInstance.registerSuperPropertiesOnce( + properties, + superProperties: self.superProperties, + defaultValue: defaultValue) + self.readWriteLock.write { + self.superProperties = updatedSuperProperties + } + self.readWriteLock.read { + MixpanelPersistence.saveSuperProperties( + superProperties: self.superProperties, instanceName: self.name) + } } - - /** + } + + /** Removes a previously registered super property. - + As an alternative to clearing all properties, unregistering specific super properties prevents them from being recorded on future events. This operation does not affect the value of other super properties. Any property name that is @@ -1421,229 +1469,234 @@ extension MixpanelInstance { Note that after removing a super property, events will show the attribute as having the value `undefined` in Mixpanel until a new value is registered. - + - parameter propertyName: array of property name strings to remove */ - public func unregisterSuperProperty(_ propertyName: String) { - trackingQueue.async { [weak self] in - guard let self = self else { return } - let updatedSuperProperties = self.trackInstance.unregisterSuperProperty(propertyName, - superProperties: self.superProperties) - self.readWriteLock.write { - self.superProperties = updatedSuperProperties - } - self.readWriteLock.read { - MixpanelPersistence.saveSuperProperties(superProperties: self.superProperties, instanceName: self.name) - } - } + public func unregisterSuperProperty(_ propertyName: String) { + trackingQueue.async { [weak self] in + guard let self = self else { return } + let updatedSuperProperties = self.trackInstance.unregisterSuperProperty( + propertyName, + superProperties: self.superProperties) + self.readWriteLock.write { + self.superProperties = updatedSuperProperties + } + self.readWriteLock.read { + MixpanelPersistence.saveSuperProperties( + superProperties: self.superProperties, instanceName: self.name) + } } - - /** + } + + /** Updates a super property atomically. The update function - + - parameter update: closure to apply to super properties */ - func updateSuperProperty(_ update: @escaping (_ superproperties: inout InternalProperties) -> Void) { - trackingQueue.async { [weak self] in - guard let self = self else { return } - var superPropertiesShadow = self.superProperties - self.trackInstance.updateSuperProperty(update, - superProperties: &superPropertiesShadow) - self.readWriteLock.write { - self.superProperties = superPropertiesShadow - } - self.readWriteLock.read { - MixpanelPersistence.saveSuperProperties(superProperties: self.superProperties, instanceName: self.name) - } - } + func updateSuperProperty( + _ update: @escaping (_ superproperties: inout InternalProperties) -> Void + ) { + trackingQueue.async { [weak self] in + guard let self = self else { return } + var superPropertiesShadow = self.superProperties + self.trackInstance.updateSuperProperty( + update, + superProperties: &superPropertiesShadow) + self.readWriteLock.write { + self.superProperties = superPropertiesShadow + } + self.readWriteLock.read { + MixpanelPersistence.saveSuperProperties( + superProperties: self.superProperties, instanceName: self.name) + } } - - /** + } + + /** Convenience method to set a single group the user belongs to. - + - parameter groupKey: The property name associated with this group type (must already have been set up). - parameter groupID: The group the user belongs to. */ - public func setGroup(groupKey: String, groupID: MixpanelType) { - if hasOptedOutTracking() { - return - } - - setGroup(groupKey: groupKey, groupIDs: [groupID]) + public func setGroup(groupKey: String, groupID: MixpanelType) { + if hasOptedOutTracking() { + return } - - /** + + setGroup(groupKey: groupKey, groupIDs: [groupID]) + } + + /** Set the groups this user belongs to. - + - parameter groupKey: The property name associated with this group type (must already have been set up). - parameter groupIDs: The list of groups the user belongs to. */ - public func setGroup(groupKey: String, groupIDs: [MixpanelType]) { - if hasOptedOutTracking() { - return - } - - let properties = [groupKey: groupIDs] - self.registerSuperProperties(properties) - people.set(properties: properties) + public func setGroup(groupKey: String, groupIDs: [MixpanelType]) { + if hasOptedOutTracking() { + return } - - /** + + let properties = [groupKey: groupIDs] + self.registerSuperProperties(properties) + people.set(properties: properties) + } + + /** Add a group to this user's membership for a particular group key - + - parameter groupKey: The property name associated with this group type (must already have been set up). - parameter groupID: The new group the user belongs to. */ - public func addGroup(groupKey: String, groupID: MixpanelType) { - if hasOptedOutTracking() { - return - } - - updateSuperProperty { superProperties in - guard let oldValue = superProperties[groupKey] else { - superProperties[groupKey] = [groupID] - self.people.set(properties: [groupKey: [groupID]]) - return - } - - if let oldValue = oldValue as? [MixpanelType] { - var vals = oldValue - if !vals.contains(where: { $0.equals(rhs: groupID) }) { - vals.append(groupID) - superProperties[groupKey] = vals - } - } else { - superProperties[groupKey] = [oldValue, groupID] - } - - // This is a best effort--if the people property is not already a list, this call does nothing. - self.people.union(properties: [groupKey: [groupID]]) - } + public func addGroup(groupKey: String, groupID: MixpanelType) { + if hasOptedOutTracking() { + return } - - /** + + updateSuperProperty { superProperties in + guard let oldValue = superProperties[groupKey] else { + superProperties[groupKey] = [groupID] + self.people.set(properties: [groupKey: [groupID]]) + return + } + + if let oldValue = oldValue as? [MixpanelType] { + var vals = oldValue + if !vals.contains(where: { $0.equals(rhs: groupID) }) { + vals.append(groupID) + superProperties[groupKey] = vals + } + } else { + superProperties[groupKey] = [oldValue, groupID] + } + + // This is a best effort--if the people property is not already a list, this call does nothing. + self.people.union(properties: [groupKey: [groupID]]) + } + } + + /** Remove a group from this user's membership for a particular group key - + - parameter groupKey: The property name associated with this group type (must already have been set up). - parameter groupID: The group value to remove. */ - public func removeGroup(groupKey: String, groupID: MixpanelType) { - if hasOptedOutTracking() { - return - } - - updateSuperProperty { (superProperties) -> Void in - guard let oldValue = superProperties[groupKey] else { - return - } - - guard let vals = oldValue as? [MixpanelType] else { - superProperties.removeValue(forKey: groupKey) - self.people.unset(properties: [groupKey]) - return - } - - if vals.count < 2 { - superProperties.removeValue(forKey: groupKey) - self.people.unset(properties: [groupKey]) - return - } - - superProperties[groupKey] = vals.filter {!$0.equals(rhs: groupID)} - self.people.remove(properties: [groupKey: groupID]) - } + public func removeGroup(groupKey: String, groupID: MixpanelType) { + if hasOptedOutTracking() { + return } - - /** + + updateSuperProperty { (superProperties) -> Void in + guard let oldValue = superProperties[groupKey] else { + return + } + + guard let vals = oldValue as? [MixpanelType] else { + superProperties.removeValue(forKey: groupKey) + self.people.unset(properties: [groupKey]) + return + } + + if vals.count < 2 { + superProperties.removeValue(forKey: groupKey) + self.people.unset(properties: [groupKey]) + return + } + + superProperties[groupKey] = vals.filter { !$0.equals(rhs: groupID) } + self.people.remove(properties: [groupKey: groupID]) + } + } + + /** Opt out tracking. - + This method is used to opt out tracking. This causes all events and people request no longer to be sent back to the Mixpanel server. */ - public func optOutTracking() { - trackingQueue.async { [weak self] in - guard let self = self else { return } - if self.people.distinctId != nil { - self.people.deleteUser() - self.people.clearCharges() - self.flush() - } - self.readWriteLock.write { [weak self] in - guard let self = self else { - return - } - - self.alias = nil - self.people.distinctId = nil - self.userId = nil - self.anonymousId = self.defaultDeviceId() - self.distinctId = self.addPrefixToDeviceId(deviceId: self.anonymousId) - self.hadPersistedDistinctId = true - self.superProperties = InternalProperties() - MixpanelPersistence.saveTimedEvents(timedEvents: InternalProperties(), instanceName: self.name) - } - self.archive() - self.readWriteLock.write { - self.optOutStatus = true - } - self.readWriteLock.read { - MixpanelPersistence.saveOptOutStatusFlag(value: self.optOutStatus!, instanceName: self.name) - } - + public func optOutTracking() { + trackingQueue.async { [weak self] in + guard let self = self else { return } + if self.people.distinctId != nil { + self.people.deleteUser() + self.people.clearCharges() + self.flush() + } + self.readWriteLock.write { [weak self] in + guard let self = self else { + return } + + self.alias = nil + self.people.distinctId = nil + self.userId = nil + self.anonymousId = self.defaultDeviceId() + self.distinctId = self.addPrefixToDeviceId(deviceId: self.anonymousId) + self.hadPersistedDistinctId = true + self.superProperties = InternalProperties() + MixpanelPersistence.saveTimedEvents( + timedEvents: InternalProperties(), instanceName: self.name) + } + self.archive() + self.readWriteLock.write { + self.optOutStatus = true + } + self.readWriteLock.read { + MixpanelPersistence.saveOptOutStatusFlag(value: self.optOutStatus!, instanceName: self.name) + } + } - - /** + } + + /** Opt in tracking. - + Use this method to opt in an already opted out user from tracking. People updates and track calls will be sent to Mixpanel after using this method. - + This method will internally track an opt in event to your project. - + - parameter distintId: an optional string to use as the distinct ID for events - parameter properties: an optional properties dictionary that could be passed to add properties to the opt-in event that is sent to Mixpanel */ - public func optInTracking(distinctId: String? = nil, properties: Properties? = nil) { - trackingQueue.async { [weak self] in - guard let self = self else { return } - self.readWriteLock.write { - self.optOutStatus = false - } - self.readWriteLock.read { - MixpanelPersistence.saveOptOutStatusFlag(value: self.optOutStatus!, instanceName: self.name) - } - if let distinctId = distinctId { - self.identify(distinctId: distinctId) - } - self.track(event: "$opt_in", properties: properties) - } - - + public func optInTracking(distinctId: String? = nil, properties: Properties? = nil) { + trackingQueue.async { [weak self] in + guard let self = self else { return } + self.readWriteLock.write { + self.optOutStatus = false + } + self.readWriteLock.read { + MixpanelPersistence.saveOptOutStatusFlag(value: self.optOutStatus!, instanceName: self.name) + } + if let distinctId = distinctId { + self.identify(distinctId: distinctId) + } + self.track(event: "$opt_in", properties: properties) } - - /** + + } + + /** Returns if the current user has opted out tracking. - + - returns: the current super opted out tracking status */ - public func hasOptedOutTracking() -> Bool { - var optOutStatusShadow: Bool? - readWriteLock.read { - optOutStatusShadow = optOutStatus - } - return optOutStatusShadow ?? false + public func hasOptedOutTracking() -> Bool { + var optOutStatusShadow: Bool? + readWriteLock.read { + optOutStatusShadow = optOutStatus } - - // MARK: - AEDelegate - func increment(property: String, by: Double) { - people?.increment(property: property, by: by) - } - - func setOnce(properties: Properties) { - people?.setOnce(properties: properties) - } - -} + return optOutStatusShadow ?? false + } + // MARK: - AEDelegate + func increment(property: String, by: Double) { + people?.increment(property: property, by: by) + } + + func setOnce(properties: Properties) { + people?.setOnce(properties: properties) + } + +} diff --git a/Sources/MixpanelLogger.swift b/Sources/MixpanelLogger.swift index 701edd6e..a7c3211e 100644 --- a/Sources/MixpanelLogger.swift +++ b/Sources/MixpanelLogger.swift @@ -11,134 +11,150 @@ import Foundation /// This defines the various levels of logging that a message may be tagged with. This allows hiding and /// showing different logging levels at run time depending on the environment public enum MixpanelLogLevel: String { - /// MixpanelLogging displays *all* logs and additional debug information that may be useful to a developer - case debug + /// MixpanelLogging displays *all* logs and additional debug information that may be useful to a developer + case debug - /// MixpanelLogging displays *all* logs (**except** debug) - case info + /// MixpanelLogging displays *all* logs (**except** debug) + case info - /// MixpanelLogging displays *only* warnings and above - case warning + /// MixpanelLogging displays *only* warnings and above + case warning - /// MixpanelLogging displays *only* errors and above - case error + /// MixpanelLogging displays *only* errors and above + case error } /// This holds all the data for each log message, since the formatting is up to each /// logging object. It is a simple bag of data public struct MixpanelLogMessage { - /// The file where this log message was created - public let file: String + /// The file where this log message was created + public let file: String - /// The function where this log message was created - public let function: String + /// The function where this log message was created + public let function: String - /// The text of the log message - public let text: String + /// The text of the log message + public let text: String - /// The level of the log message - public let level: MixpanelLogLevel + /// The level of the log message + public let level: MixpanelLogLevel - init(path: String, function: String, text: String, level: MixpanelLogLevel) { - if let file = path.components(separatedBy: "/").last { - self.file = file - } else { - self.file = path - } - self.function = function - self.text = text - self.level = level + init(path: String, function: String, text: String, level: MixpanelLogLevel) { + if let file = path.components(separatedBy: "/").last { + self.file = file + } else { + self.file = path } + self.function = function + self.text = text + self.level = level + } } /// Any object that conforms to this protocol may log messages public protocol MixpanelLogging { - func addMessage(message: MixpanelLogMessage) + func addMessage(message: MixpanelLogMessage) } public class MixpanelLogger { - private static var loggers = [MixpanelLogging]() - private static var enabledLevels = Set() - private static let readWriteLock: ReadWriteLock = ReadWriteLock(label: "loggerLock") + private static var loggers = [MixpanelLogging]() + private static var enabledLevels = Set() + private static let readWriteLock: ReadWriteLock = ReadWriteLock(label: "loggerLock") - /// Add a `MixpanelLogging` object to receive all log messages - public class func addLogging(_ logging: MixpanelLogging) { - readWriteLock.write { - loggers.append(logging) - } + /// Add a `MixpanelLogging` object to receive all log messages + public class func addLogging(_ logging: MixpanelLogging) { + readWriteLock.write { + loggers.append(logging) } + } - /// Enable log messages of a specific `LogLevel` to be added to the log - class func enableLevel(_ level: MixpanelLogLevel) { - readWriteLock.write { - enabledLevels.insert(level) - } + /// Enable log messages of a specific `LogLevel` to be added to the log + class func enableLevel(_ level: MixpanelLogLevel) { + readWriteLock.write { + enabledLevels.insert(level) } + } - /// Disable log messages of a specific `LogLevel` to prevent them from being logged - class func disableLevel(_ level: MixpanelLogLevel) { - readWriteLock.write { - enabledLevels.remove(level) - } + /// Disable log messages of a specific `LogLevel` to prevent them from being logged + class func disableLevel(_ level: MixpanelLogLevel) { + readWriteLock.write { + enabledLevels.remove(level) } + } - /// debug: Adds a debug message to the Mixpanel log - /// - Parameter message: The message to be added to the log - class func debug(message: @autoclosure() -> Any, _ path: String = #file, _ function: String = #function) { - var enabledLevels = Set() - readWriteLock.read { - enabledLevels = self.enabledLevels - } - guard enabledLevels.contains(.debug) else { return } - forwardLogMessage(MixpanelLogMessage(path: path, function: function, text: "\(message())", - level: .debug)) + /// debug: Adds a debug message to the Mixpanel log + /// - Parameter message: The message to be added to the log + class func debug( + message: @autoclosure () -> Any, _ path: String = #file, _ function: String = #function + ) { + var enabledLevels = Set() + readWriteLock.read { + enabledLevels = self.enabledLevels } + guard enabledLevels.contains(.debug) else { return } + forwardLogMessage( + MixpanelLogMessage( + path: path, function: function, text: "\(message())", + level: .debug)) + } - /// info: Adds an informational message to the Mixpanel log - /// - Parameter message: The message to be added to the log - class func info(message: @autoclosure() -> Any, _ path: String = #file, _ function: String = #function) { - var enabledLevels = Set() - readWriteLock.read { - enabledLevels = self.enabledLevels - } - guard enabledLevels.contains(.info) else { return } - forwardLogMessage(MixpanelLogMessage(path: path, function: function, text: "\(message())", - level: .info)) + /// info: Adds an informational message to the Mixpanel log + /// - Parameter message: The message to be added to the log + class func info( + message: @autoclosure () -> Any, _ path: String = #file, _ function: String = #function + ) { + var enabledLevels = Set() + readWriteLock.read { + enabledLevels = self.enabledLevels } + guard enabledLevels.contains(.info) else { return } + forwardLogMessage( + MixpanelLogMessage( + path: path, function: function, text: "\(message())", + level: .info)) + } - /// warn: Adds a warning message to the Mixpanel log - /// - Parameter message: The message to be added to the log - class func warn(message: @autoclosure() -> Any, _ path: String = #file, _ function: String = #function) { - var enabledLevels = Set() - readWriteLock.read { - enabledLevels = self.enabledLevels - } - guard enabledLevels.contains(.warning) else { return } - forwardLogMessage(MixpanelLogMessage(path: path, function: function, text: "\(message())", - level: .warning)) + /// warn: Adds a warning message to the Mixpanel log + /// - Parameter message: The message to be added to the log + class func warn( + message: @autoclosure () -> Any, _ path: String = #file, _ function: String = #function + ) { + var enabledLevels = Set() + readWriteLock.read { + enabledLevels = self.enabledLevels } + guard enabledLevels.contains(.warning) else { return } + forwardLogMessage( + MixpanelLogMessage( + path: path, function: function, text: "\(message())", + level: .warning)) + } - /// error: Adds an error message to the Mixpanel log - /// - Parameter message: The message to be added to the log - class func error(message: @autoclosure() -> Any, _ path: String = #file, _ function: String = #function) { - var enabledLevels = Set() - readWriteLock.read { - enabledLevels = self.enabledLevels - } - guard enabledLevels.contains(.error) else { return } - forwardLogMessage(MixpanelLogMessage(path: path, function: function, text: "\(message())", - level: .error)) + /// error: Adds an error message to the Mixpanel log + /// - Parameter message: The message to be added to the log + class func error( + message: @autoclosure () -> Any, _ path: String = #file, _ function: String = #function + ) { + var enabledLevels = Set() + readWriteLock.read { + enabledLevels = self.enabledLevels } + guard enabledLevels.contains(.error) else { return } + forwardLogMessage( + MixpanelLogMessage( + path: path, function: function, text: "\(message())", + level: .error)) + } - /// This forwards a `MixpanelLogMessage` to each logger that has been added - class private func forwardLogMessage(_ message: MixpanelLogMessage) { - // Forward the log message to every registered MixpanelLogging instance - var loggers = [MixpanelLogging]() - readWriteLock.read { - loggers = self.loggers - } - readWriteLock.write { - loggers.forEach { $0.addMessage(message: message) } - } + /// This forwards a `MixpanelLogMessage` to each logger that has been added + class private func forwardLogMessage(_ message: MixpanelLogMessage) { + // Forward the log message to every registered MixpanelLogging instance + var loggers = [MixpanelLogging]() + readWriteLock.read { + loggers = self.loggers } + readWriteLock.write { + loggers.forEach { $0.addMessage(message: message) } + } + } } diff --git a/Sources/MixpanelOptions.swift b/Sources/MixpanelOptions.swift index cbd30c06..2557f60d 100644 --- a/Sources/MixpanelOptions.swift +++ b/Sources/MixpanelOptions.swift @@ -6,44 +6,45 @@ // Copyright © 2025 Mixpanel. All rights reserved. // - public class MixpanelOptions { - public let token: String - public let flushInterval: Double - public let instanceName: String? - public let trackAutomaticEvents: Bool - public let optOutTrackingByDefault: Bool - public let useUniqueDistinctId: Bool - public let superProperties: Properties? - public let serverURL: String? - public let proxyServerConfig: ProxyServerConfig? - public let useGzipCompression: Bool - public let featureFlagsEnabled: Bool - public let featureFlagsContext: [String: Any] - - public init(token: String, - flushInterval: Double = 60, - instanceName: String? = nil, - trackAutomaticEvents: Bool = false, - optOutTrackingByDefault: Bool = false, - useUniqueDistinctId: Bool = false, - superProperties: Properties? = nil, - serverURL: String? = nil, - proxyServerConfig: ProxyServerConfig? = nil, - useGzipCompression: Bool = true, // NOTE: This is a new default value! - featureFlagsEnabled: Bool = false, - featureFlagsContext: [String: Any] = [:]) { - self.token = token - self.flushInterval = flushInterval - self.instanceName = instanceName - self.trackAutomaticEvents = trackAutomaticEvents - self.optOutTrackingByDefault = optOutTrackingByDefault - self.useUniqueDistinctId = useUniqueDistinctId - self.superProperties = superProperties - self.serverURL = serverURL - self.proxyServerConfig = proxyServerConfig - self.useGzipCompression = useGzipCompression - self.featureFlagsEnabled = featureFlagsEnabled - self.featureFlagsContext = featureFlagsContext - } + public let token: String + public let flushInterval: Double + public let instanceName: String? + public let trackAutomaticEvents: Bool + public let optOutTrackingByDefault: Bool + public let useUniqueDistinctId: Bool + public let superProperties: Properties? + public let serverURL: String? + public let proxyServerConfig: ProxyServerConfig? + public let useGzipCompression: Bool + public let featureFlagsEnabled: Bool + public let featureFlagsContext: [String: Any] + + public init( + token: String, + flushInterval: Double = 60, + instanceName: String? = nil, + trackAutomaticEvents: Bool = false, + optOutTrackingByDefault: Bool = false, + useUniqueDistinctId: Bool = false, + superProperties: Properties? = nil, + serverURL: String? = nil, + proxyServerConfig: ProxyServerConfig? = nil, + useGzipCompression: Bool = true, // NOTE: This is a new default value! + featureFlagsEnabled: Bool = false, + featureFlagsContext: [String: Any] = [:] + ) { + self.token = token + self.flushInterval = flushInterval + self.instanceName = instanceName + self.trackAutomaticEvents = trackAutomaticEvents + self.optOutTrackingByDefault = optOutTrackingByDefault + self.useUniqueDistinctId = useUniqueDistinctId + self.superProperties = superProperties + self.serverURL = serverURL + self.proxyServerConfig = proxyServerConfig + self.useGzipCompression = useGzipCompression + self.featureFlagsEnabled = featureFlagsEnabled + self.featureFlagsContext = featureFlagsContext + } } diff --git a/Sources/MixpanelPersistence.swift b/Sources/MixpanelPersistence.swift index ba49ce51..cf15579d 100644 --- a/Sources/MixpanelPersistence.swift +++ b/Sources/MixpanelPersistence.swift @@ -9,494 +9,544 @@ import Foundation enum LegacyArchiveType: String { - case events - case people - case groups - case properties - case optOutStatus + case events + case people + case groups + case properties + case optOutStatus } enum PersistenceType: String, CaseIterable { - case events - case people - case groups + case events + case people + case groups } struct PersistenceConstant { - static let unIdentifiedFlag = true + static let unIdentifiedFlag = true } struct MixpanelIdentity { - let distinctID: String - let peopleDistinctID: String? - let anonymousId: String? - let userId: String? - let alias: String? - let hadPersistedDistinctId: Bool? + let distinctID: String + let peopleDistinctID: String? + let anonymousId: String? + let userId: String? + let alias: String? + let hadPersistedDistinctId: Bool? } struct MixpanelUserDefaultsKeys { - static let suiteName = "Mixpanel" - static let prefix = "mixpanel" - static let optOutStatus = "OptOutStatus" - static let timedEvents = "timedEvents" - static let superProperties = "superProperties" - static let distinctID = "MPDistinctID" - static let peopleDistinctID = "MPPeopleDistinctID" - static let anonymousId = "MPAnonymousId" - static let userID = "MPUserId" - static let alias = "MPAlias" - static let hadPersistedDistinctId = "MPHadPersistedDistinctId" - static let flags = "MPFlags" + static let suiteName = "Mixpanel" + static let prefix = "mixpanel" + static let optOutStatus = "OptOutStatus" + static let timedEvents = "timedEvents" + static let superProperties = "superProperties" + static let distinctID = "MPDistinctID" + static let peopleDistinctID = "MPPeopleDistinctID" + static let anonymousId = "MPAnonymousId" + static let userID = "MPUserId" + static let alias = "MPAlias" + static let hadPersistedDistinctId = "MPHadPersistedDistinctId" + static let flags = "MPFlags" } class MixpanelPersistence { - - let instanceName: String - let mpdb: MPDB - private static let archivedClasses = [NSArray.self, NSDictionary.self, NSSet.self, NSString.self, NSDate.self, NSURL.self, NSNumber.self, NSNull.self] - - init(instanceName: String) { - self.instanceName = instanceName - mpdb = MPDB.init(token: instanceName) + + let instanceName: String + let mpdb: MPDB + private static let archivedClasses = [ + NSArray.self, NSDictionary.self, NSSet.self, NSString.self, NSDate.self, NSURL.self, + NSNumber.self, NSNull.self, + ] + + init(instanceName: String) { + self.instanceName = instanceName + mpdb = MPDB.init(token: instanceName) + } + + deinit { + mpdb.close() + } + + func closeDB() { + mpdb.close() + } + + func saveEntity(_ entity: InternalProperties, type: PersistenceType, flag: Bool = false) { + if let data = JSONHandler.serializeJSONObject(entity) { + mpdb.insertRow(type, data: data, flag: flag) } - - deinit { - mpdb.close() + } + + func saveEntities(_ entities: Queue, type: PersistenceType, flag: Bool = false) { + for entity in entities { + saveEntity(entity, type: type) } - - func closeDB() { - mpdb.close() + } + + func loadEntitiesInBatch( + type: PersistenceType, batchSize: Int = Int.max, flag: Bool = false, + excludeAutomaticEvents: Bool = false + ) -> [InternalProperties] { + var entities = mpdb.readRows(type, numRows: batchSize, flag: flag) + if excludeAutomaticEvents && type == .events { + entities = entities.filter { !($0["event"] as! String).hasPrefix("$ae_") } } - - func saveEntity(_ entity: InternalProperties, type: PersistenceType, flag: Bool = false) { - if let data = JSONHandler.serializeJSONObject(entity) { - mpdb.insertRow(type, data: data, flag: flag) - } + if type == PersistenceType.people { + let distinctId = MixpanelPersistence.loadIdentity(instanceName: instanceName).distinctID + return entities.map { entityWithDistinctId($0, distinctId: distinctId) } } - - func saveEntities(_ entities: Queue, type: PersistenceType, flag: Bool = false) { - for entity in entities { - saveEntity(entity, type: type) - } + return entities + } + + private func entityWithDistinctId(_ entity: InternalProperties, distinctId: String) + -> InternalProperties + { + var result = entity + result["$distinct_id"] = distinctId + return result + } + + func removeEntitiesInBatch(type: PersistenceType, ids: [Int32]) { + mpdb.deleteRows(type, ids: ids) + } + + func identifyPeople(token: String) { + mpdb.updateRowsFlag(.people, newFlag: !PersistenceConstant.unIdentifiedFlag) + } + + func resetEntities() { + for pType in PersistenceType.allCases { + mpdb.deleteRows(pType, isDeleteAll: true) } - - func loadEntitiesInBatch(type: PersistenceType, batchSize: Int = Int.max, flag: Bool = false, excludeAutomaticEvents: Bool = false) -> [InternalProperties] { - var entities = mpdb.readRows(type, numRows: batchSize, flag: flag) - if excludeAutomaticEvents && type == .events { - entities = entities.filter { !($0["event"] as! String).hasPrefix("$ae_") } - } - if type == PersistenceType.people { - let distinctId = MixpanelPersistence.loadIdentity(instanceName: instanceName).distinctID - return entities.map { entityWithDistinctId($0, distinctId: distinctId) } - } - return entities + } + + static func saveOptOutStatusFlag(value: Bool, instanceName: String) { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return } - - private func entityWithDistinctId(_ entity: InternalProperties, distinctId: String) -> InternalProperties { - var result = entity; - result["$distinct_id"] = distinctId - return result + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" + defaults.setValue(value, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.optOutStatus)") + defaults.synchronize() + } + + static func loadOptOutStatusFlag(instanceName: String) -> Bool? { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return nil } - - func removeEntitiesInBatch(type: PersistenceType, ids: [Int32]) { - mpdb.deleteRows(type, ids: ids) + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" + return defaults.object(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.optOutStatus)") as? Bool + } + + static func saveTimedEvents(timedEvents: InternalProperties, instanceName: String) { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return } - - func identifyPeople(token: String) { - mpdb.updateRowsFlag(.people, newFlag: !PersistenceConstant.unIdentifiedFlag) + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" + do { + let timedEventsData = try NSKeyedArchiver.archivedData( + withRootObject: timedEvents, requiringSecureCoding: false) + defaults.set(timedEventsData, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.timedEvents)") + defaults.synchronize() + } catch { + MixpanelLogger.warn(message: "Failed to archive timed events") } - - func resetEntities() { - for pType in PersistenceType.allCases { - mpdb.deleteRows(pType, isDeleteAll: true) - } + } + + static func loadTimedEvents(instanceName: String) -> InternalProperties { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return InternalProperties() } - - static func saveOptOutStatusFlag(value: Bool, instanceName: String) { - guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { - return - } - let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" - defaults.setValue(value, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.optOutStatus)") - defaults.synchronize() + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" + guard + let timedEventsData = defaults.data( + forKey: "\(prefix)\(MixpanelUserDefaultsKeys.timedEvents)") + else { + return InternalProperties() } - - static func loadOptOutStatusFlag(instanceName: String) -> Bool? { - guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { - return nil - } - let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" - return defaults.object(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.optOutStatus)") as? Bool + do { + return try NSKeyedUnarchiver.unarchivedObject( + ofClasses: archivedClasses, from: timedEventsData) as? InternalProperties + ?? InternalProperties() + } catch { + MixpanelLogger.warn(message: "Failed to unarchive timed events") + return InternalProperties() } - - static func saveTimedEvents(timedEvents: InternalProperties, instanceName: String) { - guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { - return - } - let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" - do { - let timedEventsData = try NSKeyedArchiver.archivedData(withRootObject: timedEvents, requiringSecureCoding: false) - defaults.set(timedEventsData, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.timedEvents)") - defaults.synchronize() - } catch { - MixpanelLogger.warn(message: "Failed to archive timed events") - } + } + + static func saveSuperProperties(superProperties: InternalProperties, instanceName: String) { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return } - - static func loadTimedEvents(instanceName: String) -> InternalProperties { - guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { - return InternalProperties() - } - let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" - guard let timedEventsData = defaults.data(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.timedEvents)") else { - return InternalProperties() - } - do { - return try NSKeyedUnarchiver.unarchivedObject(ofClasses: archivedClasses, from: timedEventsData) as? InternalProperties ?? InternalProperties() - } catch { - MixpanelLogger.warn(message: "Failed to unarchive timed events") - return InternalProperties() - } + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" + do { + let superPropertiesData = try NSKeyedArchiver.archivedData( + withRootObject: superProperties, requiringSecureCoding: false) + defaults.set( + superPropertiesData, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.superProperties)") + defaults.synchronize() + } catch { + MixpanelLogger.warn(message: "Failed to archive super properties") } - - static func saveSuperProperties(superProperties: InternalProperties, instanceName: String) { - guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { - return - } - let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" - do { - let superPropertiesData = try NSKeyedArchiver.archivedData(withRootObject: superProperties, requiringSecureCoding: false) - defaults.set(superPropertiesData, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.superProperties)") - defaults.synchronize() - } catch { - MixpanelLogger.warn(message: "Failed to archive super properties") - } + } + + static func loadSuperProperties(instanceName: String) -> InternalProperties { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return InternalProperties() } - - static func loadSuperProperties(instanceName: String) -> InternalProperties { - guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { - return InternalProperties() - } - let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" - guard let superPropertiesData = defaults.data(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.superProperties)") else { - return InternalProperties() - } - do { - return try NSKeyedUnarchiver.unarchivedObject(ofClasses: archivedClasses, from: superPropertiesData) as? InternalProperties ?? InternalProperties() - } catch { - MixpanelLogger.warn(message: "Failed to unarchive super properties") - return InternalProperties() - } + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" + guard + let superPropertiesData = defaults.data( + forKey: "\(prefix)\(MixpanelUserDefaultsKeys.superProperties)") + else { + return InternalProperties() } - - /// -- Feature Flags -- - /// NOT currently used - - static func saveFlags(flags: InternalProperties, instanceName: String) { - guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { - return - } - let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" - do { - let flagsData = try NSKeyedArchiver.archivedData(withRootObject: flags, requiringSecureCoding: false) - defaults.set(flagsData, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.flags)") - defaults.synchronize() - } catch { - MixpanelLogger.warn(message: "Failed to archive flags") - } + do { + return try NSKeyedUnarchiver.unarchivedObject( + ofClasses: archivedClasses, from: superPropertiesData) as? InternalProperties + ?? InternalProperties() + } catch { + MixpanelLogger.warn(message: "Failed to unarchive super properties") + return InternalProperties() } - - static func loadFlags(instanceName: String) -> InternalProperties { - guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { - return InternalProperties() - } - let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" - guard let flags = defaults.data(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.flags)") else { - return InternalProperties() - } - do { - return try NSKeyedUnarchiver.unarchivedObject(ofClasses: archivedClasses, from: flags) as? InternalProperties ?? InternalProperties() - } catch { - MixpanelLogger.warn(message: "Failed to unarchive flags") - return InternalProperties() - } + } + + /// -- Feature Flags -- + /// NOT currently used + + static func saveFlags(flags: InternalProperties, instanceName: String) { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return } - - static func saveIdentity(_ mixpanelIdentity: MixpanelIdentity, instanceName: String) { - guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { - return - } - let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" - defaults.set(mixpanelIdentity.distinctID, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.distinctID)") - defaults.set(mixpanelIdentity.peopleDistinctID, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.peopleDistinctID)") - defaults.set(mixpanelIdentity.anonymousId, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.anonymousId)") - defaults.set(mixpanelIdentity.userId, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.userID)") - defaults.set(mixpanelIdentity.alias, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.alias)") - defaults.set(mixpanelIdentity.hadPersistedDistinctId, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.hadPersistedDistinctId)") - defaults.synchronize() + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" + do { + let flagsData = try NSKeyedArchiver.archivedData( + withRootObject: flags, requiringSecureCoding: false) + defaults.set(flagsData, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.flags)") + defaults.synchronize() + } catch { + MixpanelLogger.warn(message: "Failed to archive flags") } - - static func loadIdentity(instanceName: String) -> MixpanelIdentity { - guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { - return MixpanelIdentity.init(distinctID: "", - peopleDistinctID: nil, - anonymousId: nil, - userId: nil, - alias: nil, - hadPersistedDistinctId: nil) - } - let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" - return MixpanelIdentity.init( - distinctID: defaults.string(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.distinctID)") ?? "", - peopleDistinctID: defaults.string(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.peopleDistinctID)"), - anonymousId: defaults.string(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.anonymousId)"), - userId: defaults.string(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.userID)"), - alias: defaults.string(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.alias)"), - hadPersistedDistinctId: defaults.bool(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.hadPersistedDistinctId)")) + } + + static func loadFlags(instanceName: String) -> InternalProperties { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return InternalProperties() } - - static func deleteMPUserDefaultsData(instanceName: String) { - guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { - return - } - let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" - defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.distinctID)") - defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.peopleDistinctID)") - defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.anonymousId)") - defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.userID)") - defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.alias)") - defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.hadPersistedDistinctId)") - defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.optOutStatus)") - defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.timedEvents)") - defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.superProperties)") - defaults.synchronize() + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" + guard let flags = defaults.data(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.flags)") else { + return InternalProperties() } - - // code for unarchiving from legacy archive files and migrating to SQLite / NSUserDefaults persistence - func migrate() { - if !needMigration() { - return - } - let (eventsQueue, - peopleQueue, - groupsQueue, - superProperties, - timedEvents, - distinctId, - anonymousId, - userId, - alias, - hadPersistedDistinctId, - peopleDistinctId, - peopleUnidentifiedQueue, - optOutStatus) = unarchiveFromLegacy() - saveEntities(eventsQueue, type: PersistenceType.events) - saveEntities(peopleUnidentifiedQueue, type: PersistenceType.people, flag: PersistenceConstant.unIdentifiedFlag) - saveEntities(peopleQueue, type: PersistenceType.people) - saveEntities(groupsQueue, type: PersistenceType.groups) - MixpanelPersistence.saveSuperProperties(superProperties: superProperties, instanceName: instanceName) - MixpanelPersistence.saveTimedEvents(timedEvents: timedEvents, instanceName: instanceName) - MixpanelPersistence.saveIdentity(MixpanelIdentity.init( - distinctID: distinctId, - peopleDistinctID: peopleDistinctId, - anonymousId: anonymousId, - userId: userId, - alias: alias, - hadPersistedDistinctId: hadPersistedDistinctId), instanceName: instanceName) - if let optOutFlag = optOutStatus { - MixpanelPersistence.saveOptOutStatusFlag(value: optOutFlag, instanceName: instanceName) - } - return + do { + return try NSKeyedUnarchiver.unarchivedObject(ofClasses: archivedClasses, from: flags) + as? InternalProperties ?? InternalProperties() + } catch { + MixpanelLogger.warn(message: "Failed to unarchive flags") + return InternalProperties() } - - private func filePathWithType(_ type: String) -> String? { - let filename = "mixpanel-\(instanceName)-\(type)" - let manager = FileManager.default - -#if os(iOS) - let url = manager.urls(for: .libraryDirectory, in: .userDomainMask).last -#else - let url = manager.urls(for: .cachesDirectory, in: .userDomainMask).last -#endif // os(iOS) - guard let urlUnwrapped = url?.appendingPathComponent(filename).path else { - return nil - } - - return urlUnwrapped + } + + static func saveIdentity(_ mixpanelIdentity: MixpanelIdentity, instanceName: String) { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return } - - private func unarchiveFromLegacy() -> (eventsQueue: Queue, - peopleQueue: Queue, - groupsQueue: Queue, - superProperties: InternalProperties, - timedEvents: InternalProperties, - distinctId: String, - anonymousId: String?, - userId: String?, - alias: String?, - hadPersistedDistinctId: Bool?, - peopleDistinctId: String?, - peopleUnidentifiedQueue: Queue, - optOutStatus: Bool?) { - let eventsQueue = unarchiveEvents() - let peopleQueue = unarchivePeople() - let groupsQueue = unarchiveGroups() - let optOutStatus = unarchiveOptOutStatus() - - let (superProperties, - timedEvents, - distinctId, - anonymousId, - userId, - alias, - hadPersistedDistinctId, - peopleDistinctId, - peopleUnidentifiedQueue) = unarchiveProperties() - - if let eventsFile = filePathWithType(PersistenceType.events.rawValue) { - removeArchivedFile(atPath: eventsFile) - } - if let peopleFile = filePathWithType(PersistenceType.people.rawValue) { - removeArchivedFile(atPath: peopleFile) - } - if let groupsFile = filePathWithType(PersistenceType.groups.rawValue) { - removeArchivedFile(atPath: groupsFile) - } - if let propsFile = filePathWithType("properties") { - removeArchivedFile(atPath: propsFile) - } - if let optOutFile = filePathWithType("optOutStatus") { - removeArchivedFile(atPath: optOutFile) - } - - return (eventsQueue, - peopleQueue, - groupsQueue, - superProperties, - timedEvents, - distinctId, - anonymousId, - userId, - alias, - hadPersistedDistinctId, - peopleDistinctId, - peopleUnidentifiedQueue, - optOutStatus) + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" + defaults.set( + mixpanelIdentity.distinctID, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.distinctID)") + defaults.set( + mixpanelIdentity.peopleDistinctID, + forKey: "\(prefix)\(MixpanelUserDefaultsKeys.peopleDistinctID)") + defaults.set( + mixpanelIdentity.anonymousId, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.anonymousId)") + defaults.set(mixpanelIdentity.userId, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.userID)") + defaults.set(mixpanelIdentity.alias, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.alias)") + defaults.set( + mixpanelIdentity.hadPersistedDistinctId, + forKey: "\(prefix)\(MixpanelUserDefaultsKeys.hadPersistedDistinctId)") + defaults.synchronize() + } + + static func loadIdentity(instanceName: String) -> MixpanelIdentity { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return MixpanelIdentity.init( + distinctID: "", + peopleDistinctID: nil, + anonymousId: nil, + userId: nil, + alias: nil, + hadPersistedDistinctId: nil) } - - private func unarchiveWithFilePath(_ filePath: String) -> Any? { - if #available(iOS 11.0, macOS 10.13, watchOS 4.0, tvOS 11.0, *) { - guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)), - let unarchivedData = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: MixpanelPersistence.archivedClasses, from: data) else { - MixpanelLogger.info(message: "Unable to read file at path: \(filePath)") - removeArchivedFile(atPath: filePath) - return nil - } - return unarchivedData - } else { - guard let unarchivedData = NSKeyedUnarchiver.unarchiveObject(withFile: filePath) else { - MixpanelLogger.info(message: "Unable to read file at path: \(filePath)") - removeArchivedFile(atPath: filePath) - return nil - } - return unarchivedData - } + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" + return MixpanelIdentity.init( + distinctID: defaults.string(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.distinctID)") ?? "", + peopleDistinctID: defaults.string( + forKey: "\(prefix)\(MixpanelUserDefaultsKeys.peopleDistinctID)"), + anonymousId: defaults.string(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.anonymousId)"), + userId: defaults.string(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.userID)"), + alias: defaults.string(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.alias)"), + hadPersistedDistinctId: defaults.bool( + forKey: "\(prefix)\(MixpanelUserDefaultsKeys.hadPersistedDistinctId)")) + } + + static func deleteMPUserDefaultsData(instanceName: String) { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return } - - private func removeArchivedFile(atPath filePath: String) { - do { - try FileManager.default.removeItem(atPath: filePath) - } catch let err { - MixpanelLogger.info(message: "Unable to remove file at path: \(filePath), error: \(err)") - } + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(instanceName)-" + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.distinctID)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.peopleDistinctID)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.anonymousId)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.userID)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.alias)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.hadPersistedDistinctId)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.optOutStatus)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.timedEvents)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.superProperties)") + defaults.synchronize() + } + + // code for unarchiving from legacy archive files and migrating to SQLite / NSUserDefaults persistence + func migrate() { + if !needMigration() { + return } - - private func unarchiveEvents() -> Queue { - let data = unarchiveWithType(PersistenceType.events.rawValue) - return data as? Queue ?? [] + let ( + eventsQueue, + peopleQueue, + groupsQueue, + superProperties, + timedEvents, + distinctId, + anonymousId, + userId, + alias, + hadPersistedDistinctId, + peopleDistinctId, + peopleUnidentifiedQueue, + optOutStatus + ) = unarchiveFromLegacy() + saveEntities(eventsQueue, type: PersistenceType.events) + saveEntities( + peopleUnidentifiedQueue, type: PersistenceType.people, + flag: PersistenceConstant.unIdentifiedFlag) + saveEntities(peopleQueue, type: PersistenceType.people) + saveEntities(groupsQueue, type: PersistenceType.groups) + MixpanelPersistence.saveSuperProperties( + superProperties: superProperties, instanceName: instanceName) + MixpanelPersistence.saveTimedEvents(timedEvents: timedEvents, instanceName: instanceName) + MixpanelPersistence.saveIdentity( + MixpanelIdentity.init( + distinctID: distinctId, + peopleDistinctID: peopleDistinctId, + anonymousId: anonymousId, + userId: userId, + alias: alias, + hadPersistedDistinctId: hadPersistedDistinctId), instanceName: instanceName) + if let optOutFlag = optOutStatus { + MixpanelPersistence.saveOptOutStatusFlag(value: optOutFlag, instanceName: instanceName) } - - private func unarchivePeople() -> Queue { - let data = unarchiveWithType(PersistenceType.people.rawValue) - return data as? Queue ?? [] + return + } + + private func filePathWithType(_ type: String) -> String? { + let filename = "mixpanel-\(instanceName)-\(type)" + let manager = FileManager.default + + #if os(iOS) + let url = manager.urls(for: .libraryDirectory, in: .userDomainMask).last + #else + let url = manager.urls(for: .cachesDirectory, in: .userDomainMask).last + #endif // os(iOS) + guard let urlUnwrapped = url?.appendingPathComponent(filename).path else { + return nil } - - private func unarchiveGroups() -> Queue { - let data = unarchiveWithType(PersistenceType.groups.rawValue) - return data as? Queue ?? [] + + return urlUnwrapped + } + + private func unarchiveFromLegacy() -> ( + eventsQueue: Queue, + peopleQueue: Queue, + groupsQueue: Queue, + superProperties: InternalProperties, + timedEvents: InternalProperties, + distinctId: String, + anonymousId: String?, + userId: String?, + alias: String?, + hadPersistedDistinctId: Bool?, + peopleDistinctId: String?, + peopleUnidentifiedQueue: Queue, + optOutStatus: Bool? + ) { + let eventsQueue = unarchiveEvents() + let peopleQueue = unarchivePeople() + let groupsQueue = unarchiveGroups() + let optOutStatus = unarchiveOptOutStatus() + + let ( + superProperties, + timedEvents, + distinctId, + anonymousId, + userId, + alias, + hadPersistedDistinctId, + peopleDistinctId, + peopleUnidentifiedQueue + ) = unarchiveProperties() + + if let eventsFile = filePathWithType(PersistenceType.events.rawValue) { + removeArchivedFile(atPath: eventsFile) } - - private func unarchiveOptOutStatus() -> Bool? { - return unarchiveWithType("optOutStatus") as? Bool + if let peopleFile = filePathWithType(PersistenceType.people.rawValue) { + removeArchivedFile(atPath: peopleFile) } - - private func unarchiveProperties() -> (InternalProperties, - InternalProperties, - String, - String?, - String?, - String?, - Bool?, - String?, - Queue) { - let properties = unarchiveWithType("properties") as? InternalProperties - let superProperties = - properties?["superProperties"] as? InternalProperties ?? InternalProperties() - let timedEvents = - properties?["timedEvents"] as? InternalProperties ?? InternalProperties() - let distinctId = - properties?["distinctId"] as? String ?? "" - let anonymousId = - properties?["anonymousId"] as? String ?? nil - let userId = - properties?["userId"] as? String ?? nil - let alias = - properties?["alias"] as? String ?? nil - let hadPersistedDistinctId = - properties?["hadPersistedDistinctId"] as? Bool ?? nil - let peopleDistinctId = - properties?["peopleDistinctId"] as? String ?? nil - let peopleUnidentifiedQueue = - properties?["peopleUnidentifiedQueue"] as? Queue ?? Queue() - - return (superProperties, - timedEvents, - distinctId, - anonymousId, - userId, - alias, - hadPersistedDistinctId, - peopleDistinctId, - peopleUnidentifiedQueue) + if let groupsFile = filePathWithType(PersistenceType.groups.rawValue) { + removeArchivedFile(atPath: groupsFile) } - - private func unarchiveWithType(_ type: String) -> Any? { - let filePath = filePathWithType(type) - guard let path = filePath else { - MixpanelLogger.info(message: "bad file path, cant fetch file") - return nil - } - - guard let unarchivedData = unarchiveWithFilePath(path) else { - MixpanelLogger.info(message: "can't unarchive file") - return nil - } - - return unarchivedData + if let propsFile = filePathWithType("properties") { + removeArchivedFile(atPath: propsFile) } - - private func needMigration() -> Bool { - return fileExists(type: LegacyArchiveType.events.rawValue) || - fileExists(type: LegacyArchiveType.people.rawValue) || - fileExists(type: LegacyArchiveType.people.rawValue) || - fileExists(type: LegacyArchiveType.groups.rawValue) || - fileExists(type: LegacyArchiveType.properties.rawValue) || - fileExists(type: LegacyArchiveType.optOutStatus.rawValue) + if let optOutFile = filePathWithType("optOutStatus") { + removeArchivedFile(atPath: optOutFile) } - - private func fileExists(type: String) -> Bool { - return FileManager.default.fileExists(atPath: filePathWithType(type) ?? "") + + return ( + eventsQueue, + peopleQueue, + groupsQueue, + superProperties, + timedEvents, + distinctId, + anonymousId, + userId, + alias, + hadPersistedDistinctId, + peopleDistinctId, + peopleUnidentifiedQueue, + optOutStatus + ) + } + + private func unarchiveWithFilePath(_ filePath: String) -> Any? { + if #available(iOS 11.0, macOS 10.13, watchOS 4.0, tvOS 11.0, *) { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)), + let unarchivedData = try? NSKeyedUnarchiver.unarchivedObject( + ofClasses: MixpanelPersistence.archivedClasses, from: data) + else { + MixpanelLogger.info(message: "Unable to read file at path: \(filePath)") + removeArchivedFile(atPath: filePath) + return nil + } + return unarchivedData + } else { + guard let unarchivedData = NSKeyedUnarchiver.unarchiveObject(withFile: filePath) else { + MixpanelLogger.info(message: "Unable to read file at path: \(filePath)") + removeArchivedFile(atPath: filePath) + return nil + } + return unarchivedData } - + } + + private func removeArchivedFile(atPath filePath: String) { + do { + try FileManager.default.removeItem(atPath: filePath) + } catch let err { + MixpanelLogger.info(message: "Unable to remove file at path: \(filePath), error: \(err)") + } + } + + private func unarchiveEvents() -> Queue { + let data = unarchiveWithType(PersistenceType.events.rawValue) + return data as? Queue ?? [] + } + + private func unarchivePeople() -> Queue { + let data = unarchiveWithType(PersistenceType.people.rawValue) + return data as? Queue ?? [] + } + + private func unarchiveGroups() -> Queue { + let data = unarchiveWithType(PersistenceType.groups.rawValue) + return data as? Queue ?? [] + } + + private func unarchiveOptOutStatus() -> Bool? { + return unarchiveWithType("optOutStatus") as? Bool + } + + private func unarchiveProperties() -> ( + InternalProperties, + InternalProperties, + String, + String?, + String?, + String?, + Bool?, + String?, + Queue + ) { + let properties = unarchiveWithType("properties") as? InternalProperties + let superProperties = + properties?["superProperties"] as? InternalProperties ?? InternalProperties() + let timedEvents = + properties?["timedEvents"] as? InternalProperties ?? InternalProperties() + let distinctId = + properties?["distinctId"] as? String ?? "" + let anonymousId = + properties?["anonymousId"] as? String ?? nil + let userId = + properties?["userId"] as? String ?? nil + let alias = + properties?["alias"] as? String ?? nil + let hadPersistedDistinctId = + properties?["hadPersistedDistinctId"] as? Bool ?? nil + let peopleDistinctId = + properties?["peopleDistinctId"] as? String ?? nil + let peopleUnidentifiedQueue = + properties?["peopleUnidentifiedQueue"] as? Queue ?? Queue() + + return ( + superProperties, + timedEvents, + distinctId, + anonymousId, + userId, + alias, + hadPersistedDistinctId, + peopleDistinctId, + peopleUnidentifiedQueue + ) + } + + private func unarchiveWithType(_ type: String) -> Any? { + let filePath = filePathWithType(type) + guard let path = filePath else { + MixpanelLogger.info(message: "bad file path, cant fetch file") + return nil + } + + guard let unarchivedData = unarchiveWithFilePath(path) else { + MixpanelLogger.info(message: "can't unarchive file") + return nil + } + + return unarchivedData + } + + private func needMigration() -> Bool { + return fileExists(type: LegacyArchiveType.events.rawValue) + || fileExists(type: LegacyArchiveType.people.rawValue) + || fileExists(type: LegacyArchiveType.people.rawValue) + || fileExists(type: LegacyArchiveType.groups.rawValue) + || fileExists(type: LegacyArchiveType.properties.rawValue) + || fileExists(type: LegacyArchiveType.optOutStatus.rawValue) + } + + private func fileExists(type: String) -> Bool { + return FileManager.default.fileExists(atPath: filePathWithType(type) ?? "") + } + } diff --git a/Sources/MixpanelType.swift b/Sources/MixpanelType.swift index cc114dfc..212983dc 100644 --- a/Sources/MixpanelType.swift +++ b/Sources/MixpanelType.swift @@ -12,353 +12,355 @@ import Foundation /// MixpanelType can be either String, Int, UInt, Double, Float, Bool, [MixpanelType], [String: MixpanelType], Date, URL, or NSNull. /// Numbers are not NaN or infinity public protocol MixpanelType: Any { - /** + /** Checks if this object has nested object types that Mixpanel supports. */ - func isValidNestedTypeAndValue() -> Bool - - func equals(rhs: MixpanelType) -> Bool + func isValidNestedTypeAndValue() -> Bool + + func equals(rhs: MixpanelType) -> Bool } extension Optional: MixpanelType { - /** + /** Checks if this object has nested object types that Mixpanel supports. */ - public func isValidNestedTypeAndValue() -> Bool { - guard let val = self else { return true } // nil is valid - switch val { - case let v as MixpanelType: - return v.isValidNestedTypeAndValue() - default: - // non-nil but cannot be unwrapped to MixpanelType - return false - } + public func isValidNestedTypeAndValue() -> Bool { + guard let val = self else { return true } // nil is valid + switch val { + case let v as MixpanelType: + return v.isValidNestedTypeAndValue() + default: + // non-nil but cannot be unwrapped to MixpanelType + return false } + } - public func equals(rhs: MixpanelType) -> Bool { - if let v = self as? String, rhs is String { - return v == rhs as! String - } else if let v = self as? NSString, rhs is NSString { - return v == rhs as! NSString - } else if let v = self as? NSNumber, rhs is NSNumber { - return v.isEqual(to: rhs as! NSNumber) - } else if let v = self as? Int, rhs is Int { - return v == rhs as! Int - } else if let v = self as? UInt, rhs is UInt { - return v == rhs as! UInt - } else if let v = self as? Double, rhs is Double { - return v == rhs as! Double - } else if let v = self as? Float, rhs is Float { - return v == rhs as! Float - } else if let v = self as? Bool, rhs is Bool { - return v == rhs as! Bool - } else if let v = self as? Date, rhs is Date { - return v == rhs as! Date - } else if let v = self as? URL, rhs is URL { - return v == rhs as! URL - } else if self is NSNull && rhs is NSNull { - return true - } - return false + public func equals(rhs: MixpanelType) -> Bool { + if let v = self as? String, rhs is String { + return v == rhs as! String + } else if let v = self as? NSString, rhs is NSString { + return v == rhs as! NSString + } else if let v = self as? NSNumber, rhs is NSNumber { + return v.isEqual(to: rhs as! NSNumber) + } else if let v = self as? Int, rhs is Int { + return v == rhs as! Int + } else if let v = self as? UInt, rhs is UInt { + return v == rhs as! UInt + } else if let v = self as? Double, rhs is Double { + return v == rhs as! Double + } else if let v = self as? Float, rhs is Float { + return v == rhs as! Float + } else if let v = self as? Bool, rhs is Bool { + return v == rhs as! Bool + } else if let v = self as? Date, rhs is Date { + return v == rhs as! Date + } else if let v = self as? URL, rhs is URL { + return v == rhs as! URL + } else if self is NSNull && rhs is NSNull { + return true } + return false + } } extension String: MixpanelType { - /** + /** Checks if this object has nested object types that Mixpanel supports. Will always return true. */ - public func isValidNestedTypeAndValue() -> Bool { return true } - - public func equals(rhs: MixpanelType) -> Bool { - if rhs is String { - return self == rhs as! String - } - return false + public func isValidNestedTypeAndValue() -> Bool { return true } + + public func equals(rhs: MixpanelType) -> Bool { + if rhs is String { + return self == rhs as! String } + return false + } } extension NSString: MixpanelType { - /** + /** Checks if this object has nested object types that Mixpanel supports. Will always return true. */ - public func isValidNestedTypeAndValue() -> Bool { return true } + public func isValidNestedTypeAndValue() -> Bool { return true } - public func equals(rhs: MixpanelType) -> Bool { - if rhs is NSString { - return self == rhs as! NSString - } - return false + public func equals(rhs: MixpanelType) -> Bool { + if rhs is NSString { + return self == rhs as! NSString } + return false + } } extension NSNumber: MixpanelType { - /** + /** Checks if this object has nested object types that Mixpanel supports. Will always return true. */ - public func isValidNestedTypeAndValue() -> Bool { - return !self.doubleValue.isInfinite && !self.doubleValue.isNaN - } - - public func equals(rhs: MixpanelType) -> Bool { - if rhs is NSNumber { - return self.isEqual(rhs) - } - return false + public func isValidNestedTypeAndValue() -> Bool { + return !self.doubleValue.isInfinite && !self.doubleValue.isNaN + } + + public func equals(rhs: MixpanelType) -> Bool { + if rhs is NSNumber { + return self.isEqual(rhs) } + return false + } } extension Int: MixpanelType { - /** + /** Checks if this object has nested object types that Mixpanel supports. Will always return true. */ - public func isValidNestedTypeAndValue() -> Bool { return true } - - public func equals(rhs: MixpanelType) -> Bool { - if rhs is Int { - return self == rhs as! Int - } - return false + public func isValidNestedTypeAndValue() -> Bool { return true } + + public func equals(rhs: MixpanelType) -> Bool { + if rhs is Int { + return self == rhs as! Int } + return false + } } extension UInt: MixpanelType { - /** + /** Checks if this object has nested object types that Mixpanel supports. Will always return true. */ - public func isValidNestedTypeAndValue() -> Bool { return true } - - public func equals(rhs: MixpanelType) -> Bool { - if rhs is UInt { - return self == rhs as! UInt - } - return false + public func isValidNestedTypeAndValue() -> Bool { return true } + + public func equals(rhs: MixpanelType) -> Bool { + if rhs is UInt { + return self == rhs as! UInt } + return false + } } extension Double: MixpanelType { - /** + /** Checks if this object has nested object types that Mixpanel supports. Will always return true. */ - public func isValidNestedTypeAndValue() -> Bool { - return !self.isInfinite && !self.isNaN - } - - public func equals(rhs: MixpanelType) -> Bool { - if rhs is Double { - return self == rhs as! Double - } - return false + public func isValidNestedTypeAndValue() -> Bool { + return !self.isInfinite && !self.isNaN + } + + public func equals(rhs: MixpanelType) -> Bool { + if rhs is Double { + return self == rhs as! Double } + return false + } } extension Float: MixpanelType { - /** + /** Checks if this object has nested object types that Mixpanel supports. Will always return true. */ - public func isValidNestedTypeAndValue() -> Bool { - return !self.isInfinite && !self.isNaN - } - - public func equals(rhs: MixpanelType) -> Bool { - if rhs is Float { - return self == rhs as! Float - } - return false + public func isValidNestedTypeAndValue() -> Bool { + return !self.isInfinite && !self.isNaN + } + + public func equals(rhs: MixpanelType) -> Bool { + if rhs is Float { + return self == rhs as! Float } + return false + } } extension Bool: MixpanelType { - /** + /** Checks if this object has nested object types that Mixpanel supports. Will always return true. */ - public func isValidNestedTypeAndValue() -> Bool { return true } + public func isValidNestedTypeAndValue() -> Bool { return true } - public func equals(rhs: MixpanelType) -> Bool { - if rhs is Bool { - return self == rhs as! Bool - } - return false + public func equals(rhs: MixpanelType) -> Bool { + if rhs is Bool { + return self == rhs as! Bool } + return false + } } extension Date: MixpanelType { - /** + /** Checks if this object has nested object types that Mixpanel supports. Will always return true. */ - public func isValidNestedTypeAndValue() -> Bool { return true } + public func isValidNestedTypeAndValue() -> Bool { return true } - public func equals(rhs: MixpanelType) -> Bool { - if rhs is Date { - return self == rhs as! Date - } - return false + public func equals(rhs: MixpanelType) -> Bool { + if rhs is Date { + return self == rhs as! Date } + return false + } } extension URL: MixpanelType { - /** + /** Checks if this object has nested object types that Mixpanel supports. Will always return true. */ - public func isValidNestedTypeAndValue() -> Bool { return true } + public func isValidNestedTypeAndValue() -> Bool { return true } - public func equals(rhs: MixpanelType) -> Bool { - if rhs is URL { - return self == rhs as! URL - } - return false + public func equals(rhs: MixpanelType) -> Bool { + if rhs is URL { + return self == rhs as! URL } + return false + } } extension NSNull: MixpanelType { - /** + /** Checks if this object has nested object types that Mixpanel supports. Will always return true. */ - public func isValidNestedTypeAndValue() -> Bool { return true } + public func isValidNestedTypeAndValue() -> Bool { return true } - public func equals(rhs: MixpanelType) -> Bool { - if rhs is NSNull { - return true - } - return false + public func equals(rhs: MixpanelType) -> Bool { + if rhs is NSNull { + return true } + return false + } } extension Array: MixpanelType { - /** + /** Checks if this object has nested object types that Mixpanel supports. */ - public func isValidNestedTypeAndValue() -> Bool { - for element in self { - guard let _ = element as? MixpanelType else { - return false - } - } - return true - } - - public func equals(rhs: MixpanelType) -> Bool { - if rhs is [MixpanelType] { - let rhs = rhs as! [MixpanelType] - - if self.count != rhs.count { - return false - } - - if !isValidNestedTypeAndValue() { - return false - } - - let lhs = self as! [MixpanelType] - for (i, val) in lhs.enumerated() { - if !val.equals(rhs: rhs[i]) { - return false - } - } - return true - } + public func isValidNestedTypeAndValue() -> Bool { + for element in self { + guard element as? MixpanelType != nil else { return false + } } + return true + } + + public func equals(rhs: MixpanelType) -> Bool { + if rhs is [MixpanelType] { + let rhs = rhs as! [MixpanelType] + + if self.count != rhs.count { + return false + } + + if !isValidNestedTypeAndValue() { + return false + } + + let lhs = self as! [MixpanelType] + for (i, val) in lhs.enumerated() { + if !val.equals(rhs: rhs[i]) { + return false + } + } + return true + } + return false + } } extension NSArray: MixpanelType { - /** + /** Checks if this object has nested object types that Mixpanel supports. */ - public func isValidNestedTypeAndValue() -> Bool { - for element in self { - guard let _ = element as? MixpanelType else { - return false - } - } - return true - } - - public func equals(rhs: MixpanelType) -> Bool { - if rhs is [MixpanelType] { - let rhs = rhs as! [MixpanelType] - - if self.count != rhs.count { - return false - } - - if !isValidNestedTypeAndValue() { - return false - } - - let lhs = self as! [MixpanelType] - for (i, val) in lhs.enumerated() { - if !val.equals(rhs: rhs[i]) { - return false - } - } - return true - } + public func isValidNestedTypeAndValue() -> Bool { + for element in self { + guard element as? MixpanelType != nil else { return false + } } + return true + } + + public func equals(rhs: MixpanelType) -> Bool { + if rhs is [MixpanelType] { + let rhs = rhs as! [MixpanelType] + + if self.count != rhs.count { + return false + } + + if !isValidNestedTypeAndValue() { + return false + } + + let lhs = self as! [MixpanelType] + for (i, val) in lhs.enumerated() { + if !val.equals(rhs: rhs[i]) { + return false + } + } + return true + } + return false + } } extension Dictionary: MixpanelType { - /** + /** Checks if this object has nested object types that Mixpanel supports. */ - public func isValidNestedTypeAndValue() -> Bool { - for (key, value) in self { - guard let _ = key as? String, let _ = value as? MixpanelType else { - return false - } - } - return true - } - - public func equals(rhs: MixpanelType) -> Bool { - if rhs is [String: MixpanelType] { - let rhs = rhs as! [String: MixpanelType] - - if self.keys.count != rhs.keys.count { - return false - } - - if !isValidNestedTypeAndValue() { - return false - } - - for (key, val) in self as! [String: MixpanelType] { - guard let rVal = rhs[key] else { - return false - } - - if !val.equals(rhs: rVal) { - return false - } - } - return true - } + public func isValidNestedTypeAndValue() -> Bool { + for (key, value) in self { + guard key as? String != nil, value as? MixpanelType != nil else { return false + } } + return true + } + + public func equals(rhs: MixpanelType) -> Bool { + if rhs is [String: MixpanelType] { + let rhs = rhs as! [String: MixpanelType] + + if self.keys.count != rhs.keys.count { + return false + } + + if !isValidNestedTypeAndValue() { + return false + } + + for (key, val) in self as! [String: MixpanelType] { + guard let rVal = rhs[key] else { + return false + } + + if !val.equals(rhs: rVal) { + return false + } + } + return true + } + return false + } } func assertPropertyTypes(_ properties: Properties?) { - if let properties = properties { - for (_, v) in properties { - MPAssert(v.isValidNestedTypeAndValue(), - "Property values must be of valid type (MixpanelType) and valid value. Got \(type(of: v)) and Value \(v)") - } + if let properties = properties { + for (_, v) in properties { + MPAssert( + v.isValidNestedTypeAndValue(), + "Property values must be of valid type (MixpanelType) and valid value. Got \(type(of: v)) and Value \(v)" + ) } + } } extension Dictionary { - func get(key: Key, defaultValue: T) -> T { - if let value = self[key] as? T { - return value - } - - return defaultValue + func get(key: Key, defaultValue: T) -> T { + if let value = self[key] as? T { + return value } + + return defaultValue + } } diff --git a/Sources/Network.swift b/Sources/Network.swift index 9ae2ebd1..9bf8f87c 100644 --- a/Sources/Network.swift +++ b/Sources/Network.swift @@ -9,128 +9,136 @@ import Foundation struct BasePath { - static let DefaultMixpanelAPI = "https://api.mixpanel.com" - - static func buildURL(base: String, path: String, queryItems: [URLQueryItem]?) -> URL? { - guard let url = URL(string: base) else { - return nil - } - guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { - return nil - } - components.path += path - components.queryItems = queryItems - // adding workaround to replece + for %2B as it's not done by default within URLComponents - components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B") - return components.url + static let DefaultMixpanelAPI = "https://api.mixpanel.com" + + static func buildURL(base: String, path: String, queryItems: [URLQueryItem]?) -> URL? { + guard let url = URL(string: base) else { + return nil } + guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { + return nil + } + components.path += path + components.queryItems = queryItems + // adding workaround to replece + for %2B as it's not done by default within URLComponents + components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences( + of: "+", with: "%2B") + return components.url + } } enum RequestMethod: String { - case get - case post + case get + case post } struct Resource { - let path: String - let method: RequestMethod - let requestBody: Data? - let queryItems: [URLQueryItem]? - let headers: [String: String] - let parse: (Data) -> A? + let path: String + let method: RequestMethod + let requestBody: Data? + let queryItems: [URLQueryItem]? + let headers: [String: String] + let parse: (Data) -> A? } enum Reason { - case parseError - case noData - case notOKStatusCode(statusCode: Int) - case other(Error) + case parseError + case noData + case notOKStatusCode(statusCode: Int) + case other(Error) } public struct ServerProxyResource { - public init(queryItems: [URLQueryItem]? = nil, headers: [String : String]) { - self.queryItems = queryItems - self.headers = headers - } - - public let queryItems: [URLQueryItem]? - public let headers: [String: String] + public init(queryItems: [URLQueryItem]? = nil, headers: [String: String]) { + self.queryItems = queryItems + self.headers = headers + } + + public let queryItems: [URLQueryItem]? + public let headers: [String: String] } class Network { - - var serverURL: String - - required init(serverURL: String) { - self.serverURL = serverURL - } - - class func apiRequest(base: String, - resource: Resource, - failure: @escaping (Reason, Data?, URLResponse?) -> Void, - success: @escaping (A, URLResponse?) -> Void) { - guard let request = buildURLRequest(base, resource: resource) else { - return - } - - URLSession.shared.dataTask(with: request) { (data, response, error) -> Void in - guard let httpResponse = response as? HTTPURLResponse else { - - if let hasError = error { - failure(.other(hasError), data, response) - } else { - failure(.noData, data, response) - } - return - } - guard httpResponse.statusCode == 200 else { - failure(.notOKStatusCode(statusCode: httpResponse.statusCode), data, response) - return - } - guard let responseData = data else { - failure(.noData, data, response) - return - } - guard let result = resource.parse(responseData) else { - failure(.parseError, data, response) - return - } - - success(result, response) - }.resume() - } - - private class func buildURLRequest(_ base: String, resource: Resource) -> URLRequest? { - guard let url = BasePath.buildURL(base: base, - path: resource.path, - queryItems: resource.queryItems) else { - return nil - } - - MixpanelLogger.debug(message: "Fetching URL") - MixpanelLogger.debug(message: url.absoluteURL) - var request = URLRequest(url: url) - request.httpMethod = resource.method.rawValue - request.httpBody = resource.requestBody - - for (k, v) in resource.headers { - request.setValue(v, forHTTPHeaderField: k) - } - return request as URLRequest - } - - class func buildResource(path: String, - method: RequestMethod, - requestBody: Data? = nil, - queryItems: [URLQueryItem]? = nil, - headers: [String: String], - parse: @escaping (Data) -> A?) -> Resource { - return Resource(path: path, - method: method, - requestBody: requestBody, - queryItems: queryItems, - headers: headers, - parse: parse) - } -} + var serverURL: String + + required init(serverURL: String) { + self.serverURL = serverURL + } + + class func apiRequest( + base: String, + resource: Resource, + failure: @escaping (Reason, Data?, URLResponse?) -> Void, + success: @escaping (A, URLResponse?) -> Void + ) { + guard let request = buildURLRequest(base, resource: resource) else { + return + } + + URLSession.shared.dataTask(with: request) { (data, response, error) -> Void in + guard let httpResponse = response as? HTTPURLResponse else { + + if let hasError = error { + failure(.other(hasError), data, response) + } else { + failure(.noData, data, response) + } + return + } + guard httpResponse.statusCode == 200 else { + failure(.notOKStatusCode(statusCode: httpResponse.statusCode), data, response) + return + } + guard let responseData = data else { + failure(.noData, data, response) + return + } + guard let result = resource.parse(responseData) else { + failure(.parseError, data, response) + return + } + + success(result, response) + }.resume() + } + + private class func buildURLRequest(_ base: String, resource: Resource) -> URLRequest? { + guard + let url = BasePath.buildURL( + base: base, + path: resource.path, + queryItems: resource.queryItems) + else { + return nil + } + + MixpanelLogger.debug(message: "Fetching URL") + MixpanelLogger.debug(message: url.absoluteURL) + var request = URLRequest(url: url) + request.httpMethod = resource.method.rawValue + request.httpBody = resource.requestBody + + for (k, v) in resource.headers { + request.setValue(v, forHTTPHeaderField: k) + } + return request as URLRequest + } + + class func buildResource( + path: String, + method: RequestMethod, + requestBody: Data? = nil, + queryItems: [URLQueryItem]? = nil, + headers: [String: String], + parse: @escaping (Data) -> A? + ) -> Resource { + return Resource( + path: path, + method: method, + requestBody: requestBody, + queryItems: queryItems, + headers: headers, + parse: parse) + } +} diff --git a/Sources/People.swift b/Sources/People.swift index 6eecdabc..6bdf9cdb 100644 --- a/Sources/People.swift +++ b/Sources/People.swift @@ -12,99 +12,103 @@ import Foundation /// the main Mixpanel instance. open class People { - /// controls the $ignore_time property in any subsequent MixpanelPeople operation. - /// If the $ignore_time property is present and true in your request, - /// Mixpanel will not automatically update the "Last Seen" property of the profile. - /// Otherwise, Mixpanel will add a "Last Seen" property associated with the - /// current time for all $set, $append, and $add operations - open var ignoreTime = false + /// controls the $ignore_time property in any subsequent MixpanelPeople operation. + /// If the $ignore_time property is present and true in your request, + /// Mixpanel will not automatically update the "Last Seen" property of the profile. + /// Otherwise, Mixpanel will add a "Last Seen" property associated with the + /// current time for all $set, $append, and $add operations + open var ignoreTime = false - let apiToken: String - let serialQueue: DispatchQueue - let lock: ReadWriteLock - var distinctId: String? - weak var delegate: FlushDelegate? - let metadata: SessionMetadata - let mixpanelPersistence: MixpanelPersistence - weak var mixpanelInstance: MixpanelInstance? - - init(apiToken: String, - serialQueue: DispatchQueue, - lock: ReadWriteLock, - metadata: SessionMetadata, - mixpanelPersistence: MixpanelPersistence) { - self.apiToken = apiToken - self.serialQueue = serialQueue - self.lock = lock - self.metadata = metadata - self.mixpanelPersistence = mixpanelPersistence + let apiToken: String + let serialQueue: DispatchQueue + let lock: ReadWriteLock + var distinctId: String? + weak var delegate: FlushDelegate? + let metadata: SessionMetadata + let mixpanelPersistence: MixpanelPersistence + weak var mixpanelInstance: MixpanelInstance? + + init( + apiToken: String, + serialQueue: DispatchQueue, + lock: ReadWriteLock, + metadata: SessionMetadata, + mixpanelPersistence: MixpanelPersistence + ) { + self.apiToken = apiToken + self.serialQueue = serialQueue + self.lock = lock + self.metadata = metadata + self.mixpanelPersistence = mixpanelPersistence + } + + func addPeopleRecordToQueueWithAction(_ action: String, properties: InternalProperties) { + if mixpanelInstance?.hasOptedOutTracking() ?? false { + return + } + let epochMilliseconds = round(Date().timeIntervalSince1970 * 1000) + let ignoreTimeCopy = ignoreTime + + serialQueue.async { [weak self, action, properties] in + guard let self = self else { return } + + var r = InternalProperties() + var p = InternalProperties() + r["$token"] = self.apiToken + r["$time"] = epochMilliseconds + if ignoreTimeCopy { + r["$ignore_time"] = ignoreTimeCopy ? 1 : 0 + } + if action == "$unset" { + // $unset takes an array of property names which is supplied to this method + // in the properties parameter under the key "$properties" + r[action] = properties["$properties"] + } else { + if action == "$set" || action == "$set_once" { + AutomaticProperties.automaticPropertiesLock.read { + p += AutomaticProperties.peopleProperties + } + } + p += properties + r[action] = p + } + self.metadata.toDict(isEvent: false).forEach { (k, v) in r[k] = v } + + if let anonymousId = self.mixpanelInstance?.anonymousId { + r["$device_id"] = anonymousId + } + + if let userId = self.mixpanelInstance?.userId { + r["$user_id"] = userId + } + + if let hadPersistedDistinctId = self.mixpanelInstance?.hadPersistedDistinctId { + r["$had_persisted_distinct_id"] = hadPersistedDistinctId + } + + if let distinctId = self.distinctId { + r["$distinct_id"] = distinctId + // identified + self.mixpanelPersistence.saveEntity( + r, type: .people, flag: !PersistenceConstant.unIdentifiedFlag) + } else { + self.mixpanelPersistence.saveEntity( + r, type: .people, flag: PersistenceConstant.unIdentifiedFlag) + } } - func addPeopleRecordToQueueWithAction(_ action: String, properties: InternalProperties) { - if mixpanelInstance?.hasOptedOutTracking() ?? false { - return - } - let epochMilliseconds = round(Date().timeIntervalSince1970 * 1000) - let ignoreTimeCopy = ignoreTime - - serialQueue.async { [weak self, action, properties] in - guard let self = self else { return } - - var r = InternalProperties() - var p = InternalProperties() - r["$token"] = self.apiToken - r["$time"] = epochMilliseconds - if ignoreTimeCopy { - r["$ignore_time"] = ignoreTimeCopy ? 1 : 0 - } - if action == "$unset" { - // $unset takes an array of property names which is supplied to this method - // in the properties parameter under the key "$properties" - r[action] = properties["$properties"] - } else { - if action == "$set" || action == "$set_once" { - AutomaticProperties.automaticPropertiesLock.read { - p += AutomaticProperties.peopleProperties - } - } - p += properties - r[action] = p - } - self.metadata.toDict(isEvent: false).forEach { (k, v) in r[k] = v } - - if let anonymousId = self.mixpanelInstance?.anonymousId { - r["$device_id"] = anonymousId - } - - if let userId = self.mixpanelInstance?.userId { - r["$user_id"] = userId - } - - if let hadPersistedDistinctId = self.mixpanelInstance?.hadPersistedDistinctId { - r["$had_persisted_distinct_id"] = hadPersistedDistinctId - } - - if let distinctId = self.distinctId { - r["$distinct_id"] = distinctId - // identified - self.mixpanelPersistence.saveEntity(r, type: .people, flag: !PersistenceConstant.unIdentifiedFlag) - } else { - self.mixpanelPersistence.saveEntity(r, type: .people, flag: PersistenceConstant.unIdentifiedFlag) - } - } - - if MixpanelInstance.isiOSAppExtension() { - delegate?.flush(performFullFlush: false, completion: nil) - } + if MixpanelInstance.isiOSAppExtension() { + delegate?.flush(performFullFlush: false, completion: nil) } + } - func merge(properties: InternalProperties) { - addPeopleRecordToQueueWithAction("$merge", properties: properties) - } + func merge(properties: InternalProperties) { + addPeopleRecordToQueueWithAction("$merge", properties: properties) + } - // MARK: - People + // MARK: - People - /** + /** Set properties on the current user in Mixpanel People. The properties will be set on the current user. @@ -119,12 +123,12 @@ open class People { - parameter properties: properties dictionary */ - open func set(properties: Properties) { - assertPropertyTypes(properties) - addPeopleRecordToQueueWithAction("$set", properties: properties) - } + open func set(properties: Properties) { + assertPropertyTypes(properties) + addPeopleRecordToQueueWithAction("$set", properties: properties) + } - /** + /** Convenience method for setting a single property in Mixpanel People. Property keys must be String objects and the supported value types need to conform to MixpanelType. @@ -133,11 +137,11 @@ open class People { - parameter property: property name - parameter to: property value */ - open func set(property: String, to: MixpanelType) { - set(properties: [property: to]) - } + open func set(property: String, to: MixpanelType) { + set(properties: [property: to]) + } - /** + /** Set properties on the current user in Mixpanel People, but doesn't overwrite if there is an existing value. @@ -148,12 +152,12 @@ open class People { - parameter properties: properties dictionary */ - open func setOnce(properties: Properties) { - assertPropertyTypes(properties) - addPeopleRecordToQueueWithAction("$set_once", properties: properties) - } + open func setOnce(properties: Properties) { + assertPropertyTypes(properties) + addPeopleRecordToQueueWithAction("$set_once", properties: properties) + } - /** + /** Remove a list of properties and their values from the current user's profile in Mixpanel People. @@ -162,11 +166,11 @@ open class People { - parameter properties: properties array */ - open func unset(properties: [String]) { - addPeopleRecordToQueueWithAction("$unset", properties: ["$properties": properties]) - } + open func unset(properties: [String]) { + addPeopleRecordToQueueWithAction("$unset", properties: ["$properties": properties]) + } - /** + /** Increment the given numeric properties by the given values. Property keys must be String names of numeric properties. A property is @@ -175,27 +179,28 @@ open class People { - parameter properties: properties array */ - open func increment(properties: Properties) { - let filtered = properties.values.filter { - !($0 is Int || $0 is UInt || $0 is Double || $0 is Float) } - if !filtered.isEmpty { - MPAssert(false, "increment property values should be numbers") - } - addPeopleRecordToQueueWithAction("$add", properties: properties) + open func increment(properties: Properties) { + let filtered = properties.values.filter { + !($0 is Int || $0 is UInt || $0 is Double || $0 is Float) } + if !filtered.isEmpty { + MPAssert(false, "increment property values should be numbers") + } + addPeopleRecordToQueueWithAction("$add", properties: properties) + } - /** + /** Convenience method for incrementing a single numeric property by the specified amount. - parameter property: property name - parameter by: amount to increment by */ - open func increment(property: String, by: Double) { - increment(properties: [property: by]) - } + open func increment(property: String, by: Double) { + increment(properties: [property: by]) + } - /** + /** Append values to list properties. Property keys must be String objects and the supported value types need to conform to MixpanelType. @@ -203,12 +208,12 @@ open class People { - parameter properties: mapping of list property names to values to append */ - open func append(properties: Properties) { - assertPropertyTypes(properties) - addPeopleRecordToQueueWithAction("$append", properties: properties) - } + open func append(properties: Properties) { + assertPropertyTypes(properties) + addPeopleRecordToQueueWithAction("$append", properties: properties) + } - /** + /** Removes list properties. Property keys must be String objects and the supported value types need to conform to MixpanelType. @@ -216,29 +221,29 @@ open class People { - parameter properties: mapping of list property names to values to remove */ - open func remove(properties: Properties) { - assertPropertyTypes(properties) - addPeopleRecordToQueueWithAction("$remove", properties: properties) - } + open func remove(properties: Properties) { + assertPropertyTypes(properties) + addPeopleRecordToQueueWithAction("$remove", properties: properties) + } - /** + /** Union list properties. Property values must be array objects. - parameter properties: mapping of list property names to lists to union */ - open func union(properties: Properties) { - let filtered = properties.values.filter { - !($0 is [MixpanelType]) - } - if !filtered.isEmpty { - MPAssert(false, "union property values should be an array") - } - addPeopleRecordToQueueWithAction("$union", properties: properties) + open func union(properties: Properties) { + let filtered = properties.values.filter { + !($0 is [MixpanelType]) } + if !filtered.isEmpty { + MPAssert(false, "union property values should be an array") + } + addPeopleRecordToQueueWithAction("$union", properties: properties) + } - /** + /** Track money spent by the current user for revenue analytics and associate properties with the charge. Properties is optional. @@ -249,25 +254,25 @@ open class People { - parameter amount: amount of revenue received - parameter properties: Optional. properties dictionary */ - open func trackCharge(amount: Double, properties: Properties? = nil) { - var transaction: InternalProperties = ["$amount": amount, "$time": Date()] - if let properties = properties { - transaction += properties - } - append(properties: ["$transactions": transaction]) + open func trackCharge(amount: Double, properties: Properties? = nil) { + var transaction: InternalProperties = ["$amount": amount, "$time": Date()] + if let properties = properties { + transaction += properties } + append(properties: ["$transactions": transaction]) + } - /** + /** Delete current user's revenue history. */ - open func clearCharges() { - set(properties: ["$transactions": [] as [Any]]) - } + open func clearCharges() { + set(properties: ["$transactions": [] as [Any]]) + } - /** + /** Delete current user's record from Mixpanel People. */ - open func deleteUser() { - addPeopleRecordToQueueWithAction("$delete", properties: [:]) - } + open func deleteUser() { + addPeopleRecordToQueueWithAction("$delete", properties: [:]) + } } diff --git a/Sources/PrintLogging.swift b/Sources/PrintLogging.swift index e464d72d..0c9817f0 100644 --- a/Sources/PrintLogging.swift +++ b/Sources/PrintLogging.swift @@ -10,15 +10,19 @@ import Foundation /// Simply formats and prints the object by calling `print` class PrintLogging: MixpanelLogging { - func addMessage(message: MixpanelLogMessage) { - print("[Mixpanel - \(message.file) - func \(message.function)] (\(message.level.rawValue)) - \(message.text)") - } + func addMessage(message: MixpanelLogMessage) { + print( + "[Mixpanel - \(message.file) - func \(message.function)] (\(message.level.rawValue)) - \(message.text)" + ) + } } /// Simply formats and prints the object by calling `debugPrint`, this makes things a bit easier if you /// need to print data that may be quoted for instance. class PrintDebugLogging: MixpanelLogging { - func addMessage(message: MixpanelLogMessage) { - debugPrint("[Mixpanel - \(message.file) - func \(message.function)] (\(message.level.rawValue)) - \(message.text)") - } + func addMessage(message: MixpanelLogMessage) { + debugPrint( + "[Mixpanel - \(message.file) - func \(message.function)] (\(message.level.rawValue)) - \(message.text)" + ) + } } diff --git a/Sources/ReadWriteLock.swift b/Sources/ReadWriteLock.swift index 95f65b3c..b7b23d37 100644 --- a/Sources/ReadWriteLock.swift +++ b/Sources/ReadWriteLock.swift @@ -8,20 +8,23 @@ import Foundation class ReadWriteLock { - private let concurrentQueue: DispatchQueue + private let concurrentQueue: DispatchQueue - init(label: String) { - concurrentQueue = DispatchQueue(label: label, qos: .utility, attributes: .concurrent, autoreleaseFrequency: .workItem) - } + init(label: String) { + concurrentQueue = DispatchQueue( + label: label, qos: .utility, attributes: .concurrent, autoreleaseFrequency: .workItem) + } - func read(closure: () -> Void) { - concurrentQueue.sync { - closure() - } - } - func write(closure: () -> T) -> T { - concurrentQueue.sync(flags: .barrier, execute: { - closure() - }) + func read(closure: () -> Void) { + concurrentQueue.sync { + closure() } + } + func write(closure: () -> T) -> T { + concurrentQueue.sync( + flags: .barrier, + execute: { + closure() + }) + } } diff --git a/Sources/SessionMetadata.swift b/Sources/SessionMetadata.swift index c71d3e73..02f27454 100644 --- a/Sources/SessionMetadata.swift +++ b/Sources/SessionMetadata.swift @@ -9,39 +9,43 @@ import Foundation class SessionMetadata { - var eventsCounter: UInt64 = 0 - var peopleCounter: UInt64 = 0 - var sessionID: String = String.randomId() - var sessionStartEpoch: UInt64 = 0 - var trackingQueue: DispatchQueue - - init(trackingQueue: DispatchQueue) { - self.trackingQueue = trackingQueue - } - - func applicationWillEnterForeground() { - trackingQueue.async { [weak self] in - guard let self = self else { return } - - self.eventsCounter = 0 - self.peopleCounter = 0 - self.sessionID = String.randomId() - self.sessionStartEpoch = UInt64(Date().timeIntervalSince1970) - } - } - - func toDict(isEvent: Bool = true) -> InternalProperties { - let dict: [String: Any] = ["$mp_metadata": ["$mp_event_id": String.randomId(), - "$mp_session_id": sessionID, - "$mp_session_seq_id": (isEvent ? eventsCounter : peopleCounter), - "$mp_session_start_sec": sessionStartEpoch] as [String : Any]] - isEvent ? (eventsCounter += 1) : (peopleCounter += 1) - return dict + var eventsCounter: UInt64 = 0 + var peopleCounter: UInt64 = 0 + var sessionID: String = String.randomId() + var sessionStartEpoch: UInt64 = 0 + var trackingQueue: DispatchQueue + + init(trackingQueue: DispatchQueue) { + self.trackingQueue = trackingQueue + } + + func applicationWillEnterForeground() { + trackingQueue.async { [weak self] in + guard let self = self else { return } + + self.eventsCounter = 0 + self.peopleCounter = 0 + self.sessionID = String.randomId() + self.sessionStartEpoch = UInt64(Date().timeIntervalSince1970) } + } + + func toDict(isEvent: Bool = true) -> InternalProperties { + let dict: [String: Any] = [ + "$mp_metadata": [ + "$mp_event_id": String.randomId(), + "$mp_session_id": sessionID, + "$mp_session_seq_id": (isEvent ? eventsCounter : peopleCounter), + "$mp_session_start_sec": sessionStartEpoch, + ] as [String: Any] + ] + isEvent ? (eventsCounter += 1) : (peopleCounter += 1) + return dict + } } -private extension String { - static func randomId() -> String { - return String(format: "%08x%08x", arc4random(), arc4random()) - } +extension String { + fileprivate static func randomId() -> String { + return String(format: "%08x%08x", arc4random(), arc4random()) + } } diff --git a/Sources/Track.swift b/Sources/Track.swift index 58a172ac..046cfb8d 100644 --- a/Sources/Track.swift +++ b/Sources/Track.swift @@ -8,158 +8,173 @@ import Foundation -func += (left: inout [K: V], right: [K: V]) { - for (k, v) in right { - left.updateValue(v, forKey: k) - } +func += (left: inout [K: V], right: [K: V]) { + for (k, v) in right { + left.updateValue(v, forKey: k) + } } class Track { - let instanceName: String - let apiToken: String - let lock: ReadWriteLock - let metadata: SessionMetadata - let mixpanelPersistence: MixpanelPersistence - weak var mixpanelInstance: MixpanelInstance? + let instanceName: String + let apiToken: String + let lock: ReadWriteLock + let metadata: SessionMetadata + let mixpanelPersistence: MixpanelPersistence + weak var mixpanelInstance: MixpanelInstance? - init(apiToken: String, instanceName: String, lock: ReadWriteLock, metadata: SessionMetadata, - mixpanelPersistence: MixpanelPersistence) { - self.instanceName = instanceName - self.apiToken = apiToken - self.lock = lock - self.metadata = metadata - self.mixpanelPersistence = mixpanelPersistence + init( + apiToken: String, instanceName: String, lock: ReadWriteLock, metadata: SessionMetadata, + mixpanelPersistence: MixpanelPersistence + ) { + self.instanceName = instanceName + self.apiToken = apiToken + self.lock = lock + self.metadata = metadata + self.mixpanelPersistence = mixpanelPersistence + } + + func track( + event: String?, + properties: Properties? = nil, + timedEvents: InternalProperties, + superProperties: InternalProperties, + mixpanelIdentity: MixpanelIdentity, + epochInterval: Double + ) -> InternalProperties { + var ev = "mp_event" + if let event = event { + ev = event + } else { + MixpanelLogger.info( + message: "mixpanel track called with empty event parameter. using 'mp_event'") } - - func track(event: String?, - properties: Properties? = nil, - timedEvents: InternalProperties, - superProperties: InternalProperties, - mixpanelIdentity: MixpanelIdentity, - epochInterval: Double) -> InternalProperties { - var ev = "mp_event" - if let event = event { - ev = event - } else { - MixpanelLogger.info(message: "mixpanel track called with empty event parameter. using 'mp_event'") - } - if !(mixpanelInstance?.trackAutomaticEventsEnabled ?? false) && ev.hasPrefix("$ae_") { - return timedEvents - } - assertPropertyTypes(properties) - let epochMilliseconds = round(epochInterval * 1000) - let eventStartTime = timedEvents[ev] as? Double - var p = InternalProperties() - AutomaticProperties.automaticPropertiesLock.read { - p += AutomaticProperties.properties - } - p["token"] = apiToken - p["time"] = epochMilliseconds - var shadowTimedEvents = timedEvents - if let eventStartTime = eventStartTime { - shadowTimedEvents.removeValue(forKey: ev) - p["$duration"] = Double(String(format: "%.3f", epochInterval - eventStartTime)) - } - p["distinct_id"] = mixpanelIdentity.distinctID - if mixpanelIdentity.anonymousId != nil { - p["$device_id"] = mixpanelIdentity.anonymousId - } - if mixpanelIdentity.userId != nil { - p["$user_id"] = mixpanelIdentity.userId - } - if mixpanelIdentity.hadPersistedDistinctId != nil { - p["$had_persisted_distinct_id"] = mixpanelIdentity.hadPersistedDistinctId - } - - p += superProperties - if let properties = properties { - p += properties - } - - var trackEvent: InternalProperties = ["event": ev, "properties": p] - metadata.toDict().forEach { (k, v) in trackEvent[k] = v } - - self.mixpanelPersistence.saveEntity(trackEvent, type: .events) - MixpanelPersistence.saveTimedEvents(timedEvents: shadowTimedEvents, instanceName: instanceName) - return shadowTimedEvents + if !(mixpanelInstance?.trackAutomaticEventsEnabled ?? false) && ev.hasPrefix("$ae_") { + return timedEvents + } + assertPropertyTypes(properties) + let epochMilliseconds = round(epochInterval * 1000) + let eventStartTime = timedEvents[ev] as? Double + var p = InternalProperties() + AutomaticProperties.automaticPropertiesLock.read { + p += AutomaticProperties.properties + } + p["token"] = apiToken + p["time"] = epochMilliseconds + var shadowTimedEvents = timedEvents + if let eventStartTime = eventStartTime { + shadowTimedEvents.removeValue(forKey: ev) + p["$duration"] = Double(String(format: "%.3f", epochInterval - eventStartTime)) + } + p["distinct_id"] = mixpanelIdentity.distinctID + if mixpanelIdentity.anonymousId != nil { + p["$device_id"] = mixpanelIdentity.anonymousId + } + if mixpanelIdentity.userId != nil { + p["$user_id"] = mixpanelIdentity.userId + } + if mixpanelIdentity.hadPersistedDistinctId != nil { + p["$had_persisted_distinct_id"] = mixpanelIdentity.hadPersistedDistinctId } - func registerSuperProperties(_ properties: Properties, - superProperties: InternalProperties) -> InternalProperties { - if mixpanelInstance?.hasOptedOutTracking() ?? false { - return superProperties - } - - var updatedSuperProperties = superProperties - assertPropertyTypes(properties) - updatedSuperProperties += properties - - return updatedSuperProperties + p += superProperties + if let properties = properties { + p += properties } - func registerSuperPropertiesOnce(_ properties: Properties, - superProperties: InternalProperties, - defaultValue: MixpanelType?) -> InternalProperties { - if mixpanelInstance?.hasOptedOutTracking() ?? false { - return superProperties - } + var trackEvent: InternalProperties = ["event": ev, "properties": p] + metadata.toDict().forEach { (k, v) in trackEvent[k] = v } - var updatedSuperProperties = superProperties - assertPropertyTypes(properties) - _ = properties.map { - let val = updatedSuperProperties[$0.key] - if val == nil || - (defaultValue != nil && (val as? NSObject == defaultValue as? NSObject)) { - updatedSuperProperties[$0.key] = $0.value - } - } - - return updatedSuperProperties + self.mixpanelPersistence.saveEntity(trackEvent, type: .events) + MixpanelPersistence.saveTimedEvents(timedEvents: shadowTimedEvents, instanceName: instanceName) + return shadowTimedEvents + } + + func registerSuperProperties( + _ properties: Properties, + superProperties: InternalProperties + ) -> InternalProperties { + if mixpanelInstance?.hasOptedOutTracking() ?? false { + return superProperties } - func unregisterSuperProperty(_ propertyName: String, - superProperties: InternalProperties) -> InternalProperties { - var updatedSuperProperties = superProperties - updatedSuperProperties.removeValue(forKey: propertyName) - return updatedSuperProperties - } - - func clearSuperProperties(_ superProperties: InternalProperties) -> InternalProperties { - var updatedSuperProperties = superProperties - updatedSuperProperties.removeAll() - return updatedSuperProperties - } - - func updateSuperProperty(_ update: (_ superProperties: inout InternalProperties) -> Void, superProperties: inout InternalProperties) { - update(&superProperties) + var updatedSuperProperties = superProperties + assertPropertyTypes(properties) + updatedSuperProperties += properties + + return updatedSuperProperties + } + + func registerSuperPropertiesOnce( + _ properties: Properties, + superProperties: InternalProperties, + defaultValue: MixpanelType? + ) -> InternalProperties { + if mixpanelInstance?.hasOptedOutTracking() ?? false { + return superProperties } - func time(event: String?, timedEvents: InternalProperties, startTime: Double) -> InternalProperties { - if mixpanelInstance?.hasOptedOutTracking() ?? false { - return timedEvents - } - var updatedTimedEvents = timedEvents - guard let event = event, !event.isEmpty else { - MixpanelLogger.error(message: "mixpanel cannot time an empty event") - return updatedTimedEvents - } - updatedTimedEvents[event] = startTime - return updatedTimedEvents + var updatedSuperProperties = superProperties + assertPropertyTypes(properties) + _ = properties.map { + let val = updatedSuperProperties[$0.key] + if val == nil || (defaultValue != nil && (val as? NSObject == defaultValue as? NSObject)) { + updatedSuperProperties[$0.key] = $0.value + } } - func clearTimedEvents(_ timedEvents: InternalProperties) -> InternalProperties { - var updatedTimedEvents = timedEvents - updatedTimedEvents.removeAll() - return updatedTimedEvents + return updatedSuperProperties + } + + func unregisterSuperProperty( + _ propertyName: String, + superProperties: InternalProperties + ) -> InternalProperties { + var updatedSuperProperties = superProperties + updatedSuperProperties.removeValue(forKey: propertyName) + return updatedSuperProperties + } + + func clearSuperProperties(_ superProperties: InternalProperties) -> InternalProperties { + var updatedSuperProperties = superProperties + updatedSuperProperties.removeAll() + return updatedSuperProperties + } + + func updateSuperProperty( + _ update: (_ superProperties: inout InternalProperties) -> Void, + superProperties: inout InternalProperties + ) { + update(&superProperties) + } + + func time(event: String?, timedEvents: InternalProperties, startTime: Double) + -> InternalProperties + { + if mixpanelInstance?.hasOptedOutTracking() ?? false { + return timedEvents } - - func clearTimedEvent(event: String?, timedEvents: InternalProperties) -> InternalProperties { - var updatedTimedEvents = timedEvents - guard let event = event, !event.isEmpty else { - MixpanelLogger.error(message: "mixpanel cannot clear an empty timed event") - return updatedTimedEvents - } - updatedTimedEvents.removeValue(forKey: event) - return updatedTimedEvents + var updatedTimedEvents = timedEvents + guard let event = event, !event.isEmpty else { + MixpanelLogger.error(message: "mixpanel cannot time an empty event") + return updatedTimedEvents } + updatedTimedEvents[event] = startTime + return updatedTimedEvents + } + + func clearTimedEvents(_ timedEvents: InternalProperties) -> InternalProperties { + var updatedTimedEvents = timedEvents + updatedTimedEvents.removeAll() + return updatedTimedEvents + } + + func clearTimedEvent(event: String?, timedEvents: InternalProperties) -> InternalProperties { + var updatedTimedEvents = timedEvents + guard let event = event, !event.isEmpty else { + MixpanelLogger.error(message: "mixpanel cannot clear an empty timed event") + return updatedTimedEvents + } + updatedTimedEvents.removeValue(forKey: event) + return updatedTimedEvents + } }