Skip to content

[RUM] Add-domain conflict emits an unhandled rejection and drops partial progress — 11 of 11 409s in 30 days landed in 24h, each retried 2–5× #1685

Description

@dawsontoth

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/**/*.tsxgrep -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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

rumFrom real user monitoring where we aim to keep users happy

Type

Fields

Priority

P2

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions