sentry-module icon indicating copy to clipboard operation
sentry-module copied to clipboard

Use @sentry/vue instead of browser with vue integration

Open pimlie opened this issue 3 years ago • 7 comments

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,
});

pimlie avatar May 07 '22 14:05 pimlie

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 Vue in the configuration snippet that you've pasted refers to the Vue itself and not the Vue Sentry integration
  • That snippet also seems to be missing integrations: [new BrowserTracing()], line (which to me looks like an omission on their part)

rchl avatar May 07 '22 20:05 rchl

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

pimlie avatar May 07 '22 20:05 pimlie

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.

rchl avatar May 07 '22 20:05 rchl

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

pimlie avatar May 07 '22 21:05 pimlie

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.

rchl avatar May 07 '22 21:05 rchl

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.

rchl avatar May 07 '22 21:05 rchl

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 Vue from all integrations loops
  • updated serializedConfig variable by adding integrations.Vue and tracing.vueOptions.tracingOptions
  • created serializedTracingConfig variable to work similar to serializedConfig so the resulting sentry.client.js file 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 routingInstrumentation when 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 }), in plugin.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/http to 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
}

pimlie avatar May 08 '22 10:05 pimlie

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)
   <% } %>

rchl avatar Dec 16 '22 21:12 rchl