Repository navigation
Feat/support for Enriched flag analytics #487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ioannisj
merged 19 commits into
PostHog:main
from
jeremiahseun:feat/enriched-flag-analytic
Mar 11, 2026
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
6b75002
feat: automatically capture deep link events
jeremiahseun c6791be
docs: add TESTING_DEEP_LINKS.md
jeremiahseun 239e1cb
docs: add TESTING_DEEP_LINKS.md
jeremiahseun 32d183d
docs: add TESTING_DEEP_LINKS.md
jeremiahseun 55732bc
Deep link integration, tests, and project config
jeremiahseun 995939a
Merge pull request #1 from jeremiahseun/add-referrer-url-automatically
jeremiahseun 65dc2c2
feat: add captureFeatureView and captureFeatureInteraction
jeremiahseun 17dee20
To prevent lifecycle events from interfering with test assertions
jeremiahseun f524e25
Revert "Merge pull request #1 from jeremiahseun/add-referrer-url-auto…
jeremiahseun 6cca921
feat: add captureFeatureView and captureFeatureInteraction
jeremiahseun 06c84b7
To prevent lifecycle events from interfering with test assertions
jeremiahseun 0003405
Merge branch 'feat/enriched-flag-analytics-5134481175872936769' of ht…
jeremiahseun 1376079
Merge branch 'main' into feat/enriched-flag-analytic
jeremiahseun 19fd08e
Support feature flag variants in captures
jeremiahseun 5d50daf
Migrated test from Quick and Nimble
jeremiahseun 2b1ad49
Merge branch 'main' into feat/enriched-flag-analytic
jeremiahseun 4118a20
Require feature variant when capturing events
jeremiahseun 05232f4
Merge branch 'main' into feat/enriched-flag-analytic
ioannisj b8e6461
chore: add changeset
ioannisj File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "posthog-ios": minor | ||
| --- | ||
|
|
||
| feat: Add `captureFeatureView` and `captureFeatureInteraction` methods for tracking feature flag analytics |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| import Foundation | ||
| @testable import PostHog | ||
| import Testing | ||
|
|
||
| @Suite("PostHogEnrichedAnalytics Tests", .serialized) | ||
| class PostHogEnrichedAnalyticsTest { | ||
| let server: MockPostHogServer | ||
|
|
||
| init() { | ||
| Self.deleteDefaults() | ||
| server = MockPostHogServer(version: 4) | ||
| server.start() | ||
| } | ||
|
|
||
| deinit { | ||
| server.stop() | ||
| } | ||
|
|
||
| private static func deleteDefaults() { | ||
| let userDefaults = UserDefaults.standard | ||
| userDefaults.removeObject(forKey: "PHGVersionKey") | ||
| userDefaults.removeObject(forKey: "PHGBuildKeyV2") | ||
| userDefaults.synchronize() | ||
|
|
||
| deleteSafely(applicationSupportDirectoryURL()) | ||
| } | ||
|
|
||
| func getSut(optOut: Bool = false) -> PostHogSDK { | ||
| let config = PostHogConfig(apiKey: testAPIKey, host: "http://localhost:9001") | ||
| config.flushAt = 1 | ||
| config.captureApplicationLifecycleEvents = false | ||
| config.disableReachabilityForTesting = true | ||
| config.disableQueueTimerForTesting = true | ||
| config.optOut = optOut | ||
|
|
||
| let storage = PostHogStorage(config) | ||
| storage.reset() | ||
|
|
||
| return PostHogSDK.with(config) | ||
| } | ||
|
|
||
| @Test("captures $feature_view event with explicit variant") | ||
| func capturesFeatureViewEvent() { | ||
| let sut = getSut() | ||
|
|
||
| // An explicit flagVariant must be supplied (or a flag must be loaded) so the guard passes. | ||
| sut.captureFeatureView(flag: "test-flag", flagVariant: "control") | ||
|
|
||
| let events = getBatchedEvents(server) | ||
| #expect(events.count == 1) | ||
|
|
||
| let event = events.first! | ||
| #expect(event.event == "$feature_view") | ||
| #expect(event.properties["feature_flag"] as? String == "test-flag") | ||
| #expect(event.properties["feature_flag_variant"] as? String == "control") | ||
| let setProps = event.properties["$set"] as? [String: Any] | ||
| #expect(setProps?["$feature_view/test-flag"] as? String == "control") | ||
|
|
||
| sut.reset() | ||
| sut.close() | ||
| } | ||
|
|
||
| @Test("captures $feature_interaction event with explicit variant") | ||
| func capturesFeatureInteractionEvent() { | ||
| let sut = getSut() | ||
|
|
||
| // An explicit flagVariant must be supplied (or a flag must be loaded) so the guard passes. | ||
| sut.captureFeatureInteraction(flag: "test-flag", flagVariant: "control") | ||
|
|
||
| let events = getBatchedEvents(server) | ||
| #expect(events.count == 1) | ||
|
|
||
| let event = events.first! | ||
| #expect(event.event == "$feature_interaction") | ||
| #expect(event.properties["feature_flag"] as? String == "test-flag") | ||
| #expect(event.properties["feature_flag_variant"] as? String == "control") | ||
| let setProps = event.properties["$set"] as? [String: Any] | ||
| #expect(setProps?["$feature_interaction/test-flag"] as? String == "control") | ||
|
|
||
| sut.reset() | ||
| sut.close() | ||
| } | ||
|
|
||
| @Test("does not capture $feature_view event when no variant is available") | ||
| func doesNotCaptureFeatureViewWhenNoVariant() { | ||
| let sut = getSut() | ||
|
|
||
| // No flagVariant is passed and no flags are loaded, so the guard should fire and return early. | ||
| sut.captureFeatureView(flag: "unknown-flag", flagVariant: nil) | ||
|
|
||
| let events = getBatchedEvents(server, timeout: 1.0, failIfNotCompleted: false) | ||
| #expect(events.count == 0) | ||
|
|
||
| sut.reset() | ||
| sut.close() | ||
| } | ||
|
|
||
| @Test("does not capture $feature_interaction event when no variant is available") | ||
| func doesNotCaptureFeatureInteractionWhenNoVariant() { | ||
| let sut = getSut() | ||
|
|
||
| // No flagVariant is passed and no flags are loaded, so the guard should fire and return early. | ||
| sut.captureFeatureInteraction(flag: "unknown-flag", flagVariant: nil) | ||
|
|
||
| let events = getBatchedEvents(server, timeout: 1.0, failIfNotCompleted: false) | ||
| #expect(events.count == 0) | ||
|
|
||
| sut.reset() | ||
| sut.close() | ||
| } | ||
|
|
||
| @Test("does not capture feature view if opt out") | ||
| func doesNotCaptureFeatureViewIfOptOut() { | ||
| let sut = getSut(optOut: true) | ||
|
|
||
| sut.captureFeatureView(flag: "test-flag", flagVariant: "control") | ||
|
|
||
| let events = getBatchedEvents(server, timeout: 1.0, failIfNotCompleted: false) | ||
| #expect(events.count == 0) | ||
|
|
||
| sut.reset() | ||
| sut.close() | ||
| } | ||
|
|
||
| @Test("does not capture feature interaction if opt out") | ||
| func doesNotCaptureFeatureInteractionIfOptOut() { | ||
| let sut = getSut(optOut: true) | ||
|
|
||
| sut.captureFeatureInteraction(flag: "test-flag", flagVariant: "control") | ||
|
|
||
| let events = getBatchedEvents(server, timeout: 1.0, failIfNotCompleted: false) | ||
| #expect(events.count == 0) | ||
|
|
||
| sut.reset() | ||
| sut.close() | ||
| } | ||
|
|
||
| @Test("does not capture feature view if disabled") | ||
| func doesNotCaptureFeatureViewIfDisabled() { | ||
| let sut = getSut() | ||
| sut.close() // Disable SDK | ||
|
|
||
| sut.captureFeatureView(flag: "test-flag", flagVariant: "control") | ||
|
|
||
| let events = getBatchedEvents(server, timeout: 1.0, failIfNotCompleted: false) | ||
| #expect(events.count == 0) | ||
| } | ||
|
|
||
| @Test("does not capture feature interaction if disabled") | ||
| func doesNotCaptureFeatureInteractionIfDisabled() { | ||
| let sut = getSut() | ||
| sut.close() // Disable SDK | ||
|
|
||
| sut.captureFeatureInteraction(flag: "test-flag", flagVariant: "control") | ||
|
|
||
| let events = getBatchedEvents(server, timeout: 1.0, failIfNotCompleted: false) | ||
| #expect(events.count == 0) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.