This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.
A survey widget should not be able to take down the page it is embedded on. This one could: a single API response with a null body threw an uncaught TypeError in the visitor's browser. Sentry's bot found it, filed it as a GitHub issue, and Seer pointed at the exact line. Here is the fix.
Project Overview
Formbricks is an open-source survey and experience-management platform. Websites and apps embed a small JavaScript SDK that loads a survey, shows it to a user, and posts the answers back to the Formbricks API. The SDK lives in the monorepo as two packages: @formbricks/js-core (the loader and command queue) and @formbricks/surveys (the survey renderer). Both talk to the backend through a shared makeRequest helper.
Bug Fix or Performance Improvement
I fixed issue #6581, a production crash that Sentry filed automatically:
Bug: API data is not always validated in the surveys package
TypeError: Cannot read properties of null (reading 'data')
The issue was opened automatically by sentry[bot], and its body carries the Sentry-captured (minified) stack trace plus a link to the source event, FORMBRICKS-CLOUD-3VE. Sentry did not just record this crash, it reported it.
The SDK calls makeRequest to load a workspace's environment state. That code parsed the HTTP response and immediately read .data off the result:
const json = (await response.json()) as ApiResponse;
// ...
const successResponse = json as ApiSuccessResponse<T>;
return ok(successResponse.data);
Two things go wrong here:
-
response.json()on a body of literalnullreturns JavaScriptnull. Readingnull.datathrowsTypeError: Cannot read properties of null (reading 'data'). The error path had the same problem one line up:errorResponse.codeon anullbody throwsreading 'code'. -
response.json()is not guarded at all. A non-JSON body (an empty response, or an HTML error page from a proxy or CDN) makes it throw an unhandledSyntaxError.
Both happen on the survey load path, in the visitor's browser, where there is no try/catch above them. Any visitor whose environment fetch returns a null or non-JSON body (a partial response, or an HTML error page from a proxy or CDN) hits this: the survey does not just fail to open, the SDK throws and the crash propagates into the host page. This is the exact issue Sentry surfaced as FORMBRICKS-CLOUD-3VE.
The same makeRequest is duplicated in @formbricks/surveys (src/lib/utils.ts) with the identical bug, so I fixed both.
Code
PR (on my fork, merged): https://github.com/lewisawe/formbricks/pull/1
Formbricks' contributing guide says they do not take unsolicited community PRs except in rare cases, and the challenge accepts a fork PR, so I opened and merged this on my own fork instead of adding noise upstream.
Files changed:
packages/js-core/src/lib/common/api.tspackages/js-core/src/lib/common/tests/api.test.tspackages/surveys/src/lib/utils.tspackages/surveys/src/lib/utils.test.ts
The core change:
const response = res.data;
// Guard 1: never let response.json() throw an unhandled SyntaxError.
const parsedJson = await wrapThrowsAsync(() => response.json() as Promise<ApiResponse>)();
if (!parsedJson.ok) {
return err({
code: "network_error",
status: response.status,
message: "Failed to parse response body as JSON",
url,
});
}
const json = parsedJson.data;
if (!response.ok) {
const errorResponse = json as ApiErrorResponse | null;
return err({
code: errorResponse?.code === "forbidden" ? "forbidden" : "network_error",
status: response.status,
message: errorResponse?.message || "Something went wrong",
url,
...(Object.keys(errorResponse?.details ?? {}).length > 0 && { details: errorResponse?.details }),
});
}
// Guard 2: validate the data envelope before reading `.data`.
const successResponse = json as ApiSuccessResponse<T> | null;
if (successResponse === null || successResponse.data === undefined || successResponse.data === null) {
return err({
code: "network_error",
status: response.status,
message: "Received invalid response data from the server",
url,
});
}
return ok(successResponse.data);
My Improvements
I kept the change small and inside the existing error model. makeRequest already returns a Result<T, ApiErrorResponse> and the codebase has a wrapThrowsAsync helper that turns a throwing async call into a Result. I used both so the fix reads like the rest of the file:
-
Parsing can't crash anymore.
response.json()runs throughwrapThrowsAsync. Bad JSON becomes anetwork_errorResult, not an exception. -
The error path is null-safe. Optional chaining on
errorResponsemeans anullerror body no longer throws while trying to build the error. -
The success path validates before reading. A
nullbody, a non-object body, or a missingdatafield returns a typednetwork_errorinstead of throwing. Callers already handle failed Results, so the SDK degrades quietly instead of crashing the page.
For tests I extended the existing suites rather than adding new files, matching the project's convention of colocated unit tests:
-
packages/js-core: 11 to 15 tests inapi.test.ts. -
packages/surveys: added amakeRequestblock, 76 to 80 tests inutils.test.ts.
Verification:
- With the fix:
295/295js-core tests pass,80/80surveys utils tests pass,tscand ESLint are clean, andturbo buildbuilds both packages. - Run against the actual unmodified Formbricks module (I reverted the fix with
git stashand re-ran), the four new tests fail with exactly the reported errors:Cannot read properties of null (reading 'data'),reading 'code', and the unhandledSyntaxError. This is the definitive proof, it pins the real bug in the real code, not a rephrasing. The Sentry harness below uses a faithful standalone copy of that same function so it can run without building the monorepo.
Best Use of Sentry
Sentry is what made this bug findable. It was auto-filed by sentry[bot] as issue #6581, linking the source event FORMBRICKS-CLOUD-3VE, and the grouped title, Cannot read properties of null (reading 'data'), is specific enough to trace straight to the .data read in makeRequest.
To confirm the root cause and verify the fix, I built a small Sentry-instrumented harness that reproduces the crash without needing production traffic (the challenge's suggested workflow). It initializes @sentry/node, overrides fetch to return a 200 with a null body, and calls a faithful copy of the pre-fix makeRequest. Running it throws the same TypeError, and Sentry captures it:
=== Running makeRequest in "buggy" mode against a null-body 200 response ===
UNHANDLED CRASH: TypeError: Cannot read properties of null (reading 'data')
[Sentry capture] TypeError: Cannot read properties of null (reading 'data')
The screenshots below are from that harness (the stack frames show buggy-api.mjs), not from Formbricks' production Sentry, which I have no access to. Here is the crash grouped in Sentry:
The stack trace points straight at the unguarded .data read in makeRequest:
I enriched each event with tags and context, so the issue is traceable back to the original report:
Then I ran Seer, Sentry's AI root-cause analysis, on the captured issue. It independently reached the same conclusion I did: makeRequest parses the body and reads .data with no null guard, so a 200 with a null body throws. Seer located the exact line, named the fix (add a null check on the parsed response before reading .data), and summed up the root cause in one line: the client trusts the JSON body is a non-null object, but the server can return null.
After the fix, the formbricks-6581@fixed release runs the same null-body response through makeRequest. The TypeError is gone. But rather than let the failure vanish into silence, I instrument the recovery: the handled path emits a warning-level Sentry event tagged outcome=handled_degrade, so Sentry shows the failure mode as controlled instead of fatal. The same input that was an error-level crash before the fix is now a warning-level "handled" signal:
=== Running makeRequest in "fixed" mode against a null-body 200 response ===
makeRequest returned a Result (no crash): {"ok":false,"error":{"code":"network_error","status":200,...}}
Graceful failure -> code="network_error", message="Received invalid response data from the server"
[Sentry capture] Survey load received invalid response data; degraded gracefully (no crash)
Stated as an invariant in Sentry terms: for this input, makeRequest must never raise an error-level event; at worst it emits a handled warning. That is the difference the fix makes, made observable.
Because the real crash happens in a visitor's browser, I also built a browser reproduction that loads the survey with @sentry/browser and the Replay integration, so Sentry records a Session Replay of the page crashing on survey load. You can watch the exact sequence: the widget starts loading, the null-body response comes back, and the survey fails.
To be precise about the roles: Sentry found this bug (the sentry[bot] issue), Seer explained it, and Session Replay, the trace, and the handled-degrade signal verified the fix. Code inspection and the failing tests are what proved the root cause. I am not claiming Sentry wrote the fix, I am showing where each Sentry tool earned its place in the loop.
Sentry tools used: Error Monitoring (found and grouped the crash), Session Replay (recorded the crash in the browser, the SDK's real environment), Breadcrumbs and custom Tags/Context (traced the event back to issue #6581 / FORMBRICKS-CLOUD-3VE), Distributed Tracing (each call wrapped in a named makeRequest span), and Seer (AI root-cause analysis that independently confirmed the fix).
The reproduction harness (Node and browser) is public here: https://github.com/lewisawe/sentry-repro







Top comments (0)