Skip to content

ref(replays/issues): update replay inline onboarding panel to use prompt dismiss #69525

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 56 additions & 8 deletions static/app/actionCreators/prompts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,35 @@ 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 = {
data?: PromptResponseItem;
features?: {[key: string]: PromptResponseItem};
};

/**
* Processed endpoint response data
*/
export type PromptData = null | {
/**
* Time since dismissed
*/
dismissedTime?: number;
/**
* Time since snoozed
*/
snoozedTime?: number;
};

Expand Down Expand Up @@ -101,9 +119,6 @@ export const makePromptsCheckQueryKey = ({
];
};

/**
* @param organizationId org numerical id, not the slug
*/
export function usePromptsCheck(
{feature, organization, projectId}: PromptCheckParams,
options: Partial<UseApiQueryOptions<PromptResponse>> = {}
Expand All @@ -122,9 +137,11 @@ export function usePrompt({
feature,
organization,
projectId,
daysToSnooze,
}: {
feature: string;
organization: Organization;
daysToSnooze?: number;
projectId?: string;
}) {
const api = useApi({persistInFlight: true});
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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<PromptResponse>(
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<T extends readonly string[]>(
api: Client,
Expand Down
2 changes: 1 addition & 1 deletion static/app/components/dropdownMenu/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions static/app/components/events/eventEntries.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<EventEntries
{...defaultProps}
Expand Down
4 changes: 4 additions & 0 deletions static/app/components/events/eventReplay/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ describe('EventReplay', function () {
MockUseReplayOnboardingSidebarPanel.mockReturnValue({
activateSidebar: jest.fn(),
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/prompts-activity/',
body: {data: {dismissed_ts: null}},
});
render(<EventReplay {...defaultProps} />, {organization});

expect(
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<ReplayInlineOnboardingPanel platform="react" projectId="123" />);
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(<ReplayInlineOnboardingPanel platform="react" projectId="123" />);
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(<ReplayInlineOnboardingPanel platform="react" projectId="123" />);
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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand All @@ -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;
}

Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down
Loading