Skip to content
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

fix: session replay respect feature flag variants #209

Merged
merged 13 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Next

- recording: session replay respect feature flag variants ([#209](https://github.com/PostHog/posthog-ios/pull/209))

## 3.12.7 - 2024-10-09

- add appGroupIdentifier in posthog config ([#207](https://github.com/PostHog/posthog-ios/pull/207))
Expand Down
31 changes: 29 additions & 2 deletions PostHog/PostHogFeatureFlags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,46 @@ class PostHogFeatureFlags {

private func preloadSesssionReplayFlag() {
var sessionReplay: [String: Any]?
var featureFlags: [String: Any]?
featureFlagsLock.withLock {
sessionReplay = self.storage.getDictionary(forKey: .sessionReplay) as? [String: Any]
featureFlags = self.getCachedFeatureFlags()
}

if let sessionReplay = sessionReplay {
sessionReplayFlagActive = true
sessionReplayFlagActive = isRecordingActive(featureFlags ?? [:], sessionReplay)

if let endpoint = sessionReplay["endpoint"] as? String {
config.snapshotEndpoint = endpoint
}
}
}

private func isRecordingActive(_ featureFlags: [String: Any], _ sessionRecording: [String: Any]) -> Bool {
var recordingActive = true
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default is active since the sessionRecording is a Map and not a boolean unless I find the linkedFlag and can compare things


// check for boolean flags
if let linkedFlag = sessionRecording["linkedFlag"] as? String,
let value = featureFlags[linkedFlag] as? Bool
{
recordingActive = value
// check for specific flag variant
} else if let linkedFlag = sessionRecording["linkedFlag"] as? [String: Any],
let flag = linkedFlag["flag"] as? String,
let variant = linkedFlag["variant"] as? String,
let value = featureFlags[flag] as? String
{
recordingActive = value == variant
}
// check for multi flag variant (any)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we clean up these comments here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so since it's a valid result from the API and might help while debugging things

// if let linkedFlag = sessionRecording["linkedFlag"] as? String,
// featureFlags[linkedFlag] != nil
// is also a valid check bbut since we cannot check the value of the flag,
// we consider session recording is active

return recordingActive
}

func loadFeatureFlags(
distinctId: String,
anonymousId: String,
Expand Down Expand Up @@ -94,7 +121,7 @@ class PostHogFeatureFlags {
if let endpoint = sessionRecording["endpoint"] as? String {
self.config.snapshotEndpoint = endpoint
}
self.sessionReplayFlagActive = true
self.sessionReplayFlagActive = self.isRecordingActive(featureFlags, sessionRecording)
self.storage.setDictionary(forKey: .sessionReplay, contents: sessionRecording)
}
#endif
Expand Down
11 changes: 8 additions & 3 deletions PostHog/PostHogStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,20 @@ class PostHogStorage {
}

/**
There are cases where applications using posthog-ios want to share analytics data between host app and an app extension, Widget or App Clip. If there's a defined `appGroupIdentifier` in configuration, we want to use a shared container for storing data so that extensions correcly identify a user (and batch process events)
There are cases where applications using posthog-ios want to share analytics data between host app and
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just fixed a lint issue (line_length)

an app extension, Widget or App Clip. If there's a defined `appGroupIdentifier` in configuration,
we want to use a shared container for storing data so that extensions correcly identify a user (and batch process events)
*/
private static func getAppFolderUrl(from configuration: PostHogConfig) -> URL {
/**

From Apple Docs:
In iOS, the value is nil when the group identifier is invalid. In macOS, a URL of the expected form is always returned, even if the app group is invalid, so be sure to test that you can access the underlying directory before attempting to use it.
In iOS, the value is nil when the group identifier is invalid. In macOS, a URL of the expected form is always
returned, even if the app group is invalid, so be sure to test that you can access the underlying directory
before attempting to use it.

MacOS: The system also creates the Library/Application Support, Library/Caches, and Library/Preferences subdirectories inside the group directory the first time you use it
MacOS: The system also creates the Library/Application Support, Library/Caches, and Library/Preferences
subdirectories inside the group directory the first time you use it
iOS: The system creates only the Library/Caches subdirectory automatically

see: https://developer.apple.com/documentation/foundation/filemanager/1412643-containerurl/
Expand Down
6 changes: 2 additions & 4 deletions PostHog/Replay/URLSessionInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,8 @@
private func finishAll() {
var completedTasks: [URLSessionTask: NetworkSample] = [:]
tasksLock.withLock {
for item in samplesByTask {
if item.key.state == .completed {
completedTasks[item.key] = item.value
}
for item in samplesByTask where item.key.state == .completed {
completedTasks[item.key] = item.value
Comment on lines +144 to +145
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just fixing a lint issue

}
}

Expand Down
111 changes: 111 additions & 0 deletions PostHogTests/PostHogFeatureFlagsTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,117 @@ class PostHogFeatureFlagsTest: QuickSpec {

storage.reset()
}

it("returns isSessionReplayFlagActive true if bool linked flag is enabled") {
let storage = PostHogStorage(self.config)

let sut = self.getSut(storage: storage)

expect(sut.isSessionReplayFlagActive()) == false

let group = DispatchGroup()
group.enter()

server.returnReplay = true
server.returnReplayWithVariant = true

sut.loadFeatureFlags(distinctId: "distinctId", anonymousId: "anonymousId", groups: ["group": "value"], callback: {
group.leave()
})

group.wait()

expect(storage.getDictionary(forKey: .sessionReplay)) != nil
expect(self.config.snapshotEndpoint) == "/newS/"
expect(sut.isSessionReplayFlagActive()) == true

storage.reset()
}

it("returns isSessionReplayFlagActive false if bool linked flag is disabled") {
let storage = PostHogStorage(self.config)

let sut = self.getSut(storage: storage)

expect(sut.isSessionReplayFlagActive()) == false

let group = DispatchGroup()
group.enter()

server.returnReplay = true
server.returnReplayWithVariant = true
server.replayVariantValue = false

sut.loadFeatureFlags(distinctId: "distinctId", anonymousId: "anonymousId", groups: ["group": "value"], callback: {
group.leave()
})

group.wait()

expect(storage.getDictionary(forKey: .sessionReplay)) != nil
expect(self.config.snapshotEndpoint) == "/newS/"
expect(sut.isSessionReplayFlagActive()) == false

storage.reset()
}

it("returns isSessionReplayFlagActive true if multi variant linked flag is a match") {
let storage = PostHogStorage(self.config)

let sut = self.getSut(storage: storage)

expect(sut.isSessionReplayFlagActive()) == false

let group = DispatchGroup()
group.enter()

server.returnReplay = true
server.returnReplayWithVariant = true
server.returnReplayWithMultiVariant = true
server.replayVariantName = "recording-platform"
server.replayVariantValue = ["flag": "recording-platform-check", "variant": "web"]

sut.loadFeatureFlags(distinctId: "distinctId", anonymousId: "anonymousId", groups: ["group": "value"], callback: {
group.leave()
})

group.wait()

expect(storage.getDictionary(forKey: .sessionReplay)) != nil
expect(self.config.snapshotEndpoint) == "/newS/"
expect(sut.isSessionReplayFlagActive()) == true

storage.reset()
}

it("returns isSessionReplayFlagActive false if multi variant linked flag is not a match") {
let storage = PostHogStorage(self.config)

let sut = self.getSut(storage: storage)

expect(sut.isSessionReplayFlagActive()) == false

let group = DispatchGroup()
group.enter()

server.returnReplay = true
server.returnReplayWithVariant = true
server.returnReplayWithMultiVariant = true
server.replayVariantName = "recording-platform"
server.replayVariantValue = ["flag": "recording-platform-check", "variant": "mobile"]

sut.loadFeatureFlags(distinctId: "distinctId", anonymousId: "anonymousId", groups: ["group": "value"], callback: {
group.leave()
})

group.wait()

expect(storage.getDictionary(forKey: .sessionReplay)) != nil
expect(self.config.snapshotEndpoint) == "/newS/"
expect(sut.isSessionReplayFlagActive()) == false

storage.reset()
}
#endif
}
}
17 changes: 16 additions & 1 deletion PostHogTests/TestUtils/MockPostHogServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ class MockPostHogServer {
public var errorsWhileComputingFlags = false
public var return500 = false
public var returnReplay = false
public var returnReplayWithVariant = false
public var returnReplayWithMultiVariant = false
public var replayVariantName = "myBooleanRecordingFlag"
public var replayVariantValue: Any = true

init(port _: Int = 9001) {
stub(condition: isPath("/decide")) { _ in
Expand All @@ -45,6 +49,8 @@ class MockPostHogServer {
"string-value": "test",
"disabled-flag": false,
"number-value": true,
"recording-platform-check": "web",
self.replayVariantName: self.replayVariantValue,
]

if self.errorsWhileComputingFlags {
Expand All @@ -64,9 +70,18 @@ class MockPostHogServer {
]

if self.returnReplay {
let sessionRecording: [String: Any] = [
var sessionRecording: [String: Any] = [
"endpoint": "/newS/",
]

if self.returnReplayWithVariant {
if self.returnReplayWithMultiVariant {
sessionRecording["linkedFlag"] = self.replayVariantValue
} else {
sessionRecording["linkedFlag"] = self.replayVariantName
}
}

obj["sessionRecording"] = sessionRecording
} else {
obj["sessionRecording"] = false
Expand Down
Loading