diff --git a/.swiftlint.yml b/.swiftlint.yml index bca32633..d47022c3 100755 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -61,3 +61,4 @@ disabled_rules: - type_body_length - variable_name - trailing_whitespace + - unused_optional_binding diff --git a/MixpanelDemo/MixpanelDemo.xcodeproj/project.pbxproj b/MixpanelDemo/MixpanelDemo.xcodeproj/project.pbxproj index c57b6ecb..840228ae 100644 --- a/MixpanelDemo/MixpanelDemo.xcodeproj/project.pbxproj +++ b/MixpanelDemo/MixpanelDemo.xcodeproj/project.pbxproj @@ -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 = ""; }; 8654F2FC2671636B00ACEED5 /* MixpanelOptOutTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MixpanelOptOutTests.swift; sourceTree = ""; }; 8654F3032671C4B000ACEED5 /* MixpanelGroupTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MixpanelGroupTests.swift; sourceTree = ""; }; + 8675E99C26DF356A0096858F /* mixpanel-testToken-events */ = {isa = PBXFileReference; explicitFileType = text.pbxproject; path = "mixpanel-testToken-events"; sourceTree = ""; }; + 8675E99E26DF356A0096858F /* mixpanel-testToken-groups */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = "mixpanel-testToken-groups"; sourceTree = ""; }; + 8675E99F26DF356A0096858F /* mixpanel-testToken-people */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = "mixpanel-testToken-people"; sourceTree = ""; }; + 8675E9AA26E144FE0096858F /* mixpanel-testToken-optOutStatus */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = "mixpanel-testToken-optOutStatus"; sourceTree = ""; }; + 8675E9B326E171AE0096858F /* mixpanel-testToken-properties */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = "mixpanel-testToken-properties"; sourceTree = ""; }; 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 = ""; }; 86F86E9022440C5F00B69832 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -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 = ""; }; + 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 = ""; + }; 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; }; diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift index 1d3852b6..96efa3bb 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift @@ -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) + } + } + } } diff --git a/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-events b/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-events new file mode 100644 index 00000000..e8b41994 Binary files /dev/null and b/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-events differ diff --git a/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-groups b/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-groups new file mode 100644 index 00000000..c493817f Binary files /dev/null and b/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-groups differ diff --git a/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-optOutStatus b/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-optOutStatus new file mode 100644 index 00000000..fef18536 Binary files /dev/null and b/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-optOutStatus differ diff --git a/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-people b/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-people new file mode 100644 index 00000000..045e92e7 Binary files /dev/null and b/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-people differ diff --git a/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-properties b/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-properties new file mode 100644 index 00000000..96982858 Binary files /dev/null and b/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-properties differ diff --git a/Package.swift b/Package.swift index dc48a561..2f187572 100644 --- a/Package.swift +++ b/Package.swift @@ -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])) ] - ), + ) ] ) diff --git a/Sources/AutomaticEvents.swift b/Sources/AutomaticEvents.swift index 86fea051..ce221364 100644 --- a/Sources/AutomaticEvents.swift +++ b/Sources/AutomaticEvents.swift @@ -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 - diff --git a/Sources/Decide.swift b/Sources/Decide.swift index 2effafff..744e4eeb 100644 --- a/Sources/Decide.swift +++ b/Sources/Decide.swift @@ -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) diff --git a/Sources/Flush.swift b/Sources/Flush.swift index 27adffb1..eac521f4 100644 --- a/Sources/Flush.swift +++ b/Sources/Flush.swift @@ -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() diff --git a/Sources/Group.swift b/Sources/Group.swift index 98699405..47fa6e1a 100644 --- a/Sources/Group.swift +++ b/Sources/Group.swift @@ -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 diff --git a/Sources/JSONHandler.swift b/Sources/JSONHandler.swift index 3d3cf6d8..d1927c93 100644 --- a/Sources/JSONHandler.swift +++ b/Sources/JSONHandler.swift @@ -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 { diff --git a/Sources/MPDB.swift b/Sources/MPDB.swift index 0a61d1af..a3f368ab 100644 --- a/Sources/MPDB.swift +++ b/Sources/MPDB.swift @@ -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) { diff --git a/Sources/Mixpanel.swift b/Sources/Mixpanel.swift index bd02cce7..cd1ea709 100644 --- a/Sources/Mixpanel.swift +++ b/Sources/Mixpanel.swift @@ -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 diff --git a/Sources/MixpanelInstance.swift b/Sources/MixpanelInstance.swift index 1bb996d1..d5d7594b 100644 --- a/Sources/MixpanelInstance.swift +++ b/Sources/MixpanelInstance.swift @@ -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) diff --git a/Sources/MixpanelPersistence.swift b/Sources/MixpanelPersistence.swift index 171704f5..364c576e 100644 --- a/Sources/MixpanelPersistence.swift +++ b/Sources/MixpanelPersistence.swift @@ -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) diff --git a/Sources/Network.swift b/Sources/Network.swift index b300aad3..7552064b 100644 --- a/Sources/Network.swift +++ b/Sources/Network.swift @@ -62,9 +62,9 @@ class Network { } class func apiRequest(base: String, - resource: Resource, - failure: @escaping (Reason, Data?, URLResponse?) -> Void, - success: @escaping (A, URLResponse?) -> Void) { + resource: Resource, + 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(path: String, - method: RequestMethod, - requestBody: Data? = nil, - queryItems: [URLQueryItem]? = nil, - headers: [String: String], - parse: @escaping (Data) -> A?) -> Resource { + method: RequestMethod, + requestBody: Data? = nil, + queryItems: [URLQueryItem]? = nil, + headers: [String: String], + parse: @escaping (Data) -> A?) -> Resource { return Resource(path: path, method: method, requestBody: requestBody, diff --git a/Sources/People.swift b/Sources/People.swift index a46e6ba1..12f08a26 100644 --- a/Sources/People.swift +++ b/Sources/People.swift @@ -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) } diff --git a/Sources/Track.swift b/Sources/Track.swift index 959b83ea..6b8afd28 100644 --- a/Sources/Track.swift +++ b/Sources/Track.swift @@ -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