mirror of
https://github.com/mixpanel/mixpanel-swift
synced 2026-04-21 13:37:18 +00:00
1376 lines
54 KiB
Swift
1376 lines
54 KiB
Swift
//
|
|
// MixpanelInstance.swift
|
|
// Mixpanel
|
|
//
|
|
// Created by Yarden Eitan on 6/2/16.
|
|
// Copyright © 2016 Mixpanel. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
#if !os(OSX)
|
|
import UIKit
|
|
#else
|
|
import Cocoa
|
|
#endif // os(OSX)
|
|
#if os(iOS)
|
|
import SystemConfiguration
|
|
#endif
|
|
|
|
#if os(iOS)
|
|
import CoreTelephony
|
|
#endif // os(iOS)
|
|
|
|
/**
|
|
* Delegate protocol for controlling the Mixpanel API's network behavior.
|
|
*/
|
|
public protocol MixpanelDelegate: AnyObject {
|
|
/**
|
|
Asks the delegate if data should be uploaded to the server.
|
|
|
|
- parameter mixpanel: The mixpanel instance
|
|
|
|
- returns: return true to upload now or false to defer until later
|
|
*/
|
|
func mixpanelWillFlush(_ mixpanel: MixpanelInstance) -> Bool
|
|
}
|
|
|
|
public typealias Properties = [String: MixpanelType]
|
|
typealias InternalProperties = [String: Any]
|
|
typealias Queue = [InternalProperties]
|
|
|
|
protocol AppLifecycle {
|
|
func applicationDidBecomeActive()
|
|
func applicationWillResignActive()
|
|
}
|
|
|
|
/// The class that represents the Mixpanel Instance
|
|
open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDelegate {
|
|
/// apiToken string that identifies the project to track data to
|
|
open var apiToken = ""
|
|
|
|
/// The a MixpanelDelegate object that gives control over Mixpanel network activity.
|
|
open weak var delegate: MixpanelDelegate?
|
|
|
|
/// distinctId string that uniquely identifies the current user.
|
|
open var distinctId = ""
|
|
|
|
/// anonymousId string that uniquely identifies the device.
|
|
open var anonymousId: String?
|
|
|
|
/// userId string that identify is called with.
|
|
open var userId: String?
|
|
|
|
/// hadPersistedDistinctId is a boolean value which specifies that the stored distinct_id
|
|
/// already exists in persistence
|
|
open var hadPersistedDistinctId: Bool?
|
|
|
|
/// alias string that uniquely identifies the current user.
|
|
open var alias: String?
|
|
|
|
/// Accessor to the Mixpanel People API object.
|
|
open var people: People!
|
|
|
|
let mixpanelPersistence: MixpanelPersistence
|
|
|
|
/// Accessor to the Mixpanel People API object.
|
|
var groups: [String: Group] = [:]
|
|
|
|
/// Controls whether to show spinning network activity indicator when flushing
|
|
/// data to the Mixpanel servers. Defaults to true.
|
|
open var showNetworkActivityIndicator = true
|
|
|
|
/// This allows enabling or disabling collecting common mobile events
|
|
/// If this is not set, it will query the Autotrack settings from the Mixpanel server
|
|
open var trackAutomaticEventsEnabled: Bool? {
|
|
didSet {
|
|
MixpanelPersistence.saveAutomacticEventsEnabledFlag(value: trackAutomaticEventsEnabled ?? false,
|
|
fromDecide: false,
|
|
apiToken: apiToken)
|
|
}
|
|
}
|
|
|
|
/// Flush timer's interval.
|
|
/// Setting a flush interval of 0 will turn off the flush timer and you need to call the flush() API manually
|
|
/// to upload queued data to the Mixpanel server.
|
|
open var flushInterval: Double {
|
|
get {
|
|
return flushInstance.flushInterval
|
|
}
|
|
set {
|
|
flushInstance.flushInterval = newValue
|
|
}
|
|
}
|
|
|
|
/// Control whether the library should flush data to Mixpanel when the app
|
|
/// enters the background. Defaults to true.
|
|
open var flushOnBackground: Bool {
|
|
get {
|
|
return flushInstance.flushOnBackground
|
|
}
|
|
set {
|
|
flushInstance.flushOnBackground = newValue
|
|
}
|
|
}
|
|
|
|
/// Controls whether to automatically send the client IP Address as part of
|
|
/// event tracking. With an IP address, the Mixpanel Dashboard will show you the users' city.
|
|
/// Defaults to true.
|
|
open var useIPAddressForGeoLocation: Bool {
|
|
get {
|
|
return flushInstance.useIPAddressForGeoLocation
|
|
}
|
|
set {
|
|
flushInstance.useIPAddressForGeoLocation = newValue
|
|
}
|
|
}
|
|
|
|
/// The base URL used for Mixpanel API requests.
|
|
/// Useful if you need to proxy Mixpanel requests. Defaults to
|
|
/// https://api.mixpanel.com.
|
|
open var serverURL = BasePath.DefaultMixpanelAPI {
|
|
didSet {
|
|
BasePath.namedBasePaths[name] = serverURL
|
|
}
|
|
}
|
|
|
|
open var debugDescription: String {
|
|
return "Mixpanel(\n"
|
|
+ " Token: \(apiToken),\n"
|
|
+ " Distinct Id: \(distinctId)\n"
|
|
+ ")"
|
|
}
|
|
|
|
/// This allows enabling or disabling of all Mixpanel logs at run time.
|
|
/// - Note: All logging is disabled by default. Usually, this is only required
|
|
/// if you are running in to issues with the SDK and you need support.
|
|
open var loggingEnabled: Bool = false {
|
|
didSet {
|
|
if loggingEnabled {
|
|
Logger.enableLevel(.debug)
|
|
Logger.enableLevel(.info)
|
|
Logger.enableLevel(.warning)
|
|
Logger.enableLevel(.error)
|
|
|
|
Logger.info(message: "Logging Enabled")
|
|
} else {
|
|
Logger.info(message: "Logging Disabled")
|
|
|
|
Logger.disableLevel(.debug)
|
|
Logger.disableLevel(.info)
|
|
Logger.disableLevel(.warning)
|
|
Logger.disableLevel(.error)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A unique identifier for this MixpanelInstance
|
|
public let name: String
|
|
|
|
#if DECIDE
|
|
/// The minimum session duration (ms) that is tracked in automatic events.
|
|
/// The default value is 10000 (10 seconds).
|
|
open var minimumSessionDuration: UInt64 {
|
|
get {
|
|
return automaticEvents.minimumSessionDuration
|
|
}
|
|
set {
|
|
automaticEvents.minimumSessionDuration = newValue
|
|
}
|
|
}
|
|
|
|
/// The maximum session duration (ms) that is tracked in automatic events.
|
|
/// The default value is UINT64_MAX (no maximum session duration).
|
|
open var maximumSessionDuration: UInt64 {
|
|
get {
|
|
return automaticEvents.maximumSessionDuration
|
|
}
|
|
set {
|
|
automaticEvents.maximumSessionDuration = newValue
|
|
}
|
|
}
|
|
#endif // DECIDE
|
|
|
|
var superProperties = InternalProperties()
|
|
var trackingQueue: DispatchQueue!
|
|
var optOutStatus: Bool?
|
|
var timedEvents = InternalProperties()
|
|
let readWriteLock: ReadWriteLock
|
|
#if os(iOS) && !targetEnvironment(macCatalyst)
|
|
static let reachability = SCNetworkReachabilityCreateWithName(nil, "api.mixpanel.com")
|
|
static let telephonyInfo = CTTelephonyNetworkInfo()
|
|
#endif
|
|
#if !os(OSX) && !os(watchOS)
|
|
var taskId = UIBackgroundTaskIdentifier.invalid
|
|
#endif // os(OSX)
|
|
let sessionMetadata: SessionMetadata
|
|
let flushInstance: Flush
|
|
let trackInstance: Track
|
|
#if DECIDE
|
|
let decideInstance: Decide
|
|
let automaticEvents = AutomaticEvents()
|
|
let connectIntegrations = ConnectIntegrations()
|
|
#elseif TV_AUTO_EVENTS
|
|
let automaticEvents = AutomaticEvents()
|
|
#endif // DECIDE
|
|
|
|
#if !os(OSX) && !os(watchOS)
|
|
init(apiToken: String?, flushInterval: Double, name: String, optOutTrackingByDefault: Bool = false) {
|
|
if let apiToken = apiToken, !apiToken.isEmpty {
|
|
self.apiToken = apiToken
|
|
}
|
|
mixpanelPersistence = MixpanelPersistence.init(token: self.apiToken)
|
|
mixpanelPersistence.migrate()
|
|
|
|
self.name = name
|
|
readWriteLock = ReadWriteLock(label: "com.mixpanel.globallock")
|
|
flushInstance = Flush(basePathIdentifier: name)
|
|
#if DECIDE
|
|
decideInstance = Decide(basePathIdentifier: name, lock: readWriteLock, mixpanelPersistence: mixpanelPersistence)
|
|
#endif // DECIDE
|
|
let label = "com.mixpanel.\(self.apiToken)"
|
|
trackingQueue = DispatchQueue(label: "\(label).tracking)", qos: .utility)
|
|
sessionMetadata = SessionMetadata(trackingQueue: trackingQueue)
|
|
trackInstance = Track(apiToken: self.apiToken,
|
|
lock: self.readWriteLock,
|
|
metadata: sessionMetadata, mixpanelPersistence: mixpanelPersistence)
|
|
|
|
#if os(iOS) && !targetEnvironment(macCatalyst)
|
|
if let reachability = MixpanelInstance.reachability {
|
|
var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
|
|
func reachabilityCallback(reachability: SCNetworkReachability,
|
|
flags: SCNetworkReachabilityFlags,
|
|
unsafePointer: UnsafeMutableRawPointer?) {
|
|
let wifi = flags.contains(SCNetworkReachabilityFlags.reachable) && !flags.contains(SCNetworkReachabilityFlags.isWWAN)
|
|
AutomaticProperties.automaticPropertiesLock.write {
|
|
AutomaticProperties.properties["$wifi"] = wifi
|
|
}
|
|
Logger.info(message: "reachability changed, wifi=\(wifi)")
|
|
}
|
|
if SCNetworkReachabilitySetCallback(reachability, reachabilityCallback, &context) {
|
|
if !SCNetworkReachabilitySetDispatchQueue(reachability, trackingQueue) {
|
|
// cleanup callback if setting dispatch queue failed
|
|
SCNetworkReachabilitySetCallback(reachability, nil, nil)
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
flushInstance.delegate = self
|
|
distinctId = defaultDistinctId()
|
|
people = People(apiToken: self.apiToken,
|
|
serialQueue: trackingQueue,
|
|
lock: self.readWriteLock,
|
|
metadata: sessionMetadata, mixpanelPersistence: mixpanelPersistence)
|
|
people.delegate = self
|
|
flushInstance._flushInterval = flushInterval
|
|
setupListeners()
|
|
unarchive()
|
|
|
|
// check whether we should opt out by default
|
|
// note: we don't override opt out persistence here since opt-out default state is often
|
|
// used as an initial state while GDPR information is being collected
|
|
if optOutTrackingByDefault && (hasOptedOutTracking() || optOutStatus == nil) {
|
|
optOutTracking()
|
|
}
|
|
|
|
#if DECIDE || TV_AUTO_EVENTS
|
|
if !MixpanelInstance.isiOSAppExtension() {
|
|
automaticEvents.delegate = self
|
|
automaticEvents.initializeEvents()
|
|
}
|
|
#if DECIDE
|
|
connectIntegrations.mixpanel = self
|
|
#endif
|
|
#endif // DECIDE
|
|
}
|
|
#else
|
|
init(apiToken: String?, flushInterval: Double, name: String, optOutTrackingByDefault: Bool = false) {
|
|
if let apiToken = apiToken, !apiToken.isEmpty {
|
|
self.apiToken = apiToken
|
|
}
|
|
mixpanelPersistence = MixpanelPersistence.init(token: self.apiToken)
|
|
mixpanelPersistence.migrate()
|
|
|
|
self.name = name
|
|
self.readWriteLock = ReadWriteLock(label: "com.mixpanel.globallock")
|
|
flushInstance = Flush(basePathIdentifier: name)
|
|
let label = "com.mixpanel.\(self.apiToken)"
|
|
trackingQueue = DispatchQueue(label: label, qos: .utility)
|
|
sessionMetadata = SessionMetadata(trackingQueue: trackingQueue)
|
|
trackInstance = Track(apiToken: self.apiToken,
|
|
lock: self.readWriteLock,
|
|
metadata: sessionMetadata, mixpanelPersistence: mixpanelPersistence)
|
|
flushInstance.delegate = self
|
|
distinctId = defaultDistinctId()
|
|
people = People(apiToken: self.apiToken,
|
|
serialQueue: trackingQueue,
|
|
lock: self.readWriteLock,
|
|
metadata: sessionMetadata, mixpanelPersistence: mixpanelPersistence)
|
|
flushInstance._flushInterval = flushInterval
|
|
#if !os(watchOS)
|
|
setupListeners()
|
|
#endif
|
|
unarchive()
|
|
// check whether we should opt out by default
|
|
// note: we don't override opt out persistence here since opt-out default state is often
|
|
// used as an initial state while GDPR information is being collected
|
|
if optOutTrackingByDefault && (hasOptedOutTracking() || optOutStatus == nil) {
|
|
optOutTracking()
|
|
}
|
|
}
|
|
#endif // os(OSX)
|
|
|
|
#if !os(OSX) && !os(watchOS)
|
|
private func setupListeners() {
|
|
let notificationCenter = NotificationCenter.default
|
|
trackIntegration()
|
|
#if os(iOS) && !targetEnvironment(macCatalyst)
|
|
setCurrentRadio()
|
|
// Temporarily remove the ability to monitor the radio change due to a crash issue might relate to the api from Apple
|
|
// https://openradar.appspot.com/46873673
|
|
// notificationCenter.addObserver(self,
|
|
// selector: #selector(setCurrentRadio),
|
|
// name: .CTRadioAccessTechnologyDidChange,
|
|
// object: nil)
|
|
#endif // os(iOS)
|
|
if !MixpanelInstance.isiOSAppExtension() {
|
|
notificationCenter.addObserver(self,
|
|
selector: #selector(applicationWillTerminate(_:)),
|
|
name: UIApplication.willTerminateNotification,
|
|
object: nil)
|
|
notificationCenter.addObserver(self,
|
|
selector: #selector(applicationWillResignActive(_:)),
|
|
name: UIApplication.willResignActiveNotification,
|
|
object: nil)
|
|
notificationCenter.addObserver(self,
|
|
selector: #selector(applicationDidBecomeActive(_:)),
|
|
name: UIApplication.didBecomeActiveNotification,
|
|
object: nil)
|
|
notificationCenter.addObserver(self,
|
|
selector: #selector(applicationDidEnterBackground(_:)),
|
|
name: UIApplication.didEnterBackgroundNotification,
|
|
object: nil)
|
|
notificationCenter.addObserver(self,
|
|
selector: #selector(applicationWillEnterForeground(_:)),
|
|
name: UIApplication.willEnterForegroundNotification,
|
|
object: nil)
|
|
}
|
|
}
|
|
#elseif os(OSX)
|
|
private func setupListeners() {
|
|
let notificationCenter = NotificationCenter.default
|
|
notificationCenter.addObserver(self,
|
|
selector: #selector(applicationWillTerminate(_:)),
|
|
name: NSApplication.willTerminateNotification,
|
|
object: nil)
|
|
notificationCenter.addObserver(self,
|
|
selector: #selector(applicationWillResignActive(_:)),
|
|
name: NSApplication.willResignActiveNotification,
|
|
object: nil)
|
|
notificationCenter.addObserver(self,
|
|
selector: #selector(applicationDidBecomeActive(_:)),
|
|
name: NSApplication.didBecomeActiveNotification,
|
|
object: nil)
|
|
}
|
|
#endif // os(OSX)
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
#if os(iOS) && !os(watchOS) && !targetEnvironment(macCatalyst)
|
|
if let reachability = MixpanelInstance.reachability {
|
|
if !SCNetworkReachabilitySetCallback(reachability, nil, nil) {
|
|
Logger.error(message: "\(self) error unsetting reachability callback")
|
|
}
|
|
if !SCNetworkReachabilitySetDispatchQueue(reachability, nil) {
|
|
Logger.error(message: "\(self) error unsetting reachability dispatch queue")
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static func isiOSAppExtension() -> Bool {
|
|
return Bundle.main.bundlePath.hasSuffix(".appex")
|
|
}
|
|
|
|
#if !os(OSX) && !os(watchOS)
|
|
static func sharedUIApplication() -> UIApplication? {
|
|
guard let sharedApplication =
|
|
UIApplication.perform(NSSelectorFromString("sharedApplication"))?.takeUnretainedValue() as? UIApplication else {
|
|
return nil
|
|
}
|
|
return sharedApplication
|
|
}
|
|
#endif // !os(OSX)
|
|
|
|
@objc private func applicationDidBecomeActive(_ notification: Notification) {
|
|
flushInstance.applicationDidBecomeActive()
|
|
#if DECIDE
|
|
checkDecide { decideResponse in
|
|
if let decideResponse = decideResponse {
|
|
if !decideResponse.integrations.isEmpty {
|
|
self.connectIntegrations.setupIntegrations(decideResponse.integrations)
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif // DECIDE
|
|
}
|
|
|
|
@objc private func applicationWillResignActive(_ notification: Notification) {
|
|
flushInstance.applicationWillResignActive()
|
|
#if os(OSX)
|
|
if flushOnBackground {
|
|
flush()
|
|
}
|
|
|
|
#endif
|
|
}
|
|
|
|
#if !os(OSX) && !os(watchOS)
|
|
@objc private func applicationDidEnterBackground(_ notification: Notification) {
|
|
guard let sharedApplication = MixpanelInstance.sharedUIApplication() else {
|
|
return
|
|
}
|
|
|
|
if hasOptedOutTracking() {
|
|
return
|
|
}
|
|
|
|
taskId = sharedApplication.beginBackgroundTask { [weak self] in
|
|
self?.taskId = UIBackgroundTaskIdentifier.invalid
|
|
}
|
|
|
|
if flushOnBackground {
|
|
flush()
|
|
} else {
|
|
// only need to archive if don't flush because flush archives at the end
|
|
trackingQueue.async { [weak self] in
|
|
self?.archive()
|
|
}
|
|
}
|
|
|
|
trackingQueue.async { [weak self] in
|
|
guard let self = self else { return }
|
|
|
|
#if DECIDE
|
|
self.readWriteLock.write {
|
|
self.decideInstance.decideFetched = false
|
|
}
|
|
#endif // DECIDE
|
|
if self.taskId != UIBackgroundTaskIdentifier.invalid {
|
|
sharedApplication.endBackgroundTask(self.taskId)
|
|
self.taskId = UIBackgroundTaskIdentifier.invalid
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc private func applicationWillEnterForeground(_ notification: Notification) {
|
|
guard let sharedApplication = MixpanelInstance.sharedUIApplication() else {
|
|
return
|
|
}
|
|
sessionMetadata.applicationWillEnterForeground()
|
|
trackingQueue.async { [weak self, sharedApplication] in
|
|
guard let self = self else { return }
|
|
|
|
if self.taskId != UIBackgroundTaskIdentifier.invalid {
|
|
sharedApplication.endBackgroundTask(self.taskId)
|
|
self.taskId = UIBackgroundTaskIdentifier.invalid
|
|
#if os(iOS)
|
|
self.updateNetworkActivityIndicator(false)
|
|
#endif // os(iOS)
|
|
}
|
|
}
|
|
}
|
|
#endif // os(OSX)
|
|
|
|
@objc private func applicationWillTerminate(_ notification: Notification) {
|
|
trackingQueue.async { [weak self] in
|
|
guard let self = self else { return }
|
|
self.archive()
|
|
}
|
|
}
|
|
|
|
func defaultDistinctId() -> String {
|
|
let distinctId: String?
|
|
#if MIXPANEL_UNIQUE_DISTINCT_ID
|
|
#if os(OSX)
|
|
distinctId = MixpanelInstance.macOSIdentifier()
|
|
#elseif !os(watchOS)
|
|
if NSClassFromString("UIDevice") != nil {
|
|
distinctId = UIDevice.current.identifierForVendor?.uuidString
|
|
} else {
|
|
distinctId = nil
|
|
}
|
|
#else
|
|
distinctId = nil
|
|
#endif
|
|
#else
|
|
distinctId = nil
|
|
#endif
|
|
return distinctId ?? UUID().uuidString // use a random UUID by default
|
|
}
|
|
|
|
#if os(OSX)
|
|
static func macOSIdentifier() -> String? {
|
|
let platformExpert: io_service_t = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"))
|
|
let serialNumberAsCFString =
|
|
IORegistryEntryCreateCFProperty(platformExpert, kIOPlatformSerialNumberKey as CFString, kCFAllocatorDefault, 0)
|
|
IOObjectRelease(platformExpert)
|
|
return (serialNumberAsCFString?.takeUnretainedValue() as? String)
|
|
}
|
|
#endif // os(OSX)
|
|
|
|
#if os(iOS)
|
|
func updateNetworkActivityIndicator(_ on: Bool) {
|
|
if showNetworkActivityIndicator {
|
|
DispatchQueue.main.async { [on] in
|
|
MixpanelInstance.sharedUIApplication()?.isNetworkActivityIndicatorVisible = on
|
|
}
|
|
}
|
|
}
|
|
#if os(iOS) && !targetEnvironment(macCatalyst)
|
|
@objc func setCurrentRadio() {
|
|
var radio = ""
|
|
let prefix = "CTRadioAccessTechnology"
|
|
if #available(iOS 12.0, *) {
|
|
if let radioDict = MixpanelInstance.telephonyInfo.serviceCurrentRadioAccessTechnology {
|
|
for (_, value) in radioDict where !value.isEmpty && value.hasPrefix(prefix) {
|
|
// the first should be the prefix, second the target
|
|
let components = value.components(separatedBy: prefix)
|
|
|
|
// Something went wrong and we have more than prefix:target
|
|
guard components.count == 2 else {
|
|
continue
|
|
}
|
|
|
|
// Safe to directly access by index since we confirmed count == 2 above
|
|
let radioValue = components[1]
|
|
|
|
// Send to parent
|
|
radio += radio.isEmpty ? radioValue : ", \(radioValue)"
|
|
}
|
|
|
|
radio = radio.isEmpty ? "None": radio
|
|
}
|
|
} else {
|
|
radio = MixpanelInstance.telephonyInfo.currentRadioAccessTechnology ?? "None"
|
|
if radio.hasPrefix(prefix) {
|
|
radio = (radio as NSString).substring(from: prefix.count)
|
|
}
|
|
}
|
|
|
|
trackingQueue.async {
|
|
AutomaticProperties.automaticPropertiesLock.write { [weak self, radio] in
|
|
AutomaticProperties.properties["$radio"] = radio
|
|
|
|
guard self != nil else {
|
|
return
|
|
}
|
|
|
|
AutomaticProperties.properties["$carrier"] = ""
|
|
if #available(iOS 12.0, *) {
|
|
if let carrierName = MixpanelInstance.telephonyInfo.serviceSubscriberCellularProviders?.first?.value.carrierName {
|
|
AutomaticProperties.properties["$carrier"] = carrierName
|
|
}
|
|
} else {
|
|
if let carrierName = MixpanelInstance.telephonyInfo.subscriberCellularProvider?.carrierName {
|
|
AutomaticProperties.properties["$carrier"] = carrierName
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
#endif // os(iOS)
|
|
|
|
}
|
|
|
|
extension MixpanelInstance {
|
|
// MARK: - Identity
|
|
|
|
/**
|
|
Sets the distinct ID of the current user.
|
|
|
|
Mixpanel uses a randomly generated persistent UUID as the default local distinct ID.
|
|
|
|
If you want to use a unique persistent UUID, you can define the
|
|
<code>MIXPANEL_UNIQUE_DISTINCT_ID</code> flag in your <code>Active Compilation Conditions</code>
|
|
build settings. It then uses the IFV String (`UIDevice.current().identifierForVendor`) as
|
|
the default local distinct ID. This ID will identify a user across all apps by the same vendor, but cannot be
|
|
used to link the same user across apps from different vendors. If we are unable to get an IFV, we will fall
|
|
back to generating a random persistent UUID.
|
|
|
|
For tracking events, you do not need to call `identify:`. However,
|
|
**Mixpanel User profiles always requires an explicit call to `identify:`.**
|
|
If calls are made to
|
|
`set:`, `increment` or other `People`
|
|
methods prior to calling `identify:`, then they are queued up and
|
|
flushed once `identify:` is called.
|
|
|
|
If you'd like to use the default distinct ID for Mixpanel People as well
|
|
(recommended), call `identify:` using the current distinct ID:
|
|
`mixpanelInstance.identify(mixpanelInstance.distinctId)`.
|
|
|
|
- parameter distinctId: string that uniquely identifies the current user
|
|
- parameter usePeople: boolean that controls whether or not to set the people distinctId to the event distinctId.
|
|
This should only be set to false if you wish to prevent people profile updates for that user.
|
|
*/
|
|
open func identify(distinctId: String, usePeople: Bool = true) {
|
|
if hasOptedOutTracking() {
|
|
return
|
|
}
|
|
if distinctId.isEmpty {
|
|
Logger.error(message: "\(self) cannot identify blank distinct id")
|
|
return
|
|
}
|
|
|
|
trackingQueue.async { [weak self, distinctId, usePeople] in
|
|
guard let self = self else { return }
|
|
|
|
// If there's no anonymousId assigned yet, that means distinctId is stored in the storage. Assigning already stored
|
|
// distinctId as anonymousId on identify and also setting a flag to notify that it might be previously logged in user
|
|
if self.anonymousId == nil {
|
|
self.anonymousId = self.distinctId
|
|
self.hadPersistedDistinctId = true
|
|
}
|
|
|
|
if distinctId != self.distinctId {
|
|
let oldDistinctId = self.distinctId
|
|
self.readWriteLock.write {
|
|
self.alias = nil
|
|
self.distinctId = distinctId
|
|
self.userId = distinctId
|
|
}
|
|
self.track(event: "$identify", properties: ["$anon_distinct_id": oldDistinctId])
|
|
}
|
|
|
|
if usePeople {
|
|
self.readWriteLock.write {
|
|
self.people.distinctId = distinctId
|
|
}
|
|
self.mixpanelPersistence.identifyPeople(token: self.apiToken)
|
|
|
|
} else {
|
|
self.people.distinctId = nil
|
|
}
|
|
|
|
MixpanelPersistence.saveIdentity(MixpanelIdentity.init(
|
|
distinctID: self.distinctId,
|
|
peopleDistinctID: self.people.distinctId,
|
|
anonymousId: self.anonymousId,
|
|
userId: self.userId,
|
|
alias: self.alias,
|
|
hadPersistedDistinctId: self.hadPersistedDistinctId), apiToken: self.apiToken)
|
|
}
|
|
|
|
if MixpanelInstance.isiOSAppExtension() {
|
|
flush()
|
|
}
|
|
}
|
|
|
|
/**
|
|
The alias method creates an alias which Mixpanel will use to remap one id to another.
|
|
Multiple aliases can point to the same identifier.
|
|
|
|
|
|
`mixpanelInstance.createAlias("New ID", distinctId: mixpanelInstance.distinctId)`
|
|
|
|
You can add multiple id aliases to the existing id
|
|
|
|
`mixpanelInstance.createAlias("Newer ID", distinctId: mixpanelInstance.distinctId)`
|
|
|
|
|
|
- parameter alias: A unique identifier that you want to use as an identifier for this user.
|
|
- parameter distinctId: The current user identifier.
|
|
- parameter usePeople: boolean that controls whether or not to set the people distinctId to the event distinctId.
|
|
This should only be set to false if you wish to prevent people profile updates for that user.
|
|
*/
|
|
open func createAlias(_ alias: String, distinctId: String, usePeople: Bool = true) {
|
|
if hasOptedOutTracking() {
|
|
return
|
|
}
|
|
if distinctId.isEmpty {
|
|
Logger.error(message: "\(self) cannot identify blank distinct id")
|
|
return
|
|
}
|
|
|
|
if alias.isEmpty {
|
|
Logger.error(message: "\(self) create alias called with empty alias")
|
|
return
|
|
}
|
|
|
|
if alias != distinctId {
|
|
trackingQueue.async { [weak self, alias] in
|
|
guard let self = self else {
|
|
return
|
|
}
|
|
|
|
self.alias = alias
|
|
MixpanelPersistence.saveIdentity(MixpanelIdentity.init(
|
|
distinctID: self.distinctId,
|
|
peopleDistinctID: self.people.distinctId,
|
|
anonymousId: self.anonymousId,
|
|
userId: self.userId,
|
|
alias: self.alias,
|
|
hadPersistedDistinctId: self.hadPersistedDistinctId), apiToken: self.apiToken)
|
|
}
|
|
|
|
let properties = ["distinct_id": distinctId, "alias": alias]
|
|
track(event: "$create_alias", properties: properties)
|
|
identify(distinctId: distinctId, usePeople: usePeople)
|
|
flush()
|
|
} else {
|
|
Logger.error(message: "alias: \(alias) matches distinctId: \(distinctId) - skipping api call.")
|
|
}
|
|
}
|
|
|
|
/**
|
|
Clears all stored properties including the distinct Id.
|
|
Useful if your app's user logs out.
|
|
*/
|
|
open func reset() {
|
|
flush()
|
|
trackingQueue.async { [weak self] in
|
|
self?.readWriteLock.write { [weak self] in
|
|
guard let self = self else {
|
|
return
|
|
}
|
|
|
|
MixpanelPersistence.deleteMPUserDefaultsData(apiToken: self.apiToken)
|
|
self.timedEvents = InternalProperties()
|
|
self.distinctId = self.defaultDistinctId()
|
|
self.anonymousId = self.distinctId
|
|
self.hadPersistedDistinctId = nil
|
|
self.userId = nil
|
|
self.superProperties = InternalProperties()
|
|
self.people.distinctId = nil
|
|
self.alias = nil
|
|
#if DECIDE
|
|
self.decideInstance.decideFetched = false
|
|
self.connectIntegrations.reset()
|
|
#endif // DECIDE
|
|
self.mixpanelPersistence.resetEntities()
|
|
}
|
|
self?.archive()
|
|
}
|
|
}
|
|
}
|
|
|
|
extension MixpanelInstance {
|
|
// MARK: - Persistence
|
|
|
|
open func archive() {
|
|
self.readWriteLock.read {
|
|
MixpanelPersistence.saveTimedEvents(timedEvents: timedEvents, apiToken: apiToken)
|
|
MixpanelPersistence.saveSuperProperties(superProperties: superProperties, apiToken: apiToken)
|
|
MixpanelPersistence.saveIdentity(MixpanelIdentity.init(
|
|
distinctID: distinctId,
|
|
peopleDistinctID: people.distinctId,
|
|
anonymousId: anonymousId,
|
|
userId: userId,
|
|
alias: alias,
|
|
hadPersistedDistinctId: hadPersistedDistinctId), apiToken: apiToken)
|
|
}
|
|
}
|
|
|
|
func unarchive() {
|
|
optOutStatus = MixpanelPersistence.loadOptOutStatusFlag(apiToken: apiToken)
|
|
superProperties = MixpanelPersistence.loadSuperProperties(apiToken: apiToken)
|
|
timedEvents = MixpanelPersistence.loadTimedEvents(apiToken: apiToken)
|
|
let mixpanelIdentity = MixpanelPersistence.loadIdentity(apiToken: apiToken)
|
|
(distinctId, people.distinctId, anonymousId, userId, alias, hadPersistedDistinctId) = (
|
|
mixpanelIdentity.distinctID,
|
|
mixpanelIdentity.peopleDistinctID,
|
|
mixpanelIdentity.anonymousId,
|
|
mixpanelIdentity.userId,
|
|
mixpanelIdentity.alias,
|
|
mixpanelIdentity.hadPersistedDistinctId
|
|
)
|
|
if distinctId.isEmpty {
|
|
distinctId = defaultDistinctId()
|
|
anonymousId = distinctId
|
|
hadPersistedDistinctId = nil
|
|
userId = nil
|
|
}
|
|
}
|
|
|
|
func trackIntegration() {
|
|
if hasOptedOutTracking() {
|
|
return
|
|
}
|
|
let defaultsKey = "trackedKey"
|
|
if !UserDefaults.standard.bool(forKey: defaultsKey) {
|
|
trackingQueue.async { [apiToken, defaultsKey] in
|
|
Network.trackIntegration(apiToken: apiToken, serverURL: BasePath.DefaultMixpanelAPI) { [defaultsKey] (success) in
|
|
if success {
|
|
UserDefaults.standard.set(true, forKey: defaultsKey)
|
|
UserDefaults.standard.synchronize()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension MixpanelInstance {
|
|
// MARK: - Flush
|
|
|
|
/**
|
|
Uploads queued data to the Mixpanel server.
|
|
|
|
By default, queued data is flushed to the Mixpanel servers every minute (the
|
|
default for `flushInterval`), and on background (since
|
|
`flushOnBackground` is on by default). You only need to call this
|
|
method manually if you want to force a flush at a particular moment.
|
|
|
|
- parameter completion: an optional completion handler for when the flush has completed.
|
|
*/
|
|
open func flush(completion: (() -> Void)? = nil) {
|
|
if hasOptedOutTracking() {
|
|
if let completion = completion {
|
|
DispatchQueue.main.async(execute: completion)
|
|
}
|
|
return
|
|
}
|
|
trackingQueue.async { [weak self, completion] in
|
|
guard let self = self else {
|
|
return
|
|
}
|
|
|
|
if let shouldFlush = self.delegate?.mixpanelWillFlush(self), !shouldFlush {
|
|
return
|
|
}
|
|
|
|
self.flushQueue(type: .events)
|
|
self.flushQueue(type: .people)
|
|
self.flushQueue(type: .groups)
|
|
|
|
if let completion = completion {
|
|
DispatchQueue.main.async(execute: completion)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func persistenceTypeFromFlushType(_ type: FlushType) -> PersistenceType {
|
|
switch type {
|
|
case .events:
|
|
return PersistenceType.events
|
|
case .people:
|
|
return PersistenceType.people
|
|
case .groups:
|
|
return PersistenceType.groups
|
|
}
|
|
}
|
|
|
|
func flushQueue(type: FlushType) {
|
|
if hasOptedOutTracking() {
|
|
return
|
|
}
|
|
let queue = self.mixpanelPersistence.loadEntitiesInBatch(type: persistenceTypeFromFlushType(type))
|
|
self.flushInstance.flushQueue(type: type, queue: queue)
|
|
}
|
|
|
|
func flushSuccess(type: FlushType, ids: [Int32]) {
|
|
trackingQueue.async { [weak self] in
|
|
guard let self = self else {
|
|
return
|
|
}
|
|
self.mixpanelPersistence.removeEntitiesInBatch(type: self.persistenceTypeFromFlushType(type), ids: ids)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension MixpanelInstance {
|
|
// MARK: - Track
|
|
|
|
/**
|
|
Tracks an event with properties.
|
|
Properties are optional and can be added only if needed.
|
|
|
|
Properties will allow you to segment your events in your Mixpanel reports.
|
|
Property keys must be String objects and the supported value types need to conform to MixpanelType.
|
|
MixpanelType can be either String, Int, UInt, Double, Float, Bool, [MixpanelType], [String: MixpanelType], Date, URL, or NSNull.
|
|
If the event is being timed, the timer will stop and be added as a property.
|
|
|
|
- parameter event: event name
|
|
- parameter properties: properties dictionary
|
|
*/
|
|
open func track(event: String?, properties: Properties? = nil) {
|
|
if hasOptedOutTracking() {
|
|
return
|
|
}
|
|
|
|
let epochInterval = Date().timeIntervalSince1970
|
|
|
|
trackingQueue.async { [weak self, event, properties, epochInterval] in
|
|
guard let self = self else {
|
|
return
|
|
}
|
|
let mixpanelIdentity = MixpanelIdentity.init(distinctID: self.distinctId,
|
|
peopleDistinctID: nil,
|
|
anonymousId: self.anonymousId,
|
|
userId: self.userId,
|
|
alias: nil,
|
|
hadPersistedDistinctId: self.hadPersistedDistinctId)
|
|
self.timedEvents = self.trackInstance.track(event: event,
|
|
properties: properties,
|
|
timedEvents: self.timedEvents,
|
|
superProperties: self.superProperties,
|
|
mixpanelIdentity: mixpanelIdentity,
|
|
epochInterval: epochInterval)
|
|
}
|
|
|
|
if MixpanelInstance.isiOSAppExtension() {
|
|
flush()
|
|
}
|
|
}
|
|
|
|
/**
|
|
Tracks an event with properties and to specific groups.
|
|
Properties and groups are optional and can be added only if needed.
|
|
|
|
Properties will allow you to segment your events in your Mixpanel reports.
|
|
Property and group keys must be String objects and the supported value types need to conform to MixpanelType.
|
|
MixpanelType can be either String, Int, UInt, Double, Float, Bool, [MixpanelType], [String: MixpanelType], Date, URL, or NSNull.
|
|
If the event is being timed, the timer will stop and be added as a property.
|
|
|
|
- parameter event: event name
|
|
- parameter properties: properties dictionary
|
|
- parameter groups: groups dictionary
|
|
*/
|
|
open func trackWithGroups(event: String?, properties: Properties? = nil, groups: Properties?) {
|
|
if hasOptedOutTracking() {
|
|
return
|
|
}
|
|
|
|
guard let properties = properties else {
|
|
self.track(event: event, properties: groups)
|
|
return
|
|
}
|
|
|
|
guard let groups = groups else {
|
|
self.track(event: event, properties: properties)
|
|
return
|
|
}
|
|
|
|
var mergedProperties = properties
|
|
for (groupKey, groupID) in groups {
|
|
mergedProperties[groupKey] = groupID
|
|
}
|
|
self.track(event: event, properties: mergedProperties)
|
|
}
|
|
|
|
open func getGroup(groupKey: String, groupID: MixpanelType) -> Group {
|
|
let key = makeMapKey(groupKey: groupKey, groupID: groupID)
|
|
|
|
var groupsShadow: [String: Group] = [:]
|
|
|
|
readWriteLock.read {
|
|
groupsShadow = groups
|
|
}
|
|
|
|
guard let group = groupsShadow[key] else {
|
|
readWriteLock.write {
|
|
groups[key] = Group(apiToken: apiToken,
|
|
serialQueue: trackingQueue,
|
|
lock: self.readWriteLock,
|
|
groupKey: groupKey,
|
|
groupID: groupID,
|
|
metadata: sessionMetadata,
|
|
mixpanelPersistence: mixpanelPersistence)
|
|
groupsShadow = groups
|
|
}
|
|
return groupsShadow[key]!
|
|
}
|
|
|
|
if !(group.groupKey == groupKey && group.groupID.equals(rhs: groupID)) {
|
|
// we somehow hit a collision on the map key, return a new group with the correct key and ID
|
|
Logger.info(message: "groups dictionary key collision: \(key)")
|
|
let newGroup = Group(apiToken: apiToken,
|
|
serialQueue: trackingQueue,
|
|
lock: self.readWriteLock,
|
|
groupKey: groupKey,
|
|
groupID: groupID,
|
|
metadata: sessionMetadata,
|
|
mixpanelPersistence: mixpanelPersistence)
|
|
readWriteLock.write {
|
|
groups[key] = newGroup
|
|
}
|
|
return newGroup
|
|
}
|
|
|
|
return group
|
|
}
|
|
|
|
func removeCachedGroup(groupKey: String, groupID: MixpanelType) {
|
|
readWriteLock.write {
|
|
groups.removeValue(forKey: makeMapKey(groupKey: groupKey, groupID: groupID))
|
|
}
|
|
}
|
|
|
|
func makeMapKey(groupKey: String, groupID: MixpanelType) -> String {
|
|
return "\(groupKey)_\(groupID)"
|
|
}
|
|
|
|
/**
|
|
Starts a timer that will be stopped and added as a property when a
|
|
corresponding event is tracked.
|
|
|
|
This method is intended to be used in advance of events that have
|
|
a duration. For example, if a developer were to track an "Image Upload" event
|
|
she might want to also know how long the upload took. Calling this method
|
|
before the upload code would implicitly cause the `track`
|
|
call to record its duration.
|
|
|
|
- precondition:
|
|
// begin timing the image upload:
|
|
mixpanelInstance.time(event:"Image Upload")
|
|
// upload the image:
|
|
self.uploadImageWithSuccessHandler() { _ in
|
|
// track the event
|
|
mixpanelInstance.track("Image Upload")
|
|
}
|
|
|
|
- parameter event: the event name to be timed
|
|
|
|
*/
|
|
open func time(event: String) {
|
|
let startTime = Date().timeIntervalSince1970
|
|
trackingQueue.async { [weak self, startTime, event] in
|
|
guard let self = self else { return }
|
|
let timedEvents = self.trackInstance.time(event: event, timedEvents: self.timedEvents, startTime: startTime)
|
|
self.readWriteLock.write {
|
|
self.timedEvents = timedEvents
|
|
}
|
|
MixpanelPersistence.saveTimedEvents(timedEvents: timedEvents, apiToken: self.apiToken)
|
|
}
|
|
}
|
|
|
|
/**
|
|
Retrieves the time elapsed for the named event since time(event:) was called.
|
|
|
|
- parameter event: the name of the event to be tracked that was passed to time(event:)
|
|
*/
|
|
open func eventElapsedTime(event: String) -> Double {
|
|
if let startTime = self.timedEvents[event] as? TimeInterval {
|
|
return Date().timeIntervalSince1970 - startTime
|
|
}
|
|
return 0
|
|
}
|
|
|
|
/**
|
|
Clears all current event timers.
|
|
*/
|
|
open func clearTimedEvents() {
|
|
trackingQueue.async { [weak self] in
|
|
guard let self = self else { return }
|
|
self.readWriteLock.write {
|
|
self.timedEvents = InternalProperties()
|
|
}
|
|
MixpanelPersistence.saveTimedEvents(timedEvents: InternalProperties(), apiToken: self.apiToken)
|
|
}
|
|
}
|
|
|
|
/**
|
|
Clears the event timer for the named event.
|
|
|
|
- parameter event: the name of the event to clear the timer for
|
|
*/
|
|
open func clearTimedEvent(event: String) {
|
|
trackingQueue.async { [weak self, event] in
|
|
guard let self = self else { return }
|
|
|
|
let updatedTimedEvents = self.trackInstance.clearTimedEvent(event: event, timedEvents: self.timedEvents)
|
|
MixpanelPersistence.saveTimedEvents(timedEvents: updatedTimedEvents, apiToken: self.apiToken)
|
|
}
|
|
}
|
|
|
|
/**
|
|
Returns the currently set super properties.
|
|
|
|
- returns: the current super properties
|
|
*/
|
|
open func currentSuperProperties() -> [String: Any] {
|
|
var properties = InternalProperties()
|
|
self.readWriteLock.read {
|
|
properties = superProperties
|
|
}
|
|
return properties
|
|
}
|
|
|
|
/**
|
|
Clears all currently set super properties.
|
|
*/
|
|
open func clearSuperProperties() {
|
|
self.readWriteLock.write {
|
|
self.superProperties = self.trackInstance.clearSuperProperties(self.superProperties)
|
|
}
|
|
MixpanelPersistence.saveSuperProperties(superProperties: self.superProperties, apiToken: self.apiToken)
|
|
}
|
|
|
|
/**
|
|
Registers super properties, overwriting ones that have already been set.
|
|
|
|
Super properties, once registered, are automatically sent as properties for
|
|
all event tracking calls. They save you having to maintain and add a common
|
|
set of properties to your events.
|
|
Property keys must be String objects and the supported value types need to conform to MixpanelType.
|
|
MixpanelType can be either String, Int, UInt, Double, Float, Bool, [MixpanelType], [String: MixpanelType], Date, URL, or NSNull.
|
|
|
|
- parameter properties: properties dictionary
|
|
*/
|
|
open func registerSuperProperties(_ properties: Properties) {
|
|
self.readWriteLock.write {
|
|
self.superProperties = self.trackInstance.registerSuperProperties(properties,
|
|
superProperties: self.superProperties)
|
|
}
|
|
MixpanelPersistence.saveSuperProperties(superProperties: self.superProperties, apiToken: apiToken)
|
|
}
|
|
|
|
/**
|
|
Registers super properties without overwriting ones that have already been set,
|
|
unless the existing value is equal to defaultValue. defaultValue is optional.
|
|
|
|
Property keys must be String objects and the supported value types need to conform to MixpanelType.
|
|
MixpanelType can be either String, Int, UInt, Double, Float, Bool, [MixpanelType], [String: MixpanelType], Date, URL, or NSNull.
|
|
|
|
- parameter properties: properties dictionary
|
|
- parameter defaultValue: Optional. overwrite existing properties that have this value
|
|
*/
|
|
open func registerSuperPropertiesOnce(_ properties: Properties,
|
|
defaultValue: MixpanelType? = nil) {
|
|
self.readWriteLock.write {
|
|
self.superProperties = self.trackInstance.registerSuperPropertiesOnce(properties,
|
|
superProperties: self.superProperties,
|
|
defaultValue: defaultValue)
|
|
}
|
|
MixpanelPersistence.saveSuperProperties(superProperties: self.superProperties, apiToken: apiToken)
|
|
}
|
|
|
|
/**
|
|
Removes a previously registered super property.
|
|
|
|
As an alternative to clearing all properties, unregistering specific super
|
|
properties prevents them from being recorded on future events. This operation
|
|
does not affect the value of other super properties. Any property name that is
|
|
not registered is ignored.
|
|
Note that after removing a super property, events will show the attribute as
|
|
having the value `undefined` in Mixpanel until a new value is
|
|
registered.
|
|
|
|
- parameter propertyName: array of property name strings to remove
|
|
*/
|
|
open func unregisterSuperProperty(_ propertyName: String) {
|
|
self.readWriteLock.write {
|
|
self.superProperties = self.trackInstance.unregisterSuperProperty(propertyName,
|
|
superProperties: self.superProperties)
|
|
}
|
|
MixpanelPersistence.saveSuperProperties(superProperties: self.superProperties, apiToken: apiToken)
|
|
}
|
|
|
|
/**
|
|
Updates a superproperty atomically. The update function
|
|
|
|
- parameter update: closure to apply to superproperties
|
|
*/
|
|
func updateSuperProperty(_ update: @escaping (_ superproperties: inout InternalProperties) -> Void) {
|
|
var superPropertiesShadow = InternalProperties()
|
|
self.readWriteLock.read {
|
|
superPropertiesShadow = self.superProperties
|
|
}
|
|
self.trackInstance.updateSuperProperty(update,
|
|
superProperties: &superPropertiesShadow)
|
|
self.readWriteLock.write {
|
|
self.superProperties = superPropertiesShadow
|
|
}
|
|
MixpanelPersistence.saveSuperProperties(superProperties: self.superProperties, apiToken: apiToken)
|
|
}
|
|
|
|
/**
|
|
Convenience method to set a single group the user belongs to.
|
|
|
|
- parameter groupKey: The property name associated with this group type (must already have been set up).
|
|
- parameter groupID: The group the user belongs to.
|
|
*/
|
|
open func setGroup(groupKey: String, groupID: MixpanelType) {
|
|
if hasOptedOutTracking() {
|
|
return
|
|
}
|
|
|
|
setGroup(groupKey: groupKey, groupIDs: [groupID])
|
|
}
|
|
|
|
/**
|
|
Set the groups this user belongs to.
|
|
|
|
- parameter groupKey: The property name associated with this group type (must already have been set up).
|
|
- parameter groupIDs: The list of groups the user belongs to.
|
|
*/
|
|
open func setGroup(groupKey: String, groupIDs: [MixpanelType]) {
|
|
if hasOptedOutTracking() {
|
|
return
|
|
}
|
|
|
|
let properties = [groupKey: groupIDs]
|
|
self.registerSuperProperties(properties)
|
|
people.set(properties: properties)
|
|
}
|
|
|
|
/**
|
|
Add a group to this user's membership for a particular group key
|
|
|
|
- parameter groupKey: The property name associated with this group type (must already have been set up).
|
|
- parameter groupID: The new group the user belongs to.
|
|
*/
|
|
open func addGroup(groupKey: String, groupID: MixpanelType) {
|
|
if hasOptedOutTracking() {
|
|
return
|
|
}
|
|
|
|
updateSuperProperty { superProperties -> Void in
|
|
guard let oldValue = superProperties[groupKey] else {
|
|
superProperties[groupKey] = [groupID]
|
|
self.people.set(properties: [groupKey: [groupID]])
|
|
return
|
|
}
|
|
|
|
if let oldValue = oldValue as? [MixpanelType] {
|
|
var vals = oldValue
|
|
if !vals.contains(where: { $0.equals(rhs: groupID) }) {
|
|
vals.append(groupID)
|
|
superProperties[groupKey] = vals
|
|
}
|
|
} else {
|
|
superProperties[groupKey] = [oldValue, groupID]
|
|
}
|
|
|
|
// This is a best effort--if the people property is not already a list, this call does nothing.
|
|
self.people.union(properties: [groupKey: [groupID]])
|
|
}
|
|
}
|
|
|
|
/**
|
|
Remove a group from this user's membership for a particular group key
|
|
|
|
- parameter groupKey: The property name associated with this group type (must already have been set up).
|
|
- parameter groupID: The group value to remove.
|
|
*/
|
|
open func removeGroup(groupKey: String, groupID: MixpanelType) {
|
|
if hasOptedOutTracking() {
|
|
return
|
|
}
|
|
|
|
updateSuperProperty { (superProperties) -> Void in
|
|
guard let oldValue = superProperties[groupKey] else {
|
|
return
|
|
}
|
|
|
|
guard let vals = oldValue as? [MixpanelType] else {
|
|
superProperties.removeValue(forKey: groupKey)
|
|
self.people.unset(properties: [groupKey])
|
|
return
|
|
}
|
|
|
|
if vals.count < 2 {
|
|
superProperties.removeValue(forKey: groupKey)
|
|
self.people.unset(properties: [groupKey])
|
|
return
|
|
}
|
|
|
|
superProperties[groupKey] = vals.filter {!$0.equals(rhs: groupID)}
|
|
self.people.remove(properties: [groupKey: groupID])
|
|
}
|
|
}
|
|
|
|
/**
|
|
Opt out tracking.
|
|
|
|
This method is used to opt out tracking. This causes all events and people request no longer
|
|
to be sent back to the Mixpanel server.
|
|
*/
|
|
open func optOutTracking() {
|
|
if people.distinctId != nil {
|
|
people.deleteUser()
|
|
people.clearCharges()
|
|
flush()
|
|
}
|
|
|
|
trackingQueue.async { [weak self] in
|
|
guard let self = self else { return }
|
|
self.readWriteLock.write { [weak self] in
|
|
guard let self = self else {
|
|
return
|
|
}
|
|
|
|
self.alias = nil
|
|
self.people.distinctId = nil
|
|
self.userId = nil
|
|
self.distinctId = self.defaultDistinctId()
|
|
self.anonymousId = self.distinctId
|
|
self.hadPersistedDistinctId = nil
|
|
self.superProperties = InternalProperties()
|
|
MixpanelPersistence.saveTimedEvents(timedEvents: InternalProperties(), apiToken: self.apiToken)
|
|
}
|
|
self.archive()
|
|
}
|
|
|
|
optOutStatus = true
|
|
MixpanelPersistence.saveOptOutStatusFlag(value: optOutStatus!, apiToken: apiToken)
|
|
}
|
|
|
|
/**
|
|
Opt in tracking.
|
|
|
|
Use this method to opt in an already opted out user from tracking. People updates and track calls will be
|
|
sent to Mixpanel after using this method.
|
|
|
|
This method will internally track an opt in event to your project.
|
|
|
|
- parameter distintId: an optional string to use as the distinct ID for events
|
|
- parameter properties: an optional properties dictionary that could be passed to add properties to the opt-in event
|
|
that is sent to Mixpanel
|
|
*/
|
|
open func optInTracking(distinctId: String? = nil, properties: Properties? = nil) {
|
|
optOutStatus = false
|
|
MixpanelPersistence.saveOptOutStatusFlag(value: optOutStatus!, apiToken: apiToken)
|
|
|
|
if let distinctId = distinctId {
|
|
identify(distinctId: distinctId)
|
|
}
|
|
track(event: "$opt_in", properties: properties)
|
|
}
|
|
|
|
/**
|
|
Returns if the current user has opted out tracking.
|
|
|
|
- returns: the current super opted out tracking status
|
|
*/
|
|
open func hasOptedOutTracking() -> Bool {
|
|
return optOutStatus ?? false
|
|
}
|
|
|
|
// MARK: - AEDelegate
|
|
func increment(property: String, by: Double) {
|
|
people?.increment(property: property, by: by)
|
|
}
|
|
|
|
func setOnce(properties: Properties) {
|
|
people?.setOnce(properties: properties)
|
|
}
|
|
}
|
|
|
|
#if DECIDE
|
|
extension MixpanelInstance {
|
|
|
|
// MARK: - Decide
|
|
func checkDecide(forceFetch: Bool = false, completion: @escaping ((_ response: DecideResponse?) -> Void)) {
|
|
trackingQueue.async { [weak self, completion, forceFetch] in
|
|
guard let self = self else { return }
|
|
self.decideInstance.checkDecide(forceFetch: forceFetch,
|
|
distinctId: self.people.distinctId ?? self.distinctId,
|
|
token: self.apiToken,
|
|
completion: completion)
|
|
}
|
|
}
|
|
}
|
|
#endif // DECIDE
|