Skip to content

fix: attach resource context to reported listener errors; stop retrying permission-denied - #50

Draft
posthog[bot] wants to merge 1 commit into
masterfrom
posthog-self-driving/fixlisteners-attach-firestate-context-001401
Draft

fix: attach resource context to reported listener errors; stop retrying permission-denied#50
posthog[bot] wants to merge 1 commit into
masterfrom
posthog-self-driving/fixlisteners-attach-firestate-context-001401

Conversation

@posthog

@posthog posthog Bot commented Aug 8, 2026

Copy link
Copy Markdown

Problem

  • A Firestore rules denial hit a Firestate listener and was captured with only a minified Firebase stack. Nobody could tell which collection or document was denied.
  • The cost falls on the team, not users: while the context is stripped, every future rules denial from any resource shares one opaque fingerprint. Each one then needs a manual bisect to locate.
  • Two contained defects sit behind the symptom:
    • Context is dropped. The onSnapshot error callbacks build the right context (type, path, operation: 'read'), but store.reportError passes the raw FirebaseError as argument one and the context as a separate argument two. A consumer that forwards only the first argument to an error tracker loses the path and the distinct fingerprint.
    • A retry loop that never ends. With retryOnError: true, the listener handler only did a console.warn and re-attached every retryInterval (5s). It never set state.error and never reported. permission-denied is not transient, so that is an endless retry behind a spinner that never resolves. This bug is latent today — nothing here opts into retryOnError.

Changes

  • Reported errors carry their own context. store.reportError now wraps every error in a new FirestateError. The wrapper puts the path in the message and holds type, path, operation, and the Firestore code on own fields, with the original error on cause. A consumer that forwards only the error keeps a usable path and gets a distinct fingerprint per resource. The context object still arrives as the second onError argument.
  • permission-denied is terminal. The listener retry branch now checks the Firestore code. A terminal code (permission-denied, unauthenticated) reports the error, sets state.error, and clears isLoading — even when retryOnError is set. Only transient codes (e.g. unavailable) still re-attach the listener. This matches the policy the write paths already use.
  • FirestateError is exported from the public entry point.

Behavior

Listener error Before After
permission-denied, retryOnError: true retries every 5s forever, spinner never resolves, never reported reported once, state.error set, isLoading cleared
unavailable, retryOnError: true re-attaches the listener re-attaches the listener (unchanged)
any error → onError raw error, context only as arg 2 FirestateError with path/type/operation/code, context still arg 2

Tests

  • New src/__tests__/listener-error.test.ts: wrapped-error context and path, distinct fingerprints per resource, permission-denied terminal for document and collection, transient error still retries.
  • Updated src/core/store.test.ts for the wrapped-error contract (code copy, no double-wrap, undo path).
  • pnpm typecheck, pnpm build, and the full test suite (269 passed, 3 skipped) all pass.

Why

A rare handled exception (1 occurrence, 1 user, 1 session in 30 days) exposed two code defects. The value is not the single event — it is stopping every future rules denial from collapsing into one opaque bucket, and closing a latent endless-retry loop before anyone opts into retryOnError.

Agent context


Created with PostHog Desktop from this inbox report.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Note

Fix listener error handling to attach resource context and skip retries on permission-denied

  • Introduces FirestateError in src/core/errors.ts that wraps errors with type, path, operation, and Firestore code, and includes the resource path in the message.
  • store.reportError in src/core/store.ts now wraps errors in FirestateError before passing to onError or console.error, preventing double-wrapping.
  • Document and collection listeners now treat permission-denied and unauthenticated Firestore codes as terminal: they set state.error, clear isLoading, and report immediately instead of scheduling a retry, even when retryOnError is true.
  • FirestateError is exported from the package entry point so consumers can import and inspect it.
  • Behavioral Change: previously, retryOnError: true would retry all listener errors; terminal Firestore codes now bypass retry logic entirely.

Macroscope summarized 011aa51.

…rying permission-denied

store.reportError now wraps every error in a FirestateError that carries type,
path, operation, and the Firestore code on own fields, keeps the original on
cause, and puts the path in the message. A consumer that forwards only the first
argument to an error tracker keeps the context and gets a distinct fingerprint
per resource. Context still travels as the second onError argument.

The listener retry branch now treats a terminal Firestore code
(permission-denied, unauthenticated) as terminal even when retryOnError is set:
it reports the error, sets state.error, and clears isLoading instead of
re-attaching the listener every retryInterval behind a spinner that never
resolves. Only transient codes still retry.

Generated-By: PostHog Code
Task-Id: 22c2ca54-66e1-40e9-9799-2942e40ad4ac
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.

0 participants