feat(surveys): add displaySurvey to show a survey on demand#495
Conversation
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
|
Hi @ioannisj @turnipdabeets 👋 — this closes #225 (manual |
posthog-flutter Compliance ReportDate: 2026-07-20 18:31:53 UTC ✅ All Tests Passed!45/45 tests passed Capture Tests✅ 29/29 tests passed View Details
Feature_Flags Tests✅ 16/16 tests passed View Details
|
🦔 ReviewHog reviewed this pull requestFound 1 must fix, 0 should fix, 0 consider. Published 1 finding (view the review). |
|
ReviewHog Alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏 |
There was a problem hiding this comment.
ReviewHog Report
Changes
Issues: 1 issue
Files (11)
.changeset/display-survey-manual-api.mdapi/posthog_flutter.api.jsonposthog_flutter/android/build.gradleposthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.ktposthog_flutter/darwin/posthog_flutter.podspecposthog_flutter/darwin/posthog_flutter/Package.swiftposthog_flutter/darwin/posthog_flutter/Sources/posthog_flutter/PosthogFlutterPlugin.swiftposthog_flutter/lib/src/posthog.dartposthog_flutter/lib/src/posthog_flutter_io.dartposthog_flutter/lib/src/posthog_flutter_platform_interface.dartposthog_flutter/lib/src/posthog_flutter_web_handler.dart
| case 'displaySurvey': | ||
| final surveyId = args['surveyId'] as String; | ||
| try { | ||
| posthog?.displaySurvey( | ||
| stringToJSAny(surveyId), | ||
| mapToJSAny({'displayType': 'popover'}), | ||
| ); | ||
| } catch (e) { | ||
| printIfDebug('Exception on displaySurvey: $e'); | ||
| } | ||
| break; |
There was a problem hiding this comment.
displaySurvey is completely non-functional on Flutter Web: PosthogFlutterWeb never forwards to the new handler case
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 inposthog_flutter_web.dart:40(registerWithsetsinstance = PosthogFlutterWeb()), the fullposthog_flutter_web.dart(lines 1-431), the abstract base inposthog_flutter_platform_interface.dart, the IO override, and the test files. - Found:
PosthogFlutterWeboverrides every supported method to route throughhandleWebMethodCall(e.g.registerat web.dart:237,captureat 122) and even explicitly no-opsopenUrl/showSurvey(web.dart:358-365), but has NOdisplaySurveyoverride —grepfordisplaySurveyreturns zero hits inposthog_flutter_web.dart. So on Web the call falls through to the abstract defaultFuture<void> displaySurvey(String surveyId) { throw UnimplementedError(...); }at platform_interface.dart:168-170. By contrastPosthogFlutterIO.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:286exercises only the method-channel/IO path, andposthog_test.dart:693drives a hand-written fake (posthog_flutter_platform_interface_fake.dart:194) — neither instantiatesPosthogFlutterWebnor callshandleWebMethodCallwith'displaySurvey'. - Impact:
await Posthog().displaySurvey('id')on Flutter Web throwsUnimplementedErrorinstead of forwarding toposthog.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
|
Good catch from ReviewHog — the |
💡 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:displaySurveyAPIs (feat(surveys): add displaySurvey to show a survey on demand posthog-android#643, feat(surveys): add displaySurvey to show a survey on demand posthog-ios#730). The native SDK looks the survey up, bypasses display conditions (targeting flags, event triggers, seen/wait-period checks), and renders it back through the existing Flutter survey bottom sheet — events (survey shown/sent/dismissed) and seen-state work exactly as for auto-displayed surveys.displaySurvey(surveyId, {displayType: 'popover'})directly.💚 How did you test it?
Posthog().displaySurveyforwarding to the platform interface (posthog_test.dart) and the method-channel payload (posthog_flutter_io_test.dart).flutter testpasses locally (224 tests).flutter analyzeanddart formatare clean; the public API snapshot was regenerated withscripts/check-api-dart.sh --update; ktlint and swiftformat pass on the changed plugin files.📝 Checklist
If releasing new changes
pnpm changesetto 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
displaySurveyto 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 singlesurveyIdparameter (noignoreConditions/initialResponsesoptions); Web passesdisplayType: 'popover'since inline rendering has no Flutter equivalent.Created with PostHog Code