Skip to content

Commit 25e44bf

Browse files
committed
feat: Update Vue package
1 parent ca10c32 commit 25e44bf

File tree

4 files changed

+310
-470
lines changed

4 files changed

+310
-470
lines changed

packages/vue/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@sentry/vue",
3-
"version": "5.24.2",
3+
"version": "5.27.3",
44
"description": "Offical Sentry SDK for Vue.js",
55
"repository": "git://github.com/getsentry/sentry-javascript.git",
66
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react",
@@ -16,18 +16,18 @@
1616
"access": "public"
1717
},
1818
"dependencies": {
19-
"@sentry/browser": "5.24.2",
20-
"@sentry/core": "5.24.2",
21-
"@sentry/minimal": "5.24.2",
22-
"@sentry/types": "5.24.2",
23-
"@sentry/utils": "5.24.2",
19+
"@sentry/browser": "5.27.3",
20+
"@sentry/core": "5.27.3",
21+
"@sentry/minimal": "5.27.3",
22+
"@sentry/types": "5.27.3",
23+
"@sentry/utils": "5.27.3",
2424
"tslib": "^1.9.3"
2525
},
2626
"peerDependencies": {
2727
"vue": "2.x"
2828
},
2929
"devDependencies": {
30-
"@sentry-internal/eslint-config-sdk": "5.24.2",
30+
"@sentry-internal/eslint-config-sdk": "5.27.3",
3131
"eslint": "7.6.0",
3232
"jest": "^24.7.1",
3333
"jsdom": "^16.2.2",

packages/vue/src/sdk.ts

Lines changed: 303 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
/* eslint-disable max-lines */
22
/* eslint-disable @typescript-eslint/no-explicit-any */
3-
import { BrowserClient, BrowserOptions, defaultIntegrations } from '@sentry/browser';
3+
import { BrowserClient, BrowserOptions, defaultIntegrations, getCurrentHub } from '@sentry/browser';
44
import { initAndBind } from '@sentry/core';
5-
import { Hub, Integration, IntegrationClass, Scope, Span, Transaction } from '@sentry/types';
6-
import { getGlobalObject } from '@sentry/utils';
5+
import { Hub, Scope, Span, Transaction } from '@sentry/types';
6+
import { basename, getGlobalObject, logger, timestampWithMs } from '@sentry/utils';
77

88
export interface VueOptions extends BrowserOptions {
9+
/** Vue instance to be used inside the integration */
10+
Vue: VueInstance;
11+
912
/**
1013
* When set to `false`, Sentry will suppress reporting of all props data
1114
* from your Vue components for privacy concerns.
@@ -32,14 +35,10 @@ export interface VueOptions extends BrowserOptions {
3235
* Based on https://vuejs.org/v2/api/#Options-Lifecycle-Hooks
3336
*/
3437
hooks?: Operation[];
35-
}
3638

37-
/**
38-
* Used to extract BrowserTracing integration from @sentry/tracing
39-
*/
40-
const BROWSER_TRACING_GETTER = ({
41-
id: 'BrowserTracing',
42-
} as any) as IntegrationClass<Integration>;
39+
/** {@link TracingOptions} */
40+
tracingOptions: TracingOptions;
41+
}
4342

4443
/** Global Vue object limited to the methods/attributes we require */
4544
interface VueInstance {
@@ -104,10 +103,30 @@ const COMPONENT_NAME_REGEXP = /(?:^|[-_/])(\w)/g;
104103
const ROOT_COMPONENT_NAME = 'root';
105104
const ANONYMOUS_COMPONENT_NAME = 'anonymous component';
106105

106+
/** Vue specific configuration for Tracing Integration */
107+
interface TracingOptions {
108+
/**
109+
* Decides whether to track components by hooking into its lifecycle methods.
110+
* Can be either set to `boolean` to enable/disable tracking for all of them.
111+
* Or to an array of specific component names (case-sensitive).
112+
*/
113+
trackComponents: boolean | string[];
114+
/** How long to wait until the tracked root activity is marked as finished and sent of to Sentry */
115+
timeout: number;
116+
/**
117+
* List of hooks to keep track of during component lifecycle.
118+
* Available hooks: 'activate' | 'create' | 'destroy' | 'mount' | 'update'
119+
* Based on https://vuejs.org/v2/api/#Options-Lifecycle-Hooks
120+
*/
121+
hooks: Operation[];
122+
}
123+
107124
/**
108125
* Inits the Vue SDK
109126
*/
110-
export function init(options: VueOptions = {}): void {
127+
export function init(
128+
options: Partial<Omit<VueOptions, 'tracingOptions'> & { tracingOptions: Partial<TracingOptions> }> = {},
129+
): void {
111130
if (options.defaultIntegrations === undefined) {
112131
options.defaultIntegrations = defaultIntegrations;
113132
}
@@ -118,5 +137,277 @@ export function init(options: VueOptions = {}): void {
118137
options.release = window.SENTRY_RELEASE.id;
119138
}
120139
}
121-
initAndBind(BrowserClient, options);
140+
141+
const finalOptions = {
142+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
143+
Vue: getGlobalObject<any>().Vue as VueInstance,
144+
attachProps: true,
145+
logErrors: false,
146+
tracing: false,
147+
...options,
148+
tracingOptions: {
149+
hooks: ['mount', 'update'],
150+
timeout: 2000,
151+
trackComponents: false,
152+
...options.tracingOptions,
153+
},
154+
} as VueOptions;
155+
156+
initAndBind(BrowserClient, finalOptions);
157+
const client = getCurrentHub().getClient();
158+
if (client) {
159+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
160+
// @ts-ignore
161+
client.__vueHelper = new VueHelper(finalOptions);
162+
}
163+
}
164+
165+
/** JSDoc */
166+
export class VueHelper {
167+
/**
168+
* Cache holding already processed component names
169+
*/
170+
private readonly _componentsCache: { [key: string]: string } = {};
171+
private _rootSpan?: Span;
172+
private _rootSpanTimer?: ReturnType<typeof setTimeout>;
173+
private _options: VueOptions;
174+
175+
/**
176+
* @inheritDoc
177+
*/
178+
public constructor(options: VueOptions) {
179+
this._options = options;
180+
181+
this._attachErrorHandler();
182+
183+
// TODO: Use other check to determine if tracing is enabled
184+
if (this._options.tracesSampleRate) {
185+
this._startTracing();
186+
}
187+
}
188+
189+
/**
190+
* Extract component name from the ViewModel
191+
*/
192+
private _getComponentName(vm: ViewModel): string {
193+
// Such level of granularity is most likely not necessary, but better safe than sorry. — Kamil
194+
if (!vm) {
195+
return ANONYMOUS_COMPONENT_NAME;
196+
}
197+
198+
if (vm.$root === vm) {
199+
return ROOT_COMPONENT_NAME;
200+
}
201+
202+
if (!vm.$options) {
203+
return ANONYMOUS_COMPONENT_NAME;
204+
}
205+
206+
if (vm.$options.name) {
207+
return vm.$options.name;
208+
}
209+
210+
if (vm.$options._componentTag) {
211+
return vm.$options._componentTag;
212+
}
213+
214+
// injected by vue-loader
215+
if (vm.$options.__file) {
216+
const unifiedFile = vm.$options.__file.replace(/^[a-zA-Z]:/, '').replace(/\\/g, '/');
217+
const filename = basename(unifiedFile, '.vue');
218+
return (
219+
this._componentsCache[filename] ||
220+
(this._componentsCache[filename] = filename.replace(COMPONENT_NAME_REGEXP, (_, c: string) =>
221+
c ? c.toUpperCase() : '',
222+
))
223+
);
224+
}
225+
226+
return ANONYMOUS_COMPONENT_NAME;
227+
}
228+
229+
/** Keep it as attribute function, to keep correct `this` binding inside the hooks callbacks */
230+
// eslint-disable-next-line @typescript-eslint/typedef
231+
private readonly _applyTracingHooks = (vm: ViewModel): void => {
232+
// Don't attach twice, just in case
233+
if (vm.$options.$_sentryPerfHook) {
234+
return;
235+
}
236+
vm.$options.$_sentryPerfHook = true;
237+
238+
const name = this._getComponentName(vm);
239+
const rootMount = name === ROOT_COMPONENT_NAME;
240+
const spans: { [key: string]: Span } = {};
241+
242+
// Render hook starts after once event is emitted,
243+
// but it ends before the second event of the same type.
244+
//
245+
// Because of this, we start measuring inside the first event,
246+
// but finish it before it triggers, to skip the event emitter timing itself.
247+
const rootHandler = (hook: Hook): void => {
248+
const now = timestampWithMs();
249+
250+
// On the first handler call (before), it'll be undefined, as `$once` will add it in the future.
251+
// However, on the second call (after), it'll be already in place.
252+
if (this._rootSpan) {
253+
this._finishRootSpan(now);
254+
} else {
255+
vm.$once(`hook:${hook}`, () => {
256+
// Create an activity on the first event call. There'll be no second call, as rootSpan will be in place,
257+
// thus new event handler won't be attached.
258+
const activeTransaction = getActiveTransaction(getCurrentHub());
259+
if (activeTransaction) {
260+
this._rootSpan = activeTransaction.startChild({
261+
description: 'Application Render',
262+
op: 'Vue',
263+
});
264+
}
265+
});
266+
}
267+
};
268+
269+
const childHandler = (hook: Hook, operation: Operation): void => {
270+
// Skip components that we don't want to track to minimize the noise and give a more granular control to the user
271+
const shouldTrack = Array.isArray(this._options.tracingOptions.trackComponents)
272+
? this._options.tracingOptions.trackComponents.indexOf(name) > -1
273+
: this._options.tracingOptions.trackComponents;
274+
275+
if (!this._rootSpan || !shouldTrack) {
276+
return;
277+
}
278+
279+
const now = timestampWithMs();
280+
const span = spans[operation];
281+
282+
// On the first handler call (before), it'll be undefined, as `$once` will add it in the future.
283+
// However, on the second call (after), it'll be already in place.
284+
if (span) {
285+
span.finish();
286+
this._finishRootSpan(now);
287+
} else {
288+
vm.$once(`hook:${hook}`, () => {
289+
if (this._rootSpan) {
290+
spans[operation] = this._rootSpan.startChild({
291+
description: `Vue <${name}>`,
292+
op: operation,
293+
});
294+
}
295+
});
296+
}
297+
};
298+
299+
// Each component has it's own scope, so all activities are only related to one of them
300+
this._options.tracingOptions.hooks.forEach(operation => {
301+
// Retrieve corresponding hooks from Vue lifecycle.
302+
// eg. mount => ['beforeMount', 'mounted']
303+
const internalHooks = HOOKS[operation];
304+
305+
if (!internalHooks) {
306+
logger.warn(`Unknown hook: ${operation}`);
307+
return;
308+
}
309+
310+
internalHooks.forEach(internalHook => {
311+
const handler = rootMount
312+
? rootHandler.bind(this, internalHook)
313+
: childHandler.bind(this, internalHook, operation);
314+
const currentValue = vm.$options[internalHook];
315+
316+
if (Array.isArray(currentValue)) {
317+
vm.$options[internalHook] = [handler, ...currentValue];
318+
} else if (typeof currentValue === 'function') {
319+
vm.$options[internalHook] = [handler, currentValue];
320+
} else {
321+
vm.$options[internalHook] = [handler];
322+
}
323+
});
324+
});
325+
};
326+
327+
/** Finish top-level span and activity with a debounce configured using `timeout` option */
328+
private _finishRootSpan(timestamp: number): void {
329+
if (this._rootSpanTimer) {
330+
clearTimeout(this._rootSpanTimer);
331+
}
332+
333+
this._rootSpanTimer = setTimeout(() => {
334+
// We should always finish the span, only should pop activity if using @sentry/apm
335+
if (this._rootSpan) {
336+
this._rootSpan.finish(timestamp);
337+
}
338+
}, this._options.tracingOptions.timeout);
339+
}
340+
341+
/** Inject configured tracing hooks into Vue's component lifecycles */
342+
private _startTracing(): void {
343+
const applyTracingHooks = this._applyTracingHooks;
344+
345+
this._options.Vue.mixin({
346+
beforeCreate(this: ViewModel): void {
347+
applyTracingHooks(this);
348+
},
349+
});
350+
}
351+
352+
/** Inject Sentry's handler into owns Vue's error handler */
353+
private _attachErrorHandler(): void {
354+
// eslint-disable-next-line @typescript-eslint/unbound-method
355+
const currentErrorHandler = this._options.Vue.config.errorHandler;
356+
357+
this._options.Vue.config.errorHandler = (error: Error, vm?: ViewModel, info?: string): void => {
358+
const metadata: Metadata = {};
359+
360+
if (vm) {
361+
try {
362+
metadata.componentName = this._getComponentName(vm);
363+
364+
if (this._options.attachProps) {
365+
metadata.propsData = vm.$options.propsData;
366+
}
367+
} catch (_oO) {
368+
logger.warn('Unable to extract metadata from Vue component.');
369+
}
370+
}
371+
372+
if (info) {
373+
metadata.lifecycleHook = info;
374+
}
375+
376+
// Capture exception in the next event loop, to make sure that all breadcrumbs are recorded in time.
377+
setTimeout(() => {
378+
getCurrentHub().withScope(scope => {
379+
scope.setContext('vue', metadata);
380+
getCurrentHub().captureException(error);
381+
});
382+
});
383+
384+
if (typeof currentErrorHandler === 'function') {
385+
currentErrorHandler.call(this._options.Vue, error, vm, info);
386+
}
387+
388+
if (this._options.logErrors) {
389+
if (this._options.Vue.util) {
390+
this._options.Vue.util.warn(`Error in ${info}: "${error.toString()}"`, vm);
391+
}
392+
// eslint-disable-next-line no-console
393+
console.error(error);
394+
}
395+
};
396+
}
397+
}
398+
399+
interface HubType extends Hub {
400+
getScope?(): Scope | undefined;
401+
}
402+
403+
/** Grabs active transaction off scope */
404+
export function getActiveTransaction<T extends Transaction>(hub: HubType): T | undefined {
405+
if (hub && hub.getScope) {
406+
const scope = hub.getScope() as Scope;
407+
if (scope) {
408+
return scope.getTransaction() as T | undefined;
409+
}
410+
}
411+
412+
return undefined;
122413
}

packages/vue/src/tracing.ts

Whitespace-only changes.

0 commit comments

Comments
 (0)