Skip to content

Commit

Permalink
feat: add appGroupIdentifier in posthog config (#207)
Browse files Browse the repository at this point in the history
* feat: add appGroupIdentifier in posthog config

* chore: update CHANGELOG.md

* refactor: move createDirectoryAtURLIfNeeded in Fileutils

* chore(test): improve test output with xcpretty

* chore: fix typo

* refactor: move getAppFolderUrl as a static member of PostHogStorage

* test(config): ensure application support fallback when app group identifier is nil

* refactor: remove unnecessary optional parameter
  • Loading branch information
ioannisj authored Oct 8, 2024
1 parent 1eba0e8 commit acd7e74
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 13 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Next

- add appGroupIdentifier in posthog config ([#207](https://github.com/PostHog/posthog-ios/pull/207))

## 3.12.6 - 2024-10-02

- recording: capture network logs from dataTask requests without CompletionHandler ([#203](https://github.com/PostHog/posthog-ios/pull/203))
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ swiftFormat:
swiftformat . --swiftversion 5.3

testOniOSSimulator:
set -o pipefail && xcrun xcodebuild test -scheme PostHog -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.0.1' | xcpretty
set -o pipefail && xcrun xcodebuild test -scheme PostHog -destination 'platform=iOS Simulator,name=iPhone 15,OS=18.0' | xcpretty

testOnMacSimulator:
set -o pipefail && xcrun xcodebuild test -scheme PostHog -destination 'platform=macOS' | xcpretty

test:
swift test
swift test | xcpretty

lint:
swiftformat . --lint --swiftversion 5.3 && swiftlint
Expand Down
5 changes: 5 additions & 0 deletions PostHog/PostHogConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ import Foundation
/// Determines the behavior for processing user profiles.
@objc public var personProfiles: PostHogPersonProfiles = .identifiedOnly

/// The identifier of the App Group that should be used to store shared analytics data.
/// PostHog will try to get the physical location of the App Group’s shared container, otherwise fallback to the default location
/// Default: nil
@objc public var appGroupIdentifier: String?

/// Internal
/// Do not modify it, this flag is read and updated by the SDK via feature flags
@objc public var snapshotEndpoint: String = "/s/"
Expand Down
45 changes: 34 additions & 11 deletions PostHog/PostHogStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import Foundation
posthog-ios stores data either to file or to UserDefaults in order to support tvOS. As recordings won't work on tvOS anyways and we have no tvOS users so far,
we are opting to only support iOS via File storage.
*/

func applicationSupportDirectoryURL() -> URL {
let url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
return url.appendingPathComponent(Bundle.main.bundleIdentifier!)
Expand Down Expand Up @@ -45,20 +44,11 @@ class PostHogStorage {
init(_ config: PostHogConfig) {
self.config = config

appFolderUrl = applicationSupportDirectoryURL() // .appendingPathComponent(config.apiKey)
appFolderUrl = Self.getAppFolderUrl(from: config)

createDirectoryAtURLIfNeeded(url: appFolderUrl)
}

private func createDirectoryAtURLIfNeeded(url: URL) {
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)")
}
}

public func url(forKey key: StorageKey) -> URL {
appFolderUrl.appendingPathComponent(key.rawValue)
}
Expand Down Expand Up @@ -129,6 +119,39 @@ class PostHogStorage {
setData(forKey: key, contents: data)
}

/**
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.

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/
*/
func appGroupContainerUrl() -> URL? {
guard let appGroupIdentifier = configuration.appGroupIdentifier else { return nil }

let url = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)?
.appendingPathComponent("Library/Application Support/")
.appendingPathComponent(Bundle.main.bundleIdentifier!)

if let url {
createDirectoryAtURLIfNeeded(url: url)
return directoryExists(url) ? url : nil
}

return nil
}

return appGroupContainerUrl() ?? applicationSupportDirectoryURL()
}

public func reset() {
// sadly the StorageKey.allCases does not work here
deleteSafely(url(forKey: .distinctId))
Expand Down
15 changes: 15 additions & 0 deletions PostHog/Utils/FileUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,18 @@ public func deleteSafely(_ file: URL) {
}
}
}

/// Check if provided directory exists
func directoryExists(_ directory: URL) -> Bool {
var isDirectory: ObjCBool = false
return FileManager.default.fileExists(atPath: directory.path, isDirectory: &isDirectory) && isDirectory.boolValue
}

func createDirectoryAtURLIfNeeded(url: URL) {
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)")
}
}
10 changes: 10 additions & 0 deletions PostHogTests/PostHogStorageTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,15 @@ class PostHogStorageTest: QuickSpec {

sut.reset()
}

it("falls back to application support directory when app group identifier is not provided") {
let config = PostHogConfig(apiKey: "123")
config.appGroupIdentifier = nil
let sut = PostHogStorage(config)
let url = sut.appFolderUrl
expect(url).toNot(beNil())
expect(url.pathComponents[url.pathComponents.count - 2]) == "Application Support"
expect(url.lastPathComponent) == Bundle.main.bundleIdentifier
}
}
}

0 comments on commit acd7e74

Please sign in to comment.