Skip to content

Commit

Permalink
fix: session replay respect feature flag variants (#209)
Browse files Browse the repository at this point in the history
  • Loading branch information
marandaneto authored Oct 14, 2024
1 parent 05a77c4 commit 0fe478a
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 10 deletions.
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

// 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)
// 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
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
}
}

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

0 comments on commit 0fe478a

Please sign in to comment.