Skip to content

Commit 81b4b5e

Browse files
authored
feat(vue): Expose VueIntegration to initialize vue app later (#9180)
This exposes a new `VueIntegration` from `@sentry/vue` which can be used to initialize vue error tracking for a late-defined vue app.
1 parent 4236181 commit 81b4b5e

File tree

7 files changed

+240
-99
lines changed

7 files changed

+240
-99
lines changed

packages/vue/src/errorhandler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { getCurrentHub } from '@sentry/browser';
22
import { addExceptionMechanism } from '@sentry/utils';
33

4-
import type { Options, ViewModel, Vue } from './types';
4+
import type { ViewModel, Vue, VueOptions } from './types';
55
import { formatComponentName, generateComponentTrace } from './vendor/components';
66

77
type UnknownFunc = (...args: unknown[]) => void;
88

9-
export const attachErrorHandler = (app: Vue, options: Options): void => {
9+
export const attachErrorHandler = (app: Vue, options: VueOptions): void => {
1010
const { errorHandler, warnHandler, silent } = app.config;
1111

1212
app.config.errorHandler = (error: Error, vm: ViewModel, lifecycleHook: string): void => {

packages/vue/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export { init } from './sdk';
44
export { vueRouterInstrumentation } from './router';
55
export { attachErrorHandler } from './errorhandler';
66
export { createTracingMixins } from './tracing';
7+
export { VueIntegration } from './integration';

packages/vue/src/integration.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { hasTracingEnabled } from '@sentry/core';
2+
import type { Hub, Integration } from '@sentry/types';
3+
import { arrayify, GLOBAL_OBJ } from '@sentry/utils';
4+
5+
import { DEFAULT_HOOKS } from './constants';
6+
import { attachErrorHandler } from './errorhandler';
7+
import { createTracingMixins } from './tracing';
8+
import type { Options, Vue, VueOptions } from './types';
9+
10+
const globalWithVue = GLOBAL_OBJ as typeof GLOBAL_OBJ & { Vue: Vue };
11+
12+
const DEFAULT_CONFIG: VueOptions = {
13+
Vue: globalWithVue.Vue,
14+
attachProps: true,
15+
logErrors: true,
16+
hooks: DEFAULT_HOOKS,
17+
timeout: 2000,
18+
trackComponents: false,
19+
};
20+
21+
/**
22+
* Initialize Vue error & performance tracking.
23+
*/
24+
export class VueIntegration implements Integration {
25+
/**
26+
* @inheritDoc
27+
*/
28+
public static id: string = 'Vue';
29+
30+
/**
31+
* @inheritDoc
32+
*/
33+
public name: string;
34+
35+
private readonly _options: Partial<VueOptions>;
36+
37+
public constructor(options: Partial<VueOptions> = {}) {
38+
this.name = VueIntegration.id;
39+
this._options = options;
40+
}
41+
42+
/** @inheritDoc */
43+
public setupOnce(_addGlobaleventProcessor: unknown, getCurrentHub: () => Hub): void {
44+
this._setupIntegration(getCurrentHub());
45+
}
46+
47+
/** Just here for easier testing */
48+
protected _setupIntegration(hub: Hub): void {
49+
const client = hub.getClient();
50+
const options: Options = { ...DEFAULT_CONFIG, ...(client && client.getOptions()), ...this._options };
51+
52+
if (!options.Vue && !options.app) {
53+
// eslint-disable-next-line no-console
54+
console.warn(
55+
`[@sentry/vue]: Misconfigured SDK. Vue specific errors will not be captured.
56+
Update your \`Sentry.init\` call with an appropriate config option:
57+
\`app\` (Application Instance - Vue 3) or \`Vue\` (Vue Constructor - Vue 2).`,
58+
);
59+
return;
60+
}
61+
62+
if (options.app) {
63+
const apps = arrayify(options.app);
64+
apps.forEach(app => vueInit(app, options));
65+
} else if (options.Vue) {
66+
vueInit(options.Vue, options);
67+
}
68+
}
69+
}
70+
71+
const vueInit = (app: Vue, options: Options): void => {
72+
// Check app is not mounted yet - should be mounted _after_ init()!
73+
// This is _somewhat_ private, but in the case that this doesn't exist we simply ignore it
74+
// See: https://github.com/vuejs/core/blob/eb2a83283caa9de0a45881d860a3cbd9d0bdd279/packages/runtime-core/src/component.ts#L394
75+
const appWithInstance = app as Vue & {
76+
_instance?: {
77+
isMounted?: boolean;
78+
};
79+
};
80+
81+
const isMounted = appWithInstance._instance && appWithInstance._instance.isMounted;
82+
if (isMounted === true) {
83+
// eslint-disable-next-line no-console
84+
console.warn(
85+
'[@sentry/vue]: Misconfigured SDK. Vue app is already mounted. Make sure to call `app.mount()` after `Sentry.init()`.',
86+
);
87+
}
88+
89+
attachErrorHandler(app, options);
90+
91+
if (hasTracingEnabled(options)) {
92+
app.mixin(
93+
createTracingMixins({
94+
...options,
95+
...options.tracingOptions,
96+
}),
97+
);
98+
}
99+
};

packages/vue/src/sdk.ts

Lines changed: 16 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,7 @@
1-
import { init as browserInit, SDK_VERSION } from '@sentry/browser';
2-
import { hasTracingEnabled } from '@sentry/core';
3-
import { arrayify, GLOBAL_OBJ } from '@sentry/utils';
1+
import { defaultIntegrations, init as browserInit, SDK_VERSION } from '@sentry/browser';
42

5-
import { DEFAULT_HOOKS } from './constants';
6-
import { attachErrorHandler } from './errorhandler';
7-
import { createTracingMixins } from './tracing';
8-
import type { Options, TracingOptions, Vue } from './types';
9-
10-
const globalWithVue = GLOBAL_OBJ as typeof GLOBAL_OBJ & { Vue: Vue };
11-
12-
const DEFAULT_CONFIG: Options = {
13-
Vue: globalWithVue.Vue,
14-
attachProps: true,
15-
logErrors: true,
16-
hooks: DEFAULT_HOOKS,
17-
timeout: 2000,
18-
trackComponents: false,
19-
_metadata: {
20-
sdk: {
21-
name: 'sentry.javascript.vue',
22-
packages: [
23-
{
24-
name: 'npm:@sentry/vue',
25-
version: SDK_VERSION,
26-
},
27-
],
28-
version: SDK_VERSION,
29-
},
30-
},
31-
};
3+
import { VueIntegration } from './integration';
4+
import type { Options, TracingOptions } from './types';
325

336
/**
347
* Inits the Vue SDK
@@ -37,56 +10,21 @@ export function init(
3710
config: Partial<Omit<Options, 'tracingOptions'> & { tracingOptions: Partial<TracingOptions> }> = {},
3811
): void {
3912
const options = {
40-
...DEFAULT_CONFIG,
13+
_metadata: {
14+
sdk: {
15+
name: 'sentry.javascript.vue',
16+
packages: [
17+
{
18+
name: 'npm:@sentry/vue',
19+
version: SDK_VERSION,
20+
},
21+
],
22+
version: SDK_VERSION,
23+
},
24+
},
25+
defaultIntegrations: [...defaultIntegrations, new VueIntegration()],
4126
...config,
4227
};
4328

4429
browserInit(options);
45-
46-
if (!options.Vue && !options.app) {
47-
// eslint-disable-next-line no-console
48-
console.warn(
49-
`[@sentry/vue]: Misconfigured SDK. Vue specific errors will not be captured.
50-
Update your \`Sentry.init\` call with an appropriate config option:
51-
\`app\` (Application Instance - Vue 3) or \`Vue\` (Vue Constructor - Vue 2).`,
52-
);
53-
return;
54-
}
55-
56-
if (options.app) {
57-
const apps = arrayify(options.app);
58-
apps.forEach(app => vueInit(app, options));
59-
} else if (options.Vue) {
60-
vueInit(options.Vue, options);
61-
}
6230
}
63-
64-
const vueInit = (app: Vue, options: Options): void => {
65-
// Check app is not mounted yet - should be mounted _after_ init()!
66-
// This is _somewhat_ private, but in the case that this doesn't exist we simply ignore it
67-
// See: https://github.com/vuejs/core/blob/eb2a83283caa9de0a45881d860a3cbd9d0bdd279/packages/runtime-core/src/component.ts#L394
68-
const appWithInstance = app as Vue & {
69-
_instance?: {
70-
isMounted?: boolean;
71-
};
72-
};
73-
74-
const isMounted = appWithInstance._instance && appWithInstance._instance.isMounted;
75-
if (isMounted === true) {
76-
// eslint-disable-next-line no-console
77-
console.warn(
78-
'[@sentry/vue]: Misconfigured SDK. Vue app is already mounted. Make sure to call `app.mount()` after `Sentry.init()`.',
79-
);
80-
}
81-
82-
attachErrorHandler(app, options);
83-
84-
if (hasTracingEnabled(options)) {
85-
app.mixin(
86-
createTracingMixins({
87-
...options,
88-
...options.tracingOptions,
89-
}),
90-
);
91-
}
92-
};

packages/vue/src/types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ export type ViewModel = {
2525
};
2626
};
2727

28-
export interface Options extends TracingOptions, BrowserOptions {
28+
export interface VueOptions extends TracingOptions {
2929
/** Vue constructor to be used inside the integration (as imported by `import Vue from 'vue'` in Vue2) */
3030
Vue?: Vue;
3131

32-
/** Vue app instance(s) to be used inside the integration (as generated by `createApp` in Vue3 ) */
32+
/**
33+
* Vue app instance(s) to be used inside the integration (as generated by `createApp` in Vue3).
34+
*/
3335
app?: Vue | Vue[];
3436

3537
/**
@@ -48,6 +50,8 @@ export interface Options extends TracingOptions, BrowserOptions {
4850
tracingOptions?: Partial<TracingOptions>;
4951
}
5052

53+
export interface Options extends BrowserOptions, VueOptions {}
54+
5155
/** Vue specific configuration for Tracing Integration */
5256
export interface TracingOptions {
5357
/**
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { logger } from '@sentry/utils';
2+
import { createApp } from 'vue';
3+
4+
import * as Sentry from '../../src';
5+
6+
const PUBLIC_DSN = 'https://username@domain/123';
7+
8+
describe('Sentry.VueIntegration', () => {
9+
let loggerWarnings: unknown[] = [];
10+
let warnings: unknown[] = [];
11+
12+
beforeEach(() => {
13+
warnings = [];
14+
loggerWarnings = [];
15+
16+
jest.spyOn(logger, 'warn').mockImplementation((message: unknown) => {
17+
loggerWarnings.push(message);
18+
});
19+
20+
jest.spyOn(console, 'warn').mockImplementation((message: unknown) => {
21+
warnings.push(message);
22+
});
23+
});
24+
25+
afterEach(() => {
26+
jest.resetAllMocks();
27+
});
28+
29+
it('allows to initialize integration later', () => {
30+
Sentry.init({ dsn: PUBLIC_DSN, defaultIntegrations: false, autoSessionTracking: false });
31+
32+
const el = document.createElement('div');
33+
const app = createApp({
34+
template: '<div>hello</div>',
35+
});
36+
37+
// This would normally happen through client.addIntegration()
38+
const integration = new Sentry.VueIntegration({ app });
39+
integration['_setupIntegration'](Sentry.getCurrentHub());
40+
41+
app.mount(el);
42+
43+
expect(warnings).toEqual([]);
44+
expect(loggerWarnings).toEqual([]);
45+
46+
expect(app.config.errorHandler).toBeDefined();
47+
});
48+
49+
it('warns when mounting before SDK.VueIntegration', () => {
50+
Sentry.init({ dsn: PUBLIC_DSN, defaultIntegrations: false, autoSessionTracking: false });
51+
52+
const el = document.createElement('div');
53+
const app = createApp({
54+
template: '<div>hello</div>',
55+
});
56+
57+
app.mount(el);
58+
59+
// This would normally happen through client.addIntegration()
60+
const integration = new Sentry.VueIntegration({ app });
61+
integration['_setupIntegration'](Sentry.getCurrentHub());
62+
63+
expect(warnings).toEqual([
64+
'[@sentry/vue]: Misconfigured SDK. Vue app is already mounted. Make sure to call `app.mount()` after `Sentry.init()`.',
65+
]);
66+
expect(loggerWarnings).toEqual([]);
67+
});
68+
});

0 commit comments

Comments
 (0)