1
+ import { Integrations as APMIntegrations , Span as SpanClass } from '@sentry/apm' ;
1
2
import { EventProcessor , Hub , Integration } from '@sentry/types' ;
2
3
import { basename , getGlobalObject , logger , timestampWithMs } from '@sentry/utils' ;
3
- import { Integrations as APMIntegrations , Span as SpanClass } from '@sentry/apm' ;
4
4
5
+ /** Global Vue object limited to the methods/attributes we require */
5
6
interface VueInstance {
6
7
config ?: {
7
- errorHandler ( error : Error , vm ?: ViewModel , info ?: string ) : void ;
8
+ errorHandler ? ( error : Error , vm ?: ViewModel , info ?: string ) : void ; // tslint:disable-line:completed-docs
8
9
} ;
9
- mixin ( opts : { [ key : string ] : ( ) => void } ) : void ;
10
+ mixin ( hooks : { [ key : string ] : ( ) => void } ) : void ; // tslint:disable-line:completed-docs
10
11
util : {
11
- warn ( ...input : any ) : void ;
12
+ warn ( ...input : any ) : void ; // tslint:disable-line:completed-docs
12
13
} ;
13
14
}
14
15
16
+ /** Representation of Vue component internals */
17
+ interface ViewModel {
18
+ [ key : string ] : any ;
19
+ $root : object ;
20
+ $options : {
21
+ [ key : string ] : any ;
22
+ name ?: string ;
23
+ propsData ?: { [ key : string ] : any } ;
24
+ _componentTag ?: string ;
25
+ __file ?: string ;
26
+ $_sentryPerfHook ?: boolean ;
27
+ } ;
28
+ $once ( hook : string , cb : ( ) => void ) : void ; // tslint:disable-line:completed-docs
29
+ }
30
+
31
+ // tslint:enable:completed-docs
32
+
33
+ /** Vue Integration configuration */
15
34
interface IntegrationOptions {
16
35
/** Vue instance to be used inside the integration */
17
36
Vue : VueInstance ;
@@ -41,30 +60,17 @@ interface TracingOptions {
41
60
* Can be either set to `boolean` to enable/disable tracking for all of them.
42
61
* Or to an array of specific component names (case-sensitive).
43
62
*/
44
- trackComponents : boolean | Array < string > ;
63
+ trackComponents : boolean | string [ ] ;
45
64
/** How long to wait until the tracked root activity is marked as finished and sent of to Sentry */
46
65
timeout : number ;
47
66
/**
48
67
* List of hooks to keep track of during component lifecycle.
49
68
* Available hooks: https://vuejs.org/v2/api/#Options-Lifecycle-Hooks
50
69
*/
51
- hooks : Array < Hook > ;
52
- }
53
-
54
- interface ViewModel {
55
- [ key : string ] : any ;
56
- $root : object ;
57
- $options : {
58
- [ key : string ] : any ;
59
- name ?: string ;
60
- propsData ?: { [ key : string ] : any } ;
61
- _componentTag ?: string ;
62
- __file ?: string ;
63
- $_sentryPerfHook ?: boolean ;
64
- } ;
65
- $once : ( hook : string , cb : ( ) => void ) => void ;
70
+ hooks : Hook [ ] ;
66
71
}
67
72
73
+ /** Optional metadata attached to Sentry Event */
68
74
interface Metadata {
69
75
[ key : string ] : any ;
70
76
componentName ?: string ;
@@ -74,32 +80,32 @@ interface Metadata {
74
80
75
81
// https://vuejs.org/v2/api/#Options-Lifecycle-Hooks
76
82
type Hook =
83
+ | 'activated'
77
84
| 'beforeCreate'
78
- | 'created '
85
+ | 'beforeDestroy '
79
86
| 'beforeMount'
80
- | 'mounted'
81
87
| 'beforeUpdate'
82
- | 'updated'
83
- | 'activated'
88
+ | 'created'
84
89
| 'deactivated'
85
- | 'beforeDestroy'
86
- | 'destroyed' ;
90
+ | 'destroyed'
91
+ | 'mounted'
92
+ | 'updated' ;
87
93
88
- type Operation = 'create ' | 'mount ' | 'update ' | 'activate ' | 'destroy ' ;
94
+ type Operation = 'activate ' | 'create ' | 'destroy ' | 'mount ' | 'update ' ;
89
95
90
96
// Mappings from lifecycle hook to corresponding operation,
91
97
// used to track already started measurements.
92
98
const OPERATIONS : { [ key in Hook ] : Operation } = {
99
+ activated : 'activate' ,
93
100
beforeCreate : 'create' ,
94
- created : 'create ' ,
101
+ beforeDestroy : 'destroy ' ,
95
102
beforeMount : 'mount' ,
96
- mounted : 'mount' ,
97
103
beforeUpdate : 'update' ,
98
- updated : 'update' ,
99
- activated : 'activate' ,
104
+ created : 'create' ,
100
105
deactivated : 'activate' ,
101
- beforeDestroy : 'destroy' ,
102
106
destroyed : 'destroy' ,
107
+ mounted : 'mount' ,
108
+ updated : 'update' ,
103
109
} ;
104
110
105
111
const COMPONENT_NAME_REGEXP = / (?: ^ | [ - _ / ] ) ( \w ) / g;
@@ -118,36 +124,39 @@ export class Vue implements Integration {
118
124
*/
119
125
public static id : string = 'Vue' ;
120
126
121
- private _options : IntegrationOptions ;
127
+ private readonly _options : IntegrationOptions ;
122
128
123
129
/**
124
130
* Cache holding already processed component names
125
131
*/
126
- private componentsCache = Object . create ( null ) ;
127
- private rootSpan ?: SpanClass ;
128
- private rootSpanTimer ?: ReturnType < typeof setTimeout > ;
129
- private tracingActivity ?: number ;
132
+ private readonly _componentsCache : { [ key : string ] : string } = { } ;
133
+ private _rootSpan ?: SpanClass ;
134
+ private _rootSpanTimer ?: ReturnType < typeof setTimeout > ;
135
+ private _tracingActivity ?: number ;
130
136
131
137
/**
132
138
* @inheritDoc
133
139
*/
134
140
public constructor ( options : Partial < IntegrationOptions > ) {
135
141
this . _options = {
136
- Vue : getGlobalObject < any > ( ) . Vue ,
142
+ Vue : getGlobalObject < any > ( ) . Vue , // tslint:disable-line:no-unsafe-any
137
143
attachProps : true ,
138
144
logErrors : false ,
139
145
tracing : false ,
140
146
...options ,
141
147
tracingOptions : {
142
- trackComponents : false ,
143
148
hooks : [ 'beforeMount' , 'mounted' , 'beforeUpdate' , 'updated' ] ,
144
149
timeout : 2000 ,
150
+ trackComponents : false ,
145
151
...options . tracingOptions ,
146
152
} ,
147
153
} ;
148
154
}
149
155
150
- private getComponentName ( vm : ViewModel ) : string {
156
+ /**
157
+ * Extract component name from the ViewModel
158
+ */
159
+ private _getComponentName ( vm : ViewModel ) : string {
151
160
// Such level of granularity is most likely not necessary, but better safe than sorry. — Kamil
152
161
if ( ! vm ) {
153
162
return ANONYMOUS_COMPONENT_NAME ;
@@ -174,22 +183,27 @@ export class Vue implements Integration {
174
183
const unifiedFile = vm . $options . __file . replace ( / ^ [ a - z A - Z ] : / , '' ) . replace ( / \\ / g, '/' ) ;
175
184
const filename = basename ( unifiedFile , '.vue' ) ;
176
185
return (
177
- this . componentsCache [ filename ] ||
178
- ( this . componentsCache [ filename ] = filename . replace ( COMPONENT_NAME_REGEXP , ( _ , c ) => ( c ? c . toUpperCase ( ) : '' ) ) )
186
+ this . _componentsCache [ filename ] ||
187
+ ( this . _componentsCache [ filename ] = filename . replace ( COMPONENT_NAME_REGEXP , ( _ , c : string ) =>
188
+ c ? c . toUpperCase ( ) : '' ,
189
+ ) )
179
190
) ;
180
191
}
181
192
182
193
return ANONYMOUS_COMPONENT_NAME ;
183
194
}
184
195
185
- private applyTracingHooks ( vm : ViewModel , getCurrentHub : ( ) => Hub ) : void {
196
+ /** Keep it as attribute function, to keep correct `this` binding inside the hooks callbacks */
197
+ private readonly _applyTracingHooks = ( vm : ViewModel , getCurrentHub : ( ) => Hub ) => {
186
198
// Don't attach twice, just in case
187
- if ( vm . $options . $_sentryPerfHook ) return ;
199
+ if ( vm . $options . $_sentryPerfHook ) {
200
+ return ;
201
+ }
188
202
vm . $options . $_sentryPerfHook = true ;
189
203
190
- const name = this . getComponentName ( vm ) ;
204
+ const name = this . _getComponentName ( vm ) ;
191
205
const rootMount = name === ROOT_COMPONENT_NAME ;
192
- const spans : { [ key : string ] : any } = { } ;
206
+ const spans : { [ key : string ] : SpanClass } = { } ;
193
207
194
208
// Render hook starts after once event is emitted,
195
209
// but it ends before the second event of the same type.
@@ -201,14 +215,14 @@ export class Vue implements Integration {
201
215
202
216
// On the first handler call (before), it'll be undefined, as `$once` will add it in the future.
203
217
// However, on the second call (after), it'll be already in place.
204
- if ( this . rootSpan ) {
205
- this . finishRootSpan ( now ) ;
218
+ if ( this . _rootSpan ) {
219
+ this . _finishRootSpan ( now ) ;
206
220
} else {
207
221
vm . $once ( `hook:${ hook } ` , ( ) => {
208
222
// Create an activity on the first event call. There'll be no second call, as rootSpan will be in place,
209
223
// thus new event handler won't be attached.
210
- this . tracingActivity = APMIntegrations . Tracing . pushActivity ( 'Vue Application Render' ) ;
211
- this . rootSpan = getCurrentHub ( ) . startSpan ( {
224
+ this . _tracingActivity = APMIntegrations . Tracing . pushActivity ( 'Vue Application Render' ) ;
225
+ this . _rootSpan = getCurrentHub ( ) . startSpan ( {
212
226
description : 'Application Render' ,
213
227
op : 'Vue' ,
214
228
} ) as SpanClass ;
@@ -222,7 +236,7 @@ export class Vue implements Integration {
222
236
? this . _options . tracingOptions . trackComponents . includes ( name )
223
237
: this . _options . tracingOptions . trackComponents ;
224
238
225
- if ( ! this . rootSpan || ! shouldTrack ) {
239
+ if ( ! this . _rootSpan || ! shouldTrack ) {
226
240
return ;
227
241
}
228
242
@@ -234,11 +248,11 @@ export class Vue implements Integration {
234
248
// However, on the second call (after), it'll be already in place.
235
249
if ( span ) {
236
250
span . finish ( ) ;
237
- this . finishRootSpan ( now ) ;
251
+ this . _finishRootSpan ( now ) ;
238
252
} else {
239
253
vm . $once ( `hook:${ hook } ` , ( ) => {
240
- if ( this . rootSpan ) {
241
- spans [ op ] = this . rootSpan . child ( {
254
+ if ( this . _rootSpan ) {
255
+ spans [ op ] = this . _rootSpan . child ( {
242
256
description : `Vue <${ name } >` ,
243
257
op,
244
258
} ) ;
@@ -260,28 +274,30 @@ export class Vue implements Integration {
260
274
vm . $options [ hook ] = [ handler ] ;
261
275
}
262
276
} ) ;
263
- }
277
+ } ;
264
278
265
- private finishRootSpan ( timestamp : number ) : void {
266
- if ( this . rootSpanTimer ) {
267
- clearTimeout ( this . rootSpanTimer ) ;
279
+ /** Finish top-level span and activity with a debounce configured using `timeout` option */
280
+ private _finishRootSpan ( timestamp : number ) : void {
281
+ if ( this . _rootSpanTimer ) {
282
+ clearTimeout ( this . _rootSpanTimer ) ;
268
283
}
269
284
270
- this . rootSpanTimer = setTimeout ( ( ) => {
271
- if ( this . rootSpan ) {
272
- this . rootSpan . timestamp = timestamp ;
285
+ this . _rootSpanTimer = setTimeout ( ( ) => {
286
+ if ( this . _rootSpan ) {
287
+ this . _rootSpan . timestamp = timestamp ;
273
288
}
274
- if ( this . tracingActivity ) {
275
- APMIntegrations . Tracing . popActivity ( this . tracingActivity ) ;
289
+ if ( this . _tracingActivity ) {
290
+ APMIntegrations . Tracing . popActivity ( this . _tracingActivity ) ;
276
291
}
277
292
} , this . _options . tracingOptions . timeout ) ;
278
293
}
279
294
280
- private startTracing ( getCurrentHub : ( ) => Hub ) : void {
281
- const applyTracingHooks = this . applyTracingHooks . bind ( this ) ;
295
+ /** Inject configured tracing hooks into Vue's component lifecycles */
296
+ private _startTracing ( getCurrentHub : ( ) => Hub ) : void {
297
+ const applyTracingHooks = this . _applyTracingHooks ;
282
298
283
299
this . _options . Vue . mixin ( {
284
- beforeCreate ( ) {
300
+ beforeCreate ( this : ViewModel ) : void {
285
301
// TODO: Move this check to `setupOnce` when we rework integrations initialization in v6
286
302
if ( getCurrentHub ( ) . getIntegration ( APMIntegrations . Tracing ) ) {
287
303
// `this` points to currently rendered component
@@ -293,19 +309,21 @@ export class Vue implements Integration {
293
309
} ) ;
294
310
}
295
311
296
- private attachErrorHandler ( getCurrentHub : ( ) => Hub ) : void {
312
+ /** Inject Sentry's handler into owns Vue's error handler */
313
+ private _attachErrorHandler ( getCurrentHub : ( ) => Hub ) : void {
297
314
if ( ! this . _options . Vue . config ) {
298
- return logger . error ( 'Vue instance is missing required `config` attribute' ) ;
315
+ logger . error ( 'Vue instance is missing required `config` attribute' ) ;
316
+ return ;
299
317
}
300
318
301
- const currentErrorHandler = this . _options . Vue . config . errorHandler ;
319
+ const currentErrorHandler = this . _options . Vue . config . errorHandler ; // tslint:disable-line:no-unbound-method
302
320
303
321
this . _options . Vue . config . errorHandler = ( error : Error , vm ?: ViewModel , info ?: string ) : void => {
304
322
const metadata : Metadata = { } ;
305
323
306
324
if ( vm ) {
307
325
try {
308
- metadata . componentName = this . getComponentName ( vm ) ;
326
+ metadata . componentName = this . _getComponentName ( vm ) ;
309
327
310
328
if ( this . _options . attachProps ) {
311
329
metadata . propsData = vm . $options . propsData ;
@@ -345,13 +363,14 @@ export class Vue implements Integration {
345
363
*/
346
364
public setupOnce ( _ : ( callback : EventProcessor ) => void , getCurrentHub : ( ) => Hub ) : void {
347
365
if ( ! this . _options . Vue ) {
348
- return logger . error ( 'Vue integration is missing a Vue instance' ) ;
366
+ logger . error ( 'Vue integration is missing a Vue instance' ) ;
367
+ return ;
349
368
}
350
369
351
- this . attachErrorHandler ( getCurrentHub ) ;
370
+ this . _attachErrorHandler ( getCurrentHub ) ;
352
371
353
372
if ( this . _options . tracing ) {
354
- this . startTracing ( getCurrentHub ) ;
373
+ this . _startTracing ( getCurrentHub ) ;
355
374
}
356
375
}
357
376
}
0 commit comments