Skip to content

Commit 3532ed8

Browse files
authored
feat: set custom_referrer if projects visited from org menu (#104902)
This adds `custom_referrer` tracking specifically to opening the project page, for the benefit of Amplitude. I added the ability to do so using session storage, so that we don't muck up the URL with the `?referrer=foo` query parameters (we already litter these all over the place, and people have complained about cruft in URIs). There's follow up work to refactor `onAction` to work with `to` so that the link is still copyable in the menu.
1 parent 0be602e commit 3532ed8

File tree

6 files changed

+67
-2
lines changed

6 files changed

+67
-2
lines changed

static/app/constants/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export const CUSTOMER_DOMAIN =
2121
? undefined
2222
: window?.__initialData?.customerDomain?.subdomain;
2323

24+
// Constant used for tracking referrer in session storage rather than
25+
// ?referrer=foo get parameter:
26+
export const CUSTOM_REFERRER_KEY = 'customReferrer';
27+
2428
// This is considered the "default" route/view that users should be taken
2529
// to when the application does not have any further context
2630
//

static/app/utils/useSessionStorage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import sessionStorageWrapper from 'sentry/utils/sessionStorage';
44

55
const isBrowser = typeof window !== 'undefined';
66

7-
function readStorageValue<T>(key: string, initialValue: T) {
7+
export function readStorageValue<T>(key: string, initialValue: T) {
88
const value = sessionStorageWrapper.getItem(key);
99

1010
// We check for 'undefined' because the value may have

static/app/views/nav/orgDropdown.spec.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import {UserFixture} from 'sentry-fixture/user';
33

44
import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
55

6+
import {CUSTOM_REFERRER_KEY} from 'sentry/constants';
67
import ConfigStore from 'sentry/stores/configStore';
78
import OrganizationsStore from 'sentry/stores/organizationsStore';
9+
import {readStorageValue} from 'sentry/utils/useSessionStorage';
810
import {OrgDropdown} from 'sentry/views/nav/orgDropdown';
911

1012
describe('OrgDropdown', () => {
@@ -102,4 +104,18 @@ describe('OrgDropdown', () => {
102104
within(inactiveGroup).getByRole('menuitemradio', {name: /Deleting org/})
103105
).toBeInTheDocument();
104106
});
107+
108+
it('clicking project sets referrer in session storage', async () => {
109+
render(<OrgDropdown />, {organization});
110+
await userEvent.click(screen.getByRole('button', {name: 'Toggle organization menu'}));
111+
// We use onAction to navigate, no href is set:
112+
expect(screen.getByRole('menuitemradio', {name: 'Projects'})).not.toHaveAttribute(
113+
'href'
114+
);
115+
// onClick should take precedence setting session storage value and navigating:
116+
await userEvent.click(screen.getByRole('menuitemradio', {name: 'Projects'}));
117+
expect(readStorageValue<string | null>(CUSTOM_REFERRER_KEY, null)).toBe(
118+
'org-dropdown'
119+
);
120+
});
105121
});

static/app/views/nav/orgDropdown.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {Button} from 'sentry/components/core/button';
88
import {DropdownMenu, type MenuItemProps} from 'sentry/components/dropdownMenu';
99
import OrganizationBadge from 'sentry/components/idBadge/organizationBadge';
1010
import QuestionTooltip from 'sentry/components/questionTooltip';
11+
import {CUSTOM_REFERRER_KEY} from 'sentry/constants';
1112
import {IconAdd} from 'sentry/icons';
1213
import {t, tn} from 'sentry/locale';
1314
import ConfigStore from 'sentry/stores/configStore';
@@ -16,12 +17,16 @@ import {useLegacyStore} from 'sentry/stores/useLegacyStore';
1617
import type {Organization} from 'sentry/types/organization';
1718
import {isDemoModeActive} from 'sentry/utils/demoMode';
1819
import {localizeDomain, resolveRoute} from 'sentry/utils/resolveRoute';
20+
import {useNavigate} from 'sentry/utils/useNavigate';
1921
import useOrganization from 'sentry/utils/useOrganization';
2022
import useProjects from 'sentry/utils/useProjects';
23+
import {useSessionStorage} from 'sentry/utils/useSessionStorage';
2124
import {useNavContext} from 'sentry/views/nav/context';
2225
import {NavLayout} from 'sentry/views/nav/types';
2326
import {makeProjectsPathname} from 'sentry/views/projects/pathname';
2427

28+
const ORG_DROPDOWN_REFERRER = 'org-dropdown';
29+
2530
function createOrganizationMenuItem(): MenuItemProps {
2631
const configFeatures = ConfigStore.get('features');
2732
const sentryUrl = localizeDomain(ConfigStore.get('links').sentryUrl);
@@ -58,6 +63,8 @@ export function OrgDropdown({
5863
}) {
5964
const config = useLegacyStore(ConfigStore);
6065
const organization = useOrganization();
66+
const navigate = useNavigate();
67+
const [, setReferrer] = useSessionStorage<string | null>(CUSTOM_REFERRER_KEY, null);
6168

6269
// It's possible we do not have an org in context (e.g. RouteNotFound)
6370
// Otherwise, we should have the full org
@@ -141,7 +148,10 @@ export function OrgDropdown({
141148
{
142149
key: 'projects',
143150
label: t('Projects'),
144-
to: makeProjectsPathname({path: '/', organization}),
151+
onAction: () => {
152+
setReferrer(ORG_DROPDOWN_REFERRER);
153+
navigate(makeProjectsPathname({path: '/', organization}));
154+
},
145155
hidden: hideOrgLinks,
146156
},
147157
{

static/gsApp/utils/rawTrackAnalyticsEvent.spec.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {OrganizationFixture} from 'sentry-fixture/organization';
33
import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription';
44
import {setWindowLocation} from 'sentry-test/utils';
55

6+
import {CUSTOM_REFERRER_KEY} from 'sentry/constants';
67
import ConfigStore from 'sentry/stores/configStore';
78
import {uniqueId} from 'sentry/utils/guid';
89
import sessionStorage from 'sentry/utils/sessionStorage';
@@ -184,6 +185,31 @@ describe('rawTrackAnalyticsEvent', () => {
184185
);
185186
setWindowLocation('http:/localhost/');
186187
});
188+
189+
it('sets custom_referrer if found in local storage', () => {
190+
sessionStorage.setItem(CUSTOM_REFERRER_KEY, JSON.stringify('batman'));
191+
setWindowLocation('http:/localhost');
192+
rawTrackAnalyticsEvent({
193+
eventKey: 'test_event',
194+
eventName: 'Test Event',
195+
organization,
196+
});
197+
198+
expect(trackReloadEvent).toHaveBeenCalledWith(
199+
'test_event',
200+
expect.objectContaining({custom_referrer: 'batman'})
201+
);
202+
203+
expect(trackAmplitudeEvent).toHaveBeenCalledWith(
204+
'Test Event',
205+
org_id,
206+
expect.objectContaining({custom_referrer: 'batman'}),
207+
{time: undefined}
208+
);
209+
setWindowLocation('http:/localhost/');
210+
expect(sessionStorage.getItem(CUSTOM_REFERRER_KEY)).toBeNull();
211+
});
212+
187213
it('start analytics session', () => {
188214
rawTrackAnalyticsEvent(
189215
{

static/gsApp/utils/rawTrackAnalyticsEvent.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as qs from 'query-string';
22

3+
import {CUSTOM_REFERRER_KEY} from 'sentry/constants';
34
import ConfigStore from 'sentry/stores/configStore';
45
import type {Hooks} from 'sentry/types/hooks';
56
import type {Organization} from 'sentry/types/organization';
@@ -8,6 +9,7 @@ import getDaysSinceDate from 'sentry/utils/getDaysSinceDate';
89
import {uniqueId} from 'sentry/utils/guid';
910
import localStorage from 'sentry/utils/localStorage';
1011
import sessionStorage from 'sentry/utils/sessionStorage';
12+
import {readStorageValue} from 'sentry/utils/useSessionStorage';
1113

1214
import type {Subscription} from 'getsentry/types';
1315
import {hasNewBillingUI} from 'getsentry/utils/billing';
@@ -73,9 +75,16 @@ const getCustomReferrer = () => {
7375
try {
7476
// pull the referrer from the query parameter of the page
7577
const {referrer} = qs.parse(window.location.search) || {};
78+
// pull the referrer from session storage.
79+
const storedReferrer = readStorageValue<string | null>(CUSTOM_REFERRER_KEY, null);
80+
// ?referrer takes precedence, but still unset session stored referrer.
81+
if (storedReferrer) {
82+
sessionStorage.removeItem(CUSTOM_REFERRER_KEY);
83+
}
7684
if (referrer && typeof referrer === 'string') {
7785
return referrer;
7886
}
87+
return storedReferrer;
7988
} catch {
8089
// ignore if this fails to parse
8190
// this can happen if we have an invalid query string

0 commit comments

Comments
 (0)