Skip to content

Commit

Permalink
chore: add personProfiles support (#187)
Browse files Browse the repository at this point in the history
  • Loading branch information
marandaneto authored Sep 10, 2024
1 parent ba22e7a commit 2948e77
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Next

- chore: add personProfiles support ([#187](https://github.com/PostHog/posthog-ios/pull/187))

## 3.10.0 - 2024-09-09

- recording: mask swiftui picker if masking enabled ([#184](https://github.com/PostHog/posthog-ios/pull/184))
Expand Down
8 changes: 8 additions & 0 deletions PostHog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@
69BA38D72B888E8500AA69D6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 69BA38D62B888E8500AA69D6 /* PrivacyInfo.xcprivacy */; };
69ED1A5C2C7F15F300FE7A91 /* PostHogSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69ED1A5B2C7F15F300FE7A91 /* PostHogSessionManager.swift */; };
69ED1A882C89B73100FE7A91 /* PostHogSwiftUIViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69ED1A872C89B73100FE7A91 /* PostHogSwiftUIViewModifiers.swift */; };
69ED1A9F2C8F451B00FE7A91 /* PostHogPersonProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69ED1A9E2C8F451B00FE7A91 /* PostHogPersonProfiles.swift */; };
69ED1AB62C90711D00FE7A91 /* PostHogSDKPersonProfilesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69ED1AB52C90711D00FE7A91 /* PostHogSDKPersonProfilesTest.swift */; };
69EE82BA2BA9C50400EB9542 /* PostHogReplayIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69EE82B92BA9C50400EB9542 /* PostHogReplayIntegration.swift */; };
69EE82BC2BA9C53000EB9542 /* PostHogSessionReplayConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69EE82BB2BA9C53000EB9542 /* PostHogSessionReplayConfig.swift */; };
69EE82BE2BA9C8AA00EB9542 /* ViewLayoutTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69EE82BD2BA9C8AA00EB9542 /* ViewLayoutTracker.swift */; };
Expand Down Expand Up @@ -343,6 +345,8 @@
69BA38D62B888E8500AA69D6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
69ED1A5B2C7F15F300FE7A91 /* PostHogSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSessionManager.swift; sourceTree = "<group>"; };
69ED1A872C89B73100FE7A91 /* PostHogSwiftUIViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSwiftUIViewModifiers.swift; sourceTree = "<group>"; };
69ED1A9E2C8F451B00FE7A91 /* PostHogPersonProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogPersonProfiles.swift; sourceTree = "<group>"; };
69ED1AB52C90711D00FE7A91 /* PostHogSDKPersonProfilesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSDKPersonProfilesTest.swift; sourceTree = "<group>"; };
69EE82B92BA9C50400EB9542 /* PostHogReplayIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogReplayIntegration.swift; sourceTree = "<group>"; };
69EE82BB2BA9C53000EB9542 /* PostHogSessionReplayConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSessionReplayConfig.swift; sourceTree = "<group>"; };
69EE82BD2BA9C8AA00EB9542 /* ViewLayoutTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewLayoutTracker.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -552,6 +556,7 @@
693E977A2C625208004B1030 /* PostHogPropertiesSanitizer.swift */,
69ED1A5B2C7F15F300FE7A91 /* PostHogSessionManager.swift */,
69ED1A872C89B73100FE7A91 /* PostHogSwiftUIViewModifiers.swift */,
69ED1A9E2C8F451B00FE7A91 /* PostHogPersonProfiles.swift */,
);
path = PostHog;
sourceTree = "<group>";
Expand All @@ -571,6 +576,7 @@
690FF0F42AF0F06100A0B06B /* PostHogSDKTest.swift */,
699C5FEE2C20242A007DB818 /* UUIDTest.swift */,
693E977C2C6257F9004B1030 /* ExampleSanitizer.swift */,
69ED1AB52C90711D00FE7A91 /* PostHogSDKPersonProfilesTest.swift */,
);
path = PostHogTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -1106,6 +1112,7 @@
69261D1B2AD9678C00232EC7 /* PostHogEvent.swift in Sources */,
69EE82BC2BA9C53000EB9542 /* PostHogSessionReplayConfig.swift in Sources */,
69EE82CE2BAAC76000EB9542 /* ViewTreeSnapshotStatus.swift in Sources */,
69ED1A9F2C8F451B00FE7A91 /* PostHogPersonProfiles.swift in Sources */,
69EE82BA2BA9C50400EB9542 /* PostHogReplayIntegration.swift in Sources */,
3AE3FB472992AB0000AFFC18 /* Hedgelog.swift in Sources */,
69261D132AD5685B00232EC7 /* PostHogFeatureFlags.swift in Sources */,
Expand All @@ -1125,6 +1132,7 @@
690FF0E12AEFC59100A0B06B /* PostHogFileBackedQueueTest.swift in Sources */,
3A62647529CB0168007E8C07 /* TestPostHog.swift in Sources */,
699C5FEF2C20242A007DB818 /* UUIDTest.swift in Sources */,
69ED1AB62C90711D00FE7A91 /* PostHogSDKPersonProfilesTest.swift in Sources */,
3A62646A29C9E385007E8C07 /* MockPostHogServer.swift in Sources */,
690FF0BB2AEF8B8200A0B06B /* PostHogContextTest.swift in Sources */,
690FF0E32AEFD12900A0B06B /* PostHogConfigTest.swift in Sources */,
Expand Down
19 changes: 19 additions & 0 deletions PostHog.xcodeproj/xcshareddata/xcschemes/PostHogTests.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,32 @@
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3AA34CF6296D951A003398F4"
BuildableName = "PostHogExample.app"
BlueprintName = "PostHogExample"
ReferencedContainer = "container:PostHog.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3AA34CF6296D951A003398F4"
BuildableName = "PostHogExample.app"
BlueprintName = "PostHogExample"
ReferencedContainer = "container:PostHog.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
Expand Down
2 changes: 2 additions & 0 deletions PostHog/PostHogConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import Foundation
/// Hook that allows to sanitize the event properties
/// The hook is called before the event is cached or sent over the wire
@objc public var propertiesSanitizer: PostHogPropertiesSanitizer?
/// Determines the behavior for processing user profiles.
@objc public var personProfiles: PostHogPersonProfiles = .identifiedOnly

/// Internal
var snapshotEndpoint: String = "/s/"
Expand Down
20 changes: 20 additions & 0 deletions PostHog/PostHogPersonProfiles.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// PostHogPersonProfiles.swift
// PostHog
//
// Created by Manoel Aranda Neto on 09.09.24.
//

import Foundation

/// Determines the behavior for processing user profiles.
/// - `never`: We won't process persons for any event. This means that anonymous users will not be merged once
/// they sign up or login, so you lose the ability to create funnels that track users from anonymous to identified.
/// All events (including `$identify`) will be sent with `$process_person_profile: False`.
/// - `always`: We will process persons data for all events.
/// - `identifiedOnly`: (default): we will only process persons when you call `identify`, `alias`, and `group`, Anonymous users won't get person profiles.
@objc(PostHogPersonProfiles) public enum PostHogPersonProfiles: Int {
case never
case always
case identifiedOnly
}
40 changes: 40 additions & 0 deletions PostHog/PostHogSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,26 @@ let maxRetryDelay = 30.0
return properties
}

private func hasPersonProcessing() -> Bool {
!(
config.personProfiles == .never ||
(
config.personProfiles == .identifiedOnly &&
storageManager?.isIdentified() == false &&
storageManager?.isPersonProcessing() == false
)
)
}

private func requirePersonProcessing() -> Bool {
if config.personProfiles == .never {
hedgeLog("personProfiles is set to `never`. This call will be ignored.")
return false
}
storageManager?.setPersonProcessing(true)
return true
}

private func buildProperties(distinctId: String,
properties: [String: Any]?,
userProperties: [String: Any]? = nil,
Expand Down Expand Up @@ -276,6 +296,8 @@ let maxRetryDelay = 30.0
if let isIdentified = storageManager?.isIdentified() {
props["$is_identified"] = isIdentified
}

props["$process_person_profile"] = hasPersonProcessing()
}

if let sessionId = PostHogSessionManager.shared.getSessionId() {
Expand Down Expand Up @@ -407,6 +429,10 @@ let maxRetryDelay = 30.0
return
}

if !requirePersonProcessing() {
return
}

guard let queue = queue, let storageManager = storageManager else {
return
}
Expand Down Expand Up @@ -508,6 +534,12 @@ let maxRetryDelay = 30.0

let distinctId = getDistinctId()

// if the user isn't identified but passed userProperties, userPropertiesSetOnce or groups,
// we should still enable person processing since this is intentional
if userProperties?.isEmpty == false || userPropertiesSetOnce?.isEmpty == false || groups?.isEmpty == false {
requirePersonProcessing()
}

let properties = buildProperties(distinctId: distinctId,
properties: sanitizeDicionary(properties),
userProperties: sanitizeDicionary(userProperties),
Expand Down Expand Up @@ -584,6 +616,10 @@ let maxRetryDelay = 30.0
return
}

if !requirePersonProcessing() {
return
}

guard let queue = queue else {
return
}
Expand Down Expand Up @@ -680,6 +716,10 @@ let maxRetryDelay = 30.0
return
}

if !requirePersonProcessing() {
return
}

_ = groups([type: key])

groupIdentify(type: type, key: key, groupProperties: sanitizeDicionary(groupProperties))
Expand Down
2 changes: 2 additions & 0 deletions PostHog/PostHogStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class PostHogStorage {
case optOut = "posthog.optOut"
case sessionReplay = "posthog.sessionReplay"
case isIdentified = "posthog.isIdentified"
case personProcessingEnabled = "posthog.enabledPersonProcessing"
}

private let config: PostHogConfig
Expand Down Expand Up @@ -141,6 +142,7 @@ class PostHogStorage {
deleteSafely(url(forKey: .optOut))
deleteSafely(url(forKey: .sessionReplay))
deleteSafely(url(forKey: .isIdentified))
deleteSafely(url(forKey: .personProcessingEnabled))
}

public func remove(key: StorageKey) {
Expand Down
27 changes: 27 additions & 0 deletions PostHog/PostHogStorageManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ class PostHogStorageManager {
private let anonLock = NSLock()
private let distinctLock = NSLock()
private let identifiedLock = NSLock()
private let personProcessingLock = NSLock()
private let idGen: (UUID) -> UUID

private var distinctId: String?
private var cachedDistinctId = false
private var anonymousId: String?
private var isIdentifiedValue: Bool?
private var personProcessingEnabled: Bool?

init(_ config: PostHogConfig) {
storage = PostHogStorage(config)
Expand Down Expand Up @@ -105,6 +107,25 @@ class PostHogStorageManager {
}
}

public func isPersonProcessing() -> Bool {
personProcessingLock.withLock {
if personProcessingEnabled == nil {
personProcessingEnabled = storage.getBool(forKey: .personProcessingEnabled) ?? false
}
}
return personProcessingEnabled ?? false
}

public func setPersonProcessing(_ enable: Bool) {
personProcessingLock.withLock {
// only set if its different to avoid IO since this is called more often
if self.personProcessingEnabled != enable {
self.personProcessingEnabled = enable
storage.setBool(forKey: .personProcessingEnabled, contents: enable)
}
}
}

public func reset(_ resetStorage: Bool = false) {
// resetStorage is only used for testing, when the reset method is called,
// the storage is also cleared, so we dont do here to not do it twice.
Expand All @@ -127,5 +148,11 @@ class PostHogStorageManager {
storage.remove(key: .isIdentified)
}
}
personProcessingLock.withLock {
personProcessingEnabled = nil
if resetStorage {
storage.remove(key: .personProcessingEnabled)
}
}
}
}
Loading

0 comments on commit 2948e77

Please sign in to comment.