diff --git a/src/sentry/static/sentry/app/bootstrap.tsx b/src/sentry/static/sentry/app/bootstrap.tsx index c987adf99abb95..b850c07f7c40bb 100644 --- a/src/sentry/static/sentry/app/bootstrap.tsx +++ b/src/sentry/static/sentry/app/bootstrap.tsx @@ -26,6 +26,8 @@ import ConfigStore from 'app/stores/configStore'; import Main from 'app/main'; import ajaxCsrfSetup from 'app/utils/ajaxCsrfSetup'; import plugins from 'app/plugins'; +import routes from 'app/routes'; +import {normalizeTransactionName} from 'app/utils/apm'; function getSentryIntegrations(hasReplays: boolean = false) { const integrations = [ @@ -74,11 +76,16 @@ const tracesSampleRate = config ? config.apmSampling : 0; const hasReplays = window.__SENTRY__USER && window.__SENTRY__USER.isStaff && !!process.env.DISABLE_RR_WEB; +const appRoutes = Router.createRoutes(routes()); + Sentry.init({ ...window.__SENTRY__OPTIONS, integrations: getSentryIntegrations(hasReplays), tracesSampleRate, _experiments: {useEnvelope: true}, + async beforeSend(event) { + return normalizeTransactionName(appRoutes, event); + }, }); if (window.__SENTRY__USER) { diff --git a/src/sentry/static/sentry/app/utils/apm.tsx b/src/sentry/static/sentry/app/utils/apm.tsx index d2a5a3e6989edf..34f543353982eb 100644 --- a/src/sentry/static/sentry/app/utils/apm.tsx +++ b/src/sentry/static/sentry/app/utils/apm.tsx @@ -1,4 +1,11 @@ import * as Sentry from '@sentry/browser'; +import * as Router from 'react-router'; +import {createMemoryHistory} from 'history'; +import set from 'lodash/set'; + +import getRouteStringFromRoutes from 'app/utils/getRouteStringFromRoutes'; + +const createLocation = createMemoryHistory().createLocation; /** * Sets the transaction name @@ -9,3 +16,61 @@ export function setTransactionName(name: string) { scope.setTag('ui.route', name); }); } + +export async function normalizeTransactionName( + appRoutes: Router.PlainRoute[], + event: Sentry.Event +): Promise { + if (event.type !== 'transaction') { + return event; + } + + // For JavaScript transactions, translate the transaction name if it exists and doesn't start with / + // using the app's react-router routes. If the transaction name doesn't exist, use the window.location.pathname + // as the fallback. + + let prevTransactionName = event.transaction; + + if (typeof prevTransactionName === 'string') { + if (prevTransactionName.startsWith('/')) { + return event; + } + + set(event, ['tags', 'transaction.rename.source'], 'existing transaction name'); + } else { + set(event, ['tags', 'transaction.rename.source'], 'window.location.pathname'); + + prevTransactionName = window.location.pathname; + } + + const transactionName: string | undefined = await new Promise(function(resolve) { + Router.match( + { + routes: appRoutes, + location: createLocation(prevTransactionName), + }, + (error, _redirectLocation, renderProps) => { + if (error) { + set(event, ['tags', 'transaction.rename.react-router-match'], 'error'); + return resolve(undefined); + } + + set(event, ['tags', 'transaction.rename.react-router-match'], 'success'); + + const routePath = getRouteStringFromRoutes(renderProps.routes ?? []); + return resolve(routePath); + } + ); + }); + + if (typeof transactionName === 'string' && transactionName.length) { + event.transaction = transactionName; + + set(event, ['tags', 'transaction.rename.before'], prevTransactionName); + set(event, ['tags', 'transaction.rename.after'], transactionName); + + set(event, ['tags', 'ui.route'], transactionName); + } + + return event; +}