mirror of
https://github.com/mixpanel/mixpanel-swift
synced 2026-04-21 13:37:18 +00:00
swift-format (#672)
This commit is contained in:
parent
37b00b236e
commit
e8c8783d94
58 changed files with 10526 additions and 9626 deletions
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,34 +10,34 @@ import XCTest
|
|||
|
||||
class MixpanelDemoMacUITests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
|
||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||
continueAfterFailure = false
|
||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||
continueAfterFailure = false
|
||||
|
||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// UI tests must launch the application that they test.
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Use recording to get started writing UI tests.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
func testLaunchPerformance() throws {
|
||||
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
|
||||
// This measures how long it takes to launch your application.
|
||||
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
||||
XCUIApplication().launch()
|
||||
}
|
||||
}
|
||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// UI tests must launch the application that they test.
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Use recording to get started writing UI tests.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
func testLaunchPerformance() throws {
|
||||
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
|
||||
// This measures how long it takes to launch your application.
|
||||
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
||||
XCUIApplication().launch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,25 +10,25 @@ import XCTest
|
|||
|
||||
class MixpanelDemoTVUITests: XCTestCase {
|
||||
|
||||
override func setUp() {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
override func setUp() {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
|
||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||
continueAfterFailure = false
|
||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||
continueAfterFailure = false
|
||||
|
||||
// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
|
||||
XCUIApplication().launch()
|
||||
// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
|
||||
XCUIApplication().launch()
|
||||
|
||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||
}
|
||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
override func tearDown() {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() {
|
||||
// Use recording to get started writing UI tests.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
func testExample() {
|
||||
// Use recording to get started writing UI tests.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 you’re done.
|
||||
backgroundTask.setTaskCompletedWithSnapshot(false)
|
||||
case let snapshotTask as WKSnapshotRefreshBackgroundTask:
|
||||
// Snapshot tasks have a unique completion call, make sure to set your expiration date
|
||||
snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
|
||||
case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
|
||||
// Be sure to complete the connectivity task once you’re done.
|
||||
connectivityTask.setTaskCompletedWithSnapshot(false)
|
||||
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
|
||||
// Be sure to complete the URL session task once you’re done.
|
||||
urlSessionTask.setTaskCompletedWithSnapshot(false)
|
||||
case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask:
|
||||
// Be sure to complete the relevant-shortcut task once you're done.
|
||||
relevantShortcutTask.setTaskCompletedWithSnapshot(false)
|
||||
case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask:
|
||||
// Be sure to complete the intent-did-run task once you're done.
|
||||
intentDidRunTask.setTaskCompletedWithSnapshot(false)
|
||||
default:
|
||||
// make sure to complete unhandled task types
|
||||
task.setTaskCompletedWithSnapshot(false)
|
||||
}
|
||||
}
|
||||
func handle(_ backgroundTasks: Set<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 you’re done.
|
||||
backgroundTask.setTaskCompletedWithSnapshot(false)
|
||||
case let snapshotTask as WKSnapshotRefreshBackgroundTask:
|
||||
// Snapshot tasks have a unique completion call, make sure to set your expiration date
|
||||
snapshotTask.setTaskCompleted(
|
||||
restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil
|
||||
)
|
||||
case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
|
||||
// Be sure to complete the connectivity task once you’re done.
|
||||
connectivityTask.setTaskCompletedWithSnapshot(false)
|
||||
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
|
||||
// Be sure to complete the URL session task once you’re done.
|
||||
urlSessionTask.setTaskCompletedWithSnapshot(false)
|
||||
case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask:
|
||||
// Be sure to complete the relevant-shortcut task once you're done.
|
||||
relevantShortcutTask.setTaskCompletedWithSnapshot(false)
|
||||
case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask:
|
||||
// Be sure to complete the intent-did-run task once you're done.
|
||||
intentDidRunTask.setTaskCompletedWithSnapshot(false)
|
||||
default:
|
||||
// make sure to complete unhandled task types
|
||||
task.setTaskCompletedWithSnapshot(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) ?? "")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: [:])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue