Skip to content

Commit 2cf56ef

Browse files
committed
feat: support for registering external integrations (#276)
1 parent 0f8242d commit 2cf56ef

File tree

21 files changed

+2999
-1203
lines changed

21 files changed

+2999
-1203
lines changed

.eslintrc.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@ module.exports = {
66
rules: {
77
'import/named': 'off',
88
'import/namespace': 'off',
9+
'import/no-absolute-path': 'off',
10+
'no-console': [
11+
'error', {
12+
allow: ['assert', 'warn', 'error', 'info']
13+
}
14+
],
915
'vue/multi-word-component-names': 'off'
1016
},
1117
overrides: [
1218
{
1319
files: ['test/**'],
1420
plugins: ['jest'],
15-
extends: ['plugin:jest/recommended'],
21+
extends: ['plugin:jest/recommended']
1622
},
1723
{
1824
files: ['*.ts', '*.tsx'],

docs/content/en/sentry/options.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,38 @@ Note that the module sets the following defaults when publishing is enabled:
268268
```
269269
- See https://docs.sentry.io/platforms/node/pluggable-integrations/ for more information on the integrations and their configuration
270270
271+
### customClientIntegrations
272+
273+
- Type: `String`
274+
- Default: `undefined`
275+
- This option gives the flexibility to register any custom integration that is not handled internally by the `clientIntegrations` option.
276+
- The value needs to be a file path (can include [webpack aliases](https://nuxtjs.org/docs/2.x/directory-structure/assets#aliases)) pointing to a javascript file that exports a function returning an array of initialized integrations. The function will be passed a `context` argument which is the Nuxt Context.
277+
278+
For example:
279+
```js
280+
import SentryRRWeb from '@sentry/rrweb'
281+
282+
export default function (context) {
283+
return [new SentryRRWeb()]
284+
}
285+
```
286+
287+
### customServerIntegrations
288+
289+
- Type: `String`
290+
- Default: `undefined`
291+
- This option gives the flexibility to register any custom integration that is not handled internally by the `serverIntegrations` option.
292+
- The value needs to be a file path (can include [webpack aliases](https://nuxtjs.org/docs/2.x/directory-structure/assets#aliases)) pointing to a javascript file that exports a function returning an array of initialized integrations.
293+
294+
For example:
295+
```js
296+
import MyAwesomeIntegration from 'my-awesome-integration'
297+
298+
export default function () {
299+
return [new MyAwesomeIntegration()]
300+
}
301+
```
302+
271303
### tracing
272304
273305
- Type: `Boolean` or `Object`

lib/core/options.js

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const BROWSER_INTEGRATIONS = ['InboundFilters', 'FunctionToString', 'TryC
77
const SERVER_INTEGRATIONS = ['CaptureConsole', 'Debug', 'Dedupe', 'ExtraErrorData', 'RewriteFrames', 'Modules', 'Transaction']
88

99
/** @param {import('../../types/sentry').IntegrationsConfiguration} integrations */
10-
const filterDisabledIntegration = integrations => Object.keys(integrations).filter(key => integrations[key])
10+
const filterDisabledIntegrations = integrations => Object.keys(integrations).filter(key => integrations[key])
1111

1212
/**
1313
* @param {string} packageName
@@ -131,6 +131,15 @@ export async function resolveClientOptions (moduleContainer, moduleOptions, logg
131131
}
132132
}
133133

134+
let customClientIntegrations
135+
if (options.customClientIntegrations) {
136+
if (typeof (options.customClientIntegrations) === 'string') {
137+
customClientIntegrations = moduleContainer.nuxt.resolver.resolveAlias(options.customClientIntegrations)
138+
} else {
139+
logger.warn(`Invalid customServerIntegrations option. Expected a file path, got "${typeof (options.customClientIntegrations)}".`)
140+
}
141+
}
142+
134143
return {
135144
PLUGGABLE_INTEGRATIONS,
136145
BROWSER_INTEGRATIONS,
@@ -142,10 +151,11 @@ export async function resolveClientOptions (moduleContainer, moduleOptions, logg
142151
},
143152
lazy: options.lazy,
144153
apiMethods,
154+
customClientIntegrations,
145155
logMockCalls: options.logMockCalls, // for mocked only
146156
tracing: options.tracing,
147157
initialize: canInitialize(options),
148-
integrations: filterDisabledIntegration(options.clientIntegrations)
158+
integrations: filterDisabledIntegrations(options.clientIntegrations)
149159
.reduce((res, key) => {
150160
// @ts-ignore
151161
res[key] = options.clientIntegrations[key]
@@ -171,14 +181,26 @@ export async function resolveServerOptions (moduleContainer, moduleOptions, logg
171181
}
172182
}
173183

184+
let customIntegrations = []
185+
if (options.customServerIntegrations) {
186+
const resolvedPath = moduleContainer.nuxt.resolver.resolveAlias(options.customServerIntegrations)
187+
customIntegrations = (await import(resolvedPath).then(m => m.default || m))()
188+
if (!Array.isArray(customIntegrations)) {
189+
logger.error(`Invalid value returned from customServerIntegrations plugin. Expected an array, got "${typeof (customIntegrations)}".`)
190+
}
191+
}
192+
174193
const defaultConfig = {
175194
dsn: options.dsn,
176-
intergrations: filterDisabledIntegration(options.serverIntegrations)
177-
.map((name) => {
178-
const opt = options.serverIntegrations[name]
179-
// @ts-ignore
180-
return Object.keys(opt).length ? new Integrations[name](opt) : new Integrations[name]()
181-
})
195+
intergrations: [
196+
...filterDisabledIntegrations(options.serverIntegrations)
197+
.map((name) => {
198+
const opt = options.serverIntegrations[name]
199+
// @ts-ignore
200+
return Object.keys(opt).length ? new Integrations[name](opt) : new Integrations[name]()
201+
}),
202+
...customIntegrations
203+
]
182204
}
183205
options.config = merge(defaultConfig, options.config, options.serverConfig)
184206

lib/module.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export default function SentryModule (moduleOptions) {
4343
RewriteFrames: {},
4444
Transaction: {}
4545
},
46+
customClientIntegrations: '',
47+
customServerIntegrations: '',
4648
config: {
4749
environment: this.options.dev ? 'development' : 'production'
4850
},

lib/plugin.client.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,21 @@ import * as Sentry from '@sentry/browser'
55
if (options.initialize) {
66
let integrations = options.PLUGGABLE_INTEGRATIONS.filter(key => key in options.integrations)
77
if (integrations.length) {%>import { <%= integrations.join(', ') %> } from '@sentry/integrations'
8+
<%}
9+
if (options.customClientIntegrations) {%>import getCustomIntegrations from '<%= options.customClientIntegrations %>'
810
<%}
911
integrations = options.BROWSER_INTEGRATIONS.filter(key => key in options.integrations)
10-
if (integrations.length) {%>const { <%= integrations.join(', ') %> } = Sentry.Integrations
12+
if (integrations.length) {%>
13+
const { <%= integrations.join(', ') %> } = Sentry.Integrations
1114
<%}
1215
}
1316
%>
1417
<% if (options.tracing) { %>
1518
import { Integrations as TracingIntegrations } from '@sentry/tracing'
1619
<% } %>
1720

18-
export default function (ctx, inject) {
21+
// eslint-disable-next-line require-await
22+
export default async function (ctx, inject) {
1923
<% if (options.initialize) { %>
2024
/* eslint-disable object-curly-spacing, quote-props, quotes, key-spacing, comma-spacing */
2125
const config = {
@@ -52,6 +56,14 @@ export default function (ctx, inject) {
5256
<% if (options.tracing) { %>
5357
config.integrations.push(<%= `new TracingIntegrations.BrowserTracing(${serialize(options.tracing.browserOptions)})` %>)
5458
<% } %>
59+
<% if (options.customClientIntegrations) { %>
60+
const customIntegrations = await getCustomIntegrations(ctx)
61+
if (Array.isArray(customIntegrations)) {
62+
config.integrations.push(...customIntegrations)
63+
} else {
64+
console.error(`[@nuxtjs/sentry] Invalid value returned from customClientIntegrations plugin. Expected an array, got "${typeof customIntegrations}".`)
65+
}
66+
<% } %>
5567
/* eslint-enable object-curly-spacing, quote-props, quotes, key-spacing, comma-spacing */
5668
Sentry.init(config)
5769
<% } %>

lib/plugin.lazy.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ VueLib.config.errorHandler = (error, vm, info) => {
3131
if (VueLib.util) {
3232
VueLib.util.warn(`Error in ${info}: "${error.toString()}"`, vm)
3333
}
34-
console.error(error) // eslint-disable-line no-console
34+
console.error(error)
3535
}
3636

3737
if (vueErrorHandler) {
@@ -83,7 +83,6 @@ async function attemptLoadSentry (ctx, inject) {
8383

8484
if (!window.<%= globals.nuxt %>) {
8585
<% if (options.dev) { %>
86-
// eslint-disable-next-line no-console
8786
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?`)
8887
<% } %>
8988
<% if (options.lazy.injectLoadHook) { %>
@@ -145,6 +144,14 @@ async function loadSentry (ctx, inject) {
145144
return `new ${name}(${integrationOptions.length ? '{' + integrationOptions.join(',') + '}' : ''})`
146145
}).join(',\n ')%>
147146
]
147+
<%if (options.customClientIntegrations) {%>
148+
const customIntegrations = (await import(/* <%= magicComments.join(', ') %> */ '<%= options.customClientIntegrations %>').then(m => m.default || m))(ctx)
149+
if (Array.isArray(customIntegrations)) {
150+
config.integrations.push(...customIntegrations)
151+
} else {
152+
console.error(`[@nuxtjs/sentry] Invalid value returned from customClientIntegrations plugin. Expected an array, got "${typeof customIntegrations}".`)
153+
}
154+
<% } %>
148155
/* eslint-enable object-curly-spacing, quote-props, quotes, key-spacing, comma-spacing */
149156
Sentry.init(config)
150157
<% } %>
@@ -155,7 +162,7 @@ async function loadSentry (ctx, inject) {
155162
window.removeEventListener('unhandledrejection', delayUnhandledRejection)
156163
if (delayedGlobalErrors.length) {
157164
if (window.onerror) {
158-
console.info('Reposting global errors after Sentry has loaded') // eslint-disable-line no-console
165+
console.info('Reposting global errors after Sentry has loaded')
159166
for (const errorArgs of delayedGlobalErrors) {
160167
window.onerror.apply(window, errorArgs)
161168
}
@@ -164,7 +171,7 @@ async function loadSentry (ctx, inject) {
164171
}
165172
if (delayedUnhandledRejections.length) {
166173
if (window.onunhandledrejection) {
167-
console.info('Reposting unhandled promise rejection errors after Sentry has loaded') // eslint-disable-line no-console
174+
console.info('Reposting unhandled promise rejection errors after Sentry has loaded')
168175
for (const reason of delayedUnhandledRejections) {
169176
window.onunhandledrejection(reason)
170177
}

lib/plugin.mocked.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const apiMethods = <%= JSON.stringify(options.apiMethods)%>
44
export default function (ctx, inject) {
55
const SentryMock = {}
66
apiMethods.forEach(key => {
7-
// eslint-disable-next-line no-console
87
SentryMock[key] = <%= options.logMockCalls
98
? '(...args) => console.warn(`$sentry.${key}() called, but Sentry plugin is disabled. Arguments:`, args)'
109
: '_ => _'%>

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"main": "lib/module.js",
2323
"types": "types/index.d.ts",
2424
"scripts": {
25-
"dev:fixture": "nuxt -c ./test/fixture/default/nuxt.config.js",
25+
"dev:fixture": "node ./node_modules/nuxt/bin/nuxt.js -c ./test/fixture/lazy/nuxt.config.js",
2626
"dev:fixture:build": "node ./node_modules/nuxt/bin/nuxt.js build -c ./test/fixture/default/nuxt.config.js",
2727
"dev:fixture:start": "node ./node_modules/nuxt/bin/nuxt.js start -c ./test/fixture/default/nuxt.config.js",
2828
"dev:generate": "nuxt generate -c ./test/fixture/default/nuxt.config.js --force-build",
@@ -78,6 +78,7 @@
7878
"nuxt": "^2.15.8",
7979
"playwright-chromium": "^1.22.2",
8080
"release-it": "^15.0.0",
81+
"sentry-testkit": "^3.3.2",
8182
"typescript": "^4.7.2"
8283
}
8384
}

test/default.test.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1+
import sentryTestkit from 'sentry-testkit'
12
import { setup, loadConfig, url } from '@nuxtjs/module-test-utils'
23
import { $$, createBrowser } from './utils'
34

5+
const { testkit, localServer } = sentryTestkit()
6+
const TEST_DSN = 'http://[email protected]/000001'
7+
48
describe('Smoke test (default)', () => {
59
/** @type {any} */
610
let nuxt
711
/** @type {import('playwright-chromium').Browser} */
812
let browser
913

1014
beforeAll(async () => {
11-
({ nuxt } = await setup(loadConfig(__dirname, 'default')))
15+
await localServer.start(TEST_DSN)
16+
const dsn = localServer.getDsn()
17+
nuxt = (await setup(loadConfig(__dirname, 'default', { sentry: { dsn } }, { merge: true }))).nuxt
1218
browser = await createBrowser()
1319
})
1420

@@ -17,6 +23,11 @@ describe('Smoke test (default)', () => {
1723
await browser.close()
1824
}
1925
await nuxt.close()
26+
await localServer.stop()
27+
})
28+
29+
beforeEach(() => {
30+
testkit.reset()
2031
})
2132

2233
test('builds and runs', async () => {
@@ -32,4 +43,6 @@ describe('Smoke test (default)', () => {
3243
expect(await $$('#client-side', page)).toBe('Works!')
3344
expect(errors).toEqual([])
3445
})
46+
47+
// TODO: Add tests for custom integration. Blocked by various sentry-kit bugs reported in its repo.
3548
})
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function (/** context */) {
2+
return []
3+
}

0 commit comments

Comments
 (0)