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
1 change: 1 addition & 0 deletions .changeset/v4-major-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ PostHog iOS 4.0. Every breaking change below names what to use instead.
**Added**

- Add `SessionReplay`, `CrashReporting` and `Surveys` Swift package traits (on by default) so SwiftPM apps can leave out session replay and the vendored libwebp, the vendored PLCrashReporter, or the survey engine and UI.
- Turning a trait off also removes its public API, so code that configures a left-out feature fails to compile instead of silently doing nothing: `sessionReplay`, `sessionReplayConfig`, `startSessionRecording()`, `stopSessionRecording()`, `isSessionReplayActive()` and `postHogNoMask()` need `SessionReplay`; `errorTrackingConfig.autoCapture` needs `CrashReporting`; `surveys`, `surveysConfig` and the survey delegate and display types need `Surveys`.

**Removed**

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: latest-stable
- name: Check public API snapshot
- name: Check public API snapshots (default build and package traits off)
run: make apiCheck

build-sdk-min-toolchain:
Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ This section is for external contributors. PostHog maintainers (members of the P

When reviewing or fixing someone else's PR, don't ask for or open an issue. Note an external contributor's public API change when it has neither an agreed issue nor an API-defining published spec; the pre-implementation issue policy above is not a gate on that review work.

`make apiCheck` checks [api/posthog-ios.public-api.txt](api/posthog-ios.public-api.txt), as CI does. `make apiUpdate` regenerates it only after intentional public API changes; review the snapshot diff. Both commands build the SDK and extract its public symbol graph, requiring Xcode with the iOS SDK, xcpretty and Python 3. A diff in that file means your change touches public API.
`make apiCheck` checks [api/posthog-ios.public-api.txt](api/posthog-ios.public-api.txt) and [api/posthog-ios.public-api.no-traits.txt](api/posthog-ios.public-api.no-traits.txt), as CI does. `make apiUpdate` regenerates them only after intentional public API changes; review the snapshot diff. Both commands build the SDK and extract its public symbol graph, requiring Xcode with the iOS SDK, xcpretty and Python 3. A diff in either file means your change touches public API.

Public API that only works with a package trait (`SessionReplay`, `CrashReporting` or `Surveys`) is gated with the same `#if !SWIFT_PACKAGE || <Trait>` condition as its implementation, so it is removed when the trait is off. The `no-traits` snapshot is the Swift Package Manager build with every trait off; a trait-specific symbol showing up there means its gate is missing.

Above all, thank you for contributing!
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,15 @@ lint: swiftFormatCheck swiftLintCheck
api:
periphery scan

# Checks both public API snapshots: the default build, and the Swift Package Manager build with every
# package trait off (api/posthog-ios.public-api.no-traits.txt), where trait-gated public API is removed.
apiCheck:
scripts/check-public-api.sh
scripts/check-public-api.sh --no-traits

apiUpdate:
scripts/check-public-api.sh --update
scripts/check-public-api.sh --update --no-traits

# requires gem and brew
# xcpretty needs 'export LANG=en_US.UTF-8'
Expand Down
54 changes: 28 additions & 26 deletions PostHog/ErrorTracking/PostHogErrorTrackingConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,34 @@ import Foundation
@objc public class PostHogErrorTrackingConfig: NSObject {
// MARK: - Crash Reporting

/// Enable crash autocapture
///
/// When enabled, the SDK will capture the following crash types:
/// - Mach exceptions (e.g., `EXC_BAD_ACCESS`, `EXC_CRASH`)
/// - POSIX signals (e.g., `SIGSEGV`, `SIGABRT`, `SIGBUS`)
/// - Uncaught `NSException`s
///
/// Crashes are persisted to disk and sent as `$exception` events with level "fatal" **on the next app launch**
///
/// - Note: Crash reporting is automatically disabled when a debugger is attached,
/// as the debugger intercepts signals before the crash handler can process them.
///
/// Default: false
private var _autoCapture: Bool = false

/// Whether crash autocapture is enabled.
///
/// When enabled, fatal crashes are persisted and sent as `$exception` events on the next launch.
/// With Swift Package Manager, this requires the `CrashReporting` package trait, which is enabled by default.
/// Default: `false`.
@available(watchOS, unavailable, message: "Crash autocapture is not available on watchOS")
@available(visionOS, unavailable, message: "Crash autocapture is not available on visionOS")
@objc public var autoCapture: Bool {
get { _autoCapture }
set { _autoCapture = newValue }
}
#if !SWIFT_PACKAGE || CrashReporting
/// Enable crash autocapture
///
/// When enabled, the SDK will capture the following crash types:
/// - Mach exceptions (e.g., `EXC_BAD_ACCESS`, `EXC_CRASH`)
/// - POSIX signals (e.g., `SIGSEGV`, `SIGABRT`, `SIGBUS`)
/// - Uncaught `NSException`s
///
/// Crashes are persisted to disk and sent as `$exception` events with level "fatal" **on the next app launch**
///
/// - Note: Crash reporting is automatically disabled when a debugger is attached,
/// as the debugger intercepts signals before the crash handler can process them.
///
/// Default: false
private var _autoCapture: Bool = false

/// Whether crash autocapture is enabled.
///
/// When enabled, fatal crashes are persisted and sent as `$exception` events on the next launch.
/// With Swift Package Manager, this is only available with the `CrashReporting` package trait, which is enabled by default.
/// Default: `false`.
@available(watchOS, unavailable, message: "Crash autocapture is not available on watchOS")
@available(visionOS, unavailable, message: "Crash autocapture is not available on visionOS")
@objc public var autoCapture: Bool {
get { _autoCapture }
set { _autoCapture = newValue }
}
#endif

// MARK: - In-App Detection Configuration

Expand Down
2 changes: 1 addition & 1 deletion PostHog/Models/Surveys/PostHogSurvey+Display.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#if os(iOS) || TESTING
#if (os(iOS) || TESTING) && (!SWIFT_PACKAGE || Surveys)
import Foundation

extension PostHogSurvey {
Expand Down
77 changes: 43 additions & 34 deletions PostHog/PostHogConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,12 @@ public typealias BeforeSendBlock = @Sendable (PostHogEvent) -> PostHogEvent?
/// - iOS does not send `X-POSTHOG-WINDOW-ID` because mobile apps do not have a per-window/tab concept
/// - Existing values for these headers will be overwritten
@objc public var tracingHeaders: [String]?
#endif

#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
/// Enable Recording of Session Replays for iOS
///
/// With Swift Package Manager, this requires the `SessionReplay` package trait, which is enabled by default.
/// With Swift Package Manager, this is only available with the `SessionReplay` package trait, which is enabled by default.
///
/// Note: Ingestion controls (sampling, feature flags, and event triggers) are currently applied using AND logic.
/// All configured conditions must be satisfied for recording to start.
Expand All @@ -411,7 +413,7 @@ public typealias BeforeSendBlock = @Sendable (PostHogEvent) -> PostHogEvent?
@objc public var sessionReplay: Bool = false
/// Session Replay configuration
///
/// With Swift Package Manager, this requires the `SessionReplay` package trait, which is enabled by default.
/// With Swift Package Manager, this is only available with the `SessionReplay` package trait, which is enabled by default.
@objc public let sessionReplayConfig: PostHogSessionReplayConfig = .init()
#endif

Expand All @@ -424,35 +426,37 @@ public typealias BeforeSendBlock = @Sendable (PostHogEvent) -> PostHogEvent?
/// Mutate fields on `config.logs` before calling `PostHogSDK.setup(_:)`.
@objc public let logs: PostHogLogsConfig = .init()

/// Enable mobile surveys
///
/// With Swift Package Manager, this requires the `Surveys` package trait, which is enabled by default.
///
/// Default: true
///
/// Note: Event triggers will only work with the instance that first enables surveys.
/// In case of multiple instances, please make sure you are capturing events on the instance that has config.surveys = true
@available(watchOS, unavailable, message: "Surveys are only available on iOS")
@available(macOS, unavailable, message: "Surveys are only available on iOS")
@available(tvOS, unavailable, message: "Surveys are only available on iOS")
@available(visionOS, unavailable, message: "Surveys are only available on iOS")
@objc public var surveys: Bool {
get { _surveys }
set { _surveys = newValue }
}
#if !SWIFT_PACKAGE || Surveys
/// Enable mobile surveys
///
/// With Swift Package Manager, this is only available with the `Surveys` package trait, which is enabled by default.
///
/// Default: true
///
/// Note: Event triggers will only work with the instance that first enables surveys.
/// In case of multiple instances, please make sure you are capturing events on the instance that has config.surveys = true
@available(watchOS, unavailable, message: "Surveys are only available on iOS")
@available(macOS, unavailable, message: "Surveys are only available on iOS")
@available(tvOS, unavailable, message: "Surveys are only available on iOS")
@available(visionOS, unavailable, message: "Surveys are only available on iOS")
@objc public var surveys: Bool {
get { _surveys }
set { _surveys = newValue }
}

/// Configuration for mobile survey presentation and localization.
///
/// Mutate fields on `config.surveysConfig` or replace this object before calling setup.
/// With Swift Package Manager, this requires the `Surveys` package trait, which is enabled by default.
@available(watchOS, unavailable, message: "Surveys are only available on iOS")
@available(macOS, unavailable, message: "Surveys are only available on iOS")
@available(tvOS, unavailable, message: "Surveys are only available on iOS")
@available(visionOS, unavailable, message: "Surveys are only available on iOS")
@objc public var surveysConfig: PostHogSurveysConfig {
get { _surveysConfig }
set { _surveysConfig = newValue }
}
/// Configuration for mobile survey presentation and localization.
///
/// Mutate fields on `config.surveysConfig` or replace this object before calling setup.
/// With Swift Package Manager, this is only available with the `Surveys` package trait, which is enabled by default.
@available(watchOS, unavailable, message: "Surveys are only available on iOS")
@available(macOS, unavailable, message: "Surveys are only available on iOS")
@available(tvOS, unavailable, message: "Surveys are only available on iOS")
@available(visionOS, unavailable, message: "Surveys are only available on iOS")
@objc public var surveysConfig: PostHogSurveysConfig {
get { _surveysConfig }
set { _surveysConfig = newValue }
}
#endif

/// Optional custom URLSessionConfiguration for network requests
/// If not set, uses URLSessionConfiguration.default
Expand Down Expand Up @@ -519,7 +523,7 @@ public typealias BeforeSendBlock = @Sendable (PostHogEvent) -> PostHogEvent?
func getIntegrations() -> [PostHogIntegration] {
var integrations: [PostHogIntegration] = []

#if os(iOS) || os(macOS) || os(tvOS)
#if (os(iOS) || os(macOS) || os(tvOS)) && (!SWIFT_PACKAGE || CrashReporting)
if errorTrackingConfig.autoCapture {
integrations.append(PostHogErrorTrackingAutoCaptureIntegration())
}
Expand All @@ -535,15 +539,18 @@ public typealias BeforeSendBlock = @Sendable (PostHogEvent) -> PostHogEvent?
if tracingHeaders?.isEmpty == false {
integrations.append(PostHogTracingHeadersIntegration())
}
#endif

#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
if sessionReplay {
integrations.append(PostHogReplayIntegration())
}
#endif

#if os(iOS) && (!SWIFT_PACKAGE || Surveys)
if _surveys {
integrations.append(PostHogSurveyIntegration())
}

#endif

#if os(iOS) || targetEnvironment(macCatalyst)
Expand Down Expand Up @@ -581,9 +588,11 @@ public typealias BeforeSendBlock = @Sendable (PostHogEvent) -> PostHogEvent?
}
#endif

var _surveys: Bool = true // swiftlint:disable:this identifier_name
#if !SWIFT_PACKAGE || Surveys
var _surveys: Bool = true // swiftlint:disable:this identifier_name

var _surveysConfig: PostHogSurveysConfig = .init() // swiftlint:disable:this identifier_name
var _surveysConfig: PostHogSurveysConfig = .init() // swiftlint:disable:this identifier_name
#endif

/// Hook that allows to sanitize the event
/// The hook is called before the event is cached or sent over the wire
Expand Down
35 changes: 21 additions & 14 deletions PostHog/PostHogSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ let maxRetryDelay = 30.0
/// `nil` before `setup(_:)` is called.
@objc public private(set) var logger: PostHogLogger?

#if os(iOS)
#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
private weak var _replayIntegration: PostHogReplayIntegration?
/// Same shape as `queue`/`replayQueue` above: written under `setupLock` by install/uninstall,
/// read from any caller thread by `buildProperties`.
Expand All @@ -130,13 +130,16 @@ let maxRetryDelay = 30.0
set { setupLock.withLock { _replayIntegration = newValue } }
}

private weak var surveysIntegration: PostHogSurveyIntegration?
/// Whether `optIn()` starts session replay: whether the app wants it, from `config.sessionReplay`
/// and the app's last `startSessionRecording()`/`stopSessionRecording()` while opted in, so consent
/// doesn't override a stop. Internal stops don't change it. Guarded by `setupLock`.
private var startSessionRecordingOnOptIn = true
#endif

#if os(iOS) && (!SWIFT_PACKAGE || Surveys)
private weak var surveysIntegration: PostHogSurveyIntegration?
#endif

/// Shared singleton SDK instance used by most applications.
///
/// Call `setup(_:)` once with a `PostHogConfig` before using capture APIs.
Expand Down Expand Up @@ -317,7 +320,7 @@ let maxRetryDelay = 30.0
// this only reconciles an existing local identity.
reconcileBootstrapIdentityIfNeeded()

#if os(iOS)
#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
// Launched opted out: optIn() starts replay only when the config does
startSessionRecordingOnOptIn = config.sessionReplay
#endif
Expand Down Expand Up @@ -750,7 +753,7 @@ let maxRetryDelay = 30.0
}
// only Session replay requires $window_id, so we set as the same as $session_id.
// the backend might fallback to $session_id if $window_id is not present next.
#if os(iOS)
#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
if !appendSharedProps, isSessionReplayActive() {
props["$window_id"] = sessionId
}
Expand Down Expand Up @@ -2772,14 +2775,14 @@ let maxRetryDelay = 30.0
}
}

#if os(iOS)
#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
// Replay restarts in a new session (a no-op under React Native, which owns the session id)
if startSessionRecordingOnOptIn {
sessionManager.startSession()
}
#endif
installIntegrations()
#if os(iOS)
#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
if !startSessionRecordingOnOptIn {
// install() starts recording; undo it if the app had stopped replay before opting out
replayIntegration?.stop()
Expand Down Expand Up @@ -2887,7 +2890,7 @@ let maxRetryDelay = 30.0
replayQueue = nil
logsQueue = nil
pushSubscriptionHandler = nil
#if os(iOS)
#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
startSessionRecordingOnOptIn = true
#endif
replayDebugPropertiesLock.withLock {
Expand Down Expand Up @@ -2926,7 +2929,7 @@ let maxRetryDelay = 30.0
lastScreenLock.withLock { _lastScreenName = nil }
}

#if os(iOS)
#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
/**
Starts session recording.

Expand Down Expand Up @@ -3090,7 +3093,7 @@ let maxRetryDelay = 30.0
return postHog
}

#if os(iOS)
#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
/// Returns whether session replay is currently recording.
///
/// - Returns: `true` only when replay is active, a session ID exists, and remote config allows recording.
Expand Down Expand Up @@ -3356,12 +3359,14 @@ let maxRetryDelay = 30.0

installed.append(integration)

#if os(iOS)
// TODO: Decouple these two integrations from PostHogSDK intance
// TODO: Decouple these two integrations from PostHogSDK intance
#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
if let replayIntegration = integration as? PostHogReplayIntegration {
self.replayIntegration = replayIntegration
}
#endif

#if os(iOS) && (!SWIFT_PACKAGE || Surveys)
if let surveysIntegration = integration as? PostHogSurveyIntegration {
self.surveysIntegration = surveysIntegration
}
Expand All @@ -3373,7 +3378,7 @@ let maxRetryDelay = 30.0
installedIntegrations = installed
}

#if os(iOS)
#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
private func installReplayIntegration() {
guard replayIntegration == nil else { return }

Expand Down Expand Up @@ -3428,8 +3433,10 @@ let maxRetryDelay = 30.0
}
installedIntegrations = []

#if os(iOS)
#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
replayIntegration = nil
#endif
#if os(iOS) && (!SWIFT_PACKAGE || Surveys)
surveysIntegration = nil
#endif
}
Expand Down Expand Up @@ -3801,7 +3808,7 @@ let maxRetryDelay = 30.0
}
#endif

#if os(iOS)
#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
func getReplayIntegration() -> PostHogReplayIntegration? {
getIntegration()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by Ioannis Josephides on 09/05/2025.
//

#if os(iOS)
#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
import Foundation

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by Ioannis Josephides on 09/05/2025.
//

#if os(iOS)
#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
import Foundation

/// The severity level of a console log entry.
Expand Down
Loading
Loading