Skip to content

Commit 5db937c

Browse files
chore: merge master (squash)
Made-with: Cursor
1 parent bf5ce5c commit 5db937c

File tree

1,202 files changed

+36090
-13648
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,202 files changed

+36090
-13648
lines changed
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
---
2+
name: generate-snapshot-tests
3+
description: Generate snapshot test files for Sentry frontend React components. Use when asked to "generate snapshot tests", "add snapshot tests", "create visual snapshots", "write snapshot tests", "add visual regression tests", or "snapshot this component". Accepts an optional component path or name via $ARGUMENTS.
4+
type: workflow-process
5+
---
6+
7+
# Generate Snapshot Tests
8+
9+
Generate a `*.snapshots.tsx` file colocated with a Sentry React component, following the established pattern used by core design system components.
10+
11+
## Step 1: Locate the Component
12+
13+
If `$ARGUMENTS` is provided, treat it as a path or component name. Otherwise ask the user which component to snapshot.
14+
15+
Search strategies:
16+
17+
```
18+
static/app/components/core/<name>/<name>.tsx
19+
static/app/components/core/<name>/index.tsx
20+
static/app/components/<name>.tsx
21+
static/app/components/<name>/index.tsx
22+
```
23+
24+
Use Glob or Grep to find the file if the exact path is unknown.
25+
26+
Read the component source file to understand:
27+
28+
- The component's name and its exported `Props` / `<ComponentName>Props` type
29+
- Union types and enum-like string literals on props (e.g., `variant`, `priority`, `size`)
30+
- Boolean toggle props (e.g., `disabled`, `checked`, `busy`)
31+
- Whether the component is interactive (needs `onChange={() => {}}` or similar no-op handlers)
32+
33+
## Step 2: Determine the Import Path
34+
35+
| Condition | Import style |
36+
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
37+
| Component lives under `static/app/components/core/` AND is published as `@sentry/scraps/<name>` | `import {ComponentName, type ComponentNameProps} from '@sentry/scraps/<name>';` |
38+
| Component lives under `static/app/components/core/` but is NOT in `@sentry/scraps` | `// eslint-disable-next-line @sentry/scraps/no-core-import -- SSR snapshot needs direct import to avoid barrel re-exports with heavy deps`<br>`import {ComponentName, type ComponentNameProps} from 'sentry/components/core/<path>';` |
39+
| All other components | `import {ComponentName, type ComponentNameProps} from 'sentry/components/<path>';` |
40+
41+
To check if a component is in `@sentry/scraps`, look for an existing import using `@sentry/scraps/<name>` in neighboring files, or check if other snapshot files in the same directory use `@sentry/scraps`.
42+
43+
## Step 3: Identify Props to Snapshot
44+
45+
Read the TypeScript props and classify them:
46+
47+
| Prop type | Action |
48+
| --------------------------------------------------------- | -------------------------------------------- |
49+
| Union of string literals (`'sm' \| 'md' \| 'lg'`) | Snapshot each value with `it.snapshot.each` |
50+
| Boolean toggle with visual impact (`disabled`, `checked`) | Snapshot `true` and `false` states |
51+
| Boolean flag with no visual test value | Skip or add a single named snapshot |
52+
| `children` / `className` / `style` / event handlers | Skip — not visually interesting on their own |
53+
54+
Prioritize props that change the component's visual appearance substantially. For interactive components (inputs, toggles), always include disabled/checked states.
55+
56+
## Step 4: Write the Snapshot File
57+
58+
Name the output file `<component-name>.snapshots.tsx`, colocated with the component source file.
59+
60+
### Required imports (always include)
61+
62+
```tsx
63+
import {ThemeProvider} from '@emotion/react';
64+
65+
import {ComponentName, type ComponentNameProps} from '@sentry/scraps/<name>'; // or appropriate path
66+
67+
// eslint-disable-next-line no-restricted-imports -- SSR snapshot rendering needs direct theme access
68+
import {darkTheme, lightTheme} from 'sentry/utils/theme/theme';
69+
70+
const themes = {light: lightTheme, dark: darkTheme};
71+
```
72+
73+
### Core structure
74+
75+
Always wrap in light/dark theme loop:
76+
77+
```tsx
78+
describe('ComponentName', () => {
79+
describe.each(['light', 'dark'] as const)('%s', themeName => {
80+
// ... snapshot cases here
81+
});
82+
});
83+
```
84+
85+
### `it.snapshot.each` — for union prop variants
86+
87+
Use when iterating over multiple values of a single prop:
88+
89+
```tsx
90+
it.snapshot.each<ComponentProps['variant']>(['info', 'warning', 'success', 'danger'])(
91+
'%s',
92+
variant => (
93+
<ThemeProvider theme={themes[themeName]}>
94+
<div style={{padding: 8}}>
95+
<Component variant={variant}>Label</Component>
96+
</div>
97+
</ThemeProvider>
98+
),
99+
variant => ({theme: themeName, variant: String(variant)})
100+
);
101+
```
102+
103+
The third argument to `it.snapshot.each` is the metadata function — include all props that vary in the snapshot. This metadata is used for snapshot naming and diffing.
104+
105+
### `it.snapshot` — for single named snapshots
106+
107+
Use for one-off states (disabled, checked combinations, etc.):
108+
109+
```tsx
110+
it.snapshot('disabled-unchecked', () => (
111+
<ThemeProvider theme={themes[themeName]}>
112+
<div style={{padding: 8}}>
113+
<Component disabled onChange={() => {}} />
114+
</div>
115+
</ThemeProvider>
116+
));
117+
```
118+
119+
Pass metadata as a third argument when it adds useful snapshot context:
120+
121+
```tsx
122+
it.snapshot(
123+
'bold',
124+
() => (
125+
<ThemeProvider theme={themes[themeName]}>
126+
<div style={{padding: 8}}>
127+
<Component bold>Bold text</Component>
128+
</div>
129+
</ThemeProvider>
130+
),
131+
{theme: themeName}
132+
);
133+
```
134+
135+
### Sizing the container
136+
137+
Match container sizing to what makes the component readable:
138+
139+
| Situation | Wrapper |
140+
| ------------------------------ | ---------------------------------------- |
141+
| Default | `<div style={{padding: 8}}>` |
142+
| Width-sensitive (alerts, text) | `<div style={{padding: 8, width: 400}}>` |
143+
| Narrow (icons, small controls) | `<div style={{padding: 8}}>` |
144+
145+
### Interactive components
146+
147+
For components that require event handlers (inputs, checkboxes, radios, switches), pass no-op handlers to satisfy required props:
148+
149+
```tsx
150+
<Component onChange={() => {}} />
151+
<Component checked onChange={() => {}} />
152+
```
153+
154+
## Step 5: Ordering snapshots within the theme loop
155+
156+
Order cases from most impactful to least:
157+
158+
1. Primary variant/priority prop (the most visible visual differentiator)
159+
2. Secondary variant props
160+
3. Size variants
161+
4. State combinations (disabled+unchecked, disabled+checked)
162+
5. Boolean modifiers (bold, italic, etc.)
163+
6. Edge cases and combined props
164+
165+
## Examples
166+
167+
### Simple variant component (Button-style)
168+
169+
```tsx
170+
import {ThemeProvider} from '@emotion/react';
171+
172+
import {Button, type ButtonProps} from '@sentry/scraps/button';
173+
174+
// eslint-disable-next-line no-restricted-imports -- SSR snapshot rendering needs direct theme access
175+
import {darkTheme, lightTheme} from 'sentry/utils/theme/theme';
176+
177+
const themes = {light: lightTheme, dark: darkTheme};
178+
179+
describe('Button', () => {
180+
describe.each(['light', 'dark'] as const)('%s', themeName => {
181+
it.snapshot.each<ButtonProps['priority']>([
182+
'default',
183+
'primary',
184+
'danger',
185+
'warning',
186+
'link',
187+
'transparent',
188+
])(
189+
'%s',
190+
priority => (
191+
<ThemeProvider theme={themes[themeName]}>
192+
<div style={{padding: 8}}>
193+
<Button priority={priority}>{priority}</Button>
194+
</div>
195+
</ThemeProvider>
196+
),
197+
priority => ({theme: themeName, priority: String(priority)})
198+
);
199+
});
200+
});
201+
```
202+
203+
### Interactive component with state combinations (Switch-style)
204+
205+
```tsx
206+
import {ThemeProvider} from '@emotion/react';
207+
208+
import {Switch, type SwitchProps} from '@sentry/scraps/switch';
209+
210+
// eslint-disable-next-line no-restricted-imports -- SSR snapshot rendering needs direct theme access
211+
import {darkTheme, lightTheme} from 'sentry/utils/theme/theme';
212+
213+
const themes = {light: lightTheme, dark: darkTheme};
214+
215+
describe('Switch', () => {
216+
describe.each(['light', 'dark'] as const)('theme-%s', themeName => {
217+
it.snapshot.each<SwitchProps['size']>(['sm', 'lg'])('size-%s-unchecked', size => (
218+
<ThemeProvider theme={themes[themeName]}>
219+
<div style={{padding: 8}}>
220+
<Switch size={size} onChange={() => {}} />
221+
</div>
222+
</ThemeProvider>
223+
));
224+
225+
it.snapshot.each<SwitchProps['size']>(['sm', 'lg'])('size-%s-checked', size => (
226+
<ThemeProvider theme={themes[themeName]}>
227+
<div style={{padding: 8}}>
228+
<Switch checked size={size} onChange={() => {}} />
229+
</div>
230+
</ThemeProvider>
231+
));
232+
233+
it.snapshot('disabled-unchecked', () => (
234+
<ThemeProvider theme={themes[themeName]}>
235+
<div style={{padding: 8}}>
236+
<Switch disabled onChange={() => {}} />
237+
</div>
238+
</ThemeProvider>
239+
));
240+
241+
it.snapshot('disabled-checked', () => (
242+
<ThemeProvider theme={themes[themeName]}>
243+
<div style={{padding: 8}}>
244+
<Switch checked disabled onChange={() => {}} />
245+
</div>
246+
</ThemeProvider>
247+
));
248+
});
249+
});
250+
```
251+
252+
### Component with multiple independent variant props (Alert-style)
253+
254+
When a component has multiple meaningful boolean or variant props that combine independently, add separate `it.snapshot.each` blocks per combination:
255+
256+
```tsx
257+
describe('Alert', () => {
258+
describe.each(['light', 'dark'] as const)('%s', themeName => {
259+
// Primary variants
260+
it.snapshot.each<AlertProps['variant']>([
261+
'info',
262+
'warning',
263+
'success',
264+
'danger',
265+
'muted',
266+
])(
267+
'%s',
268+
variant => (
269+
<ThemeProvider theme={themes[themeName]}>
270+
<div style={{padding: 8, width: 400}}>
271+
<Alert variant={variant}>This is a {variant} alert</Alert>
272+
</div>
273+
</ThemeProvider>
274+
),
275+
variant => ({theme: themeName, variant: String(variant)})
276+
);
277+
278+
// Modifier combination: same variants but with showIcon={false}
279+
it.snapshot.each<AlertProps['variant']>([
280+
'info',
281+
'warning',
282+
'success',
283+
'danger',
284+
'muted',
285+
])(
286+
'%s-no-icon',
287+
variant => (
288+
<ThemeProvider theme={themes[themeName]}>
289+
<div style={{padding: 8, width: 400}}>
290+
<Alert variant={variant} showIcon={false}>
291+
This is a {variant} alert without icon
292+
</Alert>
293+
</div>
294+
</ThemeProvider>
295+
),
296+
variant => ({theme: themeName, variant: String(variant), showIcon: 'false'})
297+
);
298+
});
299+
});
300+
```
301+
302+
## Anti-Patterns
303+
304+
```tsx
305+
// ❌ Don't import theme from the barrel re-export
306+
import {theme} from 'sentry/utils/theme';
307+
308+
// ✅ Import directly and suppress the lint warning
309+
// eslint-disable-next-line no-restricted-imports -- SSR snapshot rendering needs direct theme access
310+
import {darkTheme, lightTheme} from 'sentry/utils/theme/theme';
311+
```
312+
313+
```tsx
314+
// ❌ Don't omit the metadata argument — snapshot names become ambiguous
315+
it.snapshot.each<Props['variant']>(['a', 'b'])('%s', variant => (
316+
<Component variant={variant} />
317+
));
318+
319+
// ✅ Include metadata that reflects all varying props
320+
it.snapshot.each<Props['variant']>(['a', 'b'])(
321+
'%s',
322+
variant => <Component variant={variant} />,
323+
variant => ({theme: themeName, variant: String(variant)})
324+
);
325+
```
326+
327+
```tsx
328+
// ❌ Don't snapshot implementation-detail props like className or style
329+
it.snapshot('custom-class', () => <Component className="foo" />);
330+
```
331+
332+
```tsx
333+
// ❌ Don't use @sentry/scraps barrel import for components not in the scraps package
334+
import {Badge} from '@sentry/scraps/badge'; // if Badge isn't published there
335+
336+
// ✅ Use the direct path with the no-core-import suppression comment
337+
// eslint-disable-next-line @sentry/scraps/no-core-import -- SSR snapshot needs direct import to avoid barrel re-exports with heavy deps
338+
import {Badge} from 'sentry/components/core/badge/badge';
339+
```
340+
341+
## Checklist
342+
343+
Before finishing:
344+
345+
- [ ] File is named `<component-name>.snapshots.tsx` and colocated with the component
346+
- [ ] Both `light` and `dark` themes are covered via `describe.each`
347+
- [ ] All primary variant/priority props are snapshotted
348+
- [ ] Interactive components include disabled and checked/unchecked states
349+
- [ ] `no-restricted-imports` ESLint suppression comment is present on the theme import
350+
- [ ] Metadata argument is provided to `it.snapshot.each` calls
351+
- [ ] No-op handlers (`onChange={() => {}}`) provided for required event props
352+
- [ ] Import path uses `@sentry/scraps/<name>` if available, otherwise the direct `sentry/components/...` path with the `no-core-import` suppression

.agents/skills/hybrid-cloud-outboxes/SKILL.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ description: >-
1818

1919
Sentry uses a **transactional outbox pattern** for eventually consistent operations. When a model changes, an outbox row is written inside the same database transaction. After the transaction commits, the outbox is drained — firing a signal that triggers side effects such as RPC calls, tombstone propagation, or audit logging.
2020

21-
The most common use case is **cross-silo data replication**: a model saved in the Region silo produces a `RegionOutbox` that, when processed, replicates data to the Control silo (or vice versa via `ControlOutbox`). But the pattern is general — outboxes work for any operation that should happen reliably after a transaction commits, even within a single silo.
21+
The most common use case is **cross-silo data replication**: a model saved in the Cell silo produces a `CellOutbox` that, when processed, replicates data to the Control silo (or vice versa via `ControlOutbox`). But the pattern is general — outboxes work for any operation that should happen reliably after a transaction commits, even within a single silo.
2222

2323
There are two outbox types corresponding to the two directions of flow:
2424

25-
- **`RegionOutbox`** — written in a Cell silo, processed in the Cell silo to push data toward Control (via RPC calls in signal receivers).
25+
- **`CellOutbox`** — written in a Cell silo, processed in the Cell silo to push data toward Control (via RPC calls in signal receivers).
2626
- **`ControlOutbox`** — written in the Control silo, processed in the Control silo to push data toward one or more Cell silos. Each `ControlOutbox` row targets a specific `cell_name`.
2727

2828
## Critical Constraints
@@ -66,7 +66,7 @@ There are two outbox types corresponding to the two directions of flow:
6666

6767
| Data lives in... | Replicates toward... | Mixin | Outbox type |
6868
| ---------------- | -------------------- | ------------------------ | --------------- |
69-
| Cell silo | Control silo | `ReplicatedCellModel` | `RegionOutbox` |
69+
| Cell silo | Control silo | `ReplicatedCellModel` | `CellOutbox` |
7070
| Control silo | Cell silo(s) | `ReplicatedControlModel` | `ControlOutbox` |
7171

7272
### 2.2 `ReplicatedCellModel` Template
@@ -148,7 +148,7 @@ class MyModel(ReplicatedCellModel):
148148

149149
### 2.3 `ReplicatedControlModel` Template
150150

151-
Use this when a Control model needs to replicate data to Region silo(s). The key difference: Control outboxes fan out to one or more cells, so the model must declare which cells to target.
151+
Use this when a Control model needs to replicate data to Cell silo(s). The key difference: Control outboxes fan out to one or more cells, so the model must declare which cells to target.
152152

153153
```python
154154
from sentry.db.models import control_silo_model
@@ -360,7 +360,7 @@ def test_idempotent_replication(self):
360360

361361
### 7.3 Silo Test Decorators
362362

363-
- Use **`@cell_silo_test`** for tests focused on `RegionOutbox` creation
363+
- Use **`@cell_silo_test`** for tests focused on `CellOutbox` creation
364364
- Use **`@control_silo_test`** for tests focused on `ControlOutbox` creation
365365
- Use **`@all_silo_test`** for end-to-end replication tests that exercise both silos
366366
- Only use **`TransactionTestCase`** for threading/concurrency tests (e.g., `threading.Barrier`), not for standard outbox drain tests

0 commit comments

Comments
 (0)