diff --git a/CHANGELOG.md b/CHANGELOG.md index d39dfcae..b4135647 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,28 @@ # Change Log All notable changes to this project will be documented in this file. +### [Version 7.1.0](https://github.com/CleverTap/clevertap-ios-sdk/releases/tag/7.1.0) (January 21, 2024) + +#### Added +- Adds support for triggering InApps based on first-time event filtering in multiple triggers. Now you can create campaign triggers that combine recurring and first-time events. For example: Trigger a campaign when "Charged" occurs (every time) OR "App Launched" occurs (first time only). +- Adds new user-level event log tracking system to store and manage user event history. New APIs include: + - `getUserEventLog(:)`: Get details about a specific event + - `getUserEventLogCount(:)`: Get count of times an event occurred + - `getUserLastVisitTs()`: Get timestamp of user's last app visit + - `getUserAppLaunchCount()`: Get total number of times user has launched the app + - `getUserEventLogHistory()`: Get full event history for current user + +#### API Changes + +- **Deprecated:** The old event tracking APIs tracked events at the device level rather than the user level, making it difficult to maintain accurate user-specific event histories, especially in multi-user scenarios. The following methods have been deprecated in favor of new user-specific event tracking APIs that provide more accurate, user-level analytics. These deprecated methods will be removed in future versions with prior notice: + - `eventGetDetail(:)`: Use `getUserEventLog()` instead for user-specific event details + - `eventGetOccurrences(:)`: Use `getUserEventLogCount()` instead for user-specific event counts + - `eventGetFirstTime(:)`: Use `getUserEventLog()` instead for user-specific first occurrence timestamp + - `eventGetLastTime(:)`: Use `getUserEventLog()` instead for user-specific last occurrence timestamp + - `userGetPreviousVisitTime()`: Use `getUserLastVisitTs()` instead for user-specific last visit timestamp + - `userGetTotalVisits()`: Use `getUserAppLaunchCount()` instead for user-specific app launch count + - `userGetEventHistory()`: Use `getUserEventLogHistory()` instead for user-specific event history + ### [Version 7.0.3](https://github.com/CleverTap/clevertap-ios-sdk/releases/tag/7.0.3) (November 29, 2024) #### Added diff --git a/CleverTap-iOS-SDK.podspec b/CleverTap-iOS-SDK.podspec index 448fcbc2..c385c8f2 100644 --- a/CleverTap-iOS-SDK.podspec +++ b/CleverTap-iOS-SDK.podspec @@ -17,7 +17,7 @@ s.ios.source_files = 'CleverTapSDK/**/*.{h,m}' s.ios.exclude_files = 'CleverTapSDK/include/**/*.h' s.ios.public_header_files = 'CleverTapSDK/CleverTap.h', 'CleverTapSDK/CleverTap+SSLPinning.h','CleverTapSDK/CleverTap+Inbox.h', 'CleverTapSDK/CleverTapInstanceConfig.h', 'CleverTapSDK/CleverTapBuildInfo.h', 'CleverTapSDK/CleverTapEventDetail.h', 'CleverTapSDK/CleverTapInAppNotificationDelegate.h', 'CleverTapSDK/CleverTapSyncDelegate.h', 'CleverTapSDK/CleverTapTrackedViewController.h', 'CleverTapSDK/CleverTapUTMDetail.h', 'CleverTapSDK/CleverTapJSInterface.h', 'CleverTapSDK/CleverTap+DisplayUnit.h', 'CleverTapSDK/CleverTap+FeatureFlags.h', 'CleverTapSDK/CleverTap+ProductConfig.h', 'CleverTapSDK/CleverTapPushNotificationDelegate.h', 'CleverTapSDK/CleverTapURLDelegate.h', 'CleverTapSDK/CleverTap+InAppNotifications.h', 'CleverTapSDK/CleverTap+SCDomain.h', 'CleverTapSDK/CleverTap+PushPermission.h', 'CleverTapSDK/InApps/CTLocalInApp.h', 'CleverTapSDK/CleverTap+CTVar.h', 'CleverTapSDK/ProductExperiences/CTVar.h', 'CleverTapSDK/LeanplumCT.h', 'CleverTapSDK/InApps/CustomTemplates/CTInAppTemplateBuilder.h', 'CleverTapSDK/InApps/CustomTemplates/CTAppFunctionBuilder.h', 'CleverTapSDK/InApps/CustomTemplates/CTTemplatePresenter.h', 'CleverTapSDK/InApps/CustomTemplates/CTTemplateProducer.h', 'CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder.h', 'CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate.h', 'CleverTapSDK/InApps/CustomTemplates/CTTemplateContext.h', 'CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager.h', 'CleverTapSDK/InApps/CustomTemplates/CTJsonTemplateProducer.h' s.tvos.deployment_target = '9.0' -s.tvos.source_files = 'CleverTapSDK/*.{h,m}', 'CleverTapSDK/FileDownload/*.{h,m}', 'CleverTapSDK/ProductConfig/**/*.{h,m}', 'CleverTapSDK/FeatureFlags/**/*.{h,m}', 'CleverTapSDK/ProductExperiences/*.{h,m}', 'CleverTapSDK/Swizzling/*.{h,m}', 'CleverTapSDK/Session/*.{h,m}' +s.tvos.source_files = 'CleverTapSDK/*.{h,m}', 'CleverTapSDK/FileDownload/*.{h,m}', 'CleverTapSDK/ProductConfig/**/*.{h,m}', 'CleverTapSDK/FeatureFlags/**/*.{h,m}', 'CleverTapSDK/ProductExperiences/*.{h,m}', 'CleverTapSDK/Swizzling/*.{h,m}', 'CleverTapSDK/Session/*.{h,m}', 'CleverTapSDK/EventDatabase/*.{h,m}' s.tvos.exclude_files = 'CleverTapSDK/include/**/*.h', 'CleverTapSDK/CleverTapJSInterface.{h,m}', 'CleverTapSDK/CTInAppNotification.{h,m}', 'CleverTapSDK/CTNotificationButton.{h,m}', 'CleverTapSDK/CTNotificationAction.{h,m}', 'CleverTapSDK/CTPushPrimerManager.{h,m}', 'CleverTapSDK/InApps/*.{h,m}', 'CleverTapSDK/InApps/**/*.{h,m}', 'CleverTapSDK/CTInAppFCManager.{h,m}', 'CleverTapSDK/CTInAppDisplayViewController.{h,m}' s.tvos.public_header_files = 'CleverTapSDK/CleverTap.h', 'CleverTapSDK/CleverTap+SSLPinning.h', 'CleverTapSDK/CleverTapInstanceConfig.h', 'CleverTapSDK/CleverTapBuildInfo.h', 'CleverTapSDK/CleverTapEventDetail.h', 'CleverTapSDK/CleverTapSyncDelegate.h', 'CleverTapSDK/CleverTapTrackedViewController.h', 'CleverTapSDK/CleverTapUTMDetail.h', 'CleverTapSDK/CleverTap+FeatureFlags.h', 'CleverTapSDK/CleverTap+ProductConfig.h', 'CleverTapSDK/CleverTap+CTVar.h', 'CleverTapSDK/ProductExperiences/CTVar.h' end diff --git a/CleverTapSDK.xcodeproj/project.pbxproj b/CleverTapSDK.xcodeproj/project.pbxproj index 968b6a99..1df10405 100644 --- a/CleverTapSDK.xcodeproj/project.pbxproj +++ b/CleverTapSDK.xcodeproj/project.pbxproj @@ -166,10 +166,19 @@ 4808030E292EB4FB00C06E2F /* CleverTap+PushPermission.h in Headers */ = {isa = PBXBuildFile; fileRef = 4808030D292EB4FB00C06E2F /* CleverTap+PushPermission.h */; }; 48080311292EB50D00C06E2F /* CTLocalInApp.h in Headers */ = {isa = PBXBuildFile; fileRef = 4808030F292EB50D00C06E2F /* CTLocalInApp.h */; settings = {ATTRIBUTES = (Public, ); }; }; 48080312292EB50D00C06E2F /* CTLocalInApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 48080310292EB50D00C06E2F /* CTLocalInApp.m */; }; + 4847D16D2CCB90E0008DC327 /* CTEventDatabase.h in Headers */ = {isa = PBXBuildFile; fileRef = 4847D16C2CCB90E0008DC327 /* CTEventDatabase.h */; }; + 4847D16F2CCB90F5008DC327 /* CTEventDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 4847D16E2CCB90F5008DC327 /* CTEventDatabase.m */; }; + 4847D1722CDA17C2008DC327 /* CTEventDatabaseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4847D1712CDA17C2008DC327 /* CTEventDatabaseTests.m */; }; 487854072BF4BC4E00565685 /* CTFileDownloaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 487854062BF4BC4E00565685 /* CTFileDownloaderTests.m */; }; 48A2C4B92BD67DDC006C61BC /* sampleTXTStub.txt in Resources */ = {isa = PBXBuildFile; fileRef = 48A2C4B72BD67DDB006C61BC /* sampleTXTStub.txt */; }; 48A2C4BA2BD67DDC006C61BC /* samplePDFStub.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 48A2C4B82BD67DDB006C61BC /* samplePDFStub.pdf */; }; 48C0FD6F2BCD522100E01EA9 /* CTFileDownloadManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 48C0FD6E2BCD522100E01EA9 /* CTFileDownloadManagerTests.m */; }; + 48DC69B42D3FDF1F000E7BE4 /* CTEventDatabase.h in Headers */ = {isa = PBXBuildFile; fileRef = 4847D16C2CCB90E0008DC327 /* CTEventDatabase.h */; }; + 48DC69B52D3FDF23000E7BE4 /* CTEventDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 4847D16E2CCB90F5008DC327 /* CTEventDatabase.m */; }; + 48DC69B62D3FDF35000E7BE4 /* CTNotificationAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB778C92BED21CE00A41628 /* CTNotificationAction.h */; }; + 48DC69B72D3FDF39000E7BE4 /* CTNotificationAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BB778CA2BED21CE00A41628 /* CTNotificationAction.m */; }; + 48DC69B82D3FDF4D000E7BE4 /* CTCustomTemplateInAppData.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BB778C52BECEC2700A41628 /* CTCustomTemplateInAppData.h */; }; + 48DC69B92D3FDF55000E7BE4 /* CTCustomTemplateInAppData.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BB778C62BECEC2700A41628 /* CTCustomTemplateInAppData.m */; }; 48F9FD092C208F7100617770 /* CTInAppStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A4427D72ABCE5EB0098866F /* CTInAppStore.m */; }; 48F9FD1D2C3D30BF00617770 /* CTFileDownloadManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 48F9FD182C3D30B600617770 /* CTFileDownloadManager.m */; }; 48F9FD1E2C3D30BF00617770 /* CTFileDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 48F9FD192C3D30B600617770 /* CTFileDownloader.m */; }; @@ -183,6 +192,7 @@ 49C189A6243B13110003E4D4 /* CleverTapFeatureFlags.m in Sources */ = {isa = PBXBuildFile; fileRef = D0D4C9F22414EE6C0029477E /* CleverTapFeatureFlags.m */; }; 49E2B18324178E7400AD704B /* CleverTapConfigValue.m in Sources */ = {isa = PBXBuildFile; fileRef = 49E2B18124178E7400AD704B /* CleverTapConfigValue.m */; }; 49E2B18824237DCB00AD704B /* CleverTapFeatureFlagsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D4C9F42414EE770029477E /* CleverTapFeatureFlagsPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 4E0721512D0974B6000DE9C6 /* CTLocalDataStore+Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E0721502D0974B6000DE9C6 /* CTLocalDataStore+Tests.m */; }; 4E1F155227692A11009387AE /* CleverTap+Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = D02AC2E8276402CF0031C1BE /* CleverTap+Tests.m */; }; 4E1F155B276B662C009387AE /* EventDetail.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E1F155A276B662C009387AE /* EventDetail.m */; }; 4E1F1562277090D6009387AE /* inapp_alert.json in Resources */ = {isa = PBXBuildFile; fileRef = 4E1F1561277090D6009387AE /* inapp_alert.json */; }; @@ -350,8 +360,8 @@ 6B32A0AD2B9DBE31009ADC57 /* CTTemplatePresenterMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B32A0AC2B9DBE31009ADC57 /* CTTemplatePresenterMock.m */; }; 6B32A0B02B9DC374009ADC57 /* CTTemplateArgumentTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B32A0AF2B9DC374009ADC57 /* CTTemplateArgumentTest.m */; }; 6B32A0B42B9F2E8F009ADC57 /* CTTestTemplateProducer.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B32A0B32B9F2E8F009ADC57 /* CTTestTemplateProducer.m */; }; - 6B453EFE2CF74BE2003C7A89 /* CTEventAdapterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B453EFD2CF74BE2003C7A89 /* CTEventAdapterTest.m */; }; 6B453EF92CF621E3003C7A89 /* CTInAppDisplayViewControllerMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B453EF82CF621E3003C7A89 /* CTInAppDisplayViewControllerMock.m */; }; + 6B453EFE2CF74BE2003C7A89 /* CTEventAdapterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B453EFD2CF74BE2003C7A89 /* CTEventAdapterTest.m */; }; 6B4A0F912B45EF6D00A42C6D /* CTInAppTriggerManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B4A0F902B45EF6D00A42C6D /* CTInAppTriggerManagerTest.m */; }; 6B535FB62AD56C60002A2663 /* CTMultiDelegateManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 6B535FB42AD56C60002A2663 /* CTMultiDelegateManager.h */; }; 6B535FB72AD56C60002A2663 /* CTMultiDelegateManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 6B535FB42AD56C60002A2663 /* CTMultiDelegateManager.h */; }; @@ -776,6 +786,9 @@ 4808030D292EB4FB00C06E2F /* CleverTap+PushPermission.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CleverTap+PushPermission.h"; sourceTree = ""; }; 4808030F292EB50D00C06E2F /* CTLocalInApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTLocalInApp.h; sourceTree = ""; }; 48080310292EB50D00C06E2F /* CTLocalInApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CTLocalInApp.m; sourceTree = ""; }; + 4847D16C2CCB90E0008DC327 /* CTEventDatabase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTEventDatabase.h; sourceTree = ""; }; + 4847D16E2CCB90F5008DC327 /* CTEventDatabase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTEventDatabase.m; sourceTree = ""; }; + 4847D1712CDA17C2008DC327 /* CTEventDatabaseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTEventDatabaseTests.m; sourceTree = ""; }; 487854062BF4BC4E00565685 /* CTFileDownloaderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTFileDownloaderTests.m; sourceTree = ""; }; 48A2C4B72BD67DDB006C61BC /* sampleTXTStub.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = sampleTXTStub.txt; sourceTree = ""; }; 48A2C4B82BD67DDB006C61BC /* samplePDFStub.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = samplePDFStub.pdf; sourceTree = ""; }; @@ -784,10 +797,13 @@ 48F9FD182C3D30B600617770 /* CTFileDownloadManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CTFileDownloadManager.m; sourceTree = ""; }; 48F9FD192C3D30B600617770 /* CTFileDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CTFileDownloader.m; sourceTree = ""; }; 48F9FD1A2C3D30B600617770 /* CTFileDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTFileDownloader.h; sourceTree = ""; }; + 48FC50F02D2FB0E100E57914 /* CTEventDatabase+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTEventDatabase+Tests.h"; sourceTree = ""; }; 4987C663251B5E79003E6BE8 /* CTImageInAppViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTImageInAppViewController.h; sourceTree = ""; }; 4987C664251B5E79003E6BE8 /* CTImageInAppViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTImageInAppViewController.m; sourceTree = ""; }; 4987C667251B5F9E003E6BE8 /* CTImageInAppViewControllerPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTImageInAppViewControllerPrivate.h; sourceTree = ""; }; 49E2B18124178E7400AD704B /* CleverTapConfigValue.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CleverTapConfigValue.m; sourceTree = ""; }; + 4E07214D2D09738F000DE9C6 /* CTLocalDataStore+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTLocalDataStore+Tests.h"; sourceTree = ""; }; + 4E0721502D0974B6000DE9C6 /* CTLocalDataStore+Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "CTLocalDataStore+Tests.m"; sourceTree = ""; }; 4E1F154E27691CA0009387AE /* CleverTapInstanceTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CleverTapInstanceTests.m; sourceTree = ""; }; 4E1F155027692042009387AE /* EventTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EventTests.m; sourceTree = ""; }; 4E1F1559276B662B009387AE /* EventDetail.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EventDetail.h; sourceTree = ""; }; @@ -915,9 +931,9 @@ 6B32A0B12B9F2A75009ADC57 /* CTCustomTemplatesManager+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTCustomTemplatesManager+Tests.h"; sourceTree = ""; }; 6B32A0B22B9F2E8F009ADC57 /* CTTestTemplateProducer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTTestTemplateProducer.h; sourceTree = ""; }; 6B32A0B32B9F2E8F009ADC57 /* CTTestTemplateProducer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTTestTemplateProducer.m; sourceTree = ""; }; - 6B453EFD2CF74BE2003C7A89 /* CTEventAdapterTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTEventAdapterTest.m; sourceTree = ""; }; 6B453EF72CF621E3003C7A89 /* CTInAppDisplayViewControllerMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTInAppDisplayViewControllerMock.h; sourceTree = ""; }; 6B453EF82CF621E3003C7A89 /* CTInAppDisplayViewControllerMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTInAppDisplayViewControllerMock.m; sourceTree = ""; }; + 6B453EFD2CF74BE2003C7A89 /* CTEventAdapterTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTEventAdapterTest.m; sourceTree = ""; }; 6B4A0F902B45EF6D00A42C6D /* CTInAppTriggerManagerTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTInAppTriggerManagerTest.m; sourceTree = ""; }; 6B535FB42AD56C60002A2663 /* CTMultiDelegateManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTMultiDelegateManager.h; sourceTree = ""; }; 6B535FB52AD56C60002A2663 /* CTMultiDelegateManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTMultiDelegateManager.m; sourceTree = ""; }; @@ -1314,6 +1330,24 @@ name = Frameworks; sourceTree = ""; }; + 4847D16B2CCB9018008DC327 /* EventDatabase */ = { + isa = PBXGroup; + children = ( + 4847D16C2CCB90E0008DC327 /* CTEventDatabase.h */, + 4847D16E2CCB90F5008DC327 /* CTEventDatabase.m */, + ); + path = EventDatabase; + sourceTree = ""; + }; + 4847D1702CDA1788008DC327 /* EventDatabase */ = { + isa = PBXGroup; + children = ( + 4847D1712CDA17C2008DC327 /* CTEventDatabaseTests.m */, + 48FC50F02D2FB0E100E57914 /* CTEventDatabase+Tests.h */, + ); + path = EventDatabase; + sourceTree = ""; + }; 495EA4BE25554718006CADFF /* resources */ = { isa = PBXGroup; children = ( @@ -1590,6 +1624,7 @@ D02AC2D9276044F70031C1BE /* CleverTapSDKTests */ = { isa = PBXGroup; children = ( + 4847D1702CDA1788008DC327 /* EventDatabase */, 6B9E95AD2C285F2F0002D557 /* FileDownload */, 4E2CF1432AC56D8F00441E8B /* CTEncryptionTests.m */, 6A4427C32AA6513C0098866F /* InApps */, @@ -1623,6 +1658,8 @@ 6BD851C82B45CD1800FA5298 /* CTMultiDelegateManager+Tests.h */, 0B5564552C25946C00B87284 /* CTUserInfoMigratorTest.m */, 0B995A492C36AEDC00AF6006 /* CTLocalDataStoreTests.m */, + 4E07214D2D09738F000DE9C6 /* CTLocalDataStore+Tests.h */, + 4E0721502D0974B6000DE9C6 /* CTLocalDataStore+Tests.m */, 4E8739772C921D9000FDFDFD /* CTDomainFactoryTests.m */, 4E87397C2C9223B300FDFDFD /* CTDomainFactory+Tests.h */, ); @@ -1717,6 +1754,7 @@ D0C7BBBF207D82C0001345EF /* CleverTapSDK */ = { isa = PBXGroup; children = ( + 4847D16B2CCB9018008DC327 /* EventDatabase */, 6B9E95B62C2AE6740002D557 /* FileDownload */, 3242D7DA2B1DDA2E00A5E37A /* PrivacyInfo.xcprivacy */, 4803951A2A7ABAD200C4D254 /* CTAES.h */, @@ -1920,6 +1958,7 @@ 4E25E3D22788889F0008C888 /* CTLoginInfoProvider.h in Headers */, D0BD75AF241769E40006EE55 /* CleverTap+ProductConfig.h in Headers */, D014B8F020E2FAB1001E0780 /* CTPlistInfo.h in Headers */, + 48DC69B42D3FDF1F000E7BE4 /* CTEventDatabase.h in Headers */, 6B535FB72AD56C60002A2663 /* CTMultiDelegateManager.h in Headers */, 4E8B81702AD2BB8A00714BB4 /* CTDispatchQueueManager.h in Headers */, D014B8EC20E2FA9D001E0780 /* CleverTapTrackedViewController.h in Headers */, @@ -1951,6 +1990,7 @@ D014B8E120E2F9E9001E0780 /* CleverTap+SSLPinning.h in Headers */, D014B91C20E2FBD1001E0780 /* CTCertificatePinning.h in Headers */, 4E4E17842B500079009E2F1E /* CTAES.h in Headers */, + 48DC69B62D3FDF35000E7BE4 /* CTNotificationAction.h in Headers */, D014B90420E2FB59001E0780 /* CTValidator.h in Headers */, 4E8B81692AD2ADAE00714BB4 /* CTSwizzleManager.h in Headers */, 4E6383D8296DE9A8001E83E3 /* CTRequestSender.h in Headers */, @@ -1959,6 +1999,7 @@ D014B8F420E2FACA001E0780 /* CTSwizzle.h in Headers */, 4EF0D5462AD84BCA0044C48F /* CTSessionManager.h in Headers */, 6A775C3429BE78C7007790E0 /* CTVariables.h in Headers */, + 48DC69B82D3FDF4D000E7BE4 /* CTCustomTemplateInAppData.h in Headers */, D014B8E620E2FA64001E0780 /* CleverTapInstanceConfig.h in Headers */, 6BEEC2C72AE9B02100BD4EC5 /* CTSystemClock.h in Headers */, 4E41FD99294F46510001FBED /* CTVarCache.h in Headers */, @@ -1983,6 +2024,7 @@ 4E25E3C1278887A70008C888 /* CTIdentityRepo.h in Headers */, 071EB4F3217F6427008F0FAB /* CTInAppNotification.h in Headers */, F9356ED42487FE4600B4F507 /* CleverTapPushNotificationDelegate.h in Headers */, + 4847D16D2CCB90E0008DC327 /* CTEventDatabase.h in Headers */, 07B9453B219EA34300D4C542 /* CTInboxSimpleMessageCell.h in Headers */, 6BF5A5982ACEE22100CDED20 /* CleverTapJSInterfacePrivate.h in Headers */, 07053B7221E653E70085B44A /* UIView+CTToast.h in Headers */, @@ -2468,6 +2510,7 @@ 4E6383DA296DE9A8001E83E3 /* CTRequestSender.m in Sources */, D014B8ED20E2FAA2001E0780 /* CleverTapTrackedViewController.m in Sources */, 4E5A02DF2A4C5FD800DE242A /* LeanplumCT.m in Sources */, + 48DC69B92D3FDF55000E7BE4 /* CTCustomTemplateInAppData.m in Sources */, 6BD334ED2AF2A41F0099E33E /* CTBatchSentDelegateHelper.m in Sources */, D014B8F120E2FAB9001E0780 /* CTPlistInfo.m in Sources */, 4E25E3D12788889F0008C888 /* CTLoginInfoProvider.m in Sources */, @@ -2510,6 +2553,8 @@ D014B8FF20E2FB40001E0780 /* CTLogger.m in Sources */, 6B535FB92AD56C60002A2663 /* CTMultiDelegateManager.m in Sources */, 6BD334F62AF7FC660099E33E /* CTTriggerRadius.m in Sources */, + 48DC69B52D3FDF23000E7BE4 /* CTEventDatabase.m in Sources */, + 48DC69B72D3FDF39000E7BE4 /* CTNotificationAction.m in Sources */, D014B8FD20E2FB18001E0780 /* CTPreferences.m in Sources */, 4E49AE56275D24570074A774 /* CTValidationResultStack.m in Sources */, 6BAFFEAA2C45244100654CAF /* CTFileDownloader.m in Sources */, @@ -2524,6 +2569,7 @@ files = ( 6BA3B2E12B05411C004E834B /* InAppHelper.m in Sources */, 32394C2529FA272600956058 /* CTValidatorTest.m in Sources */, + 4847D1722CDA17C2008DC327 /* CTEventDatabaseTests.m in Sources */, 6BB778CE2BEE48C300A41628 /* CTCustomTemplateInAppDataTest.m in Sources */, 6BA3B2E82B07E207004E834B /* CTTriggersMatcher+Tests.m in Sources */, 6BBF05CE2C58E3FB0047E3D9 /* NSURLSessionMock.m in Sources */, @@ -2579,6 +2625,7 @@ 4E1F155227692A11009387AE /* CleverTap+Tests.m in Sources */, 6B9E95AC2C27164B0002D557 /* CTFileDownloadTestHelper.m in Sources */, 6A4427CD2AB8C3B10098866F /* CTInAppEvaluationManagerTest.m in Sources */, + 4E0721512D0974B6000DE9C6 /* CTLocalDataStore+Tests.m in Sources */, 6B9E95B12C2864EC0002D557 /* CTFileDownloaderMock.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2718,6 +2765,7 @@ 07B94540219EA34300D4C542 /* CTInboxController.m in Sources */, 4E25E3C4278887A70008C888 /* CTLegacyIdentityRepo.m in Sources */, 078C63AA22420321001FDDB8 /* CleverTapJSInterface.m in Sources */, + 4847D16F2CCB90F5008DC327 /* CTEventDatabase.m in Sources */, 4E49AE55275D24570074A774 /* CTValidationResultStack.m in Sources */, D01A0895207EC2D400423D6F /* CleverTapInstanceConfig.m in Sources */, 071EB4C8217F6427008F0FAB /* CTDismissButton.m in Sources */, diff --git a/CleverTapSDK/CTConstants.h b/CleverTapSDK/CTConstants.h index f2a2eba4..fff188bc 100644 --- a/CleverTapSDK/CTConstants.h +++ b/CleverTapSDK/CTConstants.h @@ -277,3 +277,8 @@ extern NSString *CLTAP_PROFILE_IDENTITY_KEY; #define CLTAP_ENCRYPTION_LEVEL @"CleverTapEncryptionLevel" #define CLTAP_ENCRYPTION_IV @"__CL3>3Rt#P__1V_" #define CLTAP_ENCRYPTION_PII_DATA (@[@"Identity", @"Email", @"Phone", @"Name"]); + +#pragma mark Constants for Event Database +#define CLTAP_DATABASE_VERSION 1 +#define CLTAP_EVENT_DB_MAX_ROW_LIMIT (2048 + 256) * 5; +#define CLTAP_EVENT_DB_ROWS_TO_CLEANUP 2048 + 256; diff --git a/CleverTapSDK/CTDispatchQueueManager.h b/CleverTapSDK/CTDispatchQueueManager.h index 7ba0f81f..5466ff05 100644 --- a/CleverTapSDK/CTDispatchQueueManager.h +++ b/CleverTapSDK/CTDispatchQueueManager.h @@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype _Nonnull)initWithConfig:(CleverTapInstanceConfig*)config; - (void)runSerialAsync:(void (^)(void))taskBlock; - (void)runOnNotificationQueue:(void (^)(void))taskBlock; +- (BOOL)inSerialQueue; @end diff --git a/CleverTapSDK/CTLocalDataStore.h b/CleverTapSDK/CTLocalDataStore.h index 2bbd9c02..69c904ef 100644 --- a/CleverTapSDK/CTLocalDataStore.h +++ b/CleverTapSDK/CTLocalDataStore.h @@ -7,8 +7,10 @@ @interface CTLocalDataStore : NSObject - -- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config profileValues:(NSDictionary*)profileValues andDeviceInfo:(CTDeviceInfo*)deviceInfo dispatchQueueManager:(CTDispatchQueueManager*)dispatchQueueManager; +- (instancetype)initWithConfig:(CleverTapInstanceConfig *)config + profileValues:(NSDictionary *)profileValues + andDeviceInfo:(CTDeviceInfo *)deviceInfo + dispatchQueueManager:(CTDispatchQueueManager *)dispatchQueueManager; - (void)persistEvent:(NSDictionary *)event; @@ -16,15 +18,15 @@ - (NSDictionary*)syncWithRemoteData:(NSDictionary *)responseData; -- (NSTimeInterval)getFirstTimeForEvent:(NSString *)event; +- (NSTimeInterval)getFirstTimeForEvent:(NSString *)event __attribute__((deprecated("Deprecated as of version 7.1.0, use readUserEventLog instead"))); -- (NSTimeInterval)getLastTimeForEvent:(NSString *)event; +- (NSTimeInterval)getLastTimeForEvent:(NSString *)event __attribute__((deprecated("Deprecated as of version 7.1.0, use readUserEventLog instead"))); -- (int)getOccurrencesForEvent:(NSString *)event; +- (int)getOccurrencesForEvent:(NSString *)event __attribute__((deprecated("Deprecated as of version 7.1.0, use readUserEventLogCount instead"))); -- (NSDictionary *)getEventHistory; +- (NSDictionary *)getEventHistory __attribute__((deprecated("Deprecated as of version 7.1.0, use readUserEventLogs instead"))); -- (CleverTapEventDetail *)getEventDetail:(NSString *)event; +- (CleverTapEventDetail *)getEventDetail:(NSString *)event __attribute__((deprecated("Deprecated as of version 7.1.0, use readUserEventLog instead"))); - (void)setProfileFields:(NSDictionary *)fields; @@ -44,4 +46,12 @@ - (void)changeUser; +- (BOOL)isEventLoggedFirstTime:(NSString*)eventName; + +- (int)readUserEventLogCount:(NSString *)eventName; + +- (CleverTapEventDetail *)readUserEventLog:(NSString *)eventName; + +- (NSDictionary *)readUserEventLogs; + @end diff --git a/CleverTapSDK/CTLocalDataStore.m b/CleverTapSDK/CTLocalDataStore.m index e77e3482..21c530b3 100644 --- a/CleverTapSDK/CTLocalDataStore.m +++ b/CleverTapSDK/CTLocalDataStore.m @@ -15,6 +15,7 @@ #import "CTDispatchQueueManager.h" #import "CTMultiDelegateManager.h" #import "CTProfileBuilder.h" +#import "CTEventDatabase.h" static const void *const kProfileBackgroundQueueKey = &kProfileBackgroundQueueKey; static const double kProfilePersistenceIntervalSeconds = 30.f; @@ -34,6 +35,9 @@ @interface CTLocalDataStore() { @property (nonatomic, strong) CTDeviceInfo *deviceInfo; @property (nonatomic, strong) NSArray *piiKeys; @property (nonatomic, strong) CTDispatchQueueManager *dispatchQueueManager; +@property (nonatomic, strong) NSMutableSet *userEventLogs; +@property (nonatomic, strong) CTEventDatabase *dbHelper; +@property (nonatomic, strong) NSArray *systemProfileKeys; @end @@ -44,11 +48,14 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig *)config profileValues:( _config = config; _deviceInfo = deviceInfo; self.dispatchQueueManager = dispatchQueueManager; + self.userEventLogs = [NSMutableSet set]; + self.dbHelper = [CTEventDatabase sharedInstanceWithDispatchQueueManager:dispatchQueueManager]; localProfileUpdateExpiryStore = [NSMutableDictionary new]; _backgroundQueue = dispatch_queue_create([[NSString stringWithFormat:@"com.clevertap.profileBackgroundQueue:%@", _config.accountId] UTF8String], DISPATCH_QUEUE_SERIAL); dispatch_queue_set_specific(_backgroundQueue, kProfileBackgroundQueueKey, (__bridge void *)self, NULL); lastProfilePersistenceTime = 0; _piiKeys = CLTAP_ENCRYPTION_PII_DATA; + _systemProfileKeys = @[CLTAP_SYS_CARRIER, CLTAP_SYS_CC, CLTAP_SYS_TZ]; [self runOnBackgroundQueue:^{ @synchronized (self->localProfileForSession) { // migrate to new persisted ct-accid-guid-userprofile @@ -63,6 +70,7 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig *)config profileValues:( } return self; } + - (void)addObservers { NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; @@ -102,6 +110,9 @@ - (void)changeUser { self->localProfileForSession = [self _inflateLocalProfile]; } }]; + @synchronized (self.userEventLogs) { + [self.userEventLogs removeAllObjects]; + } [self clearStoredEvents]; } @@ -149,7 +160,7 @@ - (void)addDataSyncFlag:(NSMutableDictionary *)event { # pragma mark - Events -- (NSDictionary *)getStoredEvents { +- (NSDictionary *)getStoredEvents __attribute__((deprecated("This method is deprecated in favor of the newer CTEventDatabase methods"))) { NSDictionary *events = [CTPreferences getObjectForKey:[self storageKeyWithSuffix:kWR_KEY_EVENTS]]; if (self.config.isDefaultInstance) { if (!events) { @@ -163,11 +174,11 @@ - (NSDictionary *)getStoredEvents { return events; } -- (void)setStoredEvents:(NSDictionary *)store { +- (void)setStoredEvents:(NSDictionary *)store __attribute__((deprecated("This method is deprecated in favor of the newer CTEventDatabase methods"))) { [CTPreferences putObject:store forKey:[self storageKeyWithSuffix:kWR_KEY_EVENTS]]; } -- (void)clearStoredEvents { +- (void)clearStoredEvents __attribute__((deprecated("This method is deprecated in favor of the newer CTEventDatabase methods"))) { [CTPreferences removeObjectForKey:[self storageKeyWithSuffix:kWR_KEY_EVENTS]]; } @@ -182,27 +193,10 @@ - (void)clearStoredEvents { */ - (void)persistEvent:(NSDictionary *)event { if (!event || !event[CLTAP_EVENT_NAME]) return; - [self runOnBackgroundQueue:^{ - NSString *eventName = event[CLTAP_EVENT_NAME]; - NSDictionary *s = [self getStoredEvents]; - if (!s) s = @{}; - NSTimeInterval now = [[[NSDate alloc] init] timeIntervalSince1970]; - NSArray *ed = s[eventName]; - if (!ed || ed.count < 3) { - // This event has been recorded for the very first time - // Set the count to 0, first and last to now - // Count will be incremented soon after this block - ed = @[@0.0f, @(now), @(now)]; - } - NSMutableArray *ped = [ed mutableCopy]; - double currentCount = ((NSNumber *) ped[0]).doubleValue; - currentCount++; - ped[0] = @(currentCount); - ped[2] = @(now); - NSMutableDictionary *store = [s mutableCopy]; - store[eventName] = ped; - [self setStoredEvents:store]; - }]; + + NSString *eventName = event[CLTAP_EVENT_NAME]; + NSString *normalizedEventName = [CTUtils getNormalizedName:eventName]; + [self.dbHelper upsertEvent:eventName normalizedEventName:normalizedEventName deviceID:self.deviceInfo.deviceId]; } /*! @@ -610,6 +604,43 @@ - (void)removeProfileFieldsWithKeys:(NSArray *)keys { [self removeProfileFieldsWithKeys:keys fromUpstream:NO]; } +- (BOOL)isEventLoggedFirstTime:(NSString*)eventName { + NSString *normalizedName = [CTUtils getNormalizedName:eventName]; + @synchronized (self.userEventLogs) { + if ([self.userEventLogs containsObject:normalizedName]) { + return NO; + } + } + NSInteger count = [self.dbHelper getEventCount:normalizedName deviceID:self.deviceInfo.deviceId]; + if (count > 1) { + @synchronized (self.userEventLogs) { + [self.userEventLogs addObject:normalizedName]; + } + } + return count == 1; +} + +#pragma mark - Public APIs for Event log + +- (int)readUserEventLogCount:(NSString *)eventName { + NSString *normalizedEventName = [CTUtils getNormalizedName:eventName]; + return (int) [self.dbHelper getEventCount:normalizedEventName deviceID:self.deviceInfo.deviceId]; +} + +- (CleverTapEventDetail *)readUserEventLog:(NSString *)eventName { + NSString *normalizedEventName = [CTUtils getNormalizedName:eventName]; + return [self.dbHelper getEventDetail:normalizedEventName deviceID:self.deviceInfo.deviceId]; +} + +- (NSDictionary *)readUserEventLogs { + NSArray *allEvents = [self.dbHelper getAllEventsForDeviceID:self.deviceInfo.deviceId]; + NSMutableDictionary *history = [[NSMutableDictionary alloc] init]; + for (CleverTapEventDetail *event in allEvents) { + history[event.eventName] = event; + } + return history; +} + #pragma mark - Private Local Profile Getters and Setters and disk persistence handling @@ -643,6 +674,14 @@ - (void)_setProfileValue:(id)value forKey:(NSString *)key fromUpstream:(BOOL)fro if (!fromUpstream) { [self updateLocalProfileUpdateExpiryTimeForKey:key]; } + + // PERSIST PROFILE KEY AS EVENT + if (![_systemProfileKeys containsObject:key]) { + NSDictionary *profileEvent = @{CLTAP_EVENT_NAME: key}; + [self.dispatchQueueManager runSerialAsync:^{ + [self persistEvent:profileEvent]; + }]; + } } @catch (NSException *exception) { CleverTapLogInternal(self.config.logLevel, @"%@: Exception setting profile field %@ in session cache for value %@", self, key, value); diff --git a/CleverTapSDK/CleverTap.h b/CleverTapSDK/CleverTap.h index 3ea4e882..d2c60f9b 100644 --- a/CleverTapSDK/CleverTap.h +++ b/CleverTapSDK/CleverTap.h @@ -801,7 +801,7 @@ extern NSString * _Nonnull const CleverTapGeofencesDidUpdateNotification; @param event event name */ -- (NSTimeInterval)eventGetFirstTime:(NSString *_Nonnull)event; +- (NSTimeInterval)eventGetFirstTime:(NSString *_Nonnull)event __attribute__((deprecated("Deprecated as of version 7.1.0, use getUserEventLog instead"))); /*! @method @@ -813,7 +813,7 @@ extern NSString * _Nonnull const CleverTapGeofencesDidUpdateNotification; @param event event name */ -- (NSTimeInterval)eventGetLastTime:(NSString *_Nonnull)event; +- (NSTimeInterval)eventGetLastTime:(NSString *_Nonnull)event __attribute__((deprecated("Deprecated as of version 7.1.0, use getUserEventLog instead"))); /*! @method @@ -824,7 +824,7 @@ extern NSString * _Nonnull const CleverTapGeofencesDidUpdateNotification; @param event event name */ -- (int)eventGetOccurrences:(NSString *_Nonnull)event; +- (int)eventGetOccurrences:(NSString *_Nonnull)event __attribute__((deprecated("Deprecated as of version 7.1.0, use getUserEventLogCount instead"))); /*! @method @@ -838,7 +838,7 @@ extern NSString * _Nonnull const CleverTapGeofencesDidUpdateNotification; Be sure to call enablePersonalization (typically once at app launch) prior to using this method. */ -- (NSDictionary *_Nullable)userGetEventHistory; +- (NSDictionary *_Nullable)userGetEventHistory __attribute__((deprecated("Deprecated as of version 7.1.0, use getUserEventLogHistory instead"))); /*! @method @@ -853,7 +853,48 @@ extern NSString * _Nonnull const CleverTapGeofencesDidUpdateNotification; @param event event name */ -- (CleverTapEventDetail *_Nullable)eventGetDetail:(NSString *_Nullable)event; +- (CleverTapEventDetail *_Nullable)eventGetDetail:(NSString *_Nullable)event __attribute__((deprecated("Deprecated as of version 7.1.0, use getUserEventLog instead"))); + +/*! + @method + + @abstract + Get the the count of logged events for a specific event name associated with the current user. + This operation involves a database query and should be called from a background thread. + Be sure to call enablePersonalization prior to invoking this method. + + @param eventName event name + */ +- (int)getUserEventLogCount:(NSString *_Nonnull)eventName; + +/*! + @method + + @abstract + Get the details for the event. + + @discussion + Returns a CleverTapEventDetail object (eventName, normalizedEventName, firstTime, lastTime, count, deviceID) + This operation involves a database query and should be called from a background thread. + Be sure to call enablePersonalization (typically once at app launch) prior to using this method. + + @param eventName event name + */ +- (CleverTapEventDetail *_Nullable)getUserEventLog:(NSString *_Nullable)eventName; + +/*! + @method + + @abstract + Get the user's event history. + + @discussion + Returns a dictionary of CleverTapEventDetail objects (eventName, normalizedEventName, firstTime, lastTime, count, deviceID), keyed by eventName. + This operation involves a database query and should be called from a background thread. + Be sure to call enablePersonalization (typically once at app launch) prior to using this method. + + */ +- (NSDictionary *_Nullable)getUserEventLogHistory; #pragma mark Session API @@ -889,7 +930,17 @@ extern NSString * _Nonnull const CleverTapGeofencesDidUpdateNotification; Be sure to call enablePersonalization (typically once at app launch) prior to using this method. */ -- (int)userGetTotalVisits; +- (int)userGetTotalVisits __attribute__((deprecated("Deprecated as of version 7.1.0, use getUserAppLaunchCount instead"))); + +/*! + @method + + @abstract + Get the total number of visits by this user. + This operation involves a database query and should be called from a background thread. + Be sure to call enablePersonalization (typically once at app launch) prior to using this method. + */ +- (int)getUserAppLaunchCount; /*! @method @@ -909,7 +960,17 @@ extern NSString * _Nonnull const CleverTapGeofencesDidUpdateNotification; Be sure to call enablePersonalization (typically once at app launch) prior to using this method. */ -- (NSTimeInterval)userGetPreviousVisitTime; +- (NSTimeInterval)userGetPreviousVisitTime __attribute__((deprecated("Deprecated as of version 7.1.0, use getUserLastVisitTs instead"))); + +/*! + @method + + @abstract + Get the last prior visit time for this user. + Be sure to call enablePersonalization (typically once at app launch) prior to using this method. + + */ +- (NSTimeInterval)getUserLastVisitTs; /* ------------------------------------------------------------------------------------------------------ * Synchronization diff --git a/CleverTapSDK/CleverTap.m b/CleverTapSDK/CleverTap.m index 064e8b47..30e0cd21 100644 --- a/CleverTapSDK/CleverTap.m +++ b/CleverTapSDK/CleverTap.m @@ -195,6 +195,7 @@ @interface CleverTap () { @property (nonatomic, strong, readwrite) CleverTapInstanceConfig *config; @property (nonatomic, assign) NSTimeInterval lastAppLaunchedTime; +@property (nonatomic, assign) NSTimeInterval userLastVisitTs; @property (nonatomic, strong) CTDeviceInfo *deviceInfo; @property (nonatomic, strong) CTLocalDataStore *localDataStore; @property (nonatomic, strong) CTDispatchQueueManager *dispatchQueueManager; @@ -472,7 +473,9 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig*)config andCleverTapID:( _localDataStore = [[CTLocalDataStore alloc] initWithConfig:_config profileValues:initialProfileValues andDeviceInfo: _deviceInfo dispatchQueueManager:_dispatchQueueManager]; - _lastAppLaunchedTime = [self eventGetLastTime:@"App Launched"]; + _lastAppLaunchedTime = [self eventGetLastTime:CLTAP_APP_LAUNCHED_EVENT]; + CleverTapEventDetail *eventDetails = [self getUserEventLog:CLTAP_APP_LAUNCHED_EVENT]; + _userLastVisitTs = eventDetails ? eventDetails.lastTime : -1; self.validationResultStack = [[CTValidationResultStack alloc]initWithConfig: _config]; self.userSetLocation = kCLLocationCoordinate2DInvalid; @@ -535,7 +538,7 @@ - (void)initializeInAppSupport { templatesManager:templatesManager fileDownloader:self.fileDownloader]; - CTInAppEvaluationManager *evaluationManager = [[CTInAppEvaluationManager alloc] initWithAccountId:self.config.accountId deviceId:self.deviceInfo.deviceId delegateManager:self.delegateManager impressionManager:impressionManager inAppDisplayManager:displayManager inAppStore:inAppStore inAppTriggerManager:triggerManager]; + CTInAppEvaluationManager *evaluationManager = [[CTInAppEvaluationManager alloc] initWithAccountId:self.config.accountId deviceId:self.deviceInfo.deviceId delegateManager:self.delegateManager impressionManager:impressionManager inAppDisplayManager:displayManager inAppStore:inAppStore inAppTriggerManager:triggerManager localDataStore:self.localDataStore]; self.customTemplatesManager = templatesManager; self.inAppFCManager = inAppFCManager; @@ -1957,7 +1960,9 @@ - (void)processEvent:(NSDictionary *)event withType:(CleverTapEventType)eventTyp } if (eventType == CleverTapEventTypeRaised || eventType == CleverTapEventTypeNotificationViewed) { - [self.localDataStore persistEvent:mutableEvent]; + [self.dispatchQueueManager runSerialAsync:^{ + [self.localDataStore persistEvent:mutableEvent]; + }]; } if (eventType == CleverTapEventTypeProfile) { @@ -3105,6 +3110,36 @@ - (CleverTapEventDetail *)eventGetDetail:(NSString *)event { return [self.localDataStore getEventDetail:event]; } +#pragma mark - User Event Log Methods + +- (int)getUserEventLogCount:(NSString *)eventName { + if (!self.config.enablePersonalization) { + return -1; + } + return [self.localDataStore readUserEventLogCount:eventName]; +} + +- (CleverTapEventDetail *)getUserEventLog:(NSString *)eventName { + if (!self.config.enablePersonalization) { + return nil; + } + return [self.localDataStore readUserEventLog:eventName]; +} + +- (NSDictionary *)getUserEventLogHistory { + if (!self.config.enablePersonalization) { + return nil; + } + return [self.localDataStore readUserEventLogs]; +} + +- (int)getUserAppLaunchCount { + return [self getUserEventLogCount:CLTAP_APP_LAUNCHED_EVENT]; +} + +- (NSTimeInterval)getUserLastVisitTs { + return self.userLastVisitTs; +} #pragma mark - Session API @@ -3122,7 +3157,7 @@ - (CleverTapUTMDetail *)sessionGetUTMDetails { } - (int)userGetTotalVisits { - return [self eventGetOccurrences:@"App Launched"]; + return [self eventGetOccurrences:CLTAP_APP_LAUNCHED_EVENT]; } - (int)userGetScreenCount { diff --git a/CleverTapSDK/CleverTapBuildInfo.h b/CleverTapSDK/CleverTapBuildInfo.h index 211764c8..8cf0002a 100644 --- a/CleverTapSDK/CleverTapBuildInfo.h +++ b/CleverTapSDK/CleverTapBuildInfo.h @@ -1 +1 @@ -#define WR_SDK_REVISION @"70003" +#define WR_SDK_REVISION @"71000" diff --git a/CleverTapSDK/CleverTapEventDetail.h b/CleverTapSDK/CleverTapEventDetail.h index e211c743..830595ee 100644 --- a/CleverTapSDK/CleverTapEventDetail.h +++ b/CleverTapSDK/CleverTapEventDetail.h @@ -3,8 +3,10 @@ @interface CleverTapEventDetail : NSObject @property (nonatomic, strong) NSString *eventName; +@property (nonatomic, strong) NSString *normalizedEventName; @property (nonatomic) NSTimeInterval firstTime; @property (nonatomic) NSTimeInterval lastTime; @property (nonatomic) NSUInteger count; +@property (nonatomic, strong) NSString *deviceID; @end diff --git a/CleverTapSDK/CleverTapEventDetail.m b/CleverTapSDK/CleverTapEventDetail.m index f70844d1..90f9356d 100644 --- a/CleverTapSDK/CleverTapEventDetail.m +++ b/CleverTapSDK/CleverTapEventDetail.m @@ -3,8 +3,8 @@ @implementation CleverTapEventDetail - (NSString*) description { - return [NSString stringWithFormat:@"CleverTapEventDetail (event name = %@; first time = %d, last time = %d; count = %lu)", - self.eventName, (int) self.firstTime, (int) self.lastTime, (unsigned long)self.count]; + return [NSString stringWithFormat:@"CleverTapEventDetail (event name = %@; normalized event name = %@; first time = %d, last time = %d; count = %lu; device ID = %@)", + self.eventName, self.normalizedEventName, (int) self.firstTime, (int) self.lastTime, (unsigned long)self.count, self.deviceID]; } @end diff --git a/CleverTapSDK/EventDatabase/CTEventDatabase.h b/CleverTapSDK/EventDatabase/CTEventDatabase.h new file mode 100644 index 00000000..c45812c9 --- /dev/null +++ b/CleverTapSDK/EventDatabase/CTEventDatabase.h @@ -0,0 +1,52 @@ +// +// CTEventDatabase.h +// CleverTapSDK +// +// Created by Nishant Kumar on 25/10/24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import +#import "CleverTapEventDetail.h" +#import "CTClock.h" +#import "CTDispatchQueueManager.h" + +@interface CTEventDatabase : NSObject + ++ (instancetype)sharedInstanceWithDispatchQueueManager:(CTDispatchQueueManager*)dispatchQueueManager; + +- (void)databaseVersionWithCompletion:(void (^)(NSInteger version))completion; + +- (void)insertEvent:(NSString *)eventName +normalizedEventName:(NSString *)normalizedEventName + deviceID:(NSString *)deviceID + completion:(void (^)(BOOL success))completion; + +- (void)updateEvent:(NSString *)normalizedEventName + forDeviceID:(NSString *)deviceID + completion:(void (^)(BOOL success))completion; + +- (void)upsertEvent:(NSString *)eventName +normalizedEventName:(NSString *)normalizedEventName + deviceID:(NSString *)deviceID; + +- (void)eventExists:(NSString *)normalizedEventName + forDeviceID:(NSString *)deviceID + completion:(void (^)(BOOL exists))completion; + +- (NSInteger)getEventCount:(NSString *)normalizedEventName + deviceID:(NSString *)deviceID; + +- (CleverTapEventDetail *)getEventDetail:(NSString *)normalizedEventName + deviceID:(NSString *)deviceID; + +- (NSArray *)getAllEventsForDeviceID:(NSString *)deviceID; + +- (void)deleteAllRowsWithCompletion:(void (^)(BOOL success))completion; + +- (void)deleteLeastRecentlyUsedRows:(NSInteger)maxRowLimit + numberOfRowsToCleanup:(NSInteger)numberOfRowsToCleanup + completion:(void (^)(BOOL success))completion; + +@end diff --git a/CleverTapSDK/EventDatabase/CTEventDatabase.m b/CleverTapSDK/EventDatabase/CTEventDatabase.m new file mode 100644 index 00000000..0c6a19de --- /dev/null +++ b/CleverTapSDK/EventDatabase/CTEventDatabase.m @@ -0,0 +1,583 @@ +// +// CTEventDatabase.m +// CleverTapSDK +// +// Created by Nishant Kumar on 25/10/24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTEventDatabase.h" +#import "CTConstants.h" +#import "CTSystemClock.h" + +@interface CTEventDatabase() + +@property (nonatomic, strong) CTDispatchQueueManager *dispatchQueueManager; +@property (nonatomic, strong) id clock; + +@end + +@implementation CTEventDatabase { + sqlite3 *_eventDatabase; +} + ++ (instancetype)sharedInstanceWithDispatchQueueManager:(CTDispatchQueueManager*)dispatchQueueManager { + static CTEventDatabase *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] initWithDispatchQueueManager:dispatchQueueManager + clock:[[CTSystemClock alloc] init]]; + }); + return sharedInstance; +} + +- (instancetype)initWithDispatchQueueManager:(CTDispatchQueueManager*)dispatchQueueManager + clock:(id)clock { + if (self = [super init]) { + _dispatchQueueManager = dispatchQueueManager; + _clock = clock; + [self openDatabase]; + + // Perform cleanup/deletion of rows on instance creation if total row count + // exceeds mac threshold limit. + NSInteger maxRowLimit = CLTAP_EVENT_DB_MAX_ROW_LIMIT; + NSInteger numberOfRowsToCleanup = CLTAP_EVENT_DB_ROWS_TO_CLEANUP; + [self deleteLeastRecentlyUsedRows:maxRowLimit numberOfRowsToCleanup:numberOfRowsToCleanup completion:nil]; + } + return self; +} + +- (void)databaseVersionWithCompletion:(void (^)(NSInteger version))completion { + if (!_eventDatabase) { + CleverTapLogStaticInternal(@"Event database is not open, cannot execute SQL."); + return; + } + + const char *querySQL = "PRAGMA user_version;"; + __block NSInteger version = 0; + + [self.dispatchQueueManager runSerialAsync:^{ + sqlite3_stmt *statement; + if (sqlite3_prepare_v2(self->_eventDatabase, querySQL, -1, &statement, NULL) == SQLITE_OK) { + if (sqlite3_step(statement) == SQLITE_ROW) { + version = sqlite3_column_int(statement, 0); + } + sqlite3_finalize(statement); + } else { + CleverTapLogStaticInternal(@"SQL prepare query error: %s", sqlite3_errmsg(self->_eventDatabase)); + } + + if (completion) { + completion(version); + } + }]; +} + +- (void)insertEvent:(NSString *)eventName +normalizedEventName:(NSString *)normalizedEventName + deviceID:(NSString *)deviceID + completion:(void (^)(BOOL success))completion { + if (!_eventDatabase) { + CleverTapLogStaticInternal(@"Event database is not open, cannot execute SQL."); + return; + } + + __block BOOL success = NO; + // For new event, set count as 1 + NSInteger count = 1; + NSInteger currentTs = [[self.clock timeIntervalSince1970] integerValue]; + const char *insertSQL = "INSERT INTO CTUserEventLogs (eventName, normalizedEventName, count, firstTs, lastTs, deviceID) VALUES (?, ?, ?, ?, ?, ?)"; + + [self.dispatchQueueManager runSerialAsync:^{ + sqlite3_stmt *statement; + if (sqlite3_prepare_v2(self->_eventDatabase, insertSQL, -1, &statement, NULL) == SQLITE_OK) { + sqlite3_bind_text(statement, 1, [eventName UTF8String], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 2, [normalizedEventName UTF8String], -1, SQLITE_TRANSIENT); + sqlite3_bind_int(statement, 3, (int)count); + sqlite3_bind_int(statement, 4, (int)currentTs); + sqlite3_bind_int(statement, 5, (int)currentTs); + sqlite3_bind_text(statement, 6, [deviceID UTF8String], -1, SQLITE_TRANSIENT); + + int result = sqlite3_step(statement); + if (result == SQLITE_DONE) { + success = YES; + } else { + CleverTapLogStaticInternal(@"Insert Table SQL error: %s", sqlite3_errmsg(self->_eventDatabase)); + } + + sqlite3_finalize(statement); + } else { + CleverTapLogStaticInternal(@"Failed to prepare insert statement: %s", sqlite3_errmsg(self->_eventDatabase)); + } + + if (completion) { + completion(success); + } + }]; +} + +- (void)updateEvent:(NSString *)normalizedEventName + forDeviceID:(NSString *)deviceID + completion:(void (^)(BOOL success))completion { + if (!_eventDatabase) { + CleverTapLogStaticInternal(@"Event database is not open, cannot execute SQL."); + return; + } + + NSInteger currentTs = [[self.clock timeIntervalSince1970] integerValue]; + const char *updateSQL = + "UPDATE CTUserEventLogs SET count = count + 1, lastTs = ? WHERE normalizedEventName = ? AND deviceID = ?"; + __block BOOL success = NO; + + [self.dispatchQueueManager runSerialAsync:^{ + sqlite3_stmt *statement; + if (sqlite3_prepare_v2(self->_eventDatabase, updateSQL, -1, &statement, NULL) == SQLITE_OK) { + sqlite3_bind_int(statement, 1, (int)currentTs); + sqlite3_bind_text(statement, 2, [normalizedEventName UTF8String], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 3, [deviceID UTF8String], -1, SQLITE_TRANSIENT); + + int result = sqlite3_step(statement); + if (result == SQLITE_DONE) { + success = YES; + } else { + CleverTapLogStaticInternal(@"Update Table SQL error: %s", sqlite3_errmsg(self->_eventDatabase)); + } + + sqlite3_finalize(statement); + } else { + CleverTapLogStaticInternal(@"Failed to prepare update statement: %s", sqlite3_errmsg(self->_eventDatabase)); + } + + if (completion) { + completion(success); + } + }]; +} + +- (void)upsertEvent:(NSString *)eventName +normalizedEventName:(NSString *)normalizedEventName + deviceID:(NSString *)deviceID { + [self.dispatchQueueManager runSerialAsync:^{ + [self eventExists:normalizedEventName forDeviceID:deviceID completion:^(BOOL exists) { + if (!exists) { + [self insertEvent:eventName normalizedEventName:normalizedEventName deviceID:deviceID completion:nil]; + } else { + [self updateEvent:normalizedEventName forDeviceID:deviceID completion:nil]; + } + }]; + }]; +} + +- (void)eventExists:(NSString *)normalizedEventName + forDeviceID:(NSString *)deviceID + completion:(void (^)(BOOL exists))completion { + if (!_eventDatabase) { + CleverTapLogStaticInternal(@"Event database is not open, cannot execute SQL."); + return; + } + + const char *query = "SELECT COUNT(*) FROM CTUserEventLogs WHERE normalizedEventName = ? AND deviceID = ?"; + __block BOOL exists = NO; + + [self.dispatchQueueManager runSerialAsync:^{ + sqlite3_stmt *statement; + + if (sqlite3_prepare_v2(self->_eventDatabase, query, -1, &statement, NULL) == SQLITE_OK) { + sqlite3_bind_text(statement, 1, [normalizedEventName UTF8String], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 2, [deviceID UTF8String], -1, SQLITE_TRANSIENT); + + if (sqlite3_step(statement) == SQLITE_ROW) { + // Check if the count is greater than 0 + int count = sqlite3_column_int(statement, 0); + if (count > 0) { + exists = YES; + } + } else { + CleverTapLogStaticInternal(@"SQL check query error: %s", sqlite3_errmsg(self->_eventDatabase)); + } + sqlite3_finalize(statement); + } else { + CleverTapLogStaticInternal(@"SQL prepare query error: %s", sqlite3_errmsg(self->_eventDatabase)); + } + + if (completion) { + completion(exists); + } + }]; +} + +- (void)dealloc { + [self.dispatchQueueManager runSerialAsync:^{ + [self closeDatabase]; + }]; +} + +- (NSInteger)getEventCount:(NSString *)normalizedEventName + deviceID:(NSString *)deviceID { + if (!_eventDatabase) { + CleverTapLogStaticInternal(@"Event database is not open, cannot execute SQL."); + return -1; + } + + const char *querySQL = "SELECT count FROM CTUserEventLogs WHERE normalizedEventName = ? AND deviceID = ?"; + __block NSInteger count = -1; + void (^taskBlock)(void) = ^{ + sqlite3_stmt *statement; + if (sqlite3_prepare_v2(self->_eventDatabase, querySQL, -1, &statement, NULL) == SQLITE_OK) { + sqlite3_bind_text(statement, 1, [normalizedEventName UTF8String], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 2, [deviceID UTF8String], -1, SQLITE_TRANSIENT); + + if (sqlite3_step(statement) == SQLITE_ROW) { + count = sqlite3_column_int(statement, 0); + } else { + count = 0; + CleverTapLogStaticInternal(@"No event found with eventName: %@ and deviceID: %@", normalizedEventName, deviceID); + } + + sqlite3_finalize(statement); + } else { + CleverTapLogStaticInternal(@"SQL prepare query error: %s", sqlite3_errmsg(self->_eventDatabase)); + } + }; + + if ([self.dispatchQueueManager inSerialQueue]) { + // If already on the serial queue, execute directly without semaphore + taskBlock(); + } else { + // Otherwise, use semaphore for synchronous execution + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [self.dispatchQueueManager runSerialAsync:^{ + taskBlock(); + dispatch_semaphore_signal(semaphore); + }]; + if (dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 3)) != 0) { + CleverTapLogStaticInternal(@"Timeout occurred while getting event count."); + return -1; + } + } + + return count; +} + +- (CleverTapEventDetail *)getEventDetail:(NSString *)normalizedEventName + deviceID:(NSString *)deviceID { + if (!_eventDatabase) { + CleverTapLogStaticInternal(@"Event database is not open, cannot execute SQL."); + return nil; + } + + const char *querySQL = "SELECT eventName, normalizedEventName, count, firstTs, lastTs, deviceID FROM CTUserEventLogs WHERE normalizedEventName = ? AND deviceID = ?"; + __block CleverTapEventDetail *eventDetail = nil; + void (^taskBlock)(void) = ^{ + sqlite3_stmt *statement; + if (sqlite3_prepare_v2(self->_eventDatabase, querySQL, -1, &statement, NULL) == SQLITE_OK) { + sqlite3_bind_text(statement, 1, [normalizedEventName UTF8String], -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 2, [deviceID UTF8String], -1, SQLITE_TRANSIENT); + + if (sqlite3_step(statement) == SQLITE_ROW) { + const char *eventName = (const char *)sqlite3_column_text(statement, 0); + const char *normalizedEventName = (const char *)sqlite3_column_text(statement, 1); + NSInteger count = sqlite3_column_int(statement, 2); + NSInteger firstTs = sqlite3_column_int(statement, 3); + NSInteger lastTs = sqlite3_column_int(statement, 4); + const char *deviceID = (const char *)sqlite3_column_text(statement, 5); + + eventDetail = [[CleverTapEventDetail alloc] init]; + eventDetail.eventName = eventName ? [NSString stringWithUTF8String:eventName] : nil; + eventDetail.normalizedEventName = [NSString stringWithUTF8String:normalizedEventName]; + eventDetail.count = count; + eventDetail.firstTime = firstTs; + eventDetail.lastTime = lastTs; + eventDetail.deviceID = [NSString stringWithUTF8String:deviceID]; + + } else { + CleverTapLogStaticInternal(@"No event found with eventName: %@ and deviceID: %@", normalizedEventName, deviceID); + } + sqlite3_finalize(statement); + } else { + CleverTapLogStaticInternal(@"SQL prepare query error: %s", sqlite3_errmsg(self->_eventDatabase)); + } + }; + + if ([self.dispatchQueueManager inSerialQueue]) { + // If already on the serial queue, execute directly without semaphore + taskBlock(); + } else { + // Otherwise, use semaphore for synchronous execution + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [self.dispatchQueueManager runSerialAsync:^{ + taskBlock(); + dispatch_semaphore_signal(semaphore); + }]; + if (dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 3)) != 0) { + CleverTapLogStaticInternal(@"Timeout occurred while getting event detail."); + return nil; + } + } + + return eventDetail; +} + +- (NSArray *)getAllEventsForDeviceID:(NSString *)deviceID { + if (!_eventDatabase) { + CleverTapLogStaticInternal(@"Event database is not open, cannot execute SQL."); + return nil; + } + + const char *querySQL = "SELECT eventName, normalizedEventName, count, firstTs, lastTs, deviceID FROM CTUserEventLogs WHERE deviceID = ?"; + __block NSMutableArray *eventDataArray = [NSMutableArray array]; + void (^taskBlock)(void) = ^{ + sqlite3_stmt *statement; + if (sqlite3_prepare_v2(self->_eventDatabase, querySQL, -1, &statement, NULL) == SQLITE_OK) { + sqlite3_bind_text(statement, 1, [deviceID UTF8String], -1, SQLITE_TRANSIENT); + + while (sqlite3_step(statement) == SQLITE_ROW) { + const char *eventName = (const char *)sqlite3_column_text(statement, 0); + const char *normalizedEventName = (const char *)sqlite3_column_text(statement, 1); + NSInteger count = sqlite3_column_int(statement, 2); + NSInteger firstTs = sqlite3_column_int(statement, 3); + NSInteger lastTs = sqlite3_column_int(statement, 4); + const char *deviceID = (const char *)sqlite3_column_text(statement, 5); + + CleverTapEventDetail *ed = [[CleverTapEventDetail alloc] init]; + ed.count = count; + ed.firstTime = firstTs; + ed.lastTime = lastTs; + ed.eventName = eventName ? [NSString stringWithUTF8String:eventName] : nil; + ed.normalizedEventName = [NSString stringWithUTF8String:normalizedEventName]; + ed.deviceID = [NSString stringWithUTF8String:deviceID]; + + // Adding the CleverTapEventDetail to the result array + [eventDataArray addObject:ed]; + } + sqlite3_finalize(statement); + } else { + CleverTapLogStaticInternal(@"SQL prepare query error: %s", sqlite3_errmsg(self->_eventDatabase)); + } + }; + + if ([self.dispatchQueueManager inSerialQueue]) { + // If already on the serial queue, execute directly without semaphore + taskBlock(); + } else { + // Otherwise, use semaphore for synchronous execution + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [self.dispatchQueueManager runSerialAsync:^{ + taskBlock(); + dispatch_semaphore_signal(semaphore); + }]; + if (dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 3)) != 0) { + CleverTapLogStaticInternal(@"Timeout occurred while getting all event details."); + return nil; + } + } + return [eventDataArray copy]; +} + +- (void)deleteAllRowsWithCompletion:(void (^)(BOOL success))completion { + if (!_eventDatabase) { + CleverTapLogStaticInternal(@"Event database is not open, cannot execute SQL."); + return; + } + + const char *querySQL = "DELETE FROM CTUserEventLogs"; + __block BOOL success = NO; + + [self.dispatchQueueManager runSerialAsync:^{ + char *errMsg = NULL; + int result = sqlite3_exec(self->_eventDatabase, querySQL, NULL, NULL, &errMsg); + + if (result == SQLITE_OK) { + success = YES; + } else { + CleverTapLogStaticInternal(@"SQL Error deleting all rows from CTUserEventLogs: %s", errMsg); + } + + if (completion) { + completion(success); + } + }]; +} + +- (void)deleteLeastRecentlyUsedRows:(NSInteger)maxRowLimit + numberOfRowsToCleanup:(NSInteger)numberOfRowsToCleanup + completion:(void (^)(BOOL success))completion { + if (!_eventDatabase) { + CleverTapLogStaticInternal(@"Event database is not open, cannot execute SQL."); + return; + } + + __block BOOL success = NO; + + [self.dispatchQueueManager runSerialAsync:^{ + // Begin a transaction to ensure atomicity + sqlite3_exec(self->_eventDatabase, "BEGIN TRANSACTION;", NULL, NULL, NULL); + + // Create an index on the `lastTs` column if it doesn't exist which will improve performance + // while deletion when table is large + const char *createIndexSQL = "CREATE INDEX IF NOT EXISTS idx_lastTs ON CTUserEventLogs(lastTs);"; + char *errMsg = NULL; + int indexResult = sqlite3_exec(self->_eventDatabase, createIndexSQL, NULL, NULL, &errMsg); + + if (indexResult != SQLITE_OK) { + CleverTapLogStaticInternal(@"Failed to create index on lastTs: %s", errMsg); + sqlite3_free(errMsg); + sqlite3_exec(self->_eventDatabase, "ROLLBACK;", NULL, NULL, NULL); // Rollback transaction if index creation fails + return; + } + + NSString *countQuerySQL = @"SELECT COUNT(*) FROM CTUserEventLogs;"; + sqlite3_stmt *countStatement; + if (sqlite3_prepare_v2(self->_eventDatabase, [countQuerySQL UTF8String], -1, &countStatement, NULL) == SQLITE_OK) { + if (sqlite3_step(countStatement) == SQLITE_ROW) { + NSInteger currentRowCount = sqlite3_column_int(countStatement, 0); + if (currentRowCount > maxRowLimit) { + // Calculate the number of rows to delete + NSInteger rowsToDelete = currentRowCount - (maxRowLimit - numberOfRowsToCleanup); + + // Delete the least recently used rows based on lastTs + const char *deleteSQL = "DELETE FROM CTUserEventLogs WHERE (normalizedEventName, deviceID) IN (SELECT normalizedEventName, deviceID FROM CTUserEventLogs ORDER BY lastTs ASC LIMIT ?);"; + sqlite3_stmt *deleteStatement; + if (sqlite3_prepare_v2(self->_eventDatabase, deleteSQL, -1, &deleteStatement, NULL) == SQLITE_OK) { + sqlite3_bind_int(deleteStatement, 1, (int)rowsToDelete); + + int result = sqlite3_step(deleteStatement); + if (result == SQLITE_DONE) { + success = YES; + } else { + CleverTapLogStaticInternal(@"SQL Error deleting rows: %s", sqlite3_errmsg(self->_eventDatabase)); + } + + sqlite3_finalize(deleteStatement); + } else { + CleverTapLogStaticInternal(@"SQL prepare query error: %s", sqlite3_errmsg(self->_eventDatabase)); + } + } + } else { + CleverTapLogStaticInternal(@"Failed to count rows in CTUserEventLogs"); + } + sqlite3_finalize(countStatement); + } else { + CleverTapLogStaticInternal(@"SQL prepare query error: %s", sqlite3_errmsg(self->_eventDatabase)); + } + + // Commit or rollback the transaction based on success + if (success) { + sqlite3_exec(self->_eventDatabase, "COMMIT;", NULL, NULL, NULL); + } else { + sqlite3_exec(self->_eventDatabase, "ROLLBACK;", NULL, NULL, NULL); + } + + if (completion) { + completion(success); + } + }]; +} + +#pragma mark - Private methods + +- (void)openDatabase { + NSString *databasePath = [self databasePath]; + void (^taskBlock)(void) = ^{ + if (sqlite3_open_v2([databasePath UTF8String], &self->_eventDatabase, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL) == SQLITE_OK) { + // Create table, check and update the version if needed + [self createTableWithCompletion:^(BOOL exists) { + [self checkAndUpdateDatabaseVersion]; + }]; + } else { + CleverTapLogStaticInternal(@"Failed to open database - CleverTap-Events.db"); + } + }; + + if ([self.dispatchQueueManager inSerialQueue]) { + // If already on the serial queue, execute directly without semaphore + taskBlock(); + } else { + // Otherwise, use semaphore for synchronous execution + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [self.dispatchQueueManager runSerialAsync:^{ + taskBlock(); + dispatch_semaphore_signal(semaphore); + }]; + if (dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 3)) != 0) { + CleverTapLogStaticInternal(@"Timeout occurred while opening database."); + } + } +} + +- (void)createTableWithCompletion:(void (^)(BOOL success))completion { + if (!_eventDatabase) { + CleverTapLogStaticInternal(@"Event database is not open, cannot execute SQL."); + return; + } + + __block BOOL success = NO; + + [self.dispatchQueueManager runSerialAsync:^{ + char *errMsg; + const char *createTableSQL = "CREATE TABLE IF NOT EXISTS CTUserEventLogs (eventName TEXT, normalizedEventName TEXT, count INTEGER, firstTs INTEGER, lastTs INTEGER, deviceID TEXT, PRIMARY KEY (normalizedEventName, deviceID))"; + if (sqlite3_exec(self->_eventDatabase, createTableSQL, NULL, NULL, &errMsg) == SQLITE_OK) { + success = YES; + + // Set the database version to the initial version, ie 1. + [self setDatabaseVersion:CLTAP_DATABASE_VERSION]; + } else { + CleverTapLogStaticInternal(@"Create Table SQL error: %s", errMsg); + sqlite3_free(errMsg); + } + + if (completion) { + completion(success); + } + }]; +} + +- (void)closeDatabase { + if (_eventDatabase) { + sqlite3_close(_eventDatabase); + _eventDatabase = NULL; + } +} + +- (void)setDatabaseVersion:(NSInteger)version { + if (!_eventDatabase) { + CleverTapLogStaticInternal(@"Event database is not open, cannot execute SQL."); + return; + } + + NSString *updateSQL = [NSString stringWithFormat:@"PRAGMA user_version = %ld;", (long)version]; + + [self.dispatchQueueManager runSerialAsync:^{ + sqlite3_stmt *statement; + if (sqlite3_prepare_v2(self->_eventDatabase, [updateSQL UTF8String], -1, &statement, NULL) == SQLITE_OK) { + sqlite3_bind_int(statement, 1, (int)version); + + int result = sqlite3_step(statement); + if (result != SQLITE_OK) { + CleverTapLogStaticInternal(@"SQL Error: %s", sqlite3_errmsg(self->_eventDatabase)); + } + + sqlite3_finalize(statement); + } else { + CleverTapLogStaticInternal(@"Failed to prepare update statement: %s", sqlite3_errmsg(self->_eventDatabase)); + } + }]; +} + +- (void)checkAndUpdateDatabaseVersion { + [self databaseVersionWithCompletion:^(NSInteger currentVersion) { + if (currentVersion < CLTAP_DATABASE_VERSION) { + // Handle version changes here in future. + [self setDatabaseVersion:CLTAP_DATABASE_VERSION]; + CleverTapLogStaticInternal(@"Schema migration required. Current version: %ld, Target version: %ld", (long)currentVersion, (long)CLTAP_DATABASE_VERSION); + } + }]; +} + +- (NSString *)databasePath { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDirectory = [paths objectAtIndex:0]; + return [documentsDirectory stringByAppendingPathComponent:@"CleverTap-Events.db"]; +} + +@end diff --git a/CleverTapSDK/InApps/CTInAppEvaluationManager.h b/CleverTapSDK/InApps/CTInAppEvaluationManager.h index 2dbea791..847f6469 100644 --- a/CleverTapSDK/InApps/CTInAppEvaluationManager.h +++ b/CleverTapSDK/InApps/CTInAppEvaluationManager.h @@ -10,6 +10,7 @@ #import #import "CTBatchSentDelegate.h" #import "CTAttachToBatchHeaderDelegate.h" +#import "CTLocalDataStore.h" @class CTMultiDelegateManager; @class CTImpressionManager; @@ -30,7 +31,8 @@ NS_ASSUME_NONNULL_BEGIN impressionManager:(CTImpressionManager *)impressionManager inAppDisplayManager:(CTInAppDisplayManager *)inAppDisplayManager inAppStore:(CTInAppStore *)inAppStore - inAppTriggerManager:(CTInAppTriggerManager *)inAppTriggerManager; + inAppTriggerManager:(CTInAppTriggerManager *)inAppTriggerManager + localDataStore:(CTLocalDataStore *)dataStore; - (void)evaluateOnEvent:(NSString *)eventName withProps:(NSDictionary *)properties; - (void)evaluateOnChargedEvent:(NSDictionary *)chargeDetails andItems:(NSArray *)items; diff --git a/CleverTapSDK/InApps/CTInAppEvaluationManager.m b/CleverTapSDK/InApps/CTInAppEvaluationManager.m index f2896252..c19ef7ba 100644 --- a/CleverTapSDK/InApps/CTInAppEvaluationManager.m +++ b/CleverTapSDK/InApps/CTInAppEvaluationManager.m @@ -52,7 +52,8 @@ - (instancetype)initWithAccountId:(NSString *)accountId impressionManager:(CTImpressionManager *)impressionManager inAppDisplayManager:(CTInAppDisplayManager *)inAppDisplayManager inAppStore:(CTInAppStore *)inAppStore - inAppTriggerManager:(CTInAppTriggerManager *)inAppTriggerManager { + inAppTriggerManager:(CTInAppTriggerManager *)inAppTriggerManager + localDataStore:(CTLocalDataStore *)dataStore { if (self = [super init]) { self.accountId = accountId; self.deviceId = deviceId; @@ -84,7 +85,7 @@ - (instancetype)initWithAccountId:(NSString *)accountId } self.inAppStore = inAppStore; - self.triggersMatcher = [CTTriggersMatcher new]; + self.triggersMatcher = [[CTTriggersMatcher alloc] initWithDataStore:dataStore]; self.limitsMatcher = [CTLimitsMatcher new]; self.triggerManager = inAppTriggerManager; diff --git a/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.h b/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.h index 6f326107..62239c97 100644 --- a/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.h +++ b/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.h @@ -23,6 +23,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) NSInteger itemsCount; @property (nonatomic, readonly) NSInteger geoRadiusCount; @property (nonatomic, strong, readonly) NSString *profileAttrName; +@property (nonatomic, assign) BOOL firstTimeOnly; - (CTTriggerCondition * _Nullable)propertyAtIndex:(NSInteger)index; - (CTTriggerCondition * _Nullable)itemAtIndex:(NSInteger)index; diff --git a/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.m b/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.m index c6e2eb0a..c6d7a05a 100644 --- a/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.m +++ b/CleverTapSDK/InApps/Matchers/CTTriggerAdapter.m @@ -30,6 +30,7 @@ - (instancetype)initWithJSON:(NSDictionary *)triggerJSON { self.items = triggerJSON[@"itemProperties"]; self.geoRadius = triggerJSON[@"geoRadius"]; self.profileAttrName = triggerJSON[@"profileAttrName"]; + self.firstTimeOnly = triggerJSON[@"firstTimeOnly"]; } return self; } diff --git a/CleverTapSDK/InApps/Matchers/CTTriggersMatcher.h b/CleverTapSDK/InApps/Matchers/CTTriggersMatcher.h index 404c465a..9c08333e 100644 --- a/CleverTapSDK/InApps/Matchers/CTTriggersMatcher.h +++ b/CleverTapSDK/InApps/Matchers/CTTriggersMatcher.h @@ -8,11 +8,13 @@ #import #import "CTEventAdapter.h" +#import "CTLocalDataStore.h" NS_ASSUME_NONNULL_BEGIN @interface CTTriggersMatcher : NSObject +- (instancetype)initWithDataStore:(CTLocalDataStore *)dataStore; - (BOOL)matchEventWhenTriggers:(NSArray *)whenTriggers event:(CTEventAdapter *)event; @end diff --git a/CleverTapSDK/InApps/Matchers/CTTriggersMatcher.m b/CleverTapSDK/InApps/Matchers/CTTriggersMatcher.m index 7f68301a..b006a81e 100644 --- a/CleverTapSDK/InApps/Matchers/CTTriggersMatcher.m +++ b/CleverTapSDK/InApps/Matchers/CTTriggersMatcher.m @@ -13,8 +13,19 @@ #import "CTTriggerEvaluator.h" #import "CTUtils.h" +@interface CTTriggersMatcher () {} +@property (nonatomic, strong) CTLocalDataStore *dataStore; +@end + @implementation CTTriggersMatcher +- (instancetype)initWithDataStore:(CTLocalDataStore *)dataStore { + if (self = [super init]) { + self.dataStore = dataStore; + } + return self; +} + - (BOOL)matchEventWhenTriggers:(NSArray *)whenTriggers event:(CTEventAdapter *)event { // Events in the array are OR-ed for (NSDictionary *triggerObject in whenTriggers) { @@ -41,6 +52,10 @@ - (BOOL)match:(CTTriggerAdapter *)trigger event:(CTEventAdapter *)event { return NO; } + if (![self matchFirstTimeOnlyForTrigger:trigger]) { + return NO; + } + if (![self matchGeoRadius:event trigger:trigger]) { return NO; } @@ -116,4 +131,12 @@ - (BOOL)matchCharged:(CTTriggerAdapter *)trigger event:(CTEventAdapter *)event { return YES; } +- (BOOL)matchFirstTimeOnlyForTrigger:(CTTriggerAdapter *)trigger { + if (!trigger.firstTimeOnly) { + return YES; + } + NSString *nameToCheck = trigger.profileAttrName ?: trigger.eventName; + return [self.dataStore isEventLoggedFirstTime:nameToCheck]; +} + @end diff --git a/CleverTapSDKTests/CTLocalDataStore+Tests.h b/CleverTapSDKTests/CTLocalDataStore+Tests.h new file mode 100644 index 00000000..3cc9fbcb --- /dev/null +++ b/CleverTapSDKTests/CTLocalDataStore+Tests.h @@ -0,0 +1,15 @@ +// +// CTLocalDataStore+Tests.h +// CleverTapSDKTests +// +// Created by Akash Malhotra on 11/12/24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTLocalDataStore.h" + +@interface CTLocalDataStore (Tests) +- (void)runOnBackgroundQueue:(void (^)(void))taskBlock; +@property (nonatomic, readonly) dispatch_queue_t backgroundQueue; +@end + diff --git a/CleverTapSDKTests/CTLocalDataStore+Tests.m b/CleverTapSDKTests/CTLocalDataStore+Tests.m new file mode 100644 index 00000000..2272c195 --- /dev/null +++ b/CleverTapSDKTests/CTLocalDataStore+Tests.m @@ -0,0 +1,19 @@ +// +// CTLocalDataStore+Tests.m +// CleverTapSDKTests +// +// Created by Akash Malhotra on 11/12/24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTLocalDataStore+Tests.h" +#import + +@implementation CTLocalDataStore (Tests) + +- (dispatch_queue_t)backgroundQueue { + Ivar ivar = class_getInstanceVariable([self class], "_backgroundQueue"); + return object_getIvar(self, ivar); +} + +@end diff --git a/CleverTapSDKTests/CTLocalDataStoreTests.m b/CleverTapSDKTests/CTLocalDataStoreTests.m index 270d2804..247d77e7 100644 --- a/CleverTapSDKTests/CTLocalDataStoreTests.m +++ b/CleverTapSDKTests/CTLocalDataStoreTests.m @@ -10,6 +10,8 @@ #import "CTLocalDataStore.h" #import "CTProfileBuilder.h" #import "CTConstants.h" +#import "XCTestCase+XCTestCase_Tests.h" +#import "CTLocalDataStore+Tests.h" @interface CTLocalDataStoreTests : XCTestCase @property (nonatomic, strong) CTLocalDataStore *dataStore; @@ -109,4 +111,64 @@ - (void)testGetUserAttributeChangePropertiesWithIncrementCommand { OCMVerify(mockGetProfileFieldForKey); } +- (void)testSetAndGetProfileValueForKey { + + [self waitForInitDataStore]; + NSDictionary *profile = @{@"someKey": @"someValue"}; + [self.dataStore setProfileFields:profile]; + XCTAssertEqualObjects([self.dataStore getProfileFieldForKey:@"someKey"], @"someValue"); +} + + +- (void)testSetProfileFieldWithKeyAndValue { + [self waitForInitDataStore]; + [self.dataStore setProfileFieldWithKey:@"someKey" andValue:@"someValue"]; + XCTAssertEqualObjects([self.dataStore getProfileFieldForKey:@"someKey"], @"someValue"); +} + +- (void)testPersistEventAndGetEventDetail { + NSString *eventName = [self randomString]; + NSDictionary *event = @{CLTAP_EVENT_NAME: eventName}; + XCTestExpectation *expectation = [self expectationWithDescription:@"Datastore persist event"]; + // WAIT FOR DATA STORE TO FINISH INIT + dispatch_async(self.dataStore.backgroundQueue, ^{ + // Wait for the background queue to complete datastore setup. + [self.dataStore persistEvent:event]; + [self.dataStore runOnBackgroundQueue:^{ + [expectation fulfill]; + }]; + }); + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + if (error) { + XCTFail(@"Datastore initialization did not complete in time."); + } + }]; + + + CleverTapEventDetail *eventDetails = [self.dataStore readUserEventLog:eventName]; + XCTAssertEqualObjects(eventDetails.eventName, eventName); + XCTAssertEqual(eventDetails.count, 1); + XCTAssertGreaterThan(eventDetails.firstTime, 0); + XCTAssertGreaterThan(eventDetails.lastTime, 0); +} + +- (void)waitForInitDataStore { + XCTestExpectation *expectation = [self expectationWithDescription:@"Datastore initialization"]; + + // WAIT FOR DATA STORE TO FINISH INIT + dispatch_async(self.dataStore.backgroundQueue, ^{ + // Wait for the background queue to complete datastore setup. + [self.dataStore runOnBackgroundQueue:^{ + [expectation fulfill]; + }]; + }); + + [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { + if (error) { + XCTFail(@"Datastore initialization did not complete in time."); + } + }]; +} + @end diff --git a/CleverTapSDKTests/EventDatabase/CTEventDatabase+Tests.h b/CleverTapSDKTests/EventDatabase/CTEventDatabase+Tests.h new file mode 100644 index 00000000..50d365fa --- /dev/null +++ b/CleverTapSDKTests/EventDatabase/CTEventDatabase+Tests.h @@ -0,0 +1,22 @@ +// +// CTEventDatabase+Tests.h +// CleverTapSDKTests +// +// Created by Nishant Kumar on 09/01/25. +// Copyright © 2025 CleverTap. All rights reserved. +// + +#ifndef CTEventDatabase_Tests_h +#define CTEventDatabase_Tests_h + +#import "CTEventDatabase.h" + +@interface CTEventDatabase(Tests) + +- (instancetype)initWithDispatchQueueManager:(CTDispatchQueueManager*)dispatchQueueManager + clock:(id)clock; + +@end + + +#endif /* CTEventDatabase_Tests_h */ diff --git a/CleverTapSDKTests/EventDatabase/CTEventDatabaseTests.m b/CleverTapSDKTests/EventDatabase/CTEventDatabaseTests.m new file mode 100644 index 00000000..020ad2de --- /dev/null +++ b/CleverTapSDKTests/EventDatabase/CTEventDatabaseTests.m @@ -0,0 +1,290 @@ +// +// CTEventDatabaseTests.m +// CleverTapSDKTests +// +// Created by Nishant Kumar on 05/11/24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTEventDatabase+Tests.h" +#import "CTUtils.h" +#import "CTClockMock.h" + +static NSString *kEventName = @"Test Event"; +static NSString *kDeviceID = @"Test Device"; + +@interface CTEventDatabaseTests : XCTestCase + +@property (nonatomic, strong) CTEventDatabase *eventDatabase; +@property (nonatomic, strong) NSString *normalizedEventName; +@property (nonatomic, strong) CTClockMock *mockClock; + +@end + +@implementation CTEventDatabaseTests + +- (void)setUp { + [super setUp]; + + CTClockMock *mockClock = [[CTClockMock alloc] initWithCurrentDate:[NSDate date]]; + self.mockClock = mockClock; + CleverTapInstanceConfig *config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccount" accountToken:@"testToken" accountRegion:@"testRegion"]; + CTDispatchQueueManager *queueManager = [[CTDispatchQueueManager alloc] initWithConfig:config]; + self.eventDatabase = [[CTEventDatabase alloc] initWithDispatchQueueManager:queueManager clock:mockClock]; + self.normalizedEventName = [CTUtils getNormalizedName:kEventName]; +} + +- (void)tearDown { + [super tearDown]; + + XCTestExpectation *deleteExpectation = [self expectationWithDescription:@"Delete all rows"]; + [self.eventDatabase deleteAllRowsWithCompletion:^(BOOL success) { + [deleteExpectation fulfill]; + }]; + [self waitForExpectations:@[deleteExpectation] timeout:2.0]; +} + +- (void)testGetDatabaseVersion { + XCTestExpectation *expectation = [self expectationWithDescription:@"Database version"]; + [self.eventDatabase databaseVersionWithCompletion:^(NSInteger currentVersion) { + XCTAssertEqual(currentVersion, 1); + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testInsertEventName { + XCTestExpectation *expectation = [self expectationWithDescription:@"Insert event"]; + [self.eventDatabase insertEvent:kEventName + normalizedEventName:self.normalizedEventName + deviceID:kDeviceID + completion:^(BOOL success) { + XCTAssertTrue(success); + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testEventNameExists { + [self.eventDatabase upsertEvent:kEventName normalizedEventName:self.normalizedEventName deviceID:kDeviceID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Event exists"]; + [self.eventDatabase eventExists:self.normalizedEventName forDeviceID:kDeviceID completion:^(BOOL exists) { + XCTAssertTrue(exists); + [expectation fulfill]; + }]; + + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Event exists 1"]; + NSString *normalizedEventName = [CTUtils getNormalizedName:@"TesT EveNT"]; + [self.eventDatabase eventExists:normalizedEventName forDeviceID:kDeviceID completion:^(BOOL exists) { + XCTAssertTrue(exists); + [expectation1 fulfill]; + }]; + + XCTestExpectation *expectation2 = [self expectationWithDescription:@"Event exists 2"]; + normalizedEventName = [CTUtils getNormalizedName:@"TEST EVENT"]; + [self.eventDatabase eventExists:normalizedEventName forDeviceID:kDeviceID completion:^(BOOL exists) { + XCTAssertTrue(exists); + [expectation2 fulfill]; + }]; + [self waitForExpectations:@[expectation, expectation1, expectation2] timeout:2.0]; +} + +- (void)testEventNameNotExists { + [self.eventDatabase upsertEvent:kEventName normalizedEventName:self.normalizedEventName deviceID:kDeviceID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Event exists"]; + NSString *normalizedEventName = [CTUtils getNormalizedName:@"TesT EveNT 1"]; + [self.eventDatabase eventExists:normalizedEventName forDeviceID:kDeviceID completion:^(BOOL exists) { + XCTAssertFalse(exists); + [expectation fulfill]; + }]; + + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Event exists 1"]; + normalizedEventName = [CTUtils getNormalizedName:@"Test.Event"]; + [self.eventDatabase eventExists:normalizedEventName forDeviceID:kDeviceID completion:^(BOOL exists) { + XCTAssertFalse(exists); + [expectation1 fulfill]; + }]; + [self waitForExpectations:@[expectation, expectation1] timeout:2.0]; +} + +- (void)testUpdateEventSuccess { + [self.eventDatabase upsertEvent:kEventName normalizedEventName:self.normalizedEventName deviceID:kDeviceID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Update event"]; + [self.eventDatabase updateEvent:self.normalizedEventName forDeviceID:kDeviceID completion:^(BOOL success) { + XCTAssertTrue(success); + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testUpsertEventWhenUpdate { + [self.eventDatabase upsertEvent:kEventName normalizedEventName:self.normalizedEventName deviceID:kDeviceID]; + NSInteger eventCount = [self.eventDatabase getEventCount:self.normalizedEventName deviceID:kDeviceID]; + XCTAssertEqual(eventCount, 1); + + [self.eventDatabase upsertEvent:kEventName normalizedEventName:self.normalizedEventName deviceID:kDeviceID]; + eventCount = [self.eventDatabase getEventCount:self.normalizedEventName deviceID:kDeviceID]; + XCTAssertEqual(eventCount, 2); +} + +- (void)testGetCountForEventName { + [self.eventDatabase upsertEvent:kEventName normalizedEventName:self.normalizedEventName deviceID:kDeviceID]; + [self.eventDatabase upsertEvent:kEventName normalizedEventName:self.normalizedEventName deviceID:kDeviceID]; + + // count should be 2. + NSInteger eventCount = [self.eventDatabase getEventCount:self.normalizedEventName deviceID:kDeviceID]; + XCTAssertEqual(eventCount, 2); +} + +- (void)testGetCountForEventNameNotExists { + // count should be 0. + NSInteger eventCount = [self.eventDatabase getEventCount:self.normalizedEventName deviceID:kDeviceID]; + XCTAssertEqual(eventCount, 0); +} + +- (void)testFirstTimestampForEventName { + self.mockClock.currentDate = [self.mockClock.currentDate dateByAddingTimeInterval:5 * 60]; + NSInteger currentTs = [self.mockClock.currentDate timeIntervalSince1970]; + [self.eventDatabase upsertEvent:kEventName normalizedEventName:self.normalizedEventName deviceID:kDeviceID]; + + CleverTapEventDetail *event = [self.eventDatabase getEventDetail:self.normalizedEventName deviceID:kDeviceID]; + NSInteger firstTs = event.firstTime; + XCTAssertEqual(firstTs, currentTs); +} + +- (void)testLastTimestampForEventName { + self.mockClock.currentDate = [self.mockClock.currentDate dateByAddingTimeInterval:5 * 60]; + NSInteger currentTs = [self.mockClock.currentDate timeIntervalSince1970]; + [self.eventDatabase upsertEvent:kEventName normalizedEventName:self.normalizedEventName deviceID:kDeviceID]; + + CleverTapEventDetail *event = [self.eventDatabase getEventDetail:self.normalizedEventName deviceID:kDeviceID]; + NSInteger lastTs = event.lastTime; + XCTAssertEqual(lastTs, currentTs); +} + +- (void)testLastTimestampForEventNameUpdated { + self.mockClock.currentDate = [self.mockClock.currentDate dateByAddingTimeInterval:5 * 60]; + NSInteger currentTs = [self.mockClock.currentDate timeIntervalSince1970]; + [self.eventDatabase upsertEvent:kEventName normalizedEventName:self.normalizedEventName deviceID:kDeviceID]; + + CleverTapEventDetail *event = [self.eventDatabase getEventDetail:self.normalizedEventName deviceID:kDeviceID]; + NSInteger lastTs = event.lastTime; + XCTAssertEqual(lastTs, currentTs); + + self.mockClock.currentDate = [self.mockClock.currentDate dateByAddingTimeInterval:5 * 60]; + NSInteger newCurrentTs = [self.mockClock.currentDate timeIntervalSince1970]; + [self.eventDatabase upsertEvent:kEventName normalizedEventName:self.normalizedEventName deviceID:kDeviceID]; + CleverTapEventDetail *newEvent = [self.eventDatabase getEventDetail:self.normalizedEventName deviceID:kDeviceID]; + NSInteger newLastTs = newEvent.lastTime; + XCTAssertEqual(newLastTs, newCurrentTs); + +} + +- (void)testDeleteAllRowsSuccess { + [self.eventDatabase upsertEvent:kEventName normalizedEventName:self.normalizedEventName deviceID:kDeviceID]; + NSInteger eventCount = [self.eventDatabase getEventCount:self.normalizedEventName deviceID:kDeviceID]; + XCTAssertEqual(eventCount, 1); + + // Delete table. + XCTestExpectation *expectation = [self expectationWithDescription:@"Delete all rows"]; + [self.eventDatabase deleteAllRowsWithCompletion:^(BOOL success) { + XCTAssertTrue(success); + [expectation fulfill]; + }]; + NSInteger eventCountAfterDelete = [self.eventDatabase getEventCount:self.normalizedEventName deviceID:kDeviceID]; + XCTAssertEqual(eventCountAfterDelete, 0); + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testEventDetailsForDeviceID { + NSInteger currentTs = (NSInteger)[[NSDate date] timeIntervalSince1970]; + [self.eventDatabase upsertEvent:kEventName normalizedEventName:self.normalizedEventName deviceID:kDeviceID]; + + CleverTapEventDetail *eventDetail = [self.eventDatabase getEventDetail:self.normalizedEventName deviceID:kDeviceID]; + XCTAssertEqualObjects(eventDetail.eventName, kEventName); + XCTAssertEqualObjects(eventDetail.normalizedEventName, self.normalizedEventName); + XCTAssertEqual(eventDetail.firstTime, currentTs); + XCTAssertEqual(eventDetail.lastTime, currentTs); + XCTAssertEqual(eventDetail.count, 1); + XCTAssertEqualObjects(eventDetail.deviceID, kDeviceID); +} + +- (void)testAllEventsForDeviceID { + [self.eventDatabase upsertEvent:kEventName normalizedEventName:self.normalizedEventName deviceID:kDeviceID]; + + // Insert to same device id kDeviceID + NSString *eventName = @"Test Event 1"; + NSString *normalizedName = [CTUtils getNormalizedName:eventName]; + + [self.eventDatabase upsertEvent:eventName normalizedEventName:normalizedName deviceID:kDeviceID]; + + // Insert to different device id + [self.eventDatabase upsertEvent:kEventName normalizedEventName:self.normalizedEventName deviceID:@"Test Device 1"]; + + NSArray* allEvents = [self.eventDatabase getAllEventsForDeviceID:kDeviceID]; + XCTAssertEqualObjects(allEvents[0].eventName, kEventName); + XCTAssertEqualObjects(allEvents[1].eventName, eventName); + XCTAssertEqual(allEvents.count, 2); + + allEvents = [self.eventDatabase getAllEventsForDeviceID:@"Test Device 1"]; + XCTAssertEqualObjects(allEvents[0].eventName, kEventName); + XCTAssertEqual(allEvents.count, 1); +} + +- (void)testLeastRecentlyUsedRowsDeleted { + int maxRow = 10; + int numberOfRowsToCleanup = 2; + int totalRowCount = 13; + for (int i = 0; i < totalRowCount; i++) { + NSString *eventName = [NSString stringWithFormat:@"Test Event %d", i]; + NSString *normalizedName = [CTUtils getNormalizedName:eventName]; + [self.eventDatabase upsertEvent:eventName normalizedEventName:normalizedName deviceID:kDeviceID]; + } + NSArray* allEvents = [self.eventDatabase getAllEventsForDeviceID:kDeviceID]; + XCTAssertEqual(allEvents.count, totalRowCount); + + // When deleteLeastRecentlyUsedRows is called using max row limit and numberOfRowsToCleanup + // the deleted row count will be `totalRowCount - (maxRow - numberOfRowsToCleanup)` + XCTestExpectation *expectation = [self expectationWithDescription:@"Delete rows"]; + [self.eventDatabase deleteLeastRecentlyUsedRows:maxRow numberOfRowsToCleanup:numberOfRowsToCleanup completion:^(BOOL success) { + NSArray* allEvents = [self.eventDatabase getAllEventsForDeviceID:kDeviceID]; + int deletedRowCount = totalRowCount - (maxRow - numberOfRowsToCleanup); + XCTAssertEqual(allEvents.count, totalRowCount - deletedRowCount); + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testLeastRecentlyUsedRowsNotDeleted { + int maxRow = 10; + int numberOfRowsToCleanup = 2; + int totalRowCount = 7; + for (int i = 0; i < totalRowCount; i++) { + NSString *eventName = [NSString stringWithFormat:@"Test Event %d", i]; + NSString *normalizedName = [CTUtils getNormalizedName:eventName]; + [self.eventDatabase upsertEvent:eventName normalizedEventName:normalizedName deviceID:kDeviceID]; + } + NSArray* allEvents = [self.eventDatabase getAllEventsForDeviceID:kDeviceID]; + XCTAssertEqual(allEvents.count, totalRowCount); + + // Here any row will not be deleted as it is within limit. + XCTestExpectation *expectation = [self expectationWithDescription:@"Delete rows"]; + [self.eventDatabase deleteLeastRecentlyUsedRows:maxRow numberOfRowsToCleanup:numberOfRowsToCleanup completion:^(BOOL success) { + NSArray* allEvents = [self.eventDatabase getAllEventsForDeviceID:kDeviceID]; + XCTAssertEqual(allEvents.count, totalRowCount); + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (NSString *)databasePath { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDirectory = [paths objectAtIndex:0]; + return [documentsDirectory stringByAppendingPathComponent:@"CleverTap-Events.db"]; +} + +@end diff --git a/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m b/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m index 87718b0e..328d4551 100644 --- a/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m +++ b/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m @@ -778,7 +778,7 @@ - (void)testDelegatesAdded { NSUInteger batchHeaderDelegatesCount = [[delegateManager attachToHeaderDelegates] count]; NSUInteger batchSentDelegatesCount = [[delegateManager batchSentDelegates] count]; - __unused CTInAppEvaluationManager *manager = [[CTInAppEvaluationManager alloc] initWithAccountId:self.helper.accountId deviceId:self.helper.deviceId delegateManager:delegateManager impressionManager:self.helper.impressionManager inAppDisplayManager:self.helper.inAppDisplayManager inAppStore:self.helper.inAppStore inAppTriggerManager:self.helper.inAppTriggerManager]; + __unused CTInAppEvaluationManager *manager = [[CTInAppEvaluationManager alloc] initWithAccountId:self.helper.accountId deviceId:self.helper.deviceId delegateManager:delegateManager impressionManager:self.helper.impressionManager inAppDisplayManager:self.helper.inAppDisplayManager inAppStore:self.helper.inAppStore inAppTriggerManager:self.helper.inAppTriggerManager localDataStore:self.helper.dataStore]; XCTAssertEqual([[delegateManager attachToHeaderDelegates] count], batchHeaderDelegatesCount + 1); XCTAssertEqual([[delegateManager batchSentDelegates] count], batchSentDelegatesCount + 1); diff --git a/CleverTapSDKTests/InApps/CTTriggersMatcherTest.m b/CleverTapSDKTests/InApps/CTTriggersMatcherTest.m index 81c976af..bbc905ae 100644 --- a/CleverTapSDKTests/InApps/CTTriggersMatcherTest.m +++ b/CleverTapSDKTests/InApps/CTTriggersMatcherTest.m @@ -8,6 +8,7 @@ #import #import +#import #import "CTTriggersMatcher.h" #import "CTEventAdapter.h" #import "CTTriggerEvaluator.h" @@ -15,11 +16,19 @@ #import "CTConstants.h" @interface CTTriggersMatcherTest : XCTestCase - +@property (nonatomic, strong) CTLocalDataStore *dataStore; @end @implementation CTTriggersMatcherTest +- (void)setUp { + [super setUp]; + CleverTapInstanceConfig *config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccount" accountToken:@"testToken" accountRegion:@"testRegion"]; + CTDeviceInfo *deviceInfo = [[CTDeviceInfo alloc] initWithConfig:config andCleverTapID:@"testDeviceInfo"]; + CTDispatchQueueManager *queueManager = [[CTDispatchQueueManager alloc] initWithConfig:config]; + self.dataStore = [[CTLocalDataStore alloc] initWithConfig:config profileValues:[NSMutableDictionary new] andDeviceInfo:deviceInfo dispatchQueueManager:queueManager]; +} + #pragma mark Event - (void)testMatchEventAllOperators { NSArray *whenTriggers = @[ @@ -73,7 +82,7 @@ - (void)testMatchEventAllOperators { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @160, @@ -96,7 +105,7 @@ - (void)testMatchEventWithoutTriggerProps { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"clevertap" @@ -116,7 +125,7 @@ - (void)testMatchEventWithEmptyTriggerProps { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"clevertap" @@ -140,7 +149,7 @@ - (void)testMatchEventWithoutProps { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL matchNoProps = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{}]; XCTAssertFalse(matchNoProps); @@ -233,7 +242,7 @@ - (void)testMatchChargedEvent { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchChargedEventWhenTriggers:whenTriggers details:@{ @"prop1": @150, @@ -277,7 +286,7 @@ - (void)testChargedWithoutItems { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchChargedEventWhenTriggers:whenTriggers details:@{ @"prop1": @150, @@ -337,7 +346,7 @@ - (void)testMatchChargedEventItemArrayEquals { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchChargedEventWhenTriggers:whenTriggers details:@{ @"prop1": @150, @@ -412,7 +421,7 @@ - (void)testMatchChargedEventItemArrayContains { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchChargedEventWhenTriggers:whenTriggers details:@{ @"prop1": @150, @@ -487,7 +496,7 @@ - (void)testMatchEqualsPrimitives { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @150, @@ -518,7 +527,7 @@ - (void)testMatchEqualsBoolean { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @(YES), @@ -564,7 +573,7 @@ - (void)testMatchEqualsBooleanString { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"true", @@ -593,7 +602,7 @@ - (void)testMatchEqualsBooleanCaseInsensitive { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @(YES), @@ -627,7 +636,7 @@ - (void)testMatchEqualsNumbers { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @150, @@ -696,7 +705,7 @@ - (void)testMatchEqualsNumbersCharged { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchChargedEventWhenTriggers:whenTriggers details:@{} items:@[@{ @"prop1": @"150", @@ -743,7 +752,7 @@ - (void)testMatchEqualsDouble { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @150.950 @@ -775,7 +784,7 @@ - (void)testMatchEqualsExtectedStringWithActualArray { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @[@"test", @"test2"] @@ -804,7 +813,7 @@ - (void)testMatchEqualsExtectedArrayWithActualString { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @[@"test"] @@ -836,7 +845,7 @@ - (void)testMatchEqualsExtectedNumberWithActualArray { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @[@"test", @150] @@ -859,7 +868,7 @@ - (void)testMatchEqualsExtectedStringWithActualString { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"test" @@ -951,7 +960,7 @@ - (void)testMatchEqualsExtectedNumberWithActualString { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"test" @@ -973,7 +982,7 @@ - (void)testMatchEqualsExtectedDoubleWithActualDouble { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @150.99 @@ -996,7 +1005,7 @@ - (void)testMatchEqualsExtectedDoubleWithActualDoubleString { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"150.99" @@ -1019,7 +1028,7 @@ - (void)testMatchEqualsExtectedArrayWithActualArray { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @[@"test2", @"test3", @"test"] @@ -1042,7 +1051,7 @@ - (void)testMatchEqualsExtectedArrayWithActualArrayNumber { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @[@3, @1, @2] @@ -1065,7 +1074,7 @@ - (void)testMatchSet { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @150 @@ -1086,7 +1095,7 @@ - (void)testMatchSetCharged { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchChargedEventWhenTriggers:whenTriggers details:@{} items:@[ @{ @@ -1120,7 +1129,7 @@ - (void)testMatchNotSet { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop2": @150 @@ -1142,7 +1151,7 @@ - (void)testMatchNotSetCharged { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchChargedEventWhenTriggers:whenTriggers details:@{} items:@[ @{ @@ -1177,7 +1186,7 @@ - (void)testMatchNotEquals { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @240 @@ -1204,7 +1213,7 @@ - (void)testMatchNotEqualsArrays { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @[@241] @@ -1233,7 +1242,7 @@ - (void)testMatchLessThan { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @150 @@ -1261,7 +1270,7 @@ - (void)testMatchLessThanWithString { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"-120" @@ -1290,7 +1299,7 @@ - (void)testMatchLessThanWithArrays { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"-120" @@ -1336,7 +1345,7 @@ - (void)testMatchGreaterThan { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @240 @@ -1364,7 +1373,7 @@ - (void)testMatchGreaterThanWithString { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"240" @@ -1393,7 +1402,7 @@ - (void)testMatchGreaterThanWithArrays { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @600 @@ -1439,7 +1448,7 @@ - (void)testMatchBetween { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @150 @@ -1492,7 +1501,7 @@ - (void)testMatchBetweenCharged { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchChargedEventWhenTriggers:whenTriggers details:@{} items:@[ @{ @@ -1529,7 +1538,7 @@ - (void)testMatchBetweenArrayMoreThan2 { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @150 @@ -1552,7 +1561,7 @@ - (void)testMatchBetweenEmptyArray { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @150 @@ -1576,7 +1585,7 @@ - (void)testMatchContainsString { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"clevertap" @@ -1603,7 +1612,7 @@ - (void)testMatchContainsStringBool { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"this is true" @@ -1630,7 +1639,7 @@ - (void)testMatchContainsNumber { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"1234567" @@ -1662,7 +1671,7 @@ - (void)testMatchContainsArray { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"clevertap" @@ -1685,7 +1694,7 @@ - (void)testMatchContainsArrayNumber { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"123456" @@ -1712,7 +1721,7 @@ - (void)testMatchContainsArrayEmpty { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"clevertap" @@ -1735,7 +1744,7 @@ - (void)testMatchContainsStringWithPropertyArray { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @[@"clevertap",@"test"] @@ -1759,7 +1768,7 @@ - (void)testMatchNotContainsArray { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"clevertap" @@ -1792,7 +1801,7 @@ - (void)testMatchNotContainsArrayFromTriggerArray { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @[@"clevertap", @"yes"] @@ -1815,7 +1824,7 @@ - (void)testMatchNotContainsString { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ @"prop1": @"clevertap" @@ -1838,7 +1847,7 @@ - (void)testMatchSystemProperties { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ CLTAP_SDK_VERSION: @60000 @@ -1889,7 +1898,7 @@ - (void)testMatchSystemPropertiesCurrentAndLegacy { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{ CLTAP_SDK_VERSION: @60000, CLTAP_APP_VERSION: @"6.0.0", @@ -1927,7 +1936,7 @@ - (void)testMatchNotificationProperties { } ]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"Notification Viewed" eventProperties:@{ CLTAP_PROP_WZRK_ID: @"1701172437_20231128", @@ -1959,7 +1968,7 @@ - (void)testMatchEventWithGeoRadius { CTEventAdapter *event = [[CTEventAdapter alloc] initWithEventName:@"event1" eventProperties:@{} andLocation:location1km]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers event:event]; XCTAssertTrue(match); @@ -1995,10 +2004,99 @@ - (void)testMatchEventWithGeoRadiusButNotParams { CTEventAdapter *event = [[CTEventAdapter alloc] initWithEventName:@"event1" eventProperties:@{@"prop1": @151} andLocation:location1km]; - CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] init]; + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers event:event]; XCTAssertFalse(match); } +#pragma mark FirstTimeOnly + +- (void)testMatchEventWithFirstTimeOnly { + NSArray *whenTriggers = @[ + @{ + @"eventName": @"event1", + @"firstTimeOnly": @YES + } + ]; + + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; + CTLocalDataStore *dataStoreMock = OCMPartialMock(self.dataStore); + id mockIsEventLoggedFirstTime = OCMStub([dataStoreMock isEventLoggedFirstTime:@"event1"]).andReturn(YES); + + BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers eventName:@"event1" eventProperties:@{}]; + + XCTAssertTrue(match); + OCMVerify(mockIsEventLoggedFirstTime); +} + +- (void)testMatchChargedEventWithFirstTimeOnly { + NSArray *whenTriggers = @[ + @{ + @"eventName": @"Charged", + @"firstTimeOnly": @YES, + @"eventProperties": @[ + @{ + @"propertyName": @"prop1", + @"operator": @1, + @"propertyValue": @150 + }], + @"itemProperties": @[ + @{ + @"propertyName": @"product_name", + @"operator": @1, + @"propertyValue": @"product 1" + }] + } + ]; + + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; + + CTLocalDataStore *dataStoreMock = OCMPartialMock(self.dataStore); + id mockIsEventLoggedFirstTime = OCMStub([dataStoreMock isEventLoggedFirstTime:@"Charged"]).andReturn(YES); + BOOL match = [triggerMatcher matchChargedEventWhenTriggers:whenTriggers details:@{ + @"prop1": @150, + } items:@[ + @{ + @"product_name": @"product 1", + @"price": @5.99 + }, + @{ + @"product_name": @"product 2", + @"price": @5.50 + } + ]]; + XCTAssertTrue(match); + OCMVerify(mockIsEventLoggedFirstTime); +} + +- (void)testMatchEventFirstTimeOnlyWithGeoRadius { + NSArray *whenTriggers = @[ + @{ + @"eventName": @"event1", + @"firstTimeOnly": @YES, + @"geoRadius": @[ + @{ + @"lat": @19.07609, + @"lng": @72.877426, + @"rad": @2 + }] + } + ]; + + // Distance ~1.1km + CLLocationCoordinate2D location1km = CLLocationCoordinate2DMake(19.08609, 72.877426); + + CTEventAdapter *event = [[CTEventAdapter alloc] initWithEventName:@"event1" eventProperties:@{} andLocation:location1km]; + + CTTriggersMatcher *triggerMatcher = [[CTTriggersMatcher alloc] initWithDataStore:self.dataStore]; + CTLocalDataStore *dataStoreMock = OCMPartialMock(self.dataStore); + id mockIsEventLoggedFirstTime = OCMStub([dataStoreMock isEventLoggedFirstTime:@"event1"]).andReturn(YES); + + BOOL match = [triggerMatcher matchEventWhenTriggers:whenTriggers event:event]; + XCTAssertTrue(match); +} + @end + + diff --git a/CleverTapSDKTests/InApps/InAppHelper.h b/CleverTapSDKTests/InApps/InAppHelper.h index 0a782204..a97d44c8 100644 --- a/CleverTapSDKTests/InApps/InAppHelper.h +++ b/CleverTapSDKTests/InApps/InAppHelper.h @@ -16,6 +16,7 @@ @class CTMultiDelegateManager; @class CTInAppTriggerManager; @class CTFileDownloader; +@class CTLocalDataStore; NS_ASSUME_NONNULL_BEGIN @@ -35,6 +36,7 @@ extern NSString *const CLTAP_TEST_CAMPAIGN_ID; @property (nonatomic, strong) CTInAppStore *inAppStore; @property (nonatomic, strong) CTInAppTriggerManager *inAppTriggerManager; @property (nonatomic, strong) CTFileDownloader *fileDownloader; +@property (nonatomic, strong) CTLocalDataStore *dataStore; - (NSString *)accountId; - (NSString *)accountToken; diff --git a/CleverTapSDKTests/InApps/InAppHelper.m b/CleverTapSDKTests/InApps/InAppHelper.m index c523ad61..6d105f66 100644 --- a/CleverTapSDKTests/InApps/InAppHelper.m +++ b/CleverTapSDKTests/InApps/InAppHelper.m @@ -67,6 +67,10 @@ - (instancetype)init { impressionManager:self.impressionManager inAppTriggerManager:self.inAppTriggerManager]; + CTDeviceInfo *deviceInfo = [[CTDeviceInfo alloc] initWithConfig:self.config andCleverTapID:CLTAP_TEST_DEVICE_ID]; + CTDispatchQueueManager *queueManager = [[CTDispatchQueueManager alloc] initWithConfig:self.config]; + self.dataStore = [[CTLocalDataStore alloc] initWithConfig:self.config profileValues:[NSMutableDictionary new] andDeviceInfo:deviceInfo dispatchQueueManager:queueManager]; + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wnonnull" // Initialize when needed, requires CleverTap instance @@ -79,7 +83,7 @@ - (instancetype)init { impressionManager:self.impressionManager inAppDisplayManager:self.inAppDisplayManager inAppStore:self.inAppStore - inAppTriggerManager:self.inAppTriggerManager]; + inAppTriggerManager:self.inAppTriggerManager localDataStore:self.dataStore]; } return self; } diff --git a/Package.swift b/Package.swift index c8d9b055..67bf684c 100644 --- a/Package.swift +++ b/Package.swift @@ -58,7 +58,8 @@ let package = Package( .headerSearchPath("ProductExperiences/"), .headerSearchPath("Session/"), .headerSearchPath("Swizzling/"), - .headerSearchPath("FileDownload/") + .headerSearchPath("FileDownload/"), + .headerSearchPath("EventDatabase/") ], linkerSettings: [ .linkedFramework("AVFoundation"), diff --git a/sdk-version.txt b/sdk-version.txt index 5febf260..3769235d 100644 --- a/sdk-version.txt +++ b/sdk-version.txt @@ -1 +1 @@ -7.0.3 \ No newline at end of file +7.1.0 \ No newline at end of file