Skip to content

Commit

Permalink
feat(nuxt): Configure sentry in external config (#12681)
Browse files Browse the repository at this point in the history
To be able to differentiate between a browser/client execution context,
sentry is initialized in an external config file. An import statement in
`nuxt-root.vue` is added which loads this config file.

Nuxt tracking issue:
#9095

---------

Co-authored-by: Abhijeet Prasad <[email protected]>
  • Loading branch information
s1gr1d and AbhiPrasad authored Jul 1, 2024
1 parent f4a289d commit f60aae5
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 43 deletions.
28 changes: 17 additions & 11 deletions packages/nuxt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,27 +49,33 @@ If the setup through the wizard doesn't work for you, you can also set up the SD
yarn add @sentry/nuxt
```

### 2. Client-side Setup
### 2. Nuxt Module Setup

The Sentry Nuxt SDK is based on [Nuxt Modules](https://nuxt.com/docs/api/kit/modules).

1. Add `@sentry/nuxt` to the modules section of `nuxt.config.ts`:
1. Add `@sentry/nuxt/module` to the modules section of `nuxt.config.ts`:

```javascript
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@sentry/nuxt'],
runtimeConfig: {
public: {
sentry: {
dsn: env.DSN,
// Additional config
},
},
},
modules: ['@sentry/nuxt/module'],
});
```

2. Add a `sentry.client.config.(js|ts)` file to the root of your project:

```javascript
import * as Sentry from '@sentry/nuxt';
if (!import.meta.env.SSR) {
Sentry.init({
dsn: env.DSN,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
}
```
### 3. Server-side Setup
todo: add server-side setup
Expand Down
38 changes: 27 additions & 11 deletions packages/nuxt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,28 @@
"files": [
"/build"
],
"main": "build/module.cjs",
"module": "build/module.mjs",
"types": "build/types.d.ts",
"main": "build/cjs/index.server.js",
"module": "build/esm/index.server.js",
"browser": "build/esm/index.client.js",
"types": "build/types/index.types.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./build/types.d.ts",
"import": "./build/module.mjs",
"require": "./build/module.cjs"
"types": "./build/types/index.types.d.ts",
"browser": {
"import": "./build/esm/index.client.js",
"require": "./build/cjs/index.client.js"
},
"node": {
"import": "./build/esm/index.server.js",
"require": "./build/cjs/index.server.js"
}
},
"./package.json": "./package.json"
"./module": {
"types": "./build/module/types.d.ts",
"import": "./build/module/module.mjs",
"require": "./build/module/module.cjs"
}
},
"publishConfig": {
"access": "public"
Expand All @@ -31,6 +43,7 @@
},
"dependencies": {
"@nuxt/kit": "^3.12.2",
"@sentry/browser": "8.13.0",
"@sentry/core": "8.13.0",
"@sentry/node": "8.13.0",
"@sentry/opentelemetry": "8.13.0",
Expand All @@ -44,12 +57,14 @@
"nuxt": "^3.12.2"
},
"scripts": {
"build": "run-p build:transpile",
"build": "run-p build:transpile build:types build:nuxt-module",
"build:dev": "yarn build",
"build:transpile": "nuxt-module-build build --outDir build",
"build:nuxt-module": "nuxt-module-build build --outDir build/module",
"build:transpile": "rollup -c rollup.npm.config.mjs",
"build:types": "tsc -p tsconfig.types.json",
"build:watch": "run-p build:transpile:watch build:types:watch",
"build:dev:watch": "yarn build:watch",
"build:transpile:watch": "nuxt-module-build build --outDir build --watch",
"build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch",
"build:types:watch": "tsc -p tsconfig.types.json --watch",
"build:tarball": "npm pack",
"circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts",
Expand All @@ -72,7 +87,8 @@
"^build:types"
],
"outputs": [
"{projectRoot}/build"
"{projectRoot}/build",
"{projectRoot}/build/module"
]
}
}
Expand Down
7 changes: 7 additions & 0 deletions packages/nuxt/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';

export default makeNPMConfigVariants(
makeBaseNPMConfig({
entrypoints: ['src/index.client.ts', 'src/client/index.ts'],
}),
);
8 changes: 4 additions & 4 deletions packages/nuxt/src/client/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { init as initBrowser } from '@sentry/browser';
import { applySdkMetadata } from '@sentry/core';
import type { Client } from '@sentry/types';
import { init as initVue } from '@sentry/vue';
import type { SentryVueOptions } from '../common/types';
import type { SentryNuxtOptions } from '../common/types';

/**
* Initializes the client-side of the Nuxt SDK
*
* @param options Configuration options for the SDK.
*/
export function init(options: SentryVueOptions): Client | undefined {
export function init(options: SentryNuxtOptions): Client | undefined {
const sentryOptions = {
...options,
};

applySdkMetadata(sentryOptions, 'nuxt', ['nuxt', 'vue']);

return initVue(sentryOptions);
return initBrowser(sentryOptions);
}
8 changes: 8 additions & 0 deletions packages/nuxt/src/common/debug-build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
declare const __DEBUG_BUILD__: boolean;

/**
* This serves as a build time flag that will be true by default, but false in non-debug builds or if users replace `__SENTRY_DEBUG__` in their generated code.
*
* ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.
*/
export const DEBUG_BUILD = __DEBUG_BUILD__;
47 changes: 47 additions & 0 deletions packages/nuxt/src/common/snippets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as fs from 'fs';
import * as path from 'path';

/** Returns an import snippet */
export function buildSdkInitFileImportSnippet(filePath: string): string {
const posixPath = filePath.split(path.sep).join(path.posix.sep);

// normalize to forward slashed for Windows-based systems
const normalizedPath = posixPath.replace(/\\/g, '/');

return `import '${normalizedPath}';`;
}

/**
* Script tag inside `nuxt-root.vue` (root component we get from NuxtApp)
*/
export const SCRIPT_TAG = '<script setup>';

/**
* Adds a top-level import statement right after <script setup>.
* This should happen as early as possible (e.g. in root component)
*/
export function addImportStatement(filePath: string, importStatement: string): void {
try {
const data = fs.readFileSync(filePath, 'utf8');
const scriptIndex = data.indexOf(SCRIPT_TAG);

if (scriptIndex === -1) {
// eslint-disable-next-line no-console
console.warn(`[Sentry] Sentry not initialized. Could not find ${SCRIPT_TAG} in ${filePath}`);
return;
}

// Insert the import statement after the script tag
const output = data.replace(SCRIPT_TAG, `${SCRIPT_TAG}\n${importStatement}\n`);

try {
fs.writeFileSync(filePath, output, 'utf8');
} catch (err) {
// eslint-disable-next-line no-console
console.error(`[Sentry] Error writing file to ${filePath}: ${err}`);
}
} catch (err) {
// eslint-disable-next-line no-console
console.error(`[Sentry] Error reading file at ${filePath}: ${err}`);
}
}
3 changes: 2 additions & 1 deletion packages/nuxt/src/common/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { init } from '@sentry/vue';

export type SentryVueOptions = Parameters<typeof init>[0] & object;
// Omitting 'app' as the Nuxt SDK will add the app instance in the client plugin (users do not have to provide this)
export type SentryNuxtOptions = Omit<Parameters<typeof init>[0] & object, 'app'>;
2 changes: 1 addition & 1 deletion packages/nuxt/src/index.types.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export {};
export * from './client';
38 changes: 32 additions & 6 deletions packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,49 @@
import * as fs from 'fs';
import * as path from 'path';
import { type Resolver, addPlugin, createResolver, defineNuxtModule } from '@nuxt/kit';
import type { SentryVueOptions } from './common/types';
import { addImportStatement, buildSdkInitFileImportSnippet } from './common/snippets';
import type { SentryNuxtOptions } from './common/types';

export type ModuleOptions = SentryVueOptions;
export type ModuleOptions = SentryNuxtOptions;

export default defineNuxtModule<ModuleOptions>({
meta: {
name: '@sentry/nuxt',
name: '@sentry/nuxt/module',
configKey: 'sentry',
compatibility: {
nuxt: '^3.0.0',
},
},
// Default configuration options of the Nuxt module
defaults: {},
setup(_moduleOptions, _nuxt) {
setup(_moduleOptions, nuxt) {
const resolver: Resolver = createResolver(import.meta.url);

const pathToClientInit = findDefaultSdkInitFile('client');

if (pathToClientInit) {
nuxt.hook('app:templates', nuxtApp => {
if (nuxtApp.rootComponent) {
try {
addImportStatement(nuxtApp.rootComponent, buildSdkInitFileImportSnippet(pathToClientInit));
} catch (err) {
// eslint-disable-next-line no-console
console.error(`[Sentry] Could not add import statement to root component. ${err}`);
}
}
});
}

if (resolver) {
addPlugin(resolver.resolve('runtime/plugins/sentry.client.js'));
addPlugin(resolver.resolve('./runtime/plugins/sentry.client'));
}
},
});

function findDefaultSdkInitFile(type: /* 'server' | */ 'client'): string | undefined {
const possibleFileExtensions = ['ts', 'js', 'mjs', 'cjs', 'mts', 'cts'];

const cwd = process.cwd();
return possibleFileExtensions
.map(e => path.resolve(path.join(cwd, `sentry.${type}.config.${e}`)))
.find(filename => fs.existsSync(filename));
}
15 changes: 8 additions & 7 deletions packages/nuxt/src/runtime/plugins/sentry.client.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { defineNuxtPlugin, useRuntimeConfig } from 'nuxt/app';
import { init } from '../../client';
import { getClient } from '@sentry/core';
import { vueIntegration } from '@sentry/vue';
import { defineNuxtPlugin } from 'nuxt/app';

export default defineNuxtPlugin(nuxtApp => {
const config = useRuntimeConfig();
const sentryConfig = config.public.sentry || {};
nuxtApp.hook('app:created', vueApp => {
const sentryClient = getClient();

init({
...sentryConfig,
app: nuxtApp.vueApp,
if (sentryClient) {
sentryClient.addIntegration(vueIntegration({ app: vueApp }));
}
});
});
4 changes: 2 additions & 2 deletions packages/nuxt/test/client/sdk.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as SentryBrowser from '@sentry/browser';
import { SDK_VERSION } from '@sentry/vue';
import * as SentryVue from '@sentry/vue';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { init } from '../../src/client';

const vueInit = vi.spyOn(SentryVue, 'init');
const vueInit = vi.spyOn(SentryBrowser, 'init');

describe('Nuxt Client SDK', () => {
describe('init', () => {
Expand Down
Loading

0 comments on commit f60aae5

Please sign in to comment.