Skip to content

feat: add SessionReplay and CrashReporting package traits - #916

Merged
turnipdabeets merged 8 commits into
chore/v4-remove-deprecatedfrom
feat/spm-package-traits
Oct 9, 2026
Merged

turnipdabeets merged 8 commits into
chore/v4-remove-deprecatedfrom
feat/spm-package-traits

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Every app that adds PostHog through Swift Package Manager compiles and links session replay with the vendored libwebp, and the vendored PLCrashReporter, even when it never turns on replay or crash autocapture. This adds two package traits, both on by default, so an app can leave either one out:

Trait Leaves out When it's off
SessionReplay The replay engine and phlibwebp Session replay never starts. The replay config, masking modifiers and URLSession helpers stay available and do nothing
CrashReporting PHPLCrashReporter No crash or uncaught-exception autocapture. captureException still works
// Keep crash reporting, leave out session replay
.package(url: "https://github.com/PostHog/posthog-ios.git", from: "4.0.0", traits: ["CrashReporting"])

Listing traits replaces the defaults, so an app names the traits it wants to keep. traits: [] turns both off.

A small iOS arm64 Release app that links the SDK dynamically, stripped:

Traits Size Saved
Defaults 2.87 MB
["CrashReporting"] 2.29 MB 575 KB
[] 2.02 MB 848 KB

The traits are on by default, so existing SPM setups don't change. CocoaPods and xcodeproj builds always include both, because the gates are #if !SWIFT_PACKAGE || <Trait>. Traits need swift-tools-version 6.1+, which is why this stacks on the 6.2 bump in #898.

What changes:

  • With SessionReplay off, the iOS replay engine files are compiled out and PostHogReplayIntegration is a stub that is skipped with disabledByPackageTrait. The public replay API stays compiled: config.sessionReplay, sessionReplayConfig, the console log types, the SwiftUI/UIKit masking modifiers, the URLSession helpers, startSessionRecording/stopSessionRecording. Code using it keeps compiling, and isSessionReplayActive() returns false. imageToBase64 (SPI, used by the wrapper SDKs) stays and falls back to JPEG.
  • With CrashReporting off, the autocapture integration becomes the existing watchOS/visionOS stub. It logs skipped: Disabled by a package trait when config.debug is on.
  • The sessionReplay, sessionReplayConfig and autoCapture docs mention their trait.
  • exceptionListMatchesIgnoredTypes moved out of the crash-reporting gate, so ignoredExceptionTypes applies to manual captures in every build. This also fixes it on watchOS and visionOS, where the stub always returned false. It has its own patch changeset.
  • CI: a spm-no-traits build entry (macOS, plus an iOS cross-build via swift build --triple), a test-no-traits job, and a traits-off step in the Xcode 26.0.1 job, through make buildSdkSpmNoTraits and make testNoTraits. The spm-no-traits entry also runs make buildTestsSpmTraitsIOS, which compiles the iOS test target for the simulator with traits off, CrashReporting only and SessionReplay only. To make that compile, the concurrent flag reload suite's iOS 16-only .timeLimit is limited to macOS SwiftPM builds.

Why one SessionReplay trait rather than a WebP-only one: dropping libwebp but keeping replay saves less and makes replay upload bigger JPEG frames. Nobody has asked for that, and libwebp's symbols are already prefixed, so it can't clash with another copy. If it's ever wanted, a minor release can add SessionReplayCore and SessionReplayWebP and keep SessionReplay as an umbrella that enables both. Apps that list SessionReplay keep the same behavior; I checked that with scratch packages. The reverse doesn't hold: once released, a new default-on trait added on its own silently turns its feature off for apps that list traits. AGENTS.md now records that rule.

I considered splitting replay and error tracking into separate modules, as #557 suggests, and rejected it. It would break imports and move config.sessionReplayConfig and config.errorTrackingConfig, and the wrapper SDKs would need changes. Traits get the size win without any of that.

Things to know:

  • Picking traits from an Xcode app project needs Xcode 26.4+, and there's an open report of default traits staying on there. Apps that declare dependencies in a Package.swift work. I only verified that path.
  • SwiftPM turns on a trait if any package in the graph depends on posthog-ios with defaults, so an app can only opt out if everything it depends on does too.
  • Turning off CrashReporting also turns off iOS 27+ out-of-memory autocapture. That reporter uses MetricKit, not PLCrashReporter, but it lives inside the same integration.
  • feat: add Surveys package trait #918 adds a Surveys trait on top of this. Both need to ship in 4.0 for the reason above.
  • The posthog.com install, error tracking and replay docs need a follow-up once this ships.

Closes #301. Part of #557. Stacked on #873.

💚 How did you test it?

On the current commit:

  • make lint passes with the same warnings as the base branch. make apiCheck passes, and the public API snapshot is unchanged.
  • swift build --target PostHog passes for macOS with traits off and with defaults, and for iOS with defaults, CrashReporting only, and traits off.
  • make buildTestsSpmTraitsIOS compiles the iOS test target with traits off, CrashReporting only and SessionReplay only.
  • make testNoTraits filter=IntegrationInstallation passes, including the "skipped when the CrashReporting trait is disabled" test.
  • On the iOS simulator through the xcodeproj, these pass: the replay manual-mode, plugin and event-trigger suites (46 tests), and the WebP, replay image encoding and integration installation suites (22 tests).
  • Scratch app builds for iOS, checked with nm: with ["CrashReporting"] it links no libwebp or replay capture symbols, and with [] it also links no PLCrashReporter symbols. Sizes are in the table above.

On the earlier SessionReplayWebP revision, not rerun since:

  • make buildSdk for iOS, macOS, Mac Catalyst, tvOS, watchOS and visionOS.
  • The full make testOniOSSimulator, make test and make testNoTraits. The only failures were the 13 PostHogStorage app-group tests on macOS, which fail the same way on the base on this machine and pass in the simulator.

Not tested: the replay stub's skip with the trait off. No lane runs the iOS tests through SwiftPM with traits off, so it's compile-checked only, like the JPEG encoding path. Also not tested: Xcode 26.0.1 locally (CI covers it) and choosing traits from an Xcode app project.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Claude Code (Opus 5.5). This started as a spike on the Next iOS major version plan #557 module split, which measured the size cost and prototyped traits against separate modules. The trait was first WebP-only; it became SessionReplay after measuring per-feature code size and confirming that a later split under an umbrella trait stays compatible.
  • An independent review subagent checked that SWIFT_PACKAGE isn't a Swift condition in the xcodeproj or podspec, so non-SPM builds keep both libraries. It also found that the macOS no-traits build still compiled both vendored libraries; that's fixed with --target PostHog. A watchOS test for the ignoredExceptionTypes fix is still deferred; no lane runs it today.

🤖 Generated with Claude Code

@turnipdabeets turnipdabeets self-assigned this Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

posthog-ios Compliance Report

Date: 2026-10-09 15:06:22 UTC
Duration: 233235ms

⚠️ Some Tests Failed

44/45 tests passed, 1 failed


Capture Tests

⚠️ 28/29 tests passed, 1 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 2931ms
Format Validation.Event Has Uuid ✅ 2785ms
Format Validation.Event Has Lib Properties ✅ 464ms
Format Validation.Distinct Id Is String ✅ 2838ms
Format Validation.Token Is Present ✅ 2845ms
Format Validation.Custom Properties Preserved ✅ 2818ms
Format Validation.Event Has Timestamp ✅ 435ms
Retry Behavior.Retries On 503 ✅ 11773ms
Retry Behavior.Does Not Retry On 400 ✅ 4846ms
Retry Behavior.Does Not Retry On 401 ✅ 4859ms
Retry Behavior.Respects Retry After Header ✅ 7758ms
Retry Behavior.Implements Backoff ✅ 21309ms
Retry Behavior.Retries On 500 ✅ 9274ms
Retry Behavior.Retries On 502 ✅ 9329ms
Retry Behavior.Retries On 504 ✅ 9357ms
Retry Behavior.Max Retries Respected ❌ 21811ms
Deduplication.Generates Unique Uuids ✅ 3168ms
Deduplication.Preserves Uuid On Retry ✅ 9465ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 16423ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 8032ms
Deduplication.No Duplicate Events In Batch ✅ 2994ms
Deduplication.Different Events Have Different Uuids ✅ 2955ms
Compression.Sends Gzip When Enabled ✅ 2757ms
Batch Format.Uses Proper Batch Structure ✅ 444ms
Batch Format.Flush With No Events Sends Nothing ✅ 375ms
Batch Format.Multiple Events Batched Together ✅ 3056ms
Error Handling.Does Not Retry On 403 ✅ 5420ms
Error Handling.Does Not Retry On 413 ✅ 4833ms
Error Handling.Retries On 408 ✅ 9400ms

Failures

retry_behavior.max_retries_respected

Expected 4 requests, got 6

Feature_Flags Tests

✅ 16/16 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 2772ms
Request Payload.Flags Request Uses V2 Query Param ✅ 2764ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 2968ms
Request Payload.Flags Request Omits Authorization Header ✅ 3319ms
Request Payload.Token In Flags Body Matches Init ✅ 2915ms
Request Payload.Groups Round Trip ✅ 2922ms
Request Payload.Groups Default To Empty Object ✅ 2826ms
Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It ✅ 2726ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 2877ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 2942ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 2969ms
Request Lifecycle.No Flags Request On Init Alone ✅ 344ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 3274ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 5836ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 2934ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 3101ms

@turnipdabeets
turnipdabeets marked this pull request as ready for review October 6, 2026 22:11
@turnipdabeets
turnipdabeets requested a review from a team as a code owner October 6, 2026 22:11

@dustinbyrne dustinbyrne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default-on traits preserve existing integrations, and the dependency guards, JPEG fallback, and manual exception filtering look consistent. One small test-target guard remains for the new no-WebP iOS configuration.

AI-assisted review.

Comment thread PostHog/Utils/UIImage+WebP.swift Outdated
@dustinbyrne
dustinbyrne requested a review from a team October 6, 2026 22:59
@turnipdabeets

turnipdabeets commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

@ioannisj curious what you think about this approach vs modules? Also addeds a Survey trait in #918

@turnipdabeets turnipdabeets mentioned this pull request Oct 7, 2026
5 tasks done
@turnipdabeets
turnipdabeets force-pushed the chore/v4-remove-deprecated branch from e1cc8cf to 5b6c50e Compare October 7, 2026 01:44
turnipdabeets and others added 3 commits October 6, 2026 21:45
SwiftPM apps can now leave out the vendored libwebp and PLCrashReporter
by disabling the traits. Both are on by default, so existing setups are
unchanged; CocoaPods and xcodeproj builds always include both.

Also moves the ignoredExceptionTypes matcher out of the crash-reporting
gate so it applies to manual captures everywhere, which fixes it on
watchOS and visionOS.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The WebP test suites called `UIImage.webpData`, which SwiftPM compiles out on
iOS when the trait is off, so the test target failed to build. Gate them like
the encoder and add a separate encoding test that expects WebP or the JPEG
fallback depending on the trait.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`make test` runs on macOS, where the iOS-only trait gates compile out, so a
test calling trait-gated SDK code on iOS went unnoticed. Build the iOS test
target for the simulator with traits off and with each trait on its own.

The concurrent flag reload suite's `.timeLimit` needs iOS 16, so limit it to
macOS SwiftPM builds rather than every SwiftPM build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@turnipdabeets
turnipdabeets force-pushed the feat/spm-package-traits branch from 6823d45 to 8641221 Compare October 7, 2026 02:05
SessionReplay now gates the whole replay engine along with libwebp, so an
app that doesn't use replay can leave all of it out. The public replay API
(config, masking modifiers, URLSession helpers, start/stop recording) stays
compiled; with the trait off the replay integration is a stub that is
skipped with `disabledByPackageTrait`.

A finer split (replay without WebP) can come later in a minor release by
keeping SessionReplay as an umbrella trait that enables the new ones.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@turnipdabeets turnipdabeets changed the title feat: add SessionReplayWebP and CrashReporting package traits feat: add SessionReplay and CrashReporting package traits Oct 7, 2026
turnipdabeets and others added 4 commits October 8, 2026 09:58
The session replay suite and the two SDK tests that assert replay debug
properties need the replay engine, so skip them in SPM builds with the
SessionReplay trait off. Also list buildTestsSpmTraitsIOS for trait-gated
changes in the contributor guide.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`.timeLimit` needs iOS 16, above the iOS 15 minimum, so the iOS SwiftPM
test build failed after merging the 4.0 base.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat: add Surveys package trait

SwiftPM apps that don't use surveys can now leave out the survey engine and
UI by disabling the `Surveys` trait. It is on by default, so existing setups
are unchanged; CocoaPods and xcodeproj builds always include surveys.

The public survey API (`config.surveys`, `surveysConfig`, the delegate
protocol and display models) stays compiled. With the trait off, the survey
integration is a stub that is skipped with `disabledByPackageTrait`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: note the Surveys trait on surveysConfig and surveysDelegate

A custom surveysDelegate is never called when the trait is off, so say so
where apps set it. Also drop the unused clearInstalls() from the stub.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@turnipdabeets
turnipdabeets merged commit 1383528 into chore/v4-remove-deprecated Oct 9, 2026
24 checks passed
@turnipdabeets
turnipdabeets deleted the feat/spm-package-traits branch October 9, 2026 14:51
turnipdabeets added a commit that referenced this pull request Oct 9, 2026
* feat: add SessionReplayWebP and CrashReporting package traits

SwiftPM apps can now leave out the vendored libwebp and PLCrashReporter
by disabling the traits. Both are on by default, so existing setups are
unchanged; CocoaPods and xcodeproj builds always include both.

Also moves the ignoredExceptionTypes matcher out of the crash-reporting
gate so it applies to manual captures everywhere, which fixes it on
watchOS and visionOS.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: gate WebP suites on the SessionReplayWebP trait

The WebP test suites called `UIImage.webpData`, which SwiftPM compiles out on
iOS when the trait is off, so the test target failed to build. Gate them like
the encoder and add a separate encoding test that expects WebP or the JPEG
fallback depending on the trait.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci: compile the iOS test target with package traits off

`make test` runs on macOS, where the iOS-only trait gates compile out, so a
test calling trait-gated SDK code on iOS went unnoticed. Build the iOS test
target for the simulator with traits off and with each trait on its own.

The concurrent flag reload suite's `.timeLimit` needs iOS 16, so limit it to
macOS SwiftPM builds rather than every SwiftPM build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat!: replace the SessionReplayWebP trait with SessionReplay

SessionReplay now gates the whole replay engine along with libwebp, so an
app that doesn't use replay can leave all of it out. The public replay API
(config, masking modifiers, URLSession helpers, start/stop recording) stays
compiled; with the trait off the replay integration is a stub that is
skipped with `disabledByPackageTrait`.

A finer split (replay without WebP) can come later in a minor release by
keeping SessionReplay as an umbrella trait that enables the new ones.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: gate the remaining replay tests on the SessionReplay trait

The session replay suite and the two SDK tests that assert replay debug
properties need the replay engine, so skip them in SPM builds with the
SessionReplay trait off. Also list buildTestsSpmTraitsIOS for trait-gated
changes in the contributor guide.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: limit the Swift 6 callback suite's timeLimit to macOS

`.timeLimit` needs iOS 16, above the iOS 15 minimum, so the iOS SwiftPM
test build failed after merging the 4.0 base.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: add Surveys package trait (#918)

* feat: add Surveys package trait

SwiftPM apps that don't use surveys can now leave out the survey engine and
UI by disabling the `Surveys` trait. It is on by default, so existing setups
are unchanged; CocoaPods and xcodeproj builds always include surveys.

The public survey API (`config.surveys`, `surveysConfig`, the delegate
protocol and display models) stays compiled. With the trait off, the survey
integration is a stub that is skipped with `disabledByPackageTrait`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: note the Surveys trait on surveysConfig and surveysDelegate

A custom surveysDelegate is never called when the trait is off, so say so
where apps set it. Also drop the unused clearInstalls() from the stub.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants