diff --git a/static/app/actionCreators/prompts.tsx b/static/app/actionCreators/prompts.tsx index bb721699a04bd8..ba0a223e657b13 100644 --- a/static/app/actionCreators/prompts.tsx +++ b/static/app/actionCreators/prompts.tsx @@ -48,8 +48,17 @@ type PromptCheckParams = { projectId?: string; }; +/** + * Raw response data from the endpoint + */ export type PromptResponseItem = { + /** + * Time since dismissed + */ dismissed_ts?: number; + /** + * Time since snoozed + */ snoozed_ts?: number; }; export type PromptResponse = { @@ -57,8 +66,17 @@ export type PromptResponse = { features?: {[key: string]: PromptResponseItem}; }; +/** + * Processed endpoint response data + */ export type PromptData = null | { + /** + * Time since dismissed + */ dismissedTime?: number; + /** + * Time since snoozed + */ snoozedTime?: number; }; @@ -101,9 +119,6 @@ export const makePromptsCheckQueryKey = ({ ]; }; -/** - * @param organizationId org numerical id, not the slug - */ export function usePromptsCheck( {feature, organization, projectId}: PromptCheckParams, options: Partial> = {} @@ -122,9 +137,11 @@ export function usePrompt({ feature, organization, projectId, + daysToSnooze, }: { feature: string; organization: Organization; + daysToSnooze?: number; projectId?: string; }) { const api = useApi({persistInFlight: true}); @@ -133,10 +150,13 @@ export function usePrompt({ const isPromptDismissed = prompt.isSuccess && prompt.data.data - ? promptIsDismissed({ - dismissedTime: prompt.data.data.dismissed_ts, - snoozedTime: prompt.data.data.snoozed_ts, - }) + ? promptIsDismissed( + { + dismissedTime: prompt.data.data.dismissed_ts, + snoozedTime: prompt.data.data.snoozed_ts, + }, + daysToSnooze + ) : undefined; const dismissPrompt = useCallback(() => { @@ -166,16 +186,44 @@ export function usePrompt({ ); }, [api, feature, organization, projectId, queryClient]); + const snoozePrompt = useCallback(() => { + promptsUpdate(api, { + organization, + projectId, + feature, + status: 'snoozed', + }); + + // Update cached query data + // Will set prompt to snoozed + setApiQueryData( + queryClient, + makePromptsCheckQueryKey({ + organization, + feature, + projectId, + }), + () => { + const snoozedTs = new Date().getTime() / 1000; + return { + data: {snoozed_ts: snoozedTs}, + features: {[feature]: {snoozed_ts: snoozedTs}}, + }; + } + ); + }, [api, feature, organization, projectId, queryClient]); + return { isLoading: prompt.isLoading, isError: prompt.isError, isPromptDismissed, dismissPrompt, + snoozePrompt, }; } /** - * Get the status of many prompt + * Get the status of many prompts */ export async function batchedPromptsCheck( api: Client, diff --git a/static/app/components/dropdownMenu/index.spec.tsx b/static/app/components/dropdownMenu/index.spec.tsx index c1b7fa00a29514..568be1732e4088 100644 --- a/static/app/components/dropdownMenu/index.spec.tsx +++ b/static/app/components/dropdownMenu/index.spec.tsx @@ -30,7 +30,7 @@ describe('DropdownMenu', function () { // Open the mneu await userEvent.click(screen.getByRole('button', {name: 'This is a Menu'})); - // The mneu is open + // The menu is open expect(screen.getByRole('menu')).toBeInTheDocument(); // There are two menu items diff --git a/static/app/components/events/eventEntries.spec.tsx b/static/app/components/events/eventEntries.spec.tsx index 691d90d9550012..18b9b7234e592a 100644 --- a/static/app/components/events/eventEntries.spec.tsx +++ b/static/app/components/events/eventEntries.spec.tsx @@ -36,6 +36,10 @@ describe('EventEntries', function () { }); it('renders the replay section in the correct place', async function () { + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/prompts-activity/', + body: {data: {dismissed_ts: null}}, + }); render( , {organization}); expect( diff --git a/static/app/components/events/eventReplay/replayInlineOnboardingPanel.spec.tsx b/static/app/components/events/eventReplay/replayInlineOnboardingPanel.spec.tsx index afce672b4588c9..c267bb3423cd7a 100644 --- a/static/app/components/events/eventReplay/replayInlineOnboardingPanel.spec.tsx +++ b/static/app/components/events/eventReplay/replayInlineOnboardingPanel.spec.tsx @@ -1,44 +1,103 @@ -import {render, screen} from 'sentry-test/reactTestingLibrary'; - -import useDismissAlertImport from 'sentry/utils/useDismissAlert'; +import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import ReplayInlineOnboardingPanel from './replayInlineOnboardingPanel'; -jest.mock('sentry/utils/localStorage'); -jest.mock('sentry/utils/useDismissAlert'); -const useDismissAlert = jest.mocked(useDismissAlertImport); - describe('replayInlineOnboardingPanel', () => { beforeEach(() => { - jest.clearAllMocks(); - useDismissAlert.mockClear(); + MockApiClient.clearMockResponses(); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/prompts-activity/', + body: {data: {dismissed_ts: null}}, + }); }); - it('should render if not dismissed', async () => { - const dismiss = jest.fn(); - useDismissAlert.mockImplementation(() => { - return { - dismiss, - isDismissed: false, - }; + it('shows an onboarding banner that may be dismissed', async () => { + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/prompts-activity/', + body: {data: {}}, }); + const dismissMock = MockApiClient.addMockResponse({ + url: '/organizations/org-slug/prompts-activity/', + method: 'PUT', + }); + render(); expect( await screen.findByText('Watch the errors and latency issues your users face') ).toBeInTheDocument(); + + // Open the snooze or dismiss dropdown + await userEvent.click(screen.getByTestId('icon-close')); + expect(screen.getByText('Dismiss')).toBeInTheDocument(); + expect(screen.getByText('Snooze')).toBeInTheDocument(); + + // Click dismiss + await userEvent.click(screen.getByRole('menuitemradio', {name: 'Dismiss'})); + expect(dismissMock).toHaveBeenCalledWith( + '/organizations/org-slug/prompts-activity/', + expect.objectContaining({ + data: expect.objectContaining({ + feature: 'issue_replay_inline_onboarding', + status: 'dismissed', + }), + }) + ); + expect( + screen.queryByText('Watch the errors and latency issues your users face') + ).not.toBeInTheDocument(); }); - it('should not render if dismissed', async () => { - const dismiss = jest.fn(); - useDismissAlert.mockImplementation(() => { - return { - dismiss, - isDismissed: true, - }; + it('shows an onboarding banner that may be snoozed', async () => { + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/prompts-activity/', + body: {data: {}}, }); + const snoozeMock = MockApiClient.addMockResponse({ + url: '/organizations/org-slug/prompts-activity/', + method: 'PUT', + }); + + render(); + expect( + await screen.findByText('Watch the errors and latency issues your users face') + ).toBeInTheDocument(); + + // Open the snooze or dismiss dropdown + await userEvent.click(screen.getByTestId('icon-close')); + expect(screen.getByText('Dismiss')).toBeInTheDocument(); + expect(screen.getByText('Snooze')).toBeInTheDocument(); + + // Click snooze + await userEvent.click(screen.getByRole('menuitemradio', {name: 'Snooze'})); + expect(snoozeMock).toHaveBeenCalledWith( + '/organizations/org-slug/prompts-activity/', + expect.objectContaining({ + data: expect.objectContaining({ + feature: 'issue_replay_inline_onboarding', + status: 'snoozed', + }), + }) + ); + expect( + screen.queryByText('Watch the errors and latency issues your users face') + ).not.toBeInTheDocument(); + }); + + it('does not render if already dismissed', () => { + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/prompts-activity/', + body: { + data: { + feature: 'issue_replay_inline_onboarding', + status: 'dismissed', + dismissed_ts: 3, + }, + }, + }); + render(); expect( - await screen.queryByText('Watch the errors and latency issues your users face') + screen.queryByText('Watch the errors and latency issues your users face') ).not.toBeInTheDocument(); }); }); diff --git a/static/app/components/events/eventReplay/replayInlineOnboardingPanel.tsx b/static/app/components/events/eventReplay/replayInlineOnboardingPanel.tsx index e9015b7d208ecd..ec8224ef7d9069 100644 --- a/static/app/components/events/eventReplay/replayInlineOnboardingPanel.tsx +++ b/static/app/components/events/eventReplay/replayInlineOnboardingPanel.tsx @@ -2,6 +2,7 @@ import styled from '@emotion/styled'; import replayInlineOnboarding from 'sentry-images/spot/replay-inline-onboarding-v2.svg'; +import {usePrompt} from 'sentry/actionCreators/prompts'; import {Button} from 'sentry/components/button'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; import {EventReplaySection} from 'sentry/components/events/eventReplay/eventReplaySection'; @@ -14,7 +15,6 @@ import type {PlatformKey} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; import {useReplayOnboardingSidebarPanel} from 'sentry/utils/replays/hooks/useReplayOnboarding'; import theme from 'sentry/utils/theme'; -import useDismissAlert from 'sentry/utils/useDismissAlert'; import useMedia from 'sentry/utils/useMedia'; import useOrganization from 'sentry/utils/useOrganization'; @@ -32,26 +32,22 @@ export default function ReplayInlineOnboardingPanel({ platform, projectId, }: OnboardingCTAProps) { - const LOCAL_STORAGE_KEY = `${projectId}:issue-details-replay-onboarding-hide`; - - const {dismiss: snooze, isDismissed: isSnoozed} = useDismissAlert({ - key: LOCAL_STORAGE_KEY, - expirationDays: 7, - }); - - const {dismiss, isDismissed} = useDismissAlert({ - key: LOCAL_STORAGE_KEY, - expirationDays: 365, - }); + const organization = useOrganization(); const {activateSidebar} = useReplayOnboardingSidebarPanel(); const platformKey = platforms.find(p => p.id === platform) ?? otherPlatform; const platformName = platformKey === otherPlatform ? '' : platformKey.name; const isScreenSmall = useMedia(`(max-width: ${theme.breakpoints.small})`); - const organization = useOrganization(); - if (isDismissed || isSnoozed) { + const {isLoading, isError, isPromptDismissed, dismissPrompt, snoozePrompt} = usePrompt({ + feature: 'issue_replay_inline_onboarding', + organization, + projectId, + daysToSnooze: 7, + }); + + if (isLoading || isError || isPromptDismissed) { return null; } @@ -93,7 +89,7 @@ export default function ReplayInlineOnboardingPanel({ key: 'dismiss', label: t('Dismiss'), onAction: () => { - dismiss(); + dismissPrompt(); trackAnalytics('issue-details.replay-cta-dismiss', { organization, type: 'dismiss', @@ -104,7 +100,7 @@ export default function ReplayInlineOnboardingPanel({ key: 'snooze', label: t('Snooze'), onAction: () => { - snooze(); + snoozePrompt(); trackAnalytics('issue-details.replay-cta-dismiss', { organization, type: 'snooze',