Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2128e35
chore!: remove deprecated public APIs for v4
turnipdabeets Sep 30, 2026
ee6a269
docs: add Objective-C migration steps to the deprecated-API removal c…
turnipdabeets Oct 5, 2026
16d21ba
chore!: raise minimum iOS deployment target to 15 (#876)
turnipdabeets Oct 5, 2026
f0f0c13
chore!: hide wrapper-only APIs behind SPI for v4 (#874)
turnipdabeets Oct 5, 2026
ac052e8
chore!: remove PostHogDataMode.cellular for v4 (#890)
turnipdabeets Oct 5, 2026
75ba498
chore!: require Xcode 26 (swift-tools-version 6.2) (#898)
turnipdabeets Oct 5, 2026
46e234a
chore!: raise minimum macOS to 11, tvOS to 15 and watchOS to 10 (#906)
turnipdabeets Oct 6, 2026
e79c766
chore!: rename console log types to PostHogConsoleLog* for v4 (#912)
turnipdabeets Oct 6, 2026
ec8a10d
chore!: remove the v2 legacy queue migration for v4 (#875)
turnipdabeets Oct 6, 2026
b6e730d
chore: update text and names left stale by the 4.0 changes (#910)
turnipdabeets Oct 6, 2026
2f20561
chore!: drop legacy version and build from lifecycle events (#905)
turnipdabeets Oct 7, 2026
8695aa1
chore!: pass the reload result to reloadFeatureFlags' callback (#913)
turnipdabeets Oct 7, 2026
cbae82e
chore!: return nil for malformed feature flag payloads (#903)
turnipdabeets Oct 7, 2026
0351808
chore!: replace vendored Reachability with NWPathMonitor (#909)
turnipdabeets Oct 7, 2026
0478ab7
feat: add SessionReplay and CrashReporting package traits (#916)
turnipdabeets Oct 9, 2026
e18239e
chore!: remove wireframe session replay capture (#907)
turnipdabeets Oct 9, 2026
6d30a36
chore!: remove experimental screenshotModeBackgroundCapture (#937)
turnipdabeets Oct 10, 2026
56860b3
test: gate NetworkSampleTest on the SessionReplay trait
turnipdabeets Oct 20, 2026
1fb0c40
chore: combine the 4.0 changesets into one release entry
turnipdabeets Oct 26, 2026
e512d24
test(replay): gate the flattened-content mask tests on the SessionRep…
turnipdabeets Oct 27, 2026
ef52cf1
chore: drop the ignoredExceptionTypes fix already released in 3.92.0 …
turnipdabeets Oct 27, 2026
cf12d63
chore!: remove the didReceiveFeatureFlags notification (#965)
turnipdabeets Oct 27, 2026
e5fed1c
chore: fold the didReceiveFeatureFlags changeset into the 4.0 release…
turnipdabeets Oct 27, 2026
f48401a
chore!: remove PostHogSessionReplayConfig.screenshotMode (#973)
turnipdabeets Oct 27, 2026
9a89c32
fix: accept plain closure arrays in setBeforeSend without a runtime t…
turnipdabeets Oct 27, 2026
732dc7d
chore!: remove the public API of disabled package traits (#975)
turnipdabeets Oct 27, 2026
54a392b
chore!: publish 4.0 to Swift Package Manager only (#972)
turnipdabeets Oct 27, 2026
d8bcdd9
docs: add a 3.x to 4.0 migration guide (#978)
turnipdabeets Oct 27, 2026
3c0c8eb
docs: drop the full-window ph-no-capture exception from the migration…
turnipdabeets Oct 27, 2026
75dc8f8
chore: update the no-traits API snapshot for the 4.0 API changes (#981)
turnipdabeets Oct 28, 2026
4c67719
Merge remote-tracking branch 'origin/main' into chore/v4-remove-depre…
turnipdabeets Oct 29, 2026
75e96d9
Merge remote-tracking branch 'origin/main' into chore/v4-remove-depre…
turnipdabeets Oct 30, 2026
5a16a00
fix(replay): mask content drawn outside a view's bounds (#976)
turnipdabeets Oct 30, 2026
45cbdd9
docs: consolidate 4.0 changeset and migration guide
turnipdabeets Oct 30, 2026
7b0efad
Merge remote-tracking branch 'origin/main' into audit/v4-remove-depre…
turnipdabeets Oct 31, 2026
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
32 changes: 32 additions & 0 deletions .changeset/v4-major-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
'posthog-ios': major
---

PostHog iOS 4.0: see the [migration guide](https://github.com/PostHog/posthog-ios/blob/main/MIGRATION.md) to upgrade from 3.x.

- **Breaking:** stop publishing to CocoaPods — install with Swift Package Manager (3.x versions stay on CocoaPods)
- **Breaking:** raise minimum deployment targets to iOS and Mac Catalyst 15.0, macOS 11.0, tvOS 15.0 and watchOS 10.0
- **Breaking:** require Xcode 26 or later (`swift-tools-version` 6.2)
- **Breaking:** always record session replay as masked screenshots, which shows custom-drawn content and uses more bandwidth and CPU than wireframes — remove `screenshotMode` and `screenshotModeBackgroundCapture` assignments and mask sensitive content with `postHogMask()` or `ph-no-capture`
- **Breaking:** pass a `PostHogFeatureFlagsLoaded` to the `reloadFeatureFlags(_:)` callback — use `{ result in ... }` or `{ _ in ... }`
- **Breaking:** mark `reloadFeatureFlags(_:)`, `setBeforeSend(_:)`, `getAnonymousId`, `pushIdentityProvider` and `logSanitizer` callbacks `@Sendable`, fixing a crash in Swift 6 apps
- **Breaking:** rename `PostHogLogLevel` to `PostHogConsoleLogLevel` and `PostHogLogEntry` to `PostHogConsoleLogEntry`
- **Breaking:** return `nil` instead of the raw string for a malformed, empty or whitespace-only feature flag payload
- **Breaking:** stop decoding string payloads in `PostHogBootstrapConfig.featureFlagPayloads` a second time, so a bootstrapped `"123"` stays a `String`
- **Breaking:** delete events still queued on disk by a 2.x SDK instead of sending them, and ignore a top-level `$set` and `message_id` in queued events
- **Breaking:** remove `version` and `build` from `Application Installed`, `Application Updated` and `Application Opened` events — use `$app_version` and `$app_build`
- **Breaking:** remove `PostHogConfig.apiKey`, `PostHogConfig(apiKey:)` and `PostHogConfig(apiKey:host:)` — use `projectToken`, `PostHogConfig(projectToken:)` and `PostHogConfig(projectToken:host:)`
- **Breaking:** remove `PostHogConfig.propertiesSanitizer` and `PostHogPropertiesSanitizer` — use `setBeforeSend(_:)`
- **Breaking:** remove `PostHogConfig.evaluationEnvironments` — use `evaluationContexts`
- **Breaking:** remove the no-op `PostHogConfig.remoteConfig` and `PostHogSessionReplayConfig.maskPhotoLibraryImages` options
- **Breaking:** remove `PostHogSDK.getFeatureFlagPayload(_:)` — use `getFeatureFlagResult(_:sendFeatureFlagEvent:)?.payload`
- **Breaking:** remove `PostHogSessionReplayConfig.debouncerDelay` — use `throttleDelay`
- **Breaking:** remove the `PostHogSDK.didReceiveFeatureFlags` notification — use `onFeatureFlags(_:)`
- **Breaking:** remove `PostHogDataMode.cellular` — use `.any`
- **Breaking:** move `postHogSdkName`, `postHogVersion`, `PostHogConfig.snapshotEndpoint`, `PostHogSessionManager`, `PostHogStorageManager` and `PostHogConfig.storageManager` behind `@_spi(PostHogInternal)` — use `PostHogSDK` session and identity methods
- **Breaking:** remove `dateToMillis(_:)`, `imageToBase64(_:_:)`, `toISO8601String(_:)`, `toISO8601Date(_:)`, `sanitizeDictionary(_:)`, `deleteSafely(_:)`, `postHogiOSSdkName`, `Gzip`, `GzipError`, `CompressionLevel`, `ReachabilityError`, `ReachabilityChangedNotification`, `UIColor.init(hex:)` and `UIColor.hexDescription(_:)` from the public API
- Add `SessionReplay`, `CrashReporting` and `Surveys` Swift package traits (on by default) that remove the feature and its public API when turned off
- Change network reachability detection to `NWPathMonitor`, so a path needing user action counts as reachable, queues pause while cellular data is off for the app, and early events may omit `$network_wifi` and `$network_cellular`
- Change the replay screen name to the visible screen, reporting the view name for SwiftUI screens and no longer using a plain `UIViewController`'s `title`
- Fix session replay recording nothing for apps whose window root is a SwiftUI view
- Fix secure-entry text fields being unmasked inside a `postHogNoMask()` or `ph-no-mask` subtree
31 changes: 0 additions & 31 deletions .github/workflows/build-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,34 +58,3 @@ jobs:
xcode-version: latest-stable
- name: Build XCFramework Example
run: make buildExampleXCFramework

build-pods:
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- name: Static Lib
target: buildExamplePodsStaticLib
runner: macos-15
xcode-version: latest-stable
- name: Static Lib (Xcode 16.2)
target: buildExamplePodsStaticLib
runner: macos-14-xlarge
xcode-version: '16.2'
- name: Static Framework
target: buildExamplePodsStaticFramework
runner: macos-15
xcode-version: latest-stable
- name: Dynamic Framework
target: buildExamplePodsDynamicFramework
runner: macos-15
xcode-version: latest-stable
name: Example Pods (${{ matrix.name }})
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: ${{ matrix.xcode-version }}
- name: Build Pods Example (${{ matrix.name }})
run: make ${{ matrix.target }}
47 changes: 11 additions & 36 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 @@ -62,13 +64,13 @@ 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-swift53-compat:
build-sdk-min-toolchain:
needs: detect-markdown-only
runs-on: macos-14-xlarge
name: Build SDK (Swift 5 compatibility on Xcode 15.4 with CocoaPods)
runs-on: macos-15-xlarge
name: Build SDK (minimum toolchain, Xcode 26.0.1)
steps:
- name: Complete markdown-only PR check
if: needs.detect-markdown-only.outputs.markdown_only == 'true'
Expand All @@ -78,42 +80,15 @@ jobs:
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
with:
xcode-version: '15.4'
xcode-version: '26.0.1'
- name: Print toolchain versions
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
run: |
xcodebuild -version
xcrun swift --version
- name: Install CocoaPods
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
run: gem install cocoapods
- name: Install Pods
- name: Build SDK via Swift Package Manager
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
run: |
cd PostHogExampleWithPods
USE_FRAMEWORKS=static pod install
- name: Build SDK with Swift 5 language mode via CocoaPods
run: make buildSdkSpm
- name: Build SDK via Swift Package Manager with package traits off
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
run: |
set -o pipefail
cd PostHogExampleWithPods
xcrun xcodebuild clean build \
-workspace PostHogExampleWithPods.xcworkspace \
-scheme PostHogExampleWithPods \
-destination generic/platform=ios \
SWIFT_VERSION=5 | xcpretty

podspec-lint:
needs: detect-markdown-only
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
runs-on: macos-15
name: CocoaPods package lint
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: latest-stable
- name: Install CocoaPods
run: gem install cocoapods
- name: Lint podspec
run: pod lib lint PostHog.podspec --allow-warnings --skip-tests
run: make buildSdkSpmNoTraits
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: macos-14-xlarge
runs-on: macos-15-xlarge
permissions:
security-events: write # for uploading CodeQL results
packages: read
Expand Down
94 changes: 2 additions & 92 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,6 @@ jobs:
echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
echo "Version to release: $NEW_VERSION"

# Tag must be pushed to remote before CocoaPods publish, because
# `pod trunk push` validates by cloning the repo at this tag from GitHub.
- name: Tag and push to GitHub
env:
RELEASE_VERSION: ${{ steps.get-version.outputs.version }}
Expand Down Expand Up @@ -294,99 +292,11 @@ jobs:
message: '❌ Failed to create the GitHub release for `posthog-ios@v${{ steps.get-version.outputs.version }}`! <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>'
emoji_reaction: 'x'

publish-cocoapods:
name: Publish to CocoaPods
needs: [create-github-release, notify-approval-needed]
runs-on: macos-14
if: always() && needs.create-github-release.result == 'success'
permissions:
contents: read
env:
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} # Using @ioannisj token
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.create-github-release.outputs.version }}
fetch-depth: 0

- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: '16.2'

- name: Publish to CocoaPods
env:
RELEASE_VERSION: ${{ needs.create-github-release.outputs.version }}
run: |
set -euo pipefail

on_trunk() {
pod trunk info PostHog --no-ansi 2>/dev/null \
| grep -E -- "^[[:space:]]*- ${RELEASE_VERSION//./\\.} \("
}

if make releaseCocoaPods; then
exit 0
fi

# `pod trunk push` regularly exits non-zero *after* the spec is already
# accepted (its final CDN/response verification times out). Trunk
# registration and the `pod trunk info` endpoint can lag a bit and the
# info call itself is occasionally flaky, so poll rather than check once.
echo "::warning::pod trunk push failed. Polling pod trunk info to confirm the version landed..."
for attempt in $(seq 1 10); do
if on_trunk; then
echo "PostHog $RELEASE_VERSION is on trunk (attempt $attempt/10). Treating as success."
exit 0
fi
if [ "$attempt" -lt 10 ]; then
echo "Not visible yet (attempt $attempt/10); retrying in 30s..."
sleep 30
fi
done

echo "::error::PostHog $RELEASE_VERSION is not on trunk after polling for ~5 minutes."
exit 1

# Notify in case of failure
- name: Send failure event to PostHog
if: ${{ failure() }}
continue-on-error: true
uses: PostHog/posthog-github-action@9a6a19d360820ab4d95ecc4a5a7564062c895f84 # v1.3.0
with:
posthog-token: '${{ secrets.POSTHOG_PROJECT_API_KEY }}'
event: 'posthog-sdk-github-release-workflow-failure'
properties: >-
{
"commitSha": "${{ github.sha }}",
"jobStatus": "${{ job.status }}",
"ref": "${{ github.ref }}",
"repository": "${{ github.repository }}",
"sdk": "posthog-ios",
"jobName": "publish-cocoapods",
"packageName": "posthog-ios",
"packageVersion": "${{ needs.create-github-release.outputs.version || 'unknown' }}",
"actionUrl": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
"alertText": ":rotating_light: Failed to publish posthog-ios@v${{ needs.create-github-release.outputs.version }} to CocoaPods :rotating_light:",
"alertContext": "The CocoaPods publish step failed."
}

- name: Notify Slack - Failed
continue-on-error: true
if: ${{ failure() && needs.notify-approval-needed.outputs.slack_ts != '' }}
uses: PostHog/.github/.github/actions/slack-thread-reply@cb0979b67dcd585828b61ac45927eef4da8f6287 # main
with:
slack_bot_token: ${{ secrets.SLACK_CLIENT_LIBRARIES_BOT_TOKEN }}
slack_channel_id: ${{ vars.SLACK_APPROVALS_CLIENT_LIBRARIES_CHANNEL_ID }}
thread_ts: ${{ needs.notify-approval-needed.outputs.slack_ts }}
message: '❌ Failed to publish `posthog-ios@v${{ needs.create-github-release.outputs.version }}` to CocoaPods! <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>'
emoji_reaction: 'x'

notify-released:
name: Notify Slack - Released
needs: [notify-approval-needed, publish-cocoapods]
needs: [notify-approval-needed, create-github-release]
runs-on: ubuntu-latest
if: always() && needs.publish-cocoapods.result == 'success' && needs.notify-approval-needed.outputs.slack_ts != ''
if: always() && needs.create-github-release.result == 'success' && needs.notify-approval-needed.outputs.slack_ts != ''
steps:
- name: Notify Slack - Released
continue-on-error: true
Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,35 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: make checkTestRegistration

# 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
1 change: 0 additions & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ excluded: # case-sensitive paths to ignore during linting. Takes precedence over
- PostHogExampleWatchOS
- PostHogExampleWidget
- PostHogExampleWidgetAppGroup
- PostHogExampleWithPods
- PostHogExampleWithSPM
- PostHogTests
- PostHogSurveyUITests
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
- SDK entry point: [PostHog/PostHogSDK.swift](PostHog/PostHogSDK.swift); tests: `PostHogTests/`; replay/privacy: `PostHog/Replay/` and `PostHog/Resources/`.

## Repository invariants
- Maintain Swift 5 SDK language compatibility. Tests require Swift 6+ with Swift Testing (Xcode 16+); prefer Swift Testing for new tests. Use `PostHogTests/TestUtils/MockPostHogServer.swift` for HTTP stubbing.
- Preserve deployment minima: iOS/tvOS 13, macOS 10.15, watchOS 6, 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 both `Package.swift` and `Package@swift-5.9.swift`, plus CocoaPods/Xcode packaging/build configuration; visionOS declarations belong in the Swift 5.9 manifest.
- Maintain Swift 5 SDK language mode. The package needs swift-tools-version 6.2 (Xcode 26+); prefer Swift Testing for new tests. Use `PostHogTests/TestUtils/MockPostHogServer.swift` for HTTP stubbing.
- 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 the Xcode project 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
Loading
Loading