What RUM shows
Every rejected Add Domain submit on the cluster Domains page reports twice to Error
Tracking — once handling:handled (the global React Query toast) and once
handling:unhandled — and the affected sessions retry the identical rejected submit 2–5×
before anything succeeds.
Last 24h (@application.id = Studio, prod):
| signal |
count |
POST /Domain/ → 409 (resource events) |
11 |
| …across sessions |
2 |
POST /Domain/ → 201 in the same window |
3 |
RUM errors AxiosError: Request failed with status code 409, error.source:console |
11 |
RUM errors, same fingerprint, error.source:source / handling:unhandled |
11 |
Over 30 days the endpoint has 11 conflicts against 7 creations — and all 11 conflicts landed
in the last 24 hours. Both 409 copies share fingerprint v12.DFEB1F8CB54273FE5271CBBF98B28CFC.
The handled copy's handling stack names the submit path exactly:
HandlingStack: console error
at Oi @ index-D9wn3O9N.js (errorHandler)
at Object.onError @ index-D9wn3O9N.js (mutationCache.onError)
at Ne.execute @ vendor-tanstack-*.js
at async @ index-D9wn3O9N.js (onSubmitClick)
at async @ vendor-ui-*.js (react-hook-form handleSubmit)
The unhandled copy has no handling stack, which is what an escaped promise rejection looks
like.
Per-session sequences (ids truncated, customer identifiers omitted)
session A 00:28:46 POST /Domain/ 409
00:28:59 POST /Domain/ 409 (+13s)
01:30:34 POST /Domain/ 409
01:30:40 POST /Domain/ 409 (+6s)
01:31:06 POST /Domain/ 409 (+26s)
01:45:15 POST /Domain/ 409
01:46:09 POST /Domain/ 201 <- finally
session B 05:36:20 DELETE /Domain/{id} 204
05:37:43 POST /Domain/ 409
05:37:49 POST /Domain/ 409 (+6s)
05:38:06 POST /Domain/ 409
05:38:10 POST /Domain/ 409 (+4s)
05:39:20 POST /Domain/ 409
05:39:33 POST /Domain/ 201 <- finally
Six-second gaps between identical rejected submits are a user pressing Add again because
nothing on screen told them what to do.
Why (two defects in the same handler)
src/features/cluster/domains/Management.tsx:107-124
const onSubmitClick = useCallback(
async (formData: z.infer<typeof AddOrganizationDomainSchema>) => {
if (formData) {
const domains = formData.domain.split(/[,\s]+/).map((d: string) => d.trim()).filter(Boolean);
for (const domain of domains) {
await addDomain({ ...formData, domain }); // <- mutateAsync, no catch
}
form.reset();
await refetch();
toast.success(`… added! Please add the txt record above …`);
}
},
[addDomain, form, refetch],
);
1. The rejection escapes the handler. addDomain is mutateAsync, so a 409 rejects. Nothing
catches it, and react-hook-form re-throws it after recording the failure — verified in the
installed dist, react-hook-form@7.86.0, dist/index.esm.mjs:3222-3224:
if (onValidError) {
throw onValidError;
}
so the rejection leaves the DOM submit handler entirely → unhandled RUM error. That is
crash-class signal in Error Tracking for something the app has already handled with a toast one
frame earlier, and it doubles this family's error volume.
This is the only form in the app that awaits a mutateAsync inside handleSubmit
(grep -l handleSubmit src/**/*.tsx ∩ grep -l mutateAsync → one file), so the fix has no
sibling surfaces to keep in step.
2. Partial progress is silently discarded. The loop awaits sequentially and aborts on the
first rejection, so when a submit carries several domains and one conflicts:
- the domains created before the conflict exist on the server, but
form.reset(), await refetch(), and the success toast are all skipped.
The table therefore never shows what was just created, the input keeps the whole original string,
and the only feedback is a generic Conflict toast that fades. Retrying re-submits the entire
string — which now conflicts on the first entry too. This is not a hypothetical multi-domain
path: the Add www as well button at
Management.tsx:166-170
rewrites the field to example.com, www.example.com in one click, so two-domain submits are the
suggested flow for an apex domain.
The conflict text also never reaches the input. describeError splits "Conflict: domain already exists" into a toast title + description; the form has a FormMessage slot directly under the
field that only ever renders static help text.
Expected
- A rejected add never produces an
unhandled RUM error.
- A conflict is stated inline, next to the field that caused it, naming which domain conflicted.
- Domains created before a mid-list failure are committed to the UI: list refetched, successful
entries dropped from the input, and the failure reported for only the entries that failed.
Not this issue
Same failure shape as #1612 / #1668 (sign-up 409 with no inline feedback), on a different form.
What RUM shows
Every rejected Add Domain submit on the cluster Domains page reports twice to Error
Tracking — once
handling:handled(the global React Query toast) and oncehandling:unhandled— and the affected sessions retry the identical rejected submit 2–5×before anything succeeds.
Last 24h (
@application.id= Studio, prod):POST /Domain/→ 409 (resource events)POST /Domain/→ 201 in the same windowAxiosError: Request failed with status code 409,error.source:consoleerror.source:source/handling:unhandledOver 30 days the endpoint has 11 conflicts against 7 creations — and all 11 conflicts landed
in the last 24 hours. Both 409 copies share fingerprint
v12.DFEB1F8CB54273FE5271CBBF98B28CFC.The handled copy's handling stack names the submit path exactly:
The unhandled copy has no handling stack, which is what an escaped promise rejection looks
like.
Per-session sequences (ids truncated, customer identifiers omitted)
Six-second gaps between identical rejected submits are a user pressing Add again because
nothing on screen told them what to do.
Why (two defects in the same handler)
src/features/cluster/domains/Management.tsx:107-1241. The rejection escapes the handler.
addDomainismutateAsync, so a 409 rejects. Nothingcatches it, and react-hook-form re-throws it after recording the failure — verified in the
installed dist,
react-hook-form@7.86.0,dist/index.esm.mjs:3222-3224:so the rejection leaves the DOM submit handler entirely →
unhandledRUM error. That iscrash-class signal in Error Tracking for something the app has already handled with a toast one
frame earlier, and it doubles this family's error volume.
This is the only form in the app that
awaits amutateAsyncinsidehandleSubmit(
grep -l handleSubmit src/**/*.tsx∩grep -l mutateAsync→ one file), so the fix has nosibling surfaces to keep in step.
2. Partial progress is silently discarded. The loop
awaits sequentially and aborts on thefirst rejection, so when a submit carries several domains and one conflicts:
form.reset(),await refetch(), and the success toast are all skipped.The table therefore never shows what was just created, the input keeps the whole original string,
and the only feedback is a generic Conflict toast that fades. Retrying re-submits the entire
string — which now conflicts on the first entry too. This is not a hypothetical multi-domain
path: the
Add www as wellbutton atManagement.tsx:166-170rewrites the field to
example.com, www.example.comin one click, so two-domain submits are thesuggested flow for an apex domain.
The conflict text also never reaches the input.
describeErrorsplits"Conflict: domain already exists"into a toast title + description; the form has aFormMessageslot directly under thefield that only ever renders static help text.
Expected
unhandledRUM error.entries dropped from the input, and the failure reported for only the entries that failed.
Not this issue
POST /Domain/{id}/validate→ 400 (7 events, 2 sessions in 24h) is intended control flow — theTXT record has not propagated yet — and
onValidateClickalready renders a specific,actionable toast for it.
POST /Cluster/{id}/operation/404 flood from the same view is [RUM] Cluster /operation 404s 71 → 1,399 (31% of the endpoint) — the ChallengeCertificate 5s poll can never stop because its 404 is treated as success #1630.Same failure shape as #1612 / #1668 (sign-up 409 with no inline feedback), on a different form.