Skip to content

Commit

Permalink
feat: support for registering external integrations (#276)
Browse files Browse the repository at this point in the history
  • Loading branch information
rchl committed Aug 3, 2022
1 parent 0f8242d commit 2cf56ef
Show file tree
Hide file tree
Showing 21 changed files with 2,999 additions and 1,203 deletions.
8 changes: 7 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@ module.exports = {
rules: {
'import/named': 'off',
'import/namespace': 'off',
'import/no-absolute-path': 'off',
'no-console': [
'error', {
allow: ['assert', 'warn', 'error', 'info']
}
],
'vue/multi-word-component-names': 'off'
},
overrides: [
{
files: ['test/**'],
plugins: ['jest'],
extends: ['plugin:jest/recommended'],
extends: ['plugin:jest/recommended']
},
{
files: ['*.ts', '*.tsx'],
Expand Down
32 changes: 32 additions & 0 deletions docs/content/en/sentry/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,38 @@ Note that the module sets the following defaults when publishing is enabled:
```
- See https://docs.sentry.io/platforms/node/pluggable-integrations/ for more information on the integrations and their configuration
### customClientIntegrations
- Type: `String`
- Default: `undefined`
- This option gives the flexibility to register any custom integration that is not handled internally by the `clientIntegrations` option.
- 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.
For example:
```js
import SentryRRWeb from '@sentry/rrweb'
export default function (context) {
return [new SentryRRWeb()]
}
```
### customServerIntegrations
- Type: `String`
- Default: `undefined`
- This option gives the flexibility to register any custom integration that is not handled internally by the `serverIntegrations` option.
- 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.
For example:
```js
import MyAwesomeIntegration from 'my-awesome-integration'
export default function () {
return [new MyAwesomeIntegration()]
}
```
### tracing
- Type: `Boolean` or `Object`
Expand Down
38 changes: 30 additions & 8 deletions lib/core/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const BROWSER_INTEGRATIONS = ['InboundFilters', 'FunctionToString', 'TryC
const SERVER_INTEGRATIONS = ['CaptureConsole', 'Debug', 'Dedupe', 'ExtraErrorData', 'RewriteFrames', 'Modules', 'Transaction']

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

/**
* @param {string} packageName
Expand Down Expand Up @@ -131,6 +131,15 @@ export async function resolveClientOptions (moduleContainer, moduleOptions, logg
}
}

let customClientIntegrations
if (options.customClientIntegrations) {
if (typeof (options.customClientIntegrations) === 'string') {
customClientIntegrations = moduleContainer.nuxt.resolver.resolveAlias(options.customClientIntegrations)
} else {
logger.warn(`Invalid customServerIntegrations option. Expected a file path, got "${typeof (options.customClientIntegrations)}".`)
}
}

return {
PLUGGABLE_INTEGRATIONS,
BROWSER_INTEGRATIONS,
Expand All @@ -142,10 +151,11 @@ export async function resolveClientOptions (moduleContainer, moduleOptions, logg
},
lazy: options.lazy,
apiMethods,
customClientIntegrations,
logMockCalls: options.logMockCalls, // for mocked only
tracing: options.tracing,
initialize: canInitialize(options),
integrations: filterDisabledIntegration(options.clientIntegrations)
integrations: filterDisabledIntegrations(options.clientIntegrations)
.reduce((res, key) => {
// @ts-ignore
res[key] = options.clientIntegrations[key]
Expand All @@ -171,14 +181,26 @@ export async function resolveServerOptions (moduleContainer, moduleOptions, logg
}
}

let customIntegrations = []
if (options.customServerIntegrations) {
const resolvedPath = moduleContainer.nuxt.resolver.resolveAlias(options.customServerIntegrations)
customIntegrations = (await import(resolvedPath).then(m => m.default || m))()
if (!Array.isArray(customIntegrations)) {
logger.error(`Invalid value returned from customServerIntegrations plugin. Expected an array, got "${typeof (customIntegrations)}".`)
}
}

const defaultConfig = {
dsn: options.dsn,
intergrations: filterDisabledIntegration(options.serverIntegrations)
.map((name) => {
const opt = options.serverIntegrations[name]
// @ts-ignore
return Object.keys(opt).length ? new Integrations[name](opt) : new Integrations[name]()
})
intergrations: [
...filterDisabledIntegrations(options.serverIntegrations)
.map((name) => {
const opt = options.serverIntegrations[name]
// @ts-ignore
return Object.keys(opt).length ? new Integrations[name](opt) : new Integrations[name]()
}),
...customIntegrations
]
}
options.config = merge(defaultConfig, options.config, options.serverConfig)

Expand Down
2 changes: 2 additions & 0 deletions lib/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export default function SentryModule (moduleOptions) {
RewriteFrames: {},
Transaction: {}
},
customClientIntegrations: '',
customServerIntegrations: '',
config: {
environment: this.options.dev ? 'development' : 'production'
},
Expand Down
16 changes: 14 additions & 2 deletions lib/plugin.client.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@ import * as Sentry from '@sentry/browser'
if (options.initialize) {
let integrations = options.PLUGGABLE_INTEGRATIONS.filter(key => key in options.integrations)
if (integrations.length) {%>import { <%= integrations.join(', ') %> } from '@sentry/integrations'
<%}
if (options.customClientIntegrations) {%>import getCustomIntegrations from '<%= options.customClientIntegrations %>'
<%}
integrations = options.BROWSER_INTEGRATIONS.filter(key => key in options.integrations)
if (integrations.length) {%>const { <%= integrations.join(', ') %> } = Sentry.Integrations
if (integrations.length) {%>
const { <%= integrations.join(', ') %> } = Sentry.Integrations
<%}
}
%>
<% if (options.tracing) { %>
import { Integrations as TracingIntegrations } from '@sentry/tracing'
<% } %>

export default function (ctx, inject) {
// eslint-disable-next-line require-await
export default async function (ctx, inject) {
<% if (options.initialize) { %>
/* eslint-disable object-curly-spacing, quote-props, quotes, key-spacing, comma-spacing */
const config = {
Expand Down Expand Up @@ -52,6 +56,14 @@ export default function (ctx, inject) {
<% if (options.tracing) { %>
config.integrations.push(<%= `new TracingIntegrations.BrowserTracing(${serialize(options.tracing.browserOptions)})` %>)
<% } %>
<% if (options.customClientIntegrations) { %>
const customIntegrations = await getCustomIntegrations(ctx)
if (Array.isArray(customIntegrations)) {
config.integrations.push(...customIntegrations)
} else {
console.error(`[@nuxtjs/sentry] Invalid value returned from customClientIntegrations plugin. Expected an array, got "${typeof customIntegrations}".`)
}
<% } %>
/* eslint-enable object-curly-spacing, quote-props, quotes, key-spacing, comma-spacing */
Sentry.init(config)
<% } %>
Expand Down
15 changes: 11 additions & 4 deletions lib/plugin.lazy.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ VueLib.config.errorHandler = (error, vm, info) => {
if (VueLib.util) {
VueLib.util.warn(`Error in ${info}: "${error.toString()}"`, vm)
}
console.error(error) // eslint-disable-line no-console
console.error(error)
}

if (vueErrorHandler) {
Expand Down Expand Up @@ -83,7 +83,6 @@ async function attemptLoadSentry (ctx, inject) {

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) { %>
Expand Down Expand Up @@ -145,6 +144,14 @@ async function loadSentry (ctx, inject) {
return `new ${name}(${integrationOptions.length ? '{' + integrationOptions.join(',') + '}' : ''})`
}).join(',\n ')%>
]
<%if (options.customClientIntegrations) {%>
const customIntegrations = (await import(/* <%= magicComments.join(', ') %> */ '<%= options.customClientIntegrations %>').then(m => m.default || m))(ctx)
if (Array.isArray(customIntegrations)) {
config.integrations.push(...customIntegrations)
} else {
console.error(`[@nuxtjs/sentry] Invalid value returned from customClientIntegrations plugin. Expected an array, got "${typeof customIntegrations}".`)
}
<% } %>
/* eslint-enable object-curly-spacing, quote-props, quotes, key-spacing, comma-spacing */
Sentry.init(config)
<% } %>
Expand All @@ -155,7 +162,7 @@ async function loadSentry (ctx, inject) {
window.removeEventListener('unhandledrejection', delayUnhandledRejection)
if (delayedGlobalErrors.length) {
if (window.onerror) {
console.info('Reposting global errors after Sentry has loaded') // eslint-disable-line no-console
console.info('Reposting global errors after Sentry has loaded')
for (const errorArgs of delayedGlobalErrors) {
window.onerror.apply(window, errorArgs)
}
Expand All @@ -164,7 +171,7 @@ async function loadSentry (ctx, inject) {
}
if (delayedUnhandledRejections.length) {
if (window.onunhandledrejection) {
console.info('Reposting unhandled promise rejection errors after Sentry has loaded') // eslint-disable-line no-console
console.info('Reposting unhandled promise rejection errors after Sentry has loaded')
for (const reason of delayedUnhandledRejections) {
window.onunhandledrejection(reason)
}
Expand Down
1 change: 0 additions & 1 deletion lib/plugin.mocked.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const apiMethods = <%= JSON.stringify(options.apiMethods)%>
export default function (ctx, inject) {
const SentryMock = {}
apiMethods.forEach(key => {
// eslint-disable-next-line no-console
SentryMock[key] = <%= options.logMockCalls
? '(...args) => console.warn(`$sentry.${key}() called, but Sentry plugin is disabled. Arguments:`, args)'
: '_ => _'%>
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"main": "lib/module.js",
"types": "types/index.d.ts",
"scripts": {
"dev:fixture": "nuxt -c ./test/fixture/default/nuxt.config.js",
"dev:fixture": "node ./node_modules/nuxt/bin/nuxt.js -c ./test/fixture/lazy/nuxt.config.js",
"dev:fixture:build": "node ./node_modules/nuxt/bin/nuxt.js build -c ./test/fixture/default/nuxt.config.js",
"dev:fixture:start": "node ./node_modules/nuxt/bin/nuxt.js start -c ./test/fixture/default/nuxt.config.js",
"dev:generate": "nuxt generate -c ./test/fixture/default/nuxt.config.js --force-build",
Expand Down Expand Up @@ -78,6 +78,7 @@
"nuxt": "^2.15.8",
"playwright-chromium": "^1.22.2",
"release-it": "^15.0.0",
"sentry-testkit": "^3.3.2",
"typescript": "^4.7.2"
}
}
15 changes: 14 additions & 1 deletion test/default.test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import sentryTestkit from 'sentry-testkit'
import { setup, loadConfig, url } from '@nuxtjs/module-test-utils'
import { $$, createBrowser } from './utils'

const { testkit, localServer } = sentryTestkit()
const TEST_DSN = 'http://[email protected]/000001'

describe('Smoke test (default)', () => {
/** @type {any} */
let nuxt
/** @type {import('playwright-chromium').Browser} */
let browser

beforeAll(async () => {
({ nuxt } = await setup(loadConfig(__dirname, 'default')))
await localServer.start(TEST_DSN)
const dsn = localServer.getDsn()
nuxt = (await setup(loadConfig(__dirname, 'default', { sentry: { dsn } }, { merge: true }))).nuxt
browser = await createBrowser()
})

Expand All @@ -17,6 +23,11 @@ describe('Smoke test (default)', () => {
await browser.close()
}
await nuxt.close()
await localServer.stop()
})

beforeEach(() => {
testkit.reset()
})

test('builds and runs', async () => {
Expand All @@ -32,4 +43,6 @@ describe('Smoke test (default)', () => {
expect(await $$('#client-side', page)).toBe('Works!')
expect(errors).toEqual([])
})

// TODO: Add tests for custom integration. Blocked by various sentry-kit bugs reported in its repo.
})
3 changes: 3 additions & 0 deletions test/fixture/default/config/custom-client-integrations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function (/** context */) {
return []
}
2 changes: 1 addition & 1 deletion test/fixture/default/nuxt.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ const config = {
SentryModule
],
sentry: {
dsn: 'https://[email protected]/1429779',
clientIntegrations: {
// Integration from @Sentry/browser package.
TryCatch: { eventTarget: false }
},
customClientIntegrations: '~/config/custom-client-integrations.js',
publishRelease: {
authToken: 'fakeToken',
org: 'MyCompany',
Expand Down
3 changes: 3 additions & 0 deletions test/fixture/lazy/config/custom-client-integrations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function (/** context */) {
return []
}
3 changes: 2 additions & 1 deletion test/fixture/lazy/nuxt.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const config = {
clientIntegrations: {
// Integration from @Sentry/browser package.
TryCatch: { eventTarget: false }
}
},
customClientIntegrations: '~/config/custom-client-integrations.js'
},
publicRuntimeConfig: {
sentry: {
Expand Down
2 changes: 1 addition & 1 deletion test/fixture/lazy/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default {
created () {
this.$sentryReady().then(() => {
this.isSentryReady = true
console.log('Sentry is ready') // eslint-disable-line no-console
console.info('Sentry is ready')
})
},
mounted () {
Expand Down
8 changes: 2 additions & 6 deletions test/fixture/with-lazy-config/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,21 @@ export default {
created () {
this.$sentryReady().then(() => {
this.isSentryReady = true
// eslint-disable-next-line no-console
console.log('Sentry is ready')
console.info('Sentry is ready')
})
},
mounted () {
this.$sentry.captureMessage('Hi!')
try {
this.$sentry.captureEvent({ message: 'This should fail' })
// eslint-disable-next-line no-console
console.error('The call to captureEvent should fail so this line shouldn\'t be printed (on initial load only, this line will be printed on eg HMR)')
} catch (err) {
// eslint-disable-next-line no-console
console.info('Caught expected error on $sentry.captureEvent')
}
this.$nextTick(() => {
// eslint-disable-next-line no-console
console.log('Loading Sentry in 1 second')
console.info('Loading Sentry in 1 second')
setTimeout(this.$sentryLoad, 1000)
})
}
Expand Down
6 changes: 2 additions & 4 deletions test/fixture/with-lazy-config/pages/mounted.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@ export default {
created () {
this.$sentryReady().then(() => {
this.isSentryReady = true
// eslint-disable-next-line no-console
console.log('Sentry is ready')
console.info('Sentry is ready')
})
},
mounted () {
this.$nextTick(() => {
// eslint-disable-next-line no-console
console.log('Loading Sentry in 1 second')
console.info('Loading Sentry in 1 second')
setTimeout(this.$sentryLoad, 1000)
})
Expand Down
Loading

0 comments on commit 2cf56ef

Please sign in to comment.