From b41b71223ed4d76d44a4dfec26dda8a2ff1691a3 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Wed, 27 May 2026 16:03:52 -0700 Subject: [PATCH 1/6] fix(issues): Restore issue details tour launcher The old issue details guide was still wired into anchors that no longer exist, while the current tour had no launcher when the backend marked it unseen. Remove the stale guide and anchor wrappers, keep the existing tour, and lazy-load the start tour modal when the backend says to show it. Co-Authored-By: Codex GPT-5 --- .../components/assistant/getGuidesContent.tsx | 61 --------- .../components/assistant/guideAnchor.spec.tsx | 34 ++--- .../events/eventTagsAndScreenshot/tags.tsx | 7 +- .../app/views/issueDetails/groupDetails.tsx | 2 + .../groupEventDetails.spec.tsx | 2 +- .../groupEventDetailsContent.tsx | 124 ++++++++---------- .../views/issueDetails/issueDetailsTour.tsx | 108 ++++++++++++++- 7 files changed, 177 insertions(+), 161 deletions(-) diff --git a/static/app/components/assistant/getGuidesContent.tsx b/static/app/components/assistant/getGuidesContent.tsx index 39b7e0ff26afe0..b69bfc63a7d940 100644 --- a/static/app/components/assistant/getGuidesContent.tsx +++ b/static/app/components/assistant/getGuidesContent.tsx @@ -10,67 +10,6 @@ export function getGuidesContent(): GuidesContent { return getDemoModeGuides(); } return [ - { - guide: 'issue', - requiredTargets: ['issue_header_stats', 'breadcrumbs', 'issue_sidebar_owners'], - steps: [ - { - title: t('How bad is it?'), - target: 'issue_header_stats', - description: t( - `You have Issues and that's fine. - Understand impact at a glance by viewing total issue frequency and affected users.` - ), - }, - { - title: t('Find problematic releases'), - target: 'issue_sidebar_releases', - description: t( - 'See which release introduced the issue and which release it last appeared in.' - ), - }, - { - title: t('Not your typical stack trace'), - target: 'stacktrace', - description: t( - `Sentry can show your source code in the stack trace. - See the exact sequence of function calls leading to the error in question.` - ), - }, - { - // TODO(streamline-ui): Remove from guides on GA, tag sidebar is gone - title: t('Pinpoint hotspots'), - target: 'issue_sidebar_tags', - description: t( - 'Tags are key/value string pairs that are automatically indexed and searchable in Sentry.' - ), - }, - { - title: t('Retrace Your Steps'), - target: 'breadcrumbs', - description: t( - `Not sure how you got here? Sentry automatically captures breadcrumbs for - events your user and app took that led to the error.` - ), - }, - { - title: t('Annoy the Right People'), - target: 'issue_sidebar_owners', - description: t( - `Automatically assign issues to the person who introduced the commit, - notify them over notification tools like Slack, - and triage through issue management tools like Jira. ` - ), - }, - { - title: t('Onboarding'), - target: 'onboarding_sidebar', - description: t( - 'Walk through this guide to get the most out of Sentry right away.' - ), - }, - ], - }, { guide: 'issue_stream', requiredTargets: ['issue_stream'], diff --git a/static/app/components/assistant/guideAnchor.spec.tsx b/static/app/components/assistant/guideAnchor.spec.tsx index c6fa62504ec1ec..03b5124fb5c322 100644 --- a/static/app/components/assistant/guideAnchor.spec.tsx +++ b/static/app/components/assistant/guideAnchor.spec.tsx @@ -10,11 +10,11 @@ import {GuideStore} from 'sentry/stores/guideStore'; describe('GuideAnchor', () => { const serverGuide = [ { - guide: 'issue', + guide: 'trace_view', seen: false, }, ]; - const firstGuideHeader = 'How bad is it?'; + const firstGuideHeader = 'Events'; beforeEach(() => { ConfigStore.loadInitialData( @@ -30,29 +30,19 @@ describe('GuideAnchor', () => { it('renders, async advances, async and finishes', async () => { render(
- - - + +
); act(() => GuideStore.fetchSucceeded(serverGuide)); expect(await screen.findByText(firstGuideHeader)).toBeInTheDocument(); - // XXX(epurkhiser): Skip pointer event checks due to a bug with how Popper - // renders the hovercard with pointer-events: none. See [0] - // - // [0]: https://github.com/testing-library/user-event/issues/639 - // - // NOTE(epurkhiser): We may be able to remove the skipPointerEventsCheck - // when we're on popper >= 1. await userEvent.click(screen.getByLabelText('Next')); - expect(await screen.findByText('Retrace Your Steps')).toBeInTheDocument(); + expect(await screen.findByText('Event Details')).toBeInTheDocument(); expect(screen.queryByText(firstGuideHeader)).not.toBeInTheDocument(); - await userEvent.click(screen.getByLabelText('Next')); - // Clicking on the button in the last step should finish the guide. const finishMock = MockApiClient.addMockResponse({ method: 'PUT', @@ -66,7 +56,7 @@ describe('GuideAnchor', () => { expect.objectContaining({ method: 'PUT', data: { - guide: 'issue', + guide: 'trace_view', status: 'viewed', }, }) @@ -76,9 +66,8 @@ describe('GuideAnchor', () => { it('dismisses', async () => { render(
- - - + +
); @@ -97,7 +86,7 @@ describe('GuideAnchor', () => { expect.objectContaining({ method: 'PUT', data: { - guide: 'issue', + guide: 'trace_view', status: 'dismissed', }, }) @@ -131,9 +120,8 @@ describe('GuideAnchor', () => { it('if forceHide is true, async do not render guide', async () => { render(
- - - + +
); diff --git a/static/app/components/events/eventTagsAndScreenshot/tags.tsx b/static/app/components/events/eventTagsAndScreenshot/tags.tsx index db78ed223d2ecc..33c7b95b4cea19 100644 --- a/static/app/components/events/eventTagsAndScreenshot/tags.tsx +++ b/static/app/components/events/eventTagsAndScreenshot/tags.tsx @@ -3,7 +3,6 @@ import {useMemo, useState} from 'react'; import {Grid} from '@sentry/scraps/layout'; import {SegmentedControl} from '@sentry/scraps/segmentedControl'; -import {GuideAnchor} from 'sentry/components/assistant/guideAnchor'; import {EventTags} from 'sentry/components/events/eventTags'; import { associateTagsWithMeta, @@ -83,11 +82,7 @@ export function EventTagsDataSection({ return ( - {t('Tags')} - - } + title={t('Tags')} actions={actions} sectionKey={SectionKey.TAGS} ref={ref} diff --git a/static/app/views/issueDetails/groupDetails.tsx b/static/app/views/issueDetails/groupDetails.tsx index ac0a3c10db674e..117a20b14d6d1e 100644 --- a/static/app/views/issueDetails/groupDetails.tsx +++ b/static/app/views/issueDetails/groupDetails.tsx @@ -56,6 +56,7 @@ import {useGroupDistributionsDrawer} from 'sentry/views/issueDetails/groupDistri import GroupEventDetails from 'sentry/views/issueDetails/groupEventDetails/groupEventDetails'; import { ISSUE_DETAILS_TOUR_GUIDE_KEY, + IssueDetailsTourModal, IssueDetailsTourContext, ORDERED_ISSUE_DETAILS_TOUR, type IssueDetailsTour, @@ -836,6 +837,7 @@ function GroupDetailsPageContent(props: GroupDetailsPageContentProps) { orderedStepIds={ORDERED_ISSUE_DETAILS_TOUR} TourContext={IssueDetailsTourContext} > + { initialRouterConfig, }); - expect(await screen.findByRole('region', {name: 'tags'})).toBeInTheDocument(); + expect(await screen.findByRole('region', {name: 'Tags'})).toBeInTheDocument(); const highlights = screen.getByRole('region', {name: 'Highlights'}); expect(within(highlights).getByRole('button', {name: 'Edit'})).toBeInTheDocument(); diff --git a/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx b/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx index d62f01539bb22e..441dd7783cdac0 100644 --- a/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx +++ b/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx @@ -1,8 +1,6 @@ import {Fragment, useMemo, useRef} from 'react'; -import {ClassNames} from '@emotion/react'; import Feature from 'sentry/components/acl/feature'; -import {GuideAnchor} from 'sentry/components/assistant/guideAnchor'; import {ErrorBoundary} from 'sentry/components/errorBoundary'; import {BreadcrumbsDataSection} from 'sentry/components/events/breadcrumbs/breadcrumbsDataSection'; import {EventContexts} from 'sentry/components/events/contexts'; @@ -173,79 +171,67 @@ export function EventDetailsContent({ {isMetricKitHang ? ( ) : ( - /* Wrapping all stacktrace components since multiple could appear */ - - {({css}) => ( - - {shouldShowTombstonesBanner(event) && !isSampleError && ( - - + {shouldShowTombstonesBanner(event) && !isSampleError && ( + + + + )} + {defined(eventEntries[EntryType.EXCEPTION]) && ( + + {shouldUseNewStackTrace ? ( + + ) : ( + + )} + + )} + {issueTypeConfig.stacktrace.enabled && + defined(eventEntries[EntryType.STACKTRACE]) && ( + + {shouldUseNewStackTrace ? ( + - - )} - {defined(eventEntries[EntryType.EXCEPTION]) && ( - - {shouldUseNewStackTrace ? ( - - ) : ( - - )} - - )} - {issueTypeConfig.stacktrace.enabled && - defined(eventEntries[EntryType.STACKTRACE]) && ( - - {shouldUseNewStackTrace ? ( - - ) : ( - - )} - - )} - {defined(eventEntries[EntryType.THREADS]) && ( - - - - )} - + )} + + )} + {defined(eventEntries[EntryType.THREADS]) && ( + + + )} - + )} {isANR && ( diff --git a/static/app/views/issueDetails/issueDetailsTour.tsx b/static/app/views/issueDetails/issueDetailsTour.tsx index 8a26ed4764eeb1..3c98c5e5f1dc62 100644 --- a/static/app/views/issueDetails/issueDetailsTour.tsx +++ b/static/app/views/issueDetails/issueDetailsTour.tsx @@ -1,6 +1,14 @@ -import {createContext} from 'react'; +import {createContext, useContext, useEffect, useRef} from 'react'; + +import issueDetailsPreview from 'sentry-images/issue_details/issue-details-preview.png'; + +import {useModal} from '@sentry/scraps/modal'; import type {TourContextType} from 'sentry/components/tours/tourContext'; +import {useAssistant, useMutateAssistant} from 'sentry/components/tours/useAssistant'; +import {t} from 'sentry/locale'; +import {trackAnalytics} from 'sentry/utils/analytics'; +import {useOrganization} from 'sentry/utils/useOrganization'; export const enum IssueDetailsTour { /** Trends and aggregates, the graph, and tag distributions */ @@ -27,6 +35,104 @@ export const ORDERED_ISSUE_DETAILS_TOUR = [ ]; export const ISSUE_DETAILS_TOUR_GUIDE_KEY = 'tour.issue_details'; +export const ISSUE_DETAILS_TOUR_FORCE_HASH = '#issue-details-tour'; export const IssueDetailsTourContext = createContext | null>(null); + +export function useIssueDetailsTourModal() { + const {openModal} = useModal(); + const organization = useOrganization(); + const hasOpenedTourModal = useRef(false); + const {isRegistered, currentStepId, startTour, endTour} = useContext( + IssueDetailsTourContext + )!; + const {data: assistantData} = useAssistant({ + notifyOnChangeProps: ['data'], + }); + const {mutate: mutateAssistant} = useMutateAssistant(); + const forceShowTourModal = window.location.hash === ISSUE_DETAILS_TOUR_FORCE_HASH; + const hasUnseenIssueDetailsTour = + assistantData?.find(item => item.guide === ISSUE_DETAILS_TOUR_GUIDE_KEY)?.seen === + false; + + const shouldShowTourModal = + !process.env.IS_ACCEPTANCE_TEST && + currentStepId === null && + (forceShowTourModal || hasUnseenIssueDetailsTour); + + useEffect(() => { + if (!isRegistered || !shouldShowTourModal || hasOpenedTourModal.current) { + return; + } + + let cancelled = false; + const dismissTour = () => { + mutateAssistant({ + guide: ISSUE_DETAILS_TOUR_GUIDE_KEY, + status: 'dismissed', + }); + endTour(); + trackAnalytics('issue_details.tour.skipped', {organization}); + }; + + hasOpenedTourModal.current = true; + void import('sentry/components/tours/startTour').then( + ({StartTourModal, startTourModalCss}) => { + if (cancelled) { + return; + } + + openModal( + props => ( + { + startTour(); + trackAnalytics('issue_details.tour.started', { + organization, + method: 'modal', + }); + }} + /> + ), + { + modalCss: startTourModalCss, + onClose: reason => { + if (reason) { + dismissTour(); + } + }, + } + ); + } + ); + + return () => { + cancelled = true; + }; + }, [ + endTour, + forceShowTourModal, + isRegistered, + mutateAssistant, + openModal, + organization, + shouldShowTourModal, + startTour, + ]); +} + +export function IssueDetailsTourModal() { + useIssueDetailsTourModal(); + return null; +} From eaa8442d58f7411abdcdd413555831f29a1dafef Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Wed, 27 May 2026 16:07:39 -0700 Subject: [PATCH 2/6] remove export --- static/app/views/issueDetails/issueDetailsTour.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/issueDetails/issueDetailsTour.tsx b/static/app/views/issueDetails/issueDetailsTour.tsx index 3c98c5e5f1dc62..05dcb72beda1fa 100644 --- a/static/app/views/issueDetails/issueDetailsTour.tsx +++ b/static/app/views/issueDetails/issueDetailsTour.tsx @@ -35,7 +35,7 @@ export const ORDERED_ISSUE_DETAILS_TOUR = [ ]; export const ISSUE_DETAILS_TOUR_GUIDE_KEY = 'tour.issue_details'; -export const ISSUE_DETAILS_TOUR_FORCE_HASH = '#issue-details-tour'; +const ISSUE_DETAILS_TOUR_FORCE_HASH = '#issue-details-tour'; export const IssueDetailsTourContext = createContext | null>(null); From 04028f674df532a7008080dd216354aa7824dc74 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Wed, 27 May 2026 16:08:01 -0700 Subject: [PATCH 3/6] another export --- static/app/views/issueDetails/issueDetailsTour.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/issueDetails/issueDetailsTour.tsx b/static/app/views/issueDetails/issueDetailsTour.tsx index 05dcb72beda1fa..d8145fb093465e 100644 --- a/static/app/views/issueDetails/issueDetailsTour.tsx +++ b/static/app/views/issueDetails/issueDetailsTour.tsx @@ -40,7 +40,7 @@ const ISSUE_DETAILS_TOUR_FORCE_HASH = '#issue-details-tour'; export const IssueDetailsTourContext = createContext | null>(null); -export function useIssueDetailsTourModal() { +function useIssueDetailsTourModal() { const {openModal} = useModal(); const organization = useOrganization(); const hasOpenedTourModal = useRef(false); From 7a8bcaa846eb0f79b19fad87fbc4463f23b2108d Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Wed, 27 May 2026 16:20:10 -0700 Subject: [PATCH 4/6] test(assistant): Use full trace guide in anchor test The issue details guide is gone, so use the existing trace guide as the replacement fixture. Render all of its anchors so the test covers the actual guide flow instead of depending on missing-step filtering. --- static/app/components/assistant/guideAnchor.spec.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/static/app/components/assistant/guideAnchor.spec.tsx b/static/app/components/assistant/guideAnchor.spec.tsx index 03b5124fb5c322..893ba4681e0d95 100644 --- a/static/app/components/assistant/guideAnchor.spec.tsx +++ b/static/app/components/assistant/guideAnchor.spec.tsx @@ -14,7 +14,7 @@ describe('GuideAnchor', () => { seen: false, }, ]; - const firstGuideHeader = 'Events'; + const firstGuideHeader = 'Event Breakdown'; beforeEach(() => { ConfigStore.loadInitialData( @@ -30,6 +30,7 @@ describe('GuideAnchor', () => { it('renders, async advances, async and finishes', async () => { render(
+
@@ -40,9 +41,14 @@ describe('GuideAnchor', () => { await userEvent.click(screen.getByLabelText('Next')); - expect(await screen.findByText('Event Details')).toBeInTheDocument(); + expect(await screen.findByText('Events')).toBeInTheDocument(); expect(screen.queryByText(firstGuideHeader)).not.toBeInTheDocument(); + await userEvent.click(screen.getByLabelText('Next')); + + expect(await screen.findByText('Event Details')).toBeInTheDocument(); + expect(screen.queryByText('Events')).not.toBeInTheDocument(); + // Clicking on the button in the last step should finish the guide. const finishMock = MockApiClient.addMockResponse({ method: 'PUT', @@ -66,6 +72,7 @@ describe('GuideAnchor', () => { it('dismisses', async () => { render(
+
@@ -120,6 +127,7 @@ describe('GuideAnchor', () => { it('if forceHide is true, async do not render guide', async () => { render(
+
From 320eda4f54e1a9b5bbea90ce7c53d6fe7f765c96 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Thu, 28 May 2026 16:08:47 -0700 Subject: [PATCH 5/6] test(issues): update guide store expectations --- static/app/stores/guideStore.spec.tsx | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/static/app/stores/guideStore.spec.tsx b/static/app/stores/guideStore.spec.tsx index c76eef35e3c67d..d06a3d82741e6e 100644 --- a/static/app/stores/guideStore.spec.tsx +++ b/static/app/stores/guideStore.spec.tsx @@ -25,15 +25,14 @@ describe('GuideStore', () => { GuideStore.init(); data = [ { - guide: 'issue', + guide: 'trace_view', seen: false, }, {guide: 'issue_stream', seen: true}, ]; - GuideStore.registerAnchor('issue_header_stats'); - GuideStore.registerAnchor('issue_sidebar_owners'); - GuideStore.registerAnchor('breadcrumbs'); GuideStore.registerAnchor('issue_stream'); + GuideStore.registerAnchor('trace_view_guide_row'); + GuideStore.registerAnchor('trace_view_guide_row_details'); }); afterEach(() => { @@ -44,14 +43,12 @@ describe('GuideStore', () => { GuideStore.fetchSucceeded(data); // Should pick the first non-seen guide in alphabetic order. expect(GuideStore.getState().currentStep).toBe(0); - expect(GuideStore.getState().currentGuide?.guide).toBe('issue'); + expect(GuideStore.getState().currentGuide?.guide).toBe('trace_view'); // Should prune steps that don't have anchors. - expect(GuideStore.getState().currentGuide?.steps).toHaveLength(3); + expect(GuideStore.getState().currentGuide?.steps).toHaveLength(2); GuideStore.nextStep(); expect(GuideStore.getState().currentStep).toBe(1); - GuideStore.nextStep(); - expect(GuideStore.getState().currentStep).toBe(2); GuideStore.closeGuide(); expect(GuideStore.getState().currentGuide).toBeNull(); }); @@ -59,18 +56,18 @@ describe('GuideStore', () => { it('should force show a guide with #assistant', () => { data = [ { - guide: 'issue', + guide: 'issue_stream', seen: true, }, - {guide: 'issue_stream', seen: false}, + {guide: 'trace_view', seen: false}, ]; GuideStore.fetchSucceeded(data); window.location.hash = '#assistant'; window.dispatchEvent(new Event('load')); - expect(GuideStore.getState().currentGuide?.guide).toBe('issue'); - GuideStore.closeGuide(); expect(GuideStore.getState().currentGuide?.guide).toBe('issue_stream'); + GuideStore.closeGuide(); + expect(GuideStore.getState().currentGuide?.guide).toBe('trace_view'); window.location.hash = ''; }); @@ -85,10 +82,10 @@ describe('GuideStore', () => { it('should record analytics events when guide is cued', () => { const spy = jest.spyOn(GuideStore, 'recordCue'); GuideStore.fetchSucceeded(data); - expect(spy).toHaveBeenCalledWith('issue'); + expect(spy).toHaveBeenCalledWith('trace_view'); expect(trackAnalytics).toHaveBeenCalledWith('assistant.guide_cued', { - guide: 'issue', + guide: 'trace_view', organization: null, }); @@ -105,7 +102,7 @@ describe('GuideStore', () => { it('only shows guides with server data and content', () => { data = [ { - guide: 'issue', + guide: 'issue_stream', seen: true, }, { From 3ddb1db4ae96ad88dad7caa83e37b5ce92f6e5eb Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Fri, 29 May 2026 14:18:42 -0700 Subject: [PATCH 6/6] add more copy lol --- static/app/views/issueDetails/eventNavigation/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/app/views/issueDetails/eventNavigation/index.tsx b/static/app/views/issueDetails/eventNavigation/index.tsx index a3dfed99440709..dd07dd97283f36 100644 --- a/static/app/views/issueDetails/eventNavigation/index.tsx +++ b/static/app/views/issueDetails/eventNavigation/index.tsx @@ -246,9 +246,9 @@ export function IssueEventNavigation({event, group}: IssueEventNavigationProps) tourContext={IssueDetailsTourContext} id={IssueDetailsTour.NAVIGATION} - title={t('Compare events')} + title={t('Compare and copy events')} description={t( - 'Review the events associated with an issue. Compare the first, latest, or recommended event to see what changed.' + 'Review the events associated with an issue. Compare the first, latest, or recommended event to see what changed, or use Copy as to copy the issue details as Markdown.' )} > {tourProps => (