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 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 (and 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 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 Step 2 or prompts/form-migration-ai.md.
Step 1 — Deterministic (js-ast-grep)
Mechanical transformations, safe to run:
- Add hintText — For field config objects that have help but not hintText, adds hintText with the same value.
- Migration comments for showHelpInTooltip — Adds // Migration: use variant="compact" on field.Layout.Row.
- Migration comments for disabledReason — Adds // Migration: use disabled="<reason>" on field.Input.
- 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:
- filepath — Source file path.
- migration-ease — easy or tricky only.
- 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 Forms 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 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 or 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
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
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.
Tests
bash