22/* eslint-disable @typescript-eslint/no-unsafe-member-access */
33
44import * as recast from 'recast' ;
5- import * as path from 'path' ;
65import type { namedTypes as t } from 'ast-types' ;
6+ import type { ExpressionKind } from 'ast-types/lib/gen/kinds' ;
77
88// @ts -expect-error - clack is ESM and TS complains about that. It works though
99import clack from '@clack/prompts' ;
@@ -21,30 +21,28 @@ export async function instrumentClientEntry(
2121 enableReplay : boolean ,
2222 enableLogs : boolean ,
2323 useInstrumentationAPI = false ,
24+ useOnError = false ,
2425) : Promise < void > {
2526 const clientEntryAst = await loadFile ( clientEntryPath ) ;
2627
27- if ( hasSentryContent ( clientEntryAst . $ast as t . Program ) ) {
28- const filename = path . basename ( clientEntryPath ) ;
29- clack . log . info ( `Sentry initialization found in ${ chalk . cyan ( filename ) } ` ) ;
30- return ;
31- }
28+ const alreadyHasSentry = hasSentryContent ( clientEntryAst . $ast as t . Program ) ;
3229
33- clientEntryAst . imports . $add ( {
34- from : '@sentry/react-router' ,
35- imported : '*' ,
36- local : 'Sentry' ,
37- } ) ;
30+ if ( ! alreadyHasSentry ) {
31+ clientEntryAst . imports . $add ( {
32+ from : '@sentry/react-router' ,
33+ imported : '*' ,
34+ local : 'Sentry' ,
35+ } ) ;
3836
39- let initContent : string ;
37+ let initContent : string ;
4038
41- if ( useInstrumentationAPI && enableTracing ) {
42- const integrations = [ 'tracing' ] ;
43- if ( enableReplay ) {
44- integrations . push ( 'Sentry.replayIntegration()' ) ;
45- }
39+ if ( useInstrumentationAPI && enableTracing ) {
40+ const integrations = [ 'tracing' ] ;
41+ if ( enableReplay ) {
42+ integrations . push ( 'Sentry.replayIntegration()' ) ;
43+ }
4644
47- initContent = `
45+ initContent = `
4846const tracing = Sentry.reactRouterTracingIntegration({ useInstrumentationAPI: true });
4947
5048Sentry.init({
@@ -59,63 +57,95 @@ Sentry.init({
5957 : ''
6058 }
6159});` ;
62- } else {
63- const integrations = [ ] ;
64- if ( enableTracing ) {
65- integrations . push ( 'Sentry.reactRouterTracingIntegration()' ) ;
66- }
67- if ( enableReplay ) {
68- integrations . push ( 'Sentry.replayIntegration()' ) ;
69- }
60+ } else {
61+ const integrations = [ ] ;
62+ if ( enableTracing ) {
63+ integrations . push ( 'Sentry.reactRouterTracingIntegration()' ) ;
64+ }
65+ if ( enableReplay ) {
66+ integrations . push ( 'Sentry.replayIntegration()' ) ;
67+ }
7068
71- initContent = `
69+ initContent = `
7270Sentry.init({
7371 dsn: "${ dsn } ",
7472 sendDefaultPii: true,
7573 integrations: [${ integrations . join ( ', ' ) } ],
7674 ${ enableLogs ? 'enableLogs: true,' : '' }
7775 tracesSampleRate: ${ enableTracing ? '1.0' : '0' } ,${
78- enableTracing
79- ? '\n tracePropagationTargets: [/^\\//, /^https:\\/\\/yourserver\\.io\\/api/],'
80- : ''
81- } ${
82- enableReplay
83- ? '\n replaysSessionSampleRate: 0.1,\n replaysOnErrorSampleRate: 1.0,'
84- : ''
85- }
76+ enableTracing
77+ ? '\n tracePropagationTargets: [/^\\//, /^https:\\/\\/yourserver\\.io\\/api/],'
78+ : ''
79+ } ${
80+ enableReplay
81+ ? '\n replaysSessionSampleRate: 0.1,\n replaysOnErrorSampleRate: 1.0,'
82+ : ''
83+ }
8684});` ;
85+ }
86+
87+ ( clientEntryAst . $ast as t . Program ) . body . splice (
88+ getAfterImportsInsertionIndex ( clientEntryAst . $ast as t . Program ) ,
89+ 0 ,
90+ ...recast . parse ( initContent ) . program . body ,
91+ ) ;
8792 }
8893
89- ( clientEntryAst . $ast as t . Program ) . body . splice (
90- getAfterImportsInsertionIndex ( clientEntryAst . $ast as t . Program ) ,
91- 0 ,
92- ...recast . parse ( initContent ) . program . body ,
93- ) ;
94+ const useInstrAPI = useInstrumentationAPI && enableTracing ;
95+ const addInstrProp = useInstrAPI && ! alreadyHasSentry ;
9496
95- if ( useInstrumentationAPI && enableTracing ) {
96- const hydratedRouterFound = addInstrumentationPropsToHydratedRouter (
97- clientEntryAst . $ast as t . Program ,
98- ) ;
97+ if ( addInstrProp ) {
98+ addInstrumentationPropsToHydratedRouter ( clientEntryAst . $ast as t . Program ) ;
99+ }
100+
101+ if ( useOnError ) {
102+ addOnErrorToHydratedRouter ( clientEntryAst . $ast as t . Program ) ;
103+ }
99104
100- if ( ! hydratedRouterFound ) {
101- clack . log . warn (
102- `Could not find ${ chalk . cyan (
103- 'HydratedRouter' ,
104- ) } component in your client entry file.\n` +
105- `To use the Instrumentation API, manually add the ${ chalk . cyan (
106- 'unstable_instrumentations' ,
107- ) } prop:\n` +
108- ` ${ chalk . green (
109- '<HydratedRouter unstable_instrumentations={[tracing.clientInstrumentation]} />' ,
110- ) } `,
111- ) ;
105+ // Emit a single warning if HydratedRouter wasn't found for any prop we tried to add
106+ if (
107+ ( addInstrProp || useOnError ) &&
108+ ! hasHydratedRouter ( clientEntryAst . $ast as t . Program )
109+ ) {
110+ const props : string [ ] = [ ] ;
111+ if ( useOnError ) {
112+ props . push ( 'onError={Sentry.sentryOnError}' ) ;
112113 }
114+ if ( addInstrProp ) {
115+ props . push ( 'unstable_instrumentations={[tracing.clientInstrumentation]}' ) ;
116+ }
117+ clack . log . warn (
118+ `Could not find ${ chalk . cyan (
119+ 'HydratedRouter' ,
120+ ) } component in your client entry file.\n` +
121+ `Manually add the following props:\n` +
122+ ` ${ chalk . green ( `<HydratedRouter ${ props . join ( ' ' ) } />` ) } ` ,
123+ ) ;
113124 }
114125
115126 await writeFile ( clientEntryAst . $ast , clientEntryPath ) ;
116127}
117128
118- function addInstrumentationPropsToHydratedRouter ( ast : t . Program ) : boolean {
129+ function hasHydratedRouter ( ast : t . Program ) : boolean {
130+ let found = false ;
131+ recast . visit ( ast , {
132+ visitJSXElement ( path ) {
133+ const name = path . node . openingElement . name ;
134+ if ( name . type === 'JSXIdentifier' && name . name === 'HydratedRouter' ) {
135+ found = true ;
136+ return false ;
137+ }
138+ this . traverse ( path ) ;
139+ } ,
140+ } ) ;
141+ return found ;
142+ }
143+
144+ function addPropToHydratedRouter (
145+ ast : t . Program ,
146+ propName : string ,
147+ propValue : ExpressionKind ,
148+ ) : boolean {
119149 let found = false ;
120150
121151 recast . visit ( ast , {
@@ -128,30 +158,23 @@ function addInstrumentationPropsToHydratedRouter(ast: t.Program): boolean {
128158 ) {
129159 found = true ;
130160
131- const hasInstrumentationsProp = openingElement . attributes ?. some (
161+ const hasProp = openingElement . attributes ?. some (
132162 ( attr ) =>
133163 attr . type === 'JSXAttribute' &&
134164 attr . name . type === 'JSXIdentifier' &&
135- attr . name . name === 'unstable_instrumentations' ,
165+ attr . name . name === propName ,
136166 ) ;
137167
138- if ( ! hasInstrumentationsProp ) {
139- const instrumentationsProp = recast . types . builders . jsxAttribute (
140- recast . types . builders . jsxIdentifier ( 'unstable_instrumentations' ) ,
141- recast . types . builders . jsxExpressionContainer (
142- recast . types . builders . arrayExpression ( [
143- recast . types . builders . memberExpression (
144- recast . types . builders . identifier ( 'tracing' ) ,
145- recast . types . builders . identifier ( 'clientInstrumentation' ) ,
146- ) ,
147- ] ) ,
148- ) ,
168+ if ( ! hasProp ) {
169+ const prop = recast . types . builders . jsxAttribute (
170+ recast . types . builders . jsxIdentifier ( propName ) ,
171+ recast . types . builders . jsxExpressionContainer ( propValue ) ,
149172 ) ;
150173
151174 if ( ! openingElement . attributes ) {
152175 openingElement . attributes = [ ] ;
153176 }
154- openingElement . attributes . push ( instrumentationsProp ) ;
177+ openingElement . attributes . push ( prop ) ;
155178 }
156179
157180 return false ;
@@ -163,3 +186,27 @@ function addInstrumentationPropsToHydratedRouter(ast: t.Program): boolean {
163186
164187 return found ;
165188}
189+
190+ function addOnErrorToHydratedRouter ( ast : t . Program ) : boolean {
191+ return addPropToHydratedRouter (
192+ ast ,
193+ 'onError' ,
194+ recast . types . builders . memberExpression (
195+ recast . types . builders . identifier ( 'Sentry' ) ,
196+ recast . types . builders . identifier ( 'sentryOnError' ) ,
197+ ) ,
198+ ) ;
199+ }
200+
201+ function addInstrumentationPropsToHydratedRouter ( ast : t . Program ) : boolean {
202+ return addPropToHydratedRouter (
203+ ast ,
204+ 'unstable_instrumentations' ,
205+ recast . types . builders . arrayExpression ( [
206+ recast . types . builders . memberExpression (
207+ recast . types . builders . identifier ( 'tracing' ) ,
208+ recast . types . builders . identifier ( 'clientInstrumentation' ) ,
209+ ) ,
210+ ] ) ,
211+ ) ;
212+ }
0 commit comments