From 4a42bf00059d65cccd9130dbf6b73970c1b686b2 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Tue, 6 Oct 2026 10:26:27 -0400 Subject: [PATCH 1/4] chore!: mark off-main public callbacks @Sendable for v4 The reloadFeatureFlags callback, beforeSend blocks, getAnonymousId, pushIdentityProvider and logSanitizer run off the main thread but weren't @Sendable. In Swift 6 apps a reloadFeatureFlags closure written in main-actor code traps at runtime; the other callbacks race silently. Also make PostHogFeatureFlagsLoaded Sendable and add a Swift 6 language-mode test target that calls the API as an app would. Co-Authored-By: Claude Opus 5.5 (1M context) --- .changeset/v4-sendable-callbacks.md | 7 ++ Package.swift | 9 +++ PostHog/Logs/PostHogLogsConfig.swift | 4 +- .../Models/PostHogFeatureFlagsLoaded.swift | 4 +- PostHog/PostHogConfig.swift | 11 +-- PostHog/PostHogSDK.swift | 5 +- .../PostHogPushSubscriptionHandler.swift | 3 +- ...PostHogSessionReplayConsoleLogConfig.swift | 6 +- PostHog/Utils/BeforeSendChain.swift | 6 +- PostHog/Utils/BoxedBeforeSend.swift | 4 +- PostHogExample/ContentView.swift | 6 +- .../PostHogCallbackIsolationTests.swift | 80 +++++++++++++++++++ api/posthog-ios.public-api.txt | 20 ++--- 13 files changed, 137 insertions(+), 28 deletions(-) create mode 100644 .changeset/v4-sendable-callbacks.md create mode 100644 PostHogSwift6Tests/PostHogCallbackIsolationTests.swift diff --git a/.changeset/v4-sendable-callbacks.md b/.changeset/v4-sendable-callbacks.md new file mode 100644 index 0000000000..2497b6857f --- /dev/null +++ b/.changeset/v4-sendable-callbacks.md @@ -0,0 +1,7 @@ +--- +'posthog-ios': major +--- + +**Breaking:** mark callbacks the SDK can call off the main thread `@Sendable` — `reloadFeatureFlags(_:)`, the `BeforeSendBlock` and `PostHogBeforeSendLogBlock` used by `setBeforeSend(_:)` on `PostHogConfig` and `PostHogLogsConfig`, `getAnonymousId`, `pushIdentityProvider` and its `completion`, and `logSanitizer` — and make `PostHogFeatureFlagsLoaded` `Sendable`. This fixes a crash in Swift 6 apps when a `reloadFeatureFlags(_:)` callback is written in main-actor code. + +To update main-actor state from `reloadFeatureFlags(_:)` or `pushIdentityProvider`, hop with `Task { @MainActor in ... }`. The other callbacks return a value immediately, so they must not read main-actor state. Declare closures you store in your own variables as `@Sendable` to avoid new Swift 5 warnings, and type pre-built `setBeforeSend(_:)` arrays as `[BeforeSendBlock]` or `[PostHogBeforeSendLogBlock]`: an array typed with the old function type crashes at runtime in Swift 5 mode. diff --git a/Package.swift b/Package.swift index 0c095023bb..db82f20801 100644 --- a/Package.swift +++ b/Package.swift @@ -102,6 +102,15 @@ let package = Package( .process("Resources"), ] ), + // Compiles in Swift 6 language mode to check the public API the way a Swift 6 app sees it. + .testTarget( + name: "PostHogSwift6Tests", + dependencies: ["PostHog"], + path: "PostHogSwift6Tests", + swiftSettings: [ + .swiftLanguageMode(.v6), + ] + ), ], swiftLanguageModes: [.v5] ) diff --git a/PostHog/Logs/PostHogLogsConfig.swift b/PostHog/Logs/PostHogLogsConfig.swift index 2a5d11c8b3..99b4fc2723 100644 --- a/PostHog/Logs/PostHogLogsConfig.swift +++ b/PostHog/Logs/PostHogLogsConfig.swift @@ -13,7 +13,7 @@ import Foundation /// /// - Parameter record: The log record about to be queued. /// - Returns: The record to queue, or `nil` to drop it. -public typealias PostHogBeforeSendLogBlock = (PostHogLogRecord) -> PostHogLogRecord? +public typealias PostHogBeforeSendLogBlock = @Sendable (PostHogLogRecord) -> PostHogLogRecord? /// Configuration for the PostHog logs subsystem. Mutate fields on `config.logs` /// before calling `PostHogSDK.setup(_:)`. @@ -105,7 +105,7 @@ public typealias PostHogBeforeSendLogBlock = (PostHogLogRecord) -> PostHogLogRec @available(swift, obsoleted: 1.0, message: "Use setBeforeSend(_ blocks: PostHogBeforeSendLogBlock...) instead") @objc public func setBeforeSend(_ blocks: [BoxedBeforeSendLogBlock]) { setBeforeSend(blocks.map { box in - { record in box.invokeSafely(with: record) } + { @Sendable record in box.invokeSafely(with: record) } }) } diff --git a/PostHog/Models/PostHogFeatureFlagsLoaded.swift b/PostHog/Models/PostHogFeatureFlagsLoaded.swift index 4ce21c8350..1f224eecee 100644 --- a/PostHog/Models/PostHogFeatureFlagsLoaded.swift +++ b/PostHog/Models/PostHogFeatureFlagsLoaded.swift @@ -22,7 +22,9 @@ import Foundation /// subscription.unsubscribe() /// ``` @objc(PostHogFeatureFlagsLoaded) -public final class PostHogFeatureFlagsLoaded: NSObject { +public final class PostHogFeatureFlagsLoaded: NSObject, @unchecked Sendable { + // @unchecked: every property is a `let`, and `variants` only holds `Bool` and `String` values (see init). + /// The keys of the enabled feature flags. @objc public let flags: [String] diff --git a/PostHog/PostHogConfig.swift b/PostHog/PostHogConfig.swift index e8baf0cc0c..85f54a007a 100644 --- a/PostHog/PostHogConfig.swift +++ b/PostHog/PostHogConfig.swift @@ -13,7 +13,7 @@ import Foundation /// /// - Parameter event: The event about to be queued. /// - Returns: The event to queue, or `nil` to drop it. -public typealias BeforeSendBlock = (PostHogEvent) -> PostHogEvent? +public typealias BeforeSendBlock = @Sendable (PostHogEvent) -> PostHogEvent? /// Runtime configuration for a `PostHogSDK` instance. /// @@ -205,7 +205,7 @@ public typealias BeforeSendBlock = (PostHogEvent) -> PostHogEvent? /// here stalls the SDK's push retry/offline-resume flow (though not your app's main thread). /// /// Default: `nil` (requests carry no identity token). - @objc public var pushIdentityProvider: ((_ distinctId: String, _ appId: String, _ completion: @escaping (String?) -> Void) -> Void)? + @objc public var pushIdentityProvider: (@Sendable (_ distinctId: String, _ appId: String, _ completion: @escaping @Sendable (String?) -> Void) -> Void)? #if os(iOS) || targetEnvironment(macCatalyst) /// Enables UIKit element interaction autocapture on iOS and Mac Catalyst. @@ -282,11 +282,12 @@ public typealias BeforeSendBlock = (PostHogEvent) -> PostHogEvent? /// Hook used to customize newly generated anonymous IDs. /// /// The SDK passes its generated UUID v7 and stores the UUID returned by this closure. - /// Existing stored anonymous IDs are not regenerated. + /// Existing stored anonymous IDs are not regenerated. The closure can be called on any thread, + /// so it must not read main-actor state. /// /// - Parameter uuid: The SDK-generated anonymous UUID. /// - Returns: The UUID to persist as the anonymous ID. - @objc public var getAnonymousId: ((UUID) -> UUID) = { uuid in uuid } + @objc public var getAnonymousId: (@Sendable (UUID) -> UUID) = { uuid in uuid } /// Pre-seeded identity and feature-flag state applied during setup, before any /// network request completes. @@ -604,7 +605,7 @@ public typealias BeforeSendBlock = (PostHogEvent) -> PostHogEvent? @available(swift, obsoleted: 1.0, message: "Use setBeforeSend(_ blocks: BeforeSendBlock...) instead") @objc public func setBeforeSend(_ blocks: [BoxedBeforeSendBlock]) { setBeforeSend(blocks.map { box in - { event in box.invokeSafely(with: event) } + { @Sendable event in box.invokeSafely(with: event) } }) } diff --git a/PostHog/PostHogSDK.swift b/PostHog/PostHogSDK.swift index ca5d78ef07..84f0779253 100644 --- a/PostHog/PostHogSDK.swift +++ b/PostHog/PostHogSDK.swift @@ -2322,9 +2322,10 @@ let maxRetryDelay = 30.0 /// - Parameter callback: Invoked when the reload finishes, possibly on a background thread. /// If the request fails, ``PostHogFeatureFlagsLoaded/errorsLoading`` is `true` and the flags are /// the last known ones. If the reload is skipped because the SDK isn't set up, it's invoked - /// right away with no flags and `errorsLoading` set to `true`. + /// right away with no flags and `errorsLoading` set to `true`. To update UI or other main-actor + /// state, hop to the main actor, for example with `Task { @MainActor in ... }`. @objc(reloadFeatureFlagsWithCallback:) - public func reloadFeatureFlags(_ callback: @escaping (PostHogFeatureFlagsLoaded) -> Void) { + public func reloadFeatureFlags(_ callback: @escaping @Sendable (PostHogFeatureFlagsLoaded) -> Void) { if !isEnabled() { callback(PostHogFeatureFlagsLoaded(featureFlags: [:], errorsLoading: true)) return diff --git a/PostHog/PushNotifications/PostHogPushSubscriptionHandler.swift b/PostHog/PushNotifications/PostHogPushSubscriptionHandler.swift index 198c96bb51..e0bb2748b4 100644 --- a/PostHog/PushNotifications/PostHogPushSubscriptionHandler.swift +++ b/PostHog/PushNotifications/PostHogPushSubscriptionHandler.swift @@ -697,7 +697,8 @@ final class PostHogPushSubscriptionHandler { return completion(cached) } - var completed = false + // Guarded by stateLock. + nonisolated(unsafe) var completed = false // A provider that never calls its completion would hold isSending for the whole process and // wedge every later send. Bound the wait: if the mint doesn't land in time, fall back to a // token-less send. A late real completion doesn't deliver (via `completed`) but still diff --git a/PostHog/Replay/PostHogSessionReplayConsoleLogConfig.swift b/PostHog/Replay/PostHogSessionReplayConsoleLogConfig.swift index 48d498eb25..a77c6a33da 100644 --- a/PostHog/Replay/PostHogSessionReplayConsoleLogConfig.swift +++ b/PostHog/Replay/PostHogSessionReplayConsoleLogConfig.swift @@ -23,9 +23,11 @@ /// - Detect log level (best effort) /// - Process OSLog messages to remove metadata /// + /// The block runs on a background thread, so it must not read main-actor state. + /// /// - Parameter output: The raw console output to process. /// - Returns: A `PostHogConsoleLogEntry` to capture, or `nil` to skip the log output. - @objc public var logSanitizer: ((String) -> PostHogConsoleLogEntry?) = PostHogSessionReplayConsoleLogConfig.defaultLogSanitizer + @objc public var logSanitizer: (@Sendable (String) -> PostHogConsoleLogEntry?) = PostHogSessionReplayConsoleLogConfig.defaultLogSanitizer /// The minimum log level to capture in session replay. /// Only log messages with this level or higher will be captured. @@ -38,7 +40,7 @@ @objc public var minLogLevel: PostHogConsoleLogLevel = .error /// Default implementation for processing console output. - static func defaultLogSanitizer(_ message: String) -> PostHogConsoleLogEntry? { + @Sendable static func defaultLogSanitizer(_ message: String) -> PostHogConsoleLogEntry? { let message = String(message) // Determine console log level let level: PostHogConsoleLogLevel = { diff --git a/PostHog/Utils/BeforeSendChain.swift b/PostHog/Utils/BeforeSendChain.swift index 446ad4cc2e..098c1b084f 100644 --- a/PostHog/Utils/BeforeSendChain.swift +++ b/PostHog/Utils/BeforeSendChain.swift @@ -5,11 +5,13 @@ import Foundation -/// Composable `(T) -> T?` pipeline shared by the events and logs configs. +/// Composable `@Sendable (T) -> T?` pipeline shared by the events and logs configs. /// Blocks run in registration order; the first `nil` drops the value. /// Reference type so concurrent `set` / `run` see a coherent block snapshot. final class BeforeSendChain { - typealias Block = (T) -> T? + // @Sendable to match the public block types: converting their arrays to a + // non-Sendable element type fails at runtime (`_arrayForceCast`). + typealias Block = @Sendable (T) -> T? private let lock = NSLock() private var block: Block = { $0 } diff --git a/PostHog/Utils/BoxedBeforeSend.swift b/PostHog/Utils/BoxedBeforeSend.swift index f67fbfe1c2..f3c29e862e 100644 --- a/PostHog/Utils/BoxedBeforeSend.swift +++ b/PostHog/Utils/BoxedBeforeSend.swift @@ -13,7 +13,7 @@ internal import PostHogObjCExceptionSupport /// ObjC wrapper for the events `beforeSend` block. Use with /// `PostHogConfig.setBeforeSend(_:)`. -@objc public final class BoxedBeforeSendBlock: NSObject { +@objc public final class BoxedBeforeSendBlock: NSObject, Sendable { /// Wrapped event callback. @objc public let block: BeforeSendBlock @@ -35,7 +35,7 @@ internal import PostHogObjCExceptionSupport /// ObjC wrapper for the logs `beforeSend` block. Use with /// `PostHogLogsConfig.setBeforeSend(_:)`. -@objc public final class BoxedBeforeSendLogBlock: NSObject { +@objc public final class BoxedBeforeSendLogBlock: NSObject, Sendable { /// Wrapped log callback. @objc public let block: PostHogBeforeSendLogBlock diff --git a/PostHogExample/ContentView.swift b/PostHogExample/ContentView.swift index cdb969a0ba..9a5fc9cbe1 100644 --- a/PostHogExample/ContentView.swift +++ b/PostHogExample/ContentView.swift @@ -36,6 +36,7 @@ class SignInViewModel: NSObject, ObservableObject, ASWebAuthenticationPresentati } } +@MainActor class FeatureFlagsModel: ObservableObject { @Published var boolValue: Bool? @Published var stringValue: String? @@ -56,7 +57,10 @@ class FeatureFlagsModel: ObservableObject { isReloading = true PostHogSDK.shared.reloadFeatureFlags { _ in - self.isReloading = false + // The callback can run on a background thread; @Published changes belong on main. + Task { @MainActor in + self.isReloading = false + } } } } diff --git a/PostHogSwift6Tests/PostHogCallbackIsolationTests.swift b/PostHogSwift6Tests/PostHogCallbackIsolationTests.swift new file mode 100644 index 0000000000..670fb07765 --- /dev/null +++ b/PostHogSwift6Tests/PostHogCallbackIsolationTests.swift @@ -0,0 +1,80 @@ +// +// PostHogCallbackIsolationTests.swift +// PostHogSwift6Tests +// + +import Foundation +import PostHog +import Testing + +/// Calls the public callback APIs from main-actor code, as a Swift 6 app would. +/// +/// In Swift 6 mode, a closure written in main-actor code and passed as a non-`Sendable` +/// parameter is main-actor isolated, and the runtime traps if the SDK calls it off the main thread. +@MainActor +@Suite(.timeLimit(.minutes(1))) +struct PostHogCallbackIsolationTests { + @MainActor final class AppState { + var reloadResult: PostHogFeatureFlagsLoaded? + var loaded: PostHogFeatureFlagsLoaded? + var deliveredOnMain = false + } + + /// Nothing listens on this port, so requests fail fast without leaving the machine. + private func makeSDK(bootstrapFlags: [String: Any]? = nil) -> PostHogSDK { + let config = PostHogConfig(projectToken: "phc_swift6_\(UUID().uuidString)", host: "http://127.0.0.1:9") + config.captureApplicationLifecycleEvents = false + config.preloadFeatureFlags = false + if let bootstrapFlags { + let bootstrap = PostHogBootstrapConfig() + bootstrap.featureFlags = bootstrapFlags + config.bootstrap = bootstrap + } + return PostHogSDK.with(config) + } + + @Test("reloadFeatureFlags callback written in main-actor code runs without an isolation trap") + func reloadCallbackFromMainActorCode() async { + let sdk = makeSDK() + defer { sdk.close() } + let state = AppState() + + // The SDK invokes this from its network completion, off the main thread. + await withCheckedContinuation { continuation in + sdk.reloadFeatureFlags { result in + // Sending the result into the hop requires it to be Sendable. + Task { @MainActor in + state.reloadResult = result + continuation.resume() + } + } + } + + #expect(state.reloadResult != nil) + } + + @Test("onFeatureFlags callback written in main-actor code runs on main with a Sendable payload") + func onFeatureFlagsDeliversOnMainActor() async { + let sdk = makeSDK(bootstrapFlags: ["swift6-flag": true]) + defer { sdk.close() } + let state = AppState() + + var subscription: PostHogFeatureFlagsSubscription? + await withCheckedContinuation { continuation in + subscription = sdk.onFeatureFlags { loaded in + // Touches main-actor state directly, with no hop. + guard state.loaded == nil else { return } + state.loaded = loaded + state.deliveredOnMain = Thread.isMainThread + continuation.resume() + } + } + subscription?.unsubscribe() + + #expect(state.deliveredOnMain) + // Sending the payload to another task requires it to be Sendable. + let loaded = state.loaded + let flags = await Task.detached { loaded?.flags ?? [] }.value + #expect(flags == ["swift6-flag"]) + } +} diff --git a/api/posthog-ios.public-api.txt b/api/posthog-ios.public-api.txt index 75787a27a1..54060371f8 100644 --- a/api/posthog-ios.public-api.txt +++ b/api/posthog-ios.public-api.txt @@ -2,7 +2,7 @@ # Do not edit manually. # Public API scope: public/open symbols emitted for the PostHog iOS symbol graphs. -PostHog | BeforeSendBlock | typealias | typealias BeforeSendBlock = (PostHogEvent) -> PostHogEvent? | s:7PostHog15BeforeSendBlocka +PostHog | BeforeSendBlock | typealias | typealias BeforeSendBlock = @Sendable (PostHogEvent) -> PostHogEvent? | s:7PostHog15BeforeSendBlocka PostHog | BoxedBeforeSendBlock | class | @objc final class BoxedBeforeSendBlock | c:@M@PostHog@objc(cs)BoxedBeforeSendBlock PostHog | BoxedBeforeSendBlock.block | property | @objc let block: BeforeSendBlock | c:@M@PostHog@objc(cs)BoxedBeforeSendBlock(py)block PostHog | BoxedBeforeSendBlock.init(block:) | init | @objc(block:) init(block: @escaping BeforeSendBlock) | c:@M@PostHog@objc(cs)BoxedBeforeSendBlock(im)block: @@ -12,7 +12,7 @@ PostHog | BoxedBeforeSendLogBlock.init(block:) | init | @objc(block:) init(block PostHog | OnPostHogSurveyClosed | typealias | typealias OnPostHogSurveyClosed = (PostHogDisplaySurvey) -> Void | s:7PostHog02OnaB12SurveyCloseda PostHog | OnPostHogSurveyResponse | typealias | typealias OnPostHogSurveyResponse = (PostHogDisplaySurvey, Int, PostHogSurveyResponse) -> PostHogNextSurveyQuestion? | s:7PostHog02OnaB14SurveyResponsea PostHog | OnPostHogSurveyShown | typealias | typealias OnPostHogSurveyShown = (PostHogDisplaySurvey) -> Void | s:7PostHog02OnaB11SurveyShowna -PostHog | PostHogBeforeSendLogBlock | typealias | typealias PostHogBeforeSendLogBlock = (PostHogLogRecord) -> PostHogLogRecord? | s:7PostHog0aB18BeforeSendLogBlocka +PostHog | PostHogBeforeSendLogBlock | typealias | typealias PostHogBeforeSendLogBlock = @Sendable (PostHogLogRecord) -> PostHogLogRecord? | s:7PostHog0aB18BeforeSendLogBlocka PostHog | PostHogBootstrapConfig | class | @objc(PostHogBootstrapConfig) class PostHogBootstrapConfig | c:@M@PostHog@objc(cs)PostHogBootstrapConfig PostHog | PostHogBootstrapConfig.distinctId | property | @objc var distinctId: String? | c:@M@PostHog@objc(cs)PostHogBootstrapConfig(py)distinctId PostHog | PostHogBootstrapConfig.featureFlagPayloads | property | @objc var featureFlagPayloads: [String : Any]? | c:@M@PostHog@objc(cs)PostHogBootstrapConfig(py)featureFlagPayloads @@ -52,7 +52,7 @@ PostHog | PostHogConfig.evaluationContexts | property | @objc var evaluationCont PostHog | PostHogConfig.featureFlagRequestMaxRetries | property | @objc var featureFlagRequestMaxRetries: Int | c:@M@PostHog@objc(cs)PostHogConfig(py)featureFlagRequestMaxRetries PostHog | PostHogConfig.flushAt | property | @objc var flushAt: Int | c:@M@PostHog@objc(cs)PostHogConfig(py)flushAt PostHog | PostHogConfig.flushIntervalSeconds | property | @objc var flushIntervalSeconds: TimeInterval | c:@M@PostHog@objc(cs)PostHogConfig(py)flushIntervalSeconds -PostHog | PostHogConfig.getAnonymousId | property | @objc var getAnonymousId: (UUID) -> UUID | c:@M@PostHog@objc(cs)PostHogConfig(py)getAnonymousId +PostHog | PostHogConfig.getAnonymousId | property | @objc var getAnonymousId: @Sendable (UUID) -> UUID | c:@M@PostHog@objc(cs)PostHogConfig(py)getAnonymousId PostHog | PostHogConfig.host | property | @objc let host: URL | c:@M@PostHog@objc(cs)PostHogConfig(py)host PostHog | PostHogConfig.init(projectToken:) | init | @objc(projectToken:) init(projectToken: String) | c:@M@PostHog@objc(cs)PostHogConfig(im)projectToken: PostHog | PostHogConfig.init(projectToken:host:) | init | @objc(projectToken:host:) init(projectToken: String, host: String = defaultHost) | c:@M@PostHog@objc(cs)PostHogConfig(im)projectToken:host: @@ -64,7 +64,7 @@ PostHog | PostHogConfig.optOut | property | @objc var optOut: Bool | c:@M@PostHo PostHog | PostHogConfig.personProfiles | property | @objc var personProfiles: PostHogPersonProfiles | c:@M@PostHog@objc(cs)PostHogConfig(py)personProfiles PostHog | PostHogConfig.preloadFeatureFlags | property | @objc var preloadFeatureFlags: Bool | c:@M@PostHog@objc(cs)PostHogConfig(py)preloadFeatureFlags PostHog | PostHogConfig.projectToken | property | @objc let projectToken: String | c:@M@PostHog@objc(cs)PostHogConfig(py)projectToken -PostHog | PostHogConfig.pushIdentityProvider | property | @objc var pushIdentityProvider: ((String, String, @escaping (String?) -> Void) -> Void)? | c:@M@PostHog@objc(cs)PostHogConfig(py)pushIdentityProvider +PostHog | PostHogConfig.pushIdentityProvider | property | @objc var pushIdentityProvider: (@Sendable (String, String, @escaping @Sendable (String?) -> Void) -> Void)? | c:@M@PostHog@objc(cs)PostHogConfig(py)pushIdentityProvider PostHog | PostHogConfig.rageClickConfig | property | @objc let rageClickConfig: PostHogRageClickConfig | c:@M@PostHog@objc(cs)PostHogConfig(py)rageClickConfig PostHog | PostHogConfig.requestHeaders | property | @objc var requestHeaders: [String : String]? | c:@M@PostHog@objc(cs)PostHogConfig(py)requestHeaders PostHog | PostHogConfig.reuseAnonymousId | property | @objc var reuseAnonymousId: Bool | c:@M@PostHog@objc(cs)PostHogConfig(py)reuseAnonymousId @@ -72,8 +72,8 @@ PostHog | PostHogConfig.sendFeatureFlagEvent | property | @objc var sendFeatureF PostHog | PostHogConfig.sessionReplay | property | @objc var sessionReplay: Bool | c:@M@PostHog@objc(cs)PostHogConfig(py)sessionReplay PostHog | PostHogConfig.sessionReplayConfig | property | @objc let sessionReplayConfig: PostHogSessionReplayConfig | c:@M@PostHog@objc(cs)PostHogConfig(py)sessionReplayConfig PostHog | PostHogConfig.setBeforeSend(_:) | method | @objc func setBeforeSend(_ blocks: [BoxedBeforeSendBlock]) | c:@M@PostHog@objc(cs)PostHogConfig(im)setBeforeSend: -PostHog | PostHogConfig.setBeforeSend(_:) | method | func setBeforeSend(_ blocks: BeforeSendBlock...) | s:7PostHog0aB6ConfigC13setBeforeSendyyAA0aB5EventCSgAFcd_tF -PostHog | PostHogConfig.setBeforeSend(_:) | method | func setBeforeSend(_ blocks: [BeforeSendBlock]) | s:7PostHog0aB6ConfigC13setBeforeSendyySayAA0aB5EventCSgAFcGF +PostHog | PostHogConfig.setBeforeSend(_:) | method | func setBeforeSend(_ blocks: BeforeSendBlock...) | s:7PostHog0aB6ConfigC13setBeforeSendyyAA0aB5EventCSgAFYbcd_tF +PostHog | PostHogConfig.setBeforeSend(_:) | method | func setBeforeSend(_ blocks: [BeforeSendBlock]) | s:7PostHog0aB6ConfigC13setBeforeSendyySayAA0aB5EventCSgAFYbcGF PostHog | PostHogConfig.setDefaultPersonProperties | property | @objc var setDefaultPersonProperties: Bool | c:@M@PostHog@objc(cs)PostHogConfig(py)setDefaultPersonProperties PostHog | PostHogConfig.surveys | property | @objc var surveys: Bool { get set } | c:@M@PostHog@objc(cs)PostHogConfig(py)surveys PostHog | PostHogConfig.surveysConfig | property | @objc var surveysConfig: PostHogSurveysConfig { get set } | c:@M@PostHog@objc(cs)PostHogConfig(py)surveysConfig @@ -230,8 +230,8 @@ PostHog | PostHogLogsConfig.resourceAttributes | property | @objc var resourceAt PostHog | PostHogLogsConfig.serviceName | property | @objc var serviceName: String | c:@M@PostHog@objc(cs)PostHogLogsConfig(py)serviceName PostHog | PostHogLogsConfig.serviceVersion | property | @objc var serviceVersion: String | c:@M@PostHog@objc(cs)PostHogLogsConfig(py)serviceVersion PostHog | PostHogLogsConfig.setBeforeSend(_:) | method | @objc func setBeforeSend(_ blocks: [BoxedBeforeSendLogBlock]) | c:@M@PostHog@objc(cs)PostHogLogsConfig(im)setBeforeSend: -PostHog | PostHogLogsConfig.setBeforeSend(_:) | method | func setBeforeSend(_ blocks: PostHogBeforeSendLogBlock...) | s:7PostHog0aB10LogsConfigC13setBeforeSendyyAA0aB9LogRecordCSgAFcd_tF -PostHog | PostHogLogsConfig.setBeforeSend(_:) | method | func setBeforeSend(_ blocks: [PostHogBeforeSendLogBlock]) | s:7PostHog0aB10LogsConfigC13setBeforeSendyySayAA0aB9LogRecordCSgAFcGF +PostHog | PostHogLogsConfig.setBeforeSend(_:) | method | func setBeforeSend(_ blocks: PostHogBeforeSendLogBlock...) | s:7PostHog0aB10LogsConfigC13setBeforeSendyyAA0aB9LogRecordCSgAFYbcd_tF +PostHog | PostHogLogsConfig.setBeforeSend(_:) | method | func setBeforeSend(_ blocks: [PostHogBeforeSendLogBlock]) | s:7PostHog0aB10LogsConfigC13setBeforeSendyySayAA0aB9LogRecordCSgAFYbcGF PostHog | PostHogNextSurveyQuestion | class | @objc class PostHogNextSurveyQuestion | c:@M@PostHog@objc(cs)PostHogNextSurveyQuestion PostHog | PostHogNextSurveyQuestion.isSurveyCompleted | property | let isSurveyCompleted: Bool | s:7PostHog0aB18NextSurveyQuestionC02isD9CompletedSbvp PostHog | PostHogNextSurveyQuestion.questionIndex | property | let questionIndex: Int | s:7PostHog0aB18NextSurveyQuestionC13questionIndexSivp @@ -309,7 +309,7 @@ PostHog | PostHogSDK.register(_:) | method | @objc(registerProperties:) func reg PostHog | PostHogSDK.registerPushNotificationToken(_:) | method | @objc func registerPushNotificationToken(_ deviceToken: String) | c:@M@PostHog@objc(cs)PostHogSDK(im)registerPushNotificationToken: PostHog | PostHogSDK.registerPushNotificationToken(_:appId:) | method | @objc func registerPushNotificationToken(_ deviceToken: String, appId: String?) | c:@M@PostHog@objc(cs)PostHogSDK(im)registerPushNotificationToken:appId: PostHog | PostHogSDK.reloadFeatureFlags() | method | @objc func reloadFeatureFlags() | c:@M@PostHog@objc(cs)PostHogSDK(im)reloadFeatureFlags -PostHog | PostHogSDK.reloadFeatureFlags(_:) | method | @objc(reloadFeatureFlagsWithCallback:) func reloadFeatureFlags(_ callback: @escaping (PostHogFeatureFlagsLoaded) -> Void) | c:@M@PostHog@objc(cs)PostHogSDK(im)reloadFeatureFlagsWithCallback: +PostHog | PostHogSDK.reloadFeatureFlags(_:) | method | @objc(reloadFeatureFlagsWithCallback:) func reloadFeatureFlags(_ callback: @escaping @Sendable (PostHogFeatureFlagsLoaded) -> Void) | c:@M@PostHog@objc(cs)PostHogSDK(im)reloadFeatureFlagsWithCallback: PostHog | PostHogSDK.reset() | method | @objc func reset() | c:@M@PostHog@objc(cs)PostHogSDK(im)reset PostHog | PostHogSDK.resetGroupPropertiesForFlags() | method | @objc func resetGroupPropertiesForFlags() | c:@M@PostHog@objc(cs)PostHogSDK(im)resetGroupPropertiesForFlags PostHog | PostHogSDK.resetGroupPropertiesForFlags(_:) | method | @objc(resetGroupPropertiesForFlagsWithGroupType:) func resetGroupPropertiesForFlags(_ groupType: String) | c:@M@PostHog@objc(cs)PostHogSDK(im)resetGroupPropertiesForFlagsWithGroupType: @@ -347,7 +347,7 @@ PostHog | PostHogSessionReplayConfig.screenshotMode | property | @objc var scree PostHog | PostHogSessionReplayConfig.screenshotModeBackgroundCapture | property | @objc var screenshotModeBackgroundCapture: Bool | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)screenshotModeBackgroundCapture PostHog | PostHogSessionReplayConfig.throttleDelay | property | @objc var throttleDelay: TimeInterval | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)throttleDelay PostHog | PostHogSessionReplayConsoleLogConfig | class | @objc class PostHogSessionReplayConsoleLogConfig | c:@M@PostHog@objc(cs)PostHogSessionReplayConsoleLogConfig -PostHog | PostHogSessionReplayConsoleLogConfig.logSanitizer | property | @objc var logSanitizer: (String) -> PostHogConsoleLogEntry? | c:@M@PostHog@objc(cs)PostHogSessionReplayConsoleLogConfig(py)logSanitizer +PostHog | PostHogSessionReplayConsoleLogConfig.logSanitizer | property | @objc var logSanitizer: @Sendable (String) -> PostHogConsoleLogEntry? | c:@M@PostHog@objc(cs)PostHogSessionReplayConsoleLogConfig(py)logSanitizer PostHog | PostHogSessionReplayConsoleLogConfig.minLogLevel | property | @objc var minLogLevel: PostHogConsoleLogLevel | c:@M@PostHog@objc(cs)PostHogSessionReplayConsoleLogConfig(py)minLogLevel PostHog | PostHogSurveyResponse | class | @objc @objcMembers class PostHogSurveyResponse | c:@M@PostHog@objc(cs)PostHogSurveyResponse PostHog | PostHogSurveyResponse.link(_:) | type.method | static func link(_ clicked: Bool) -> PostHogSurveyResponse | c:@M@PostHog@objc(cs)PostHogSurveyResponse(cm)link: From 7ed1e93482f70b15da259e7cd0991a4727e414b8 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Tue, 6 Oct 2026 11:01:07 -0400 Subject: [PATCH 2/4] chore: match the public API snapshot to CI's Xcode 26.3 output Xcode 26.3's symbol graph leaves @Sendable out of declaration fragments, which 26.4 prints; the mangled names still differ. Co-Authored-By: Claude Opus 5.5 (1M context) --- api/posthog-ios.public-api.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/posthog-ios.public-api.txt b/api/posthog-ios.public-api.txt index 54060371f8..68fa353c9a 100644 --- a/api/posthog-ios.public-api.txt +++ b/api/posthog-ios.public-api.txt @@ -2,7 +2,7 @@ # Do not edit manually. # Public API scope: public/open symbols emitted for the PostHog iOS symbol graphs. -PostHog | BeforeSendBlock | typealias | typealias BeforeSendBlock = @Sendable (PostHogEvent) -> PostHogEvent? | s:7PostHog15BeforeSendBlocka +PostHog | BeforeSendBlock | typealias | typealias BeforeSendBlock = (PostHogEvent) -> PostHogEvent? | s:7PostHog15BeforeSendBlocka PostHog | BoxedBeforeSendBlock | class | @objc final class BoxedBeforeSendBlock | c:@M@PostHog@objc(cs)BoxedBeforeSendBlock PostHog | BoxedBeforeSendBlock.block | property | @objc let block: BeforeSendBlock | c:@M@PostHog@objc(cs)BoxedBeforeSendBlock(py)block PostHog | BoxedBeforeSendBlock.init(block:) | init | @objc(block:) init(block: @escaping BeforeSendBlock) | c:@M@PostHog@objc(cs)BoxedBeforeSendBlock(im)block: @@ -12,7 +12,7 @@ PostHog | BoxedBeforeSendLogBlock.init(block:) | init | @objc(block:) init(block PostHog | OnPostHogSurveyClosed | typealias | typealias OnPostHogSurveyClosed = (PostHogDisplaySurvey) -> Void | s:7PostHog02OnaB12SurveyCloseda PostHog | OnPostHogSurveyResponse | typealias | typealias OnPostHogSurveyResponse = (PostHogDisplaySurvey, Int, PostHogSurveyResponse) -> PostHogNextSurveyQuestion? | s:7PostHog02OnaB14SurveyResponsea PostHog | OnPostHogSurveyShown | typealias | typealias OnPostHogSurveyShown = (PostHogDisplaySurvey) -> Void | s:7PostHog02OnaB11SurveyShowna -PostHog | PostHogBeforeSendLogBlock | typealias | typealias PostHogBeforeSendLogBlock = @Sendable (PostHogLogRecord) -> PostHogLogRecord? | s:7PostHog0aB18BeforeSendLogBlocka +PostHog | PostHogBeforeSendLogBlock | typealias | typealias PostHogBeforeSendLogBlock = (PostHogLogRecord) -> PostHogLogRecord? | s:7PostHog0aB18BeforeSendLogBlocka PostHog | PostHogBootstrapConfig | class | @objc(PostHogBootstrapConfig) class PostHogBootstrapConfig | c:@M@PostHog@objc(cs)PostHogBootstrapConfig PostHog | PostHogBootstrapConfig.distinctId | property | @objc var distinctId: String? | c:@M@PostHog@objc(cs)PostHogBootstrapConfig(py)distinctId PostHog | PostHogBootstrapConfig.featureFlagPayloads | property | @objc var featureFlagPayloads: [String : Any]? | c:@M@PostHog@objc(cs)PostHogBootstrapConfig(py)featureFlagPayloads @@ -52,7 +52,7 @@ PostHog | PostHogConfig.evaluationContexts | property | @objc var evaluationCont PostHog | PostHogConfig.featureFlagRequestMaxRetries | property | @objc var featureFlagRequestMaxRetries: Int | c:@M@PostHog@objc(cs)PostHogConfig(py)featureFlagRequestMaxRetries PostHog | PostHogConfig.flushAt | property | @objc var flushAt: Int | c:@M@PostHog@objc(cs)PostHogConfig(py)flushAt PostHog | PostHogConfig.flushIntervalSeconds | property | @objc var flushIntervalSeconds: TimeInterval | c:@M@PostHog@objc(cs)PostHogConfig(py)flushIntervalSeconds -PostHog | PostHogConfig.getAnonymousId | property | @objc var getAnonymousId: @Sendable (UUID) -> UUID | c:@M@PostHog@objc(cs)PostHogConfig(py)getAnonymousId +PostHog | PostHogConfig.getAnonymousId | property | @objc var getAnonymousId: (UUID) -> UUID | c:@M@PostHog@objc(cs)PostHogConfig(py)getAnonymousId PostHog | PostHogConfig.host | property | @objc let host: URL | c:@M@PostHog@objc(cs)PostHogConfig(py)host PostHog | PostHogConfig.init(projectToken:) | init | @objc(projectToken:) init(projectToken: String) | c:@M@PostHog@objc(cs)PostHogConfig(im)projectToken: PostHog | PostHogConfig.init(projectToken:host:) | init | @objc(projectToken:host:) init(projectToken: String, host: String = defaultHost) | c:@M@PostHog@objc(cs)PostHogConfig(im)projectToken:host: @@ -64,7 +64,7 @@ PostHog | PostHogConfig.optOut | property | @objc var optOut: Bool | c:@M@PostHo PostHog | PostHogConfig.personProfiles | property | @objc var personProfiles: PostHogPersonProfiles | c:@M@PostHog@objc(cs)PostHogConfig(py)personProfiles PostHog | PostHogConfig.preloadFeatureFlags | property | @objc var preloadFeatureFlags: Bool | c:@M@PostHog@objc(cs)PostHogConfig(py)preloadFeatureFlags PostHog | PostHogConfig.projectToken | property | @objc let projectToken: String | c:@M@PostHog@objc(cs)PostHogConfig(py)projectToken -PostHog | PostHogConfig.pushIdentityProvider | property | @objc var pushIdentityProvider: (@Sendable (String, String, @escaping @Sendable (String?) -> Void) -> Void)? | c:@M@PostHog@objc(cs)PostHogConfig(py)pushIdentityProvider +PostHog | PostHogConfig.pushIdentityProvider | property | @objc var pushIdentityProvider: ((String, String, @escaping (String?) -> Void) -> Void)? | c:@M@PostHog@objc(cs)PostHogConfig(py)pushIdentityProvider PostHog | PostHogConfig.rageClickConfig | property | @objc let rageClickConfig: PostHogRageClickConfig | c:@M@PostHog@objc(cs)PostHogConfig(py)rageClickConfig PostHog | PostHogConfig.requestHeaders | property | @objc var requestHeaders: [String : String]? | c:@M@PostHog@objc(cs)PostHogConfig(py)requestHeaders PostHog | PostHogConfig.reuseAnonymousId | property | @objc var reuseAnonymousId: Bool | c:@M@PostHog@objc(cs)PostHogConfig(py)reuseAnonymousId @@ -309,7 +309,7 @@ PostHog | PostHogSDK.register(_:) | method | @objc(registerProperties:) func reg PostHog | PostHogSDK.registerPushNotificationToken(_:) | method | @objc func registerPushNotificationToken(_ deviceToken: String) | c:@M@PostHog@objc(cs)PostHogSDK(im)registerPushNotificationToken: PostHog | PostHogSDK.registerPushNotificationToken(_:appId:) | method | @objc func registerPushNotificationToken(_ deviceToken: String, appId: String?) | c:@M@PostHog@objc(cs)PostHogSDK(im)registerPushNotificationToken:appId: PostHog | PostHogSDK.reloadFeatureFlags() | method | @objc func reloadFeatureFlags() | c:@M@PostHog@objc(cs)PostHogSDK(im)reloadFeatureFlags -PostHog | PostHogSDK.reloadFeatureFlags(_:) | method | @objc(reloadFeatureFlagsWithCallback:) func reloadFeatureFlags(_ callback: @escaping @Sendable (PostHogFeatureFlagsLoaded) -> Void) | c:@M@PostHog@objc(cs)PostHogSDK(im)reloadFeatureFlagsWithCallback: +PostHog | PostHogSDK.reloadFeatureFlags(_:) | method | @objc(reloadFeatureFlagsWithCallback:) func reloadFeatureFlags(_ callback: @escaping (PostHogFeatureFlagsLoaded) -> Void) | c:@M@PostHog@objc(cs)PostHogSDK(im)reloadFeatureFlagsWithCallback: PostHog | PostHogSDK.reset() | method | @objc func reset() | c:@M@PostHog@objc(cs)PostHogSDK(im)reset PostHog | PostHogSDK.resetGroupPropertiesForFlags() | method | @objc func resetGroupPropertiesForFlags() | c:@M@PostHog@objc(cs)PostHogSDK(im)resetGroupPropertiesForFlags PostHog | PostHogSDK.resetGroupPropertiesForFlags(_:) | method | @objc(resetGroupPropertiesForFlagsWithGroupType:) func resetGroupPropertiesForFlags(_ groupType: String) | c:@M@PostHog@objc(cs)PostHogSDK(im)resetGroupPropertiesForFlagsWithGroupType: @@ -347,7 +347,7 @@ PostHog | PostHogSessionReplayConfig.screenshotMode | property | @objc var scree PostHog | PostHogSessionReplayConfig.screenshotModeBackgroundCapture | property | @objc var screenshotModeBackgroundCapture: Bool | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)screenshotModeBackgroundCapture PostHog | PostHogSessionReplayConfig.throttleDelay | property | @objc var throttleDelay: TimeInterval | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)throttleDelay PostHog | PostHogSessionReplayConsoleLogConfig | class | @objc class PostHogSessionReplayConsoleLogConfig | c:@M@PostHog@objc(cs)PostHogSessionReplayConsoleLogConfig -PostHog | PostHogSessionReplayConsoleLogConfig.logSanitizer | property | @objc var logSanitizer: @Sendable (String) -> PostHogConsoleLogEntry? | c:@M@PostHog@objc(cs)PostHogSessionReplayConsoleLogConfig(py)logSanitizer +PostHog | PostHogSessionReplayConsoleLogConfig.logSanitizer | property | @objc var logSanitizer: (String) -> PostHogConsoleLogEntry? | c:@M@PostHog@objc(cs)PostHogSessionReplayConsoleLogConfig(py)logSanitizer PostHog | PostHogSessionReplayConsoleLogConfig.minLogLevel | property | @objc var minLogLevel: PostHogConsoleLogLevel | c:@M@PostHog@objc(cs)PostHogSessionReplayConsoleLogConfig(py)minLogLevel PostHog | PostHogSurveyResponse | class | @objc @objcMembers class PostHogSurveyResponse | c:@M@PostHog@objc(cs)PostHogSurveyResponse PostHog | PostHogSurveyResponse.link(_:) | type.method | static func link(_ clicked: Bool) -> PostHogSurveyResponse | c:@M@PostHog@objc(cs)PostHogSurveyResponse(cm)link: From 2abbfceb9e76eb6777ffea35126613b633557427 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Tue, 6 Oct 2026 15:29:55 -0400 Subject: [PATCH 3/4] fix: store bridged flag values in PostHogFeatureFlagsLoaded The init kept the original variant values after filtering, so a mutable NSString passed via bootstrap stayed mutable inside a Sendable payload. Store the `as? Bool`/`as? String` results instead. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Models/PostHogFeatureFlagsLoaded.swift | 13 +++++++---- .../PostHogCallbackIsolationTests.swift | 23 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/PostHog/Models/PostHogFeatureFlagsLoaded.swift b/PostHog/Models/PostHogFeatureFlagsLoaded.swift index 1f224eecee..1782054cb0 100644 --- a/PostHog/Models/PostHogFeatureFlagsLoaded.swift +++ b/PostHog/Models/PostHogFeatureFlagsLoaded.swift @@ -23,7 +23,7 @@ import Foundation /// ``` @objc(PostHogFeatureFlagsLoaded) public final class PostHogFeatureFlagsLoaded: NSObject, @unchecked Sendable { - // @unchecked: every property is a `let`, and `variants` only holds `Bool` and `String` values (see init). + // @unchecked: every property is a `let`, and `variants` only holds Swift `Bool` and `String` values (see init). /// The keys of the enabled feature flags. @objc public let flags: [String] @@ -40,11 +40,16 @@ public final class PostHogFeatureFlagsLoaded: NSObject, @unchecked Sendable { @objc public let errorsLoading: Bool init(featureFlags: [String: Any], errorsLoading: Bool) { - let enabled = featureFlags.filter { _, value in + // Store the bridged values, not the originals: a bootstrap `NSMutableString` would stay mutable. + var enabled: [String: Any] = [:] + for (key, value) in featureFlags { if let bool = value as? Bool { - return bool + if bool { + enabled[key] = bool + } + } else if let string = value as? String { + enabled[key] = string } - return value is String } flags = Array(enabled.keys) variants = enabled diff --git a/PostHogSwift6Tests/PostHogCallbackIsolationTests.swift b/PostHogSwift6Tests/PostHogCallbackIsolationTests.swift index 670fb07765..d9b646a4ea 100644 --- a/PostHogSwift6Tests/PostHogCallbackIsolationTests.swift +++ b/PostHogSwift6Tests/PostHogCallbackIsolationTests.swift @@ -77,4 +77,27 @@ struct PostHogCallbackIsolationTests { let flags = await Task.detached { loaded?.flags ?? [] }.value #expect(flags == ["swift6-flag"]) } + + @Test("onFeatureFlags payload keeps its variant values when a mutable bootstrap value changes") + func onFeatureFlagsPayloadIsImmutable() async { + let source = NSMutableString(string: "original") + let sdk = makeSDK(bootstrapFlags: ["swift6-variant": source]) + defer { sdk.close() } + let state = AppState() + + var subscription: PostHogFeatureFlagsSubscription? + await withCheckedContinuation { continuation in + subscription = sdk.onFeatureFlags { loaded in + guard state.loaded == nil else { return } + state.loaded = loaded + continuation.resume() + } + } + subscription?.unsubscribe() + + source.setString("changed") + let loaded = state.loaded + let variant = await Task.detached { loaded?.variants["swift6-variant"] as? String }.value + #expect(variant == "original") + } } From be9478faf2faddda69ad338702c09d72fbd59bd4 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Tue, 6 Oct 2026 15:43:08 -0400 Subject: [PATCH 4/4] docs: clarify the Sendable callbacks changeset Name the config properties unambiguously and warn that array conversions crash in both directions. Co-Authored-By: Claude Opus 5.5 (1M context) --- .changeset/v4-sendable-callbacks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/v4-sendable-callbacks.md b/.changeset/v4-sendable-callbacks.md index 2497b6857f..68bb07b2ae 100644 --- a/.changeset/v4-sendable-callbacks.md +++ b/.changeset/v4-sendable-callbacks.md @@ -2,6 +2,6 @@ 'posthog-ios': major --- -**Breaking:** mark callbacks the SDK can call off the main thread `@Sendable` — `reloadFeatureFlags(_:)`, the `BeforeSendBlock` and `PostHogBeforeSendLogBlock` used by `setBeforeSend(_:)` on `PostHogConfig` and `PostHogLogsConfig`, `getAnonymousId`, `pushIdentityProvider` and its `completion`, and `logSanitizer` — and make `PostHogFeatureFlagsLoaded` `Sendable`. This fixes a crash in Swift 6 apps when a `reloadFeatureFlags(_:)` callback is written in main-actor code. +**Breaking:** mark callbacks the SDK can call off the main thread `@Sendable` — `reloadFeatureFlags(_:)`, the `BeforeSendBlock` and `PostHogBeforeSendLogBlock` used by `setBeforeSend(_:)` on `PostHogConfig` and `PostHogLogsConfig`, `PostHogConfig.getAnonymousId`, `PostHogConfig.pushIdentityProvider` and its `completion`, and `sessionReplayConfig.captureLogsConfig.logSanitizer` — and make `PostHogFeatureFlagsLoaded` `Sendable`. This fixes a crash in Swift 6 apps when a `reloadFeatureFlags(_:)` callback is written in main-actor code. -To update main-actor state from `reloadFeatureFlags(_:)` or `pushIdentityProvider`, hop with `Task { @MainActor in ... }`. The other callbacks return a value immediately, so they must not read main-actor state. Declare closures you store in your own variables as `@Sendable` to avoid new Swift 5 warnings, and type pre-built `setBeforeSend(_:)` arrays as `[BeforeSendBlock]` or `[PostHogBeforeSendLogBlock]`: an array typed with the old function type crashes at runtime in Swift 5 mode. +To update main-actor state from `reloadFeatureFlags(_:)` or `pushIdentityProvider`, hop with `Task { @MainActor in ... }`. The other callbacks return a value immediately, so they must not read main-actor state. Declare closures you store in your own variables as `@Sendable` to avoid new Swift 5 warnings. Use the `BeforeSendBlock` and `PostHogBeforeSendLogBlock` typealiases for any `setBeforeSend(_:)` arrays you build or pass around, including your own helpers: in Swift 5 mode, converting an array between the old function type and these types compiles but crashes at runtime, in either direction.