diff --git a/.swiftlint.yml b/.swiftlint.yml index bca326333..d47022c3c 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/Mixpanel-swift.podspec b/Mixpanel-swift.podspec index b9051b220..dd5b1269d 100644 --- a/Mixpanel-swift.podspec +++ b/Mixpanel-swift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Mixpanel-swift' - s.version = '3.0.0.beta.2' + s.version = '3.0.0.beta.4' s.module_name = 'Mixpanel' s.license = 'Apache License, Version 2.0' s.summary = 'Mixpanel tracking library for iOS (Swift)' @@ -18,9 +18,9 @@ Pod::Spec.new do |s| base_source_files = ['Sources/Network.swift', 'Sources/FlushRequest.swift', 'Sources/PrintLogging.swift', 'Sources/FileLogging.swift', 'Sources/Logger.swift', 'Sources/JSONHandler.swift', 'Sources/Error.swift', 'Sources/AutomaticProperties.swift', 'Sources/Constants.swift', 'Sources/MixpanelType.swift', 'Sources/Mixpanel.swift', 'Sources/MixpanelInstance.swift', - 'Sources/Persistence.swift', 'Sources/Flush.swift','Sources/Track.swift', 'Sources/People.swift', 'Sources/AutomaticEvents.swift', + 'Sources/Flush.swift','Sources/Track.swift', 'Sources/People.swift', 'Sources/AutomaticEvents.swift', 'Sources/Group.swift', - 'Sources/ReadWriteLock.swift', 'Sources/SessionMetadata.swift'] + 'Sources/ReadWriteLock.swift', 'Sources/SessionMetadata.swift', 'Sources/MPDB.swift', 'Sources/MixpanelPersistence.swift'] s.tvos.deployment_target = '9.0' s.tvos.frameworks = 'UIKit', 'Foundation' s.tvos.pod_target_xcconfig = { diff --git a/Mixpanel.xcodeproj/project.pbxproj b/Mixpanel.xcodeproj/project.pbxproj index 493223ad3..8c4a403d2 100644 --- a/Mixpanel.xcodeproj/project.pbxproj +++ b/Mixpanel.xcodeproj/project.pbxproj @@ -13,6 +13,14 @@ 673ABE3A21360CBE00B1784B /* Group.swift in Sources */ = {isa = PBXBuildFile; fileRef = 673ABE3921360CBE00B1784B /* Group.swift */; }; 67FF65E421878414005161FA /* Group.swift in Sources */ = {isa = PBXBuildFile; fileRef = 673ABE3921360CBE00B1784B /* Group.swift */; }; 67FF65E521878416005161FA /* Group.swift in Sources */ = {isa = PBXBuildFile; fileRef = 673ABE3921360CBE00B1784B /* Group.swift */; }; + 8625BEBB26D045CE0009BAA9 /* MPDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8625BEBA26D045CE0009BAA9 /* MPDB.swift */; }; + 8625BEBC26D045CE0009BAA9 /* MPDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8625BEBA26D045CE0009BAA9 /* MPDB.swift */; }; + 8625BEBD26D045CE0009BAA9 /* MPDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8625BEBA26D045CE0009BAA9 /* MPDB.swift */; }; + 8625BEBE26D045CE0009BAA9 /* MPDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8625BEBA26D045CE0009BAA9 /* MPDB.swift */; }; + 868550AC2699096F001FCDDC /* MixpanelPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 868550AB2699096F001FCDDC /* MixpanelPersistence.swift */; }; + 868550AD2699096F001FCDDC /* MixpanelPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 868550AB2699096F001FCDDC /* MixpanelPersistence.swift */; }; + 868550AE2699096F001FCDDC /* MixpanelPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 868550AB2699096F001FCDDC /* MixpanelPersistence.swift */; }; + 868550AF2699096F001FCDDC /* MixpanelPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 868550AB2699096F001FCDDC /* MixpanelPersistence.swift */; }; 86F86EB7224439D300B69832 /* Mixpanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E115948A1CFF1538007F8B4F /* Mixpanel.swift */; }; 86F86EB8224439D300B69832 /* MixpanelInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E115948D1D000709007F8B4F /* MixpanelInstance.swift */; }; 86F86EB9224439DC00B69832 /* AutomaticProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D335CF1D3059A800E68E12 /* AutomaticProperties.swift */; }; @@ -21,7 +29,6 @@ 86F86EBC224439F100B69832 /* PrintLogging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DD56801D306B7B0045D3DB /* PrintLogging.swift */; }; 86F86EBD224439F500B69832 /* Flush.swift in Sources */ = {isa = PBXBuildFile; fileRef = E115949E1D01BE14007F8B4F /* Flush.swift */; }; 86F86EBE224439FA00B69832 /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11594961D006022007F8B4F /* Network.swift */; }; - 86F86EBF22443A0000B69832 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11594921D0036E1007F8B4F /* Persistence.swift */; }; 86F86EC022443A0800B69832 /* MixpanelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E165228E1D6781DF000D5949 /* MixpanelType.swift */; }; 86F86EC122443A0E00B69832 /* JSONHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11594981D01689F007F8B4F /* JSONHandler.swift */; }; 86F86EC222443A1300B69832 /* Track.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11594A01D01C597007F8B4F /* Track.swift */; }; @@ -38,7 +45,6 @@ E10D118D1EC0F30900195CCD /* AutomaticEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = E151FA371E70DFB5002EF53D /* AutomaticEvents.swift */; }; E115948B1CFF1538007F8B4F /* Mixpanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E115948A1CFF1538007F8B4F /* Mixpanel.swift */; }; E115948E1D000709007F8B4F /* MixpanelInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E115948D1D000709007F8B4F /* MixpanelInstance.swift */; }; - E11594931D0036E1007F8B4F /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11594921D0036E1007F8B4F /* Persistence.swift */; }; E11594971D006022007F8B4F /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11594961D006022007F8B4F /* Network.swift */; }; E11594991D01689F007F8B4F /* JSONHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11594981D01689F007F8B4F /* JSONHandler.swift */; }; E115949F1D01BE14007F8B4F /* Flush.swift in Sources */ = {isa = PBXBuildFile; fileRef = E115949E1D01BE14007F8B4F /* Flush.swift */; }; @@ -48,7 +54,6 @@ E12782BD1D4AB5CB0025FB05 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DD56791D306B740045D3DB /* Logger.swift */; }; E12782BE1D4AB5CB0025FB05 /* Mixpanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E115948A1CFF1538007F8B4F /* Mixpanel.swift */; }; E12782BF1D4AB5CB0025FB05 /* MixpanelInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E115948D1D000709007F8B4F /* MixpanelInstance.swift */; }; - E12782C01D4AB5CB0025FB05 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11594921D0036E1007F8B4F /* Persistence.swift */; }; E12782C11D4AB5CB0025FB05 /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11594961D006022007F8B4F /* Network.swift */; }; E12782C21D4AB5CB0025FB05 /* JSONHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11594981D01689F007F8B4F /* JSONHandler.swift */; }; E12782C31D4AB5CB0025FB05 /* Flush.swift in Sources */ = {isa = PBXBuildFile; fileRef = E115949E1D01BE14007F8B4F /* Flush.swift */; }; @@ -86,7 +91,6 @@ E1F15FDE1E64B60A00391AE3 /* MixpanelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E165228E1D6781DF000D5949 /* MixpanelType.swift */; }; E1F15FDF1E64B60D00391AE3 /* Mixpanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E115948A1CFF1538007F8B4F /* Mixpanel.swift */; }; E1F15FE01E64B60D00391AE3 /* MixpanelInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E115948D1D000709007F8B4F /* MixpanelInstance.swift */; }; - E1F15FE11E64B60D00391AE3 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11594921D0036E1007F8B4F /* Persistence.swift */; }; E1F15FE21E64B60D00391AE3 /* Flush.swift in Sources */ = {isa = PBXBuildFile; fileRef = E115949E1D01BE14007F8B4F /* Flush.swift */; }; E1F15FE31E64B60D00391AE3 /* Track.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11594A01D01C597007F8B4F /* Track.swift */; }; E1F15FE41E64B60D00391AE3 /* People.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15FF7C71D0435670076CDE3 /* People.swift */; }; @@ -98,13 +102,14 @@ 51DD56801D306B7B0045D3DB /* PrintLogging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrintLogging.swift; sourceTree = ""; }; 51DD56811D306B7B0045D3DB /* FileLogging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileLogging.swift; sourceTree = ""; }; 673ABE3921360CBE00B1784B /* Group.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Group.swift; sourceTree = ""; }; + 8625BEBA26D045CE0009BAA9 /* MPDB.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MPDB.swift; sourceTree = ""; }; + 868550AB2699096F001FCDDC /* MixpanelPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MixpanelPersistence.swift; sourceTree = ""; }; 86F86E81224404BD00B69832 /* Mixpanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Mixpanel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BB9614161F3BB87700C3EF3E /* ReadWriteLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteLock.swift; sourceTree = ""; }; E115947D1CFF1491007F8B4F /* Mixpanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Mixpanel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E11594821CFF1491007F8B4F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E115948A1CFF1538007F8B4F /* Mixpanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mixpanel.swift; sourceTree = ""; }; E115948D1D000709007F8B4F /* MixpanelInstance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MixpanelInstance.swift; sourceTree = ""; }; - E11594921D0036E1007F8B4F /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; E11594961D006022007F8B4F /* Network.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; E11594981D01689F007F8B4F /* JSONHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONHandler.swift; sourceTree = ""; }; E115949E1D01BE14007F8B4F /* Flush.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Flush.swift; sourceTree = ""; }; @@ -215,7 +220,6 @@ E189D8FA1D5A692A007F3F29 /* Utilities */, E115948A1CFF1538007F8B4F /* Mixpanel.swift */, E115948D1D000709007F8B4F /* MixpanelInstance.swift */, - E11594921D0036E1007F8B4F /* Persistence.swift */, E115949E1D01BE14007F8B4F /* Flush.swift */, E11594A01D01C597007F8B4F /* Track.swift */, E15FF7C71D0435670076CDE3 /* People.swift */, @@ -258,8 +262,10 @@ E151FA371E70DFB5002EF53D /* AutomaticEvents.swift */, E1D335CD1D30578E00E68E12 /* Constants.swift */, E165228E1D6781DF000D5949 /* MixpanelType.swift */, + 8625BEBA26D045CE0009BAA9 /* MPDB.swift */, BB9614161F3BB87700C3EF3E /* ReadWriteLock.swift */, E190522C1F9FC1BC00900E5D /* SessionMetadata.swift */, + 868550AB2699096F001FCDDC /* MixpanelPersistence.swift */, ); name = Utilities; sourceTree = ""; @@ -484,13 +490,14 @@ 86F86EC622443A3100B69832 /* Error.swift in Sources */, 86F86EC522443A2C00B69832 /* People.swift in Sources */, 86F86EC422443A2300B69832 /* ReadWriteLock.swift in Sources */, + 8625BEBE26D045CE0009BAA9 /* MPDB.swift in Sources */, 86F86EC222443A1300B69832 /* Track.swift in Sources */, 86F86EC122443A0E00B69832 /* JSONHandler.swift in Sources */, 86F86EC022443A0800B69832 /* MixpanelType.swift in Sources */, - 86F86EBF22443A0000B69832 /* Persistence.swift in Sources */, 86F86EBE224439FA00B69832 /* Network.swift in Sources */, 86F86EBD224439F500B69832 /* Flush.swift in Sources */, 86F86EBC224439F100B69832 /* PrintLogging.swift in Sources */, + 868550AF2699096F001FCDDC /* MixpanelPersistence.swift in Sources */, 86F86EBB224439EB00B69832 /* FlushRequest.swift in Sources */, 86F86EBA224439E300B69832 /* Logger.swift in Sources */, 86F86EB9224439DC00B69832 /* AutomaticProperties.swift in Sources */, @@ -509,7 +516,6 @@ E151FA381E70DFB5002EF53D /* AutomaticEvents.swift in Sources */, E1D335CE1D30578E00E68E12 /* Constants.swift in Sources */, E115949F1D01BE14007F8B4F /* Flush.swift in Sources */, - E11594931D0036E1007F8B4F /* Persistence.swift in Sources */, E189D8F61D54ECBF007F3F29 /* Decide.swift in Sources */, E11594971D006022007F8B4F /* Network.swift in Sources */, E15FF7C81D0435670076CDE3 /* People.swift in Sources */, @@ -521,6 +527,8 @@ E165228F1D6781DF000D5949 /* MixpanelType.swift in Sources */, BB9614171F3BB87700C3EF3E /* ReadWriteLock.swift in Sources */, E190522D1F9FC1BC00900E5D /* SessionMetadata.swift in Sources */, + 8625BEBB26D045CE0009BAA9 /* MPDB.swift in Sources */, + 868550AC2699096F001FCDDC /* MixpanelPersistence.swift in Sources */, E1982BFF1D0AC2E2006B7330 /* Error.swift in Sources */, 51DD56841D306B7B0045D3DB /* FileLogging.swift in Sources */, E1D335CC1D303A0D00E68E12 /* FlushRequest.swift in Sources */, @@ -540,14 +548,15 @@ E12782BD1D4AB5CB0025FB05 /* Logger.swift in Sources */, E12782BE1D4AB5CB0025FB05 /* Mixpanel.swift in Sources */, E12782BF1D4AB5CB0025FB05 /* MixpanelInstance.swift in Sources */, - E12782C01D4AB5CB0025FB05 /* Persistence.swift in Sources */, E12782C11D4AB5CB0025FB05 /* Network.swift in Sources */, + 8625BEBC26D045CE0009BAA9 /* MPDB.swift in Sources */, E12782C21D4AB5CB0025FB05 /* JSONHandler.swift in Sources */, E12782C31D4AB5CB0025FB05 /* Flush.swift in Sources */, E12782C41D4AB5CB0025FB05 /* FlushRequest.swift in Sources */, E12782C51D4AB5CB0025FB05 /* Track.swift in Sources */, E12782C61D4AB5CB0025FB05 /* People.swift in Sources */, E19052001F9548F000900E5D /* ReadWriteLock.swift in Sources */, + 868550AD2699096F001FCDDC /* MixpanelPersistence.swift in Sources */, E12782C71D4AB5CB0025FB05 /* Error.swift in Sources */, E16522901D67D421000D5949 /* MixpanelType.swift in Sources */, E12782C81D4AB5CB0025FB05 /* Constants.swift in Sources */, @@ -568,13 +577,14 @@ E1F15FD91E64B60600391AE3 /* Logger.swift in Sources */, E1F15FD61E64B5FC00391AE3 /* FlushRequest.swift in Sources */, E1F15FD71E64B60200391AE3 /* PrintLogging.swift in Sources */, + 8625BEBD26D045CE0009BAA9 /* MPDB.swift in Sources */, E1F15FE21E64B60D00391AE3 /* Flush.swift in Sources */, E1F15FD51E64B5F800391AE3 /* Network.swift in Sources */, - E1F15FE11E64B60D00391AE3 /* Persistence.swift in Sources */, E1F15FDE1E64B60A00391AE3 /* MixpanelType.swift in Sources */, E1F15FDA1E64B60A00391AE3 /* JSONHandler.swift in Sources */, E1F15FE31E64B60D00391AE3 /* Track.swift in Sources */, E19052011F9548F000900E5D /* ReadWriteLock.swift in Sources */, + 868550AE2699096F001FCDDC /* MixpanelPersistence.swift in Sources */, E1F15FE41E64B60D00391AE3 /* People.swift in Sources */, E1F15FDB1E64B60A00391AE3 /* Error.swift in Sources */, E1F15FD81E64B60200391AE3 /* FileLogging.swift in Sources */, diff --git a/MixpanelDemo/MixpanelDemo.xcodeproj/project.pbxproj b/MixpanelDemo/MixpanelDemo.xcodeproj/project.pbxproj index c57b6ecbe..840228aec 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/MixpanelDemo.xcodeproj/xcshareddata/xcschemes/MixpanelDemo.xcscheme b/MixpanelDemo/MixpanelDemo.xcodeproj/xcshareddata/xcschemes/MixpanelDemo.xcscheme index 893ea1ed6..1ada79b62 100644 --- a/MixpanelDemo/MixpanelDemo.xcodeproj/xcshareddata/xcschemes/MixpanelDemo.xcscheme +++ b/MixpanelDemo/MixpanelDemo.xcodeproj/xcshareddata/xcschemes/MixpanelDemo.xcscheme @@ -27,6 +27,7 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" + disableMainThreadChecker = "YES" codeCoverageEnabled = "YES"> Bool { var ADD_YOUR_MIXPANEL_TOKEN_BELOW_🛠🛠🛠🛠🛠🛠: String - Mixpanel.initialize(token: "MIXPANEL_TOKEN") Mixpanel.mainInstance().loggingEnabled = true - Mixpanel.mainInstance().flushInterval = 5 - + return true } diff --git a/MixpanelDemo/MixpanelDemo/Base.lproj/Main.storyboard b/MixpanelDemo/MixpanelDemo/Base.lproj/Main.storyboard index 5941c833d..db3564bd9 100644 --- a/MixpanelDemo/MixpanelDemo/Base.lproj/Main.storyboard +++ b/MixpanelDemo/MixpanelDemo/Base.lproj/Main.storyboard @@ -1,9 +1,10 @@ - + - + + @@ -58,7 +59,7 @@ - - - + - + - + @@ -428,7 +406,7 @@ diff --git a/MixpanelDemo/MixpanelDemo/GDPRViewController.swift b/MixpanelDemo/MixpanelDemo/GDPRViewController.swift index e3ca89492..a3761fcca 100644 --- a/MixpanelDemo/MixpanelDemo/GDPRViewController.swift +++ b/MixpanelDemo/MixpanelDemo/GDPRViewController.swift @@ -62,10 +62,10 @@ class GDPRViewController: UIViewController, UITableViewDelegate, UITableViewData Mixpanel.mainInstance().optInTracking(distinctId: "aDistinctIdForOptIn", properties: p) descStr = "Opt In with distinctId 'aDistinctIdForOptIn' and \(p)" case 5: - Mixpanel.initialize(token: "a token id", optOutTrackingByDefault: true) + Mixpanel.initialize(token: "testtoken", optOutTrackingByDefault: true) descStr = "Init Mixpanel with default opt-out(sample only), to make it work, place it in your startup stage of your app" case 6: - Mixpanel.initialize(token: "a token id", optOutTrackingByDefault: false) + Mixpanel.initialize(token: "testtoken", optOutTrackingByDefault: false) descStr = "Init Mixpanel with default opt-in(sample only), to make it work, place it in your startup stage of your app" default: break diff --git a/MixpanelDemo/MixpanelDemo/LoginViewController.swift b/MixpanelDemo/MixpanelDemo/LoginViewController.swift index ac412c40a..fcd9a7079 100644 --- a/MixpanelDemo/MixpanelDemo/LoginViewController.swift +++ b/MixpanelDemo/MixpanelDemo/LoginViewController.swift @@ -32,7 +32,7 @@ class LoginViewController: UIViewController { @IBAction func start(_ sender: Any) { - Mixpanel.mainInstance().identify(distinctId: distinctIdTextField.text ?? "") + Mixpanel.mainInstance().identify(distinctId: distinctIdTextField.text ?? "demo_user") Mixpanel.mainInstance().people.set(property: "$name", to: nameTextField.text ?? "") Mixpanel.mainInstance().track(event: "Logged in") Mixpanel.mainInstance().flush() diff --git a/MixpanelDemo/MixpanelDemo/PeopleViewController.swift b/MixpanelDemo/MixpanelDemo/PeopleViewController.swift index 82dbe8a73..ee4f25b7b 100644 --- a/MixpanelDemo/MixpanelDemo/PeopleViewController.swift +++ b/MixpanelDemo/MixpanelDemo/PeopleViewController.swift @@ -101,7 +101,7 @@ class PeopleViewController: UIViewController, UITableViewDelegate, UITableViewDa // Note that the call to Mixpanel People identify: can come after properties have been set. We queue them until // identify: is called and flush them at that time. That way, you can set properties before a user is logged in // and identify them once you know their user ID. - Mixpanel.mainInstance().identify(distinctId: Mixpanel.mainInstance().distinctId) + Mixpanel.mainInstance().identify(distinctId: "testDistinctId111") descStr = "Identified User" default: break diff --git a/MixpanelDemo/MixpanelDemo/TrackingViewController.swift b/MixpanelDemo/MixpanelDemo/TrackingViewController.swift index a26dec5bb..6f59aa26e 100644 --- a/MixpanelDemo/MixpanelDemo/TrackingViewController.swift +++ b/MixpanelDemo/MixpanelDemo/TrackingViewController.swift @@ -22,7 +22,6 @@ class TrackingViewController: UIViewController, UITableViewDelegate, UITableView "Register SuperProperties Once", "Register SP Once w Default Value", "Unregister SuperProperty"] - var counter: Int = 0 override func viewDidLoad() { super.viewDidLoad() @@ -51,8 +50,7 @@ class TrackingViewController: UIViewController, UITableViewDelegate, UITableView descStr = "Event: \"\(ev)\"" case 1: let ev = "Track Event With Properties!" - counter += 1 - let p = ["Cool Property": "Property Value \(counter)"] + let p = ["Cool Property": "Property Value"] Mixpanel.mainInstance().track(event: ev, properties: p) descStr = "Event: \"\(ev)\"\n Properties: \(p)" case 2: diff --git a/MixpanelDemo/MixpanelDemoMacTests/MixpanelBaseTests.swift b/MixpanelDemo/MixpanelDemoMacTests/MixpanelBaseTests.swift index 200c4168c..6107c28b4 100644 --- a/MixpanelDemo/MixpanelDemoMacTests/MixpanelBaseTests.swift +++ b/MixpanelDemo/MixpanelDemoMacTests/MixpanelBaseTests.swift @@ -8,62 +8,84 @@ import XCTest import Nocilla +import SQLite3 @testable import Mixpanel @testable import MixpanelDemoMac class MixpanelBaseTests: XCTestCase, MixpanelDelegate { - var mixpanel: MixpanelInstance! var mixpanelWillFlush: Bool! static var requestCount = 0 override func setUp() { NSLog("starting test setup...") super.setUp() + stubCalls() + mixpanelWillFlush = false + + NSLog("finished test setup") + } + + func stubCalls() { stubTrack() stubDecide() stubEngage() stubGroups() LSNocilla.sharedInstance().start() - mixpanelWillFlush = false - mixpanel = Mixpanel.initialize(token: kTestToken) - mixpanel.reset() - waitForTrackingQueue() - - NSLog("finished test setup") } - + override func tearDown() { super.tearDown() stubTrack() stubDecide() stubEngage() stubGroups() - deleteOptOutSettings(mixpanelInstance: mixpanel) - mixpanel.reset() - waitForTrackingQueue() LSNocilla.sharedInstance().stop() LSNocilla.sharedInstance().clearStubs() - - mixpanel = nil } - - func deleteOptOutSettings(mixpanelInstance: MixpanelInstance) - { - let filePath = Persistence.filePathWithType(.optOutStatus, token: mixpanelInstance.apiToken) + + func removeDBfile(_ token: String? = nil) { do { - try FileManager.default.removeItem(atPath: filePath!) - } catch { - Logger.info(message: "Unable to remove file at path: \(filePath!)") + let fileManager = FileManager.default + + // Check if file exists + if fileManager.fileExists(atPath: dbFilePath(token)) { + // Delete file + try fileManager.removeItem(atPath: dbFilePath(token)) + } else { + print("Unable to delete the test db file at \(dbFilePath(token)), the file does not exist") + } + + } + catch let error as NSError { + print("An error took place: \(error)") } } + func dbFilePath(_ token: String? = nil) -> String { + let manager = FileManager.default + #if os(iOS) + let url = manager.urls(for: .libraryDirectory, in: .userDomainMask).last + #else + let url = manager.urls(for: .cachesDirectory, in: .userDomainMask).last + #endif // os(iOS) + guard let apiToken = token else { + return "" + } + + guard let urlUnwrapped = url?.appendingPathComponent("\(token ?? apiToken)_MPDB.sqlite").path else { + return "" + } + return urlUnwrapped + } + + func mixpanelWillFlush(_ mixpanel: MixpanelInstance) -> Bool { return mixpanelWillFlush } - func waitForTrackingQueue() { + func waitForTrackingQueue(_ mixpanel: MixpanelInstance) { mixpanel.trackingQueue.sync() { return } @@ -74,20 +96,6 @@ class MixpanelBaseTests: XCTestCase, MixpanelDelegate { return String(format: "%08x%08x", arc4random(), arc4random()) } - func waitForMixpanelQueues() { - mixpanel.trackingQueue.sync() { - mixpanel.networkQueue.sync() { - return - } - } - } - - func waitForNetworkQueue() { - mixpanel.networkQueue.sync() { - return - } - } - func waitForAsyncTasks() { var hasCompletedTask = false DispatchQueue.main.async { @@ -99,10 +107,28 @@ class MixpanelBaseTests: XCTestCase, MixpanelDelegate { RunLoop.current.run(mode: RunLoop.Mode.default, before: loopUntil) } } + + func eventQueue(token: String) -> Queue { + return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .events) + } - func flushAndWaitForNetworkQueue() { + func peopleQueue(token: String) -> Queue { + return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .people) + } + + func unIdentifiedPeopleQueue(token: String) -> Queue { + return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .people, flag: PersistenceConstant.unIdentifiedFlag) + } + + func groupQueue(token: String) -> Queue { + return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .groups) + } + + func flushAndWaitForTrackingQueue(_ mixpanel: MixpanelInstance) { + mixpanel.flush() + waitForTrackingQueue(mixpanel) mixpanel.flush() - waitForMixpanelQueues() + waitForTrackingQueue(mixpanel) } func assertDefaultPeopleProperties(_ properties: InternalProperties) { @@ -113,6 +139,12 @@ class MixpanelBaseTests: XCTestCase, MixpanelDelegate { XCTAssertNotNil(properties["$ios_app_release"], "missing $ios_app_release property") } + func compareDate(dateString: String, dateDate: Date) { + let dateFormatter: ISO8601DateFormatter = ISO8601DateFormatter() + let date = dateFormatter.string(from: dateDate) + XCTAssertEqual(String(date.prefix(19)), String(dateString.prefix(19))) + } + func allPropertyTypes() -> Properties { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss zzz" diff --git a/MixpanelDemo/MixpanelDemoMacTests/MixpanelDemoTests.swift b/MixpanelDemo/MixpanelDemoMacTests/MixpanelDemoTests.swift index 36987a3e2..c353c6c6b 100644 --- a/MixpanelDemo/MixpanelDemoMacTests/MixpanelDemoTests.swift +++ b/MixpanelDemo/MixpanelDemoMacTests/MixpanelDemoTests.swift @@ -17,303 +17,312 @@ class MixpanelDemoTests: MixpanelBaseTests { func test5XXResponse() { LSNocilla.sharedInstance().clearStubs() _ = stubTrack().andReturn(503) - - mixpanel.track(event: "Fake Event") - - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - flushAndWaitForNetworkQueue() + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.track(event: "Fake Event") + flushAndWaitForTrackingQueue(testMixpanel) // Failure count should be 3 let waitTime = - mixpanel.flushInstance.flushRequest.networkRequestsAllowedAfterTime - Date().timeIntervalSince1970 + testMixpanel.flushInstance.flushRequest.networkRequestsAllowedAfterTime - Date().timeIntervalSince1970 print("Delta wait time is \(waitTime)") XCTAssert(waitTime >= 110, "Network backoff time is less than 2 minutes.") - XCTAssert(mixpanel.flushInstance.flushRequest.networkConsecutiveFailures == 2, + XCTAssert(testMixpanel.flushInstance.flushRequest.networkConsecutiveFailures == 2, "Network failures did not equal 2") - XCTAssert(mixpanel.eventsQueue.count == 1, + + XCTAssert(eventQueue(token: testMixpanel.apiToken).count == 1, "Removed an event from the queue that was not sent") + removeDBfile(testMixpanel.apiToken) } func testRetryAfterHTTPHeader() { LSNocilla.sharedInstance().clearStubs() _ = stubTrack().andReturn(200)?.withHeader("Retry-After", "60") - - mixpanel.track(event: "Fake Event") - - waitForTrackingQueue() - flushAndWaitForNetworkQueue() + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.track(event: "Fake Event") + flushAndWaitForTrackingQueue(testMixpanel) // Failure count should be 3 let waitTime = - mixpanel.flushInstance.flushRequest.networkRequestsAllowedAfterTime - Date().timeIntervalSince1970 + testMixpanel.flushInstance.flushRequest.networkRequestsAllowedAfterTime - Date().timeIntervalSince1970 print("Delta wait time is \(waitTime)") XCTAssert(fabs(60 - waitTime) < 5, "Mixpanel did not respect 'Retry-After' HTTP header") - XCTAssert(mixpanel.flushInstance.flushRequest.networkConsecutiveFailures == 0, + XCTAssert(testMixpanel.flushInstance.flushRequest.networkConsecutiveFailures == 0, "Network failures did not equal 0") + removeDBfile(testMixpanel.apiToken) } func testFlushEvents() { stubTrack() - - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") for i in 0..<50 { - mixpanel.track(event: "event \(i)") + testMixpanel.track(event: "event \(i)") } - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.eventsQueue.isEmpty, + flushAndWaitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "events should have been flushed") for i in 0..<60 { - mixpanel.track(event: "event \(i)") + testMixpanel.track(event: "event \(i)") } - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.eventsQueue.isEmpty, + flushAndWaitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "events should have been flushed") + removeDBfile(testMixpanel.apiToken) } func testFlushPeople() { stubEngage() - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") for i in 0..<50 { - mixpanel.people.set(property: "p1", to: "\(i)") + testMixpanel.people.set(property: "p1", to: "\(i)") } - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.people.peopleQueue.isEmpty, "people should have been flushed") + flushAndWaitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "people should have been flushed") for i in 0..<60 { - mixpanel.people.set(property: "p1", to: "\(i)") + testMixpanel.people.set(property: "p1", to: "\(i)") } - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.people.peopleQueue.isEmpty, "people should have been flushed") + flushAndWaitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "people should have been flushed") + removeDBfile(testMixpanel.apiToken) } func testFlushGroups() { stubGroups() - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") let groupKey = "test_key" let groupValue = "test_value" for i in 0..<50 { - mixpanel.getGroup(groupKey: groupKey, groupID: groupValue).set(property: "p1", to: "\(i)") + testMixpanel.getGroup(groupKey: groupKey, groupID: groupValue).set(property: "p1", to: "\(i)") } - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.groupsQueue.isEmpty, "groups should have been flushed") + flushAndWaitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertTrue(groupQueue(token: testMixpanel.apiToken).isEmpty, "groups should have been flushed") for i in 0..<60 { - mixpanel.getGroup(groupKey: groupKey, groupID: groupValue).set(property: "p1", to: "\(i)") + testMixpanel.getGroup(groupKey: groupKey, groupID: groupValue).set(property: "p1", to: "\(i)") } - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.people.peopleQueue.isEmpty, "groups should have been flushed") + flushAndWaitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "groups should have been flushed") + removeDBfile(testMixpanel.apiToken) } func testFlushNetworkFailure() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) LSNocilla.sharedInstance().clearStubs() stubTrack().andFailWithError( NSError(domain: "com.mixpanel.sdk.testing", code: 1, userInfo: nil)) for i in 0..<50 { - mixpanel.track(event: "event \(UInt(i))") + testMixpanel.track(event: "event \(UInt(i))") } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.eventsQueue.count == 50, "50 events should be queued up") - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.eventsQueue.count == 50, + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 50, "50 events should be queued up") + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 50, "events should still be in the queue if flush fails") - + removeDBfile(testMixpanel.apiToken) } func testFlushQueueContainsCorruptedEvent() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) stubTrack() - mixpanel.eventsQueue.append(["event": "bad event1", "properties": ["BadProp": Double.nan]]) - mixpanel.eventsQueue.append(["event": "bad event2", "properties": ["BadProp": Float.nan]]) - mixpanel.eventsQueue.append(["event": "bad event3", "properties": ["BadProp": Double.infinity]]) - mixpanel.eventsQueue.append(["event": "bad event4", "properties": ["BadProp": Float.infinity]]) + testMixpanel.mixpanelPersistence.saveEntity(["event": "bad event1", "properties": ["BadProp": Double.nan]], type: .events) + testMixpanel.mixpanelPersistence.saveEntity(["event": "bad event2", "properties": ["BadProp": Float.nan]], type: .events) + testMixpanel.mixpanelPersistence.saveEntity(["event": "bad event3", "properties": ["BadProp": Double.infinity]], type: .events) + testMixpanel.mixpanelPersistence.saveEntity(["event": "bad event4", "properties": ["BadProp": Float.infinity]], type: .events) for i in 0..<10 { - mixpanel.track(event: "event \(UInt(i))") + testMixpanel.track(event: "event \(UInt(i))") } - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.eventsQueue.count == 0, "good events should still be flushed") + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 0, "good events should still be flushed") + removeDBfile(testMixpanel.apiToken) } func testAddEventContainsInvalidJsonObjectDoubleNaN() { stubTrack() + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) XCTExpectAssert("unsupported property type was allowed") { - mixpanel.track(event: "bad event", properties: ["BadProp": Double.nan]) + testMixpanel.track(event: "bad event", properties: ["BadProp": Double.nan]) } + removeDBfile(testMixpanel.apiToken) } func testAddEventContainsInvalidJsonObjectFloatNaN() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) XCTExpectAssert("unsupported property type was allowed") { - mixpanel.track(event: "bad event", properties: ["BadProp": Float.nan]) + testMixpanel.track(event: "bad event", properties: ["BadProp": Float.nan]) } + removeDBfile(testMixpanel.apiToken) } func testAddEventContainsInvalidJsonObjectDoubleInfinity() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) XCTExpectAssert("unsupported property type was allowed") { - mixpanel.track(event: "bad event", properties: ["BadProp": Double.infinity]) + testMixpanel.track(event: "bad event", properties: ["BadProp": Double.infinity]) } + removeDBfile(testMixpanel.apiToken) } func testAddEventContainsInvalidJsonObjectFloatInfinity() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) XCTExpectAssert("unsupported property type was allowed") { - mixpanel.track(event: "bad event", properties: ["BadProp": Float.infinity]) + testMixpanel.track(event: "bad event", properties: ["BadProp": Float.infinity]) } + removeDBfile(testMixpanel.apiToken) } func testAddingEventsAfterFlush() { stubTrack() + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) for i in 0..<10 { - mixpanel.track(event: "event \(UInt(i))") - } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.eventsQueue.count == 10, "10 events should be queued up") - flushAndWaitForNetworkQueue() - for i in 0..<5 { - mixpanel.track(event: "event \(UInt(i))") - } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.eventsQueue.count == 5, "5 more events should be queued up") - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.eventsQueue.isEmpty, "events should have been flushed") - } - - func testDropEvents() { - mixpanel.delegate = self - var events = Queue() - for i in 0..<5000 { - events.append(["i": i]) + testMixpanel.track(event: "event \(UInt(i))") } - mixpanel.eventsQueue = events - waitForTrackingQueue() - XCTAssertTrue(mixpanel.eventsQueue.count == 5000) + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 10, "10 events should be queued up") + flushAndWaitForTrackingQueue(testMixpanel) for i in 0..<5 { - mixpanel.track(event: "event", properties: ["i": 5000 + i]) + testMixpanel.track(event: "event \(UInt(i))") } - waitForTrackingQueue() - let e: InternalProperties = mixpanel.eventsQueue.last! - XCTAssertTrue(mixpanel.eventsQueue.count == 5000) - XCTAssertEqual((e["properties"] as? InternalProperties)?["i"] as? Int, 5004) + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 5, "5 more events should be queued up") + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "events should have been flushed") + removeDBfile(testMixpanel.apiToken) } func testIdentify() { stubTrack() stubEngage() + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) for _ in 0..<2 { // run this twice to test reset works correctly wrt to distinct ids let distinctId: String = "d1" // try this for ODIN and nil #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(mixpanel.distinctId, - mixpanel.defaultDistinctId(), + XCTAssertEqual(testMixpanel.distinctId, + testMixpanel.defaultDistinctId(), "mixpanel identify failed to set default distinct id") - XCTAssertEqual(mixpanel.anonymousId, - mixpanel.defaultDistinctId(), + XCTAssertEqual(testMixpanel.anonymousId, + testMixpanel.defaultDistinctId(), "mixpanel failed to set default anonymous id") #endif - XCTAssertNil(mixpanel.people.distinctId, + XCTAssertNil(testMixpanel.people.distinctId, "mixpanel people distinct id should default to nil") - XCTAssertNil(mixpanel.people.distinctId, + XCTAssertNil(testMixpanel.people.distinctId, "mixpanel user id should default to nil") - mixpanel.track(event: "e1") - waitForTrackingQueue() - XCTAssertTrue(mixpanel.eventsQueue.count == 1, + testMixpanel.track(event: "e1") + waitForTrackingQueue(testMixpanel) + let eventsQueue = eventQueue(token: testMixpanel.apiToken) + XCTAssertTrue(eventsQueue.count == 1, "events should be sent right away with default distinct id") #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual((mixpanel.eventsQueue.last?["properties"] as? InternalProperties)?["distinct_id"] as? String, + XCTAssertEqual((eventsQueue.last?["properties"] as? InternalProperties)?["distinct_id"] as? String, mixpanel.defaultDistinctId(), "events should use default distinct id if none set") #endif - XCTAssertEqual((mixpanel.eventsQueue.last?["properties"] as? InternalProperties)?["$lib_version"] as? String, + XCTAssertEqual((eventsQueue.last?["properties"] as? InternalProperties)?["$lib_version"] as? String, AutomaticProperties.libVersion(), "events should has lib version in internal properties") - mixpanel.people.set(property: "p1", to: "a") - waitForTrackingQueue() - XCTAssertTrue(mixpanel.people.peopleQueue.isEmpty, + testMixpanel.people.set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + var peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + var unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) + XCTAssertTrue(peopleQueue_value.isEmpty, "people records should go to unidentified queue before identify:") - XCTAssertTrue(mixpanel.people.unidentifiedQueue.count == 1, + XCTAssertTrue(unidentifiedQueue.count == 1, "unidentified people records not queued") - XCTAssertEqual(mixpanel.people.unidentifiedQueue.last?["$token"] as? String, - kTestToken, + XCTAssertEqual(unidentifiedQueue.last?["$token"] as? String, + testMixpanel.apiToken, "incorrect project token in people record") - let anonymousId = mixpanel.anonymousId - mixpanel.identify(distinctId: distinctId) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.distinctId, distinctId, + testMixpanel.identify(distinctId: distinctId) + waitForTrackingQueue(testMixpanel) + sleep(2) + let anonymousId = testMixpanel.anonymousId + peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) + XCTAssertEqual(testMixpanel.distinctId, distinctId, "mixpanel identify failed to set distinct id") - XCTAssertEqual(mixpanel.userId, distinctId, + XCTAssertEqual(testMixpanel.userId, distinctId, "mixpanel identify failed to set user id") - XCTAssertEqual(mixpanel.anonymousId, anonymousId, + XCTAssertEqual(testMixpanel.anonymousId, anonymousId, "mixpanel identify shouldn't change anonymousId") - XCTAssertEqual(mixpanel.people.distinctId, distinctId, + XCTAssertEqual(testMixpanel.people.distinctId, distinctId, "mixpanel identify failed to set people distinct id") - XCTAssertTrue(mixpanel.people.unidentifiedQueue.isEmpty, + XCTAssertTrue(unidentifiedQueue.isEmpty, "identify: should move records from unidentified queue") - XCTAssertTrue(mixpanel.people.peopleQueue.count == 1, + XCTAssertTrue(peopleQueue_value.count > 0, "identify: should move records to main people queue") - XCTAssertEqual(mixpanel.people.peopleQueue.last?["$token"] as? String, - kTestToken, "incorrect project token in people record") - XCTAssertEqual(mixpanel.people.peopleQueue.last?["$distinct_id"] as? String, - distinctId, "distinct id not set properly on unidentified people record") - let p: InternalProperties = mixpanel.people.peopleQueue.last?["$set"] as! InternalProperties + XCTAssertEqual(peopleQueue_value.last?["$token"] as? String, + testMixpanel.apiToken, "incorrect project token in people record") + let p: InternalProperties = peopleQueue_value.last?["$set"] as! InternalProperties XCTAssertEqual(p["p1"] as? String, "a", "custom people property not queued") assertDefaultPeopleProperties(p) - mixpanel.people.set(property: "p1", to: "a") - waitForTrackingQueue() - XCTAssertTrue(mixpanel.people.unidentifiedQueue.isEmpty, + peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + + testMixpanel.people.set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + + peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) + XCTAssertEqual(peopleQueue_value.last?["$distinct_id"] as? String, + distinctId, "distinct id not set properly on unidentified people record") + XCTAssertTrue(unidentifiedQueue.isEmpty, "once idenitfy: is called, unidentified queue should be skipped") - XCTAssertTrue(mixpanel.people.peopleQueue.count == 2, + XCTAssertTrue(peopleQueue_value.count > 0 , "once identify: is called, records should go straight to main queue") - mixpanel.track(event: "e2") - waitForTrackingQueue() - let newDistinctId = (mixpanel.eventsQueue.last?["properties"] as? InternalProperties)?["distinct_id"] as? String + testMixpanel.track(event: "e2") + waitForTrackingQueue(testMixpanel) + let newDistinctId = (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?["distinct_id"] as? String XCTAssertEqual(newDistinctId, distinctId, "events should use new distinct id after identify:") - mixpanel.reset() - waitForTrackingQueue() + testMixpanel.reset() + waitForTrackingQueue(testMixpanel) } + removeDBfile(testMixpanel.apiToken) } func testIdentifyTrack() { stubTrack() stubEngage() - - let distinctIdBeforeIdentify: String? = mixpanel.distinctId + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let distinctIdBeforeIdentify: String? = testMixpanel.distinctId let distinctId = "testIdentifyTrack" - mixpanel.identify(distinctId: distinctId) - mixpanel.identify(distinctId: distinctId) - waitForTrackingQueue() - waitForTrackingQueue() - - let e: InternalProperties = mixpanel.eventsQueue.last! + testMixpanel.identify(distinctId: distinctId) + waitForTrackingQueue(testMixpanel) + sleep(1) + let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(e["event"] as? String, "$identify", "incorrect event name") let p: InternalProperties = e["properties"] as! InternalProperties XCTAssertEqual(p["distinct_id"] as? String, distinctId, "wrong distinct_id") XCTAssertEqual(p["$anon_distinct_id"] as? String, distinctIdBeforeIdentify, "wrong $anon_distinct_id") + removeDBfile(testMixpanel.apiToken) } func testIdentifyResetTrack() { stubTrack() stubEngage() - - let originalDistinctId: String? = mixpanel.distinctId + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let originalDistinctId: String? = testMixpanel.distinctId let distinctId = "testIdentifyTrack" - mixpanel.reset() - waitForTrackingQueue() + testMixpanel.reset() + waitForTrackingQueue(testMixpanel) for i in 1...3 { - let prevDistinctId: String? = mixpanel.distinctId + let prevDistinctId: String? = testMixpanel.distinctId let newDistinctId = distinctId + String(i) - mixpanel.identify(distinctId: newDistinctId) - waitForTrackingQueue() - waitForTrackingQueue() + testMixpanel.identify(distinctId: newDistinctId) + waitForTrackingQueue(testMixpanel) + sleep(1) - let e: InternalProperties = mixpanel.eventsQueue.last! + let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(e["event"] as? String, "$identify", "incorrect event name") let p: InternalProperties = e["properties"] as! InternalProperties XCTAssertEqual(p["distinct_id"] as? String, newDistinctId, "wrong distinct_id") @@ -322,61 +331,67 @@ class MixpanelDemoTests: MixpanelBaseTests { #if MIXPANEL_UNIQUE_DISTINCT_ID XCTAssertEqual(prevDistinctId, originalDistinctId, "After reset, IFV will be used - always the same"); #endif - mixpanel.reset() - waitForTrackingQueue() + testMixpanel.reset() + waitForTrackingQueue(testMixpanel) } + removeDBfile(testMixpanel.apiToken) } func testPersistentIdentity() { stubTrack() - let anonymousId: String? = mixpanel.anonymousId + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let distinctId: String = "d1" let alias: String = "a1" - mixpanel.identify(distinctId: distinctId) - waitForTrackingQueue() - mixpanel.createAlias(alias, distinctId: mixpanel.distinctId) - waitForTrackingQueue() - var tuple = Persistence.restoreIdentity(token: mixpanel.apiToken) - XCTAssertTrue(distinctId == tuple.0 && distinctId == tuple.1 && anonymousId == tuple.2 && distinctId == tuple.3 && alias == tuple.4) - mixpanel.archive() - waitForTrackingQueue() - mixpanel.unarchive() - waitForTrackingQueue() - tuple = Persistence.restoreIdentity(token: mixpanel.apiToken) - XCTAssertTrue(mixpanel.distinctId == tuple.0 && mixpanel.people.distinctId == tuple.1 && mixpanel.anonymousId == tuple.2 && - mixpanel.userId == tuple.3 && mixpanel.alias == tuple.4) - Persistence.deleteMPUserDefaultsData(token: mixpanel.apiToken) - waitForTrackingQueue() - tuple = Persistence.restoreIdentity(token: mixpanel.apiToken) - XCTAssertTrue("" == tuple.0 && nil == tuple.1 && nil == tuple.2 && nil == tuple.3 && nil == tuple.4) + testMixpanel.identify(distinctId: distinctId) + waitForTrackingQueue(testMixpanel) + sleep(1) + testMixpanel.createAlias(alias, distinctId: testMixpanel.distinctId) + waitForTrackingQueue(testMixpanel) + var mixpanelIdentity = MixpanelPersistence.loadIdentity(apiToken: testMixpanel.apiToken) + XCTAssertTrue(distinctId == mixpanelIdentity.distinctID && distinctId == mixpanelIdentity.peopleDistinctID && distinctId == mixpanelIdentity.userId && alias == mixpanelIdentity.alias) + testMixpanel.archive() + waitForTrackingQueue(testMixpanel) + testMixpanel.unarchive() + waitForTrackingQueue(testMixpanel) + mixpanelIdentity = MixpanelPersistence.loadIdentity(apiToken: testMixpanel.apiToken) + XCTAssertTrue(testMixpanel.distinctId == mixpanelIdentity.distinctID && testMixpanel.people.distinctId == mixpanelIdentity.peopleDistinctID && testMixpanel.anonymousId == mixpanelIdentity.anonymousId && + testMixpanel.userId == mixpanelIdentity.userId && testMixpanel.alias == mixpanelIdentity.alias) + MixpanelPersistence.deleteMPUserDefaultsData(apiToken: testMixpanel.apiToken) + waitForTrackingQueue(testMixpanel) + mixpanelIdentity = MixpanelPersistence.loadIdentity(apiToken: testMixpanel.apiToken) + XCTAssertTrue("" == mixpanelIdentity.distinctID && nil == mixpanelIdentity.peopleDistinctID && nil == mixpanelIdentity.anonymousId && nil == mixpanelIdentity.userId && nil == mixpanelIdentity.alias) + removeDBfile(testMixpanel.apiToken) } func testHadPersistedDistinctId() { - stubTrack() - XCTAssertNotNil(mixpanel.anonymousId) - XCTAssertNotNil(mixpanel.distinctId) - let distinctId: String = "d1" - mixpanel.anonymousId = nil - mixpanel.userId = nil - mixpanel.alias = nil - mixpanel.distinctId = distinctId - mixpanel.archive() - - XCTAssertEqual(mixpanel.distinctId, distinctId) + stubTrack() + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + XCTAssertNotNil(testMixpanel.distinctId) + let distinctId: String = "d1" + testMixpanel.anonymousId = nil + testMixpanel.userId = nil + testMixpanel.alias = nil + testMixpanel.distinctId = distinctId + testMixpanel.archive() - let userId: String = "u1" - mixpanel.identify(distinctId: userId) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.anonymousId, distinctId) - XCTAssertEqual(mixpanel.userId, userId) - XCTAssertEqual(mixpanel.distinctId, userId) - XCTAssertTrue(mixpanel.hadPersistedDistinctId!) + XCTAssertEqual(testMixpanel.distinctId, distinctId) + + let userId: String = "u1" + testMixpanel.identify(distinctId: userId) + waitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertEqual(testMixpanel.anonymousId, distinctId) + XCTAssertEqual(testMixpanel.userId, userId) + XCTAssertEqual(testMixpanel.distinctId, userId) + XCTAssertTrue(testMixpanel.hadPersistedDistinctId!) + removeDBfile(testMixpanel.apiToken) } func testTrackWithDefaultProperties() { - mixpanel.track(event: "Something Happened") - waitForTrackingQueue() - let e: InternalProperties = mixpanel.eventsQueue.last! + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.track(event: "Something Happened") + waitForTrackingQueue(testMixpanel) + let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(e["event"] as? String, "Something Happened", "incorrect event name") let p: InternalProperties = e["properties"] as! InternalProperties XCTAssertNotNil(p["$app_build_number"], "$app_build_number not set") @@ -391,23 +406,27 @@ class MixpanelDemoTests: MixpanelBaseTests { XCTAssertNotNil(p["time"], "time not set") XCTAssertEqual(p["$manufacturer"] as? String, "Apple", "incorrect $manufacturer") XCTAssertEqual(p["mp_lib"] as? String, "swift", "incorrect mp_lib") - XCTAssertEqual(p["token"] as? String, kTestToken, "incorrect token") + XCTAssertEqual(p["token"] as? String, testMixpanel.apiToken, "incorrect token") + removeDBfile(testMixpanel.apiToken) } func testTrackWithCustomProperties() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let now = Date() let p: Properties = ["string": "yello", "number": 3, "date": now, "$app_version": "override"] - mixpanel.track(event: "Something Happened", properties: p) - waitForTrackingQueue() - let props: InternalProperties = mixpanel.eventsQueue.last?["properties"] as! InternalProperties + testMixpanel.track(event: "Something Happened", properties: p) + waitForTrackingQueue(testMixpanel) + let props: InternalProperties = eventQueue(token: testMixpanel.apiToken).last?["properties"] as! InternalProperties XCTAssertEqual(props["string"] as? String, "yello") XCTAssertEqual(props["number"] as? Int, 3) - XCTAssertEqual(props["date"] as? Date, now) + let dateValue = props["date"] as! String + compareDate(dateString: dateValue, dateDate: now) XCTAssertEqual(props["$app_version"] as? String, "override", "reserved property override failed") + removeDBfile(testMixpanel.apiToken) } func testTrackWithOptionalProperties() { @@ -422,34 +441,39 @@ class MixpanelDemoTests: MixpanelBaseTests { "dict": optDict, "nested": nested, ] - mixpanel.track(event: "Optional Test", properties: p) - waitForTrackingQueue() - let props: InternalProperties = mixpanel.eventsQueue.last?["properties"] as! InternalProperties + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.track(event: "Optional Test", properties: p) + waitForTrackingQueue(testMixpanel) + let props: InternalProperties = eventQueue(token: testMixpanel.apiToken).last?["properties"] as! InternalProperties XCTAssertNil(props["nil"] as? Double) XCTAssertEqual(props["double"] as? Double, 1.0) - XCTAssertEqual(props["list"] as? Array, [nil, 1.0, 2.0]) + XCTAssertEqual(props["list"] as? Array, [1.0, 2.0]) XCTAssertEqual(props["dict"] as? Dictionary, ["nil": nil, "double": 1.0]) let nestedProp = props["nested"] as? Dictionary XCTAssertEqual(nestedProp?["dict"] as? Dictionary, ["nil": nil, "double": 1.0]) - XCTAssertEqual(nestedProp?["list"] as? Array, [nil, 1.0, 2.0]) + XCTAssertEqual(nestedProp?["list"] as? Array, [1.0, 2.0]) + removeDBfile(testMixpanel.apiToken) } func testTrackWithCustomDistinctIdAndToken() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let p: Properties = ["token": "t1", "distinct_id": "d1"] - mixpanel.track(event: "e1", properties: p) - waitForTrackingQueue() - let trackToken = (mixpanel.eventsQueue.last?["properties"] as? InternalProperties)?["token"] as? String - let trackDistinctId = (mixpanel.eventsQueue.last?["properties"] as? InternalProperties)?["distinct_id"] as? String + testMixpanel.track(event: "e1", properties: p) + waitForTrackingQueue(testMixpanel) + let trackToken = (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?["token"] as? String + let trackDistinctId = (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?["distinct_id"] as? String XCTAssertEqual(trackToken, "t1", "user-defined distinct id not used in track.") XCTAssertEqual(trackDistinctId, "d1", "user-defined distinct id not used in track.") + removeDBfile(testMixpanel.apiToken) } - + func testTrackWithGroups() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" - mixpanel.trackWithGroups(event: "Something Happened", properties: [groupKey: "some other value", "p1": "value"], groups: [groupKey: groupID]) - waitForTrackingQueue() - let e: InternalProperties = mixpanel.eventsQueue.last! + testMixpanel.trackWithGroups(event: "Something Happened", properties: [groupKey: "some other value", "p1": "value"], groups: [groupKey: groupID]) + waitForTrackingQueue(testMixpanel) + let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(e["event"] as? String, "Something Happened", "incorrect event name") let p: InternalProperties = e["properties"] as! InternalProperties XCTAssertNotNil(p["$app_build_number"], "$app_build_number not set") @@ -464,154 +488,174 @@ class MixpanelDemoTests: MixpanelBaseTests { XCTAssertNotNil(p["time"], "time not set") XCTAssertEqual(p["$manufacturer"] as? String, "Apple", "incorrect $manufacturer") XCTAssertEqual(p["mp_lib"] as? String, "swift", "incorrect mp_lib") - XCTAssertEqual(p["token"] as? String, kTestToken, "incorrect token") + XCTAssertEqual(p["token"] as? String, testMixpanel.apiToken, "incorrect token") XCTAssertEqual(p[groupKey] as? String, groupID, "incorrect group id") XCTAssertEqual(p["p1"] as? String, "value", "incorrect group value") + removeDBfile(testMixpanel.apiToken) } func testRegisterSuperProperties() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) var p: Properties = ["p1": "a", "p2": 3, "p3": Date()] - mixpanel.registerSuperProperties(p) - waitForTrackingQueue() - XCTAssertEqual(NSDictionary(dictionary: mixpanel.currentSuperProperties()), + testMixpanel.registerSuperProperties(p) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(NSDictionary(dictionary: testMixpanel.currentSuperProperties()), NSDictionary(dictionary: p), "register super properties failed") p = ["p1": "b"] - mixpanel.registerSuperProperties(p) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.currentSuperProperties()["p1"] as? String, "b", + testMixpanel.registerSuperProperties(p) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()["p1"] as? String, "b", "register super properties failed to overwrite existing value") p = ["p4": "a"] - mixpanel.registerSuperPropertiesOnce(p) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.currentSuperProperties()["p4"] as? String, "a", + testMixpanel.registerSuperPropertiesOnce(p) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()["p4"] as? String, "a", "register super properties once failed first time") p = ["p4": "b"] - mixpanel.registerSuperPropertiesOnce(p) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.currentSuperProperties()["p4"] as? String, "a", + testMixpanel.registerSuperPropertiesOnce(p) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()["p4"] as? String, "a", "register super properties once failed second time") p = ["p4": "c"] - mixpanel.registerSuperPropertiesOnce(p, defaultValue: "d") - waitForTrackingQueue() - XCTAssertEqual(mixpanel.currentSuperProperties()["p4"] as? String, "a", + testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "d") + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()["p4"] as? String, "a", "register super properties once with default value failed when no match") - mixpanel.registerSuperPropertiesOnce(p, defaultValue: "a") - waitForTrackingQueue() - XCTAssertEqual(mixpanel.currentSuperProperties()["p4"] as? String, "c", + testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "a") + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()["p4"] as? String, "c", "register super properties once with default value failed when match") - mixpanel.unregisterSuperProperty("a") - waitForTrackingQueue() - XCTAssertNil(mixpanel.currentSuperProperties()["a"], + testMixpanel.unregisterSuperProperty("a") + waitForTrackingQueue(testMixpanel) + XCTAssertNil(testMixpanel.currentSuperProperties()["a"], "unregister super property failed") // unregister non-existent super property should not throw - mixpanel.unregisterSuperProperty("a") - mixpanel.clearSuperProperties() - waitForTrackingQueue() - XCTAssertTrue(mixpanel.currentSuperProperties().isEmpty, + testMixpanel.unregisterSuperProperty("a") + testMixpanel.clearSuperProperties() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(testMixpanel.currentSuperProperties().isEmpty, "clear super properties failed") + removeDBfile(testMixpanel.apiToken) } func testInvalidPropertiesTrack() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let p: Properties = ["data": [Data()]] XCTExpectAssert("property type should not be allowed") { - mixpanel.track(event: "e1", properties: p) + testMixpanel.track(event: "e1", properties: p) } + removeDBfile(testMixpanel.apiToken) } func testInvalidSuperProperties() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let p: Properties = ["data": [Data()]] XCTExpectAssert("property type should not be allowed") { - mixpanel.registerSuperProperties(p) + testMixpanel.registerSuperProperties(p) } + removeDBfile(testMixpanel.apiToken) } - + func testInvalidSuperProperties2() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let p: Properties = ["data": [Data()]] XCTExpectAssert("property type should not be allowed") { - mixpanel.registerSuperPropertiesOnce(p) + testMixpanel.registerSuperPropertiesOnce(p) } + removeDBfile(testMixpanel.apiToken) } func testInvalidSuperProperties3() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let p: Properties = ["data": [Data()]] XCTExpectAssert("property type should not be allowed") { - mixpanel.registerSuperPropertiesOnce(p, defaultValue: "v") + testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "v") } + removeDBfile(testMixpanel.apiToken) } - + func testValidPropertiesTrack() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let p: Properties = allPropertyTypes() - mixpanel.track(event: "e1", properties: p) + testMixpanel.track(event: "e1", properties: p) + removeDBfile(testMixpanel.apiToken) } func testValidSuperProperties() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let p: Properties = allPropertyTypes() - mixpanel.registerSuperProperties(p) - mixpanel.registerSuperPropertiesOnce(p) - mixpanel.registerSuperPropertiesOnce(p, defaultValue: "v") + testMixpanel.registerSuperProperties(p) + testMixpanel.registerSuperPropertiesOnce(p) + testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "v") + removeDBfile(testMixpanel.apiToken) } - func testReset() { stubTrack() stubEngage() - mixpanel.identify(distinctId: "d1") - mixpanel.track(event: "e1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.track(event: "e1") + sleep(1) let p: Properties = ["p1": "a"] - mixpanel.registerSuperProperties(p) - mixpanel.people.set(properties: p) - mixpanel.archive() - mixpanel.reset() - waitForTrackingQueue() + testMixpanel.registerSuperProperties(p) + testMixpanel.people.set(properties: p) + testMixpanel.archive() + testMixpanel.reset() + waitForTrackingQueue(testMixpanel) #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(mixpanel.distinctId, - mixpanel.defaultDistinctId(), + XCTAssertEqual(testMixpanel.distinctId, + testMixpanel.defaultDistinctId(), "distinct id failed to reset") #endif - XCTAssertNil(mixpanel.people.distinctId, "people distinct id failed to reset") - XCTAssertTrue(mixpanel.currentSuperProperties().isEmpty, + XCTAssertNil(testMixpanel.people.distinctId, "people distinct id failed to reset") + XCTAssertTrue(testMixpanel.currentSuperProperties().isEmpty, "super properties failed to reset") - XCTAssertTrue(mixpanel.eventsQueue.isEmpty, "events queue failed to reset") - XCTAssertTrue(mixpanel.people.peopleQueue.isEmpty, "people queue failed to reset") - mixpanel = Mixpanel.initialize(token: kTestToken) - waitForTrackingQueue() + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "events queue failed to reset") + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "people queue failed to reset") + let testMixpanel2 = Mixpanel.initialize(token: randomId(), flushInterval: 60) + waitForTrackingQueue(testMixpanel2) #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(mixpanel.distinctId, mixpanel.defaultDistinctId(), + XCTAssertEqual(testMixpanel2.distinctId, testMixpanel2.defaultDistinctId(), "distinct id failed to reset after archive") #endif - XCTAssertNil(mixpanel.people.distinctId, + XCTAssertNil(testMixpanel2.people.distinctId, "people distinct id failed to reset after archive") - XCTAssertTrue(mixpanel.currentSuperProperties().isEmpty, + XCTAssertTrue(testMixpanel2.currentSuperProperties().isEmpty, "super properties failed to reset after archive") - XCTAssertTrue(mixpanel.eventsQueue.isEmpty, + XCTAssertTrue(eventQueue(token: testMixpanel2.apiToken).isEmpty, "events queue failed to reset after archive") - XCTAssertTrue(mixpanel.people.peopleQueue.isEmpty, + XCTAssertTrue(peopleQueue(token: testMixpanel2.apiToken).isEmpty, "people queue failed to reset after archive") + removeDBfile(testMixpanel.apiToken) + removeDBfile(testMixpanel2.apiToken) } func testArchiveNSNumberBoolIntProperty() { let testToken = randomId() - mixpanel = Mixpanel.initialize(token: testToken) + let testMixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) let aBoolNumber: Bool = true let aBoolNSNumber = NSNumber(value: aBoolNumber) - + let aIntNumber: Int = 1 let aIntNSNumber = NSNumber(value: aIntNumber) + + testMixpanel.track(event: "e1", properties: ["p1": aBoolNSNumber, "p2": aIntNSNumber]) + testMixpanel.archive() + waitForTrackingQueue(testMixpanel) - mixpanel.track(event: "e1", properties: ["p1": aBoolNSNumber, "p2": aIntNSNumber]) - mixpanel.archive() - waitForTrackingQueue() - mixpanel = Mixpanel.initialize(token: testToken) - waitForTrackingQueue() - let properties: [String: Any] = mixpanel.eventsQueue[0]["properties"] as! [String: Any] - + let testMixpanel2 = Mixpanel.initialize(token: testToken, flushInterval: 60) + waitForTrackingQueue(testMixpanel2) + let properties: [String: Any] = eventQueue(token: testMixpanel2.apiToken)[0]["properties"] as! [String: Any] + XCTAssertTrue(isBoolNumber(num: properties["p1"]! as! NSNumber), "The bool value should be unarchived as bool") XCTAssertFalse(isBoolNumber(num: properties["p2"]! as! NSNumber), "The int value should not be unarchived as bool") + removeDBfile(testToken) } - + private func isBoolNumber(num: NSNumber) -> Bool { let boolID = CFBooleanGetTypeID() // the type ID of CFBoolean @@ -621,219 +665,135 @@ class MixpanelDemoTests: MixpanelBaseTests { func testArchive() { let testToken = randomId() - mixpanel = Mixpanel.initialize(token: testToken) + let testMixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(mixpanel.distinctId, mixpanel.defaultDistinctId(), + XCTAssertEqual(testMixpanel.distinctId, testMixpanel.defaultDistinctId(), "default distinct id archive failed") #endif - XCTAssertTrue(mixpanel.currentSuperProperties().isEmpty, + XCTAssertTrue(testMixpanel.currentSuperProperties().isEmpty, "default super properties archive failed") - XCTAssertTrue(mixpanel.eventsQueue.isEmpty, "default events queue archive failed") - XCTAssertNil(mixpanel.people.distinctId, "default people distinct id archive failed") - XCTAssertTrue(mixpanel.people.peopleQueue.isEmpty, "default people queue archive failed") + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "default events queue archive failed") + XCTAssertNil(testMixpanel.people.distinctId, "default people distinct id archive failed") + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "default people queue archive failed") let p: Properties = ["p1": "a"] - mixpanel.identify(distinctId: "d1") - mixpanel.registerSuperProperties(p) - sleep(2) - mixpanel.track(event: "e1") - mixpanel.track(event: "e3") - mixpanel.track(event: "e4") - mixpanel.track(event: "e5") - mixpanel.track(event: "e6") - mixpanel.track(event: "e7") - mixpanel.track(event: "e8") - mixpanel.track(event: "e9") - mixpanel.track(event: "e10") - mixpanel.people.set(properties: p) - mixpanel.timedEvents["e2"] = 5 - mixpanel.archive() - waitForTrackingQueue() - - mixpanel = Mixpanel.initialize(token: testToken) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.distinctId, "d1", "custom distinct archive failed") - XCTAssertTrue(mixpanel.currentSuperProperties().count == 1, + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + sleep(1) + testMixpanel.registerSuperProperties(p) + testMixpanel.track(event: "e1") + testMixpanel.track(event: "e2") + testMixpanel.track(event: "e3") + testMixpanel.track(event: "e4") + testMixpanel.track(event: "e5") + testMixpanel.track(event: "e6") + testMixpanel.track(event: "e7") + testMixpanel.track(event: "e8") + testMixpanel.track(event: "e9") + testMixpanel.people.set(properties: p) + waitForTrackingQueue(testMixpanel) + testMixpanel.timedEvents["e2"] = 5 + testMixpanel.archive() + let testMixpanel2 = Mixpanel.initialize(token: testToken, flushInterval: 60) + waitForTrackingQueue(testMixpanel2) + sleep(1) + XCTAssertEqual(testMixpanel2.distinctId, "d1", "custom distinct archive failed") + XCTAssertTrue(testMixpanel2.currentSuperProperties().count == 1, "custom super properties archive failed") - XCTAssertEqual(mixpanel.eventsQueue[1]["event"] as? String, "e1", + let eventQueueValue = eventQueue(token: testMixpanel2.apiToken) + + XCTAssertEqual(eventQueueValue[1]["event"] as? String, "e1", "event was not successfully archived/unarchived") - XCTAssertEqual(mixpanel.eventsQueue[2]["event"] as? String, "e3", + XCTAssertEqual(eventQueueValue[2]["event"] as? String, "e2", "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(mixpanel.eventsQueue[3]["event"] as? String, "e4", + XCTAssertEqual(eventQueueValue[3]["event"] as? String, "e3", "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(mixpanel.eventsQueue[4]["event"] as? String, "e5", + XCTAssertEqual(eventQueueValue[4]["event"] as? String, "e4", "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(mixpanel.eventsQueue[5]["event"] as? String, "e6", + XCTAssertEqual(eventQueueValue[5]["event"] as? String, "e5", "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(mixpanel.eventsQueue[6]["event"] as? String, "e7", + XCTAssertEqual(eventQueueValue[6]["event"] as? String, "e6", "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(mixpanel.eventsQueue[7]["event"] as? String, "e8", + XCTAssertEqual(eventQueueValue[7]["event"] as? String, "e7", "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(mixpanel.eventsQueue[8]["event"] as? String, "e9", + XCTAssertEqual(eventQueueValue[8]["event"] as? String, "e8", "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(mixpanel.eventsQueue[9]["event"] as? String, "e10", + XCTAssertEqual(eventQueueValue[9]["event"] as? String, "e9", "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(mixpanel.people.distinctId, "d1", + XCTAssertEqual(testMixpanel2.people.distinctId, "d1", "custom people distinct id archive failed") - XCTAssertTrue(mixpanel.people.peopleQueue.count == 1, "pending people queue archive failed") - XCTAssertEqual(mixpanel.timedEvents["e2"] as? Double, 5.0, + XCTAssertTrue(peopleQueue(token: testMixpanel2.apiToken).count == 1, "pending people queue archive failed") + XCTAssertEqual(testMixpanel2.timedEvents["e2"] as? Double, 5.0, "timedEvents archive failed") - let fileManager = FileManager.default - XCTAssertTrue(fileManager.fileExists( - atPath: Persistence.filePathWithType(.events, token: testToken)!), - "events archive file not removed") - XCTAssertTrue(fileManager.fileExists( - atPath: Persistence.filePathWithType(.people, token: testToken)!), - "people archive file not removed") - XCTAssertTrue(fileManager.fileExists( - atPath: Persistence.filePathWithType(.properties, token: testToken)!), - "properties archive file not removed") - mixpanel = Mixpanel.initialize(token: testToken) - XCTAssertEqual(mixpanel.distinctId, "d1", "expecting d1 as distinct id as initialised") - XCTAssertTrue(mixpanel.currentSuperProperties().count == 1, + + let testMixpanel3 = Mixpanel.initialize(token: testToken, flushInterval: 60) + XCTAssertEqual(testMixpanel3.distinctId, "d1", "expecting d1 as distinct id as initialised") + XCTAssertTrue(testMixpanel3.currentSuperProperties().count == 1, "default super properties expected to have 1 item") - XCTAssertNotNil(mixpanel.eventsQueue, "default events queue from no file is nil") - XCTAssertTrue(mixpanel.eventsQueue.count == 10, "default events queue expecting 10 items ($identify call added)") - XCTAssertNotNil(mixpanel.people.distinctId, + XCTAssertNotNil(eventQueue(token: testMixpanel3.apiToken), "default events queue is nil") + XCTAssertTrue(eventQueue(token: testMixpanel3.apiToken).count == 10, "default events queue expecting 10 items ($identify call added)") + XCTAssertNotNil(testMixpanel3.people.distinctId, "default people distinct id from no file failed") - XCTAssertNotNil(mixpanel.people.peopleQueue, "default people queue from no file is nil") - XCTAssertTrue(mixpanel.people.peopleQueue.count == 1, "default people queue expecting 1 item") - XCTAssertTrue(mixpanel.timedEvents.count == 1, "timedEvents expecting 1 item") - // corrupt file - let garbage = "garbage".data(using: String.Encoding.utf8)! - do { - try garbage.write(to: URL( - fileURLWithPath: Persistence.filePathWithType(.events, token: testToken)!), - options: []) - try garbage.write(to: URL( - fileURLWithPath: Persistence.filePathWithType(.people, token: testToken)!), - options: []) - try garbage.write(to: URL( - fileURLWithPath: Persistence.filePathWithType(.properties, token: testToken)!), - options: []) - } catch { - print("couldn't write data") - } - XCTAssertTrue(fileManager.fileExists( - atPath: Persistence.filePathWithType(.events, token: testToken)!), - "events archive file not removed") - XCTAssertTrue(fileManager.fileExists( - atPath: Persistence.filePathWithType(.people, token: testToken)!), - "people archive file not removed") - XCTAssertTrue(fileManager.fileExists( - atPath: Persistence.filePathWithType(.properties, token: testToken)!), - "properties archive file not removed") - Persistence.deleteMPUserDefaultsData(token: testToken) - mixpanel = Mixpanel.initialize(token: testToken) - waitForTrackingQueue() - #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(mixpanel.distinctId, mixpanel.defaultDistinctId(), - "default distinct id from garbage failed") - #endif - XCTAssertTrue(mixpanel.currentSuperProperties().isEmpty, - "default super properties from garbage failed") - XCTAssertNotNil(mixpanel.eventsQueue, "default events queue from garbage is nil") - XCTAssertTrue(mixpanel.eventsQueue.isEmpty, - "default events queue from garbage not empty") - XCTAssertNil(mixpanel.people.distinctId, - "default people distinct id from garbage failed") - XCTAssertNotNil(mixpanel.people.peopleQueue, - "default people queue from garbage is nil") - XCTAssertTrue(mixpanel.people.peopleQueue.isEmpty, - "default people queue from garbage not empty") - XCTAssertTrue(mixpanel.timedEvents.isEmpty, - "timedEvents is not empty") - } - - - func testUnarchiveCorruptedData() { - // corrupt file - let fileManager = FileManager.default - let garbage = "garbage".data(using: String.Encoding.utf8)! - let testToken = randomId() - - do { - try garbage.write(to: URL( - fileURLWithPath: Persistence.filePathWithType(.events, token: testToken)!), - options: []) - try garbage.write(to: URL( - fileURLWithPath: Persistence.filePathWithType(.people, token: testToken)!), - options: []) - try garbage.write(to: URL( - fileURLWithPath: Persistence.filePathWithType(.properties, token: testToken)!), - options: []) - try garbage.write(to: URL( - fileURLWithPath: Persistence.filePathWithType(.optOutStatus, token: testToken)!), - options: []) - } catch { - print("couldn't write data") - } - - mixpanel = Mixpanel.initialize(token: testToken) - waitForTrackingQueue() - - - XCTAssertTrue(!fileManager.fileExists( - atPath: Persistence.filePathWithType(.events, token: testToken)!), - "events archive file not removed") - XCTAssertTrue(!fileManager.fileExists( - atPath: Persistence.filePathWithType(.people, token: testToken)!), - "people archive file not removed") - XCTAssertTrue(!fileManager.fileExists( - atPath: Persistence.filePathWithType(.properties, token: testToken)!), - "properties archive file not removed") - XCTAssertTrue(!fileManager.fileExists( - atPath: Persistence.filePathWithType(.optOutStatus, token: testToken)!), - "properties archive file not removed") - waitForTrackingQueue() + XCTAssertNotNil(peopleQueue(token:testMixpanel3.apiToken), "default people queue from no file is nil") + XCTAssertTrue(peopleQueue(token:testMixpanel3.apiToken).count == 1, "default people queue expecting 1 item") + XCTAssertTrue(testMixpanel3.timedEvents.count == 1, "timedEvents expecting 1 item") + removeDBfile(testToken) } + func testMixpanelDelegate() { - mixpanel.delegate = self - mixpanel.identify(distinctId: "d1") - mixpanel.track(event: "e1") - mixpanel.people.set(property: "p1", to: "a") - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.eventsQueue.count == 2, "delegate should have stopped flush") - XCTAssertTrue(mixpanel.people.peopleQueue.count == 1, "delegate should have stopped flush") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.delegate = self + testMixpanel.identify(distinctId: "d1") + testMixpanel.track(event: "e1") + testMixpanel.people.set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + sleep(1) + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 2, "delegate should have stopped flush") + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).count == 1, "delegate should have stopped flush") + removeDBfile(testMixpanel.apiToken) } func testEventTiming() { - mixpanel.track(event: "Something Happened") - waitForTrackingQueue() - var e: InternalProperties = mixpanel.eventsQueue.last! + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.track(event: "Something Happened") + waitForTrackingQueue(testMixpanel) + var e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! var p = e["properties"] as! InternalProperties XCTAssertNil(p["$duration"], "New events should not be timed.") - mixpanel.time(event: "400 Meters") - mixpanel.track(event: "500 Meters") - waitForTrackingQueue() - e = mixpanel.eventsQueue.last! + testMixpanel.time(event: "400 Meters") + testMixpanel.track(event: "500 Meters") + waitForTrackingQueue(testMixpanel) + e = eventQueue(token: testMixpanel.apiToken).last! p = e["properties"] as! InternalProperties XCTAssertNil(p["$duration"], "The exact same event name is required for timing.") - mixpanel.track(event: "400 Meters") - waitForTrackingQueue() - e = mixpanel.eventsQueue.last! + testMixpanel.track(event: "400 Meters") + waitForTrackingQueue(testMixpanel) + e = eventQueue(token: testMixpanel.apiToken).last! p = e["properties"] as! InternalProperties XCTAssertNotNil(p["$duration"], "This event should be timed.") - mixpanel.track(event: "400 Meters") - waitForTrackingQueue() - e = mixpanel.eventsQueue.last! + testMixpanel.track(event: "400 Meters") + waitForTrackingQueue(testMixpanel) + e = eventQueue(token: testMixpanel.apiToken).last! p = e["properties"] as! InternalProperties XCTAssertNil(p["$duration"], "Tracking the same event should require a second call to timeEvent.") - mixpanel.time(event: "Time Event A") - mixpanel.time(event: "Time Event B") - mixpanel.time(event: "Time Event C") - waitForTrackingQueue() - XCTAssertTrue(mixpanel.timedEvents.count == 3, "Each call to time() should add an event to timedEvents") - XCTAssertNotNil(mixpanel.timedEvents["Time Event A"], "Keys in timedEvents should be event names") - mixpanel.clearTimedEvent(event: "Time Event A") - waitForTrackingQueue() - XCTAssertNil(mixpanel.timedEvents["Time Event A"], "clearTimedEvent should remove key/value pair") - XCTAssertTrue(mixpanel.timedEvents.count == 2, "clearTimedEvent shoud remove only one key/value pair") - mixpanel.clearTimedEvents() - waitForTrackingQueue() - XCTAssertTrue(mixpanel.timedEvents.count == 0, "clearTimedEvents should remove all key/value pairs") + testMixpanel.time(event: "Time Event A") + testMixpanel.time(event: "Time Event B") + testMixpanel.time(event: "Time Event C") + waitForTrackingQueue(testMixpanel) + var testTimedEvents = MixpanelPersistence.loadTimedEvents(apiToken: testMixpanel.apiToken) + XCTAssertTrue(testTimedEvents.count == 3, "Each call to time() should add an event to timedEvents") + XCTAssertNotNil(testTimedEvents["Time Event A"], "Keys in timedEvents should be event names") + testMixpanel.clearTimedEvent(event: "Time Event A") + waitForTrackingQueue(testMixpanel) + testTimedEvents = MixpanelPersistence.loadTimedEvents(apiToken: testMixpanel.apiToken) + XCTAssertNil(testTimedEvents["Time Event A"], "clearTimedEvent should remove key/value pair") + XCTAssertTrue(testTimedEvents.count == 2, "clearTimedEvent shoud remove only one key/value pair") + testMixpanel.clearTimedEvents() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(MixpanelPersistence.loadTimedEvents(apiToken: testMixpanel.apiToken).count == 0, "clearTimedEvents should remove all key/value pairs") + removeDBfile(testMixpanel.apiToken) } func testReadWriteLock() { @@ -856,76 +816,178 @@ class MixpanelDemoTests: MixpanelBaseTests { } } } - + func testSetGroup() { stubTrack() stubEngage() - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") let groupKey = "test_key" let groupValue = "test_value" - mixpanel.setGroup(groupKey: groupKey, groupIDs: [groupValue]) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) - let q = mixpanel.people.peopleQueue.last!["$set"] as! InternalProperties + testMixpanel.setGroup(groupKey: groupKey, groupIDs: [groupValue]) + waitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties XCTAssertEqual(q[groupKey] as? [String], [groupValue], "group value people property not queued") assertDefaultPeopleProperties(q) + removeDBfile(testMixpanel.apiToken) } - + func testAddGroup() { stubTrack() stubEngage() - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") let groupKey = "test_key" let groupValue = "test_value" - - mixpanel.addGroup(groupKey: groupKey, groupID: groupValue) - waitForMixpanelQueues() - XCTAssertEqual(mixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) - waitForMixpanelQueues() - let q = mixpanel.people.peopleQueue.last!["$set"] as! InternalProperties + + testMixpanel.addGroup(groupKey: groupKey, groupID: groupValue) + waitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) + waitForTrackingQueue(testMixpanel) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties XCTAssertEqual(q[groupKey] as? [String], [groupValue], "addGroup people update not queued") assertDefaultPeopleProperties(q) - - mixpanel.addGroup(groupKey: groupKey, groupID: groupValue) - waitForMixpanelQueues() - XCTAssertEqual(mixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) - waitForMixpanelQueues() - let q2 = mixpanel.people.peopleQueue.last!["$union"] as! InternalProperties + + testMixpanel.addGroup(groupKey: groupKey, groupID: groupValue) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) + waitForTrackingQueue(testMixpanel) + waitForTrackingQueue(testMixpanel) + let q2 = peopleQueue(token: testMixpanel.apiToken).last!["$union"] as! InternalProperties XCTAssertEqual(q2[groupKey] as? [String], [groupValue], "addGroup people update not queued") let newVal = "new_group" - mixpanel.addGroup(groupKey: groupKey, groupID: newVal) - waitForMixpanelQueues() - XCTAssertEqual(mixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue, newVal]) - waitForMixpanelQueues() - let q3 = mixpanel.people.peopleQueue.last!["$union"] as! InternalProperties + testMixpanel.addGroup(groupKey: groupKey, groupID: newVal) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue, newVal]) + waitForTrackingQueue(testMixpanel) + waitForTrackingQueue(testMixpanel) + let q3 = peopleQueue(token: testMixpanel.apiToken).last!["$union"] as! InternalProperties XCTAssertEqual(q3[groupKey] as? [String], [newVal], "addGroup people update not queued") + removeDBfile(testMixpanel.apiToken) } - + func testRemoveGroup() { stubTrack() stubEngage() - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") let groupKey = "test_key" let groupValue = "test_value" let newVal = "new_group" - - mixpanel.setGroup(groupKey: groupKey, groupIDs: [groupValue, newVal]) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue, newVal]) - - mixpanel.removeGroup(groupKey: groupKey, groupID: groupValue) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.currentSuperProperties()[groupKey] as? [String], [newVal]) - waitForTrackingQueue() - let q2 = mixpanel.people.peopleQueue.last!["$remove"] as! InternalProperties + + testMixpanel.setGroup(groupKey: groupKey, groupIDs: [groupValue, newVal]) + waitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue, newVal]) + + testMixpanel.removeGroup(groupKey: groupKey, groupID: groupValue) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [newVal]) + waitForTrackingQueue(testMixpanel) + let q2 = peopleQueue(token: testMixpanel.apiToken).last!["$remove"] as! InternalProperties XCTAssertEqual(q2[groupKey] as? String, groupValue, "removeGroup people update not queued") - - mixpanel.removeGroup(groupKey: groupKey, groupID: groupValue) - waitForTrackingQueue() - XCTAssertNil(mixpanel.currentSuperProperties()[groupKey]) - waitForTrackingQueue() - let q3 = mixpanel.people.peopleQueue.last!["$unset"] as! [String] + + testMixpanel.removeGroup(groupKey: groupKey, groupID: groupValue) + waitForTrackingQueue(testMixpanel) + XCTAssertNil(testMixpanel.currentSuperProperties()[groupKey]) + waitForTrackingQueue(testMixpanel) + let q3 = peopleQueue(token: testMixpanel.apiToken).last!["$unset"] as! [String] XCTAssertEqual(q3, [groupKey], "removeGroup people update not queued") + removeDBfile(testMixpanel.apiToken) } + + + func testReadWriteMultiThreadShouldNotCrash() { + let concurentQueue = DispatchQueue(label: "multithread", attributes: .concurrent) + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + + for n in 1...10 { + concurentQueue.async { + testMixpanel.track(event: "event\(n)") + } + concurentQueue.async { + testMixpanel.flush() + } + concurentQueue.async { + testMixpanel.archive() + } + concurentQueue.async { + testMixpanel.reset() + } + concurentQueue.async { + testMixpanel.createAlias("aaa11", distinctId: testMixpanel.distinctId) + testMixpanel.identify(distinctId: "test") + } + concurentQueue.async { + testMixpanel.registerSuperProperties(["Plan": "Mega"]) + } + concurentQueue.async { + let _ = testMixpanel.currentSuperProperties() + } + concurentQueue.async { + testMixpanel.people.set(property: "aaa", to: "SwiftSDK Cocoapods") + testMixpanel.getGroup(groupKey: "test", groupID: 123).set(properties: ["test": 123]) + testMixpanel.removeGroup(groupKey: "test", groupID: 123) + } + concurentQueue.async { + testMixpanel.track(event: "test") + testMixpanel.time(event: "test") + testMixpanel.clearTimedEvents() + } + } + sleep(5) + removeDBfile(testMixpanel.apiToken) + } + + func testMPDB() { + let testToken = randomId() + let numRows = 50 + let halfRows = numRows/2 + let eventName = "Test Event" + func _inner() { + removeDBfile(testToken) + let mpdb = MPDB.init(token: testToken) + mpdb.open() + for pType in PersistenceType.allCases { + let emptyArray: [InternalProperties] = mpdb.readRows(pType, numRows: numRows) + XCTAssertTrue(emptyArray.isEmpty, "Table should be empty") + for i in 0...numRows-1 { + let eventObj : InternalProperties = ["event": eventName, "properties": ["index": i]] + let eventData = JSONHandler.serializeJSONObject(eventObj)! + mpdb.insertRow(pType, data: eventData) + } + let dataArray : [InternalProperties] = mpdb.readRows(pType, numRows: halfRows) + XCTAssertEqual(dataArray.count, halfRows, "Should have read only half of the rows") + var ids: [Int32] = [] + for (n, entity) in dataArray.enumerated() { + guard let id = entity["id"] as? Int32 else { + continue + } + ids.append(id) + XCTAssertEqual(entity["event"] as! String, eventName, "Event name should be unchanged") + // index should be oldest events, 0 - 24 + XCTAssertEqual(entity["properties"] as! [String : Int], ["index": n], "Should read oldest events first") + } + + mpdb.deleteRows(pType, ids: [1, 2, 3]) + let dataArray2 : [InternalProperties] = mpdb.readRows(pType, numRows: numRows) + // even though we requested numRows, there should only be halfRows left + XCTAssertEqual(dataArray2.count, numRows - 3, "Should have deleted half the rows") + for (n, entity) in dataArray2.enumerated() { + XCTAssertEqual(entity["event"] as! String, eventName, "Event name should be unchanged") + // old events (0-24) should have been deleted so index should be recent events 25-49 + XCTAssertEqual(entity["properties"] as! [String : Int], ["index": n + halfRows], "Should have deleted oldest events first") + } + mpdb.close() + } + } + removeDBfile(testToken) + } + } + + diff --git a/MixpanelDemo/MixpanelDemoMacTests/MixpanelGroupTests.swift b/MixpanelDemo/MixpanelDemoMacTests/MixpanelGroupTests.swift index 24e9826a8..d8f269e3b 100644 --- a/MixpanelDemo/MixpanelDemoMacTests/MixpanelGroupTests.swift +++ b/MixpanelDemo/MixpanelDemoMacTests/MixpanelGroupTests.swift @@ -15,127 +15,129 @@ import Nocilla class MixpanelGroupTests: MixpanelBaseTests { func testGroupSet() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" let p: Properties = ["p1": "a"] - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) - waitForTrackingQueue() - let msg = mixpanel.groupsQueue.last! + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(msg["$group_key"] as! String, groupKey) XCTAssertEqual(msg["$group_id"] as! String, groupID) let q = msg["$set"] as! InternalProperties XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") + removeDBfile(testMixpanel.apiToken) } func testGroupSetIntegerID() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" - let groupID = 3 + let groupID = 3 let p: Properties = ["p1": "a"] - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) - waitForTrackingQueue() - let msg = mixpanel.groupsQueue.last! + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(msg["$group_key"] as! String, groupKey) XCTAssertEqual(msg["$group_id"] as! Int, groupID) let q = msg["$set"] as! InternalProperties XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") + removeDBfile(testMixpanel.apiToken) } func testGroupSetOnce() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" let p: Properties = ["p1": "a"] - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).setOnce(properties: p) - waitForTrackingQueue() - let msg = mixpanel.groupsQueue.last! + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).setOnce(properties: p) + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(msg["$group_key"] as! String, groupKey) XCTAssertEqual(msg["$group_id"] as! String, groupID) let q = msg["$set_once"] as! InternalProperties XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") + removeDBfile(testMixpanel.apiToken) } func testGroupSetTo() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "p1", to: "a") - waitForTrackingQueue() - let msg = mixpanel.groupsQueue.last! + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(msg["$group_key"] as! String, groupKey) XCTAssertEqual(msg["$group_id"] as! String, groupID) let p = msg["$set"] as! InternalProperties XCTAssertEqual(p["p1"] as? String, "a", "custom group property not queued") + removeDBfile(testMixpanel.apiToken) } func testGroupUnset() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).unset(property: "p1") - waitForTrackingQueue() - let msg = mixpanel.groupsQueue.last! + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).unset(property: "p1") + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(msg["$group_key"] as! String, groupKey) XCTAssertEqual(msg["$group_id"] as! String, groupID) XCTAssertEqual(msg["$unset"] as! [String], ["p1"], "group property unset not queued") + removeDBfile(testMixpanel.apiToken) } func testGroupRemove() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).remove(key: "p1", value: "a") - waitForTrackingQueue() - let msg = mixpanel.groupsQueue.last! + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).remove(key: "p1", value: "a") + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(msg["$group_key"] as! String, groupKey) XCTAssertEqual(msg["$group_id"] as! String, groupID) XCTAssertEqual(msg["$remove"] as? [String: String], ["p1": "a"], "group property remove not queued") + removeDBfile(testMixpanel.apiToken) } func testGroupUnion() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).union(key: "p1", values: ["a"]) - waitForTrackingQueue() - let msg = mixpanel.groupsQueue.last! + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).union(key: "p1", values: ["a"]) + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(msg["$group_key"] as! String, groupKey) XCTAssertEqual(msg["$group_id"] as! String, groupID) XCTAssertEqual(msg["$union"] as? [String: [String]], ["p1": ["a"]], "group property union not queued") + removeDBfile(testMixpanel.apiToken) } - func testDropGroupRecords() { - QueueConstants.queueSize = 500 - let groupKey = "test_key" - let groupID = "test_id" - for i in 0..<505 { - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "i", to: i) - } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.groupsQueue.count == 500) - var r: InternalProperties = mixpanel.groupsQueue.first! - XCTAssertEqual(r["$group_key"] as! String, groupKey) - XCTAssertEqual(r["$group_id"] as! String, groupID) - XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 5) - r = mixpanel.groupsQueue.last! - XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 504) - } func testGroupAssertPropertyTypes() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" let p: Properties = ["URL": [Data()]] XCTExpectAssert("unsupported property type was allowed") { - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) } XCTExpectAssert("unsupported property type was allowed") { - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "p1", to: [Data()]) + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "p1", to: [Data()]) } + removeDBfile(testMixpanel.apiToken) } func testDeleteGroup() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).deleteGroup() - waitForTrackingQueue() - let msg = mixpanel.groupsQueue.last! + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).deleteGroup() + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(msg["$group_key"] as! String, groupKey) XCTAssertEqual(msg["$group_id"] as! String, groupID) let p: InternalProperties = msg["$delete"] as! InternalProperties XCTAssertTrue(p.isEmpty, "incorrect group properties: \(p)") + removeDBfile(testMixpanel.apiToken) } } diff --git a/MixpanelDemo/MixpanelDemoMacTests/MixpanelOptOutTests.swift b/MixpanelDemo/MixpanelDemoMacTests/MixpanelOptOutTests.swift index f4c2837d9..296db7a1c 100644 --- a/MixpanelDemo/MixpanelDemoMacTests/MixpanelOptOutTests.swift +++ b/MixpanelDemo/MixpanelDemoMacTests/MixpanelOptOutTests.swift @@ -13,46 +13,46 @@ class MixpanelOptOutTests: MixpanelBaseTests { func testHasOptOutTrackingFlagBeingSetProperlyAfterInitializedWithOptedOutYES() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - XCTAssertTrue(mixpanel.hasOptedOutTracking(), "When initialize with opted out flag set to YES, the current user should have opted out tracking") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(testMixpanel.hasOptedOutTracking(), "When initialize with opted out flag set to YES, the current user should have opted out tracking") + testMixpanel.reset() + removeDBfile(testMixpanel.apiToken) } func testOptInWillAddOptInEvent() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optInTracking() - XCTAssertFalse(mixpanel.hasOptedOutTracking(), "The current user should have opted in tracking") - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.eventsQueue.count == 1, "When opted in, event queue should have one even(opt in) being queued") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optInTracking() + XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "The current user should have opted in tracking") + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "When opted in, event queue should have one even(opt in) being queued") - if mixpanel.eventsQueue.count > 0 { - let event = mixpanel.eventsQueue.first + if eventQueue(token: testMixpanel.apiToken).count > 0 { + let event = eventQueue(token: testMixpanel.apiToken).first XCTAssertEqual((event!["event"] as? String), "$opt_in", "When opted in, a track '$opt_in' should have been queued") } else { - XCTAssertTrue(mixpanel.eventsQueue.count == 1, "When opted in, event queue should have one even(opt in) being queued") + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "When opted in, event queue should have one even(opt in) being queued") } + removeDBfile(testMixpanel.apiToken) } func testOptInTrackingForDistinctId() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optInTracking(distinctId: "testDistinctId") - XCTAssertFalse(mixpanel.hasOptedOutTracking(), "The current user should have opted in tracking") - waitForTrackingQueue() - if mixpanel.eventsQueue.count > 0 { - let event = mixpanel.eventsQueue.first - XCTAssertEqual((event!["event"] as? String), "$opt_in", "When opted in, a track '$opt_in' should have been queued") - } - else { - XCTAssertTrue(mixpanel.eventsQueue.count == 1, "When opted in, event queue should have one even(opt in) being queued") - } - - XCTAssertEqual(mixpanel.distinctId, "testDistinctId", "mixpanel identify failed to set distinct id") - XCTAssertEqual(mixpanel.people.distinctId, "testDistinctId", "mixpanel identify failed to set people distinct id") - XCTAssertTrue(mixpanel.people.unidentifiedQueue.count == 0, "identify: should move records from unidentified queue") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optInTracking(distinctId: "testDistinctId") + XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "The current user should have opted in tracking") + waitForTrackingQueue(testMixpanel) + sleep(1) + let event = eventQueue(token: testMixpanel.apiToken).first + XCTAssertEqual((event!["event"] as? String), "$opt_in", "When opted in, a track '$opt_in' should have been queued") + XCTAssertEqual(testMixpanel.distinctId, "testDistinctId", "mixpanel identify failed to set distinct id") + XCTAssertEqual(testMixpanel.people.distinctId, "testDistinctId", "mixpanel identify failed to set people distinct id") + XCTAssertTrue(unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 0, "identify: should move records from unidentified queue") + removeDBfile(testMixpanel.apiToken) } - + func testOptInTrackingForDistinctIdAndWithEventProperties() { let now = Date() @@ -60,216 +60,197 @@ class MixpanelOptOutTests: MixpanelBaseTests { "number": 3, "date": now, "$app_version": "override"] - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optInTracking(distinctId: "testDistinctId", properties: testProperties) - waitForTrackingQueue() - let props = mixpanel.eventsQueue.first!["properties"] as? InternalProperties + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optInTracking(distinctId: "testDistinctId", properties: testProperties) + waitForTrackingQueue(testMixpanel) + sleep(1) + let eventQueueValue = eventQueue(token: testMixpanel.apiToken) + + let props = eventQueueValue.first!["properties"] as? InternalProperties XCTAssertEqual(props!["string"] as? String, "yello") XCTAssertEqual(props!["number"] as? NSNumber, 3) - XCTAssertEqual(props!["date"] as? Date, now) + compareDate(dateString: props!["date"] as! String, dateDate: now) XCTAssertEqual(props!["$app_version"] as? String, "override", "reserved property override failed") - - if mixpanel.eventsQueue.count > 0 { - let event = mixpanel.eventsQueue.first + + if eventQueueValue.count > 0 { + let event = eventQueueValue.first XCTAssertEqual((event!["event"] as? String), "$opt_in", "When opted in, a track '$opt_in' should have been queued") } else { - XCTAssertTrue(mixpanel.eventsQueue.count == 1, "When opted in, event queue should have one even(opt in) being queued") + XCTAssertTrue(eventQueueValue.count == 1, "When opted in, event queue should have one even(opt in) being queued") } - - XCTAssertEqual(mixpanel.distinctId, "testDistinctId", "mixpanel identify failed to set distinct id") - XCTAssertEqual(mixpanel.people.distinctId, "testDistinctId", "mixpanel identify failed to set people distinct id") - XCTAssertTrue(mixpanel.people.unidentifiedQueue.count == 0, "identify: should move records from unidentified queue") + + XCTAssertEqual(testMixpanel.distinctId, "testDistinctId", "mixpanel identify failed to set distinct id") + XCTAssertEqual(testMixpanel.people.distinctId, "testDistinctId", "mixpanel identify failed to set people distinct id") + XCTAssertTrue(unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 0, "identify: should move records from unidentified queue") + removeDBfile(testMixpanel.apiToken) } - + func testHasOptOutTrackingFlagBeingSetProperlyForMultipleInstances() { let mixpanel1 = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) XCTAssertTrue(mixpanel1.hasOptedOutTracking(), "When initialize with opted out flag set to YES, the current user should have opted out tracking") - + removeDBfile(mixpanel1.apiToken) + let mixpanel2 = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: false) XCTAssertFalse(mixpanel2.hasOptedOutTracking(), "When initialize with opted out flag set to NO, the current user should have opted in tracking") - - deleteOptOutSettings(mixpanelInstance: mixpanel1) - deleteOptOutSettings(mixpanelInstance: mixpanel2) + removeDBfile(mixpanel2.apiToken) } - + func testHasOptOutTrackingFlagBeingSetProperlyAfterInitializedWithOptedOutNO() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: false) - XCTAssertFalse(mixpanel.hasOptedOutTracking(), "When initialize with opted out flag set to NO, the current user should have opted out tracking") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: false) + XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "When initialize with opted out flag set to NO, the current user should have opted out tracking") + removeDBfile(testMixpanel.apiToken) } - + func testHasOptOutTrackingFlagBeingSetProperlyByDefault() { - mixpanel = Mixpanel.initialize(token: randomId()) - XCTAssertFalse(mixpanel.hasOptedOutTracking(), "By default, the current user should not opted out tracking") + let testMixpanel = Mixpanel.initialize(token: randomId()) + XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "By default, the current user should not opted out tracking") + removeDBfile(testMixpanel.apiToken) } - + func testHasOptOutTrackingFlagBeingSetProperlyForOptOut() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optOutTracking() - XCTAssertTrue(mixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted out tracking") - } - - func testHasOptOutTrackingFlagBeingSetProperlyForOptIn() - { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optOutTracking() - XCTAssertTrue(mixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted out tracking") - mixpanel.optInTracking() - XCTAssertFalse(mixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted in tracking") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + XCTAssertTrue(testMixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted out tracking") + removeDBfile(testMixpanel.apiToken) } - func testEventBeingTrackedBeforeOptOutShouldNotBeCleared() + func testHasOptOutTrackingFlagBeingSetProperlyForOptIn() { - mixpanel = Mixpanel.initialize(token: randomId()) - mixpanel.track(event: "a normal event") - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.eventsQueue.count == 1, "events should be queued") - mixpanel.optOutTracking() - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.eventsQueue.count == 1, "When opted out, any events tracked before opted out should not be cleared") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + XCTAssertTrue(testMixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted out tracking") + testMixpanel.optInTracking() + XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted in tracking") + removeDBfile(testMixpanel.apiToken) } func testOptOutTrackingWillNotGenerateEventQueue() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optOutTracking() + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optOutTracking() for i in 0..<50 { - mixpanel.track(event: "event \(i)") + testMixpanel.track(event: "event \(i)") } - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.eventsQueue.count == 0, "When opted out, events should not be queued") + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 0, "When opted out, events should not be queued") + removeDBfile(testMixpanel.apiToken) } func testOptOutTrackingWillNotGeneratePeopleQueue() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optOutTracking() + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optOutTracking() for i in 0..<50 { - mixpanel.people.set(property: "p1", to: "\(i)") + testMixpanel.people.set(property: "p1", to: "\(i)") } - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.people.peopleQueue.count == 0, "When opted out, events should not be queued") + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).count == 0, "When opted out, events should not be queued") + removeDBfile(testMixpanel.apiToken) } func testOptOutTrackingWillSkipAlias() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optOutTracking() - mixpanel.createAlias("testAlias", distinctId: "aDistinctId") - XCTAssertNotEqual(mixpanel.alias, "testAlias", "When opted out, alias should not be set") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + testMixpanel.createAlias("testAlias", distinctId: "aDistinctId") + XCTAssertNotEqual(testMixpanel.alias, "testAlias", "When opted out, alias should not be set") + removeDBfile(testMixpanel.apiToken) + } + + func testEventBeingTrackedBeforeOptOutShouldNotBeCleared() + { + let testMixpanel = Mixpanel.initialize(token: randomId()) + testMixpanel.track(event: "a normal event") + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "events should be queued") + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "When opted out, any events tracked before opted out should not be cleared") + removeDBfile(testMixpanel.apiToken) } func testOptOutTrackingRegisterSuperProperties() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) let properties: Properties = ["p1": "a", "p2": 3, "p3": Date()] - mixpanel.optOutTracking() - mixpanel.registerSuperProperties(properties) - waitForMixpanelQueues() - XCTAssertNotEqual(NSDictionary(dictionary: mixpanel.currentSuperProperties()), + testMixpanel.optOutTracking() + testMixpanel.registerSuperProperties(properties) + waitForTrackingQueue(testMixpanel) + XCTAssertNotEqual(NSDictionary(dictionary: testMixpanel.currentSuperProperties()), NSDictionary(dictionary: properties), "When opted out, register super properties should not be successful") + removeDBfile(testMixpanel.apiToken) } func testOptOutTrackingRegisterSuperPropertiesOnce() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) let properties: Properties = ["p1": "a", "p2": 3, "p3": Date()] - mixpanel.optOutTracking() - mixpanel.registerSuperPropertiesOnce(properties) - waitForMixpanelQueues() - XCTAssertNotEqual(NSDictionary(dictionary: mixpanel.currentSuperProperties()), - NSDictionary(dictionary: properties), + testMixpanel.optOutTracking() + testMixpanel.registerSuperPropertiesOnce(properties) + waitForTrackingQueue(testMixpanel) + XCTAssertNotEqual(NSDictionary(dictionary: testMixpanel.currentSuperProperties()), + NSDictionary(dictionary: properties), "When opted out, register super properties once should not be successful") + removeDBfile(testMixpanel.apiToken) } func testOptOutWilSkipTimeEvent() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optOutTracking() - mixpanel.time(event: "400 Meters") - mixpanel.track(event: "400 Meters") - waitForMixpanelQueues() - XCTAssertNil(mixpanel.eventsQueue.last, "When opted out, this event should not be timed.") - } - - func testOptOutTrackingWillPurgeEventQueue() - { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optInTracking() - mixpanel.identify(distinctId: "d1") - for i in 0..<50 { - mixpanel.track(event: "event \(i)") - } - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.eventsQueue.count > 50, "When opted in, events should have been queued") - XCTAssertEqual(mixpanel.eventsQueue.first!["event"] as? String, "$opt_in", "incorrect optin event name") - - mixpanel.optOutTracking() - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.eventsQueue.count == 0, "When opted out, events should have been purged") - } - - func testOptOutTrackingWillPurgePeopleQueue() - { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optInTracking() - mixpanel.identify(distinctId: "d1") - for i in 0..<50 { - mixpanel.people.set(property: "p1", to: "\(i)") - } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.people.peopleQueue.count == 50, "When opted in, people should have been queued") - - mixpanel.optOutTracking() - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.people.peopleQueue.count == 0, "When opted out, people should have been purged") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + testMixpanel.time(event: "400 Meters") + testMixpanel.track(event: "400 Meters") + waitForTrackingQueue(testMixpanel) + XCTAssertNil(eventQueue(token:testMixpanel.apiToken).last, "When opted out, this event should not be timed.") + removeDBfile(testMixpanel.apiToken) } func testOptOutWillSkipFlushPeople() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optInTracking() - mixpanel.identify(distinctId: "d1") - for i in 0..<50 { - mixpanel.people.set(property: "p1", to: "\(i)") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optInTracking() + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + for i in 0..<1 { + testMixpanel.people.set(property: "p1", to: "\(i)") } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.people.peopleQueue.count == 50, "When opted in, people queue should have been queued") + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).count == 1, "When opted in, people queue should have been queued") - let peopleQueue = mixpanel.people.peopleQueue - mixpanel.optOutTracking() - waitForMixpanelQueues() + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) - mixpanel.people.peopleQueue = peopleQueue - mixpanel.flush() - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.people.peopleQueue.count == 50, "When opted out, people queue should not be flushed") + testMixpanel.flush() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).count == 3, "When opted out, people queue should not be flushed and 2 more people being set") + removeDBfile(testMixpanel.apiToken) } func testOptOutWillSkipFlushEvent() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optInTracking() - mixpanel.identify(distinctId: "d1") - for i in 0..<50 { - mixpanel.track(event: "event \(i)") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optInTracking() + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + for i in 0..<1 { + testMixpanel.track(event: "event \(i)") } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.eventsQueue.count > 50, "When opted in, events should have been queued") + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 3, "When opted in, events should have been queued") - let eventsQueue = mixpanel.eventsQueue - mixpanel.optOutTracking() - waitForMixpanelQueues() + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) - //In order to test if flush will be skipped, we have to create a fake eventsQueue since optOutTracking will clear eventsQueue. - mixpanel.eventsQueue = eventsQueue - mixpanel.flush() - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.eventsQueue.count > 50, "When opted out, events should not be flushed") + testMixpanel.flush() + waitForTrackingQueue(testMixpanel) + + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 3, "When opted out, events should not be flushed") + removeDBfile(testMixpanel.apiToken) } } diff --git a/MixpanelDemo/MixpanelDemoMacTests/MixpanelPeopleTests.swift b/MixpanelDemo/MixpanelDemoMacTests/MixpanelPeopleTests.swift index 0b0e70e18..862f51a9d 100644 --- a/MixpanelDemo/MixpanelDemoMacTests/MixpanelPeopleTests.swift +++ b/MixpanelDemo/MixpanelDemoMacTests/MixpanelPeopleTests.swift @@ -15,177 +15,210 @@ import Nocilla class MixpanelPeopleTests: MixpanelBaseTests { func testPeopleSet() { - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) let p: Properties = ["p1": "a"] - mixpanel.people.set(properties: p) - waitForTrackingQueue() - let q = mixpanel.people.peopleQueue.last!["$set"] as! InternalProperties + testMixpanel.people.set(properties: p) + waitForTrackingQueue(testMixpanel) + sleep(1) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties XCTAssertEqual(q["p1"] as? String, "a", "custom people property not queued") assertDefaultPeopleProperties(q) + removeDBfile(testMixpanel.apiToken) } func testPeopleSetOnce() { - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") let p: Properties = ["p1": "a"] - mixpanel.people.setOnce(properties: p) - waitForTrackingQueue() - let q = mixpanel.people.peopleQueue.last!["$set_once"] as! InternalProperties + testMixpanel.people.setOnce(properties: p) + waitForTrackingQueue(testMixpanel) + sleep(1) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set_once"] as! InternalProperties XCTAssertEqual(q["p1"] as? String, "a", "custom people property not queued") assertDefaultPeopleProperties(q) + removeDBfile(testMixpanel.apiToken) } func testPeopleSetReservedProperty() { - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") let p: Properties = ["$ios_app_version": "override"] - mixpanel.people.set(properties: p) - waitForTrackingQueue() - let q = mixpanel.people.peopleQueue.last!["$set"] as! InternalProperties + testMixpanel.people.set(properties: p) + waitForTrackingQueue(testMixpanel) + sleep(1) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties XCTAssertEqual(q["$ios_app_version"] as? String, "override", "reserved property override failed") assertDefaultPeopleProperties(q) + removeDBfile(testMixpanel.apiToken) } func testPeopleSetTo() { - mixpanel.identify(distinctId: "d1") - mixpanel.people.set(property: "p1", to: "a") - waitForTrackingQueue() - let p: InternalProperties = mixpanel.people.peopleQueue.last!["$set"] as! InternalProperties + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + sleep(1) + let p: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties XCTAssertEqual(p["p1"] as? String, "a", "custom people property not queued") assertDefaultPeopleProperties(p) + removeDBfile(testMixpanel.apiToken) } func testDropUnidentifiedPeopleRecords() { - QueueConstants.queueSize = 500 + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) for i in 0..<505 { - mixpanel.people.set(property: "i", to: i) + testMixpanel.people.set(property: "i", to: i) } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.people.unidentifiedQueue.count == 500) - var r: InternalProperties = mixpanel.people.unidentifiedQueue.first! - XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 5) - r = mixpanel.people.unidentifiedQueue.last! + waitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertTrue(unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 505) + var r: InternalProperties = unIdentifiedPeopleQueue(token: testMixpanel.apiToken).first! + XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 0) + r = unIdentifiedPeopleQueue(token: testMixpanel.apiToken).last! XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 504) + removeDBfile(testMixpanel.apiToken) } - func testDropPeopleRecords() { - QueueConstants.queueSize = 500 - mixpanel.identify(distinctId: "d1") - for i in 0..<505 { - mixpanel.people.set(property: "i", to: i) - } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.people.peopleQueue.count == 500) - var r: InternalProperties = mixpanel.people.peopleQueue.first! - XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 5) - r = mixpanel.people.peopleQueue.last! - XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 504) - } func testPeopleAssertPropertyTypes() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) var p: Properties = ["URL": [Data()]] XCTExpectAssert("unsupported property type was allowed") { - mixpanel.people.set(properties: p) + testMixpanel.people.set(properties: p) } XCTExpectAssert("unsupported property type was allowed") { - mixpanel.people.set(property: "p1", to: [Data()]) + testMixpanel.people.set(property: "p1", to: [Data()]) } p = ["p1": "a"] // increment should require a number XCTExpectAssert("unsupported property type was allowed") { - mixpanel.people.increment(properties: p) + testMixpanel.people.increment(properties: p) } + removeDBfile(testMixpanel.apiToken) } func testPeopleIncrement() { - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") let p: Properties = ["p1": 3] - mixpanel.people.increment(properties: p) - waitForTrackingQueue() - let q = mixpanel.people.peopleQueue.last!["$add"] as! InternalProperties + testMixpanel.people.increment(properties: p) + waitForTrackingQueue(testMixpanel) + sleep(1) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$add"] as! InternalProperties XCTAssertTrue(q.count == 1, "incorrect people properties: \(p)") XCTAssertEqual(q["p1"] as? Int, 3, "custom people property not queued") + removeDBfile(testMixpanel.apiToken) } func testPeopleIncrementBy() { - mixpanel.identify(distinctId: "d1") - mixpanel.people.increment(property: "p1", by: 3) - waitForTrackingQueue() - let p: InternalProperties = mixpanel.people.peopleQueue.last!["$add"] as! InternalProperties + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.increment(property: "p1", by: 3) + waitForTrackingQueue(testMixpanel) + sleep(1) + let p: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last!["$add"] as! InternalProperties XCTAssertTrue(p.count == 1, "incorrect people properties: \(p)") XCTAssertEqual(p["p1"] as? Double, 3, "custom people property not queued") + removeDBfile(testMixpanel.apiToken) } func testPeopleDeleteUser() { - mixpanel.identify(distinctId: "d1") - mixpanel.people.deleteUser() - waitForTrackingQueue() - let p: InternalProperties = mixpanel.people.peopleQueue.last!["$delete"] as! InternalProperties + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.deleteUser() + waitForTrackingQueue(testMixpanel) + sleep(1) + let p: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last!["$delete"] as! InternalProperties XCTAssertTrue(p.isEmpty, "incorrect people properties: \(p)") + removeDBfile(testMixpanel.apiToken) } func testPeopleTrackChargeDecimal() { - mixpanel.identify(distinctId: "d1") - mixpanel.people.trackCharge(amount: 25.34) - waitForTrackingQueue() - let r: InternalProperties = mixpanel.people.peopleQueue.last! + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.trackCharge(amount: 25.34) + waitForTrackingQueue(testMixpanel) + sleep(1) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] XCTAssertEqual(prop, 25.34) XCTAssertNotNil(prop2) + removeDBfile(testMixpanel.apiToken) } func testPeopleTrackChargeZero() { - mixpanel.identify(distinctId: "d1") - mixpanel.people.trackCharge(amount: 0) - waitForTrackingQueue() - let r: InternalProperties = mixpanel.people.peopleQueue.last! + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + testMixpanel.people.trackCharge(amount: 0) + waitForTrackingQueue(testMixpanel) + sleep(1) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] XCTAssertEqual(prop, 0) XCTAssertNotNil(prop2) + removeDBfile(testMixpanel.apiToken) } + func testPeopleTrackChargeWithTime() { - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") let p: Properties = allPropertyTypes() - mixpanel.people.trackCharge(amount: 25, properties: ["$time": p["date"]!]) - waitForTrackingQueue() - let r: InternalProperties = mixpanel.people.peopleQueue.last! + testMixpanel.people.trackCharge(amount: 25, properties: ["$time": p["date"]!]) + waitForTrackingQueue(testMixpanel) + sleep(1) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double - let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] + let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] as? String XCTAssertEqual(prop, 25) - XCTAssertEqual(prop2 as? Date, p["date"] as? Date) + compareDate(dateString: prop2!, dateDate: p["date"] as! Date) + removeDBfile(testMixpanel.apiToken) } func testPeopleTrackChargeWithProperties() { - mixpanel.identify(distinctId: "d1") - mixpanel.people.trackCharge(amount: 25, properties: ["p1": "a"]) - waitForTrackingQueue() - let r: InternalProperties = mixpanel.people.peopleQueue.last! + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.trackCharge(amount: 25, properties: ["p1": "a"]) + waitForTrackingQueue(testMixpanel) + sleep(1) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["p1"] XCTAssertEqual(prop, 25) XCTAssertEqual(prop2 as? String, "a") + removeDBfile(testMixpanel.apiToken) } func testPeopleTrackCharge() { - mixpanel.identify(distinctId: "d1") - mixpanel.people.trackCharge(amount: 25) - waitForTrackingQueue() - let r: InternalProperties = mixpanel.people.peopleQueue.last! + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.trackCharge(amount: 25) + waitForTrackingQueue(testMixpanel) + sleep(1) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] XCTAssertEqual(prop, 25) XCTAssertNotNil(prop2) + removeDBfile(testMixpanel.apiToken) } func testPeopleClearCharges() { - mixpanel.identify(distinctId: "d1") - mixpanel.people.clearCharges() - waitForTrackingQueue() - let r: InternalProperties = mixpanel.people.peopleQueue.last! + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.clearCharges() + waitForTrackingQueue(testMixpanel) + sleep(1) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! let transactions = (r["$set"] as? InternalProperties)?["$transactions"] as? [MixpanelType] XCTAssertEqual(transactions?.count, 0) + removeDBfile(testMixpanel.apiToken) } } diff --git a/MixpanelDemo/MixpanelDemoTV/AppDelegate.swift b/MixpanelDemo/MixpanelDemoTV/AppDelegate.swift index a88e6dce8..e93d2cbc3 100644 --- a/MixpanelDemo/MixpanelDemoTV/AppDelegate.swift +++ b/MixpanelDemo/MixpanelDemoTV/AppDelegate.swift @@ -23,7 +23,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { Mixpanel.initialize(token: "MIXPANEL_TOKEN") Mixpanel.mainInstance().loggingEnabled = true Mixpanel.mainInstance().registerSuperProperties(["super apple tv properties": 1]); - Mixpanel.mainInstance().track(event: "apple tv track") return true diff --git a/MixpanelDemo/MixpanelDemoTests/LoggerTests.swift b/MixpanelDemo/MixpanelDemoTests/LoggerTests.swift index bd61d0739..62867bf49 100644 --- a/MixpanelDemo/MixpanelDemoTests/LoggerTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/LoggerTests.swift @@ -11,16 +11,11 @@ import XCTest @testable import Mixpanel class LoggerTests: XCTestCase { - var counter: CounterLogging! - override func setUp() { - super.setUp() - - counter = CounterLogging() - Logger.addLogging(counter) - } func testEnableDebug() { + let counter = CounterLogging() + Logger.addLogging(counter) Logger.enableLevel(.debug) Logger.debug(message: "logged") @@ -28,27 +23,32 @@ class LoggerTests: XCTestCase { } func testEnableInfo() { + let counter = CounterLogging() + Logger.addLogging(counter) Logger.enableLevel(.info) - Logger.info(message: "logged") XCTAssertEqual(1, counter.count) } func testEnableWarning() { + let counter = CounterLogging() + Logger.addLogging(counter) Logger.enableLevel(.warning) - Logger.warn(message: "logged") XCTAssertEqual(1, counter.count) } func testEnableError() { + let counter = CounterLogging() + Logger.addLogging(counter) Logger.enableLevel(.error) - Logger.error(message: "logged") XCTAssertEqual(1, counter.count) } func testDisabledLogging() { + let counter = CounterLogging() + Logger.addLogging(counter) Logger.disableLevel(.debug) Logger.debug(message: "not logged") XCTAssertEqual(0, counter.count) diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelAutomaticEventsTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelAutomaticEventsTests.swift index 08ecab32f..1605ab717 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelAutomaticEventsTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelAutomaticEventsTests.swift @@ -14,75 +14,93 @@ import Nocilla class MixpanelAutomaticEventsTests: MixpanelBaseTests { - override func setUp() { - super.setUp() - } - - override func tearDown() { - super.tearDown() - } - func testSession() { - self.mixpanel.minimumSessionDuration = 0; - self.mixpanel.identify(distinctId: "d1") - sleep(1) - self.mixpanel.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.minimumSessionDuration = 0; + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + testMixpanel.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), with: Notification(name: Notification.Name(rawValue: "test"))) - self.waitForMixpanelQueues() - let event = self.mixpanel.eventsQueue.last - let people1 = self.mixpanel.people.peopleQueue[0]["$add"] as! InternalProperties - let people2 = self.mixpanel.people.peopleQueue[1]["$add"] as! InternalProperties - XCTAssertEqual(people1["$ae_total_app_sessions"] as? Double, 1, "total app sessions should be added by 1") + waitForTrackingQueue(testMixpanel) + + let event = eventQueue(token: testMixpanel.apiToken).last + let people1 = peopleQueue(token: testMixpanel.apiToken)[0]["$add"] as! InternalProperties + let people2 = peopleQueue(token: testMixpanel.apiToken)[1]["$add"] as! InternalProperties + XCTAssertEqual((people1["$ae_total_app_sessions"] as? NSNumber)?.intValue, 1, "total app sessions should be added by 1") XCTAssertNotNil((people2["$ae_total_app_session_length"], "should have session length in $add queue")) XCTAssertNotNil(event, "Should have an event") XCTAssertEqual(event?["event"] as? String, "$ae_session", "should be app session event") XCTAssertNotNil((event?["properties"] as? [String: Any])?["$ae_session_length"], "should have session length") + removeDBfile(testMixpanel.apiToken) } func testKeepAutomaticEventsIfNetworkNotAvailable() { - self.mixpanel.minimumSessionDuration = 0; - self.mixpanel.decideInstance.automaticEventsEnabled = nil - self.mixpanel.identify(distinctId: "d1") - sleep(1) - self.mixpanel.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.minimumSessionDuration = 0; + testMixpanel.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), with: Notification(name: Notification.Name(rawValue: "test"))) - - self.waitForMixpanelQueues() - flushAndWaitForNetworkQueue() - let event = self.mixpanel.eventsQueue.last - XCTAssertTrue(self.mixpanel.eventsQueue.count > 0, "automatic events should be accumulated if check decide is offline(decideInstance.automaticEventsEnabled is nil)") + waitForTrackingQueue(testMixpanel) + let event = eventQueue(token: testMixpanel.apiToken).last + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "automatic events should be accumulated if check decide is offline(decideInstance.automaticEventsEnabled is nil)") XCTAssertEqual(event?["event"] as? String, "$ae_session", "should be app session event") + removeDBfile(testMixpanel.apiToken) } - + func testDiscardAutomaticEventsIftrackAutomaticEventsEnabledIsFalse() { - self.mixpanel.minimumSessionDuration = 0; - self.mixpanel.trackAutomaticEventsEnabled = false - self.mixpanel.decideInstance.automaticEventsEnabled = nil - self.mixpanel.identify(distinctId: "d1") - sleep(1) - self.mixpanel.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.minimumSessionDuration = 0; + testMixpanel.trackAutomaticEventsEnabled = false + MixpanelPersistence.saveAutomacticEventsEnabledFlag(value: true, fromDecide: true, apiToken: testMixpanel.apiToken) + testMixpanel.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), with: Notification(name: Notification.Name(rawValue: "test"))) - - self.waitForMixpanelQueues() - flushAndWaitForNetworkQueue() - XCTAssertTrue(self.mixpanel.eventsQueue.count == 0, "automatic events should be discarded") + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 0, "automatic events should not be tracked") + removeDBfile(testMixpanel.apiToken) } - + func testFlushAutomaticEventsIftrackAutomaticEventsEnabledIsTrue() { - self.mixpanel.minimumSessionDuration = 0; - self.mixpanel.trackAutomaticEventsEnabled = false - self.mixpanel.decideInstance.automaticEventsEnabled = nil - self.mixpanel.identify(distinctId: "d1") - sleep(1) - self.mixpanel.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.minimumSessionDuration = 0; + testMixpanel.trackAutomaticEventsEnabled = true + MixpanelPersistence.saveAutomacticEventsEnabledFlag(value: false, fromDecide: true, apiToken: testMixpanel.apiToken) + testMixpanel.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), with: Notification(name: Notification.Name(rawValue: "test"))) + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "automatic events should be tracked") - self.waitForMixpanelQueues() - flushAndWaitForNetworkQueue() - XCTAssertTrue(self.mixpanel.eventsQueue.count == 0, "automatic events should be flushed") + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 0, "automatic events should be flushed") + removeDBfile(testMixpanel.apiToken) + } + + + func testDiscardAutomaticEventsIfDecideIsFalse() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.minimumSessionDuration = 0; + MixpanelPersistence.saveAutomacticEventsEnabledFlag(value: false, fromDecide: true, apiToken: testMixpanel.apiToken) + testMixpanel.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), + with: Notification(name: Notification.Name(rawValue: "test"))) + + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 0, "automatic events should be discarded") + removeDBfile(testMixpanel.apiToken) } + func testFlushAutomaticEventsIfDecideIsTrue() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.minimumSessionDuration = 0; + MixpanelPersistence.saveAutomacticEventsEnabledFlag(value: true, fromDecide: true, apiToken: testMixpanel.apiToken) + testMixpanel.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), + with: Notification(name: Notification.Name(rawValue: "test"))) + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "automatic events should be tracked") + + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 0, "automatic events should be flushed") + removeDBfile(testMixpanel.apiToken) + } + func testUpdated() { let defaults = UserDefaults(suiteName: "Mixpanel") let infoDict = Bundle.main.infoDictionary @@ -107,41 +125,43 @@ class MixpanelAutomaticEventsTests: MixpanelBaseTests { } catch { XCTFail() } - - let mp = Mixpanel.initialize(token: "abc") + + let mp = Mixpanel.initialize(token: randomId()) mp.reset() mp.minimumSessionDuration = 0; - let mp2 = Mixpanel.initialize(token: "xyz") + let mp2 = Mixpanel.initialize(token: randomId()) mp2.reset() mp2.minimumSessionDuration = 0; - + mp.automaticEvents.perform(#selector(AutomaticEvents.appDidBecomeActive(_:)), with: Notification(name: Notification.Name(rawValue: "test"))) mp2.automaticEvents.perform(#selector(AutomaticEvents.appDidBecomeActive(_:)), with: Notification(name: Notification.Name(rawValue: "test"))) mp.trackingQueue.sync { } mp2.trackingQueue.sync { } - - XCTAssertEqual(mp.eventsQueue.count, 1, "there should be only 1 event") - let appOpenEvent = mp.eventsQueue.last + + XCTAssertEqual(eventQueue(token: mp.apiToken).count, 1, "there should be only 1 event") + let appOpenEvent = eventQueue(token: mp.apiToken).last XCTAssertEqual(appOpenEvent?["event"] as? String, "$ae_first_open", "should be first app open event") - - XCTAssertEqual(mp2.eventsQueue.count, 1, "there should be only 1 event") - let otherAppOpenEvent = mp2.eventsQueue.last + + XCTAssertEqual(eventQueue(token: mp2.apiToken).count, 1, "there should be only 1 event") + let otherAppOpenEvent = eventQueue(token: mp2.apiToken).last XCTAssertEqual(otherAppOpenEvent?["event"] as? String, "$ae_first_open", "should be first app open event") - + mp.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), with: Notification(name: Notification.Name(rawValue: "test"))) mp2.automaticEvents.perform(#selector(AutomaticEvents.appWillResignActive(_:)), with: Notification(name: Notification.Name(rawValue: "test"))) mp.trackingQueue.sync { } mp2.trackingQueue.sync { } - let appSessionEvent = mp.eventsQueue.last + let appSessionEvent = eventQueue(token: mp.apiToken).last XCTAssertNotNil(appSessionEvent, "Should have an event") XCTAssertEqual(appSessionEvent?["event"] as? String, "$ae_session", "should be app session event") XCTAssertNotNil((appSessionEvent?["properties"] as? [String: Any])?["$ae_session_length"], "should have session length") - let otherAppSessionEvent = mp2.eventsQueue.last + let otherAppSessionEvent = eventQueue(token: mp2.apiToken).last XCTAssertEqual(otherAppSessionEvent?["event"] as? String, "$ae_session", "should be app session event") XCTAssertNotNil((otherAppSessionEvent?["properties"] as? [String: Any])?["$ae_session_length"], "should have session length") + removeDBfile(mp.apiToken) + removeDBfile(mp2.apiToken) } } diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelBaseTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelBaseTests.swift index 377d60ad9..6fb80afe0 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelBaseTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelBaseTests.swift @@ -8,28 +8,21 @@ import XCTest import Nocilla +import SQLite3 @testable import Mixpanel @testable import MixpanelDemo class MixpanelBaseTests: XCTestCase, MixpanelDelegate { - var mixpanel: MixpanelInstance! var mixpanelWillFlush: Bool! static var requestCount = 0 override func setUp() { NSLog("starting test setup...") super.setUp() - stubTrack() - stubDecide() - stubEngage() - stubGroups() - LSNocilla.sharedInstance().start() + stubCalls() mixpanelWillFlush = false - mixpanel = Mixpanel.initialize(token: kTestToken, flushInterval: 0) - mixpanel.reset() - waitForTrackingQueue() - + if let loginView = self.topViewController() as? LoginViewController { loginView.goToMainView() } else { @@ -39,37 +32,66 @@ class MixpanelBaseTests: XCTestCase, MixpanelDelegate { NSLog("finished test setup") } + func stubCalls() { + stubTrack() + stubDecide() + stubEngage() + stubGroups() + LSNocilla.sharedInstance().start() + } + override func tearDown() { super.tearDown() stubTrack() stubDecide() stubEngage() stubGroups() - deleteOptOutSettings(mixpanelInstance: mixpanel) - mixpanel.reset() - waitForTrackingQueue() LSNocilla.sharedInstance().stop() LSNocilla.sharedInstance().clearStubs() - - mixpanel = nil } - - func deleteOptOutSettings(mixpanelInstance: MixpanelInstance) - { - let filePath = Persistence.filePathWithType(.optOutStatus, token: mixpanelInstance.apiToken) + + func removeDBfile(_ token: String? = nil) { do { - try FileManager.default.removeItem(atPath: filePath!) - } catch { - Logger.info(message: "Unable to remove file at path: \(filePath!)") + let fileManager = FileManager.default + + // Check if file exists + if fileManager.fileExists(atPath: dbFilePath(token)) { + // Delete file + try fileManager.removeItem(atPath: dbFilePath(token)) + } else { + print("Unable to delete the test db file at \(dbFilePath(token)), the file does not exist") + } + + } + catch let error as NSError { + print("An error took place: \(error)") + } + } + + func dbFilePath(_ token: String? = nil) -> String { + let manager = FileManager.default + #if os(iOS) + let url = manager.urls(for: .libraryDirectory, in: .userDomainMask).last + #else + let url = manager.urls(for: .cachesDirectory, in: .userDomainMask).last + #endif // os(iOS) + guard let apiToken = token else { + return "" } + + guard let urlUnwrapped = url?.appendingPathComponent("\(token ?? apiToken)_MPDB.sqlite").path else { + return "" + } + return urlUnwrapped } + func mixpanelWillFlush(_ mixpanel: MixpanelInstance) -> Bool { return mixpanelWillFlush } - func waitForTrackingQueue() { + func waitForTrackingQueue(_ mixpanel: MixpanelInstance) { mixpanel.trackingQueue.sync() { return } @@ -80,20 +102,6 @@ class MixpanelBaseTests: XCTestCase, MixpanelDelegate { return String(format: "%08x%08x", arc4random(), arc4random()) } - func waitForMixpanelQueues() { - mixpanel.trackingQueue.sync() { - mixpanel.networkQueue.sync() { - return - } - } - } - - func waitForNetworkQueue() { - mixpanel.networkQueue.sync() { - return - } - } - func waitForAsyncTasks() { var hasCompletedTask = false DispatchQueue.main.async { @@ -105,10 +113,28 @@ class MixpanelBaseTests: XCTestCase, MixpanelDelegate { RunLoop.current.run(mode: RunLoop.Mode.default, before: loopUntil) } } + + func eventQueue(token: String) -> Queue { + return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .events) + } - func flushAndWaitForNetworkQueue() { + func peopleQueue(token: String) -> Queue { + return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .people) + } + + func unIdentifiedPeopleQueue(token: String) -> Queue { + return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .people, flag: PersistenceConstant.unIdentifiedFlag) + } + + func groupQueue(token: String) -> Queue { + return MixpanelPersistence.init(token: token).loadEntitiesInBatch(type: .groups) + } + + func flushAndWaitForTrackingQueue(_ mixpanel: MixpanelInstance) { + mixpanel.flush() + waitForTrackingQueue(mixpanel) mixpanel.flush() - waitForMixpanelQueues() + waitForTrackingQueue(mixpanel) } func assertDefaultPeopleProperties(_ properties: InternalProperties) { @@ -119,6 +145,12 @@ class MixpanelBaseTests: XCTestCase, MixpanelDelegate { XCTAssertNotNil(properties["$ios_app_release"], "missing $ios_app_release property") } + func compareDate(dateString: String, dateDate: Date) { + let dateFormatter: ISO8601DateFormatter = ISO8601DateFormatter() + let date = dateFormatter.string(from: dateDate) + XCTAssertEqual(String(date.prefix(19)), String(dateString.prefix(19))) + } + func allPropertyTypes() -> Properties { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss zzz" diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift index 0a20b67a4..96efa3bb0 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelDemoTests.swift @@ -17,303 +17,312 @@ class MixpanelDemoTests: MixpanelBaseTests { func test5XXResponse() { LSNocilla.sharedInstance().clearStubs() _ = stubTrack().andReturn(503) - - mixpanel.track(event: "Fake Event") - - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - flushAndWaitForNetworkQueue() + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.track(event: "Fake Event") + flushAndWaitForTrackingQueue(testMixpanel) // Failure count should be 3 let waitTime = - mixpanel.flushInstance.flushRequest.networkRequestsAllowedAfterTime - Date().timeIntervalSince1970 + testMixpanel.flushInstance.flushRequest.networkRequestsAllowedAfterTime - Date().timeIntervalSince1970 print("Delta wait time is \(waitTime)") XCTAssert(waitTime >= 110, "Network backoff time is less than 2 minutes.") - XCTAssert(mixpanel.flushInstance.flushRequest.networkConsecutiveFailures == 2, + XCTAssert(testMixpanel.flushInstance.flushRequest.networkConsecutiveFailures == 2, "Network failures did not equal 2") - XCTAssert(mixpanel.eventsQueue.count == 1, + + XCTAssert(eventQueue(token: testMixpanel.apiToken).count == 1, "Removed an event from the queue that was not sent") + removeDBfile(testMixpanel.apiToken) } func testRetryAfterHTTPHeader() { LSNocilla.sharedInstance().clearStubs() _ = stubTrack().andReturn(200)?.withHeader("Retry-After", "60") - - mixpanel.track(event: "Fake Event") - - waitForTrackingQueue() - flushAndWaitForNetworkQueue() + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.track(event: "Fake Event") + flushAndWaitForTrackingQueue(testMixpanel) // Failure count should be 3 let waitTime = - mixpanel.flushInstance.flushRequest.networkRequestsAllowedAfterTime - Date().timeIntervalSince1970 + testMixpanel.flushInstance.flushRequest.networkRequestsAllowedAfterTime - Date().timeIntervalSince1970 print("Delta wait time is \(waitTime)") XCTAssert(fabs(60 - waitTime) < 5, "Mixpanel did not respect 'Retry-After' HTTP header") - XCTAssert(mixpanel.flushInstance.flushRequest.networkConsecutiveFailures == 0, + XCTAssert(testMixpanel.flushInstance.flushRequest.networkConsecutiveFailures == 0, "Network failures did not equal 0") + removeDBfile(testMixpanel.apiToken) } func testFlushEvents() { stubTrack() - - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") for i in 0..<50 { - mixpanel.track(event: "event \(i)") + testMixpanel.track(event: "event \(i)") } - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.eventsQueue.isEmpty, + flushAndWaitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "events should have been flushed") for i in 0..<60 { - mixpanel.track(event: "event \(i)") + testMixpanel.track(event: "event \(i)") } - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.eventsQueue.isEmpty, + flushAndWaitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "events should have been flushed") + removeDBfile(testMixpanel.apiToken) } func testFlushPeople() { stubEngage() - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") for i in 0..<50 { - mixpanel.people.set(property: "p1", to: "\(i)") + testMixpanel.people.set(property: "p1", to: "\(i)") } - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.people.peopleQueue.isEmpty, "people should have been flushed") + flushAndWaitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "people should have been flushed") for i in 0..<60 { - mixpanel.people.set(property: "p1", to: "\(i)") + testMixpanel.people.set(property: "p1", to: "\(i)") } - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.people.peopleQueue.isEmpty, "people should have been flushed") + flushAndWaitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "people should have been flushed") + removeDBfile(testMixpanel.apiToken) } func testFlushGroups() { stubGroups() - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") let groupKey = "test_key" let groupValue = "test_value" for i in 0..<50 { - mixpanel.getGroup(groupKey: groupKey, groupID: groupValue).set(property: "p1", to: "\(i)") + testMixpanel.getGroup(groupKey: groupKey, groupID: groupValue).set(property: "p1", to: "\(i)") } - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.groupsQueue.isEmpty, "groups should have been flushed") + flushAndWaitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertTrue(groupQueue(token: testMixpanel.apiToken).isEmpty, "groups should have been flushed") for i in 0..<60 { - mixpanel.getGroup(groupKey: groupKey, groupID: groupValue).set(property: "p1", to: "\(i)") + testMixpanel.getGroup(groupKey: groupKey, groupID: groupValue).set(property: "p1", to: "\(i)") } - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.people.peopleQueue.isEmpty, "groups should have been flushed") + flushAndWaitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "groups should have been flushed") + removeDBfile(testMixpanel.apiToken) } func testFlushNetworkFailure() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) LSNocilla.sharedInstance().clearStubs() stubTrack().andFailWithError( NSError(domain: "com.mixpanel.sdk.testing", code: 1, userInfo: nil)) for i in 0..<50 { - mixpanel.track(event: "event \(UInt(i))") + testMixpanel.track(event: "event \(UInt(i))") } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.eventsQueue.count == 50, "50 events should be queued up") - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.eventsQueue.count == 50, + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 50, "50 events should be queued up") + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 50, "events should still be in the queue if flush fails") - + removeDBfile(testMixpanel.apiToken) } func testFlushQueueContainsCorruptedEvent() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) stubTrack() - mixpanel.eventsQueue.append(["event": "bad event1", "properties": ["BadProp": Double.nan]]) - mixpanel.eventsQueue.append(["event": "bad event2", "properties": ["BadProp": Float.nan]]) - mixpanel.eventsQueue.append(["event": "bad event3", "properties": ["BadProp": Double.infinity]]) - mixpanel.eventsQueue.append(["event": "bad event4", "properties": ["BadProp": Float.infinity]]) + testMixpanel.mixpanelPersistence.saveEntity(["event": "bad event1", "properties": ["BadProp": Double.nan]], type: .events) + testMixpanel.mixpanelPersistence.saveEntity(["event": "bad event2", "properties": ["BadProp": Float.nan]], type: .events) + testMixpanel.mixpanelPersistence.saveEntity(["event": "bad event3", "properties": ["BadProp": Double.infinity]], type: .events) + testMixpanel.mixpanelPersistence.saveEntity(["event": "bad event4", "properties": ["BadProp": Float.infinity]], type: .events) for i in 0..<10 { - mixpanel.track(event: "event \(UInt(i))") + testMixpanel.track(event: "event \(UInt(i))") } - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.eventsQueue.count == 0, "good events should still be flushed") + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 0, "good events should still be flushed") + removeDBfile(testMixpanel.apiToken) } func testAddEventContainsInvalidJsonObjectDoubleNaN() { stubTrack() + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) XCTExpectAssert("unsupported property type was allowed") { - mixpanel.track(event: "bad event", properties: ["BadProp": Double.nan]) + testMixpanel.track(event: "bad event", properties: ["BadProp": Double.nan]) } + removeDBfile(testMixpanel.apiToken) } func testAddEventContainsInvalidJsonObjectFloatNaN() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) XCTExpectAssert("unsupported property type was allowed") { - mixpanel.track(event: "bad event", properties: ["BadProp": Float.nan]) + testMixpanel.track(event: "bad event", properties: ["BadProp": Float.nan]) } + removeDBfile(testMixpanel.apiToken) } func testAddEventContainsInvalidJsonObjectDoubleInfinity() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) XCTExpectAssert("unsupported property type was allowed") { - mixpanel.track(event: "bad event", properties: ["BadProp": Double.infinity]) + testMixpanel.track(event: "bad event", properties: ["BadProp": Double.infinity]) } + removeDBfile(testMixpanel.apiToken) } func testAddEventContainsInvalidJsonObjectFloatInfinity() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) XCTExpectAssert("unsupported property type was allowed") { - mixpanel.track(event: "bad event", properties: ["BadProp": Float.infinity]) + testMixpanel.track(event: "bad event", properties: ["BadProp": Float.infinity]) } + removeDBfile(testMixpanel.apiToken) } func testAddingEventsAfterFlush() { stubTrack() + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) for i in 0..<10 { - mixpanel.track(event: "event \(UInt(i))") + testMixpanel.track(event: "event \(UInt(i))") } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.eventsQueue.count == 10, "10 events should be queued up") - flushAndWaitForNetworkQueue() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 10, "10 events should be queued up") + flushAndWaitForTrackingQueue(testMixpanel) for i in 0..<5 { - mixpanel.track(event: "event \(UInt(i))") + testMixpanel.track(event: "event \(UInt(i))") } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.eventsQueue.count == 5, "5 more events should be queued up") - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.eventsQueue.isEmpty, "events should have been flushed") - } - - func testDropEvents() { - mixpanel.delegate = self - var events = Queue() - for i in 0..<5000 { - events.append(["i": i]) - } - mixpanel.eventsQueue = events - waitForTrackingQueue() - XCTAssertTrue(mixpanel.eventsQueue.count == 5000) - for i in 0..<5 { - mixpanel.track(event: "event", properties: ["i": 5000 + i]) - } - waitForTrackingQueue() - let e: InternalProperties = mixpanel.eventsQueue.last! - XCTAssertTrue(mixpanel.eventsQueue.count == 5000) - XCTAssertEqual((e["properties"] as? InternalProperties)?["i"] as? Int, 5004) + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 5, "5 more events should be queued up") + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "events should have been flushed") + removeDBfile(testMixpanel.apiToken) } func testIdentify() { stubTrack() stubEngage() + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) for _ in 0..<2 { // run this twice to test reset works correctly wrt to distinct ids let distinctId: String = "d1" // try this for ODIN and nil #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(mixpanel.distinctId, - mixpanel.defaultDistinctId(), + XCTAssertEqual(testMixpanel.distinctId, + testMixpanel.defaultDistinctId(), "mixpanel identify failed to set default distinct id") - XCTAssertEqual(mixpanel.anonymousId, - mixpanel.defaultDistinctId(), + XCTAssertEqual(testMixpanel.anonymousId, + testMixpanel.defaultDistinctId(), "mixpanel failed to set default anonymous id") #endif - XCTAssertNil(mixpanel.people.distinctId, + XCTAssertNil(testMixpanel.people.distinctId, "mixpanel people distinct id should default to nil") - XCTAssertNil(mixpanel.people.distinctId, + XCTAssertNil(testMixpanel.people.distinctId, "mixpanel user id should default to nil") - mixpanel.track(event: "e1") - waitForTrackingQueue() - XCTAssertTrue(mixpanel.eventsQueue.count == 1, + testMixpanel.track(event: "e1") + waitForTrackingQueue(testMixpanel) + let eventsQueue = eventQueue(token: testMixpanel.apiToken) + XCTAssertTrue(eventsQueue.count == 1, "events should be sent right away with default distinct id") #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual((mixpanel.eventsQueue.last?["properties"] as? InternalProperties)?["distinct_id"] as? String, + XCTAssertEqual((eventsQueue.last?["properties"] as? InternalProperties)?["distinct_id"] as? String, mixpanel.defaultDistinctId(), "events should use default distinct id if none set") #endif - XCTAssertEqual((mixpanel.eventsQueue.last?["properties"] as? InternalProperties)?["$lib_version"] as? String, + XCTAssertEqual((eventsQueue.last?["properties"] as? InternalProperties)?["$lib_version"] as? String, AutomaticProperties.libVersion(), "events should has lib version in internal properties") - mixpanel.people.set(property: "p1", to: "a") - waitForTrackingQueue() - XCTAssertTrue(mixpanel.people.peopleQueue.isEmpty, + testMixpanel.people.set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + var peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + var unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) + XCTAssertTrue(peopleQueue_value.isEmpty, "people records should go to unidentified queue before identify:") - XCTAssertTrue(mixpanel.people.unidentifiedQueue.count == 1, + XCTAssertTrue(unidentifiedQueue.count == 1, "unidentified people records not queued") - XCTAssertEqual(mixpanel.people.unidentifiedQueue.last?["$token"] as? String, - kTestToken, + XCTAssertEqual(unidentifiedQueue.last?["$token"] as? String, + testMixpanel.apiToken, "incorrect project token in people record") - let anonymousId = mixpanel.anonymousId - mixpanel.identify(distinctId: distinctId) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.distinctId, distinctId, + testMixpanel.identify(distinctId: distinctId) + waitForTrackingQueue(testMixpanel) + sleep(2) + let anonymousId = testMixpanel.anonymousId + peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) + XCTAssertEqual(testMixpanel.distinctId, distinctId, "mixpanel identify failed to set distinct id") - XCTAssertEqual(mixpanel.userId, distinctId, + XCTAssertEqual(testMixpanel.userId, distinctId, "mixpanel identify failed to set user id") - XCTAssertEqual(mixpanel.anonymousId, anonymousId, + XCTAssertEqual(testMixpanel.anonymousId, anonymousId, "mixpanel identify shouldn't change anonymousId") - XCTAssertEqual(mixpanel.people.distinctId, distinctId, + XCTAssertEqual(testMixpanel.people.distinctId, distinctId, "mixpanel identify failed to set people distinct id") - XCTAssertTrue(mixpanel.people.unidentifiedQueue.isEmpty, + XCTAssertTrue(unidentifiedQueue.isEmpty, "identify: should move records from unidentified queue") - XCTAssertTrue(mixpanel.people.peopleQueue.count == 1, + XCTAssertTrue(peopleQueue_value.count > 0, "identify: should move records to main people queue") - XCTAssertEqual(mixpanel.people.peopleQueue.last?["$token"] as? String, - kTestToken, "incorrect project token in people record") - XCTAssertEqual(mixpanel.people.peopleQueue.last?["$distinct_id"] as? String, - distinctId, "distinct id not set properly on unidentified people record") - let p: InternalProperties = mixpanel.people.peopleQueue.last?["$set"] as! InternalProperties + XCTAssertEqual(peopleQueue_value.last?["$token"] as? String, + testMixpanel.apiToken, "incorrect project token in people record") + let p: InternalProperties = peopleQueue_value.last?["$set"] as! InternalProperties XCTAssertEqual(p["p1"] as? String, "a", "custom people property not queued") assertDefaultPeopleProperties(p) - mixpanel.people.set(property: "p1", to: "a") - waitForTrackingQueue() - XCTAssertTrue(mixpanel.people.unidentifiedQueue.isEmpty, + peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + + testMixpanel.people.set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + + peopleQueue_value = peopleQueue(token: testMixpanel.apiToken) + unidentifiedQueue = unIdentifiedPeopleQueue(token: testMixpanel.apiToken) + XCTAssertEqual(peopleQueue_value.last?["$distinct_id"] as? String, + distinctId, "distinct id not set properly on unidentified people record") + XCTAssertTrue(unidentifiedQueue.isEmpty, "once idenitfy: is called, unidentified queue should be skipped") - XCTAssertTrue(mixpanel.people.peopleQueue.count == 2, + XCTAssertTrue(peopleQueue_value.count > 0 , "once identify: is called, records should go straight to main queue") - mixpanel.track(event: "e2") - waitForTrackingQueue() - let newDistinctId = (mixpanel.eventsQueue.last?["properties"] as? InternalProperties)?["distinct_id"] as? String + testMixpanel.track(event: "e2") + waitForTrackingQueue(testMixpanel) + let newDistinctId = (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?["distinct_id"] as? String XCTAssertEqual(newDistinctId, distinctId, "events should use new distinct id after identify:") - mixpanel.reset() - waitForTrackingQueue() + testMixpanel.reset() + waitForTrackingQueue(testMixpanel) } + removeDBfile(testMixpanel.apiToken) } func testIdentifyTrack() { stubTrack() stubEngage() - - let distinctIdBeforeIdentify: String? = mixpanel.distinctId + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let distinctIdBeforeIdentify: String? = testMixpanel.distinctId let distinctId = "testIdentifyTrack" - mixpanel.identify(distinctId: distinctId) - mixpanel.identify(distinctId: distinctId) - waitForTrackingQueue() - waitForTrackingQueue() - - let e: InternalProperties = mixpanel.eventsQueue.last! + testMixpanel.identify(distinctId: distinctId) + waitForTrackingQueue(testMixpanel) + sleep(1) + let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(e["event"] as? String, "$identify", "incorrect event name") let p: InternalProperties = e["properties"] as! InternalProperties XCTAssertEqual(p["distinct_id"] as? String, distinctId, "wrong distinct_id") XCTAssertEqual(p["$anon_distinct_id"] as? String, distinctIdBeforeIdentify, "wrong $anon_distinct_id") + removeDBfile(testMixpanel.apiToken) } func testIdentifyResetTrack() { stubTrack() stubEngage() - - let originalDistinctId: String? = mixpanel.distinctId + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + let originalDistinctId: String? = testMixpanel.distinctId let distinctId = "testIdentifyTrack" - mixpanel.reset() - waitForTrackingQueue() + testMixpanel.reset() + waitForTrackingQueue(testMixpanel) for i in 1...3 { - let prevDistinctId: String? = mixpanel.distinctId + let prevDistinctId: String? = testMixpanel.distinctId let newDistinctId = distinctId + String(i) - mixpanel.identify(distinctId: newDistinctId) - waitForTrackingQueue() - waitForTrackingQueue() + testMixpanel.identify(distinctId: newDistinctId) + waitForTrackingQueue(testMixpanel) + sleep(1) - let e: InternalProperties = mixpanel.eventsQueue.last! + let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(e["event"] as? String, "$identify", "incorrect event name") let p: InternalProperties = e["properties"] as! InternalProperties XCTAssertEqual(p["distinct_id"] as? String, newDistinctId, "wrong distinct_id") @@ -322,61 +331,67 @@ class MixpanelDemoTests: MixpanelBaseTests { #if MIXPANEL_UNIQUE_DISTINCT_ID XCTAssertEqual(prevDistinctId, originalDistinctId, "After reset, IFV will be used - always the same"); #endif - mixpanel.reset() - waitForTrackingQueue() + testMixpanel.reset() + waitForTrackingQueue(testMixpanel) } + removeDBfile(testMixpanel.apiToken) } func testPersistentIdentity() { stubTrack() - let anonymousId: String? = mixpanel.anonymousId + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let distinctId: String = "d1" let alias: String = "a1" - mixpanel.identify(distinctId: distinctId) - waitForTrackingQueue() - mixpanel.createAlias(alias, distinctId: mixpanel.distinctId) - waitForTrackingQueue() - var tuple = Persistence.restoreIdentity(token: mixpanel.apiToken) - XCTAssertTrue(distinctId == tuple.0 && distinctId == tuple.1 && anonymousId == tuple.2 && distinctId == tuple.3 && alias == tuple.4) - mixpanel.archive() - waitForTrackingQueue() - mixpanel.unarchive() - waitForTrackingQueue() - tuple = Persistence.restoreIdentity(token: mixpanel.apiToken) - XCTAssertTrue(mixpanel.distinctId == tuple.0 && mixpanel.people.distinctId == tuple.1 && mixpanel.anonymousId == tuple.2 && - mixpanel.userId == tuple.3 && mixpanel.alias == tuple.4) - Persistence.deleteMPUserDefaultsData(token: mixpanel.apiToken) - waitForTrackingQueue() - tuple = Persistence.restoreIdentity(token: mixpanel.apiToken) - XCTAssertTrue("" == tuple.0 && nil == tuple.1 && nil == tuple.2 && nil == tuple.3 && nil == tuple.4) + testMixpanel.identify(distinctId: distinctId) + waitForTrackingQueue(testMixpanel) + sleep(1) + testMixpanel.createAlias(alias, distinctId: testMixpanel.distinctId) + waitForTrackingQueue(testMixpanel) + var mixpanelIdentity = MixpanelPersistence.loadIdentity(apiToken: testMixpanel.apiToken) + XCTAssertTrue(distinctId == mixpanelIdentity.distinctID && distinctId == mixpanelIdentity.peopleDistinctID && distinctId == mixpanelIdentity.userId && alias == mixpanelIdentity.alias) + testMixpanel.archive() + waitForTrackingQueue(testMixpanel) + testMixpanel.unarchive() + waitForTrackingQueue(testMixpanel) + mixpanelIdentity = MixpanelPersistence.loadIdentity(apiToken: testMixpanel.apiToken) + XCTAssertTrue(testMixpanel.distinctId == mixpanelIdentity.distinctID && testMixpanel.people.distinctId == mixpanelIdentity.peopleDistinctID && testMixpanel.anonymousId == mixpanelIdentity.anonymousId && + testMixpanel.userId == mixpanelIdentity.userId && testMixpanel.alias == mixpanelIdentity.alias) + MixpanelPersistence.deleteMPUserDefaultsData(apiToken: testMixpanel.apiToken) + waitForTrackingQueue(testMixpanel) + mixpanelIdentity = MixpanelPersistence.loadIdentity(apiToken: testMixpanel.apiToken) + XCTAssertTrue("" == mixpanelIdentity.distinctID && nil == mixpanelIdentity.peopleDistinctID && nil == mixpanelIdentity.anonymousId && nil == mixpanelIdentity.userId && nil == mixpanelIdentity.alias) + removeDBfile(testMixpanel.apiToken) } func testHadPersistedDistinctId() { - stubTrack() - XCTAssertNotNil(mixpanel.anonymousId) - XCTAssertNotNil(mixpanel.distinctId) - let distinctId: String = "d1" - mixpanel.anonymousId = nil - mixpanel.userId = nil - mixpanel.alias = nil - mixpanel.distinctId = distinctId - mixpanel.archive() - - XCTAssertEqual(mixpanel.distinctId, distinctId) + stubTrack() + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + XCTAssertNotNil(testMixpanel.distinctId) + let distinctId: String = "d1" + testMixpanel.anonymousId = nil + testMixpanel.userId = nil + testMixpanel.alias = nil + testMixpanel.distinctId = distinctId + testMixpanel.archive() + + XCTAssertEqual(testMixpanel.distinctId, distinctId) - let userId: String = "u1" - mixpanel.identify(distinctId: userId) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.anonymousId, distinctId) - XCTAssertEqual(mixpanel.userId, userId) - XCTAssertEqual(mixpanel.distinctId, userId) - XCTAssertTrue(mixpanel.hadPersistedDistinctId!) + let userId: String = "u1" + testMixpanel.identify(distinctId: userId) + waitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertEqual(testMixpanel.anonymousId, distinctId) + XCTAssertEqual(testMixpanel.userId, userId) + XCTAssertEqual(testMixpanel.distinctId, userId) + XCTAssertTrue(testMixpanel.hadPersistedDistinctId!) + removeDBfile(testMixpanel.apiToken) } func testTrackWithDefaultProperties() { - mixpanel.track(event: "Something Happened") - waitForTrackingQueue() - let e: InternalProperties = mixpanel.eventsQueue.last! + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.track(event: "Something Happened") + waitForTrackingQueue(testMixpanel) + let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(e["event"] as? String, "Something Happened", "incorrect event name") let p: InternalProperties = e["properties"] as! InternalProperties XCTAssertNotNil(p["$app_build_number"], "$app_build_number not set") @@ -391,23 +406,27 @@ class MixpanelDemoTests: MixpanelBaseTests { XCTAssertNotNil(p["time"], "time not set") XCTAssertEqual(p["$manufacturer"] as? String, "Apple", "incorrect $manufacturer") XCTAssertEqual(p["mp_lib"] as? String, "swift", "incorrect mp_lib") - XCTAssertEqual(p["token"] as? String, kTestToken, "incorrect token") + XCTAssertEqual(p["token"] as? String, testMixpanel.apiToken, "incorrect token") + removeDBfile(testMixpanel.apiToken) } func testTrackWithCustomProperties() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let now = Date() let p: Properties = ["string": "yello", "number": 3, "date": now, "$app_version": "override"] - mixpanel.track(event: "Something Happened", properties: p) - waitForTrackingQueue() - let props: InternalProperties = mixpanel.eventsQueue.last?["properties"] as! InternalProperties + testMixpanel.track(event: "Something Happened", properties: p) + waitForTrackingQueue(testMixpanel) + let props: InternalProperties = eventQueue(token: testMixpanel.apiToken).last?["properties"] as! InternalProperties XCTAssertEqual(props["string"] as? String, "yello") XCTAssertEqual(props["number"] as? Int, 3) - XCTAssertEqual(props["date"] as? Date, now) + let dateValue = props["date"] as! String + compareDate(dateString: dateValue, dateDate: now) XCTAssertEqual(props["$app_version"] as? String, "override", "reserved property override failed") + removeDBfile(testMixpanel.apiToken) } func testTrackWithOptionalProperties() { @@ -422,34 +441,39 @@ class MixpanelDemoTests: MixpanelBaseTests { "dict": optDict, "nested": nested, ] - mixpanel.track(event: "Optional Test", properties: p) - waitForTrackingQueue() - let props: InternalProperties = mixpanel.eventsQueue.last?["properties"] as! InternalProperties + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.track(event: "Optional Test", properties: p) + waitForTrackingQueue(testMixpanel) + let props: InternalProperties = eventQueue(token: testMixpanel.apiToken).last?["properties"] as! InternalProperties XCTAssertNil(props["nil"] as? Double) XCTAssertEqual(props["double"] as? Double, 1.0) - XCTAssertEqual(props["list"] as? Array, [nil, 1.0, 2.0]) + XCTAssertEqual(props["list"] as? Array, [1.0, 2.0]) XCTAssertEqual(props["dict"] as? Dictionary, ["nil": nil, "double": 1.0]) let nestedProp = props["nested"] as? Dictionary XCTAssertEqual(nestedProp?["dict"] as? Dictionary, ["nil": nil, "double": 1.0]) - XCTAssertEqual(nestedProp?["list"] as? Array, [nil, 1.0, 2.0]) + XCTAssertEqual(nestedProp?["list"] as? Array, [1.0, 2.0]) + removeDBfile(testMixpanel.apiToken) } func testTrackWithCustomDistinctIdAndToken() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let p: Properties = ["token": "t1", "distinct_id": "d1"] - mixpanel.track(event: "e1", properties: p) - waitForTrackingQueue() - let trackToken = (mixpanel.eventsQueue.last?["properties"] as? InternalProperties)?["token"] as? String - let trackDistinctId = (mixpanel.eventsQueue.last?["properties"] as? InternalProperties)?["distinct_id"] as? String + testMixpanel.track(event: "e1", properties: p) + waitForTrackingQueue(testMixpanel) + let trackToken = (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?["token"] as? String + let trackDistinctId = (eventQueue(token: testMixpanel.apiToken).last?["properties"] as? InternalProperties)?["distinct_id"] as? String XCTAssertEqual(trackToken, "t1", "user-defined distinct id not used in track.") XCTAssertEqual(trackDistinctId, "d1", "user-defined distinct id not used in track.") + removeDBfile(testMixpanel.apiToken) } - + func testTrackWithGroups() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" - mixpanel.trackWithGroups(event: "Something Happened", properties: [groupKey: "some other value", "p1": "value"], groups: [groupKey: groupID]) - waitForTrackingQueue() - let e: InternalProperties = mixpanel.eventsQueue.last! + testMixpanel.trackWithGroups(event: "Something Happened", properties: [groupKey: "some other value", "p1": "value"], groups: [groupKey: groupID]) + waitForTrackingQueue(testMixpanel) + let e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(e["event"] as? String, "Something Happened", "incorrect event name") let p: InternalProperties = e["properties"] as! InternalProperties XCTAssertNotNil(p["$app_build_number"], "$app_build_number not set") @@ -464,153 +488,174 @@ class MixpanelDemoTests: MixpanelBaseTests { XCTAssertNotNil(p["time"], "time not set") XCTAssertEqual(p["$manufacturer"] as? String, "Apple", "incorrect $manufacturer") XCTAssertEqual(p["mp_lib"] as? String, "swift", "incorrect mp_lib") - XCTAssertEqual(p["token"] as? String, kTestToken, "incorrect token") + XCTAssertEqual(p["token"] as? String, testMixpanel.apiToken, "incorrect token") XCTAssertEqual(p[groupKey] as? String, groupID, "incorrect group id") XCTAssertEqual(p["p1"] as? String, "value", "incorrect group value") + removeDBfile(testMixpanel.apiToken) } func testRegisterSuperProperties() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) var p: Properties = ["p1": "a", "p2": 3, "p3": Date()] - mixpanel.registerSuperProperties(p) - waitForTrackingQueue() - XCTAssertEqual(NSDictionary(dictionary: mixpanel.currentSuperProperties()), + testMixpanel.registerSuperProperties(p) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(NSDictionary(dictionary: testMixpanel.currentSuperProperties()), NSDictionary(dictionary: p), "register super properties failed") p = ["p1": "b"] - mixpanel.registerSuperProperties(p) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.currentSuperProperties()["p1"] as? String, "b", + testMixpanel.registerSuperProperties(p) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()["p1"] as? String, "b", "register super properties failed to overwrite existing value") p = ["p4": "a"] - mixpanel.registerSuperPropertiesOnce(p) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.currentSuperProperties()["p4"] as? String, "a", + testMixpanel.registerSuperPropertiesOnce(p) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()["p4"] as? String, "a", "register super properties once failed first time") p = ["p4": "b"] - mixpanel.registerSuperPropertiesOnce(p) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.currentSuperProperties()["p4"] as? String, "a", + testMixpanel.registerSuperPropertiesOnce(p) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()["p4"] as? String, "a", "register super properties once failed second time") p = ["p4": "c"] - mixpanel.registerSuperPropertiesOnce(p, defaultValue: "d") - waitForTrackingQueue() - XCTAssertEqual(mixpanel.currentSuperProperties()["p4"] as? String, "a", + testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "d") + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()["p4"] as? String, "a", "register super properties once with default value failed when no match") - mixpanel.registerSuperPropertiesOnce(p, defaultValue: "a") - waitForTrackingQueue() - XCTAssertEqual(mixpanel.currentSuperProperties()["p4"] as? String, "c", + testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "a") + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()["p4"] as? String, "c", "register super properties once with default value failed when match") - mixpanel.unregisterSuperProperty("a") - waitForTrackingQueue() - XCTAssertNil(mixpanel.currentSuperProperties()["a"], + testMixpanel.unregisterSuperProperty("a") + waitForTrackingQueue(testMixpanel) + XCTAssertNil(testMixpanel.currentSuperProperties()["a"], "unregister super property failed") // unregister non-existent super property should not throw - mixpanel.unregisterSuperProperty("a") - mixpanel.clearSuperProperties() - waitForTrackingQueue() - XCTAssertTrue(mixpanel.currentSuperProperties().isEmpty, + testMixpanel.unregisterSuperProperty("a") + testMixpanel.clearSuperProperties() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(testMixpanel.currentSuperProperties().isEmpty, "clear super properties failed") + removeDBfile(testMixpanel.apiToken) } func testInvalidPropertiesTrack() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let p: Properties = ["data": [Data()]] XCTExpectAssert("property type should not be allowed") { - mixpanel.track(event: "e1", properties: p) + testMixpanel.track(event: "e1", properties: p) } + removeDBfile(testMixpanel.apiToken) } func testInvalidSuperProperties() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let p: Properties = ["data": [Data()]] XCTExpectAssert("property type should not be allowed") { - mixpanel.registerSuperProperties(p) + testMixpanel.registerSuperProperties(p) } + removeDBfile(testMixpanel.apiToken) } - + func testInvalidSuperProperties2() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let p: Properties = ["data": [Data()]] XCTExpectAssert("property type should not be allowed") { - mixpanel.registerSuperPropertiesOnce(p) + testMixpanel.registerSuperPropertiesOnce(p) } + removeDBfile(testMixpanel.apiToken) } func testInvalidSuperProperties3() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let p: Properties = ["data": [Data()]] XCTExpectAssert("property type should not be allowed") { - mixpanel.registerSuperPropertiesOnce(p, defaultValue: "v") + testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "v") } + removeDBfile(testMixpanel.apiToken) } - + func testValidPropertiesTrack() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let p: Properties = allPropertyTypes() - mixpanel.track(event: "e1", properties: p) + testMixpanel.track(event: "e1", properties: p) + removeDBfile(testMixpanel.apiToken) } func testValidSuperProperties() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let p: Properties = allPropertyTypes() - mixpanel.registerSuperProperties(p) - mixpanel.registerSuperPropertiesOnce(p) - mixpanel.registerSuperPropertiesOnce(p, defaultValue: "v") + testMixpanel.registerSuperProperties(p) + testMixpanel.registerSuperPropertiesOnce(p) + testMixpanel.registerSuperPropertiesOnce(p, defaultValue: "v") + removeDBfile(testMixpanel.apiToken) } func testReset() { stubTrack() stubEngage() - mixpanel.identify(distinctId: "d1") - mixpanel.track(event: "e1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.track(event: "e1") + sleep(1) let p: Properties = ["p1": "a"] - mixpanel.registerSuperProperties(p) - mixpanel.people.set(properties: p) - mixpanel.archive() - mixpanel.reset() - waitForTrackingQueue() + testMixpanel.registerSuperProperties(p) + testMixpanel.people.set(properties: p) + testMixpanel.archive() + testMixpanel.reset() + waitForTrackingQueue(testMixpanel) #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(mixpanel.distinctId, - mixpanel.defaultDistinctId(), + XCTAssertEqual(testMixpanel.distinctId, + testMixpanel.defaultDistinctId(), "distinct id failed to reset") #endif - XCTAssertNil(mixpanel.people.distinctId, "people distinct id failed to reset") - XCTAssertTrue(mixpanel.currentSuperProperties().isEmpty, + XCTAssertNil(testMixpanel.people.distinctId, "people distinct id failed to reset") + XCTAssertTrue(testMixpanel.currentSuperProperties().isEmpty, "super properties failed to reset") - XCTAssertTrue(mixpanel.eventsQueue.isEmpty, "events queue failed to reset") - XCTAssertTrue(mixpanel.people.peopleQueue.isEmpty, "people queue failed to reset") - mixpanel = Mixpanel.initialize(token: kTestToken, flushInterval: 60) - waitForTrackingQueue() + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "events queue failed to reset") + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "people queue failed to reset") + let testMixpanel2 = Mixpanel.initialize(token: randomId(), flushInterval: 60) + waitForTrackingQueue(testMixpanel2) #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(mixpanel.distinctId, mixpanel.defaultDistinctId(), + XCTAssertEqual(testMixpanel2.distinctId, testMixpanel2.defaultDistinctId(), "distinct id failed to reset after archive") #endif - XCTAssertNil(mixpanel.people.distinctId, + XCTAssertNil(testMixpanel2.people.distinctId, "people distinct id failed to reset after archive") - XCTAssertTrue(mixpanel.currentSuperProperties().isEmpty, + XCTAssertTrue(testMixpanel2.currentSuperProperties().isEmpty, "super properties failed to reset after archive") - XCTAssertTrue(mixpanel.eventsQueue.isEmpty, + XCTAssertTrue(eventQueue(token: testMixpanel2.apiToken).isEmpty, "events queue failed to reset after archive") - XCTAssertTrue(mixpanel.people.peopleQueue.isEmpty, + XCTAssertTrue(peopleQueue(token: testMixpanel2.apiToken).isEmpty, "people queue failed to reset after archive") + removeDBfile(testMixpanel.apiToken) + removeDBfile(testMixpanel2.apiToken) } func testArchiveNSNumberBoolIntProperty() { let testToken = randomId() - mixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) + let testMixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) let aBoolNumber: Bool = true let aBoolNSNumber = NSNumber(value: aBoolNumber) - + let aIntNumber: Int = 1 let aIntNSNumber = NSNumber(value: aIntNumber) + + testMixpanel.track(event: "e1", properties: ["p1": aBoolNSNumber, "p2": aIntNSNumber]) + testMixpanel.archive() + waitForTrackingQueue(testMixpanel) - mixpanel.track(event: "e1", properties: ["p1": aBoolNSNumber, "p2": aIntNSNumber]) - mixpanel.archive() - waitForTrackingQueue() - mixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) - waitForTrackingQueue() - let properties: [String: Any] = mixpanel.eventsQueue[0]["properties"] as! [String: Any] - + let testMixpanel2 = Mixpanel.initialize(token: testToken, flushInterval: 60) + waitForTrackingQueue(testMixpanel2) + let properties: [String: Any] = eventQueue(token: testMixpanel2.apiToken)[0]["properties"] as! [String: Any] + XCTAssertTrue(isBoolNumber(num: properties["p1"]! as! NSNumber), "The bool value should be unarchived as bool") XCTAssertFalse(isBoolNumber(num: properties["p2"]! as! NSNumber), "The int value should not be unarchived as bool") + removeDBfile(testToken) } - + private func isBoolNumber(num: NSNumber) -> Bool { let boolID = CFBooleanGetTypeID() // the type ID of CFBoolean @@ -620,250 +665,135 @@ class MixpanelDemoTests: MixpanelBaseTests { func testArchive() { let testToken = randomId() - mixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) + let testMixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(mixpanel.distinctId, mixpanel.defaultDistinctId(), + XCTAssertEqual(testMixpanel.distinctId, testMixpanel.defaultDistinctId(), "default distinct id archive failed") #endif - XCTAssertTrue(mixpanel.currentSuperProperties().isEmpty, + XCTAssertTrue(testMixpanel.currentSuperProperties().isEmpty, "default super properties archive failed") - XCTAssertTrue(mixpanel.eventsQueue.isEmpty, "default events queue archive failed") - XCTAssertNil(mixpanel.people.distinctId, "default people distinct id archive failed") - XCTAssertTrue(mixpanel.people.peopleQueue.isEmpty, "default people queue archive failed") + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).isEmpty, "default events queue archive failed") + XCTAssertNil(testMixpanel.people.distinctId, "default people distinct id archive failed") + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).isEmpty, "default people queue archive failed") let p: Properties = ["p1": "a"] - mixpanel.identify(distinctId: "d1") - mixpanel.registerSuperProperties(p) - sleep(2) - mixpanel.track(event: "e1") - mixpanel.track(event: "e3") - mixpanel.track(event: "e4") - mixpanel.track(event: "e5") - mixpanel.track(event: "e6") - mixpanel.track(event: "e7") - mixpanel.track(event: "e8") - mixpanel.track(event: "e9") - mixpanel.track(event: "e10") - mixpanel.people.set(properties: p) - mixpanel.timedEvents["e2"] = 5 - mixpanel.archive() - waitForTrackingQueue() - - mixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.distinctId, "d1", "custom distinct archive failed") - XCTAssertTrue(mixpanel.currentSuperProperties().count == 1, + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + sleep(1) + testMixpanel.registerSuperProperties(p) + testMixpanel.track(event: "e1") + testMixpanel.track(event: "e2") + testMixpanel.track(event: "e3") + testMixpanel.track(event: "e4") + testMixpanel.track(event: "e5") + testMixpanel.track(event: "e6") + testMixpanel.track(event: "e7") + testMixpanel.track(event: "e8") + testMixpanel.track(event: "e9") + testMixpanel.people.set(properties: p) + waitForTrackingQueue(testMixpanel) + testMixpanel.timedEvents["e2"] = 5 + testMixpanel.archive() + let testMixpanel2 = Mixpanel.initialize(token: testToken, flushInterval: 60) + waitForTrackingQueue(testMixpanel2) + sleep(1) + XCTAssertEqual(testMixpanel2.distinctId, "d1", "custom distinct archive failed") + XCTAssertTrue(testMixpanel2.currentSuperProperties().count == 1, "custom super properties archive failed") - XCTAssertEqual(mixpanel.eventsQueue[1]["event"] as? String, "e1", + let eventQueueValue = eventQueue(token: testMixpanel2.apiToken) + + XCTAssertEqual(eventQueueValue[1]["event"] as? String, "e1", "event was not successfully archived/unarchived") - XCTAssertEqual(mixpanel.eventsQueue[2]["event"] as? String, "e3", + XCTAssertEqual(eventQueueValue[2]["event"] as? String, "e2", "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(mixpanel.eventsQueue[3]["event"] as? String, "e4", + XCTAssertEqual(eventQueueValue[3]["event"] as? String, "e3", "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(mixpanel.eventsQueue[4]["event"] as? String, "e5", + XCTAssertEqual(eventQueueValue[4]["event"] as? String, "e4", "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(mixpanel.eventsQueue[5]["event"] as? String, "e6", + XCTAssertEqual(eventQueueValue[5]["event"] as? String, "e5", "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(mixpanel.eventsQueue[6]["event"] as? String, "e7", + XCTAssertEqual(eventQueueValue[6]["event"] as? String, "e6", "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(mixpanel.eventsQueue[7]["event"] as? String, "e8", + XCTAssertEqual(eventQueueValue[7]["event"] as? String, "e7", "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(mixpanel.eventsQueue[8]["event"] as? String, "e9", + XCTAssertEqual(eventQueueValue[8]["event"] as? String, "e8", "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(mixpanel.eventsQueue[9]["event"] as? String, "e10", + XCTAssertEqual(eventQueueValue[9]["event"] as? String, "e9", "event was not successfully archived/unarchived or order is incorrect") - XCTAssertEqual(mixpanel.people.distinctId, "d1", + XCTAssertEqual(testMixpanel2.people.distinctId, "d1", "custom people distinct id archive failed") - XCTAssertTrue(mixpanel.people.peopleQueue.count == 1, "pending people queue archive failed") - XCTAssertEqual(mixpanel.timedEvents["e2"] as? Double, 5.0, + XCTAssertTrue(peopleQueue(token: testMixpanel2.apiToken).count == 1, "pending people queue archive failed") + XCTAssertEqual(testMixpanel2.timedEvents["e2"] as? Double, 5.0, "timedEvents archive failed") - let fileManager = FileManager.default - XCTAssertTrue(fileManager.fileExists( - atPath: Persistence.filePathWithType(.events, token: testToken)!), - "events archive file not removed") - XCTAssertTrue(fileManager.fileExists( - atPath: Persistence.filePathWithType(.people, token: testToken)!), - "people archive file not removed") - XCTAssertTrue(fileManager.fileExists( - atPath: Persistence.filePathWithType(.properties, token: testToken)!), - "properties archive file not removed") - mixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) - XCTAssertEqual(mixpanel.distinctId, "d1", "expecting d1 as distinct id as initialised") - XCTAssertTrue(mixpanel.currentSuperProperties().count == 1, + + let testMixpanel3 = Mixpanel.initialize(token: testToken, flushInterval: 60) + XCTAssertEqual(testMixpanel3.distinctId, "d1", "expecting d1 as distinct id as initialised") + XCTAssertTrue(testMixpanel3.currentSuperProperties().count == 1, "default super properties expected to have 1 item") - XCTAssertNotNil(mixpanel.eventsQueue, "default events queue from no file is nil") - XCTAssertTrue(mixpanel.eventsQueue.count == 10, "default events queue expecting 10 items ($identify call added)") - XCTAssertNotNil(mixpanel.people.distinctId, + XCTAssertNotNil(eventQueue(token: testMixpanel3.apiToken), "default events queue is nil") + XCTAssertTrue(eventQueue(token: testMixpanel3.apiToken).count == 10, "default events queue expecting 10 items ($identify call added)") + XCTAssertNotNil(testMixpanel3.people.distinctId, "default people distinct id from no file failed") - XCTAssertNotNil(mixpanel.people.peopleQueue, "default people queue from no file is nil") - XCTAssertTrue(mixpanel.people.peopleQueue.count == 1, "default people queue expecting 1 item") - XCTAssertTrue(mixpanel.timedEvents.count == 1, "timedEvents expecting 1 item") - // corrupt file - let garbage = "garbage".data(using: String.Encoding.utf8)! - do { - try garbage.write(to: URL( - fileURLWithPath: Persistence.filePathWithType(.events, token: testToken)!), - options: []) - try garbage.write(to: URL( - fileURLWithPath: Persistence.filePathWithType(.people, token: testToken)!), - options: []) - try garbage.write(to: URL( - fileURLWithPath: Persistence.filePathWithType(.properties, token: testToken)!), - options: []) - } catch { - print("couldn't write data") - } - XCTAssertTrue(fileManager.fileExists( - atPath: Persistence.filePathWithType(.events, token: testToken)!), - "events archive file not removed") - XCTAssertTrue(fileManager.fileExists( - atPath: Persistence.filePathWithType(.people, token: testToken)!), - "people archive file not removed") - XCTAssertTrue(fileManager.fileExists( - atPath: Persistence.filePathWithType(.properties, token: testToken)!), - "properties archive file not removed") - Persistence.deleteMPUserDefaultsData(token: testToken) - mixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) - waitForTrackingQueue() - #if MIXPANEL_UNIQUE_DISTINCT_ID - XCTAssertEqual(mixpanel.distinctId, mixpanel.defaultDistinctId(), - "default distinct id from garbage failed") - #endif - XCTAssertTrue(mixpanel.currentSuperProperties().isEmpty, - "default super properties from garbage failed") - XCTAssertNotNil(mixpanel.eventsQueue, "default events queue from garbage is nil") - XCTAssertTrue(mixpanel.eventsQueue.isEmpty, - "default events queue from garbage not empty") - XCTAssertNil(mixpanel.people.distinctId, - "default people distinct id from garbage failed") - XCTAssertNotNil(mixpanel.people.peopleQueue, - "default people queue from garbage is nil") - XCTAssertTrue(mixpanel.people.peopleQueue.isEmpty, - "default people queue from garbage not empty") - XCTAssertTrue(mixpanel.timedEvents.isEmpty, - "timedEvents is not empty") - } - - func testUnarchiveInconsistentData() { - // corrupt file - let fileManager = FileManager.default - let testToken = randomId() - // Prior 2.1.7 we used to share every class between main target and extension target(appex). For serialization, this will cause problem. - // Because if the archive is triggered in extension, the class object will be saved as [Target Name].[Class name] for the key. Since in later version, - // we removed extension target. If the archive happened in 2.1.6, and unarchive happened in 2.4.4 (this is the case for upgrading the sdk), it will trigger a crash - // (throw NSException) because when try to map the key [Class name] to [Target Name].[Class name] and [Target Name].[Class name] no longer exists. - // The below line is to simulate this situation. Foo <--> Extension.Foo, Extension.Foo doesn't exist. We should catch the NSException and reset the file instead of - // crash the app - - mixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) - waitForTrackingQueue() - // waitForArchive() - XCTAssertTrue(!fileManager.fileExists( - atPath: Persistence.filePathWithType(.events, token: testToken)!), - "events archive file not removed") - XCTAssertTrue(!fileManager.fileExists( - atPath: Persistence.filePathWithType(.people, token: testToken)!), - "people archive file not removed") - XCTAssertTrue(!fileManager.fileExists( - atPath: Persistence.filePathWithType(.properties, token: testToken)!), - "properties archive file not removed") - XCTAssertTrue(!fileManager.fileExists( - atPath: Persistence.filePathWithType(.optOutStatus, token: testToken)!), - "properties archive file not removed") - } - - func testUnarchiveCorruptedData() { - // corrupt file - let fileManager = FileManager.default - let garbage = "garbage".data(using: String.Encoding.utf8)! - let testToken = randomId() - - do { - try garbage.write(to: URL( - fileURLWithPath: Persistence.filePathWithType(.events, token: testToken)!), - options: []) - try garbage.write(to: URL( - fileURLWithPath: Persistence.filePathWithType(.people, token: testToken)!), - options: []) - try garbage.write(to: URL( - fileURLWithPath: Persistence.filePathWithType(.properties, token: testToken)!), - options: []) - try garbage.write(to: URL( - fileURLWithPath: Persistence.filePathWithType(.optOutStatus, token: testToken)!), - options: []) - } catch { - print("couldn't write data") - } - - mixpanel = Mixpanel.initialize(token: testToken, flushInterval: 60) - waitForTrackingQueue() - - - XCTAssertTrue(!fileManager.fileExists( - atPath: Persistence.filePathWithType(.events, token: testToken)!), - "events archive file not removed") - XCTAssertTrue(!fileManager.fileExists( - atPath: Persistence.filePathWithType(.people, token: testToken)!), - "people archive file not removed") - XCTAssertTrue(!fileManager.fileExists( - atPath: Persistence.filePathWithType(.properties, token: testToken)!), - "properties archive file not removed") - XCTAssertTrue(!fileManager.fileExists( - atPath: Persistence.filePathWithType(.optOutStatus, token: testToken)!), - "properties archive file not removed") - waitForTrackingQueue() + XCTAssertNotNil(peopleQueue(token:testMixpanel3.apiToken), "default people queue from no file is nil") + XCTAssertTrue(peopleQueue(token:testMixpanel3.apiToken).count == 1, "default people queue expecting 1 item") + XCTAssertTrue(testMixpanel3.timedEvents.count == 1, "timedEvents expecting 1 item") + removeDBfile(testToken) } + func testMixpanelDelegate() { - mixpanel.delegate = self - mixpanel.identify(distinctId: "d1") - mixpanel.track(event: "e1") - mixpanel.people.set(property: "p1", to: "a") - waitForTrackingQueue() - flushAndWaitForNetworkQueue() - XCTAssertTrue(mixpanel.eventsQueue.count == 2, "delegate should have stopped flush") - XCTAssertTrue(mixpanel.people.peopleQueue.count == 1, "delegate should have stopped flush") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.delegate = self + testMixpanel.identify(distinctId: "d1") + testMixpanel.track(event: "e1") + testMixpanel.people.set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + sleep(1) + flushAndWaitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 2, "delegate should have stopped flush") + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).count == 1, "delegate should have stopped flush") + removeDBfile(testMixpanel.apiToken) } func testEventTiming() { - mixpanel.track(event: "Something Happened") - waitForTrackingQueue() - var e: InternalProperties = mixpanel.eventsQueue.last! + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.track(event: "Something Happened") + waitForTrackingQueue(testMixpanel) + var e: InternalProperties = eventQueue(token: testMixpanel.apiToken).last! var p = e["properties"] as! InternalProperties XCTAssertNil(p["$duration"], "New events should not be timed.") - mixpanel.time(event: "400 Meters") - mixpanel.track(event: "500 Meters") - waitForTrackingQueue() - e = mixpanel.eventsQueue.last! + testMixpanel.time(event: "400 Meters") + testMixpanel.track(event: "500 Meters") + waitForTrackingQueue(testMixpanel) + e = eventQueue(token: testMixpanel.apiToken).last! p = e["properties"] as! InternalProperties XCTAssertNil(p["$duration"], "The exact same event name is required for timing.") - mixpanel.track(event: "400 Meters") - waitForTrackingQueue() - e = mixpanel.eventsQueue.last! + testMixpanel.track(event: "400 Meters") + waitForTrackingQueue(testMixpanel) + e = eventQueue(token: testMixpanel.apiToken).last! p = e["properties"] as! InternalProperties XCTAssertNotNil(p["$duration"], "This event should be timed.") - mixpanel.track(event: "400 Meters") - waitForTrackingQueue() - e = mixpanel.eventsQueue.last! + testMixpanel.track(event: "400 Meters") + waitForTrackingQueue(testMixpanel) + e = eventQueue(token: testMixpanel.apiToken).last! p = e["properties"] as! InternalProperties XCTAssertNil(p["$duration"], "Tracking the same event should require a second call to timeEvent.") - mixpanel.time(event: "Time Event A") - mixpanel.time(event: "Time Event B") - mixpanel.time(event: "Time Event C") - waitForTrackingQueue() - XCTAssertTrue(mixpanel.timedEvents.count == 3, "Each call to time() should add an event to timedEvents") - XCTAssertNotNil(mixpanel.timedEvents["Time Event A"], "Keys in timedEvents should be event names") - mixpanel.clearTimedEvent(event: "Time Event A") - waitForTrackingQueue() - XCTAssertNil(mixpanel.timedEvents["Time Event A"], "clearTimedEvent should remove key/value pair") - XCTAssertTrue(mixpanel.timedEvents.count == 2, "clearTimedEvent shoud remove only one key/value pair") - mixpanel.clearTimedEvents() - waitForTrackingQueue() - XCTAssertTrue(mixpanel.timedEvents.count == 0, "clearTimedEvents should remove all key/value pairs") - } - - func testTelephonyInfoInitialized() { - XCTAssertNotNil(MixpanelInstance.telephonyInfo, "telephonyInfo wasn't initialized") + testMixpanel.time(event: "Time Event A") + testMixpanel.time(event: "Time Event B") + testMixpanel.time(event: "Time Event C") + waitForTrackingQueue(testMixpanel) + var testTimedEvents = MixpanelPersistence.loadTimedEvents(apiToken: testMixpanel.apiToken) + XCTAssertTrue(testTimedEvents.count == 3, "Each call to time() should add an event to timedEvents") + XCTAssertNotNil(testTimedEvents["Time Event A"], "Keys in timedEvents should be event names") + testMixpanel.clearTimedEvent(event: "Time Event A") + waitForTrackingQueue(testMixpanel) + testTimedEvents = MixpanelPersistence.loadTimedEvents(apiToken: testMixpanel.apiToken) + XCTAssertNil(testTimedEvents["Time Event A"], "clearTimedEvent should remove key/value pair") + XCTAssertTrue(testTimedEvents.count == 2, "clearTimedEvent shoud remove only one key/value pair") + testMixpanel.clearTimedEvents() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(MixpanelPersistence.loadTimedEvents(apiToken: testMixpanel.apiToken).count == 0, "clearTimedEvents should remove all key/value pairs") + removeDBfile(testMixpanel.apiToken) } func testReadWriteLock() { @@ -886,76 +816,256 @@ class MixpanelDemoTests: MixpanelBaseTests { } } } - + func testSetGroup() { stubTrack() stubEngage() - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") let groupKey = "test_key" let groupValue = "test_value" - mixpanel.setGroup(groupKey: groupKey, groupIDs: [groupValue]) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) - let q = mixpanel.people.peopleQueue.last!["$set"] as! InternalProperties + testMixpanel.setGroup(groupKey: groupKey, groupIDs: [groupValue]) + waitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties XCTAssertEqual(q[groupKey] as? [String], [groupValue], "group value people property not queued") assertDefaultPeopleProperties(q) + removeDBfile(testMixpanel.apiToken) } - + func testAddGroup() { stubTrack() stubEngage() - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") let groupKey = "test_key" let groupValue = "test_value" - - mixpanel.addGroup(groupKey: groupKey, groupID: groupValue) - waitForMixpanelQueues() - XCTAssertEqual(mixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) - waitForMixpanelQueues() - let q = mixpanel.people.peopleQueue.last!["$set"] as! InternalProperties + + testMixpanel.addGroup(groupKey: groupKey, groupID: groupValue) + waitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) + waitForTrackingQueue(testMixpanel) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties XCTAssertEqual(q[groupKey] as? [String], [groupValue], "addGroup people update not queued") assertDefaultPeopleProperties(q) - - mixpanel.addGroup(groupKey: groupKey, groupID: groupValue) - waitForMixpanelQueues() - XCTAssertEqual(mixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) - waitForMixpanelQueues() - let q2 = mixpanel.people.peopleQueue.last!["$union"] as! InternalProperties + + testMixpanel.addGroup(groupKey: groupKey, groupID: groupValue) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue]) + waitForTrackingQueue(testMixpanel) + waitForTrackingQueue(testMixpanel) + let q2 = peopleQueue(token: testMixpanel.apiToken).last!["$union"] as! InternalProperties XCTAssertEqual(q2[groupKey] as? [String], [groupValue], "addGroup people update not queued") let newVal = "new_group" - mixpanel.addGroup(groupKey: groupKey, groupID: newVal) - waitForMixpanelQueues() - XCTAssertEqual(mixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue, newVal]) - waitForMixpanelQueues() - let q3 = mixpanel.people.peopleQueue.last!["$union"] as! InternalProperties + testMixpanel.addGroup(groupKey: groupKey, groupID: newVal) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue, newVal]) + waitForTrackingQueue(testMixpanel) + waitForTrackingQueue(testMixpanel) + let q3 = peopleQueue(token: testMixpanel.apiToken).last!["$union"] as! InternalProperties XCTAssertEqual(q3[groupKey] as? [String], [newVal], "addGroup people update not queued") + removeDBfile(testMixpanel.apiToken) } - + func testRemoveGroup() { stubTrack() stubEngage() - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") let groupKey = "test_key" let groupValue = "test_value" let newVal = "new_group" + + testMixpanel.setGroup(groupKey: groupKey, groupIDs: [groupValue, newVal]) + waitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue, newVal]) + + testMixpanel.removeGroup(groupKey: groupKey, groupID: groupValue) + waitForTrackingQueue(testMixpanel) + XCTAssertEqual(testMixpanel.currentSuperProperties()[groupKey] as? [String], [newVal]) + waitForTrackingQueue(testMixpanel) + let q2 = peopleQueue(token: testMixpanel.apiToken).last!["$remove"] as! InternalProperties + XCTAssertEqual(q2[groupKey] as? String, groupValue, "removeGroup people update not queued") + + testMixpanel.removeGroup(groupKey: groupKey, groupID: groupValue) + waitForTrackingQueue(testMixpanel) + XCTAssertNil(testMixpanel.currentSuperProperties()[groupKey]) + waitForTrackingQueue(testMixpanel) + let q3 = peopleQueue(token: testMixpanel.apiToken).last!["$unset"] as! [String] + XCTAssertEqual(q3, [groupKey], "removeGroup people update not queued") + removeDBfile(testMixpanel.apiToken) + } + + + func testReadWriteMultiThreadShouldNotCrash() { + let concurentQueue = DispatchQueue(label: "multithread", attributes: .concurrent) + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + + for n in 1...10 { + concurentQueue.async { + testMixpanel.track(event: "event\(n)") + } + concurentQueue.async { + testMixpanel.flush() + } + concurentQueue.async { + testMixpanel.archive() + } + concurentQueue.async { + testMixpanel.reset() + } + concurentQueue.async { + testMixpanel.createAlias("aaa11", distinctId: testMixpanel.distinctId) + testMixpanel.identify(distinctId: "test") + } + concurentQueue.async { + testMixpanel.registerSuperProperties(["Plan": "Mega"]) + } + concurentQueue.async { + let _ = testMixpanel.currentSuperProperties() + } + concurentQueue.async { + testMixpanel.people.set(property: "aaa", to: "SwiftSDK Cocoapods") + testMixpanel.getGroup(groupKey: "test", groupID: 123).set(properties: ["test": 123]) + testMixpanel.removeGroup(groupKey: "test", groupID: 123) + } + concurentQueue.async { + testMixpanel.track(event: "test") + testMixpanel.time(event: "test") + testMixpanel.clearTimedEvents() + } + } + sleep(5) + removeDBfile(testMixpanel.apiToken) + } + + func testMPDB() { + let testToken = randomId() + let numRows = 50 + let halfRows = numRows/2 + let eventName = "Test Event" + func _inner() { + removeDBfile(testToken) + let mpdb = MPDB.init(token: testToken) + mpdb.open() + for pType in PersistenceType.allCases { + let emptyArray: [InternalProperties] = mpdb.readRows(pType, numRows: numRows) + XCTAssertTrue(emptyArray.isEmpty, "Table should be empty") + for i in 0...numRows-1 { + let eventObj : InternalProperties = ["event": eventName, "properties": ["index": i]] + let eventData = JSONHandler.serializeJSONObject(eventObj)! + mpdb.insertRow(pType, data: eventData) + } + let dataArray : [InternalProperties] = mpdb.readRows(pType, numRows: halfRows) + XCTAssertEqual(dataArray.count, halfRows, "Should have read only half of the rows") + var ids: [Int32] = [] + for (n, entity) in dataArray.enumerated() { + guard let id = entity["id"] as? Int32 else { + continue + } + ids.append(id) + XCTAssertEqual(entity["event"] as! String, eventName, "Event name should be unchanged") + // index should be oldest events, 0 - 24 + XCTAssertEqual(entity["properties"] as! [String : Int], ["index": n], "Should read oldest events first") + } + + mpdb.deleteRows(pType, ids: [1, 2, 3]) + let dataArray2 : [InternalProperties] = mpdb.readRows(pType, numRows: numRows) + // even though we requested numRows, there should only be halfRows left + XCTAssertEqual(dataArray2.count, numRows - 3, "Should have deleted half the rows") + for (n, entity) in dataArray2.enumerated() { + XCTAssertEqual(entity["event"] as! String, eventName, "Event name should be unchanged") + // old events (0-24) should have been deleted so index should be recent events 25-49 + XCTAssertEqual(entity["properties"] as! [String : Int], ["index": n + halfRows], "Should have deleted oldest events first") + } + mpdb.close() + } + } + 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") - mixpanel.setGroup(groupKey: groupKey, groupIDs: [groupValue, newVal]) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.currentSuperProperties()[groupKey] as? [String], [groupValue, newVal]) + let events = eventQueue(token: testMixpanel.apiToken) + XCTAssertEqual(events.count, 306) - mixpanel.removeGroup(groupKey: groupKey, groupID: groupValue) - waitForTrackingQueue() - XCTAssertEqual(mixpanel.currentSuperProperties()[groupKey] as? [String], [newVal]) - waitForTrackingQueue() - let q2 = mixpanel.people.peopleQueue.last!["$remove"] as! InternalProperties - XCTAssertEqual(q2[groupKey] as? String, groupValue, "removeGroup people update not queued") + 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") - mixpanel.removeGroup(groupKey: groupKey, groupID: groupValue) - waitForTrackingQueue() - XCTAssertNil(mixpanel.currentSuperProperties()[groupKey]) - waitForTrackingQueue() - let q3 = mixpanel.people.peopleQueue.last!["$unset"] as! [String] - XCTAssertEqual(q3, [groupKey], "removeGroup people update not queued") + 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/MixpanelGroupTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelGroupTests.swift index 14f6d50bb..5829ff156 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelGroupTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelGroupTests.swift @@ -15,127 +15,129 @@ import Nocilla class MixpanelGroupTests: MixpanelBaseTests { func testGroupSet() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" let p: Properties = ["p1": "a"] - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) - waitForTrackingQueue() - let msg = mixpanel.groupsQueue.last! + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(msg["$group_key"] as! String, groupKey) XCTAssertEqual(msg["$group_id"] as! String, groupID) let q = msg["$set"] as! InternalProperties XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") + removeDBfile(testMixpanel.apiToken) } func testGroupSetIntegerID() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" - let groupID = 3 + let groupID = 3 let p: Properties = ["p1": "a"] - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) - waitForTrackingQueue() - let msg = mixpanel.groupsQueue.last! + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(msg["$group_key"] as! String, groupKey) XCTAssertEqual(msg["$group_id"] as! Int, groupID) let q = msg["$set"] as! InternalProperties XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") + removeDBfile(testMixpanel.apiToken) } func testGroupSetOnce() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" let p: Properties = ["p1": "a"] - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).setOnce(properties: p) - waitForTrackingQueue() - let msg = mixpanel.groupsQueue.last! + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).setOnce(properties: p) + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(msg["$group_key"] as! String, groupKey) XCTAssertEqual(msg["$group_id"] as! String, groupID) let q = msg["$set_once"] as! InternalProperties XCTAssertEqual(q["p1"] as? String, "a", "custom group property not queued") + removeDBfile(testMixpanel.apiToken) } func testGroupSetTo() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "p1", to: "a") - waitForTrackingQueue() - let msg = mixpanel.groupsQueue.last! + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(msg["$group_key"] as! String, groupKey) XCTAssertEqual(msg["$group_id"] as! String, groupID) let p = msg["$set"] as! InternalProperties XCTAssertEqual(p["p1"] as? String, "a", "custom group property not queued") + removeDBfile(testMixpanel.apiToken) } func testGroupUnset() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).unset(property: "p1") - waitForTrackingQueue() - let msg = mixpanel.groupsQueue.last! + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).unset(property: "p1") + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(msg["$group_key"] as! String, groupKey) XCTAssertEqual(msg["$group_id"] as! String, groupID) XCTAssertEqual(msg["$unset"] as! [String], ["p1"], "group property unset not queued") + removeDBfile(testMixpanel.apiToken) } func testGroupRemove() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).remove(key: "p1", value: "a") - waitForTrackingQueue() - let msg = mixpanel.groupsQueue.last! + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).remove(key: "p1", value: "a") + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(msg["$group_key"] as! String, groupKey) XCTAssertEqual(msg["$group_id"] as! String, groupID) XCTAssertEqual(msg["$remove"] as? [String: String], ["p1": "a"], "group property remove not queued") + removeDBfile(testMixpanel.apiToken) } func testGroupUnion() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).union(key: "p1", values: ["a"]) - waitForTrackingQueue() - let msg = mixpanel.groupsQueue.last! + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).union(key: "p1", values: ["a"]) + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(msg["$group_key"] as! String, groupKey) XCTAssertEqual(msg["$group_id"] as! String, groupID) XCTAssertEqual(msg["$union"] as? [String: [String]], ["p1": ["a"]], "group property union not queued") + removeDBfile(testMixpanel.apiToken) } - func testDropGroupRecords() { - QueueConstants.queueSize = 500 - let groupKey = "test_key" - let groupID = "test_id" - for i in 0..<505 { - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "i", to: i) - } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.groupsQueue.count == 500) - var r: InternalProperties = mixpanel.groupsQueue.first! - XCTAssertEqual(r["$group_key"] as! String, groupKey) - XCTAssertEqual(r["$group_id"] as! String, groupID) - XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 5) - r = mixpanel.groupsQueue.last! - XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 504) - } func testGroupAssertPropertyTypes() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" let p: Properties = ["URL": [Data()]] XCTExpectAssert("unsupported property type was allowed") { - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(properties: p) } XCTExpectAssert("unsupported property type was allowed") { - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "p1", to: [Data()]) + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).set(property: "p1", to: [Data()]) } + removeDBfile(testMixpanel.apiToken) } func testDeleteGroup() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) let groupKey = "test_key" let groupID = "test_id" - mixpanel.getGroup(groupKey: groupKey, groupID: groupID).deleteGroup() - waitForTrackingQueue() - let msg = mixpanel.groupsQueue.last! + testMixpanel.getGroup(groupKey: groupKey, groupID: groupID).deleteGroup() + waitForTrackingQueue(testMixpanel) + let msg = groupQueue(token: testMixpanel.apiToken).last! XCTAssertEqual(msg["$group_key"] as! String, groupKey) XCTAssertEqual(msg["$group_id"] as! String, groupID) let p: InternalProperties = msg["$delete"] as! InternalProperties XCTAssertTrue(p.isEmpty, "incorrect group properties: \(p)") + removeDBfile(testMixpanel.apiToken) } } diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelOptOutTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelOptOutTests.swift index c399e6990..ae328ff4b 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelOptOutTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelOptOutTests.swift @@ -13,46 +13,47 @@ class MixpanelOptOutTests: MixpanelBaseTests { func testHasOptOutTrackingFlagBeingSetProperlyAfterInitializedWithOptedOutYES() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - XCTAssertTrue(mixpanel.hasOptedOutTracking(), "When initialize with opted out flag set to YES, the current user should have opted out tracking") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(testMixpanel.hasOptedOutTracking(), "When initialize with opted out flag set to YES, the current user should have opted out tracking") + testMixpanel.reset() + removeDBfile(testMixpanel.apiToken) } func testOptInWillAddOptInEvent() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optInTracking() - XCTAssertFalse(mixpanel.hasOptedOutTracking(), "The current user should have opted in tracking") - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.eventsQueue.count == 1, "When opted in, event queue should have one even(opt in) being queued") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optInTracking() + XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "The current user should have opted in tracking") + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "When opted in, event queue should have one even(opt in) being queued") - if mixpanel.eventsQueue.count > 0 { - let event = mixpanel.eventsQueue.first + if eventQueue(token: testMixpanel.apiToken).count > 0 { + let event = eventQueue(token: testMixpanel.apiToken).first XCTAssertEqual((event!["event"] as? String), "$opt_in", "When opted in, a track '$opt_in' should have been queued") } else { - XCTAssertTrue(mixpanel.eventsQueue.count == 1, "When opted in, event queue should have one even(opt in) being queued") + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "When opted in, event queue should have one even(opt in) being queued") } + removeDBfile(testMixpanel.apiToken) } func testOptInTrackingForDistinctId() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel .optInTracking(distinctId: "testDistinctId") - XCTAssertFalse(mixpanel.hasOptedOutTracking(), "The current user should have opted in tracking") - waitForTrackingQueue() - if mixpanel.eventsQueue.count > 0 { - let event = mixpanel.eventsQueue.first - XCTAssertEqual((event!["event"] as? String), "$opt_in", "When opted in, a track '$opt_in' should have been queued") - } - else { - XCTAssertTrue(mixpanel.eventsQueue.count == 1, "When opted in, event queue should have one even(opt in) being queued") - } - - XCTAssertEqual(mixpanel.distinctId, "testDistinctId", "mixpanel identify failed to set distinct id") - XCTAssertEqual(mixpanel.people.distinctId, "testDistinctId", "mixpanel identify failed to set people distinct id") - XCTAssertTrue(mixpanel.people.unidentifiedQueue.count == 0, "identify: should move records from unidentified queue") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optInTracking(distinctId: "testDistinctId") + XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "The current user should have opted in tracking") + waitForTrackingQueue(testMixpanel) + sleep(1) + let event1 = eventQueue(token: testMixpanel.apiToken).first + let event2 = eventQueue(token: testMixpanel.apiToken).last + XCTAssertTrue((event1!["event"] as? String) == "$opt_in" || (event2!["event"] as? String) == "$opt_in", "When opted in, a track '$opt_in' should have been queued") + XCTAssertEqual(testMixpanel.distinctId, "testDistinctId", "mixpanel identify failed to set distinct id") + XCTAssertEqual(testMixpanel.people.distinctId, "testDistinctId", "mixpanel identify failed to set people distinct id") + XCTAssertTrue(unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 0, "identify: should move records from unidentified queue") + removeDBfile(testMixpanel.apiToken) } - + func testOptInTrackingForDistinctIdAndWithEventProperties() { let now = Date() @@ -60,183 +61,197 @@ class MixpanelOptOutTests: MixpanelBaseTests { "number": 3, "date": now, "$app_version": "override"] - mixpanel.optInTracking(distinctId: "testDistinctId", properties: testProperties) - waitForTrackingQueue() - let props = mixpanel.eventsQueue.first!["properties"] as? InternalProperties + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optInTracking(distinctId: "testDistinctId", properties: testProperties) + waitForTrackingQueue(testMixpanel) + sleep(1) + let eventQueueValue = eventQueue(token: testMixpanel.apiToken) + + let props = eventQueueValue.first!["properties"] as? InternalProperties XCTAssertEqual(props!["string"] as? String, "yello") XCTAssertEqual(props!["number"] as? NSNumber, 3) - XCTAssertEqual(props!["date"] as? Date, now) + compareDate(dateString: props!["date"] as! String, dateDate: now) XCTAssertEqual(props!["$app_version"] as? String, "override", "reserved property override failed") - - if mixpanel.eventsQueue.count > 0 { - let event = mixpanel.eventsQueue.first + + if eventQueueValue.count > 0 { + let event = eventQueueValue.first XCTAssertEqual((event!["event"] as? String), "$opt_in", "When opted in, a track '$opt_in' should have been queued") } else { - XCTAssertTrue(mixpanel.eventsQueue.count == 1, "When opted in, event queue should have one even(opt in) being queued") + XCTAssertTrue(eventQueueValue.count == 1, "When opted in, event queue should have one even(opt in) being queued") } - - XCTAssertEqual(mixpanel.distinctId, "testDistinctId", "mixpanel identify failed to set distinct id") - XCTAssertEqual(mixpanel.people.distinctId, "testDistinctId", "mixpanel identify failed to set people distinct id") - XCTAssertTrue(mixpanel.people.unidentifiedQueue.count == 0, "identify: should move records from unidentified queue") + + XCTAssertEqual(testMixpanel.distinctId, "testDistinctId", "mixpanel identify failed to set distinct id") + XCTAssertEqual(testMixpanel.people.distinctId, "testDistinctId", "mixpanel identify failed to set people distinct id") + XCTAssertTrue(unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 0, "identify: should move records from unidentified queue") + removeDBfile(testMixpanel.apiToken) } - + func testHasOptOutTrackingFlagBeingSetProperlyForMultipleInstances() { let mixpanel1 = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) XCTAssertTrue(mixpanel1.hasOptedOutTracking(), "When initialize with opted out flag set to YES, the current user should have opted out tracking") - + removeDBfile(mixpanel1.apiToken) + let mixpanel2 = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: false) XCTAssertFalse(mixpanel2.hasOptedOutTracking(), "When initialize with opted out flag set to NO, the current user should have opted in tracking") - - deleteOptOutSettings(mixpanelInstance: mixpanel1) - deleteOptOutSettings(mixpanelInstance: mixpanel2) + removeDBfile(mixpanel2.apiToken) } - + func testHasOptOutTrackingFlagBeingSetProperlyAfterInitializedWithOptedOutNO() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: false) - XCTAssertFalse(mixpanel.hasOptedOutTracking(), "When initialize with opted out flag set to NO, the current user should have opted out tracking") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: false) + XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "When initialize with opted out flag set to NO, the current user should have opted out tracking") + removeDBfile(testMixpanel.apiToken) } - + func testHasOptOutTrackingFlagBeingSetProperlyByDefault() { - mixpanel = Mixpanel.initialize(token: randomId()) - XCTAssertFalse(mixpanel.hasOptedOutTracking(), "By default, the current user should not opted out tracking") + let testMixpanel = Mixpanel.initialize(token: randomId()) + XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "By default, the current user should not opted out tracking") + removeDBfile(testMixpanel.apiToken) } - + func testHasOptOutTrackingFlagBeingSetProperlyForOptOut() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optOutTracking() - XCTAssertTrue(mixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted out tracking") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + XCTAssertTrue(testMixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted out tracking") + removeDBfile(testMixpanel.apiToken) } - + func testHasOptOutTrackingFlagBeingSetProperlyForOptIn() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optOutTracking() - XCTAssertTrue(mixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted out tracking") - mixpanel.optInTracking() - XCTAssertFalse(mixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted in tracking") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + XCTAssertTrue(testMixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted out tracking") + testMixpanel.optInTracking() + XCTAssertFalse(testMixpanel.hasOptedOutTracking(), "When optOutTracking is called, the current user should have opted in tracking") + removeDBfile(testMixpanel.apiToken) } func testOptOutTrackingWillNotGenerateEventQueue() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optOutTracking() + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optOutTracking() for i in 0..<50 { - mixpanel.track(event: "event \(i)") + testMixpanel.track(event: "event \(i)") } - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.eventsQueue.count == 0, "When opted out, events should not be queued") + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 0, "When opted out, events should not be queued") + removeDBfile(testMixpanel.apiToken) } func testOptOutTrackingWillNotGeneratePeopleQueue() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optOutTracking() + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optOutTracking() for i in 0..<50 { - mixpanel.people.set(property: "p1", to: "\(i)") + testMixpanel.people.set(property: "p1", to: "\(i)") } - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.people.peopleQueue.count == 0, "When opted out, events should not be queued") + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).count == 0, "When opted out, events should not be queued") + removeDBfile(testMixpanel.apiToken) } func testOptOutTrackingWillSkipAlias() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optOutTracking() - mixpanel.createAlias("testAlias", distinctId: "aDistinctId") - XCTAssertNotEqual(mixpanel.alias, "testAlias", "When opted out, alias should not be set") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + testMixpanel.createAlias("testAlias", distinctId: "aDistinctId") + XCTAssertNotEqual(testMixpanel.alias, "testAlias", "When opted out, alias should not be set") + removeDBfile(testMixpanel.apiToken) } func testEventBeingTrackedBeforeOptOutShouldNotBeCleared() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel = Mixpanel.initialize(token: randomId()) - mixpanel.track(event: "a normal event") - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.eventsQueue.count == 1, "events should be queued") - mixpanel.optOutTracking() - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.eventsQueue.count == 1, "When opted out, any events tracked before opted out should not be cleared") + let testMixpanel = Mixpanel.initialize(token: randomId()) + testMixpanel.track(event: "a normal event") + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "events should be queued") + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 1, "When opted out, any events tracked before opted out should not be cleared") + removeDBfile(testMixpanel.apiToken) } - + func testOptOutTrackingRegisterSuperProperties() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) let properties: Properties = ["p1": "a", "p2": 3, "p3": Date()] - mixpanel.optOutTracking() - mixpanel.registerSuperProperties(properties) - waitForMixpanelQueues() - XCTAssertNotEqual(NSDictionary(dictionary: mixpanel.currentSuperProperties()), + testMixpanel.optOutTracking() + testMixpanel.registerSuperProperties(properties) + waitForTrackingQueue(testMixpanel) + XCTAssertNotEqual(NSDictionary(dictionary: testMixpanel.currentSuperProperties()), NSDictionary(dictionary: properties), "When opted out, register super properties should not be successful") + removeDBfile(testMixpanel.apiToken) } func testOptOutTrackingRegisterSuperPropertiesOnce() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) let properties: Properties = ["p1": "a", "p2": 3, "p3": Date()] - mixpanel.optOutTracking() - mixpanel.registerSuperPropertiesOnce(properties) - waitForMixpanelQueues() - XCTAssertNotEqual(NSDictionary(dictionary: mixpanel.currentSuperProperties()), - NSDictionary(dictionary: properties), + testMixpanel.optOutTracking() + testMixpanel.registerSuperPropertiesOnce(properties) + waitForTrackingQueue(testMixpanel) + XCTAssertNotEqual(NSDictionary(dictionary: testMixpanel.currentSuperProperties()), + NSDictionary(dictionary: properties), "When opted out, register super properties once should not be successful") + removeDBfile(testMixpanel.apiToken) } func testOptOutWilSkipTimeEvent() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optOutTracking() - mixpanel.time(event: "400 Meters") - mixpanel.track(event: "400 Meters") - waitForMixpanelQueues() - XCTAssertNil(mixpanel.eventsQueue.last, "When opted out, this event should not be timed.") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optOutTracking() + testMixpanel.time(event: "400 Meters") + testMixpanel.track(event: "400 Meters") + waitForTrackingQueue(testMixpanel) + XCTAssertNil(eventQueue(token:testMixpanel.apiToken).last, "When opted out, this event should not be timed.") + removeDBfile(testMixpanel.apiToken) } func testOptOutWillSkipFlushPeople() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optInTracking() - mixpanel.identify(distinctId: "d1") - for i in 0..<50 { - mixpanel.people.set(property: "p1", to: "\(i)") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optInTracking() + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + for i in 0..<1 { + testMixpanel.people.set(property: "p1", to: "\(i)") } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.people.peopleQueue.count == 50, "When opted in, people queue should have been queued") + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).count == 1, "When opted in, people queue should have been queued") - let peopleQueue = mixpanel.people.peopleQueue - mixpanel.optOutTracking() - waitForMixpanelQueues() + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) - mixpanel.people.peopleQueue = peopleQueue - mixpanel.flush() - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.people.peopleQueue.count == 50, "When opted out, people queue should not be flushed") + testMixpanel.flush() + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(peopleQueue(token: testMixpanel.apiToken).count == 3, "When opted out, people queue should not be flushed and 2 more people being set") + removeDBfile(testMixpanel.apiToken) } func testOptOutWillSkipFlushEvent() { - mixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) - mixpanel.optInTracking() - mixpanel.identify(distinctId: "d1") - for i in 0..<50 { - mixpanel.track(event: "event \(i)") + let testMixpanel = Mixpanel.initialize(token: randomId(), optOutTrackingByDefault: true) + testMixpanel.optInTracking() + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + for i in 0..<1 { + testMixpanel.track(event: "event \(i)") } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.eventsQueue.count > 50, "When opted in, events should have been queued") - - let eventsQueue = mixpanel.eventsQueue - mixpanel.optOutTracking() - waitForMixpanelQueues() - - //In order to test if flush will be skipped, we have to create a fake eventsQueue since optOutTracking will clear eventsQueue. - mixpanel.eventsQueue = eventsQueue - mixpanel.flush() - waitForMixpanelQueues() - XCTAssertTrue(mixpanel.eventsQueue.count > 50, "When opted out, events should not be flushed") + waitForTrackingQueue(testMixpanel) + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 3, "When opted in, events should have been queued") + + testMixpanel.optOutTracking() + waitForTrackingQueue(testMixpanel) + + testMixpanel.flush() + waitForTrackingQueue(testMixpanel) + + XCTAssertTrue(eventQueue(token: testMixpanel.apiToken).count == 3, "When opted out, events should not be flushed") + removeDBfile(testMixpanel.apiToken) } } diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelPeopleTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelPeopleTests.swift index b6c47489a..5055c4b6c 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelPeopleTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelPeopleTests.swift @@ -15,177 +15,210 @@ import Nocilla class MixpanelPeopleTests: MixpanelBaseTests { func testPeopleSet() { - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) let p: Properties = ["p1": "a"] - mixpanel.people.set(properties: p) - waitForTrackingQueue() - let q = mixpanel.people.peopleQueue.last!["$set"] as! InternalProperties + testMixpanel.people.set(properties: p) + waitForTrackingQueue(testMixpanel) + sleep(1) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties XCTAssertEqual(q["p1"] as? String, "a", "custom people property not queued") assertDefaultPeopleProperties(q) + removeDBfile(testMixpanel.apiToken) } func testPeopleSetOnce() { - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") let p: Properties = ["p1": "a"] - mixpanel.people.setOnce(properties: p) - waitForTrackingQueue() - let q = mixpanel.people.peopleQueue.last!["$set_once"] as! InternalProperties + testMixpanel.people.setOnce(properties: p) + waitForTrackingQueue(testMixpanel) + sleep(1) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set_once"] as! InternalProperties XCTAssertEqual(q["p1"] as? String, "a", "custom people property not queued") assertDefaultPeopleProperties(q) + removeDBfile(testMixpanel.apiToken) } func testPeopleSetReservedProperty() { - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") let p: Properties = ["$ios_app_version": "override"] - mixpanel.people.set(properties: p) - waitForTrackingQueue() - let q = mixpanel.people.peopleQueue.last!["$set"] as! InternalProperties + testMixpanel.people.set(properties: p) + waitForTrackingQueue(testMixpanel) + sleep(1) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties XCTAssertEqual(q["$ios_app_version"] as? String, "override", "reserved property override failed") assertDefaultPeopleProperties(q) + removeDBfile(testMixpanel.apiToken) } func testPeopleSetTo() { - mixpanel.identify(distinctId: "d1") - mixpanel.people.set(property: "p1", to: "a") - waitForTrackingQueue() - let p: InternalProperties = mixpanel.people.peopleQueue.last!["$set"] as! InternalProperties + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.set(property: "p1", to: "a") + waitForTrackingQueue(testMixpanel) + sleep(1) + let p: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last!["$set"] as! InternalProperties XCTAssertEqual(p["p1"] as? String, "a", "custom people property not queued") assertDefaultPeopleProperties(p) + removeDBfile(testMixpanel.apiToken) } func testDropUnidentifiedPeopleRecords() { - QueueConstants.queueSize = 500 + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) for i in 0..<505 { - mixpanel.people.set(property: "i", to: i) + testMixpanel.people.set(property: "i", to: i) } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.people.unidentifiedQueue.count == 500) - var r: InternalProperties = mixpanel.people.unidentifiedQueue.first! - XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 5) - r = mixpanel.people.unidentifiedQueue.last! + waitForTrackingQueue(testMixpanel) + sleep(1) + XCTAssertTrue(unIdentifiedPeopleQueue(token: testMixpanel.apiToken).count == 505) + var r: InternalProperties = unIdentifiedPeopleQueue(token: testMixpanel.apiToken).first! + XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 0) + r = unIdentifiedPeopleQueue(token: testMixpanel.apiToken).last! XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 504) + removeDBfile(testMixpanel.apiToken) } - func testDropPeopleRecords() { - QueueConstants.queueSize = 500 - mixpanel.identify(distinctId: "d1") - for i in 0..<505 { - mixpanel.people.set(property: "i", to: i) - } - waitForTrackingQueue() - XCTAssertTrue(mixpanel.people.peopleQueue.count == 500) - var r: InternalProperties = mixpanel.people.peopleQueue.first! - XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 5) - r = mixpanel.people.peopleQueue.last! - XCTAssertEqual((r["$set"] as? InternalProperties)?["i"] as? Int, 504) - } func testPeopleAssertPropertyTypes() { + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) var p: Properties = ["URL": [Data()]] XCTExpectAssert("unsupported property type was allowed") { - mixpanel.people.set(properties: p) + testMixpanel.people.set(properties: p) } XCTExpectAssert("unsupported property type was allowed") { - mixpanel.people.set(property: "p1", to: [Data()]) + testMixpanel.people.set(property: "p1", to: [Data()]) } p = ["p1": "a"] // increment should require a number XCTExpectAssert("unsupported property type was allowed") { - mixpanel.people.increment(properties: p) + testMixpanel.people.increment(properties: p) } + removeDBfile(testMixpanel.apiToken) } func testPeopleIncrement() { - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") let p: Properties = ["p1": 3] - mixpanel.people.increment(properties: p) - waitForTrackingQueue() - let q = mixpanel.people.peopleQueue.last!["$add"] as! InternalProperties + testMixpanel.people.increment(properties: p) + waitForTrackingQueue(testMixpanel) + sleep(1) + let q = peopleQueue(token: testMixpanel.apiToken).last!["$add"] as! InternalProperties XCTAssertTrue(q.count == 1, "incorrect people properties: \(p)") XCTAssertEqual(q["p1"] as? Int, 3, "custom people property not queued") + removeDBfile(testMixpanel.apiToken) } func testPeopleIncrementBy() { - mixpanel.identify(distinctId: "d1") - mixpanel.people.increment(property: "p1", by: 3) - waitForTrackingQueue() - let p: InternalProperties = mixpanel.people.peopleQueue.last!["$add"] as! InternalProperties + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.increment(property: "p1", by: 3) + waitForTrackingQueue(testMixpanel) + sleep(1) + let p: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last!["$add"] as! InternalProperties XCTAssertTrue(p.count == 1, "incorrect people properties: \(p)") XCTAssertEqual(p["p1"] as? Double, 3, "custom people property not queued") + removeDBfile(testMixpanel.apiToken) } func testPeopleDeleteUser() { - mixpanel.identify(distinctId: "d1") - mixpanel.people.deleteUser() - waitForTrackingQueue() - let p: InternalProperties = mixpanel.people.peopleQueue.last!["$delete"] as! InternalProperties + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.deleteUser() + waitForTrackingQueue(testMixpanel) + sleep(1) + let p: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last!["$delete"] as! InternalProperties XCTAssertTrue(p.isEmpty, "incorrect people properties: \(p)") + removeDBfile(testMixpanel.apiToken) } func testPeopleTrackChargeDecimal() { - mixpanel.identify(distinctId: "d1") - mixpanel.people.trackCharge(amount: 25.34) - waitForTrackingQueue() - let r: InternalProperties = mixpanel.people.peopleQueue.last! + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.trackCharge(amount: 25.34) + waitForTrackingQueue(testMixpanel) + sleep(1) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] XCTAssertEqual(prop, 25.34) XCTAssertNotNil(prop2) + removeDBfile(testMixpanel.apiToken) } func testPeopleTrackChargeZero() { - mixpanel.identify(distinctId: "d1") - mixpanel.people.trackCharge(amount: 0) - waitForTrackingQueue() - let r: InternalProperties = mixpanel.people.peopleQueue.last! + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + waitForTrackingQueue(testMixpanel) + testMixpanel.people.trackCharge(amount: 0) + waitForTrackingQueue(testMixpanel) + sleep(1) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] XCTAssertEqual(prop, 0) XCTAssertNotNil(prop2) + removeDBfile(testMixpanel.apiToken) } + func testPeopleTrackChargeWithTime() { - mixpanel.identify(distinctId: "d1") + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") let p: Properties = allPropertyTypes() - mixpanel.people.trackCharge(amount: 25, properties: ["$time": p["date"]!]) - waitForTrackingQueue() - let r: InternalProperties = mixpanel.people.peopleQueue.last! + testMixpanel.people.trackCharge(amount: 25, properties: ["$time": p["date"]!]) + waitForTrackingQueue(testMixpanel) + sleep(1) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double - let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] + let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] as? String XCTAssertEqual(prop, 25) - XCTAssertEqual(prop2 as? Date, p["date"] as? Date) + compareDate(dateString: prop2!, dateDate: p["date"] as! Date) + removeDBfile(testMixpanel.apiToken) } func testPeopleTrackChargeWithProperties() { - mixpanel.identify(distinctId: "d1") - mixpanel.people.trackCharge(amount: 25, properties: ["p1": "a"]) - waitForTrackingQueue() - let r: InternalProperties = mixpanel.people.peopleQueue.last! + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.trackCharge(amount: 25, properties: ["p1": "a"]) + waitForTrackingQueue(testMixpanel) + sleep(1) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["p1"] XCTAssertEqual(prop, 25) XCTAssertEqual(prop2 as? String, "a") + removeDBfile(testMixpanel.apiToken) } func testPeopleTrackCharge() { - mixpanel.identify(distinctId: "d1") - mixpanel.people.trackCharge(amount: 25) - waitForTrackingQueue() - let r: InternalProperties = mixpanel.people.peopleQueue.last! + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.trackCharge(amount: 25) + waitForTrackingQueue(testMixpanel) + sleep(1) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! let prop = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$amount"] as? Double let prop2 = ((r["$append"] as? InternalProperties)?["$transactions"] as? InternalProperties)?["$time"] XCTAssertEqual(prop, 25) XCTAssertNotNil(prop2) + removeDBfile(testMixpanel.apiToken) } func testPeopleClearCharges() { - mixpanel.identify(distinctId: "d1") - mixpanel.people.clearCharges() - waitForTrackingQueue() - let r: InternalProperties = mixpanel.people.peopleQueue.last! + let testMixpanel = Mixpanel.initialize(token: randomId(), flushInterval: 60) + testMixpanel.identify(distinctId: "d1") + testMixpanel.people.clearCharges() + waitForTrackingQueue(testMixpanel) + sleep(1) + let r: InternalProperties = peopleQueue(token: testMixpanel.apiToken).last! let transactions = (r["$set"] as? InternalProperties)?["$transactions"] as? [MixpanelType] XCTAssertEqual(transactions?.count, 0) + removeDBfile(testMixpanel.apiToken) } } diff --git a/MixpanelDemo/MixpanelDemoTests/TestConstants.swift b/MixpanelDemo/MixpanelDemoTests/TestConstants.swift index fec5df68c..72e674765 100644 --- a/MixpanelDemo/MixpanelDemoTests/TestConstants.swift +++ b/MixpanelDemo/MixpanelDemoTests/TestConstants.swift @@ -12,22 +12,22 @@ import XCTest @testable import Mixpanel let kTestToken = "abc123" -let kDefaultServerString = "https://api.mixpanel.com" -let kDefaultServerTrackString = "https://api.mixpanel.com/track/" -let kDefaultServerEngageString = "https://api.mixpanel.com/engage/" -let kDefaultServerGroupsString = "https://api.mixpanel.com/groups/" +let kDefaultServerString = "^https://api.mixpanel.com" +let kDefaultServerTrackString = "^https://api.mixpanel.com/track/".regex +let kDefaultServerEngageString = "^https://api.mixpanel.com/engage/".regex +let kDefaultServerGroupsString = "^https://api.mixpanel.com/groups/".regex let kDefaultServerDecideString = "^https://api.mixpanel.com/decide(.*?)".regex @discardableResult func stubEngage() -> LSStubRequestDSL { - return stubRequest("POST", kDefaultServerEngageString as LSMatcheable).withHeader("Accept-Encoding", "gzip")! + return stubRequest("POST", kDefaultServerEngageString()).withHeader("Accept-Encoding", "gzip")! } @discardableResult func stubGroups() -> LSStubRequestDSL { - return stubRequest("POST", kDefaultServerGroupsString as LSMatcheable?).withHeader("Accept-Encoding", "gzip")! + return stubRequest("POST", kDefaultServerGroupsString()).withHeader("Accept-Encoding", "gzip")! } @discardableResult func stubTrack() -> LSStubRequestDSL { - return stubRequest("POST", kDefaultServerTrackString as LSMatcheable).withHeader("Accept-Encoding", "gzip")! + return stubRequest("POST", kDefaultServerTrackString()).withHeader("Accept-Encoding", "gzip")! } @discardableResult func stubDecide() -> LSStubRequestDSL { @@ -48,7 +48,7 @@ extension XCTestCase { // Call code. block() - waitForExpectations(timeout: 0.5, handler: nil) + waitForExpectations(timeout: 2, handler: nil) Assertions.assertClosure = Assertions.swiftAssertClosure } diff --git a/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-events b/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-events new file mode 100644 index 000000000..e8b419947 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 000000000..c493817ff 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 000000000..fef185365 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 000000000..045e92e7a 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 000000000..969828582 Binary files /dev/null and b/MixpanelDemo/MixpanelDemoTests/mixpanel-testToken-properties differ diff --git a/Package.swift b/Package.swift index dc48a561c..2f1875721 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 9ca25665e..ce221364c 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) @@ -18,34 +18,36 @@ import UIKit import StoreKit class AutomaticEvents: NSObject, SKPaymentTransactionObserver, SKProductsRequestDelegate { - + 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 var firstAppOpen = false + let awaitingTransactionsWriteLock = DispatchQueue(label: "com.mixpanel.awaiting_transactions_writeLock", qos: .utility) - + func initializeEvents() { let firstOpenKey = "MPFirstOpen" if let defaults = defaults, !defaults.bool(forKey: firstOpenKey) { @@ -60,8 +62,8 @@ class AutomaticEvents: NSObject, SKPaymentTransactionObserver, SKProductsRequest let appVersionValue = infoDict["CFBundleShortVersionString"] let savedVersionValue = defaults.string(forKey: appVersionKey) if let appVersionValue = appVersionValue as? String, - let savedVersionValue = savedVersionValue, - appVersionValue.compare(savedVersionValue, options: .numeric, range: nil, locale: nil) == .orderedDescending { + let savedVersionValue = savedVersionValue, + appVersionValue.compare(savedVersionValue, options: .numeric, range: nil, locale: nil) == .orderedDescending { delegate?.track(event: "$ae_updated", properties: ["$ae_updated_version": appVersionValue]) defaults.set(appVersionValue, forKey: appVersionKey) defaults.synchronize() @@ -70,32 +72,32 @@ class AutomaticEvents: NSObject, SKPaymentTransactionObserver, SKProductsRequest defaults.synchronize() } } - + NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive(_:)), name: UIApplication.willResignActiveNotification, object: nil) - + NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil) - + #if DECIDE SKPaymentQueue.default().add(self) #endif } - + @objc func appWillResignActive(_ notification: Notification) { sessionLength = roundOneDigit(num: Date().timeIntervalSince1970 - sessionStartTime) if sessionLength >= Double(minimumSessionDuration / 1000) && - sessionLength <= Double(maximumSessionDuration / 1000) { + sessionLength <= Double(maximumSessionDuration / 1000) { delegate?.track(event: "$ae_session", properties: ["$ae_session_length": sessionLength]) delegate?.increment(property: "$ae_total_app_sessions", by: 1) delegate?.increment(property: "$ae_total_app_session_length", by: sessionLength) } } - + @objc func appDidBecomeActive(_ notification: Notification) { sessionStartTime = Date().timeIntervalSince1970 if firstAppOpen { @@ -108,7 +110,7 @@ class AutomaticEvents: NSObject, SKPaymentTransactionObserver, SKProductsRequest firstAppOpen = false } } - + func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { var productsRequest = SKProductsRequest() var productIdentifiers: Set = [] @@ -119,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 @@ -127,18 +128,18 @@ class AutomaticEvents: NSObject, SKPaymentTransactionObserver, SKProductsRequest } } } - + if !productIdentifiers.isEmpty { productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers) productsRequest.delegate = self productsRequest.start() } } - + func roundOneDigit(num: TimeInterval) -> TimeInterval { return round(num * 10.0) / 10.0 } - + func isExistingUser() -> Bool { do { if let searchPath = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).last { @@ -154,14 +155,14 @@ class AutomaticEvents: NSObject, SKPaymentTransactionObserver, SKProductsRequest } return false } - + func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { awaitingTransactionsWriteLock.sync { for product in response.products { if let trans = awaitingTransactions[product.productIdentifier] { delegate?.track(event: "$ae_iap", properties: ["$ae_iap_price": "\(product.price)", - "$ae_iap_quantity": trans.payment.quantity, - "$ae_iap_name": product.productIdentifier]) + "$ae_iap_quantity": trans.payment.quantity, + "$ae_iap_name": product.productIdentifier]) awaitingTransactions.removeValue(forKey: product.productIdentifier) } } diff --git a/Sources/AutomaticProperties.swift b/Sources/AutomaticProperties.swift index 690cc143a..a2f6976d8 100644 --- a/Sources/AutomaticProperties.swift +++ b/Sources/AutomaticProperties.swift @@ -120,7 +120,7 @@ class AutomaticProperties { #endif class func libVersion() -> String { - return "3.0.0.beta.2" + return "3.0.0.beta.4" } } diff --git a/Sources/Decide.swift b/Sources/Decide.swift index 8b296bb5c..744e4eeb4 100644 --- a/Sources/Decide.swift +++ b/Sources/Decide.swift @@ -21,11 +21,12 @@ class Decide { let decideRequest: DecideRequest let lock: ReadWriteLock var decideFetched = false - var automaticEventsEnabled: Bool? + let mixpanelPersistence: MixpanelPersistence - required init(basePathIdentifier: String, lock: ReadWriteLock) { + required init(basePathIdentifier: String, lock: ReadWriteLock, mixpanelPersistence: MixpanelPersistence) { self.decideRequest = DecideRequest(basePathIdentifier: basePathIdentifier) self.lock = lock + self.mixpanelPersistence = mixpanelPersistence } func checkDecide(forceFetch: Bool = false, @@ -46,8 +47,8 @@ class Decide { return } - if let automaticEvents = result["automatic_events"] as? Bool { - self.automaticEventsEnabled = automaticEvents + if let automaticEventsEnabled = result["automatic_events"] as? Bool { + MixpanelPersistence.saveAutomacticEventsEnabledFlag(value: automaticEventsEnabled, fromDecide: true, apiToken: token) } if let integrations = result["integrations"] as? [String] { diff --git a/Sources/Flush.swift b/Sources/Flush.swift index 164dac77a..eac521f47 100644 --- a/Sources/Flush.swift +++ b/Sources/Flush.swift @@ -8,9 +8,10 @@ import Foundation -protocol FlushDelegate { +protocol FlushDelegate: AnyObject { func flush(completion: (() -> Void)?) - func updateQueue(_ queue: Queue, type: FlushType) + func flushSuccess(type: FlushType, ids: [Int32]) + #if os(iOS) func updateNetworkActivityIndicator(_ on: Bool) #endif // os(iOS) @@ -18,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 @@ -26,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 @@ -34,11 +40,6 @@ class Flush: AppLifecycle { delegate?.flush(completion: nil) startFlushTimer() } - get { - flushIntervalReadWriteLock.sync { - return _flushInterval - } - } } required init(basePathIdentifier: String) { @@ -46,47 +47,11 @@ class Flush: AppLifecycle { flushIntervalReadWriteLock = DispatchQueue(label: "com.mixpanel.flush_interval.lock", qos: .utility, attributes: .concurrent) } - func flushEventsQueue(_ eventsQueue: Queue, automaticEventsEnabled: Bool?) -> Queue? { - let (automaticEventsQueue, eventsQueue) = orderAutomaticEvents(queue: eventsQueue, - automaticEventsEnabled: automaticEventsEnabled) - var mutableEventsQueue = flushQueue(type: .events, queue: eventsQueue) - if let automaticEventsQueue = automaticEventsQueue { - mutableEventsQueue?.append(contentsOf: automaticEventsQueue) - } - return mutableEventsQueue - } - - func orderAutomaticEvents(queue: Queue, automaticEventsEnabled: Bool?) -> - (automaticEventQueue: Queue?, eventsQueue: Queue) { - var eventsQueue = queue - if automaticEventsEnabled == nil || !automaticEventsEnabled! { - var discardedItems = Queue() - for (i, ev) in eventsQueue.enumerated().reversed() { - if let eventName = ev["event"] as? String, eventName.hasPrefix("$ae_") { - discardedItems.append(ev) - eventsQueue.remove(at: i) - } - } - if automaticEventsEnabled == nil { - return (discardedItems, eventsQueue) - } - } - return (nil, eventsQueue) - } - - func flushPeopleQueue(_ peopleQueue: Queue) -> Queue? { - return flushQueue(type: .people, queue: peopleQueue) - } - - func flushGroupsQueue(_ groupsQueue: Queue) -> Queue? { - return flushQueue(type: .groups, queue: groupsQueue) - } - - func flushQueue(type: FlushType, queue: Queue) -> Queue? { + func flushQueue(type: FlushType, queue: Queue) { if flushRequest.requestNotAllowed() { - return queue + return } - return flushQueueInBatches(queue, type: type) + flushQueueInBatches(queue, type: type) } func startFlushTimer() { @@ -119,13 +84,16 @@ class Flush: AppLifecycle { } } - func flushQueueInBatches(_ queue: Queue, type: FlushType) -> Queue { + func flushQueueInBatches(_ queue: Queue, type: FlushType) { var mutableQueue = queue while !mutableQueue.isEmpty { var shouldContinue = false let batchSize = min(mutableQueue.count, APIConstants.batchSize) let range = 0.. Queue { @@ -171,7 +141,6 @@ class Flush: AppLifecycle { } else { shadowQueue.removeAll() } - delegate?.updateQueue(shadowQueue, type: type) return shadowQueue } diff --git a/Sources/Group.swift b/Sources/Group.swift index ec829feb9..47fa6e1af 100644 --- a/Sources/Group.swift +++ b/Sources/Group.swift @@ -17,16 +17,24 @@ open class Group { let lock: ReadWriteLock let groupKey: String let groupID: MixpanelType - var delegate: FlushDelegate? + weak var delegate: FlushDelegate? let metadata: SessionMetadata - - init(apiToken: String, serialQueue: DispatchQueue, lock: ReadWriteLock, groupKey: String, groupID: MixpanelType, metadata: SessionMetadata) { + let 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 self.groupKey = groupKey self.groupID = groupID self.metadata = metadata + self.mixpanelPersistence = mixpanelPersistence } func addGroupRecordToQueueWithAction(_ action: String, properties: InternalProperties) { @@ -52,11 +60,7 @@ open class Group { r["$group_key"] = self.groupKey r["$group_id"] = self.groupID - self.addGroupObject(r) - - self.lock.read { - Persistence.archiveGroups(Mixpanel.mainInstance().flushGroupsQueue + Mixpanel.mainInstance().groupsQueue, token: self.apiToken) - } + self.mixpanelPersistence.saveEntity(r, type: .groups) } if MixpanelInstance.isiOSAppExtension() { @@ -64,15 +68,6 @@ open class Group { } } - func addGroupObject(_ r: InternalProperties) { - self.lock.write { - Mixpanel.mainInstance().groupsQueue.append(r) - if Mixpanel.mainInstance().groupsQueue.count > QueueConstants.queueSize { - Mixpanel.mainInstance().groupsQueue.remove(at: 0) - } - } - } - // MARK: - Group /** diff --git a/Sources/Info.plist b/Sources/Info.plist index eab5ad311..86859bfde 100644 --- a/Sources/Info.plist +++ b/Sources/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.0.0.beta.2 + 3.0.0.beta.4 CFBundleSignature ???? CFBundleVersion diff --git a/Sources/JSONHandler.swift b/Sources/JSONHandler.swift index 74dd7470e..d1927c938 100644 --- a/Sources/JSONHandler.swift +++ b/Sources/JSONHandler.swift @@ -30,8 +30,18 @@ class JSONHandler { return b64 } + + class func deserializeData(_ data: Data) -> MPObjectToParse? { + var object: MPObjectToParse? + do { + object = try JSONSerialization.jsonObject(with: data, options: []) + } catch { + Logger.warn(message: "exception decoding object data") + } + return object + } - class func serializeJSONObject(_ obj: MPObjectToParse) -> Data? { + class func serializeJSONObject(_ obj: MPObjectToParse) -> Data? { let serializableJSONObject: MPObjectToParse if let jsonObject = makeObjectSerializable(obj) as? [Any] { serializableJSONObject = jsonObject.filter { diff --git a/Sources/MPDB.swift b/Sources/MPDB.swift new file mode 100644 index 000000000..a3f368abe --- /dev/null +++ b/Sources/MPDB.swift @@ -0,0 +1,246 @@ +// +// MPDB.swift +// Mixpanel +// +// Created by Jared McFarland on 7/2/21. +// Copyright © 2021 Mixpanel. All rights reserved. +// + +import Foundation +import SQLite3 + +class MPDB { + private var connection: OpaquePointer? + private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + private let DB_FILE_NAME: String = "MPDB.sqlite" + + let apiToken: String + + init(token: String) { + apiToken = token + open() + } + + private func pathToDb() -> String? { + let manager = FileManager.default + #if os(iOS) + let url = manager.urls(for: .libraryDirectory, in: .userDomainMask).last + #else + let url = manager.urls(for: .cachesDirectory, in: .userDomainMask).last + #endif // os(iOS) + + guard let urlUnwrapped = url?.appendingPathComponent(apiToken + "_" + DB_FILE_NAME).path else { + return nil + } + return urlUnwrapped + } + + private func tableNameFor(_ persistenceType: PersistenceType) -> String { + return "mixpanel_\(apiToken)_\(persistenceType)" + } + + private func reconnect() { + Logger.warn(message: "No database connection found. Calling MPDB.open()") + open() + } + + func open() { + if apiToken.isEmpty { + Logger.error(message: "Project token must not be empty. Database cannot be opened.") + return + } + if let dbPath = pathToDb() { + if sqlite3_open_v2(dbPath, &connection, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nil) != SQLITE_OK { + logSqlError(message: "Error opening or creating database at path: \(dbPath)") + close() + } else { + Logger.info(message: "Successfully opened connection to database at path: \(dbPath)") + createTables() + } + } + } + + func close() { + sqlite3_close(connection) + connection = nil + Logger.info(message: "Connection to database closed.") + } + + private func recreate() { + close() + if let dbPath = pathToDb() { + do { + let manager = FileManager.default + if manager.fileExists(atPath: dbPath) { + try manager.removeItem(atPath: dbPath) + Logger.info(message: "Deleted database file at path: \(dbPath)") + } + } catch let error { + Logger.error(message: "Unable to remove database file at path: \(dbPath), error: \(error)") + } + } + reconnect() + } + + private func createTableFor(_ persistenceType: PersistenceType) { + if let db = connection { + let tableName = tableNameFor(persistenceType) + let createTableString = + "CREATE TABLE IF NOT EXISTS \(tableName)(id integer primary key autoincrement,data blob,time real,flag integer);" + var createTableStatement: OpaquePointer? + if sqlite3_prepare_v2(db, createTableString, -1, &createTableStatement, nil) == SQLITE_OK { + if sqlite3_step(createTableStatement) == SQLITE_DONE { + Logger.info(message: "\(tableName) table created") + } else { + logSqlError(message: "\(tableName) table create failed") + } + } else { + logSqlError(message: "CREATE statement for table \(tableName) could not be prepared") + } + sqlite3_finalize(createTableStatement) + } else { + reconnect() + } + } + + private func createTables() { + createTableFor(PersistenceType.events) + createTableFor(PersistenceType.people) + createTableFor(PersistenceType.groups) + } + + func insertRow(_ persistenceType: PersistenceType, data: Data, flag: Bool = false) { + if let db = connection { + let tableName = tableNameFor(persistenceType) + let insertString = "INSERT INTO \(tableName) (data, flag, time) VALUES(?, ?, ?);" + var insertStatement: OpaquePointer? + data.withUnsafeBytes { rawBuffer in + if let pointer = rawBuffer.baseAddress { + if sqlite3_prepare_v2(db, insertString, -1, &insertStatement, nil) == SQLITE_OK { + sqlite3_bind_blob(insertStatement, 1, pointer, Int32(rawBuffer.count), SQLITE_TRANSIENT) + sqlite3_bind_int(insertStatement, 2, flag ? 1 : 0) + sqlite3_bind_double(insertStatement, 3, Date().timeIntervalSince1970) + if sqlite3_step(insertStatement) == SQLITE_DONE { + Logger.info(message: "Successfully inserted row into table \(tableName)") + } else { + logSqlError(message: "Failed to insert row into table \(tableName)") + recreate() + } + } else { + logSqlError(message: "INSERT statement for table \(tableName) could not be prepared") + recreate() + } + sqlite3_finalize(insertStatement) + } + } + } else { + reconnect() + } + } + + func deleteRows(_ persistenceType: PersistenceType, ids: [Int32] = []) { + if let db = connection { + let tableName = tableNameFor(persistenceType) + let deleteString = "DELETE FROM \(tableName)\(ids.isEmpty ? "" : " WHERE id IN \(idsSqlString(ids))")" + var deleteStatement: OpaquePointer? + if sqlite3_prepare_v2(db, deleteString, -1, &deleteStatement, nil) == SQLITE_OK { + if sqlite3_step(deleteStatement) == SQLITE_DONE { + Logger.info(message: "Succesfully deleted rows from table \(tableName)") + } else { + logSqlError(message: "Failed to delete rows from table \(tableName)") + recreate() + } + } else { + logSqlError(message: "DELETE statement for table \(tableName) could not be prepared") + recreate() + } + sqlite3_finalize(deleteStatement) + } else { + reconnect() + } + } + + private func idsSqlString(_ ids: [Int32] = []) -> String { + var sqlString = "(" + for id in ids { + sqlString += "\(id)," + } + sqlString = String(sqlString.dropLast()) + sqlString += ")" + return sqlString + } + + func updateRowsFlag(_ persistenceType: PersistenceType, newFlag: Bool) { + if let db = connection { + let tableName = tableNameFor(persistenceType) + let updateString = "UPDATE \(tableName) SET flag = \(newFlag) where flag = \(!newFlag)" + var updateStatement: OpaquePointer? + if sqlite3_prepare_v2(db, updateString, -1, &updateStatement, nil) == SQLITE_OK { + if sqlite3_step(updateStatement) == SQLITE_DONE { + Logger.info(message: "Succesfully update rows from table \(tableName)") + } else { + logSqlError(message: "Failed to update rows from table \(tableName)") + recreate() + } + } else { + logSqlError(message: "UPDATE statement for table \(tableName) could not be prepared") + recreate() + } + sqlite3_finalize(updateStatement) + } else { + reconnect() + } + } + + func readRows(_ persistenceType: PersistenceType, numRows: Int, flag: Bool = false) -> [InternalProperties] { + var rows: [InternalProperties] = [] + if let db = connection { + let tableName = tableNameFor(persistenceType) + let selectString = """ + SELECT id, data FROM \(tableName) WHERE flag = \(flag ? 1 : 0) \ + ORDER BY time\(numRows == Int.max ? "" : " LIMIT \(numRows)") + """ + var selectStatement: OpaquePointer? + var rowsRead: Int = 0 + if sqlite3_prepare_v2(db, selectString, -1, &selectStatement, nil) == SQLITE_OK { + while sqlite3_step(selectStatement) == SQLITE_ROW { + if let blob = sqlite3_column_blob(selectStatement, 1) { + let blobLength = sqlite3_column_bytes(selectStatement, 1) + let data = Data(bytes: blob, count: Int(blobLength)) + let id = sqlite3_column_int(selectStatement, 0) + + if let jsonObject = JSONHandler.deserializeData(data) as? InternalProperties { + var entity = jsonObject + entity["id"] = id + rows.append(entity) + } + rowsRead += 1 + } else { + logSqlError(message: "No blob found in data column for row in \(tableName)") + } + } + if rowsRead > 0 { + Logger.info(message: "Successfully read \(rowsRead) from table \(tableName)") + } + } else { + logSqlError(message: "SELECT statement for table \(tableName) could not be prepared") + } + sqlite3_finalize(selectStatement) + } else { + reconnect() + } + return rows + } + + private func logSqlError(message: String? = nil) { + if let db = connection { + if let msg = message { + Logger.error(message: msg) + } + let sqlError = String(cString: sqlite3_errmsg(db)!) + Logger.error(message: sqlError) + } else { + reconnect() + } + } +} diff --git a/Sources/Mixpanel.swift b/Sources/Mixpanel.swift index bd02cce77..cd1ea709d 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 5ac1c94da..d5d7594b5 100644 --- a/Sources/MixpanelInstance.swift +++ b/Sources/MixpanelInstance.swift @@ -23,12 +23,12 @@ 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. - + - parameter mixpanel: The mixpanel instance - + - returns: return true to upload now or false to defer until later */ func mixpanelWillFlush(_ mixpanel: MixpanelInstance) -> Bool @@ -47,74 +47,83 @@ protocol AppLifecycle { 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 var delegate: MixpanelDelegate? - + 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? - + 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. + /// 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. /// Useful if you need to proxy Mixpanel requests. Defaults to /// https://api.mixpanel.com. @@ -123,16 +132,14 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele BasePath.namedBasePaths[name] = serverURL } } - + open var debugDescription: String { return "Mixpanel(\n" - + " Token: \(apiToken),\n" - + " Events Queue Count: \(eventsQueue.count),\n" - + " People Queue Count: \(people.peopleQueue.count),\n" - + " Distinct Id: \(distinctId)\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. @@ -143,11 +150,11 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele 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) @@ -155,43 +162,38 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele } } } - + /// 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 { - 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 - + var superProperties = InternalProperties() - var eventsQueue = Queue() - var flushEventsQueue = Queue() - var groupsQueue = Queue() - var flushGroupsQueue = Queue() - var timedEvents = InternalProperties() var trackingQueue: DispatchQueue! - var networkQueue: DispatchQueue! var optOutStatus: Bool? + var timedEvents = InternalProperties() let readWriteLock: ReadWriteLock #if os(iOS) && !targetEnvironment(macCatalyst) static let reachability = SCNetworkReachabilityCreateWithName(nil, "api.mixpanel.com") @@ -208,57 +210,61 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele let automaticEvents = AutomaticEvents() let connectIntegrations = ConnectIntegrations() #elseif TV_AUTO_EVENTS - let automaticEvents = AutomaticEvents() + 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 - self.readWriteLock = ReadWriteLock(label: "com.mixpanel.globallock") + readWriteLock = ReadWriteLock(label: "com.mixpanel.globallock") flushInstance = Flush(basePathIdentifier: name) #if DECIDE - decideInstance = Decide(basePathIdentifier: name, lock: self.readWriteLock) + 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) - networkQueue = DispatchQueue(label: "\(label).network)", qos: .utility) - + 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 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 } - if SCNetworkReachabilitySetCallback(reachability, reachabilityCallback, &context) { - if !SCNetworkReachabilitySetDispatchQueue(reachability, trackingQueue) { - // cleanup callback if setting dispatch queue failed - SCNetworkReachabilitySetCallback(reachability, nil, nil) - } + 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) + 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 @@ -267,13 +273,13 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele } #if DECIDE || TV_AUTO_EVENTS - if !MixpanelInstance.isiOSAppExtension() { - automaticEvents.delegate = self - automaticEvents.initializeEvents() - } - #if DECIDE - connectIntegrations.mixpanel = self - #endif + if !MixpanelInstance.isiOSAppExtension() { + automaticEvents.delegate = self + automaticEvents.initializeEvents() + } + #if DECIDE + connectIntegrations.mixpanel = self + #endif #endif // DECIDE } #else @@ -281,6 +287,9 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele 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) @@ -289,18 +298,17 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele sessionMetadata = SessionMetadata(trackingQueue: trackingQueue) trackInstance = Track(apiToken: self.apiToken, lock: self.readWriteLock, - metadata: sessionMetadata) + metadata: sessionMetadata, mixpanelPersistence: mixpanelPersistence) flushInstance.delegate = self - networkQueue = DispatchQueue(label: label, qos: .utility) distinctId = defaultDistinctId() people = People(apiToken: self.apiToken, serialQueue: trackingQueue, lock: self.readWriteLock, - metadata: sessionMetadata) + metadata: sessionMetadata, mixpanelPersistence: mixpanelPersistence) flushInstance._flushInterval = flushInterval - #if !os(watchOS) + #if !os(watchOS) setupListeners() - #endif + #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 @@ -310,13 +318,13 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele } } #endif // os(OSX) - + #if !os(OSX) && !os(watchOS) private func setupListeners() { let notificationCenter = NotificationCenter.default trackIntegration() #if os(iOS) && !targetEnvironment(macCatalyst) - setCurrentRadio() + 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, @@ -364,34 +372,35 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele 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") - } + 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 { + 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 @@ -402,30 +411,30 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele } } } - + #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 } @@ -434,14 +443,14 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele flush() } else { // only need to archive if don't flush because flush archives at the end - networkQueue.async { [weak self] in + trackingQueue.async { [weak self] in self?.archive() } } - networkQueue.async { [weak self] in + trackingQueue.async { [weak self] in guard let self = self else { return } - + #if DECIDE self.readWriteLock.write { self.decideInstance.decideFetched = false @@ -453,7 +462,7 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele } } } - + @objc private func applicationWillEnterForeground(_ notification: Notification) { guard let sharedApplication = MixpanelInstance.sharedUIApplication() else { return @@ -461,25 +470,25 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele 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) + self.updateNetworkActivityIndicator(false) #endif // os(iOS) } } } #endif // os(OSX) - + @objc private func applicationWillTerminate(_ notification: Notification) { - networkQueue.async { [weak self] in + trackingQueue.async { [weak self] in guard let self = self else { return } self.archive() } } - + func defaultDistinctId() -> String { let distinctId: String? #if MIXPANEL_UNIQUE_DISTINCT_ID @@ -499,16 +508,17 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele #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) + 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 { @@ -526,19 +536,19 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele 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 { @@ -551,7 +561,7 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele trackingQueue.async { AutomaticProperties.automaticPropertiesLock.write { [weak self, radio] in AutomaticProperties.properties["$radio"] = radio - + guard self != nil else { return } @@ -571,38 +581,38 @@ open class MixpanelInstance: CustomDebugStringConvertible, FlushDelegate, AEDele } #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 MIXPANEL_UNIQUE_DISTINCT_ID flag in your Active Compilation Conditions 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. + 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() { @@ -612,17 +622,17 @@ extension MixpanelInstance { 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 + self.anonymousId = self.distinctId + self.hadPersistedDistinctId = true } - + if distinctId != self.distinctId { let oldDistinctId = self.distinctId self.readWriteLock.write { @@ -632,54 +642,43 @@ extension MixpanelInstance { } self.track(event: "$identify", properties: ["$anon_distinct_id": oldDistinctId]) } - + if usePeople { self.readWriteLock.write { self.people.distinctId = distinctId } - if !self.people.unidentifiedQueue.isEmpty { - self.readWriteLock.write { - for var r in self.people.unidentifiedQueue { - r["$distinct_id"] = self.distinctId - self.people.peopleQueue.append(r) - } - self.people.unidentifiedQueue.removeAll() - } - self.readWriteLock.read { - Persistence.archivePeople(self.people.peopleQueue, token: self.apiToken) - } - } + self.mixpanelPersistence.identifyPeople(token: self.apiToken) + } else { self.people.distinctId = nil } - self.archiveProperties() - Persistence.storeIdentity(token: self.apiToken, - distinctID: self.distinctId, - peopleDistinctID: self.people.distinctId, - anonymousID: self.anonymousId, - userID: self.userId, - alias: self.alias, - hadPersistedDistinctId: self.hadPersistedDistinctId) + 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. @@ -693,29 +692,28 @@ extension MixpanelInstance { 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 - self.archiveProperties() - Persistence.storeIdentity(token: self.apiToken, - distinctID: self.distinctId, - peopleDistinctID: self.people.distinctId, - anonymousID: self.anonymousId, - userID: self.userId, - alias: self.alias, - hadPersistedDistinctId: self.hadPersistedDistinctId) + 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) @@ -724,7 +722,7 @@ extension MixpanelInstance { 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. @@ -732,181 +730,69 @@ extension MixpanelInstance { open func reset() { flush() trackingQueue.async { [weak self] in - self?.networkQueue.sync { [weak self] in - self?.readWriteLock.write { [weak self] in - guard let self = self else { - return - } - - Persistence.deleteMPUserDefaultsData(token: self.apiToken) - self.distinctId = self.defaultDistinctId() - self.anonymousId = self.distinctId - self.hadPersistedDistinctId = nil - self.userId = nil - self.superProperties = InternalProperties() - self.eventsQueue = Queue() - self.timedEvents = InternalProperties() - self.people.distinctId = nil - self.alias = nil - self.people.peopleQueue = Queue() - self.people.unidentifiedQueue = Queue() - #if DECIDE - self.decideInstance.decideFetched = false - self.connectIntegrations.reset() - #endif // DECIDE + self?.readWriteLock.write { [weak self] in + guard let self = self else { + return } - self?.archive() + + 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 - - #if DECIDE - /** - Writes current project info including the distinct Id, super properties, - and pending event and People record queues to disk. - - This state will be recovered when the app is launched again if the Mixpanel - library is initialized with the same project token. - The library listens for app state changes and handles - persisting data as needed. - - - important: You do not need to call this method.** - */ - open func archive() { - self.readWriteLock.read { - let properties = ArchivedProperties(superProperties: superProperties, - timedEvents: timedEvents, - distinctId: distinctId, - anonymousId: anonymousId, - userId: userId, - alias: alias, - hadPersistedDistinctId: hadPersistedDistinctId, - peopleDistinctId: people.distinctId, - peopleUnidentifiedQueue: people.unidentifiedQueue, - automaticEventsEnabled: trackAutomaticEventsEnabled ?? decideInstance.automaticEventsEnabled) - Persistence.archive(eventsQueue: flushEventsQueue + eventsQueue, - peopleQueue: people.flushPeopleQueue + people.peopleQueue, - groupsQueue: flushGroupsQueue + groupsQueue, - properties: properties, - token: apiToken) - } - } - #else - /** - Writes current project info including the distinct Id, super properties, - and pending event and People record queues to disk. - - This state will be recovered when the app is launched again if the Mixpanel - library is initialized with the same project token. - The library listens for app state changes and handles - persisting data as needed. - - - important: You do not need to call this method.** - */ + open func archive() { self.readWriteLock.read { - let properties = ArchivedProperties(superProperties: superProperties, - timedEvents: timedEvents, - distinctId: distinctId, - anonymousId: anonymousId, - userId: userId, - alias: alias, - hadPersistedDistinctId: hadPersistedDistinctId, - peopleDistinctId: people.distinctId, - peopleUnidentifiedQueue: people.unidentifiedQueue) - Persistence.archive(eventsQueue: flushEventsQueue + eventsQueue, - peopleQueue: people.flushPeopleQueue + people.peopleQueue, - groupsQueue: flushGroupsQueue + groupsQueue, - properties: properties, - token: apiToken) - } - } - #endif // DECIDE - - #if DECIDE - func unarchive() { - (eventsQueue, - people.peopleQueue, - groupsQueue, - superProperties, - timedEvents, - distinctId, - anonymousId, - userId, - alias, - hadPersistedDistinctId, - people.distinctId, - people.unidentifiedQueue, - optOutStatus, - decideInstance.automaticEventsEnabled) = Persistence.unarchive(token: apiToken) - - if distinctId == "" { - distinctId = defaultDistinctId() - anonymousId = distinctId - hadPersistedDistinctId = nil - userId = nil - } - } - - func archiveProperties() { - self.readWriteLock.read { - let properties = ArchivedProperties(superProperties: superProperties, - timedEvents: timedEvents, - distinctId: distinctId, - anonymousId: anonymousId, - userId: userId, - alias: alias, - hadPersistedDistinctId: hadPersistedDistinctId, - peopleDistinctId: people.distinctId, - peopleUnidentifiedQueue: people.unidentifiedQueue, - automaticEventsEnabled: trackAutomaticEventsEnabled ?? decideInstance.automaticEventsEnabled) - Persistence.archiveProperties(properties, token: apiToken) + 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) } } - #else + func unarchive() { - (eventsQueue, - people.peopleQueue, - groupsQueue, - superProperties, - timedEvents, - distinctId, - anonymousId, - userId, - alias, - hadPersistedDistinctId, - people.distinctId, - people.unidentifiedQueue, - optOutStatus) = Persistence.unarchive(token: apiToken) - - if distinctId == "" { + 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 archiveProperties() { - self.readWriteLock.read { - let properties = ArchivedProperties(superProperties: superProperties, - timedEvents: timedEvents, - distinctId: distinctId, - anonymousId: anonymousId, - userId: userId, - alias: alias, - hadPersistedDistinctId: hadPersistedDistinctId, - peopleDistinctId: people.distinctId, - peopleUnidentifiedQueue: people.unidentifiedQueue) - Persistence.archiveProperties(properties, token: apiToken) - } - } - #endif // DECIDE - + func trackIntegration() { if hasOptedOutTracking() { return @@ -927,15 +813,15 @@ extension MixpanelInstance { 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) { @@ -946,96 +832,66 @@ extension MixpanelInstance { return } trackingQueue.async { [weak self, completion] in - self?.networkQueue.async { [weak self] in - guard let self = self else { - return - } - - if let shouldFlush = self.delegate?.mixpanelWillFlush(self), !shouldFlush { - return - } - - self.readWriteLock.write { - self.flushEventsQueue = self.eventsQueue - self.people.flushPeopleQueue = self.people.peopleQueue - self.flushGroupsQueue = self.groupsQueue - self.eventsQueue.removeAll() - self.people.peopleQueue.removeAll() - self.groupsQueue.removeAll() - } - - #if DECIDE - let automaticEventsEnabled = self.trackAutomaticEventsEnabled ?? self.decideInstance.automaticEventsEnabled - #elseif TV_AUTO_EVENTS - let automaticEventsEnabled = true - #else - let automaticEventsEnabled = false - #endif - - let flushEventsQueue = self.flushInstance.flushEventsQueue(self.flushEventsQueue, - automaticEventsEnabled: automaticEventsEnabled) - let flushPeopleQueue = self.flushInstance.flushPeopleQueue(self.people.flushPeopleQueue) - let flushGroupsQueue = self.flushInstance.flushGroupsQueue(self.flushGroupsQueue) - - var shadowEventsQueue = Queue() - var shadowPeopleQueue = Queue() - var shadowGroupsQueue = Queue() - - self.readWriteLock.read { - shadowEventsQueue = self.eventsQueue - shadowPeopleQueue = self.people.peopleQueue - shadowGroupsQueue = self.groupsQueue - } - self.readWriteLock.write { - if let flushEventsQueue = flushEventsQueue { - self.eventsQueue = flushEventsQueue + shadowEventsQueue - } - if let flushPeopleQueue = flushPeopleQueue { - self.people.peopleQueue = flushPeopleQueue + shadowPeopleQueue - } - if let flushGroupsQueue = flushGroupsQueue { - self.groupsQueue = flushGroupsQueue + shadowGroupsQueue - } - self.flushEventsQueue.removeAll() - self.people.flushPeopleQueue.removeAll() - self.flushGroupsQueue.removeAll() - } - - self.archive() - - if let completion = completion { - DispatchQueue.main.async(execute: completion) - } + 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) } - }} + } + } - func updateQueue(_ queue: Queue, type: FlushType) { - self.readWriteLock.write { - if type == .events { - self.flushEventsQueue = queue - } else if type == .people { - self.people.flushPeopleQueue = queue - } else if type == .groups { - self.flushGroupsQueue = queue + 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) } - - self.archive() } + } 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 */ @@ -1045,51 +901,39 @@ extension MixpanelInstance { } let epochInterval = Date().timeIntervalSince1970 + trackingQueue.async { [weak self, event, properties, epochInterval] in - guard let self = self else { return } - var shadowEventsQueue = Queue() - var shadowTimedEvents = InternalProperties() - var shadowSuperProperties = InternalProperties() - - self.readWriteLock.read { - shadowEventsQueue = self.eventsQueue - shadowTimedEvents = self.timedEvents - shadowSuperProperties = self.superProperties - } - let (eventsQueue, timedEvents, _) = self.trackInstance.track(event: event, - properties: properties, - eventsQueue: shadowEventsQueue, - timedEvents: shadowTimedEvents, - superProperties: shadowSuperProperties, - distinctId: self.distinctId, - anonymousId: self.anonymousId, - userId: self.userId, - hadPersistedDistinctId: self.hadPersistedDistinctId, - epochInterval: epochInterval) - self.readWriteLock.write { - self.eventsQueue = eventsQueue - self.timedEvents = timedEvents - } - - self.readWriteLock.read { - Persistence.archiveEvents(self.flushEventsQueue + self.eventsQueue, token: self.apiToken) + 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 @@ -1098,27 +942,27 @@ extension MixpanelInstance { 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 { @@ -1127,45 +971,57 @@ 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) + 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) + 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") @@ -1174,24 +1030,25 @@ extension MixpanelInstance { // 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 = self.trackInstance.time(event: event, timedEvents: self.timedEvents, startTime: startTime) + 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 { @@ -1200,7 +1057,7 @@ extension MixpanelInstance { } return 0 } - + /** Clears all current event timers. */ @@ -1208,30 +1065,29 @@ extension MixpanelInstance { trackingQueue.async { [weak self] in guard let self = self else { return } self.readWriteLock.write { - self.timedEvents = self.trackInstance.clearTimedEvents(self.timedEvents) + 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 + trackingQueue.async { [weak self, event] in guard let self = self else { return } - self.readWriteLock.write { - self.timedEvents = self.trackInstance.clearTimedEvent(event: event, timedEvents: self.timedEvents) - } + 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] { @@ -1241,65 +1097,59 @@ extension MixpanelInstance { } return properties } - + /** Clears all currently set super properties. */ open func clearSuperProperties() { - dispatchAndTrack { [weak self] in - guard let self = self else { return } - self.readWriteLock.write { - self.superProperties = self.trackInstance.clearSuperProperties(self.superProperties) - } + 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) { - dispatchAndTrack { [weak self] in - guard let self = self else { return } - self.readWriteLock.write { - self.superProperties = self.trackInstance.registerSuperProperties(properties, - superProperties: self.superProperties) - } + 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) { - dispatchAndTrack { [weak self] in - guard let self = self else { return } - self.readWriteLock.write { - self.superProperties = self.trackInstance.registerSuperPropertiesOnce(properties, - superProperties: self.superProperties, - defaultValue: defaultValue) - } + 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 @@ -1307,42 +1157,38 @@ extension MixpanelInstance { 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) { - dispatchAndTrack { [weak self] in - guard let self = self else { return } - self.readWriteLock.write { - self.superProperties = self.trackInstance.unregisterSuperProperty(propertyName, - superProperties: self.superProperties) - } + 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) { - dispatchAndTrack { [weak self] in - guard let self = self else { return } - var superPropertiesShadow = InternalProperties() - self.readWriteLock.read { - superPropertiesShadow = self.superProperties - } - self.trackInstance.updateSuperProperty(update, - superProperties: &superPropertiesShadow) - self.readWriteLock.write { - self.superProperties = superPropertiesShadow - } + 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. */ @@ -1350,13 +1196,13 @@ extension MixpanelInstance { 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. */ @@ -1364,15 +1210,15 @@ extension MixpanelInstance { 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. */ @@ -1380,14 +1226,14 @@ extension MixpanelInstance { 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) }) { @@ -1397,15 +1243,15 @@ extension MixpanelInstance { } 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. */ @@ -1413,32 +1259,32 @@ extension MixpanelInstance { 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. */ @@ -1448,72 +1294,60 @@ extension MixpanelInstance { people.clearCharges() flush() } - + trackingQueue.async { [weak self] in guard let self = self else { return } - self.networkQueue.async { [weak self] in + self.readWriteLock.write { [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() - self.people.unidentifiedQueue = Queue() - self.timedEvents = InternalProperties() - } - self.archive() + + 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 - Persistence.archiveOptOutStatus(optOutStatus!, token: apiToken) + 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 + - 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 - Persistence.archiveOptOutStatus(optOutStatus!, token: apiToken) - + 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 } - - func dispatchAndTrack(closure: @escaping () -> Void) { - trackingQueue.async { [weak self, closure] in - closure() - self?.archiveProperties() - } - } // MARK: - AEDelegate func increment(property: String, by: Double) { @@ -1527,23 +1361,15 @@ extension MixpanelInstance { #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.networkQueue.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) - } + self.decideInstance.checkDecide(forceFetch: forceFetch, + distinctId: self.people.distinctId ?? self.distinctId, + token: self.apiToken, + completion: completion) } } } diff --git a/Sources/MixpanelPersistence.swift b/Sources/MixpanelPersistence.swift new file mode 100644 index 000000000..364c576e5 --- /dev/null +++ b/Sources/MixpanelPersistence.swift @@ -0,0 +1,475 @@ +// +// MixpanelPersistence.swift +// Mixpanel +// +// Created by ZIHE JIA on 7/9/21. +// Copyright © 2021 Mixpanel. All rights reserved. +// + +import Foundation + +enum LegacyArchiveType: String { + case events + case people + case groups + case properties + case optOutStatus +} + +enum PersistenceType: String, CaseIterable { + case events + case people + case groups +} + +struct PersistenceConstant { + static let unIdentifiedFlag = true +} + +struct MixpanelIdentity { + let distinctID: String + let peopleDistinctID: String? + let anonymousId: String? + let userId: String? + let alias: String? + let hadPersistedDistinctId: Bool? +} + +struct MixpanelUserDefaultsKeys { + static let suiteName = "Mixpanel" + static let prefix = "mixpanel" + static let optOutStatus = "OptOutStatus" + static let automaticEventEnabled = "AutomaticEventEnabled" + static let automaticEventEnabledFromDecide = "AutomaticEventEnabledFromDecide" + static let timedEvents = "timedEvents" + static let superProperties = "superProperties" + static let distinctID = "MPDistinctID" + static let peopleDistinctID = "MPPeopleDistinctID" + static let anonymousId = "MPAnonymousId" + static let userID = "MPUserId" + static let alias = "MPAlias" + static let hadPersistedDistinctId = "MPHadPersistedDistinctId" +} + +class MixpanelPersistence { + + let apiToken: String + let mpdb: MPDB + + init(token: String) { + apiToken = token + mpdb = MPDB.init(token: apiToken) + } + + func saveEntity(_ entity: InternalProperties, type: PersistenceType, flag: Bool = false) { + if let data = JSONHandler.serializeJSONObject(entity) { + mpdb.insertRow(type, data: data, flag: flag) + } + } + + func saveEntities(_ entities: Queue, type: PersistenceType, flag: Bool = false) { + for entity in entities { + saveEntity(entity, type: type) + } + } + + func loadEntitiesInBatch(type: PersistenceType, batchSize: Int = Int.max, flag: Bool = false) -> [InternalProperties] { + return mpdb.readRows(type, numRows: batchSize, flag: flag) + } + + func removeEntitiesInBatch(type: PersistenceType, ids: [Int32]) { + mpdb.deleteRows(type, ids: ids) + } + + func identifyPeople(token: String) { + mpdb.updateRowsFlag(.people, newFlag: !PersistenceConstant.unIdentifiedFlag) + } + + func resetEntities() { + for pType in PersistenceType.allCases { + mpdb.deleteRows(pType) + } + } + + static func saveOptOutStatusFlag(value: Bool, apiToken: String) { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return + } + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(apiToken)-" + defaults.setValue(value, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.optOutStatus)") + defaults.synchronize() + } + + static func loadOptOutStatusFlag(apiToken: String) -> Bool? { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return nil + } + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(apiToken)-" + 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 + } + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(apiToken)-" + if fromDecide { + defaults.setValue(value, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.automaticEventEnabledFromDecide)") + } else { + defaults.setValue(value, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.automaticEventEnabled)") + } + defaults.synchronize() + } + + static func loadAutomacticEventsEnabledFlag(apiToken: String) -> Bool { + #if TV_AUTO_EVENTS + return true + #else + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(apiToken)-" + 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 { + return true // default true + } + if defaults.object(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.automaticEventEnabled)") != nil { + return defaults.bool(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.automaticEventEnabled)") + } else { // if there is no local settings, get the value from Decide + return defaults.bool(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.automaticEventEnabledFromDecide)") + } + #endif + } + + static func saveTimedEvents(timedEvents: InternalProperties, apiToken: String) { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return + } + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(apiToken)-" + let timedEventsData = NSKeyedArchiver.archivedData(withRootObject: timedEvents) + defaults.set(timedEventsData, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.timedEvents)") + defaults.synchronize() + } + + static func loadTimedEvents(apiToken: String) -> InternalProperties { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return InternalProperties() + } + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(apiToken)-" + guard let timedEventsData = defaults.data(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.timedEvents)") else { + return InternalProperties() + } + return NSKeyedUnarchiver.unarchiveObject(with: timedEventsData) as? InternalProperties ?? InternalProperties() + } + + static func saveSuperProperties(superProperties: InternalProperties, apiToken: String) { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return + } + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(apiToken)-" + let superPropertiesData = NSKeyedArchiver.archivedData(withRootObject: superProperties) + defaults.set(superPropertiesData, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.superProperties)") + defaults.synchronize() + } + + static func loadSuperProperties(apiToken: String) -> InternalProperties { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return InternalProperties() + } + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(apiToken)-" + guard let superPropertiesData = defaults.data(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.superProperties)") else { + return InternalProperties() + } + return NSKeyedUnarchiver.unarchiveObject(with: superPropertiesData) as? InternalProperties ?? InternalProperties() + } + + static func saveIdentity(_ mixpanelIdentity: MixpanelIdentity, apiToken: String) { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return + } + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(apiToken)-" + defaults.set(mixpanelIdentity.distinctID, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.distinctID)") + defaults.set(mixpanelIdentity.peopleDistinctID, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.peopleDistinctID)") + defaults.set(mixpanelIdentity.anonymousId, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.anonymousId)") + defaults.set(mixpanelIdentity.userId, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.userID)") + defaults.set(mixpanelIdentity.alias, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.alias)") + defaults.set(mixpanelIdentity.hadPersistedDistinctId, forKey: "\(prefix)\(MixpanelUserDefaultsKeys.hadPersistedDistinctId)") + defaults.synchronize() + } + + static func loadIdentity(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) + } + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(apiToken)-" + return MixpanelIdentity.init( + distinctID: defaults.string(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.distinctID)") ?? "", + peopleDistinctID: defaults.string(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.peopleDistinctID)"), + anonymousId: defaults.string(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.anonymousId)"), + userId: defaults.string(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.userID)"), + alias: defaults.string(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.alias)"), + hadPersistedDistinctId: defaults.bool(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.hadPersistedDistinctId)")) + } + + static func deleteMPUserDefaultsData(apiToken: String) { + guard let defaults = UserDefaults(suiteName: MixpanelUserDefaultsKeys.suiteName) else { + return + } + let prefix = "\(MixpanelUserDefaultsKeys.prefix)-\(apiToken)-" + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.distinctID)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.peopleDistinctID)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.anonymousId)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.userID)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.alias)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.hadPersistedDistinctId)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.automaticEventEnabled)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.automaticEventEnabledFromDecide)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.optOutStatus)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.timedEvents)") + defaults.removeObject(forKey: "\(prefix)\(MixpanelUserDefaultsKeys.superProperties)") + defaults.synchronize() + } + + // code for unarchiving from legacy archive files and migrating to SQLite / NSUserDefaults persistence + func migrate() { + if !needMigration() { + return + } + let (eventsQueue, + peopleQueue, + groupsQueue, + superProperties, + timedEvents, + distinctId, + anonymousId, + userId, + alias, + hadPersistedDistinctId, + peopleDistinctId, + peopleUnidentifiedQueue, + optOutStatus, + automaticEventsEnabled) = unarchive() + saveEntities(eventsQueue, type: PersistenceType.events) + saveEntities(peopleUnidentifiedQueue, type: PersistenceType.people, flag: PersistenceConstant.unIdentifiedFlag) + saveEntities(peopleQueue, type: PersistenceType.people) + saveEntities(groupsQueue, type: PersistenceType.groups) + MixpanelPersistence.saveSuperProperties(superProperties: superProperties, apiToken: apiToken) + MixpanelPersistence.saveTimedEvents(timedEvents: timedEvents, apiToken: apiToken) + MixpanelPersistence.saveIdentity(MixpanelIdentity.init( + distinctID: distinctId, + peopleDistinctID: peopleDistinctId, + anonymousId: anonymousId, + userId: userId, + alias: alias, + hadPersistedDistinctId: hadPersistedDistinctId), apiToken: apiToken) + if let optOutFlag = optOutStatus { + MixpanelPersistence.saveOptOutStatusFlag(value: optOutFlag, apiToken: apiToken) + } + if let automaticEventsFlag = automaticEventsEnabled { + MixpanelPersistence.saveAutomacticEventsEnabledFlag(value: automaticEventsFlag, fromDecide: false, apiToken: apiToken) + } + return + } + + private func filePathWithType(_ type: String) -> String? { + let filename = "mixpanel-\(apiToken)-\(type)" + let manager = FileManager.default + + #if os(iOS) + let url = manager.urls(for: .libraryDirectory, in: .userDomainMask).last + #else + let url = manager.urls(for: .cachesDirectory, in: .userDomainMask).last + #endif // os(iOS) + guard let urlUnwrapped = url?.appendingPathComponent(filename).path else { + return nil + } + + return urlUnwrapped + } + + private func unarchive() -> (eventsQueue: Queue, + peopleQueue: Queue, + groupsQueue: Queue, + superProperties: InternalProperties, + timedEvents: InternalProperties, + distinctId: String, + anonymousId: String?, + userId: String?, + alias: String?, + hadPersistedDistinctId: Bool?, + peopleDistinctId: String?, + peopleUnidentifiedQueue: Queue, + optOutStatus: Bool?, + automaticEventsEnabled: Bool?) { + let eventsQueue = unarchiveEvents() + let peopleQueue = unarchivePeople() + let groupsQueue = unarchiveGroups() + let optOutStatus = unarchiveOptOutStatus() + + let (superProperties, + timedEvents, + distinctId, + anonymousId, + userId, + alias, + hadPersistedDistinctId, + peopleDistinctId, + peopleUnidentifiedQueue, + automaticEventsEnabled) = unarchiveProperties() + + if let eventsFile = filePathWithType(PersistenceType.events.rawValue) { + removeArchivedFile(atPath: eventsFile) + } + if let peopleFile = filePathWithType(PersistenceType.people.rawValue) { + removeArchivedFile(atPath: peopleFile) + } + if let groupsFile = filePathWithType(PersistenceType.groups.rawValue) { + removeArchivedFile(atPath: groupsFile) + } + if let propsFile = filePathWithType("properties") { + removeArchivedFile(atPath: propsFile) + } + if let optOutFile = filePathWithType("optOutStatus") { + removeArchivedFile(atPath: optOutFile) + } + + return (eventsQueue, + peopleQueue, + groupsQueue, + superProperties, + timedEvents, + distinctId, + anonymousId, + userId, + alias, + hadPersistedDistinctId, + peopleDistinctId, + peopleUnidentifiedQueue, + optOutStatus, + automaticEventsEnabled) + } + + private func unarchiveWithFilePath(_ filePath: String) -> Any? { + if #available(iOS 11.0, macOS 10.13, watchOS 4.0, tvOS 11.0, *) { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)), + let unarchivedData = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) else { + Logger.info(message: "Unable to read file at path: \(filePath)") + removeArchivedFile(atPath: filePath) + return nil + } + return unarchivedData + } else { + guard let unarchivedData = NSKeyedUnarchiver.unarchiveObject(withFile: filePath) else { + Logger.info(message: "Unable to read file at path: \(filePath)") + removeArchivedFile(atPath: filePath) + return nil + } + return unarchivedData + } + } + + private func removeArchivedFile(atPath filePath: String) { + do { + try FileManager.default.removeItem(atPath: filePath) + } catch let err { + Logger.info(message: "Unable to remove file at path: \(filePath), error: \(err)") + } + } + + private func unarchiveEvents() -> Queue { + let data = unarchiveWithType(PersistenceType.events.rawValue) + return data as? Queue ?? [] + } + + private func unarchivePeople() -> Queue { + let data = unarchiveWithType(PersistenceType.people.rawValue) + return data as? Queue ?? [] + } + + private func unarchiveGroups() -> Queue { + let data = unarchiveWithType(PersistenceType.groups.rawValue) + return data as? Queue ?? [] + } + + private func unarchiveOptOutStatus() -> Bool? { + return unarchiveWithType("optOutStatus") as? Bool + } + + private func unarchiveProperties() -> (InternalProperties, + InternalProperties, + String, + String?, + String?, + String?, + Bool?, + String?, + Queue, + Bool?) { + let properties = unarchiveWithType("properties") as? InternalProperties + let superProperties = + properties?["superProperties"] as? InternalProperties ?? InternalProperties() + let timedEvents = + properties?["timedEvents"] as? InternalProperties ?? InternalProperties() + let distinctId = + properties?["distinctId"] as? String ?? "" + let anonymousId = + properties?["anonymousId"] as? String ?? nil + let userId = + properties?["userId"] as? String ?? nil + let alias = + properties?["alias"] as? String ?? nil + let hadPersistedDistinctId = + properties?["hadPersistedDistinctId"] as? Bool ?? nil + let peopleDistinctId = + properties?["peopleDistinctId"] as? String ?? nil + let peopleUnidentifiedQueue = + properties?["peopleUnidentifiedQueue"] as? Queue ?? Queue() + let automaticEventsEnabled = + properties?["automaticEvents"] as? Bool ?? nil + + return (superProperties, + timedEvents, + distinctId, + anonymousId, + userId, + alias, + hadPersistedDistinctId, + peopleDistinctId, + peopleUnidentifiedQueue, + automaticEventsEnabled) + } + + private func unarchiveWithType(_ type: String) -> Any? { + let filePath = filePathWithType(type) + guard let path = filePath else { + Logger.info(message: "bad file path, cant fetch file") + return nil + } + + guard let unarchivedData = unarchiveWithFilePath(path) else { + Logger.info(message: "can't unarchive file") + return nil + } + + return unarchivedData + } + + private func needMigration() -> Bool { + return fileExists(type: LegacyArchiveType.events.rawValue) || + fileExists(type: LegacyArchiveType.people.rawValue) || + fileExists(type: LegacyArchiveType.people.rawValue) || + fileExists(type: LegacyArchiveType.groups.rawValue) || + fileExists(type: LegacyArchiveType.properties.rawValue) || + fileExists(type: LegacyArchiveType.optOutStatus.rawValue) + } + + private func fileExists(type: String) -> Bool { + return FileManager.default.fileExists(atPath: filePathWithType(type) ?? "") + } + +} diff --git a/Sources/MixpanelType.swift b/Sources/MixpanelType.swift index 0d725da31..cc114dfc2 100644 --- a/Sources/MixpanelType.swift +++ b/Sources/MixpanelType.swift @@ -352,3 +352,13 @@ func assertPropertyTypes(_ properties: Properties?) { } } } + +extension Dictionary { + func get(key: Key, defaultValue: T) -> T { + if let value = self[key] as? T { + return value + } + + return defaultValue + } +} diff --git a/Sources/Network.swift b/Sources/Network.swift index f5fef2ba4..7552064b8 100644 --- a/Sources/Network.swift +++ b/Sources/Network.swift @@ -19,6 +19,7 @@ struct BasePath { guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return nil } + components.path = path components.queryItems = queryItems // adding workaround to replece + for %2B as it's not done by default within URLComponents @@ -61,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 } @@ -115,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 0e1391c28..12f08a262 100644 --- a/Sources/People.swift +++ b/Sources/People.swift @@ -20,20 +20,23 @@ open class People { open var ignoreTime = false let apiToken: String - let serialQueue: DispatchQueue + let serialQueue: DispatchQueue! let lock: ReadWriteLock - var peopleQueue = Queue() - var flushPeopleQueue = Queue() - var unidentifiedQueue = Queue() var distinctId: String? - var delegate: FlushDelegate? + weak var delegate: FlushDelegate? let metadata: SessionMetadata - - init(apiToken: String, serialQueue: DispatchQueue, lock: ReadWriteLock, metadata: SessionMetadata) { + let mixpanelPersistence: MixpanelPersistence + + init(apiToken: String, + serialQueue: DispatchQueue, + lock: ReadWriteLock, + metadata: SessionMetadata, + mixpanelPersistence: MixpanelPersistence) { self.apiToken = apiToken self.serialQueue = serialQueue self.lock = lock self.metadata = metadata + self.mixpanelPersistence = mixpanelPersistence } func addPeopleRecordToQueueWithAction(_ action: String, properties: InternalProperties) { @@ -82,18 +85,10 @@ open class People { if let distinctId = self.distinctId { r["$distinct_id"] = distinctId - self.addPeopleObject(r) + // identified + self.mixpanelPersistence.saveEntity(r, type: .people, flag: !PersistenceConstant.unIdentifiedFlag) } else { - self.lock.write { - self.unidentifiedQueue.append(r) - if self.unidentifiedQueue.count > QueueConstants.queueSize { - self.unidentifiedQueue.remove(at: 0) - } - } - - } - self.lock.read { - Persistence.archivePeople(self.flushPeopleQueue + self.peopleQueue, token: self.apiToken) + self.mixpanelPersistence.saveEntity(r, type: .people, flag: PersistenceConstant.unIdentifiedFlag) } } @@ -102,17 +97,6 @@ open class People { } } - func addPeopleObject(_ r: InternalProperties) { - lock.write { - Logger.debug(message: "adding to people queue") - Logger.debug(message: r) - peopleQueue.append(r) - if peopleQueue.count > QueueConstants.queueSize { - peopleQueue.remove(at: 0) - } - } - } - func merge(properties: InternalProperties) { addPeopleRecordToQueueWithAction("$merge", properties: properties) } diff --git a/Sources/Persistence.swift b/Sources/Persistence.swift deleted file mode 100644 index e113862d4..000000000 --- a/Sources/Persistence.swift +++ /dev/null @@ -1,466 +0,0 @@ -// -// Persistence.swift -// Mixpanel -// -// Created by Yarden Eitan on 6/2/16. -// Copyright © 2016 Mixpanel. All rights reserved. -// - -import Foundation - -struct ArchivedProperties { - let superProperties: InternalProperties - let timedEvents: InternalProperties - let distinctId: String - let anonymousId: String? - let userId: String? - let alias: String? - let hadPersistedDistinctId: Bool? - let peopleDistinctId: String? - let peopleUnidentifiedQueue: Queue - #if DECIDE - let automaticEventsEnabled: Bool? - #endif // DECIDE -} - -class Persistence { - private static let archiveQueue: DispatchQueue = DispatchQueue(label: "com.mixpanel.archiveQueue", qos: .utility) - - enum ArchiveType: String { - case events - case people - case groups - case properties - case optOutStatus - } - - static func filePathWithType(_ type: ArchiveType, token: String) -> String? { - return filePathFor(type.rawValue, token: token) - } - - static private func filePathFor(_ archiveType: String, token: String) -> String? { - let filename = "mixpanel-\(token)-\(archiveType)" - let manager = FileManager.default - - #if os(iOS) - let url = manager.urls(for: .libraryDirectory, in: .userDomainMask).last - #else - let url = manager.urls(for: .cachesDirectory, in: .userDomainMask).last - #endif // os(iOS) - - guard let urlUnwrapped = url?.appendingPathComponent(filename).path else { - return nil - } - - return urlUnwrapped - } - - #if DECIDE - static func archive(eventsQueue: Queue, - peopleQueue: Queue, - groupsQueue: Queue, - properties: ArchivedProperties, - token: String) { - archiveEvents(eventsQueue, token: token) - archivePeople(peopleQueue, token: token) - archiveGroups(groupsQueue, token: token) - archiveProperties(properties, token: token) - } - #else - static func archive(eventsQueue: Queue, - peopleQueue: Queue, - groupsQueue: Queue, - properties: ArchivedProperties, - token: String) { - archiveEvents(eventsQueue, token: token) - archivePeople(peopleQueue, token: token) - archiveGroups(groupsQueue, token: token) - archiveProperties(properties, token: token) - } - #endif // DECIDE - - static func archiveEvents(_ eventsQueue: Queue, token: String) { - archiveQueue.sync { [eventsQueue, token] in - archiveToFile(.events, object: eventsQueue, token: token) - } - } - - static func archivePeople(_ peopleQueue: Queue, token: String) { - archiveQueue.sync { [peopleQueue, token] in - archiveToFile(.people, object: peopleQueue, token: token) - } - } - - static func archiveGroups(_ groupsQueue: Queue, token: String) { - archiveQueue.sync { [groupsQueue, token] in - archiveToFile(.groups, object: groupsQueue, token: token) - } - } - - static func archiveOptOutStatus(_ optOutStatus: Bool, token: String) { - archiveQueue.sync { [optOutStatus, token] in - archiveToFile(.optOutStatus, object: optOutStatus, token: token) - } - } - - static func archiveProperties(_ properties: ArchivedProperties, token: String) { - archiveQueue.sync { [properties, token] in - var p = InternalProperties() - p["distinctId"] = properties.distinctId - p["anonymousId"] = properties.anonymousId - p["userId"] = properties.userId - p["alias"] = properties.alias - p["hadPersistedDistinctId"] = properties.hadPersistedDistinctId - p["superProperties"] = properties.superProperties - p["peopleDistinctId"] = properties.peopleDistinctId - p["peopleUnidentifiedQueue"] = properties.peopleUnidentifiedQueue - p["timedEvents"] = properties.timedEvents - #if DECIDE - p["automaticEvents"] = properties.automaticEventsEnabled - #endif // DECIDE - archiveToFile(.properties, object: p, token: token) - } - } - - static private func archiveToFile(_ type: ArchiveType, object: Any, token: String) { - var archiveObject = object - if var queue = archiveObject as? Queue { - if queue.count > QueueConstants.queueSize { - queue.removeSubrange(0..<(queue.count - QueueConstants.queueSize)) - archiveObject = queue - } - } - - let filePath = filePathWithType(type, token: token) - guard let path = filePath else { - Logger.error(message: "bad file path, cant fetch file") - return - } - - if #available(iOS 11.0, macOS 10.13, watchOS 4.0, tvOS 11.0, *) { - do { - let data = try NSKeyedArchiver.archivedData(withRootObject: archiveObject, requiringSecureCoding: false) - try data.write(to: URL(fileURLWithPath: path)) - } catch { - Logger.error(message: "failed to archive \(type.rawValue)") - return - } - } else { - if !NSKeyedArchiver.archiveRootObject(archiveObject, toFile: path) { - Logger.error(message: "failed to archive \(type.rawValue)") - return - } - } - - addSkipBackupAttributeToItem(at: path) - } - - static private func addSkipBackupAttributeToItem(at path: String) { - var url = URL.init(fileURLWithPath: path) - var resourceValues = URLResourceValues() - resourceValues.isExcludedFromBackup = true - do { - try url.setResourceValues(resourceValues) - } catch { - Logger.info(message: "Error excluding \(path) from backup.") - } - } - - #if DECIDE - static func unarchive(token: String) -> (eventsQueue: Queue, - peopleQueue: Queue, - groupsQueue: Queue, - superProperties: InternalProperties, - timedEvents: InternalProperties, - distinctId: String, - anonymousId: String?, - userId: String?, - alias: String?, - hadPersistedDistinctId: Bool?, - peopleDistinctId: String?, - peopleUnidentifiedQueue: Queue, - optOutStatus: Bool?, - automaticEventsEnabled: Bool?) { - let eventsQueue = unarchiveEvents(token: token) - let peopleQueue = unarchivePeople(token: token) - let groupsQueue = unarchiveGroups(token: token) - let optOutStatus = unarchiveOptOutStatus(token: token) - - let (superProperties, - timedEvents, - distinctId, - anonymousId, - userId, - alias, - hadPersistedDistinctId, - peopleDistinctId, - peopleUnidentifiedQueue, - automaticEventsEnabled) = unarchiveProperties(token: token) - - return (eventsQueue, - peopleQueue, - groupsQueue, - superProperties, - timedEvents, - distinctId, - anonymousId, - userId, - alias, - hadPersistedDistinctId, - peopleDistinctId, - peopleUnidentifiedQueue, - optOutStatus, - automaticEventsEnabled) - } - #else - static func unarchive(token: String) -> (eventsQueue: Queue, - peopleQueue: Queue, - groupsQueue: Queue, - superProperties: InternalProperties, - timedEvents: InternalProperties, - distinctId: String, - anonymousId: String?, - userId: String?, - alias: String?, - hadPersistedDistinctId: Bool?, - peopleDistinctId: String?, - peopleUnidentifiedQueue: Queue, - optOutStatus: Bool?) { - let eventsQueue = unarchiveEvents(token: token) - let peopleQueue = unarchivePeople(token: token) - let groupsQueue = unarchiveGroups(token: token) - let optOutStatus = unarchiveOptOutStatus(token: token) - - let (superProperties, - timedEvents, - distinctId, - anonymousId, - userId, - alias, - hadPersistedDistinctId, - peopleDistinctId, - peopleUnidentifiedQueue, - _) = unarchiveProperties(token: token) - - return (eventsQueue, - peopleQueue, - groupsQueue, - superProperties, - timedEvents, - distinctId, - anonymousId, - userId, - alias, - hadPersistedDistinctId, - peopleDistinctId, - peopleUnidentifiedQueue, - optOutStatus) - } - #endif // DECIDE - - static private func unarchiveWithFilePath(_ filePath: String) -> Any? { - if #available(iOS 11.0, macOS 10.13, watchOS 4.0, tvOS 11.0, *) { - guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)), - let unarchivedData = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) else { - Logger.info(message: "Unable to read file at path: \(filePath)") - removeArchivedFile(atPath: filePath) - return nil - } - return unarchivedData - } else { - guard let unarchivedData = NSKeyedUnarchiver.unarchiveObject(withFile: filePath) else { - Logger.info(message: "Unable to read file at path: \(filePath)") - removeArchivedFile(atPath: filePath) - return nil - } - return unarchivedData - } - } - - static private func removeArchivedFile(atPath filePath: String) { - do { - try FileManager.default.removeItem(atPath: filePath) - } catch let err { - Logger.info(message: "Unable to remove file at path: \(filePath), error: \(err)") - } - } - - static private func unarchiveEvents(token: String) -> Queue { - let data = unarchiveWithType(.events, token: token) - return data as? Queue ?? [] - } - - static private func unarchivePeople(token: String) -> Queue { - let data = unarchiveWithType(.people, token: token) - return data as? Queue ?? [] - } - - static private func unarchiveGroups(token: String) -> Queue { - let data = unarchiveWithType(.groups, token: token) - return data as? Queue ?? [] - } - - static private func unarchiveOptOutStatus(token: String) -> Bool? { - return unarchiveWithType(.optOutStatus, token: token) as? Bool - } - - #if DECIDE - static private func unarchiveProperties(token: String) -> (InternalProperties, - InternalProperties, - String, - String?, - String?, - String?, - Bool?, - String?, - Queue, - Bool?) { - let (superProperties, - timedEvents, - distinctId, - anonymousId, - userId, - alias, - hadPersistedDistinctId, - peopleDistinctId, - peopleUnidentifiedQueue, - automaticEventsEnabled) = unarchivePropertiesHelper(token: token) - - return (superProperties, - timedEvents, - distinctId, - anonymousId, - userId, - alias, - hadPersistedDistinctId, - peopleDistinctId, - peopleUnidentifiedQueue, - automaticEventsEnabled) - } - #else - static private func unarchiveProperties(token: String) -> (InternalProperties, - InternalProperties, - String, - String?, - String?, - String?, - Bool?, - String?, - Queue, - Bool?) { - return unarchivePropertiesHelper(token: token) - } - #endif // DECIDE - - static private func unarchivePropertiesHelper(token: String) -> (InternalProperties, - InternalProperties, - String, - String?, - String?, - String?, - Bool?, - String?, - Queue, - Bool?) { - let properties = unarchiveWithType(.properties, token: token) as? InternalProperties - let superProperties = - properties?["superProperties"] as? InternalProperties ?? InternalProperties() - let timedEvents = - properties?["timedEvents"] as? InternalProperties ?? InternalProperties() - var distinctId = - properties?["distinctId"] as? String ?? "" - var anonymousId = - properties?["anonymousId"] as? String ?? nil - var userId = - properties?["userId"] as? String ?? nil - var alias = - properties?["alias"] as? String ?? nil - var hadPersistedDistinctId = - properties?["hadPersistedDistinctId"] as? Bool ?? nil - var peopleDistinctId = - properties?["peopleDistinctId"] as? String ?? nil - let peopleUnidentifiedQueue = - properties?["peopleUnidentifiedQueue"] as? Queue ?? Queue() - let automaticEventsEnabled = - properties?["automaticEvents"] as? Bool ?? nil - - if properties == nil { - (distinctId, peopleDistinctId, anonymousId, userId, alias, hadPersistedDistinctId) = restoreIdentity(token: token) - } - - return (superProperties, - timedEvents, - distinctId, - anonymousId, - userId, - alias, - hadPersistedDistinctId, - peopleDistinctId, - peopleUnidentifiedQueue, - automaticEventsEnabled) - } - - static private func unarchiveWithType(_ type: ArchiveType, token: String) -> Any? { - let filePath = filePathWithType(type, token: token) - guard let path = filePath else { - Logger.info(message: "bad file path, cant fetch file") - return nil - } - - guard let unarchivedData = unarchiveWithFilePath(path) else { - Logger.info(message: "can't unarchive file") - return nil - } - - if var queue = unarchivedData as? Queue { - if queue.count > QueueConstants.queueSize { - queue.removeSubrange(0..<(queue.count - QueueConstants.queueSize)) - return queue - } - } - - return unarchivedData - } - - static func storeIdentity(token: String, distinctID: String, peopleDistinctID: String?, anonymousID: String?, userID: String?, alias: String?, hadPersistedDistinctId: Bool?) { - guard let defaults = UserDefaults(suiteName: "Mixpanel") else { - return - } - let prefix = "mixpanel-\(token)-" - defaults.set(distinctID, forKey: prefix + "MPDistinctID") - defaults.set(peopleDistinctID, forKey: prefix + "MPPeopleDistinctID") - defaults.set(anonymousID, forKey: prefix + "MPAnonymousId") - defaults.set(userID, forKey: prefix + "MPUserId") - defaults.set(alias, forKey: prefix + "MPAlias") - defaults.set(hadPersistedDistinctId, forKey: prefix + "MPHadPersistedDistinctId") - defaults.synchronize() - } - - static func restoreIdentity(token: String) -> (String, String?, String?, String?, String?, Bool?) { - guard let defaults = UserDefaults(suiteName: "Mixpanel") else { - return ("", nil, nil, nil, nil, nil) - } - let prefix = "mixpanel-\(token)-" - return (defaults.string(forKey: prefix + "MPDistinctID") ?? "", - defaults.string(forKey: prefix + "MPPeopleDistinctID"), - defaults.string(forKey: prefix + "MPAnonymousId"), - defaults.string(forKey: prefix + "MPUserId"), - defaults.string(forKey: prefix + "MPAlias"), - defaults.bool(forKey: prefix + "MPHadPersistedDistinctId")) - } - - static func deleteMPUserDefaultsData(token: String) { - guard let defaults = UserDefaults(suiteName: "Mixpanel") else { - return - } - let prefix = "mixpanel-\(token)-" - defaults.removeObject(forKey: prefix + "MPDistinctID") - defaults.removeObject(forKey: prefix + "MPPeopleDistinctID") - defaults.removeObject(forKey: prefix + "MPAnonymousId") - defaults.removeObject(forKey: prefix + "MPUserId") - defaults.removeObject(forKey: prefix + "MPAlias") - defaults.removeObject(forKey: prefix + "MPHadPersistedDistinctId") - defaults.synchronize() - } - -} diff --git a/Sources/Track.swift b/Sources/Track.swift index afd180a36..6b8afd285 100644 --- a/Sources/Track.swift +++ b/Sources/Track.swift @@ -18,29 +18,35 @@ class Track { let apiToken: String let lock: ReadWriteLock let metadata: SessionMetadata + let mixpanelPersistence: MixpanelPersistence + + var isAutomaticEventEnabled: Bool { + return MixpanelPersistence.loadAutomacticEventsEnabledFlag(apiToken: apiToken) + } - init(apiToken: String, lock: ReadWriteLock, metadata: SessionMetadata) { + init(apiToken: String, lock: ReadWriteLock, metadata: SessionMetadata, mixpanelPersistence: MixpanelPersistence) { self.apiToken = apiToken self.lock = lock self.metadata = metadata + self.mixpanelPersistence = mixpanelPersistence } func track(event: String?, properties: Properties? = nil, - eventsQueue: Queue, timedEvents: InternalProperties, superProperties: InternalProperties, - distinctId: String, - anonymousId: String?, - userId: String?, - hadPersistedDistinctId: Bool?, - epochInterval: Double) -> (eventsQueque: Queue, timedEvents: InternalProperties, properties: InternalProperties) { + mixpanelIdentity: MixpanelIdentity, + epochInterval: Double) -> InternalProperties { var ev = event if ev == nil || ev!.isEmpty { Logger.info(message: "mixpanel track called with empty event parameter. using 'mp_event'") ev = "mp_event" } + if !isAutomaticEventEnabled && ev!.hasPrefix("$ae_") { + return timedEvents + } assertPropertyTypes(properties) + let epochSeconds = Int(round(epochInterval)) let eventStartTime = timedEvents[ev!] as? Double var p = InternalProperties() @@ -54,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 @@ -72,17 +78,10 @@ class Track { var trackEvent: InternalProperties = ["event": ev!, "properties": p] metadata.toDict().forEach { (k, v) in trackEvent[k] = v } - var shadowEventsQueue = eventsQueue - Logger.debug(message: "adding event to queue") - Logger.debug(message: trackEvent) - shadowEventsQueue.append(trackEvent) - if shadowEventsQueue.count > QueueConstants.queueSize { - Logger.warn(message: "queue is full, dropping the oldest event from the queue") - Logger.warn(message: shadowEventsQueue.first as Any) - shadowEventsQueue.remove(at: 0) - } - return (shadowEventsQueue, shadowTimedEvents, p) + self.mixpanelPersistence.saveEntity(trackEvent, type: .events) + MixpanelPersistence.saveTimedEvents(timedEvents: shadowTimedEvents, apiToken: apiToken) + return shadowTimedEvents } func registerSuperProperties(_ properties: Properties, @@ -120,12 +119,11 @@ class Track { func unregisterSuperProperty(_ propertyName: String, superProperties: InternalProperties) -> InternalProperties { - var updatedSuperProperties = superProperties updatedSuperProperties.removeValue(forKey: propertyName) return updatedSuperProperties } - + func clearSuperProperties(_ superProperties: InternalProperties) -> InternalProperties { var updatedSuperProperties = superProperties updatedSuperProperties.removeAll() diff --git a/scripts/generate_docs.sh b/scripts/generate_docs.sh index ac8d64c97..332eb50d9 100755 --- a/scripts/generate_docs.sh +++ b/scripts/generate_docs.sh @@ -3,6 +3,6 @@ jazzy \ -a Mixpanel \ -u http://mixpanel.com \ --github_url https://github.com/mixpanel/mixpanel-swift \ ---module-version 3.0.0.beta.2 \ +--module-version 3.0.0.beta.4 \ --framework-root . \ --module Mixpanel