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
5 changes: 5 additions & 0 deletions .changeset/ignored-exception-types-watchos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-ios': patch
---

Fix `errorTrackingConfig.ignoredExceptionTypes` not applying to manually captured exceptions on watchOS and visionOS.
5 changes: 5 additions & 0 deletions .changeset/spm-package-traits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-ios': minor
---

Add `SessionReplay` and `CrashReporting` Swift package traits (on by default) so SwiftPM apps can leave out session replay with the vendored libwebp, and the vendored PLCrashReporter, with `traits: []`.
5 changes: 5 additions & 0 deletions .changeset/spm-surveys-trait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-ios': minor
---

Add a `Surveys` Swift package trait (on by default) so SwiftPM apps that don't use surveys can leave out the survey engine and UI.
5 changes: 5 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ jobs:
command: set -o pipefail && xcrun xcodebuild clean build -scheme PostHog -destination generic/platform=ios | xcpretty
- platform: macos-spm
command: set -o pipefail && xcrun swift build --arch arm64
- platform: spm-no-traits
command: make buildSdkSpmNoTraits buildTestsSpmTraitsIOS
- platform: macos
command: set -o pipefail && xcrun xcodebuild clean build -scheme PostHog -destination generic/platform=macos | xcpretty
- platform: tvos
Expand Down Expand Up @@ -87,6 +89,9 @@ jobs:
- name: Build SDK via Swift Package Manager
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
run: make buildSdkSpm
- name: Build SDK via Swift Package Manager with package traits off
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
run: make buildSdkSpmNoTraits
- name: Install CocoaPods
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
run: gem install cocoapods
Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,35 @@ jobs:
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
run: make test

# Package traits off: libwebp and PLCrashReporter are not linked, so the crash-reporting stub
# and the trait-gated tests run.
test-no-traits:
needs: detect-markdown-only
runs-on: macos-15
steps:
- name: Complete markdown-only PR check
if: needs.detect-markdown-only.outputs.markdown_only == 'true'
run: echo "Only Markdown files changed; no additional work is required for this check."
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
with:
xcode-version: latest-stable
- name: Cache SPM dependencies
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
.build
~/Library/Caches/org.swift.swiftpm
key: ${{ runner.os }}-spm-no-traits-${{ hashFiles('Package.swift', 'Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-no-traits-
- name: Test SDK (package traits off)
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
run: make testNoTraits

# `make test` runs `swift test` on macOS, which compiles out every `#if os(iOS)` block, so the
# iOS-only suites (session replay, autocapture, tracing headers, crash reporting, etc.) never run
# there. Run the iOS test target on a simulator so those suites are actually exercised in CI.
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
- Preserve deployment minima: iOS 15, tvOS 15, macOS 11, watchOS 10, visionOS 1. Keep platform guards and graceful degradation, especially for session replay. Availability annotations/checks do not require adding platform support or raising targets. Intentional support changes require checking `Package.swift` plus CocoaPods/Xcode packaging/build configuration.
- Public APIs must remain thread-safe and callable from any thread. Preserve offline operation and queue-based event batching; never assume connectivity. `PostHogSDK.shared` is the default singleton; independent instances use `PostHogSDK.with(_:)` with `PostHogConfig`.
- Preserve replay masking/privacy behavior and privacy-safe error logging; do not expose sensitive user data.
- Prefer no new dependencies. libwebp is embedded; PHPLCrashReporter is vendored, prefixed PLCrashReporter for native crash reporting on iOS/macOS/tvOS, excluded on watchOS/visionOS.
- Prefer no new dependencies. libwebp is embedded; PHPLCrashReporter is vendored, prefixed PLCrashReporter for native crash reporting on iOS/macOS/tvOS, excluded on watchOS/visionOS. Both are behind default-on package traits (`SessionReplay`, which also gates the replay engine, and `CrashReporting`); gate code that uses them with `#if !SWIFT_PACKAGE || <Trait>`. The survey engine and UI are behind the default-on `Surveys` trait the same way. Once released, never add a new default-on trait on its own: apps that list traits would silently lose the feature. Split an existing trait instead, keeping it as an umbrella that enables the new ones.

## Validation selector
- Use `make` wrappers, not direct `swift`/`xcodebuild`. Iterate with focused tests; before submitting SDK changes, run contributor checks: `make lint`, `make test`, `make buildSdk`.
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Markdown-only changes need documentation/link checks and `git diff --check`, not
| Replay presentation/privacy masking in an app host | `make testPresentationMasks` |
| Masking golden-image verification | `make maskSnapshots` |
| SDK across supported platforms, including Mac Catalyst | `make buildSdk` |
| Code gated by the `SessionReplay`, `CrashReporting` or `Surveys` package traits | `make buildSdkSpmNoTraits`, `make testNoTraits`, `make buildTestsSpmTraitsIOS` |
| Example/platform integrations | `make buildExamplesPlatforms` |
| CocoaPods integration modes | `make buildExamplePodsStaticLib`, `make buildExamplePodsStaticFramework`, `make buildExamplePodsDynamicFramework` |
| XCFramework integration | `make buildExampleXCFramework` |
Expand Down
20 changes: 19 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: testSurveyUI build buildSdk buildExamples format swiftLint swiftFormat swiftLintCheck swiftFormatCheck installSwiftLint installSwiftFormat test testUploadSymbols recordEventShapeSnapshots testDowngradeCompatibility testOniOSSimulator testOnMacSimulator maskSnapshots recordMaskSnapshots checkMaskSnapshotRuntime lint bootstrap releaseCocoaPods api apiCheck apiUpdate buildIOS buildSdkSpm
.PHONY: testSurveyUI build buildSdk buildExamples format swiftLint swiftFormat swiftLintCheck swiftFormatCheck installSwiftLint installSwiftFormat test testUploadSymbols recordEventShapeSnapshots testDowngradeCompatibility testOniOSSimulator testOnMacSimulator maskSnapshots recordMaskSnapshots checkMaskSnapshotRuntime lint bootstrap releaseCocoaPods api apiCheck apiUpdate buildIOS buildSdkSpm buildSdkSpmNoTraits buildTestsSpmTraitsIOS testNoTraits

build: buildSdk buildExamples

Expand All @@ -8,6 +8,7 @@ buildIOS:
buildSdk:
set -o pipefail && xcrun xcodebuild clean build -scheme PostHog -destination generic/platform=ios | xcpretty #ios
$(MAKE) buildSdkSpm #macOS
$(MAKE) buildSdkSpmNoTraits #macOS + iOS, package traits off
set -o pipefail && xcrun xcodebuild clean build -scheme PostHog -destination generic/platform=macos | xcpretty #macOS
set -o pipefail && xcrun xcodebuild clean build -scheme PostHog -destination 'platform=macOS,variant=Mac Catalyst' | xcpretty #Mac Catalyst
set -o pipefail && xcrun xcodebuild clean build -scheme PostHog -destination generic/platform=tvos | xcpretty #tvOS
Expand All @@ -17,6 +18,20 @@ buildSdk:
buildSdkSpm:
set -o pipefail && xcrun swift build --arch arm64

# Builds with every package trait off, so libwebp and PLCrashReporter are not compiled or linked.
# The iOS build covers the WebP gate, which macOS compiles out.
buildSdkSpmNoTraits:
set -o pipefail && xcrun swift build --target PostHog --arch arm64 --disable-default-traits #macOS
set -o pipefail && xcrun swift build --target PostHog --disable-default-traits --triple arm64-apple-ios15.0 --sdk "$$(xcrun --sdk iphoneos --show-sdk-path)" #iOS

# Compiles the iOS test target with traits off and with each trait on its own, so trait-gated SDK code and
# the tests that use it stay in sync. `make test` runs on macOS, which compiles out the iOS-only code.
buildTestsSpmTraitsIOS:
set -o pipefail && xcrun swift build --build-tests --disable-default-traits --triple arm64-apple-ios15.0-simulator --sdk "$$(xcrun --sdk iphonesimulator --show-sdk-path)" -Xswiftc -DTESTING
set -o pipefail && xcrun swift build --build-tests --traits CrashReporting --triple arm64-apple-ios15.0-simulator --sdk "$$(xcrun --sdk iphonesimulator --show-sdk-path)" -Xswiftc -DTESTING
set -o pipefail && xcrun swift build --build-tests --traits SessionReplay --triple arm64-apple-ios15.0-simulator --sdk "$$(xcrun --sdk iphonesimulator --show-sdk-path)" -Xswiftc -DTESTING
set -o pipefail && xcrun swift build --build-tests --traits Surveys --triple arm64-apple-ios15.0-simulator --sdk "$$(xcrun --sdk iphonesimulator --show-sdk-path)" -Xswiftc -DTESTING

buildExamples: \
buildExamplesPlatforms \
buildExampleXCFramework \
Expand Down Expand Up @@ -203,6 +218,9 @@ testIOSResultParser:
test: testUploadSymbols testIOSResultParser
set -o pipefail && swift test --no-parallel -Xswiftc -DTESTING $(if $(filter),--filter $(filter))

testNoTraits:
set -o pipefail && swift test --no-parallel --disable-default-traits -Xswiftc -DTESTING $(if $(filter),--filter $(filter))

recordEventShapeSnapshots:
UPDATE_EVENT_SHAPE_SNAPSHOTS=1 $(MAKE) test filter=PostHogEventSnapshotTests

Expand Down
21 changes: 18 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ let package = Package(
targets: ["PostHog"]
),
],
traits: [
.trait(
name: "SessionReplay",
description: "Session replay on iOS, including the vendored libwebp. When disabled, session replay is not available."
),
.trait(
name: "CrashReporting",
description: "Captures crashes and uncaught exceptions using the vendored PLCrashReporter. When disabled, only manual captures are sent."
),
.trait(
name: "Surveys",
description: "Shows mobile surveys on iOS. When disabled, surveys are not loaded or shown."
),
.default(enabledTraits: ["SessionReplay", "CrashReporting", "Surveys"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/AliSoftware/OHHTTPStubs.git", from: "9.0.0"),
Expand All @@ -24,8 +39,8 @@ let package = Package(
name: "PostHog",
dependencies: [
"PostHogObjCExceptionSupport",
"phlibwebp",
.target(name: "PHPLCrashReporter", condition: .when(platforms: [.iOS, .macOS, .macCatalyst, .tvOS])),
.target(name: "phlibwebp", condition: .when(traits: ["SessionReplay"])),
.target(name: "PHPLCrashReporter", condition: .when(platforms: [.iOS, .macOS, .macCatalyst, .tvOS], traits: ["CrashReporting"])),
],
path: "PostHog",
exclude: [
Expand Down Expand Up @@ -95,7 +110,7 @@ let package = Package(
"OHHTTPStubs",
.product(name: "OHHTTPStubsSwift", package: "OHHTTPStubs"),
// The crash-report processor tests import this directly to build a PHPLCrashReport.
.target(name: "PHPLCrashReporter", condition: .when(platforms: [.iOS, .macOS, .macCatalyst, .tvOS])),
.target(name: "PHPLCrashReporter", condition: .when(platforms: [.iOS, .macOS, .macCatalyst, .tvOS], traits: ["CrashReporting"])),
],
path: "PostHogTests",
resources: [
Expand Down
2 changes: 1 addition & 1 deletion PostHog/ErrorTracking/PostHogCrashReportProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

#if os(iOS) || os(macOS) || os(tvOS)
#if (os(iOS) || os(macOS) || os(tvOS)) && (!SWIFT_PACKAGE || CrashReporting)
internal import PHPLCrashReporter

enum PostHogCrashReportProcessor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

#if os(iOS) || os(macOS) || os(tvOS)
#if (os(iOS) || os(macOS) || os(tvOS)) && (!SWIFT_PACKAGE || CrashReporting)
internal import PHPLCrashReporter

class PostHogErrorTrackingAutoCaptureIntegration: PostHogIntegration {
Expand Down Expand Up @@ -294,41 +294,45 @@ import Foundation
hedgeLog("Failed to process crash report: \(error)")
}
}

/// Returns `true` if any entry in `properties["$exception_list"]` has a
/// `type` matching one of `ignoredTypes`. Walks the exception list rather
/// than only the outermost entry so a wrapped exception whose underlying
/// cause has an ignored type is still suppressed. Match is
/// case-sensitive and exact. For an `NSException` the type is its `name`,
/// which can embed free text.
static func exceptionListMatchesIgnoredTypes(_ properties: [String: Any], ignoredTypes: [String]) -> Bool {
guard let exceptionList = properties["$exception_list"] as? [[String: Any]] else {
return false
}
let ignored = Set(ignoredTypes)
return exceptionList.contains { entry in
guard let exType = entry["type"] as? String else { return false }
return ignored.contains(exType)
}
}
}

#else
// watchOS/visionOS stub - crash reporting is not available on these platforms
// Stub for watchOS/visionOS, where crash reporting is not available, and for SPM builds
// with the `CrashReporting` trait disabled, where PHPLCrashReporter is not linked.
class PostHogErrorTrackingAutoCaptureIntegration: PostHogIntegration {
static func clearInstalls() { /* no-op */ }

var requiresSwizzling: Bool { false }

func install(_: PostHogSDK) -> PostHogIntegrationInstallResult {
.skipped(.notAvailableOnPlatform)
#if os(iOS) || os(macOS) || os(tvOS)
.skipped(.disabledByPackageTrait)
#else
.skipped(.notAvailableOnPlatform)
#endif
}

func uninstall(_: PostHogSDK) { /* no-op */ }
func start() { /* no-op */ }
func stop() { /* no-op */ }
}
#endif

/// Crash reporting is unavailable on this platform; always returns `false`.
static func exceptionListMatchesIgnoredTypes(_: [String: Any], ignoredTypes _: [String]) -> Bool {
false
extension PostHogErrorTrackingAutoCaptureIntegration {
/// Returns `true` if any entry in `properties["$exception_list"]` has a
/// `type` matching one of `ignoredTypes`. Walks the exception list rather
/// than only the outermost entry so a wrapped exception whose underlying
/// cause has an ignored type is still suppressed. Match is
/// case-sensitive and exact. For an `NSException` the type is its `name`,
/// which can embed free text.
static func exceptionListMatchesIgnoredTypes(_ properties: [String: Any], ignoredTypes: [String]) -> Bool {
guard let exceptionList = properties["$exception_list"] as? [[String: Any]] else {
return false
}
let ignored = Set(ignoredTypes)
return exceptionList.contains { entry in
guard let exType = entry["type"] as? String else { return false }
return ignored.contains(exType)
}
}
#endif
}
1 change: 1 addition & 0 deletions PostHog/ErrorTracking/PostHogErrorTrackingConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import Foundation
/// 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")
Expand Down
7 changes: 7 additions & 0 deletions PostHog/PostHogConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -402,12 +402,16 @@ public typealias BeforeSendBlock = @Sendable (PostHogEvent) -> PostHogEvent?

/// Enable Recording of Session Replays for iOS
///
/// With Swift Package Manager, this requires 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.
///
/// Default: false
@objc public var sessionReplay: Bool = false
/// Session Replay configuration
///
/// With Swift Package Manager, this requires the `SessionReplay` package trait, which is enabled by default.
@objc public let sessionReplayConfig: PostHogSessionReplayConfig = .init()
#endif

Expand All @@ -422,6 +426,8 @@ public typealias BeforeSendBlock = @Sendable (PostHogEvent) -> PostHogEvent?

/// 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.
Expand All @@ -438,6 +444,7 @@ public typealias BeforeSendBlock = @Sendable (PostHogEvent) -> PostHogEvent?
/// 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")
Expand Down
1 change: 1 addition & 0 deletions PostHog/PostHogIntegration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ enum PostHogIntegrationInstallSkipReason: String, CustomStringConvertible {
case alreadyInstalled = "Already installed to another PostHogSDK instance"
case disabledByRemoteConfig = "Disabled in remote config"
case notAvailableOnPlatform = "Not available on this platform"
case disabledByPackageTrait = "Disabled by a package trait"
}

enum PostHogIntegrationInstallResult: Equatable {
Expand Down
2 changes: 1 addition & 1 deletion PostHog/PostHogSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -729,7 +729,7 @@ let maxRetryDelay = 30.0

// SDK-computed debug keys overwrite a same-named registered super property (js: `extend`
// after super properties), so a stale `register()` can't shadow the live status.
#if os(iOS)
#if os(iOS) && (!SWIFT_PACKAGE || SessionReplay)
var replayDebugProperties = replayIntegration?.debugProperties() ?? [
"$recording_status": "disabled",
"$sdk_debug_replay_capture_mode": PostHogReplayIntegration.captureMode(config: config),
Expand Down
2 changes: 1 addition & 1 deletion PostHog/Replay/CGColor+Util.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by Manoel Aranda Neto on 21.03.24.
//

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

import Foundation
import UIKit
Expand Down
2 changes: 1 addition & 1 deletion PostHog/Replay/NetworkSample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by Manoel Aranda Neto on 26.03.24.
//

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

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

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

final class PostHogConsoleLogInterceptor {
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

final class PostHogSessionReplayConsoleLogsPlugin: PostHogSessionReplayPlugin {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by Ioannis Josephides on 28/05/2025.
//

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

/// Session replay plugin that captures network requests using URLSession swizzling.
Expand Down
Loading
Loading