Use @sentry/vue instead of browser with vue integration
Is your feature request related to a problem? Please describe.
No problem, just small optimization
Describe the solution you'd like
Use @sentry/vue instead of @sentry/browser
Describe alternatives you've considered
na
Additional context
Seems a bit simpler solution as we wouldnt have to split browser & vue options anymore
// @see https://docs.sentry.io/platforms/javascript/guides/vue/configuration/integrations/vue-router/
Sentry.init({
Vue,
trackComponents: true,
});
Hmm, I've read that documentation and I'm confused as it doesn't seem to be saying anything to the effect of what this issue is saying.
I do agree that we should use @sentry/vue for better VueRouter integration for tracing but that doesn't seem to be in any way connected to dropping the Vue integration.
- That documentation page seems to be talking only about the tracing part and never mentions the Vue integration
- The
Vuein the configuration snippet that you've pasted refers to theVueitself and not theVueSentry integration - That snippet also seems to be missing
integrations: [new BrowserTracing()],line (which to me looks like an omission on their part)
See the package readme for more info: https://github.com/getsentry/sentry-javascript/tree/master/packages/vue
Its just a wrapper around @sentry/browser specifically for Vue.
But you are right it doesnt seem to add a lot of benefits besides unifying browser & vue specific options. And I was looking into it because of vueRouterInstrumentation tracing, trying to get that to work right now
Ah, so it provides own Sentry.init that initializes the Vue integration automatically. That wasn't clear from that documentation page.
The Vue integration options also become part of the init options then.
I don't mind changing to it. More future proof, I guess.
I can share my updated lazy template if you wish? Im not planning to update the options of the module thought as that would probably be a breaking change
I'm not sure what I would do with it if we are also not updating the settings at the same time :)
The current master version already has breaking changes that weren't released yet. For example removal of deprecated attachCommits, repo and webpackConfig options. Also refactorings that I wasn't entirely sure about so didn't commit to a release yet...
So it would be a fairly good moment to do breaking changes now and release new major version. The only thing is that Sentry seems to be planning a major release soon also so maybe we should wait for that.
Sentry 7 actually removes the Vue integration from @sentry/integrations so doing this makes even more sense. But it would make sense to do that as part of upgrading to Sentry 7 when final version is released.
Yeah, that might make sense given that they already released a beta of Sentry 7. Probably best to release a new version at the same time then.
I have added my updated template below so you get an idea of the changes needed. Can also make a pr if you wish, but overall this might require more changes then just what I did below so feel free to cherry-pick from below.
Things I changed are:
- filter out
Vuefrom allintegrationsloops - updated
serializedConfigvariable by addingintegrations.Vueandtracing.vueOptions.tracingOptions - created
serializedTracingConfigvariable to work similar toserializedConfigso the resultingsentry.client.jsfile is nicer/clearer readable (i liked your approach of not serializing the whole object but only serialize all first level values of object 💯 ) - hardcoded using
routingInstrumentationwhen tracing is enabled (this is not ideal probably)
A couple of other things that might be worthwhile to look at:
- Support using
Sentry.Integrations.Http({ tracing: true }),inplugin.server.js. Http is an Integration only available from@sentry/node. This didn't seem possible to do atm without overwriting the template as users cannot add imports to the template - Auto configure tree shaking in production builds: https://docs.sentry.io/platforms/javascript/guides/vue/configuration/tree-shaking/
- We could add an integration with
@nuxt/httpto mimic Http tracing on the client
Ping me if there is anything you would like me to help with :)
plugin.lazy.js
import Vue from 'vue'
<% if (options.lazy.injectMock) { %>
/* eslint-enable object-curly-spacing, quote-props, quotes, key-spacing, comma-spacing */
let delayedCalls = []
let SentryMock = {}
<% } %>
let sentryReadyResolve
let loadInitiated = false
let loadCompleted = false
<% if (options.lazy.injectMock) { %>
let delayedGlobalErrors = []
let delayedUnhandledRejections = []
/** @param {ErrorEvent} event */
const delayGlobalError = function (event) {
delayedGlobalErrors.push([event.message, event.filename, event.lineno, event.colno, event.error])
}
const delayUnhandledRejection = function (event) {
delayedUnhandledRejections.push('reason' in event ? event.reason : 'detail' in event && 'reason' in event.detail ? event.detail.reason : event)
}
const vueErrorHandler = Vue.config.errorHandler
Vue.config.errorHandler = (error, vm, info) => {
if (!loadCompleted) {
if (vm) {
vm.$sentry.captureException(error)
}
if (Vue.util) {
Vue.util.warn(`Error in ${info}: "${error.toString()}"`, vm)
}
console.error(error) // eslint-disable-line no-console
}
if (vueErrorHandler) {
return vueErrorHandler(error, vm, info)
}
}
<% } %>
export default function SentryPlugin (ctx, inject) {
<% if (options.lazy.injectMock) { %>
/* eslint-disable object-curly-spacing, quote-props, quotes, key-spacing, comma-spacing */
const apiMethods = <%= JSON.stringify(options.lazy.mockApiMethods)%>
apiMethods.forEach((key) => {
SentryMock[key] = (...args) => delayedCalls.push([key, args])
})
window.addEventListener('error', delayGlobalError)
window.addEventListener('unhandledrejection', delayUnhandledRejection)
inject('sentry', SentryMock)
ctx.$sentry = SentryMock
<% } %>
const loadSentryHook = () => attemptLoadSentry(ctx, inject)
<% if (options.lazy.injectLoadHook) { %>
inject('sentryLoad', loadSentryHook)
ctx.$sentryLoad = loadSentryHook
<% } else { %>
window.<%= globals.readyCallback %>(loadSentryHook)
<% } %>
const sentryReadyPromise = new Promise((resolve) => {
sentryReadyResolve = resolve
})
const sentryReady = () => sentryReadyPromise
inject('sentryReady', sentryReady)
ctx.$sentryReady = sentryReady
}
async function attemptLoadSentry (ctx, inject) {
if (loadInitiated) {
return
}
loadInitiated = true
if (!window.<%= globals.nuxt %>) {
<% if (options.dev) { %>
// eslint-disable-next-line no-console
console.warn(`$sentryLoad was called but window.<%= globals.nuxt %> is not available, delaying sentry loading until onNuxtReady callback. Do you really need to use lazy loading for Sentry?`)
<% } %>
<% if (options.lazy.injectLoadHook) { %>
window.<%= globals.readyCallback %>(() => loadSentry(ctx, inject))
<% } else { %>
// Wait for onNuxtReady hook to trigger.
<% } %>
return
}
await loadSentry(ctx, inject)
}
async function loadSentry (ctx, inject) {
if (loadCompleted) {
return
}
<%
const magicComments = [`webpackChunkName: '${options.lazy.chunkName}'`]
if (options.lazy.webpackPrefetch) {
magicComments.push('webpackPrefetch: true')
}
if (options.lazy.webpackPreload) {
magicComments.push('webpackPreload: true')
}
%>
const Sentry = await import(/* <%= magicComments.join(', ') %> */ '@sentry/vue')
<%
if (options.initialize) {
let integrations = options.SENTRY_PLUGGABLE_INTEGRATIONS.filter(key => key !== 'Vue' && key in options.integrations)
if (integrations.length) {%>const { <%= integrations.join(', ') %> } = await import(/* <%= magicComments.join(', ') %> */ '@sentry/integrations')
<% }
integrations = options.SENTRY_BROWSER_INTEGRATIONS.filter(key => key in options.integrations)
if (integrations.length) {%> const { <%= integrations.join(', ') %> } = Sentry.Integrations
<% } %>
<% if (options.tracing) { %>
const { BrowserTracing } = await import(/* <%= magicComments.join(', ') %> */ '@sentry/tracing')
<% }
const serializedConfig = Object
.entries({
...options.config,
...options.integrations.Vue,
...(options.tracing ? options.tracing.vueOptions.tracingOptions : {}),
})
.map(([key, option]) => {
const value = typeof option === 'function'
? serializeFunction(option)
: serialize(option)
return`${key}: ${value}`
})
.join(',\n ')
%>
/* eslint-disable object-curly-spacing, quote-props, quotes, key-spacing, comma-spacing */
const config = {
Vue,
<%= serializedConfig %>
}
<% if (options.tracing) {
const serializedTracingConfig = Object
.entries(options.tracing.browserOptions)
.map(([key, option]) => {
const value = typeof option === 'function'
? serializeFunction(option)
: serialize(option)
return`${key}: ${value}`
})
.join(',\n ')
%>
const tracingConfig = {
routingInstrumentation: Sentry.vueRouterInstrumentation(ctx.app.router),
<%= serializedTracingConfig %>
}
<% } %>
const runtimeConfigKey = <%= serialize(options.runtimeConfigKey) %>
if (ctx.$config && runtimeConfigKey && ctx.$config[runtimeConfigKey]) {
const { default: merge } = await import(/* <%= magicComments.join(', ') %> */ 'lodash.merge')
merge(config, ctx.$config[runtimeConfigKey].config, ctx.$config[runtimeConfigKey].clientConfig)
}
config.integrations = [
<%= Object
.entries(options.integrations)
.filter(([name]) => name !== 'Vue')
.map(([name, integration]) => {
const integrationOptions = Object
.entries(integration)
.map(([key, option]) => {
const value = typeof option === 'function'
? serializeFunction(option)
: serialize(option)
return `${key}:${value}`
})
return `new ${name}({${integrationOptions.join(',')}})`
}).join(',\n ')
%>,
<%= options.tracing ? `new BrowserTracing(tracingConfig),` : '' %>
]
/* eslint-enable object-curly-spacing, quote-props, quotes, key-spacing, comma-spacing */
Sentry.init(config)
<% } %>
loadCompleted = true
<% if (options.lazy.injectMock) { %>
window.removeEventListener('error', delayGlobalError)
window.removeEventListener('unhandledrejection', delayUnhandledRejection)
if (delayedGlobalErrors.length) {
if (window.onerror) {
console.info('Reposting global errors after Sentry has loaded') // eslint-disable-line no-console
for (const errorArgs of delayedGlobalErrors) {
window.onerror.apply(window, errorArgs)this
}
}
delayedGlobalErrors = []
}
if (delayedUnhandledRejections.length) {
if (window.onunhandledrejection) {
console.info('Reposting unhandled promise rejection errors after Sentry has loaded') // eslint-disable-line no-console
for (const reason of delayedUnhandledRejections) {
window.onunhandledrejection(reason)
}
}
delayedUnhandledRejections = []
}
delayedCalls.forEach(([methodName, args]) => Sentry[methodName].apply(Sentry, args))
<% } %>
forceInject(ctx, inject, 'sentry', Sentry)
sentryReadyResolve(Sentry)
// help gc
<% if (options.lazy.injectMock) { %>
// Dont unset delayedCalls & SentryMock during
// development, this will cause HMR issues
<% if (!options.dev) { %>
delayedCalls = undefined
SentryMock = undefined
<% } else { %>
delayedCalls = []
<% } %>
<% } %>
sentryReadyResolve = undefined
}
// Custom inject function that is able to overwrite previously injected values,
// which original inject doesn't allow to do.
// This method is adapted from the inject method in nuxt/vue-app/template/index.js
function forceInject (ctx, inject, key, value) {
inject(key, value)
const injectKey = '$' + key
ctx[injectKey] = value
window.<%= globals.nuxt %>.$options[injectKey] = value
}
For referencing changes with existing Sentry 7 upgrade PR, I'll paste the diff of your changes:
plugin.lazy.js
@@ -1,4 +1,4 @@
-import VueLib from 'vue'
+import Vue from 'vue'
<% if (options.lazy.injectMock) { %>
/* eslint-enable object-curly-spacing, quote-props, quotes, key-spacing, comma-spacing */
@@ -20,16 +20,16 @@
delayedUnhandledRejections.push('reason' in event ? event.reason : 'detail' in event && 'reason' in event.detail ? event.detail.reason : event)
}
-const vueErrorHandler = VueLib.config.errorHandler
-
-VueLib.config.errorHandler = (error, vm, info) => {
+const vueErrorHandler = Vue.config.errorHandler
+
+Vue.config.errorHandler = (error, vm, info) => {
if (!loadCompleted) {
if (vm) {
vm.$sentry.captureException(error)
}
- if (VueLib.util) {
- VueLib.util.warn(`Error in ${info}: "${error.toString()}"`, vm)
+ if (Vue.util) {
+ Vue.util.warn(`Error in ${info}: "${error.toString()}"`, vm)
}
console.error(error) // eslint-disable-line no-console
}
@@ -111,40 +111,86 @@
magicComments.push('webpackPreload: true')
}
%>
- const Sentry = await import(/* <%= magicComments.join(', ') %> */ '@sentry/browser')
+ const Sentry = await import(/* <%= magicComments.join(', ') %> */ '@sentry/vue')
<%
if (options.initialize) {
let integrations = options.PLUGGABLE_INTEGRATIONS.filter(key => key in options.integrations)
options.integrations)
if (integrations.length) {%>const { <%= integrations.join(', ') %> } = await import(/* <%= magicComments.join(', ') %> */ '@sentry/integrations')
<% }
integrations = options.BROWSER_INTEGRATIONS.filter(key => key in options.integrations)
if (integrations.length) {%> const { <%= integrations.join(', ') %> } = Sentry.Integrations
-<%}
- %>
+<% } %>
+<% if (options.tracing) { %>
+ const { BrowserTracing } = await import(/* <%= magicComments.join(', ') %> */ '@sentry/tracing')
+<% }
+
+ const serializedConfig = Object
+ .entries({
+ ...options.config,
+ ...options.integrations.Vue,
+ ...(options.tracing ? options.tracing.vueOptions.tracingOptions : {}),
+ })
+ .map(([key, option]) => {
+ const value = typeof option === 'function'
+ ? serializeFunction(option)
+ : serialize(option)
+
+ return`${key}: ${value}`
+ })
+ .join(',\n ')
+%>
+
/* eslint-disable object-curly-spacing, quote-props, quotes, key-spacing, comma-spacing */
- const config = <%= serialize(options.config) %>
+ const config = {
+ Vue,
+ <%= serializedConfig %>
+ }
+
+<% if (options.tracing) {
+ const serializedTracingConfig = Object
+ .entries(options.tracing.browserOptions)
+ .map(([key, option]) => {
+ const value = typeof option === 'function'
+ ? serializeFunction(option)
+ : serialize(option)
+
+ return`${key}: ${value}`
+ })
+ .join(',\n ')
+%>
+ const tracingConfig = {
+ routingInstrumentation: Sentry.vueRouterInstrumentation(ctx.app.router),
+ <%= serializedTracingConfig %>
+ }
+<% } %>
const runtimeConfigKey = <%= serialize(options.runtimeConfigKey) %>
if (ctx.$config && runtimeConfigKey && ctx.$config[runtimeConfigKey]) {
- const { default: merge } = await import(/* <%= magicComments.join(', ') %> */ 'lodash.mergewith')
+ const { default: merge } = await import(/* <%= magicComments.join(', ') %> */ 'lodash.merge')
merge(config, ctx.$config[runtimeConfigKey].config, ctx.$config[runtimeConfigKey].clientConfig)
}
config.integrations = [
- <%= Object.entries(options.integrations).map(([name, integration]) => {
- if (name === 'Vue') {
- return `new ${name}({ Vue: VueLib, ...${serialize(integration)}})`
- }
-
- const integrationOptions = Object.entries(integration).map(([key, option]) =>
- typeof option === 'function'
- ? `${key}:${serializeFunction(option)}`
- : `${key}:${serialize(option)}`
- )
-
- return `new ${name}(${integrationOptions.length ? '{' + integrationOptions.join(',') + '}' : ''})`
- }).join(',\n ')%>
+ <%= Object
+ .entries(options.integrations)
+ .filter(([name]) => name !== 'Vue')
+ .map(([name, integration]) => {
+ const integrationOptions = Object
+ .entries(integration)
+ .map(([key, option]) => {
+ const value = typeof option === 'function'
+ ? serializeFunction(option)
+ : serialize(option)
+
+ return `${key}:${value}`
+ })
+
+ return `new ${name}({${integrationOptions.join(',')}})`
+ }).join(',\n ')
+ %>,
+ <%= options.tracing ? `new BrowserTracing(tracingConfig),` : '' %>
]
+
/* eslint-enable object-curly-spacing, quote-props, quotes, key-spacing, comma-spacing */
Sentry.init(config)
<% } %>