Skip to content

Commit 4c68765

Browse files
wowrumalharitowaioannisj
authored
feat: add support for beforeSend function to edit or drop events (#357)
* Introduce beforeSend hook - Replace EventsSanitizer with beforeSend closure. Integrate the hook in all places before queue.add(); - Add test for the new beforeSend hook; - Fix capture snapshots event check; --------- Co-authored-by: Kirill Tsyvaka <[email protected]> Co-authored-by: haritowa <[email protected]> * Fixes according to git discussions Add logging for dropped events. Add @objc property decorations. Add known events warning * Rename to PostHogKnownUnsafeEditableEvent. Add optional to param in BeforeSendBlock * Implement new hook setup interface: with support for multiple blocks and better objc interop * Update PostHogTests/PostHogSDKTest.swift comment Co-authored-by: Ioannis J <[email protected]> * Edit changelog --------- Co-authored-by: haritowa <[email protected]> Co-authored-by: Ioannis J <[email protected]>
1 parent 95bba2b commit 4c68765

File tree

6 files changed

+408
-66
lines changed

6 files changed

+408
-66
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## Next
22

3+
- feat: add support for beforeSend function to edit or drop events ([#357](https://github.com/PostHog/posthog-ios/pull/357))
4+
35
## 3.27.0 - 2025-06-16
46

57
- fix: unify storage path for `appGroupIdentifier` across targets ([#356](https://github.com/PostHog/posthog-ios/pull/356))

PostHog/Models/PostHogEvent.swift

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77

88
import Foundation
99

10-
public class PostHogEvent {
11-
public var event: String
12-
public var distinctId: String
13-
public var properties: [String: Any]
14-
public var timestamp: Date
15-
public var uuid: UUID
10+
@objc(PostHogEvent) public class PostHogEvent: NSObject {
11+
@objc public var event: String
12+
@objc public var distinctId: String
13+
@objc public var properties: [String: Any]
14+
@objc public var timestamp: Date
15+
@objc public private(set) var uuid: UUID
1616
// Only used for Replay
17-
public var apiKey: String?
17+
var apiKey: String?
1818

1919
init(event: String, distinctId: String, properties: [String: Any]? = nil, timestamp: Date = Date(), uuid: UUID = UUID.v7(), apiKey: String? = nil) {
2020
self.event = event
@@ -83,3 +83,24 @@ public class PostHogEvent {
8383
return json
8484
}
8585
}
86+
87+
enum PostHogKnownUnsafeEditableEvent: String {
88+
case snapshot = "$snapshot"
89+
case pageview = "$pageview"
90+
case pageleave = "$pageleave"
91+
case set = "$set"
92+
case surveyDismissed = "survey dismissed"
93+
case surveySent = "survey sent"
94+
case surveyShown = "survey shown"
95+
case identify = "$identify"
96+
case groupidentify = "$groupidentify"
97+
case createAlias = "$create_alias"
98+
case clientIngestionWarning = "$$client_ingestion_warning"
99+
case webExperimentApplied = "$web_experiment_applied"
100+
case featureEnrollmentUpdate = "$feature_enrollment_update"
101+
case featureFlagCalled = "$feature_flag_called"
102+
103+
static func contains(_ name: String) -> Bool {
104+
PostHogKnownUnsafeEditableEvent(rawValue: name) != nil
105+
}
106+
}

PostHog/PostHogConfig.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@
66
//
77
import Foundation
88

9+
public typealias BeforeSendBlock = (PostHogEvent) -> PostHogEvent?
10+
11+
@objc public final class BoxedBeforeSendBlock: NSObject {
12+
@objc public let block: BeforeSendBlock
13+
14+
@objc(block:)
15+
public init(block: @escaping BeforeSendBlock) {
16+
self.block = block
17+
}
18+
}
19+
920
@objc(PostHogConfig) public class PostHogConfig: NSObject {
1021
enum Defaults {
1122
#if os(tvOS)
@@ -66,6 +77,7 @@ import Foundation
6677

6778
/// Hook that allows to sanitize the event properties
6879
/// The hook is called before the event is cached or sent over the wire
80+
@available(*, deprecated, message: "Use beforeSend instead")
6981
@objc public var propertiesSanitizer: PostHogPropertiesSanitizer?
7082
/// Determines the behavior for processing user profiles.
7183
@objc public var personProfiles: PostHogPersonProfiles = .identifiedOnly
@@ -171,4 +183,33 @@ import Foundation
171183
_surveys = value
172184
}
173185
}
186+
187+
/// Hook that allows to sanitize the event
188+
/// The hook is called before the event is cached or sent over the wire
189+
private var beforeSend: BeforeSendBlock = { $0 }
190+
191+
private static func buildBeforeSendBlock(_ blocks: [BeforeSendBlock]) -> BeforeSendBlock {
192+
{ event in
193+
blocks.reduce(event) { event, block in
194+
event.flatMap(block)
195+
}
196+
}
197+
}
198+
199+
public func setBeforeSend(_ blocks: [BeforeSendBlock]) {
200+
beforeSend = Self.buildBeforeSendBlock(blocks)
201+
}
202+
203+
public func setBeforeSend(_ blocks: BeforeSendBlock...) {
204+
setBeforeSend(blocks)
205+
}
206+
207+
@available(*, unavailable, message: "Use setBeforeSend(_ blocks: BeforeSendBlock...) instead")
208+
@objc public func setBeforeSend(_ blocks: [BoxedBeforeSendBlock]) {
209+
setBeforeSend(blocks.map(\.block))
210+
}
211+
212+
func runBeforeSend(_ event: PostHogEvent) -> PostHogEvent? {
213+
beforeSend(event)
214+
}
174215
}

PostHog/PostHogSDK.swift

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -461,13 +461,12 @@ let maxRetryDelay = 30.0
461461
userProperties: sanitizeDictionary(userProperties),
462462
userPropertiesSetOnce: sanitizeDictionary(userPropertiesSetOnce)
463463
)
464-
let sanitizedProperties = sanitizeProperties(properties)
465464

466-
queue.add(PostHogEvent(
467-
event: "$identify",
468-
distinctId: distinctId,
469-
properties: sanitizedProperties
470-
))
465+
guard let event = buildEvent(event: "$identify", distinctId: distinctId, properties: properties) else {
466+
return
467+
}
468+
469+
queue.add(event)
471470

472471
remoteConfig?.reloadFeatureFlags()
473472

@@ -570,7 +569,7 @@ let maxRetryDelay = 30.0
570569
return
571570
}
572571

573-
let isSnapshotEvent = event == "$snapshot"
572+
var isSnapshotEvent = event == "$snapshot"
574573
let eventTimestamp = timestamp ?? now()
575574
let eventDistinctId = distinctId ?? getDistinctId()
576575

@@ -587,23 +586,30 @@ let maxRetryDelay = 30.0
587586
groups: groups,
588587
appendSharedProps: !isSnapshotEvent,
589588
timestamp: timestamp)
590-
let sanitizedProperties = sanitizeProperties(properties)
589+
590+
// Sanitize is now called in buildEvent
591+
let posthogEvent = buildEvent(
592+
event: event,
593+
distinctId: eventDistinctId,
594+
properties: properties,
595+
timestamp: eventTimestamp
596+
)
597+
598+
guard let posthogEvent else {
599+
return
600+
}
601+
602+
// Reevaluate if this is a snapshot event because the event might have been updated by the beforeSend hook
603+
isSnapshotEvent = posthogEvent.event == "$snapshot"
591604

592605
// if this is a $snapshot event and $session_id is missing, don't process then event
593-
if isSnapshotEvent, sanitizedProperties["$session_id"] == nil {
606+
if isSnapshotEvent, posthogEvent.properties["$session_id"] == nil {
594607
return
595608
}
596609

597610
// Session Replay has its own queue
598611
let targetQueue = isSnapshotEvent ? replayQueue : queue
599612

600-
let posthogEvent = PostHogEvent(
601-
event: event,
602-
distinctId: eventDistinctId,
603-
properties: sanitizedProperties,
604-
timestamp: eventTimestamp
605-
)
606-
607613
targetQueue?.add(posthogEvent)
608614

609615
#if os(iOS)
@@ -636,13 +642,12 @@ let maxRetryDelay = 30.0
636642
let distinctId = getDistinctId()
637643

638644
let properties = buildProperties(distinctId: distinctId, properties: props)
639-
let sanitizedProperties = sanitizeProperties(properties)
640645

641-
queue.add(PostHogEvent(
642-
event: "$screen",
643-
distinctId: distinctId,
644-
properties: sanitizedProperties
645-
))
646+
guard let event = buildEvent(event: "$screen", distinctId: distinctId, properties: properties) else {
647+
return
648+
}
649+
650+
queue.add(event)
646651
}
647652

648653
func autocapture(
@@ -670,13 +675,12 @@ let maxRetryDelay = 30.0
670675
let distinctId = getDistinctId()
671676

672677
let properties = buildProperties(distinctId: distinctId, properties: props)
673-
let sanitizedProperties = sanitizeProperties(properties)
674678

675-
queue.add(PostHogEvent(
676-
event: "$autocapture",
677-
distinctId: distinctId,
678-
properties: sanitizedProperties
679-
))
679+
guard let event = buildEvent(event: "$autocapture", distinctId: distinctId, properties: properties) else {
680+
return
681+
}
682+
683+
queue.add(event)
680684
}
681685

682686
private func sanitizeProperties(_ properties: [String: Any]) -> [String: Any] {
@@ -708,13 +712,12 @@ let maxRetryDelay = 30.0
708712
let distinctId = getDistinctId()
709713

710714
let properties = buildProperties(distinctId: distinctId, properties: props)
711-
let sanitizedProperties = sanitizeProperties(properties)
712715

713-
queue.add(PostHogEvent(
714-
event: "$create_alias",
715-
distinctId: distinctId,
716-
properties: sanitizedProperties
717-
))
716+
guard let event = buildEvent(event: "$create_alias", distinctId: distinctId, properties: properties) else {
717+
return
718+
}
719+
720+
queue.add(event)
718721
}
719722

720723
private func groups(_ newGroups: [String: String]) -> [String: String] {
@@ -771,13 +774,36 @@ let maxRetryDelay = 30.0
771774
let distinctId = getDistinctId()
772775

773776
let properties = buildProperties(distinctId: distinctId, properties: props)
777+
778+
guard let event = buildEvent(event: "$groupidentify", distinctId: distinctId, properties: properties) else {
779+
return
780+
}
781+
782+
queue.add(event)
783+
}
784+
785+
func buildEvent(event eventName: String, distinctId: String, properties: [String: Any], timestamp: Date = Date()) -> PostHogEvent? {
774786
let sanitizedProperties = sanitizeProperties(properties)
775787

776-
queue.add(PostHogEvent(
777-
event: "$groupidentify",
788+
let event = PostHogEvent(
789+
event: eventName,
778790
distinctId: distinctId,
779-
properties: sanitizedProperties
780-
))
791+
properties: sanitizedProperties,
792+
timestamp: timestamp
793+
)
794+
795+
let resultEvent = config.runBeforeSend(event)
796+
797+
if resultEvent == nil {
798+
let originalMessage = "PostHog event \(eventName) was dropped"
799+
let message = PostHogKnownUnsafeEditableEvent.contains(eventName)
800+
? "\(originalMessage). This can cause unexpected behavior."
801+
: originalMessage
802+
803+
hedgeLog(message)
804+
}
805+
806+
return resultEvent
781807
}
782808

783809
@objc(groupWithType:key:)

PostHogExampleWithPods/PostHogExampleWithPods.xcodeproj/project.pbxproj

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
690FF0262AE7C5BA00A0B06B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 690FF0252AE7C5BA00A0B06B /* Assets.xcassets */; };
1313
690FF0292AE7C5BA00A0B06B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 690FF0282AE7C5BA00A0B06B /* Preview Assets.xcassets */; };
1414
690FF0362AE7C61300A0B06B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0352AE7C61300A0B06B /* AppDelegate.swift */; };
15-
C8454434344E03D10F62E3D1 /* Pods_PostHogExampleWithPods.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F797B3856C0D3A6C42F1903F /* Pods_PostHogExampleWithPods.framework */; };
15+
E7087D98D8DE0D2DD84278DD /* Pods_PostHogExampleWithPods.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8266354C5603FEBCC7C01E4B /* Pods_PostHogExampleWithPods.framework */; };
1616
/* End PBXBuildFile section */
1717

1818
/* Begin PBXFileReference section */
@@ -23,16 +23,16 @@
2323
690FF0252AE7C5BA00A0B06B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
2424
690FF0282AE7C5BA00A0B06B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
2525
690FF0352AE7C61300A0B06B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
26+
8266354C5603FEBCC7C01E4B /* Pods_PostHogExampleWithPods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PostHogExampleWithPods.framework; sourceTree = BUILT_PRODUCTS_DIR; };
2627
9E7CD1F6231834F08BF61E6C /* Pods-PostHogExampleWithPods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PostHogExampleWithPods.debug.xcconfig"; path = "Target Support Files/Pods-PostHogExampleWithPods/Pods-PostHogExampleWithPods.debug.xcconfig"; sourceTree = "<group>"; };
27-
F797B3856C0D3A6C42F1903F /* Pods_PostHogExampleWithPods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PostHogExampleWithPods.framework; sourceTree = BUILT_PRODUCTS_DIR; };
2828
/* End PBXFileReference section */
2929

3030
/* Begin PBXFrameworksBuildPhase section */
3131
690FF01B2AE7C5B900A0B06B /* Frameworks */ = {
3232
isa = PBXFrameworksBuildPhase;
3333
buildActionMask = 2147483647;
3434
files = (
35-
C8454434344E03D10F62E3D1 /* Pods_PostHogExampleWithPods.framework in Frameworks */,
35+
E7087D98D8DE0D2DD84278DD /* Pods_PostHogExampleWithPods.framework in Frameworks */,
3636
);
3737
runOnlyForDeploymentPostprocessing = 0;
3838
};
@@ -89,7 +89,7 @@
8989
9A20B85D4D10AA6F7B0C13FB /* Frameworks */ = {
9090
isa = PBXGroup;
9191
children = (
92-
F797B3856C0D3A6C42F1903F /* Pods_PostHogExampleWithPods.framework */,
92+
8266354C5603FEBCC7C01E4B /* Pods_PostHogExampleWithPods.framework */,
9393
);
9494
name = Frameworks;
9595
sourceTree = "<group>";
@@ -105,7 +105,7 @@
105105
690FF01A2AE7C5B900A0B06B /* Sources */,
106106
690FF01B2AE7C5B900A0B06B /* Frameworks */,
107107
690FF01C2AE7C5B900A0B06B /* Resources */,
108-
E4608544082294C46D768457 /* [CP] Embed Pods Frameworks */,
108+
6E4C2BEF8CB8F259A129729E /* [CP] Embed Pods Frameworks */,
109109
);
110110
buildRules = (
111111
);
@@ -162,43 +162,43 @@
162162
/* End PBXResourcesBuildPhase section */
163163

164164
/* Begin PBXShellScriptBuildPhase section */
165-
ACA09137F648E598E95E500F /* [CP] Check Pods Manifest.lock */ = {
165+
6E4C2BEF8CB8F259A129729E /* [CP] Embed Pods Frameworks */ = {
166166
isa = PBXShellScriptBuildPhase;
167167
buildActionMask = 2147483647;
168168
files = (
169169
);
170170
inputFileListPaths = (
171+
"${PODS_ROOT}/Target Support Files/Pods-PostHogExampleWithPods/Pods-PostHogExampleWithPods-frameworks-${CONFIGURATION}-input-files.xcfilelist",
171172
);
172-
inputPaths = (
173-
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
174-
"${PODS_ROOT}/Manifest.lock",
175-
);
176-
name = "[CP] Check Pods Manifest.lock";
173+
name = "[CP] Embed Pods Frameworks";
177174
outputFileListPaths = (
178-
);
179-
outputPaths = (
180-
"$(DERIVED_FILE_DIR)/Pods-PostHogExampleWithPods-checkManifestLockResult.txt",
175+
"${PODS_ROOT}/Target Support Files/Pods-PostHogExampleWithPods/Pods-PostHogExampleWithPods-frameworks-${CONFIGURATION}-output-files.xcfilelist",
181176
);
182177
runOnlyForDeploymentPostprocessing = 0;
183178
shellPath = /bin/sh;
184-
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
179+
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-PostHogExampleWithPods/Pods-PostHogExampleWithPods-frameworks.sh\"\n";
185180
showEnvVarsInLog = 0;
186181
};
187-
E4608544082294C46D768457 /* [CP] Embed Pods Frameworks */ = {
182+
ACA09137F648E598E95E500F /* [CP] Check Pods Manifest.lock */ = {
188183
isa = PBXShellScriptBuildPhase;
189184
buildActionMask = 2147483647;
190185
files = (
191186
);
192187
inputFileListPaths = (
193-
"${PODS_ROOT}/Target Support Files/Pods-PostHogExampleWithPods/Pods-PostHogExampleWithPods-frameworks-${CONFIGURATION}-input-files.xcfilelist",
194188
);
195-
name = "[CP] Embed Pods Frameworks";
189+
inputPaths = (
190+
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
191+
"${PODS_ROOT}/Manifest.lock",
192+
);
193+
name = "[CP] Check Pods Manifest.lock";
196194
outputFileListPaths = (
197-
"${PODS_ROOT}/Target Support Files/Pods-PostHogExampleWithPods/Pods-PostHogExampleWithPods-frameworks-${CONFIGURATION}-output-files.xcfilelist",
195+
);
196+
outputPaths = (
197+
"$(DERIVED_FILE_DIR)/Pods-PostHogExampleWithPods-checkManifestLockResult.txt",
198198
);
199199
runOnlyForDeploymentPostprocessing = 0;
200200
shellPath = /bin/sh;
201-
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-PostHogExampleWithPods/Pods-PostHogExampleWithPods-frameworks.sh\"\n";
201+
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
202202
showEnvVarsInLog = 0;
203203
};
204204
/* End PBXShellScriptBuildPhase section */

0 commit comments

Comments
 (0)