Skip to content

Commit 907d497

Browse files
ref(replays/issues): update replay inline onboarding panel to use prompt dismiss (#69525)
- followup from #69513 - closes #69207 - update the issue details replay inline onboarding CTA to use prompt dismiss instead of `useDismissAlert` - behavior should still be the same: snooze is 7 days, dismiss is forever - also made some updates to `usePrompt` hook to allow for snoozing ![image](https://github.com/getsentry/sentry/assets/56095982/93541900-5df6-4da9-ab14-0af54cda7575)
1 parent 3b9675f commit 907d497

File tree

6 files changed

+160
-49
lines changed

6 files changed

+160
-49
lines changed

static/app/actionCreators/prompts.tsx

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,35 @@ type PromptCheckParams = {
4848
projectId?: string;
4949
};
5050

51+
/**
52+
* Raw response data from the endpoint
53+
*/
5154
export type PromptResponseItem = {
55+
/**
56+
* Time since dismissed
57+
*/
5258
dismissed_ts?: number;
59+
/**
60+
* Time since snoozed
61+
*/
5362
snoozed_ts?: number;
5463
};
5564
export type PromptResponse = {
5665
data?: PromptResponseItem;
5766
features?: {[key: string]: PromptResponseItem};
5867
};
5968

69+
/**
70+
* Processed endpoint response data
71+
*/
6072
export type PromptData = null | {
73+
/**
74+
* Time since dismissed
75+
*/
6176
dismissedTime?: number;
77+
/**
78+
* Time since snoozed
79+
*/
6280
snoozedTime?: number;
6381
};
6482

@@ -101,9 +119,6 @@ export const makePromptsCheckQueryKey = ({
101119
];
102120
};
103121

104-
/**
105-
* @param organizationId org numerical id, not the slug
106-
*/
107122
export function usePromptsCheck(
108123
{feature, organization, projectId}: PromptCheckParams,
109124
options: Partial<UseApiQueryOptions<PromptResponse>> = {}
@@ -122,9 +137,11 @@ export function usePrompt({
122137
feature,
123138
organization,
124139
projectId,
140+
daysToSnooze,
125141
}: {
126142
feature: string;
127143
organization: Organization;
144+
daysToSnooze?: number;
128145
projectId?: string;
129146
}) {
130147
const api = useApi({persistInFlight: true});
@@ -133,10 +150,13 @@ export function usePrompt({
133150

134151
const isPromptDismissed =
135152
prompt.isSuccess && prompt.data.data
136-
? promptIsDismissed({
137-
dismissedTime: prompt.data.data.dismissed_ts,
138-
snoozedTime: prompt.data.data.snoozed_ts,
139-
})
153+
? promptIsDismissed(
154+
{
155+
dismissedTime: prompt.data.data.dismissed_ts,
156+
snoozedTime: prompt.data.data.snoozed_ts,
157+
},
158+
daysToSnooze
159+
)
140160
: undefined;
141161

142162
const dismissPrompt = useCallback(() => {
@@ -166,16 +186,44 @@ export function usePrompt({
166186
);
167187
}, [api, feature, organization, projectId, queryClient]);
168188

189+
const snoozePrompt = useCallback(() => {
190+
promptsUpdate(api, {
191+
organization,
192+
projectId,
193+
feature,
194+
status: 'snoozed',
195+
});
196+
197+
// Update cached query data
198+
// Will set prompt to snoozed
199+
setApiQueryData<PromptResponse>(
200+
queryClient,
201+
makePromptsCheckQueryKey({
202+
organization,
203+
feature,
204+
projectId,
205+
}),
206+
() => {
207+
const snoozedTs = new Date().getTime() / 1000;
208+
return {
209+
data: {snoozed_ts: snoozedTs},
210+
features: {[feature]: {snoozed_ts: snoozedTs}},
211+
};
212+
}
213+
);
214+
}, [api, feature, organization, projectId, queryClient]);
215+
169216
return {
170217
isLoading: prompt.isLoading,
171218
isError: prompt.isError,
172219
isPromptDismissed,
173220
dismissPrompt,
221+
snoozePrompt,
174222
};
175223
}
176224

177225
/**
178-
* Get the status of many prompt
226+
* Get the status of many prompts
179227
*/
180228
export async function batchedPromptsCheck<T extends readonly string[]>(
181229
api: Client,

static/app/components/dropdownMenu/index.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe('DropdownMenu', function () {
3030
// Open the mneu
3131
await userEvent.click(screen.getByRole('button', {name: 'This is a Menu'}));
3232

33-
// The mneu is open
33+
// The menu is open
3434
expect(screen.getByRole('menu')).toBeInTheDocument();
3535

3636
// There are two menu items

static/app/components/events/eventEntries.spec.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ describe('EventEntries', function () {
3636
});
3737

3838
it('renders the replay section in the correct place', async function () {
39+
MockApiClient.addMockResponse({
40+
url: '/organizations/org-slug/prompts-activity/',
41+
body: {data: {dismissed_ts: null}},
42+
});
3943
render(
4044
<EventEntries
4145
{...defaultProps}

static/app/components/events/eventReplay/index.spec.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ describe('EventReplay', function () {
148148
MockUseReplayOnboardingSidebarPanel.mockReturnValue({
149149
activateSidebar: jest.fn(),
150150
});
151+
MockApiClient.addMockResponse({
152+
url: '/organizations/org-slug/prompts-activity/',
153+
body: {data: {dismissed_ts: null}},
154+
});
151155
render(<EventReplay {...defaultProps} />, {organization});
152156

153157
expect(
Lines changed: 83 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,103 @@
1-
import {render, screen} from 'sentry-test/reactTestingLibrary';
2-
3-
import useDismissAlertImport from 'sentry/utils/useDismissAlert';
1+
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
42

53
import ReplayInlineOnboardingPanel from './replayInlineOnboardingPanel';
64

7-
jest.mock('sentry/utils/localStorage');
8-
jest.mock('sentry/utils/useDismissAlert');
9-
const useDismissAlert = jest.mocked(useDismissAlertImport);
10-
115
describe('replayInlineOnboardingPanel', () => {
126
beforeEach(() => {
13-
jest.clearAllMocks();
14-
useDismissAlert.mockClear();
7+
MockApiClient.clearMockResponses();
8+
MockApiClient.addMockResponse({
9+
url: '/organizations/org-slug/prompts-activity/',
10+
body: {data: {dismissed_ts: null}},
11+
});
1512
});
1613

17-
it('should render if not dismissed', async () => {
18-
const dismiss = jest.fn();
19-
useDismissAlert.mockImplementation(() => {
20-
return {
21-
dismiss,
22-
isDismissed: false,
23-
};
14+
it('shows an onboarding banner that may be dismissed', async () => {
15+
MockApiClient.addMockResponse({
16+
url: '/organizations/org-slug/prompts-activity/',
17+
body: {data: {}},
2418
});
19+
const dismissMock = MockApiClient.addMockResponse({
20+
url: '/organizations/org-slug/prompts-activity/',
21+
method: 'PUT',
22+
});
23+
2524
render(<ReplayInlineOnboardingPanel platform="react" projectId="123" />);
2625
expect(
2726
await screen.findByText('Watch the errors and latency issues your users face')
2827
).toBeInTheDocument();
28+
29+
// Open the snooze or dismiss dropdown
30+
await userEvent.click(screen.getByTestId('icon-close'));
31+
expect(screen.getByText('Dismiss')).toBeInTheDocument();
32+
expect(screen.getByText('Snooze')).toBeInTheDocument();
33+
34+
// Click dismiss
35+
await userEvent.click(screen.getByRole('menuitemradio', {name: 'Dismiss'}));
36+
expect(dismissMock).toHaveBeenCalledWith(
37+
'/organizations/org-slug/prompts-activity/',
38+
expect.objectContaining({
39+
data: expect.objectContaining({
40+
feature: 'issue_replay_inline_onboarding',
41+
status: 'dismissed',
42+
}),
43+
})
44+
);
45+
expect(
46+
screen.queryByText('Watch the errors and latency issues your users face')
47+
).not.toBeInTheDocument();
2948
});
3049

31-
it('should not render if dismissed', async () => {
32-
const dismiss = jest.fn();
33-
useDismissAlert.mockImplementation(() => {
34-
return {
35-
dismiss,
36-
isDismissed: true,
37-
};
50+
it('shows an onboarding banner that may be snoozed', async () => {
51+
MockApiClient.addMockResponse({
52+
url: '/organizations/org-slug/prompts-activity/',
53+
body: {data: {}},
3854
});
55+
const snoozeMock = MockApiClient.addMockResponse({
56+
url: '/organizations/org-slug/prompts-activity/',
57+
method: 'PUT',
58+
});
59+
60+
render(<ReplayInlineOnboardingPanel platform="react" projectId="123" />);
61+
expect(
62+
await screen.findByText('Watch the errors and latency issues your users face')
63+
).toBeInTheDocument();
64+
65+
// Open the snooze or dismiss dropdown
66+
await userEvent.click(screen.getByTestId('icon-close'));
67+
expect(screen.getByText('Dismiss')).toBeInTheDocument();
68+
expect(screen.getByText('Snooze')).toBeInTheDocument();
69+
70+
// Click snooze
71+
await userEvent.click(screen.getByRole('menuitemradio', {name: 'Snooze'}));
72+
expect(snoozeMock).toHaveBeenCalledWith(
73+
'/organizations/org-slug/prompts-activity/',
74+
expect.objectContaining({
75+
data: expect.objectContaining({
76+
feature: 'issue_replay_inline_onboarding',
77+
status: 'snoozed',
78+
}),
79+
})
80+
);
81+
expect(
82+
screen.queryByText('Watch the errors and latency issues your users face')
83+
).not.toBeInTheDocument();
84+
});
85+
86+
it('does not render if already dismissed', () => {
87+
MockApiClient.addMockResponse({
88+
url: '/organizations/org-slug/prompts-activity/',
89+
body: {
90+
data: {
91+
feature: 'issue_replay_inline_onboarding',
92+
status: 'dismissed',
93+
dismissed_ts: 3,
94+
},
95+
},
96+
});
97+
3998
render(<ReplayInlineOnboardingPanel platform="react" projectId="123" />);
4099
expect(
41-
await screen.queryByText('Watch the errors and latency issues your users face')
100+
screen.queryByText('Watch the errors and latency issues your users face')
42101
).not.toBeInTheDocument();
43102
});
44103
});

static/app/components/events/eventReplay/replayInlineOnboardingPanel.tsx

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import styled from '@emotion/styled';
22

33
import replayInlineOnboarding from 'sentry-images/spot/replay-inline-onboarding-v2.svg';
44

5+
import {usePrompt} from 'sentry/actionCreators/prompts';
56
import {Button} from 'sentry/components/button';
67
import {DropdownMenu} from 'sentry/components/dropdownMenu';
78
import {EventReplaySection} from 'sentry/components/events/eventReplay/eventReplaySection';
@@ -14,7 +15,6 @@ import type {PlatformKey} from 'sentry/types/project';
1415
import {trackAnalytics} from 'sentry/utils/analytics';
1516
import {useReplayOnboardingSidebarPanel} from 'sentry/utils/replays/hooks/useReplayOnboarding';
1617
import theme from 'sentry/utils/theme';
17-
import useDismissAlert from 'sentry/utils/useDismissAlert';
1818
import useMedia from 'sentry/utils/useMedia';
1919
import useOrganization from 'sentry/utils/useOrganization';
2020

@@ -32,26 +32,22 @@ export default function ReplayInlineOnboardingPanel({
3232
platform,
3333
projectId,
3434
}: OnboardingCTAProps) {
35-
const LOCAL_STORAGE_KEY = `${projectId}:issue-details-replay-onboarding-hide`;
36-
37-
const {dismiss: snooze, isDismissed: isSnoozed} = useDismissAlert({
38-
key: LOCAL_STORAGE_KEY,
39-
expirationDays: 7,
40-
});
41-
42-
const {dismiss, isDismissed} = useDismissAlert({
43-
key: LOCAL_STORAGE_KEY,
44-
expirationDays: 365,
45-
});
35+
const organization = useOrganization();
4636

4737
const {activateSidebar} = useReplayOnboardingSidebarPanel();
4838

4939
const platformKey = platforms.find(p => p.id === platform) ?? otherPlatform;
5040
const platformName = platformKey === otherPlatform ? '' : platformKey.name;
5141
const isScreenSmall = useMedia(`(max-width: ${theme.breakpoints.small})`);
52-
const organization = useOrganization();
5342

54-
if (isDismissed || isSnoozed) {
43+
const {isLoading, isError, isPromptDismissed, dismissPrompt, snoozePrompt} = usePrompt({
44+
feature: 'issue_replay_inline_onboarding',
45+
organization,
46+
projectId,
47+
daysToSnooze: 7,
48+
});
49+
50+
if (isLoading || isError || isPromptDismissed) {
5551
return null;
5652
}
5753

@@ -93,7 +89,7 @@ export default function ReplayInlineOnboardingPanel({
9389
key: 'dismiss',
9490
label: t('Dismiss'),
9591
onAction: () => {
96-
dismiss();
92+
dismissPrompt();
9793
trackAnalytics('issue-details.replay-cta-dismiss', {
9894
organization,
9995
type: 'dismiss',
@@ -104,7 +100,7 @@ export default function ReplayInlineOnboardingPanel({
104100
key: 'snooze',
105101
label: t('Snooze'),
106102
onAction: () => {
107-
snooze();
103+
snoozePrompt();
108104
trackAnalytics('issue-details.replay-cta-dismiss', {
109105
organization,
110106
type: 'snooze',

0 commit comments

Comments
 (0)