Skip to content

feat: add Surveys package trait - #918

Merged
turnipdabeets merged 4 commits into
feat/spm-package-traitsfrom
feat/spm-surveys-trait
Oct 9, 2026
Merged

turnipdabeets merged 4 commits into
feat/spm-package-traitsfrom
feat/spm-surveys-trait

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Surveys are the largest feature in the SDK, and every iOS app that adds PostHog through Swift Package Manager compiles and links the survey engine and its SwiftUI UI, even when it never turns surveys on. This adds a Surveys package trait, on by default, so an app can leave surveys out:

Trait Leaves out When it's off
Surveys The survey engine (loading, matching, event triggers, progress, translations) and the built-in survey UI Surveys are not loaded or shown. config.surveys has no effect
.package(url: "https://github.com/PostHog/posthog-ios.git", from: "4.0.0", traits: ["SessionReplay", "CrashReporting"])

On an iOS arm64 Release build of a small app that links the SDK dynamically, the stripped binary (strip -x) is 2.87 MB with every trait on and 2.23 MB with only Surveys off, about 636 KB less. With every trait off it is 1.35 MB.

This builds on the trait setup from #916: the same #if !SWIFT_PACKAGE || <Trait> gates, so CocoaPods and xcodeproj builds always include surveys, and the same .skipped(.disabledByPackageTrait) result for an integration whose trait is off.

What changes:

  • The survey engine and UI files under PostHog/Surveys are gated on the trait. The public API stays compiled with the trait off: config.surveys, config.surveysConfig, PostHogSurveysDelegate and the display models. Code that sets these keeps compiling when an app turns the trait off.
  • With the trait off on iOS, PostHogSurveyIntegration is a stub that is skipped with disabledByPackageTrait (logged when config.debug is on), and PostHogSurveysDefaultDelegate renders nothing. The docs on config.surveys, config.surveysConfig and surveysDelegate say they need the trait.
  • Survey tests are gated on the trait. Most of the diff in PostHogSurveysTest.swift, PostHogSurveyEventsTest.swift, PostHogSurveySharedStorageTest.swift and StoredSurveyResponseTest.swift is re-indentation from the new file-level #if, which --ifdef indent requires. Hide whitespace to review them; each one changes by two lines.
  • make buildTestsSpmTraitsIOS now also compiles the iOS test target with only Surveys on.

One trait for all of surveys, rather than a separate trait for the built-in UI only, keeps it simple. The trade-off is that an app rendering surveys through its own surveysDelegate has to keep the trait on.

Things to know:

  • A few unused survey pieces still compile with the trait off: the survey JSON models and some small helpers. They were already compiled, unused, on macOS.
  • The posthog.com install and surveys docs need a follow-up once this ships, along with the feat: add SessionReplay and CrashReporting package traits #916 traits.

Part of #557. Stacked on #916.

💚 How did you test it?

  • make lint passes, with the same warnings as the base branch. make apiCheck passes, and the public API snapshot is unchanged.
  • make buildTestsSpmTraitsIOS compiles the iOS test target with traits off, and with CrashReporting, SessionReplay or Surveys on its own. Its first run caught three iOS-only test files that still referenced gated survey code.
  • swift build --target PostHog passes on macOS with traits off, and on iOS with traits off, defaults, and Surveys only. A clean iOS release build with traits off compiles no survey UI symbols.
  • make test filter=Survey passes: 168 tests. make testNoTraits filter=IntegrationInstallation passes: 8 tests. On macOS with traits off the survey tests are compiled out.
  • Survey and integration-installation tests pass on the iOS simulator through the xcodeproj: 152 Swift Testing tests.
  • With only Surveys off (--traits SessionReplay,CrashReporting), the library and tests build for macOS and the iOS simulator.
  • A scratch app depending on the package with traits: ["SessionReplay", "CrashReporting"] builds for iOS and links no survey engine or UI symbols (SurveysWindow, SurveySheet, SurveyDisplayController, matching). The public survey API and models are still linked.
  • An iOS 26.4 simulator app built with traits: [] launches, logs Integration PostHogSurveyIntegration skipped: Disabled by a package trait, and still sends captured events. Its binary has no survey UI symbols.
  • Not tested: the new "surveys integration is skipped when the Surveys trait is disabled" test only compiles. No lane runs the iOS tests through SwiftPM with traits off. The full make test and make testNoTraits suites were not run.

📝 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). Follow-up to feat: add SessionReplay and CrashReporting package traits #916, after measuring the SDK's own code by area: surveys are the largest feature, at about a quarter of the SDK's compiled Swift code.
  • A pre-PR review by an independent subagent found no blocking issues and no spec deviations. The surveys spec says disabled surveys no-op. It led to the doc notes on surveysConfig/surveysDelegate and to removing an unused stub method. Deferred: survey model decoding tests are also compiled out with the trait off. They still run with the default traits.

🤖 Generated with Claude Code

@turnipdabeets
turnipdabeets marked this pull request as ready for review October 7, 2026 01:26
@turnipdabeets
turnipdabeets requested a review from a team as a code owner October 7, 2026 01:26
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

posthog-ios Compliance Report

Date: 2026-10-08 14:31:26 UTC
Duration: 225783ms

⚠️ 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 ✅ 2724ms
Format Validation.Event Has Uuid ✅ 2794ms
Format Validation.Event Has Lib Properties ✅ 3053ms
Format Validation.Distinct Id Is String ✅ 2812ms
Format Validation.Token Is Present ✅ 2750ms
Format Validation.Custom Properties Preserved ✅ 2711ms
Format Validation.Event Has Timestamp ✅ 2797ms
Retry Behavior.Retries On 503 ✅ 11895ms
Retry Behavior.Does Not Retry On 400 ✅ 2415ms
Retry Behavior.Does Not Retry On 401 ✅ 4695ms
Retry Behavior.Respects Retry After Header ✅ 7767ms
Retry Behavior.Implements Backoff ✅ 21731ms
Retry Behavior.Retries On 500 ✅ 9268ms
Retry Behavior.Retries On 502 ✅ 9278ms
Retry Behavior.Retries On 504 ✅ 8826ms
Retry Behavior.Max Retries Respected ❌ 21715ms
Deduplication.Generates Unique Uuids ✅ 2987ms
Deduplication.Preserves Uuid On Retry ✅ 5420ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 16302ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7838ms
Deduplication.No Duplicate Events In Batch ✅ 2891ms
Deduplication.Different Events Have Different Uuids ✅ 2836ms
Compression.Sends Gzip When Enabled ✅ 2731ms
Batch Format.Uses Proper Batch Structure ✅ 2746ms
Batch Format.Flush With No Events Sends Nothing ✅ 250ms
Batch Format.Multiple Events Batched Together ✅ 2977ms
Error Handling.Does Not Retry On 403 ✅ 2257ms
Error Handling.Does Not Retry On 413 ✅ 4778ms
Error Handling.Retries On 408 ✅ 9324ms

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 ✅ 2807ms
Request Payload.Flags Request Uses V2 Query Param ✅ 2699ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 2825ms
Request Payload.Flags Request Omits Authorization Header ✅ 2709ms
Request Payload.Token In Flags Body Matches Init ✅ 2674ms
Request Payload.Groups Round Trip ✅ 2825ms
Request Payload.Groups Default To Empty Object ✅ 2813ms
Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It ✅ 2677ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 2700ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 2824ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 2819ms
Request Lifecycle.No Flags Request On Init Alone ✅ 67ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 2803ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 5417ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 2814ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 3045ms

turnipdabeets and others added 2 commits October 6, 2026 22:21
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>
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>

@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 Surveys trait preserves existing public declarations and default behavior, and the disabled integration follows the SDK’s existing skip semantics. No issues found.

AI-assisted review.

turnipdabeets and others added 2 commits October 8, 2026 09:58
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@turnipdabeets
turnipdabeets merged commit 8ec90cb into feat/spm-package-traits Oct 9, 2026
46 checks passed
@turnipdabeets
turnipdabeets deleted the feat/spm-surveys-trait 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>
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>
turnipdabeets added a commit that referenced this pull request Oct 20, 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