Skip to content

fix(observability): the four production Sentry findings of 2026-09-03, each at its root - #1384

Open
teetangh wants to merge 7 commits into
devfrom
fix/sentry-2026-09-03-findings
Open

fix(observability): the four production Sentry findings of 2026-09-03, each at its root#1384
teetangh wants to merge 7 commits into
devfrom
fix/sentry-2026-09-03-findings

Conversation

@teetangh

@teetangh teetangh commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

What

Wave-6 PR F (umbrella #1319): the four production Sentry issues collected on 2026-09-03, each fixed at its root in its own commit so a revert is surgical. The Prisma connection-timeout issue (FAMILIARISE_WEB-9) is the instance-boot class tracked under #1124 and is deliberately untouched.

  • FAMILIARISE_WEB-1B, Novu ResponseValidationError on request-for-approval. Replaying the event's exact Zod error against every matcher in the installed SDK shows it is the 422 schema, whose errors field the API does not send for this endpoint's two documented 422 causes (workflow_not_found, or an idempotency key reused with a different body). Novu rejected the trigger; the SDK then failed to parse the rejection. No SDK version fixes the schema (checked up to 3.19.1), so the wrapper in lib/novu/service.ts now reads the status off the thrown error, reports a 2xx as accepted, and otherwise reports the real status, Novu's message and the workflow id. Both trigger sites are wired.
  • FAMILIARISE_WEB-1C, HTML parsed as JSON on the consultee appointments page. The cancel and reschedule actions called response.json() before checking response.ok. A shared requireJsonResponse in lib/fetch-helpers.ts checks the status and the content type before parsing and throws the existing typed ApiResponseError (also for a followed sign-in redirect that comes back 200 with HTML). The dashboard query helper and both actions use it; the cancel dialog's 409 branch keys off the status.
  • FAMILIARISE_WEB-1D, Failed to fetch in the Novu subscriber sync. The server route only ever answers JSON, so this is transport-level. The hook no longer reports a fault with no HTTP status (React Query retries it), reports a real answer at warning, and reads connectivity through useSyncExternalStore because the installed query core initialises online to true and never reads navigator.onLine.
  • FAMILIARISE_WEB-10, Stream token minted without a session. StreamProviderImpl gates the mint on the client's own session (a pending session still counts, so the first mint keeps its place off the join path per 🐌 Performance: Stream Chat sync runs on every dashboard load causing excessive API calls #248); the server-side throw stays as the backstop and now carries an expected-error marker that beforeSend re-levels without dropping.

Verification

Cold tsc clean; 202 suites / 2,628 tests green; eslint zero warnings on all ten touched files; prettier clean. One pin file, __tests__/lib/require-json-response.test.ts (HTML at 502, HTML at 200, a 409 envelope, the happy path). Commit 1 is format-only for five files that were prettier-dirty on dev.

Owner action surfaced

The 422 shape points at new-booking-request not being published in the production Novu environment; there is no workflow-sync script in this repo, so the ids in lib/novu/workflows.ts are hand-created in the dashboard and can drift silently. This PR makes the next occurrence say so in one line; publishing the workflow is a dashboard task, alongside appointment-partially-scheduled.

Sentry issue status is unchanged by this PR; the issues are resolved against the release that carries it, then re-checked by reproduction.

Part of #1319.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7

teetangh and others added 6 commits September 3, 2026 18:50
These five were already prettier-dirty on dev; formatting them first keeps
the fix commits reviewable as behaviour changes only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
…on error (FAMILIARISE_WEB-1B)

The SDK validates the RESPONSE against its own generated schema and throws
before handing back the status. Its 422 schema requires an `errors` record,
but both 422s Novu documents for this endpoint — an unknown or unpublished
workflow, and an idempotency key reused with a different body — answer with
`statusCode` and `message` only. So a rejected trigger reached Sentry as a
ZodError about a field of the SDK's own error envelope, and neither the
status nor Novu's reason survived.

No published @novu/api relaxes that field (checked through 3.19.1), so the
wrapper reads the status off the thrown error instead of chasing a bump.
A 2xx the SDK merely failed to parse counts as an accepted send, because the
notification is already queued in that case; anything else reports its real
status, Novu's own message and the workflow id.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
…MILIARISE_WEB-1C)

Both write actions on the consultee appointments page read the body before
checking the response, so a Netlify crash page, a function timeout or a
followed redirect to sign-in became `SyntaxError: Unexpected token '<'` — in
the toast the consultee read and in the error feed. `res.ok` alone never
caught the redirect, which arrives as HTML at status 200.

`requireJsonResponse` checks the status and the content type before parsing
and throws the existing typed `ApiResponseError`, which carries the status.
`parseJsonResponse` now delegates to it, the dashboard query factory uses it
(its old message ended in an empty `statusText` over HTTP/2), and the cancel
and reschedule actions go through it — so the cancel dialog still recognises
the 409 that means the booking changed state underneath it, and Sentry gets a
warning naming the status instead of a parse error.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
…FAMILIARISE_WEB-1D)

`TypeError: Failed to fetch` carries no status at all — it means the request
never got an answer. React Query already retries this sync and nothing
user-visible depends on it, so reporting it at error level paged someone for
a dropped connection. Only a real HTTP answer reports now: a 4xx as a
modelled outcome, a 5xx as a fault, both at warning with the status attached.

The hook also skips the call while the browser reports itself offline.
React Query's `networkMode: "online"` does not cover this on its own — its
`onlineManager` starts at `online = true` and only ever flips on the window
connectivity EVENTS, so a tab opened while already offline sails past that
guard and fires the request anyway. Reading `navigator.onLine` through
`useSyncExternalStore` closes that opening without latching `enabled` off.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
…RISE_WEB-10)

The token action refuses to mint without a session, and a dashboard left open
past its cookie's expiry called it regardless — 8 unauthorized throws on the
error feed with nobody to show them to. The connector now consults its own
session and skips the mint once the session is known to be absent. A session
that is still loading counts as permitted on purpose: blocking the first mint
on that round trip would put a serial wait back on the join path, which is
what the prefetch effect exists to remove (#248). The video SDK's later
`tokenProvider` refreshes read the answer through a ref, so `getCachedToken`
keeps its identity and the connect effect does not re-fire.

The server-side refusal stays as the backstop, now marked expected. An error
that escapes a server action is captured by Next's `onRequestError` hook,
which takes no per-call options, so the marker rides on the thrown error and
`beforeSend` stamps the tag — the only way to reach that capture path. It
re-levels an event and never drops one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgadJydWEKkdhhzY58yiL7
@netlify

netlify Bot commented Sep 3, 2026

Copy link
Copy Markdown

Deploy Preview for familiarise ready!

Name Link
🔨 Latest commit 3a61eb4
🔍 Latest deploy log https://app.netlify.com/projects/familiarise/deploys/6a9992fa925ec50009ddfb29
😎 Deploy Preview https://deploy-preview-1384--familiarise.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 35 (🔴 down 18 from production)
Accessibility: 90 (no change from production)
Best Practices: 83 (no change from production)
SEO: 90 (no change from production)
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 53 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 64 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 984a7c8d-30db-4650-85d3-3f5eb5bac16b

📥 Commits

Reviewing files that changed from the base of the PR and between 4d161dd and 3a61eb4.

📒 Files selected for processing (11)
  • __tests__/lib/require-json-response.test.ts
  • actions/stream/chat/stream.action.ts
  • components/appointments/consultee/useEventActions.ts
  • docs/booking/05-troubleshooting-and-changelog.md
  • hooks/useNovuSubscriberSync.ts
  • lib/dashboard-queries.ts
  • lib/fetch-helpers.ts
  • lib/novu/service.ts
  • lib/observability/expected.ts
  • providers/StreamProviderImpl.tsx
  • sentry.shared.config.ts

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

@sonarqubecloud

sonarqubecloud Bot commented Sep 3, 2026

Copy link
Copy Markdown

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