sentry-form-migration
Migrate JsonForm/FormModel to TanStack form system
# Sentry Form Migration Codemod
Two-step migration from JsonForm/FormModel to the TanStack form system (`useScrapsForm`, `AutoSaveField`).
See [.agents/skills/migrate-frontend-forms/SKILL.md](../../.agents/skills/migrate-frontend-forms/SKILL.md) for the full migration guide.
## Two-Step Workflow
**Step 1** (this codemod) applies **safe, deterministic** edits and leaves **comments/markers** so **Step 2 (AI)** can find each legacy form and finish the TanStack migration. The graph, globs, and the full AI instructions live in [`workflow.yaml`](workflow.yaml) (and [`prompts/form-migration-ai.md`](prompts/form-migration-ai.md) mirrors the AI-facing text—**keep both in sync** when editing instructions).
### Scope: test files and explicit targets
[`workflow.yaml`](workflow.yaml) **excludes** `**/*.spec.*`, `**/*.test.*`, and `**/*.stories.*` from Step 1 when scanning directories (e.g. `-t ./static`). **Default runs skip unit/integration tests and stories.**
If you pass **`-t` to a single file path** (e.g. a `*.spec.tsx`), the CLI may still process that file—Step 1 can add markers to the spec. **Prefer migrating production form code first** (`*.tsx` under views/components, not `*.spec.*`). For tests, Step 2 expects a **test harness** pattern (`useScrapsForm` + `form.AppField`) when wrapping custom controls—not the same checklist as production (`AutoSaveField`, `FormSearch`, `extract-form-fields`). See **Pick the migration context** in [`workflow.yaml`](workflow.yaml) Step 2 or [`prompts/form-migration-ai.md`](prompts/form-migration-ai.md).
### Step 1 — Deterministic (js-ast-grep)
Mechanical transformations, safe to run:
1. **Add `hintText`** — For field config objects that have `help` but not `hintText`, adds `hintText` with the same value.
2. **Migration comments for `showHelpInTooltip`** — Adds `// Migration: use variant="compact" on field.Layout.Row`.
3. **Migration comments for `disabledReason`** — Adds `// Migration: use disabled="<reason>" on field.Input`.
4. **Legacy `Form` marker** — Inserts `{/* Migration: legacy Form — TanStack */}` as the first child inside each `<Form>...</Form>`. Self-closing `<Form />` is expanded to an open/close pair so the marker stays valid JSX. This aligns **touched files** with **metered** `Form` sites (every counted `Form` produces at least this edit unless the marker is already present).
### Metrics (`form-migration`)
One increment per legacy **`<Form>`** imported from `sentry/components/forms/form` (not `jsonForm` or other modules). Cardinality dimensions:
1. **`filepath`** — Source file path.
2. **`migration-ease`** — `easy` or `tricky` only.
3. **`features`** — Sorted `|`-joined tokens for **that** `Form`, or `none`.
Files that do not match the legacy-form text gate emit no metrics. **Before the per-`Form` marker**, only files with hintText / field migration comments changed; metrics still counted every legacy `<Form>`, so the number of **changed files could be far below** the number of files with **form-migration** metrics. The marker step fixes that alignment for `Form`-metered files.
**How features are collected**
- **On the `Form` element:** `mapFormErrors` → `form_mapFormErrors`, `resetOnError` → `form_resetOnError`, `saveOnBlur={false}` → `saveOnBlur_false`. Presence-only shell props → `form_apiEndpoint`, `form_initialData`, `form_model`, `form_onCancel`, `form_onSubmit`, `form_requireChanges`.
- **Under that `Form` in JSX:** inline `fields={...}` / `forms={...}` on descendants (e.g. `JsonForm`, `StyledJsonForm`). Nested legacy `<Form>` tags are skipped so inner forms are not attributed to the outer shell.
- **Single-`Form` files:** any remaining `{ fields: [...] }` form-group object literals in the file (not necessarily under JSX) are merged into that lone `Form`’s tokens and ease (e.g. `forms={helper()}` where the config lives in a `const`).
**Limitations:** `forms={fn()}` / `fields={ident}` with no inline literal are not expanded without semantic analysis; those configs only contribute in the **single-`Form` file** merge above.
**Cardinality collision:** Two `Form`s in the same file with identical `filepath`, `migration-ease`, and `features` merge into one count in aggregated metrics. Use raw exports if you need each instance distinguished.
See [Feature tokens](#feature-tokens) for token meanings.
#### Feature tokens
**Per field config** (if the prop exists): `confirm`, `disabledReason`, `extraHelp`, `formatMessageValue`, `getData`, `mapFormErrors`, `resetOnError`, `saveMessage`, `showHelpInTooltip`, and `saveOnBlur_false` when `saveOnBlur` is `false` on a field.
**On the `Form` JSX element:** `form_mapFormErrors`, `form_resetOnError`, `saveOnBlur_false`, and `form_*` shell tokens above (see “How features are collected”).
**`migration-ease` (tricky)** when any tricky token appears in the merged feature set for that `Form` (same idea as before: custom mutation, errors, save UX, reset behavior, or `saveOnBlur` false on the shell).
CLI output may only summarize a subset of dimensions; full cardinality appears in metric exports / dashboards.
**Future improvement:** emitting a `file_role` (e.g. `spec` vs `prod`) dimension for touched files is not implemented; classification is documented in the Step 2 prompt instead.
### Step 2 — AI-Assisted
Handles transformations that require context and judgment. The prompt **branches by context** (production vs test vs custom `FormField`); read **Pick the migration context** in [`workflow.yaml`](workflow.yaml) or [`prompts/form-migration-ai.md`](prompts/form-migration-ai.md).
**Production (typical settings / PR-style migration):**
- **`getData` → `mutationFn`** — Data transformation in mutation function
- **`mapFormErrors` → `setFieldErrors`** — Error mapping in catch block
- **`saveOnBlur: false` → `useScrapsForm`** — Explicit Save button forms
- **`AutoSaveField`** — Per-field auto-save with `mutationOptions` (e.g. boolean toggles + org PUT)
- **`FormSearch` + `extract-form-fields`** — When the form participates in settings search
**Tests:** use a **TanStack test harness** where appropriate; do not require `AutoSaveField` or field registry extraction unless the test covers that behavior.
**Choosing patterns:**
| Situation | Prefer |
| --- | --- |
| Save-on-blur field + API per field | `AutoSaveField` + Zod + `mutationOptions` |
| Full form submit / Save-Cancel / `saveOnBlur: false` on shell | `useScrapsForm` + `form.AppForm` + validators |
| `*.spec.*` mounting legacy `Form` + `FormModel` for a custom control | `useScrapsForm` harness + `form.AppField` (or defer until the field component migrates) |
| Custom `FormField` still in production | Migrate field + parents together, or align harness with shared transforms |
## Usage
### Run full two-step workflow (recommended)
Runs Step 1 (deterministic) then Step 2 (AI):
```bash
# From repo root
npx codemod@latest workflow run \
-w ./codemods/sentry-form-migration/workflow.yaml \
-t ./static
```
**AI step requires `LLM_API_KEY`.** Without it, the CLI prints `[AI INSTRUCTIONS]` so a coding agent (e.g. Cursor) can complete the migration manually.
### Run Step 1 only (deterministic)
```bash
pnpm dlx codemod@latest jssg run \
--language tsx \
-t ./static \
./codemods/sentry-form-migration/scripts/codemod.ts \
--dry-run \
--allow-dirty
```
Remove `--dry-run` to apply.
### AI prompt reference
The full AI prompt lives in `prompts/form-migration-ai.md` for reference or manual agent handoff.
## Changelog
Release notes: [`CHANGELOG.md`](CHANGELOG.md).
## Tests
```bash
cd codemods/sentry-form-migration
pnpm test
```