swift-format (#672)

This commit is contained in:
Jared McFarland 2025-05-29 17:45:08 -07:00 committed by GitHub
parent 37b00b236e
commit e8c8783d94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 10526 additions and 9626 deletions

View file

@ -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)
}
}

View file

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

View file

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

View file

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

View file

@ -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)
}
}
}

View file

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

View file

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

View file

@ -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<String>)
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<String>)
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
}
}
}
}
}

View file

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

View file

@ -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.
}
}
}
}

View file

@ -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,
]
}
}

File diff suppressed because it is too large Load diff

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

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

View file

@ -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 its 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 its 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()
}
}
}
}

View file

@ -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")
}
}

View file

@ -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)
}
}

View file

@ -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.
}
}
}

View file

@ -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 its 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 its 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.
}
}

View file

@ -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<Double?> = [nil, 1.0, 2.0]
let arrayProp: Array<Double> = [0.0, 1.0, 2.0]
let dictProp: Dictionary<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: Dictionary<String, Dictionary<String, String?>> = ["nested": dictProp]
let nestedArraryProp: Array<Array<Double?>> = [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<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 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<Dictionary<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])
}
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])
}
}

View file

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

View file

@ -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)
}
}

View file

@ -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,
]
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

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

View file

@ -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<WKRefreshBackgroundTask>) {
// 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 youre 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 youre done.
connectivityTask.setTaskCompletedWithSnapshot(false)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once youre 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<WKRefreshBackgroundTask>) {
// 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 youre 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 youre done.
connectivityTask.setTaskCompletedWithSnapshot(false)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once youre 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)
}
}
}
}

View file

@ -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()
}
}

View file

@ -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")
]
)
]
)

View file

@ -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<String> = []
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<String> = []
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

View file

@ -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<CChar>.size
modelCode = withUnsafePointer(to: &systemInfo.machine) {
$0.withMemoryRebound(to: CChar.self, capacity: size) {
String(cString: UnsafePointer<CChar>($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<CChar>.size
modelCode = withUnsafePointer(to: &systemInfo.machine) {
$0.withMemoryRebound(to: CChar.self, capacity: size) {
String(cString: UnsafePointer<CChar>($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"
}
}

View file

@ -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)

View file

@ -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<Bytef>(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<z_stream>.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<Bytef>(
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<z_stream>.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
}
}

View file

@ -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<ReturnType>(_ f: () throws -> ReturnType?) -> ReturnType? {
do {
return try f()
} catch let error {
logError(error)
return nil
}
class func wrap<ReturnType>(_ 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)")
}
}

View file

@ -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<String> = 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<String> = 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
}
}
}

View file

@ -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)
}
}
}

View file

@ -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..<batchSize
let batch = Array(mutableQueue[range])
let ids: [Int32] = batch.map { entity in
(entity["id"] as? Int32) ?? 0
}
// Log data payload sent
MixpanelLogger.debug(message: "Sending batch of data")
MixpanelLogger.debug(message: batch as Any)
let requestData = JSONHandler.encodeAPIData(batch)
if let requestData = requestData {
#if os(iOS)
if !MixpanelInstance.isiOSAppExtension() {
delegate?.updateNetworkActivityIndicator(true)
}
#endif // os(iOS)
let success = flushRequest.sendRequest(requestData,
type: type,
useIP: useIPAddressForGeoLocation,
headers: headers,
queryItems: queryItems, useGzipCompression: useGzipCompression)
#if os(iOS)
if !MixpanelInstance.isiOSAppExtension() {
delegate?.updateNetworkActivityIndicator(false)
}
#endif // os(iOS)
if success {
// remove
delegate?.flushSuccess(type: type, ids: ids)
mutableQueue = self.removeProcessedBatch(batchSize: batchSize,
queue: mutableQueue,
type: type)
} else {
break
}
}
}
func flushQueue(
_ queue: Queue, type: FlushType, headers: [String: String], queryItems: [URLQueryItem]
) {
if flushRequest.requestNotAllowed() {
return
}
func removeProcessedBatch(batchSize: Int, queue: Queue, type: FlushType) -> Queue {
var shadowQueue = queue
let range = 0..<batchSize
if let lastIndex = range.last, shadowQueue.count - 1 > 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..<batchSize
let batch = Array(mutableQueue[range])
let ids: [Int32] = batch.map { entity in
(entity["id"] as? Int32) ?? 0
}
// Log data payload sent
MixpanelLogger.debug(message: "Sending batch of data")
MixpanelLogger.debug(message: batch as Any)
let requestData = JSONHandler.encodeAPIData(batch)
if let requestData = requestData {
#if os(iOS)
if !MixpanelInstance.isiOSAppExtension() {
delegate?.updateNetworkActivityIndicator(true)
}
#endif // os(iOS)
let success = flushRequest.sendRequest(
requestData,
type: type,
useIP: useIPAddressForGeoLocation,
headers: headers,
queryItems: queryItems, useGzipCompression: useGzipCompression)
#if os(iOS)
if !MixpanelInstance.isiOSAppExtension() {
delegate?.updateNetworkActivityIndicator(false)
}
#endif // os(iOS)
if success {
// remove
delegate?.flushSuccess(type: type, ids: ids)
mutableQueue = self.removeProcessedBatch(
batchSize: batchSize,
queue: mutableQueue,
type: type)
} else {
shadowQueue.removeAll()
break
}
return shadowQueue
}
}
}
// MARK: - Lifecycle
func applicationDidBecomeActive() {
startFlushTimer()
func removeProcessedBatch(batchSize: Int, queue: Queue, type: FlushType) -> Queue {
var shadowQueue = queue
let range = 0..<batchSize
if let lastIndex = range.last, shadowQueue.count - 1 > lastIndex {
shadowQueue.removeSubrange(range)
} else {
shadowQueue.removeAll()
}
return shadowQueue
}
func applicationWillResignActive() {
stopFlushTimer()
}
// MARK: - Lifecycle
func applicationDidBecomeActive() {
startFlushTimer()
}
func applicationWillResignActive() {
stopFlushTimer()
}
}

View file

@ -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<Int>,
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<Int>,
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
}
}

View file

@ -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)
}
}

View file

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

View file

@ -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()
}
}
}

View file

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

File diff suppressed because it is too large Load diff

View file

@ -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<MixpanelLogLevel>()
private static let readWriteLock: ReadWriteLock = ReadWriteLock(label: "loggerLock")
private static var loggers = [MixpanelLogging]()
private static var enabledLevels = Set<MixpanelLogLevel>()
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<MixpanelLogLevel>()
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<MixpanelLogLevel>()
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<MixpanelLogLevel>()
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<MixpanelLogLevel>()
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<MixpanelLogLevel>()
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<MixpanelLogLevel>()
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<MixpanelLogLevel>()
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<MixpanelLogLevel>()
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) }
}
}
}

View file

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

View file

@ -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) ?? "")
}
}

View file

@ -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<T>(key: Key, defaultValue: T) -> T {
if let value = self[key] as? T {
return value
}
return defaultValue
func get<T>(key: Key, defaultValue: T) -> T {
if let value = self[key] as? T {
return value
}
return defaultValue
}
}

View file

@ -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<A> {
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<A>(base: String,
resource: Resource<A>,
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<A>(_ base: String, resource: Resource<A>) -> 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<A>(path: String,
method: RequestMethod,
requestBody: Data? = nil,
queryItems: [URLQueryItem]? = nil,
headers: [String: String],
parse: @escaping (Data) -> A?) -> Resource<A> {
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<A>(
base: String,
resource: Resource<A>,
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<A>(_ base: String, resource: Resource<A>) -> 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<A>(
path: String,
method: RequestMethod,
requestBody: Data? = nil,
queryItems: [URLQueryItem]? = nil,
headers: [String: String],
parse: @escaping (Data) -> A?
) -> Resource<A> {
return Resource(
path: path,
method: method,
requestBody: requestBody,
queryItems: queryItems,
headers: headers,
parse: parse)
}
}

View file

@ -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: [:])
}
}

View file

@ -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)"
)
}
}

View file

@ -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<T>(closure: () -> T) -> T {
concurrentQueue.sync(flags: .barrier, execute: {
closure()
})
func read(closure: () -> Void) {
concurrentQueue.sync {
closure()
}
}
func write<T>(closure: () -> T) -> T {
concurrentQueue.sync(
flags: .barrier,
execute: {
closure()
})
}
}

View file

@ -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())
}
}

View file

@ -8,158 +8,173 @@
import Foundation
func += <K, V> (left: inout [K: V], right: [K: V]) {
for (k, v) in right {
left.updateValue(v, forKey: k)
}
func += <K, V>(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
}
}