Skip to content
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Next

- fix: call the flags api with the correct groups key name (the api has a back compatible fix already) ([#389](https://github.com/PostHog/posthog-ios/pull/389))
- fix: only set getDefaultPersonProperties with person properties that are evaluated on the server ([#389](https://github.com/PostHog/posthog-ios/pull/389))
- fix: set and reset PersonPropertiesForFlags and GroupPropertiesForFlags reload flags automatically (or set reloadFeatureFlags: false) ([#389](https://github.com/PostHog/posthog-ios/pull/389))

## 3.34.0 - 2025-10-15

- feat: add config option to disable swizzling ([#388](https://github.com/PostHog/posthog-ios/pull/388))
Expand Down
2 changes: 1 addition & 1 deletion PostHog/PostHogApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ class PostHogApi {
var toSend: [String: Any] = [
"api_key": self.config.apiKey,
"distinct_id": distinctId,
"$groups": groups,
"groups": groups,
]

if let anonymousId {
Expand Down
78 changes: 36 additions & 42 deletions PostHog/PostHogContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,41 @@ class PostHogContext {
theSdkInfo
}

private lazy var thePersonPropertiesContext: [String: Any] = {
let staticCtx = staticContext()
let sdkInfo = sdkInfo()
var personProperties: [String: Any] = [:]

// App information
if let appVersion = staticCtx["$app_version"] {
personProperties["$app_version"] = appVersion
}
if let appBuild = staticCtx["$app_build"] {
personProperties["$app_build"] = appBuild
}

if let appNamespace = staticCtx["$app_namespace"] {
personProperties["$app_namespace"] = appNamespace
}

// Operating system information
if let osName = staticCtx["$os_name"] {
personProperties["$os_name"] = osName
}
if let osVersion = staticCtx["$os_version"] {
personProperties["$os_version"] = osVersion
}

// Device information
if let deviceType = staticCtx["$device_type"] {
personProperties["$device_type"] = deviceType
}

personProperties.merge(sdkInfo) { _, new in new }

return personProperties
}()

private func platform() -> String {
var sysctlName = "hw.machine"

Expand Down Expand Up @@ -202,48 +237,7 @@ class PostHogContext {
/// Returns person properties context by extracting relevant properties from static context.
/// This centralizes the logic for determining which properties should be used as person properties.
func personPropertiesContext() -> [String: Any] {
let staticCtx = staticContext()
var personProperties: [String: Any] = [:]

// App information
if let appVersion = staticCtx["$app_version"] {
personProperties["$app_version"] = appVersion
}
if let appBuild = staticCtx["$app_build"] {
personProperties["$app_build"] = appBuild
}

// Operating system information
if let osName = staticCtx["$os_name"] {
personProperties["$os_name"] = osName
}
if let osVersion = staticCtx["$os_version"] {
personProperties["$os_version"] = osVersion
}

// Device information
if let deviceType = staticCtx["$device_type"] {
personProperties["$device_type"] = deviceType
}
if let deviceManufacturer = staticCtx["$device_manufacturer"] {
personProperties["$device_manufacturer"] = deviceManufacturer
}
if let deviceModel = staticCtx["$device_model"] {
personProperties["$device_model"] = deviceModel
}

// Localization - read directly to avoid expensive dynamicContext call
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) {
if let languageCode = Locale.current.language.languageCode {
personProperties["$locale"] = languageCode.identifier
}
} else {
if let languageCode = Locale.current.languageCode {
personProperties["$locale"] = languageCode
}
}

return personProperties
thePersonPropertiesContext
}

private func registerNotifications() {
Expand Down
73 changes: 61 additions & 12 deletions PostHog/PostHogSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -983,10 +983,28 @@ let maxRetryDelay = 30.0
/// // Feature flags will now use only server-side properties
/// let flagValue = PostHogSDK.shared.isFeatureEnabled("feature")
/// ```
///
/// - Note: This method does not automatically reload feature flags. Call `reloadFeatureFlags()`
Copy link
Member Author

@marandaneto marandaneto Nov 6, 2025

Choose a reason for hiding this comment

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

people might be calling resetPersonPropertiesForFlags and then reloadFeatureFlags
2 options:
we default reloadFeatureFlags to false to avoid calling reloadFeatureFlags twice
we let it as is since the loadingFeatureFlagsLock and loadingFeatureFlags will do the job and bail out immediatelly on the 2nd call
i chose the later, let me know if thats not ideal.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah the only problem I see is that the reloadFeatureFlags callback will never be called.

Maybe not for this PR but we need a way to save the callback if there's another reload request in-flight and call it once the original request finishes

Copy link
Member Author

Choose a reason for hiding this comment

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

not sure i follow, which callback?

Copy link
Member Author

Choose a reason for hiding this comment

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

this is similar to call identify, we just call reloadFeatureFlags and dont allow the user to pass a callback unless they want to relaod it manually passing a callback

Copy link
Member Author

Choose a reason for hiding this comment

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

if thats the case, they could call the method with reloadFeatureFlags: false and then manually reloadFeatureFlags(...)

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 wonder if we should add a bool parameters to reloadFeatureFlags then that will reset person or group parameters

not sure i understand, mind clarifying?

Copy link
Contributor

Choose a reason for hiding this comment

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

What I mean is maybe we can give people a way to do:

PostHog.shared.reloadFeatureFlags(
  resetPersonProperties: true
) {
  // completion handler
}

Instead of them calling resetPersonPropertiesForFlags and then reloadFeatureFlags

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 dont think thats a good API
there are so many reasons to reload the flags, in this case the reloadFeatureFlags method would have a bunch of things (reset groups, reset all, etc).
its a much more natural API to eg
resetPersonPropertiesForFlags and flags are automatically reloaded unless opt out
or resetPersonPropertiesForFlags never reloads and the user has to reload manually, it seems a much better UX IMO
its the intention and the side effect and not the other way around if that makes sense

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 will merge to unblock the fix

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeap agreed. I think a future improvement is saving the callbacks from reloadFeatureFlags calls in memory and calling them whenever flags reload (for whatever reason) so that we make sure that the logic that people attach there always gets executed

/// after resetting if you want to immediately refresh flags with the cleared properties.
@objc public func resetPersonPropertiesForFlags() {
resetPersonPropertiesForFlags(reloadFeatureFlags: true)
}

/// Resets all person properties that were set for feature flag evaluation.
///
/// After calling this method, feature flag evaluation will only use server-side person properties
/// and will not include any locally overridden properties.
///
/// ## Example Usage
/// ```swift
/// // Clear all locally set person properties for flags
/// PostHogSDK.shared.resetPersonPropertiesForFlags(reloadFeatureFlags: true)
///
/// // Feature flags will now use only server-side properties
/// let flagValue = PostHogSDK.shared.isFeatureEnabled("feature")
/// ```
///
/// - Parameters:
/// - reloadFeatureFlags: Whether to automatically reload feature flags after resetting properties
@objc(resetPersonPropertiesForFlagsWithReloadFeatureFlags:)
public func resetPersonPropertiesForFlags(reloadFeatureFlags: Bool = true) {
if !isEnabled() {
return
}
Expand All @@ -996,6 +1014,10 @@ let maxRetryDelay = 30.0
}

remoteConfig?.resetPersonPropertiesForFlags()

if reloadFeatureFlags {
remoteConfig?.reloadFeatureFlags()
}
}

/// Sets properties for a specific group type to include when evaluating feature flags.
Expand Down Expand Up @@ -1042,7 +1064,7 @@ let maxRetryDelay = 30.0
/// - properties: Dictionary of properties to set for this group type
/// - reloadFeatureFlags: Whether to automatically reload feature flags after setting properties
@objc(setGroupPropertiesForFlags:properties:reloadFeatureFlags:)
public func setGroupPropertiesForFlags(_ groupType: String, properties: [String: Any], reloadFeatureFlags: Bool) {
public func setGroupPropertiesForFlags(_ groupType: String, properties: [String: Any], reloadFeatureFlags: Bool = true) {
if !isEnabled() {
return
}
Expand All @@ -1067,11 +1089,20 @@ let maxRetryDelay = 30.0
/// // Clear all group properties
/// PostHogSDK.shared.resetGroupPropertiesForFlags()
/// ```
///
/// - Note: This method does not automatically reload feature flags. Call `reloadFeatureFlags()`
/// after resetting if you want to immediately refresh flags with the cleared properties.
@objc public func resetGroupPropertiesForFlags() {
resetGroupPropertiesForFlags(groupType: nil)
internalResetGroupPropertiesForFlags(groupType: nil, reloadFeatureFlags: true)
}

/// Clears all group properties for feature flag evaluation.
///
/// ## Example Usage
/// ```swift
/// // Clear all group properties
/// PostHogSDK.shared.resetGroupPropertiesForFlags(reloadFeatureFlags: true)
/// ```
@objc(resetGroupPropertiesForFlagsWithReloadFeatureFlags:)
public func resetGroupPropertiesForFlags(reloadFeatureFlags: Bool = true) {
internalResetGroupPropertiesForFlags(groupType: nil, reloadFeatureFlags: reloadFeatureFlags)
}

/// Clears group properties for feature flag evaluation for a specific group type.
Expand All @@ -1083,15 +1114,29 @@ let maxRetryDelay = 30.0
/// ```
///
/// - Parameter groupType: The group type to clear properties for
/// - Note: This method does not automatically reload feature flags. Call `reloadFeatureFlags()`
/// after resetting if you want to immediately refresh flags with the cleared properties.
@objc(resetGroupPropertiesForFlagsWithGroupType:)
public func resetGroupPropertiesForFlags(_ groupType: String) {
resetGroupPropertiesForFlags(groupType: groupType)
internalResetGroupPropertiesForFlags(groupType: groupType, reloadFeatureFlags: true)
}

/// Clears group properties for feature flag evaluation for a specific group type.
///
/// ## Example Usage
/// ```swift
/// // Clear properties for specific group type
/// PostHogSDK.shared.resetGroupPropertiesForFlags("organization", reloadFeatureFlags: true)
/// ```
///
/// - Parameters:
/// - groupType: The group type to clear properties for
/// - reloadFeatureFlags: Whether to automatically reload feature flags after setting properties
@objc(resetGroupPropertiesForFlagsWithGroupType:reloadFeatureFlags:)
public func resetGroupPropertiesForFlags(_ groupType: String, reloadFeatureFlags: Bool = true) {
internalResetGroupPropertiesForFlags(groupType: groupType, reloadFeatureFlags: reloadFeatureFlags)
}

/// Internal implementation for resetting group properties.
private func resetGroupPropertiesForFlags(groupType: String?) {
private func internalResetGroupPropertiesForFlags(groupType: String?, reloadFeatureFlags: Bool) {
Copy link
Member Author

@marandaneto marandaneto Nov 6, 2025

Choose a reason for hiding this comment

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

to avoid name clash with the new method

if !isEnabled() {
return
}
Expand All @@ -1101,6 +1146,10 @@ let maxRetryDelay = 30.0
}

remoteConfig?.resetGroupPropertiesForFlags(groupType)

if reloadFeatureFlags {
remoteConfig?.reloadFeatureFlags()
}
}

@objc public func reloadFeatureFlags() {
Expand Down
8 changes: 4 additions & 4 deletions PostHogTests/PostHogContextTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,18 @@ class PostHogContextTest: QuickSpec {
// Check that it includes expected properties from static context
expect(context["$app_version"] as? String) != nil
expect(context["$app_build"] as? String) != nil
expect(context["$locale"] as? String) != nil
expect(context["$app_namespace"] as? String) != nil

#if os(iOS) || os(tvOS) || os(visionOS)
expect(context["$os_name"] as? String) != nil
expect(context["$os_version"] as? String) != nil
expect(context["$device_type"] as? String) != nil
expect(context["$device_manufacturer"] as? String) == "Apple"
expect(context["$device_model"] as? String) != nil
#endif

expect(context["$lib"] as? String) == "posthog-ios"
expect(context["$lib_version"] as? String) == postHogVersion

// Verify it doesn't include non-person properties
expect(context["$app_namespace"] as? String) == nil
expect(context["$is_emulator"] as? Bool) == nil
}
}
Expand Down
6 changes: 4 additions & 2 deletions PostHogTests/PostHogSDKTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ class PostHogSDKTest: QuickSpec {
expect(requests.count) == 1
let request = requests.first

let groups = request!["$groups"] as? [String: String] ?? [:]
let groups = request!["groups"] as? [String: String] ?? [:]
expect(groups["some-type"]) == "some-key"

sut.reset()
Expand Down Expand Up @@ -906,10 +906,12 @@ class PostHogSDKTest: QuickSpec {
// Verify expected default properties are set
expect(personProperties["$app_version"]).toNot(beNil())
expect(personProperties["$app_build"]).toNot(beNil())
expect(personProperties["$app_namespace"]).toNot(beNil())
expect(personProperties["$os_name"]).toNot(beNil())
expect(personProperties["$os_version"]).toNot(beNil())
expect(personProperties["$device_type"]).toNot(beNil())
expect(personProperties["$locale"]).toNot(beNil())
expect(personProperties["$lib"]).toNot(beNil())
expect(personProperties["$lib_version"]).toNot(beNil())
}

it("does not set default person properties when disabled") {
Expand Down
Loading