Updated at September 27, 2023
β 11 min read
Lots of people ask for certain Nuxt 3 compatible modules. Besides authentication, the most popular question revolves around the Sentry integration. A few weeks ago, I announced working on a port for the Nuxt 2 module - but this might take a little. In this article, I want to explain why it takes longer than writing an average module and also want to give you a simple example recipe for your own Nuxt 3 project, so you don't have to wait for the module.
If you want to integrate a solution into one single project, no matter if it is a feature, a third-party service, or a library, it is pretty straightforward. No matter how weird the requirements are, you can almost always find a way to make it work. It only has to work for you and your team - your project only.
The tricky part starts when you want to make it work for more people, dare I say, for everyone. It starts with thinking about all the different use cases, all the different setups, all the different requirements. You have to make sure that your solution is flexible enough to be used in all those different scenarios, or at least 80 percent of them. Also, your solution should not be too opinionated, so it can be used in a wide range of projects, no matter how weird the requirements are - maybe even weirder than in the project you've built the integration initially for.
And then it comes to configuration...
I personally am a big advocate of convention over configuration, but sometimes you just can't get around it. If you want to make your solution flexible enough, you have to provide a way to configure it. The options should be straightforward and easy to use, ideally aligning with the mental model of potential users already. You want to find the sweet spot between too many and too few options.
Frankly, this is a common struggle for module authors! Fellow Nuxt contributor Julien Huang (did I hear someone saying server components?) also shares similar experiences:
While developing the Nuxt Application Insights module, I had to split what I created as my own implementation into a Nitro package (nitro-applicationinsights) first and am now working on creating a Nuxt module.
It is kinda difficult to take out what specific implementation you've made for your own project and then decide, for example how to allow build time and runtime configuration so that the module or library is generic enough.
Thankfully, we have a nice hooking system for that π
Testing will also be quite complex as I have to dive into the source code to figure out what to mock.
In the case of the Sentry module, RafaΕ, the author and maintainer of the Nuxt 2 module did a great job and laid out a solid foundation - but this also adds another part of complexity: Which features and settings should be ported to the Nuxt 3 module? Which features and settings should be dropped, and what should added? And how to adapt the existing set of features to make it work seamlessly with the Composition API or Nitro, Nuxt's server engine?
I am afraid I can't answer these questions yet - but I am working on it. While being committed to finding the best possible solution, it will take some time. But just because the module needs a bit more time to become Nuxt 3 compatible, this doesn't mean you have to wait.
And actually, it can be beneficial to own your implementation. It depends on the use case but can make sure that it fits your needs and requirements as needed and don't rely on module authors for a fix. Furthermore, you decide the scope of features and can omit unnecessary ones that might bloat your bundle, reducing complexity. In addition, you actually learn how the integration works under the hood, which means less unknown magic.
On the other hand, it might lead to increased maintenance effort as you are responsible for the implementation and have to keep it up to date. But this is a trade-off you have to make eventually, for every dependency, framework, library, or module you use.
Okay, enough of the theory and preamble. Let's get into integrating Sentry! The following approach was also implemented by me in one of my client's projects, which is Intrinsify's academic online portal.
We will take a look at both sides of your application, the server-side covered via integrating Sentry with Nitro, and the client side which will be covered through the Sentry Vue plugin.
To make things easier, I've created a tiny GitHub repository containing the code for this implementation, so you can easily move it into your own project. You can find the GitHub repository here.
Before we get into the details of each implementation, we need to define a common interface for configuration variables, which will be the runtimeConfig
of our Nuxt app!
There are lots of configuration options, but we start simple. We need Sentry's DSN to work and also provide an environment flag to later differentiate between development, staging, and production environments.
We have to put the content in the public part of the runtime config, as the Sentry Vue plugin will be used on the client side and needs access to the configuration.
By the way: If you want to learn more about how to properly set keys in your
runtimeConfig
, Friday's launch week surprise will be really helpful for you!
export default defineNuxtConfig({
runtimeConfig: {
public: {
sentry: {
dsn: '',
environment: 'development',
}
}
}
})
As mentioned before, various other configuration options can be part of your runtimeConfig
depending on your needs.
The best part is that we can now use environment variables to override the settings for different deploy:
NUXT_PUBLIC_SENTRY_DSN
to set up Sentry's DSNNUXT_PUBLIC_SENTRY_ENVIRONMENT
to set up the correct environment tagAlright, we are good to go for starting with the server-side implementation!
When I originally started implementing the Nitro part of the Sentry integration, it was... tricky, to say the least.
But since Nitro v2.6, things got way easier thanks to the new hooks! We will use three of them straight away.
Also, this guide will work for a pure Nitro server too! Just skip the Nuxt-specific parts (e.g. ignore the server/
folder prefix) and you are good to go. Because Nitro also supports the runtime config and is the server engine for Nuxt 3, it will be really easy to adapt the code.
Before we start with using Nitro though, we need to install the dependencies, namely Sentry's node package and the profiling integration (if desired). This can be done via pnpm i -D @sentry/node @sentry/profiling-node
. Feel free to switch out the package manager to whatever you use.
After we do this, we will create a new Nitro plugin. In there, we want to initialize Sentry and set up the profiling integration. We will also retrieve our variables from the runtime config and set up the nitro hook:
import * as Sentry from '@sentry/node'
import { nodeProfilingIntegration } from '@sentry/profiling-node'
export default defineNitroPlugin((nitroApp) => {
const { public: { sentry } } = useRuntimeConfig()
// If no sentry DSN set, ignore and warn in the console
if (!sentry.dsn) {
console.warn('Sentry DSN not set, skipping Sentry initialization')
return
}
// Initialize Sentry
Sentry.init({
dsn: sentry.dsn,
environment: sentry.environment,
integrations: [nodeProfilingIntegration()],
// Performance Monitoring
tracesSampleRate: 1.0, // Change in production!
// Set sampling rate for profiling - this is relative to tracesSampleRate
profilesSampleRate: 1.0 // Change in production!
})
// Here comes the hooks
})
So far so good, we set up the configuration. As mentioned before, lots of things can be added to the runtimeConfig
, such as the sample rates. We could also switch them based on sentry.environment
though. And now let's jump into how to use the hooks.
First, we want to ensure that Sentry will be capturing all kinds of errors Nitro is throwing at us the user. For this, we can use the nitroApp
param from the plugin function, together with the error
hook which will be called when an unhandled error is thrown:
// Inside the plugin, after initializing sentry
nitroApp.hooks.hook('error', (error) => {
Sentry.captureException(error)
})
Now, all errors will be captured. You might not want to capture some of these, e.g. 404s or 422s, as they are usually not relevant for error tracking. This can be done with a bit of custom logic - by checking if the error is an H3Error, and if so, if the status code is one of the exceptions we don't want to track:
// On the top of the file, import H3Error!
import { H3Error } from 'h3'
// Inside the plugin, after initializing sentry
nitroApp.hooks.hook('error', (error) => {
// Do not handle 404s and 422s
if (error instanceof H3Error) {
if (error.statusCode === 404 || error.statusCode === 422) {
return
}
}
Sentry.captureException(error)
})
After the initial setup is done, we also want to ensure that we can use Sentry in our API routes, e.g. to attach a user, send messages, and whatever your use case is. The best way is attaching Sentry to the event context, so we can access it from any event handler. We can do this for every request by using the request
hook! This hook will be called for every request, so we can attach Sentry to the event context in just four (soon three) lines.
There is a type issue in Nitro at the time of writing, but I'm confident it won't stay for long. Until then, we need to use a @ts-ignore
, or better @ts-expect-error
with a comment!
nitroApp.hooks.hook('request', (event) => {
event.context.$sentry = Sentry
})
Okay, we are not fully done yet if we use TypeScript because the event context doesn't know about the $sentry
property yet. We can fix this by augmenting the EventContext type in a .d.ts
file in your project root:
import type { Sentry } from '@sentry/node'
declare module 'h3' {
interface H3EventContext {
$sentry?: Sentry
}
}
It might be a bit confusing that you have to augment the H3EventContext
but it makes total sense: Nitro is using H3 under the hood, using its event structure and context. So we have to augment the H3 event context, which will then be used by Nitro and our event handlers.
Now we can do something like this in any event handler:
export default defineApiHandler(async (event) => {
const sentry = event.context.$sentry
if(sentry) {
// Do something with Sentry if exists
// e.g.
sentry.setUser({/*...*/})
}
})
An often forgotten task is cleaning up! We want to ensure that Sentry is gracefully shut down when the Nitro server stops.
We can achieve this by using the close
hook:
nitroApp.hooks.hookOnce('close', async () => {
await Sentry.close(2000)
})
And that's it! Our nitro integration is ready. Now, up to the client side.
The client side is also not that complicated. Technically, we have to do what we did in the Nitro plugin, but on the client side. We will use a client-only Nuxt plugin for this. In there, we also initialize Sentry based on our runtimeConfig
values and inject it into the app.
Let's not forget to add the Sentry vue plugin to our dependencies before via pnpm i -D @sentry/vue
.
Then, we provide the vue app via nuxtApp.vueApp
and the router via the useRouter()
composable. Also, we use the dsn
and environment
keys through the useRuntimeConfig
composable.
import * as Sentry from '@sentry/vue'
export default defineNuxtPlugin((nuxtApp) => {
const router = useRouter()
const { public: { sentry } } = useRuntimeConfig()
if (!sentry.dsn) {
return
}
Sentry.init({
app: nuxtApp.vueApp,
dsn: sentry.dsn,
environment: sentry.environment,
integrations: [
Sentry.browserTracingIntegration({ router }),
Sentry.replayIntegration({
maskAllText: false,
blockAllMedia: false,
}),
],
// Configure this whole part as you need it!
tracesSampleRate: 0.2, // Change in prod
// Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled
tracePropagationTargets: ['localhost', 'https://your-server.com'],
replaysSessionSampleRate: 1.0, // Change in prod
replaysOnErrorSampleRate: 1.0, // Change in prod if necessary
})
})
From here, error tracking works out of the box. Of course, you can build your own composable exposing Sentry
helpers or import it in the components as needed - but that's up to you now as you own the implementation!
We did it! Together, we set up a very simple Sentry integration for our Nuxt 3 project, only by relying on Nuxt, Nitro, and Sentry. Don't forget to check out the full result on GitHub.
I hope this article helped you understand why module development can be tricky, might take a little longer than just adding your own implementation, and what pros and cons owning an implementation brings with it. Also, I hope now your Sentry setup is up and running.
If you have any questions, feel free to reach out to me as usual! And please don't forget to share this article with your friends and colleagues if you found it helpful.
Happy hacking!
I'm Alex, a German web engineering consultant and content creator. Helping companies with my experience in TypeScript, Vue.js, and Nuxt.js is my daily business.
More about meServer Side Rendering does have some limitations, such as the inability to access platform-based APIs like local storage on the server side. However, what if you could activate SSR exclusively for pages that need SEO and employ the classic single-page application (SPA) mode for other pages? Discover how to achieve this in the following article.
With the release of Vue 3, a lot of things became easier. Unfortunately, loading dynamic assets such as images is not one of them. In the following article, I want to demystify the process of dynamic asset loading in Vue 3 or Nuxt 3 and explain why static assets can be loaded easily...