Skip to content

fix(surveys): don't restart the popup delay on repeat trigger events - #849

Merged
turnipdabeets merged 5 commits into
mainfrom
fix/survey-delay-first-trigger
Oct 12, 2026
Merged

turnipdabeets merged 5 commits into
mainfrom
fix/survey-delay-first-trigger

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

With surveyPopupDelaySeconds set, a repeat trigger event during the delay restarts it on Android. Events that keep firing faster than the delay can keep the survey from showing at all.

Android marked a survey active only once it was shown. While the delay ran nothing was active, so each matching event re-rendered the survey and the delegate (the Compose host, or Flutter in PostHog/posthog-flutter#627) restarted its timer.

iOS and the browser already reserve the survey before the delay starts, so repeat events are ignored and the delay runs from the first trigger. This brings Android in line.

Changes

  • Reserve the survey (pendingPresentation) just before calling renderSurvey. canShowNextSurvey() returns false while one is pending.
  • Release the reservation when the delegate reports it shown (it becomes the active survey as before), when it's closed before showing, when the render is skipped, and on reset or uninstall.
  • Closing before showing still doesn't mark the survey seen or send survey dismissed.

Because the integration now waits to hear back, a delegate that drops a survey silently would block every later one until reset or restart (iOS works the same way). So:

  • The PostHogSurveysDelegate.renderSurvey KDoc now says a delegate that can't show a survey must call onSurveyClosed. Closing before shown doesn't mark the survey seen.
  • The Compose host now reports a failed present (a Compose linkage error) as a close instead of dropping it.

Flutter compatibility: posthog_flutter declares posthog-android:[3.71.0,4.0.0), so existing apps pick this up without a plugin update. Up to 5.50.12, Flutter drops a survey that arrives before PosthogObserver has a context and never reports back, so on Android surveys stop until restart. iOS already behaves this way. posthog_flutter 5.50.13 (PostHog/posthog-flutter#631) waits for a context instead.

💚 How did you test it?

  • New test: a survey waiting to be shown isn't rendered again. It fails without the fix.
  • New test: closing a survey before it shows releases it.
  • Updated stale shown callback does not clean up a newer survey: it relied on a second survey replacing a pending one. It now closes the stale survey first and still checks that its late shown callback doesn't affect the newer one.
  • spotlessCheck passes, and the posthog-android (751) and posthog-android-surveys-compose (49) unit tests pass.
  • Device test on an Android emulator: Flutter example app with feat(surveys): honor surveyPopupDelaySeconds before showing posthog-flutter#627, an event-triggered survey with surveyPopupDelaySeconds: 3, and trigger events at 0, 1.2 and 2.4 s. With main the survey showed at 5.57 s (3 s after the last event). With this branch it showed at 3.24 s (3 s after the first). Both runs sent exactly one survey shown.
  • The Compose failed-present path isn't covered by a test, because forcing a Compose linkage error in a unit test isn't practical.

📝 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 came out of a review comment on PostHog/posthog-flutter#627. We compared repeat-event handling across platforms: iOS sets activeSurvey before renderSurvey, and the browser sets _surveyInFocus before the delay and keeps the first activation timestamp. The fix matches that here rather than adding first-deadline logic to every delegate.

🤖 Generated with Claude Code

Android marked a survey active only once it was shown, so a repeat trigger event during surveyPopupDelaySeconds re-rendered it and the delegate restarted its delay. Reserve the survey before rendering, as iOS and the browser do, and release it when it is shown, closed, skipped or reset.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The integration now holds a survey until the delegate reports it shown or closed, so a delegate that drops one silently would block every later survey. The Compose host now reports a failed present as a close, and the delegate KDoc documents the contract.

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

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor
⚠️ `posthog` is modified but not declared in any changeset

This is informational — the PR is not blocked. Click the triangle above to collapse, or push a fix and this comment will auto-delete.

Modified in this PR but not in any changeset:

  • posthog

If this package should ship the change, add it to the changeset frontmatter:

---
"posthog": patch
---

Changesets in this PR:

  • posthog-android — patch
  • posthog-android-surveys-compose — patch

A delegate that drops a survey without calling onSurveyClosed now blocks later surveys, so call it out for custom delegates. Closing is enough to release the survey; it doesn't need to be shown first.

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

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

posthog-android Compliance Report

Date: 2026-10-08 16:01:49 UTC
Duration: 118483ms

✅ All Tests Passed!

46/46 tests passed


Capture Tests

✅ 29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 388ms
Format Validation.Event Has Uuid ✅ 33ms
Format Validation.Event Has Lib Properties ✅ 28ms
Format Validation.Distinct Id Is String ✅ 30ms
Format Validation.Token Is Present ✅ 30ms
Format Validation.Custom Properties Preserved ✅ 30ms
Format Validation.Event Has Timestamp ✅ 27ms
Retry Behavior.Retries On 503 ✅ 7037ms
Retry Behavior.Does Not Retry On 400 ✅ 4028ms
Retry Behavior.Does Not Retry On 401 ✅ 4026ms
Retry Behavior.Respects Retry After Header ✅ 7032ms
Retry Behavior.Implements Backoff ✅ 17035ms
Retry Behavior.Retries On 500 ✅ 7020ms
Retry Behavior.Retries On 502 ✅ 7022ms
Retry Behavior.Retries On 504 ✅ 7020ms
Retry Behavior.Max Retries Respected ✅ 17038ms
Deduplication.Generates Unique Uuids ✅ 41ms
Deduplication.Preserves Uuid On Retry ✅ 7016ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 12033ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7018ms
Deduplication.No Duplicate Events In Batch ✅ 39ms
Deduplication.Different Events Have Different Uuids ✅ 24ms
Compression.Sends Gzip When Enabled ✅ 23ms
Batch Format.Uses Proper Batch Structure ✅ 18ms
Batch Format.Flush With No Events Sends Nothing ✅ 17ms
Batch Format.Multiple Events Batched Together ✅ 38ms
Error Handling.Does Not Retry On 403 ✅ 4020ms
Error Handling.Does Not Retry On 413 ✅ 4022ms
Error Handling.Retries On 408 ✅ 5031ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 37ms
Request Payload.Flags Request Uses V2 Query Param ✅ 25ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 24ms
Request Payload.Flags Request Omits Authorization Header ✅ 29ms
Request Payload.Token In Flags Body Matches Init ✅ 38ms
Request Payload.Groups Round Trip ✅ 30ms
Request Payload.Groups Default To Empty Object ✅ 28ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 25ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 26ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 26ms
Request Lifecycle.No Flags Request On Init Alone ✅ 10ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 24ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 35ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 22ms
Retry Behavior.Retries Flags On 502 ✅ 325ms
Retry Behavior.Retries Flags On 504 ✅ 324ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 27ms

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.

One startup compatibility edge: the SDK can attempt an eligible cached survey during setup, before Flutter’s navigation context exists. Flutter 5.50.12 drops that request without a callback, so the new native reservation can block later surveys even after the UI is ready.

Flutter 5.50.13 already fixes this, but applications retaining 5.50.12 can still resolve the newer Android SDK. Worth making the Flutter upgrade requirement explicit.

AI-assisted review.

@dustinbyrne
dustinbyrne requested a review from a team October 8, 2026 15:41
…angeset

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

Copy link
Copy Markdown
Contributor Author

Good call. Added a note to the changeset that Flutter apps need posthog_flutter 5.50.13 or later (PostHog/posthog-flutter#631), so it lands in the CHANGELOG.

@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.

to expand on my earlier comment, the finding was that if remote config returns an eligible survey before runApp is called, this will prevent surveys from displaying in older versions of posthog-flutter (v5.50.12 and below, so pretty recent).

i'd assume this is relatively unlikely, given you'd need to defer runApp longer than it takes for the remote config round trip, however worth raising nonetheless because this version of posthog-android will be resolvable by older versions of posthog-flutter. i'd reproduced it locally by adding a timeout between Posthog().setup() and runApp(). it didn't occur without the added delay.

i didn't expect to submit this as a pure documentation recommendation originally (sorry for the confusion). given this seems unlikely to occur in practice, i’m comfortable approving with that compatibility caveat 👍👍

@turnipdabeets
turnipdabeets merged commit 0859ca7 into main Oct 12, 2026
17 checks passed
@turnipdabeets
turnipdabeets deleted the fix/survey-delay-first-trigger branch October 12, 2026 17:01
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