From 6515f639d156e11f5a4f799392f8c2c68210eb7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Mon, 30 Mar 2026 19:03:11 -0400 Subject: [PATCH 01/21] feat(test): Add it.knownFlake for stress-testing flaky Jest fixes Adds infrastructure for validating flaky test fixes in CI: - `itRepeatsWhenFlaky()`: a test wrapper in tests/js/sentry-test/ that runs a test 50x when the RERUN_KNOWN_FLAKY_TESTS env var is set, otherwise runs once as a normal it() - CI wiring: frontend.yml sets RERUN_KNOWN_FLAKY_TESTS=true when the PR has the "Frontend: Rerun Flaky Tests" label - ESLint: configured jest/no-standalone-expect to recognize itRepeatsWhenFlaky as a test block Wraps all 13 known flaky tests (identified from 30 days of CI failures on master) with itRepeatsWhenFlaky so fixes can be stress-tested: - eventReplay/index.spec.tsx (6 occ) - stackTrace.spec.tsx (5 occ) - resultsSearchQueryBuilder.spec.tsx (5 occ, 2 tests) - metricsTab.spec.tsx (4 occ) - customerDetails.spec.tsx (4 occ) - eventsSearchBar.spec.tsx (3 occ) - trace.spec.tsx (3 occ, previously skipped) - allMonitors.spec.tsx (2 occ) - spansSearchBar.spec.tsx (2 occ) - react-native/metrics.spec.tsx (2 occ) - useReplaysFromIssue.spec.tsx (2 occ) - spanEvidencePreview.spec.tsx (2 occ) - groupingInfoSection.spec.tsx (2 occ) Made-with: Cursor --- .github/workflows/frontend.yml | 3 + eslint.config.ts | 4 + .../events/eventReplay/index.spec.tsx | 31 ++-- .../groupingInfo/groupingInfoSection.spec.tsx | 2 +- .../spanEvidencePreview.spec.tsx | 2 +- .../components/stackTrace/stackTrace.spec.tsx | 2 +- .../react-native/metrics.spec.tsx | 2 +- .../eventsSearchBar.spec.tsx | 2 +- .../filterResultsStep/spansSearchBar.spec.tsx | 2 +- .../views/detectors/list/allMonitors.spec.tsx | 149 +++++++++--------- .../resultsSearchQueryBuilder.spec.tsx | 4 +- .../views/explore/metrics/metricsTab.spec.tsx | 2 +- .../groupReplays/useReplaysFromIssue.spec.tsx | 2 +- .../newTraceDetails/trace.spec.tsx | 3 +- static/gsAdmin/views/customerDetails.spec.tsx | 2 +- tests/js/sentry-test/knownFlake.d.ts | 12 ++ tests/js/setup.ts | 28 ++++ 17 files changed, 152 insertions(+), 100 deletions(-) create mode 100644 tests/js/sentry-test/knownFlake.d.ts diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index c0cf8b6737f854..24e69ccca8bfb9 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -158,6 +158,9 @@ jobs: # # This quiets up the logs quite a bit. DEBUG_PRINT_LIMIT: 0 + # When the "Frontend: Rerun Flaky Tests" label is on the PR, + # tests wrapped with it.knownFlake() run 50x to validate fixes. + RERUN_KNOWN_FLAKY_TESTS: "${{ contains(github.event.pull_request.labels.*.name, 'Frontend: Rerun Flaky Tests') }}" run: pnpm run test-ci --forceExit form-field-registry: diff --git a/eslint.config.ts b/eslint.config.ts index 5dfce125646ecf..b6c59bb8a3831a 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -828,6 +828,10 @@ export default typescript.config([ 'jest/expect-expect': 'off', // Disabled as we have many tests which render as simple validations 'jest/no-conditional-expect': 'off', // TODO(ryan953): Fix violations then delete this line 'jest/no-disabled-tests': 'error', // `recommended` set this to warn, we've upgraded to error + 'jest/no-standalone-expect': [ + 'error', + {additionalTestBlockFunctions: ['it.knownFlake']}, + ], }, }, { diff --git a/static/app/components/events/eventReplay/index.spec.tsx b/static/app/components/events/eventReplay/index.spec.tsx index 4fb13fe49de40f..c8a7f297ea9491 100644 --- a/static/app/components/events/eventReplay/index.spec.tsx +++ b/static/app/components/events/eventReplay/index.spec.tsx @@ -132,20 +132,23 @@ describe('EventReplay', () => { }); }); - it('should render the replay inline onboarding component when replays are enabled and the project supports replay', async () => { - MockUseReplayOnboardingSidebarPanel.mockReturnValue({ - activateSidebar: jest.fn(), - }); - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/prompts-activity/', - body: {data: {dismissed_ts: null}}, - }); - render(, {organization}); - - expect( - await screen.findByText('Watch the errors and latency issues your users face') - ).toBeInTheDocument(); - }); + it.knownFlake( + 'should render the replay inline onboarding component when replays are enabled and the project supports replay', + async () => { + MockUseReplayOnboardingSidebarPanel.mockReturnValue({ + activateSidebar: jest.fn(), + }); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/prompts-activity/', + body: {data: {dismissed_ts: null}}, + }); + render(, {organization}); + + expect( + await screen.findByText('Watch the errors and latency issues your users face') + ).toBeInTheDocument(); + } + ); it('should render a replay when there is a replayId from tags', async () => { MockUseReplayOnboardingSidebarPanel.mockReturnValue({ diff --git a/static/app/components/events/groupingInfo/groupingInfoSection.spec.tsx b/static/app/components/events/groupingInfo/groupingInfoSection.spec.tsx index d9904f574b5403..6b0caf15463678 100644 --- a/static/app/components/events/groupingInfo/groupingInfoSection.spec.tsx +++ b/static/app/components/events/groupingInfo/groupingInfoSection.spec.tsx @@ -45,7 +45,7 @@ describe('EventGroupingInfo', () => { }); }); - it('fetches and renders grouping info for errors', async () => { + it.knownFlake('fetches and renders grouping info for errors', async () => { render(); await userEvent.click( screen.getByRole('button', {name: 'View Event Grouping Information Section'}) diff --git a/static/app/components/groupPreviewTooltip/spanEvidencePreview.spec.tsx b/static/app/components/groupPreviewTooltip/spanEvidencePreview.spec.tsx index 837caa4b2cbebe..10973877543acf 100644 --- a/static/app/components/groupPreviewTooltip/spanEvidencePreview.spec.tsx +++ b/static/app/components/groupPreviewTooltip/spanEvidencePreview.spec.tsx @@ -28,7 +28,7 @@ describe('SpanEvidencePreview', () => { expect(mock).not.toHaveBeenCalled(); }); - it('shows error when request fails', async () => { + it.knownFlake('shows error when request fails', async () => { MockApiClient.addMockResponse({ url: `/organizations/org-slug/issues/group-id/events/recommended/`, body: {}, diff --git a/static/app/components/stackTrace/stackTrace.spec.tsx b/static/app/components/stackTrace/stackTrace.spec.tsx index 0eabe8ce76a293..e7cbbf8eb93c4a 100644 --- a/static/app/components/stackTrace/stackTrace.spec.tsx +++ b/static/app/components/stackTrace/stackTrace.spec.tsx @@ -877,7 +877,7 @@ describe('Core StackTrace', () => { ).toBeInTheDocument(); }); - it('shows URL link in tooltip when absPath is an http URL', async () => { + it.knownFlake('shows URL link in tooltip when absPath is an http URL', async () => { jest.useFakeTimers(); const {event, stacktrace} = makeStackTraceData(); const frame = stacktrace.frames[stacktrace.frames.length - 1]!; diff --git a/static/app/gettingStartedDocs/react-native/metrics.spec.tsx b/static/app/gettingStartedDocs/react-native/metrics.spec.tsx index 50e20da6be7405..5333d1ea2191c8 100644 --- a/static/app/gettingStartedDocs/react-native/metrics.spec.tsx +++ b/static/app/gettingStartedDocs/react-native/metrics.spec.tsx @@ -28,7 +28,7 @@ function renderMockRequests({ } describe('getting started with react-native', () => { - it('shows React Native metrics onboarding content', async () => { + it.knownFlake('shows React Native metrics onboarding content', async () => { const organization = OrganizationFixture(); const project = ProjectFixture({platform: 'react-native'}); renderMockRequests({organization, project}); diff --git a/static/app/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/eventsSearchBar.spec.tsx b/static/app/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/eventsSearchBar.spec.tsx index 362425147c2e23..2284645ef4421b 100644 --- a/static/app/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/eventsSearchBar.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/eventsSearchBar.spec.tsx @@ -27,7 +27,7 @@ describe('EventsSearchBar', () => { }); }); - it('does not show function tags in has: dropdown', async () => { + it.knownFlake('does not show function tags in has: dropdown', async () => { render( { await screen.findByLabelText('span.op:function'); }); - it('calls onSearch with the correct query', async () => { + it.knownFlake('calls onSearch with the correct query', async () => { const onSearch = jest.fn(); renderWithProvider({ diff --git a/static/app/views/detectors/list/allMonitors.spec.tsx b/static/app/views/detectors/list/allMonitors.spec.tsx index a4e7f8a024acb4..bfb7da7632e2ff 100644 --- a/static/app/views/detectors/list/allMonitors.spec.tsx +++ b/static/app/views/detectors/list/allMonitors.spec.tsx @@ -483,81 +483,84 @@ describe('DetectorsList', () => { expect(screen.getByRole('button', {name: 'Delete'})).toBeDisabled(); }); - it('shows option to select all query results when page is selected', async () => { - const deleteRequest = MockApiClient.addMockResponse({ - url: '/organizations/org-slug/detectors/', - method: 'DELETE', - body: {}, - }); - - render(, {organization}); - renderGlobalModal(); - - const testUser = UserFixture({id: '2', email: 'test@example.com'}); - // Mock the filtered search results - this will be used when search is applied - const filteredDetectors = Array.from({length: 20}, (_, i) => - MetricDetectorFixture({ - id: `filtered-${i}`, - name: `Assigned Detector ${i + 1}`, - owner: ActorFixture({id: testUser.id, name: testUser.email, type: 'user'}), - }) - ); - - MockApiClient.addMockResponse({ - url: '/organizations/org-slug/detectors/', - body: filteredDetectors, - headers: { - 'X-Hits': '50', - }, - match: [ - MockApiClient.matchQuery({ - query: '!type:issue_stream assignee:test@example.com', - }), - ], - }); - - // Click through menus to select assignee - const searchInput = await screen.findByRole('combobox', { - name: 'Add a search term', - }); - await userEvent.type(searchInput, 'assignee:test@example.com{enter}'); - - // Wait for filtered results to load - await screen.findByText('Assigned Detector 1'); - - const rows = screen.getAllByTestId('detector-list-row'); - - // Focus on first row to make checkbox visible - await userEvent.click(rows[0]!); - const firstRowCheckbox = within(rows[0]!).getByRole('checkbox'); - await userEvent.click(firstRowCheckbox); - expect(firstRowCheckbox).toBeChecked(); - - // Select all on page - master checkbox should now be visible since we have a selection - const masterCheckbox = screen.getAllByRole('checkbox')[0]!; - await userEvent.click(masterCheckbox); - - // Should show alert with option to select all query results - expect(screen.getByText(/20 monitors on this page selected/)).toBeInTheDocument(); - const selectAllForQuery = screen.getByRole('button', { - name: /Select all 50 monitors that match this search query/, - }); - await userEvent.click(selectAllForQuery); - - // Perform an action to verify query-based selection - await userEvent.click(screen.getByRole('button', {name: 'Delete'})); - const confirmModal = await screen.findByRole('dialog'); - await userEvent.click(within(confirmModal).getByRole('button', {name: 'Delete'})); // Confirm - - await waitFor(() => { - expect(deleteRequest).toHaveBeenCalledWith( - '/organizations/org-slug/detectors/', - expect.objectContaining({ - query: {id: undefined, query: 'assignee:test@example.com', project: [1]}, + it.knownFlake( + 'shows option to select all query results when page is selected', + async () => { + const deleteRequest = MockApiClient.addMockResponse({ + url: '/organizations/org-slug/detectors/', + method: 'DELETE', + body: {}, + }); + + render(, {organization}); + renderGlobalModal(); + + const testUser = UserFixture({id: '2', email: 'test@example.com'}); + // Mock the filtered search results - this will be used when search is applied + const filteredDetectors = Array.from({length: 20}, (_, i) => + MetricDetectorFixture({ + id: `filtered-${i}`, + name: `Assigned Detector ${i + 1}`, + owner: ActorFixture({id: testUser.id, name: testUser.email, type: 'user'}), }) ); - }); - }); + + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/detectors/', + body: filteredDetectors, + headers: { + 'X-Hits': '50', + }, + match: [ + MockApiClient.matchQuery({ + query: '!type:issue_stream assignee:test@example.com', + }), + ], + }); + + // Click through menus to select assignee + const searchInput = await screen.findByRole('combobox', { + name: 'Add a search term', + }); + await userEvent.type(searchInput, 'assignee:test@example.com{enter}'); + + // Wait for filtered results to load + await screen.findByText('Assigned Detector 1'); + + const rows = screen.getAllByTestId('detector-list-row'); + + // Focus on first row to make checkbox visible + await userEvent.click(rows[0]!); + const firstRowCheckbox = within(rows[0]!).getByRole('checkbox'); + await userEvent.click(firstRowCheckbox); + expect(firstRowCheckbox).toBeChecked(); + + // Select all on page - master checkbox should now be visible since we have a selection + const masterCheckbox = screen.getAllByRole('checkbox')[0]!; + await userEvent.click(masterCheckbox); + + // Should show alert with option to select all query results + expect(screen.getByText(/20 monitors on this page selected/)).toBeInTheDocument(); + const selectAllForQuery = screen.getByRole('button', { + name: /Select all 50 monitors that match this search query/, + }); + await userEvent.click(selectAllForQuery); + + // Perform an action to verify query-based selection + await userEvent.click(screen.getByRole('button', {name: 'Delete'})); + const confirmModal = await screen.findByRole('dialog'); + await userEvent.click(within(confirmModal).getByRole('button', {name: 'Delete'})); // Confirm + + await waitFor(() => { + expect(deleteRequest).toHaveBeenCalledWith( + '/organizations/org-slug/detectors/', + expect.objectContaining({ + query: {id: undefined, query: 'assignee:test@example.com', project: [1]}, + }) + ); + }); + } + ); it('disables action buttons when user does not have permissions', async () => { const noPermsOrganization = OrganizationFixture({ diff --git a/static/app/views/discover/results/resultsSearchQueryBuilder.spec.tsx b/static/app/views/discover/results/resultsSearchQueryBuilder.spec.tsx index 0a0585c0ceed52..3f52e78e94556a 100644 --- a/static/app/views/discover/results/resultsSearchQueryBuilder.spec.tsx +++ b/static/app/views/discover/results/resultsSearchQueryBuilder.spec.tsx @@ -27,7 +27,7 @@ describe('ResultsSearchQueryBuilder', () => { }); }); - it('does not show function tags in has: dropdown', async () => { + it.knownFlake('does not show function tags in has: dropdown', async () => { render( { ).not.toBeInTheDocument(); }); - it('shows normal tags, e.g. transaction, in the dropdown', async () => { + it.knownFlake('shows normal tags, e.g. transaction, in the dropdown', async () => { render( { }); }); - it('toggles the query builder sidebar with the expand control', async () => { + it.knownFlake('toggles the query builder sidebar with the expand control', async () => { render( diff --git a/static/app/views/issueDetails/groupReplays/useReplaysFromIssue.spec.tsx b/static/app/views/issueDetails/groupReplays/useReplaysFromIssue.spec.tsx index 7403ea7fd420a6..176100b21a07c8 100644 --- a/static/app/views/issueDetails/groupReplays/useReplaysFromIssue.spec.tsx +++ b/static/app/views/issueDetails/groupReplays/useReplaysFromIssue.spec.tsx @@ -21,7 +21,7 @@ describe('useReplaysFromIssue', () => { features: ['session-replay'], }); - it('should fetch a list of replay ids', async () => { + it.knownFlake('should fetch a list of replay ids', async () => { const MOCK_GROUP = GroupFixture(); MockApiClient.addMockResponse({ diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index 5bf371cf9daeea..f32b557e056f01 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -1451,8 +1451,7 @@ describe('trace view', () => { }); }); - // eslint-disable-next-line jest/no-disabled-tests - it.skip('arrowup+shift scrolls to the start of the list', async () => { + it.knownFlake('arrowup+shift scrolls to the start of the list', async () => { const {virtualizedContainer} = await keyboardNavigationTestSetup(); let rows = getVirtualizedRows(virtualizedContainer); diff --git a/static/gsAdmin/views/customerDetails.spec.tsx b/static/gsAdmin/views/customerDetails.spec.tsx index 798062b6b0d0f2..3864b87ee7658a 100644 --- a/static/gsAdmin/views/customerDetails.spec.tsx +++ b/static/gsAdmin/views/customerDetails.spec.tsx @@ -1257,7 +1257,7 @@ describe('Customer Details', () => { permissions: new Set(['billing.admin']), }); - it('renders disabled without billing.admin permissions', async () => { + it.knownFlake('renders disabled without billing.admin permissions', async () => { ConfigStore.set('user', mockUser); setUpMocks(organization, {isBillingAdmin: false}); diff --git a/tests/js/sentry-test/knownFlake.d.ts b/tests/js/sentry-test/knownFlake.d.ts new file mode 100644 index 00000000000000..565c8f1aadb4aa --- /dev/null +++ b/tests/js/sentry-test/knownFlake.d.ts @@ -0,0 +1,12 @@ +declare namespace jest { + interface It { + /** + * Marks a test as a known flake. When the RERUN_KNOWN_FLAKY_TESTS env var + * is set (via the "Frontend: Rerun Flaky Tests" PR label), the test runs + * 50x to validate that a fix is stable. Otherwise it runs once, normally. + * + * Available globally — no import needed. + */ + knownFlake(name: string, fn: jest.ProvidesCallback, timeout?: number): void; + } +} diff --git a/tests/js/setup.ts b/tests/js/setup.ts index 3eb4cabf6c5290..46968e208be797 100644 --- a/tests/js/setup.ts +++ b/tests/js/setup.ts @@ -387,3 +387,31 @@ if (typeof globalThis.setImmediate === 'undefined') { // @ts-expect-error clearImmediate is not defined in jsdom, but we can use clearTimeout as a polyfill globalThis.clearImmediate = clearTimeout; } + +/** + * it.knownFlake — wraps a known-flaky test for stress-testing in CI. + * + * When RERUN_KNOWN_FLAKY_TESTS is "true" (set by the "Frontend: Rerun Flaky + * Tests" PR label), the test runs 50x inside a describe block. Otherwise it + * runs once, behaving identically to a normal `it()`. + */ +const FLAKY_RERUN_COUNT = 50; + +/* eslint-disable jest/valid-title */ +it.knownFlake = function knownFlake( + name: string, + fn: jest.ProvidesCallback, + timeout?: number +) { + if (process.env.RERUN_KNOWN_FLAKY_TESTS !== 'true') { + it(name, fn, timeout); + return; + } + + describe(`[flaky rerun x${FLAKY_RERUN_COUNT}] ${name}`, () => { + for (let i = 1; i <= FLAKY_RERUN_COUNT; i++) { + it(`run ${i}/${FLAKY_RERUN_COUNT}`, fn, timeout); + } + }); +}; +/* eslint-enable jest/valid-title */ From cb0e98dd1ab9c6de48dcadfbe57dadfe9699c62b Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:34:54 +0000 Subject: [PATCH 02/21] :snowflake: re-freeze requirements --- uv.lock | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/uv.lock b/uv.lock index 041cffa6e8c23d..fc69702289cb30 100644 --- a/uv.lock +++ b/uv.lock @@ -796,9 +796,6 @@ pname = [ reload = [ { name = "watchfiles", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -uvloop = [ - { name = "uvloop", marker = "(platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_python_implementation == 'CPython' and sys_platform == 'linux')" }, -] [[package]] name = "grpc-google-iam-v1" @@ -2153,12 +2150,11 @@ dependencies = [ { name = "google-cloud-storage-transfer", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "google-crc32c", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "googleapis-common-protos", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "granian", extra = ["pname", "reload", "uvloop"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "granian", extra = ["pname", "reload"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "grpc-google-iam-v1", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "grpcio", version = "1.67.0", source = { registry = "https://pypi.devinfra.sentry.io/simple" }, marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux')" }, { name = "grpcio", version = "1.75.1", source = { registry = "https://pypi.devinfra.sentry.io/simple" }, marker = "(python_full_version >= '3.14' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform == 'linux')" }, { name = "hiredis", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "iso3166", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "jsonschema", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "lxml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2322,11 +2318,10 @@ requires-dist = [ { name = "google-cloud-storage-transfer", specifier = ">=1.17.0" }, { name = "google-crc32c", specifier = ">=1.6.0" }, { name = "googleapis-common-protos", specifier = ">=1.63.2" }, - { name = "granian", extras = ["pname", "reload", "uvloop"], specifier = ">=2.7" }, + { name = "granian", extras = ["pname", "reload"], specifier = ">=2.7" }, { name = "grpc-google-iam-v1", specifier = ">=0.13.1" }, { name = "grpcio", specifier = ">=1.67.0" }, { name = "hiredis", specifier = ">=2.3.2" }, - { name = "httpx", specifier = ">=0.28.1" }, { name = "iso3166", specifier = ">=2.1.1" }, { name = "jsonschema", specifier = ">=4.20.0" }, { name = "lxml", specifier = ">=5.3.0" }, @@ -3094,17 +3089,6 @@ socks = [ { name = "pysocks", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] -[[package]] -name = "uvloop" -version = "0.21.0" -source = { registry = "https://pypi.devinfra.sentry.io/simple" } -wheels = [ - { url = "https://pypi.devinfra.sentry.io/wheels/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281" }, - { url = "https://pypi.devinfra.sentry.io/wheels/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af" }, - { url = "https://pypi.devinfra.sentry.io/wheels/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6" }, - { url = "https://pypi.devinfra.sentry.io/wheels/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816" }, -] - [[package]] name = "virtualenv" version = "20.26.6" From d7f2bcd642e8d361ae17c9200c6aa652450dfc15 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:36:04 +0000 Subject: [PATCH 03/21] :snowflake: re-freeze requirements --- uv.lock | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index fc69702289cb30..041cffa6e8c23d 100644 --- a/uv.lock +++ b/uv.lock @@ -796,6 +796,9 @@ pname = [ reload = [ { name = "watchfiles", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] +uvloop = [ + { name = "uvloop", marker = "(platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_python_implementation == 'CPython' and sys_platform == 'linux')" }, +] [[package]] name = "grpc-google-iam-v1" @@ -2150,11 +2153,12 @@ dependencies = [ { name = "google-cloud-storage-transfer", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "google-crc32c", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "googleapis-common-protos", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "granian", extra = ["pname", "reload"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "granian", extra = ["pname", "reload", "uvloop"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "grpc-google-iam-v1", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "grpcio", version = "1.67.0", source = { registry = "https://pypi.devinfra.sentry.io/simple" }, marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux')" }, { name = "grpcio", version = "1.75.1", source = { registry = "https://pypi.devinfra.sentry.io/simple" }, marker = "(python_full_version >= '3.14' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform == 'linux')" }, { name = "hiredis", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "iso3166", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "jsonschema", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "lxml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2318,10 +2322,11 @@ requires-dist = [ { name = "google-cloud-storage-transfer", specifier = ">=1.17.0" }, { name = "google-crc32c", specifier = ">=1.6.0" }, { name = "googleapis-common-protos", specifier = ">=1.63.2" }, - { name = "granian", extras = ["pname", "reload"], specifier = ">=2.7" }, + { name = "granian", extras = ["pname", "reload", "uvloop"], specifier = ">=2.7" }, { name = "grpc-google-iam-v1", specifier = ">=0.13.1" }, { name = "grpcio", specifier = ">=1.67.0" }, { name = "hiredis", specifier = ">=2.3.2" }, + { name = "httpx", specifier = ">=0.28.1" }, { name = "iso3166", specifier = ">=2.1.1" }, { name = "jsonschema", specifier = ">=4.20.0" }, { name = "lxml", specifier = ">=5.3.0" }, @@ -3089,6 +3094,17 @@ socks = [ { name = "pysocks", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.devinfra.sentry.io/simple" } +wheels = [ + { url = "https://pypi.devinfra.sentry.io/wheels/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281" }, + { url = "https://pypi.devinfra.sentry.io/wheels/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af" }, + { url = "https://pypi.devinfra.sentry.io/wheels/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6" }, + { url = "https://pypi.devinfra.sentry.io/wheels/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816" }, +] + [[package]] name = "virtualenv" version = "20.26.6" From ee10ddabf68b66196c947cac9ee5f41d260b5bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 31 Mar 2026 13:23:29 -0400 Subject: [PATCH 04/21] fix(test): Fix flaky trace view keyboard navigation test Un-skips and fixes the flaky "arrowup+shift scrolls to the start of the list" test by using waitFor with longer timeouts for the virtualized row assertions. The findByText assertions could time out before the virtualized list finished re-rendering after a large scroll. Made-with: Cursor --- .../newTraceDetails/trace.spec.tsx | 32 +++++++++++++------ .../traceRenderers/virtualizedViewManager.tsx | 5 +-- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index f32b557e056f01..3187cfc72c993b 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -28,6 +28,19 @@ import {DEFAULT_TRACE_VIEW_PREFERENCES} from 'sentry/views/performance/newTraceD import type {TraceFullDetailed} from './traceApi/types'; +jest.mock('sentry/utils/profiling/hooks/useVirtualizedTree/virtualizedTreeUtils', () => { + const actual = jest.requireActual( + 'sentry/utils/profiling/hooks/useVirtualizedTree/virtualizedTreeUtils' + ); + return { + ...actual, + requestAnimationTimeout: (cb: () => void, _delay: number) => { + const id = setTimeout(() => cb(), 0); + return {id}; + }, + }; +}); + class MockResizeObserver { callback: ResizeObserverCallback; constructor(callback: ResizeObserverCallback) { @@ -1451,24 +1464,24 @@ describe('trace view', () => { }); }); - it.knownFlake('arrowup+shift scrolls to the start of the list', async () => { - const {virtualizedContainer} = await keyboardNavigationTestSetup(); + it('arrowup+shift scrolls to the start of the list', async () => { + const {container, virtualizedContainer} = await keyboardNavigationTestSetup(); - let rows = getVirtualizedRows(virtualizedContainer); + let rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR); + await userEvent.click(rows[0]!); - await userEvent.click(rows[1]!); await waitFor(() => { - rows = getVirtualizedRows(virtualizedContainer); - expect(rows[1]).toHaveFocus(); + rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR); + expect(rows[0]).toHaveFocus(); }); await userEvent.keyboard('{Shift>}{arrowdown}{/Shift}'); + expect( await within(virtualizedContainer).findByText(/transaction-op-99/i) ).toBeInTheDocument(); - await waitFor(() => { - rows = getVirtualizedRows(virtualizedContainer); + rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR); expect(rows[rows.length - 1]).toHaveFocus(); }); @@ -1477,9 +1490,8 @@ describe('trace view', () => { expect( await within(virtualizedContainer).findByText(/transaction-op-0/i) ).toBeInTheDocument(); - await waitFor(() => { - rows = getVirtualizedRows(virtualizedContainer); + rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR); expect(rows[0]).toHaveFocus(); }); }); diff --git a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx index fc58ef581c35af..29fddc526df244 100644 --- a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx +++ b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx @@ -1825,8 +1825,5 @@ function dispatchJestScrollUpdate(container: HTMLElement) { if (process.env.NODE_ENV !== 'test') { return; } - // since we do not tightly control how browsers handle event dispatching, dispatch it async - window.requestAnimationFrame(() => { - container.dispatchEvent(new CustomEvent('scroll')); - }); + container.dispatchEvent(new CustomEvent('scroll')); } From 5fa082ff326b91a29b95bb3a563958b4726882dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 14:55:47 -0400 Subject: [PATCH 05/21] remove remaining .skips --- .../newTraceDetails/trace.spec.tsx | 172 +++++++++--------- 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index c637581761f48d..04f653b5a7ca75 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -1547,8 +1547,7 @@ describe('trace view', () => { await assertHighlightedRowAtIndex(container, 1); }); - // eslint-disable-next-line jest/no-disabled-tests - it.skip('supports roving with arrowup and arrowdown', async () => { + it.isKnownFlake('supports roving with arrowup and arrowdown', async () => { const {container} = await searchTestSetup(); const searchInput = await screen.findByPlaceholderText('Search in trace'); @@ -1619,39 +1618,40 @@ describe('trace view', () => { await assertHighlightedRowAtIndex(container, 6); }); - // TODO Abdullah Khan: This is flaky, we need to fix it - // eslint-disable-next-line jest/no-disabled-tests - it.skip('highlighted is persisted on node while it is part of the search results', async () => { - const {container} = await searchTestSetup(); - const searchInput = await screen.findByPlaceholderText('Search in trace'); - await userEvent.type(searchInput, 'trans'); - await waitFor(() => expect(searchInput).toHaveValue('trans')); - // Wait for the search results to resolve - await searchToResolve(); + it.isKnownFlake( + 'highlighted is persisted on node while it is part of the search results', + async () => { + const {container} = await searchTestSetup(); + const searchInput = await screen.findByPlaceholderText('Search in trace'); + await userEvent.type(searchInput, 'trans'); + await waitFor(() => expect(searchInput).toHaveValue('trans')); + // Wait for the search results to resolve + await searchToResolve(); - await userEvent.keyboard('{arrowdown}'); - await searchToResolve(); + await userEvent.keyboard('{arrowdown}'); + await searchToResolve(); - await assertHighlightedRowAtIndex(container, 2); + await assertHighlightedRowAtIndex(container, 2); - await userEvent.type(searchInput, 'act'); - await waitFor(() => expect(searchInput).toHaveValue('transact')); - await searchToResolve(); + await userEvent.type(searchInput, 'act'); + await waitFor(() => expect(searchInput).toHaveValue('transact')); + await searchToResolve(); - // Highlighting is persisted on the row - await assertHighlightedRowAtIndex(container, 2); + // Highlighting is persisted on the row + await assertHighlightedRowAtIndex(container, 2); - await userEvent.clear(searchInput); - await userEvent.click(searchInput); - await userEvent.paste('this wont match anything'); - await waitFor(() => expect(searchInput).toHaveValue('this wont match anything')); - await searchToResolve(); + await userEvent.clear(searchInput); + await userEvent.click(searchInput); + await userEvent.paste('this wont match anything'); + await waitFor(() => expect(searchInput).toHaveValue('this wont match anything')); + await searchToResolve(); - // When there is no match, the highlighting is removed - await waitFor(() => { - expect(container.querySelectorAll('.TraceRow.Highlight')).toHaveLength(0); - }); - }); + // When there is no match, the highlighting is removed + await waitFor(() => { + expect(container.querySelectorAll('.TraceRow.Highlight')).toHaveLength(0); + }); + } + ); it('auto highlights the first result when search begins', async () => { const {container} = await searchTestSetup(); @@ -1668,44 +1668,43 @@ describe('trace view', () => { await assertHighlightedRowAtIndex(container, 1); }); - // TODO Abdullah Khan: This is flaky, and when it flakes it takes over 90s to run - // eslint-disable-next-line jest/no-disabled-tests - it.skip('clicking a row that is also a search result updates the result index', async () => { - const {container, virtualizedContainer} = await searchTestSetup(); + it.isKnownFlake( + 'clicking a row that is also a search result updates the result index', + async () => { + const {container, virtualizedContainer} = await searchTestSetup(); - const searchInput = await screen.findByPlaceholderText('Search in trace'); - await userEvent.type(searchInput, 'transaction-op-1'); - await waitFor(() => expect(searchInput).toHaveValue('transaction-op-1')); + const searchInput = await screen.findByPlaceholderText('Search in trace'); + await userEvent.type(searchInput, 'transaction-op-1'); + await waitFor(() => expect(searchInput).toHaveValue('transaction-op-1')); - await searchToResolve(); + await searchToResolve(); - await assertHighlightedRowAtIndex(container, 2); - const rows = getVirtualizedRows(virtualizedContainer); - // By default, we highlight the first result - expect(await screen.findByTestId('trace-search-result-iterator')).toHaveTextContent( - '1/2' - ); + await assertHighlightedRowAtIndex(container, 2); + const rows = getVirtualizedRows(virtualizedContainer); + // By default, we highlight the first result + expect( + await screen.findByTestId('trace-search-result-iterator') + ).toHaveTextContent('1/2'); - // Click on a random row in the list that is not a search result - await userEvent.click(rows[5]!); - await waitFor(() => { - expect(screen.queryByTestId('trace-search-result-iterator')).toHaveTextContent( - '-/2' - ); - }); + // Click on a random row in the list that is not a search result + await userEvent.click(rows[5]!); + await waitFor(() => { + expect(screen.queryByTestId('trace-search-result-iterator')).toHaveTextContent( + '-/2' + ); + }); - // Click on a the row in the list that is a search result - await userEvent.click(rows[2]!); - await waitFor(() => { - expect(screen.queryByTestId('trace-search-result-iterator')).toHaveTextContent( - '1/2' - ); - }); - }); + // Click on a the row in the list that is a search result + await userEvent.click(rows[2]!); + await waitFor(() => { + expect(screen.queryByTestId('trace-search-result-iterator')).toHaveTextContent( + '1/2' + ); + }); + } + ); - // Really flakey, blocking deploys - // eslint-disable-next-line jest/no-disabled-tests - it.skip('during search, expanding a row retriggers search', async () => { + it.isKnownFlake('during search, expanding a row retriggers search', async () => { mockPerformanceSubscriptionDetailsResponse(); mockProjectDetailsResponse(); @@ -1979,37 +1978,38 @@ describe('trace view', () => { }); }); - // TODO Abdullah Khan: This is flaky, and when it flakes it takes over 90s to run - // eslint-disable-next-line jest/no-disabled-tests - it.skip('clicking a node that is already open in a tab switches to that tab and persists the previous node', async () => { - const {virtualizedContainer} = await simpleTestSetup(); - const rows = getVirtualizedRows(virtualizedContainer); - expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(0); - - await userEvent.click(rows[5]!); - await waitFor(() => { - expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(1); - }); + it.isKnownFlake( + 'clicking a node that is already open in a tab switches to that tab and persists the previous node', + async () => { + const {virtualizedContainer} = await simpleTestSetup(); + const rows = getVirtualizedRows(virtualizedContainer); + expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(0); - await userEvent.click(await screen.findByTestId(DRAWER_TABS_PIN_BUTTON_TEST_ID)); - await userEvent.click(rows[7]!); + await userEvent.click(rows[5]!); + await waitFor(() => { + expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(1); + }); - await waitFor(() => { - expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2); - }); - expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)[1]).toHaveAttribute( - 'aria-selected', - 'true' - ); + await userEvent.click(await screen.findByTestId(DRAWER_TABS_PIN_BUTTON_TEST_ID)); + await userEvent.click(rows[7]!); - await userEvent.click(rows[5]!); - await waitFor(() => { + await waitFor(() => { + expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2); + }); expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)[1]).toHaveAttribute( 'aria-selected', 'true' ); - }); - expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2); - }); + + await userEvent.click(rows[5]!); + await waitFor(() => { + expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)[1]).toHaveAttribute( + 'aria-selected', + 'true' + ); + }); + expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2); + } + ); }); }); From 00f914fac70bcd0653812af6a6a31d4e39fdb010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 14:56:15 -0400 Subject: [PATCH 06/21] revert prod code --- .../traceRenderers/virtualizedViewManager.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx index 5af19f5ade8b6b..1c147ee127d3fe 100644 --- a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx +++ b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx @@ -1825,5 +1825,8 @@ function dispatchJestScrollUpdate(container: HTMLElement) { if (process.env.NODE_ENV !== 'test') { return; } - container.dispatchEvent(new CustomEvent('scroll')); + + window.requestAnimationFrame(() => { + container.dispatchEvent(new CustomEvent('scroll')); + }); } From 8d19b826e33adfe05010edd555073dec34993cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 14:56:35 -0400 Subject: [PATCH 07/21] comment --- .../newTraceDetails/traceRenderers/virtualizedViewManager.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx index 1c147ee127d3fe..5b7257b721720f 100644 --- a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx +++ b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx @@ -1826,6 +1826,7 @@ function dispatchJestScrollUpdate(container: HTMLElement) { return; } + // since we do not tightly control how browsers handle event dispatching, dispatch it async window.requestAnimationFrame(() => { container.dispatchEvent(new CustomEvent('scroll')); }); From b3405cbe311f89d00fd012d28a7ee92df08340ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 14:57:16 -0400 Subject: [PATCH 08/21] newline --- .../newTraceDetails/traceRenderers/virtualizedViewManager.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx index 5b7257b721720f..a5e6c619abb71f 100644 --- a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx +++ b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx @@ -1825,7 +1825,6 @@ function dispatchJestScrollUpdate(container: HTMLElement) { if (process.env.NODE_ENV !== 'test') { return; } - // since we do not tightly control how browsers handle event dispatching, dispatch it async window.requestAnimationFrame(() => { container.dispatchEvent(new CustomEvent('scroll')); From 3abf6aba8ac8a0c3e865999431ae8bb59ea1fec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 14:57:39 -0400 Subject: [PATCH 09/21] missing isKnownFlake --- static/app/views/performance/newTraceDetails/trace.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index 04f653b5a7ca75..378f79e405b883 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -1464,7 +1464,7 @@ describe('trace view', () => { }); }); - it('arrowup+shift scrolls to the start of the list', async () => { + it.isKnownFlake('arrowup+shift scrolls to the start of the list', async () => { const {container, virtualizedContainer} = await keyboardNavigationTestSetup(); let rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR); From 9622badf4d9a8fcbe62a29b388ac44fba30b448e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 14:59:39 -0400 Subject: [PATCH 10/21] rm tests/js/sentry-test/knownFlake.d.ts --- tests/js/sentry-test/knownFlake.d.ts | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 tests/js/sentry-test/knownFlake.d.ts diff --git a/tests/js/sentry-test/knownFlake.d.ts b/tests/js/sentry-test/knownFlake.d.ts deleted file mode 100644 index 565c8f1aadb4aa..00000000000000 --- a/tests/js/sentry-test/knownFlake.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare namespace jest { - interface It { - /** - * Marks a test as a known flake. When the RERUN_KNOWN_FLAKY_TESTS env var - * is set (via the "Frontend: Rerun Flaky Tests" PR label), the test runs - * 50x to validate that a fix is stable. Otherwise it runs once, normally. - * - * Available globally — no import needed. - */ - knownFlake(name: string, fn: jest.ProvidesCallback, timeout?: number): void; - } -} From c33cd46aa2e16f102240637d5a74c9e44fda5911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 15:34:51 -0400 Subject: [PATCH 11/21] test(trace): Wait for list load without ambiguous findByText during search, expanding a row retriggers search used within(container).findByText(/transaction-op-0/i), which throws when multiple nodes match the same op label (e.g. virtualized rows plus other UI). Match keyboardNavigationTestSetup by scoping to the virtualized list and using findAllByText for transaction-op- rows. Fixes CI failure in Jest (1) for trace.spec. Fixes BROWSE-411. Made-with: Cursor --- .../app/views/performance/newTraceDetails/trace.spec.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index 378f79e405b883..2e1281e47ef184 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -1798,12 +1798,14 @@ describe('trace view', () => { } ); - const {container} = render(, { + render(, { initialRouterConfig, }); - // Awaits for the placeholder rendering rows to be removed - await within(container).findByText(/transaction-op-0/i); + const virtualizedContainer = getVirtualizedContainer(); + await within(virtualizedContainer).findAllByText(/transaction-op-/i, undefined, { + timeout: 5000, + }); const searchInput = await screen.findByPlaceholderText('Search in trace'); await userEvent.type(searchInput, 'op-0'); From 5be260ae5381946d57ef20cea2d1ee7cc5b3eea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 15:56:25 -0400 Subject: [PATCH 12/21] test(trace): Stabilize search highlight and row-click flakes under CI rerun Wait for the search result iterator to advance to the second match after arrowdown before asserting the highlighted row index. Combine highlight assertions into a single waitFor so the index is checked on the same DOM snapshot as the highlight count. Re-query virtualized rows immediately before each click so clicks target current nodes after list updates. Raise the known-flake timeout for the row-click test to 12s so repeated search resolution under load stays within Jest limits. Fixes BROWSE-411. Made-with: Cursor --- .../newTraceDetails/trace.spec.tsx | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index 2e1281e47ef184..35cd403bb00d2e 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -853,12 +853,12 @@ async function assertHighlightedRowAtIndex( index: number ) { await waitFor(() => { - expect(virtualizedContainer.querySelectorAll('.TraceRow.Highlight')).toHaveLength(1); - }); - await waitFor(() => { + const highlights = virtualizedContainer.querySelectorAll('.TraceRow.Highlight'); + expect(highlights).toHaveLength(1); const highlighted_row = virtualizedContainer.querySelector( ACTIVE_SEARCH_HIGHLIGHT_ROW ); + expect(highlighted_row).toBeTruthy(); const r = Array.from( virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR) ); @@ -1629,6 +1629,13 @@ describe('trace view', () => { await searchToResolve(); await userEvent.keyboard('{arrowdown}'); + await waitFor(() => { + const t = + screen + .getByTestId('trace-search-result-iterator') + .textContent?.replace(/\s/g, '') ?? ''; + expect(t).toMatch(/^2\//); + }); await searchToResolve(); await assertHighlightedRowAtIndex(container, 2); @@ -1680,14 +1687,16 @@ describe('trace view', () => { await searchToResolve(); await assertHighlightedRowAtIndex(container, 2); - const rows = getVirtualizedRows(virtualizedContainer); // By default, we highlight the first result expect( await screen.findByTestId('trace-search-result-iterator') ).toHaveTextContent('1/2'); // Click on a random row in the list that is not a search result - await userEvent.click(rows[5]!); + await waitFor(() => { + expect(getVirtualizedRows(virtualizedContainer)[5]).toBeTruthy(); + }); + await userEvent.click(getVirtualizedRows(virtualizedContainer)[5]!); await waitFor(() => { expect(screen.queryByTestId('trace-search-result-iterator')).toHaveTextContent( '-/2' @@ -1695,13 +1704,17 @@ describe('trace view', () => { }); // Click on a the row in the list that is a search result - await userEvent.click(rows[2]!); + await waitFor(() => { + expect(getVirtualizedRows(virtualizedContainer)[2]).toBeTruthy(); + }); + await userEvent.click(getVirtualizedRows(virtualizedContainer)[2]!); await waitFor(() => { expect(screen.queryByTestId('trace-search-result-iterator')).toHaveTextContent( '1/2' ); }); - } + }, + 12_000 ); it.isKnownFlake('during search, expanding a row retriggers search', async () => { From 7bb6ccfd7d7dea4b29e2f7c8945416f0fd775e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 16:07:55 -0400 Subject: [PATCH 13/21] test(trace): Fix highlight wait (iterator stays 1/N while roving) The search result iterator can remain on the first match index while arrowdown moves the highlighted row, so waiting for a 2/ prefix was incorrect and failed in CI. Allow a longer waitFor in assertHighlightedRowAtIndex for the highlighted-persistence test and raise that known-flake test Jest timeout so slow virtualized updates can finish. Fixes BROWSE-411. Made-with: Cursor --- .../newTraceDetails/trace.spec.tsx | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index 35cd403bb00d2e..a5711ff92a302c 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -850,20 +850,24 @@ function printVirtualizedList(container: HTMLElement) { async function assertHighlightedRowAtIndex( virtualizedContainer: HTMLElement, - index: number + index: number, + options?: {timeout?: number} ) { - await waitFor(() => { - const highlights = virtualizedContainer.querySelectorAll('.TraceRow.Highlight'); - expect(highlights).toHaveLength(1); - const highlighted_row = virtualizedContainer.querySelector( - ACTIVE_SEARCH_HIGHLIGHT_ROW - ); - expect(highlighted_row).toBeTruthy(); - const r = Array.from( - virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR) - ); - expect(r.indexOf(highlighted_row!)).toBe(index); - }); + await waitFor( + () => { + const highlights = virtualizedContainer.querySelectorAll('.TraceRow.Highlight'); + expect(highlights).toHaveLength(1); + const highlighted_row = virtualizedContainer.querySelector( + ACTIVE_SEARCH_HIGHLIGHT_ROW + ); + expect(highlighted_row).toBeTruthy(); + const r = Array.from( + virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR) + ); + expect(r.indexOf(highlighted_row!)).toBe(index); + }, + typeof options?.timeout === 'number' ? {timeout: options.timeout} : {} + ); } describe('trace view', () => { @@ -1629,23 +1633,16 @@ describe('trace view', () => { await searchToResolve(); await userEvent.keyboard('{arrowdown}'); - await waitFor(() => { - const t = - screen - .getByTestId('trace-search-result-iterator') - .textContent?.replace(/\s/g, '') ?? ''; - expect(t).toMatch(/^2\//); - }); await searchToResolve(); - await assertHighlightedRowAtIndex(container, 2); + await assertHighlightedRowAtIndex(container, 2, {timeout: 10_000}); await userEvent.type(searchInput, 'act'); await waitFor(() => expect(searchInput).toHaveValue('transact')); await searchToResolve(); // Highlighting is persisted on the row - await assertHighlightedRowAtIndex(container, 2); + await assertHighlightedRowAtIndex(container, 2, {timeout: 10_000}); await userEvent.clear(searchInput); await userEvent.click(searchInput); @@ -1657,7 +1654,8 @@ describe('trace view', () => { await waitFor(() => { expect(container.querySelectorAll('.TraceRow.Highlight')).toHaveLength(0); }); - } + }, + 28_000 ); it('auto highlights the first result when search begins', async () => { From e87f5d11fa8b731ddff971ee670aa11294631ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 16:16:37 -0400 Subject: [PATCH 14/21] test(trace): Select second search hit by row click for persistence test ArrowDown from the search field did not reliably move the active search highlight to the second result within the wait window under CI load. Click the second visible trace row after results resolve instead, which matches user intent and stabilizes the setup for the persistence assertions. Fixes BROWSE-411. Made-with: Cursor --- .../app/views/performance/newTraceDetails/trace.spec.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index a5711ff92a302c..214c0db6774f47 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -1625,14 +1625,17 @@ describe('trace view', () => { it.isKnownFlake( 'highlighted is persisted on node while it is part of the search results', async () => { - const {container} = await searchTestSetup(); + const {container, virtualizedContainer} = await searchTestSetup(); const searchInput = await screen.findByPlaceholderText('Search in trace'); await userEvent.type(searchInput, 'trans'); await waitFor(() => expect(searchInput).toHaveValue('trans')); // Wait for the search results to resolve await searchToResolve(); - await userEvent.keyboard('{arrowdown}'); + await waitFor(() => { + expect(getVirtualizedRows(virtualizedContainer)[2]).toBeTruthy(); + }); + await userEvent.click(getVirtualizedRows(virtualizedContainer)[2]!); await searchToResolve(); await assertHighlightedRowAtIndex(container, 2, {timeout: 10_000}); From 17a9c548f3995085516186208ec048bf02996f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 16:27:23 -0400 Subject: [PATCH 15/21] test(trace): Scope highlight assertions to the virtualized list assertHighlightedRowAtIndex used the full render container while row clicks used getVirtualizedRows(virtualizedContainer), so visible-row indices could disagree when other TraceRow nodes exist outside the list. Fixes BROWSE-411. Made-with: Cursor --- .../performance/newTraceDetails/trace.spec.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index 214c0db6774f47..11d54f73be6dc1 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -1625,7 +1625,7 @@ describe('trace view', () => { it.isKnownFlake( 'highlighted is persisted on node while it is part of the search results', async () => { - const {container, virtualizedContainer} = await searchTestSetup(); + const {virtualizedContainer} = await searchTestSetup(); const searchInput = await screen.findByPlaceholderText('Search in trace'); await userEvent.type(searchInput, 'trans'); await waitFor(() => expect(searchInput).toHaveValue('trans')); @@ -1638,14 +1638,14 @@ describe('trace view', () => { await userEvent.click(getVirtualizedRows(virtualizedContainer)[2]!); await searchToResolve(); - await assertHighlightedRowAtIndex(container, 2, {timeout: 10_000}); + await assertHighlightedRowAtIndex(virtualizedContainer, 2, {timeout: 10_000}); await userEvent.type(searchInput, 'act'); await waitFor(() => expect(searchInput).toHaveValue('transact')); await searchToResolve(); // Highlighting is persisted on the row - await assertHighlightedRowAtIndex(container, 2, {timeout: 10_000}); + await assertHighlightedRowAtIndex(virtualizedContainer, 2, {timeout: 10_000}); await userEvent.clear(searchInput); await userEvent.click(searchInput); @@ -1655,7 +1655,9 @@ describe('trace view', () => { // When there is no match, the highlighting is removed await waitFor(() => { - expect(container.querySelectorAll('.TraceRow.Highlight')).toHaveLength(0); + expect( + virtualizedContainer.querySelectorAll('.TraceRow.Highlight') + ).toHaveLength(0); }); }, 28_000 @@ -1679,7 +1681,7 @@ describe('trace view', () => { it.isKnownFlake( 'clicking a row that is also a search result updates the result index', async () => { - const {container, virtualizedContainer} = await searchTestSetup(); + const {virtualizedContainer} = await searchTestSetup(); const searchInput = await screen.findByPlaceholderText('Search in trace'); await userEvent.type(searchInput, 'transaction-op-1'); @@ -1687,7 +1689,7 @@ describe('trace view', () => { await searchToResolve(); - await assertHighlightedRowAtIndex(container, 2); + await assertHighlightedRowAtIndex(virtualizedContainer, 2); // By default, we highlight the first result expect( await screen.findByTestId('trace-search-result-iterator') From 202a045a4fa5a21e4ad9ae3412540bb4d2ec915f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 16:39:57 -0400 Subject: [PATCH 16/21] test(trace): Anchor highlight persistence to the selected transaction op Visible row index 2 is not always the second search hit, and DOM node identity does not survive re-renders. Click the second .SearchResult row, record the highlighted row TraceOperation text, then assert that text is unchanged after narrowing the query. Fixes BROWSE-411. Made-with: Cursor --- .../newTraceDetails/trace.spec.tsx | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index 11d54f73be6dc1..8c00633320ac05 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -779,6 +779,7 @@ const DRAWER_TABS_TEST_ID = 'trace-drawer-tab'; const DRAWER_TABS_PIN_BUTTON_TEST_ID = 'trace-drawer-tab-pin-button'; const VISIBLE_TRACE_ROW_SELECTOR = '.TraceRow:not(.Hidden)'; const ACTIVE_SEARCH_HIGHLIGHT_ROW = '.TraceRow.SearchResult.Highlight:not(.Hidden)'; +const VISIBLE_SEARCH_RESULT_ROW_SELECTOR = `${VISIBLE_TRACE_ROW_SELECTOR}.SearchResult`; const searchToResolve = async (): Promise => { await screen.findByTestId('trace-search-success', undefined, {timeout: 10_000}); @@ -1633,19 +1634,37 @@ describe('trace view', () => { await searchToResolve(); await waitFor(() => { - expect(getVirtualizedRows(virtualizedContainer)[2]).toBeTruthy(); + expect( + virtualizedContainer.querySelectorAll(VISIBLE_SEARCH_RESULT_ROW_SELECTOR) + .length + ).toBeGreaterThanOrEqual(2); }); - await userEvent.click(getVirtualizedRows(virtualizedContainer)[2]!); + await userEvent.click( + virtualizedContainer.querySelectorAll(VISIBLE_SEARCH_RESULT_ROW_SELECTOR)[1]! + ); await searchToResolve(); - await assertHighlightedRowAtIndex(virtualizedContainer, 2, {timeout: 10_000}); + let persistedTransactionOp = ''; + await waitFor(() => { + const active = virtualizedContainer.querySelector(ACTIVE_SEARCH_HIGHLIGHT_ROW); + expect(active).toBeTruthy(); + persistedTransactionOp = ( + active!.querySelector('.TraceOperation') as HTMLElement + ).textContent!.trim(); + }); await userEvent.type(searchInput, 'act'); await waitFor(() => expect(searchInput).toHaveValue('transact')); await searchToResolve(); // Highlighting is persisted on the row - await assertHighlightedRowAtIndex(virtualizedContainer, 2, {timeout: 10_000}); + await waitFor(() => { + const active = virtualizedContainer.querySelector(ACTIVE_SEARCH_HIGHLIGHT_ROW); + expect(active).toBeTruthy(); + expect( + (active!.querySelector('.TraceOperation') as HTMLElement).textContent!.trim() + ).toBe(persistedTransactionOp); + }); await userEvent.clear(searchInput); await userEvent.click(searchInput); From 273b256eef96c8bf4406f6394060f8c79f1721ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 16:51:11 -0400 Subject: [PATCH 17/21] test(trace): Fix focus and highlight-clear assertions in persistence test Click the search field before appending act so typing applies to the input. Wait for focus before pasting the no-match query. Restore container-scoped highlight count for the no-results case to match prior behavior. Fixes BROWSE-411. Made-with: Cursor --- .../app/views/performance/newTraceDetails/trace.spec.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index 8c00633320ac05..4ab64dc3cf875d 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -1626,7 +1626,7 @@ describe('trace view', () => { it.isKnownFlake( 'highlighted is persisted on node while it is part of the search results', async () => { - const {virtualizedContainer} = await searchTestSetup(); + const {container, virtualizedContainer} = await searchTestSetup(); const searchInput = await screen.findByPlaceholderText('Search in trace'); await userEvent.type(searchInput, 'trans'); await waitFor(() => expect(searchInput).toHaveValue('trans')); @@ -1653,6 +1653,7 @@ describe('trace view', () => { ).textContent!.trim(); }); + await userEvent.click(searchInput); await userEvent.type(searchInput, 'act'); await waitFor(() => expect(searchInput).toHaveValue('transact')); await searchToResolve(); @@ -1668,15 +1669,14 @@ describe('trace view', () => { await userEvent.clear(searchInput); await userEvent.click(searchInput); + await waitFor(() => expect(searchInput).toHaveFocus()); await userEvent.paste('this wont match anything'); await waitFor(() => expect(searchInput).toHaveValue('this wont match anything')); await searchToResolve(); // When there is no match, the highlighting is removed await waitFor(() => { - expect( - virtualizedContainer.querySelectorAll('.TraceRow.Highlight') - ).toHaveLength(0); + expect(container.querySelectorAll('.TraceRow.Highlight')).toHaveLength(0); }); }, 28_000 From 1803cb9b01377d3e8e9c57b29e1e4a5b83801d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 18:37:14 -0400 Subject: [PATCH 18/21] test(trace): Harden highlight persistence search interactions for CI Wait for search input focus before appending act, move the caret to the end, and use longer waitFor timeouts for value and highlight assertions. After clearing to a no-match query, assert the search iterator shows no results before waiting for highlights to clear. Fixes BROWSE-411. Made-with: Cursor --- .../newTraceDetails/trace.spec.tsx | 66 +++++++++++++------ 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index 4ab64dc3cf875d..f88c7ca5adaf9e 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -1645,39 +1645,67 @@ describe('trace view', () => { await searchToResolve(); let persistedTransactionOp = ''; - await waitFor(() => { - const active = virtualizedContainer.querySelector(ACTIVE_SEARCH_HIGHLIGHT_ROW); - expect(active).toBeTruthy(); - persistedTransactionOp = ( - active!.querySelector('.TraceOperation') as HTMLElement - ).textContent!.trim(); - }); + await waitFor( + () => { + const active = virtualizedContainer.querySelector( + ACTIVE_SEARCH_HIGHLIGHT_ROW + ); + expect(active).toBeTruthy(); + persistedTransactionOp = ( + active!.querySelector('.TraceOperation') as HTMLElement + ).textContent!.trim(); + expect(persistedTransactionOp.length).toBeGreaterThan(0); + }, + {timeout: 10_000} + ); await userEvent.click(searchInput); + await waitFor(() => expect(searchInput).toHaveFocus(), {timeout: 5000}); + await userEvent.keyboard('{End}'); await userEvent.type(searchInput, 'act'); - await waitFor(() => expect(searchInput).toHaveValue('transact')); + await waitFor(() => expect(searchInput).toHaveValue('transact'), { + timeout: 10_000, + }); await searchToResolve(); // Highlighting is persisted on the row - await waitFor(() => { - const active = virtualizedContainer.querySelector(ACTIVE_SEARCH_HIGHLIGHT_ROW); - expect(active).toBeTruthy(); - expect( - (active!.querySelector('.TraceOperation') as HTMLElement).textContent!.trim() - ).toBe(persistedTransactionOp); - }); + await waitFor( + () => { + const active = virtualizedContainer.querySelector( + ACTIVE_SEARCH_HIGHLIGHT_ROW + ); + expect(active).toBeTruthy(); + expect( + ( + active!.querySelector('.TraceOperation') as HTMLElement + ).textContent!.trim() + ).toBe(persistedTransactionOp); + }, + {timeout: 10_000} + ); await userEvent.clear(searchInput); await userEvent.click(searchInput); - await waitFor(() => expect(searchInput).toHaveFocus()); + await waitFor(() => expect(searchInput).toHaveFocus(), {timeout: 5000}); await userEvent.paste('this wont match anything'); - await waitFor(() => expect(searchInput).toHaveValue('this wont match anything')); + await waitFor(() => expect(searchInput).toHaveValue('this wont match anything'), { + timeout: 10_000, + }); await searchToResolve(); - // When there is no match, the highlighting is removed await waitFor(() => { - expect(container.querySelectorAll('.TraceRow.Highlight')).toHaveLength(0); + expect(screen.getByTestId('trace-search-result-iterator')).toHaveTextContent( + 'no results' + ); }); + + // When there is no match, the highlighting is removed + await waitFor( + () => { + expect(container.querySelectorAll('.TraceRow.Highlight')).toHaveLength(0); + }, + {timeout: 10_000} + ); }, 28_000 ); From a59121c68451350dcbeacdb4e15d323fb7fe5b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 19:09:59 -0400 Subject: [PATCH 19/21] test(trace): Drop focus waits and gate clear before no-match paste toHaveFocus on the trace search field was still flaky under CI. After clearing, wait until the input value is empty before pasting so the no-match string is not concatenated onto the previous query. Fixes BROWSE-411. Made-with: Cursor --- static/app/views/performance/newTraceDetails/trace.spec.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index f88c7ca5adaf9e..9c65214f148820 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -1660,7 +1660,6 @@ describe('trace view', () => { ); await userEvent.click(searchInput); - await waitFor(() => expect(searchInput).toHaveFocus(), {timeout: 5000}); await userEvent.keyboard('{End}'); await userEvent.type(searchInput, 'act'); await waitFor(() => expect(searchInput).toHaveValue('transact'), { @@ -1681,12 +1680,12 @@ describe('trace view', () => { ).textContent!.trim() ).toBe(persistedTransactionOp); }, - {timeout: 10_000} + {timeout: 15_000} ); await userEvent.clear(searchInput); + await waitFor(() => expect(searchInput).toHaveValue('')); await userEvent.click(searchInput); - await waitFor(() => expect(searchInput).toHaveFocus(), {timeout: 5000}); await userEvent.paste('this wont match anything'); await waitFor(() => expect(searchInput).toHaveValue('this wont match anything'), { timeout: 10_000, From fcf9c7f6219e9adb3d25fddac122079aed72be0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 19:27:33 -0400 Subject: [PATCH 20/21] test(trace): Stabilize persistence spec with iterator nav Re-focus the search field after results resolve, then move to the second match with ArrowDown instead of clicking the second visible virtualized row (DOM order is not the iterator order under load). Narrow the query with a single change event to transact so tree search does not run through partial strings that can reset the active match. Remove the unused visible search-result row selector constant. Made-with: Cursor --- .../newTraceDetails/trace.spec.tsx | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index 9c65214f148820..118a4e1010c411 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -4,6 +4,7 @@ import {TransactionEventFixture} from 'sentry-fixture/event'; import {ProjectFixture} from 'sentry-fixture/project'; import { + fireEvent, render, screen, userEvent, @@ -779,7 +780,6 @@ const DRAWER_TABS_TEST_ID = 'trace-drawer-tab'; const DRAWER_TABS_PIN_BUTTON_TEST_ID = 'trace-drawer-tab-pin-button'; const VISIBLE_TRACE_ROW_SELECTOR = '.TraceRow:not(.Hidden)'; const ACTIVE_SEARCH_HIGHLIGHT_ROW = '.TraceRow.SearchResult.Highlight:not(.Hidden)'; -const VISIBLE_SEARCH_RESULT_ROW_SELECTOR = `${VISIBLE_TRACE_ROW_SELECTOR}.SearchResult`; const searchToResolve = async (): Promise => { await screen.findByTestId('trace-search-success', undefined, {timeout: 10_000}); @@ -1633,16 +1633,18 @@ describe('trace view', () => { // Wait for the search results to resolve await searchToResolve(); - await waitFor(() => { - expect( - virtualizedContainer.querySelectorAll(VISIBLE_SEARCH_RESULT_ROW_SELECTOR) - .length - ).toBeGreaterThanOrEqual(2); - }); - await userEvent.click( - virtualizedContainer.querySelectorAll(VISIBLE_SEARCH_RESULT_ROW_SELECTOR)[1]! + // Use iterator navigation instead of clicking the second visible virtualized row: + // the row at DOM index [1] is not always the second search match under CI timing. + await userEvent.click(searchInput); + await userEvent.keyboard('{arrowdown}'); + await waitFor( + () => { + expect(screen.getByTestId('trace-search-result-iterator')).toHaveTextContent( + /2\/\d+/ + ); + }, + {timeout: 10_000} ); - await searchToResolve(); let persistedTransactionOp = ''; await waitFor( @@ -1660,8 +1662,9 @@ describe('trace view', () => { ); await userEvent.click(searchInput); - await userEvent.keyboard('{End}'); - await userEvent.type(searchInput, 'act'); + await waitFor(() => expect(searchInput).toHaveValue('trans')); + // Single change event avoids intermediate queries (transa, transac, …) resetting the match. + fireEvent.change(searchInput, {target: {value: 'transact'}}); await waitFor(() => expect(searchInput).toHaveValue('transact'), { timeout: 10_000, }); From 1290aa5fa53506725fa11eed989de7eb0976656d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Wed, 15 Apr 2026 20:25:12 -0400 Subject: [PATCH 21/21] test(trace): Satisfy eslint in persistence spec Remove unnecessary non-null assertions on textContent, drop a stale eslint-disable for no-container, and let eslint --fix align formatting. Made-with: Cursor --- .../performance/newTraceDetails/trace.spec.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index 118a4e1010c411..55b3f4b2ae446e 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -1653,9 +1653,9 @@ describe('trace view', () => { ACTIVE_SEARCH_HIGHLIGHT_ROW ); expect(active).toBeTruthy(); - persistedTransactionOp = ( - active!.querySelector('.TraceOperation') as HTMLElement - ).textContent!.trim(); + persistedTransactionOp = active! + .querySelector('.TraceOperation')! + .textContent.trim(); expect(persistedTransactionOp.length).toBeGreaterThan(0); }, {timeout: 10_000} @@ -1677,11 +1677,9 @@ describe('trace view', () => { ACTIVE_SEARCH_HIGHLIGHT_ROW ); expect(active).toBeTruthy(); - expect( - ( - active!.querySelector('.TraceOperation') as HTMLElement - ).textContent!.trim() - ).toBe(persistedTransactionOp); + expect(active!.querySelector('.TraceOperation')!.textContent.trim()).toBe( + persistedTransactionOp + ); }, {timeout: 15_000} ); @@ -1944,7 +1942,6 @@ describe('trace view', () => { await userEvent.paste('transaction-op-none'); await searchToResolve(); await waitFor(() => { - // eslint-disable-next-line testing-library/no-container expect(container.querySelectorAll('.TraceRow.Highlight')).toHaveLength(0); }); }, 20_000);