From 2c472aaf627024e1aa63dcb532f4880576a929c9 Mon Sep 17 00:00:00 2001 From: billy Date: Wed, 8 Jul 2026 17:04:20 +0000 Subject: [PATCH 1/2] fix(replays): handle sentry.cocoa.unreal in configure replay card sentry-unreal embeds the Cocoa SDK for iOS builds of Unreal Engine games, which reports its SDK name as 'sentry.cocoa.unreal'. This SDK name was not in the getPath() switch in configureReplayCard.tsx, causing it to fall to the default branch: captureMessage fired on every affected page load, and all three Configure Replay dropdown items were disabled. Adding 'sentry.cocoa.unreal' as a fall-through to the existing 'sentry.cocoa' case routes it to the same apple/guides/ios docs path. Refs SENTRY-issue Co-Authored-By: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> --- .../header/configureReplayCard.spec.tsx | 72 +++++++++++++++++++ .../replays/header/configureReplayCard.tsx | 1 + 2 files changed, 73 insertions(+) create mode 100644 static/app/components/replays/header/configureReplayCard.spec.tsx diff --git a/static/app/components/replays/header/configureReplayCard.spec.tsx b/static/app/components/replays/header/configureReplayCard.spec.tsx new file mode 100644 index 000000000000..c153606dfa22 --- /dev/null +++ b/static/app/components/replays/header/configureReplayCard.spec.tsx @@ -0,0 +1,72 @@ +import * as Sentry from '@sentry/react'; +import {OrganizationFixture} from 'sentry-fixture/organization'; +import {ReplayRecordFixture} from 'sentry-fixture/replayRecord'; + +import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; + +import {ConfigureReplayCard} from 'sentry/components/replays/header/configureReplayCard'; + +describe('ConfigureReplayCard', () => { + let captureSpy: jest.SpyInstance; + + beforeEach(() => { + captureSpy = jest.spyOn(Sentry, 'captureMessage').mockImplementation(() => ''); + }); + + afterEach(() => { + captureSpy.mockRestore(); + }); + + describe('getPath — mobile SDK routing', () => { + it('enables menu items for sentry.cocoa (iOS)', async () => { + const replayRecord = ReplayRecordFixture({ + sdk: {name: 'sentry.cocoa', version: '8.0.0'}, + }); + render( + , + {organization: OrganizationFixture()} + ); + + await userEvent.click(screen.getByRole('button', {name: 'Configure Replay'})); + + expect(captureSpy).not.toHaveBeenCalled(); + expect(screen.queryAllByRole('option')).not.toHaveLength(0); + }); + + it('enables menu items for sentry.cocoa.unreal (iOS Unreal via embedded Cocoa SDK)', async () => { + const replayRecord = ReplayRecordFixture({ + sdk: {name: 'sentry.cocoa.unreal', version: '1.0.0'}, + }); + render( + , + {organization: OrganizationFixture()} + ); + + await userEvent.click(screen.getByRole('button', {name: 'Configure Replay'})); + + // Should NOT fall through to the default case; no captureMessage call + expect(captureSpy).not.toHaveBeenCalled(); + + // All three items should be enabled (not aria-disabled) + const items = screen.getAllByRole('option'); + expect(items.length).toBeGreaterThan(0); + items.forEach(item => { + expect(item).not.toHaveAttribute('aria-disabled', 'true'); + }); + }); + + it('logs and disables menu items for unknown mobile platforms', async () => { + const replayRecord = ReplayRecordFixture({ + sdk: {name: 'sentry.unknown.platform', version: '0.0.0'}, + }); + render( + , + {organization: OrganizationFixture()} + ); + + expect(captureSpy).toHaveBeenCalledWith( + 'Unknown mobile platform in configure card: sentry.unknown.platform' + ); + }); + }); +}); diff --git a/static/app/components/replays/header/configureReplayCard.tsx b/static/app/components/replays/header/configureReplayCard.tsx index 2eb1753ac36b..c42125140d2c 100644 --- a/static/app/components/replays/header/configureReplayCard.tsx +++ b/static/app/components/replays/header/configureReplayCard.tsx @@ -44,6 +44,7 @@ export function ConfigureReplayCard({ function getPath(sdkName: string | null | undefined) { switch (sdkName) { case 'sentry.cocoa': + case 'sentry.cocoa.unreal': // Session Replay on iOS builds of Unreal Engine games via the embedded Cocoa SDK return 'apple/guides/ios'; // https://docs.sentry.io/platforms/apple/guides/ios/session-replay/ case 'sentry.java.android': return 'android'; // https://docs.sentry.io/platforms/android/session-replay/ From 27ac81453a1a5301242bf461049468afc58f538e Mon Sep 17 00:00:00 2001 From: billy Date: Wed, 8 Jul 2026 17:10:44 +0000 Subject: [PATCH 2/2] fix(replays): fix configureReplayCard test role and async usage - Use menuitemradio role (react-aria renders DropdownMenu items as role=menuitemradio, not option) - Remove spurious async from test that has no await expression Co-Authored-By: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> --- .../header/configureReplayCard.spec.tsx | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/static/app/components/replays/header/configureReplayCard.spec.tsx b/static/app/components/replays/header/configureReplayCard.spec.tsx index c153606dfa22..3e03d1070cfb 100644 --- a/static/app/components/replays/header/configureReplayCard.spec.tsx +++ b/static/app/components/replays/header/configureReplayCard.spec.tsx @@ -22,47 +22,44 @@ describe('ConfigureReplayCard', () => { const replayRecord = ReplayRecordFixture({ sdk: {name: 'sentry.cocoa', version: '8.0.0'}, }); - render( - , - {organization: OrganizationFixture()} - ); + render(, { + organization: OrganizationFixture(), + }); await userEvent.click(screen.getByRole('button', {name: 'Configure Replay'})); expect(captureSpy).not.toHaveBeenCalled(); - expect(screen.queryAllByRole('option')).not.toHaveLength(0); + expect(screen.getAllByRole('menuitemradio')).not.toHaveLength(0); }); it('enables menu items for sentry.cocoa.unreal (iOS Unreal via embedded Cocoa SDK)', async () => { const replayRecord = ReplayRecordFixture({ sdk: {name: 'sentry.cocoa.unreal', version: '1.0.0'}, }); - render( - , - {organization: OrganizationFixture()} - ); + render(, { + organization: OrganizationFixture(), + }); await userEvent.click(screen.getByRole('button', {name: 'Configure Replay'})); - // Should NOT fall through to the default case; no captureMessage call + // Should NOT fall through to the default case — no captureMessage call expect(captureSpy).not.toHaveBeenCalled(); - // All three items should be enabled (not aria-disabled) - const items = screen.getAllByRole('option'); + // All menu items should be enabled (not aria-disabled) + const items = screen.getAllByRole('menuitemradio'); expect(items.length).toBeGreaterThan(0); items.forEach(item => { expect(item).not.toHaveAttribute('aria-disabled', 'true'); }); }); - it('logs and disables menu items for unknown mobile platforms', async () => { + it('logs and disables menu items for unknown mobile platforms', () => { const replayRecord = ReplayRecordFixture({ sdk: {name: 'sentry.unknown.platform', version: '0.0.0'}, }); - render( - , - {organization: OrganizationFixture()} - ); + render(, { + organization: OrganizationFixture(), + }); expect(captureSpy).toHaveBeenCalledWith( 'Unknown mobile platform in configure card: sentry.unknown.platform'