Skip to content
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

feat(nav): add analytics tracking #80512

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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'},
id: '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/',
id: '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 />,
id: '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 />,
id: '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'),
id: 'projects',
to: `/${prefix}/projects/`,
icon: <IconProject />,
},
{
label: t('Explore'),
icon: <IconSearch />,
id: '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'),
id: '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'),
id: 'alerts',
to: `/${prefix}/alerts/rules/`,
icon: <IconSiren />,
},
],
footer: [
{
label: t('Help'),
icon: <IconQuestion />,
id: 'help',
dropdown: [
{
key: 'search',
Expand Down Expand Up @@ -242,6 +259,7 @@ export function createNavConfig({organization}: {organization: Organization}): N
},
{
label: t('Settings'),
id: 'settings',
to: `/settings/${organization.slug}/`,
icon: <IconSettings />,
},
Expand Down
28 changes: 27 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,11 @@ 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 * as analytics from 'sentry/utils/analytics';

const analyticsSpy = jest.spyOn(analytics, 'trackAnalytics');

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

import Nav from 'sentry/components/nav';

Expand Down Expand Up @@ -165,4 +169,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(analyticsSpy).toHaveBeenCalledWith(
'growth.clicked_sidebar',
expect.objectContaining({
item: 'issues',
})
);
});
});
});
28 changes: 23 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,27 @@ 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.id, organization}),
[organization, item.id]
);

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 +138,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 +153,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 +163,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 +174,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 @@ -37,6 +37,10 @@ export interface NavSidebarItem extends NavItem {
* The icon to render in the sidebar
*/
icon: React.ReactElement;
/**
* A unique identifier string, used as a key for analytics
*/
id: string;
Comment on lines +40 to +43
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there a reasonable chance (and from experience, I would probably say that there is), then I would rather namespace this under analytics object. It's not much of a difference, but it gives you more flexibility and isolation of the analytics and item concept (you'd just access it via item.analytics.id)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a separate note, you could apply inversion of control here, and define this as a callback function, so that if someone needs to extend anything, they don't need to go through the item definition and add fields, bloating the struct

/**
* dropdown menu to display when this SidebarItem is clicked
*/
Expand Down
Loading