diff --git a/dockerfiles/Dockerfile.browser b/dockerfiles/Dockerfile.browser index 701e8c54ac..f98bbb39e9 100644 --- a/dockerfiles/Dockerfile.browser +++ b/dockerfiles/Dockerfile.browser @@ -10,6 +10,7 @@ COPY babel.config.js lerna.json .eslintignore .eslintrc.js jest.config.js tsconf ADD min_packages.tar . COPY .rollup ./.rollup COPY bin ./bin +COPY jest ./jest COPY packages ./packages RUN npm install diff --git a/dockerfiles/Dockerfile.ci b/dockerfiles/Dockerfile.ci index b8560c6d7b..83cbaf1b0d 100644 --- a/dockerfiles/Dockerfile.ci +++ b/dockerfiles/Dockerfile.ci @@ -12,6 +12,7 @@ COPY .rollup ./.rollup COPY bin ./bin COPY scripts ./scripts COPY test ./test +COPY jest ./jest COPY packages ./packages RUN npm install --unsafe-perm diff --git a/dockerfiles/Dockerfile.node b/dockerfiles/Dockerfile.node index 187ec60a9d..4852f01a2e 100644 --- a/dockerfiles/Dockerfile.node +++ b/dockerfiles/Dockerfile.node @@ -10,6 +10,7 @@ COPY babel.config.js lerna.json .eslintignore .eslintrc.js jest.config.js tsconf ADD min_packages.tar . COPY .rollup ./.rollup COPY bin ./bin +COPY jest ./jest COPY packages ./packages RUN npm install diff --git a/jest.config.js b/jest.config.js index 64bc93ba34..40bc65fde5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -49,7 +49,9 @@ module.exports = { 'plugin-simple-throttle', 'plugin-console-breadcrumbs', 'plugin-browser-session' - ]), + ], { + setupFiles: ['/jest/setup/mockEventTarget.js'] + }), project('react native', [ 'react-native', 'delivery-react-native', diff --git a/jest/setup/mockEventTarget.js b/jest/setup/mockEventTarget.js new file mode 100644 index 0000000000..784b0d699a --- /dev/null +++ b/jest/setup/mockEventTarget.js @@ -0,0 +1,40 @@ +// copy the code from https://developer.mozilla.org/en-US/docs/Web/API/EventTarget#Simple_implementation_of_EventTarget +var EventTarget = function () { + this.listeners = {} +} + +EventTarget.prototype.listeners = null +EventTarget.prototype.addEventListener = function (type, callback) { + if (!(type in this.listeners)) { + this.listeners[type] = [] + } + this.listeners[type].push(callback) +} + +EventTarget.prototype.removeEventListener = function (type, callback) { + if (!(type in this.listeners)) { + return + } + var stack = this.listeners[type] + for (var i = 0, l = stack.length; i < l; i++) { + if (stack[i] === callback) { + stack.splice(i, 1) + return + } + } +} + +EventTarget.prototype.dispatchEvent = function (event) { + if (!(event.type in this.listeners)) { + return true + } + var stack = this.listeners[event.type].slice() + + for (var i = 0, l = stack.length; i < l; i++) { + stack[i].call(this, event) + } + return !event.defaultPrevented +} + +// make the EventTarget global +global.EventTarget = EventTarget diff --git a/packages/browser/package.json b/packages/browser/package.json index 5e81bc2618..6cc6cf081e 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,8 +1,15 @@ { "name": "@bugsnag/browser", "version": "8.1.2", - "main": "dist/bugsnag.js", + "main": "dist/notifier.js", "types": "types/bugsnag.d.ts", + "exports": { + ".": { + "types": "./types/bugsnag.d.ts", + "default": "./dist/notifier.js", + "import": "./dist/notifier.mjs" + } + }, "description": "Bugsnag error reporter for browser JavaScript", "homepage": "https://www.bugsnag.com/", "repository": { @@ -22,7 +29,8 @@ "scripts": { "size": "../../bin/size dist/bugsnag.min.js", "clean": "rm -fr dist && mkdir dist", - "build": "npm run clean && npm run build:dist && npm run build:dist:min", + "build": "npm run clean && npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", "build:dist": "cross-env NODE_ENV=production bash -c '../../bin/bundle src/notifier.js --standalone=Bugsnag | ../../bin/extract-source-map dist/bugsnag.js'", "build:dist:min": "cross-env NODE_ENV=production bash -c '../../bin/bundle src/notifier.js --standalone=Bugsnag | ../../bin/minify dist/bugsnag.min.js'", "cdn-upload": "../../bin/cdn-upload dist/*" diff --git a/packages/browser/rollup.config.npm.mjs b/packages/browser/rollup.config.npm.mjs new file mode 100644 index 0000000000..6235d5fe1a --- /dev/null +++ b/packages/browser/rollup.config.npm.mjs @@ -0,0 +1,33 @@ +import createRollupConfig from "../../.rollup/index.mjs"; + +export default createRollupConfig({ + input: "src/notifier.ts", + external: [ + "@bugsnag/core/client", + "@bugsnag/core/event", + "@bugsnag/core/session", + "@bugsnag/core/breadcrumb", + "@bugsnag/core/config", + "@bugsnag/core/types", + "@bugsnag/core/lib/es-utils/map", + "@bugsnag/core/lib/es-utils/keys", + "@bugsnag/core/lib/es-utils/assign", + "@bugsnag/plugin-window-onerror", + "@bugsnag/plugin-window-unhandled-rejection", + "@bugsnag/plugin-app-duration", + "@bugsnag/plugin-browser-device", + "@bugsnag/plugin-browser-context", + "@bugsnag/plugin-browser-request", + "@bugsnag/plugin-simple-throttle", + "@bugsnag/plugin-console-breadcrumbs", + "@bugsnag/plugin-network-breadcrumbs", + "@bugsnag/plugin-navigation-breadcrumbs", + "@bugsnag/plugin-interaction-breadcrumbs", + "@bugsnag/plugin-inline-script-content", + "@bugsnag/plugin-browser-session", + "@bugsnag/plugin-client-ip", + "@bugsnag/plugin-strip-query-string", + "@bugsnag/delivery-x-domain-request", + "@bugsnag/delivery-xml-http-request" + ], +}); diff --git a/packages/browser/src/config.js b/packages/browser/src/config.js deleted file mode 100644 index 12a003b5f1..0000000000 --- a/packages/browser/src/config.js +++ /dev/null @@ -1,35 +0,0 @@ -const { schema } = require('@bugsnag/core/config') -const map = require('@bugsnag/core/lib/es-utils/map') -const assign = require('@bugsnag/core/lib/es-utils/assign') - -module.exports = { - releaseStage: assign({}, schema.releaseStage, { - defaultValue: () => { - if (/^localhost(:\d+)?$/.test(window.location.host)) return 'development' - return 'production' - } - }), - appType: { - ...schema.appType, - defaultValue: () => 'browser' - }, - logger: assign({}, schema.logger, { - defaultValue: () => - // set logger based on browser capability - (typeof console !== 'undefined' && typeof console.debug === 'function') - ? getPrefixedConsole() - : undefined - }) -} - -const getPrefixedConsole = () => { - const logger = {} - const consoleLog = console.log - map(['debug', 'info', 'warn', 'error'], (method) => { - const consoleMethod = console[method] - logger[method] = typeof consoleMethod === 'function' - ? consoleMethod.bind(console, '[bugsnag]') - : consoleLog.bind(console, '[bugsnag]') - }) - return logger -} diff --git a/packages/browser/src/config.ts b/packages/browser/src/config.ts new file mode 100644 index 0000000000..fd9c49a335 --- /dev/null +++ b/packages/browser/src/config.ts @@ -0,0 +1,24 @@ +import { schema } from '@bugsnag/core/config' +import assign from '@bugsnag/core/lib/es-utils/assign' +import getPrefixedConsole from './get-prefixed-console' + +const config = { + releaseStage: assign({}, schema.releaseStage, { + defaultValue: () => { + if (/^localhost(:\d+)?$/.test(window.location.host)) return 'development' + return 'production' + } + }), + appType: assign({}, schema.appType, { + defaultValue: () => 'browser' + }), + logger: assign({}, schema.logger, { + defaultValue: () => + // set logger based on browser capability + (typeof console !== 'undefined' && typeof console.debug === 'function') + ? getPrefixedConsole() + : undefined + }) +} + +export default config diff --git a/packages/browser/src/get-prefixed-console.ts b/packages/browser/src/get-prefixed-console.ts new file mode 100644 index 0000000000..2fcb2bc1b0 --- /dev/null +++ b/packages/browser/src/get-prefixed-console.ts @@ -0,0 +1,17 @@ +import map from '@bugsnag/core/lib/es-utils/map' + +type LoggerMethod = 'debug' | 'info' | 'warn' | 'error' + +const getPrefixedConsole = () => { + const logger: Record = {} + const consoleLog = console.log + map(['debug', 'info', 'warn', 'error'], (method: LoggerMethod) => { + const consoleMethod = console[method] + logger[method] = typeof consoleMethod === 'function' + ? consoleMethod.bind(console, '[bugsnag]') + : consoleLog.bind(console, '[bugsnag]') + }) + return logger +} + +export default getPrefixedConsole diff --git a/packages/browser/src/notifier.d.ts b/packages/browser/src/notifier.d.ts deleted file mode 100644 index 844bfb518c..0000000000 --- a/packages/browser/src/notifier.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from '../types/bugsnag' -export * from '../types/bugsnag' diff --git a/packages/browser/src/notifier.js b/packages/browser/src/notifier.js deleted file mode 100644 index dcb9fb1eda..0000000000 --- a/packages/browser/src/notifier.js +++ /dev/null @@ -1,110 +0,0 @@ -const name = 'Bugsnag JavaScript' -const version = '__VERSION__' -const url = 'https://github.com/bugsnag/bugsnag-js' - -const Client = require('@bugsnag/core/client') -const Event = require('@bugsnag/core/event') -const Session = require('@bugsnag/core/session') -const Breadcrumb = require('@bugsnag/core/breadcrumb') - -const map = require('@bugsnag/core/lib/es-utils/map') -const keys = require('@bugsnag/core/lib/es-utils/keys') -const assign = require('@bugsnag/core/lib/es-utils/assign') - -// extend the base config schema with some browser-specific options -const schema = assign({}, require('@bugsnag/core/config').schema, require('./config')) - -const pluginWindowOnerror = require('@bugsnag/plugin-window-onerror') -const pluginUnhandledRejection = require('@bugsnag/plugin-window-unhandled-rejection') -const pluginApp = require('@bugsnag/plugin-app-duration') -const pluginDevice = require('@bugsnag/plugin-browser-device') -const pluginContext = require('@bugsnag/plugin-browser-context') -const pluginRequest = require('@bugsnag/plugin-browser-request') -const pluginThrottle = require('@bugsnag/plugin-simple-throttle') -const pluginConsoleBreadcrumbs = require('@bugsnag/plugin-console-breadcrumbs') -const pluginNetworkBreadcrumbs = require('@bugsnag/plugin-network-breadcrumbs') -const pluginNavigationBreadcrumbs = require('@bugsnag/plugin-navigation-breadcrumbs') -const pluginInteractionBreadcrumbs = require('@bugsnag/plugin-interaction-breadcrumbs') -const pluginInlineScriptContent = require('@bugsnag/plugin-inline-script-content') -const pluginSession = require('@bugsnag/plugin-browser-session') -const pluginIp = require('@bugsnag/plugin-client-ip') -const pluginStripQueryString = require('@bugsnag/plugin-strip-query-string') - -// delivery mechanisms -const dXDomainRequest = require('@bugsnag/delivery-x-domain-request') -const dXMLHttpRequest = require('@bugsnag/delivery-xml-http-request') - -const Bugsnag = { - _client: null, - createClient: (opts) => { - // handle very simple use case where user supplies just the api key as a string - if (typeof opts === 'string') opts = { apiKey: opts } - if (!opts) opts = {} - - const internalPlugins = [ - // add browser-specific plugins - pluginApp, - pluginDevice(), - pluginContext(), - pluginRequest(), - pluginThrottle, - pluginSession, - pluginIp, - pluginStripQueryString, - pluginWindowOnerror(), - pluginUnhandledRejection(), - pluginNavigationBreadcrumbs(), - pluginInteractionBreadcrumbs(), - pluginNetworkBreadcrumbs(), - pluginConsoleBreadcrumbs, - - // this one added last to avoid wrapping functionality before bugsnag uses it - pluginInlineScriptContent() - ] - - // configure a client with user supplied options - const bugsnag = new Client(opts, schema, internalPlugins, { name, version, url }) - - // set delivery based on browser capability (IE 8+9 have an XDomainRequest object) - bugsnag._setDelivery(window.XDomainRequest ? dXDomainRequest : dXMLHttpRequest) - - bugsnag._logger.debug('Loaded!') - bugsnag.leaveBreadcrumb('Bugsnag loaded', {}, 'state') - - return bugsnag._config.autoTrackSessions - ? bugsnag.startSession() - : bugsnag - }, - start: (opts) => { - if (Bugsnag._client) { - Bugsnag._client._logger.warn('Bugsnag.start() was called more than once. Ignoring.') - return Bugsnag._client - } - Bugsnag._client = Bugsnag.createClient(opts) - return Bugsnag._client - }, - isStarted: () => { - return Bugsnag._client != null - } -} - -map(['resetEventCount'].concat(keys(Client.prototype)), (m) => { - if (/^_/.test(m)) return - Bugsnag[m] = function () { - if (!Bugsnag._client) return console.log(`Bugsnag.${m}() was called before Bugsnag.start()`) - Bugsnag._client._depth += 1 - const ret = Bugsnag._client[m].apply(Bugsnag._client, arguments) - Bugsnag._client._depth -= 1 - return ret - } -}) - -module.exports = Bugsnag - -module.exports.Client = Client -module.exports.Event = Event -module.exports.Session = Session -module.exports.Breadcrumb = Breadcrumb - -// Export a "default" property for compatibility with ESM imports -module.exports.default = Bugsnag diff --git a/packages/browser/src/notifier.ts b/packages/browser/src/notifier.ts new file mode 100644 index 0000000000..f95b38e111 --- /dev/null +++ b/packages/browser/src/notifier.ts @@ -0,0 +1,124 @@ +import Client from '@bugsnag/core/client' +// import Event from '@bugsnag/core/event' +// import Session from '@bugsnag/core/session' +// import Breadcrumb from '@bugsnag/core/breadcrumb' +import { Config } from '@bugsnag/core/types' + +import map from '@bugsnag/core/lib/es-utils/map' +import keys from '@bugsnag/core/lib/es-utils/keys' +import assign from '@bugsnag/core/lib/es-utils/assign' + +// extend the base config schema with some browser-specific options +import { schema as baseConfig } from '@bugsnag/core/config' +import browserConfig from './config' + +import pluginWindowOnerror from '@bugsnag/plugin-window-onerror' +import pluginUnhandledRejection from '@bugsnag/plugin-window-unhandled-rejection' +import pluginApp from '@bugsnag/plugin-app-duration' +import pluginDevice from '@bugsnag/plugin-browser-device' +import pluginContext from '@bugsnag/plugin-browser-context' +import pluginRequest from '@bugsnag/plugin-browser-request' +import pluginThrottle from '@bugsnag/plugin-simple-throttle' +import pluginConsoleBreadcrumbs from '@bugsnag/plugin-console-breadcrumbs' +import pluginNetworkBreadcrumbs from '@bugsnag/plugin-network-breadcrumbs' +import pluginNavigationBreadcrumbs from '@bugsnag/plugin-navigation-breadcrumbs' +import pluginInteractionBreadcrumbs from '@bugsnag/plugin-interaction-breadcrumbs' +import pluginInlineScriptContent from '@bugsnag/plugin-inline-script-content' +import pluginSession from '@bugsnag/plugin-browser-session' +import pluginIp from '@bugsnag/plugin-client-ip' +import pluginStripQueryString from '@bugsnag/plugin-strip-query-string' + +// delivery mechanisms +import dXDomainRequest from '@bugsnag/delivery-x-domain-request' +import dXMLHttpRequest from '@bugsnag/delivery-xml-http-request' + +const name = 'Bugsnag JavaScript' +const version = '__VERSION__' +const url = 'https://github.com/bugsnag/bugsnag-js' + +const schema = assign({}, baseConfig, browserConfig) + +declare global { + interface Window { + XDomainRequest: unknown + } +} + +type BrowserClient = Partial & { + _client: Client | null + createClient: (opts?: Config) => Client + start: (opts?: Config) => Client + isStarted: () => boolean + _setDelivery?: (handler: typeof dXDomainRequest | typeof dXMLHttpRequest) => void +} + +const Bugsnag: BrowserClient = { + _client: null, + createClient: (opts) => { + // handle very simple use case where user supplies just the api key as a string + if (typeof opts === 'string') opts = { apiKey: opts } + if (!opts) opts = {} as unknown as Config + + const internalPlugins = [ + // add browser-specific plugins + pluginApp, + pluginDevice(), + pluginContext(), + pluginRequest(), + pluginThrottle, + pluginSession, + pluginIp, + pluginStripQueryString, + pluginWindowOnerror(), + pluginUnhandledRejection(), + pluginNavigationBreadcrumbs(), + pluginInteractionBreadcrumbs(), + pluginNetworkBreadcrumbs(), + pluginConsoleBreadcrumbs, + + // this one added last to avoid wrapping functionality before bugsnag uses it + pluginInlineScriptContent() + ] + + // configure a client with user supplied options + const bugsnag = new Client(opts, schema, internalPlugins, { name, version, url }); + + // set delivery based on browser capability (IE 8+9 have an XDomainRequest object) + (bugsnag as BrowserClient)._setDelivery?.(window.XDomainRequest ? dXDomainRequest : dXMLHttpRequest) + + bugsnag._logger.debug('Loaded!') + bugsnag.leaveBreadcrumb('Bugsnag loaded', {}, 'state') + + return bugsnag._config.autoTrackSessions + ? bugsnag.startSession() + : bugsnag + }, + start: (opts) => { + if (Bugsnag._client) { + Bugsnag._client._logger.warn('Bugsnag.start() was called more than once. Ignoring.') + return Bugsnag._client + } + Bugsnag._client = Bugsnag.createClient(opts) + return Bugsnag._client + }, + isStarted: () => { + return Bugsnag._client != null + } +} + +type Method = keyof typeof Client.prototype + +map(['resetEventCount'].concat(keys(Client.prototype)) as Method[], (m) => { + if (/^_/.test(m)) return + Bugsnag[m] = function () { + if (!Bugsnag._client) return console.log(`Bugsnag.${m}() was called before Bugsnag.start()`) + Bugsnag._client._depth += 1 + const ret = Bugsnag._client[m].apply(Bugsnag._client, arguments) + Bugsnag._client._depth -= 1 + return ret + } +}) + +// export { Client, Event, Session, Breadcrumb, Bugsnag } + +export default Bugsnag diff --git a/packages/browser/test/index.test.ts b/packages/browser/test/index.test.ts index 7e39b8cb13..3834a9f5e3 100644 --- a/packages/browser/test/index.test.ts +++ b/packages/browser/test/index.test.ts @@ -1,4 +1,4 @@ -import BugsnagBrowserStatic, { Breadcrumb, BrowserConfig, Session } from '../src/notifier' +import BugsnagBrowserStatic, { Breadcrumb, BrowserConfig, Session } from '../' const DONE = window.XMLHttpRequest.DONE @@ -39,7 +39,7 @@ describe('browser notifier', () => { }) function getBugsnag (): typeof BugsnagBrowserStatic { - const Bugsnag = require('../src/notifier') as typeof BugsnagBrowserStatic + const Bugsnag = require('../src/notifier').default return Bugsnag } @@ -52,7 +52,7 @@ describe('browser notifier', () => { load: client => 10 }] }) - expect(Bugsnag.getPlugin('foobar')).toBe(10) + expect(Bugsnag.getPlugin?.('foobar')).toBe(10) }) it('notifies handled errors', (done) => { @@ -138,7 +138,7 @@ describe('browser notifier', () => { it('accepts all config options', (done) => { const Bugsnag = getBugsnag() - const completeConfig: Required = { + const completeConfig: BrowserConfig = { apiKey: API_KEY, appVersion: '1.2.3', appType: 'worker', @@ -148,7 +148,7 @@ describe('browser notifier', () => { unhandledRejections: true }, onError: [ - event => true + () => true ], onBreadcrumb: (b: Breadcrumb) => { return false diff --git a/packages/browser/tsconfig.json b/packages/browser/tsconfig.json new file mode 100644 index 0000000000..a5cb75c562 --- /dev/null +++ b/packages/browser/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"] +} diff --git a/packages/core/config.d.ts b/packages/core/config.d.ts new file mode 100644 index 0000000000..cee871526c --- /dev/null +++ b/packages/core/config.d.ts @@ -0,0 +1,115 @@ +export interface Schema { + apiKey: { + defaultValue: () => null + message: string + validate: (value: unknown) => boolean + } + appVersion: { + defaultValue: () => undefined + message: string + validate: (value: unknown) => boolean + } + appType: { + defaultValue: () => undefined + message: string + validate: (value: unknown) => boolean + } + autoDetectErrors: { + defaultValue: () => true + message: string + validate: (value: unknown) => boolean + } + enabledErrorTypes: { + defaultValue: () => { unhandledExceptions: boolean, unhandledRejections: boolean } + message: string + allowPartialObject: boolean + validate: (value: unknown) => boolean + } + onError: { + defaultValue: () => [] + message: string + validate: (value: unknown) => boolean + } + onSession: { + defaultValue: () => [] + message: string + validate: (value: unknown) => boolean + } + onBreadcrumb: { + defaultValue: () => [] + message: string + validate: (value: unknown) => boolean + } + endpoints: { + defaultValue: (endpoints: { notify: string, sessions: string } | undefined) => { notify: string | null, sessions: string | null } + message: string + validate: (value: unknown) => boolean + } + autoTrackSessions: { + defaultValue: () => boolean + message: string + validate: (value: unknown) => boolean + } + enabledReleaseStages: { + defaultValue: () => null + message: string + validate: (value: unknown) => boolean + } + releaseStage: { + defaultValue: () => 'production' + message: string + validate: (value: unknown) => boolean + } + maxBreadcrumbs: { + defaultValue: () => 25 + message: string + validate: (value: unknown) => boolean + } + enabledBreadcrumbTypes: { + defaultValue: () => ['navigation', 'request', 'process', 'log', 'user', 'state', 'error', 'manual'] + message: string + validate: (value: unknown) => boolean + } + context: { + defaultValue: () => undefined + message: string + validate: (value: unknown) => boolean + } + user: { + defaultValue: () => {} + message: string + validate: (value: unknown) => boolean + } + metadata: { + defaultValue: () => {} + message: string + validate: (value: unknown) => boolean + } + logger: { + defaultValue: () => undefined + message: string + validate: (value: unknown) => boolean + } + redactedKeys: { + defaultValue: () => ['password'] + message: string + validate: (value: unknown) => boolean + } + plugins: { + defaultValue: () => [] + message: string + validate: (value: unknown) => boolean + } + featureFlags: { + defaultValue: () => [] + message: string + validate: (value: unknown) => boolean + } + reportUnhandledPromiseRejectionsAsHandled: { + defaultValue: () => false + message: string + validate: (value: unknown) => boolean + } +} + +export const schema: Schema diff --git a/packages/plugin-inline-script-content/package.json b/packages/plugin-inline-script-content/package.json index af39d4b493..9d22fed43c 100644 --- a/packages/plugin-inline-script-content/package.json +++ b/packages/plugin-inline-script-content/package.json @@ -1,7 +1,15 @@ { "name": "@bugsnag/plugin-inline-script-content", "version": "8.1.1", - "main": "inline-script-content.js", + "main": "dist/inline-script-content.js", + "types": "dist/types/inline-script-content.d.ts", + "exports": { + ".": { + "types": "./dist/types/inline-script-content.d.ts", + "default": "./dist/inline-script-content.js", + "import": "./dist/inline-script-content.mjs" + } + }, "description": "@bugsnag/js plugin to attach inline script content to error events", "homepage": "https://www.bugsnag.com/", "repository": { @@ -14,7 +22,11 @@ "files": [ "*.js" ], - "scripts": {}, + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" + }, "author": "Bugsnag", "license": "MIT", "devDependencies": { diff --git a/packages/plugin-inline-script-content/rollup.config.npm.mjs b/packages/plugin-inline-script-content/rollup.config.npm.mjs new file mode 100644 index 0000000000..a472a8a594 --- /dev/null +++ b/packages/plugin-inline-script-content/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from '../../.rollup/index.mjs' + +export default createRollupConfig({ + input: 'src/inline-script-content.ts', + external: ['@bugsnag/core/lib/es-utils/map', '@bugsnag/core/lib/es-utils/reduce', '@bugsnag/core/lib/es-utils/filter'] +}) diff --git a/packages/plugin-inline-script-content/inline-script-content.js b/packages/plugin-inline-script-content/src/inline-script-content.ts similarity index 72% rename from packages/plugin-inline-script-content/inline-script-content.js rename to packages/plugin-inline-script-content/src/inline-script-content.ts index 26749c6a2f..826effeec8 100644 --- a/packages/plugin-inline-script-content/inline-script-content.js +++ b/packages/plugin-inline-script-content/src/inline-script-content.ts @@ -1,13 +1,33 @@ -const map = require('@bugsnag/core/lib/es-utils/map') -const reduce = require('@bugsnag/core/lib/es-utils/reduce') -const filter = require('@bugsnag/core/lib/es-utils/filter') +import map from '@bugsnag/core/lib/es-utils/map' +import reduce from '@bugsnag/core/lib/es-utils/reduce' +import filter from '@bugsnag/core/lib/es-utils/filter' +import type { Config, Plugin } from '@bugsnag/core/types' +import type ClientWithInternals from 'packages/core/client' const MAX_LINE_LENGTH = 200 const MAX_SCRIPT_LENGTH = 500000 -module.exports = (doc = document, win = window) => ({ +interface ExtendedConfig extends Config { + trackInlineScripts: boolean +} + +interface ExtendedDocument extends Document { + attachEvent?: unknown +} + +interface ValidationOption { + validate: (value: unknown) => boolean + defaultValue: () => unknown + message: string +} + +interface ExtendedPlugin extends Plugin { + configSchema: Record +} + +export default (doc: ExtendedDocument = document, win = window): ExtendedPlugin => ({ load: (client) => { - if (!client._config.trackInlineScripts) return + if (!(client as ClientWithInternals)._config.trackInlineScripts) return const originalLocation = win.location.href let html = '' @@ -28,15 +48,15 @@ module.exports = (doc = document, win = window) => ({ html = getHtml() DOMContentLoaded = true } - try { prev.apply(this, arguments) } catch (e) {} + try { prev && prev.apply(this, arguments as unknown as Parameters) } catch (e) {} } - let _lastScript = null - const updateLastScript = script => { + let _lastScript: HTMLOrSVGScriptElement | null = null + const updateLastScript = (script: HTMLOrSVGScriptElement | null) => { _lastScript = script } - const getCurrentScript = () => { + const getCurrentScript = (): HTMLOrSVGScriptElement | null => { let script = doc.currentScript || _lastScript if (!script && !DOMContentLoaded) { const scripts = doc.scripts || doc.getElementsByTagName('script') @@ -45,7 +65,7 @@ module.exports = (doc = document, win = window) => ({ return script } - const addSurroundingCode = lineNumber => { + const addSurroundingCode = (lineNumber: number) => { // get whatever html has rendered at this point if (!DOMContentLoaded || !html) html = getHtml() // simulate the raw html @@ -62,12 +82,12 @@ module.exports = (doc = document, win = window) => ({ client.addOnError(event => { // remove any of our own frames that may be part the stack this // happens before the inline script check as it happens for all errors - event.errors[0].stacktrace = filter(event.errors[0].stacktrace, f => !(/__trace__$/.test(f.method))) + event.errors[0].stacktrace = filter(event.errors[0].stacktrace, f => !(/__trace__$/.test(String(f.method)))) const frame = event.errors[0].stacktrace[0] // remove hash and query string from url - const cleanUrl = (url) => url.replace(/#.*$/, '').replace(/\?.*$/, '') + const cleanUrl = (url: string) => url.replace(/#.*$/, '').replace(/\?.*$/, '') // if frame.file exists and is not the original location of the page, this can't be an inline script if (frame && frame.file && cleanUrl(frame.file) !== cleanUrl(originalLocation)) return @@ -87,6 +107,7 @@ module.exports = (doc = document, win = window) => ({ frame.code = addSurroundingCode(frame.lineNumber) } } + // @ts-expect-error second argument is private API }, true) // Proxy all the timer functions whose callback is their 0th argument. @@ -105,6 +126,10 @@ module.exports = (doc = document, win = window) => ({ ) ) + type ValidWindowProperties = 'EventTarget' | 'Window' | 'Node' | 'ChannelMergerNode' | 'EventSource' | 'FileReader' | 'HTMLUnknownElement' | 'IDBDatabase' | 'IDBRequest' | 'IDBTransaction' | 'MessagePort' | 'Notification' | 'Screen' | 'TextTrack' | 'TextTrackCue' | 'TextTrackList' | 'WebSocket' | 'Worker' | 'XMLHttpRequest' | 'XMLHttpRequestEventTarget' | 'XMLHttpRequestUpload' + + type WindowProperties = keyof Pick + // Proxy all the host objects whose prototypes have an addEventListener function map([ 'EventTarget', 'Window', 'Node', 'ApplicationCache', 'AudioTrackList', 'ChannelMergerNode', @@ -112,7 +137,7 @@ module.exports = (doc = document, win = window) => ({ 'IDBRequest', 'IDBTransaction', 'KeyOperation', 'MediaController', 'MessagePort', 'ModalWindow', 'Notification', 'SVGElementInstance', 'Screen', 'TextTrack', 'TextTrackCue', 'TextTrackList', 'WebSocket', 'WebSocketWorker', 'Worker', 'XMLHttpRequest', 'XMLHttpRequestEventTarget', 'XMLHttpRequestUpload' - ], o => { + ] as WindowProperties[], o => { if (!win[o] || !win[o].prototype || !Object.prototype.hasOwnProperty.call(win[o].prototype, 'addEventListener')) return __proxy(win[o].prototype, 'addEventListener', original => __traceOriginalScript(original, eventTargetCallbackAccessor) @@ -122,7 +147,7 @@ module.exports = (doc = document, win = window) => ({ ) }) - function __traceOriginalScript (fn, callbackAccessor, alsoCallOriginal = false) { + function __traceOriginalScript (fn: Function, callbackAccessor: EventTargetCallbackAccessor, alsoCallOriginal = false) { return function () { // this is required for removeEventListener to remove anything added with // addEventListener before the functions started being wrapped by Bugsnag @@ -130,8 +155,8 @@ module.exports = (doc = document, win = window) => ({ try { const cba = callbackAccessor(args) const cb = cba.get() - if (alsoCallOriginal) fn.apply(this, args) - if (typeof cb !== 'function') return fn.apply(this, args) + if (alsoCallOriginal) fn.apply(fn, args) + if (typeof cb !== 'function') return fn.apply(fn, args) if (cb.__trace__) { cba.replace(cb.__trace__) } else { @@ -159,7 +184,7 @@ module.exports = (doc = document, win = window) => ({ // WebDriverException: Message: Permission denied to access property "handleEvent" } // IE8 doesn't let you call .apply() on setTimeout/setInterval - if (fn.apply) return fn.apply(this, args) + if (fn.apply) return fn.apply(fn, args) switch (args.length) { case 1: return fn(args[0]) case 2: return fn(args[0], args[1]) @@ -177,7 +202,7 @@ module.exports = (doc = document, win = window) => ({ } }) -function __proxy (host, name, replacer) { +function __proxy (host: any, name: string, replacer: (original: Function) => Function) { const original = host[name] if (!original) return original const replacement = replacer(original) @@ -185,13 +210,19 @@ function __proxy (host, name, replacer) { return original } -function eventTargetCallbackAccessor (args) { +type NestedFunction = Function & { __trace__?: NestedFunction } + +type Argument = NestedFunction & { + handleEvent?: NestedFunction +} + +function eventTargetCallbackAccessor (args: Argument[]) { const isEventHandlerObj = !!args[1] && typeof args[1].handleEvent === 'function' return { get: function () { return isEventHandlerObj ? args[1].handleEvent : args[1] }, - replace: function (fn) { + replace: function (fn: Function) { if (isEventHandlerObj) { args[1].handleEvent = fn } else { @@ -200,3 +231,5 @@ function eventTargetCallbackAccessor (args) { } } } + +type EventTargetCallbackAccessor = typeof eventTargetCallbackAccessor diff --git a/packages/plugin-inline-script-content/test/inline-script-content.test.ts b/packages/plugin-inline-script-content/test/inline-script-content.test.ts index afaff413de..b8b2b12e52 100644 --- a/packages/plugin-inline-script-content/test/inline-script-content.test.ts +++ b/packages/plugin-inline-script-content/test/inline-script-content.test.ts @@ -1,4 +1,4 @@ -import plugin from '../inline-script-content' +import plugin from '../' import Client from '@bugsnag/core/client' import Event from '@bugsnag/core/event' diff --git a/packages/plugin-inline-script-content/tsconfig.json b/packages/plugin-inline-script-content/tsconfig.json new file mode 100644 index 0000000000..09758ceb4c --- /dev/null +++ b/packages/plugin-inline-script-content/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"] +} + \ No newline at end of file diff --git a/packages/plugin-interaction-breadcrumbs/package.json b/packages/plugin-interaction-breadcrumbs/package.json index 3d11035d01..1549f67872 100644 --- a/packages/plugin-interaction-breadcrumbs/package.json +++ b/packages/plugin-interaction-breadcrumbs/package.json @@ -1,7 +1,15 @@ { "name": "@bugsnag/plugin-interaction-breadcrumbs", "version": "8.1.1", - "main": "interaction-breadcrumbs.js", + "main": "dist/interaction-breadcrumbs.js", + "types": "dist/types/interaction-breadcrumbs.d.ts", + "exports": { + ".": { + "types": "./dist/types/interaction-breadcrumbs.d.ts", + "default": "./dist/interaction-breadcrumbs.js", + "import": "./dist/interaction-breadcrumbs.mjs" + } + }, "description": "@bugsnag/js plugin to record UI click events as breadcrumbs", "homepage": "https://www.bugsnag.com/", "repository": { @@ -14,7 +22,11 @@ "files": [ "*.js" ], - "scripts": {}, + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" + }, "author": "Bugsnag", "license": "MIT", "devDependencies": { diff --git a/packages/plugin-interaction-breadcrumbs/rollup.config.npm.mjs b/packages/plugin-interaction-breadcrumbs/rollup.config.npm.mjs new file mode 100644 index 0000000000..5683a04647 --- /dev/null +++ b/packages/plugin-interaction-breadcrumbs/rollup.config.npm.mjs @@ -0,0 +1,5 @@ +import createRollupConfig from '../../.rollup/index.mjs' + +export default createRollupConfig({ + input: 'src/interaction-breadcrumbs.ts' +}) diff --git a/packages/plugin-interaction-breadcrumbs/interaction-breadcrumbs.js b/packages/plugin-interaction-breadcrumbs/src/interaction-breadcrumbs.ts similarity index 78% rename from packages/plugin-interaction-breadcrumbs/interaction-breadcrumbs.js rename to packages/plugin-interaction-breadcrumbs/src/interaction-breadcrumbs.ts index 86c7b03086..d910565175 100644 --- a/packages/plugin-interaction-breadcrumbs/interaction-breadcrumbs.js +++ b/packages/plugin-interaction-breadcrumbs/src/interaction-breadcrumbs.ts @@ -1,10 +1,13 @@ +import { Plugin } from '@bugsnag/core' +import type ClientWithInternals from 'packages/core/client' + /* * Leaves breadcrumbs when the user interacts with the DOM */ -module.exports = (win = window) => ({ +export default (win = window): Plugin => ({ load: (client) => { if (!('addEventListener' in win)) return - if (!client._isBreadcrumbTypeEnabled('user')) return + if (!(client as ClientWithInternals)._isBreadcrumbTypeEnabled('user')) return win.addEventListener('click', (event) => { let targetText, targetSelector @@ -13,8 +16,8 @@ module.exports = (win = window) => ({ targetSelector = getNodeSelector(event.target, win) } catch (e) { targetText = '[hidden]' - targetSelector = '[hidden]' - client._logger.error('Cross domain error when tracking click event. See docs: https://tinyurl.com/yy3rn63z') + targetSelector = '[hidden]'; + (client as ClientWithInternals)._logger.error('Cross domain error when tracking click event. See docs: https://tinyurl.com/yy3rn63z') } client.leaveBreadcrumb('UI click', { targetText, targetSelector }, 'user') }, true) @@ -23,7 +26,8 @@ module.exports = (win = window) => ({ const trim = /^\s*([^\s][\s\S]{0,139}[^\s])?\s*/ -function getNodeText (el) { +// TODO: Fix Type +function getNodeText (el: any) { let text = el.textContent || el.innerText || '' if (!text && (el.type === 'submit' || el.type === 'button')) { @@ -40,7 +44,8 @@ function getNodeText (el) { } // Create a label from tagname, id and css class of the element -function getNodeSelector (el, win) { +// TODO: Fix Type +function getNodeSelector (el: any, win: Window): string { const parts = [el.tagName] if (el.id) parts.push('#' + el.id) if (el.className && el.className.length) parts.push(`.${el.className.split(' ').join('.')}`) diff --git a/packages/plugin-interaction-breadcrumbs/test/interaction-breadcrumbs.test.ts b/packages/plugin-interaction-breadcrumbs/test/interaction-breadcrumbs.test.ts index e2c198dec5..8ce1f00285 100644 --- a/packages/plugin-interaction-breadcrumbs/test/interaction-breadcrumbs.test.ts +++ b/packages/plugin-interaction-breadcrumbs/test/interaction-breadcrumbs.test.ts @@ -1,4 +1,4 @@ -import plugin from '../' +import plugin from '../src/interaction-breadcrumbs' import Client from '@bugsnag/core/client' import Breadcrumb from '@bugsnag/core/breadcrumb' diff --git a/packages/plugin-interaction-breadcrumbs/tsconfig.json b/packages/plugin-interaction-breadcrumbs/tsconfig.json new file mode 100644 index 0000000000..09758ceb4c --- /dev/null +++ b/packages/plugin-interaction-breadcrumbs/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"] +} + \ No newline at end of file diff --git a/packages/plugin-navigation-breadcrumbs/package.json b/packages/plugin-navigation-breadcrumbs/package.json index 29d32cf7e9..2561c83511 100644 --- a/packages/plugin-navigation-breadcrumbs/package.json +++ b/packages/plugin-navigation-breadcrumbs/package.json @@ -1,7 +1,15 @@ { "name": "@bugsnag/plugin-navigation-breadcrumbs", "version": "8.1.1", - "main": "navigation-breadcrumbs.js", + "main": "dist/navigation-breadcrumbs.js", + "types": "dist/types/navigation-breadcrumbs.d.ts", + "exports": { + ".": { + "types": "./dist/types/navigation-breadcrumbs.d.ts", + "default": "./dist/navigation-breadcrumbs.js", + "import": "./dist/navigation-breadcrumbs.mjs" + } + }, "description": "@bugsnag/js plugin to record browser navigation as breadcrumbs", "homepage": "https://www.bugsnag.com/", "repository": { @@ -14,6 +22,11 @@ "files": [ "*.js" ], + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" + }, "author": "Bugsnag", "license": "MIT", "devDependencies": { diff --git a/packages/plugin-navigation-breadcrumbs/rollup.config.npm.mjs b/packages/plugin-navigation-breadcrumbs/rollup.config.npm.mjs new file mode 100644 index 0000000000..e74dd35c57 --- /dev/null +++ b/packages/plugin-navigation-breadcrumbs/rollup.config.npm.mjs @@ -0,0 +1,5 @@ +import createRollupConfig from '../../.rollup/index.mjs' + +export default createRollupConfig({ + input: 'src/navigation-breadcrumbs.ts' +}) diff --git a/packages/plugin-navigation-breadcrumbs/navigation-breadcrumbs.js b/packages/plugin-navigation-breadcrumbs/src/navigation-breadcrumbs.ts similarity index 59% rename from packages/plugin-navigation-breadcrumbs/navigation-breadcrumbs.js rename to packages/plugin-navigation-breadcrumbs/src/navigation-breadcrumbs.ts index 873215dd83..10559b8053 100644 --- a/packages/plugin-navigation-breadcrumbs/navigation-breadcrumbs.js +++ b/packages/plugin-navigation-breadcrumbs/src/navigation-breadcrumbs.ts @@ -1,14 +1,29 @@ +import { Client, Plugin } from 'packages/core/types' + +interface PluginClient extends Client { + _isBreadcrumbTypeEnabled: (type: string) => boolean +} + +type ExtendedHistory = History & { + replaceState: History['replaceState'] & { _restore?: () => void } + pushState: History['pushState'] & { _restore?: () => void } +} + +type ExtendedWindow = Window & { + history: ExtendedHistory +} + /* * Leaves breadcrumbs when navigation methods are called or events are emitted */ -module.exports = (win = window) => { - const plugin = { +export default (win = window): Plugin => { + const plugin: Plugin = { load: (client) => { if (!('addEventListener' in win)) return - if (!client._isBreadcrumbTypeEnabled('navigation')) return + if (!(client as PluginClient)._isBreadcrumbTypeEnabled('navigation')) return // returns a function that will drop a breadcrumb with a given name - const drop = name => () => client.leaveBreadcrumb(name, {}, 'navigation') + const drop = (name: string) => () => client.leaveBreadcrumb(name, {}, 'navigation') // simple drops – just names, no meta win.addEventListener('pagehide', drop('Page hidden'), true) @@ -27,15 +42,15 @@ module.exports = (win = window) => { }, true) // the only way to know about replaceState/pushState is to wrap them… >_< - if (win.history.pushState) wrapHistoryFn(client, win.history, 'pushState', win, true) - if (win.history.replaceState) wrapHistoryFn(client, win.history, 'replaceState', win) + if (typeof win.history.pushState === 'function') wrapHistoryFn(client, win.history, 'pushState', win, true) + if (typeof win.history.replaceState === 'function') wrapHistoryFn(client, win.history, 'replaceState', win) } } if (process.env.NODE_ENV !== 'production') { - plugin.destroy = (win = window) => { - win.history.replaceState._restore() - win.history.pushState._restore() + plugin.destroy = (win: ExtendedWindow = window) => { + if (win.history.replaceState._restore) win.history.replaceState._restore() + if (win.history.pushState._restore) win.history.pushState._restore() } } @@ -43,26 +58,27 @@ module.exports = (win = window) => { } if (process.env.NODE_ENV !== 'production') { - exports.destroy = (win = window) => { - win.history.replaceState._restore() - win.history.pushState._restore() + exports.destroy = (win: ExtendedWindow = window) => { + if (win.history.replaceState._restore) win.history.replaceState._restore() + if (win.history.pushState._restore) win.history.pushState._restore() } } // takes a full url like http://foo.com:1234/pages/01.html?yes=no#section-2 and returns // just the path and hash parts, e.g. /pages/01.html?yes=no#section-2 -const relativeLocation = (url, win) => { - const a = win.document.createElement('A') +const relativeLocation = (url: string, win: Window) => { + const a = win.document.createElement('a') a.href = url return `${a.pathname}${a.search}${a.hash}` } -const stateChangeToMetadata = (win, state, title, url) => { +const stateChangeToMetadata = (win: Window, state: string, title: string, url?: string | URL | null) => { const currentPath = relativeLocation(win.location.href, win) return { title, state, prevState: getCurrentState(win), to: url || currentPath, from: currentPath } } -const wrapHistoryFn = (client, target, fn, win, resetEventCount = false) => { +type HistoryMethods = 'pushState' | 'replaceState' +const wrapHistoryFn = (client: Client, target: ExtendedHistory, fn: HistoryMethods, win: Window, resetEventCount = false) => { const orig = target[fn] target[fn] = (state, title, url) => { client.leaveBreadcrumb(`History ${fn}`, stateChangeToMetadata(win, state, title, url), 'navigation') @@ -70,14 +86,14 @@ const wrapHistoryFn = (client, target, fn, win, resetEventCount = false) => { if (resetEventCount && typeof client.resetEventCount === 'function') client.resetEventCount() // Internet Explorer will convert `undefined` to a string when passed, causing an unintended redirect // to '/undefined'. therefore we only pass the url if it's not undefined. - orig.apply(target, [state, title].concat(url !== undefined ? url : [])) + orig.apply(target, [state, title].concat(url !== undefined ? url : []) as Parameters) } if (process.env.NODE_ENV !== 'production') { target[fn]._restore = () => { target[fn] = orig } } } -const getCurrentState = (win) => { +const getCurrentState = (win: Window) => { try { return win.history.state } catch (e) {} diff --git a/packages/plugin-navigation-breadcrumbs/test/navigation-breadcrumbs.test.ts b/packages/plugin-navigation-breadcrumbs/test/navigation-breadcrumbs.test.ts index 1b05070d53..0d2c34b9de 100644 --- a/packages/plugin-navigation-breadcrumbs/test/navigation-breadcrumbs.test.ts +++ b/packages/plugin-navigation-breadcrumbs/test/navigation-breadcrumbs.test.ts @@ -1,4 +1,4 @@ -import plugin from '../navigation-breadcrumbs' +import plugin from '../src/navigation-breadcrumbs' import Client from '@bugsnag/core/client' diff --git a/packages/plugin-navigation-breadcrumbs/tsconfig.json b/packages/plugin-navigation-breadcrumbs/tsconfig.json new file mode 100644 index 0000000000..a4cc3029ff --- /dev/null +++ b/packages/plugin-navigation-breadcrumbs/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "types": ["node"] + } +} + \ No newline at end of file diff --git a/packages/plugin-simple-throttle/rollup.config.npm.mjs b/packages/plugin-simple-throttle/rollup.config.npm.mjs index 17c25ef1a6..bb2a0d724e 100644 --- a/packages/plugin-simple-throttle/rollup.config.npm.mjs +++ b/packages/plugin-simple-throttle/rollup.config.npm.mjs @@ -1,5 +1,6 @@ -import createRollupConfig from "../../.rollup/index.mjs"; +import createRollupConfig from '../../.rollup/index.mjs' export default createRollupConfig({ - input: "src/throttle.ts" -}); + input: 'src/throttle.ts', + external: ['@bugsnag/core/lib/validators/int-range'] +}) diff --git a/packages/plugin-simple-throttle/src/throttle.ts b/packages/plugin-simple-throttle/src/throttle.ts index b2aa3e3b81..234f9102b9 100644 --- a/packages/plugin-simple-throttle/src/throttle.ts +++ b/packages/plugin-simple-throttle/src/throttle.ts @@ -1,10 +1,26 @@ -import { Plugin } from '@bugsnag/core' import intRange from '@bugsnag/core/lib/validators/int-range' +import { Client, Config, Logger, Plugin } from '@bugsnag/core' + +interface ThrottlePlugin extends Plugin { + configSchema: { + [key: string]: { + defaultValue: () => unknown + message: string + validate: (value: unknown) => boolean + } + } +} + +interface InternalClient extends Client { + _config: Config & { maxEvents: number } + _logger: Logger +} + /* * Throttles and dedupes events */ -const plugin: Plugin = { +const plugin: ThrottlePlugin = { load: (client) => { // track sent events for each init of the plugin let n = 0 @@ -12,10 +28,8 @@ const plugin: Plugin = { // add onError hook client.addOnError((event) => { // have max events been sent already? - // @ts-expect-error _config is private API - if (n >= client._config.maxEvents) { - // @ts-expect-error _config is private API - client._logger.warn(`Cancelling event send due to maxEvents per session limit of ${client._config.maxEvents} being reached`) + if (n >= (client as InternalClient)._config.maxEvents) { + (client as InternalClient)._logger.warn(`Cancelling event send due to maxEvents per session limit of ${(client as InternalClient)._config.maxEvents} being reached`) return false } n++ @@ -23,7 +37,6 @@ const plugin: Plugin = { client.resetEventCount = () => { n = 0 } }, - // @ts-expect-error _config is private API configSchema: { maxEvents: { defaultValue: () => 10,