add unit test cases for migration

lint fixes
This commit is contained in:
Zihe Jia 2021-09-02 14:35:48 -07:00
parent 74bce0de3c
commit 6ee1f2cfd6
21 changed files with 248 additions and 131 deletions

View file

@ -61,3 +61,4 @@ disabled_rules:
- type_body_length
- variable_name
- trailing_whitespace
- unused_optional_binding

View file

@ -28,6 +28,11 @@
8654F3002671636B00ACEED5 /* MixpanelPeopleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8654F2FB2671636B00ACEED5 /* MixpanelPeopleTests.swift */; };
8654F3012671636B00ACEED5 /* MixpanelOptOutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8654F2FC2671636B00ACEED5 /* MixpanelOptOutTests.swift */; };
8654F3052671C4B000ACEED5 /* MixpanelGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8654F3032671C4B000ACEED5 /* MixpanelGroupTests.swift */; };
8675E9A126DF356B0096858F /* mixpanel-testToken-events in Resources */ = {isa = PBXBuildFile; fileRef = 8675E99C26DF356A0096858F /* mixpanel-testToken-events */; };
8675E9A226DF356B0096858F /* mixpanel-testToken-groups in Resources */ = {isa = PBXBuildFile; fileRef = 8675E99E26DF356A0096858F /* mixpanel-testToken-groups */; };
8675E9A326DF356B0096858F /* mixpanel-testToken-people in Resources */ = {isa = PBXBuildFile; fileRef = 8675E99F26DF356A0096858F /* mixpanel-testToken-people */; };
8675E9AB26E144FE0096858F /* mixpanel-testToken-optOutStatus in Resources */ = {isa = PBXBuildFile; fileRef = 8675E9AA26E144FE0096858F /* mixpanel-testToken-optOutStatus */; };
8675E9B426E171AE0096858F /* mixpanel-testToken-properties in Resources */ = {isa = PBXBuildFile; fileRef = 8675E9B326E171AE0096858F /* mixpanel-testToken-properties */; };
86F86E8F22440C5D00B69832 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 86F86E8D22440C5C00B69832 /* Interface.storyboard */; };
86F86E9122440C5F00B69832 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 86F86E9022440C5F00B69832 /* Assets.xcassets */; };
86F86E9822440C5F00B69832 /* MixpanelDemoWatch Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 86F86E9722440C5F00B69832 /* MixpanelDemoWatch Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@ -275,6 +280,11 @@
8654F2FB2671636B00ACEED5 /* MixpanelPeopleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MixpanelPeopleTests.swift; sourceTree = "<group>"; };
8654F2FC2671636B00ACEED5 /* MixpanelOptOutTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MixpanelOptOutTests.swift; sourceTree = "<group>"; };
8654F3032671C4B000ACEED5 /* MixpanelGroupTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MixpanelGroupTests.swift; sourceTree = "<group>"; };
8675E99C26DF356A0096858F /* mixpanel-testToken-events */ = {isa = PBXFileReference; explicitFileType = text.pbxproject; path = "mixpanel-testToken-events"; sourceTree = "<group>"; };
8675E99E26DF356A0096858F /* mixpanel-testToken-groups */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = "mixpanel-testToken-groups"; sourceTree = "<group>"; };
8675E99F26DF356A0096858F /* mixpanel-testToken-people */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = "mixpanel-testToken-people"; sourceTree = "<group>"; };
8675E9AA26E144FE0096858F /* mixpanel-testToken-optOutStatus */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = "mixpanel-testToken-optOutStatus"; sourceTree = "<group>"; };
8675E9B326E171AE0096858F /* mixpanel-testToken-properties */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = "mixpanel-testToken-properties"; sourceTree = "<group>"; };
86F86E8B22440C5C00B69832 /* MixpanelDemoWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MixpanelDemoWatch.app; sourceTree = BUILT_PRODUCTS_DIR; };
86F86E8E22440C5C00B69832 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = "<group>"; };
86F86E9022440C5F00B69832 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -323,13 +333,6 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
0AAB97762ADB463FE09D6381 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
8654F2C1266ED84F00ACEED5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -470,6 +473,18 @@
path = MixpanelDemoMacUITests;
sourceTree = "<group>";
};
8675E9A626DF37B20096858F /* Resources */ = {
isa = PBXGroup;
children = (
8675E99C26DF356A0096858F /* mixpanel-testToken-events */,
8675E9B326E171AE0096858F /* mixpanel-testToken-properties */,
8675E99E26DF356A0096858F /* mixpanel-testToken-groups */,
8675E99F26DF356A0096858F /* mixpanel-testToken-people */,
8675E9AA26E144FE0096858F /* mixpanel-testToken-optOutStatus */,
);
name = Resources;
sourceTree = "<group>";
};
86F86E8C22440C5C00B69832 /* MixpanelDemoWatch */ = {
isa = PBXGroup;
children = (
@ -524,6 +539,7 @@
E12BD03A1D8A14D6008989C9 /* Supporting Files */ = {
isa = PBXGroup;
children = (
8675E9A626DF37B20096858F /* Resources */,
E12BD03C1D8A14F3008989C9 /* Assets.xcassets */,
E15FF7ED1D0461130076CDE3 /* Info.plist */,
);
@ -825,11 +841,6 @@
LastUpgradeCheck = 1200;
ORGANIZATIONNAME = Mixpanel;
TargetAttributes = {
60DDD14623D39620004F7CBB = {
CreatedOnToolsVersion = 11.2;
DevelopmentTeam = E8FVX7QLET;
ProvisioningStyle = Automatic;
};
8654F2C3266ED84F00ACEED5 = {
CreatedOnToolsVersion = 12.5;
};
@ -960,13 +971,6 @@
/* End PBXReferenceProxy section */
/* Begin PBXResourcesBuildPhase section */
60DDD14523D39620004F7CBB /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
8654F2C2266ED84F00ACEED5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -1045,6 +1049,11 @@
buildActionMask = 2147483647;
files = (
E12BD03D1D8A14F3008989C9 /* Assets.xcassets in Resources */,
8675E9A226DF356B0096858F /* mixpanel-testToken-groups in Resources */,
8675E9B426E171AE0096858F /* mixpanel-testToken-properties in Resources */,
8675E9A126DF356B0096858F /* mixpanel-testToken-events in Resources */,
8675E9A326DF356B0096858F /* mixpanel-testToken-people in Resources */,
8675E9AB26E144FE0096858F /* mixpanel-testToken-optOutStatus in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View file

@ -988,4 +988,84 @@ class MixpanelDemoTests: MixpanelBaseTests {
removeDBfile(testToken)
}
func testMigration() {
let token = "testToken"
// clean up
removeDBfile(token)
// copy the legacy archived file for the migration test
let legacyFiles = ["mixpanel-testToken-events", "mixpanel-testToken-properties", "mixpanel-testToken-groups", "mixpanel-testToken-people", "mixpanel-testToken-optOutStatus"]
prepareForMigrationFiles(legacyFiles)
// initialize mixpanel will do the migration automatically if found legacy archive files.
let testMixpanel = Mixpanel.initialize(token: token, flushInterval: 60)
let fileManager = FileManager.default
let libraryUrls = fileManager.urls(for: .libraryDirectory,
in: .userDomainMask)
XCTAssertFalse(fileManager.fileExists(atPath: (libraryUrls.first?.appendingPathComponent("mixpanel-testToken-events"))!.path), "after migration, the legacy archive files should be removed")
XCTAssertFalse(fileManager.fileExists(atPath: (libraryUrls.first?.appendingPathComponent("mixpanel-testToken-properties"))!.path), "after migration, the legacy archive files should be removed")
XCTAssertFalse(fileManager.fileExists(atPath: (libraryUrls.first?.appendingPathComponent("mixpanel-testToken-groups"))!.path), "after migration, the legacy archive files should be removed")
XCTAssertFalse(fileManager.fileExists(atPath: (libraryUrls.first?.appendingPathComponent("mixpanel-testToken-people"))!.path), "after migration, the legacy archive files should be removed")
let events = eventQueue(token: testMixpanel.apiToken)
XCTAssertEqual(events.count, 306)
XCTAssertEqual(events[0]["event"] as? String, "$identify")
XCTAssertEqual(events[1]["event"] as? String, "Logged in")
XCTAssertEqual(events[2]["event"] as? String, "$ae_first_open")
XCTAssertEqual(events[3]["event"] as? String, "Tracked event 1")
let properties = events.last?["properties"] as? InternalProperties
XCTAssertEqual(properties?["Cool Property"] as? [Int], [12345,301])
XCTAssertEqual(properties?["Super Property 2"] as? String, "p2")
let people = peopleQueue(token: testMixpanel.apiToken)
XCTAssertEqual(people.count, 6)
XCTAssertEqual(people[0]["$distinct_id"] as? String, "demo_user")
XCTAssertEqual(people[0]["$token"] as? String, "testToken")
let appendProperties = people[5]["$append"] as! InternalProperties
XCTAssertEqual(appendProperties["d"] as? String, "goodbye")
let group = groupQueue(token: testMixpanel.apiToken)
XCTAssertEqual(group.count, 2)
XCTAssertEqual(group[0]["$group_key"] as? String, "Cool Property")
let setProperties = group[0]["$set"] as! InternalProperties
XCTAssertEqual(setProperties["g"] as? String, "yo")
let setProperties2 = group[1]["$set"] as! InternalProperties
XCTAssertEqual(setProperties2["a"] as? Int, 1)
XCTAssertTrue(MixpanelPersistence.loadOptOutStatusFlag(apiToken: token)!)
XCTAssertTrue(MixpanelPersistence.loadAutomacticEventsEnabledFlag(apiToken: token))
//timedEvents
let testTimedEvents = MixpanelPersistence.loadTimedEvents(apiToken: token)
XCTAssertEqual(testTimedEvents.count, 3)
XCTAssertNotNil(testTimedEvents["Time Event A"])
XCTAssertNotNil(testTimedEvents["Time Event B"])
XCTAssertNotNil(testTimedEvents["Time Event C"])
let identity = MixpanelPersistence.loadIdentity(apiToken: token)
XCTAssertEqual(identity.distinctID, "demo_user")
XCTAssertEqual(identity.peopleDistinctID, "demo_user")
XCTAssertNotNil(identity.anonymousId)
XCTAssertEqual(identity.userId, "demo_user")
XCTAssertEqual(identity.alias, "New Alias")
XCTAssertEqual(identity.hadPersistedDistinctId, false)
let superProperties = MixpanelPersistence.loadSuperProperties(apiToken: token)
XCTAssertEqual(superProperties.count, 7)
XCTAssertEqual(superProperties["Super Property 1"] as? Int, 1)
XCTAssertEqual(superProperties["Super Property 7"] as? NSNull, NSNull())
removeDBfile("testToken")
}
func prepareForMigrationFiles(_ fileNames: [String]) {
for fileName in fileNames {
let fileManager = FileManager.default
let filepath = Bundle(for: type(of: self)).url(forResource: fileName, withExtension: nil)!
let libraryUrls = fileManager.urls(for: .libraryDirectory,
in: .userDomainMask)
let destURL = libraryUrls.first?.appendingPathComponent(fileName)
do {
try FileManager.default.copyItem(at: filepath, to: destURL!)
} catch let error {
print(error)
}
}
}
}

View file

@ -8,10 +8,10 @@ let package = Package(
.iOS(.v9),
.tvOS(.v9),
.macOS(.v10_10),
.watchOS(.v3),
.watchOS(.v3)
],
products: [
.library(name: "Mixpanel", targets: ["Mixpanel"]),
.library(name: "Mixpanel", targets: ["Mixpanel"])
],
targets: [
.target(
@ -21,8 +21,8 @@ let package = Package(
"Info.plist"
],
swiftSettings: [
.define("DECIDE", .when(platforms: [.iOS])),
.define("DECIDE", .when(platforms: [.iOS]))
]
),
)
]
)

View file

@ -6,7 +6,7 @@
// Copyright © 2017 Mixpanel. All rights reserved.
//
protocol AEDelegate {
protocol AEDelegate: AnyObject {
func track(event: String?, properties: Properties?)
func setOnce(properties: Properties)
func increment(property: String, by: Double)
@ -21,26 +21,26 @@ class AutomaticEvents: NSObject, SKPaymentTransactionObserver, SKProductsRequest
var _minimumSessionDuration: UInt64 = 10000
var minimumSessionDuration: UInt64 {
set {
_minimumSessionDuration = newValue
}
get {
return _minimumSessionDuration
}
set {
_minimumSessionDuration = newValue
}
}
var _maximumSessionDuration: UInt64 = UINT64_MAX
var maximumSessionDuration: UInt64 {
set {
_maximumSessionDuration = newValue
}
get {
return _maximumSessionDuration
}
set {
_maximumSessionDuration = newValue
}
}
var awaitingTransactions = [String: SKPaymentTransaction]()
let defaults = UserDefaults(suiteName: "Mixpanel")
var delegate: AEDelegate?
weak var delegate: AEDelegate?
var sessionLength: TimeInterval = 0
var sessionStartTime: TimeInterval = Date().timeIntervalSince1970
var hasAddedObserver = false
@ -121,7 +121,6 @@ class AutomaticEvents: NSObject, SKPaymentTransactionObserver, SKProductsRequest
case .purchased:
productIdentifiers.insert(trans.payment.productIdentifier)
awaitingTransactions[trans.payment.productIdentifier] = trans
break
case .failed: break
case .restored: break
default: break
@ -172,4 +171,3 @@ class AutomaticEvents: NSObject, SKPaymentTransactionObserver, SKProductsRequest
}
#endif

View file

@ -22,7 +22,6 @@ class Decide {
let lock: ReadWriteLock
var decideFetched = false
let mixpanelPersistence: MixpanelPersistence
required init(basePathIdentifier: String, lock: ReadWriteLock, mixpanelPersistence: MixpanelPersistence) {
self.decideRequest = DecideRequest(basePathIdentifier: basePathIdentifier)

View file

@ -8,7 +8,7 @@
import Foundation
protocol FlushDelegate {
protocol FlushDelegate: AnyObject {
func flush(completion: (() -> Void)?)
func flushSuccess(type: FlushType, ids: [Int32])
@ -19,7 +19,7 @@ protocol FlushDelegate {
class Flush: AppLifecycle {
var timer: Timer?
var delegate: FlushDelegate?
weak var delegate: FlushDelegate?
var useIPAddressForGeoLocation = true
var flushRequest: FlushRequest
var flushOnBackground = true
@ -27,6 +27,11 @@ class Flush: AppLifecycle {
private let flushIntervalReadWriteLock: DispatchQueue
var flushInterval: Double {
get {
flushIntervalReadWriteLock.sync {
return _flushInterval
}
}
set {
flushIntervalReadWriteLock.sync(flags: .barrier, execute: {
_flushInterval = newValue
@ -35,11 +40,6 @@ class Flush: AppLifecycle {
delegate?.flush(completion: nil)
startFlushTimer()
}
get {
flushIntervalReadWriteLock.sync {
return _flushInterval
}
}
}
required init(basePathIdentifier: String) {
@ -47,7 +47,6 @@ class Flush: AppLifecycle {
flushIntervalReadWriteLock = DispatchQueue(label: "com.mixpanel.flush_interval.lock", qos: .utility, attributes: .concurrent)
}
func flushQueue(type: FlushType, queue: Queue) {
if flushRequest.requestNotAllowed() {
return
@ -119,7 +118,9 @@ class Flush: AppLifecycle {
if success {
// remove
self.delegate?.flushSuccess(type: type, ids: ids)
mutableQueue = self.removeProcessedBatch(batchSize: batchSize, queue: mutableQueue, type: type)
mutableQueue = self.removeProcessedBatch(batchSize: batchSize,
queue: mutableQueue,
type: type)
}
shouldContinue = success
semaphore.signal()
@ -143,7 +144,6 @@ class Flush: AppLifecycle {
return shadowQueue
}
// MARK: - Lifecycle
func applicationDidBecomeActive() {
startFlushTimer()

View file

@ -17,11 +17,17 @@ open class Group {
let lock: ReadWriteLock
let groupKey: String
let groupID: MixpanelType
var delegate: FlushDelegate?
weak var delegate: FlushDelegate?
let metadata: SessionMetadata
let mixpanelPersistence: MixpanelPersistence
init(apiToken: String, serialQueue: DispatchQueue, lock: ReadWriteLock, groupKey: String, groupID: MixpanelType, metadata: SessionMetadata, mixpanelPersistence: MixpanelPersistence) {
init(apiToken: String,
serialQueue: DispatchQueue,
lock: ReadWriteLock,
groupKey: String,
groupID: MixpanelType,
metadata: SessionMetadata,
mixpanelPersistence: MixpanelPersistence) {
self.apiToken = apiToken
self.serialQueue = serialQueue
self.lock = lock

View file

@ -32,7 +32,7 @@ class JSONHandler {
}
class func deserializeData(_ data: Data) -> MPObjectToParse? {
var object: MPObjectToParse? = nil
var object: MPObjectToParse?
do {
object = try JSONSerialization.jsonObject(with: data, options: [])
} catch {

View file

@ -53,8 +53,7 @@ class MPDB {
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 {
} else {
Logger.info(message: "Successfully opened connection to database at path: \(dbPath)")
createTables()
}
@ -76,8 +75,7 @@ class MPDB {
try manager.removeItem(atPath: dbPath)
Logger.info(message: "Deleted database file at path: \(dbPath)")
}
}
catch let error {
} catch let error {
Logger.error(message: "Unable to remove database file at path: \(dbPath), error: \(error)")
}
}
@ -87,8 +85,9 @@ class MPDB {
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? = nil
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 {
Logger.info(message: "\(tableName) table created")
@ -114,7 +113,7 @@ class MPDB {
if let db = connection {
let tableName = tableNameFor(persistenceType)
let insertString = "INSERT INTO \(tableName) (data, flag, time) VALUES(?, ?, ?);"
var insertStatement: OpaquePointer? = nil
var insertStatement: OpaquePointer?
data.withUnsafeBytes { rawBuffer in
if let pointer = rawBuffer.baseAddress {
if sqlite3_prepare_v2(db, insertString, -1, &insertStatement, nil) == SQLITE_OK {
@ -143,7 +142,7 @@ class MPDB {
if let db = connection {
let tableName = tableNameFor(persistenceType)
let deleteString = "DELETE FROM \(tableName)\(ids.isEmpty ? "" : " WHERE id IN \(idsSqlString(ids))")"
var deleteStatement: OpaquePointer? = nil
var deleteStatement: OpaquePointer?
if sqlite3_prepare_v2(db, deleteString, -1, &deleteStatement, nil) == SQLITE_OK {
if sqlite3_step(deleteStatement) == SQLITE_DONE {
Logger.info(message: "Succesfully deleted rows from table \(tableName)")
@ -175,7 +174,7 @@ class MPDB {
if let db = connection {
let tableName = tableNameFor(persistenceType)
let updateString = "UPDATE \(tableName) SET flag = \(newFlag) where flag = \(!newFlag)"
var updateStatement: OpaquePointer? = nil
var updateStatement: OpaquePointer?
if sqlite3_prepare_v2(db, updateString, -1, &updateStatement, nil) == SQLITE_OK {
if sqlite3_step(updateStatement) == SQLITE_DONE {
Logger.info(message: "Succesfully update rows from table \(tableName)")
@ -193,13 +192,16 @@ class MPDB {
}
}
func readRows(_ persistenceType: PersistenceType, numRows: Int, flag: Bool = false) -> [InternalProperties] {
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? = nil
var rowsRead: Int = 0
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) {

View file

@ -38,9 +38,9 @@ open class Mixpanel {
flushInterval: Double = 60,
instanceName: String = UUID().uuidString,
optOutTrackingByDefault: Bool = false) -> MixpanelInstance {
return MixpanelManager.sharedInstance.initialize(token: apiToken,
return MixpanelManager.sharedInstance.initialize(token: apiToken,
flushInterval: flushInterval,
instanceName: instanceName,
instanceName: instanceName,
optOutTrackingByDefault: optOutTrackingByDefault)
}
#else

View file

@ -23,7 +23,7 @@ import CoreTelephony
/**
* Delegate protocol for controlling the Mixpanel API's network behavior.
*/
public protocol MixpanelDelegate {
public protocol MixpanelDelegate: AnyObject {
/**
Asks the delegate if data should be uploaded to the server.
@ -49,7 +49,7 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele
open var apiToken = ""
/// The a MixpanelDelegate object that gives control over Mixpanel network activity.
open var delegate: MixpanelDelegate?
open weak var delegate: MixpanelDelegate?
/// distinctId string that uniquely identifies the current user.
open var distinctId = ""
@ -72,7 +72,6 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele
let mixpanelPersistence: MixpanelPersistence
/// Accessor to the Mixpanel People API object.
var groups: [String: Group] = [:]
@ -84,42 +83,45 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele
/// 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)
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.
/// 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 {
set {
flushInstance.flushInterval = newValue
}
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 {
set {
flushInstance.flushOnBackground = newValue
}
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 {
set {
flushInstance.useIPAddressForGeoLocation = newValue
}
get {
return flushInstance.useIPAddressForGeoLocation
}
set {
flushInstance.useIPAddressForGeoLocation = newValue
}
}
/// The base URL used for Mixpanel API requests.
@ -168,23 +170,23 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele
/// The minimum session duration (ms) that is tracked in automatic events.
/// The default value is 10000 (10 seconds).
open var minimumSessionDuration: UInt64 {
set {
automaticEvents.minimumSessionDuration = newValue
}
get {
return automaticEvents.minimumSessionDuration
}
set {
automaticEvents.minimumSessionDuration = newValue
}
}
/// The maximum session duration (ms) that is tracked in automatic events.
/// The default value is UINT64_MAX (no maximum session duration).
open var maximumSessionDuration: UInt64 {
set {
automaticEvents.maximumSessionDuration = newValue
}
get {
return automaticEvents.maximumSessionDuration
}
set {
automaticEvents.maximumSessionDuration = newValue
}
}
#endif // DECIDE
@ -235,7 +237,9 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele
#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?) {
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
@ -389,7 +393,8 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele
#if !os(OSX) && !os(watchOS)
static func sharedUIApplication() -> UIApplication? {
guard let sharedApplication = UIApplication.perform(NSSelectorFromString("sharedApplication"))?.takeUnretainedValue() as? UIApplication else {
guard let sharedApplication =
UIApplication.perform(NSSelectorFromString("sharedApplication"))?.takeUnretainedValue() as? UIApplication else {
return nil
}
return sharedApplication
@ -507,7 +512,8 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele
#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)
let serialNumberAsCFString =
IORegistryEntryCreateCFProperty(platformExpert, kIOPlatformSerialNumberKey as CFString, kCFAllocatorDefault, 0)
IOObjectRelease(platformExpert)
return (serialNumberAsCFString?.takeUnretainedValue() as? String)
}
@ -752,7 +758,6 @@ extension MixpanelInstance {
extension MixpanelInstance {
// MARK: - Persistence
open func archive() {
self.readWriteLock.read {
MixpanelPersistence.saveTimedEvents(timedEvents: timedEvents, apiToken: apiToken)
@ -767,7 +772,6 @@ extension MixpanelInstance {
}
}
func unarchive() {
optOutStatus = MixpanelPersistence.loadOptOutStatusFlag(apiToken: apiToken)
superProperties = MixpanelPersistence.loadSuperProperties(apiToken: apiToken)
@ -781,7 +785,7 @@ extension MixpanelInstance {
mixpanelIdentity.alias,
mixpanelIdentity.hadPersistedDistinctId
)
if distinctId == "" {
if distinctId.isEmpty {
distinctId = defaultDistinctId()
anonymousId = distinctId
hadPersistedDistinctId = nil
@ -902,15 +906,18 @@ extension MixpanelInstance {
guard let self = self else {
return
}
self.timedEvents = self.trackInstance.track(event: event, properties: properties,
timedEvents: self.timedEvents,
superProperties: self.superProperties,
distinctId: self.distinctId,
anonymousId: self.anonymousId,
userId: self.userId,
hadPersistedDistinctId: self.hadPersistedDistinctId,
epochInterval: epochInterval)
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() {
@ -964,7 +971,13 @@ extension MixpanelInstance {
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)
groups[key] = Group(apiToken: apiToken,
serialQueue: trackingQueue,
lock: self.readWriteLock,
groupKey: groupKey,
groupID: groupID,
metadata: sessionMetadata,
mixpanelPersistence: mixpanelPersistence)
groupsShadow = groups
}
return groupsShadow[key]!
@ -973,7 +986,13 @@ extension MixpanelInstance {
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)
let newGroup = Group(apiToken: apiToken,
serialQueue: trackingQueue,
lock: self.readWriteLock,
groupKey: groupKey,
groupID: groupID,
metadata: sessionMetadata,
mixpanelPersistence: mixpanelPersistence)
readWriteLock.write {
groups[key] = newGroup
}
@ -1058,8 +1077,7 @@ extension MixpanelInstance {
- parameter event: the name of the event to clear the timer for
*/
open func clearTimedEvent(event: String) {
trackingQueue.async {
[weak self, event] in
trackingQueue.async { [weak self, event] in
guard let self = self else { return }
let updatedTimedEvents = self.trackInstance.clearTimedEvent(event: event, timedEvents: self.timedEvents)
@ -1309,7 +1327,8 @@ extension MixpanelInstance {
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
- 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
@ -1330,7 +1349,6 @@ extension MixpanelInstance {
return optOutStatus ?? false
}
// MARK: - AEDelegate
func increment(property: String, by: Double) {
people?.increment(property: property, by: by)

View file

@ -108,7 +108,6 @@ class MixpanelPersistence {
return defaults.object(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.optOutStatus)") as? Bool
}
static func saveAutomacticEventsEnabledFlag(value: Bool, fromDecide: Bool, apiToken: String) {
guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else {
return
@ -130,7 +129,8 @@ class MixpanelPersistence {
guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else {
return true
}
if defaults.object(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.automaticEventEnabled)") == nil && defaults.object(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.automaticEventEnabledFromDecide)") == nil {
if defaults.object(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.automaticEventEnabled)") == nil &&
defaults.object(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.automaticEventEnabledFromDecide)") == nil {
return true // default true
}
if defaults.object(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.automaticEventEnabled)") != nil {
@ -199,7 +199,12 @@ class MixpanelPersistence {
static func loadIdentity(apiToken: String) -> MixpanelIdentity {
guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else {
return MixpanelIdentity.init(distinctID: "", peopleDistinctID: nil, anonymousId: nil, userId: nil, alias: nil, hadPersistedDistinctId: nil)
return MixpanelIdentity.init(distinctID: "",
peopleDistinctID: nil,
anonymousId: nil,
userId: nil,
alias: nil,
hadPersistedDistinctId: nil)
}
let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(apiToken)-"
return MixpanelIdentity.init(
@ -266,7 +271,7 @@ class MixpanelPersistence {
MixpanelPersistence.saveOptOutStatusFlag(value: optOutFlag, apiToken: apiToken)
}
if let automaticEventsFlag = automaticEventsEnabled {
MixpanelPersistence.saveAutomacticEventsEnabledFlag(value: automaticEventsFlag, fromDecide: false, apiToken: apiToken) // should fromDecide = false here?
MixpanelPersistence.saveAutomacticEventsEnabledFlag(value: automaticEventsFlag, fromDecide: false, apiToken: apiToken)
}
return
}
@ -327,7 +332,7 @@ class MixpanelPersistence {
removeArchivedFile(atPath: groupsFile)
}
if let propsFile = filePathWithType("properties") {
removeArchivedFile(atPath:propsFile)
removeArchivedFile(atPath: propsFile)
}
if let optOutFile = filePathWithType("optOutStatus") {
removeArchivedFile(atPath: optOutFile)

View file

@ -62,9 +62,9 @@ class Network {
}
class func apiRequest<A>(base: String,
resource: Resource<A>,
failure: @escaping (Reason, Data?, URLResponse?) -> Void,
success: @escaping (A, URLResponse?) -> Void) {
resource: Resource<A>,
failure: @escaping (Reason, Data?, URLResponse?) -> Void,
success: @escaping (A, URLResponse?) -> Void) {
guard let request = buildURLRequest(base, resource: resource) else {
return
}
@ -116,11 +116,11 @@ class Network {
}
class func buildResource<A>(path: String,
method: RequestMethod,
requestBody: Data? = nil,
queryItems: [URLQueryItem]? = nil,
headers: [String: String],
parse: @escaping (Data) -> A?) -> Resource<A> {
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,

View file

@ -23,12 +23,15 @@ open class People {
let serialQueue: DispatchQueue!
let lock: ReadWriteLock
var distinctId: String?
var delegate: FlushDelegate?
weak var delegate: FlushDelegate?
let metadata: SessionMetadata
let mixpanelPersistence: MixpanelPersistence
init(apiToken: String, serialQueue: DispatchQueue, lock: ReadWriteLock, metadata: SessionMetadata, mixpanelPersistence: MixpanelPersistence) {
init(apiToken: String,
serialQueue: DispatchQueue,
lock: ReadWriteLock,
metadata: SessionMetadata,
mixpanelPersistence: MixpanelPersistence) {
self.apiToken = apiToken
self.serialQueue = serialQueue
self.lock = lock
@ -94,7 +97,6 @@ open class People {
}
}
func merge(properties: InternalProperties) {
addPeopleRecordToQueueWithAction("$merge", properties: properties)
}

View file

@ -35,10 +35,7 @@ class Track {
properties: Properties? = nil,
timedEvents: InternalProperties,
superProperties: InternalProperties,
distinctId: String,
anonymousId: String?,
userId: String?,
hadPersistedDistinctId: Bool?,
mixpanelIdentity: MixpanelIdentity,
epochInterval: Double) -> InternalProperties {
var ev = event
if ev == nil || ev!.isEmpty {
@ -63,15 +60,15 @@ class Track {
shadowTimedEvents.removeValue(forKey: ev!)
p["$duration"] = Double(String(format: "%.3f", epochInterval - eventStartTime))
}
p["distinct_id"] = distinctId
if anonymousId != nil {
p["$device_id"] = anonymousId
p["distinct_id"] = mixpanelIdentity.distinctID
if mixpanelIdentity.anonymousId != nil {
p["$device_id"] = mixpanelIdentity.anonymousId
}
if userId != nil {
p["$user_id"] = userId
if mixpanelIdentity.userId != nil {
p["$user_id"] = mixpanelIdentity.userId
}
if hadPersistedDistinctId != nil {
p["$had_persisted_distinct_id"] = hadPersistedDistinctId
if mixpanelIdentity.hadPersistedDistinctId != nil {
p["$had_persisted_distinct_id"] = mixpanelIdentity.hadPersistedDistinctId
}
p += superProperties