From 52a6d7604774b853f42573866e9bdccfb17edf6c Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Thu, 2 Nov 2023 10:42:49 +0100 Subject: [PATCH] Unit tests and refactoring to make it testable (#84) --- .github/workflows/build-examples.yml | 18 + .github/workflows/build.yml | 8 - .github/workflows/test.yml | 18 + Makefile | 7 +- Package.resolved | 52 ++ Package.swift | 11 +- PostHog.xcodeproj/project.pbxproj | 124 ++-- PostHog/PostHogApi.swift | 6 +- PostHog/PostHogConfig.swift | 4 + PostHog/PostHogContext.swift | 5 +- PostHog/PostHogFeatureFlags.swift | 47 +- PostHog/PostHogFileBackedQueue.swift | 45 +- PostHog/PostHogLegacyQueue.swift | 17 +- PostHog/PostHogQueue.swift | 105 ++-- PostHog/PostHogSDK.swift | 60 +- PostHog/PostHogSessionManager.swift | 11 +- PostHog/PostHogStorage.swift | 79 ++- PostHog/UIViewController.swift | 120 ++-- PostHog/Utils/FileUtils.swift | 18 + PostHogObjCExample/AppDelegate.m | 112 ++-- PostHogTests/CaptureTests.swift | 100 ---- PostHogTests/FeatureFlagsTest.swift | 89 --- PostHogTests/PostHogConfigTest.swift | 44 ++ PostHogTests/PostHogContextTest.swift | 60 ++ PostHogTests/PostHogFeatureFlagsTest.swift | 140 +++++ PostHogTests/PostHogFileBackedQueueTest.swift | 177 ++++++ PostHogTests/PostHogLegacyQueueTest.swift | 133 +++++ PostHogTests/PostHogQueueTest.swift | 68 +++ PostHogTests/PostHogSDKTest.swift | 528 ++++++++++++++++++ PostHogTests/PostHogSessionManagerTest.swift | 49 ++ PostHogTests/PostHogStorageTest.swift | 97 ++++ PostHogTests/PostHogTest.swift | 184 ------ PostHogTests/QueueTest.swift | 80 --- PostHogTests/SessionManagerTest.swift | 44 -- PostHogTests/StorageTest.swift | 109 ---- .../TestUtils/MockPostHogServer.swift | 91 ++- PostHogTests/TestUtils/TestPostHog.swift | 50 +- 37 files changed, 1890 insertions(+), 1020 deletions(-) create mode 100644 .github/workflows/build-examples.yml create mode 100644 .github/workflows/test.yml create mode 100644 Package.resolved create mode 100644 PostHog/Utils/FileUtils.swift delete mode 100644 PostHogTests/CaptureTests.swift delete mode 100644 PostHogTests/FeatureFlagsTest.swift create mode 100644 PostHogTests/PostHogConfigTest.swift create mode 100644 PostHogTests/PostHogContextTest.swift create mode 100644 PostHogTests/PostHogFeatureFlagsTest.swift create mode 100644 PostHogTests/PostHogFileBackedQueueTest.swift create mode 100644 PostHogTests/PostHogLegacyQueueTest.swift create mode 100644 PostHogTests/PostHogQueueTest.swift create mode 100644 PostHogTests/PostHogSDKTest.swift create mode 100644 PostHogTests/PostHogSessionManagerTest.swift create mode 100644 PostHogTests/PostHogStorageTest.swift delete mode 100644 PostHogTests/PostHogTest.swift delete mode 100644 PostHogTests/QueueTest.swift delete mode 100644 PostHogTests/SessionManagerTest.swift delete mode 100644 PostHogTests/StorageTest.swift diff --git a/.github/workflows/build-examples.yml b/.github/workflows/build-examples.yml new file mode 100644 index 000000000..26e808f32 --- /dev/null +++ b/.github/workflows/build-examples.yml @@ -0,0 +1,18 @@ +name: Build Examples +on: + push: + branches: + - master + - main + - v3.0.0 + pull_request: +jobs: + build: + runs-on: macos-13 + steps: + - uses: actions/checkout@v4 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '15.0' + - name: Build Example + run: make buildExample diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 96fc48f94..c0f423db5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,16 +11,8 @@ jobs: runs-on: macos-13 steps: - uses: actions/checkout@v4 - - - uses: swift-actions/setup-swift@v1 - with: - swift-version: "5.3.0" - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: '15.0' - name: Build SDK run: make buildSdk - - name: Build Example - run: make buildExample - - name: Test SDK - run: make test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..ca3012f18 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,18 @@ +name: Tests +on: + push: + branches: + - master + - main + - v3.0.0 + pull_request: +jobs: + test: + runs-on: macos-13 + steps: + - uses: actions/checkout@v4 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '15.0' + - name: Test SDK + run: make test diff --git a/Makefile b/Makefile index dce8018d0..63f7ddda2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build buildSdk buildExample format swiftLint swiftFormat test lint bootstrap releaseCocoaPods +.PHONY: build buildSdk buildExample format swiftLint swiftFormat test testOnSimulator lint bootstrap releaseCocoaPods build: buildSdk buildExample @@ -16,9 +16,12 @@ swiftLint: swiftFormat: swiftformat . --swiftversion 5.3 -test: +testOnSimulator: set -o pipefail && xcodebuild test -project PostHog.xcodeproj -scheme PostHog -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.0.1' | xcpretty +test: + swift test + lint: swiftformat . --lint --swiftversion 5.3 && swiftlint diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 000000000..caad2dd4d --- /dev/null +++ b/Package.resolved @@ -0,0 +1,52 @@ +{ + "object": { + "pins": [ + { + "package": "CwlCatchException", + "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", + "state": { + "branch": null, + "revision": "3b123999de19bf04905bc1dfdb76f817b0f2cc00", + "version": "2.1.2" + } + }, + { + "package": "CwlPreconditionTesting", + "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", + "state": { + "branch": null, + "revision": "a23ded2c91df9156628a6996ab4f347526f17b6b", + "version": "2.1.2" + } + }, + { + "package": "Nimble", + "repositoryURL": "https://github.com/Quick/Nimble.git", + "state": { + "branch": null, + "revision": "edaedc1ec86f14ac6e2ca495b94f0ff7150d98d0", + "version": "12.3.0" + } + }, + { + "package": "OHHTTPStubs", + "repositoryURL": "https://github.com/AliSoftware/OHHTTPStubs.git", + "state": { + "branch": null, + "revision": "12f19662426d0434d6c330c6974d53e2eb10ecd9", + "version": "9.1.0" + } + }, + { + "package": "Quick", + "repositoryURL": "https://github.com/Quick/Quick.git", + "state": { + "branch": null, + "revision": "16910e406be96e08923918315388c3e989deac9e", + "version": "6.1.0" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index 6aa3aab1a..10a12c459 100644 --- a/Package.swift +++ b/Package.swift @@ -15,6 +15,9 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. + .package(url: "https://github.com/Quick/Quick.git", from: "6.0.0"), + .package(url: "https://github.com/Quick/Nimble.git", from: "12.0.0"), + .package(url: "https://github.com/AliSoftware/OHHTTPStubs.git", from: "9.0.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -25,7 +28,13 @@ let package = Package( ), .testTarget( name: "PostHogTests", - dependencies: ["PostHog"], + dependencies: [ + "PostHog", + "Quick", + "Nimble", + "OHHTTPStubs", + .product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs"), + ], path: "PostHogTests" ), ] diff --git a/PostHog.xcodeproj/project.pbxproj b/PostHog.xcodeproj/project.pbxproj index 9e1674d43..f2fb76048 100644 --- a/PostHog.xcodeproj/project.pbxproj +++ b/PostHog.xcodeproj/project.pbxproj @@ -10,26 +10,17 @@ 3A0F108329C47940002C0084 /* UIViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0F108229C47940002C0084 /* UIViewExample.swift */; }; 3A0F108529C9ABB6002C0084 /* ReadWriteLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0F108429C9ABB6002C0084 /* ReadWriteLock.swift */; }; 3A0F108929C9BD76002C0084 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0F108829C9BD76002C0084 /* Errors.swift */; }; - 3A2BCF4C299E4E35008BB5F3 /* QueueTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BCF4B299E4E35008BB5F3 /* QueueTest.swift */; }; - 3A580B3F29E481F200C5C6F3 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = 3A580B3E29E481F200C5C6F3 /* OHHTTPStubs */; }; - 3A580B4129E481F200C5C6F3 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 3A580B4029E481F200C5C6F3 /* OHHTTPStubsSwift */; }; 3A580B4329E489D000C5C6F3 /* URLSession+body.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A580B4229E489D000C5C6F3 /* URLSession+body.swift */; }; - 3A62646429C9E0E7007E8C07 /* PostHogTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A62646329C9E0E7007E8C07 /* PostHogTest.swift */; }; - 3A62646729C9E36B007E8C07 /* Shock in Frameworks */ = {isa = PBXBuildFile; productRef = 3A62646629C9E36B007E8C07 /* Shock */; }; 3A62646A29C9E385007E8C07 /* MockPostHogServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A62646929C9E385007E8C07 /* MockPostHogServer.swift */; }; - 3A62647129CAF67B007E8C07 /* SessionManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A62647029CAF67B007E8C07 /* SessionManagerTest.swift */; }; - 3A62647329CB0043007E8C07 /* CaptureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A62647229CB0043007E8C07 /* CaptureTests.swift */; }; + 3A62647129CAF67B007E8C07 /* PostHogSessionManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A62647029CAF67B007E8C07 /* PostHogSessionManagerTest.swift */; }; 3A62647529CB0168007E8C07 /* TestPostHog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A62647429CB0168007E8C07 /* TestPostHog.swift */; }; 3A81BEAE297704F400A6908D /* PostHog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; }; 3A81BEAF297704F400A6908D /* PostHog.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 3A867B7029C1DF73009D0852 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = 3A867B6F29C1DF73009D0852 /* Quick */; }; - 3A867B7329C1DFEF009D0852 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 3A867B7229C1DFEF009D0852 /* Nimble */; }; 3AA34CFA296D951A003398F4 /* PostHogExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA34CF9296D951A003398F4 /* PostHogExampleApp.swift */; }; 3AA34CFC296D951A003398F4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA34CFB296D951A003398F4 /* ContentView.swift */; }; 3AA34CFE296D951B003398F4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AA34CFD296D951B003398F4 /* Assets.xcassets */; }; 3AA34D01296D951B003398F4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AA34D00296D951B003398F4 /* Preview Assets.xcassets */; }; 3AA34D17296D9993003398F4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA34D16296D9993003398F4 /* AppDelegate.swift */; }; - 3AB7330D29E420E400C8AA71 /* FeatureFlagsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7330C29E420E400C8AA71 /* FeatureFlagsTest.swift */; }; 3AC745C0296D6FE60025C109 /* PostHog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; }; 3AC745C6296D6FE60025C109 /* PostHog.h in Headers */ = {isa = PBXBuildFile; fileRef = 3AC745B8296D6FE60025C109 /* PostHog.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3AE3FB2C2991320300AFFC18 /* Api.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE3FB2B2991320300AFFC18 /* Api.swift */; }; @@ -40,12 +31,24 @@ 3AE3FB432992985A00AFFC18 /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE3FB422992985A00AFFC18 /* Reachability.swift */; }; 3AE3FB472992AB0000AFFC18 /* Hedgelog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE3FB462992AB0000AFFC18 /* Hedgelog.swift */; }; 3AE3FB49299391DF00AFFC18 /* PostHogStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE3FB48299391DF00AFFC18 /* PostHogStorage.swift */; }; - 3AE3FB4B2993A68500AFFC18 /* StorageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE3FB4A2993A68500AFFC18 /* StorageTest.swift */; }; + 3AE3FB4B2993A68500AFFC18 /* PostHogStorageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE3FB4A2993A68500AFFC18 /* PostHogStorageTest.swift */; }; 3AE3FB4E2993D1D600AFFC18 /* PostHogSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE3FB4D2993D1D600AFFC18 /* PostHogSessionManager.swift */; }; 690FF05F2AE7E2D400A0B06B /* Data+Gzip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF05E2AE7E2D400A0B06B /* Data+Gzip.swift */; }; - 690FF0B52AEBBD3C00A0B06B /* DictUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0B42AEBBD3C00A0B06B /* DictUtils.swift */; }; 690FF0AF2AEB9C1400A0B06B /* DateUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0AE2AEB9C1400A0B06B /* DateUtils.swift */; }; + 690FF0B52AEBBD3C00A0B06B /* DictUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0B42AEBBD3C00A0B06B /* DictUtils.swift */; }; + 690FF0BB2AEF8B8200A0B06B /* PostHogContextTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0BA2AEF8B8200A0B06B /* PostHogContextTest.swift */; }; + 690FF0BD2AEF93F400A0B06B /* PostHogFeatureFlagsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0BC2AEF93F400A0B06B /* PostHogFeatureFlagsTest.swift */; }; + 690FF0BF2AEFA97F00A0B06B /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0BE2AEFA97F00A0B06B /* FileUtils.swift */; }; 690FF0C52AEFAE8200A0B06B /* PostHogLegacyQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0C42AEFAE8200A0B06B /* PostHogLegacyQueue.swift */; }; + 690FF0DF2AEFBC5700A0B06B /* PostHogLegacyQueueTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0DE2AEFBC5700A0B06B /* PostHogLegacyQueueTest.swift */; }; + 690FF0E12AEFC59100A0B06B /* PostHogFileBackedQueueTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0E02AEFC59100A0B06B /* PostHogFileBackedQueueTest.swift */; }; + 690FF0E32AEFD12900A0B06B /* PostHogConfigTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0E22AEFD12900A0B06B /* PostHogConfigTest.swift */; }; + 690FF0E92AEFD3BD00A0B06B /* PostHogQueueTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0E82AEFD3BD00A0B06B /* PostHogQueueTest.swift */; }; + 690FF0EB2AEFF22F00A0B06B /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = 690FF0EA2AEFF22F00A0B06B /* Quick */; }; + 690FF0ED2AEFF23300A0B06B /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 690FF0EC2AEFF23300A0B06B /* Nimble */; }; + 690FF0EF2AEFF23D00A0B06B /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 690FF0EE2AEFF23D00A0B06B /* OHHTTPStubsSwift */; }; + 690FF0F12AEFF24200A0B06B /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = 690FF0F02AEFF24200A0B06B /* OHHTTPStubs */; }; + 690FF0F52AF0F06100A0B06B /* PostHogSDKTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0F42AF0F06100A0B06B /* PostHogSDKTest.swift */; }; 69261D132AD5685B00232EC7 /* PostHogFeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69261D122AD5685B00232EC7 /* PostHogFeatureFlags.swift */; }; 69261D192AD9673500232EC7 /* PostHogBatchUploadInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69261D182AD9673500232EC7 /* PostHogBatchUploadInfo.swift */; }; 69261D1B2AD9678C00232EC7 /* PostHogEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69261D1A2AD9678C00232EC7 /* PostHogEvent.swift */; }; @@ -140,12 +143,9 @@ 3A0F108229C47940002C0084 /* UIViewExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExample.swift; sourceTree = ""; }; 3A0F108429C9ABB6002C0084 /* ReadWriteLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadWriteLock.swift; sourceTree = ""; }; 3A0F108829C9BD76002C0084 /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; - 3A2BCF4B299E4E35008BB5F3 /* QueueTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueueTest.swift; sourceTree = ""; }; 3A580B4229E489D000C5C6F3 /* URLSession+body.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+body.swift"; sourceTree = ""; }; - 3A62646329C9E0E7007E8C07 /* PostHogTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogTest.swift; sourceTree = ""; }; 3A62646929C9E385007E8C07 /* MockPostHogServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPostHogServer.swift; sourceTree = ""; }; - 3A62647029CAF67B007E8C07 /* SessionManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManagerTest.swift; sourceTree = ""; }; - 3A62647229CB0043007E8C07 /* CaptureTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaptureTests.swift; sourceTree = ""; }; + 3A62647029CAF67B007E8C07 /* PostHogSessionManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSessionManagerTest.swift; sourceTree = ""; }; 3A62647429CB0168007E8C07 /* TestPostHog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestPostHog.swift; sourceTree = ""; }; 3AA34CF7296D951A003398F4 /* PostHogExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PostHogExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3AA34CF9296D951A003398F4 /* PostHogExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogExampleApp.swift; sourceTree = ""; }; @@ -154,7 +154,6 @@ 3AA34D00296D951B003398F4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 3AA34D16296D9993003398F4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 3AAFB13129C0C699004F485B /* Package.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; - 3AB7330C29E420E400C8AA71 /* FeatureFlagsTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlagsTest.swift; sourceTree = ""; }; 3AC745B5296D6FE60025C109 /* PostHog.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PostHog.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3AC745B8296D6FE60025C109 /* PostHog.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PostHog.h; sourceTree = ""; }; 3AC745BF296D6FE60025C109 /* PostHogTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PostHogTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -166,14 +165,22 @@ 3AE3FB422992985A00AFFC18 /* Reachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reachability.swift; sourceTree = ""; }; 3AE3FB462992AB0000AFFC18 /* Hedgelog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hedgelog.swift; sourceTree = ""; }; 3AE3FB48299391DF00AFFC18 /* PostHogStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogStorage.swift; sourceTree = ""; }; - 3AE3FB4A2993A68500AFFC18 /* StorageTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageTest.swift; sourceTree = ""; }; + 3AE3FB4A2993A68500AFFC18 /* PostHogStorageTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogStorageTest.swift; sourceTree = ""; }; 3AE3FB4D2993D1D600AFFC18 /* PostHogSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSessionManager.swift; sourceTree = ""; }; 690FF02F2AE7C5BA00A0B06B /* PostHogExampleWithPods.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PostHogExampleWithPods.xcodeproj; path = PostHogExampleWithPods/PostHogExampleWithPods.xcodeproj; sourceTree = ""; }; 690FF0532AE7DB3700A0B06B /* PostHogExampleWithSPM.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PostHogExampleWithSPM.xcodeproj; path = PostHogExampleWithSPM/PostHogExampleWithSPM.xcodeproj; sourceTree = ""; }; 690FF05E2AE7E2D400A0B06B /* Data+Gzip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Gzip.swift"; sourceTree = ""; }; - 690FF0B42AEBBD3C00A0B06B /* DictUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictUtils.swift; sourceTree = ""; }; 690FF0AE2AEB9C1400A0B06B /* DateUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUtils.swift; sourceTree = ""; }; + 690FF0B42AEBBD3C00A0B06B /* DictUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictUtils.swift; sourceTree = ""; }; + 690FF0BA2AEF8B8200A0B06B /* PostHogContextTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogContextTest.swift; sourceTree = ""; }; + 690FF0BC2AEF93F400A0B06B /* PostHogFeatureFlagsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogFeatureFlagsTest.swift; sourceTree = ""; }; + 690FF0BE2AEFA97F00A0B06B /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; }; 690FF0C42AEFAE8200A0B06B /* PostHogLegacyQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogLegacyQueue.swift; sourceTree = ""; }; + 690FF0DE2AEFBC5700A0B06B /* PostHogLegacyQueueTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogLegacyQueueTest.swift; sourceTree = ""; }; + 690FF0E02AEFC59100A0B06B /* PostHogFileBackedQueueTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogFileBackedQueueTest.swift; sourceTree = ""; }; + 690FF0E22AEFD12900A0B06B /* PostHogConfigTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogConfigTest.swift; sourceTree = ""; }; + 690FF0E82AEFD3BD00A0B06B /* PostHogQueueTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogQueueTest.swift; sourceTree = ""; }; + 690FF0F42AF0F06100A0B06B /* PostHogSDKTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSDKTest.swift; sourceTree = ""; }; 69261D122AD5685B00232EC7 /* PostHogFeatureFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogFeatureFlags.swift; sourceTree = ""; }; 69261D182AD9673500232EC7 /* PostHogBatchUploadInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogBatchUploadInfo.swift; sourceTree = ""; }; 69261D1A2AD9678C00232EC7 /* PostHogEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogEvent.swift; sourceTree = ""; }; @@ -217,11 +224,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3A580B3F29E481F200C5C6F3 /* OHHTTPStubs in Frameworks */, - 3A580B4129E481F200C5C6F3 /* OHHTTPStubsSwift in Frameworks */, - 3A867B7329C1DFEF009D0852 /* Nimble in Frameworks */, - 3A62646729C9E36B007E8C07 /* Shock in Frameworks */, - 3A867B7029C1DF73009D0852 /* Quick in Frameworks */, + 690FF0F12AEFF24200A0B06B /* OHHTTPStubs in Frameworks */, + 690FF0ED2AEFF23300A0B06B /* Nimble in Frameworks */, + 690FF0EF2AEFF23D00A0B06B /* OHHTTPStubsSwift in Frameworks */, + 690FF0EB2AEFF22F00A0B06B /* Quick in Frameworks */, 3AC745C0296D6FE60025C109 /* PostHog.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -285,6 +291,7 @@ 3A0F108429C9ABB6002C0084 /* ReadWriteLock.swift */, 3A0F108829C9BD76002C0084 /* Errors.swift */, 690FF05E2AE7E2D400A0B06B /* Data+Gzip.swift */, + 690FF0BE2AEFA97F00A0B06B /* FileUtils.swift */, 690FF0B42AEBBD3C00A0B06B /* DictUtils.swift */, 690FF0AE2AEB9C1400A0B06B /* DateUtils.swift */, ); @@ -346,12 +353,15 @@ isa = PBXGroup; children = ( 3A62646829C9E37A007E8C07 /* TestUtils */, - 3AE3FB4A2993A68500AFFC18 /* StorageTest.swift */, - 3AB7330C29E420E400C8AA71 /* FeatureFlagsTest.swift */, - 3A2BCF4B299E4E35008BB5F3 /* QueueTest.swift */, - 3A62646329C9E0E7007E8C07 /* PostHogTest.swift */, - 3A62647229CB0043007E8C07 /* CaptureTests.swift */, - 3A62647029CAF67B007E8C07 /* SessionManagerTest.swift */, + 3AE3FB4A2993A68500AFFC18 /* PostHogStorageTest.swift */, + 3A62647029CAF67B007E8C07 /* PostHogSessionManagerTest.swift */, + 690FF0BA2AEF8B8200A0B06B /* PostHogContextTest.swift */, + 690FF0BC2AEF93F400A0B06B /* PostHogFeatureFlagsTest.swift */, + 690FF0DE2AEFBC5700A0B06B /* PostHogLegacyQueueTest.swift */, + 690FF0E02AEFC59100A0B06B /* PostHogFileBackedQueueTest.swift */, + 690FF0E22AEFD12900A0B06B /* PostHogConfigTest.swift */, + 690FF0E82AEFD3BD00A0B06B /* PostHogQueueTest.swift */, + 690FF0F42AF0F06100A0B06B /* PostHogSDKTest.swift */, ); path = PostHogTests; sourceTree = ""; @@ -474,11 +484,10 @@ ); name = PostHogTests; packageProductDependencies = ( - 3A867B6F29C1DF73009D0852 /* Quick */, - 3A867B7229C1DFEF009D0852 /* Nimble */, - 3A62646629C9E36B007E8C07 /* Shock */, - 3A580B3E29E481F200C5C6F3 /* OHHTTPStubs */, - 3A580B4029E481F200C5C6F3 /* OHHTTPStubsSwift */, + 690FF0EA2AEFF22F00A0B06B /* Quick */, + 690FF0EC2AEFF23300A0B06B /* Nimble */, + 690FF0EE2AEFF23D00A0B06B /* OHHTTPStubsSwift */, + 690FF0F02AEFF24200A0B06B /* OHHTTPStubs */, ); productName = PostHogTests; productReference = 3AC745BF296D6FE60025C109 /* PostHogTests.xctest */; @@ -638,6 +647,7 @@ files = ( 690FF05F2AE7E2D400A0B06B /* Data+Gzip.swift in Sources */, 69261D1F2AD9681300232EC7 /* PostHogConsumerPayload.swift in Sources */, + 690FF0BF2AEFA97F00A0B06B /* FileUtils.swift in Sources */, 69261D252AD9787A00232EC7 /* PostHogExtensions.swift in Sources */, 3AE3FB4E2993D1D600AFFC18 /* PostHogSessionManager.swift in Sources */, 3AE3FB49299391DF00AFFC18 /* PostHogStorage.swift in Sources */, @@ -666,15 +676,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 690FF0F52AF0F06100A0B06B /* PostHogSDKTest.swift in Sources */, + 690FF0E12AEFC59100A0B06B /* PostHogFileBackedQueueTest.swift in Sources */, 3A62647529CB0168007E8C07 /* TestPostHog.swift in Sources */, 3A62646A29C9E385007E8C07 /* MockPostHogServer.swift in Sources */, - 3A62647129CAF67B007E8C07 /* SessionManagerTest.swift in Sources */, - 3AE3FB4B2993A68500AFFC18 /* StorageTest.swift in Sources */, + 690FF0BB2AEF8B8200A0B06B /* PostHogContextTest.swift in Sources */, + 690FF0E32AEFD12900A0B06B /* PostHogConfigTest.swift in Sources */, + 3A62647129CAF67B007E8C07 /* PostHogSessionManagerTest.swift in Sources */, + 690FF0DF2AEFBC5700A0B06B /* PostHogLegacyQueueTest.swift in Sources */, + 690FF0BD2AEF93F400A0B06B /* PostHogFeatureFlagsTest.swift in Sources */, + 690FF0E92AEFD3BD00A0B06B /* PostHogQueueTest.swift in Sources */, + 3AE3FB4B2993A68500AFFC18 /* PostHogStorageTest.swift in Sources */, 3A580B4329E489D000C5C6F3 /* URLSession+body.swift in Sources */, - 3A62646429C9E0E7007E8C07 /* PostHogTest.swift in Sources */, - 3A62647329CB0043007E8C07 /* CaptureTests.swift in Sources */, - 3A2BCF4C299E4E35008BB5F3 /* QueueTest.swift in Sources */, - 3AB7330D29E420E400C8AA71 /* FeatureFlagsTest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1201,31 +1214,26 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 3A580B3E29E481F200C5C6F3 /* OHHTTPStubs */ = { - isa = XCSwiftPackageProductDependency; - package = 3A580B3D29E481F200C5C6F3 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */; - productName = OHHTTPStubs; - }; - 3A580B4029E481F200C5C6F3 /* OHHTTPStubsSwift */ = { - isa = XCSwiftPackageProductDependency; - package = 3A580B3D29E481F200C5C6F3 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */; - productName = OHHTTPStubsSwift; - }; - 3A62646629C9E36B007E8C07 /* Shock */ = { - isa = XCSwiftPackageProductDependency; - package = 3A62646529C9E36B007E8C07 /* XCRemoteSwiftPackageReference "Shock" */; - productName = Shock; - }; - 3A867B6F29C1DF73009D0852 /* Quick */ = { + 690FF0EA2AEFF22F00A0B06B /* Quick */ = { isa = XCSwiftPackageProductDependency; package = 3A867B6E29C1DF73009D0852 /* XCRemoteSwiftPackageReference "Quick" */; productName = Quick; }; - 3A867B7229C1DFEF009D0852 /* Nimble */ = { + 690FF0EC2AEFF23300A0B06B /* Nimble */ = { isa = XCSwiftPackageProductDependency; package = 3A867B7129C1DFEF009D0852 /* XCRemoteSwiftPackageReference "Nimble" */; productName = Nimble; }; + 690FF0EE2AEFF23D00A0B06B /* OHHTTPStubsSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 3A580B3D29E481F200C5C6F3 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */; + productName = OHHTTPStubsSwift; + }; + 690FF0F02AEFF24200A0B06B /* OHHTTPStubs */ = { + isa = XCSwiftPackageProductDependency; + package = 3A580B3D29E481F200C5C6F3 /* XCRemoteSwiftPackageReference "OHHTTPStubs" */; + productName = OHHTTPStubs; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 3AC745AC296D6FE60025C109 /* Project object */; diff --git a/PostHog/PostHogApi.swift b/PostHog/PostHogApi.swift index 16c3cf543..9a96f0c0a 100644 --- a/PostHog/PostHogApi.swift +++ b/PostHog/PostHogApi.swift @@ -49,7 +49,7 @@ class PostHogApi { do { data = try JSONSerialization.data(withJSONObject: toSend) - } catch let error as NSError { + } catch { return completion(PostHogBatchUploadInfo(statusCode: nil, error: error)) } @@ -109,7 +109,7 @@ class PostHogApi { do { data = try JSONSerialization.data(withJSONObject: toSend) - } catch let error as NSError { + } catch { return completion(nil, error) } @@ -128,7 +128,7 @@ class PostHogApi { do { let jsonData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any] completion(jsonData, nil) - } catch let error as NSError { + } catch { completion(nil, error) } }.resume() diff --git a/PostHog/PostHogConfig.swift b/PostHog/PostHogConfig.swift index bcb2cc0a1..eb171de22 100644 --- a/PostHog/PostHogConfig.swift +++ b/PostHog/PostHogConfig.swift @@ -28,6 +28,10 @@ import Foundation @objc public var optOut: Bool = false public static let defaultHost: String = "https://app.posthog.com" + // only internal + var disableReachabilityForTesting: Bool = false + var disableQueueTimerForTesting: Bool = false + @objc(apiKey:) public init( apiKey: String diff --git a/PostHog/PostHogContext.swift b/PostHog/PostHogContext.swift index 456f9184b..9c7f84447 100644 --- a/PostHog/PostHogContext.swift +++ b/PostHog/PostHogContext.swift @@ -61,7 +61,6 @@ class PostHogContext { if deviceType != nil { properties["$device_type"] = deviceType } - #endif return properties @@ -87,8 +86,8 @@ class PostHogContext { var properties: [String: Any] = [:] #if os(iOS) || os(tvOS) - properties["$screen_width"] = UIScreen.main.bounds.width - properties["$screen_height"] = UIScreen.main.bounds.height + properties["$screen_width"] = Float(UIScreen.main.bounds.width) + properties["$screen_height"] = Float(UIScreen.main.bounds.height) #endif properties["$lib"] = "posthog-ios" diff --git a/PostHog/PostHogFeatureFlags.swift b/PostHog/PostHogFeatureFlags.swift index 303b4a785..2b7d27f8c 100644 --- a/PostHog/PostHogFeatureFlags.swift +++ b/PostHog/PostHogFeatureFlags.swift @@ -16,9 +16,13 @@ class PostHogFeatureFlags { private let featureFlagsLock = NSLock() private var isLoadingFeatureFlags = false - private let dispatchQueue = DispatchQueue(label: "com.posthog.FeatureFlags", target: .global(qos: .utility)) + private let dispatchQueue = DispatchQueue(label: "com.posthog.FeatureFlags", + target: .global(qos: .utility)) - init(_ config: PostHogConfig, _ storage: PostHogStorage, _ api: PostHogApi) { + init(_ config: PostHogConfig, + _ storage: PostHogStorage, + _ api: PostHogApi) + { self.config = config self.storage = storage self.api = api @@ -52,7 +56,9 @@ class PostHogFeatureFlags { let featureFlagPayloads = data?["featureFlagPayloads"] as? [String: Any] else { hedgeLog("Error: Decide response missing correct featureFlags format") - self.setLoading(false) + + self.notifyAndRelease() + return callback() } let errorsWhileComputingFlags = data?["errorsWhileComputingFlags"] as? Bool ?? false @@ -74,17 +80,21 @@ class PostHogFeatureFlags { } } - DispatchQueue.main.async { - NotificationCenter.default.post(name: PostHogSDK.didReceiveFeatureFlags, object: nil) - } - - self.setLoading(false) + self.notifyAndRelease() return callback() } } } + private func notifyAndRelease() { + DispatchQueue.main.async { + NotificationCenter.default.post(name: PostHogSDK.didReceiveFeatureFlags, object: nil) + } + + setLoading(false) + } + func getFeatureFlags() -> [String: Any]? { var flags: [String: Any]? featureFlagsLock.withLock { @@ -103,9 +113,9 @@ class PostHogFeatureFlags { let value = flags?[key] if value != nil { - let boolValue = value as? Bool ?? false - if boolValue { - return boolValue + let boolValue = value as? Bool + if boolValue != nil { + return boolValue! } else { return true } @@ -135,14 +145,15 @@ class PostHogFeatureFlags { return value } - // The payload value is stored as a string and is not pre-parsed... - // We need to mimic the JSON.parse of JS which is what posthog-js uses - let jsonData = try? JSONSerialization.jsonObject(with: stringValue.data(using: .utf8)!, options: .fragmentsAllowed) - - if jsonData == nil { - return value + do { + // The payload value is stored as a string and is not pre-parsed... + // We need to mimic the JSON.parse of JS which is what posthog-js uses + return try JSONSerialization.jsonObject(with: stringValue.data(using: .utf8)!, options: .fragmentsAllowed) + } catch { + hedgeLog("Error parsing the object \(String(describing: value)): \(error)") } - return jsonData + // fallbak to original value if not possible to serialize + return value } } diff --git a/PostHog/PostHogFileBackedQueue.swift b/PostHog/PostHogFileBackedQueue.swift index 9539a338f..bb47a7873 100644 --- a/PostHog/PostHogFileBackedQueue.swift +++ b/PostHog/PostHogFileBackedQueue.swift @@ -8,7 +8,7 @@ import Foundation class PostHogFileBackedQueue { - private let queue: URL + let queue: URL @ReadWriteLock private var items = [String]() @@ -22,8 +22,10 @@ class PostHogFileBackedQueue { } private func setup(oldQueue: URL?) { - if !FileManager.default.fileExists(atPath: queue.path) { - try? FileManager.default.createDirectory(atPath: queue.path, withIntermediateDirectories: true) + do { + try FileManager.default.createDirectory(atPath: queue.path, withIntermediateDirectories: true) + } catch { + hedgeLog("Error trying to create caching folder \(error)") } if oldQueue != nil { @@ -34,7 +36,7 @@ class PostHogFileBackedQueue { items = try FileManager.default.contentsOfDirectory(atPath: queue.path) items.sort { Double($0)! < Double($1)! } } catch { - hedgeLog("Failed to load files for queue") + hedgeLog("Failed to load files for queue \(error)") // failed to read directory – bad permissions, perhaps? } } @@ -46,13 +48,12 @@ class PostHogFileBackedQueue { func delete(index: Int) { if items.isEmpty { return } let removed = items.remove(at: index) - try? FileManager.default.removeItem(at: queue.appendingPathComponent(removed)) + + deleteSafely(queue.appendingPathComponent(removed)) } - func pop(_ count: Int) -> [Data] { - let result = loadFiles(count) + func pop(_ count: Int) { deleteFiles(count) - return result } func add(_ contents: Data) { @@ -61,14 +62,12 @@ class PostHogFileBackedQueue { try contents.write(to: queue.appendingPathComponent(filename)) items.append(filename) } catch { - hedgeLog("Could not write file") + hedgeLog("Could not write file \(error)") } } func clear() { - if FileManager.default.fileExists(atPath: queue.path) { - try? FileManager.default.removeItem(at: queue) - } + deleteSafely(queue) setup(oldQueue: nil) } @@ -76,14 +75,21 @@ class PostHogFileBackedQueue { var results = [Data]() for item in items { - let itemPath = queue.appendingPathComponent(item) - guard let contents = try? Data(contentsOf: itemPath) else { - try? FileManager.default.removeItem(at: itemPath) - hedgeLog("File \(itemPath) is corrupted") - continue + let itemURL = queue.appendingPathComponent(item) + do { + if !FileManager.default.fileExists(atPath: itemURL.path) { + hedgeLog("File \(itemURL) does not exist") + continue + } + let contents = try Data(contentsOf: itemURL) + + results.append(contents) + } catch { + hedgeLog("File \(itemURL) is corrupted \(error)") + + deleteSafely(itemURL) } - results.append(contents) if results.count == count { return results } @@ -96,7 +102,8 @@ class PostHogFileBackedQueue { for _ in 0 ..< count { if items.isEmpty { return } let removed = items.remove(at: 0) // We always remove from the top of the queue - try? FileManager.default.removeItem(at: queue.appendingPathComponent(removed)) + + deleteSafely(queue.appendingPathComponent(removed)) } } } diff --git a/PostHog/PostHogLegacyQueue.swift b/PostHog/PostHogLegacyQueue.swift index 2bf0c7a9a..f0938c7b7 100644 --- a/PostHog/PostHogLegacyQueue.swift +++ b/PostHog/PostHogLegacyQueue.swift @@ -13,11 +13,8 @@ func migrateOldQueue(queue: URL, oldQueue: URL) { return } - var deleteFiles = false defer { - if deleteFiles { - try? FileManager.default.removeItem(at: oldQueue) - } + deleteSafely(oldQueue) } do { @@ -25,7 +22,6 @@ func migrateOldQueue(queue: URL, oldQueue: URL) { let array = try JSONSerialization.jsonObject(with: data) as? [Any] if array == nil { - deleteFiles = true return } @@ -39,16 +35,11 @@ func migrateOldQueue(queue: URL, oldQueue: URL) { let filename = "\(timestampDate.timeIntervalSince1970)" - let contents = try? JSONSerialization.data(withJSONObject: event) - - if contents == nil { - continue - } - try? contents!.write(to: queue.appendingPathComponent(filename)) + let contents = try JSONSerialization.data(withJSONObject: event) - deleteFiles = true + try contents.write(to: queue.appendingPathComponent(filename)) } } catch { - deleteFiles = false + hedgeLog("Failed to migrate queue \(error)") } } diff --git a/PostHog/PostHogQueue.swift b/PostHog/PostHogQueue.swift index ff0035b68..187a8a102 100644 --- a/PostHog/PostHogQueue.swift +++ b/PostHog/PostHogQueue.swift @@ -51,8 +51,6 @@ class PostHogQueue { private func eventHandler(_ payload: PostHogConsumerPayload) { hedgeLog("Sending batch of \(payload.events.count) events to PostHog") - hedgeLog("Events: \(payload.events.map(\.event).joined(separator: ","))") - api.batch(events: payload.events) { result in // -1 means its not anything related to the API but rather network or something else, so we try again let statusCode = result.statusCode ?? -1 @@ -75,41 +73,51 @@ class PostHogQueue { } } - func start() { - // Setup the monitoring of network status for the queue - reachability?.whenReachable = { reachability in - self.pausedLock.withLock { - if self.config.dataMode == .wifi, reachability.connection != .wifi { - hedgeLog("Queue is paused because its not in WiFi mode") - self.paused = true - } else { - self.paused = false + func start(disableReachabilityForTesting: Bool, + disableQueueTimerForTesting: Bool) + { + if !disableReachabilityForTesting { + // Setup the monitoring of network status for the queue + reachability?.whenReachable = { reachability in + self.pausedLock.withLock { + if self.config.dataMode == .wifi, reachability.connection != .wifi { + hedgeLog("Queue is paused because its not in WiFi mode") + self.paused = true + } else { + self.paused = false + } } - } - // Always trigger a flush when we are on wifi - if reachability.connection == .wifi { - self.flush() + // Always trigger a flush when we are on wifi + if reachability.connection == .wifi { + if !self.isFlushing { + self.flush() + } + } } - } - reachability?.whenUnreachable = { _ in - self.pausedLock.withLock { - hedgeLog("Queue is paused because network is unreachable") - self.paused = true + reachability?.whenUnreachable = { _ in + self.pausedLock.withLock { + hedgeLog("Queue is paused because network is unreachable") + self.paused = true + } } - } - do { - try reachability?.startNotifier() - } catch { - hedgeLog("Error: Unable to monitor network reachability") + do { + try reachability?.startNotifier() + } catch { + hedgeLog("Error: Unable to monitor network reachability") + } } - timerLock.withLock { - timer = Timer.scheduledTimer(withTimeInterval: config.flushIntervalSeconds, repeats: true, block: { _ in - self.flush() - }) + if !disableQueueTimerForTesting { + timerLock.withLock { + timer = Timer.scheduledTimer(withTimeInterval: config.flushIntervalSeconds, repeats: true, block: { _ in + if !self.isFlushing { + self.flush() + } + }) + } } } @@ -125,7 +133,10 @@ class PostHogQueue { } func flush() { - if !canFlush() { return } + if !canFlush() { + hedgeLog("Already flushing") + return + } take(config.maxBatchSize) { payload in self.eventHandler(payload) @@ -139,11 +150,15 @@ class PostHogQueue { } func add(_ event: PostHogEvent) { - guard let data = try? JSONSerialization.data(withJSONObject: event.toJSON()) else { - hedgeLog("Tried to queue unserialisable PostHogEvent") + var data: Data? + do { + data = try JSONSerialization.data(withJSONObject: event.toJSON()) + } catch { + hedgeLog("Tried to queue unserialisable PostHogEvent \(error)") return } - fileQueue.add(data) + + fileQueue.add(data!) hedgeLog("Queued event '\(event.event)'. Depth: \(fileQueue.depth)") flushIfOverThreshold() } @@ -157,36 +172,22 @@ class PostHogQueue { self.isFlushing = true } - let datas = self.fileQueue.peek(count) + let items = self.fileQueue.peek(count) var processing = [PostHogEvent]() - var deleteIndexes = [Int]() - for (index, data) in datas.enumerated() { + + for item in items { // each element is a PostHogEvent if fromJSON succeeds - guard let event = PostHogEvent.fromJSON(data) else { - deleteIndexes.append(index) + guard let event = PostHogEvent.fromJSON(item) else { continue } processing.append(event) } - // delete files that aren't valid as a event JSON - if !deleteIndexes.isEmpty { - for index in deleteIndexes { - // TOOD: check if the indexes dont move and delete the wrong files - // might need an array of file paths instead of the data, so we know what to delete correctly - self.fileQueue.delete(index: index) - } - } - - if processing.isEmpty { - return - } - completion(PostHogConsumerPayload(events: processing) { success in hedgeLog("Completed!") if success { - _ = self.fileQueue.pop(processing.count) + self.fileQueue.pop(items.count) } self.isFlushingLock.withLock { diff --git a/PostHog/PostHogSDK.swift b/PostHog/PostHogSDK.swift index b66bb74c7..5c15dcab5 100644 --- a/PostHog/PostHogSDK.swift +++ b/PostHog/PostHogSDK.swift @@ -77,7 +77,7 @@ let maxRetryDelay = 30.0 let theApi = PostHogApi(config) api = theApi featureFlags = PostHogFeatureFlags(config, theStorage, theApi) - sessionManager = PostHogSessionManager(config: config) + sessionManager = PostHogSessionManager(config) do { reachability = try Reachability() } catch { @@ -92,7 +92,8 @@ let maxRetryDelay = 30.0 queue = PostHogQueue(config, theStorage, theApi, reachability) - queue?.start() + queue?.start(disableReachabilityForTesting: config.disableReachabilityForTesting, + disableQueueTimerForTesting: config.disableQueueTimerForTesting) registerNotifications() captureScreenViews() @@ -126,24 +127,31 @@ let maxRetryDelay = 30.0 // EVENT CAPTURE private func dynamicContext() -> [String: Any] { - var properties: [String: Any] = [:] + var properties = getRegisteredProperties() var groups: [String: String]? groupsLock.withLock { groups = getGroups() } - properties["$groups"] = groups ?? [:] + if groups != nil, !groups!.isEmpty { + properties["$groups"] = groups! + } guard let flags = featureFlags?.getFeatureFlags() as? [String: Any] else { - return [:] + return properties } var keys: [String] = [] for (key, value) in flags { properties["$feature/\(key)"] = value - let boolValue = value as? Bool ?? false - let active = boolValue ? boolValue : true + var active = true + let boolValue = value as? Bool + if boolValue != nil { + active = boolValue! + } else { + active = true + } if active { keys.append(key) @@ -157,7 +165,7 @@ let maxRetryDelay = 30.0 return properties } - private func buildProperties(properties _: [String: Any]?, + private func buildProperties(properties: [String: Any]?, userProperties: [String: Any]? = nil, userPropertiesSetOnce: [String: Any]? = nil, groupProperties: [String: Any]? = nil) -> [String: Any] @@ -182,8 +190,13 @@ let maxRetryDelay = 30.0 props["$set_once"] = (userPropertiesSetOnce ?? [:]) } if groupProperties != nil { - props["$groups"] = (groupProperties ?? [:]) + // $groups are also set via the dynamicContext + let currentGroups = props["$groups"] as? [String: Any] ?? [:] + let mergedGroups = currentGroups.merging(groupProperties ?? [:]) { current, _ in current } + props["$groups"] = mergedGroups } + props = props.merging(properties ?? [:]) { current, _ in current } + return props } @@ -213,7 +226,7 @@ let maxRetryDelay = 30.0 } private func getRegisteredProperties() -> [String: Any] { - guard let props = storage?.getDictionary(forKey: .registerProperties) as? [String: String] else { + guard let props = storage?.getDictionary(forKey: .registerProperties) as? [String: Any] else { return [:] } return props @@ -452,9 +465,7 @@ let maxRetryDelay = 30.0 _ = groups([type: key]) - if groupProperties != nil { - groupIdentify(type: type, key: key, groupProperties: sanitizeDicionary(groupProperties)) - } + groupIdentify(type: type, key: key, groupProperties: sanitizeDicionary(groupProperties)) } // FEATURE FLAGS @@ -513,7 +524,13 @@ let maxRetryDelay = 30.0 return false } - return featureFlags.isFeatureEnabled(key) + let value = featureFlags.isFeatureEnabled(key) + + if config.sendFeatureFlagEvent { + reportFeatureFlagCalled(flagKey: key, flagValue: value) + } + + return value } @objc public func getFeatureFlagPayload(_ key: String) -> Any? { @@ -626,11 +643,13 @@ let maxRetryDelay = 30.0 private func captureScreenViews() { if config.captureScreenViews { - UIViewController.swizzleScreenView() + #if os(iOS) || os(tvOS) + UIViewController.swizzleScreenView() + #endif } } - private func captureAppInstalled() { + func captureAppInstalled() { let bundle = Bundle.main let versionName = bundle.infoDictionary?["CFBundleShortVersionString"] as? String @@ -685,7 +704,7 @@ let maxRetryDelay = 30.0 } } - private func captureAppOpened() { + func captureAppOpened() { var props: [String: Any] = [:] props["from_background"] = false @@ -701,13 +720,10 @@ let maxRetryDelay = 30.0 props["build"] = versionCode } -// props["referring_application"] = launchOptions[UIApplicationLaunchOptionsSourceApplicationKey] -// props["url"] = launchOptions[UIApplicationLaunchOptionsURLKey] - capture("Application Opened", properties: props) } - @objc private func captureAppOpenedFromBackground() { + @objc func captureAppOpenedFromBackground() { var props: [String: Any] = [:] props["from_background"] = appFromBackground @@ -727,7 +743,7 @@ let maxRetryDelay = 30.0 captureAppOpened() } - @objc private func captureAppBackgrounded() { + @objc func captureAppBackgrounded() { if !config.captureApplicationLifecycleEvents { return } diff --git a/PostHog/PostHogSessionManager.swift b/PostHog/PostHogSessionManager.swift index 619ef9d2e..0c771f524 100644 --- a/PostHog/PostHogSessionManager.swift +++ b/PostHog/PostHogSessionManager.swift @@ -12,7 +12,7 @@ class PostHogSessionManager { private let anonLock = NSLock() private let distinctLock = NSLock() - init(config: PostHogConfig) { + init(_ config: PostHogConfig) { storage = PostHogStorage(config) } @@ -53,4 +53,13 @@ class PostHogSessionManager { storage.setString(forKey: .distinctId, contents: id) } } + + public func reset() { + distinctLock.withLock { + storage.remove(key: .distinctId) + } + anonLock.withLock { + storage.remove(key: .anonymousId) + } + } } diff --git a/PostHog/PostHogStorage.swift b/PostHog/PostHogStorage.swift index ff53cb14f..e80754448 100644 --- a/PostHog/PostHogStorage.swift +++ b/PostHog/PostHogStorage.swift @@ -46,11 +46,11 @@ class PostHogStorage { } private func createDirectoryAtURLIfNeeded(url: URL) { - if FileManager.default.fileExists(atPath: url.path, isDirectory: nil) { return } + if FileManager.default.fileExists(atPath: url.path) { return } do { try FileManager.default.createDirectory(atPath: url.path, withIntermediateDirectories: true) } catch { - hedgeLog("Error creating storage directory: \(error.localizedDescription)") + hedgeLog("Error creating storage directory: \(error)") } } @@ -64,11 +64,13 @@ class PostHogStorage { let url = url(forKey: forKey) do { - let data = try Data(contentsOf: url) - return data + if FileManager.default.fileExists(atPath: url.path) { + return try Data(contentsOf: url) + } } catch { - return nil + hedgeLog("Error reading data from key \(forKey): \(error)") } + return nil } private func setData(forKey: StorageKey, contents: Data?) { @@ -76,7 +78,7 @@ class PostHogStorage { do { if contents == nil { - try FileManager.default.removeItem(at: url) + deleteSafely(url) return } @@ -84,16 +86,21 @@ class PostHogStorage { var resourceValues = URLResourceValues() resourceValues.isExcludedFromBackup = true - try? url.setResourceValues(resourceValues) - + try url.setResourceValues(resourceValues) } catch { - hedgeLog("Failed to write data for key '\(forKey)' error: \(error.localizedDescription)") + hedgeLog("Failed to write data for key '\(forKey)' error: \(error)") } } private func getJson(forKey key: StorageKey) -> Any? { guard let data = getData(forKey: key) else { return nil } - return try? JSONSerialization.jsonObject(with: data) + + do { + return try JSONSerialization.jsonObject(with: data) + } catch { + hedgeLog("Failed to serialize key '\(key)' error: \(error)") + } + return nil } private func setJson(forKey key: StorageKey, json: Any) { @@ -108,26 +115,24 @@ class PostHogStorage { jsonObject = [key.rawValue: json] } - let data = try? JSONSerialization.data(withJSONObject: jsonObject!) + var data: Data? + do { + data = try JSONSerialization.data(withJSONObject: jsonObject!) + } catch { + hedgeLog("Failed to serialize key '\(key)' error: \(error)") + } setData(forKey: key, contents: data) } public func reset() { - do { - try FileManager.default.removeItem(at: appFolderUrl) - createDirectoryAtURLIfNeeded(url: appFolderUrl) - } catch { - hedgeLog("Failed to reset storage folder, error: \(error.localizedDescription)") - } + deleteSafely(appFolderUrl) + createDirectoryAtURLIfNeeded(url: appFolderUrl) } public func remove(key: StorageKey) { let url = url(forKey: key) - do { - try FileManager.default.removeItem(at: url) - } catch { - hedgeLog("Failed to remove key '\(key)', error: \(error.localizedDescription)") - } + + deleteSafely(url) } public func getString(forKey key: StorageKey) -> String? { @@ -144,20 +149,6 @@ class PostHogStorage { setJson(forKey: key, json: contents) } - public func getNumber(forKey key: StorageKey) -> Double? { - let value = getJson(forKey: key) - if let doubleValue = value as? Double { - return doubleValue - } else if let dictValue = value as? [String: Double] { - return dictValue[key.rawValue] - } - return nil - } - - public func setNumber(forKey key: StorageKey, contents: Double) { - setJson(forKey: key, json: contents) - } - public func getDictionary(forKey key: StorageKey) -> [AnyHashable: Any]? { getJson(forKey: key) as? [AnyHashable: Any] } @@ -166,16 +157,14 @@ class PostHogStorage { setJson(forKey: key, json: contents) } - public func getArray(forKey key: StorageKey) -> [Any]? { - getJson(forKey: key) as? [Any] - } - - public func setArray(forKey key: StorageKey, contents: [Any]) { - setJson(forKey: key, json: contents) - } - public func getBool(forKey key: StorageKey) -> Bool? { - getJson(forKey: key) as? Bool + let value = getJson(forKey: key) + if let boolValue = value as? Bool { + return boolValue + } else if let dictValue = value as? [String: Bool] { + return dictValue[key.rawValue] + } + return nil } public func setBool(forKey key: StorageKey, contents: Bool) { diff --git a/PostHog/UIViewController.swift b/PostHog/UIViewController.swift index 51c18480e..087401937 100644 --- a/PostHog/UIViewController.swift +++ b/PostHog/UIViewController.swift @@ -9,78 +9,80 @@ // import Foundation -import UIKit +#if os(iOS) || os(tvOS) + import UIKit -extension UIViewController { - static func swizzle(forClass: AnyClass, original: Selector, new: Selector) { - guard let originalMethod = class_getInstanceMethod(forClass, original) else { return } - guard let swizzledMethod = class_getInstanceMethod(forClass, new) else { return } - method_exchangeImplementations(originalMethod, swizzledMethod) - } + extension UIViewController { + static func swizzle(forClass: AnyClass, original: Selector, new: Selector) { + guard let originalMethod = class_getInstanceMethod(forClass, original) else { return } + guard let swizzledMethod = class_getInstanceMethod(forClass, new) else { return } + method_exchangeImplementations(originalMethod, swizzledMethod) + } - static func swizzleScreenView() { - UIViewController.swizzle(forClass: UIViewController.self, - original: #selector(UIViewController.viewDidAppear(_:)), - new: #selector(UIViewController.viewDidApperOverride)) - } + static func swizzleScreenView() { + UIViewController.swizzle(forClass: UIViewController.self, + original: #selector(UIViewController.viewDidAppear(_:)), + new: #selector(UIViewController.viewDidApperOverride)) + } - private func activeController() -> UIViewController? { - // if a view is being dismissed, this will return nil - if let root = viewIfLoaded?.window?.rootViewController { - return root - } else if #available(iOS 13.0, *) { - // preferred way to get active controller in ios 13+ - for scene in UIApplication.shared.connectedScenes where scene.activationState == .foregroundActive { - let windowScene = scene as? UIWindowScene - let sceneDelegate = windowScene?.delegate as? UIWindowSceneDelegate - if let target = sceneDelegate, let window = target.window { - return window?.rootViewController + private func activeController() -> UIViewController? { + // if a view is being dismissed, this will return nil + if let root = viewIfLoaded?.window?.rootViewController { + return root + } else if #available(iOS 13.0, *) { + // preferred way to get active controller in ios 13+ + for scene in UIApplication.shared.connectedScenes where scene.activationState == .foregroundActive { + let windowScene = scene as? UIWindowScene + let sceneDelegate = windowScene?.delegate as? UIWindowSceneDelegate + if let target = sceneDelegate, let window = target.window { + return window?.rootViewController + } } + } else { + // this was deprecated in ios 13.0 + return UIApplication.shared.keyWindow?.rootViewController } - } else { - // this was deprecated in ios 13.0 - return UIApplication.shared.keyWindow?.rootViewController + return nil } - return nil - } - private func captureScreenView() { - var rootController = viewIfLoaded?.window?.rootViewController - if rootController == nil { - rootController = activeController() - } - guard let top = findVisibleViewController(activeController()) else { return } + private func captureScreenView() { + var rootController = viewIfLoaded?.window?.rootViewController + if rootController == nil { + rootController = activeController() + } + guard let top = findVisibleViewController(activeController()) else { return } - var name = String(describing: top.classForCoder).replacingOccurrences(of: "ViewController", with: "") + var name = String(describing: top.classForCoder).replacingOccurrences(of: "ViewController", with: "") - if name.count == 0 { - name = top.title ?? "Unknown" - } + if name.count == 0 { + name = top.title ?? "Unknown" + } - if name != "Unknown" { - PostHogSDK.shared.capture(name) + if name != "Unknown" { + PostHogSDK.shared.screen(name) + } } - } - @objc func viewDidApperOverride(animated: Bool) { - captureScreenView() - // it looks like we're calling ourselves, but we're actually - // calling the original implementation of viewDidAppear since it's been swizzled. - viewDidApperOverride(animated: animated) - } - - private func findVisibleViewController(_ controller: UIViewController?) -> UIViewController? { - if let navigationController = controller as? UINavigationController { - return findVisibleViewController(navigationController.visibleViewController) + @objc func viewDidApperOverride(animated: Bool) { + captureScreenView() + // it looks like we're calling ourselves, but we're actually + // calling the original implementation of viewDidAppear since it's been swizzled. + viewDidApperOverride(animated: animated) } - if let tabController = controller as? UITabBarController { - if let selected = tabController.selectedViewController { - return findVisibleViewController(selected) + + private func findVisibleViewController(_ controller: UIViewController?) -> UIViewController? { + if let navigationController = controller as? UINavigationController { + return findVisibleViewController(navigationController.visibleViewController) } + if let tabController = controller as? UITabBarController { + if let selected = tabController.selectedViewController { + return findVisibleViewController(selected) + } + } + if let presented = controller?.presentedViewController { + return findVisibleViewController(presented) + } + return controller } - if let presented = controller?.presentedViewController { - return findVisibleViewController(presented) - } - return controller } -} +#endif diff --git a/PostHog/Utils/FileUtils.swift b/PostHog/Utils/FileUtils.swift new file mode 100644 index 000000000..a884e1fab --- /dev/null +++ b/PostHog/Utils/FileUtils.swift @@ -0,0 +1,18 @@ +// +// FileUtils.swift +// PostHog +// +// Created by Manoel Aranda Neto on 30.10.23. +// + +import Foundation + +public func deleteSafely(_ file: URL) { + if FileManager.default.fileExists(atPath: file.path) { + do { + try FileManager.default.removeItem(at: file) + } catch { + hedgeLog("Error trying to delete file \(file.path) \(error)") + } + } +} diff --git a/PostHogObjCExample/AppDelegate.m b/PostHogObjCExample/AppDelegate.m index 6e5a343c5..ef15bf5d3 100644 --- a/PostHogObjCExample/AppDelegate.m +++ b/PostHogObjCExample/AppDelegate.m @@ -31,64 +31,64 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( config.preloadFeatureFlags = NO; [[PostHogSDK shared] debug:YES]; [[PostHogSDK shared] setup:config]; - NSLog(@"getDistinctId: %@", [[PostHogSDK shared] getDistinctId]); - NSLog(@"getAnonymousId: %@", [[PostHogSDK shared] getAnonymousId]); - - NSMutableDictionary *props = [NSMutableDictionary dictionary]; - props[@"state"] = @"running"; - - NSMutableDictionary *userProps = [NSMutableDictionary dictionary]; - userProps[@"userAge"] = @50; - - NSMutableDictionary *userPropsOnce = [NSMutableDictionary dictionary]; - userPropsOnce[@"userAlive"] = @YES; - - NSMutableDictionary *groupProps = [NSMutableDictionary dictionary]; - groupProps[@"groupName"] = @"theGroup"; - - NSMutableDictionary *registerProps = [NSMutableDictionary dictionary]; - props[@"loggedIn"] = @YES; - [[PostHogSDK shared] registerProperties:registerProps]; - [[PostHogSDK shared] unregisterProperties:@"test2"]; - - [[PostHogSDK shared] identify:@"my_new_id"]; - [[PostHogSDK shared] identifyWithDistinctId:@"my_new_id" userProperties:userProps]; - [[PostHogSDK shared] identifyWithDistinctId:@"my_new_id" userProperties:userProps userPropertiesSetOnce:userPropsOnce]; - - - [[PostHogSDK shared] optIn]; - [[PostHogSDK shared] optOut]; - NSLog(@"isOptOut: %d", [[PostHogSDK shared] isOptOut]); - NSLog(@"isFeatureEnabled: %d", [[PostHogSDK shared] isFeatureEnabled:@"myFlag"]); - NSLog(@"getFeatureFlag: %@", [[PostHogSDK shared] getFeatureFlag:@"myFlag"]); - NSLog(@"getFeatureFlagPayload: %@", [[PostHogSDK shared] getFeatureFlagPayload:@"myFlag"]); - - [[PostHogSDK shared] reloadFeatureFlags]; - [[PostHogSDK shared] reloadFeatureFlagsWithCallback:^(){ - NSLog(@"called"); - }]; - - [[PostHogSDK shared] capture:@"theEvent"]; - [[PostHogSDK shared] captureWithEvent:@"theEvent" properties:props]; - [[PostHogSDK shared] captureWithEvent:@"theEvent" properties:props userProperties:userProps]; - [[PostHogSDK shared] captureWithEvent:@"theEvent" properties:props userProperties:userProps userPropertiesSetOnce:userPropsOnce]; - [[PostHogSDK shared] captureWithEvent:@"theEvent" properties:props userProperties:userProps userPropertiesSetOnce:userPropsOnce groupProperties:groupProps]; - - [[PostHogSDK shared] groupWithType:@"theType" key:@"theKey"]; - [[PostHogSDK shared] groupWithType:@"theType" key:@"theKey" groupProperties:groupProps]; - - [[PostHogSDK shared] alias:@"theAlias"]; - - [[PostHogSDK shared] screen:@"theScreen"]; - [[PostHogSDK shared] screenWithTitle:@"theScreen" properties:props]; +// NSLog(@"getDistinctId: %@", [[PostHogSDK shared] getDistinctId]); +// NSLog(@"getAnonymousId: %@", [[PostHogSDK shared] getAnonymousId]); +// +// NSMutableDictionary *props = [NSMutableDictionary dictionary]; +// props[@"state"] = @"running"; +// +// NSMutableDictionary *userProps = [NSMutableDictionary dictionary]; +// userProps[@"userAge"] = @50; +// +// NSMutableDictionary *userPropsOnce = [NSMutableDictionary dictionary]; +// userPropsOnce[@"userAlive"] = @YES; +// +// NSMutableDictionary *groupProps = [NSMutableDictionary dictionary]; +// groupProps[@"groupName"] = @"theGroup"; +// +// NSMutableDictionary *registerProps = [NSMutableDictionary dictionary]; +// props[@"loggedIn"] = @YES; +// [[PostHogSDK shared] registerProperties:registerProps]; +// [[PostHogSDK shared] unregisterProperties:@"test2"]; +// +// [[PostHogSDK shared] identify:@"my_new_id"]; +// [[PostHogSDK shared] identifyWithDistinctId:@"my_new_id" userProperties:userProps]; +// [[PostHogSDK shared] identifyWithDistinctId:@"my_new_id" userProperties:userProps userPropertiesSetOnce:userPropsOnce]; +// +// +// [[PostHogSDK shared] optIn]; +// [[PostHogSDK shared] optOut]; +// NSLog(@"isOptOut: %d", [[PostHogSDK shared] isOptOut]); +// NSLog(@"isFeatureEnabled: %d", [[PostHogSDK shared] isFeatureEnabled:@"myFlag"]); +// NSLog(@"getFeatureFlag: %@", [[PostHogSDK shared] getFeatureFlag:@"myFlag"]); +// NSLog(@"getFeatureFlagPayload: %@", [[PostHogSDK shared] getFeatureFlagPayload:@"myFlag"]); +// +// [[PostHogSDK shared] reloadFeatureFlags]; +// [[PostHogSDK shared] reloadFeatureFlagsWithCallback:^(){ +// NSLog(@"called"); +// }]; +// +// [[PostHogSDK shared] capture:@"theEvent"]; +// [[PostHogSDK shared] captureWithEvent:@"theEvent" properties:props]; +// [[PostHogSDK shared] captureWithEvent:@"theEvent" properties:props userProperties:userProps]; +// [[PostHogSDK shared] captureWithEvent:@"theEvent" properties:props userProperties:userProps userPropertiesSetOnce:userPropsOnce]; +// [[PostHogSDK shared] captureWithEvent:@"theEvent" properties:props userProperties:userProps userPropertiesSetOnce:userPropsOnce groupProperties:groupProps]; +// +// [[PostHogSDK shared] groupWithType:@"theType" key:@"theKey"]; +// [[PostHogSDK shared] groupWithType:@"theType" key:@"theKey" groupProperties:groupProps]; +// +// [[PostHogSDK shared] alias:@"theAlias"]; +// +// [[PostHogSDK shared] screen:@"theScreen"]; +// [[PostHogSDK shared] screenWithTitle:@"theScreen" properties:props]; - [[PostHogSDK shared] flush]; - [[PostHogSDK shared] reset]; - [[PostHogSDK shared] close]; +// [[PostHogSDK shared] flush]; +// [[PostHogSDK shared] reset]; +// [[PostHogSDK shared] close]; - PostHogSDK *postHog = [PostHogSDK with:config]; - - [postHog capture:@"theCapture"]; +// PostHogSDK *postHog = [PostHogSDK with:config]; +// +// [postHog capture:@"theCapture"]; return YES; } diff --git a/PostHogTests/CaptureTests.swift b/PostHogTests/CaptureTests.swift deleted file mode 100644 index 9b5ced40e..000000000 --- a/PostHogTests/CaptureTests.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// CaptureTests.swift -// PostHogTests -// -// Created by Ben White on 21.03.23. -// - -import Nimble -import Quick - -@testable import PostHog - -// As E2E as possible tests -class CaptureTest: QuickSpec { - override func spec() { - var harness: TestPostHog! - var posthog: PostHogSDK! - - beforeEach { - harness = TestPostHog() - posthog = harness.posthog - } - afterEach { - harness.stop() - } - - it(".capture") { - posthog.capture("test event") - posthog.capture("test event2", properties: ["foo": "bar"]) - - let events = harness.getBatchedEvents() - - expect(events.count) == 2 - - expect(events[0].event) == "test event" - expect(Set(events[0].properties.keys)) == ["$device_id", "$os_name", "$app_version", "$lib_version", "$screen_height", "$app_name", "$timezone", "$screen_width", "$app_namespace", "$network_cellular", "$os_version", "$device_name", "$network_wifi", "distinct_id", "$lib", "$session_id", "$locale", "$app_build", "$device_type", "$groups"] - - expect(events[1].event) == "test event2" - expect(events[1].properties["foo"] as? String) == "bar" - } - - it(".capture handles null values") { - posthog.capture("null test", properties: [ - "nullTest": NSNull(), - ]) - - let events = harness.getBatchedEvents() - expect(events[0].properties["nullTest"] is NSNull) == true - } - - it(".identify") { - let anonymousId = posthog.getAnonymousId() - posthog.identify("testDistinctId1", userProperties: [ - "firstName": "Peter", - ]) - - let event = harness.getBatchedEvents()[0] - expect(event.event) == "$identify" - expect(event.properties["distinct_id"] as? String) == "testDistinctId1" - expect(event.properties["$anon_distinct_id"] as? String) == anonymousId - expect((event.properties["$set"] as? [String: String])?["firstName"] as? String) == "Peter" - } - - it(".alias") { - posthog.alias("persistentDistinctId") - - let event = harness.getBatchedEvents()[0] - expect(event.event) == "$create_alias" - expect(event.properties["alias"] as? String) == "persistentDistinctId" - } - - it(".screen") { - posthog.screen("Home", properties: [ - "referrer": "Google", - ]) - - let event = harness.getBatchedEvents()[0] - expect(event.event) == "$screen" - expect(event.properties["$screen_name"] as? String) == "Home" - expect(event.properties["referrer"] as? String) == "Google" - } - - it(".group") { - posthog.group(type: "some-type", key: "some-key", groupProperties: [ - "name": "some-company-name", - ]) - posthog.capture("test-event") - - let events = harness.getBatchedEvents() - expect(events[0].event) == "$groupidentify" - expect(events[0].properties["$group_type"] as? String?) == "some-type" - expect(events[0].properties["$group_key"] as? String?) == "some-key" - expect((events[0].properties["$group_set"] as? [String: String])?["name"] as? String) == "some-company-name" - - // Verify that subsequent call has the groups - let groups = events[1].properties["$groups"] as? [String: String] - expect(groups?["some-type"]) == "some-key" - } - } -} diff --git a/PostHogTests/FeatureFlagsTest.swift b/PostHogTests/FeatureFlagsTest.swift deleted file mode 100644 index db6f011d7..000000000 --- a/PostHogTests/FeatureFlagsTest.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// FeatureFlagsTest.swift -// PostHogTests -// -// Created by Ben White on 08.02.23. -// - -import Nimble -import Quick - -@testable import PostHog - -class FeatureFlagTests: QuickSpec { - override func spec() { - var harness: TestPostHog! - var posthog: PostHogSDK! - - beforeEach { - harness = TestPostHog() - posthog = harness.posthog - - let expectation = self.expectation(description: "Waits for flags") - posthog.reloadFeatureFlags { - expectation.fulfill() - } - - await self.fulfillment(of: [expectation]) - } - afterEach { - harness.stop() - } - - it("responds false for missing flag enabled") { - let isEnabled = posthog.isFeatureEnabled("missing") - expect(isEnabled) == false - } - - it("responds nil for missing flag get") { - let isEnabled = posthog.getFeatureFlag("missing") - expect(isEnabled) == nil - } - - it("checks flag is enabled") { - let isEnabled = posthog.isFeatureEnabled("bool-value") - expect(isEnabled) == true - } - - it("checks multivariate flag is enabled") { - guard let flagValue = posthog.getFeatureFlag("string-value") as? String else { - fail("Wrong type for flag") - return - } - expect(flagValue) == "test" - } - - it("returns payload - bool") { - guard let flagValue = posthog.getFeatureFlagPayload("payload-bool") as? Bool else { - return fail("Wrong type for flag") - } - expect(flagValue) == true - } - - it("returns payload - number") { - guard let flagValue = posthog.getFeatureFlagPayload("payload-number") as? Int else { - return fail("Wrong type for flag") - } - expect(flagValue) == 2 - } - - it("returns payload - string") { - guard let flagValue = posthog.getFeatureFlagPayload("payload-string") as? String else { - return fail("Wrong type for flag") - } - expect(flagValue) == "string-value" - } - - it("returns payload - dict") { - guard let flagValue = posthog.getFeatureFlagPayload("payload-json") as? [String: String] else { - return fail("Wrong type for flag") - } - expect(flagValue) == ["foo": "bar"] - } - - it("returns nil for wrong type") { - let flagValue = posthog.getFeatureFlagPayload("payload-json") as? String? - expect(flagValue) == nil - } - } -} diff --git a/PostHogTests/PostHogConfigTest.swift b/PostHogTests/PostHogConfigTest.swift new file mode 100644 index 000000000..10ff27d9f --- /dev/null +++ b/PostHogTests/PostHogConfigTest.swift @@ -0,0 +1,44 @@ +// +// PostHogConfigTest.swift +// PostHogTests +// +// Created by Manoel Aranda Neto on 30.10.23. +// + +import Foundation +import Nimble +@testable import PostHog +import Quick + +class PostHogConfigTest: QuickSpec { + override func spec() { + it("init config with default values") { + let config = PostHogConfig(apiKey: "123") + + expect(config.host) == URL(string: PostHogConfig.defaultHost) + expect(config.flushAt) == 20 + expect(config.maxQueueSize) == 1000 + expect(config.maxBatchSize) == 50 + expect(config.flushIntervalSeconds) == 30 + expect(config.dataMode) == .any + expect(config.sendFeatureFlagEvent) == true + expect(config.preloadFeatureFlags) == true + expect(config.captureApplicationLifecycleEvents) == true + expect(config.captureScreenViews) == true + expect(config.debug) == false + expect(config.optOut) == false + } + + it("init takes api key") { + let config = PostHogConfig(apiKey: "123") + + expect(config.apiKey) == "123" + } + + it("init takes host") { + let config = PostHogConfig(apiKey: "123", host: "localhost:9000") + + expect(config.host) == URL(string: "localhost:9000")! + } + } +} diff --git a/PostHogTests/PostHogContextTest.swift b/PostHogTests/PostHogContextTest.swift new file mode 100644 index 000000000..edef24f56 --- /dev/null +++ b/PostHogTests/PostHogContextTest.swift @@ -0,0 +1,60 @@ +// +// PostHogContextTest.swift +// PostHogTests +// +// Created by Manoel Aranda Neto on 30.10.23. +// + +import Foundation +import Nimble +@testable import PostHog +import Quick + +class PostHogContextTest: QuickSpec { + func getSut() -> PostHogContext { + var reachability: Reachability? + do { + reachability = try Reachability() + } catch { + // ignored + } + return PostHogContext(reachability) + } + + override func spec() { + it("returns static context") { + let sut = self.getSut() + + let context = sut.staticContext() + expect(context["$app_name"] as? String) == "xctest" + expect(context["$app_version"] as? String) != nil + expect(context["$app_build"] as? String) != nil + expect(context["$app_namespace"] as? String) == "com.apple.dt.xctest.tool" + #if os(iOS) || os(tvOS) + expect(context["$device_name"] as? String) != nil + expect(context["$os_name"] as? String) != nil + expect(context["$os_version"] as? String) != nil + expect(context["$device_type"] as? String) != nil + expect(context["$device_model"] as? String) != nil + expect(context["$device_manufacturer"] as? String) == "Apple" + #endif + } + + it("returns dynamic context") { + let sut = self.getSut() + + let context = sut.dynamicContext() + + #if os(iOS) || os(tvOS) + expect(context["$screen_width"] as? Float) != nil + expect(context["$screen_height"] as? Float) != nil + #endif + expect(context["$lib"] as? String) == "posthog-ios" + expect(context["$lib_version"] as? String) == postHogVersion + expect(context["$locale"] as? String) != nil + expect(context["$timezone"] as? String) != nil + expect(context["$network_wifi"] as? Bool) != nil + expect(context["$network_cellular"] as? Bool) != nil + } + } +} diff --git a/PostHogTests/PostHogFeatureFlagsTest.swift b/PostHogTests/PostHogFeatureFlagsTest.swift new file mode 100644 index 000000000..41b275c85 --- /dev/null +++ b/PostHogTests/PostHogFeatureFlagsTest.swift @@ -0,0 +1,140 @@ +// +// PostHogFeatureFlagsTest.swift +// PostHogTests +// +// Created by Manoel Aranda Neto on 30.10.23. +// + +import Foundation +import Nimble +@testable import PostHog +import Quick + +class PostHogFeatureFlagsTest: QuickSpec { + func getSut() -> PostHogFeatureFlags { + let config = PostHogConfig(apiKey: "123", host: "http://localhost:9001") + let storage = PostHogStorage(config) + let api = PostHogApi(config) + return PostHogFeatureFlags(config, storage, api) + } + + override func spec() { + var server: MockPostHogServer! + + beforeEach { + server = MockPostHogServer() + server.start() + } + afterEach { + server.stop() + } + + it("returns true for enabled flag - boolean") { + let sut = self.getSut() + let group = DispatchGroup() + group.enter() + + sut.loadFeatureFlags(distinctId: "distinctId", anonymousId: "anonymousId", groups: ["group": "value"], callback: { + group.leave() + }) + + group.wait() + + expect(sut.isFeatureEnabled("bool-value")) == true + } + + it("returns true for enabled flag - string") { + let sut = self.getSut() + let group = DispatchGroup() + group.enter() + + sut.loadFeatureFlags(distinctId: "distinctId", anonymousId: "anonymousId", groups: ["group": "value"], callback: { + group.leave() + }) + + group.wait() + + expect(sut.isFeatureEnabled("string-value")) == true + } + + it("returns false for disabled flag") { + let sut = self.getSut() + let group = DispatchGroup() + group.enter() + + sut.loadFeatureFlags(distinctId: "distinctId", anonymousId: "anonymousId", groups: ["group": "value"], callback: { + group.leave() + }) + + group.wait() + + expect(sut.isFeatureEnabled("disabled-flag")) == false + } + + it("returns feature flag value") { + let sut = self.getSut() + let group = DispatchGroup() + group.enter() + + sut.loadFeatureFlags(distinctId: "distinctId", anonymousId: "anonymousId", groups: ["group": "value"], callback: { + group.leave() + }) + + group.wait() + + expect(sut.getFeatureFlag("string-value") as? String) == "test" + } + + it("returns feature flag payload") { + let sut = self.getSut() + let group = DispatchGroup() + group.enter() + + sut.loadFeatureFlags(distinctId: "distinctId", anonymousId: "anonymousId", groups: ["group": "value"], callback: { + group.leave() + }) + + group.wait() + + expect(sut.getFeatureFlagPayload("number-value") as? Int) == 2 + } + + it("returns feature flag payload as dict") { + let sut = self.getSut() + let group = DispatchGroup() + group.enter() + + sut.loadFeatureFlags(distinctId: "distinctId", anonymousId: "anonymousId", groups: ["group": "value"], callback: { + group.leave() + }) + + expect(sut.getFeatureFlagPayload("payload-json") as? [String: String]) == ["foo": "bar"] + } + + it("merge flags if computed errors") { + let sut = self.getSut() + let group = DispatchGroup() + group.enter() + + sut.loadFeatureFlags(distinctId: "distinctId", anonymousId: "anonymousId", groups: ["group": "value"], callback: { + group.leave() + }) + + group.wait() + + server.errorsWhileComputingFlags = true + + let group2 = DispatchGroup() + group2.enter() + + sut.loadFeatureFlags(distinctId: "distinctId", anonymousId: "anonymousId", groups: ["group": "value"], callback: { + group2.leave() + }) + + group2.wait() + + expect(sut.isFeatureEnabled("new-flag")) == true + expect(sut.isFeatureEnabled("bool-value")) == true + } + } +} diff --git a/PostHogTests/PostHogFileBackedQueueTest.swift b/PostHogTests/PostHogFileBackedQueueTest.swift new file mode 100644 index 000000000..267a71c35 --- /dev/null +++ b/PostHogTests/PostHogFileBackedQueueTest.swift @@ -0,0 +1,177 @@ +// +// PostHogFileBackedQueueTest.swift +// PostHogTests +// +// Created by Manoel Aranda Neto on 30.10.23. +// + +import Foundation +import Nimble +@testable import PostHog +import Quick + +class PostHogFileBackedQueueTest: QuickSpec { + let eventJson = + """ + { + "properties": { + "$network_cellular": false, + "$groups": { + "some-group": "id:4" + }, + "$app_build": "1", + "$os_name": "iOS", + "$feature/multivariant": "payload", + "$screen_width": 852, + "$app_version": "1.0", + "$device_type": "Mobile", + "$active_feature_flags__0": "multivariant", + "$feature/4535-funnel-bar-viz": true, + "$network_wifi": true, + "$timezone": "Europe/Vienna", + "$device_id": "48DA3429-495A-4903-B347-F096FC31C3AB", + "$active_feature_flags": [ + "$feature/multivariant", + "$feature/testJson", + "$feature/4535-funnel-bar-viz", + "$feature/disabledFlag" + ], + "$active_feature_flags__3": "disabledFlag", + "$device_name": "iPhone", + "$app_name": "", + "$app_namespace": "com.posthog.CocoapodsExample", + "$locale": "en-US", + "$feature/disabledFlag": true, + "$active_feature_flags__1": "testJson", + "$screen_height": 393, + "$device_model": "arm64", + "$device_manufacturer": "Apple", + "$feature/testJson": "theInteger", + "$lib_version": "2.1.0", + "$os_version": "17.0.1", + "$lib": "posthog-ios", + "$active_feature_flags__2": "4535-funnel-bar-viz" + }, + "timestamp": "2023-10-25T14:14:04.407Z", + "message_id": "5CE069F8-E967-4B47-9D89-207EF7519453", + "event": "Cocoapods Example Button", + "distinct_id": "Prateek" + } + """ + + func getSut() -> PostHogFileBackedQueue { + let baseUrl = applicationSupportDirectoryURL() + let oldURL = baseUrl.appendingPathComponent("oldQueue") + let newURL = baseUrl.appendingPathComponent("queue") + + return PostHogFileBackedQueue(queue: newURL, oldQueue: oldURL) + } + + override func spec() { + it("create folder and init queue") { + let sut = self.getSut() + + expect(sut.depth) == 0 + expect(FileManager.default.fileExists(atPath: sut.queue.path)) == true + + sut.clear() + } + + it("load cached files into memory") { + let baseUrl = applicationSupportDirectoryURL() + let newURL = baseUrl.appendingPathComponent("queue") + try FileManager.default.createDirectory(atPath: newURL.path, withIntermediateDirectories: true) + + let eventURL = newURL.appendingPathComponent("1698236044.407") + let eventsData = self.eventJson.data(using: .utf8)! + try eventsData.write(to: eventURL) + + expect(FileManager.default.fileExists(atPath: eventURL.path)) == true + + let sut = self.getSut() + + expect(sut.depth) == 1 + let items = sut.peek(1) + expect(items.first) != nil + + sut.clear() + } + + it("delete from queue and disk") { + let baseUrl = applicationSupportDirectoryURL() + let newURL = baseUrl.appendingPathComponent("queue") + try FileManager.default.createDirectory(atPath: newURL.path, withIntermediateDirectories: true) + + let eventURL = newURL.appendingPathComponent("1698236044.407") + let eventsData = self.eventJson.data(using: .utf8)! + try eventsData.write(to: eventURL) + + expect(FileManager.default.fileExists(atPath: eventURL.path)) == true + + let sut = self.getSut() + + sut.delete(index: 0) + + expect(sut.depth) == 0 + expect(FileManager.default.fileExists(atPath: eventURL.path)) == false + + sut.clear() + } + + it("pop from queue and disk") { + let baseUrl = applicationSupportDirectoryURL() + let newURL = baseUrl.appendingPathComponent("queue") + try FileManager.default.createDirectory(atPath: newURL.path, withIntermediateDirectories: true) + + let eventURL = newURL.appendingPathComponent("1698236044.407") + let eventsData = self.eventJson.data(using: .utf8)! + try eventsData.write(to: eventURL) + + expect(FileManager.default.fileExists(atPath: eventURL.path)) == true + + let sut = self.getSut() + + sut.pop(1) + + expect(sut.depth) == 0 + expect(FileManager.default.fileExists(atPath: eventURL.path)) == false + + sut.clear() + } + + it("add to queue and disk") { + let baseUrl = applicationSupportDirectoryURL() + let newURL = baseUrl.appendingPathComponent("queue") + try FileManager.default.createDirectory(atPath: newURL.path, withIntermediateDirectories: true) + + let eventsData = self.eventJson.data(using: .utf8)! + + let sut = self.getSut() + + sut.add(eventsData) + + let items = try FileManager.default.contentsOfDirectory(atPath: newURL.path) + expect(sut.depth) == 1 + expect(items.count) == 1 + + sut.clear() + } + + it("clear queue and disk") { + let baseUrl = applicationSupportDirectoryURL() + let newURL = baseUrl.appendingPathComponent("queue") + try FileManager.default.createDirectory(atPath: newURL.path, withIntermediateDirectories: true) + + let eventsData = self.eventJson.data(using: .utf8)! + + let sut = self.getSut() + + sut.add(eventsData) + sut.clear() + + let items = try FileManager.default.contentsOfDirectory(atPath: newURL.path) + expect(sut.depth) == 0 + expect(items.count) == 0 + } + } +} diff --git a/PostHogTests/PostHogLegacyQueueTest.swift b/PostHogTests/PostHogLegacyQueueTest.swift new file mode 100644 index 000000000..2da6f7b65 --- /dev/null +++ b/PostHogTests/PostHogLegacyQueueTest.swift @@ -0,0 +1,133 @@ +// +// PostHogLegacyQueueTest.swift +// PostHogTests +// +// Created by Manoel Aranda Neto on 30.10.23. +// + +import Foundation +import Nimble +@testable import PostHog +import Quick + +class PostHogLegacyQueueTest: QuickSpec { + override func spec() { + it("migrate old queue to new queue") { + let baseUrl = applicationSupportDirectoryURL() + try FileManager.default.createDirectory(atPath: baseUrl.path, withIntermediateDirectories: true) + + let newURL = baseUrl.appendingPathComponent("queue") + try FileManager.default.createDirectory(atPath: newURL.path, withIntermediateDirectories: true) + + let oldURL = baseUrl.appendingPathComponent("oldQueue") + + let eventsArray = + """ + [ + { + "properties": { + "$network_cellular": false, + "$groups": { + "some-group": "id:4" + }, + "$app_build": "1", + "$os_name": "iOS", + "$feature/multivariant": "payload", + "$screen_width": 852, + "$app_version": "1.0", + "$device_type": "Mobile", + "$active_feature_flags__0": "multivariant", + "$feature/4535-funnel-bar-viz": true, + "$network_wifi": true, + "$timezone": "Europe/Vienna", + "$device_id": "48DA3429-495A-4903-B347-F096FC31C3AB", + "$active_feature_flags": [ + "$feature/multivariant", + "$feature/testJson", + "$feature/4535-funnel-bar-viz", + "$feature/disabledFlag" + ], + "$active_feature_flags__3": "disabledFlag", + "$device_name": "iPhone", + "$app_name": "", + "$app_namespace": "com.posthog.CocoapodsExample", + "$locale": "en-US", + "$feature/disabledFlag": true, + "$active_feature_flags__1": "testJson", + "$screen_height": 393, + "$device_model": "arm64", + "$device_manufacturer": "Apple", + "$feature/testJson": "theInteger", + "$lib_version": "2.1.0", + "$os_version": "17.0.1", + "$lib": "posthog-ios", + "$active_feature_flags__2": "4535-funnel-bar-viz" + }, + "timestamp": "2023-10-25T14:14:04.407Z", + "message_id": "5CE069F8-E967-4B47-9D89-207EF7519453", + "event": "Cocoapods Example Button", + "distinct_id": "Prateek" + } + ] + """ + + let eventsData = eventsArray.data(using: .utf8)! + try eventsData.write(to: oldURL) + + expect(FileManager.default.fileExists(atPath: oldURL.path)) == true + + migrateOldQueue(queue: newURL, oldQueue: oldURL) + + expect(FileManager.default.fileExists(atPath: oldURL.path)) == false + + let items = try FileManager.default.contentsOfDirectory(atPath: newURL.path) + + let eventURL = newURL.appendingPathComponent(items[0]) + expect(FileManager.default.fileExists(atPath: eventURL.path)) == true + + let eventData = try Data(contentsOf: eventURL) + let eventObject = try JSONSerialization.jsonObject(with: eventData, options: .allowFragments) as? [String: Any] + + expect(eventObject!["distinct_id"] as? String) == "Prateek" + expect(eventObject!["event"] as? String) == "Cocoapods Example Button" + expect(eventObject!["message_id"] as? String) == "5CE069F8-E967-4B47-9D89-207EF7519453" + expect(eventObject!["timestamp"] as? String) == "2023-10-25T14:14:04.407Z" + expect(eventObject!["properties"] as? [String: Any]) != nil + + deleteSafely(oldURL) + deleteSafely(newURL) + } + + it("ignore and delete corrupted file") { + let baseUrl = applicationSupportDirectoryURL() + try FileManager.default.createDirectory(atPath: baseUrl.path, withIntermediateDirectories: true) + + let newURL = baseUrl.appendingPathComponent("queue") + try FileManager.default.createDirectory(atPath: newURL.path, withIntermediateDirectories: true) + + let oldURL = baseUrl.appendingPathComponent("oldQueue") + + let eventsArray = + """ + [ + i am broken + ] + """ + + let eventsData = eventsArray.data(using: .utf8)! + try eventsData.write(to: oldURL) + + expect(FileManager.default.fileExists(atPath: oldURL.path)) == true + + migrateOldQueue(queue: newURL, oldQueue: oldURL) + + expect(FileManager.default.fileExists(atPath: oldURL.path)) == false + + let items = try FileManager.default.contentsOfDirectory(atPath: newURL.path) + expect(items.isEmpty) == true + + deleteSafely(oldURL) + deleteSafely(newURL) + } + } +} diff --git a/PostHogTests/PostHogQueueTest.swift b/PostHogTests/PostHogQueueTest.swift new file mode 100644 index 000000000..109acf3b2 --- /dev/null +++ b/PostHogTests/PostHogQueueTest.swift @@ -0,0 +1,68 @@ +// +// PostHogQueueTest.swift +// PostHogTests +// +// Created by Manoel Aranda Neto on 30.10.23. +// + +import Foundation +import Nimble +@testable import PostHog +import Quick +import XCTest + +class PostHogQueueTest: QuickSpec { + func getSut() -> PostHogQueue { + let config = PostHogConfig(apiKey: "123", host: "http://localhost:9001") + config.flushAt = 1 + let storage = PostHogStorage(config) + let api = PostHogApi(config) + return PostHogQueue(config, storage, api, nil) + } + + override func spec() { + var server: MockPostHogServer! + + beforeEach { + server = MockPostHogServer() + server.start() + } + afterEach { + server.stop() + } + + it("add item to queue") { + let sut = self.getSut() + + let event = PostHogEvent(event: "event", distinctId: "distinctId") + sut.add(event) + + expect(sut.depth) == 1 + + let events = getBatchedEvents(server) + expect(events.count) == 1 + + expect(sut.depth) == 0 + + sut.clear() + } + + it("add item to queue and flush respecting flushAt") { + let sut = self.getSut() + + let event = PostHogEvent(event: "event", distinctId: "distinctId") + let event2 = PostHogEvent(event: "event2", distinctId: "distinctId2") + sut.add(event) + sut.add(event2) + + expect(sut.depth) == 2 + + let events = getBatchedEvents(server) + expect(events.count) == 1 + + expect(sut.depth) == 1 + + sut.clear() + } + } +} diff --git a/PostHogTests/PostHogSDKTest.swift b/PostHogTests/PostHogSDKTest.swift new file mode 100644 index 000000000..196b53cbd --- /dev/null +++ b/PostHogTests/PostHogSDKTest.swift @@ -0,0 +1,528 @@ +// +// PostHogSDKTest.swift +// PostHogTests +// +// Created by Manoel Aranda Neto on 31.10.23. +// + +import Foundation +import Nimble +import Quick + +@testable import PostHog + +class PostHogSDKTest: QuickSpec { + func getSut(preloadFeatureFlags: Bool = false, + sendFeatureFlagEvent: Bool = false, + flushAt: Int = 1, + optOut: Bool = false) -> PostHogSDK + { + let config = PostHogConfig(apiKey: "123", host: "http://localhost:9001") + config.flushAt = flushAt + config.preloadFeatureFlags = preloadFeatureFlags + config.sendFeatureFlagEvent = sendFeatureFlagEvent + config.disableReachabilityForTesting = true + config.disableQueueTimerForTesting = true + config.optOut = optOut + return PostHogSDK.with(config) + } + + override func spec() { + var server: MockPostHogServer! + + func deleteDefaults() { + let userDefaults = UserDefaults.standard + userDefaults.removeObject(forKey: "PHGVersionKey") + userDefaults.removeObject(forKey: "PHGBuildKeyV2") + userDefaults.synchronize() + + deleteSafely(applicationSupportDirectoryURL()) + } + + beforeEach { + deleteDefaults() + server = MockPostHogServer() + server.start() + } + afterEach { + server.stop() + server = nil + } + + it("captures the capture event") { + let sut = self.getSut() + + sut.capture("test event", + properties: ["foo": "bar"], + userProperties: ["userProp": "value"], + userPropertiesSetOnce: ["userPropOnce": "value"], + groupProperties: ["groupProp": "value"]) + + let events = getBatchedEvents(server) + + expect(events.count) == 1 + + let event = events.first! + expect(event.event) == "test event" + + expect(event.properties["foo"] as? String) == "bar" + + let set = event.properties["$set"] as? [String: Any] ?? [:] + expect(set["userProp"] as? String) == "value" + + let setOnce = event.properties["$set_once"] as? [String: Any] ?? [:] + expect(setOnce["userPropOnce"] as? String) == "value" + + let groupProps = event.properties["$groups"] as? [String: Any] ?? [:] + expect(groupProps["groupProp"] as? String) == "value" + + sut.reset() + sut.close() + } + + it("captures an identify event") { + let sut = self.getSut() + + sut.identify("distinctId", + userProperties: ["userProp": "value"], + userPropertiesSetOnce: ["userPropOnce": "value"]) + + let events = getBatchedEvents(server) + + expect(events.count) == 1 + + let event = events.first! + expect(event.event) == "$identify" + + expect(event.distinctId) == "distinctId" + let anonId = sut.getAnonymousId() + expect(event.properties["$anon_distinct_id"] as? String) == anonId + + let set = event.properties["$set"] as? [String: Any] ?? [:] + expect(set["userProp"] as? String) == "value" + + let setOnce = event.properties["$set_once"] as? [String: Any] ?? [:] + expect(setOnce["userPropOnce"] as? String) == "value" + + sut.reset() + sut.close() + } + + it("captures an alias event") { + let sut = self.getSut() + + sut.alias("theAlias") + + let events = getBatchedEvents(server) + + expect(events.count) == 1 + + let event = events.first! + expect(event.event) == "$create_alias" + + expect(event.properties["alias"] as? String) == "theAlias" + + sut.reset() + sut.close() + } + + it("captures a screen event") { + let sut = self.getSut() + + sut.screen("theScreen", properties: ["prop": "value"]) + + let events = getBatchedEvents(server) + + expect(events.count) == 1 + + let event = events.first! + expect(event.event) == "$screen" + + expect(event.properties["$screen_name"] as? String) == "theScreen" + expect(event.properties["prop"] as? String) == "value" + + sut.reset() + sut.close() + } + + it("captures a group event") { + let sut = self.getSut() + + sut.group(type: "some-type", key: "some-key", groupProperties: [ + "name": "some-company-name", + ]) + + let events = getBatchedEvents(server) + + expect(events.count) == 1 + + let groupEvent = events.first! + expect(groupEvent.event) == "$groupidentify" + expect(groupEvent.properties["$group_type"] as? String?) == "some-type" + expect(groupEvent.properties["$group_key"] as? String?) == "some-key" + expect((groupEvent.properties["$group_set"] as? [String: String])?["name"] as? String) == "some-company-name" + + sut.reset() + sut.close() + } + + it("setups default IDs") { + let sut = self.getSut() + + expect(sut.getAnonymousId()).toNot(beNil()) + expect(sut.getDistinctId()) == sut.getAnonymousId() + + sut.reset() + sut.close() + } + + it("setups optOut") { + let sut = self.getSut() + + sut.optOut() + + expect(sut.isOptOut()) == true + + sut.optIn() + + expect(sut.isOptOut()) == false + + sut.reset() + sut.close() + } + + it("sets opt out via config") { + let sut = self.getSut(optOut: true) + + sut.optOut() + + expect(sut.isOptOut()) == true + + sut.reset() + sut.close() + } + + it("calls reloadFeatureFlags") { + let sut = self.getSut() + + let group = DispatchGroup() + group.enter() + + sut.reloadFeatureFlags { + group.leave() + } + + group.wait() + + expect(sut.isFeatureEnabled("bool-value")) == true + + sut.reset() + sut.close() + } + + it("identify sets distinct and anon Ids") { + let sut = self.getSut() + + let distId = sut.getDistinctId() + + sut.identify("newDistinctId") + + expect(sut.getDistinctId()) == "newDistinctId" + expect(sut.getAnonymousId()) == distId + + sut.reset() + sut.close() + } + + it("loads feature flags automatically") { + let sut = self.getSut(preloadFeatureFlags: true) + + waitDecideRequest(server) + expect(sut.isFeatureEnabled("bool-value")) == true + + sut.reset() + sut.close() + } + + it("send feature flag event for isFeatureEnabled when enabled") { + let sut = self.getSut(preloadFeatureFlags: true, sendFeatureFlagEvent: true) + + waitDecideRequest(server) + expect(sut.isFeatureEnabled("bool-value")) == true + + let events = getBatchedEvents(server) + + expect(events.count) == 1 + + let event = events.first! + expect(event.event) == "$feature_flag_called" + expect(event.properties["$feature_flag"] as? String) == "bool-value" + expect(event.properties["$feature_flag_response"] as? Bool) == true + + sut.reset() + sut.close() + } + + it("send feature flag event for getFeatureFlag when enabled") { + let sut = self.getSut(preloadFeatureFlags: true, sendFeatureFlagEvent: true) + + waitDecideRequest(server) + expect(sut.getFeatureFlag("bool-value") as? Bool) == true + + let events = getBatchedEvents(server) + + expect(events.count) == 1 + + let event = events.first! + expect(event.event) == "$feature_flag_called" + expect(event.properties["$feature_flag"] as? String) == "bool-value" + expect(event.properties["$feature_flag_response"] as? Bool) == true + + sut.reset() + sut.close() + } + + it("capture AppBackgrounded") { + let sut = self.getSut() + + sut.captureAppBackgrounded() + + let events = getBatchedEvents(server) + + expect(events.count) == 1 + + let event = events.first! + expect(event.event) == "Application Backgrounded" + + sut.reset() + sut.close() + } + + it("capture AppInstalled") { + let sut = self.getSut() + + sut.captureAppInstalled() + + let events = getBatchedEvents(server) + + expect(events.count) == 1 + + let event = events.first! + expect(event.event) == "Application Installed" + expect(event.properties["version"] as? String) != nil + expect(event.properties["build"] as? String) != nil + + sut.reset() + sut.close() + } + + it("capture AppUpdated") { + let sut = self.getSut() + + let userDefaults = UserDefaults.standard + userDefaults.setValue("1.0.0", forKey: "PHGVersionKey") + userDefaults.setValue("1", forKey: "PHGBuildKeyV2") + userDefaults.synchronize() + + sut.captureAppInstalled() + + let events = getBatchedEvents(server) + + expect(events.count) == 1 + + let event = events.first! + expect(event.event) == "Application Updated" + expect(event.properties["version"] as? String) != nil + expect(event.properties["build"] as? String) != nil + expect(event.properties["previous_version"] as? String) != nil + expect(event.properties["previous_build"] as? String) != nil + + sut.reset() + sut.close() + } + + it("capture AppOpenedFromBackground") { + let sut = self.getSut() + + sut.captureAppOpenedFromBackground() + + let events = getBatchedEvents(server) + + expect(events.count) == 1 + + let event = events.first! + expect(event.event) == "Application Opened" + expect(event.properties["from_background"] as? Bool) == false + + sut.reset() + sut.close() + } + + it("capture AppOpenedFromBackground") { + let sut = self.getSut(flushAt: 2) + + sut.captureAppOpenedFromBackground() + sut.captureAppOpenedFromBackground() + + let events = getBatchedEvents(server) + + expect(events.count) == 2 + + let event = events.last! + expect(event.event) == "Application Opened" + expect(event.properties["from_background"] as? Bool) == true + + sut.reset() + sut.close() + } + + it("capture captureAppOpened") { + let sut = self.getSut() + + sut.captureAppOpened() + + let events = getBatchedEvents(server) + + expect(events.count) == 1 + + let event = events.first! + expect(event.event) == "Application Opened" + expect(event.properties["from_background"] as? Bool) == false + expect(event.properties["version"] as? String) != nil + expect(event.properties["build"] as? String) != nil + + sut.reset() + sut.close() + } + + it("reloadFeatureFlags adds groups if any") { + let sut = self.getSut() + + sut.group(type: "some-type", key: "some-key", groupProperties: [ + "name": "some-company-name", + ]) + + let events = getBatchedEvents(server) + + expect(events.count) == 1 + + sut.reloadFeatureFlags() + + let requests = getDecideRequest(server) + + expect(requests.count) == 1 + let request = requests.first + + let groups = request!["$groups"] as? [String: Any] + expect(groups!["some-type"] as? String) == "some-key" + + sut.reset() + sut.close() + } + + it("merge groups when group is called") { + let sut = self.getSut(flushAt: 3) + + sut.group(type: "some-type", key: "some-key") + + sut.group(type: "some-type-2", key: "some-key-2") + + sut.capture("event") + + let events = getBatchedEvents(server) + + expect(events.count) == 3 + let event = events.last! + + let groups = event.properties["$groups"] as? [String: Any] + expect(groups!["some-type"] as? String) == "some-key" + expect(groups!["some-type-2"] as? String) == "some-key-2" + + sut.reset() + sut.close() + } + + it("register and unregister properties") { + let sut = self.getSut(flushAt: 1) + + sut.register(["test1": "test"]) + sut.register(["test2": "test"]) + sut.unregister("test2") + sut.register(["test3": "test"]) + + sut.capture("event") + + let events = getBatchedEvents(server) + + expect(events.count) == 1 + let event = events.last! + + expect(event.properties["test1"] as? String) == "test" + expect(event.properties["test3"] as? String) == "test" + expect(event.properties["test2"] as? String) == nil + + sut.reset() + sut.close() + } + + it("add active feature flags as part of the event") { + let sut = self.getSut() + + sut.reloadFeatureFlags() + waitDecideRequest(server) + + sut.capture("event") + + let events = getBatchedEvents(server) + + expect(events.count) == 1 + let event = events.first! + + let activeFlags = event.properties["$active_feature_flags"] as? [Any] ?? [] + expect(activeFlags.contains { $0 as? String == "bool-value" }) == true + expect(activeFlags.contains { $0 as? String == "disabled-flag" }) == false + + expect(event.properties["$feature/bool-value"] as? Bool) == true + expect(event.properties["$feature/disabled-flag"] as? Bool) == false + + sut.reset() + sut.close() + } + + it("sanitize properties") { + let sut = self.getSut(flushAt: 1) + + sut.register(["boolIsOk": true, + "test5": UserDefaults.standard]) + + sut.capture("test event", + properties: ["foo": "bar", + "test1": UserDefaults.standard, + "arrayIsOk": [1, 2, 3], + "dictIsOk": ["1": "one"]], + userProperties: ["userProp": "value", + "test2": UserDefaults.standard], + userPropertiesSetOnce: ["userPropOnce": "value", + "test3": UserDefaults.standard], + groupProperties: ["groupProp": "value", + "test4": UserDefaults.standard]) + + let events = getBatchedEvents(server) + + expect(events.count) == 1 + let event = events.first! + + expect(event.properties["test1"]) == nil + expect(event.properties["test2"]) == nil + expect(event.properties["test3"]) == nil + expect(event.properties["test4"]) == nil + expect(event.properties["test5"]) == nil + expect(event.properties["arrayIsOk"]) != nil + expect(event.properties["dictIsOk"]) != nil + expect(event.properties["boolIsOk"]) != nil + + sut.reset() + sut.close() + } + } +} diff --git a/PostHogTests/PostHogSessionManagerTest.swift b/PostHogTests/PostHogSessionManagerTest.swift new file mode 100644 index 000000000..3ae343116 --- /dev/null +++ b/PostHogTests/PostHogSessionManagerTest.swift @@ -0,0 +1,49 @@ +// +// PostHogSessionManagerTest.swift +// PostHogTests +// +// Created by Ben White on 22.03.23. +// + +import Foundation +import Nimble +@testable import PostHog +import Quick + +class PostHogSessionManagerTest: QuickSpec { + func getSut() -> PostHogSessionManager { + let config = PostHogConfig(apiKey: "123") + return PostHogSessionManager(config) + } + + override func spec() { + it("Generates an anonymousId") { + let sut = self.getSut() + + let anonymousId = sut.getAnonymousId() + expect(anonymousId) != nil + let secondAnonymousId = sut.getAnonymousId() + expect(secondAnonymousId) == anonymousId + + sut.reset() + } + + it("Uses the anonymousId for distinctId if not set") { + let sut = self.getSut() + + let anonymousId = sut.getAnonymousId() + let distinctId = sut.getDistinctId() + expect(distinctId) == anonymousId + + let idToSet = UUID().uuidString + sut.setDistinctId(idToSet) + let newAnonymousId = sut.getAnonymousId() + let newDistinctId = sut.getDistinctId() + expect(newAnonymousId) == anonymousId + expect(newAnonymousId) != newDistinctId + expect(newDistinctId) == idToSet + + sut.reset() + } + } +} diff --git a/PostHogTests/PostHogStorageTest.swift b/PostHogTests/PostHogStorageTest.swift new file mode 100644 index 000000000..65f30ef23 --- /dev/null +++ b/PostHogTests/PostHogStorageTest.swift @@ -0,0 +1,97 @@ +// +// PostHogStorageTest.swift +// PostHogTests +// +// Created by Ben White on 08.02.23. +// + +import Foundation +import Nimble +@testable import PostHog +import Quick + +class PostHogStorageTest: QuickSpec { + func getSut() -> PostHogStorage { + let config = PostHogConfig(apiKey: "123") + return PostHogStorage(config) + } + + override func spec() { + it("returns the support dir URL") { + let url = applicationSupportDirectoryURL() + expect(url).toNot(beNil()) + expect(url.pathComponents[url.pathComponents.count - 2]) == "Application Support" + expect(url.lastPathComponent) == Bundle.main.bundleIdentifier + } + + it("creates folder if none exists") { + let url = applicationSupportDirectoryURL() + try? FileManager.default.removeItem(at: url) + + expect(FileManager.default.fileExists(atPath: url.path)) == false + + let sut = self.getSut() + + expect(FileManager.default.fileExists(atPath: sut.appFolderUrl.path)) == true + + sut.reset() + } + + it("persists and loads string") { + let sut = self.getSut() + + let str = "san francisco" + sut.setString(forKey: .distinctId, contents: str) + + expect(sut.getString(forKey: .distinctId)) == str + + sut.remove(key: .distinctId) + expect(sut.getString(forKey: .distinctId)).to(beNil()) + + sut.reset() + } + + it("persists and loads bool") { + let sut = self.getSut() + + sut.setBool(forKey: .optOut, contents: true) + + expect(sut.getBool(forKey: .optOut)) == true + + sut.remove(key: .optOut) + expect(sut.getString(forKey: .optOut)).to(beNil()) + + sut.reset() + } + + it("persists and loads dictionary") { + let sut = self.getSut() + + let dict = [ + "san francisco": "tech", + "new york": "finance", + "paris": "fashion", + ] + sut.setDictionary(forKey: .distinctId, contents: dict) + expect(sut.getDictionary(forKey: .distinctId) as? [String: String]) == dict + + sut.remove(key: .distinctId) + expect(sut.getDictionary(forKey: .distinctId)).to(beNil()) + + sut.reset() + } + + it("saves file to disk and removes from disk") { + let sut = self.getSut() + + let url = sut.url(forKey: .distinctId) + expect(try? url.checkResourceIsReachable()).to(beNil()) + sut.setString(forKey: .distinctId, contents: "sloth") + expect(try! url.checkResourceIsReachable()) == true + sut.remove(key: .distinctId) + expect(try? url.checkResourceIsReachable()).to(beNil()) + + sut.reset() + } + } +} diff --git a/PostHogTests/PostHogTest.swift b/PostHogTests/PostHogTest.swift deleted file mode 100644 index f4ae5e312..000000000 --- a/PostHogTests/PostHogTest.swift +++ /dev/null @@ -1,184 +0,0 @@ -import Foundation -import Nimble -import Quick - -@testable import PostHog - -class PostHogTest: QuickSpec { - override func spec() { - var harness: TestPostHog! - var posthog: PostHogSDK! - - beforeEach { - harness = TestPostHog() - posthog = harness.posthog - } - - afterEach { - harness.stop() - } - - it("creates a sensible default config") { - let config = PostHogConfig(apiKey: "test-api-key") - - expect(config.host) == URL(string: "https://app.posthog.com")! - expect(config.apiKey) == "test-api-key" - expect(config.flushAt) == 20 - expect(config.maxQueueSize) == 1000 - expect(config.maxBatchSize) == 100 - expect(config.flushIntervalSeconds) == 30 - expect(config.dataMode) == .wifi - } - - it("initialized correctly with api host") { - // The harness posthog is already setup with a different host -// expect(posthog.config.host) == URL(string: "http://localhost:9001") - } - - it("setups default IDs") { - expect(posthog.getAnonymousId()).toNot(beNil()) - expect(posthog.getDistinctId()) == posthog.getAnonymousId() - } - - it("persits IDs but resets the session ID on load") { - let anonymousId = posthog.getAnonymousId() - let distinctId = posthog.getDistinctId() - - let config = PostHogConfig(apiKey: "test-api-key") - let otherPostHog = PostHogSDK.with(config) - - let otherAnonymousId = otherPostHog.getAnonymousId() - let otherDistinctId = otherPostHog.getDistinctId() - - expect(anonymousId) == otherAnonymousId - expect(distinctId) == otherDistinctId - } - -// it("fires Application Opened for UIApplicationDidFinishLaunching") { -// testMiddleware.swallowEvent = true -// NotificationCenter.default.post(name: NSNotification.Name.UIApplicationDidFinishLaunching, object: testApplication, userInfo: [ -// UIApplication.LaunchOptionsKey.sourceApplication: "testApp", -// UIApplication.LaunchOptionsKey.url: "test://test", -// ]) -// -// let event = testMiddleware.lastContext?.payload as? PHGCapturePayload -// expect(event?.event) == "Application Opened" -// expect(event?.properties?["from_background"] as? Bool) == false -// expect(event?.properties?["referring_application"] as? String) == "testApp" -// expect(event?.properties?["url"] as? String) == "test://test" -// } -// -// it("fires Application Opened during UIApplicationWillEnterForeground") { -// testMiddleware.swallowEvent = true -// NotificationCenter.default.post(name: NSNotification.Name.UIApplicationWillEnterForeground, object: testApplication) -// let event = testMiddleware.lastContext?.payload as? PHGCapturePayload -// expect(event?.event) == "Application Opened" -// expect(event?.properties?["from_background"] as? Bool) == true -// } -// -// it("fires Application Backgrounded during UIApplicationDidEnterBackground") { -// testMiddleware.swallowEvent = true -// NotificationCenter.default.post(name: Notification.Name.UIApplicationDidEnterBackground, object: testApplication) -// let event = testMiddleware.lastContext?.payload as? PHGCapturePayload -// expect(event?.event) == "Application Backgrounded" -// } -// -// it("flushes when UIApplicationDidEnterBackground is fired") { -// posthog.capture("test") -// NotificationCenter.default.post(name: Notification.Name.UIApplicationDidEnterBackground, object: testApplication) -// expect(testApplication.backgroundTasks.count).toEventually(equal(1)) -// expect(testApplication.backgroundTasks[0].isEnded).toEventually(beFalse()) -// } -// -// it("respects maxQueueSize") { -// let max = 72 -// config.maxQueueSize = UInt(max) -// -// for i in 1...max * 2 { -// posthog.capture("test #\(i)") -// } -// -// let integration = posthog.test_payloadManager()?.test_postHogIntegration() -// expect(integration).notTo(beNil()) -// -// posthog.flush() -// waitUntil(timeout: DispatchTimeInterval.seconds(60)) {done in -// let queue = DispatchQueue(label: "test") -// -// queue.async { -// while(integration?.test_queue()?.count != max) { -// sleep(1) -// } -// -// done() -// } -// } -// } -// -// it("protocol conformance should not interfere with UIApplication interface") { -// // In Xcode8/iOS10, UIApplication.h typedefs UIBackgroundTaskIdentifier as NSUInteger, -// // whereas Swift has UIBackgroundTaskIdentifier typealiaed to Int. -// // This is likely due to a custom Swift mapping for UIApplication which got out of sync. -// // If we extract the exact UIApplication method names in PHGApplicationProtocol, -// // it will cause a type mismatch between the return value from beginBackgroundTask -// // and the argument for endBackgroundTask. -// // This would impact all code in a project that imports the framework. -// // Note that this doesn't appear to be an issue any longer in Xcode9b3. -// let task = UIApplication.shared.beginBackgroundTask(expirationHandler: nil) -// UIApplication.shared.endBackgroundTask(task) -// } -// -// it("flushes using flushTimer") { -// let integration = posthog.test_payloadManager()?.test_postHogIntegration() -// -// posthog.capture("test") -// -// expect(integration?.test_flushTimer()).toEventuallyNot(beNil()) -// expect(integration?.test_batchRequest()).to(beNil()) -// -// integration?.test_flushTimer()?.fire() -// -// expect(integration?.test_batchRequest()).toEventuallyNot(beNil()) -// } -// -// it("respects flushInterval") { -// let timer = posthog -// .test_payloadManager()? -// .test_postHogIntegration()? -// .test_flushTimer() -// -// expect(timer).toNot(beNil()) -// expect(timer?.timeInterval) == config.flushInterval -// } -// -// it("redacts sensible URLs from deep links capturing") { -// testMiddleware.swallowEvent = true -// posthog.config.captureDeepLinks = true -// posthog.open(URL(string: "fb123456789://authorize#access_token=hastoberedacted")!, options: [:]) -// -// -// let event = testMiddleware.lastContext?.payload as? PHGCapturePayload -// expect(event?.event) == "Deep Link Opened" -// expect(event?.properties?["url"] as? String) == "fb123456789://authorize#access_token=((redacted/fb-auth-token))" -// } -// -// it("redacts sensible URLs from deep links capturing using custom filters") { -// testMiddleware.swallowEvent = true -// posthog.config.payloadFilters["(myapp://auth\\?token=)([^&]+)"] = "$1((redacted/my-auth))" -// posthog.config.captureDeepLinks = true -// posthog.open(URL(string: "myapp://auth?token=hastoberedacted&other=stuff")!, options: [:]) -// -// -// let event = testMiddleware.lastContext?.payload as? PHGCapturePayload -// expect(event?.event) == "Deep Link Opened" -// expect(event?.properties?["url"] as? String) == "myapp://auth?token=((redacted/my-auth))&other=stuff" -// } -// -// it("defaults PHGQueue to an empty array when missing from file storage") { -// let integration = posthog.test_payloadManager()?.test_postHogIntegration() -// expect(integration).notTo(beNil()) -// integration?.test_fileStorage()?.resetAll() -// expect(integration?.test_queue()).to(beEmpty()) -// } - } -} diff --git a/PostHogTests/QueueTest.swift b/PostHogTests/QueueTest.swift deleted file mode 100644 index 26ffd7143..000000000 --- a/PostHogTests/QueueTest.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// QueueTest.swift -// PostHogTests -// -// Created by Ben White on 08.02.23. -// - -import Nimble -import Quick - -@testable import PostHog - -class QueueTest: QuickSpec { - override func spec() { - var queue: PostHogQueue! - - beforeEach { - let config = PostHogConfig(apiKey: "test") - let storage = PostHogStorage(config) - let api = PostHogApi(config) - queue = PostHogQueue(config, storage, api, nil) - } - - it("Adds items to the queue") { - queue.add(PostHogEvent(event: "event1", distinctId: "123")) - queue.add(PostHogEvent(event: "event1", distinctId: "123")) - queue.add(PostHogEvent(event: "event1", distinctId: "123")) - - expect(queue.depth) == 3 - } - - it("Consumes items from the queue") { - let consumedEvents = [PostHogEvent]() - let expectation = self.expectation(description: "Callback") - -// queue.consume { payload in -// consumedEvents = payload.events -// payload.completion(true) -// expectation.fulfill() -// } - - queue.add(PostHogEvent(event: "event1", distinctId: "123")) - queue.add(PostHogEvent(event: "event2", distinctId: "123")) - queue.add(PostHogEvent(event: "event3", distinctId: "123")) - queue.flush() - - await self.fulfillment(of: [expectation], timeout: 5) - - expect(consumedEvents.count) == 3 - expect(consumedEvents[0].event) == "event1" - expect(consumedEvents[1].event) == "event2" - expect(consumedEvents[2].event) == "event3" - expect(queue.depth) == 0 - } - - it("Returns processing to the queue if failed") { - let consumedEvents = [PostHogEvent]() - let expectation = self.expectation(description: "Callback") - -// queue.consume { payload in -// consumedEvents = payload.events -// payload.completion(false) -// expectation.fulfill() -// } - - queue.add(PostHogEvent(event: "event1", distinctId: "123")) - queue.add(PostHogEvent(event: "event2", distinctId: "123")) - queue.add(PostHogEvent(event: "event3", distinctId: "123")) - queue.flush() - - await self.fulfillment(of: [expectation], timeout: 5) - expect(consumedEvents.count) == 3 - expect(queue.depth) == 3 - } - - afterEach { - queue.clear() - } - } -} diff --git a/PostHogTests/SessionManagerTest.swift b/PostHogTests/SessionManagerTest.swift deleted file mode 100644 index 552282d45..000000000 --- a/PostHogTests/SessionManagerTest.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// SessionManagerTest.swift -// PostHogTests -// -// Created by Ben White on 22.03.23. -// - -import Foundation -import Nimble -@testable import PostHog -import Quick - -class SessionManagerTest: QuickSpec { - override func spec() { - var sessionManager: PostHogSessionManager! - var config: PostHogConfig! - - beforeEach { - config = PostHogConfig(apiKey: "test") - sessionManager = PostHogSessionManager(config: config) - } - - it("Generates an anonymousId") { - let anonymousId = sessionManager.getAnonymousId() - expect(anonymousId) != nil - let secondAnonymousId = sessionManager.getAnonymousId() - expect(secondAnonymousId) == anonymousId - } - - it("Uses the anonymousId for distinctId if not set") { - let anonymousId = sessionManager.getAnonymousId() - let distinctId = sessionManager.getDistinctId() - expect(distinctId) == anonymousId - - let idToSet = UUID().uuidString - sessionManager.setDistinctId(idToSet) - let newAnonymousId = sessionManager.getAnonymousId() - let newDistinctId = sessionManager.getDistinctId() - expect(newAnonymousId) == anonymousId - expect(newAnonymousId) != newDistinctId - expect(newDistinctId) == idToSet - } - } -} diff --git a/PostHogTests/StorageTest.swift b/PostHogTests/StorageTest.swift deleted file mode 100644 index 13a56c050..000000000 --- a/PostHogTests/StorageTest.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// StorageTest.swift -// PostHogTests -// -// Created by Ben White on 08.02.23. -// - -import Foundation -import Nimble -import Quick - -@testable import PostHog - -class StorageTest: QuickSpec { - override func spec() { - var storage: PostHogStorage! - beforeEach { - let url = applicationSupportDirectoryURL() - expect(url).toNot(beNil()) - expect(url.pathComponents[url.pathComponents.count - 2]) == "Application Support" - expect(url.lastPathComponent) == Bundle.main.bundleIdentifier - storage = PostHogStorage(PostHogConfig(apiKey: "test")) - } - - it("creates folder if none exists") { - var isDir: ObjCBool = false - let exists = FileManager.default.fileExists(atPath: storage.appFolderUrl.path, isDirectory: &isDir) - - expect(exists) == true - expect(isDir.boolValue) == true - } - - it("persists and loads string") { - let str = "san francisco" - storage.setString(forKey: .distinctId, contents: str) - - expect(storage.getString(forKey: .distinctId)) == str - - storage.remove(key: .distinctId) - expect(storage.getString(forKey: .distinctId)).to(beNil()) - } - - it("persists and loads numbers") { - storage.setNumber(forKey: .distinctId, contents: 1) - expect(storage.getNumber(forKey: .distinctId)) == 1 - - storage.setNumber(forKey: .distinctId, contents: 1.2) - expect(storage.getNumber(forKey: .distinctId)) == 1.2 - - storage.remove(key: .distinctId) - expect(storage.getNumber(forKey: .distinctId)).to(beNil()) - } - - it("persists and loads array") { - let array = [ - "san francisco", - "new york", - "tallinn", - ] - storage.setArray(forKey: .distinctId, contents: array) - expect(storage.getArray(forKey: .distinctId) as? [String]) == array - - storage.remove(key: .distinctId) - expect(storage.getArray(forKey: .distinctId)).to(beNil()) - } - - it("persists and loads dictionary") { - let dict = [ - "san francisco": "tech", - "new york": "finance", - "paris": "fashion", - ] - storage.setDictionary(forKey: .distinctId, contents: dict) - expect(storage.getDictionary(forKey: .distinctId) as? [String: String]) == dict - - storage.remove(key: .distinctId) - expect(storage.getDictionary(forKey: .distinctId)).to(beNil()) - } - - it("saves file to disk and removes from disk") { - let url = storage.url(forKey: .distinctId) - expect(try? url.checkResourceIsReachable()).to(beNil()) - storage.setString(forKey: .distinctId, contents: "sloth") - expect(try! url.checkResourceIsReachable()) == true - storage.remove(key: .distinctId) - expect(try? url.checkResourceIsReachable()).to(beNil()) - } - -// it("should work with crypto") { -// let url = PHGFileStorage.applicationSupportDirectoryURL() -// let crypto = PHGAES256Crypto(password: "thetrees") -// let s = PHGFileStorage(folder: url!, crypto: crypto) -// let dict = [ -// "san francisco": "tech", -// "new york": "finance", -// "paris": "fashion", -// ] -// s.setDictionary(dict, forKey: "cityMap") -// expect(s.dictionary(forKey: "cityMap") as? [String: String]) == dict -// -// s.removeKey("cityMap") -// expect(s.dictionary(forKey: "cityMap")).to(beNil()) -// } - - afterEach { - storage.reset() - } - } -} diff --git a/PostHogTests/TestUtils/MockPostHogServer.swift b/PostHogTests/TestUtils/MockPostHogServer.swift index 0c4b70ad2..b2be267a0 100644 --- a/PostHogTests/TestUtils/MockPostHogServer.swift +++ b/PostHogTests/TestUtils/MockPostHogServer.swift @@ -14,73 +14,104 @@ import OHHTTPStubsSwift @testable import PostHog class MockPostHogServer { - var requests = [URLRequest]() - var expectation: XCTestExpectation? - var expectationCount: Int? + var batchRequests = [URLRequest]() + var batchExpectation: XCTestExpectation? + var decideExpectation: XCTestExpectation? + var batchExpectationCount: Int? + var decideRequests = [URLRequest]() - func trackRequest(_ request: URLRequest) { - requests.append(request) + func trackBatchRequest(_ request: URLRequest) { + batchRequests.append(request) - if requests.count >= (expectationCount ?? 0) { - expectation?.fulfill() + if batchRequests.count >= (batchExpectationCount ?? 0) { + batchExpectation?.fulfill() } } + func trackDecide(_ request: URLRequest) { + decideRequests.append(request) + + decideExpectation?.fulfill() + } + + public var errorsWhileComputingFlags = false + public var return500 = false + init(port _: Int = 9001) { stub(condition: isPath("/decide")) { _ in + var flags = [ + "bool-value": true, + "string-value": "test", + "disabled-flag": false, + "number-value": true, + ] + + if self.errorsWhileComputingFlags { + flags["new-flag"] = true + flags.removeValue(forKey: "bool-value") + } + let obj: [String: Any] = [ - "featureFlags": [ - "bool-value": true, - "string-value": "test", - ], + "featureFlags": flags, "featureFlagPayloads": [ "payload-bool": "true", - "payload-number": "2", + "number-value": "2", "payload-string": "\"string-value\"", "payload-json": "{ \"foo\": \"bar\" }", ], + "errorsWhileComputingFlags": self.errorsWhileComputingFlags, ] return HTTPStubsResponse(jsonObject: obj, statusCode: 200, headers: nil) } stub(condition: isPath("/batch")) { _ in - HTTPStubsResponse(jsonObject: ["status": "ok"], statusCode: 200, headers: nil) + if self.return500 { + HTTPStubsResponse(jsonObject: [], statusCode: 500, headers: nil) + } else { + HTTPStubsResponse(jsonObject: ["status": "ok"], statusCode: 200, headers: nil) + } } HTTPStubs.onStubActivation { request, _, _ in if request.url?.path == "/batch" { - self.trackRequest(request) + self.trackBatchRequest(request) + } else if request.url?.path == "/decide" { + self.trackDecide(request) } } } - func start() { + func start(batchCount: Int = 1) { + reset(batchCount: batchCount) + HTTPStubs.setEnabled(true) } func stop() { - requests = [] - HTTPStubs.removeAllStubs() - } + reset() - func expectation(_ requestCount: Int) -> XCTestExpectation { - expectation = XCTestExpectation(description: "\(requestCount) requests to occur") - expectationCount = requestCount - - return expectation! + HTTPStubs.removeAllStubs() } - var posthogConfig: PostHogConfig { - let config = PostHogConfig(apiKey: "test-123", host: "http://localhost:9001") - - return config + func reset(batchCount: Int = 1) { + batchRequests = [] + decideRequests = [] + batchExpectation = XCTestExpectation(description: "\(batchCount) batch requests to occur") + decideExpectation = XCTestExpectation(description: "1 decide requests to occur") + batchExpectationCount = batchCount + errorsWhileComputingFlags = false + return500 = false } - func parseBatchRequest(_ context: URLRequest) -> [String: Any]? { + func parseRequest(_ context: URLRequest, gzip: Bool = true) -> [String: Any]? { var unzippedData: Data? do { - unzippedData = try context.body()!.gzipped() + if gzip { + unzippedData = try context.body()!.gunzipped() + } else { + unzippedData = context.body()! + } } catch { // its ok } @@ -89,7 +120,7 @@ class MockPostHogServer { } func parsePostHogEvents(_ context: URLRequest) -> [PostHogEvent] { - let data = parseBatchRequest(context) + let data = parseRequest(context) guard let batchEvents = data?["batch"] as? [[String: Any]] else { return [] } diff --git a/PostHogTests/TestUtils/TestPostHog.swift b/PostHogTests/TestUtils/TestPostHog.swift index 2bd6a299c..ee6107c4a 100644 --- a/PostHogTests/TestUtils/TestPostHog.swift +++ b/PostHogTests/TestUtils/TestPostHog.swift @@ -9,36 +9,38 @@ import Foundation import PostHog import XCTest -class TestPostHog { - var server: MockPostHogServer! - var posthog: PostHogSDK! - - init() { - server = MockPostHogServer() - server.start() - let config = server.posthogConfig - posthog = PostHogSDK.with(config) +func getBatchedEvents(_ server: MockPostHogServer) -> [PostHogEvent] { + let result = XCTWaiter.wait(for: [server.batchExpectation!], timeout: 15.0) + + if result != XCTWaiter.Result.completed { + XCTFail("The expected requests never arrived") } - func stop() { - server.stop() - posthog.reset() + var events: [PostHogEvent] = [] + for request in server.batchRequests.reversed() { + let items = server.parsePostHogEvents(request) + events.append(contentsOf: items) } - func getBatchedEvents() -> [PostHogEvent] { - posthog.flush() - let result = XCTWaiter.wait(for: [server.expectation(1)], timeout: 2.0) + return events +} + +func waitDecideRequest(_ server: MockPostHogServer) { + let result = XCTWaiter.wait(for: [server.decideExpectation!], timeout: 15) - if result != XCTWaiter.Result.completed { - XCTFail("The expected requests never arrived") - } + if result != XCTWaiter.Result.completed { + XCTFail("The expected requests never arrived") + } +} - for request in server.requests.reversed() { - if request.url?.path == "/batch" { - return server.parsePostHogEvents(request) - } - } +func getDecideRequest(_ server: MockPostHogServer) -> [[String: Any]] { + waitDecideRequest(server) - return [] + var requests: [[String: Any]] = [] + for request in server.decideRequests.reversed() { + let item = server.parseRequest(request, gzip: false) + requests.append(item!) } + + return requests }