Skip to content

ref: move use-button-tracking from sentry hooks to react context #95220

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
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
1 change: 1 addition & 0 deletions knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const productionEntryPoints = [
// dynamic imports _not_ recognized by knip
'static/app/bootstrap/{index,initializeMain}.tsx',
'static/gsApp/initializeBundleMetrics.tsx',
'static/gsApp/registerHooks.tsx',
// defined in webpack.config pipelines
'static/app/utils/statics-setup.tsx',
'static/app/views/integrationPipeline/index.tsx',
Expand Down
7 changes: 5 additions & 2 deletions static/app/bootstrap/initializeApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ import {processInitQueue} from './processInitQueue';
import {renderMain} from './renderMain';
import {renderOnDomReady} from './renderOnDomReady';

export function initializeApp(config: Config) {
export function initializeApp(
config: Config,
SentryHooksProvider?: React.ComponentType<React.PropsWithChildren>
) {
initializeSdk(config);
// Initialize the config store after the SDK, so we can log errors to Sentry during config initialization if needed. N.B. This mutates the config slightly
commonInitialization(config);

// Used for operational metrics to determine that the application js
// bundle was loaded by browser.
metric.mark({name: 'sentry-app-init'});
renderOnDomReady(renderMain);
renderOnDomReady(renderMain(SentryHooksProvider));
processInitQueue();
}
7 changes: 5 additions & 2 deletions static/app/bootstrap/initializeMain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import type {Config} from 'sentry/types/system';

import {initializeLocale} from './initializeLocale';

export async function initializeMain(config: Config) {
export async function initializeMain(
config: Config,
SentryHooksProvider?: React.ComponentType<React.PropsWithChildren>
) {
// This needs to be loaded as early as possible, or else the locale library can
// throw an exception and prevent the application from being loaded.
//
Expand All @@ -12,5 +15,5 @@ export async function initializeMain(config: Config) {
// This is dynamically imported because we need to make sure locale is configured
// before proceeding.
const {initializeApp} = await import('./initializeApp');
initializeApp(config);
initializeApp(config, SentryHooksProvider);
}
32 changes: 19 additions & 13 deletions static/app/bootstrap/renderMain.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import type * as React from 'react';

import {ROOT_ELEMENT} from 'sentry/constants';
import Main from 'sentry/main';

import {renderDom} from './renderDom';

export function renderMain() {
try {
renderDom(Main, `#${ROOT_ELEMENT}`, {});
} catch (err) {
if (err.message === 'URI malformed') {
// eslint-disable-next-line no-console
console.error(
new Error(
'An unencoded "%" has appeared, it is super effective! (See https://github.com/ReactTraining/history/issues/505)'
)
);
window.location.assign(window.location.pathname);
export function renderMain(
SentryHooksProvider?: React.ComponentType<React.PropsWithChildren>
) {
return () => {
try {
renderDom(Main, `#${ROOT_ELEMENT}`, {SentryHooksProvider});
} catch (err) {
if (err.message === 'URI malformed') {
// eslint-disable-next-line no-console
console.error(
new Error(
'An unencoded "%" has appeared, it is super effective! (See https://github.com/ReactTraining/history/issues/505)'
)
);
window.location.assign(window.location.pathname);
}
}
}
};
}
22 changes: 11 additions & 11 deletions static/app/components/core/button/useButtonFunctionality.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,7 @@ export function useButtonFunctionality(props: ButtonProps | LinkButtonProps) {
props['aria-label'] ??
(typeof props.children === 'string' ? props.children : undefined);

const buttonTracking = useButtonTracking({
analyticsEventName: props.analyticsEventName,
analyticsEventKey: props.analyticsEventKey,
analyticsParams: {
priority: props.priority,
href: 'href' in props ? props.href : undefined,
...props.analyticsParams,
},
'aria-label': accessibleLabel || '',
});
const buttonTracking = useButtonTracking();

const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
// Don't allow clicks when disabled or busy
Expand All @@ -32,7 +23,16 @@ export function useButtonFunctionality(props: ButtonProps | LinkButtonProps) {
return;
}

buttonTracking();
buttonTracking({
analyticsEventName: props.analyticsEventName,
analyticsEventKey: props.analyticsEventKey,
analyticsParams: {
priority: props.priority,
href: 'href' in props ? props.href : undefined,
...props.analyticsParams,
},
'aria-label': accessibleLabel || '',
});
// @ts-expect-error at this point, we don't know if the button is a button or a link
props.onClick?.(e);
};
Expand Down
10 changes: 5 additions & 5 deletions static/app/components/core/trackingContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import {createContext, useContext} from 'react';

import type {DO_NOT_USE_ButtonProps as ButtonProps} from './button/types';

const defaultButtonTracking = (props: ButtonProps) => {
const defaultButtonTracking = () => {
const hasAnalyticsDebug = window.localStorage?.getItem('DEBUG_ANALYTICS') === '1';
return () => {
return (props: ButtonProps) => {
const hasCustomAnalytics =
props.analyticsEventName || props.analyticsEventKey || props.analyticsParams;
if (hasCustomAnalytics && hasAnalyticsDebug) {
Expand All @@ -21,13 +21,13 @@ const defaultButtonTracking = (props: ButtonProps) => {
};

const TrackingContext = createContext<{
useButtonTracking?: (props: ButtonProps) => () => void;
buttonTracking?: (props: ButtonProps) => void;
}>({});

export const TrackingContextProvider = TrackingContext.Provider;

export const useButtonTracking = (props: ButtonProps) => {
export const useButtonTracking = () => {
const context = useContext(TrackingContext);

return context.useButtonTracking?.(props) ?? defaultButtonTracking(props);
return context.buttonTracking ?? defaultButtonTracking();
};
4 changes: 2 additions & 2 deletions static/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ async function app() {
// getsentry augments Sentry's application through a 'hook' mechanism. Sentry
// provides various hooks into parts of its application. Thus all getsentry
// functionality is initialized by registering its hook functions.
const {default: registerHooks} = await registerHooksImport;
const {default: registerHooks, SentryHooksProvider} = await registerHooksImport;
registerHooks();

const {initializeMain} = await initalizeMainImport;
initializeMain(config);
initializeMain(config, SentryHooksProvider);

const {initializeBundleMetrics} = await initalizeBundleMetricsImport;
initializeBundleMetrics();
Expand Down
17 changes: 9 additions & 8 deletions static/app/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,30 @@ import {OnboardingContextProvider} from 'sentry/components/onboarding/onboarding
import {ThemeAndStyleProvider} from 'sentry/components/themeAndStyleProvider';
import {USE_REACT_QUERY_DEVTOOL} from 'sentry/constants';
import {routes} from 'sentry/routes';
import {SentryTrackingProvider} from 'sentry/tracking';
import {DANGEROUS_SET_REACT_ROUTER_6_HISTORY} from 'sentry/utils/browserHistory';

import {buildReactRouter6Routes} from './utils/reactRouter6Compat/router';

function buildRouter() {
function buildRouter(SentryHooksProvider?: React.ComponentType<React.PropsWithChildren>) {
const sentryCreateBrowserRouter = wrapCreateBrowserRouterV6(createBrowserRouter);
const router = sentryCreateBrowserRouter(buildReactRouter6Routes(routes()));
const router = sentryCreateBrowserRouter(
buildReactRouter6Routes(routes(SentryHooksProvider))
);
DANGEROUS_SET_REACT_ROUTER_6_HISTORY(router);

return router;
}

function Main() {
const [router] = useState(buildRouter);
function Main(props: {
SentryHooksProvider?: React.ComponentType<React.PropsWithChildren>;
}) {
const [router] = useState(() => buildRouter(props.SentryHooksProvider));

return (
<AppQueryClientProvider>
<ThemeAndStyleProvider>
<OnboardingContextProvider>
<SentryTrackingProvider>
<RouterProvider router={router} />
</SentryTrackingProvider>
<RouterProvider router={router} />
</OnboardingContextProvider>
{USE_REACT_QUERY_DEVTOOL && (
<ReactQueryDevtools initialIsOpen={false} buttonPosition="bottom-left" />
Expand Down
32 changes: 20 additions & 12 deletions static/app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ import {makeLazyloadComponent as make} from './makeLazyloadComponent';

const hook = (name: HookName) => HookStore.get(name).map(cb => cb());

function buildRoutes() {
function buildRoutes(
SentryHooksProvider: React.ComponentType<React.PropsWithChildren> = Fragment
) {
// Read this to understand where to add new routes, how / why the routing
// tree is structured the way it is, and how the lazy-loading /
// code-splitting works for pages.
Expand Down Expand Up @@ -2560,18 +2562,24 @@ function buildRoutes() {
);

const appRoutes = (
<ProvideAriaRouter>
<Route>
{experimentalSpaRoutes}
<Route path="/" component={errorHandler(App)}>
{rootRoutes}
{authV2Routes}
{organizationRoutes}
{legacyRedirectRoutes}
<Route path="*" component={errorHandler(RouteNotFound)} />
</Route>
<Route
component={({children}: {children: React.ReactNode}) => {
return (
<ProvideAriaRouter>
<SentryHooksProvider>{children}</SentryHooksProvider>
</ProvideAriaRouter>
);
}}
>
{experimentalSpaRoutes}
<Route path="/" component={errorHandler(App)}>
{rootRoutes}
{authV2Routes}
{organizationRoutes}
{legacyRedirectRoutes}
<Route path="*" component={errorHandler(RouteNotFound)} />
</Route>
</ProvideAriaRouter>
</Route>
);

return appRoutes;
Expand Down
15 changes: 0 additions & 15 deletions static/app/tracking.tsx

This file was deleted.

2 changes: 0 additions & 2 deletions static/app/types/hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type {ChildrenRenderFn} from 'sentry/components/acl/feature';
import type {Guide} from 'sentry/components/assistant/types';
import type {ButtonProps} from 'sentry/components/core/button';
import type {SelectKey} from 'sentry/components/core/compactSelect';
import type {FormPanelProps} from 'sentry/components/forms/formPanel';
import type {JsonFormObject} from 'sentry/components/forms/types';
Expand Down Expand Up @@ -315,7 +314,6 @@ type ReactHooks = {
'react-hook:route-activated': (
props: RouteContextInterface
) => React.ContextType<typeof RouteAnalyticsContext>;
'react-hook:use-button-tracking': (props: ButtonProps) => () => void;
'react-hook:use-get-max-retention-days': () => number | undefined;
};

Expand Down
27 changes: 12 additions & 15 deletions static/gsApp/hooks/useButtonTracking.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,10 @@ describe('buttonTracking', function () {

it('calls rawTrackAnalyticsEvent with default values', function () {
const {result} = renderHook(useButtonTracking, {
initialProps: {'aria-label': 'Create Alert'},
wrapper,
});

result.current();
result.current({'aria-label': 'Create Alert'});

expect(rawTrackAnalyticsEvent).toHaveBeenCalledWith({
eventName: null,
Expand All @@ -63,16 +62,15 @@ describe('buttonTracking', function () {

it('calls rawTrackAnalyticsEvent with data', function () {
const {result} = renderHook(useButtonTracking, {
initialProps: {
'aria-label': 'Create Alert',
analyticsEventKey: 'settings.create_alert',
analyticsEventName: 'Settings: Create Alert',
analyticsParams: {priority: 'primary', href: 'sentry.io/settings/create_alert'},
},
wrapper,
});

result.current();
result.current({
'aria-label': 'Create Alert',
analyticsEventKey: 'settings.create_alert',
analyticsEventName: 'Settings: Create Alert',
analyticsParams: {priority: 'primary', href: 'sentry.io/settings/create_alert'},
});

expect(rawTrackAnalyticsEvent).toHaveBeenCalledWith({
eventName: 'Settings: Create Alert',
Expand All @@ -88,15 +86,14 @@ describe('buttonTracking', function () {

it('calls rawTrackAnalyticsEvent with new event names', function () {
const {result} = renderHook(useButtonTracking, {
initialProps: {
'aria-label': 'Create Alert',
analyticsEventKey: 'settings.create_alert',
analyticsEventName: 'Settings: Create Alert',
},
wrapper,
});

result.current();
result.current({
'aria-label': 'Create Alert',
analyticsEventKey: 'settings.create_alert',
analyticsEventName: 'Settings: Create Alert',
});

expect(rawTrackAnalyticsEvent).toHaveBeenCalledWith({
eventName: 'Settings: Create Alert',
Expand Down
Loading
Loading