1- import { Transaction , TransactionContext } from '@sentry/types' ;
1+ import { Transaction } from '@sentry/types' ;
22import { getGlobalObject } from '@sentry/utils' ;
3+ import * as React from 'react' ;
34
4- type ReactRouterInstrumentation = < T extends Transaction > (
5- startTransaction : ( context : TransactionContext ) => T | undefined ,
6- startTransactionOnPageLoad ?: boolean ,
7- startTransactionOnLocationChange ?: boolean ,
8- ) => void ;
5+ import { Action , Location , ReactRouterInstrumentation } from './types' ;
96
10- // Many of the types below had to be mocked out to prevent typescript issues
11- // these types are required for correct functionality.
7+ type Match = { path : string ; url : string ; params : Record < string , any > ; isExact : boolean } ;
128
13- export type Route = { path ?: string ; childRoutes ?: Route [ ] } ;
14-
15- export type Match = (
16- props : { location : Location ; routes : Route [ ] } ,
17- cb : ( error ?: Error , _redirectLocation ?: Location , renderProps ?: { routes ?: Route [ ] } ) => void ,
18- ) => void ;
19-
20- type Location = {
21- pathname : string ;
22- action ?: 'PUSH' | 'REPLACE' | 'POP' ;
23- } & Record < string , any > ;
24-
25- type History = {
9+ export type RouterHistory = {
2610 location ?: Location ;
27- listen ?( cb : ( location : Location ) => void ) : void ;
11+ listen ?( cb : ( location : Location , action : Action ) => void ) : void ;
2812} & Record < string , any > ;
2913
14+ export type RouteConfig = {
15+ path ?: string | string [ ] ;
16+ exact ?: boolean ;
17+ component ?: JSX . Element ;
18+ routes ?: RouteConfig [ ] ;
19+ [ propName : string ] : any ;
20+ } ;
21+
22+ type MatchPath = ( pathname : string , props : string | string [ ] | any , parent ?: Match | null ) => Match | null ;
23+
3024const global = getGlobalObject < Window > ( ) ;
3125
32- /**
33- * Creates routing instrumentation for React Router v3
34- * Works for React Router >= 3.2.0 and < 4.0.0
35- *
36- * @param history object from the `history` library
37- * @param routes a list of all routes, should be
38- * @param match `Router.match` utility
39- */
40- export function reactRouterV3Instrumentation (
41- history : History ,
42- routes : Route [ ] ,
43- match : Match ,
26+ let activeTransaction : Transaction | undefined ;
27+
28+ export function reactRouterV4Instrumentation (
29+ history : RouterHistory ,
30+ routes ?: RouteConfig [ ] ,
31+ matchPath ?: MatchPath ,
32+ ) : ReactRouterInstrumentation {
33+ return reactRouterInstrumentation ( history , 'react-router-v4' , routes , matchPath ) ;
34+ }
35+
36+ export function reactRouterV5Instrumentation (
37+ history : RouterHistory ,
38+ routes ?: RouteConfig [ ] ,
39+ matchPath ?: MatchPath ,
4440) : ReactRouterInstrumentation {
45- return (
46- startTransaction : ( context : TransactionContext ) => Transaction | undefined ,
47- startTransactionOnPageLoad : boolean = true ,
48- startTransactionOnLocationChange : boolean = true ,
49- ) => {
50- let activeTransaction : Transaction | undefined ;
51- let prevName : string | undefined ;
41+ return reactRouterInstrumentation ( history , 'react-router-v5' , routes , matchPath ) ;
42+ }
43+
44+ function reactRouterInstrumentation (
45+ history : RouterHistory ,
46+ name : string ,
47+ allRoutes : RouteConfig [ ] = [ ] ,
48+ matchPath ?: MatchPath ,
49+ ) : ReactRouterInstrumentation {
50+ function getName ( pathname : string ) : string {
51+ if ( allRoutes === [ ] || ! matchPath ) {
52+ return pathname ;
53+ }
5254
55+ const branches = matchRoutes ( allRoutes , pathname , matchPath ) ;
56+ // tslint:disable-next-line: prefer-for-of
57+ for ( let x = 0 ; x < branches . length ; x ++ ) {
58+ if ( branches [ x ] . match . isExact ) {
59+ return branches [ x ] . match . path ;
60+ }
61+ }
62+
63+ return pathname ;
64+ }
65+
66+ return ( startTransaction , startTransactionOnPageLoad = true , startTransactionOnLocationChange = true ) => {
5367 if ( startTransactionOnPageLoad && global && global . location ) {
54- // Have to use global.location because history.location might not be defined.
55- prevName = normalizeTransactionName ( routes , global . location , match ) ;
5668 activeTransaction = startTransaction ( {
57- name : prevName ,
69+ name : getName ( global . location . pathname ) ,
5870 op : 'pageload' ,
5971 tags : {
60- 'routing.instrumentation' : 'react-router-v3' ,
72+ 'routing.instrumentation' : name ,
6173 } ,
6274 } ) ;
6375 }
6476
6577 if ( startTransactionOnLocationChange && history . listen ) {
66- history . listen ( location => {
67- if ( location . action === 'PUSH' ) {
78+ history . listen ( ( location , action ) => {
79+ if ( action && ( action === 'PUSH' || action === 'POP' ) ) {
6880 if ( activeTransaction ) {
6981 activeTransaction . finish ( ) ;
7082 }
71- const tags : Record < string , string > = { 'routing.instrumentation' : 'react-router-v3' } ;
72- if ( prevName ) {
73- tags . from = prevName ;
74- }
83+ const tags = {
84+ 'routing.instrumentation' : name ,
85+ } ;
7586
76- prevName = normalizeTransactionName ( routes , location , match ) ;
7787 activeTransaction = startTransaction ( {
78- name : prevName ,
88+ name : getName ( location . pathname ) ,
7989 op : 'navigation' ,
8090 tags,
8191 } ) ;
@@ -86,54 +96,43 @@ export function reactRouterV3Instrumentation(
8696}
8797
8898/**
89- * Normalize transaction names using `Router.match`
99+ * Matches a set of routes to a pathname
100+ * Based on implementation from
90101 */
91- function normalizeTransactionName ( appRoutes : Route [ ] , location : Location , match : Match ) : string {
92- let name = location . pathname ;
93- match (
94- {
95- location,
96- routes : appRoutes ,
97- } ,
98- ( error , _redirectLocation , renderProps ) => {
99- if ( error || ! renderProps ) {
100- return name ;
102+ function matchRoutes (
103+ routes : RouteConfig [ ] ,
104+ pathname : string ,
105+ matchPath : MatchPath ,
106+ branch : Array < { route : RouteConfig ; match : Match } > = [ ] ,
107+ ) : Array < { route : RouteConfig ; match : Match } > {
108+ routes . some ( route => {
109+ const match = route . path
110+ ? matchPath ( pathname , route )
111+ : branch . length
112+ ? branch [ branch . length - 1 ] . match // use parent match
113+ : computeRootMatch ( pathname ) ; // use default "root" match
114+
115+ if ( match ) {
116+ branch . push ( { route, match } ) ;
117+
118+ if ( route . routes ) {
119+ matchRoutes ( route . routes , pathname , matchPath , branch ) ;
101120 }
121+ }
102122
103- const routePath = getRouteStringFromRoutes ( renderProps . routes || [ ] ) ;
104- if ( routePath . length === 0 || routePath === '/*' ) {
105- return name ;
106- }
123+ return ! ! match ;
124+ } ) ;
107125
108- name = routePath ;
109- return name ;
110- } ,
111- ) ;
112- return name ;
126+ return branch ;
113127}
114128
115- /**
116- * Generate route name from array of routes
117- */
118- function getRouteStringFromRoutes ( routes : Route [ ] ) : string {
119- if ( ! Array . isArray ( routes ) || routes . length === 0 ) {
120- return '' ;
121- }
122-
123- const routesWithPaths : Route [ ] = routes . filter ( ( route : Route ) => ! ! route . path ) ;
129+ function computeRootMatch ( pathname : string ) : Match {
130+ return { path : '/' , url : '/' , params : { } , isExact : pathname === '/' } ;
131+ }
124132
125- let index = - 1 ;
126- for ( let x = routesWithPaths . length - 1 ; x >= 0 ; x -- ) {
127- const route = routesWithPaths [ x ] ;
128- if ( route . path && route . path . startsWith ( '/' ) ) {
129- index = x ;
130- break ;
131- }
133+ export const withSentryRouting = ( Route : React . ElementType ) => ( props : { computedMatch ?: Match } ) => {
134+ if ( activeTransaction && props && props . computedMatch && props . computedMatch . isExact ) {
135+ activeTransaction . setName ( props . computedMatch . path ) ;
132136 }
133-
134- return routesWithPaths
135- . slice ( index )
136- . filter ( ( { path } ) => ! ! path )
137- . map ( ( { path } ) => path )
138- . join ( '' ) ;
139- }
137+ return < Route { ...props } /> ;
138+ } ;
0 commit comments