Agent Skills › getsentry/sentry › migrate-breadcrumb-list

migrate-breadcrumb-list

GitHub

将前端页面导航面包屑从旧版组件迁移至新版 BreadcrumbList,拆分父级与当前页标题至 TopBar 不同插槽,并处理过滤器状态保留等逻辑。

.agents/skills/migrate-breadcrumb-list/SKILL.md getsentry/sentry

Trigger Scenarios

migrate breadcrumbs replace sentry/components/breadcrumbs split breadcrumbs into TopBar slots

Install

npx skills add getsentry/sentry --skill migrate-breadcrumb-list -g -y
More Options

Non-standard path

npx skills add https://github.com/getsentry/sentry/tree/master/.agents/skills/migrate-breadcrumb-list -g -y

Use without installing

npx skills use getsentry/sentry@migrate-breadcrumb-list

指定 Agent (Claude Code)

npx skills add getsentry/sentry --skill migrate-breadcrumb-list -a claude-code -g -y

安装 repo 全部 skill

npx skills add getsentry/sentry --all -g -y

预览 repo 内 skill

npx skills add getsentry/sentry --list

SKILL.md

Frontmatter
{
    "name": "migrate-breadcrumb-list",
    "description": "Migrates page-navigation breadcrumbs from the legacy `sentry\/components\/breadcrumbs` component to `@sentry\/scraps\/breadcrumbList`, splitting one flat crumb array across the TopBar `breadcrumbs` and `title` slots. Use when a page still renders `<Breadcrumbs crumbs={...}\/>`, when parent crumbs and the page title need separating into two TopBar slots, when `Layout.Title` double-renders the page name, when a migrated crumb needs to keep the project or date selection, or when working through the BreadcrumbList migration backlog. Trigger on \"migrate breadcrumbs\", \"migrate to BreadcrumbList\", \"replace sentry\/components\/breadcrumbs\", \"split breadcrumbs into TopBar slots\", \"BreadcrumbList.Title\", \"the page title renders twice\", \"preservePageFilters\". Not for event breadcrumbs (`sentry\/types\/breadcrumbs`, the issue-detail timeline), and not for the route-driven SettingsBreadcrumb system."
}

Migrate page breadcrumbs to BreadcrumbList

Migrate $0 (a file, a view directory, or the next unmigrated call site when omitted) off sentry/components/breadcrumbs.

The transformation

Legacy passes one flat array in which the last crumb is the current page (the component strips its to automatically). The new API splits that across two TopBar slots.

// Old — one array, leaf included
<Breadcrumbs crumbs={[{label: t('Monitors'), to: basePath}, {label: monitor.name}]} />

// New — parents in one slot, the current page in the other
<Fragment>
  <TopBar.Slot name="breadcrumbs">
    <BreadcrumbList items={[{type: 'link', label: t('Monitors'), to: basePath}]} />
  </TopBar.Slot>
  <TopBar.Slot name="title">
    <BreadcrumbList.Title item={{type: 'page-title', label: monitor.name}} />
  </TopBar.Slot>
</Fragment>

BreadcrumbList.Title renders no heading. The title outlet already wraps its children in <Heading as="h1"> — verify with grep -n 'Heading as="h1"' static/app/views/navigation/topBar.tsx. Never wrap BreadcrumbList.Title in a Heading, and never nest the breadcrumbs slot inside the title slot. Both mistakes are invisible: the outlet and the title item both use variant="inherit", so a nested heading looks identical and only fails an a11y audit.

⚠️ preservePageFilters survives a spread

The prop does not exist on BreadcrumbItemLinkProps. As a direct literal it is caught:

// error TS2353: 'preservePageFilters' does not exist in type 'LinkBreadcrumbItem'
items={[{type: 'link', label: 'Issues', to: '/issues/', preservePageFilters: true}]}

Through a spread it compiles clean — and spreading legacy crumbs is the migration idiom:

// Compiles. Ships a page that silently drops project/environment/date filters on click.
.map(crumb => ({type: 'link' as const, ...crumb}))

Excess-property checking only applies to fresh object literals, so a Crumb carrying preservePageFilters passes straight through to <Link>, which ignores it. No type error, no failing test. Destructure explicitly instead of spreading, then rebuild the query.

Note the stakes. A crumb that loses the flag does not merely fail to carry filters — it clears them. PageFiltersContainer reconciles its store against the URL on navigation, and an absent project reads as an empty selection rather than "unchanged". Replicating the flag is not polish; skipping it changes what the destination shows.

import {extractSelectionParameters} from 'sentry/components/pageFilters/parse';

// Preserve all six — project, environment, statsPeriod, start, end, utc.
// A legacy `to` is often a bare pathname string; restructure it into an
// object, as there is nowhere to hang a query otherwise.
const preserveAll = {
  pathname: makeReleasesPathname({organization, path: '/'}),
  query: extractSelectionParameters(location.query),
};

// Preserve some — spread, then override. Clearing `start`/`end` is required
// whenever you set `statsPeriod`, or an absolute range and a relative period
// both travel and the destination picks one.
const preserveSome = {
  pathname: makeReleasesPathname({organization, path: '/'}),
  query: {
    ...extractSelectionParameters(location.query),
    statsPeriod: '24h',
    start: undefined,
    end: undefined,
  },
};

Preserve nothing by leaving the bare pathname alone — that is what a crumb without the flag did, and migrating one is not an occasion to start preserving. When the crumb already has a to object, merge rather than replace: {...to, query: {...extractSelectionParameters(location.query), ...to.query}}.

Find the call sites that still pass it: grep -rln "preservePageFilters: true" static/app --include='*.tsx'.

Which API takes what

Accepts Shape
<BreadcrumbList items={...}/> 'link' (label: string, to, leadingGraphic?), 'select-projects' array
<BreadcrumbList.Title item={...}/> 'page-title' (label: string, labelTooltip?, leadingGraphic?, pagination?, trailingActions?), 'editable-title' single object

Title actions are a third union — one object, or an array whose absent entries are null:

trailingActions Required fields
{type: 'copy'} text, label
{type: 'menu'} items, triggerLabel
{type: 'button'} element, typed ReactElement<ButtonProps | LinkButtonProps>

Import is always import {BreadcrumbList} from '@sentry/scraps/breadcrumbList' — an alias onto static/app/components/core/. The barrel exports only BreadcrumbList and the type BreadcrumbTitleItem. type: 'link' requires label: string and a non-null to; leadingGraphic is optional.

Pick the call-site shape

Classify before editing — the shapes need different amounts of work, and three of them touch more than one file.

Shape Pattern Extra work
A1 <Breadcrumbs> inside TopBar.Slot name="title" Pure split of the slots. A JSX leaf label may still need full decomposition — see references/title-item.md. Note the element is sometimes assigned to a variable first (automations/detail.tsx) or aliased through styled(Breadcrumbs) (preprod/install/buildInstallHeader.tsx), so <Breadcrumbs may not appear inside the slot
A2 A wrapper component rendered inside the caller's title slot Two-site edit. The wrapper returns the two-slot Fragment; the caller's wrapping title slot must be deleted, or breadcrumbs nests inside title and parent links render into the <h1>
B <Breadcrumbs> in Layout.HeaderContent beside Layout.Title Fixes a live double-render. Layout.Title wins the title; the displaced leaf is usually a category descriptor (e.g. "Cron Monitor") that wants a real to not yet in the file. Exception below
C Exported wrapper consumed by other pages — only when a consumer wraps it in a title slot; an exported header that owns its own slots is B or E Every consumer changes in the same PR. Per consumer: hoist any surrounding ternary above the slot, delete the wrapping slot, and place or drop every sibling node in that slot
E <Breadcrumbs> beside a raw <Heading as="h1"> Delete the local heading, or the page ships two <h1>s

If no row fits, do not force one. The table describes the headers present when this skill was written. Classify by what the file has — a Layout.Title, a raw heading, a wrapping title slot, an exported wrapper — and follow the closest row. If a header is structured unlike any of them, ask before restructuring it rather than guessing, and add a row here once the shape is settled.

views/performance/breadcrumb.tsx is type-only (import type {Crumb}, no JSX). Its work is deleting a legacy adapter, and it is blocked — see references/call-site-inventory.md.

Shape B has one exception with no leaf crumb at all, where applying "Layout.Title wins" mechanically renders the same text twice — see references/call-site-inventory.md.

Layout.Title is already a shim for TopBar.Slot name="title" (grep -n 'export function Title' static/app/components/layouts/thirds.tsx), which is why Shape B pages double-render today.

Build items

Parents only — the leaf became the title.

// Drop crumbs with no destination: `to` is required and non-nullable.
const items = parents.flatMap(c =>
  c.to ? [{type: 'link' as const, label: c.label, to: c.to}] : []
);

An empty items renders nothing, and that is correct — the title slot still renders. Do not invent a parent link to avoid it. If the legacy code gated the trail on a length check, port the condition.

That does not conflict with Shape B's "give the displaced leaf a to" — they are different crumbs. A leaf that is the page name becomes the title and leaves items. A leaf that is a category descriptor ("Cron Monitor") is a real parent that was merely unlinked: look for a make*Pathname for that category beside the one you already import, and prefer the two-link trail. Drop it only when there is nowhere to point.

Per-page workflow

  1. Read the file. Find the crumb array; note whether its last element has a to.
  2. Classify the shape. For A2/C, grep -rn '<WrapperName' now and list every sibling node in each consumer's title slot — consumers are part of this change, not a follow-up. Read references/call-site-inventory.md only if the file is a wrapper or appears on its hard-rows list; for a plain A1/B/E file the shape table above is enough.
  3. Decide the title before writing anything. In priority order: (1) Layout.Title's content, (2) a raw <Heading as="h1"> sibling, (3) the last crumb. If the header renders no heading and the last crumb has no to, the last crumb is the title. If the choice is gated on a boolean, hoist that boolean above both slots and give each branch its own BreadcrumbList.Title — never migrate one branch. Use editable-title if the name is user-editable, else page-title.
  4. Build the title item. → references/title-item.md
  5. Build items from what remains. Drop the leaf. Drop or re-point crumbs with no to. Replace preservePageFilters. Port any length guard.
  6. Render both slots, breadcrumbs then title. Wrap them in a Fragment when the component returns them directly. Then check what the old wrapper has left: if a Layout.Header or Layout.HeaderContent is now left holding only TopBar.Slot children, delete it. Slots render nothing in place, and Layout.Header is a real <Grid as="header"> with padding and a bottom border — leaving it ships an empty bordered strip above the page body. Keep it only if it still has non-slot children, such as Layout.HeaderTabs.
  7. Delete only what the move orphaned — see the checklist guard below.
  8. Fix the specs. → references/tests.md
  9. Verify: pnpm run typecheck (whole project, takes no paths), .venv/bin/prek run -q --files <files>, pnpm test-ci <spec>, and re-run the count — it must have decreased and still be at or above 3.

Step 3 precedes step 5 because it determines which crumbs are left. Building items first ships the leaf twice and forces a redo of both slots.

References

Open when you need to Read
Know what shape a file is, who consumes a wrapper, or what blocks a file references/call-site-inventory.md
Build a title whose name is more than a plain string — a badge, tooltip, editable field, pagination, or actions references/title-item.md
Write or fix a spec for a migrated page references/tests.md

Reference implementations

Already migrated, simplest first. Each answers one question.

File Answers
views/detectors/components/details/common/header.tsx The minimal two-slot split
views/explore/conversations/components/conversationsBreadcrumbs.tsx Replacing preservePageFilters; a standalone copy action
views/performance/transactionSummary/transactionBreadcrumbs.tsx Mapping a shared crumb builder into typed items; leadingGraphic; a menu with a submenu
views/performance/traceDetails/traceHeader/traceBreadcrumbs.tsx pagination; dropping unlinked crumbs with .flatMap
views/dashboards/dashboardBreadcrumbTitle.tsx editable-title; one early return per page state
views/explore/replays/detail/header/replayDetailsPageBreadcrumbs.tsx The richest title — pagination, leadingGraphic, and a nulled action array together
views/issueDetails/header/issueIdBreadcrumb.tsx Building a title item outside JSX with as const satisfies

Intentionally not migrated

Three importers keep the legacy component — its own spec, plus two call sites that render outside the page <h1> and need a <nav> landmark BreadcrumbList has no mode for. The count floors at 3; driving it to 0 destroys a landmark. The route-driven SettingsBreadcrumb system is out of scope too. Both lists are in references/call-site-inventory.md.

Rollout

The count is the progress state — no scratch file to keep in sync.

grep -rl "from 'sentry/components/breadcrumbs'" static/app | wc -l

Migrate one view area per PR. Split at roughly 50 changed files along ownership boundaries in @.github/CODEOWNERS. Title as ref(<area>): Migrate breadcrumbs to BreadcrumbList, or feat(<area>): ... if page actions moved into the title menu. There is no feature flag — ui-migration-breadcrumbs and useHasNewBreadcrumbs() were deleted, so every page flips for all users on merge. Some older reference PRs still show a flag fork; that pattern is dead code now.

Migration checklist

Each item is a grep or a compile, because every judgment-shaped check here was one an earlier draft got wrong.

  • No preservePageFilters: in the diff, and no spread of legacy crumbs into typed items — grep for preservePageFilters: (with the colon: a bare grep also matches the comment explaining the replacement, so it flags a correct migration) and for ...crumb inside a .map that produces type: 'link'. A spread compiles and silently drops page filters.
  • No wrapper is left holding only TopBar.Slot children — slots render nothing in place, so a surviving padded/bordered Layout.Header becomes an empty strip above the page body.
  • The file has exactly one TopBar.Slot name="title", zero as="h1", zero Layout.Title — a survivor of any of these double-renders the page name or ships two <h1>s.
  • No TopBar.Slot name="breadcrumbs" inside a name="title" subtree — nesting renders parent links into the page heading, and looks correct because both use variant="inherit".
  • Every type: 'link' has a real to. An empty items is fine and renders nothing; an invented parent link is not.
  • No useHasNewBreadcrumbs or ui-migration-breadcrumbs — both are dead repo-wide, so a flag fork can never render.
  • label is a plain string. Stringifying a JSX label deletes the badge, tooltip, or copy affordance instead of moving it.
  • Every IdBadge/ProjectBadge in leadingGraphic passes disableLink — the slot is aria-hidden, and IdBadge/ProjectBadge render a focusable <a href> without it, putting a tabbable link inside a hidden subtree. Typechecks, passes tests, fails an axe audit.
  • leadingGraphic avatars pass avatarSize={16} with a <Placeholder width="16px" height="16px"/> fallback — the slot is a fixed 16×16, so 28px clips and a missing graphic shifts the title as data arrives.
  • A title item built outside JSX ends as const satisfies BreadcrumbTitleItem — otherwise type widens to string and the union stops narrowing.
  • Array trailingActions use null for absent entries; a lone action is a bare object, not a one-element array.
  • Every touched spec mounts <TopBar /> inside <TopBar.Slot.Provider>, or declares a breadcrumbs outlet — a spec stubbing only title/actions/feedback renders no parent crumbs and fails as if the component were broken.
  • The leaf is asserted absent from the trail, not just present as the heading.
  • The count decreased and is still at or above 3.

Overflow collapse is deliberately absent from this list — jsdom never evaluates container queries. See references/tests.md.

Version History

  • 991ee88 Current 2026-09-28 21:59
  • 2f300b6 2026-09-23 11:33

Same Skill Collection

.agents/skills/bump-sentry-dependency/SKILL.md
.agents/skills/cmdk-actions/SKILL.md
.agents/skills/design-system/SKILL.md
.agents/skills/feature-flags/SKILL.md
.agents/skills/frontend-data-fetching/SKILL.md
.agents/skills/generate-frontend-forms/SKILL.md
.agents/skills/generate-migration/SKILL.md
.agents/skills/generate-snapshot-tests/SKILL.md
.agents/skills/hybrid-cloud-rpc/SKILL.md
.agents/skills/hybrid-cloud-test-gen/SKILL.md
.agents/skills/lint-fix/SKILL.md
.agents/skills/lint-new/SKILL.md
.agents/skills/migrate-container-queries/SKILL.md
.agents/skills/migrate-frontend-forms/SKILL.md
.agents/skills/notification-platform/SKILL.md
.agents/skills/react-component-documentation/SKILL.md
.agents/skills/react-testing/SKILL.md
.agents/skills/scraps-review/SKILL.md
.agents/skills/seer-embed/SKILL.md
.agents/skills/sentry-backend-bugs/SKILL.md
.agents/skills/sentry-javascript-bugs/SKILL.md
.agents/skills/sentry-security/SKILL.md
.agents/skills/analytics/SKILL.md
.agents/skills/backend-conventions/SKILL.md
.agents/skills/cell-architecture/SKILL.md
.agents/skills/django-models/SKILL.md
.agents/skills/hybrid-cloud-outboxes/SKILL.md
.agents/skills/remove-option-or-flag/SKILL.md
.agents/skills/setup-dev/SKILL.md

Metadata

Files
0
Version
991ee88
Hash
633b777c
Indexed
2026-09-23 11:33

inicio - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-10-04 19:39
浙ICP备14020137号-1