Skip to content

Commit 12875e2

Browse files
authored
Merge branch 'master' into fix/cron-monitor-interval-bug
2 parents eb46ec6 + c91a49a commit 12875e2

2,587 files changed

Lines changed: 122511 additions & 44094 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
---
2+
name: migrate-container-queries
3+
description: Guide for migrating viewport media queries (@media, useMedia) to container queries in Sentry's frontend. Use when migrating responsive layout to container queries, replacing @media/useMedia, refactoring styled responsive components to Container/Flex/Grid primitives, or working on the DE container-query migration.
4+
---
5+
6+
# Container Query Migration Guide
7+
8+
Migrate viewport-based responsive logic (`@media` + `useMedia`) to container queries so components respond to their own available space instead of the raw viewport.
9+
10+
> **Always do a visual check.** After every migration, resize the _element_ (not just the window) and confirm the layout is identical and flips at the intended width. A good way to narrow an element without touching the window is to open a resizable panel next to it — e.g. drag out the Seer explorer sidebar, which squeezes the middle content. The token scales differ, so a mechanical swap that compiles can still render wrong.
11+
12+
## Approach: refactor first, swap second
13+
14+
Stop at the first rung that fits. Prefer replacing hand-rolled CSS with primitives over a mechanical token swap.
15+
16+
| Rung | When | Do |
17+
| ---------------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
18+
| 1. Primitive props | The `@media` only flips layout (`flex-direction`, `display`, `grid-template`, gap, visibility, width) | Delete the styled component; use `Container`/`Flex`/`Grid`/`Stack` responsive props (`direction={{xs: 'column', md: 'row'}}`) |
19+
| 2. `@container` swap | CSS can't be a prop (descendant selectors, pseudo-elements, `font-size`, complex `grid-template-areas`) | Keep the styled component; swap `@media``@container`, `theme.breakpoints.*``theme.container.*` |
20+
| 3. Container-scoped JS | Width is read in JS to branch rendering | Replace `useMedia(...)` with `useResponsivePropValue({...})` for a threshold boolean, or `useContainerBreakpoint()` to branch on the active key |
21+
| 4. Leave as `useMedia` | Genuine media feature, not width | Do nothing — these do not migrate |
22+
23+
## ⚠️ Convert to the nearest container scale
24+
25+
Breakpoint and container scales have **different keys and different pixel values** — this is not a rename. **MAP BY PIXEL VALUE, NOT BY KEY:** `breakpoints.sm` does NOT become `container.sm`. Reusing the same key is the #1 migration bug.
26+
27+
`theme.breakpoints` (viewport / `@media`), base `2xs`:
28+
29+
| `2xs` | `xs` | `sm` | `md` | `lg` | `xl` | `2xl` |
30+
| ----- | ----- | ----- | ----- | ------ | ------ | ------ |
31+
| 0px | 500px | 800px | 992px | 1200px | 1440px | 2560px |
32+
33+
`theme.container` (container / `@container`), base `zero`:
34+
35+
| `zero` | `3xs` | `2xs` | `xs` | `sm` | `md` | `lg` | `xl` | `2xl` | `3xl` | `4xl` | `5xl` |
36+
| ------ | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ------ | ------ | ------ |
37+
| 0px | 320px | 384px | 448px | 512px | 576px | 640px | 768px | 896px | 1024px | 1152px | 1280px |
38+
39+
**Rule:** take the old breakpoint's pixel value and pick the `container` token whose pixel value is _nearest_ to it — not the token with the same name. `breakpoints.sm` is 800px, so it maps to `container.xl` (768px), not `container.sm` (512px). Then confirm with a visual check: the container is often narrower than the viewport, so the nearest-px token is a starting point, not a guarantee.
40+
41+
## Genuine viewport width → `screen:` keys, not `useMedia`
42+
43+
When layout truly must follow the _window_ (not the component's room), don't keep `useMedia` — use a `screen:`-prefixed responsive prop, which resolves against the viewport on the `theme.breakpoints` scale: `direction={{zero: 'column', 'screen:lg': 'row'}}`. Bare keys and `screen:` keys can mix on one prop. Prefer bare (container) keys; reach for `screen:` only when the viewport genuinely drives the layout.
44+
45+
## Keep `useMedia` only for non-width media features
46+
47+
Width — container or viewport — has a prop/hook path above. Leave `useMedia` in place only for:
48+
`prefers-color-scheme`, `prefers-reduced-motion`, `hover`, `pointer`, `max-height` / height-based, `resolution`, `print`.
49+
50+
## container-type: only when no query container is in scope
51+
52+
**Default: don't add one.** Bare keys and `@container` already resolve against the nearest ancestor container, and product views have one: `ContentStack` (`#main`, `views/organizationLayout/index.tsx`) wraps the routed `<Outlet />` with `containerType="inline-size"`; `topBar` and `#modal-portal` cover their own subtrees. Add `container-type` only when a subtree must respond to _its own_ width rather than the page's — then:
53+
54+
- Use `inline-size` (width only). `size` also queries height, which collapses content unless height is set elsewhere.
55+
- In a reusable component that may already sit inside a container, make it conditional to avoid a redundant one — `containerType={hasParentQueryContainer ? 'normal' : 'inline-size'}` via `useHasContainerQuery()` (see `components/core/breadcrumbList/breadcrumbList.tsx`).
56+
57+
## Examples
58+
59+
### Rung 1 — styled `@media` → primitive props (preferred)
60+
61+
```tsx
62+
// Old — delete the styled component
63+
const Row = styled('div')`
64+
display: flex;
65+
flex-direction: row;
66+
gap: ${p => p.theme.space.md};
67+
@media (max-width: ${p => p.theme.breakpoints.sm}) {
68+
flex-direction: column;
69+
}
70+
`;
71+
72+
// New
73+
import {Flex} from '@sentry/scraps/layout';
74+
<Flex direction={{xs: 'column', sm: 'row'}} gap="md">
75+
```
76+
77+
### Rung 2 — `@media` → `@container` (when it can't be a prop)
78+
79+
```tsx
80+
// Old
81+
@media (max-width: ${p => p.theme.breakpoints.md}) { ... }
82+
83+
// New — swap at-rule AND scale; md breakpoint (992px) → nearest container token by px
84+
// is 3xl (1024px), NOT theme.container.md by matching key
85+
@container (max-width: ${p => p.theme.container['3xl']}) { ... }
86+
```
87+
88+
### Rung 3 — `useMedia` (width) → container-scoped JS
89+
90+
Both helpers below read the nearest query container (call from a descendant of one) and re-render as it crosses a breakpoint. A single `max-width` boolean is cleanest as a responsive value; reach for the active key only when you branch on the key itself.
91+
92+
```tsx
93+
// Old
94+
const isNarrow = useMedia(`(max-width: ${theme.breakpoints.sm})`);
95+
96+
// New — resolve a responsive boolean against the container, same mobile-first
97+
// cascade as CSS. A max-width query is "on by default, off past the threshold",
98+
// so name only the threshold key. Map by pixel value: breakpoints.sm (800px) →
99+
// nearest container token is xl (768px).
100+
import {useResponsivePropValue} from '@sentry/scraps/layout';
101+
102+
const isNarrow = useResponsivePropValue({zero: true, xl: false});
103+
// below xl → true, at/above xl → false — one key on each side, nothing to enumerate.
104+
```
105+
106+
Reach for `useContainerBreakpoint()` instead only when you branch on the key
107+
itself (e.g. picking one of several layouts), not a single threshold. It returns
108+
the container's active key (`'zero'` … `'5xl'`) — don't compare it with
109+
`=== 'zero'` for a max-width case: that fires only below 320px and drops the
110+
320–768px range the original query treated as narrow.
111+
112+
## Migration Checklist
113+
114+
Took the lowest rung that fits (above). Then verify the gotchas:
115+
116+
- [ ] Mapped to the `container` token with the nearest pixel value, not the same name — e.g. `breakpoints.sm` → `container.xl`, not `container.sm`
117+
- [ ] For width read in JS, used `useResponsivePropValue({...})` for a threshold boolean; reserved `useContainerBreakpoint()` for branching on the key — never `=== 'zero'` to mean "narrow" (that's only <320px)
118+
- [ ] Routed genuine viewport-width cases to `screen:` keys; kept `useMedia` only for non-width media features
119+
- [ ] Added `container-type` only when a subtree needs its own; used `inline-size`
120+
- [ ] Confirmed a query-container ancestor exists (`@container` silently no-ops without one)
121+
- [ ] **Visual check:** resized the element and confirmed identical output flipping at the intended width
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Container Query Migration Guide Specification
2+
3+
## Intent
4+
5+
Provide a reliable, low-risk path for converting viewport-based responsive logic (`@media`, `useMedia`) to container queries, so components respond to their own available width instead of the raw viewport. The dominant failure mode this skill guards against is mapping a breakpoint token to the container token of the same name: the two scales share names but not pixel values.
6+
7+
## Scope
8+
9+
In scope: migrating `@media`/`useMedia` width logic to primitive props, `@container` CSS, `useContainerBreakpoint()`, or `screen:` keys, and deciding when to add `container-type`.
10+
11+
Out of scope: non-width media features (`prefers-*`, `hover`, `pointer`, `resolution`, height-based, `print`) stay on `useMedia`; building the primitives or tokens themselves.
12+
13+
## Non-negotiable Constraints
14+
15+
- Map to the `container` token with the pixel value _nearest_ the old breakpoint's — never the same-named token.
16+
- Always visually verify by resizing the element (the container is often narrower than the viewport, so the nearest-px token is a starting point).
17+
- Route genuine viewport-width cases to `screen:` keys; keep `useMedia` only for non-width media features.
18+
19+
## Sources
20+
21+
- Scraps `Container` story, "Container Queries" section — authoritative for container vs. `screen:` keys, both token scales, `useContainerBreakpoint`, and `container-type` guidance.
22+
- Reference migration PR getsentry/sentry#120315 (trace-view).
23+
- `components/core/breadcrumbList/breadcrumbList.tsx` — conditional `container-type` pattern.
24+
25+
## Known Limitations
26+
27+
- The nearest-px token is only a starting point; the true reflow width needs a browser visual check the skill cannot perform.
28+
- `@container` silently no-ops without a query-container ancestor; the skill flags this but cannot detect it statically.
29+
30+
## Maintenance
31+
32+
- Update `SKILL.md` when token scales change, primitives gain/lose props, or a rung is added.
33+
- Update `SPEC.md` when intent, scope, the non-negotiable constraints, or the sources change.

.agents/skills/seer-embed/SKILL.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
---
2+
name: seer-embed
3+
description: Add a new Seer embed widget — a rich component rendered inline in Seer's markdown output via tag syntax. Covers schema, component, registration, and backend codegen. Use when asked to "add an embed", "new seer embed", "create a seer widget", "add a markdown widget", "new seer tag", or "embed widget".
4+
---
5+
6+
# Add a Seer Embed
7+
8+
Seer embeds are rich widgets rendered inline in Seer's markdown output using Markdoc-style tag syntax (`{% name %}{ ... }{% /name %}`). Each embed has a Zod schema, a React component, and a registry entry.
9+
10+
## Before You Start
11+
12+
1. Read `static/app/components/seer/markdown/embeds/schemas.ts` to see existing schemas.
13+
2. Read `static/app/components/seer/markdown/embeds/index.ts` to see registered embeds.
14+
3. Confirm the embed name doesn't already exist.
15+
16+
## Step 1: Add the Schema
17+
18+
In `static/app/components/seer/markdown/embeds/schemas.ts`, add an entry to `SEER_EMBED_SCHEMAS`:
19+
20+
```ts
21+
export const SEER_EMBED_SCHEMAS = {
22+
// ...existing entries
23+
24+
myEmbed: {
25+
description:
26+
"One sentence describing what this embed does—this passes through directly to the LLM's system prompt.",
27+
level: ['inline'], // 'inline', 'block', or both
28+
schema: z.object({
29+
// Define the data shape the LLM will produce
30+
someField: z.string(),
31+
optionalField: z.number().optional(),
32+
}),
33+
examples: [{label: 'Basic', data: {someField: 'hello'}}],
34+
// featureFlag: 'organizations:seer-explorer-my-embed', // optional
35+
},
36+
} as const satisfies Record<string, SeerEmbedSchema>;
37+
```
38+
39+
**Key decisions:**
40+
41+
- **`description`**: Write for the LLM — it uses this to decide when to emit the embed. Be specific about the use case.
42+
- **`level`**: Use `['inline']` for widgets that flow within text (timestamps, badges). Use `['block']` for widgets that need their own line (cards, charts). Use both if the embed adapts.
43+
- **`schema`**: Use Zod. Keep it flat and simple — the LLM has to produce valid JSON. Use `.default()` for optional fields with sensible defaults. Use `.enum()` to constrain string values.
44+
- **`examples`**: An array of `{label, data, level?}` objects. Each `data` must be valid against the schema. These are included in the generated JSON sent to the LLM as few-shot examples. In the stories page, all examples for an embed are composed into a single markdown block and rendered through one `<SeerMarkdown>` — inline examples are wrapped in prose text, block examples are appended at the end. Use multiple examples to show different prop combinations or block vs inline rendering. Set `level` on an example only when it differs from the schema's default (first entry in `level`).
45+
- **`featureFlag`**: Set this to gate the embed behind a feature flag. The backend filters it out of the schema sent to the LLM when the flag is off.
46+
47+
## Step 2: Create the Component
48+
49+
Create `static/app/components/seer/markdown/embeds/components/<name>.tsx`:
50+
51+
```tsx
52+
import {defineSeerEmbed} from 'sentry/components/seer/markdown/embeds/utils';
53+
54+
export const MyEmbed = defineSeerEmbed({
55+
name: 'myEmbed', // must match the key in SEER_EMBED_SCHEMAS
56+
render({someField, optionalField}) {
57+
// Props are typed from the Zod schema — already validated
58+
return <span>{someField}</span>;
59+
},
60+
});
61+
```
62+
63+
**What `defineSeerEmbed` does for you:**
64+
65+
- Looks up the Zod schema by name
66+
- `safeParse`s the `data` prop against it
67+
- Returns `null` for invalid data (logs a warning in dev)
68+
- Sets `displayName` on the component (used by the registry)
69+
70+
**Rules:**
71+
72+
- The `name` parameter **must** match the key in `SEER_EMBED_SCHEMAS` exactly.
73+
- The `render` function receives the Zod output type — props are already parsed and validated.
74+
- Keep the component simple. Import existing Sentry components (`DateTime`, `TimeSince`, `Link`, etc.) rather than building from scratch.
75+
- The component receives no context about where it appears — it only gets the data from the tag body.
76+
77+
## Step 3: Register the Component
78+
79+
In `static/app/components/seer/markdown/embeds/index.ts`, import and add it to the `embeds` array:
80+
81+
```ts
82+
import {MyEmbed} from './components/myEmbed';
83+
import {Timestamp} from './components/timestamp';
84+
import {SeerEmbedRegistry} from './registry';
85+
86+
const embeds = [Timestamp, MyEmbed];
87+
for (const embed of embeds) {
88+
SeerEmbedRegistry.register(embed.displayName, embed);
89+
}
90+
```
91+
92+
Registration uses `displayName` (set by `defineSeerEmbed`) as the registry key.
93+
94+
## Step 4: Regenerate Backend Schema
95+
96+
Run the codegen script to update the JSON Schema file the backend sends to the Seer agent:
97+
98+
```bash
99+
pnpm gen:embed-widgets
100+
```
101+
102+
This writes to `src/sentry/seer/agent/embed_widgets.generated.json`. **Commit this generated file** — it's checked in, not gitignored.
103+
104+
## Step 5: Verify
105+
106+
1. **Lint**: Run `pnpm run lint:js` on your new files.
107+
2. **Types**: Run `pnpm run typecheck` to confirm the schema types flow through.
108+
3. **Manual test**: In the Seer Explorer, trigger a response that would use your embed. Or test directly:
109+
110+
```tsx
111+
<SeerMarkdown raw={`{% myEmbed %}{"someField":"hello"}{% /myEmbed %}`} />
112+
```
113+
114+
## File Summary
115+
116+
| File | What to do |
117+
| ------------------------------------------------------------------ | --------------------------------------- |
118+
| `static/app/components/seer/markdown/embeds/schemas.ts` | Add Zod schema entry |
119+
| `static/app/components/seer/markdown/embeds/components/<name>.tsx` | Create component with `defineSeerEmbed` |
120+
| `static/app/components/seer/markdown/embeds/index.ts` | Import and register |
121+
| `src/sentry/seer/agent/embed_widgets.generated.json` | Regenerated by `pnpm gen:embed-widgets` |
122+
123+
## Optional: Feature Flag
124+
125+
If the embed should be gated:
126+
127+
1. Add `featureFlag: 'organizations:seer-explorer-<name>'` to the schema entry.
128+
2. Register the flag in `src/sentry/features/temporary.py`.
129+
3. The backend (`src/sentry/seer/agent/embed_widgets.py`) automatically filters flagged embeds using `features.has()`.

.github/CODEOWNERS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,7 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get
577577

578578
## SDK
579579
/src/sentry/utils/sdk.py @getsentry/team-web-sdk-backend
580+
/src/sentry/utils/tracing.py @getsentry/team-web-sdk-backend
580581
/src/sentry/build/ @getsentry/team-web-sdk-backend
581582
/src/sentry/api/endpoints/source_map_debug.py @getsentry/team-javascript-sdks
582583
/src/sentry/web/frontend/setup_wizard.py @getsentry/team-javascript-sdks
@@ -601,6 +602,8 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get
601602
/src/sentry/relay/config/ai_model_costs.py @getsentry/telemetry-experience
602603
/src/sentry/tasks/ai_agent_monitoring.py @getsentry/telemetry-experience
603604
/tests/sentry/tasks/test_ai_agent_monitoring.py @getsentry/telemetry-experience
605+
/src/sentry/ai_monitoring/ @getsentry/telemetry-experience
606+
/tests/sentry/ai_monitoring/ @getsentry/telemetry-experience
604607
/static/app/actionCreators/metrics.tsx @getsentry/telemetry-experience
605608
/static/app/views/settings/dynamicSampling/ @getsentry/telemetry-experience
606609
/static/app/views/insights/pages/agents/ @getsentry/telemetry-experience

.github/actions/setup-node-pnpm/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ runs:
1414
id: nodemodulescache
1515
with:
1616
path: node_modules
17-
key: ${{ runner.os }}-node-modules-${{ hashFiles('pnpm-lock.yaml', 'api-docs/pnpm-lock.yaml', '.node-version') }}
17+
key: ${{ runner.os }}-node-modules-${{ hashFiles('pnpm-lock.yaml', '.node-version') }}
1818

1919
- name: Install Javascript Dependencies
2020
if: steps.nodemodulescache.outputs.cache-hit != 'true'

0 commit comments

Comments
 (0)