Skip to content

feat(surveys): add displaySurvey to show a survey on demand#495

Draft
bs1180 wants to merge 2 commits into
mainfrom
posthog-code/display-survey-manual-api
Draft

feat(surveys): add displaySurvey to show a survey on demand#495
bs1180 wants to merge 2 commits into
mainfrom
posthog-code/display-survey-manual-api

Conversation

@bs1180

@bs1180 bs1180 commented Jul 20, 2026

Copy link
Copy Markdown

💡 Motivation and Context

Closes #225

The web SDK exposes posthog.displaySurvey() to display a survey on demand (see implementing custom surveys), but the Flutter SDK has no way to trigger a survey manually — surveys only appear when the automatic display conditions are met, and API-type surveys can't be shown at all. This adds:

await Posthog().displaySurvey('survey-id');

⚠️ Depends on unreleased native SDKs. The native dependency minimums are bumped to posthog-android 3.56.0 and posthog-ios 3.67.0, which don't exist yet — Android/iOS CI jobs will fail until PostHog/posthog-android#643 and PostHog/posthog-ios#730 are merged and released. Keeping this as a draft until then.

💚 How did you test it?

  • Added tests: Posthog().displaySurvey forwarding to the platform interface (posthog_test.dart) and the method-channel payload (posthog_flutter_io_test.dart). flutter test passes locally (224 tests).
  • flutter analyze and dart format are clean; the public API snapshot was regenerated with scripts/check-api-dart.sh --update; ktlint and swiftformat pass on the changed plugin files.
  • The native plugin code paths (Kotlin/Swift) compile only against the unreleased native SDKs, so they're validated by inspection + the native PRs' own tests until the releases land.

📝 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)

Authored with PostHog Code (Claude Code) from a request to bring the JS SDK's displaySurvey to the Flutter SDK. Since Flutter delegates survey eligibility/state to the native SDKs and only renders in Dart, the API was implemented natively first (sibling PRs above) and this PR wires Dart → method channel → native. Scoped v1 to a single surveyId parameter (no ignoreConditions/initialResponses options); Web passes displayType: 'popover' since inline rendering has no Flutter equivalent.


Created with PostHog Code

Adds Posthog().displaySurvey(surveyId), the Flutter counterpart of the web SDK's posthog.displaySurvey(). On Android and iOS the call routes through the new native displaySurvey APIs, which bypass display conditions and render through the existing Flutter survey UI; on Web it calls the JS SDK's displaySurvey directly. Bumps the native SDK minimums to the versions that will ship the API (posthog-android 3.56.0, posthog-ios 3.67.0).

Generated-By: PostHog Code
Task-Id: 96def33a-2460-42b7-9690-0a75ae8383fe
@bs1180
bs1180 requested review from ioannisj and turnipdabeets July 20, 2026 13:25

bs1180 commented Jul 20, 2026

Copy link
Copy Markdown
Author

Hi @ioannisj @turnipdabeets 👋 — this closes #225 (manual displaySurvey for Flutter). It builds on the native APIs in PostHog/posthog-android#643 and PostHog/posthog-ios#730, so it should stay in draft until those are merged and released (the bumped native minimums 3.56.0/3.67.0 don't exist yet, so Android/iOS CI will fail here for now). Dart tests, analyzer, formatting, and the public API snapshot are all green locally. No rush — thanks for taking a look!

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

posthog-flutter Compliance Report

Date: 2026-07-20 18:31:53 UTC
Duration: 96734ms

✅ All Tests Passed!

45/45 tests passed


Capture Tests

29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields 135ms
Format Validation.Event Has Uuid 117ms
Format Validation.Event Has Lib Properties 116ms
Format Validation.Distinct Id Is String 116ms
Format Validation.Token Is Present 114ms
Format Validation.Custom Properties Preserved 117ms
Format Validation.Event Has Timestamp 116ms
Retry Behavior.Retries On 503 5332ms
Retry Behavior.Does Not Retry On 400 2118ms
Retry Behavior.Does Not Retry On 401 2116ms
Retry Behavior.Respects Retry After Header 8125ms
Retry Behavior.Implements Backoff 15446ms
Retry Behavior.Retries On 500 5226ms
Retry Behavior.Retries On 502 5221ms
Retry Behavior.Retries On 504 5224ms
Retry Behavior.Max Retries Respected 15445ms
Deduplication.Generates Unique Uuids 123ms
Deduplication.Preserves Uuid On Retry 5225ms
Deduplication.Preserves Uuid And Timestamp On Retry 10335ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 5231ms
Deduplication.No Duplicate Events In Batch 123ms
Deduplication.Different Events Have Different Uuids 117ms
Compression.Sends Gzip When Enabled 115ms
Batch Format.Uses Proper Batch Structure 114ms
Batch Format.Flush With No Events Sends Nothing 109ms
Batch Format.Multiple Events Batched Together 123ms
Error Handling.Does Not Retry On 403 2116ms
Error Handling.Does Not Retry On 413 2118ms
Error Handling.Retries On 408 5221ms

Feature_Flags Tests

16/16 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id 13ms
Request Payload.Flags Request Uses V2 Query Param 9ms
Request Payload.Flags Request Hits Flags Path Not Decide 9ms
Request Payload.Flags Request Omits Authorization Header 8ms
Request Payload.Token In Flags Body Matches Init 8ms
Request Payload.Groups Round Trip 9ms
Request Payload.Groups Default To Empty Object 9ms
Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It 8ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False 10ms
Request Payload.Disable Geoip Omitted Defaults To False 8ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key 9ms
Request Lifecycle.No Flags Request On Init Alone 4ms
Request Lifecycle.No Flags Request On Normal Capture 110ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests 16ms
Request Lifecycle.Mock Response Value Is Returned To Caller 8ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event 114ms

@posthog

posthog Bot commented Jul 20, 2026

Copy link
Copy Markdown

🦔 ReviewHog reviewed this pull request

Found 1 must fix, 0 should fix, 0 consider.

Published 1 finding (view the review).

@posthog

posthog Bot commented Jul 20, 2026

Copy link
Copy Markdown

ReviewHog Alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏

@posthog posthog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ReviewHog Report

Changes

Issues: 1 issue

Files (11)
  • .changeset/display-survey-manual-api.md
  • api/posthog_flutter.api.json
  • posthog_flutter/android/build.gradle
  • posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt
  • posthog_flutter/darwin/posthog_flutter.podspec
  • posthog_flutter/darwin/posthog_flutter/Package.swift
  • posthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PosthogFlutterPlugin.swift
  • posthog_flutter/lib/src/posthog.dart
  • posthog_flutter/lib/src/posthog_flutter_io.dart
  • posthog_flutter/lib/src/posthog_flutter_platform_interface.dart
  • posthog_flutter/lib/src/posthog_flutter_web_handler.dart

Comment on lines +425 to +435
case 'displaySurvey':
final surveyId = args['surveyId'] as String;
try {
posthog?.displaySurvey(
stringToJSAny(surveyId),
mapToJSAny({'displayType': 'popover'}),
);
} catch (e) {
printIfDebug('Exception on displaySurvey: $e');
}
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

displaySurvey is completely non-functional on Flutter Web: PosthogFlutterWeb never forwards to the new handler case

must_fix bug

Why we think it's a valid issue
  • Checked: Traced the Web call path end-to-end — posthog.dart:783-784 (displaySurvey_posthog.displaySurvey), the platform-instance wiring in posthog_flutter_web.dart:40 (registerWith sets instance = PosthogFlutterWeb()), the full posthog_flutter_web.dart (lines 1-431), the abstract base in posthog_flutter_platform_interface.dart, the IO override, and the test files.
  • Found: PosthogFlutterWeb overrides every supported method to route through handleWebMethodCall (e.g. register at web.dart:237, capture at 122) and even explicitly no-ops openUrl/showSurvey (web.dart:358-365), but has NO displaySurvey override — grep for displaySurvey returns zero hits in posthog_flutter_web.dart. So on Web the call falls through to the abstract default Future<void> displaySurvey(String surveyId) { throw UnimplementedError(...); } at platform_interface.dart:168-170. By contrast PosthogFlutterIO.displaySurvey (io.dart:515) does override and forward over the method channel.
  • Found: The switch case at web_handler.dart:425-435 and the extern binding at 69-70 are only reachable via a handleWebMethodCall(MethodCall('displaySurvey', ...)), which nothing invokes given the missing override — dead code on Web.
  • Found: Tests confirm the gap: posthog_flutter_io_test.dart:286 exercises only the method-channel/IO path, and posthog_test.dart:693 drives a hand-written fake (posthog_flutter_platform_interface_fake.dart:194) — neither instantiates PosthogFlutterWeb nor calls handleWebMethodCall with 'displaySurvey'.
  • Impact: await Posthog().displaySurvey('id') on Flutter Web throws UnimplementedError instead of forwarding to posthog.displaySurvey(surveyId, {displayType: 'popover'}), so the Web behavior the PR adds and documents (doc-comment at posthog.dart:779-780: 'on Web it is rendered by the JS SDK as a popover') is entirely non-functional. This is a real, directly-introduced correctness/contract defect with a named trigger and consequence, and the fix (add the override mirroring the other methods + a Web test) is concrete and actionable.
Issue description

The new case 'displaySurvey': branch added to handleWebMethodCall (lines 425-435) correctly builds and issues the JS SDK call posthog.displaySurvey(surveyId, {displayType: 'popover'}), but this code path is unreachable in practice. On Flutter Web, PosthogFlutterWeb.registerWith sets PosthogFlutterPlatformInterface.instance = instance (lib/posthog_flutter_web.dart:40), and Posthog()._posthog (posthog.dart:22-23) calls methods directly on that instance rather than through the method channel. Every other platform-interface method that PosthogFlutterWeb supports (setup, identify, capture, register, addExceptionStep, etc.) is explicitly overridden in posthog_flutter_web.dart to call handleWebMethodCall(MethodCall(...)) — but displaySurvey is not overridden there at all (confirmed by reading the full, current lib/posthog_flutter_web.dart, which has no displaySurvey override). Because PosthogFlutterPlatformInterface is abstract and PosthogFlutterWeb extends it directly, calling Posthog().displaySurvey('survey-123') on Web resolves to the base class's default in posthog_flutter_platform_interface.dart:168-171: Future<void> displaySurvey(String surveyId) { throw UnimplementedError('displaySurvey() has not been implemented.'); }. That method is not async, so the UnimplementedError is thrown synchronously the moment the call is made, before the added JS-forwarding logic in this file ever runs. In other words, the entire Web implementation added by this PR (the extern JS binding at lines 69-70 and the switch case at 425-435) is dead code, and the feature the PR title/changeset/doc-comments advertise for Web ('on Web it is displayed by the JS SDK as a popover') is completely broken. This gap is not caught by the PR's new tests: posthog_flutter_io_test.dart only exercises the IO/method-channel path (mobile), and posthog_test.dart only exercises a hand-written fake PosthogFlutterPlatformInterface — neither test instantiates PosthogFlutterWeb or invokes handleWebMethodCall with 'displaySurvey', so nothing exercises the actual Web code path added here.

Suggested fix

Add the missing override in lib/posthog_flutter_web.dart (mirroring the pattern used for every other supported method, e.g. unregister/register) so calls are actually forwarded to this handler:

@override
Future<void> displaySurvey(String surveyId) async {
  return handleWebMethodCall(
    MethodCall('displaySurvey', {'surveyId': surveyId}),
  );
}

Without this override, the switch case added in posthog_flutter_web_handler.dart can never execute. After adding the override, add a test that exercises PosthogFlutterWeb().displaySurvey(...) (or at least calls handleWebMethodCall with a 'displaySurvey' MethodCall directly) so this regression is caught in CI, consistent with how the mobile path is tested in posthog_flutter_io_test.dart.

Prompt to fix with AI (copy-paste)
## Context
@posthog_flutter/lib/src/posthog_flutter_web_handler.dart#L425-435

<issue_description>
The new `case 'displaySurvey':` branch added to `handleWebMethodCall` (lines 425-435) correctly builds and issues the JS SDK call `posthog.displaySurvey(surveyId, {displayType: 'popover'})`, but this code path is unreachable in practice. On Flutter Web, `PosthogFlutterWeb.registerWith` sets `PosthogFlutterPlatformInterface.instance = instance` (lib/posthog_flutter_web.dart:40), and `Posthog()._posthog` (posthog.dart:22-23) calls methods directly on that instance rather than through the method channel. Every other platform-interface method that `PosthogFlutterWeb` supports (setup, identify, capture, register, addExceptionStep, etc.) is explicitly overridden in `posthog_flutter_web.dart` to call `handleWebMethodCall(MethodCall(...))` — but `displaySurvey` is not overridden there at all (confirmed by reading the full, current `lib/posthog_flutter_web.dart`, which has no `displaySurvey` override). Because `PosthogFlutterPlatformInterface` is abstract and `PosthogFlutterWeb extends` it directly, calling `Posthog().displaySurvey('survey-123')` on Web resolves to the base class's default in `posthog_flutter_platform_interface.dart:168-171`: `Future<void> displaySurvey(String surveyId) { throw UnimplementedError('displaySurvey() has not been implemented.'); }`. That method is not `async`, so the `UnimplementedError` is thrown synchronously the moment the call is made, before the added JS-forwarding logic in this file ever runs. In other words, the entire Web implementation added by this PR (the extern JS binding at lines 69-70 and the switch case at 425-435) is dead code, and the feature the PR title/changeset/doc-comments advertise for Web ('on Web it is displayed by the JS SDK as a popover') is completely broken. This gap is not caught by the PR's new tests: `posthog_flutter_io_test.dart` only exercises the IO/method-channel path (mobile), and `posthog_test.dart` only exercises a hand-written fake `PosthogFlutterPlatformInterface` — neither test instantiates `PosthogFlutterWeb` or invokes `handleWebMethodCall` with `'displaySurvey'`, so nothing exercises the actual Web code path added here.
</issue_description>

<issue_validation>
- **Checked:** Traced the Web call path end-to-end — `posthog.dart:783-784` (`displaySurvey` → `_posthog.displaySurvey`), the platform-instance wiring in `posthog_flutter_web.dart:40` (`registerWith` sets `instance = PosthogFlutterWeb()`), the full `posthog_flutter_web.dart` (lines 1-431), the abstract base in `posthog_flutter_platform_interface.dart`, the IO override, and the test files.
- **Found:** `PosthogFlutterWeb` overrides every supported method to route through `handleWebMethodCall` (e.g. `register` at web.dart:237, `capture` at 122) and even explicitly no-ops `openUrl`/`showSurvey` (web.dart:358-365), but has NO `displaySurvey` override — `grep` for `displaySurvey` returns zero hits in `posthog_flutter_web.dart`. So on Web the call falls through to the abstract default `Future<void> displaySurvey(String surveyId) { throw UnimplementedError(...); }` at platform_interface.dart:168-170. By contrast `PosthogFlutterIO.displaySurvey` (io.dart:515) does override and forward over the method channel.
- **Found:** The switch case at web_handler.dart:425-435 and the extern binding at 69-70 are only reachable via a `handleWebMethodCall(MethodCall('displaySurvey', ...))`, which nothing invokes given the missing override — dead code on Web.
- **Found:** Tests confirm the gap: `posthog_flutter_io_test.dart:286` exercises only the method-channel/IO path, and `posthog_test.dart:693` drives a hand-written fake (`posthog_flutter_platform_interface_fake.dart:194`) — neither instantiates `PosthogFlutterWeb` nor calls `handleWebMethodCall` with `'displaySurvey'`.
- **Impact:** `await Posthog().displaySurvey('id')` on Flutter Web throws `UnimplementedError` instead of forwarding to `posthog.displaySurvey(surveyId, {displayType: 'popover'})`, so the Web behavior the PR adds and documents (doc-comment at posthog.dart:779-780: 'on Web it is rendered by the JS SDK as a popover') is entirely non-functional. This is a real, directly-introduced correctness/contract defect with a named trigger and consequence, and the fix (add the override mirroring the other methods + a Web test) is concrete and actionable.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Add the missing override in `lib/posthog_flutter_web.dart` (mirroring the pattern used for every other supported method, e.g. `unregister`/`register`) so calls are actually forwarded to this handler:

```dart
@override
Future<void> displaySurvey(String surveyId) async {
  return handleWebMethodCall(
    MethodCall('displaySurvey', {'surveyId': surveyId}),
  );
}

Without this override, the switch case added in posthog_flutter_web_handler.dart can never execute. After adding the override, add a test that exercises PosthogFlutterWeb().displaySurvey(...) (or at least calls handleWebMethodCall with a 'displaySurvey' MethodCall directly) so this regression is caught in CI, consistent with how the mobile path is tested in posthog_flutter_io_test.dart.
</potential_solution>


</details>

PosthogFlutterWeb overrides each platform-interface method explicitly, so without a displaySurvey override the call fell through to the base class UnimplementedError and the web handler case was unreachable. Adds the override plus a browser test guarding the wiring, following the existing addExceptionStep pattern.

Generated-By: PostHog Code
Task-Id: 96def33a-2460-42b7-9690-0a75ae8383fe

bs1180 commented Jul 20, 2026

Copy link
Copy Markdown
Author

Good catch from ReviewHog — the displaySurvey handler case was unreachable on Web because PosthogFlutterWeb never overrode the method, so calls fell through to the platform interface's UnimplementedError. Fixed in b7e3f1a by adding the override, plus a browser test (flutter test --platform chrome) guarding the wiring, following the existing addExceptionStep pattern — verified green locally along with the VM tests, analyzer, and formatting. The remaining red checks are still the expected native-dependency ones (posthog-android 3.56.0 / posthog-ios 3.67.0 not released yet).

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.

Support manual API displaySurvey

1 participant