Skip to content
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
22 changes: 20 additions & 2 deletions static/app/components/nav/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export function createNavConfig({organization}: {organization: Organization}): N
label: t('Insights'),
icon: <IconGraph />,
feature: {features: 'insights-entry-points'},
analyticsKey: 'insights',
submenu: [
{
label: MODULE_TITLE_HTTP,
Expand Down Expand Up @@ -94,6 +95,7 @@ export function createNavConfig({organization}: {organization: Organization}): N
const perf: NavSidebarItem = {
label: t('Perf.'),
to: '/performance/',
analyticsKey: 'performance',
icon: <IconLightning />,
feature: {
features: 'performance-view',
Expand All @@ -104,6 +106,7 @@ export function createNavConfig({organization}: {organization: Organization}): N
const perfDomainViews: NavSidebarItem = {
label: t('Perf.'),
icon: <IconLightning />,
analyticsKey: 'insights-domains',
feature: {features: ['insights-domain-view', 'performance-view']},
submenu: [
{
Expand All @@ -130,6 +133,7 @@ export function createNavConfig({organization}: {organization: Organization}): N
{
label: t('Issues'),
icon: <IconIssues />,
analyticsKey: 'issues',
submenu: [
{
label: t('All'),
Expand All @@ -154,10 +158,16 @@ export function createNavConfig({organization}: {organization: Organization}): N
{label: t('Feedback'), to: `/${prefix}/feedback/`},
],
},
{label: t('Projects'), to: `/${prefix}/projects/`, icon: <IconProject />},
{
label: t('Projects'),
analyticsKey: 'projects',
to: `/${prefix}/projects/`,
icon: <IconProject />,
},
{
label: t('Explore'),
icon: <IconSearch />,
analyticsKey: 'explore',
submenu: [
{
label: t('Traces'),
Expand Down Expand Up @@ -201,6 +211,7 @@ export function createNavConfig({organization}: {organization: Organization}): N
...(hasPerfDomainViews ? [perfDomainViews, perf] : [insights, perf]),
{
label: t('Boards'),
analyticsKey: 'customizable-dashboards',
to: '/dashboards/',
icon: <IconDashboard />,
feature: {
Expand All @@ -209,12 +220,18 @@ export function createNavConfig({organization}: {organization: Organization}): N
requireAll: false,
},
},
{label: t('Alerts'), to: `/${prefix}/alerts/rules/`, icon: <IconSiren />},
{
label: t('Alerts'),
analyticsKey: 'alerts',
to: `/${prefix}/alerts/rules/`,
icon: <IconSiren />,
},
],
footer: [
{
label: t('Help'),
icon: <IconQuestion />,
analyticsKey: 'help',
dropdown: [
{
key: 'search',
Expand Down Expand Up @@ -242,6 +259,7 @@ export function createNavConfig({organization}: {organization: Organization}): N
},
{
label: t('Settings'),
analyticsKey: 'settings',
to: `/settings/${organization.slug}/`,
icon: <IconSettings />,
},
Expand Down
30 changes: 29 additions & 1 deletion static/app/components/nav/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import {LocationFixture} from 'sentry-fixture/locationFixture';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {RouterFixture} from 'sentry-fixture/routerFixture';

import {getAllByRole, render, screen} from 'sentry-test/reactTestingLibrary';
import {trackAnalytics} from 'sentry/utils/analytics';

jest.mock('sentry/utils/analytics', () => ({
trackAnalytics: jest.fn(),
}));

import {getAllByRole, render, screen, userEvent} from 'sentry-test/reactTestingLibrary';

import Nav from 'sentry/components/nav';

Expand Down Expand Up @@ -165,4 +171,26 @@ describe('Nav', function () {
});
});
});

describe('analytics', function () {
beforeEach(() => {
render(<Nav />, {
router: RouterFixture({
location: LocationFixture({pathname: '/organizations/org-slug/traces/'}),
}),
organization: OrganizationFixture({features: ALL_AVAILABLE_FEATURES}),
});
});

it('tracks primary sidebar item', async function () {
const issues = screen.getByRole('link', {name: 'Issues'});
await userEvent.click(issues);
expect(trackAnalytics).toHaveBeenCalledWith(
'growth.clicked_sidebar',
expect.objectContaining({
item: 'issues',
})
);
});
});
});
29 changes: 24 additions & 5 deletions static/app/components/nav/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Fragment} from 'react';
import type {MouseEventHandler} from 'react';
import {Fragment, useCallback} from 'react';
import styled from '@emotion/styled';

import Feature from 'sentry/components/acl/feature';
Expand All @@ -18,8 +19,10 @@ import {
} from 'sentry/components/nav/utils';
import SidebarDropdown from 'sentry/components/sidebar/sidebarDropdown';
import {space} from 'sentry/styles/space';
import {trackAnalytics} from 'sentry/utils/analytics';
import theme from 'sentry/utils/theme';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';

function Sidebar() {
return (
Expand Down Expand Up @@ -93,19 +96,28 @@ const SidebarItemList = styled('ul')`

interface SidebarItemProps {
item: NavSidebarItem;
children?: React.ReactNode;
onClick?: MouseEventHandler<HTMLElement>;
}

function SidebarItem({item}: SidebarItemProps) {
const to = resolveNavItemTo(item);
const SidebarChild = to ? SidebarLink : SidebarMenu;
const organization = useOrganization();

const FeatureGuard = item.feature ? Feature : Fragment;
const featureGuardProps: any = item.feature ?? {};

const recordAnalytics = useCallback(
() =>
trackAnalytics('growth.clicked_sidebar', {item: item.analyticsKey, organization}),
[organization, item.analyticsKey]
);

return (
<FeatureGuard {...featureGuardProps}>
<SidebarItemWrapper>
<SidebarChild item={item} key={item.label}>
<SidebarChild item={item} key={item.label} onClick={recordAnalytics}>
{item.icon}
<span>{item.label}</span>
</SidebarChild>
Expand All @@ -127,7 +139,7 @@ const NavButton = styled('button')`
${linkStyles}
`;

function SidebarLink({children, item}: SidebarItemProps & {children: React.ReactNode}) {
function SidebarLink({children, item, onClick}: SidebarItemProps) {
const location = useLocation();
const isActive = isNavItemActive(item, location);
const isSubmenuActive = isSubmenuItemActive(item, location);
Expand All @@ -142,6 +154,7 @@ function SidebarLink({children, item}: SidebarItemProps & {children: React.React
return (
<NavLink
{...linkProps}
onClick={onClick}
className={isActive || isSubmenuActive ? 'active' : undefined}
aria-current={isActive ? 'page' : undefined}
>
Expand All @@ -151,7 +164,7 @@ function SidebarLink({children, item}: SidebarItemProps & {children: React.React
);
}

function SidebarMenu({item, children}: SidebarItemProps & {children: React.ReactNode}) {
function SidebarMenu({item, children, onClick}: SidebarItemProps) {
if (!item.dropdown) {
throw new Error(
`Nav item "${item.label}" must have either a \`dropdown\` or \`to\` value!`
Expand All @@ -162,7 +175,13 @@ function SidebarMenu({item, children}: SidebarItemProps & {children: React.React
position="right-end"
trigger={(props, isOpen) => {
return (
<NavButton {...props}>
<NavButton
{...props}
onClick={event => {
onClick?.(event);
props.onClick?.(event);
}}
>
<InteractionStateLayer hasSelectedBackground={isOpen} />
{children}
</NavButton>
Expand Down
4 changes: 4 additions & 0 deletions static/app/components/nav/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export interface NavItemLayout<Item extends NavSidebarItem | NavSubmenuItem> {
* SidebarItem is a top-level NavItem which is always displayed in the app sidebar
*/
export interface NavSidebarItem extends NavItem {
/**
* A unique identifier string, used as a key for analytics
*/
analyticsKey: string;
/**
* The icon to render in the sidebar
*/
Expand Down
Loading