Skip to content

feat(browser): headless user feedback API - #1155

Closed
Makisuo wants to merge 4 commits into
feat/browser-sdk-error-replayfrom
feat/browser-sdk-feedback
Closed

Makisuo wants to merge 4 commits into
feat/browser-sdk-error-replayfrom
feat/browser-sdk-feedback

Conversation

@Makisuo

@Makisuo Makisuo commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Part 9 of the browser SDK stack. Based on the error-triggered replay PR.

What changes

  • MapleBrowser.sendFeedback({ message, email?, name?, attributes? }) sends a maple.user_feedback OTel log event with:
    • session.id;
    • user.email / user.name (semconv; the email is dropped when privacy.captureUserEmail is false);
    • maple.feedback.has_replay;
    • maple.feedback.error_trace_id, when the user hit an error earlier on the page.
  • The event is linked to that error's span, so it shows up on the error's trace.
  • Feedback also keeps a buffered replay the same way an error does: a user reporting a problem is as good a signal.
  • Returns false when nothing was sent (empty message, no consent).

Headless for now (bring your own form). A drop-in widget can come later as its own entry so it never touches the eager bundle. Reading feedback in the product (a list, or on the session detail page) is backend/UI follow-up; until then it's queryable as logs.

Eager budget 43 → 43.5 kB, first-party 17.5 → 18 kB.

Testing

Browser tests through init(): the event carries the message, user fields, attributes, session and error link; captureUserEmail: false drops the email; an empty message sends nothing.


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

MapleBrowser.sendFeedback({ message, email?, name?, attributes? }) sends
a maple.user_feedback OTel log event: session.id, user.email / user.name
(semconv; the email is dropped when privacy.captureUserEmail is false),
maple.feedback.has_replay and, when the user hit an error earlier in the
page, maple.feedback.error_trace_id. The event is linked to that error's
span, so it shows on the error's trace.

Feedback also keeps a buffered replay, the same way an error does: a user
reporting a problem is as good a signal. Headless for now; a drop-in
widget can come as its own entry so it never touches the eager bundle.

Eager budget 43 -> 43.5 kB, first-party 17.5 -> 18 kB.
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 32fad358-6d50-46ba-8a0b-1d5f7c2e72b9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@maple-review-bot

maple-review-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 2/5 · risky as written
The new has_replay attribute is wrong exactly on the buffered-replay sessions feedback is meant for, and no test runs with replay enabled.
quality 88/100 · 1 warning · 1 note · tests partial · risk medium · 1/2 new units observable

Adds MapleBrowser.sendFeedback, emitting a maple.user_feedback log event linked to the session, the last error's span, and a kept buffered replay. The API is small and the privacy path is respected, but the replay flag is computed before the replay is kept.

  • sendFeedback({ message, email?, name?, attributes? }) emits a maple.user_feedback log event
  • The event links to the last error's span via maple.feedback.error_trace_id
  • Feedback calls keepReplay(), keeping a buffered replay like an error does
  • captureUserEmail: false drops user.email; empty message or no consent returns false

Findings

Warning · F1 · maple.feedback.has_replay is false on the buffered replay feedback keeps

correctness · packages/browser/src/feedback.ts:53

recorded is read from getSession().replaySampled before keepReplay() runs, so on the buffered-replay path (replay.onErrorSampleRate, no earlier error on the page) it is always false: the session is buffered precisely because claimReplaySample rolled replaySampled: false (packages/browser-session/src/session/session.ts:622-647). The event therefore reports maple.feedback.has_replay = false for the very feedback whose keepReplay() uploads the buffered minute — trigger() writes replaySampled: true (session.ts:650-654) only after the attribute was already computed. Call keepReplay() before reading the record so the attribute matches what was actually uploaded.

Note · F2 · lastError survives shutdown() and links feedback to a dead trace

correctness · packages/browser/src/init.ts:249

shutdown() resets the other feedback state (captureUserEmail, keepReplay) but not the module-level lastError, which only resetFeedbackForTests clears (packages/browser/src/feedback.ts:39-43). A page that calls init() again after shutdown() — or a fresh session after a consent revoke/re-grant — sends its first feedback linked to a span from the previous SDK lifecycle, so the event lands on an unrelated trace and carries a stale maple.feedback.error_trace_id.

Have `configureFeedback` (or a small `clearFeedback()` beside it) drop `lastError` too, and call the same reset here as `resetFeedbackForTests`.
What was checked
  • emitLog refuses without consent and stamps session.id, so the event carries it (logs.ts:40-47)
  • The module-level listener uses onErrorRecorded, whose span context is the error's own one-off span (errors.ts:77-92)
  • The test asserts body, user fields, attributes, session id and the error-span link through init()
Observability coverage: 1 of 2 changes observable
Change Kind Observable Evidence
sendFeedback OTel log event log yes Emits through emitLog (packages/browser/src/feedback.ts:54-67), which the deferred OTLP logs exporter sinks
buffered replay kept on feedback background no keepReplay() triggers the existing replay handle; nothing records which signal triggered it
Copy all findings (2)
Findings from an automated review of commit 338c1b5e70addb092fb76b15fdac044d827fd10f. Verify each one against the current code before changing anything, fix only those that still apply, and keep each fix to the lines it names.

---

F1 · Warning · correctness · packages/browser/src/feedback.ts:53
`maple.feedback.has_replay` is false on the buffered replay feedback keeps
`recorded` is read from `getSession().replaySampled` before `keepReplay()` runs, so on the buffered-replay path (`replay.onErrorSampleRate`, no earlier error on the page) it is always `false`: the session is buffered precisely because `claimReplaySample` rolled `replaySampled: false` (`packages/browser-session/src/session/session.ts:622-647`). The event therefore reports `maple.feedback.has_replay = false` for the very feedback whose `keepReplay()` uploads the buffered minute — `trigger()` writes `replaySampled: true` (`session.ts:650-654`) only after the attribute was already computed. Call `keepReplay()` before reading the record so the attribute matches what was actually uploaded.

---

F2 · Note · correctness · packages/browser/src/init.ts:249
`lastError` survives `shutdown()` and links feedback to a dead trace
`shutdown()` resets the other feedback state (`captureUserEmail`, `keepReplay`) but not the module-level `lastError`, which only `resetFeedbackForTests` clears (`packages/browser/src/feedback.ts:39-43`). A page that calls `init()` again after `shutdown()` — or a fresh session after a consent revoke/re-grant — sends its first feedback linked to a span from the previous SDK lifecycle, so the event lands on an unrelated trace and carries a stale `maple.feedback.error_trace_id`.
Suggested fix: Have `configureFeedback` (or a small `clearFeedback()` beside it) drop `lastError` too, and call the same reset here as `resetFeedbackForTests`.

338c1b5 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

@maple-review-bot maple-review-bot 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.

1 inline note from Maple's review. The score and summary are in the review comment above.

Comment thread packages/browser/src/feedback.ts
maple.feedback.has_replay was read before keepReplay() ran, so on the
buffered-replay path it was always false for the very feedback that kept
the replay. keepReplay() now runs first; the trigger marks the session
recorded synchronously.
@maple-review-bot

maple-review-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 4/5 · likely safe to merge
The has_replay reorder is correct, but the earlier lastError-after-shutdown() defect at init.ts:249 is still unfixed.
quality 98/100 · 1 note · tests covered · risk medium · 1/1 new units observable

Adds headless MapleBrowser.sendFeedback, emitting a maple.user_feedback log event linked to the session, the last error's trace and a buffered replay. The has_replay ordering fix is correct; the earlier stale-lastError defect after shutdown() is still present.

  • sendFeedback emits maple.user_feedback with session, user and last-error links
  • keepReplay() runs before has_replay is read, so buffered feedback reports true
  • init() connects feedback's keep-replay to the live replay handle
  • MapleBrowser exports sendFeedback and FeedbackInput

Still open from earlier reviews

What was checked
  • trigger() marks the session before its first await (replay-session.ts:147), so has_replay is accurate
  • captureEmail default true matches the meta-row rule (meta-row.ts:159)
  • emitLog stamps session.id after caller attributes (logs.ts:43), so it cannot be overridden
Observability coverage: 1 of 1 changes observable
Change Kind Observable Evidence
MapleBrowser.sendFeedback browser SDK log event (OTel log, linked to the error span) yes emitLog with spanContext/attributes at feedback.ts:57-70

1af65de · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

lastError survived shutdown(), so feedback after a re-init linked to a
trace from the previous lifecycle. configureFeedback, run by init() and
shutdown(), now clears it.
@maple-review-bot

maple-review-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 5/5 · safe to merge
The reset of lastError in configureFeedback closes the shutdown case the previous review flagged, and the new browser tests exercise it.
quality 100/100 · no findings · tests covered · risk low · 1/1 new units observable

Adds MapleBrowser.sendFeedback, a headless API that emits a maple.user_feedback OTel log event linked to the session, the last error's span and a replay it keeps. The earlier lastError-after-shutdown finding is fixed; the change is contained and safe to merge.

  • sendFeedback emits maple.user_feedback with session.id, user.email/user.name and maple.feedback.* attributes
  • configureFeedback is called from init() and from shutdown(), clearing lastError on either lifecycle start
  • keepReplay runs before has_replay is read, so a buffered replay it keeps is reported

Fixed since the last review

  • F2 · lastError survives shutdown() and links feedback to a dead trace
What was checked
  • F2: shutdown() now calls configureFeedback (init.ts:249), which sets lastError = undefined; the re-init test at feedback.browser.test.ts:88 covers it
  • has_replay timing: keepReplay() precedes the read (feedback.ts:57-58) and trigger calls markReplayTriggered synchronously (replay-session.ts:147-151)
  • user.email drop path matches resolved config: privacy.captureUserEmail → config.captureUserEmail (config.ts:267), tested at feedback.browser.test.ts:98
Observability coverage: 1 of 1 changes observable
Change Kind Observable Evidence
sendFeedback user feedback event log event yes emitLog queues a structured record with session.id and the linked span context, exported through the OTLP logs pipeline (packages/browser/src/deferred/logs.ts:56)

04e5138 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

@Makisuo

Makisuo commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator Author

Dropping this: a headless feedback call with nowhere in the product to read it is a half-measure. End-user feedback should come back as a real feature built on product events. #1156 now sits on #1154 and removes this code from the rest of the stack.

@Makisuo Makisuo closed this Sep 29, 2026
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.

1 participant