Skip to content

feat(forms): Migrate Sentry App schema forms behind a flag#114953

Merged
priscilawebdev merged 26 commits into
masterfrom
priscila/feat/sentry-app-form-flag-gating
May 12, 2026
Merged

feat(forms): Migrate Sentry App schema forms behind a flag#114953
priscilawebdev merged 26 commits into
masterfrom
priscila/feat/sentry-app-form-flag-gating

Conversation

@priscilawebdev
Copy link
Copy Markdown
Member

@priscilawebdev priscilawebdev commented May 6, 2026

Migrate the Sentry App schema-backed forms to BackendJsonSubmitForm, behind the organizations:sentry-app-schema-form-migration flag.

SentryAppExternalForm becomes a function component and renders via BackendJsonSubmitForm. Both consumers (SentryAppExternalIssueForm, SentryAppRuleModal) keep the same Sentry App schema form path.

Adds a customAsyncQueryOptions prop on BackendJsonSubmitForm so consumers can override the default URL-based async-select fetcher. The Sentry App external-requests endpoint expects {uri, query, dependentData}, which doesn't fit the adapter's built-in shape.

The new implementation lives at sentryAppExternalForm.new.tsx; the pre-migration class component stays at sentryAppExternalForm.tsx and is gated behind the flag. Sentry.captureException is added to the new form's two user-visible failure paths (save mutation onError, dependent-field initialization on open) for rollout observability.

closes https://linear.app/getsentry/issue/DE-1055/sentry-app-schema-forms-custom-uris

@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 6, 2026

@github-actions github-actions Bot added the Scope: Frontend Automatically applied to PRs that change frontend components label May 6, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

📊 Type Coverage Diff

Metric Before After Delta
Coverage 93.46% 93.46% ±0%
Typed 134,762 135,016 🟢 +254
Untyped 9,431 9,443 🔴 +12
🔍 12 new type safety issues introduced

any-typed symbols (5 new)

File Line Detail
static/app/views/alerts/rules/issue/sentryAppRuleModal.tsx 14 response (param)
static/app/views/alerts/rules/issue/sentryAppRuleModal.tsx 43 formConfig (var)
static/app/views/settings/organizationIntegrations/sentryAppExternalForm.new.tsx 437 response (var)
static/app/views/settings/organizationIntegrations/sentryAppExternalForm.new.tsx 518 results (var)
static/app/views/settings/organizationIntegrations/sentryAppExternalForm.new.tsx 537 result (var)

Non-null assertions (!) (1 new)

File Line Detail
static/app/views/settings/organizationIntegrations/sentryAppExternalForm.new.tsx 498 triggerQueue.shift()!

Type assertions (as) (6 new)

File Line Detail
static/app/components/group/sentryAppExternalIssueForm.tsx 50 as PlatformExternalIssueresponse as PlatformExternalIssue
static/app/components/group/sentryAppExternalIssueForm.tsx 106 as ComponentProps<typeof SentryAppExternalFormNew>['config']config as ComponentProps<typeof SentryAppExternalFormNew>['config']
static/app/views/alerts/rules/issue/sentryAppRuleModal.tsx 57 `as React.ComponentProps<
            typeof SentryAppExternalFormNew
          >['config']` — `formConfig as React.ComponentProps< typeof SentryAppExternalFormNew >['config']` |

| static/app/views/settings/organizationIntegrations/sentryAppExternalForm.new.tsx | 158 | as [string, string][String(value), choiceLabelToString(label)] as [string, string] |
| static/app/views/settings/organizationIntegrations/sentryAppExternalForm.new.tsx | 452 | as Choicesresponse.choices as Choices |
| static/app/views/settings/organizationIntegrations/sentryAppExternalForm.new.tsx | 838 | as [string, string][String(value), choiceLabelToString(label)] as [string, string] |

This is informational only and does not block the PR.

priscilawebdev and others added 8 commits May 6, 2026 13:31
Migrate the Sentry App schema-backed forms (issue-link and
alert-rule-action) off FormModel/FieldFromConfig and onto
BackendJsonSubmitForm. SentryAppExternalForm becomes a function
component; both consumers (SentryAppExternalIssueForm,
SentryAppRuleModal) switch to the named export.

Add a customAsyncQueryOptions prop on BackendJsonSubmitForm so
consumers can override the default URL-based async-select fetcher.
The Sentry App external-requests endpoint expects {uri, query,
dependentData} which doesn't fit the adapter's built-in shape, and
this is the actual consumer the prop was added for.

Add an onValueChange callback so the migration can mirror every
field change into its own ref/state for dependent-field tracking,
distinct from onFieldChange (which only fires for updatesForm fields).

Preserve legacy behavior where it matters: depends_on dependent
field loading and resets, backend-provided default values, saved
labels for async selects, alert-rule submit payload shape, and the
form reset on action prop change.

Defer required-field validation to the form's auto-generated zod
schema. The submit button stays enabled; clicking surfaces inline
per-field errors via aria-invalid + the design-system warning icon,
matching the pattern in ticketRuleModal and externalIssueForm.

Refs DE-1055
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The mutation's onError checked '!(error instanceof Error)', which
was always false because RequestError extends Error — the fallback
toast never ran.

Type the mutation's TError as Error and check
'!(error instanceof RequestError)' so the fallback only fires for
non-RequestError errors. RequestError flows through
BackendJsonSubmitForm's catch (which surfaces the API's detail
message), so this avoids duplicate toasts while still covering
non-RequestError cases that BackendJsonSubmitForm silently swallows.

Refs DE-1055
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drop the (field.choices?.length ?? 0) > 0 guard from
customAsyncQueryOptions. Any select with a uri now goes through the
async path, with field.choices seeded as initialData (shown before
typing) and the uri fetched on typing.

The backend schema validator's oneOf rule already prevents
production Sentry App schemas from declaring both uri and choices,
so this only changes behavior for fixtures and improves coverage
for any future schema that wants both.

Refs DE-1055
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Resolve schema defaults and request payload inputs by value so the initialization effect only reruns when the form semantics change. This avoids clearing dependent field state when callers rerender with equivalent callback and object props.

Memoize the issue-link and alert-rule callers that were recreating those props on every render and add a rerender regression test for dependent field prefetches.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Keep the rerender reset protection in SentryAppExternalForm while moving the issue-link and alert-rule callers back to inline props. This keeps the change focused on the shared form instead of spreading memoization churn across wrappers.

Replace the JSON parse normalization with a small serialized-value memo helper so equivalent object props still compare by value without the extra parse step.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Keep dependent field refreshes from overwriting newer form input by invalidating stale async responses and rebuilding remount values from the latest live form state.

Also normalize async select choice values before caching labels and forward textarea autosize metadata through the adapter so migrated schema forms keep their intended behavior.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Restore the pre-migration class component as sentryAppExternalForm.legacy.tsx
(exported as LegacySentryAppExternalForm) and have the two consumers —
SentryAppExternalIssueForm and SentryAppRuleModal — pick between the
legacy and new form based on the organizations:sentry-app-schema-form-migration
flag.

Add Sentry.captureException to the new form's two failure paths so we can
observe issues during rollout:
  - Save: onError of the create-external-issue mutation
  - Edit: dependent-field initialization (Promise.all of fetchFieldChoices
    when the form opens)

The flag is registered in #114945 and rolled out via
getsentry/sentry-options-automator#7678.

Refs DE-1055
Refs #114945
Refs #112911
Co-Authored-By: Claude <noreply@anthropic.com>
Replace the any escape with the canonical FieldFromSchema type
exported from sentryAppExternalForm.tsx, which has all the fields
the helper reads or assigns (type, default, maxRows, autosize).
@priscilawebdev priscilawebdev force-pushed the priscila/feat/sentry-app-form-flag-gating branch from 106d769 to 9440af7 Compare May 6, 2026 11:32
@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label May 6, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

🚨 Warning: This pull request contains Frontend and Backend changes!

It's discouraged to make changes to Sentry's Frontend and Backend in a single pull request. The Frontend and Backend are not atomically deployed. If the changes are interdependent of each other, they must be separated into two pull requests and be made forward or backwards compatible, such that the Backend or Frontend can be safely deployed independently.

Have questions? Please ask in the #discuss-dev-infra channel.

@priscilawebdev priscilawebdev changed the title feat(forms): gate sentry app form rewrite on flag feat(forms): Migrate Sentry App schema forms behind a flag May 6, 2026
@priscilawebdev priscilawebdev changed the base branch from priscila/ref/backend-json-form-custom-async to master May 6, 2026 11:33
@priscilawebdev priscilawebdev removed the Scope: Backend Automatically applied to PRs that change backend components label May 6, 2026
Restore the four spec files to their master state and drop the new
sentryAppExternalForm.spec.tsx, per request to keep this PR focused on
the migration + flag gating without new test coverage.
Restore sentryAppExternalForm.tsx to its master state byte-for-byte
and move the new BackendJsonSubmitForm-based component to
sentryAppExternalForm.new.tsx (exported as SentryAppExternalFormNew).
Delete sentryAppExternalForm.legacy.tsx.

This makes the diff against master much easier to review: the
existing file is untouched, and the new code stands alone as a
brand-new file.
Three small fixes after swapping the file naming:

  - Drop the FieldFromSchema/SchemaFormConfig type exports from the
    .new.tsx file. Consumers don't need them and knip was flagging
    them as unused exports.
  - Cast config/getFieldDefault/onSubmitSuccess at the SentryAppExternalFormNew
    boundary in both consumers. The legacy file (now master canonical)
    exports its own Field-derived types that don't structurally match
    the .new.tsx narrower types, so explicit casts are needed at the
    call sites that pass legacy-typed values to the new component.
  - Narrow the single-select SelectAsync value/onChange types in
    backendJsonSubmitForm.tsx from string|number to string. The earlier
    rebase merge resolution kept master's wider 'string|number' typing,
    but SelectAsync's value prop is 'string|string[]|null' so the wider
    type didn't compile.
AsyncSelectQueryOptions in BackendJsonSubmitForm:
  - TQueryFnData: any -> Array<SelectValue<string>> (matches what every
    queryFn actually returns)
  - TData: ReadonlyArray<SelectValue<string>> -> Array<SelectValue<string>>
    (no select transformation, so it equals TQueryFnData)
  - TQueryKey: kept as any with a comment explaining why — TanStack's
    contravariant Enabled<...,TQueryKey> rejects any wider type when
    factories build literal-tuple query keys.

sentryAppExternalForm.new.tsx:
  - OnSubmitSuccess.response: any -> unknown
  - ResetValues index signature: any -> unknown

sentryAppExternalIssueForm.tsx:
  - handleSubmitSuccess now takes (response: unknown) and casts to
    PlatformExternalIssue inside, since the form no longer hands back
    a typed response.
@priscilawebdev
Copy link
Copy Markdown
Member Author

bugbot run

Both FieldFromSchema variants (legacy and new) share the few fields
getFieldDefault actually reads and writes. Narrow the parameter to
that minimal shape so the same function satisfies both form props via
contravariance, and drop the ComponentProps cast at the call site.
@priscilawebdev
Copy link
Copy Markdown
Member Author

bugbot run

Comment thread static/app/views/settings/organizationIntegrations/sentryAppExternalForm.new.tsx Outdated
choicesByField was spreading asyncOptionsCache after lookup, so any
field the user had searched lost its initial choices entirely. The
async cache only stores the last non-empty search and is never cleared,
so after a user searched, cleared the input, then selected an initial
option, handleSubmit could not resolve a label for it — the saved
setting omitted the label and the alert rule modal redisplayed a raw
value on reopen.

Union the two per-field instead, with initial choices first and any
search results that aren't already represented appended after.
@priscilawebdev
Copy link
Copy Markdown
Member Author

bugbot run

Comment thread static/app/views/settings/organizationIntegrations/sentryAppExternalForm.new.tsx Outdated
When action toggles between create and link, the parent typically
swaps to a different schema and resolvedFieldGroups changes — but if
the two schemas happen to be structurally identical, the serialized
memo keeps the same reference and the reset effect never fires.
formKey still flips (it includes action), so BackendJsonSubmitForm
remounts, but currentFormValuesRef and the other state mirrors keep
the previous action's values, poisoning dependentData on the next
cascade fetch.

Add action to the reset effect's deps so the form's identity
(action × config × element) drives the reset, matching the legacy
class component's componentDidUpdate behavior.
@priscilawebdev
Copy link
Copy Markdown
Member Author

bugbot run

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ffff2c7. Configure here.

handleFieldChange only fetched fields directly listed in the changed
trigger's depends_on. If field B got a default value from that fetch
and field C had B in its depends_on, C never refetched — so chained
schemas (A → B → C) silently degraded compared to the legacy form,
which cascaded naturally because each model.setValue fired its own
onFieldChange.

Walk the dependency graph BFS-style: each level fetches its dependent
fields; any field that comes back with a defaultValue is queued as
the trigger for the next level. processedTriggers and fetchedFields
guard against diamonds and cycles, and a snapshot of working state
per iteration keeps the in-loop closure safe under no-loop-func.
manual QA on the new sentry app schema form surfaced several bugs the
existing single-test suite did not catch. fix them together so the
migration ships with working cascade, loading, and error UX:

- cascade transitively on mount, not just on click. extract the BFS
  into a shared cascadeFetchDependents helper used by both the click
  cascade and the mount-time prefetch so chained schemas (A -> B -> C)
  populate when a parent has a baked-in default.
- preserve the user's just-picked trigger value in the click cascade
  by only applying newly-resolved defaults to fields that were
  actually impacted, instead of spreading the full defaultValues over
  the user's choice.
- drop the staging step in handleFieldChange. it triggered a mid-flight
  remount that bumped dependentFetchVersionRef and silently aborted
  the cascade after the dependent fetch returned.
- split the dependent-fetch flag into isFetchingInitialCascade (drives
  the whole-form LoadingIndicator) and isFetchingDependentFields
  (only disables submit during click cascades) so clicking a select
  no longer blanks the form.
- preserve saved/initial values through the mount cascade in both the
  BFS (don't overwrite workingValues[name] when it already has a
  value) and the prefetch effect (use cascadeResult.values directly
  instead of spreading defaultValues over nextInitialValues). fixes
  alert-rule-action settings round-tripping from the DB.
- surface a toast when the mount cascade fails. previously the failure
  was silent and only logged to Sentry.
- rewrite the error toast in plain language: "Couldn't load options
  for some fields." (was "Unable to load dependent options.")

Co-Authored-By: Claude <noreply@anthropic.com>
textareas without a schema `default` skipped maxRows + autosize
normalization because cloneSchemaFields only invoked getFieldDefault
(where the mutation lived as a side effect) when `default` was set.
the result: textarea fields with a default rendered with maxRows=10
and autosize, but a sibling textarea without `default` rendered
unbounded — inconsistent within the same form.

apply the textarea defaults inside cloneSchemaFields itself so they
land regardless of whether the field has a `default`. honor any
schema-provided maxRows/autosize values when present. side effect of
getFieldDefault is now redundant for the new form but left in place
so the legacy form (still flag-gated and being deprecated) keeps its
prior behavior.

also fixes the alert-rule-action path, where no getFieldDefault is
passed and textareas previously never got these defaults.

Co-Authored-By: Claude <noreply@anthropic.com>
the cascade BFS fetches issue-types only after the boards fetch
resolves with a defaultValue, so waiting on the transitive call
implies the parent already ran. one waitFor + a synchronous
assertion is enough, with the bonus that a timeout points
unambiguously at the transitive step the test is exercising.

Co-Authored-By: Claude <noreply@anthropic.com>
@priscilawebdev priscilawebdev marked this pull request as ready for review May 11, 2026 08:55
@priscilawebdev priscilawebdev requested review from a team as code owners May 11, 2026 08:55
Copy link
Copy Markdown
Collaborator

@TkDodo TkDodo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let’s ship it 🚢

@priscilawebdev priscilawebdev merged commit 60182dd into master May 12, 2026
77 checks passed
@priscilawebdev priscilawebdev deleted the priscila/feat/sentry-app-form-flag-gating branch May 12, 2026 06:11
nikkikapadia pushed a commit that referenced this pull request May 12, 2026
Migrate the Sentry App schema-backed forms to `BackendJsonSubmitForm`,
behind the
[`organizations:sentry-app-schema-form-migration`](#114945)
flag.

`SentryAppExternalForm` becomes a function component and renders via
`BackendJsonSubmitForm`. Both consumers (`SentryAppExternalIssueForm`,
`SentryAppRuleModal`) keep the same Sentry App schema form path.

Adds a `customAsyncQueryOptions` prop on `BackendJsonSubmitForm` so
consumers can override the default URL-based async-select fetcher. The
Sentry App `external-requests` endpoint expects `{uri, query,
dependentData}`, which doesn't fit the adapter's built-in shape.

The new implementation lives at `sentryAppExternalForm.new.tsx`; the
pre-migration class component stays at `sentryAppExternalForm.tsx` and
is gated behind the flag. `Sentry.captureException` is added to the new
form's two user-visible failure paths (save mutation `onError`,
dependent-field initialization on open) for rollout observability.

closes
https://linear.app/getsentry/issue/DE-1055/sentry-app-schema-forms-custom-uris

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants