Skip to content

Commit bdf02c6

Browse files
perf(dynamic-sampling): Batch bulk org rate updates
The bulk org rate edit was calling setFieldValue in a loop for each project, triggering N separate validations. Use a single atomic update for all project rates instead, matching the old behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d3206e7 commit bdf02c6

File tree

3 files changed

+50
-4
lines changed

3 files changed

+50
-4
lines changed

static/app/views/settings/dynamicSampling/projectSampling.spec.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,44 @@ describe('ProjectSampling', () => {
170170
);
171171
});
172172

173+
it('updates project rates atomically via bulk org rate edit', async () => {
174+
const putMock = MockApiClient.addMockResponse({
175+
url: '/organizations/org-slug/sampling/project-rates/',
176+
method: 'PUT',
177+
body: [{id: 1, sampleRate: 0.8}],
178+
});
179+
180+
render(<ProjectSampling />, {organization});
181+
182+
await waitForProjectRateInput();
183+
184+
// Activate bulk edit mode
185+
await userEvent.click(
186+
screen.getByRole('button', {name: 'Proportionally scale project rates'})
187+
);
188+
189+
// Type a new org rate — this should update all project rates in one atomic call
190+
const orgRateInput = screen.getAllByRole('spinbutton')[0]!;
191+
await userEvent.clear(orgRateInput);
192+
await userEvent.type(orgRateInput, '80');
193+
194+
// The project rate should have been scaled
195+
const projectInput = screen.getByRole('spinbutton', {
196+
name: 'Sample rate for project-slug',
197+
});
198+
expect(projectInput).toHaveValue(80);
199+
200+
// Submit and verify the API call
201+
await userEvent.click(screen.getByRole('button', {name: 'Apply Changes'}));
202+
203+
await waitFor(() => {
204+
expect(putMock).toHaveBeenCalledWith(
205+
'/organizations/org-slug/sampling/project-rates/',
206+
expect.objectContaining({data: [{id: 1, sampleRate: 0.8}]})
207+
);
208+
});
209+
});
210+
173211
it('disables Apply Changes for users without org:write access', async () => {
174212
const orgWithoutAccess = OrganizationFixture({
175213
access: [],

static/app/views/settings/dynamicSampling/projectSampling.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ export function ProjectSampling() {
9595
[form]
9696
);
9797

98+
const handleBulkProjectRateChange = useCallback(
99+
(updates: Record<string, string>) => {
100+
form.setFieldValue('projectRates', prev => ({...prev, ...updates}));
101+
},
102+
[form]
103+
);
104+
98105
// Mirror enableReInitialize: reset the form whenever the server data changes
99106
useEffect(() => {
100107
form.reset({projectRates});
@@ -160,6 +167,7 @@ export function ProjectSampling() {
160167
editMode={editMode}
161168
onEditModeChange={setEditMode}
162169
onProjectRateChange={handleProjectRateChange}
170+
onBulkProjectRateChange={handleBulkProjectRateChange}
163171
projectRates={currentProjectRates}
164172
projectErrors={projectErrors}
165173
isLoading={sampleRatesQuery.isPending || sampleCountsQuery.isPending}

static/app/views/settings/dynamicSampling/projectsEditTable.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ interface Props {
2323
actions: React.ReactNode;
2424
editMode: 'single' | 'bulk';
2525
isLoading: boolean;
26+
onBulkProjectRateChange: (updates: Record<string, string>) => void;
2627
onEditModeChange: (mode: 'single' | 'bulk') => void;
2728
onProjectRateChange: (projectId: string, rate: string) => void;
2829
period: ProjectionSamplePeriod;
@@ -41,6 +42,7 @@ export function ProjectsEditTable({
4142
editMode,
4243
period,
4344
onEditModeChange,
45+
onBulkProjectRateChange,
4446
onProjectRateChange,
4547
projectRates,
4648
projectErrors,
@@ -100,13 +102,11 @@ export function ProjectsEditTable({
100102
valueSelector: item => formatPercent(item.sampleRate),
101103
});
102104

103-
for (const [projectId, rate] of Object.entries(newProjectValues)) {
104-
onProjectRateChange(projectId, rate);
105-
}
105+
onBulkProjectRateChange(newProjectValues);
106106
setOrgRate(newRate);
107107
onEditModeChange('bulk');
108108
},
109-
[dataByProjectId, editMode, onProjectRateChange, onEditModeChange, projectRates]
109+
[dataByProjectId, editMode, onBulkProjectRateChange, onEditModeChange, projectRates]
110110
);
111111

112112
const handleBulkEditChange = useCallback((newIsActive: boolean) => {

0 commit comments

Comments
 (0)