diff --git a/.buildkite/basic/browser-pipeline.yml b/.buildkite/basic/browser-pipeline.yml index 79b596dfd9..4082564460 100644 --- a/.buildkite/basic/browser-pipeline.yml +++ b/.buildkite/basic/browser-pipeline.yml @@ -83,7 +83,8 @@ steps: - safari_10 - ios_15 - android_8 - - chrome_43 + # TODO: Move these to BitBar + # - chrome_43 - chrome_72 - firefox_78 depends_on: "browser-maze-runner-bs" @@ -144,30 +145,31 @@ steps: concurrency_group: "bitbar" concurrency_method: eager - - label: ":bitbar: ie_11 Browser tests" - depends_on: "browser-maze-runner-bb" - timeout_in_minutes: 30 - plugins: - docker-compose#v4.12.0: - pull: browser-maze-runner-bb - run: browser-maze-runner-bb - service-ports: true - use-aliases: true - command: - - "--farm=bb" - - "--browser=ie_11" - - "--no-tunnel" - - "--aws-public-ip" - artifacts#v1.5.0: - upload: - - "./test/browser/maze_output/failed/**/*" - test-collector#v1.10.2: - files: "reports/TEST-*.xml" - format: "junit" - branch: "^main|next$$" - api-token-env-name: "BROWSER_BUILDKITE_ANALYTICS_TOKEN" - concurrency: 25 - concurrency_group: "bitbar" - concurrency_method: eager - env: - HOST: "localhost" # IE11 needs the host set to localhost for some reason + # Uncomment the following block to enable IE11 tests on BitBar + # - label: ":bitbar: ie_11 Browser tests" + # depends_on: "browser-maze-runner-bb" + # timeout_in_minutes: 30 + # plugins: + # docker-compose#v4.12.0: + # pull: browser-maze-runner-bb + # run: browser-maze-runner-bb + # service-ports: true + # use-aliases: true + # command: + # - "--farm=bb" + # - "--browser=ie_11" + # - "--no-tunnel" + # - "--aws-public-ip" + # artifacts#v1.5.0: + # upload: + # - "./test/browser/maze_output/failed/**/*" + # test-collector#v1.10.2: + # files: "reports/TEST-*.xml" + # format: "junit" + # branch: "^main|next$$" + # api-token-env-name: "BROWSER_BUILDKITE_ANALYTICS_TOKEN" + # concurrency: 25 + # concurrency_group: "bitbar" + # concurrency_method: eager + # env: + # HOST: "localhost" # IE11 needs the host set to localhost for some reason diff --git a/.buildkite/basic/electron-pipeline.yml b/.buildkite/basic/electron-pipeline.yml index 7881c2685d..303f69a156 100644 --- a/.buildkite/basic/electron-pipeline.yml +++ b/.buildkite/basic/electron-pipeline.yml @@ -26,7 +26,7 @@ steps: - echo "Running on Node `node -v`" - npm install electron@${ELECTRON_VERSION} --no-audit --progress=false --no-save - npm ci --no-audit --progress=false - - npm run build:electron + - npm run build - defaults write com.apple.CrashReporter DialogType none - npm run test:unit:electron-runner - npm run test:electron diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index ccaeb1a00f..0000000000 --- a/.eslintignore +++ /dev/null @@ -1,12 +0,0 @@ -dist -examples -**/node_modules/** -scratch -coverage -packages/core/types/test/*.js -packages/react-native/android/build/reports -packages/**/features -packages/**/fixtures -test/browser -test/node -test/react-native-cli/features/fixtures diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 5d56a803b8..0000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,94 +0,0 @@ -const ruleOverrides = { - // Disable preferring Promise-based async tests - 'jest/no-test-callback': 'off', - - // Let TypeScript inference work without being verbose - '@typescript-eslint/explicit-function-return-type': 'off', - - // (Explicit) any has its valid use cases - '@typescript-eslint/no-explicit-any': 'off', - - // We use noop functions liberally (() => {}) - '@typescript-eslint/no-empty-function': 'off', - - // This incorrectly fails on TypeScript method override signatures - 'no-dupe-class-members': 'off', - - // Disable all rules that require parserServices (for now) - '@typescript-eslint/no-floating-promises': 'off', - '@typescript-eslint/no-misused-promises': 'off', - '@typescript-eslint/no-unnecessary-type-assertion': 'off', - '@typescript-eslint/prefer-nullish-coalescing': 'off', - '@typescript-eslint/prefer-readonly': 'off', - '@typescript-eslint/promise-function-async': 'off', - '@typescript-eslint/require-array-sort-compare': 'off', - '@typescript-eslint/require-await': 'off', - '@typescript-eslint/restrict-plus-operands': 'off', - '@typescript-eslint/restrict-template-expressions': 'off', - '@typescript-eslint/strict-boolean-expressions': 'off', - '@typescript-eslint/no-throw-literal': 'off', - '@typescript-eslint/no-implied-eval': 'off', - '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off', - '@typescript-eslint/prefer-includes': 'off', - '@typescript-eslint/no-for-in-array': 'off', -} - -module.exports = { - plugins: [ - 'react' - ], - rules: { - 'react/jsx-uses-react': 'error', - 'react/jsx-uses-vars': 'error' - }, - parser: '@typescript-eslint/parser', - parserOptions: { - jsx: true, - ecmaVersion: 2018 - }, - overrides: [ - // linting for js files - { - files: ['**/*.js'], - extends: [ - 'standard' - ] - }, - { - files: ['**/*.test.js'], - extends: ['standard'], - plugins: ['eslint-plugin-jest'], - env: { jest: true } - }, - // linting for ts files - { - files: ['**/*.ts'], - extends: 'standard-with-typescript', - // We can't use rules which requires parserServices as there is no tsconfig that represents the whole monorepo (yet). - // 'parserOptions': { - // 'project': './tsconfig.json' - // }, - rules: { - ...ruleOverrides - } - }, - // Linting for tests - { - files: [ - '**/*.test.ts?(x)' - ], - env: { - jest: true, - browser: true, - }, - plugins: ['eslint-plugin-jest'], - extends: [ - 'standard-with-typescript', - 'plugin:jest/recommended' - ], - rules: { - ...ruleOverrides - } - } - ] -} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index dfcd880f68..75d3a1e95f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -14,10 +14,10 @@ permissions: read-all on: push: - branches: [ "next", integration/*, main ] + branches: [ "next", "integration/*", main ] pull_request: # The branches below must be a subset of the branches above - branches: [ "next", "main" ] + branches: [ "next", "integration/*", "main" ] schedule: - cron: '23 13 * * 5' diff --git a/.github/workflows/test-electron.yml b/.github/workflows/test-electron.yml index 4c61f1f51e..d019f04070 100644 --- a/.github/workflows/test-electron.yml +++ b/.github/workflows/test-electron.yml @@ -44,7 +44,7 @@ jobs: - run: npm ci --no-audit --progress=false env: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - - run: npm run build:electron + - run: npm run build shell: bash - run: sudo apt-get install cppcheck --assume-yes name: Install cppcheck diff --git a/.gitignore b/.gitignore index a8f0de51d9..66e57d32aa 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ Gemfile.lock # IDE files .idea/ *.iml +.ts38-validation diff --git a/.rollup/index.mjs b/.rollup/index.mjs new file mode 100644 index 0000000000..c698a2fb30 --- /dev/null +++ b/.rollup/index.mjs @@ -0,0 +1,68 @@ +import typescript from '@rollup/plugin-typescript' +import replace from '@rollup/plugin-replace' +import fs from 'fs' + +const defaultOptions = () => ({ + // additional variables to define with '@rollup/plugin-replace' + // e.g. '{ ABC: 123 }' is equivalent to running 'globalThis.ABC = 123' + additionalReplacements: {}, + // additional external dependencies, such as '@bugsnag/browser' + external: [], + // the entry point for the bundle + input: undefined, + // output directory for the bundle + output: undefined +}) + +export const sharedOutput = { + dir: 'dist', + generatedCode: { + preset: 'es2015', + } +} + +function createRollupConfig (options = defaultOptions()) { + const packageJson = JSON.parse(fs.readFileSync(`${process.cwd()}/package.json`)) + + return { + input: options.input || 'src/index.ts', + output: options.output || [ + { + ...sharedOutput, + entryFileNames: '[name].js', + format: 'cjs' + }, + { + ...sharedOutput, + preserveModules: true, + entryFileNames: '[name].mjs', + format: 'esm' + } + ], + external: options.external, + plugins: [ + replace({ + preventAssignment: true, + values: { + __VERSION__: packageJson.version, + ...options.additionalReplacements, + } + }), + typescript({ + removeComments: true, + // don't output anything if there's a TS error + noEmitOnError: true, + // turn on declaration files and declaration maps + compilerOptions: { + declaration: true, + declarationMap: true, + emitDeclarationOnly: true, + declarationDir: 'dist/types', + } + }), + ...(options.plugins ?? []) + ] + } +} + +export default createRollupConfig diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000000..488a9fee6c --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 18.20.5 diff --git a/TESTING.md b/TESTING.md index dcc6beecf6..8e242a1d87 100644 --- a/TESTING.md +++ b/TESTING.md @@ -39,7 +39,7 @@ npm run test:types ## Linting -Lints the entire repo with ESLint. On JavaScript files this uses the [standard](https://github.com/standard/eslint-config-standard) ruleset and on TypeScript files this uses the [@typescript/eslint](https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin) recommended set of rules. +Lints the entire repo with ESLint. ```sh npm run test:lint diff --git a/babel.config.js b/babel.config.js index 7f358ff799..bf03e31bea 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,15 +1,13 @@ module.exports = api => { - // NB: This function can be called without an api argument, e.g. by bin/bundle + // NB: This function can be called without an api argument const presets = [] const plugins = [] const overrides = [] if (api && api.env('test')) { + presets.push(['@babel/preset-env', {targets: {node: 'current'}}]) presets.push('@babel/preset-typescript') - plugins.push(['@babel/plugin-proposal-class-properties', { loose: true }]) - plugins.push('@babel/plugin-transform-modules-commonjs') - plugins.push('@babel/plugin-proposal-optional-chaining') overrides.push({ test: 'node_modules/react-native/**/*', presets: ['module:metro-react-native-babel-preset'] @@ -22,24 +20,24 @@ module.exports = api => { test: './packages/plugin-react-navigation/**/*', presets: ['@babel/preset-react', 'module:metro-react-native-babel-preset'] }) + } else { + plugins.push( + ['@babel/plugin-transform-arrow-functions'], + ['@babel/plugin-transform-block-scoping'], + ['@babel/plugin-transform-classes', { loose: true }], + ['@babel/plugin-transform-computed-properties', { loose: true }], + ['@babel/plugin-transform-destructuring', { loose: true }], + ['@babel/plugin-transform-member-expression-literals'], + ['@babel/plugin-transform-property-literals'], + ['@babel/plugin-transform-parameters', { loose: true }], + ['@babel/plugin-transform-shorthand-properties'], + ['@babel/plugin-transform-spread', { loose: true }], + ['@babel/plugin-transform-template-literals', { loose: true }], + ['@babel/plugin-proposal-object-rest-spread', { loose: true }], + ['@babel/syntax-object-rest-spread'] + ) } - plugins.push( - ['@babel/plugin-transform-arrow-functions'], - ['@babel/plugin-transform-block-scoping'], - ['@babel/plugin-transform-classes', { loose: true }], - ['@babel/plugin-transform-computed-properties', { loose: true }], - ['@babel/plugin-transform-destructuring', { loose: true }], - ['@babel/plugin-transform-member-expression-literals'], - ['@babel/plugin-transform-property-literals'], - ['@babel/plugin-transform-parameters', { loose: true }], - ['@babel/plugin-transform-shorthand-properties'], - ['@babel/plugin-transform-spread', { loose: true }], - ['@babel/plugin-transform-template-literals', { loose: true }], - ['@babel/plugin-proposal-object-rest-spread', { loose: true }], - ['@babel/syntax-object-rest-spread'] - ) - if (api && !api.env('test')) { api.cache(false) } diff --git a/bin/local-test-util b/bin/local-test-util index 9b2cfc91bc..7c35964a85 100755 --- a/bin/local-test-util +++ b/bin/local-test-util @@ -121,7 +121,7 @@ async function buildNotifiers (notifier) { async function packNotifiers (notifier) { const notifiers = notifier ? [ notifier ] - : [ 'js', 'browser', 'node', 'web-worker', 'plugin-angular', 'plugin-react', 'plugin-vue' ] + : [ 'core', 'js', 'browser', 'node', 'web-worker', 'plugin-angular', 'plugin-react', 'plugin-vue' ] for (const n of notifiers) { let packageLocation = `packages/${n}/` if (n === 'plugin-angular') packageLocation += 'dist/' @@ -138,9 +138,11 @@ async function installNotifiers (notifier) { `--no-save`, ].concat(notifier ? [ + `../../../../bugsnag-core-${require(`../packages/core/package.json`).version}.tgz`, `../../../../bugsnag-${notifier}-${require(`../packages/${notifier}/package.json`).version}.tgz` ] : [ + `../../../../bugsnag-core-${require('../packages/core/package.json').version}.tgz`, `../../../../bugsnag-browser-${require('../packages/browser/package.json').version}.tgz`, `../../../../bugsnag-web-worker-${require('../packages/web-worker/package.json').version}.tgz`, `../../../../bugsnag-plugin-react-${require('../packages/plugin-react/package.json').version}.tgz`, @@ -158,6 +160,7 @@ async function installNgNotifier (notifier, version = '12') { `install`, `--no-package-lock`, `--no-save`, + `../../../../../../bugsnag-core-${require('../packages/core/package.json').version}.tgz`, `../../../../../../bugsnag-browser-${require('../packages/browser/package.json').version}.tgz`, `../../../../../../bugsnag-js-${require('../packages/js/package.json').version}.tgz`, `../../../../../../bugsnag-node-${require('../packages/node/package.json').version}.tgz`, diff --git a/config/electron-jest.config.js b/config/electron-jest.config.js index 32c67a9cc7..99a4a1cb69 100644 --- a/config/electron-jest.config.js +++ b/config/electron-jest.config.js @@ -1,6 +1,7 @@ module.exports = { projects: [ { + resolver: '/jest/node-exports-resolver', setupFilesAfterEnv: ['/test/electron/setup.ts'], clearMocks: true, modulePathIgnorePatterns: ['.verdaccio', 'fixtures', 'examples'], @@ -9,6 +10,7 @@ module.exports = { testMatch: ['**/test/**/*.test-main.ts'] }, { + resolver: '/jest/node-exports-resolver', setupFilesAfterEnv: ['/test/electron/setup.ts'], clearMocks: true, modulePathIgnorePatterns: ['.verdaccio', 'fixtures', 'examples'], diff --git a/docker-compose.yml b/docker-compose.yml index 800cf12031..d063053664 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.6' - x-common-environment: &common-environment BUILDKITE: BUILDKITE_BRANCH: diff --git a/dockerfiles/Dockerfile.browser b/dockerfiles/Dockerfile.browser index 251d42028d..cf85642314 100644 --- a/dockerfiles/Dockerfile.browser +++ b/dockerfiles/Dockerfile.browser @@ -6,15 +6,17 @@ RUN apk add --update bash python3 make gcc g++ musl-dev xvfb-run curl WORKDIR /app COPY package*.json ./ -COPY babel.config.js lerna.json .eslintignore .eslintrc.js jest.config.js tsconfig.json ./ +COPY babel.config.js lerna.json eslint.config.mjs jest.config.js tsconfig.json ./ COPY jest ./jest ADD min_packages.tar . +COPY .rollup ./.rollup COPY bin ./bin COPY packages ./packages RUN npm ci RUN npm run build +RUN npm pack --verbose packages/core/ RUN npm pack --verbose packages/js/ RUN npm pack --verbose packages/browser/ RUN npm pack --verbose packages/node/ @@ -26,13 +28,15 @@ RUN npm pack --verbose packages/web-worker/ COPY test/browser/features test/browser/features WORKDIR /app/test/browser/features/fixtures RUN npm install --no-package-lock --no-save \ -../../../../bugsnag-browser-*.tgz \ -../../../../bugsnag-plugin-react-*.tgz \ -../../../../bugsnag-plugin-vue-*.tgz \ -../../../../bugsnag-web-worker-*.tgz + ../../../../bugsnag-core-*.tgz \ + ../../../../bugsnag-browser-*.tgz \ + ../../../../bugsnag-plugin-react-*.tgz \ + ../../../../bugsnag-plugin-vue-*.tgz \ + ../../../../bugsnag-web-worker-*.tgz WORKDIR /app/test/browser/features/fixtures/plugin_angular/angular_12 RUN npm install --no-package-lock --no-save \ + ../../../../../../bugsnag-core-*.tgz \ ../../../../../../bugsnag-plugin-angular-*.tgz \ ../../../../../../bugsnag-node-*.tgz \ ../../../../../../bugsnag-browser-*.tgz \ @@ -40,11 +44,16 @@ RUN npm install --no-package-lock --no-save \ WORKDIR /app/test/browser/features/fixtures/plugin_angular/angular_17 RUN npm install --no-package-lock --no-save \ + ../../../../../../bugsnag-core-*.tgz \ ../../../../../../bugsnag-plugin-angular-*.tgz \ ../../../../../../bugsnag-node-*.tgz \ ../../../../../../bugsnag-browser-*.tgz \ ../../../../../../bugsnag-js-*.tgz +WORKDIR /app/test/browser/features/fixtures/typescript/3_8 +RUN npm install --no-package-lock --no-save \ + ../../../../../../bugsnag-browser-*.tgz + # install the dependencies and build each fixture WORKDIR /app/test/browser/features/fixtures RUN find . -name package.json -type f -mindepth 2 -maxdepth 3 ! -path "./node_modules/*" | \ diff --git a/dockerfiles/Dockerfile.ci b/dockerfiles/Dockerfile.ci index a141b16590..2b87a49c7a 100644 --- a/dockerfiles/Dockerfile.ci +++ b/dockerfiles/Dockerfile.ci @@ -6,10 +6,12 @@ RUN apk add --update bash python3 make gcc g++ musl-dev xvfb-run curl WORKDIR /app COPY package*.json ./ -COPY babel.config.js lerna.json .eslintignore .eslintrc.js jest.config.js tsconfig.json ./ +COPY babel.config.js lerna.json eslint.config.mjs jest.config.js tsconfig.json ./ COPY jest ./jest ADD min_packages.tar . +COPY .rollup ./.rollup COPY bin ./bin +COPY eslint-rules ./eslint-rules COPY scripts ./scripts COPY test ./test COPY packages ./packages diff --git a/dockerfiles/Dockerfile.node b/dockerfiles/Dockerfile.node index 09275fa94b..fefd5d3f34 100644 --- a/dockerfiles/Dockerfile.node +++ b/dockerfiles/Dockerfile.node @@ -1,30 +1,33 @@ # CI test image for unit/lint/type tests -FROM node:18-alpine@sha256:974afb6cbc0314dc6502b14243b8a39fbb2d04d975e9059dd066be3e274fbb25 as node-feature-builder +FROM node:18-alpine@sha256:974afb6cbc0314dc6502b14243b8a39fbb2d04d975e9059dd066be3e274fbb25 AS node-feature-builder RUN apk add --update bash python3 make gcc g++ musl-dev xvfb-run curl WORKDIR /app COPY package*.json ./ -COPY babel.config.js lerna.json .eslintignore .eslintrc.js jest.config.js tsconfig.json ./ +COPY babel.config.js lerna.json eslint.config.mjs jest.config.js tsconfig.json ./ COPY jest ./jest ADD min_packages.tar . +COPY .rollup ./.rollup COPY bin ./bin COPY packages ./packages RUN npm ci + RUN npm run build +RUN npm pack --verbose packages/core/ RUN npm pack --verbose packages/node/ +RUN npm pack --verbose packages/path-normalizer/ RUN npm pack --verbose packages/plugin-express/ RUN npm pack --verbose packages/plugin-koa/ RUN npm pack --verbose packages/plugin-restify/ RUN npm pack --verbose packages/plugin-hono/ # The maze-runner node tests -FROM 855461928731.dkr.ecr.us-west-1.amazonaws.com/maze-runner-releases:latest-v9-cli as node-maze-runner +FROM 855461928731.dkr.ecr.us-west-1.amazonaws.com/maze-runner-releases:latest-v9-cli AS node-maze-runner WORKDIR /app/ -COPY packages/node/ . COPY test/node/features test/node/features COPY --from=node-feature-builder /app/*.tgz ./ RUN for d in test/node/features/fixtures/*/; do cp /app/*.tgz "$d"; done diff --git a/eslint-rules/index.js b/eslint-rules/index.js new file mode 100644 index 0000000000..557d43a81f --- /dev/null +++ b/eslint-rules/index.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + 'no-inline-type-exports': require('./no-inline-type-exports'), + }, +}; diff --git a/eslint-rules/no-inline-type-exports.js b/eslint-rules/no-inline-type-exports.js new file mode 100644 index 0000000000..bd20a42786 --- /dev/null +++ b/eslint-rules/no-inline-type-exports.js @@ -0,0 +1,97 @@ +/** + * Custom ESLint rule to prevent inline type exports like `export { type Bugsnag }` + * and enforce top-level type exports like `export type { Bugsnag }` + */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'Prevent inline type exports and enforce top-level type exports', + category: 'Stylistic Issues', + recommended: false, + }, + fixable: 'code', + schema: [], + messages: { + noInlineTypeExport: 'Use top-level type exports instead of inline type specifiers. Change `export { type {{name}} }` to `export type { {{name}} }`.', + }, + }, + + create(context) { + return { + ExportNamedDeclaration(node) { + // Check if this is an export with specifiers (export { ... }) + if (node.specifiers && node.specifiers.length > 0) { + const typeSpecifiers = []; + const valueSpecifiers = []; + + // Separate type and value specifiers + node.specifiers.forEach(specifier => { + if (specifier.exportKind === 'type') { + typeSpecifiers.push(specifier); + } else { + valueSpecifiers.push(specifier); + } + }); + + // If we have inline type specifiers, report them + if (typeSpecifiers.length > 0) { + typeSpecifiers.forEach(specifier => { + context.report({ + node: specifier, + messageId: 'noInlineTypeExport', + data: { + name: specifier.exported.name, + }, + fix(fixer) { + const sourceCode = context.getSourceCode(); + + // If this export only contains type specifiers, convert to export type + if (valueSpecifiers.length === 0) { + // Replace "export {" with "export type {" + const exportToken = sourceCode.getFirstToken(node); + + const fixes = [ + // Add "type" after "export" + fixer.insertTextAfter(exportToken, ' type'), + ]; + + // Remove "type" from each specifier + typeSpecifiers.forEach(spec => { + const typeToken = sourceCode.getFirstToken(spec); + if (typeToken.value === 'type') { + const nextToken = sourceCode.getTokenAfter(typeToken); + fixes.push(fixer.removeRange([typeToken.range[0], nextToken.range[0]])); + } + }); + + return fixes; + } else { + // Mixed exports: split into separate export statements + + // Create separate export type statement + const typeNames = typeSpecifiers.map(spec => spec.exported.name).join(', '); + const typeExport = `export type { ${typeNames} };`; + + // Create value export statement + const valueNames = valueSpecifiers.map(spec => { + if (spec.local.name === spec.exported.name) { + return spec.exported.name; + } else { + return `${spec.local.name} as ${spec.exported.name}`; + } + }).join(', '); + const valueExport = `export { ${valueNames} };`; + + // Replace the entire export with both statements + return fixer.replaceText(node, `${typeExport}\n${valueExport}`); + } + }, + }); + }); + } + } + }, + }; + }, +}; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000000..d42516322c --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,210 @@ +import eslint from '@eslint/js'; +import eslintPluginImport from 'eslint-plugin-import'; +import eslintPluginJest from 'eslint-plugin-jest'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); + +/** @type {import('@typescript-eslint/utils').TSESLint.FlatConfig} */ +const c = tseslint.config( + // Files to ignore + { + ignores: [ + '**/dist', + 'examples', + 'scratch', + 'coverage', + 'packages/core/types/test/*.js', + 'packages/react-native/android/build/reports', + 'packages/**/features', + 'packages/**/fixtures', + 'test/browser', + 'test/node', + 'test/react-native-cli/features/fixtures', + 'packages/react-native/ios/vendor' + ], + }, + eslint.configs.recommended, + { + rules: { + 'no-undef': 'warn', + 'no-unused-vars': 'warn', + 'no-empty': 'warn', + 'no-redeclare': 'warn', + 'no-func-assign': 'warn', + 'no-prototype-builtins': 'warn', + 'require-yield': 'warn', + }, + }, + { + plugins: { + '@typescript-eslint': tseslint.plugin, + import: eslintPluginImport, + 'custom-rules': require('./eslint-rules'), + }, + languageOptions: { + parser: tseslint.parser, + parserOptions: { + /** + * This config also targets files not included in tsconfig so this must be false + * and rules requiring type information cannot be used + */ + project: false, + }, + }, + }, + // Base linting config + { + files: ['**/*.json', '**/*.[tj]s?(x)', '**/*.cjs', '**/*.mjs'], + }, + // Node environment + { + files: ['jest/**/*.[js|mjs]', '**/*/babel.config.js', '**/*/rollup.config.mjs', '.rollup/index.mjs'], + languageOptions: { + globals: { + ...globals.node, + }, + }, + }, + // Linting config for TypeScript + { + files: [ + '**/*.ts?(x)' + ], + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: false, // Allows use of rules which require type information + }, + }, + plugins: { + '@typescript-eslint': tseslint.plugin, + }, + rules: { + ...tseslint.configs.recommended[1].rules, + ...tseslint.configs.recommended[2].rules, + ...tseslint.configs.strict[2].rules, + + // Let TypeScript inference work without being verbose + '@typescript-eslint/explicit-function-return-type': 'off', + + // (Explicit) any has its valid use cases + '@typescript-eslint/no-explicit-any': 'off', + + // We use noop functions liberally (() => {}) + '@typescript-eslint/no-empty-function': 'off', + + // This incorrectly fails on TypeScript method override signatures + 'no-dupe-class-members': 'off', + + /* + * TypeScript 3.8 Compatibility Rules + * + * TypeScript 3.8 was released in February 2020. The following rules ensure + * we don't use syntax or features introduced in later versions: + * + * Avoided features from TS 3.9+: + * - `// @ts-expect-error` comments (prefer `// @ts-ignore`) + * + * Avoided features from TS 4.0+: + * - Variadic tuple types: [...T, ...U] + * - Labeled tuple elements: [first: string, second: number] + * - catch clause variable type annotations: catch (e: Error) + * + * Avoided features from TS 4.1+: + * - Template literal types: `${string}-${number}` + * - Key remapping in mapped types: { [K in keyof T as `get${K}`]: T[K] } + * - Recursive conditional types + * + * Limited browser support features (available in TS 3.7-3.8 but avoided): + * - Optional chaining (?.) - limited IE support + * - Nullish coalescing (??) - limited IE support + */ + + // TypeScript 3.8 compatibility rules + // Optional chaining compiles to a lot more code and has limited browser support + '@typescript-eslint/prefer-optional-chain': 'off', + + // Prevent nullish coalescing (??) - introduced in TS 3.7 but limited browser support + '@typescript-eslint/prefer-nullish-coalescing': 'off', + + // Support TypeScript 3.8 by disallowing import { type Module } from 'module' + 'import/consistent-type-specifier-style': ['warn', 'prefer-top-level'], + + // Prevent inline type exports like export { type Bugsnag } + 'custom-rules/no-inline-type-exports': 'warn', + + // Warn about confusing non-null assertions for code clarity + '@typescript-eslint/no-confusing-non-null-assertion': 'warn', + + // Disallow @ts-expect-error (TS 3.9+) and enforce descriptions for @ts-ignore + '@typescript-eslint/ban-ts-comment': ['warn', { + 'ts-expect-error': true, // Disallow @ts-expect-error for TypeScript 3.8 compatibility + 'ts-ignore': 'allow-with-description', + 'ts-nocheck': 'allow-with-description', + 'ts-check': false, + 'minimumDescriptionLength': 10 + }], + + // General code quality rules (not specifically TypeScript 3.8 related) + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-require-imports': 'warn', + 'prefer-rest-params': 'warn', + 'prefer-spread': 'warn', + '@typescript-eslint/no-unsafe-function-type': 'warn', + '@typescript-eslint/no-wrapper-object-types': 'warn', + '@typescript-eslint/no-empty-object-type': 'warn', + '@typescript-eslint/no-unsafe-declaration-merging': 'warn', + '@typescript-eslint/no-invalid-void-type': 'warn', + + // Additional TypeScript 3.8 compatibility notes: + // - Variadic tuple types (TS 4.0+) are handled by parser compatibility + // - Labeled tuple elements (TS 4.0+) are handled by parser compatibility + // - Template literal types (TS 4.1+) are handled by parser compatibility + + // Allow explicit constructors for better compatibility + '@typescript-eslint/no-useless-constructor': 'off', + + // Ensure we don't use assertions that require newer TS versions + '@typescript-eslint/consistent-type-assertions': ['warn', { + 'assertionStyle': 'as', + 'objectLiteralTypeAssertions': 'allow' + }], + }, + }, + // Jest tests + { + files: [ + '**/*.test.ts?(x)' + ], + languageOptions: { + globals: { + ...globals.jest, + ...globals.node, + ...globals.browser, + process: 'readonly', // require to access process.env values + }, + }, + plugins: { + jest: eslintPluginJest, + }, + rules: { + ...eslintPluginJest.configs['flat/recommended'].rules, + + // Disable preferring Promise-based async tests + 'jest/no-test-callback': 'off', + + 'jest/no-done-callback': 'warn', + 'jest/no-alias-methods': 'warn', + 'jest/no-conditional-expect': 'warn', + 'jest/valid-title': 'warn', + 'jest/no-standalone-expect': 'warn', + 'jest/no-deprecated-functions': 'warn', + 'no-var': 'warn', + }, + }, +); + +export default c; diff --git a/jest.config.js b/jest.config.js index 19f82304ad..97495adaee 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,7 @@ const testsForPackage = (packageName) => `/packages/${packageName}/**/*.test.[jt]s?(x)` const project = (displayName, packageNames, config = {}) => ({ + resolver: '/jest/node-exports-resolver', roots: ['/packages'], displayName, testMatch: packageNames.map(testsForPackage), @@ -26,6 +27,7 @@ module.exports = { ], projects: [ project('core', ['core']), + project('utilities', ['derecursify', 'json-payload']), project('web workers', ['web-worker'], { testEnvironment: '/jest/FixJSDOMEnvironment.js' }), diff --git a/jest/node-exports-resolver.js b/jest/node-exports-resolver.js new file mode 100644 index 0000000000..d6164935c9 --- /dev/null +++ b/jest/node-exports-resolver.js @@ -0,0 +1,182 @@ +/* eslint-disable no-inner-declarations */ +const fs = require('fs') +const path = require('path') +const packageUp = require('pkg-up') + +const defaultConditions = ['require', 'node', 'default'] + +function findMainPackageJson (entryPath, packageName) { + entryPath = entryPath.replace(/\//g, path.sep) + + let directoryName = path.dirname(entryPath) + + let suspect = packageUp.sync({ cwd: directoryName }) + if (fs.existsSync(suspect)) { + return JSON.parse(fs.readFileSync(suspect).toString()) + } + + while (directoryName && !directoryName.endsWith(packageName)) { + const parentDirectoryName = path.resolve(directoryName, '..') + + if (parentDirectoryName === directoryName) break + + directoryName = parentDirectoryName + } + + suspect = path.resolve(directoryName, 'package.json') + if (fs.existsSync(suspect)) { + return JSON.parse(fs.readFileSync(suspect).toString()) + } + + return null +} + +function getSelfReferencePath (packageName) { + let parentDirectoryName = __dirname + let directoryName + + while (directoryName !== parentDirectoryName) { + directoryName = parentDirectoryName + + try { + const { name } = require(path.resolve(directoryName, 'package.json')) + + if (name === packageName) return directoryName + } catch {} + + parentDirectoryName = path.resolve(directoryName, '..') + } +} + +function getPackageJson (packageName) { + // Require `package.json` from the package, both from exported `exports` field + // in ESM packages, or directly from the file itself in CommonJS packages. + try { + return require(`${packageName}/package.json`) + } catch (requireError) { + if (requireError.code === 'MODULE_NOT_FOUND') { + return null + } + if (requireError.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') { + return console.error( + `Unexpected error while requiring ${packageName}:`, requireError + ) + } + } + + // modules's `package.json` does not provide the "./package.json" path at it's + // "exports" field. Get package level export or main field and try to resolve + // the package.json from it. + try { + const requestPath = require.resolve(packageName) + + return requestPath && findMainPackageJson(requestPath, packageName) + } catch (resolveError) { + if (resolveError.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') { + console.log( + `Unexpected error while performing require.resolve(${packageName}):` + ) + + return console.error(resolveError) + } + } + + // modules's `package.json` does not provide a package level export nor main + // field. Try to find the package manually from `node_modules` folder. + const suspect = path.resolve(__dirname, '..', packageName, 'package.json') + if (fs.existsSync(suspect)) { + return JSON.parse(fs.readFileSync(suspect).toString()) + } + + console.warn( + 'Could not retrieve package.json neither through require (package.json ' + + 'itself is not within "exports" field), nor through require.resolve ' + + '(package.json does not specify "main" field) - falling back to default ' + + 'resolver logic' + ) +} + +module.exports = (request, options) => { + const { conditions = defaultConditions, defaultResolver } = options + + // NOTE: jest-sequencer is a special prefixed jest request + const isNodeModuleRequest = + !( + request.startsWith('.') || + path.isAbsolute(request) || + request.startsWith('jest-sequencer') + ) + + if (isNodeModuleRequest) { + const pkgPathParts = request.split('/') + const { length } = pkgPathParts + + let packageName + let submoduleName + + if (!request.startsWith('@')) { + packageName = pkgPathParts.shift() + submoduleName = length > 1 ? `./${pkgPathParts.join('/')}` : '.' + } else if (length >= 2) { + packageName = `${pkgPathParts.shift()}/${pkgPathParts.shift()}` + submoduleName = length > 2 ? `./${pkgPathParts.join('/')}` : '.' + } + + if (packageName) { + const selfReferencePath = getSelfReferencePath(packageName) + if (selfReferencePath) packageName = selfReferencePath + + const packageJson = getPackageJson(packageName) + + if (packageJson) { + const { exports } = packageJson || {} + if (exports) { + let targetFilePath + + if (typeof exports === 'string') { targetFilePath = exports } else if (Object.keys(exports).every((k) => k.startsWith('.'))) { + const [exportKey, exportValue] = Object.entries(exports) + .find(([k]) => { + if (k === submoduleName) return true + if (k.endsWith('*')) return submoduleName.startsWith(k.slice(0, -1)) + + return false + }) || [] + + if (typeof exportValue === 'string') { + targetFilePath = exportKey.endsWith('*') + ? exportValue.replace( + /\*/, submoduleName.slice(exportKey.length - 1) + ) + : exportValue + } else if ( + conditions && exportValue != null && typeof exportValue === 'object' + ) { + function resolveExport (exportValue, prevKeys) { + for (const [key, value] of Object.entries(exportValue)) { + // Duplicated nested conditions are undefined behaviour (and + // probably a format error or spec loop-hole), abort and + // delegate to Jest default resolver + if (prevKeys.includes(key)) return + + if (!conditions.includes(key)) continue + + if (typeof value === 'string') return value + + return resolveExport(value, prevKeys.concat(key)) + } + } + + targetFilePath = resolveExport(exportValue, []) + } + } + + if (targetFilePath) { + request = targetFilePath.replace('./', `${packageName}/`) + } + } + } + } + } + + return defaultResolver(request, options) +} diff --git a/package.json b/package.json index dc66982fea..f9930182e6 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@babel/plugin-transform-destructuring": "^7.23.3", "@babel/plugin-transform-member-expression-literals": "^7.23.3", "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-object-assign": "^7.27.1", "@babel/plugin-transform-parameters": "^7.23.3", "@babel/plugin-transform-property-literals": "^7.23.3", "@babel/plugin-transform-shorthand-properties": "^7.23.3", @@ -21,9 +22,12 @@ "@babel/preset-typescript": "^7.23.3", "@cucumber/cucumber": "^7.2.1", "@jest-runner/electron": "^3.0.1", + "@rollup/plugin-commonjs": "^28.0.0", + "@rollup/plugin-node-resolve": "^15.3.0", + "@rollup/plugin-replace": "^6.0.1", + "@rollup/plugin-typescript": "^12.1.0", "@types/jest": "^26.0.14", "@types/ncp": "^2.0.4", - "@types/node": "^13.7.1", "@types/node-fetch": "^2.5.7", "@types/react": "^16.9.49", "@types/react-dom": "^16.9.16", @@ -31,19 +35,11 @@ "@types/react-test-renderer": "^16.9.3", "@types/rimraf": "^3.0.0", "@types/uuid": "^8.3.0", - "@typescript-eslint/eslint-plugin": "^2.19.2", - "@typescript-eslint/parser": "^2.19.2", "aws-sdk": "^2.1303.0", "babel-jest": "^29.7.0", - "babelify": "^10.0.0", - "browser-pack-flat": "^3.2.0", - "browserify": "^17.0.1", - "browserify-versionify": "^1.0.6", "coverage-diff": "^1.6.0", - "cross-env": "^7.0.3", "electron": "*", "envify": "^4.1.0", - "eslint": "^6.8.0", "eslint-config-standard": "^14.1.0", "eslint-config-standard-with-typescript": "^13.0.0", "eslint-plugin-import": "^2.20.1", @@ -56,22 +52,24 @@ "form-data": "^4.0.4", "formidable": "^1.2.2", "glob": "^7.1.6", + "globals": "^15.14.0", "jest": "^26.4.2", "lerna": "^8.1.8", "metro-react-native-babel-preset": "^0.58.0", "ncp": "^2.0.0", + "pkg-up": "3.1.0", "playwright": "^1.10.0", - "proxyquire": "^2.1.3", "ps-list": "^7.2.0", "react": "^16.13.1", - "react-native": "^0.63.4", "react-test-renderer": "^16.13.1", "rimraf": "^3.0.0", + "rollup": "^4.24.0", "semver": "^5.5.1", "source-map": "^0.5.7", "timekeeper": "^2.2.0", "ts-node": "^9.1.1", - "typescript": "^3.9.7", + "typescript-eslint": "^8.22.0", + "typescript": "^4.8.4", "uglify-js": "^3.15.1", "verdaccio": "^6.0.5", "xvfb-maybe": "^0.2.1" @@ -79,7 +77,7 @@ "scripts": { "build": "lerna run build", "build:electron": "lerna run build --scope '@bugsnag/plugin-electron-ipc' --scope '@bugsnag/plugin-electron-app' --scope '@bugsnag/plugin-electron-client-state-persistence'", - "test:lint": "eslint --ext .ts,.js --report-unused-disable-directives --max-warnings=0 .", + "test:lint": "eslint --report-unused-disable-directives .", "test:lint-native": "bash scripts/cppcheck.sh", "test:unit:electron-runner": "xvfb-maybe --auto-servernum -- jest -c config/electron-jest.config.js --rootDir .", "test:unit": "xvfb-maybe --auto-servernum -- jest", @@ -101,6 +99,7 @@ }, "@types/babel__traverse": "7.17.1", "@types/express-serve-static-core": "4.17.28", + "@types/node": "^18", "@types/prettier": "2.6.0", "@types/yargs": "17.0.10", "braces": "^3.0.3", diff --git a/packages/browser/babel.config.js b/packages/browser/babel.config.js new file mode 100644 index 0000000000..55002be9ac --- /dev/null +++ b/packages/browser/babel.config.js @@ -0,0 +1,47 @@ +const rootBabelConfig = require('../../babel.config.js') + +module.exports = api => { + // For production builds (browser), use modern preset targeting old browsers + if (api && !api.env('test')) { + return { + presets: [ + ['@babel/preset-env', { + // Target Chrome 43 and IE 11 for maximum compatibility + targets: { + chrome: '43', + ie: '11' + }, + // Disable bugfixes to ensure maximum compatibility + bugfixes: false, + // Use loose mode for smaller bundle size + loose: true, + // Don't include polyfills, only transform syntax + useBuiltIns: false, + // Exclude modules transformation since rollup handles it + modules: false, + // Force all transforms to ensure ES5 compatibility + forceAllTransforms: true + }] + ], + plugins: [ + // Add specific plugins for Object.assign polyfill if needed + ['@babel/plugin-transform-object-assign'], + // Explicitly transform const/let to var + ['@babel/plugin-transform-block-scoping'], + // Transform arrow functions + ['@babel/plugin-transform-arrow-functions'], + // Transform classes + ['@babel/plugin-transform-classes'], + // Transform spread + ['@babel/plugin-transform-spread'], + // Transform template literals + ['@babel/plugin-transform-template-literals'], + // Transform destructuring + ['@babel/plugin-transform-destructuring'] + ] + } + } + + // For tests and other environments, use the root config + return rootBabelConfig(api) +} diff --git a/packages/browser/package.json b/packages/browser/package.json index a28b7399e2..407ceedb5e 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,36 +1,39 @@ { "name": "@bugsnag/browser", "version": "8.4.0", - "main": "dist/bugsnag.js", - "types": "types/bugsnag.d.ts", + "main": "dist/index-cjs.cjs", + "types": "dist/types/index-es.d.ts", + "browser": "./dist/bugsnag.js", + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/types/index-es.d.ts", + "import": "./dist/index-es.mjs", + "default": "./dist/index-cjs.cjs" + } + }, "description": "Bugsnag error reporter for browser JavaScript", "homepage": "https://www.bugsnag.com/", "repository": { "type": "git", "url": "git@github.com:bugsnag/bugsnag-js.git" }, - "browser": { - "types/bugsnag": "./dist/bugsnag.js" - }, "publishConfig": { "access": "public" }, "files": [ - "dist", - "types" + "dist" ], "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: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'", + "build": "npm run clean && npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", "cdn-upload": "../../bin/cdn-upload dist/*" }, "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/delivery-x-domain-request": "^8.4.0", "@bugsnag/delivery-xml-http-request": "^8.4.0", "@bugsnag/plugin-app-duration": "^8.4.0", "@bugsnag/plugin-browser-context": "^8.4.0", @@ -46,7 +49,12 @@ "@bugsnag/plugin-simple-throttle": "^8.4.0", "@bugsnag/plugin-strip-query-string": "^8.4.0", "@bugsnag/plugin-window-onerror": "^8.4.0", - "@bugsnag/plugin-window-unhandled-rejection": "^8.4.0" + "@bugsnag/plugin-window-unhandled-rejection": "^8.4.0", + "@rollup/plugin-babel": "^6.0.4", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-replace": "^6.0.2", + "@rollup/plugin-terser": "^0.4.4" }, "dependencies": { "@bugsnag/core": "^8.4.0" diff --git a/packages/browser/rollup.config.npm.mjs b/packages/browser/rollup.config.npm.mjs new file mode 100644 index 0000000000..833437b967 --- /dev/null +++ b/packages/browser/rollup.config.npm.mjs @@ -0,0 +1,162 @@ +import babel from '@rollup/plugin-babel'; +import commonjs from '@rollup/plugin-commonjs' +import nodeResolve from '@rollup/plugin-node-resolve' +import replace from '@rollup/plugin-replace' +import terser from '@rollup/plugin-terser' +import typescript from '@rollup/plugin-typescript' +import fs from 'fs' + +import createRollupConfig, { sharedOutput as commonSharedOutput } from "../../.rollup/index.mjs" + +const packageJson = JSON.parse(fs.readFileSync('./package.json')) + +const sharedOutput = { + ...commonSharedOutput, + strict: false, // 'use strict' in WebKit enables Tail Call Optimization, which breaks stack trace handling +} + +const treeshake = { + preset: 'smallest', // More aggressive than 'safest' + propertyReadSideEffects: false, + unknownGlobalSideEffects: false, + // Be more aggressive with module side effects + moduleSideEffects: false +}; + +const plugins = [ + nodeResolve({ + browser: true, + preferBuiltins: false, + // Enable tree-shaking for better dead code elimination + exportConditions: ['import'] + }), + commonjs({ + // Improve tree-shaking for CommonJS modules + ignoreTryCatch: 'remove' + }), + typescript({ + removeComments: true, + // don't output anything if there's a TS error + noEmitOnError: true, + compilerOptions: { + target: 'es2015', // Output ES2015 for babel to process + } + }), + babel({ + babelHelpers: 'bundled', + // Use the local babel configuration that targets Chrome 43 + configFile: './babel.config.js', + // Process ALL files, including dependencies + exclude: [], + // Include all extensions that might contain code + extensions: ['.js', '.ts', '.mjs', '.cjs'], + // Ensure babel processes the entire bundle, including node_modules + include: ['**/*'] + }), + replace({ + preventAssignment: true, + values: { + 'process.env.NODE_ENV': JSON.stringify('production'), + __BUGSNAG_NOTIFIER_VERSION__: packageJson.version, + }, + }) +] + +// External dependencies to reduce bundle size +// For ES modules and CJS, we'll keep dependencies bundled for now +// to avoid runtime dependency resolution issues +const external = [ + '@bugsnag/core', + '@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-xml-http-request' +] + +export default [ + createRollupConfig({ + input: "src/index-es.ts", + external, // Keep dependencies bundled for ESM + output: [ + { + ...sharedOutput, + preserveModules: false, + entryFileNames: '[name].mjs', + format: 'esm' + } + ], + plugins, + treeshake + }), + createRollupConfig({ + input: "src/index-cjs.ts", + external, // Keep dependencies bundled for CJS + output: [ + { + ...sharedOutput, + entryFileNames: '[name].cjs', + format: 'cjs', + }, + ], + plugins, + treeshake + }), + createRollupConfig({ + input: "src/index-umd.ts", + // UMD needs all dependencies bundled for standalone use + external: [], // Keep dependencies bundled for UMD + output: [ + { + ...sharedOutput, + entryFileNames: 'bugsnag.js', + format: 'umd', + name: 'Bugsnag' + }, + { + ...sharedOutput, + entryFileNames: 'bugsnag.min.js', + format: 'umd', + compact: true, + name: 'Bugsnag', + plugins: [terser({ + compress: { + passes: 3, // Increase from 2 + drop_console: true, // Remove console statements + drop_debugger: true, // Remove debugger statements + pure_getters: true, + unsafe_math: true, + unsafe_methods: true, + unsafe_proto: true, + unsafe_regexp: true, + unsafe_undefined: true, + conditionals: true, + dead_code: true, + evaluate: true, + if_return: true, + join_vars: true, + reduce_vars: true, + unused: true + }, + mangle: true, + format: { + comments: false // Remove all comments + } + })] + }, + ], + plugins, + treeshake + }) +]; diff --git a/packages/browser/src/bugsnag.ts b/packages/browser/src/bugsnag.ts new file mode 100644 index 0000000000..17498e5de0 --- /dev/null +++ b/packages/browser/src/bugsnag.ts @@ -0,0 +1,143 @@ +import { BugsnagStatic, Config, Client, schema as baseConfig } from '@bugsnag/core' + +// extend the base config schema with some browser-specific options +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 dXMLHttpRequest from '@bugsnag/delivery-xml-http-request' + +const name = 'Bugsnag JavaScript' +const version = '__BUGSNAG_NOTIFIER_VERSION__' +const url = 'https://github.com/bugsnag/bugsnag-js' + +const schema = { ...baseConfig, ...browserConfig } + +export interface BrowserConfig extends Config { + maxEvents?: number + collectUserIp?: boolean + generateAnonymousId?: boolean + trackInlineScripts?: boolean +} + +export interface BrowserBugsnagStatic extends BugsnagStatic { + start(apiKeyOrOpts: string | BrowserConfig): Client + createClient(apiKeyOrOpts: string | BrowserConfig): Client +} + +declare global { + interface Window { + XDomainRequest: unknown + } +} + +type BrowserClient = Partial & { + _client: Client | null + createClient: (opts?: Config) => Client + start: (opts?: Config) => Client + isStarted: () => boolean +} + +const notifier: BrowserClient = { + _client: null, + // @ts-expect-error + 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 + // @ts-expect-error + const bugsnag = new Client(opts, schema, internalPlugins, { name, version, url }); + + // @ts-expect-error + (bugsnag as BrowserClient)._setDelivery?.(dXMLHttpRequest) + + bugsnag._logger.debug('Loaded!') + bugsnag.leaveBreadcrumb('Bugsnag loaded', {}, 'state') + + return bugsnag._config.autoTrackSessions + ? bugsnag.startSession() + : bugsnag + }, + start: (opts) => { + if (notifier._client) { + notifier._client._logger.warn('Bugsnag.start() was called more than once. Ignoring.') + return notifier._client + } + notifier._client = notifier.createClient(opts) + return notifier._client + }, + isStarted: () => { + return notifier._client != null + } +} + +const clientMethods = Object.getOwnPropertyNames(Client.prototype).concat(['resetEventCount']) + +clientMethods.map((m) => { + if (/^_/.test(m) || m === 'constructor') return + // @ts-expect-error + notifier[m] = function () { + if (!notifier._client) return console.log(`Bugsnag.${m}() was called before Bugsnag.start()`) + notifier._client._depth += 1 + // @ts-expect-error + const ret = notifier._client[m].apply(notifier._client, arguments) + notifier._client._depth -= 1 + return ret + } +}) + +// @ts-expect-error +const Bugsnag = notifier as BrowserBugsnagStatic + +export default Bugsnag + +export interface BrowserConfig extends Config { + maxEvents?: number + collectUserIp?: boolean + generateAnonymousId?: boolean + trackInlineScripts?: boolean + sendPayloadChecksums?: boolean +} + +export interface BrowserBugsnagStatic extends BugsnagStatic { + start(apiKeyOrOpts: string | BrowserConfig): Client + createClient(apiKeyOrOpts: string | BrowserConfig): Client +} 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..653aeccbc5 --- /dev/null +++ b/packages/browser/src/config.ts @@ -0,0 +1,29 @@ +import { schema } from '@bugsnag/core' +import getPrefixedConsole from './get-prefixed-console' + +const config = { + releaseStage: { + ...schema.releaseStage, ...{ + defaultValue: () => { + if (/^localhost(:\d+)?$/.test(window.location.host)) return 'development' + return 'production' + } + } + }, + appType: { + ...schema.appType, ...{ + defaultValue: () => 'browser' + } + }, + logger: { + ...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..27be40dd43 --- /dev/null +++ b/packages/browser/src/get-prefixed-console.ts @@ -0,0 +1,16 @@ +type LoggerMethod = 'debug' | 'info' | 'warn' | 'error' + +const getPrefixedConsole = () => { + const logger: Record = {} + const consoleLog = console.log + const loggerMethods = ['debug', 'info', 'warn', 'error'] as const + loggerMethods.map((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/index-cjs.ts b/packages/browser/src/index-cjs.ts new file mode 100644 index 0000000000..6040a2d5e8 --- /dev/null +++ b/packages/browser/src/index-cjs.ts @@ -0,0 +1,7 @@ +import { Breadcrumb, Client, Event, Session } from '@bugsnag/core' + + + +import Bugsnag from './bugsnag' + +export default Object.assign(Bugsnag, { Breadcrumb, Client, Event, Session }) diff --git a/packages/browser/src/index-es.ts b/packages/browser/src/index-es.ts new file mode 100644 index 0000000000..90e1694e35 --- /dev/null +++ b/packages/browser/src/index-es.ts @@ -0,0 +1,25 @@ +export { default } from './bugsnag' +export type { BrowserBugsnagStatic, BrowserConfig } from './bugsnag' + +// Export only the essential parts from core to reduce bundle size +export { + Breadcrumb, + Client, + Event, + Session, + schema, + BREADCRUMB_TYPES +} from '@bugsnag/core' + +export type { + Config, + BugsnagStatic, + Plugin, + OnErrorCallback, + OnBreadcrumbCallback, + OnSessionCallback, + User, + FeatureFlag, + BreadcrumbType, + NotifiableError +} from '@bugsnag/core' diff --git a/packages/browser/src/index-umd.ts b/packages/browser/src/index-umd.ts new file mode 100644 index 0000000000..6040a2d5e8 --- /dev/null +++ b/packages/browser/src/index-umd.ts @@ -0,0 +1,7 @@ +import { Breadcrumb, Client, Event, Session } from '@bugsnag/core' + + + +import Bugsnag from './bugsnag' + +export default Object.assign(Bugsnag, { Breadcrumb, Client, Event, Session }) 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/test/index.test.ts b/packages/browser/test/index.test.ts index bfd9d47fc0..00edd6399f 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 @@ -31,12 +31,12 @@ function mockFetch (onSessionSend?: SendCallback, onNotifySend?: SendCallback) { const session = makeMockXHR(onSessionSend) const notify = makeMockXHR(onNotifySend) - // @ts-ignore + // @ts-expect-error window.XMLHttpRequest = jest.fn() .mockImplementationOnce(() => session) .mockImplementationOnce(() => notify) .mockImplementation(() => makeMockXHR(() => {})) - // @ts-ignore + // @ts-expect-error window.XMLHttpRequest.DONE = DONE return { session, notify } @@ -56,7 +56,7 @@ describe('browser notifier', () => { }) function getBugsnag (): typeof BugsnagBrowserStatic { - const Bugsnag = require('../src/notifier') as typeof BugsnagBrowserStatic + const Bugsnag = require('../src/bugsnag').default return Bugsnag } @@ -102,7 +102,7 @@ describe('browser notifier', () => { type: 'state', message: 'Bugsnag loaded' })) - expect(event.originalError.message).toBe('123') + expect((event.originalError as Error).message).toBe('123') }) }) @@ -155,7 +155,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', @@ -165,7 +165,7 @@ describe('browser notifier', () => { unhandledRejections: true }, onError: [ - event => true + () => true ], onBreadcrumb: (b: Breadcrumb) => { return false @@ -204,7 +204,7 @@ describe('browser notifier', () => { done(err) } expect(event.breadcrumbs.length).toBe(0) - expect(event.originalError.message).toBe('123') + expect((event.originalError as Error).message).toBe('123') expect(event.getMetadata('debug')).toEqual({ foo: 'bar' }) done() }) @@ -235,6 +235,7 @@ describe('browser notifier', () => { it('resets events on pushState', () => { const Bugsnag = getBugsnag() const client = Bugsnag.createClient('API_KEY') + // @ts-expect-error const resetEventCount = jest.spyOn(client, 'resetEventCount') window.history.pushState('', '', 'new-url') @@ -247,6 +248,7 @@ describe('browser notifier', () => { it('does not reset events on replaceState', () => { const Bugsnag = getBugsnag() const client = Bugsnag.createClient('API_KEY') + // @ts-expect-error const resetEventCount = jest.spyOn(client, 'resetEventCount') window.history.replaceState('', '', 'new-url') @@ -274,12 +276,10 @@ describe('browser notifier', () => { describe('payload checksum behavior (Bugsnag-Integrity header)', () => { beforeEach(() => { - // @ts-ignore window.isSecureContext = true }) afterEach(() => { - // @ts-ignore window.isSecureContext = 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/browser/types/bugsnag.d.ts b/packages/browser/types/bugsnag.d.ts deleted file mode 100644 index 2b9fcfe37e..0000000000 --- a/packages/browser/types/bugsnag.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Client, Config, BugsnagStatic } from '@bugsnag/core' - -interface BrowserConfig extends Config { - maxEvents?: number - collectUserIp?: boolean - generateAnonymousId?: boolean - trackInlineScripts?: boolean - sendPayloadChecksums?: boolean -} - -export interface BrowserBugsnagStatic extends BugsnagStatic { - start(apiKeyOrOpts: string | BrowserConfig): Client - createClient(apiKeyOrOpts: string | BrowserConfig): Client -} - -declare const Bugsnag: BrowserBugsnagStatic - -export default Bugsnag -export * from '@bugsnag/core' -export { BrowserConfig } diff --git a/packages/browser/types/global.d.ts b/packages/browser/types/global.d.ts deleted file mode 100644 index 713330a3f0..0000000000 --- a/packages/browser/types/global.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import bugsnag from './bugsnag' - -export as namespace bugsnag; -export = bugsnag; diff --git a/packages/core/babel.config.js b/packages/core/babel.config.js new file mode 100644 index 0000000000..a3f7ee0a58 --- /dev/null +++ b/packages/core/babel.config.js @@ -0,0 +1,6 @@ +module.exports = { + presets: [ + '@babel/preset-env', + '@babel/preset-typescript' + ] +} diff --git a/packages/core/breadcrumb.d.ts b/packages/core/breadcrumb.d.ts deleted file mode 100644 index 793e2a73ec..0000000000 --- a/packages/core/breadcrumb.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Breadcrumb } from './types' - -export default class BreadcrumbWithInternals extends Breadcrumb { - constructor (message: string, metadata: Breadcrumb['metadata'], type: Breadcrumb['type'], timestamp?: Date) - toJSON(): { - type: Breadcrumb['type'] - name: Breadcrumb['message'] - timestamp: Breadcrumb['timestamp'] - metaData: Breadcrumb['metadata'] - } -} diff --git a/packages/core/breadcrumb.js b/packages/core/breadcrumb.js deleted file mode 100644 index ee1693dd3a..0000000000 --- a/packages/core/breadcrumb.js +++ /dev/null @@ -1,19 +0,0 @@ -class Breadcrumb { - constructor (message, metadata, type, timestamp = new Date()) { - this.type = type - this.message = message - this.metadata = metadata - this.timestamp = timestamp - } - - toJSON () { - return { - type: this.type, - name: this.message, - timestamp: this.timestamp, - metaData: this.metadata - } - } -} - -module.exports = Breadcrumb diff --git a/packages/core/client.d.ts b/packages/core/client.d.ts deleted file mode 100644 index d869ee50b0..0000000000 --- a/packages/core/client.d.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Client, OnErrorCallback, Config, Breadcrumb, Session, OnSessionCallback, OnBreadcrumbCallback, Plugin, Device, App, User, FeatureFlag } from './types' -import EventWithInternals from './event' - -interface LoggerConfig { - debug: (msg: any) => void - info: (msg: any) => void - warn: (msg: any) => void - error: (msg: any) => void -} - -interface Notifier { - name: string - version: string - url: string -} - -interface EventDeliveryPayload { - apiKey: string - notifier: Notifier - events: EventWithInternals[] -} - -interface SessionDeliveryPayload { - notifier?: Notifier - device?: Device - app?: App - sessions?: Session[] -} - -interface Delivery { - sendEvent(payload: EventDeliveryPayload, cb: (err?: Error | null) => void): void - sendSession(session: SessionDeliveryPayload, cb: (err?: Error | null) => void): void -} - -/** - * Extend the public type definitions with internal declarations. - * - * This is currently used by the unit tests. These will be rolled into the - * module itself once it is converted to TypeScript. - */ -export default class ClientWithInternals extends Client { - public constructor(opts: T, schema?: {[key: string]: any}, internalPlugins?: Plugin[], notifier?: Notifier) - _config: T - _depth: number - _logger: LoggerConfig - _breadcrumbs: Breadcrumb[] - _delivery: Delivery - _setDelivery: (handler: (client: Client) => Delivery) => void - _clientContext: any - _user: User - - _metadata: { [key: string]: any } - _features: Array - _featuresIndex: { [key: string]: number } - - startSession(): ClientWithInternals - resumeSession(): ClientWithInternals - _session: Session | null - _pausedSession: Session | null - - _sessionDelegate: { - startSession: (client: ClientWithInternals, session: Session) => ClientWithInternals - pauseSession: (client: ClientWithInternals) => void - resumeSession: (client: ClientWithInternals) => ClientWithInternals - } - - _addOnSessionPayload: (cb: (sessionPayload: Session) => void) => void - - _cbs: { - e: OnErrorCallback[] - s: OnSessionCallback[] - sp: any[] - b: OnBreadcrumbCallback[] - } - - _loadPlugin(plugin: Plugin): void - - _isBreadcrumbTypeEnabled(type: string): boolean -} diff --git a/packages/core/config.js b/packages/core/config.js deleted file mode 100644 index 04081a4acc..0000000000 --- a/packages/core/config.js +++ /dev/null @@ -1,182 +0,0 @@ -const filter = require('./lib/es-utils/filter') -const reduce = require('./lib/es-utils/reduce') -const keys = require('./lib/es-utils/keys') -const isArray = require('./lib/es-utils/is-array') -const includes = require('./lib/es-utils/includes') -const intRange = require('./lib/validators/int-range') -const stringWithLength = require('./lib/validators/string-with-length') -const listOfFunctions = require('./lib/validators/list-of-functions') - -const BREADCRUMB_TYPES = require('./lib/breadcrumb-types') -const defaultErrorTypes = () => ({ unhandledExceptions: true, unhandledRejections: true }) - -module.exports.schema = { - apiKey: { - defaultValue: () => null, - message: 'is required', - validate: stringWithLength - }, - appVersion: { - defaultValue: () => undefined, - message: 'should be a string', - validate: value => value === undefined || stringWithLength(value) - }, - appType: { - defaultValue: () => undefined, - message: 'should be a string', - validate: value => value === undefined || stringWithLength(value) - }, - autoDetectErrors: { - defaultValue: () => true, - message: 'should be true|false', - validate: value => value === true || value === false - }, - enabledErrorTypes: { - defaultValue: () => defaultErrorTypes(), - message: 'should be an object containing the flags { unhandledExceptions:true|false, unhandledRejections:true|false }', - allowPartialObject: true, - validate: value => { - // ensure we have an object - if (typeof value !== 'object' || !value) return false - const providedKeys = keys(value) - const defaultKeys = keys(defaultErrorTypes()) - // ensure it only has a subset of the allowed keys - if (filter(providedKeys, k => includes(defaultKeys, k)).length < providedKeys.length) return false - // ensure all of the values are boolean - if (filter(keys(value), k => typeof value[k] !== 'boolean').length > 0) return false - return true - } - }, - onError: { - defaultValue: () => [], - message: 'should be a function or array of functions', - validate: listOfFunctions - }, - onSession: { - defaultValue: () => [], - message: 'should be a function or array of functions', - validate: listOfFunctions - }, - onBreadcrumb: { - defaultValue: () => [], - message: 'should be a function or array of functions', - validate: listOfFunctions - }, - endpoints: { - defaultValue: (endpoints) => { - // only apply the default value if no endpoints have been provided, otherwise prevent delivery by setting to null - if (typeof endpoints === 'undefined') { - return ({ - notify: 'https://notify.bugsnag.com', - sessions: 'https://sessions.bugsnag.com' - }) - } else { - return ({ notify: null, sessions: null }) - } - }, - message: 'should be an object containing endpoint URLs { notify, sessions }', - validate: (val) => - // first, ensure it's an object - (val && typeof val === 'object') && - ( - // notify and sessions must always be set - stringWithLength(val.notify) && stringWithLength(val.sessions) - ) && - // ensure no keys other than notify/session are set on endpoints object - filter(keys(val), k => !includes(['notify', 'sessions'], k)).length === 0 - }, - autoTrackSessions: { - defaultValue: val => true, - message: 'should be true|false', - validate: val => val === true || val === false - }, - enabledReleaseStages: { - defaultValue: () => null, - message: 'should be an array of strings', - validate: value => value === null || (isArray(value) && filter(value, f => typeof f === 'string').length === value.length) - }, - releaseStage: { - defaultValue: () => 'production', - message: 'should be a string', - validate: value => typeof value === 'string' && value.length - }, - maxBreadcrumbs: { - defaultValue: () => 25, - message: 'should be a number ≤100', - validate: value => intRange(0, 100)(value) - }, - enabledBreadcrumbTypes: { - defaultValue: () => BREADCRUMB_TYPES, - message: `should be null or a list of available breadcrumb types (${BREADCRUMB_TYPES.join(',')})`, - validate: value => value === null || (isArray(value) && reduce(value, (accum, maybeType) => { - if (accum === false) return accum - return includes(BREADCRUMB_TYPES, maybeType) - }, true)) - }, - context: { - defaultValue: () => undefined, - message: 'should be a string', - validate: value => value === undefined || typeof value === 'string' - }, - user: { - defaultValue: () => ({}), - message: 'should be an object with { id, email, name } properties', - validate: value => - (value === null) || - (value && reduce( - keys(value), - (accum, key) => accum && includes(['id', 'email', 'name'], key), - true - )) - }, - metadata: { - defaultValue: () => ({}), - message: 'should be an object', - validate: (value) => typeof value === 'object' && value !== null - }, - logger: { - defaultValue: () => undefined, - message: 'should be null or an object with methods { debug, info, warn, error }', - validate: value => - (!value) || - (value && reduce( - ['debug', 'info', 'warn', 'error'], - (accum, method) => accum && typeof value[method] === 'function', - true - )) - }, - redactedKeys: { - defaultValue: () => ['password'], - message: 'should be an array of strings|regexes', - validate: value => - isArray(value) && value.length === filter(value, s => - (typeof s === 'string' || (s && typeof s.test === 'function')) - ).length - }, - plugins: { - defaultValue: () => ([]), - message: 'should be an array of plugin objects', - validate: value => - isArray(value) && value.length === filter(value, p => - (p && typeof p === 'object' && typeof p.load === 'function') - ).length - }, - featureFlags: { - defaultValue: () => [], - message: 'should be an array of objects that have a "name" property', - validate: value => - isArray(value) && value.length === filter(value, feature => - feature && typeof feature === 'object' && typeof feature.name === 'string' - ).length - }, - reportUnhandledPromiseRejectionsAsHandled: { - defaultValue: () => false, - message: 'should be true|false', - validate: value => value === true || value === false - }, - sendPayloadChecksums: { - defaultValue: () => false, - message: 'should be true|false', - validate: value => value === true || value === false - } -} diff --git a/packages/core/event.d.ts b/packages/core/event.d.ts deleted file mode 100644 index 9ace225a89..0000000000 --- a/packages/core/event.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { App, Device, Event, Request, Breadcrumb, User, Session, FeatureFlag } from './types' -import { Error } from './types/event' - -interface HandledState { - unhandled: boolean - severity: string - severityReason: { type: string } -} - -interface FeatureFlagPayload { - featureFlag: string - variant?: string -} - -/** - * Extend the public type definitions with internal declarations. - * - * This is currently used by the unit tests. These will be rolled into the - * module itself once it is converted to TypeScript. - */ -export default class EventWithInternals extends Event { - constructor (errorClass: string, errorMessage: string, stacktrace: any[], handledState?: HandledState, originalError?: Error) - _metadata: { [key: string]: any } - _features: FeatureFlag | null[] - _featuresIndex: { [key: string]: number } - _user: User - _handledState: HandledState - _correlation?: { spanId: string, traceId: string } - _session?: Session - _groupingDiscriminator?: string | null - toJSON(): { - payloadVersion: '4' - exceptions: Array - severity: Event['severity'] - unhandled: boolean - severityReason: { - type: string - [key: string]: any - } - app: App - device: Device - request: Request - breadcrumbs: Breadcrumb[] - context: string | undefined - correlation: { spanId: string, traceId: string } | undefined - groupingHash: string | undefined - groupingDiscriminator: string | undefined - metaData: { [key: string]: any } - user: User - session: Session - featureFlags: FeatureFlagPayload[] - }; -} diff --git a/packages/core/index.js b/packages/core/index.js deleted file mode 100644 index 13bad19a8c..0000000000 --- a/packages/core/index.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports.Breadcrumb = require('./breadcrumb') -module.exports.Client = require('./client') -module.exports.Event = require('./event') -module.exports.Session = require('./session') diff --git a/packages/core/lib/async-every.d.ts b/packages/core/lib/async-every.d.ts deleted file mode 100644 index 7b82bb15fe..0000000000 --- a/packages/core/lib/async-every.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -type NodeCallbackType = (error?: Error | null, result?: T) => void; - -export default function every( - arr: T[], - fn: (item: T, cb: NodeCallbackType) => void, - cb: NodeCallbackType -): void diff --git a/packages/core/lib/breadcrumb-types.js b/packages/core/lib/breadcrumb-types.js deleted file mode 100644 index c02cebe209..0000000000 --- a/packages/core/lib/breadcrumb-types.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = ['navigation', 'request', 'process', 'log', 'user', 'state', 'error', 'manual'] diff --git a/packages/core/lib/callback-runner.d.ts b/packages/core/lib/callback-runner.d.ts deleted file mode 100644 index 9d33887df1..0000000000 --- a/packages/core/lib/callback-runner.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { NodeCallbackType } from './async-every' - -export default function callbackRunner( - callbacks: any, - event: T, - onCallbackError: (err: Error) => void, - cb: NodeCallbackType -): void diff --git a/packages/core/lib/clone-client.js b/packages/core/lib/clone-client.js deleted file mode 100644 index 2e62781004..0000000000 --- a/packages/core/lib/clone-client.js +++ /dev/null @@ -1,35 +0,0 @@ -const assign = require('./es-utils/assign') - -const onCloneCallbacks = [] - -module.exports = (client) => { - const clone = new client.Client({}, {}, [], client._notifier) - - clone._config = client._config - - // changes to these properties should not be reflected in the original client, - // so ensure they are are (shallow) cloned - clone._breadcrumbs = client._breadcrumbs.slice() - clone._metadata = assign({}, client._metadata) - clone._features = [...client._features] - clone._featuresIndex = assign({}, client._featuresIndex) - clone._user = assign({}, client._user) - clone._context = client._context - - clone._cbs = { - e: client._cbs.e.slice(), - s: client._cbs.s.slice(), - sp: client._cbs.sp.slice(), - b: client._cbs.b.slice() - } - - clone._logger = client._logger - clone._delivery = client._delivery - clone._sessionDelegate = client._sessionDelegate - - onCloneCallbacks.forEach(callback => { callback(clone) }) - - return clone -} - -module.exports.registerCallback = callback => { onCloneCallbacks.push(callback) } diff --git a/packages/core/lib/error-stack-parser.js b/packages/core/lib/error-stack-parser.js deleted file mode 100644 index b78b77d5ee..0000000000 --- a/packages/core/lib/error-stack-parser.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('error-stack-parser') diff --git a/packages/core/lib/es-utils/assign.d.ts b/packages/core/lib/es-utils/assign.d.ts deleted file mode 100644 index b7d8207083..0000000000 --- a/packages/core/lib/es-utils/assign.d.ts +++ /dev/null @@ -1 +0,0 @@ -export default function assign(target: object, ...sources: any[]): any diff --git a/packages/core/lib/es-utils/assign.js b/packages/core/lib/es-utils/assign.js deleted file mode 100644 index f0387b3abb..0000000000 --- a/packages/core/lib/es-utils/assign.js +++ /dev/null @@ -1,13 +0,0 @@ -// extends helper from babel -// https://github.com/babel/babel/blob/916429b516e6466fd06588ee820e40e025d7f3a3/packages/babel-helpers/src/helpers.js#L377-L393 -module.exports = function (target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i] - for (var key in source) { - if (Object.prototype.hasOwnProperty.call(source, key)) { - target[key] = source[key] - } - } - } - return target -} diff --git a/packages/core/lib/es-utils/filter.d.ts b/packages/core/lib/es-utils/filter.d.ts deleted file mode 100644 index cd8210113b..0000000000 --- a/packages/core/lib/es-utils/filter.d.ts +++ /dev/null @@ -1 +0,0 @@ -export default function filter(arr: T[], fn: (item: T) => boolean): T[] diff --git a/packages/core/lib/es-utils/filter.js b/packages/core/lib/es-utils/filter.js deleted file mode 100644 index c365ed8e31..0000000000 --- a/packages/core/lib/es-utils/filter.js +++ /dev/null @@ -1,5 +0,0 @@ -const reduce = require('./reduce') - -// Array#filter -module.exports = (arr, fn) => - reduce(arr, (accum, item, i, arr) => !fn(item, i, arr) ? accum : accum.concat(item), []) diff --git a/packages/core/lib/es-utils/includes.d.ts b/packages/core/lib/es-utils/includes.d.ts deleted file mode 100644 index 41072c0688..0000000000 --- a/packages/core/lib/es-utils/includes.d.ts +++ /dev/null @@ -1 +0,0 @@ -export default function includes(arr: T[], item: T): boolean diff --git a/packages/core/lib/es-utils/includes.js b/packages/core/lib/es-utils/includes.js deleted file mode 100644 index 63cd6c4a94..0000000000 --- a/packages/core/lib/es-utils/includes.js +++ /dev/null @@ -1,4 +0,0 @@ -const reduce = require('./reduce') -// Array#includes -module.exports = (arr, x) => - reduce(arr, (accum, item, i, arr) => accum === true || item === x, false) diff --git a/packages/core/lib/es-utils/is-array.d.ts b/packages/core/lib/es-utils/is-array.d.ts deleted file mode 100644 index c9bab14cea..0000000000 --- a/packages/core/lib/es-utils/is-array.d.ts +++ /dev/null @@ -1 +0,0 @@ -export default function isArray(obj: any): boolean diff --git a/packages/core/lib/es-utils/is-array.js b/packages/core/lib/es-utils/is-array.js deleted file mode 100644 index b23dea97d3..0000000000 --- a/packages/core/lib/es-utils/is-array.js +++ /dev/null @@ -1,2 +0,0 @@ -// Array#isArray -module.exports = obj => Object.prototype.toString.call(obj) === '[object Array]' diff --git a/packages/core/lib/es-utils/keys.d.ts b/packages/core/lib/es-utils/keys.d.ts deleted file mode 100644 index 803124daa3..0000000000 --- a/packages/core/lib/es-utils/keys.d.ts +++ /dev/null @@ -1 +0,0 @@ -export default function keys(obj: {}): string[] diff --git a/packages/core/lib/es-utils/keys.js b/packages/core/lib/es-utils/keys.js deleted file mode 100644 index d63e354514..0000000000 --- a/packages/core/lib/es-utils/keys.js +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint-disable-next-line no-prototype-builtins */ -const _hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString') -const _dontEnums = [ - 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', - 'isPrototypeOf', 'propertyIsEnumerable', 'constructor' -] - -// Object#keys -module.exports = obj => { - // stripped down version of - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/Keys - const result = [] - let prop - for (prop in obj) { - if (Object.prototype.hasOwnProperty.call(obj, prop)) result.push(prop) - } - if (!_hasDontEnumBug) return result - for (let i = 0, len = _dontEnums.length; i < len; i++) { - if (Object.prototype.hasOwnProperty.call(obj, _dontEnums[i])) result.push(_dontEnums[i]) - } - return result -} diff --git a/packages/core/lib/es-utils/map.d.ts b/packages/core/lib/es-utils/map.d.ts deleted file mode 100644 index d737a0ba71..0000000000 --- a/packages/core/lib/es-utils/map.d.ts +++ /dev/null @@ -1 +0,0 @@ -export default function map(arr: T[], fn: (item: T) => U): U[] diff --git a/packages/core/lib/es-utils/map.js b/packages/core/lib/es-utils/map.js deleted file mode 100644 index 393ede97fb..0000000000 --- a/packages/core/lib/es-utils/map.js +++ /dev/null @@ -1,5 +0,0 @@ -const reduce = require('./reduce') - -// Array#map -module.exports = (arr, fn) => - reduce(arr, (accum, item, i, arr) => accum.concat(fn(item, i, arr)), []) diff --git a/packages/core/lib/es-utils/reduce.d.ts b/packages/core/lib/es-utils/reduce.d.ts deleted file mode 100644 index 576384d933..0000000000 --- a/packages/core/lib/es-utils/reduce.d.ts +++ /dev/null @@ -1 +0,0 @@ -export default function reduce(arr: T[], fn: (accum: any, item: T) => any, accum: any): any diff --git a/packages/core/lib/es-utils/reduce.js b/packages/core/lib/es-utils/reduce.js deleted file mode 100644 index d5335c0931..0000000000 --- a/packages/core/lib/es-utils/reduce.js +++ /dev/null @@ -1,6 +0,0 @@ -// Array#reduce -module.exports = (arr, fn, accum) => { - let val = accum - for (let i = 0, len = arr.length; i < len; i++) val = fn(val, arr[i], i, arr) - return val -} diff --git a/packages/core/lib/extract-object.js b/packages/core/lib/extract-object.js deleted file mode 100644 index c14e087b06..0000000000 --- a/packages/core/lib/extract-object.js +++ /dev/null @@ -1,10 +0,0 @@ -// Given a host object, return the value at host[key] if it is an object -// and it has at least one key/value - -module.exports = (host, key) => { - if (host[key] && typeof host[key] === 'object' && Object.keys(host[key]).length > 0) { - return host[key] - } else { - return undefined - } -} diff --git a/packages/core/lib/feature-flag-delegate.js b/packages/core/lib/feature-flag-delegate.js deleted file mode 100644 index 69ad3bda49..0000000000 --- a/packages/core/lib/feature-flag-delegate.js +++ /dev/null @@ -1,73 +0,0 @@ -const map = require('./es-utils/map') -const filter = require('./es-utils/filter') -const isArray = require('./es-utils/is-array') -const jsonStringify = require('@bugsnag/safe-json-stringify') - -function add (existingFeatures, existingFeatureKeys, name, variant) { - if (typeof name !== 'string') { - return - } - - if (variant === undefined) { - variant = null - } else if (variant !== null && typeof variant !== 'string') { - variant = jsonStringify(variant) - } - - const existingIndex = existingFeatureKeys[name] - if (typeof existingIndex === 'number') { - existingFeatures[existingIndex] = { name, variant } - return - } - - existingFeatures.push({ name, variant }) - existingFeatureKeys[name] = existingFeatures.length - 1 -} - -function merge (existingFeatures, newFeatures, existingFeatureKeys) { - if (!isArray(newFeatures)) { - return - } - - for (let i = 0; i < newFeatures.length; ++i) { - const feature = newFeatures[i] - - if (feature === null || typeof feature !== 'object') { - continue - } - - // 'add' will handle if 'name' doesn't exist & 'variant' is optional - add(existingFeatures, existingFeatureKeys, feature.name, feature.variant) - } - - return existingFeatures -} - -// convert feature flags from a map of 'name -> variant' into the format required -// by the Bugsnag Event API: -// [{ featureFlag: 'name', variant: 'variant' }, { featureFlag: 'name 2' }] -function toEventApi (featureFlags) { - return map( - filter(featureFlags, Boolean), - ({ name, variant }) => { - const flag = { featureFlag: name } - - // don't add a 'variant' property unless there's actually a value - if (typeof variant === 'string') { - flag.variant = variant - } - - return flag - } - ) -} - -function clear (features, featuresIndex, name) { - const existingIndex = featuresIndex[name] - if (typeof existingIndex === 'number') { - features[existingIndex] = null - delete featuresIndex[name] - } -} - -module.exports = { add, clear, merge, toEventApi } diff --git a/packages/core/lib/iserror.js b/packages/core/lib/iserror.js deleted file mode 100644 index 743a96bd97..0000000000 --- a/packages/core/lib/iserror.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('iserror') diff --git a/packages/core/lib/metadata-delegate.js b/packages/core/lib/metadata-delegate.js deleted file mode 100644 index ae5db228a9..0000000000 --- a/packages/core/lib/metadata-delegate.js +++ /dev/null @@ -1,60 +0,0 @@ -const assign = require('./es-utils/assign') - -const add = (state, section, keyOrObj, maybeVal) => { - if (!section) return - let updates - - // addMetadata("section", null) -> clears section - if (keyOrObj === null) return clear(state, section) - - // normalise the two supported input types into object form - if (typeof keyOrObj === 'object') updates = keyOrObj - if (typeof keyOrObj === 'string') updates = { [keyOrObj]: maybeVal } - - // exit if we don't have an updates object at this point - if (!updates) return - - // preventing the __proto__ property from being used as a key - if (section === '__proto__' || section === 'constructor' || section === 'prototype') { - return - } - - // ensure a section with this name exists - if (!state[section]) state[section] = {} - - // merge the updates with the existing section - state[section] = assign({}, state[section], updates) -} - -const get = (state, section, key) => { - if (typeof section !== 'string') return undefined - if (!key) { - return state[section] - } - if (state[section]) { - return state[section][key] - } - return undefined -} - -const clear = (state, section, key) => { - if (typeof section !== 'string') return - - // clear an entire section - if (!key) { - delete state[section] - return - } - - // preventing the __proto__ property from being used as a key - if (section === '__proto__' || section === 'constructor' || section === 'prototype') { - return - } - - // clear a single value from a section - if (state[section]) { - delete state[section][key] - } -} - -module.exports = { add, get, clear } diff --git a/packages/core/lib/node-fallback-stack.js b/packages/core/lib/node-fallback-stack.js deleted file mode 100644 index 7504b9b567..0000000000 --- a/packages/core/lib/node-fallback-stack.js +++ /dev/null @@ -1,28 +0,0 @@ -// The utilities in this file are used to save the stackframes from a known execution context -// to use when a subsequent error has no stack frames. This happens with a lot of -// node's builtin async callbacks when they return from the native layer with no context -// for example: -// -// fs.readFile('does not exist', (err) => { -// /* node 8 */ -// err.stack = "ENOENT: no such file or directory, open 'nope'" -// /* node 4,6 */ -// err.stack = "Error: ENOENT: no such file or directory, open 'nope'\n at Error (native)" -// }) - -// Gets the stack string for the current execution context -exports.getStack = () => { - // slice(3) removes the first line + this function's frame + the caller's frame, - // so the stack begins with the caller of this function - return (new Error()).stack.split('\n').slice(3).join('\n') -} - -// Given an Error and a fallbackStack from getStack(), use the fallbackStack -// if error.stack has no genuine stackframes (according to the example above) -exports.maybeUseFallbackStack = (err, fallbackStack) => { - const lines = err.stack.split('\n') - if (lines.length === 1 || (lines.length === 2 && /at Error \(native\)/.test(lines[1]))) { - err.stack = `${lines[0]}\n${fallbackStack}` - } - return err -} diff --git a/packages/core/lib/sync-callback-runner.js b/packages/core/lib/sync-callback-runner.js deleted file mode 100644 index dfb8c08ac3..0000000000 --- a/packages/core/lib/sync-callback-runner.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = (callbacks, callbackArg, callbackType, logger) => { - let ignore = false - const cbs = callbacks.slice() - while (!ignore) { - if (!cbs.length) break - try { - ignore = cbs.pop()(callbackArg) === false - } catch (e) { - logger.error(`Error occurred in ${callbackType} callback, continuing anyway…`) - logger.error(e) - } - } - return ignore -} diff --git a/packages/core/lib/test/clone-client.test.ts b/packages/core/lib/test/clone-client.test.ts deleted file mode 100644 index cd0e4e2210..0000000000 --- a/packages/core/lib/test/clone-client.test.ts +++ /dev/null @@ -1,220 +0,0 @@ -import Client from '../../client' -import cloneClient from '../clone-client' - -describe('clone-client', () => { - it('should not copy the configuration', () => { - const apiKey = '123456abcdef123456abcdef123456ab' - - const original = new Client({ apiKey }) - const clone = cloneClient(original) - - expect(clone._config).toBe(original._config) - expect(clone._config.apiKey).toBe(original._config.apiKey) - }) - - it('should not copy the logger', () => { - const logger = { - debug: () => {}, - info: () => {}, - warn: () => {}, - error: () => {} - } - - const original = new Client({ apiKey: '123456abcdef123456abcdef123456ab', logger }) - const clone = cloneClient(original) - - expect(clone._logger).toBe(logger) - expect(clone._logger).toBe(original._logger) - }) - - it('should not copy the delivery implementation', () => { - const delivery = { - sendEvent: () => {}, - sendSession: () => {} - } - - const original = new Client({ apiKey: '123456abcdef123456abcdef123456ab' }) - original._setDelivery(() => delivery) - - const clone = cloneClient(original) - - expect(clone._delivery).toBe(delivery) - expect(clone._delivery).toBe(original._delivery) - }) - - it('should not copy the session delegate', () => { - const sessionDelegate = { - startSession: (client: Client) => client, - resumeSession: (client: Client) => client, - pauseSession: () => {} - } - - const original = new Client({ apiKey: '123456abcdef123456abcdef123456ab' }) - original._sessionDelegate = sessionDelegate - - const clone = cloneClient(original) - - expect(clone._sessionDelegate).toBe(sessionDelegate) - expect(clone._sessionDelegate).toBe(original._sessionDelegate) - }) - - it('should not copy the context', () => { - const original = new Client({ apiKey: '123456abcdef123456abcdef123456ab' }) - original.setContext('abc') - - const clone = cloneClient(original) - - expect(clone.getContext()).toBe('abc') - expect(clone.getContext()).toBe(original.getContext()) - - // changing the clone's context shouldn't affect the original's context - clone.setContext('xyz') - - expect(clone.getContext()).toBe('xyz') - expect(original.getContext()).toBe('abc') - }) - - it('should clone breadcrumbs', () => { - const original = new Client({ apiKey: '123456abcdef123456abcdef123456ab' }) - original.leaveBreadcrumb('breadcrumb 1') - - expect(original._breadcrumbs).toHaveLength(1) - const breadcrumb = original._breadcrumbs[0] - - const clone = cloneClient(original) - - expect(clone._breadcrumbs).toHaveLength(1) - expect(clone._breadcrumbs[0]).toBe(breadcrumb) - expect(clone._breadcrumbs).not.toBe(original._breadcrumbs) - - // leaving a new breadcrumb on the clone shouldn't affect the original - clone.leaveBreadcrumb('breadcrumb 2') - - expect(clone._breadcrumbs).toHaveLength(2) - expect(original._breadcrumbs).toHaveLength(1) - }) - - it('should clone metadata', () => { - const original = new Client({ apiKey: '123456abcdef123456abcdef123456ab' }) - original.addMetadata('abc', 'xyz', 123) - - const clone = cloneClient(original) - - expect(clone._metadata).toStrictEqual({ abc: { xyz: 123 } }) - expect(clone._metadata).not.toBe(original._metadata) - - // changing the clone's metadata shouldn't affect the original - clone.addMetadata('abc', 'xyz', 999) - - expect(clone._metadata).toStrictEqual({ abc: { xyz: 999 } }) - expect(original._metadata).toStrictEqual({ abc: { xyz: 123 } }) - }) - - it('should clone feature flags', () => { - const original = new Client({ apiKey: '123456abcdef123456abcdef123456ab' }) - original.addFeatureFlag('abc', '123') - - const clone = cloneClient(original) - - expect(clone._features).toStrictEqual([{ name: 'abc', variant: '123' }]) - expect(clone._features).not.toBe(original._features) - - // changing the clone's feature flags shouldn't affect the original - clone.addFeatureFlag('xyz', '999') - - expect(clone._features).toStrictEqual([{ name: 'abc', variant: '123' }, { name: 'xyz', variant: '999' }]) - expect(original._features).toStrictEqual([{ name: 'abc', variant: '123' }]) - }) - - it('should clone user information', () => { - const original = new Client({ apiKey: '123456abcdef123456abcdef123456ab' }) - original.setUser('123', 'abc@example.com', 'abc') - - const clone = cloneClient(original) - - expect(clone.getUser()).toStrictEqual({ id: '123', email: 'abc@example.com', name: 'abc' }) - expect(clone.getUser()).not.toBe(original.getUser()) - - // changing the clone's user shouldn't affect the original - clone.setUser('999', 'xyz@example.com', 'xyz') - - expect(clone.getUser()).toStrictEqual({ id: '999', email: 'xyz@example.com', name: 'xyz' }) - expect(original.getUser()).toStrictEqual({ id: '123', email: 'abc@example.com', name: 'abc' }) - }) - - it('should clone the on error callback array', () => { - const onError = () => {} - - const original = new Client({ apiKey: '123456abcdef123456abcdef123456ab' }) - original.addOnError(onError) - - const clone = cloneClient(original) - - expect(clone._cbs.e).toHaveLength(1) - expect(clone._cbs.e[0]).toBe(onError) - expect(clone._cbs.e).not.toBe(original._cbs.e) - - // changing the clone's callbacks shouldn't affect the original - clone.addOnError(onError) - - expect(clone._cbs.e).toHaveLength(2) - expect(original._cbs.e).toHaveLength(1) - }) - - it('should clone the on session callback array', () => { - const onSession = () => {} - - const original = new Client({ apiKey: '123456abcdef123456abcdef123456ab' }) - original.addOnSession(onSession) - - const clone = cloneClient(original) - - expect(clone._cbs.s).toHaveLength(1) - expect(clone._cbs.s[0]).toBe(onSession) - expect(clone._cbs.s).not.toBe(original._cbs.s) - - // changing the clone's callbacks shouldn't affect the original - clone.addOnSession(onSession) - - expect(clone._cbs.s).toHaveLength(2) - expect(original._cbs.s).toHaveLength(1) - }) - - it('should clone the on session payload callback array', () => { - const onSessionPayload = () => {} - - const original = new Client({ apiKey: '123456abcdef123456abcdef123456ab' }) - original._addOnSessionPayload(onSessionPayload) - - const clone = cloneClient(original) - - expect(clone._cbs.sp).toHaveLength(1) - expect(clone._cbs.sp[0]).toBe(onSessionPayload) - expect(clone._cbs.sp).not.toBe(original._cbs.sp) - - // changing the clone's callbacks shouldn't affect the original - clone._addOnSessionPayload(onSessionPayload) - - expect(clone._cbs.sp).toHaveLength(2) - expect(original._cbs.sp).toHaveLength(1) - }) - - it('should clone the on breadcrumb callback array', () => { - const onBreadcrumb = () => {} - - const original = new Client({ apiKey: '123456abcdef123456abcdef123456ab' }) - original.addOnBreadcrumb(onBreadcrumb) - - const clone = cloneClient(original) - - expect(clone._cbs.b).toHaveLength(1) - expect(clone._cbs.b[0]).toBe(onBreadcrumb) - expect(clone._cbs.b).not.toBe(original._cbs.b) - - // changing the clone's callbacks shouldn't affect the original - clone.addOnBreadcrumb(onBreadcrumb) - - expect(clone._cbs.b).toHaveLength(2) - expect(original._cbs.b).toHaveLength(1) - }) -}) diff --git a/packages/core/lib/test/es-utils.test.ts b/packages/core/lib/test/es-utils.test.ts deleted file mode 100644 index dc4e96c953..0000000000 --- a/packages/core/lib/test/es-utils.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import map from '../es-utils/map' -import reduce from '../es-utils/reduce' -import filter from '../es-utils/filter' -import keys from '../es-utils/keys' -import isArray from '../es-utils/is-array' -import includes from '../es-utils/includes' - -describe('es-utils', () => { - describe('reduce(arr, fn, accum)', () => { - it('works with a variety of examples', () => { - expect(reduce([1, 2, 3, 4, 5], (accum, x) => accum + x, 0)).toBe(15) - expect(reduce([() => 100, () => 250, () => 25], (accum, x) => Math.max(x(), accum), -Infinity)).toBe(250) - }) - }) - - describe('map(arr, fn)', () => { - it('works with a variety of examples', () => { - expect(map(['a', 'b', 'c'], x => x)).toEqual(['a', 'b', 'c']) - expect(map(['a', 'b', 'c'], (x) => x.toUpperCase())).toEqual(['A', 'B', 'C']) - }) - }) - - describe('filter(arr, fn)', () => { - it('works with a variety of examples', () => { - const arr = ['a', 0, false, undefined, 1, 'undefined'] - expect(filter(arr, () => true)).toEqual(arr) - expect(filter(arr, () => false)).toEqual([]) - expect(filter(arr, Boolean)).toEqual(['a', 1, 'undefined']) - }) - }) - - describe('keys(obj)', () => { - it('works with a variety of examples', () => { - expect(keys({ a: 1, b: 2 })).toEqual(['a', 'b']) - expect(keys({ toString: 2 })).toEqual(['toString']) - }) - }) - - describe('isArray(obj)', () => { - it('works with a variety of examples', () => { - expect(isArray([])).toBe(true) - expect(isArray('[object Array]')).toBe(false) - expect(isArray(0)).toBe(false) - expect(isArray(1)).toBe(false) - expect(isArray({})).toBe(false) - }) - }) - - describe('includes(arr, item)', () => { - it('works with a variety of examples', () => { - expect(includes(['a', 'b', 'c'], 'a')).toBe(true) - expect(includes(['a', 'b', 'c'], 'd')).toBe(false) - }) - }) -}) diff --git a/packages/core/lib/test/json-payload.test.ts b/packages/core/lib/test/json-payload.test.ts deleted file mode 100644 index 44cf86c760..0000000000 --- a/packages/core/lib/test/json-payload.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import jsonPayload from '../json-payload' - -function makeBigObject () { - var big: Record = {} - var i = 0 - while (JSON.stringify(big).length < 2 * 10e5) { - big['entry' + i] = 'long repetitive string'.repeat(1000) - i++ - } - return big -} - -describe('jsonPayload.event', () => { - it('safe stringifies the payload and redacts values from certain paths of the supplied keys', () => { - expect(jsonPayload.event({ - api_key: 'd145b8e5afb56516423bc4d605e45442', - events: [ - { - errorMessage: 'Failed load tickets', - errorClass: 'CheckoutError', - user: { - name: 'Jim Bug', - email: 'jim@bugsnag.com' - }, - request: { - api_key: '245b39ebd3cd3992e85bffc81c045924' - } - } - ] - }, ['api_key'])).toBe('{"api_key":"d145b8e5afb56516423bc4d605e45442","events":[{"errorMessage":"Failed load tickets","errorClass":"CheckoutError","user":{"name":"Jim Bug","email":"jim@bugsnag.com"},"request":{"api_key":"[REDACTED]"}}]}') - }) - - it('strips the metaData of the first event if the payload is too large', () => { - const payload = { - api_key: 'd145b8e5afb56516423bc4d605e45442', - events: [ - { - errorMessage: 'Failed load tickets', - errorClass: 'CheckoutError', - user: { - name: 'Jim Bug', - email: 'jim@bugsnag.com' - }, - request: { - api_key: '245b39ebd3cd3992e85bffc81c045924' - }, - _metadata: {} - } - ] - } - - payload.events[0]._metadata = { 'big thing': makeBigObject() } - - expect(jsonPayload.event(payload)).toBe('{"api_key":"d145b8e5afb56516423bc4d605e45442","events":[{"errorMessage":"Failed load tickets","errorClass":"CheckoutError","user":{"name":"Jim Bug","email":"jim@bugsnag.com"},"request":{"api_key":"245b39ebd3cd3992e85bffc81c045924"},"_metadata":{"notifier":"WARNING!\\nSerialized payload was 2.003435MB (limit = 1MB)\\nmetadata was removed"}}]}') - }) - - it('does not attempt to strip any other data paths from the payload to reduce the size', () => { - const payload = { - api_key: 'd145b8e5afb56516423bc4d605e45442', - events: [ - { - errorMessage: 'Failed load tickets', - errorClass: 'CheckoutError', - user: { - name: 'Jim Bug', - email: 'jim@bugsnag.com' - }, - _metadata: {} - }, - { - errorMessage: 'Request failed', - errorClass: 'APIError', - _metadata: {} - } - ] - } - payload.events[1]._metadata = { 'big thing': makeBigObject() } - - expect(jsonPayload.event(payload).length).toBeGreaterThan(10e5) - }) -}) - -describe('jsonPayload.session', () => { - it('safe stringifies the payload', () => { - expect(jsonPayload.session({ - api_key: 'd145b8e5afb56516423bc4d605e45442', - events: [ - { - errorMessage: 'Failed load tickets', - errorClass: 'CheckoutError', - user: { - name: 'Jim Bug', - email: 'jim@bugsnag.com' - }, - request: { - api_key: '245b39ebd3cd3992e85bffc81c045924' - } - } - ] - }, ['api_key'])).toBe('{"api_key":"d145b8e5afb56516423bc4d605e45442","events":[{"errorMessage":"Failed load tickets","errorClass":"CheckoutError","user":{"name":"Jim Bug","email":"jim@bugsnag.com"},"request":{"api_key":"245b39ebd3cd3992e85bffc81c045924"}}]}') - }) -}) diff --git a/packages/core/lib/validators/int-range.js b/packages/core/lib/validators/int-range.js deleted file mode 100644 index 017819b3d6..0000000000 --- a/packages/core/lib/validators/int-range.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = (min = 1, max = Infinity) => value => - typeof value === 'number' && - parseInt('' + value, 10) === value && - value >= min && value <= max diff --git a/packages/core/lib/validators/list-of-functions.js b/packages/core/lib/validators/list-of-functions.js deleted file mode 100644 index 5492a99e2e..0000000000 --- a/packages/core/lib/validators/list-of-functions.js +++ /dev/null @@ -1,4 +0,0 @@ -const filter = require('../es-utils/filter') -const isArray = require('../es-utils/is-array') - -module.exports = value => typeof value === 'function' || (isArray(value) && filter(value, f => typeof f === 'function').length === value.length) diff --git a/packages/core/lib/validators/string-with-length.js b/packages/core/lib/validators/string-with-length.js deleted file mode 100644 index 199c2d759f..0000000000 --- a/packages/core/lib/validators/string-with-length.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = value => typeof value === 'string' && !!value.length diff --git a/packages/core/package.json b/packages/core/package.json index 996819b5fa..d651c286ab 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,8 +1,15 @@ { "name": "@bugsnag/core", - "main": "index.js", "version": "8.4.0", - "types": "types/index.d.ts", + "main": "dist/index.cjs", + "types": "dist/types/index.d.ts", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/index.mjs", + "default": "./dist/index.cjs" + } + }, "description": "Core classes and utilities for Bugsnag notifiers", "homepage": "https://www.bugsnag.com/", "repository": { @@ -13,17 +20,34 @@ "access": "public" }, "files": [ - "lib", + "src", "types", - "*.js" + "dist" ], + "scripts": { + "size": "../../bin/size dist/bugsnag.min.js", + "clean": "rm -fr dist && mkdir dist", + "build": "npm run clean && npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs" + }, "author": "Bugsnag", "license": "MIT", "dependencies": { "@bugsnag/cuid": "^3.0.0", "@bugsnag/safe-json-stringify": "^6.0.0", "error-stack-parser": "^2.0.3", - "iserror": "^0.0.2", "stack-generator": "^2.0.3" + }, + "devDependencies": { + "@babel/preset-env": "^7.26.8", + "@babel/preset-typescript": "^7.26.0", + "@rollup/plugin-babel": "^6.0.4", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-replace": "^6.0.2", + "@rollup/plugin-typescript": "^12.1.2", + "@types/jest": "^26", + "rollup": "^4.24.0", + "tslib": "^2.8.1" } } diff --git a/packages/core/rollup.config.npm.mjs b/packages/core/rollup.config.npm.mjs new file mode 100644 index 0000000000..655cf61e36 --- /dev/null +++ b/packages/core/rollup.config.npm.mjs @@ -0,0 +1,53 @@ +import babel from '@rollup/plugin-babel'; +import commonjs from '@rollup/plugin-commonjs' +import nodeResolve from '@rollup/plugin-node-resolve' +import typescript from '@rollup/plugin-typescript' + +import { sharedOutput } from "../../.rollup/index.mjs" + +const extensions = ['.js', '.ts'] + +const plugins = [ + nodeResolve({ + extensions + }), + commonjs(), + babel({ + babelHelpers: 'bundled', + exclude: 'node_modules/**', + extensions, + }), + typescript({ + noForceEmit: true, + }), +] + +const external = [/node_modules/] + +export default [ + { + input: "src/index.ts", + external, + output: [ + { + ...sharedOutput, + preserveModules: false, + entryFileNames: '[name].mjs', + format: 'esm' + } + ], + plugins + }, + { + input: "src/index.ts", + external, + output: [ + { + ...sharedOutput, + entryFileNames: '[name].cjs', + format: 'cjs', + }, + ], + plugins + } +]; diff --git a/packages/core/safe-json-stringify.d.ts b/packages/core/safe-json-stringify.d.ts new file mode 100644 index 0000000000..d56d7cea30 --- /dev/null +++ b/packages/core/safe-json-stringify.d.ts @@ -0,0 +1,11 @@ +declare module '@bugsnag/safe-json-stringify' { + export default function stringify( + value: any, + replacer?: null | ((this: any, key: string, value: any) => any), + space?: null | string | number, + options?: { + redactedKeys?: Array; + redactedPaths?: string[]; + } + ): string; +} \ No newline at end of file diff --git a/packages/core/session.d.ts b/packages/core/session.d.ts deleted file mode 100644 index c60d922ce3..0000000000 --- a/packages/core/session.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import Session from './types/session' - -interface MinimalEvent { - _handledState: { - unhandled: boolean - } -} - -interface SessionJson { - id: string - startedAt: Date - events: { - handled: number - unhandled: number - } -} - -export default class SessionWithInternals extends Session { - _track(event: MinimalEvent): void - toJSON(): SessionJson - - public _handled: number - public _unhandled: number -} diff --git a/packages/core/session.js b/packages/core/session.js deleted file mode 100644 index f078d76a59..0000000000 --- a/packages/core/session.js +++ /dev/null @@ -1,35 +0,0 @@ -const cuid = require('@bugsnag/cuid') - -class Session { - constructor () { - this.id = cuid() - this.startedAt = new Date() - this._handled = 0 - this._unhandled = 0 - this._user = {} - this.app = {} - this.device = {} - } - - getUser () { - return this._user - } - - setUser (id, email, name) { - this._user = { id, email, name } - } - - toJSON () { - return { - id: this.id, - startedAt: this.startedAt, - events: { handled: this._handled, unhandled: this._unhandled } - } - } - - _track (event) { - this[event._handledState.unhandled ? '_unhandled' : '_handled'] += 1 - } -} - -module.exports = Session diff --git a/packages/core/src/breadcrumb.ts b/packages/core/src/breadcrumb.ts new file mode 100644 index 0000000000..0fbe80b937 --- /dev/null +++ b/packages/core/src/breadcrumb.ts @@ -0,0 +1,19 @@ +import { BreadcrumbType } from "./common" + +export default class Breadcrumb { + constructor( + public readonly message: string, + public readonly metadata: { [key: string]: any }, + public readonly type: BreadcrumbType, + public readonly timestamp: Date = new Date() + ) {} + + toJSON () { + return { + type: this.type, + name: this.message, + timestamp: this.timestamp, + metaData: this.metadata + } + } +} diff --git a/packages/core/client.js b/packages/core/src/client.ts similarity index 56% rename from packages/core/client.js rename to packages/core/src/client.ts index bb58111b95..206b4efcef 100644 --- a/packages/core/client.js +++ b/packages/core/src/client.ts @@ -1,31 +1,103 @@ -const config = require('./config') -const Event = require('./event') -const Breadcrumb = require('./breadcrumb') -const Session = require('./session') -const map = require('./lib/es-utils/map') -const includes = require('./lib/es-utils/includes') -const filter = require('./lib/es-utils/filter') -const reduce = require('./lib/es-utils/reduce') -const keys = require('./lib/es-utils/keys') -const assign = require('./lib/es-utils/assign') -const runCallbacks = require('./lib/callback-runner') -const metadataDelegate = require('./lib/metadata-delegate') -const runSyncCallbacks = require('./lib/sync-callback-runner') -const BREADCRUMB_TYPES = require('./lib/breadcrumb-types') -const { add, clear, merge } = require('./lib/feature-flag-delegate') +import configSchema from './config' +import Event from './event' +import Breadcrumb from './breadcrumb' +import Session from './session' +import runCallbacks from './lib/callback-runner' +import metadataDelegate from './lib/metadata-delegate' +import runSyncCallbacks from './lib/sync-callback-runner' +import featureFlagDelegate from './lib/feature-flag-delegate' + +import { BreadcrumbType, BREADCRUMB_TYPES, Config, Delivery, FeatureFlag, LoggerConfig, NotifiableError, Notifier, OnBreadcrumbCallback, OnErrorCallback, OnSessionCallback, Plugin, SessionDelegate, User } from './common' + const HUB_PREFIX = '00000' const HUB_NOTIFY = 'https://notify.insighthub.smartbear.com' const HUB_SESSION = 'https://sessions.insighthub.smartbear.com' const noop = () => { } -class Client { - constructor (configuration, schema = config.schema, internalPlugins = [], notifier) { +interface LoggerConfig { + debug: (msg: any) => void + info: (msg: any) => void + warn: (msg: any) => void + error: (msg: any, err?: unknown) => void +} + +interface Notifier { + name: string + version: string + url: string +} + +interface EventDeliveryPayload { + apiKey: string + notifier: Notifier + events: Event[] +} + +interface SessionDeliveryPayload { + notifier?: Notifier + device?: Device + app?: App + sessions?: Array<{ + id: string + startedAt: Date + user?: User + }> +} + +interface Delivery { + sendEvent(payload: EventDeliveryPayload, cb: (err?: Error | null) => void): void + sendSession(session: SessionDeliveryPayload, cb: (err?: Error | null) => void): void +} + +export interface SessionDelegate { + startSession: (client: Client, session: Session) => Client + pauseSession: (client: Client) => void + resumeSession: (client: Client) => Client +} + +export default class Client { + public readonly _notifier?: Notifier + public readonly _config: T & Required + private readonly _schema: any + + public _delivery: Delivery + + private readonly _plugins: { [key: string]: any } + + public _breadcrumbs: Breadcrumb[] + public _session: Session | null + public _metadata: { [key: string]: any } + public _featuresIndex: { [key: string]: number } + public _features: Array + + private _context: string | undefined + + public _user: User + + public _logger: LoggerConfig + + public readonly _cbs: { + e: OnErrorCallback[] + s: OnSessionCallback[] + sp: any[] + b: OnBreadcrumbCallback[] + } + + public readonly Client: typeof Client + public readonly Event: typeof Event + public readonly Breadcrumb: typeof Breadcrumb + public readonly Session: typeof Session + + public _depth: number + + public _pausedSession?: Session | null + public _sessionDelegate?: SessionDelegate + + constructor (configuration: T, schema = configSchema, internalPlugins: Plugin[] = [], notifier?: Notifier) { // notifier id this._notifier = notifier - // intialise opts and config - this._config = {} this._schema = schema // i/o @@ -66,44 +138,47 @@ class Client { this.Session = Session this._config = this._configure(configuration, internalPlugins) - map(internalPlugins.concat(this._config.plugins), pl => { + internalPlugins.concat(this._config.plugins).map(pl => { if (pl) this._loadPlugin(pl) }) // when notify() is called we need to know how many frames are from our own source - // this inital value is 1 not 0 because we wrap notify() to ensure it is always + // this initial value is 1 not 0 because we wrap notify() to ensure it is always // bound to have the client as its `this` value – see below. this._depth = 1 + // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this const notify = this.notify this.notify = function () { + // @ts-ignore + // eslint-disable-next-line prefer-rest-params return notify.apply(self, arguments) } } - addMetadata (section, keyOrObj, maybeVal) { + addMetadata (section: string, keyOrObj: object | string, maybeVal?: any) { return metadataDelegate.add(this._metadata, section, keyOrObj, maybeVal) } - getMetadata (section, key) { + getMetadata (section: string, key?: string) { return metadataDelegate.get(this._metadata, section, key) } - clearMetadata (section, key) { + clearMetadata (section: string, key?: string) { return metadataDelegate.clear(this._metadata, section, key) } - addFeatureFlag (name, variant = null) { - add(this._features, this._featuresIndex, name, variant) + addFeatureFlag (name: string, variant: string | null = null) { + featureFlagDelegate.add(this._features, this._featuresIndex, name, variant) } - addFeatureFlags (featureFlags) { - merge(this._features, featureFlags, this._featuresIndex) + addFeatureFlags (featureFlags: FeatureFlag[]) { + featureFlagDelegate.merge(this._features, featureFlags, this._featuresIndex) } - clearFeatureFlag (name) { - clear(this._features, this._featuresIndex, name) + clearFeatureFlag (name: string) { + featureFlagDelegate.clear(this._features, this._featuresIndex, name) } clearFeatureFlags () { @@ -115,7 +190,11 @@ class Client { return this._context } - setContext (c) { + getNotifier () { + return this._notifier + } + + setContext (c: string) { this._context = c } @@ -129,9 +208,9 @@ class Client { return previousValue } - _configure (opts, internalPlugins) { - const schema = reduce(internalPlugins, (schema, plugin) => { - if (plugin && plugin.configSchema) return assign({}, schema, plugin.configSchema) + _configure (opts: T, internalPlugins: Plugin[]) { + const schema = internalPlugins.reduce((schema, plugin) => { + if (plugin && plugin.configSchema) return Object.assign({}, schema, plugin.configSchema) return schema }, this._schema) @@ -141,7 +220,7 @@ class Client { } // accumulate configuration and error messages - const { errors, config } = reduce(keys(schema), (accum, key) => { + const { errors, config } = (Object.keys(schema) as (keyof T)[]).reduce((accum: {errors: Record, config: Record}, key: keyof typeof opts) => { const defaultValue = schema[key].defaultValue(opts[key]) if (opts[key] !== undefined) { @@ -151,7 +230,7 @@ class Client { accum.config[key] = defaultValue } else { if (schema[key].allowPartialObject) { - accum.config[key] = assign(defaultValue, opts[key]) + accum.config[key] = Object.assign(defaultValue, opts[key]) } else { accum.config[key] = opts[key] } @@ -178,9 +257,9 @@ class Client { } // update and elevate some options - this._metadata = assign({}, config.metadata) - merge(this._features, config.featureFlags, this._featuresIndex) - this._user = assign({}, config.user) + this._metadata = Object.assign({}, config.metadata) + featureFlagDelegate.merge(this._features, config.featureFlags, this._featuresIndex) + this._user = Object.assign({}, config.user) this._context = config.context if (config.logger) this._logger = config.logger @@ -190,22 +269,22 @@ class Client { if (config.onSession) this._cbs.s = this._cbs.s.concat(config.onSession) // finally warn about any invalid config where we fell back to the default - if (keys(errors).length) { + if (Object.keys(errors).length) { this._logger.warn(generateConfigErrorMessage(errors, opts)) } - return config + return config as T & Required } getUser () { return this._user } - setUser (id, email, name) { + setUser (id?: string | null, email?: string | null, name?: string | null) { this._user = { id, email, name } } - _loadPlugin (plugin) { + _loadPlugin (plugin: Plugin) { const result = plugin.load(this) // JS objects are not the safest way to store arbitrarily keyed values, // so bookend the key with some characters that prevent tampering with @@ -214,11 +293,11 @@ class Client { if (plugin.name) this._plugins[`~${plugin.name}~`] = result } - getPlugin (name) { + getPlugin (name: string) { return this._plugins[`~${name}~`] } - _setDelivery (d) { + _setDelivery (d: (client: Client) => Delivery) { this._delivery = d(this) } @@ -229,7 +308,7 @@ class Client { session.app.version = this._config.appVersion session.app.type = this._config.appType - session._user = assign({}, this._user) + session._user = Object.assign({}, this._user) // run onSession callbacks const ignore = runSyncCallbacks(this._cbs.s, session, 'onSession', this._logger) @@ -239,49 +318,49 @@ class Client { return this } - return this._sessionDelegate.startSession(this, session) + return this._sessionDelegate?.startSession(this, session) } - addOnError (fn, front = false) { + addOnError (fn: OnErrorCallback, front = false) { this._cbs.e[front ? 'unshift' : 'push'](fn) } - removeOnError (fn) { - this._cbs.e = filter(this._cbs.e, f => f !== fn) + removeOnError (fn: OnErrorCallback) { + this._cbs.e = this._cbs.e.filter( f => f !== fn) } - _addOnSessionPayload (fn) { + _addOnSessionPayload (fn: OnSessionCallback) { this._cbs.sp.push(fn) } - addOnSession (fn) { + addOnSession (fn: OnSessionCallback) { this._cbs.s.push(fn) } - removeOnSession (fn) { - this._cbs.s = filter(this._cbs.s, f => f !== fn) + removeOnSession (fn: OnSessionCallback) { + this._cbs.s = this._cbs.s.filter( f => f !== fn) } - addOnBreadcrumb (fn, front = false) { + addOnBreadcrumb (fn: OnBreadcrumbCallback, front = false) { this._cbs.b[front ? 'unshift' : 'push'](fn) } - removeOnBreadcrumb (fn) { - this._cbs.b = filter(this._cbs.b, f => f !== fn) + removeOnBreadcrumb (fn: OnBreadcrumbCallback) { + this._cbs.b = this._cbs.b.filter( f => f !== fn) } pauseSession () { - return this._sessionDelegate.pauseSession(this) + return this._sessionDelegate?.pauseSession(this) } resumeSession () { - return this._sessionDelegate.resumeSession(this) + return this._sessionDelegate?.resumeSession(this) } - leaveBreadcrumb (message, metadata, type) { + leaveBreadcrumb (message: string, metadata?: any, type?: BreadcrumbType) { // coerce bad values so that the defaults get set message = typeof message === 'string' ? message : '' - type = (typeof type === 'string' && includes(BREADCRUMB_TYPES, type)) ? type : 'manual' + type = (typeof type === 'string' && BREADCRUMB_TYPES.indexOf(type) !== -1) ? type : 'manual' metadata = typeof metadata === 'object' && metadata !== null ? metadata : {} // if no message, discard @@ -304,45 +383,45 @@ class Client { } } - _isBreadcrumbTypeEnabled (type) { + _isBreadcrumbTypeEnabled (type: any) { const types = this._config.enabledBreadcrumbTypes - return types === null || includes(types, type) + return types === null || types.indexOf(type) !== -1 } - notify (maybeError, onError, postReportCallback = noop) { + notify (maybeError: NotifiableError, onError?: OnErrorCallback, postReportCallback: (err: Error | null | undefined, event: Event) => void = noop) { const event = Event.create(maybeError, true, undefined, 'notify()', this._depth + 1, this._logger) this._notify(event, onError, postReportCallback) } - _notify (event, onError, postReportCallback = noop) { - event.app = assign({}, event.app, { + _notify (event: Event, onError?: OnErrorCallback, postReportCallback: (err: Error | null | undefined, event: Event) => void = noop) { + event.app = Object.assign({}, event.app, { releaseStage: this._config.releaseStage, version: this._config.appVersion, type: this._config.appType }) event.context = event.context || this._context - event._metadata = assign({}, event._metadata, this._metadata) - event._user = assign({}, event._user, this._user) + event._metadata = Object.assign({}, event._metadata, this._metadata) + event._user = Object.assign({}, event._user, this._user) event.breadcrumbs = this._breadcrumbs.slice() event.setGroupingDiscriminator(this._groupingDiscriminator) - merge(event._features, this._features, event._featuresIndex) + featureFlagDelegate.merge(event._features, this._features, event._featuresIndex) // exit early if events should not be sent on the current releaseStage - if (this._config.enabledReleaseStages !== null && !includes(this._config.enabledReleaseStages, this._config.releaseStage)) { + if (this._config.enabledReleaseStages !== null && this._config.enabledReleaseStages.indexOf(this._config.releaseStage) === -1) { this._logger.warn('Event not sent due to releaseStage/enabledReleaseStages configuration') return postReportCallback(null, event) } const originalSeverity = event.severity - const onCallbackError = err => { + const onCallbackError = (err: Error) => { // errors in callbacks are tolerated but we want to log them out this._logger.error('Error occurred in onError callback, continuing anyway…') this._logger.error(err) } - const callbacks = [].concat(this._cbs.e).concat(onError) + const callbacks = ([] as (OnErrorCallback | undefined)[]).concat(this._cbs.e).concat(onError) runCallbacks(callbacks, event, onCallbackError, (err, shouldSend) => { if (err) onCallbackError(err) @@ -376,20 +455,21 @@ class Client { this._delivery.sendEvent({ apiKey: event.apiKey || this._config.apiKey, - notifier: this._notifier, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + notifier: this._notifier!, events: [event] }, (err) => postReportCallback(err, event)) }) } } -const generateConfigErrorMessage = (errors, rawInput) => { +const generateConfigErrorMessage = (errors: Record, rawInput: Config) => { const er = new Error( - `Invalid configuration\n${map(keys(errors), key => ` - ${key} ${errors[key]}, got ${stringify(rawInput[key])}`).join('\n\n')}`) + `Invalid configuration\n${(Object.keys(errors) as unknown as (keyof Config)[]).map(key => ` - ${key} ${errors[key]}, got ${stringify(rawInput[key])}`).join('\n\n')}`) return er } -const stringify = val => { +const stringify = (val: unknown) => { switch (typeof val) { case 'string': case 'number': @@ -398,5 +478,3 @@ const stringify = val => { default: return String(val) } } - -module.exports = Client diff --git a/packages/core/types/common.d.ts b/packages/core/src/common.ts similarity index 61% rename from packages/core/types/common.d.ts rename to packages/core/src/common.ts index 4fd333d235..8d14fae067 100644 --- a/packages/core/types/common.d.ts +++ b/packages/core/src/common.ts @@ -1,7 +1,11 @@ -import Client from './client' -import Event from './event' -import Session from './session' -import Breadcrumb from './breadcrumb' +import Breadcrumb from "./breadcrumb"; +import Client from "./client"; +import Session from "./session"; +import Event from "./event"; + +export type BreadcrumbType = 'error' | 'log' | 'manual' | 'navigation' | 'process' | 'request' | 'state' | 'user'; + +export const BREADCRUMB_TYPES = ['navigation', 'request', 'process', 'log', 'user', 'state', 'error', 'manual'] as const; export interface Config { apiKey: string @@ -36,10 +40,17 @@ export type OnErrorCallback = (event: Event, cb: (err: null | Error, shouldSend? export type OnSessionCallback = (session: Session) => void | boolean; export type OnBreadcrumbCallback = (breadcrumb: Breadcrumb) => void | boolean; -export interface Plugin { +export interface Plugin { name?: string - load: (client: Client) => any + load: (client: Client) => any destroy?(): void + configSchema?: { + [key: string]: { + defaultValue: () => unknown + message: string + validate: (value: unknown) => boolean + } + } } export interface Logger { @@ -49,20 +60,6 @@ export interface Logger { error: (...args: any[]) => void } -export interface SessionDelegate { - startSession: (client: Client) => Client -} - -export interface EventPayload { - apiKey: string - notifier: { - name: string - version: string - url: string - } - events: Event[] -} - export interface SessionPayload { notifier: { name: string @@ -80,9 +77,7 @@ export type NotifiableError = Error | { name: string, message: string } | string -export type BreadcrumbType = 'error' | 'log' | 'manual' | 'navigation' | 'process' | 'request' | 'state' | 'user'; - -interface Device { +export interface Device { id?: string hostname?: string locale?: string @@ -100,7 +95,7 @@ interface Device { [key: string]: any } -interface App { +export interface App { codeBundleId?: string duration?: number durationInForeground?: number @@ -111,7 +106,7 @@ interface App { [key: string]: any } -interface Request { +export interface Request { clientIp?: string headers?: { [key: string]: string } httpMethod?: string @@ -121,9 +116,9 @@ interface Request { } export interface User { - id?: string - email?: string - name?: string + id?: string | null + email?: string | null + name?: string | null } type ThreadType = 'cocoa' | 'android' | 'browserJs' @@ -149,3 +144,58 @@ export interface FeatureFlag { name: string variant?: string | null } + +export interface LoggerConfig { + debug: (msg: any) => void + info: (msg: any) => void + warn: (msg: any) => void + error: (msg: any, err?: unknown) => void +} + +export interface Notifier { + name: string + version: string + url: string +} + +export interface EventDeliveryPayload { + apiKey?: string + payloadVersion?: string + notifier: Notifier + events: Event[] +} + +export interface SessionDeliveryPayload { + notifier?: Notifier + device?: Device + app?: App + sessions?: Array<{ + id: string + startedAt: Date + user?: User + }> +} + +export interface Delivery { + sendEvent(payload: EventDeliveryPayload, cb: (err?: Error | null) => void): void + sendSession(session: SessionDeliveryPayload, cb: (err?: Error | null) => void): void +} + +export interface SessionDelegate { + startSession: (client: Client, session: Session) => Client + pauseSession: (client: Client) => void + resumeSession: (client: Client) => Client +} + +export interface BugsnagStatic extends Client { + start(apiKeyOrOpts: string | Config): Client + createClient(apiKeyOrOpts: string | Config): Client + isStarted(): boolean +} + +export interface BugsnagError { + errorClass: string + errorMessage: string + type: string + stacktrace: Stackframe[] +} diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts new file mode 100644 index 0000000000..b38302a120 --- /dev/null +++ b/packages/core/src/config.ts @@ -0,0 +1,299 @@ +import intRange from './lib/validators/int-range' +import stringWithLength from './lib/validators/string-with-length' +import listOfFunctions from './lib/validators/list-of-functions' + +import { BREADCRUMB_TYPES } from './common' + +const defaultErrorTypes = () => ({ unhandledExceptions: true, unhandledRejections: true }) + +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: () => typeof BREADCRUMB_TYPES + 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 + }, + sendPayloadChecksums: { + defaultValue: () => false, + message: string, + validate: (value: unknown) => boolean + } +} + +const schema: Schema = { + apiKey: { + defaultValue: () => null, + message: 'is required', + validate: stringWithLength + }, + appVersion: { + defaultValue: () => undefined, + message: 'should be a string', + validate: (value: unknown) => value === undefined || stringWithLength(value) + }, + appType: { + defaultValue: () => undefined, + message: 'should be a string', + validate: (value: unknown) => value === undefined || stringWithLength(value) + }, + autoDetectErrors: { + defaultValue: () => true, + message: 'should be true|false', + validate: (value: unknown) => value === true || value === false + }, + enabledErrorTypes: { + defaultValue: () => defaultErrorTypes(), + message: 'should be an object containing the flags { unhandledExceptions:true|false, unhandledRejections:true|false }', + allowPartialObject: true, + validate: (value: unknown) => { + // ensure we have an object + if (typeof value !== 'object' || !value) return false + const providedKeys = Object.keys(value) + const defaultKeys = Object.keys(defaultErrorTypes()) + // ensure it only has a subset of the allowed keys + if (providedKeys.filter( k => defaultKeys.indexOf(k) !== -1).length < providedKeys.length) return false + // ensure all of the values are boolean + if (providedKeys.filter(k => typeof value[k as keyof typeof value] !== 'boolean').length > 0) return false + return true + } + }, + onError: { + defaultValue: () => [], + message: 'should be a function or array of functions', + validate: listOfFunctions + }, + onSession: { + defaultValue: () => [], + message: 'should be a function or array of functions', + validate: listOfFunctions + }, + onBreadcrumb: { + defaultValue: () => [], + message: 'should be a function or array of functions', + validate: listOfFunctions + }, + endpoints: { + defaultValue: (endpoints?: unknown) => { + // only apply the default value if no endpoints have been provided, otherwise prevent delivery by setting to null + if (typeof endpoints === 'undefined') { + return ({ + notify: 'https://notify.bugsnag.com', + sessions: 'https://sessions.bugsnag.com' + }) + } else { + return ({ notify: null, sessions: null }) + } + }, + message: 'should be an object containing endpoint URLs { notify, sessions }', + validate: (val: unknown) => + // first, ensure it's an object + !!(val && typeof val === 'object') && + ( + // notify and sessions must always be set + 'notify' in val && stringWithLength(val.notify) && 'sessions' in val && stringWithLength(val.sessions) + ) && + // ensure no keys other than notify/session are set on endpoints object + Object.keys(val).filter(k => ['notify', 'sessions'].indexOf(k) === -1).length === 0 + }, + autoTrackSessions: { + defaultValue: () => true, + message: 'should be true|false', + validate: (val: unknown) => val === true || val === false + }, + enabledReleaseStages: { + defaultValue: () => null, + message: 'should be an array of strings', + validate: (value: unknown) => value === null || (Array.isArray(value) && value.filter(f => typeof f === 'string').length === value.length) + }, + releaseStage: { + defaultValue: () => 'production', + message: 'should be a string', + validate: (value: unknown) => typeof value === 'string' && !!value.length + }, + maxBreadcrumbs: { + defaultValue: () => 25, + message: 'should be a number ≤100', + validate: (value: unknown) => intRange(0, 100)(value) + }, + enabledBreadcrumbTypes: { + defaultValue: () => BREADCRUMB_TYPES, + message: `should be null or a list of available breadcrumb types (${BREADCRUMB_TYPES.join(',')})`, + validate: (value: unknown) => value === null || (Array.isArray(value) && value.reduce((accum, maybeType) => { + if (accum === false) return accum + // TS doesn't like passing a readonly to a function that might mutate an array + return BREADCRUMB_TYPES.indexOf(maybeType) !== -1 + }, true)) + }, + context: { + defaultValue: () => undefined, + message: 'should be a string', + validate: (value: unknown) => value === undefined || typeof value === 'string' + }, + user: { + defaultValue: () => ({}), + message: 'should be an object with { id, email, name } properties', + validate: (value: unknown) => + (value === null) || + !!(value && Object.keys(value).reduce( + (accum, key) => accum && ['id', 'email', 'name'].indexOf(key) !== -1, + true + )) + }, + metadata: { + defaultValue: () => ({}), + message: 'should be an object', + validate: (value: unknown) => typeof value === 'object' && value !== null + }, + logger: { + defaultValue: () => undefined, + message: 'should be null or an object with methods { debug, info, warn, error }', + validate: (value: unknown) => + (!value) || + (value && ['debug', 'info', 'warn', 'error'].reduce( + // @ts-expect-error - TS doesn't know that value is an object + (accum, method) => accum && typeof value[method] === 'function', + true + )) + }, + redactedKeys: { + defaultValue: () => ['password'], + message: 'should be an array of strings|regexes', + validate: (value: unknown) => + Array.isArray(value) && value.length === value.filter(s => + (typeof s === 'string' || (s && typeof s.test === 'function')) + ).length + }, + plugins: { + defaultValue: () => ([]), + message: 'should be an array of plugin objects', + validate: (value: unknown) => + Array.isArray(value) && value.length === value.filter(p => + (p && typeof p === 'object' && typeof p.load === 'function') + ).length + }, + featureFlags: { + defaultValue: () => [], + message: 'should be an array of objects that have a "name" property', + validate: (value: unknown) => + Array.isArray(value) && value.length === value.filter(feature => + feature && typeof feature === 'object' && typeof feature.name === 'string' + ).length + }, + reportUnhandledPromiseRejectionsAsHandled: { + defaultValue: () => false, + message: 'should be true|false', + validate: (value: unknown) => value === true || value === false + }, + sendPayloadChecksums: { + defaultValue: () => false, + message: 'should be true|false', + validate: (value: unknown) => value === true || value === false + } +} + +export default schema diff --git a/packages/core/event.js b/packages/core/src/event.ts similarity index 68% rename from packages/core/event.js rename to packages/core/src/event.ts index 9db8c8e743..15eb3b02b3 100644 --- a/packages/core/event.js +++ b/packages/core/src/event.ts @@ -1,22 +1,60 @@ -const ErrorStackParser = require('./lib/error-stack-parser') -const StackGenerator = require('stack-generator') -const hasStack = require('./lib/has-stack') -const map = require('./lib/es-utils/map') -const reduce = require('./lib/es-utils/reduce') -const filter = require('./lib/es-utils/filter') -const assign = require('./lib/es-utils/assign') -const metadataDelegate = require('./lib/metadata-delegate') -const featureFlagDelegate = require('./lib/feature-flag-delegate') -const isError = require('./lib/iserror') - -class Event { - constructor (errorClass, errorMessage, stacktrace = [], handledState = defaultHandledState(), originalError) { +import { App, Device, FeatureFlag, Logger, Request, Stackframe, Thread, User, BugsnagError, NotifiableError } from "./common" + +import ErrorStackParser from 'error-stack-parser' +// @ts-expect-error no types +import StackGenerator from 'stack-generator' +import hasStack from './lib/has-stack' + +import metadataDelegate from './lib/metadata-delegate' +import featureFlagDelegate from './lib/feature-flag-delegate' +import isError from './lib/iserror' +import Breadcrumb from "./breadcrumb" +import Session from "./session" + +interface HandledState { + unhandled: boolean + severity: string + severityReason: { type: string; unhandledOverridden?: boolean } +} + +export default class Event { + public apiKey: string | undefined + public context: string | undefined + public groupingHash: string | undefined + public severity: string + public unhandled: boolean + + public app: App + public device: Device + public request: Request + + public errors: BugsnagError[]; + public breadcrumbs: Breadcrumb[] + public threads: Thread[] + + public _metadata: { [key: string]: any } + public _features: (FeatureFlag | null)[] + public _featuresIndex: { [key: string]: number } + + public _user: User + private _correlation?: { spanId?: string, traceId: string } + public _session?: Session + + // default value for stacktrace.type + public static __type: string = 'browserjs' + + constructor ( + public readonly errorClass: string, + public readonly errorMessage: string, + public readonly stacktrace: any[] = [], + public readonly _handledState: HandledState = defaultHandledState(), + public readonly originalError?: NotifiableError + ) { this.apiKey = undefined this.context = undefined this.groupingHash = undefined this.originalError = originalError - this._handledState = handledState this.severity = this._handledState.severity this.unhandled = this._handledState.unhandled @@ -46,7 +84,7 @@ class Event { /* this.attemptImmediateDelivery, default: true */ } - addMetadata (section, keyOrObj, maybeVal) { + addMetadata (section: string, keyOrObj?: object | string, maybeVal?: any) { return metadataDelegate.add(this._metadata, section, keyOrObj, maybeVal) } @@ -57,7 +95,7 @@ class Event { * @param traceId the ID of the trace the event occurred within * @param spanId the ID of the span that the event occurred within */ - setTraceCorrelation (traceId, spanId) { + setTraceCorrelation (traceId?: string, spanId?: string) { if (typeof traceId === 'string') { this._correlation = { traceId, ...typeof spanId === 'string' ? { spanId } : { } } } @@ -73,19 +111,19 @@ class Event { return previousValue } - getMetadata (section, key) { + getMetadata (section: string, key?: string) { return metadataDelegate.get(this._metadata, section, key) } - clearMetadata (section, key) { + clearMetadata (section: string, key?: string) { return metadataDelegate.clear(this._metadata, section, key) } - addFeatureFlag (name, variant = null) { + addFeatureFlag (name: string, variant: string | null = null) { featureFlagDelegate.add(this._features, this._featuresIndex, name, variant) } - addFeatureFlags (featureFlags) { + addFeatureFlags (featureFlags: FeatureFlag[]) { featureFlagDelegate.merge(this._features, featureFlags, this._featuresIndex) } @@ -93,7 +131,7 @@ class Event { return featureFlagDelegate.toEventApi(this._features) } - clearFeatureFlag (name) { + clearFeatureFlag (name: string) { featureFlagDelegate.clear(this._features, this._featuresIndex, name) } @@ -106,14 +144,14 @@ class Event { return this._user } - setUser (id, email, name) { + setUser (id?: string | null, email?: string | null, name?: string | null) { this._user = { id, email, name } } toJSON () { return { payloadVersion: '4', - exceptions: map(this.errors, er => assign({}, er, { message: er.errorMessage })), + exceptions: this.errors.map(er => Object.assign({}, er, { message: er.errorMessage })), severity: this.severity, unhandled: this._handledState.unhandled, severityReason: this._handledState.severityReason, @@ -131,11 +169,73 @@ class Event { correlation: this._correlation } } + + // Helpers +static getStacktrace = function (error: Error, errorFramesToSkip: number, backtraceFramesToSkip: number) { + if (hasStack(error)) return ErrorStackParser.parse(error).slice(errorFramesToSkip) + // error wasn't provided or didn't have a stacktrace so try to walk the callstack + try { + return StackGenerator.backtrace().filter((frame: StackTraceJsStyleStackframe) => + (frame.functionName || '').indexOf('StackGenerator$$') === -1 + ).slice(1 + backtraceFramesToSkip) + } catch (e) { + return [] + } +} + +static create = function (maybeError: NotifiableError, tolerateNonErrors: boolean, handledState: HandledState | undefined, component: string, errorFramesToSkip = 0, logger?: Logger) { + const [error, internalFrames] = normaliseError(maybeError, tolerateNonErrors, component, logger) + let event + try { + const stacktrace = Event.getStacktrace( + error, + // if an error was created/throw in the normaliseError() function, we need to + // tell the getStacktrace() function to skip the number of frames we know will + // be from our own functions. This is added to the number of frames deep we + // were told about + internalFrames > 0 ? 1 + internalFrames + errorFramesToSkip : 0, + // if there's no stacktrace, the callstack may be walked to generated one. + // this is how many frames should be removed because they come from our library + 1 + errorFramesToSkip + ) + event = new Event(error.name, error.message, stacktrace, handledState, maybeError) + } catch (e) { + event = new Event(error.name, error.message, [], handledState, maybeError) + } + if (error.name === 'InvalidError') { + event.addMetadata(`${component}`, 'non-error parameter', makeSerialisable(maybeError)) + } + if (error.cause) { + const causes = getCauseStack(error).slice(1) + const normalisedCauses = causes.map((cause) => { + // Only get stacktrace for error causes that are a valid JS Error and already have a stack + const stacktrace = (isError(cause) && hasStack(cause)) ? ErrorStackParser.parse(cause) : [] + const [error] = normaliseError(cause, true, 'error cause') + if (error.name === 'InvalidError') event.addMetadata('error cause', makeSerialisable(cause)) + return createBugsnagError(error.name, error.message, Event.__type, stacktrace) + }) + + event.errors.push(...normalisedCauses) + } + + return event +} +} + +interface StackTraceJsStyleStackframe { + functionName: string, + args: string[], + fileName: string, + lineNumber: number, + columnNumber: number, + isEval: boolean, + isNative: boolean, + source: string, } // takes a stacktrace.js style stackframe (https://github.com/stacktracejs/stackframe) // and returns a Bugsnag compatible stackframe (https://docs.bugsnag.com/api/error-reporting/#json-payload) -const formatStackframe = frame => { +const formatStackframe = (frame: StackTraceJsStyleStackframe): Stackframe => { const f = { file: frame.fileName, method: normaliseFunctionName(frame.functionName), @@ -154,7 +254,7 @@ const formatStackframe = frame => { return f } -const normaliseFunctionName = name => /^global code$/i.test(name) ? 'global code' : name +const normaliseFunctionName = (name: string) => /^global code$/i.test(name) ? 'global code' : name const defaultHandledState = () => ({ unhandled: false, @@ -162,14 +262,14 @@ const defaultHandledState = () => ({ severityReason: { type: 'handledException' } }) -const ensureString = (str) => typeof str === 'string' ? str : '' +const ensureString = (str: unknown): string => typeof str === 'string' ? str : '' -function createBugsnagError (errorClass, errorMessage, type, stacktrace) { +function createBugsnagError (errorClass: unknown, errorMessage: unknown, type: string, stacktrace: any[]): BugsnagError { return { errorClass: ensureString(errorClass), errorMessage: ensureString(errorMessage), type, - stacktrace: reduce(stacktrace, (accum, frame) => { + stacktrace: stacktrace.reduce((accum, frame) => { const f = formatStackframe(frame) // don't include a stackframe if none of its properties are defined try { @@ -182,77 +282,25 @@ function createBugsnagError (errorClass, errorMessage, type, stacktrace) { } } -function getCauseStack (error) { +function getCauseStack (error: Error): Error[] { if (error.cause) { - return [error, ...getCauseStack(error.cause)] + return [error, ...getCauseStack(error.cause as Error)] } else { return [error] } } -// Helpers - -Event.getStacktrace = function (error, errorFramesToSkip, backtraceFramesToSkip) { - if (hasStack(error)) return ErrorStackParser.parse(error).slice(errorFramesToSkip) - // error wasn't provided or didn't have a stacktrace so try to walk the callstack - try { - return filter(StackGenerator.backtrace(), frame => - (frame.functionName || '').indexOf('StackGenerator$$') === -1 - ).slice(1 + backtraceFramesToSkip) - } catch (e) { - return [] - } -} - -Event.create = function (maybeError, tolerateNonErrors, handledState, component, errorFramesToSkip = 0, logger) { - const [error, internalFrames] = normaliseError(maybeError, tolerateNonErrors, component, logger) - let event - try { - const stacktrace = Event.getStacktrace( - error, - // if an error was created/throw in the normaliseError() function, we need to - // tell the getStacktrace() function to skip the number of frames we know will - // be from our own functions. This is added to the number of frames deep we - // were told about - internalFrames > 0 ? 1 + internalFrames + errorFramesToSkip : 0, - // if there's no stacktrace, the callstack may be walked to generated one. - // this is how many frames should be removed because they come from our library - 1 + errorFramesToSkip - ) - event = new Event(error.name, error.message, stacktrace, handledState, maybeError) - } catch (e) { - event = new Event(error.name, error.message, [], handledState, maybeError) - } - if (error.name === 'InvalidError') { - event.addMetadata(`${component}`, 'non-error parameter', makeSerialisable(maybeError)) - } - if (error.cause) { - const causes = getCauseStack(error).slice(1) - const normalisedCauses = map(causes, (cause) => { - // Only get stacktrace for error causes that are a valid JS Error and already have a stack - const stacktrace = (isError(cause) && hasStack(cause)) ? ErrorStackParser.parse(cause) : [] - const [error] = normaliseError(cause, true, 'error cause') - if (error.name === 'InvalidError') event.addMetadata('error cause', makeSerialisable(cause)) - return createBugsnagError(error.name, error.message, Event.__type, stacktrace) - }) - - event.errors.push(...normalisedCauses) - } - - return event -} - -const makeSerialisable = (err) => { +const makeSerialisable = (err?: any) => { if (err === null) return 'null' if (err === undefined) return 'undefined' return err } -const normaliseError = (maybeError, tolerateNonErrors, component, logger) => { +const normaliseError = (maybeError: unknown, tolerateNonErrors: boolean, component: string, logger?: Logger): [Error, number] => { let error let internalFrames = 0 - const createAndLogInputError = (reason) => { + const createAndLogInputError = (reason: string) => { const verb = (component === 'error cause' ? 'was' : 'received') if (logger) logger.warn(`${component} ${verb} a non-error: "${reason}"`) const err = new Error(`${component} ${verb} a non-error. See "${component}" tab for more detail.`) @@ -292,7 +340,7 @@ const normaliseError = (maybeError, tolerateNonErrors, component, logger) => { error = maybeError } else if (maybeError !== null && hasNecessaryFields(maybeError)) { error = new Error(maybeError.message || maybeError.errorMessage) - error.name = maybeError.name || maybeError.errorClass + error.name = maybeError.name || maybeError.errorClass || '' internalFrames += 1 } else { error = createAndLogInputError(maybeError === null ? 'null' : 'unsupported object') @@ -320,14 +368,12 @@ const normaliseError = (maybeError, tolerateNonErrors, component, logger) => { } } - return [error, internalFrames] + return [error as Error, internalFrames] } -// default value for stacktrace.type -Event.__type = 'browserjs' - -const hasNecessaryFields = error => +const hasNecessaryFields = (error: unknown): error is { name?: string; errorClass?: string; message?: string; errorMessage?: string } => + // @ts-expect-error - needs rewriting to be more type safe (typeof error.name === 'string' || typeof error.errorClass === 'string') && + // @ts-expect-error - needs rewriting to be more type safe (typeof error.message === 'string' || typeof error.errorMessage === 'string') -module.exports = Event diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000000..d3f62b3a60 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,18 @@ +export { default as Breadcrumb } from './breadcrumb' +export { default as Client } from './client' +export { default as Event } from './event' +export { default as Session } from './session' +export { default as cloneClient } from './lib/clone-client' +export { default as featureFlagDelegate } from './lib/feature-flag-delegate' +export { default as metadataDelegate } from './lib/metadata-delegate' +export { default as nodeFallbackStack } from './lib/node-fallback-stack' +export { default as schema } from './config' +export { default as extractObject } from './lib/extract-object' +export { default as intRange } from './lib/validators/int-range' +export { default as isError } from './lib/iserror' +export { default as listOfFunctions } from './lib/validators/list-of-functions' +export { default as runCallbacks } from './lib/callback-runner' +export { default as runSyncCallbacks } from './lib/sync-callback-runner' +export { default as stringWithLength } from './lib/validators/string-with-length' + +export * from './common' diff --git a/packages/core/lib/async-every.js b/packages/core/src/lib/async-every.ts similarity index 77% rename from packages/core/lib/async-every.js rename to packages/core/src/lib/async-every.ts index 4b5fb78290..0859c0bfac 100644 --- a/packages/core/lib/async-every.js +++ b/packages/core/src/lib/async-every.ts @@ -1,3 +1,5 @@ +export type NodeCallbackType = (error?: Error | null, result?: T) => void; + // This is a heavily modified/simplified version of // https://github.com/othiym23/async-some // with the logic flipped so that it is akin to the @@ -9,7 +11,11 @@ // - or the end of the array is reached // the callback (cb) will be passed (null, false) if any of the items in arr // caused fn to call back with false, otherwise it will be passed (null, true) -module.exports = (arr, fn, cb) => { +const every = ( + arr: T[], + fn: (item: T, cb: NodeCallbackType) => void, + cb: NodeCallbackType +): void => { let index = 0 const next = () => { @@ -24,3 +30,5 @@ module.exports = (arr, fn, cb) => { next() } + +export default every diff --git a/packages/core/src/lib/breadcrumb-types.ts b/packages/core/src/lib/breadcrumb-types.ts new file mode 100644 index 0000000000..1d5ec648fd --- /dev/null +++ b/packages/core/src/lib/breadcrumb-types.ts @@ -0,0 +1 @@ +export default ['navigation', 'request', 'process', 'log', 'user', 'state', 'error', 'manual'] diff --git a/packages/core/lib/callback-runner.js b/packages/core/src/lib/callback-runner.ts similarity index 64% rename from packages/core/lib/callback-runner.js rename to packages/core/src/lib/callback-runner.ts index ca938083e8..7cf6e326c9 100644 --- a/packages/core/lib/callback-runner.js +++ b/packages/core/src/lib/callback-runner.ts @@ -1,12 +1,18 @@ -const some = require('./async-every') +import every from './async-every' +import type { NodeCallbackType } from './async-every' -module.exports = (callbacks, event, onCallbackError, cb) => { +const runCallbacks = ( + callbacks: any[], + event: T, + onCallbackError: (err: Error) => void, + cb: NodeCallbackType +): void => { // This function is how we support different kinds of callback: // - synchronous - return value // - node-style async with callback - cb(err, value) // - promise/thenable - resolve(value) // It normalises each of these into the lowest common denominator – a node-style callback - const runMaybeAsyncCallback = (fn, cb) => { + const runMaybeAsyncCallback = (fn: any, cb: NodeCallbackType) => { if (typeof fn !== 'function') return cb(null) try { // if function appears sync… @@ -16,9 +22,9 @@ module.exports = (callbacks, event, onCallbackError, cb) => { if (ret && typeof ret.then === 'function') { return ret.then( // resolve - val => setTimeout(() => cb(null, val)), + (val: boolean | undefined ) => setTimeout(() => cb(null, val)), // reject - err => { + (err: Error) => { setTimeout(() => { onCallbackError(err) return cb(null, true) @@ -29,18 +35,20 @@ module.exports = (callbacks, event, onCallbackError, cb) => { return cb(null, ret) } // if function is async… - fn(event, (err, result) => { + fn(event, (err: Error, result: any) => { if (err) { onCallbackError(err) return cb(null) } cb(null, result) }) - } catch (e) { + } catch (e: any) { onCallbackError(e) cb(null) } } - some(callbacks, runMaybeAsyncCallback, cb) + return every(callbacks, runMaybeAsyncCallback, cb) } + +export default runCallbacks diff --git a/packages/core/src/lib/clone-client.ts b/packages/core/src/lib/clone-client.ts new file mode 100644 index 0000000000..ae333f1655 --- /dev/null +++ b/packages/core/src/lib/clone-client.ts @@ -0,0 +1,59 @@ +import Client from '../client' +import { Config } from '../common' + + +interface InternalClient { + _config: Client['_config'] + _context: Client['_context'] + _breadcrumbs: Client['_breadcrumbs'] + _metadata: Client['_metadata'] + _features: Client['_features'] + _featuresIndex: Client['_featuresIndex'] + _user: Client['_user'] + _logger: Client['_logger'] + _delivery: Client['_delivery'] + _sessionDelegate: Client['_sessionDelegate'] + _cbs: Client['_cbs'] +} + +type OnCloneCallback = (client: Client) => void +const onCloneCallbacks: Array = [] + +const cloneClient = (client: Client): Client => { + // @ts-expect-error overwriting properties manually so do not need to match constructor signature + const clone: InternalClient = new client.Client({}, {}, [], client.getNotifier()) + + clone._config = client._config + + // changes to these properties should not be reflected in the original client, + // so ensure they are are (shallow) cloned + clone._breadcrumbs = client._breadcrumbs.slice() + clone._metadata = Object.assign({}, client._metadata) + clone._features = [...client._features] + clone._featuresIndex = Object.assign({}, client._featuresIndex) + clone._user = Object.assign({}, client._user) + clone._context = client.getContext() + + clone._cbs = { + e: client._cbs.e.slice(), + s: client._cbs.s.slice(), + sp: client._cbs.sp.slice(), + b: client._cbs.b.slice() + } + + clone._logger = client._logger + clone._delivery = client._delivery + clone._sessionDelegate = client._sessionDelegate + + onCloneCallbacks.forEach(callback => { + callback(clone as unknown as Client) + }) + + return clone as unknown as Client +} + +cloneClient.registerCallback = (callback: OnCloneCallback) => { + onCloneCallbacks.push(callback) +} + +export default cloneClient diff --git a/packages/core/src/lib/extract-object.ts b/packages/core/src/lib/extract-object.ts new file mode 100644 index 0000000000..ed6cb016fb --- /dev/null +++ b/packages/core/src/lib/extract-object.ts @@ -0,0 +1,12 @@ +// Given a host object, return the value at host[key] if it is an object +// and it has at least one key/value + +const extractObject = (host: object, key: any): object | undefined => { + if (host[key as keyof typeof host] && typeof host[key as keyof typeof host] === 'object' && Object.keys(host[key as keyof typeof host]).length > 0) { + return host[key as keyof typeof host] + } else { + return undefined + } +} + +export default extractObject diff --git a/packages/core/src/lib/feature-flag-delegate.ts b/packages/core/src/lib/feature-flag-delegate.ts new file mode 100644 index 0000000000..8942cedc16 --- /dev/null +++ b/packages/core/src/lib/feature-flag-delegate.ts @@ -0,0 +1,87 @@ +import jsonStringify from '@bugsnag/safe-json-stringify' +import type { FeatureFlag } from "../common"; + +type FeatureFlagEventApi = { + featureFlag: string + variant?: string +} + +interface FeatureFlagDelegate{ + add: (existingFeatures: Array, existingFeatureKeys: { [key: string]: number }, name?: string | null, variant?: any ) => void + merge: ( + existingFeatures: Array<{ name: string; variant?: any } | null>, + newFeatures: any, + existingFeatureKeys: { [key: string]: any } + ) => Array<{ name: string; variant: any }> | undefined + toEventApi: (featureFlags: Array) => FeatureFlagEventApi[] + clear: (features: (FeatureFlag | null)[], featuresIndex: { [key: string]: number }, name: string) => void +} + +const featureFlagDelegate: FeatureFlagDelegate = { + add: (existingFeatures, existingFeatureKeys, name, variant) => { + if (typeof name !== 'string') { + return + } + + if (variant === undefined) { + variant = null + } else if (variant !== null && typeof variant !== 'string') { + variant = jsonStringify(variant) + } + + const existingIndex = existingFeatureKeys[name] + if (typeof existingIndex === 'number') { + existingFeatures[existingIndex] = {name, variant} + return + } + + existingFeatures.push({name, variant}) + existingFeatureKeys[name] = existingFeatures.length - 1 + }, + + merge: (existingFeatures, newFeatures, existingFeatureKeys) => { + if (!Array.isArray(newFeatures)) { + return + } + + for (let i = 0; i < newFeatures.length; ++i) { + const feature = newFeatures[i] + + if (feature === null || typeof feature !== 'object') { + continue + } + + // 'add' will handle if 'name' doesn't exist & 'variant' is optional + featureFlagDelegate.add(existingFeatures, existingFeatureKeys, feature.name, feature.variant) + } + + // Remove any nulls from the array to match the return type + return existingFeatures.filter(f => f) as Array<{ name: string; variant: any }> + }, + +// convert feature flags from a map of 'name -> variant' into the format required +// by the Bugsnag Event API: +// [{ featureFlag: 'name', variant: 'variant' }, { featureFlag: 'name 2' }] + toEventApi: (featureFlags) => { + return (featureFlags || []) + .filter((flag): flag is FeatureFlag => flag !== null && typeof flag === 'object') + .map((flag) => { + const result: FeatureFlagEventApi = { featureFlag: flag.name }; + if (typeof flag.variant === 'string') { + result.variant = flag.variant; + } + return result; + }); + }, + + clear: (features, featuresIndex, name) => { + const existingIndex = featuresIndex[name] + if (typeof existingIndex === 'number') { + features[existingIndex] = null + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete featuresIndex[name] + } + } +} + +export default featureFlagDelegate \ No newline at end of file diff --git a/packages/core/lib/has-stack.js b/packages/core/src/lib/has-stack.ts similarity index 81% rename from packages/core/lib/has-stack.js rename to packages/core/src/lib/has-stack.ts index bdd0c5797c..981cabb1b9 100644 --- a/packages/core/lib/has-stack.js +++ b/packages/core/src/lib/has-stack.ts @@ -1,6 +1,8 @@ // Given `err` which may be an error, does it have a stack property which is a string? -module.exports = err => +const hasStack = (err: any): boolean => !!err && (!!err.stack || !!err.stacktrace || !!err['opera#sourceloc']) && typeof (err.stack || err.stacktrace || err['opera#sourceloc']) === 'string' && err.stack !== `${err.name}: ${err.message}` + +export default hasStack diff --git a/packages/core/src/lib/iserror.ts b/packages/core/src/lib/iserror.ts new file mode 100644 index 0000000000..dd226b86d5 --- /dev/null +++ b/packages/core/src/lib/iserror.ts @@ -0,0 +1,36 @@ +// MIT License + +// Copyright (c) 2017 Anton Yefremov + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +const isError = (maybeError: unknown): maybeError is Error => { + switch (Object.prototype.toString.call(maybeError)) { + case '[object Error]': + return true + case '[object Exception]': + return true + case '[object DOMException]': + return true + default: + return maybeError instanceof Error + } +} + +export default isError diff --git a/packages/core/src/lib/metadata-delegate.ts b/packages/core/src/lib/metadata-delegate.ts new file mode 100644 index 0000000000..f129513f08 --- /dev/null +++ b/packages/core/src/lib/metadata-delegate.ts @@ -0,0 +1,70 @@ + + +interface MetadataDelegate { + add: (state: { [key: string]: any }, section: string, keyOrObj?: object | string, maybeVal?: any) => any + get: (state: { [key: string]: any }, section: string, key?: string) => any + clear: (state: { [key: string]: any }, section: string, key?: string) => any +} + +const metadataDelegate: MetadataDelegate = { + add: (state, section, keyOrObj, maybeVal) => { + if (!section) return + let updates + + // addMetadata("section", null) -> clears section + if (keyOrObj === null) return metadataDelegate.clear(state, section); + + // normalise the two supported input types into object form + if (typeof keyOrObj === 'object') updates = keyOrObj + if (typeof keyOrObj === 'string') updates = { [keyOrObj]: maybeVal } + + // exit if we don't have an updates object at this point + if (!updates) return + + // preventing the __proto__ property from being used as a key + if (section === '__proto__' || section === 'constructor' || section === 'prototype') { + return + } + + // ensure a section with this name exists + if (!state[section]) state[section] = {} + + // merge the updates with the existing section + state[section] = Object.assign({}, state[section], updates) + }, + + get: (state, section, key) => { + if (typeof section !== 'string') return undefined + if (!key) { + return state[section] + } + if (state[section]) { + return state[section][key] + } + return undefined + }, + + clear: (state, section, key) => { + if (typeof section !== 'string') return + + // clear an entire section + if (!key) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete state[section] + return + } + + // preventing the __proto__ property from being used as a key + if (section === '__proto__' || section === 'constructor' || section === 'prototype') { + return + } + + // clear a single value from a section + if (state[section]) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete state[section][key] + } + } +} + +export default metadataDelegate; \ No newline at end of file diff --git a/packages/core/src/lib/node-fallback-stack.ts b/packages/core/src/lib/node-fallback-stack.ts new file mode 100644 index 0000000000..03dd4dfdac --- /dev/null +++ b/packages/core/src/lib/node-fallback-stack.ts @@ -0,0 +1,37 @@ +// The utilities in this file are used to save the stackframes from a known execution context +// to use when a subsequent error has no stack frames. This happens with a lot of +// node's builtin async callbacks when they return from the native layer with no context +// for example: +// +// fs.readFile('does not exist', (err) => { +// /* node 8 */ +// err.stack = "ENOENT: no such file or directory, open 'nope'" +// /* node 4,6 */ +// err.stack = "Error: ENOENT: no such file or directory, open 'nope'\n at Error (native)" +// }) + +interface NodeFallbackStack { + getStack: () => string | undefined + maybeUseFallbackStack: (err: Error, fallbackStack: string) => Error +} + +const nodeFallbackStack: NodeFallbackStack = { + // Gets the stack string for the current execution context + getStack: () => { + // slice(3) removes the first line + this function's frame + the caller's frame, + // so the stack begins with the caller of this function + return (new Error()).stack?.split('\n').slice(3).join('\n') + }, + + // Given an Error and a fallbackStack from getStack(), use the fallbackStack + // if error.stack has no genuine stackframes (according to the example above) + maybeUseFallbackStack: (err, fallbackStack) => { + const lines = err.stack?.split('\n') + if (lines?.length === 1 || (lines?.length === 2 && /at Error \(native\)/.test(lines[1]))) { + err.stack = `${lines[0]}\n${fallbackStack}` + } + return err + } +} + +export default nodeFallbackStack \ No newline at end of file diff --git a/packages/core/src/lib/sync-callback-runner.ts b/packages/core/src/lib/sync-callback-runner.ts new file mode 100644 index 0000000000..2218404f20 --- /dev/null +++ b/packages/core/src/lib/sync-callback-runner.ts @@ -0,0 +1,28 @@ +import type Breadcrumb from '../breadcrumb' +import { LoggerConfig, OnBreadcrumbCallback, OnSessionCallback } from '../common' +import type Session from '../session' +type Callbacks = (...args: any[]) => any + +const runSyncCallbacks = ( + callbacks: OnSessionCallback[] | OnBreadcrumbCallback[] | Callbacks[] | any[], + callbackArg: Session | Breadcrumb | Event, + callbackType: string, + logger: LoggerConfig +): boolean => { + let ignore = false + const cbs = callbacks.slice() + while (!ignore) { + if (!cbs.length) break + try { + ignore = cbs.pop()(callbackArg) === false + } catch (e) { + logger.error( + `Error occurred in ${callbackType} callback, continuing anyway…` + ) + logger.error(e) + } + } + return ignore +} + +export default runSyncCallbacks \ No newline at end of file diff --git a/packages/core/src/lib/validators/int-range.ts b/packages/core/src/lib/validators/int-range.ts new file mode 100644 index 0000000000..2d468d0c71 --- /dev/null +++ b/packages/core/src/lib/validators/int-range.ts @@ -0,0 +1,9 @@ +const intRange: (min?: number, max?: number) => (value: T) => boolean = ( + min = 1, + max = Infinity +) => (value) => + typeof value === "number" && + parseInt("" + value, 10) === value && + value >= min && value <= max; + +export default intRange \ No newline at end of file diff --git a/packages/core/src/lib/validators/list-of-functions.ts b/packages/core/src/lib/validators/list-of-functions.ts new file mode 100644 index 0000000000..91dbc50113 --- /dev/null +++ b/packages/core/src/lib/validators/list-of-functions.ts @@ -0,0 +1,3 @@ +const listOfFunctions = (value: unknown): boolean => typeof value === 'function' || (Array.isArray(value) && value.filter(f => typeof f === 'function').length === value.length) + +export default listOfFunctions diff --git a/packages/core/src/lib/validators/string-with-length.ts b/packages/core/src/lib/validators/string-with-length.ts new file mode 100644 index 0000000000..868249c144 --- /dev/null +++ b/packages/core/src/lib/validators/string-with-length.ts @@ -0,0 +1,3 @@ +const stringWithLength = (value: unknown): boolean => typeof value === 'string' && !!value.length + +export default stringWithLength \ No newline at end of file diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts new file mode 100644 index 0000000000..aa137132d7 --- /dev/null +++ b/packages/core/src/session.ts @@ -0,0 +1,40 @@ +import cuid from '@bugsnag/cuid' +import { App, Device, User } from './common' + +interface MinimalEvent { + _handledState: { + unhandled: boolean + } +} + +export default class Session { + constructor ( + public readonly id: string = cuid(), + public readonly startedAt: Date = new Date(), + public _handled = 0, + public _unhandled = 0, + public _user: User = {}, + public app: App = {}, + public device: Device = {} + ) { } + + getUser () { + return this._user + } + + setUser (id?: string, email?: string, name?: string) { + this._user = { id, email, name } + } + + toJSON () { + return { + id: this.id, + startedAt: this.startedAt, + events: { handled: this._handled, unhandled: this._unhandled } + } + } + + _track (event: MinimalEvent) { + this[event._handledState.unhandled ? '_unhandled' : '_handled'] += 1 + } +} diff --git a/packages/core/test/breadcrumb.test.ts b/packages/core/test/breadcrumb.test.ts index 9a8b9ece21..526dadd6e2 100644 --- a/packages/core/test/breadcrumb.test.ts +++ b/packages/core/test/breadcrumb.test.ts @@ -1,6 +1,6 @@ -import Breadcrumb from '../breadcrumb' +import Breadcrumb from '../src/breadcrumb' -describe('@bugsnag/core/breadcrumb', () => { +describe('Breadcrumb', () => { describe('toJSON()', () => { it('returns the correct data structure', () => { const d = new Date() diff --git a/packages/core/test/client.test.ts b/packages/core/test/client.test.ts index 39441937de..199c82015a 100644 --- a/packages/core/test/client.test.ts +++ b/packages/core/test/client.test.ts @@ -1,18 +1,17 @@ -import Client from '../client' -import Event from '../event' -import Session from '../session' -import breadcrumbTypes from '../lib/breadcrumb-types' -import { BreadcrumbType } from '../types/common' +import Client from '../src/client' +import Event from '../src/event' +import Session from '../src/session' +import { BreadcrumbType, BREADCRUMB_TYPES } from '../src/common' const noop = () => {} const id = (a: T) => a -describe('@bugsnag/core/client', () => { +describe('Client', () => { describe('constructor', () => { it('can handle bad input', () => { - // @ts-ignore + // @ts-expect-error - testing with unexpected arguments expect(() => new Client()).toThrow() - // @ts-ignore + // @ts-expect-error - testing with unexpected arguments expect(() => new Client('foo')).toThrow() }) }) @@ -21,7 +20,7 @@ describe('@bugsnag/core/client', () => { it('handles bad/good input', () => { expect(() => { // no opts supplied - // @ts-ignore + // @ts-expect-error - testing with unexpected arguments const client = new Client({}) expect(client).toBe(client) }).toThrow() @@ -168,7 +167,6 @@ describe('@bugsnag/core/client', () => { const client = new Client({ apiKey: 'API_KEY_YEAH' }) const session = new Session() - // @ts-ignore client._session = session client._setDelivery(client => ({ @@ -351,6 +349,7 @@ describe('@bugsnag/core/client', () => { client.notify({ name: 'some message' }) // @ts-ignore client.notify(1) + // @ts-ignore client.notify('errrororor') // @ts-ignore client.notify('str1', 'str2') @@ -458,7 +457,7 @@ describe('@bugsnag/core/client', () => { client.notify(new Error('111'), () => {}, (err, event) => { expect(err).toBeTruthy() - expect(err.message).toBe('flerp') + expect(err?.message).toBe('flerp') expect(event).toBeTruthy() expect(event.errors[0].errorMessage).toBe('111') @@ -633,32 +632,32 @@ describe('@bugsnag/core/client', () => { }) describe('_isBreadcrumbTypeEnabled()', () => { - it.each(breadcrumbTypes)('returns true for "%s" when enabledBreadcrumbTypes is not configured', (type) => { + it.each(BREADCRUMB_TYPES)('returns true for "%s" when enabledBreadcrumbTypes is not configured', (type) => { const client = new Client({ apiKey: 'API_KEY_YEAH' }) expect(client._isBreadcrumbTypeEnabled(type)).toBe(true) }) - it.each(breadcrumbTypes)('returns true for "%s" when enabledBreadcrumbTypes=null', (type) => { + it.each(BREADCRUMB_TYPES)('returns true for "%s" when enabledBreadcrumbTypes=null', (type) => { const client = new Client({ apiKey: 'API_KEY_YEAH', enabledBreadcrumbTypes: null }) expect(client._isBreadcrumbTypeEnabled(type)).toBe(true) }) - it.each(breadcrumbTypes)('returns false for "%s" when enabledBreadcrumbTypes=[]', (type) => { + it.each(BREADCRUMB_TYPES)('returns false for "%s" when enabledBreadcrumbTypes=[]', (type) => { const client = new Client({ apiKey: 'API_KEY_YEAH', enabledBreadcrumbTypes: [] }) expect(client._isBreadcrumbTypeEnabled(type)).toBe(false) }) - it.each(breadcrumbTypes)('returns true for "%s" when enabledBreadcrumbTypes only contains it', (type) => { + it.each(BREADCRUMB_TYPES)('returns true for "%s" when enabledBreadcrumbTypes only contains it', (type) => { const client = new Client({ apiKey: 'API_KEY_YEAH', enabledBreadcrumbTypes: [type as BreadcrumbType] }) expect(client._isBreadcrumbTypeEnabled(type)).toBe(true) }) - it.each(breadcrumbTypes)('returns false for "%s" when enabledBreadcrumbTypes does not contain it', (type) => { - const enabledBreadcrumbTypes = breadcrumbTypes.filter(enabledType => enabledType !== type) + it.each(BREADCRUMB_TYPES)('returns false for "%s" when enabledBreadcrumbTypes does not contain it', (type) => { + const enabledBreadcrumbTypes = BREADCRUMB_TYPES.filter(enabledType => enabledType !== type) const client = new Client({ apiKey: 'API_KEY_YEAH', @@ -705,7 +704,8 @@ describe('@bugsnag/core/client', () => { done() } })) - const sessionClient = client.startSession() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const sessionClient = client.startSession()! sessionClient.notify(new Error('broke')) sessionClient._notify(new Event('err', 'bad', [], { unhandled: true, severity: 'error', severityReason: { type: 'unhandledException' } })) sessionClient.notify(new Error('broke')) diff --git a/packages/core/test/config.test.ts b/packages/core/test/config.test.ts index 26a169043c..d4fd612dba 100644 --- a/packages/core/test/config.test.ts +++ b/packages/core/test/config.test.ts @@ -1,26 +1,26 @@ -import config from '../config' +import schema from '../src/config' -describe('@bugsnag/core/config', () => { +describe('config', () => { describe('schema', () => { it('has the required properties { validate(), defaultValue(), message }', () => { - Object.keys(config.schema).forEach(k => { - const key = k as unknown as keyof typeof config.schema - config.schema[key].defaultValue(undefined) - // @ts-expect-error - config.schema[key].validate() - config.schema[key].validate(-1) - config.schema[key].validate('stringy stringerson') - config.schema[key].validate(['foo', 'bar', 'baz']) - config.schema[key].validate(new Date()) - config.schema[key].validate(null) - expect(typeof config.schema[key].message).toBe('string') + Object.keys(schema).forEach(k => { + const key = k as unknown as keyof typeof schema + schema[key].defaultValue(undefined) + // @ts-expect-error testing invalid arguments + schema[key].validate() + schema[key].validate(-1) + schema[key].validate('stringy stringerson') + schema[key].validate(['foo', 'bar', 'baz']) + schema[key].validate(new Date()) + schema[key].validate(null) + expect(typeof schema[key].message).toBe('string') }) }) }) describe('user', () => { it('should only allow id, name and email', () => { - const userValidator = config.schema.user.validate + const userValidator = schema.user.validate expect(userValidator(null)).toBe(true) expect(userValidator({ id: '123', email: 'bug@sn.ag', name: 'Bugsnag' })).toBe(true) expect(userValidator({ id: '123', email: 'bug@sn.ag', name: 'Bugsnag', extra: 'aaa' })).toBe(false) @@ -31,24 +31,24 @@ describe('@bugsnag/core/config', () => { describe('enabledBreadcrumbTypes', () => { it('fails when a supplied value is not a valid breadcrumb type', () => { - const enabledBreadcrumbTypesValidator = config.schema.enabledBreadcrumbTypes.validate + const enabledBreadcrumbTypesValidator = schema.enabledBreadcrumbTypes.validate expect(enabledBreadcrumbTypesValidator(['UNKNOWN_BREADCRUMB_TYPE'])).toBe(false) }) }) describe('enabledErrorTypes', () => { it('is ok with an empty object', () => { - const enabledErrorTypesValidator = config.schema.enabledErrorTypes.validate + const enabledErrorTypesValidator = schema.enabledErrorTypes.validate expect(enabledErrorTypesValidator({})).toBe(true) }) it('works with a subset of error types', () => { - const enabledErrorTypesValidator = config.schema.enabledErrorTypes.validate + const enabledErrorTypesValidator = schema.enabledErrorTypes.validate expect(enabledErrorTypesValidator({ unhandledExceptions: true })).toBe(true) }) it('fails when an additional unsupported type is provided', () => { - const enabledErrorTypesValidator = config.schema.enabledErrorTypes.validate + const enabledErrorTypesValidator = schema.enabledErrorTypes.validate expect(enabledErrorTypesValidator({ unhandledExceptions: true, unhandledRejections: false, @@ -66,19 +66,19 @@ describe('@bugsnag/core/config', () => { { name: 'example' }, { length: 1000 } ])('fails when the supplied value is not an array (%p)', value => { - const validator = config.schema.featureFlags.validate + const validator = schema.featureFlags.validate expect(validator(value)).toBe(false) }) it('fails when a value does not have a "name"', () => { - const validator = config.schema.featureFlags.validate + const validator = schema.featureFlags.validate expect(validator([{ name: 'hello' }, { notName: 'oops' }])).toBe(false) }) it('passes when all values have a "name"', () => { - const validator = config.schema.featureFlags.validate + const validator = schema.featureFlags.validate const featureFlags = [ { name: 'hello' }, { name: 'abc', variant: 'xyz' }, diff --git a/packages/core/test/event.test.ts b/packages/core/test/event.test.ts index 1231fa577f..18518d16d7 100644 --- a/packages/core/test/event.test.ts +++ b/packages/core/test/event.test.ts @@ -1,11 +1,11 @@ import ErrorStackParser from 'error-stack-parser' -import Event from '../event' +import Event from '../src/event' jest.mock('stack-generator', () => ({ backtrace: () => [{}, {}] })) -describe('@bugsnag/core/event', () => { +describe('Event', () => { describe('constructor', () => { it('sets default handledState', () => { const err = new Error('noooooo') diff --git a/packages/core/lib/test/async-every.test.ts b/packages/core/test/lib/async-every.test.ts similarity index 96% rename from packages/core/lib/test/async-every.test.ts rename to packages/core/test/lib/async-every.test.ts index 8aff108e4f..1e02ba1e61 100644 --- a/packages/core/lib/test/async-every.test.ts +++ b/packages/core/test/lib/async-every.test.ts @@ -1,4 +1,4 @@ -import every from '../async-every' +import every from '../../src/lib/async-every' describe('async-every', () => { it('handles iterator errors', done => { diff --git a/packages/core/lib/test/callback-runner.test.ts b/packages/core/test/lib/callback-runner.test.ts similarity index 89% rename from packages/core/lib/test/callback-runner.test.ts rename to packages/core/test/lib/callback-runner.test.ts index f25b6698be..ad5d5e283e 100644 --- a/packages/core/lib/test/callback-runner.test.ts +++ b/packages/core/test/lib/callback-runner.test.ts @@ -1,5 +1,5 @@ -import runCallbacks from '../callback-runner' -import { NodeCallbackType } from '../async-every' +import runCallbacks from '../../src/lib/callback-runner' +import { NodeCallbackType } from '../../src/lib/async-every' interface TestEvent { name: string @@ -15,7 +15,7 @@ describe('runCallbacks()', () => { const callbacks: Array> = [ (event) => { event.age = 10 }, (event, cb) => { setTimeout(() => cb(null, true), 5) }, - (event) => new Promise((resolve) => { + (event) => new Promise((resolve) => { event.promiseRan = 'yes' resolve() }) @@ -48,9 +48,9 @@ describe('runCallbacks()', () => { const event = {} let called = false const callbacks: Array> = [ - (event) => new Promise((resolve) => resolve()), + (event) => new Promise((resolve) => resolve()), (event) => new Promise((resolve, reject) => reject(new Error('derp'))), - (event) => new Promise((resolve) => { + (event) => new Promise((resolve) => { called = true resolve() }) diff --git a/packages/core/test/lib/clone-client.test.ts b/packages/core/test/lib/clone-client.test.ts index 13bc6782d8..d9d2b12e29 100644 --- a/packages/core/test/lib/clone-client.test.ts +++ b/packages/core/test/lib/clone-client.test.ts @@ -1,5 +1,5 @@ -import Client from '../../client' -import clone from '../../lib/clone-client' +import Client from '../../src/client' +import cloneClient from '../../src/lib/clone-client' const apiKey = 'abcabcabcabcabcabcabc1234567890f' @@ -7,7 +7,7 @@ describe('@bugsnag/core/lib/clone-client', () => { describe('clone', () => { it('clones a client', () => { const original = new Client({ apiKey }) - const cloned = clone(original) + const cloned = cloneClient(original); expect(cloned._config.apiKey).toEqual(apiKey) expect(cloned).not.toBe(original) @@ -17,7 +17,7 @@ describe('@bugsnag/core/lib/clone-client', () => { const original = new Client({ apiKey }) original.leaveBreadcrumb('abc', { a: 1 }, 'navigation') - const cloned = clone(original) + const cloned = cloneClient(original); expect(cloned._breadcrumbs).not.toBe(original._breadcrumbs) expect(cloned._breadcrumbs).toHaveLength(1) @@ -41,7 +41,7 @@ describe('@bugsnag/core/lib/clone-client', () => { original.addMetadata('abc', { a: 1, b: 2, c: 3 }) original.addMetadata('xyz', { x: 9, y: 8, z: 7 }) - const cloned = clone(original) + const cloned = cloneClient(original); expect(cloned._metadata).not.toBe(original._metadata) expect(cloned._metadata).toEqual({ abc: { a: 1, b: 2, c: 3 }, @@ -69,7 +69,7 @@ describe('@bugsnag/core/lib/clone-client', () => { { name: 'c' } ]) - const cloned = clone(original) + const cloned = cloneClient(original) expect(cloned._features).not.toBe(original._features) expect(cloned._features).toEqual([ { name: 'a', variant: '1' }, @@ -95,7 +95,7 @@ describe('@bugsnag/core/lib/clone-client', () => { const original = new Client({ apiKey }) original.setUser('123', 'user@bugsnag.com', 'bug snag') - const cloned = clone(original) + const cloned = cloneClient(original) expect(cloned.getUser()).toEqual({ id: '123', email: 'user@bugsnag.com', @@ -118,7 +118,7 @@ describe('@bugsnag/core/lib/clone-client', () => { const original = new Client({ apiKey }) original.setContext('contextual') - const cloned = clone(original) + const cloned = cloneClient(original) expect(cloned.getContext()).toEqual('contextual') // changing the original's context should not affect the clone @@ -135,7 +135,7 @@ describe('@bugsnag/core/lib/clone-client', () => { const original = new Client({ apiKey }) original.addOnError(onError1) - const cloned = clone(original) + const cloned = cloneClient(original) expect(cloned._cbs.e).not.toBe(original._cbs.e) // @ts-ignore @@ -164,7 +164,7 @@ describe('@bugsnag/core/lib/clone-client', () => { const original = new Client({ apiKey }) original.addOnSession(onSession1) - const cloned = clone(original) + const cloned = cloneClient(original) expect(cloned._cbs.s).not.toBe(original._cbs.s) // @ts-ignore @@ -193,7 +193,7 @@ describe('@bugsnag/core/lib/clone-client', () => { const original = new Client({ apiKey }) original._addOnSessionPayload(onSessionPayload1) - const cloned = clone(original) + const cloned = cloneClient(original) expect(cloned._cbs.sp).not.toBe(original._cbs.sp) // @ts-ignore @@ -222,7 +222,7 @@ describe('@bugsnag/core/lib/clone-client', () => { const original = new Client({ apiKey }) original.addOnBreadcrumb(onBreadcrumb1) - const cloned = clone(original) + const cloned = cloneClient(original) expect(cloned._cbs.b).not.toBe(original._cbs.b) // @ts-ignore @@ -253,7 +253,7 @@ describe('@bugsnag/core/lib/clone-client', () => { } const original = new Client({ apiKey, logger }) - const cloned = clone(original) + const cloned = cloneClient(original) expect(cloned._logger).toBe(original._logger) }) @@ -267,7 +267,7 @@ describe('@bugsnag/core/lib/clone-client', () => { const original = new Client({ apiKey }) original._setDelivery(delivery) - const cloned = clone(original) + const cloned = cloneClient(original) expect(cloned._delivery).toBe(original._delivery) }) @@ -282,7 +282,7 @@ describe('@bugsnag/core/lib/clone-client', () => { const original = new Client({ apiKey }) original._sessionDelegate = sessionDelegate - const cloned = clone(original) + const cloned = cloneClient(original) expect(cloned._sessionDelegate).toBe(original._sessionDelegate) }) @@ -294,9 +294,9 @@ describe('@bugsnag/core/lib/clone-client', () => { const callback2 = jest.fn() const callback3 = jest.fn() - clone.registerCallback(callback1) - clone.registerCallback(callback2) - clone.registerCallback(callback3) + cloneClient.registerCallback(callback1) + cloneClient.registerCallback(callback2) + cloneClient.registerCallback(callback3) const original = new Client({ apiKey }) @@ -304,13 +304,13 @@ describe('@bugsnag/core/lib/clone-client', () => { expect(callback2).not.toHaveBeenCalled() expect(callback3).not.toHaveBeenCalled() - clone(original) + cloneClient(original) expect(callback1).toHaveBeenCalledTimes(1) expect(callback2).toHaveBeenCalledTimes(1) expect(callback3).toHaveBeenCalledTimes(1) - clone(original) + cloneClient(original) expect(callback1).toHaveBeenCalledTimes(2) expect(callback2).toHaveBeenCalledTimes(2) @@ -322,9 +322,9 @@ describe('@bugsnag/core/lib/clone-client', () => { const callback2 = jest.fn() const callback3 = jest.fn() - clone.registerCallback(callback1) - clone.registerCallback(callback2) - clone.registerCallback(callback3) + cloneClient.registerCallback(callback1) + cloneClient.registerCallback(callback2) + cloneClient.registerCallback(callback3) const original = new Client({ apiKey }) @@ -332,13 +332,13 @@ describe('@bugsnag/core/lib/clone-client', () => { expect(callback2).not.toHaveBeenCalled() expect(callback3).not.toHaveBeenCalled() - const cloned = clone(original) + const cloned = cloneClient(original) expect(callback1).toHaveBeenCalledWith(cloned) expect(callback2).toHaveBeenCalledWith(cloned) expect(callback3).toHaveBeenCalledWith(cloned) - const cloned2 = clone(original) + const cloned2 = cloneClient(original) expect(callback1).toHaveBeenCalledWith(cloned2) expect(callback2).toHaveBeenCalledWith(cloned2) @@ -357,19 +357,19 @@ describe('@bugsnag/core/lib/clone-client', () => { const callback2 = () => { order.push('callback2') } const callback3 = () => { order.push('callback3') } - clone.registerCallback(callback1) - clone.registerCallback(callback2) - clone.registerCallback(callback3) + cloneClient.registerCallback(callback1) + cloneClient.registerCallback(callback2) + cloneClient.registerCallback(callback3) const original = new Client({ apiKey }) expect(order).toEqual([]) - clone(original) + cloneClient(original) expect(order).toEqual(['callback1', 'callback2', 'callback3']) - clone(original) + cloneClient(original) expect(order).toEqual(['callback1', 'callback2', 'callback3', 'callback1', 'callback2', 'callback3']) }) diff --git a/packages/core/lib/test/extract-object.test.ts b/packages/core/test/lib/extract-object.test.ts similarity index 85% rename from packages/core/lib/test/extract-object.test.ts rename to packages/core/test/lib/extract-object.test.ts index fb78f31e51..713c434b75 100644 --- a/packages/core/lib/test/extract-object.test.ts +++ b/packages/core/test/lib/extract-object.test.ts @@ -1,4 +1,4 @@ -import extractObject from '../extract-object' +import extractObject from '../../src/lib/extract-object' describe('extractObject', () => { it('returns undefined if the key is not an object, or the value otherwise', () => { diff --git a/packages/core/lib/test/feature-flag-delegate.test.ts b/packages/core/test/lib/feature-flag-delegate.test.ts similarity index 85% rename from packages/core/lib/test/feature-flag-delegate.test.ts rename to packages/core/test/lib/feature-flag-delegate.test.ts index 8d8a2d69f3..ac2f34454f 100644 --- a/packages/core/lib/test/feature-flag-delegate.test.ts +++ b/packages/core/test/lib/feature-flag-delegate.test.ts @@ -1,12 +1,12 @@ -import delegate from '../feature-flag-delegate' +import featureFlagDelegate from '../../src/lib/feature-flag-delegate' -describe('feature flag delegate', () => { +describe('feature flag featureFlagDelegate', () => { describe('#add', () => { it('should do nothing if name is not passed', () => { const existingFeatures = [{ name: 'abc', variant: 'xyz' }] const existingFeaturesIndex = { abc: 0 } - delegate.add(existingFeatures, existingFeaturesIndex) + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex) expect(existingFeatures).toStrictEqual([{ name: 'abc', variant: 'xyz' }]) expect(existingFeaturesIndex).toStrictEqual({ abc: 0 }) @@ -16,7 +16,7 @@ describe('feature flag delegate', () => { const existingFeatures = [{ name: 'abc', variant: 'xyz' }] const existingFeaturesIndex = { abc: 0 } - delegate.add(existingFeatures, existingFeaturesIndex, undefined, '???') + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, undefined, '???') expect(existingFeatures).toStrictEqual([{ name: 'abc', variant: 'xyz' }]) expect(existingFeaturesIndex).toStrictEqual({ abc: 0 }) @@ -26,7 +26,7 @@ describe('feature flag delegate', () => { const existingFeatures = [{ name: 'abc', variant: 'xyz' }] const existingFeaturesIndex = { abc: 0 } - delegate.add(existingFeatures, existingFeaturesIndex, null, '???') + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, null, '???') expect(existingFeatures).toStrictEqual([{ name: 'abc', variant: 'xyz' }]) expect(existingFeaturesIndex).toStrictEqual({ abc: 0 }) @@ -36,7 +36,7 @@ describe('feature flag delegate', () => { const existingFeatures: any[] = [] const existingFeaturesIndex = {} - delegate.add(existingFeatures, existingFeaturesIndex, 'good_feature') + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, 'good_feature') expect(existingFeatures).toStrictEqual([{ name: 'good_feature', variant: null }]) expect(existingFeaturesIndex).toStrictEqual({ good_feature: 0 }) @@ -46,7 +46,7 @@ describe('feature flag delegate', () => { const existingFeatures: [] = [] const existingFeaturesIndex = {} - delegate.add(existingFeatures, existingFeaturesIndex, 'ok_feature', null) + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, 'ok_feature', null) expect(existingFeatures).toStrictEqual([{ name: 'ok_feature', variant: null }]) expect(existingFeaturesIndex).toStrictEqual({ ok_feature: 0 }) @@ -56,7 +56,7 @@ describe('feature flag delegate', () => { const existingFeatures: any[] = [] const existingFeaturesIndex = {} - delegate.add(existingFeatures, existingFeaturesIndex, 'cool_feature', 'very ant') + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, 'cool_feature', 'very ant') expect(existingFeatures).toStrictEqual([{ name: 'cool_feature', variant: 'very ant' }]) expect(existingFeaturesIndex).toStrictEqual({ cool_feature: 0 }) @@ -75,7 +75,7 @@ describe('feature flag delegate', () => { const existingFeatures: any[] = [] const existingFeaturesIndex = {} - delegate.add(existingFeatures, existingFeaturesIndex, 'some_feature', variant) + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, 'some_feature', variant) expect(existingFeatures).toStrictEqual([{ name: 'some_feature', variant: expected }]) expect(existingFeaturesIndex).toStrictEqual({ some_feature: 0 }) @@ -85,9 +85,9 @@ describe('feature flag delegate', () => { const existingFeatures = [{ name: 'a', variant: 'a' }, { name: 'b', variant: 'b' }, { name: 'c', variant: 'c' }, { name: 'd', variant: 'd' }, { name: 'e', variant: 'e' }] const existingFeaturesIndex = { a: 0, b: 1, c: 2, d: 3, e: 4 } - delegate.add(existingFeatures, existingFeaturesIndex, 'b', 'x') - delegate.add(existingFeatures, existingFeaturesIndex, 'e', 'y') - delegate.add(existingFeatures, existingFeaturesIndex, 'a', 'z') + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, 'b', 'x') + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, 'e', 'y') + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, 'a', 'z') expect(existingFeatures).toStrictEqual([{ name: 'a', variant: 'z' }, { name: 'b', variant: 'x' }, { name: 'c', variant: 'c' }, { name: 'd', variant: 'd' }, { name: 'e', variant: 'y' }]) expect(existingFeaturesIndex).toStrictEqual({ a: 0, b: 1, c: 2, d: 3, e: 4 }) @@ -99,7 +99,7 @@ describe('feature flag delegate', () => { const existingFeatures: any[] = [] const existingFeaturesIndex = {} - delegate.merge(existingFeatures, [ + featureFlagDelegate.merge(existingFeatures, [ { name: 'a', variant: 'b' }, { name: 'c', variant: 'd' } ], existingFeaturesIndex) @@ -112,7 +112,7 @@ describe('feature flag delegate', () => { const existingFeatures = [{ name: 'a', variant: 'a' }, { name: 'b', variant: 'b' }, { name: 'c', variant: 'c' }, { name: 'd', variant: 'd' }, { name: 'e', variant: 'e' }] const existingFeaturesIndex = { a: 0, b: 1, c: 2, d: 3, e: 4 } - delegate.merge(existingFeatures, [ + featureFlagDelegate.merge(existingFeatures, [ { name: 'b', variant: 'x' }, { name: 'e', variant: 'y' }, { name: 'a', variant: 'z' } @@ -143,7 +143,7 @@ describe('feature flag delegate', () => { { name: 'p', variant: 'p' } ] - delegate.merge(existingFeatures, [ + featureFlagDelegate.merge(existingFeatures, [ { name: 'a', variant: 12345 }, { name: 'c', variant: 0 }, { name: 'e', variant: true }, @@ -183,7 +183,7 @@ describe('feature flag delegate', () => { const existingFeatures = [{ name: 'a', variant: 'a' }, { name: 'b', variant: 'b' }, { name: 'c', variant: 'c' }, { name: 'd', variant: 'd' }, { name: 'e', variant: 'e' }] const existingFeaturesIndex = { a: 0, b: 1, c: 2, d: 3, e: 3 } - delegate.merge(existingFeatures, [ + featureFlagDelegate.merge(existingFeatures, [ { name: 'b', variant: 'x' }, { variant: 'y' }, { name: 'a', variant: 'z' }, @@ -200,7 +200,7 @@ describe('feature flag delegate', () => { const existingFeatures = [{ name: 'a', variant: 'a' }, { name: 'b', variant: 'b' }, { name: 'c', variant: 'c' }, { name: 'd', variant: 'd' }, { name: 'e', variant: 'e' }] const existingFeaturesIndex = { a: 'a', b: 'b', c: 'c', d: 'd', e: 'e' } - delegate.merge(existingFeatures, { a: 'a', b: 'b', c: 'c' }, existingFeaturesIndex) + featureFlagDelegate.merge(existingFeatures, { a: 'a', b: 'b', c: 'c' }, existingFeaturesIndex) expect(existingFeatures).toStrictEqual([{ name: 'a', variant: 'a' }, { name: 'b', variant: 'b' }, { name: 'c', variant: 'c' }, { name: 'd', variant: 'd' }, { name: 'e', variant: 'e' }]) expect(existingFeaturesIndex).toStrictEqual({ a: 'a', b: 'b', c: 'c', d: 'd', e: 'e' }) @@ -210,7 +210,7 @@ describe('feature flag delegate', () => { const features = [{ name: 'a', variant: 'a' }, { name: 'b', variant: 'b' }, { name: 'c', variant: 'c' }, { name: 'd', variant: 'd' }, { name: 'e', variant: 'e' }] const featuresIndex = { a: 0, b: 1, c: 2, d: 3, e: 4 } - delegate.merge(features, [ + featureFlagDelegate.merge(features, [ { name: 'b', variant: 'x' }, 'name: yes', undefined, @@ -230,8 +230,8 @@ describe('feature flag delegate', () => { const features: any[] = [] const featuresIndex = {} - delegate.add(features, featuresIndex, 'a', 'b') - delegate.merge(features, [ + featureFlagDelegate.add(features, featuresIndex, 'a', 'b') + featureFlagDelegate.merge(features, [ { name: 'c', variant: 'd' }, { name: 'e' }, { name: 'f', variant: 'g' } @@ -239,7 +239,7 @@ describe('feature flag delegate', () => { expect(features).toStrictEqual([{ name: 'a', variant: 'b' }, { name: 'c', variant: 'd' }, { name: 'e', variant: null }, { name: 'f', variant: 'g' }]) - expect(delegate.toEventApi(features)).toStrictEqual([ + expect(featureFlagDelegate.toEventApi(features)).toStrictEqual([ { featureFlag: 'a', variant: 'b' }, { featureFlag: 'c', variant: 'd' }, { featureFlag: 'e' }, @@ -248,7 +248,7 @@ describe('feature flag delegate', () => { }) it('should handle an empty array', () => { - expect(delegate.toEventApi([])).toStrictEqual([]) + expect(featureFlagDelegate.toEventApi([])).toStrictEqual([]) }) }) }) diff --git a/packages/core/lib/test/validators.test.ts b/packages/core/test/lib/validators.test.ts similarity index 88% rename from packages/core/lib/test/validators.test.ts rename to packages/core/test/lib/validators.test.ts index f95b80c531..8fad64f23b 100644 --- a/packages/core/lib/test/validators.test.ts +++ b/packages/core/test/lib/validators.test.ts @@ -1,5 +1,5 @@ -import intRange from '../validators/int-range' -import stringWithLength from '../validators/string-with-length' +import intRange from '../../src/lib/validators/int-range' +import stringWithLength from '../../src/lib/validators/string-with-length' describe('intRange(min, max)(val)', () => { it('work with various values', () => { diff --git a/packages/core/test/metadata-delegate.test.js b/packages/core/test/metadata-delegate.test.js index 3dded77630..11fd8e1cb2 100644 --- a/packages/core/test/metadata-delegate.test.js +++ b/packages/core/test/metadata-delegate.test.js @@ -1,4 +1,4 @@ -import { add, clear } from '../lib/metadata-delegate' +import metadataDelegate from '../src/lib/metadata-delegate' // it doesn't seem easy or even impossible to check whether __proto__ keys can be overwritten // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto @@ -17,7 +17,7 @@ describe('metadata delegate', () => { } ])('should not add $key keys', ({ key, expected }) => { const state = {} - add(state, key, 'foo', 'bar') + metadataDelegate.add(state, key, "foo", "bar"); expect(state).toEqual(expected) }) }) @@ -51,7 +51,7 @@ describe('metadata delegate', () => { } } ])('should not overwrite $key keys', ({ key, state, expected }) => { - clear(state, key, 'foo') + metadataDelegate.clear(state, key, "foo"); expect(state).toEqual(expected) }) }) diff --git a/packages/core/test/session.test.ts b/packages/core/test/session.test.ts index 931570e567..0b4c9be594 100644 --- a/packages/core/test/session.test.ts +++ b/packages/core/test/session.test.ts @@ -1,6 +1,6 @@ -import Session from '../session' +import Session from '../src/session' -describe('@bugsnag/core/session', () => { +describe('Session', () => { describe('toJSON()', () => { it('returns the correct data structure', () => { const s = new Session().toJSON() diff --git a/packages/core/test/types.test.ts b/packages/core/test/types.test.ts index 5c1ab46bed..0124aac409 100644 --- a/packages/core/test/types.test.ts +++ b/packages/core/test/types.test.ts @@ -1,22 +1,23 @@ -import Bugsnag, { Client, Config } from '..' +import { Client, Breadcrumb, Event, Session } from '@bugsnag/core' +import type { Config } from '@bugsnag/core' // the client's constructor isn't public in TS so this drops down to JS to create one for the tests function createClient (opts: Config): Client { - const c = new (Bugsnag.Client as any)(opts, undefined, [], { name: 'Type Tests', version: 'nope', url: 'https://github.com/bugsnag/bugsnag-js' }) + const c = new Client(opts, undefined, [], { name: 'Type Tests', version: 'nope', url: 'https://github.com/bugsnag/bugsnag-js' }) c._setDelivery(() => ({ sendSession: (p: any, cb: () => void): void => { cb() }, sendEvent: (p: any, cb: () => void): void => { cb() } })) - c._sessionDelegate = { startSession: () => c, pauseSession: () => {}, resumeSession: () => {} } - return (c as Bugsnag.Client) + c._sessionDelegate = { startSession: () => c, pauseSession: () => {}, resumeSession: () => c } + return c } describe('Type definitions', () => { it('has all the classes matching the types available at runtime', () => { - expect(Bugsnag.Client).toBeDefined() - expect(Bugsnag.Breadcrumb).toBeDefined() - expect(Bugsnag.Event).toBeDefined() - expect(Bugsnag.Session).toBeDefined() + expect(Client).toBeDefined() + expect(Breadcrumb).toBeDefined() + expect(Event).toBeDefined() + expect(Session).toBeDefined() const client = createClient({ apiKey: 'API_KEY' }) expect(client.Breadcrumb).toBeDefined() expect(client.Event).toBeDefined() @@ -45,7 +46,8 @@ describe('Type definitions', () => { // eslint-disable-next-line jest/expect-expect it('works for reporting sessions', () => { const client = createClient({ apiKey: 'API_KEY' }) - const sessionClient = client.startSession() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const sessionClient = client.startSession()! sessionClient.notify(new Error('oh')) client.pauseSession() client.resumeSession() diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000000..4f17d361f3 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "lib": [ "dom", "es2022" ], + "strict": true, + "allowJs": true, + "outDir": "dist", + "declaration": true, + "emitDeclarationOnly": true, + "declarationDir": "dist/types" + }, + "include": ["src/**/*.ts", "src/**/*.js", "safe-json-stringify.d.ts"] +} diff --git a/packages/core/types/breadcrumb.d.ts b/packages/core/types/breadcrumb.d.ts deleted file mode 100644 index 409676e8cb..0000000000 --- a/packages/core/types/breadcrumb.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BreadcrumbType } from './common' - -declare class Breadcrumb { - public message: string; - public metadata: { [key: string]: any }; - public type: BreadcrumbType; - public timestamp: Date; -} - -export default Breadcrumb diff --git a/packages/core/types/bugsnag.d.ts b/packages/core/types/bugsnag.d.ts deleted file mode 100644 index 865dbaa349..0000000000 --- a/packages/core/types/bugsnag.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import Client from './client' -import { Config } from './common' - -export default interface BugsnagStatic extends Client { - start(apiKeyOrOpts: string | Config): Client - createClient(apiKeyOrOpts: string | Config): Client - isStarted(): boolean -} diff --git a/packages/core/types/client.d.ts b/packages/core/types/client.d.ts deleted file mode 100644 index 2a1c4fafc0..0000000000 --- a/packages/core/types/client.d.ts +++ /dev/null @@ -1,88 +0,0 @@ -import Breadcrumb from './breadcrumb' -import { - NotifiableError, - BreadcrumbType, - OnErrorCallback, - OnSessionCallback, - OnBreadcrumbCallback, - User, - FeatureFlag -} from './common' -import Event from './event' -import Session from './session' - -declare class Client { - protected constructor(); - - // reporting errors - public notify( - error: NotifiableError, - onError?: OnErrorCallback, - postReportCallback?: (err: any, event: Event) => void - ): void; - - public _notify( - event: Event, - onError?: OnErrorCallback, - postReportCallback?: (err: any, event: Event) => void, - ): void; - - // breadcrumbs - public leaveBreadcrumb( - message: string, - metadata?: { [key: string]: any }, - type?: BreadcrumbType - ): void; - - // metadata - public addMetadata(section: string, values: { [key: string]: any }): void; - public addMetadata(section: string, key: string, value: any): void; - public getMetadata(section: string, key?: string): any; - public clearMetadata(section: string, key?: string): void; - - // feature flags - public addFeatureFlag(name: string, variant?: string | null): void - public addFeatureFlags(featureFlags: FeatureFlag[]): void - public clearFeatureFlag(name: string): void - public clearFeatureFlags(): void - - // context - public getContext(): string | undefined; - public setContext(c: string): void; - - // grouping discriminator - public getGroupingDiscriminator(): string | undefined; - public setGroupingDiscriminator(groupingDiscriminator: string | undefined): string | undefined; - - // user - public getUser(): User; - public setUser(id?: string | null, email?: string | null, name?: string | null): void; - - // sessions - public startSession(): Client; - public pauseSession(): void; - public resumeSession(): Client; - - // callbacks - public addOnError(fn: OnErrorCallback): void; - public removeOnError(fn: OnErrorCallback): void; - - public addOnSession(fn: OnSessionCallback): void; - public removeOnSession(fn: OnSessionCallback): void; - - public addOnBreadcrumb(fn: OnBreadcrumbCallback): void; - public removeOnBreadcrumb(fn: OnBreadcrumbCallback): void; - - // plugins - public getPlugin(name: string): any; - - // implemented on the browser notifier only - public resetEventCount?(): void; - - // access to internal classes - public Breadcrumb: typeof Breadcrumb; - public Event: typeof Event; - public Session: typeof Session; -} - -export default Client diff --git a/packages/core/types/event.d.ts b/packages/core/types/event.d.ts deleted file mode 100644 index 114b0d2a4a..0000000000 --- a/packages/core/types/event.d.ts +++ /dev/null @@ -1,81 +0,0 @@ -import Breadcrumb from './breadcrumb' -import { - App, - Device, - Request, - Logger, - User, - Thread, - Stackframe, - FeatureFlag -} from './common' - -declare class Event { - public static create( - maybeError: any, - tolerateNonErrors: boolean, - handledState: HandledState, - component: string, - errorFramesToSkip: number, - logger?: Logger - ): Event - - public app: App - public device: Device - public request: Request - - public errors: Error[]; - public breadcrumbs: Breadcrumb[] - public threads: Thread[] - - public severity: 'info' | 'warning' | 'error' - - public readonly originalError: any - public unhandled: boolean - - public apiKey?: string - public context?: string - public groupingHash?: string - - // user - public getUser(): User - public setUser(id?: string, email?: string, name?: string): void - - // metadata - public addMetadata(section: string, values: { [key: string]: any }): void - public addMetadata(section: string, key: string, value: any): void - public getMetadata(section: string, key?: string): any - public clearMetadata(section: string, key?: string): void - - // feature flags - public getFeatureFlags(): FeatureFlag[] - public addFeatureFlag(name: string, variant?: string | null): void - public addFeatureFlags(featureFlags: FeatureFlag[]): void - public clearFeatureFlag(name: string): void - public clearFeatureFlags(): void - - // trace correlation - public setTraceCorrelation(traceId: string, spanId?: string): void - - // grouping discriminators - public getGroupingDiscriminator(): string | undefined - public setGroupingDiscriminator(value: string | undefined): string | undefined -} - -interface HandledState { - severity: string - unhandled: boolean - severityReason: { - type: string - [key: string]: any - } -} - -export interface Error { - errorClass: string - errorMessage: string - stacktrace: Stackframe[] - type: string -} - -export default Event diff --git a/packages/core/types/index.d.ts b/packages/core/types/index.d.ts deleted file mode 100644 index bccb0aafd7..0000000000 --- a/packages/core/types/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import Breadcrumb from './breadcrumb' -import Client from './client' -import Event from './event' -import Session from './session' -import BugsnagStatic from './bugsnag' - -export * from './common' -export { Breadcrumb, Client, Event, Session, BugsnagStatic } diff --git a/packages/core/types/session.d.ts b/packages/core/types/session.d.ts deleted file mode 100644 index 73ec509c1a..0000000000 --- a/packages/core/types/session.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { App, Device, User } from './common' - -declare class Session { - public startedAt: Date; - public id: string; - - public device?: Device; - public app?: App; - - public getUser(): User; - public setUser(id?: string, email?: string, name?: string): void; -} - -export default Session diff --git a/packages/delivery-electron/delivery.d.ts b/packages/delivery-electron/delivery.d.ts index 33c75280c5..0c0bafc87e 100644 --- a/packages/delivery-electron/delivery.d.ts +++ b/packages/delivery-electron/delivery.d.ts @@ -1,5 +1,4 @@ -import { Delivery } from '@bugsnag/core/client' -import { Client } from '@bugsnag/core' +import { Client, Delivery } from '@bugsnag/core' declare const delivery: (filestore: any, net: any, app: any) => (client: Client) => Delivery diff --git a/packages/delivery-electron/delivery.js b/packages/delivery-electron/delivery.js index 1fa8359899..b4ef029343 100644 --- a/packages/delivery-electron/delivery.js +++ b/packages/delivery-electron/delivery.js @@ -1,5 +1,5 @@ const { createHash } = require('crypto') -const payload = require('@bugsnag/core/lib/json-payload') +const jsonPayload = require('@bugsnag/json-payload') const PayloadQueue = require('./queue') const PayloadDeliveryLoop = require('./payload-loop') const NetworkStatus = require('@bugsnag/electron-network-status') @@ -74,9 +74,9 @@ const delivery = (client, filestore, net, app) => { const statusUpdater = new NetworkStatus(stateManagerPlugin, net, app) const { queues } = initRedelivery(filestore.getPaths(), statusUpdater, client._logger, send) - const hash = payload => { + const hash = jsonPayload => { const h = createHash('sha1') - h.update(payload) + h.update(jsonPayload) return h.digest('hex') } @@ -91,7 +91,7 @@ const delivery = (client, filestore, net, app) => { let body, opts try { - body = payload.event(event, client._config.redactedKeys) + body = jsonPayload.event(event, client._config.redactedKeys) opts = { url, method: 'POST', @@ -131,7 +131,7 @@ const delivery = (client, filestore, net, app) => { let body, opts try { - body = payload.session(session, client._config.redactedKeys) + body = jsonPayload.session(session, client._config.redactedKeys) opts = { url, method: 'POST', diff --git a/packages/delivery-electron/package.json b/packages/delivery-electron/package.json index 10eeb1569b..d617d382e6 100644 --- a/packages/delivery-electron/package.json +++ b/packages/delivery-electron/package.json @@ -21,8 +21,10 @@ "@bugsnag/electron-network-status": "^8.4.0", "@bugsnag/plugin-electron-client-state-manager": "^8.4.0" }, + "dependencies": { + "@bugsnag/json-payload": "^8.4.0" + }, "peerDependencies": { - "@bugsnag/core": "^8.0.0", "@bugsnag/electron-network-status": "^8.0.0" } } diff --git a/packages/delivery-electron/test/delivery.test-main.ts b/packages/delivery-electron/test/delivery.test-main.ts index c4321167ea..f6e62c2e74 100644 --- a/packages/delivery-electron/test/delivery.test-main.ts +++ b/packages/delivery-electron/test/delivery.test-main.ts @@ -2,8 +2,7 @@ import { createServer, IncomingHttpHeaders, STATUS_CODES } from 'http' import { app, net } from 'electron' import { AddressInfo } from 'net' import delivery from '../delivery' -import { EventDeliveryPayload } from '@bugsnag/core/client' -import { Client } from '@bugsnag/core' +import type { Client, EventDeliveryPayload } from '@bugsnag/core' import PayloadQueue from '../queue' import PayloadDeliveryLoop from '../payload-loop' import { mkdtemp, rm } from 'fs/promises' diff --git a/packages/delivery-electron/test/queue.test.ts b/packages/delivery-electron/test/queue.test.ts index cf1827e9be..c3dfddf650 100644 --- a/packages/delivery-electron/test/queue.test.ts +++ b/packages/delivery-electron/test/queue.test.ts @@ -35,7 +35,6 @@ describe('delivery: electron -> queue', () => { try { await queue.init() } catch (e) { - // eslint-disable-next-line jest/no-try-expect expect(e).toBeTruthy() didErr = true } @@ -48,7 +47,7 @@ describe('delivery: electron -> queue', () => { const queue = new PayloadQueue(storagePath, 'stuff') const errs: any[] = [] - // eslint-disable-next-line jest/valid-expect-in-promise + await Promise.all([ queue.init().catch(err => errs.push(err)), queue.init().catch(err => errs.push(err)), diff --git a/packages/delivery-fetch/delivery.d.ts b/packages/delivery-fetch/delivery.d.ts index 9d13cd84f3..a5636f74a5 100644 --- a/packages/delivery-fetch/delivery.d.ts +++ b/packages/delivery-fetch/delivery.d.ts @@ -1,6 +1,9 @@ -import type { Client } from '@bugsnag/core' -import type { Delivery } from '@bugsnag/core/client' +/// -declare const delivery: (client: Client, fetch?: GlobalFetch['fetch']) => Delivery +import type { Client, Delivery } from '@bugsnag/core' + +type Fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise + +declare const delivery: (client: Client, fetch?: Fetch, windowOrWorkerGlobalScope?: Window | ServiceWorkerGlobalScope) => Delivery export default delivery diff --git a/packages/delivery-fetch/delivery.js b/packages/delivery-fetch/delivery.js index 31616800db..2397efc2fb 100644 --- a/packages/delivery-fetch/delivery.js +++ b/packages/delivery-fetch/delivery.js @@ -1,4 +1,4 @@ -import payload from '@bugsnag/core/lib/json-payload' +import * as jsonPayload from '@bugsnag/json-payload' function getIntegrityHeaderValue (sendPayloadChecksums, windowOrWorkerGlobalScope, requestBody, headers) { if (sendPayloadChecksums && windowOrWorkerGlobalScope.isSecureContext && windowOrWorkerGlobalScope.crypto && windowOrWorkerGlobalScope.crypto.subtle && windowOrWorkerGlobalScope.crypto.subtle.digest && typeof TextEncoder === 'function') { @@ -20,7 +20,7 @@ const delivery = (client, fetch = global.fetch, windowOrWorkerGlobalScope = wind sendEvent: (event, cb = () => {}) => { const url = client._config.endpoints.notify - const body = payload.event(event, client._config.redactedKeys) + const body = jsonPayload.event(event, client._config.redactedKeys) getIntegrityHeaderValue(client._config.sendPayloadChecksums, windowOrWorkerGlobalScope, body).then(integrityHeaderValue => { const headers = { @@ -49,7 +49,7 @@ const delivery = (client, fetch = global.fetch, windowOrWorkerGlobalScope = wind sendSession: (session, cb = () => { }) => { const url = client._config.endpoints.sessions - const body = payload.session(session, client._config.redactedKeys) + const body = jsonPayload.session(session, client._config.redactedKeys) getIntegrityHeaderValue(client._config.sendPayloadChecksums, windowOrWorkerGlobalScope, body).then((integrityHeaderValue) => { const headers = { diff --git a/packages/delivery-fetch/package.json b/packages/delivery-fetch/package.json index a540d4610c..77ac54aa2a 100644 --- a/packages/delivery-fetch/package.json +++ b/packages/delivery-fetch/package.json @@ -13,10 +13,10 @@ "access": "public" }, "license": "MIT", + "dependencies": { + "@bugsnag/json-payload": "^8.4.0" + }, "devDependencies": { "@bugsnag/core": "^8.4.0" - }, - "peerDependencies": { - "@bugsnag/core": "^8.0.0" } } diff --git a/packages/delivery-fetch/test/delivery.test.ts b/packages/delivery-fetch/test/delivery.test.ts index afd9911c11..1b130b496a 100644 --- a/packages/delivery-fetch/test/delivery.test.ts +++ b/packages/delivery-fetch/test/delivery.test.ts @@ -1,6 +1,5 @@ import delivery from '../delivery' -import type { Client } from '@bugsnag/core' -import type { EventDeliveryPayload, SessionDeliveryPayload } from '@bugsnag/core/client' +import type { Client, EventDeliveryPayload, SessionDeliveryPayload } from '@bugsnag/core' const globalAny: any = global diff --git a/packages/delivery-node/delivery.d.ts b/packages/delivery-node/delivery.d.ts index 9dd36b3a1a..b6f8c20bf9 100644 --- a/packages/delivery-node/delivery.d.ts +++ b/packages/delivery-node/delivery.d.ts @@ -1,5 +1,4 @@ -import { Delivery } from '@bugsnag/core/client' -import { Client } from '@bugsnag/core' +import { Client, Delivery } from '@bugsnag/core' declare const delivery: (client: Client) => Delivery diff --git a/packages/delivery-node/delivery.js b/packages/delivery-node/delivery.js index 248edae682..e1e0afe3ad 100644 --- a/packages/delivery-node/delivery.js +++ b/packages/delivery-node/delivery.js @@ -1,9 +1,9 @@ -const payload = require('@bugsnag/core/lib/json-payload') +const jsonPayload = require('@bugsnag/json-payload') const request = require('./request') module.exports = (client) => ({ sendEvent: (event, cb = () => {}) => { - const body = payload.event(event, client._config.redactedKeys) + const body = jsonPayload.event(event, client._config.redactedKeys) const _cb = err => { if (err) client._logger.error(`Event failed to send…\n${(err && err.stack) ? err.stack : err}`, err) @@ -54,7 +54,7 @@ module.exports = (client) => ({ 'Bugsnag-Payload-Version': '1', 'Bugsnag-Sent-At': (new Date()).toISOString() }, - body: payload.session(session, client._config.redactedKeys), + body: jsonPayload.session(session, client._config.redactedKeys), agent: client._config.agent }, err => _cb(err)) } catch (e) { diff --git a/packages/delivery-node/package.json b/packages/delivery-node/package.json index 8392d86837..3c38648429 100644 --- a/packages/delivery-node/package.json +++ b/packages/delivery-node/package.json @@ -19,7 +19,7 @@ "devDependencies": { "@bugsnag/core": "^8.4.0" }, - "peerDependencies": { - "@bugsnag/core": "^8.0.0" + "dependencies": { + "@bugsnag/json-payload": "^8.4.0" } } diff --git a/packages/delivery-node/request.js b/packages/delivery-node/request.js index 2e8c3bf29d..d60b0fb2c8 100644 --- a/packages/delivery-node/request.js +++ b/packages/delivery-node/request.js @@ -1,6 +1,5 @@ const http = require('http') const https = require('https') -// eslint-disable-next-line node/no-deprecated-api const { parse } = require('url') module.exports = ({ url, headers, body, agent }, cb) => { diff --git a/packages/delivery-node/test/delivery.test.ts b/packages/delivery-node/test/delivery.test.ts index 23c2c35fc7..787102ce6f 100644 --- a/packages/delivery-node/test/delivery.test.ts +++ b/packages/delivery-node/test/delivery.test.ts @@ -1,8 +1,7 @@ import delivery from '../' import http from 'http' -import { Client } from '@bugsnag/core' -import { EventDeliveryPayload, SessionDeliveryPayload } from '@bugsnag/core/client' -import { AddressInfo } from 'net' +import type { Client, EventDeliveryPayload, SessionDeliveryPayload } from '@bugsnag/core' +import type { AddressInfo } from 'net' interface Request { url?: string diff --git a/packages/delivery-react-native/package.json b/packages/delivery-react-native/package.json index 47c6ec45f7..e65501c3ec 100644 --- a/packages/delivery-react-native/package.json +++ b/packages/delivery-react-native/package.json @@ -1,7 +1,7 @@ { "name": "@bugsnag/delivery-react-native", "version": "8.4.0", - "main": "delivery.js", + "main": "dist/delivery.js", "description": "@bugsnag/js delivery mechanism for React Native", "homepage": "https://www.bugsnag.com/", "repository": { @@ -12,12 +12,16 @@ "access": "public" }, "files": [ - "*.js" + "dist" ], + "scripts": { + "build": "rollup --config rollup.config.js" + }, "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^8.4.0" + "@bugsnag/core": "^8.4.0", + "@bugsnag/derecursify": "^8.4.0" }, "peerDependencies": { "@bugsnag/core": "^8.0.0" diff --git a/packages/delivery-react-native/rollup.config.js b/packages/delivery-react-native/rollup.config.js new file mode 100644 index 0000000000..35c0438633 --- /dev/null +++ b/packages/delivery-react-native/rollup.config.js @@ -0,0 +1,21 @@ +/* eslint-env node */ +/* global require, module */ +const { nodeResolve } = require('@rollup/plugin-node-resolve') +const commonjs = require('@rollup/plugin-commonjs') +const babel = require('@rollup/plugin-babel'); + +module.exports = { + input: 'src/delivery.js', + output: { + file: 'dist/delivery.js', + format: 'cjs' + }, + plugins: [ + nodeResolve({ + preferBuiltins: true + }), + commonjs(), + babel({ babelHelpers: 'bundled' }) + ], + external: ['@bugsnag/core'] +} diff --git a/packages/delivery-react-native/delivery.js b/packages/delivery-react-native/src/delivery.js similarity index 96% rename from packages/delivery-react-native/delivery.js rename to packages/delivery-react-native/src/delivery.js index 49561f9061..d6c6791c8f 100644 --- a/packages/delivery-react-native/delivery.js +++ b/packages/delivery-react-native/src/delivery.js @@ -1,4 +1,4 @@ -const derecursify = require('@bugsnag/core/lib/derecursify') +const { derecursify } = require('@bugsnag/derecursify') module.exports = (client, NativeClient) => ({ sendEvent: (payload, cb = () => {}) => { diff --git a/packages/delivery-react-native/test/delivery.test.ts b/packages/delivery-react-native/test/delivery.test.ts index 27d847cc2d..3f5c078de6 100644 --- a/packages/delivery-react-native/test/delivery.test.ts +++ b/packages/delivery-react-native/test/delivery.test.ts @@ -1,6 +1,5 @@ -import Client from '@bugsnag/core/client' -import delivery from '../' -import EventWithInternals from '@bugsnag/core/event' +import { Client, Event } from '@bugsnag/core' +import delivery from '../src/delivery' type NativeStackIOS = string[] interface AndroidStackFrame { @@ -11,7 +10,7 @@ interface AndroidStackFrame { } type NativeStackAndroid = AndroidStackFrame[] -type NativeClientEvent = Pick ({ - sendEvent: (event, cb = () => {}) => { - if (client._config.endpoints.notify === null) { - const err = new Error('Event not sent due to incomplete endpoint configuration') - return cb(err) - } - - const url = getApiUrl(client._config, 'notify', '4', win) - const body = payload.event(event, client._config.redactedKeys) - - const req = new win.XDomainRequest() - req.onload = function () { - cb(null) - } - req.onerror = function () { - const err = new Error('Event failed to send') - client._logger.error('Event failed to send…', err) - if (body.length > 10e5) { - client._logger.warn(`Event oversized (${(body.length / 10e5).toFixed(2)} MB)`) - } - cb(err) - } - req.open('POST', url) - setTimeout(() => { - try { - req.send(body) - } catch (e) { - client._logger.error(e) - cb(e) - } - }, 0) - }, - sendSession: (session, cb = () => {}) => { - if (client._config.endpoints.sessions === null) { - const err = new Error('Session not sent due to incomplete endpoint configuration') - return cb(err) - } - - const url = getApiUrl(client._config, 'sessions', '1', win) - const req = new win.XDomainRequest() - req.onload = function () { - cb(null) - } - req.open('POST', url) - setTimeout(() => { - try { - req.send(payload.session(session, client._config.redactedKeys)) - } catch (e) { - client._logger.error(e) - cb(e) - } - }, 0) - } -}) - -const getApiUrl = (config, endpoint, version, win) => { - // IE8 doesn't support Date.prototype.toISOstring(), but it does convert a date - // to an ISO string when you use JSON stringify. Simply parsing the result of - // JSON.stringify is smaller than using a toISOstring() polyfill. - const isoDate = JSON.parse(JSON.stringify(new Date())) - const url = matchPageProtocol(config.endpoints[endpoint], win.location.protocol) - return `${url}?apiKey=${encodeURIComponent(config.apiKey)}&payloadVersion=${version}&sentAt=${encodeURIComponent(isoDate)}` -} - -const matchPageProtocol = module.exports._matchPageProtocol = (endpoint, pageProtocol) => - pageProtocol === 'http:' - ? endpoint.replace(/^https:/, 'http:') - : endpoint diff --git a/packages/delivery-x-domain-request/package.json b/packages/delivery-x-domain-request/package.json deleted file mode 100644 index 1701000e7b..0000000000 --- a/packages/delivery-x-domain-request/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@bugsnag/delivery-x-domain-request", - "version": "8.4.0", - "main": "delivery.js", - "description": "@bugsnag/js delivery mechanism for IE 8, 9 and 10", - "homepage": "https://www.bugsnag.com/", - "repository": { - "type": "git", - "url": "git@github.com:bugsnag/bugsnag-js.git" - }, - "publishConfig": { - "access": "public" - }, - "files": [ - "*.js" - ], - "author": "Bugsnag", - "license": "MIT", - "devDependencies": { - "@bugsnag/core": "^8.4.0" - }, - "peerDependencies": { - "@bugsnag/core": "^8.0.0" - } -} diff --git a/packages/delivery-x-domain-request/test/delivery.test.ts b/packages/delivery-x-domain-request/test/delivery.test.ts deleted file mode 100644 index cf70215220..0000000000 --- a/packages/delivery-x-domain-request/test/delivery.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -import delivery from '../' -import { Client } from '@bugsnag/core' -import { SessionDeliveryPayload, EventDeliveryPayload } from '@bugsnag/core/client' - -interface XDomainRequest { - method: string | null - url: string | null - data: string | null -} - -describe('delivery:XDomainRequest', () => { - it('sends events successfully', done => { - const requests: XDomainRequest[] = [] - - // mock XDomainRequest class - function XDomainRequest (this: XDomainRequest) { - this.method = null - this.url = null - this.data = null - requests.push(this) - } - XDomainRequest.DONE = 4 - XDomainRequest.prototype.open = function (method: string, url: string) { - this.method = method - this.url = url - } - XDomainRequest.prototype.send = function (data: string) { - this.data = data - this.onload() - } - - const window = { XDomainRequest, location: { protocol: 'https://' } } as unknown as Window - const payload = { sample: 'payload' } as unknown as EventDeliveryPayload - const config = { - apiKey: 'aaaaaaaa', - endpoints: { notify: '/echo/', sessions: '/sessions/' }, - redactedKeys: [] - } - delivery({ logger: {}, _config: config } as unknown as Client, window).sendEvent(payload, (err) => { - expect(err).toBe(null) - expect(requests.length).toBe(1) - expect(requests[0].method).toBe('POST') - expect(requests[0].url).toMatch( - /\/echo\/\?apiKey=aaaaaaaa&payloadVersion=4&sentAt=\d{4}-\d{2}-\d{2}T\d{2}%3A\d{2}%3A\d{2}\.\d{3}Z/ - ) - expect(requests[0].data).toBe(JSON.stringify(payload)) - done() - }) - }) - - it('prevents event delivery with incomplete config', done => { - const requests: XDomainRequest[] = [] - - // mock XDomainRequest class - function XDomainRequest (this: XDomainRequest) { - this.method = null - this.url = null - this.data = null - requests.push(this) - } - XDomainRequest.DONE = 4 - XDomainRequest.prototype.open = function (method: string, url: string) { - this.method = method - this.url = url - } - XDomainRequest.prototype.send = function (data: string) { - this.data = data - this.onload() - } - - const window = { XDomainRequest, location: { protocol: 'https://' } } as unknown as Window - const payload = { sample: 'payload' } as unknown as EventDeliveryPayload - const config = { - apiKey: 'aaaaaaaa', - endpoints: { notify: null }, - redactedKeys: [] - } - delivery({ logger: {}, _config: config } as unknown as Client, window).sendEvent(payload, (err) => { - expect(err).toStrictEqual(new Error('Event not sent due to incomplete endpoint configuration')) - expect(requests.length).toBe(0) - done() - }) - }) - - it('calls back with an error when report sending fails', done => { - // mock XDomainRequest class - function XDomainRequest () {} - XDomainRequest.prototype.open = function (method: string, url: string) { - this.method = method - this.url = url - } - XDomainRequest.prototype.send = function (method: string, url: string) { - throw new Error('send error') - } - const window = { XDomainRequest, location: { protocol: 'https://' } } as unknown as Window - const payload = { sample: 'payload' } as unknown as EventDeliveryPayload - const config = { - apiKey: 'aaaaaaaa', - endpoints: { notify: '/echo/', sessions: '/sessions/' }, - redactedKeys: [] - } - delivery({ _logger: { error: () => {} }, _config: config } as unknown as Client, window).sendEvent(payload, (err) => { - expect(err).not.toBe(null) - expect(err?.message).toBe('send error') - done() - }) - }) - - it('logs failures and large payloads', done => { - // mock XDomainRequest class - function XDomainRequest () { - } - XDomainRequest.prototype.open = function (method: string, url: string) { - this.method = method - this.url = url - } - XDomainRequest.prototype.send = function (method: string, url: string) { - this.onerror() - } - const window = { XDomainRequest, location: { protocol: 'https://' } } as unknown as Window - - const lotsOfEvents: any[] = [] - while (JSON.stringify(lotsOfEvents).length < 10e5) { - lotsOfEvents.push({ errors: [{ errorClass: 'Error', errorMessage: 'long repetitive string'.repeat(1000) }] }) - } - const payload = { - events: lotsOfEvents - } as unknown as EventDeliveryPayload - - const config = { - apiKey: 'aaaaaaaa', - endpoints: { notify: '/echo/', sessions: '/sessions/' }, - redactedKeys: [] - } - const logger = { error: jest.fn(), warn: jest.fn() } - delivery({ _logger: logger, _config: config } as unknown as Client, window).sendEvent(payload, (err) => { - const expectedError = new Error('Event failed to send') - expect(err).toStrictEqual(expectedError) - expect(logger.error).toHaveBeenCalledWith('Event failed to send…', expectedError) - expect(logger.warn).toHaveBeenCalledWith('Event oversized (1.01 MB)') - done() - }) - }) - - it('sends sessions successfully', done => { - const requests: XDomainRequest[] = [] - - // mock XDomainRequest class - function XDomainRequest (this: XDomainRequest, t: typeof window) { - this.method = null - this.url = null - this.data = null - requests.push(this) - } - XDomainRequest.DONE = 4 - XDomainRequest.prototype.open = function (method: string, url: string) { - this.method = method - this.url = url - } - XDomainRequest.prototype.send = function (data: string) { - this.data = data - this.onload() - } - - const window = { XDomainRequest, location: { protocol: 'https://' } } as unknown as Window - const payload = { sample: 'payload' } as unknown as SessionDeliveryPayload - const config = { - apiKey: 'aaaaaaaa', - endpoints: { notify: '/echo/', sessions: '/sessions/' }, - redactedKeys: [] - } - delivery({ logger: {}, _config: config } as unknown as Client, window).sendSession(payload, (err) => { - expect(err).toBe(null) - expect(requests.length).toBe(1) - expect(requests[0].method).toBe('POST') - expect(requests[0].url).toMatch( - /\/sessions\/\?apiKey=aaaaaaaa&payloadVersion=1&sentAt=\d{4}-\d{2}-\d{2}T\d{2}%3A\d{2}%3A\d{2}\.\d{3}Z/ - ) - expect(requests[0].data).toBe(JSON.stringify(payload)) - done() - }) - }) - - it('prevents session delivery with incomplete config', done => { - const requests: XDomainRequest[] = [] - - // mock XDomainRequest class - function XDomainRequest (this: XDomainRequest, t: typeof window) { - this.method = null - this.url = null - this.data = null - requests.push(this) - } - XDomainRequest.DONE = 4 - XDomainRequest.prototype.open = function (method: string, url: string) { - this.method = method - this.url = url - } - XDomainRequest.prototype.send = function (data: string) { - this.data = data - this.onload() - } - - const window = { XDomainRequest, location: { protocol: 'https://' } } as unknown as Window - const payload = { sample: 'payload' } as unknown as SessionDeliveryPayload - const config = { - apiKey: 'aaaaaaaa', - endpoints: { sessions: null }, - redactedKeys: [] - } - delivery({ logger: {}, _config: config } as unknown as Client, window).sendSession(payload, (err) => { - expect(err).toStrictEqual(new Error('Session not sent due to incomplete endpoint configuration')) - expect(requests.length).toBe(0) - done() - }) - }) - - it('calls back with an error when session sending fails', done => { - // mock XDomainRequest class - function XDomainRequest () {} - XDomainRequest.prototype.open = function (method: string, url: string) { - this.method = method - this.url = url - } - XDomainRequest.prototype.send = function (method: string, url: string) { - throw new Error('send error') - } - const window = { XDomainRequest, location: { protocol: 'https://' } } as unknown as Window - const payload = { sample: 'payload' } as unknown as SessionDeliveryPayload - const config = { - apiKey: 'aaaaaaaa', - endpoints: { notify: '/echo/', sessions: '/sessions/' }, - filters: [] - } - delivery({ _logger: { error: () => {} }, _config: config } as unknown as Client, window).sendSession(payload, (err) => { - expect(err).not.toBe(null) - expect(err?.message).toBe('send error') - done() - }) - }) -}) - -describe('delivery:XDomainRequest matchPageProtocol()', () => { - it('should swap https: -> http: when the current protocol is http', () => { - expect( - delivery._matchPageProtocol('https://notify.bugsnag.com/', 'http:') - ).toBe('http://notify.bugsnag.com/') - }) - it('should not swap https: -> http: when the current protocol is https', () => { - expect( - delivery._matchPageProtocol('https://notify.bugsnag.com/', 'https:') - ).toBe('https://notify.bugsnag.com/') - }) -}) diff --git a/packages/delivery-xml-http-request/delivery.d.ts b/packages/delivery-xml-http-request/delivery.d.ts deleted file mode 100644 index 05552de3af..0000000000 --- a/packages/delivery-xml-http-request/delivery.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Delivery } from '@bugsnag/core/client' -import { Client } from '@bugsnag/core' - -declare const delivery: (client: Client, window?: Window) => Delivery - -export default delivery diff --git a/packages/delivery-xml-http-request/package.json b/packages/delivery-xml-http-request/package.json index ff0bf6c6ec..0ebb30a3bf 100644 --- a/packages/delivery-xml-http-request/package.json +++ b/packages/delivery-xml-http-request/package.json @@ -1,7 +1,15 @@ { "name": "@bugsnag/delivery-xml-http-request", "version": "8.4.0", - "main": "delivery.js", + "main": "dist/delivery.js", + "types": "dist/types/delivery.d.ts", + "exports": { + ".": { + "types": "./dist/types/delivery.d.ts", + "default": "./dist/delivery.js", + "import": "./dist/delivery.mjs" + } + }, "description": "@bugsnag/js delivery mechanism for most browsers", "homepage": "https://www.bugsnag.com/", "repository": { @@ -12,14 +20,19 @@ "access": "public" }, "files": [ - "*.js" + "dist" ], "author": "Bugsnag", "license": "MIT", + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" + }, "devDependencies": { "@bugsnag/core": "^8.4.0" }, - "peerDependencies": { - "@bugsnag/core": "^8.0.0" + "dependencies": { + "@bugsnag/json-payload": "^8.4.0" } } diff --git a/packages/delivery-xml-http-request/rollup.config.npm.mjs b/packages/delivery-xml-http-request/rollup.config.npm.mjs new file mode 100644 index 0000000000..398bb7e9b4 --- /dev/null +++ b/packages/delivery-xml-http-request/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from "../../.rollup/index.mjs"; + +export default createRollupConfig({ + input: "src/delivery.ts", + external: ['@bugsnag/core'] +}); diff --git a/packages/delivery-xml-http-request/delivery.js b/packages/delivery-xml-http-request/src/delivery.ts similarity index 67% rename from packages/delivery-xml-http-request/delivery.js rename to packages/delivery-xml-http-request/src/delivery.ts index 2714c1c8a4..becd123a94 100644 --- a/packages/delivery-xml-http-request/delivery.js +++ b/packages/delivery-xml-http-request/src/delivery.ts @@ -1,6 +1,7 @@ -const payload = require('@bugsnag/core/lib/json-payload') +import type { Delivery, Client, Config } from '@bugsnag/core' +import * as jsonPayload from '@bugsnag/json-payload' -function getIntegrityHeaderValue (windowOrWorkerGlobalScope, requestBody) { +function getIntegrityHeaderValue (windowOrWorkerGlobalScope: Window, requestBody: string) { if (windowOrWorkerGlobalScope.isSecureContext && windowOrWorkerGlobalScope.crypto && windowOrWorkerGlobalScope.crypto.subtle && windowOrWorkerGlobalScope.crypto.subtle.digest && typeof TextEncoder === 'function') { const msgUint8 = new TextEncoder().encode(requestBody) return windowOrWorkerGlobalScope.crypto.subtle.digest('SHA-1', msgUint8).then((hashBuffer) => { @@ -15,25 +16,28 @@ function getIntegrityHeaderValue (windowOrWorkerGlobalScope, requestBody) { return Promise.resolve() } -module.exports = (client, win = window) => ({ +const delivery = (client: Client, win = window): Delivery => ({ sendEvent: (event, cb = () => {}) => { + const config = client._config as Required + const logger = client._logger + try { - const url = client._config.endpoints.notify + const url = config.endpoints.notify if (url === null) { const err = new Error('Event not sent due to incomplete endpoint configuration') return cb(err) } const req = new win.XMLHttpRequest() - const body = payload.event(event, client._config.redactedKeys) + const body = jsonPayload.event(event, config.redactedKeys) req.onreadystatechange = function () { if (req.readyState === win.XMLHttpRequest.DONE) { const status = req.status if (status === 0 || status >= 400) { - const err = new Error(`Request failed with status ${status}`) - client._logger.error('Event failed to send…', err) + const err = new Error(`Request failed with status ${status}`); + logger.error('Event failed to send…', err) if (body.length > 10e5) { - client._logger.warn(`Event oversized (${(body.length / 10e5).toFixed(2)} MB)`) + logger.warn(`Event oversized (${(body.length / 10e5).toFixed(2)} MB)`) } cb(err) } else { @@ -44,43 +48,46 @@ module.exports = (client, win = window) => ({ req.open('POST', url) req.setRequestHeader('Content-Type', 'application/json') - req.setRequestHeader('Bugsnag-Api-Key', event.apiKey || client._config.apiKey) + req.setRequestHeader('Bugsnag-Api-Key', event.apiKey || config.apiKey) req.setRequestHeader('Bugsnag-Payload-Version', '4') req.setRequestHeader('Bugsnag-Sent-At', (new Date()).toISOString()) - if (client._config.sendPayloadChecksums && typeof Promise !== 'undefined' && Promise.toString().indexOf('[native code]') !== -1) { + if (config.sendPayloadChecksums && typeof Promise !== 'undefined' && Promise.toString().indexOf('[native code]') !== -1) { getIntegrityHeaderValue(win, body).then((integrity) => { if (integrity) { req.setRequestHeader('Bugsnag-Integrity', integrity) } req.send(body) }).catch((err) => { - client._logger.error(err) + logger.error(err) req.send(body) }) } else { req.send(body) } } catch (e) { - client._logger.error(e) + logger.error(e) } }, sendSession: (session, cb = () => {}) => { + const config = client._config as Required + const logger = client._logger + try { - const url = client._config.endpoints.sessions + const url = config.endpoints.sessions if (url === null) { const err = new Error('Session not sent due to incomplete endpoint configuration') return cb(err) } const req = new win.XMLHttpRequest() - const body = payload.session(session, client._config.redactedKeys) + const body = jsonPayload.session(session, config.redactedKeys) req.onreadystatechange = function () { if (req.readyState === win.XMLHttpRequest.DONE) { const status = req.status if (status === 0 || status >= 400) { - const err = new Error(`Request failed with status ${status}`) - client._logger.error('Session failed to send…', err) + const err = new Error(`Request failed with status ${status}`); + logger.error('Session failed to send…', err) cb(err) } else { cb(null) @@ -90,25 +97,27 @@ module.exports = (client, win = window) => ({ req.open('POST', url) req.setRequestHeader('Content-Type', 'application/json') - req.setRequestHeader('Bugsnag-Api-Key', client._config.apiKey) + req.setRequestHeader('Bugsnag-Api-Key', config.apiKey) req.setRequestHeader('Bugsnag-Payload-Version', '1') req.setRequestHeader('Bugsnag-Sent-At', (new Date()).toISOString()) - if (client._config.sendPayloadChecksums && typeof Promise !== 'undefined' && Promise.toString().indexOf('[native code]') !== -1) { + if (config.sendPayloadChecksums && typeof Promise !== 'undefined' && Promise.toString().indexOf('[native code]') !== -1) { getIntegrityHeaderValue(win, body).then((integrity) => { if (integrity) { req.setRequestHeader('Bugsnag-Integrity', integrity) } req.send(body) }).catch((err) => { - client._logger.error(err) + logger.error(err) req.send(body) }) } else { req.send(body) } } catch (e) { - client._logger.error(e) + logger.error(e) } } }) + +export default delivery \ No newline at end of file diff --git a/packages/delivery-xml-http-request/test/delivery.test.ts b/packages/delivery-xml-http-request/test/delivery.test.ts index 652ab458b6..066ca69a8d 100644 --- a/packages/delivery-xml-http-request/test/delivery.test.ts +++ b/packages/delivery-xml-http-request/test/delivery.test.ts @@ -1,6 +1,5 @@ import delivery from '../' -import { Client } from '@bugsnag/core' -import { EventDeliveryPayload } from '@bugsnag/core/client' +import type { Client, EventDeliveryPayload } from '@bugsnag/core' interface MockXMLHttpRequest { method: string | null diff --git a/packages/delivery-xml-http-request/tsconfig.json b/packages/delivery-xml-http-request/tsconfig.json new file mode 100644 index 0000000000..a5cb75c562 --- /dev/null +++ b/packages/delivery-xml-http-request/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"] +} diff --git a/packages/derecursify/README.md b/packages/derecursify/README.md new file mode 100644 index 0000000000..691a9d4d3a --- /dev/null +++ b/packages/derecursify/README.md @@ -0,0 +1,26 @@ +# @bugsnag/derecursify + +Internal utility for safely serializing objects by removing circular references and handling various data types without calling `toJSON` methods. + +This is a private package used internally by other Bugsnag packages. + +## Usage + +```javascript +const { derecursify } = require('@bugsnag/derecursify') + +const obj = { a: 1, b: new Date(), c: new Error('test') } +obj.circular = obj + +const safe = derecursify(obj) +// Returns: { a: 1, b: '2025-07-03T...' c: { name: 'Error', message: 'test' }, circular: '[Circular]' } +``` + +## Features + +- Removes circular references +- Handles errors by extracting name and message +- Converts dates to ISO strings +- Preserves arrays, Sets, and Maps +- Does not call `toJSON` methods +- No fixed depth limit diff --git a/packages/derecursify/package.json b/packages/derecursify/package.json new file mode 100644 index 0000000000..0f999d33f9 --- /dev/null +++ b/packages/derecursify/package.json @@ -0,0 +1,27 @@ +{ + "name": "@bugsnag/derecursify", + "version": "8.4.0", + "description": "Internal utility for safely serializing objects", + "main": "dist/derecursify.js", + "module": "dist/derecursify.js", + "types": "dist/derecursify.d.ts", + "private": true, + "exports": { + ".": { + "types": "./dist/derecursify.d.ts", + "default": "./dist/derecursify.js" + } + }, + "license": "MIT", + "author": "Bugsnag", + "repository": { + "type": "git", + "url": "https://github.com/bugsnag/bugsnag-js.git" + }, + "scripts": { + "clean": "rm -fr dist", + "build": "npm run clean && tsc", + "test:types": "tsc -p tsconfig.json --noEmit" + }, + "files": ["dist"] +} diff --git a/packages/core/lib/derecursify.js b/packages/derecursify/src/derecursify.ts similarity index 54% rename from packages/core/lib/derecursify.js rename to packages/derecursify/src/derecursify.ts index 9c8938eba8..40215e9005 100644 --- a/packages/core/lib/derecursify.js +++ b/packages/derecursify/src/derecursify.ts @@ -1,21 +1,17 @@ -const isArray = require('./es-utils/is-array') - -const isSafeLiteral = (obj) => ( +const isSafeLiteral = (obj: unknown): obj is string | number | boolean => typeof obj === 'string' || obj instanceof String || typeof obj === 'number' || obj instanceof Number || typeof obj === 'boolean' || obj instanceof Boolean -) -const isError = o => ( +const isError = (o: unknown): o is Error => o instanceof Error || /^\[object (Error|(Dom)?Exception)]$/.test(Object.prototype.toString.call(o)) -) -const throwsMessage = err => '[Throws: ' + (err ? err.message : '?') + ']' +const throwsMessage = (err: Error) => '[Throws: ' + (err ? err.message : '?') + ']' -const safelyGetProp = (obj, propName) => { +const safelyGetProp = (obj: object, propName: string) => { try { - return obj[propName] - } catch (err) { + return obj[propName as keyof typeof obj] + } catch (err: any) { return throwsMessage(err) } } @@ -29,10 +25,10 @@ const safelyGetProp = (obj, propName) => { * @param data the value to be made safe for the ReactNative bridge * @returns a safe version of the given `data` */ -module.exports = function (data) { - const seen = [] +const derecursify = (data: unknown): object => { + const seen: Array = [] - const visit = (obj) => { + const visit = (obj: unknown): any => { if (obj === null || obj === undefined) return obj if (isSafeLiteral(obj)) { @@ -53,23 +49,47 @@ module.exports = function (data) { } // handle arrays, and all iterable non-array types (such as Set) - if (isArray(obj) || obj[Symbol.iterator]) { + if (Array.isArray(obj)) { seen.push(obj) const safeArray = [] try { - for (const value of obj) { - safeArray.push(visit(value)) + for (let i = 0; i < obj.length; i++) { + safeArray.push(visit(obj[i])) } - } catch (err) { + } catch (err: any) { // if retrieving the Iterator fails return throwsMessage(err) } seen.pop() return safeArray + } else if (obj instanceof Set) { + seen.push(obj) + const safeArray = [] + try { + for (const value of Array.from(obj)) { + safeArray.push(visit(value)) + } + } catch (err: any) { + return throwsMessage(err) + } + seen.pop() + return safeArray + } else if (obj instanceof Map) { + seen.push(obj) + const safeArray = [] + try { + for (const [key, value] of Array.from(obj.entries())) { + safeArray.push([visit(key), visit(value)]) + } + } catch (err: any) { + return throwsMessage(err) + } + seen.pop() + return safeArray } seen.push(obj) - const safeObj = {} + const safeObj: Record = {} for (const propName in obj) { safeObj[propName] = visit(safelyGetProp(obj, propName)) } @@ -80,3 +100,7 @@ module.exports = function (data) { return visit(data) } + +// Export both named and default for CommonJS compatibility +export { derecursify } +export default derecursify diff --git a/packages/core/lib/test/derecursify.test.ts b/packages/derecursify/test/derecursify.test.ts similarity index 87% rename from packages/core/lib/test/derecursify.test.ts rename to packages/derecursify/test/derecursify.test.ts index 5e6cf12ae0..b095123096 100644 --- a/packages/core/lib/test/derecursify.test.ts +++ b/packages/derecursify/test/derecursify.test.ts @@ -1,4 +1,4 @@ -import derecursift from '../derecursify' +import derecursify from '..' // Use default import from the package.json main field describe('delivery: react native makeSafe', () => { it('leaves simple types intact', () => { @@ -25,7 +25,7 @@ describe('delivery: react native makeSafe', () => { _undefined: undefined } - const result = derecursift(data) + const result = derecursify(data) /* eslint-disable-next-line @typescript-eslint/no-dynamic-delete */ delete data[symbol] // we don't copy Symbol keys over @@ -50,13 +50,13 @@ describe('delivery: react native makeSafe', () => { enumerable: true }) - const result = derecursift(object) + const result = derecursify(object) expect(result).toStrictEqual({ badProperty: '[Throws: failure]' }) }) it('when they are properties', () => { const value = { errorProp: new Error('something wrong') } - const result = derecursift(value) + const result = derecursify(value) expect(result).toStrictEqual({ errorProp: { name: 'Error', message: 'something wrong' } }) }) }) @@ -66,7 +66,7 @@ describe('delivery: react native makeSafe', () => { const object: { self?: any } = {} object.self = object - const result = derecursift(object) + const result = derecursify(object) expect(result).toStrictEqual({ self: '[Circular]' }) }) @@ -77,7 +77,7 @@ describe('delivery: react native makeSafe', () => { outer.inner.parent = outer - const result = derecursift(outer) + const result = derecursify(outer) expect(result).toStrictEqual({ inner: { parent: '[Circular]' } }) }) @@ -85,7 +85,7 @@ describe('delivery: react native makeSafe', () => { const array: any[] = [{}, {}] array[0].circularRef = array - const result = derecursift(array) + const result = derecursify(array) expect(result).toStrictEqual([{ circularRef: '[Circular]' }, {}]) }) @@ -96,7 +96,7 @@ describe('delivery: react native makeSafe', () => { object.container = values - const result = derecursift(values) + const result = derecursify(values) expect(result).toStrictEqual([{ container: '[Circular]' }]) }) @@ -112,7 +112,7 @@ describe('delivery: react native makeSafe', () => { someObject: metaData }] - const result = derecursift(array) + const result = derecursify(array) expect(result).toStrictEqual([ { someObject: { diff --git a/packages/derecursify/tsconfig.json b/packages/derecursify/tsconfig.json new file mode 100644 index 0000000000..8b66702be2 --- /dev/null +++ b/packages/derecursify/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "lib": [ "es2022" ], + "strict": true, + "allowJs": true, + "target": "es5", + "module": "commonjs", + "moduleResolution": "node", + "outDir": "dist", + "declaration": true, + "declarationDir": "dist", + "skipLibCheck": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "**/*.test.ts"] +} diff --git a/packages/electron-network-status/tests/network-status.test.ts b/packages/electron-network-status/tests/network-status.test.ts index 5a8611eaeb..ca20ac631a 100644 --- a/packages/electron-network-status/tests/network-status.test.ts +++ b/packages/electron-network-status/tests/network-status.test.ts @@ -1,4 +1,4 @@ -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' import stateManager from '@bugsnag/plugin-electron-client-state-manager' import EventEmitter from 'events' import NetworkStatus from '../network-status' diff --git a/packages/electron-test-helpers/src/client.ts b/packages/electron-test-helpers/src/client.ts index 9199fb3ee2..64bb10436c 100644 --- a/packages/electron-test-helpers/src/client.ts +++ b/packages/electron-test-helpers/src/client.ts @@ -1,6 +1,4 @@ -import Client from '@bugsnag/core/client' -import { schema as defaultSchema } from '@bugsnag/core/config' -import { Event, Session, SessionPayload, EventPayload, Plugin } from '@bugsnag/core' +import { Client, schema as defaultSchema, Event, Session, SessionPayload, Plugin, SessionDelegate } from '@bugsnag/core' interface ClientTestHelpers { client: Client @@ -26,7 +24,7 @@ export function makeClientForPlugin ({ let lastSession: SessionPayload client._setDelivery(() => ({ - sendEvent (payload: EventPayload, cb: (err: Error|null, obj: unknown) => void) { + sendEvent (payload, cb: (err: Error|null, obj: unknown) => void) { expect(payload.events).toHaveLength(1) cb(null, payload.events[0]) }, @@ -36,7 +34,7 @@ export function makeClientForPlugin ({ } })) - client._sessionDelegate = { + client._sessionDelegate = ({ startSession (client: Client, session: Session) { client._delivery.sendSession(session, () => {}) }, @@ -44,7 +42,7 @@ export function makeClientForPlugin ({ }, pauseSession () { } - } + } as unknown as SessionDelegate) const sendEvent = async () => new Promise((resolve, reject) => { // @ts-expect-error - we don't have Client internals to correctly type this diff --git a/packages/electron/package.json b/packages/electron/package.json index 3749d06c03..f97f9e2ddb 100644 --- a/packages/electron/package.json +++ b/packages/electron/package.json @@ -37,6 +37,7 @@ "@bugsnag/delivery-electron": "^8.4.0", "@bugsnag/electron-filestore": "^8.0.0", "@bugsnag/electron-network-status": "^8.4.0", + "@bugsnag/path-normalizer": "^8.4.0", "@bugsnag/plugin-console-breadcrumbs": "^8.4.0", "@bugsnag/plugin-electron-app": "^8.4.0", "@bugsnag/plugin-electron-app-breadcrumbs": "^8.4.0", diff --git a/packages/electron/src/client/createClient.js b/packages/electron/src/client/createClient.js index a7bc71015f..bab2684175 100644 --- a/packages/electron/src/client/createClient.js +++ b/packages/electron/src/client/createClient.js @@ -1,4 +1,4 @@ -const Client = require('@bugsnag/core/client') +const { Client } = require('@bugsnag/core') const createClient = (createProcessClient, process) => { const Bugsnag = { @@ -37,7 +37,7 @@ const createClient = (createProcessClient, process) => { const methods = Object.getOwnPropertyNames(Client.prototype).concat(['markLaunchComplete']) methods.forEach((m) => { - if (/^_/.test(m)) return + if (/^_/.test(m) || m === 'constructor') return Bugsnag[m] = function () { if (!Bugsnag._client) return console.error(`Bugsnag.${m}() was called before Bugsnag.start()`) Bugsnag._client._depth += 1 diff --git a/packages/electron/src/client/main.js b/packages/electron/src/client/main.js index 4313b5d277..39229656cf 100644 --- a/packages/electron/src/client/main.js +++ b/packages/electron/src/client/main.js @@ -1,9 +1,6 @@ const electron = require('electron') -const Client = require('@bugsnag/core/client') -const Event = require('@bugsnag/core/event') -const Breadcrumb = require('@bugsnag/core/breadcrumb') -const Session = require('@bugsnag/core/session') +const { Breadcrumb, Client, Event, Session } = require('@bugsnag/core') const { plugin: PluginClientStatePersistence, NativeClient @@ -32,7 +29,7 @@ const createMainClient = (opts) => { // Normalise the project root upfront so renderers have a fully resolved path // The renderers can't do this themselves as they cannot access the 'path' module if (opts.projectRoot) { - const normalizePath = require('@bugsnag/core/lib/path-normalizer') + const normalizePath = require('@bugsnag/path-normalizer') opts.projectRoot = normalizePath(opts.projectRoot) } diff --git a/packages/electron/src/client/renderer.js b/packages/electron/src/client/renderer.js index 876e362596..762021d1c6 100644 --- a/packages/electron/src/client/renderer.js +++ b/packages/electron/src/client/renderer.js @@ -1,7 +1,4 @@ -const Client = require('@bugsnag/core/client') -const Event = require('@bugsnag/core/event') -const Breadcrumb = require('@bugsnag/core/breadcrumb') -const Session = require('@bugsnag/core/session') +const { Breadcrumb, Client, Event, Session } = require('@bugsnag/core') const createClient = require('./createClient') diff --git a/packages/electron/src/config/common.js b/packages/electron/src/config/common.js index a34a587ad9..0f1f155460 100644 --- a/packages/electron/src/config/common.js +++ b/packages/electron/src/config/common.js @@ -1,5 +1,4 @@ -const { schema } = require('@bugsnag/core/config') -const stringWithLength = require('@bugsnag/core/lib/validators/string-with-length') +const { schema, stringWithLength } = require('@bugsnag/core') const defaultErrorTypes = () => ({ unhandledExceptions: true, unhandledRejections: true, nativeCrashes: true }) diff --git a/packages/electron/src/config/main.js b/packages/electron/src/config/main.js index 932e083de8..7cbcd56942 100644 --- a/packages/electron/src/config/main.js +++ b/packages/electron/src/config/main.js @@ -1,9 +1,8 @@ const { schema } = require('./common') -const stringWithLength = require('@bugsnag/core/lib/validators/string-with-length') -const listOfFunctions = require('@bugsnag/core/lib/validators/list-of-functions') +const { listOfFunctions, stringWithLength } = require('@bugsnag/core') const { inspect } = require('util') const { app } = require('electron') -const normalizePath = require('@bugsnag/core/lib/path-normalizer') +const normalizePath = require('@bugsnag/path-normalizer') module.exports.schema = { ...schema, diff --git a/packages/electron/src/config/renderer.js b/packages/electron/src/config/renderer.js index 5b7a06445a..8392794e9d 100644 --- a/packages/electron/src/config/renderer.js +++ b/packages/electron/src/config/renderer.js @@ -1,5 +1,5 @@ const { schema } = require('./common') -const stringWithLength = require('@bugsnag/core/lib/validators/string-with-length') +const { stringWithLength } = require('@bugsnag/core') const ALLOWED_IN_RENDERER = [ // a list of config keys that are allowed to be supplied to the renderer client diff --git a/packages/in-flight/src/in-flight.js b/packages/in-flight/src/in-flight.js index 86db03f06c..20f9d2bc4e 100644 --- a/packages/in-flight/src/in-flight.js +++ b/packages/in-flight/src/in-flight.js @@ -1,5 +1,5 @@ const cuid = require('@bugsnag/cuid') -const clone = require('@bugsnag/core/lib/clone-client') +const { cloneClient } = require('@bugsnag/core') const FLUSH_POLL_INTERVAL_MS = 50 const inFlightRequests = new Map() @@ -9,7 +9,7 @@ const noop = () => {} // when a client is cloned, make sure to patch the clone's notify method too // we don't need to patch delivery when a client is cloned because the // original client's delivery method will be copied over to the clone -clone.registerCallback(patchNotify) +cloneClient.registerCallback(patchNotify); module.exports = { trackInFlight (client) { diff --git a/packages/in-flight/test/in-flight.test.ts b/packages/in-flight/test/in-flight.test.ts index 301f5d0081..6bcdc50db5 100644 --- a/packages/in-flight/test/in-flight.test.ts +++ b/packages/in-flight/test/in-flight.test.ts @@ -1,5 +1,4 @@ -import clone from '@bugsnag/core/lib/clone-client' -import Client, { EventDeliveryPayload, SessionDeliveryPayload } from '@bugsnag/core/client' +import { Client, cloneClient, EventDeliveryPayload, SessionDeliveryPayload } from '@bugsnag/core' // The in-flight package has module level state which can leak between tests // We can avoid this using jest's 'isolateModules' but need to type the @@ -69,7 +68,7 @@ describe('@bugsnag/in-flight', () => { const onError = jest.fn() const callback = jest.fn() - cloned = clone(client) + cloned = cloneClient(client) expect(cloned._depth).toBe(1) @@ -155,15 +154,15 @@ describe('@bugsnag/in-flight', () => { expect(client._sessionDelegate.pauseSession).not.toHaveBeenCalled() expect(client._sessionDelegate.resumeSession).not.toHaveBeenCalled() - const cloned = clone(client) + const cloned = cloneClient(client) cloned.startSession() expect(payloads.length).toBe(1) expect(callback).toHaveBeenCalledTimes(1) - expect(cloned._sessionDelegate.startSession).toHaveBeenCalledTimes(1) - expect(cloned._sessionDelegate.pauseSession).not.toHaveBeenCalled() - expect(cloned._sessionDelegate.resumeSession).not.toHaveBeenCalled() + expect(cloned._sessionDelegate?.startSession).toHaveBeenCalledTimes(1) + expect(cloned._sessionDelegate?.pauseSession).not.toHaveBeenCalled() + expect(cloned._sessionDelegate?.resumeSession).not.toHaveBeenCalled() }) it('tracks all in-flight requests', () => { diff --git a/packages/js/package.json b/packages/js/package.json index 5b23647700..959f7e7d79 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -32,6 +32,9 @@ ], "author": "Bugsnag", "license": "MIT", + "scripts": { + "build": "echo 'No build step required for @bugsnag/js'" + }, "dependencies": { "@bugsnag/browser": "^8.4.0", "@bugsnag/node": "^8.4.0" diff --git a/packages/delivery-x-domain-request/LICENSE.txt b/packages/json-payload/LICENSE.txt similarity index 52% rename from packages/delivery-x-domain-request/LICENSE.txt rename to packages/json-payload/LICENSE.txt index ddc0631e24..3596c5b072 100644 --- a/packages/delivery-x-domain-request/LICENSE.txt +++ b/packages/json-payload/LICENSE.txt @@ -1,19 +1,21 @@ -Copyright (c) Bugsnag, https://www.bugsnag.com/ +MIT License -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following conditions: +Copyright (c) 2017 Bugsnag -The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/json-payload/README.md b/packages/json-payload/README.md new file mode 100644 index 0000000000..845114a878 --- /dev/null +++ b/packages/json-payload/README.md @@ -0,0 +1,54 @@ +# @bugsnag/json-payload + +A utility for safely serializing BugSnag event and session payloads to JSON with built-in data redaction and size management. + +## Installation + +```bash +npm install @bugsnag/json-payload +``` + +## Usage + +This package provides two main functions for serializing BugSnag payloads: + +### Event Payloads + +```javascript +import { event } from '@bugsnag/json-payload' + +// Serialize an event payload with optional data redaction +const jsonString = event(eventPayload, ['apiKey', 'password']) +``` + +The `event` function: +- Safely stringifies event delivery payloads +- Automatically redacts sensitive data from predefined paths: + - `events.[].metaData` + - `events.[].breadcrumbs.[].metaData` + - `events.[].request` +- Accepts an optional array of keys/patterns to redact +- Enforces a 1MB size limit - if exceeded, it strips metadata from the first event and adds a warning message +- Only attempts to reduce payload size by removing metadata from the first event + +### Session Payloads + +```javascript +import { session } from '@bugsnag/json-payload' + +// Serialize a session payload +const jsonString = session(sessionPayload) +``` + +The `session` function safely stringifies session delivery payloads. + +## Features + +- **Safe JSON Stringification**: Uses `@bugsnag/safe-json-stringify` to handle circular references and other edge cases +- **Automatic Data Redaction**: Redacts sensitive information from known paths +- **Size Management**: Automatically handles payloads that exceed size limits +- **TypeScript Support**: Full TypeScript definitions included + +## License + +MIT diff --git a/packages/json-payload/package.json b/packages/json-payload/package.json new file mode 100644 index 0000000000..545bb3f657 --- /dev/null +++ b/packages/json-payload/package.json @@ -0,0 +1,44 @@ +{ + "name": "@bugsnag/json-payload", + "version": "8.4.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "default": "./dist/index.js" + } + }, + "description": "Path normalization utility for Bugsnag", + "homepage": "https://www.bugsnag.com/", + "repository": { + "type": "git", + "url": "git@github.com:bugsnag/bugsnag-js.git" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "rollup --config rollup.config.js", + "test:types": "tsc -p tsconfig.json" + }, + "author": "Bugsnag", + "license": "MIT", + "dependencies": { + "@bugsnag/safe-json-stringify": "^6.0.0" + }, + "devDependencies": { + "@bugsnag/core": "^8.4.0", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-replace": "^6.0.2", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.2", + "rollup": "^4.24.0", + "typescript": "^5.7.2" + } +} diff --git a/packages/json-payload/rollup.config.js b/packages/json-payload/rollup.config.js new file mode 100644 index 0000000000..d335d0f0c4 --- /dev/null +++ b/packages/json-payload/rollup.config.js @@ -0,0 +1,89 @@ +/* eslint-env node */ +/* global require, module */ +const { nodeResolve } = require('@rollup/plugin-node-resolve') +const commonjs = require('@rollup/plugin-commonjs') +const typescript = require('@rollup/plugin-typescript') +const terser = require('@rollup/plugin-terser') +const replace = require('@rollup/plugin-replace') + +module.exports = [ + // CommonJS build + { + input: 'src/index.ts', + output: { + file: 'dist/index.js', + format: 'cjs', + }, + plugins: [ + replace({ + 'process.env.NODE_ENV': JSON.stringify('production'), + preventAssignment: true + }), + nodeResolve({ + preferBuiltins: true, + browser: false + }), + commonjs(), + typescript({ + declaration: true, + declarationDir: 'dist', + rootDir: 'src' + }), + terser({ + compress: { + drop_console: false, + drop_debugger: true, + pure_funcs: ['console.log'], + passes: 2 + }, + mangle: { + reserved: ['event', 'session'] + }, + format: { + comments: false + } + }) + ], + external: ['path'] + }, + // ES Module build (optimized for browsers) + { + input: 'src/index.ts', + output: { + file: 'dist/index.mjs', + format: 'es' + }, + plugins: [ + replace({ + 'process.env.NODE_ENV': JSON.stringify('production'), + preventAssignment: true + }), + nodeResolve({ + preferBuiltins: false, + browser: true + }), + commonjs(), + typescript({ + declaration: false, + target: 'ES2018' // Modern target for smaller output + }), + terser({ + compress: { + drop_console: false, + drop_debugger: true, + pure_funcs: ['console.log'], + passes: 2, + module: true + }, + mangle: { + reserved: ['event', 'session'], + module: true + }, + format: { + comments: false + } + }) + ], + external: ['path'] + } +] diff --git a/packages/json-payload/safe-json-stringify.d.ts b/packages/json-payload/safe-json-stringify.d.ts new file mode 100644 index 0000000000..d56d7cea30 --- /dev/null +++ b/packages/json-payload/safe-json-stringify.d.ts @@ -0,0 +1,11 @@ +declare module '@bugsnag/safe-json-stringify' { + export default function stringify( + value: any, + replacer?: null | ((this: any, key: string, value: any) => any), + space?: null | string | number, + options?: { + redactedKeys?: Array; + redactedPaths?: string[]; + } + ): string; +} \ No newline at end of file diff --git a/packages/core/lib/json-payload.js b/packages/json-payload/src/index.ts similarity index 62% rename from packages/core/lib/json-payload.js rename to packages/json-payload/src/index.ts index d543aed646..3f165f0fd7 100644 --- a/packages/core/lib/json-payload.js +++ b/packages/json-payload/src/index.ts @@ -1,11 +1,15 @@ -const jsonStringify = require('@bugsnag/safe-json-stringify') +import type { EventDeliveryPayload, SessionDeliveryPayload } from "@bugsnag/core"; +import jsonStringify from '@bugsnag/safe-json-stringify'; + +type RedactedKey = string | RegExp + const EVENT_REDACTION_PATHS = [ 'events.[].metaData', 'events.[].breadcrumbs.[].metaData', 'events.[].request' ] -module.exports.event = (event, redactedKeys) => { +export const event = (event: EventDeliveryPayload, redactedKeys?: RedactedKey[]) => { let payload = jsonStringify(event, null, null, { redactedPaths: EVENT_REDACTION_PATHS, redactedKeys }) if (payload.length > 10e5) { event.events[0]._metadata = { @@ -19,7 +23,8 @@ metadata was removed` return payload } -module.exports.session = (session, redactedKeys) => { +export const session = (session: SessionDeliveryPayload, redactedKeys?: RedactedKey[]) => { const payload = jsonStringify(session, null, null) return payload } + diff --git a/packages/json-payload/test/json-payload.test.ts b/packages/json-payload/test/json-payload.test.ts new file mode 100644 index 0000000000..193754e59b --- /dev/null +++ b/packages/json-payload/test/json-payload.test.ts @@ -0,0 +1,74 @@ +import type { EventDeliveryPayload, SessionDeliveryPayload } from '@bugsnag/core' +import { Event, Session } from '@bugsnag/core' +import jsonPayload from '../' + +function makeBigObject () { + const big: Record = {} + let i = 0 + while (JSON.stringify(big).length < 2 * 10e5) { + big['entry' + i] = 'long repetitive string'.repeat(1000) + i++ + } + return big +} + +describe('jsonPayload.event', () => { + it('safe stringifies the payload and redacts values from certain paths of the supplied keys', () => { + const event = new Event('CheckoutError', 'Failed load tickets') + event.setUser('123', 'jim@bugsnag.com', 'Jim Bug') + event.request = { apiKey: '245b39ebd3cd3992e85bffc81c045924' } + + expect(jsonPayload.event({ + apiKey: 'd145b8e5afb56516423bc4d605e45442', + notifier: { name: 'Bugsnag', version: '1.0.0', url: 'https://bugsnag.com' }, + events: [event] + }, ['apiKey'])).toBe('{"apiKey":"d145b8e5afb56516423bc4d605e45442","notifier":{"name":"Bugsnag","version":"1.0.0","url":"https://bugsnag.com"},"events":[{"payloadVersion":"4","exceptions":[{"errorClass":"CheckoutError","errorMessage":"Failed load tickets","type":"browserjs","stacktrace":[],"message":"Failed load tickets"}],"severity":"warning","unhandled":false,"severityReason":{"type":"handledException"},"app":{},"device":{},"request":{"apiKey":"[REDACTED]"},"breadcrumbs":[],"metaData":{},"user":{"id":"123","email":"jim@bugsnag.com","name":"Jim Bug"},"featureFlags":[]}]}') + }) + + it('strips the metaData of the first event if the payload is too large', () => { + const event = new Event('CheckoutError', 'Failed load tickets') + event.setUser('123', 'jim@bugsnag.com', 'Jim Bug') + event.request = { apiKey: '245b39ebd3cd3992e85bffc81c045924' } + event._metadata = { 'big thing': makeBigObject() } + + const payload: EventDeliveryPayload = { + apiKey: 'd145b8e5afb56516423bc4d605e45442', + notifier: { name: 'Bugsnag', version: '1.0.0', url: 'https://bugsnag.com' }, + events: [event] + } + + expect(jsonPayload.event(payload)).toBe('{"apiKey":"d145b8e5afb56516423bc4d605e45442","notifier":{"name":"Bugsnag","version":"1.0.0","url":"https://bugsnag.com"},"events":[{"payloadVersion":"4","exceptions":[{"errorClass":"CheckoutError","errorMessage":"Failed load tickets","type":"browserjs","stacktrace":[],"message":"Failed load tickets"}],"severity":"warning","unhandled":false,"severityReason":{"type":"handledException"},"app":{},"device":{},"request":{"apiKey":"245b39ebd3cd3992e85bffc81c045924"},"breadcrumbs":[],"metaData":{"notifier":"WARNING!\\nSerialized payload was 2.003764MB (limit = 1MB)\\nmetadata was removed"},"user":{"id":"123","email":"jim@bugsnag.com","name":"Jim Bug"},"featureFlags":[]}]}') + }) + + it('does not attempt to strip any other data paths from the payload to reduce the size', () => { + const event1 = new Event('CheckoutError', 'Failed load tickets') + event1.setUser('123', 'jim@bugsnag.com', 'Jim Bug') + event1.request = { apiKey: '245b39ebd3cd3992e85bffc81c045924' } + + // Second event metadata should not be stripped, only the first + const event2 = new Event('APIError', 'Request failed') + event2._metadata = { 'big thing': makeBigObject() } + + const payload = { + apiKey: 'd145b8e5afb56516423bc4d605e45442', + notifier: { name: 'Bugsnag', version: '1.0.0', url: 'https://bugsnag.com' }, + events: [event1, event2] + } + + expect(jsonPayload.event(payload).length).toBeGreaterThan(10e5) + }) +}) + +describe('jsonPayload.session', () => { + it('safe stringifies the payload', () => { + const session = new Session('123', new Date('2012-12-21T00:00:00.0000Z')) + const sessionPayload: SessionDeliveryPayload = { + app: { version: '1.0.0' }, + device: { id: '123' }, + notifier: { name: 'Bugsnag', version: '1.0.0', url: 'https://bugsnag.com' }, + sessions: [session] + } + + expect(jsonPayload.session(sessionPayload)).toBe('{"app":{"version":"1.0.0"},"device":{"id":"123"},"notifier":{"name":"Bugsnag","version":"1.0.0","url":"https://bugsnag.com"},"sessions":[{"id":"123","startedAt":"2012-12-21T00:00:00.000Z","events":{"handled":0,"unhandled":0}}]}') + }) +}) diff --git a/packages/json-payload/tsconfig.json b/packages/json-payload/tsconfig.json new file mode 100644 index 0000000000..ed4a99b806 --- /dev/null +++ b/packages/json-payload/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "lib": ["es2017"], + "types": ["node", "./safe-json-stringify.d.ts"], + } +} diff --git a/packages/node/babel.config.js b/packages/node/babel.config.js new file mode 100644 index 0000000000..235ee4504b --- /dev/null +++ b/packages/node/babel.config.js @@ -0,0 +1,3 @@ +const babelConfig = require('../../babel.config.js') + +module.exports = babelConfig diff --git a/packages/node/package.json b/packages/node/package.json index 6f102c5476..2c4bceb32c 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,8 +1,15 @@ { "name": "@bugsnag/node", "version": "8.4.0", - "main": "dist/bugsnag.js", - "types": "types/bugsnag.d.ts", + "main": "dist/index-cjs.cjs", + "types": "dist/types/index-es.d.ts", + "exports": { + ".": { + "types": "./dist/types/index-es.d.ts", + "import": "./dist/index-es.mjs", + "default": "./dist/index-cjs.cjs" + } + }, "description": "Bugsnag error reporter for Node.js", "homepage": "https://www.bugsnag.com/", "repository": { @@ -18,8 +25,8 @@ ], "scripts": { "clean": "rm -fr dist && mkdir dist", - "build": "npm run clean && npm run build:dist", - "build:dist": "../../bin/bundle src/notifier.js --node --exclude=iserror,stack-generator,error-stack-parser,pump,byline,async_hooks --standalone=bugsnag | ../../bin/extract-source-map dist/bugsnag.js" + "build": "npm run clean && npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs" }, "author": "Bugsnag", "license": "MIT", @@ -36,13 +43,13 @@ "@bugsnag/plugin-node-unhandled-rejection": "^8.4.0", "@bugsnag/plugin-server-session": "^8.4.0", "@bugsnag/plugin-stackframe-path-normaliser": "^8.4.0", - "@bugsnag/plugin-strip-project-root": "^8.4.0" + "@bugsnag/plugin-strip-project-root": "^8.4.0", + "@types/node": "^18.19.74" }, "dependencies": { "@bugsnag/core": "^8.4.0", "byline": "^5.0.0", "error-stack-parser": "^2.0.3", - "iserror": "^0.0.2", "pump": "^3.0.0", "stack-generator": "^2.0.3" } diff --git a/packages/node/rollup.config.npm.mjs b/packages/node/rollup.config.npm.mjs new file mode 100644 index 0000000000..2b8ffe434c --- /dev/null +++ b/packages/node/rollup.config.npm.mjs @@ -0,0 +1,59 @@ +import babel from '@rollup/plugin-babel' +import commonjs from '@rollup/plugin-commonjs' +import nodeResolve from '@rollup/plugin-node-resolve' +import replace from '@rollup/plugin-replace' +import typescript from '@rollup/plugin-typescript' +import fs from 'fs' + +import createRollupConfig, { sharedOutput } from '../../.rollup/index.mjs' + +const packageJson = JSON.parse(fs.readFileSync('./package.json')) + +const plugins = [ + nodeResolve({ + preferBuiltins: true + }), + commonjs(), + typescript({ + removeComments: true, + // don't output anything if there's a TS error + noEmitOnError: true, + compilerOptions: { + target: 'es2015' + } + }), + babel({ babelHelpers: 'bundled' }), + replace({ + preventAssignment: true, + values: { + 'process.env.NODE_ENV': JSON.stringify('production'), + __BUGSNAG_NOTIFIER_VERSION__: packageJson.version, + } + }) +] + +export default [ + createRollupConfig({ + input: 'src/index-es.ts', + output: [ + { + ...sharedOutput, + preserveModules: false, + entryFileNames: '[name].mjs', + format: 'esm' + } + ], + plugins + }), + createRollupConfig({ + input: 'src/index-cjs.ts', + output: [ + { + ...sharedOutput, + entryFileNames: '[name].cjs', + format: 'cjs' + } + ], + plugins + }) +] diff --git a/packages/node/src/bugsnag.ts b/packages/node/src/bugsnag.ts new file mode 100644 index 0000000000..42446c2004 --- /dev/null +++ b/packages/node/src/bugsnag.ts @@ -0,0 +1,157 @@ +import { AsyncLocalStorage } from 'async_hooks' + + + +// extend the base config schema with some browser-specific options +import { schema as baseConfig } from '@bugsnag/core' +import browserConfig from './config' + +import delivery from '@bugsnag/delivery-node' + +import pluginApp from '@bugsnag/plugin-app-duration' +import pluginSurroundingCode from '@bugsnag/plugin-node-surrounding-code' +import pluginInProject from '@bugsnag/plugin-node-in-project' +import pluginStripProjectRoot from '@bugsnag/plugin-strip-project-root' +import pluginServerSession from '@bugsnag/plugin-server-session' +import pluginNodeDevice from '@bugsnag/plugin-node-device' +import pluginNodeUncaughtException from '@bugsnag/plugin-node-uncaught-exception' +import pluginNodeUnhandledRejection from '@bugsnag/plugin-node-unhandled-rejection' +import pluginIntercept from '@bugsnag/plugin-intercept' +import pluginContextualize from '@bugsnag/plugin-contextualize' +import pluginStackframePathNormaliser from '@bugsnag/plugin-stackframe-path-normaliser' +import pluginConsoleBreadcrumbs from '@bugsnag/plugin-console-breadcrumbs' +import { BugsnagStatic, Client, Config, Event, Logger } from '@bugsnag/core' + +type AfterErrorCb = (err: any, event: Event, logger: Logger) => void; + +const schema = Object.assign({}, baseConfig, browserConfig) + +export interface NodeConfig extends Config { + hostname?: string + onUncaughtException?: AfterErrorCb + onUnhandledRejection?: AfterErrorCb + agent?: any + projectRoot?: string + sendCode?: boolean +} + +export interface NodeBugsnagStatic extends BugsnagStatic { + start(apiKeyOrOpts: string | NodeConfig): Client + createClient(apiKeyOrOpts: string | NodeConfig): Client +} + +const name = 'Bugsnag Node' +const version = '__BUGSNAG_NOTIFIER_VERSION__' +const url = 'https://github.com/bugsnag/bugsnag-js' + +Event.__type = 'nodejs' + +// extend the base config schema with some node-specific options +const internalPlugins = [ + pluginApp, + pluginSurroundingCode, + pluginInProject, + pluginStripProjectRoot, + pluginServerSession, + pluginNodeDevice, + pluginNodeUncaughtException, + pluginNodeUnhandledRejection, + pluginIntercept, + pluginContextualize, + pluginStackframePathNormaliser, + pluginConsoleBreadcrumbs +] + +type NodeClient = Partial & { + _client: Client | null + createClient: (opts?: Config) => Client + start: (opts?: Config) => Client + isStarted: () => boolean +} + +const clientMethods = Object.getOwnPropertyNames(Client.prototype) + +const notifier: NodeClient = { + _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 bugsnag = new Client(opts, schema, internalPlugins, { name, version, url }); + + /** + * Patch all calls to the client in order to forwards them to the context client if it exists + * + * This is useful for when client methods are called later, such as in the console breadcrumbs + * plugin where we want to call `leaveBreadcrumb` on the request-scoped client, if it exists. + */ + clientMethods.forEach((m) => { + // @ts-expect-error + const original = bugsnag[m] + // @ts-expect-error + bugsnag[m] = function () { + // if we are in an async context, use the client from that context + // @ts-expect-error + const contextClient = bugsnag._clientContext && typeof bugsnag._clientContext.getStore === 'function' ? bugsnag._clientContext.getStore() : null + const client = contextClient || bugsnag + const originalMethod = contextClient ? contextClient[m] : original + + client._depth += 1 + const ret = originalMethod.apply(client, arguments) + client._depth -= 1 + return ret + } + }) + + // Used to store and retrieve the request-scoped client which makes it easy to obtain the request-scoped client + // from anywhere in the codebase e.g. when calling Bugsnag.leaveBreadcrumb() or even within the global unhandled + // promise rejection handler. + // @ts-expect-error + bugsnag._clientContext = new AsyncLocalStorage() + + bugsnag._setDelivery(delivery) + + bugsnag._logger.debug('Loaded!') + + return bugsnag + }, + start: (opts) => { + if (notifier._client) { + notifier._client._logger.warn('Bugsnag.start() was called more than once. Ignoring.') + return notifier._client + } + notifier._client = notifier.createClient(opts) + return notifier._client + }, + isStarted: () => { + return notifier._client != null + } +} + +clientMethods.forEach((m) => { + if (/^_/.test(m) || m === 'constructor') return + // @ts-expect-error + notifier[m] = function () { + // if we are in an async context, use the client from that context + let client = notifier._client + // @ts-expect-error + const ctx = client && client._clientContext && client._clientContext.getStore() + if (ctx) { + client = ctx + } + + if (!client) return console.error(`Bugsnag.${m}() was called before Bugsnag.start()`) + + client._depth += 1 + // @ts-expect-error + const ret = client[m].apply(client, arguments) + client._depth -= 1 + return ret + } +}) + +// @ts-expect-error +const Bugsnag = notifier as NodeBugsnagStatic + +export default Bugsnag diff --git a/packages/node/src/config.js b/packages/node/src/config.js deleted file mode 100644 index d0045fd4dc..0000000000 --- a/packages/node/src/config.js +++ /dev/null @@ -1,66 +0,0 @@ -const { schema } = require('@bugsnag/core/config') -const stringWithLength = require('@bugsnag/core/lib/validators/string-with-length') -const os = require('os') -const { inspect } = require('util') - -module.exports = { - appType: { - ...schema.appType, - defaultValue: () => 'node' - }, - projectRoot: { - defaultValue: () => process.cwd(), - validate: value => value === null || stringWithLength(value), - message: 'should be string' - }, - hostname: { - defaultValue: () => os.hostname(), - message: 'should be a string', - validate: value => value === null || stringWithLength(value) - }, - logger: { - ...schema.logger, - defaultValue: () => getPrefixedConsole() - }, - releaseStage: { - ...schema.releaseStage, - defaultValue: () => process.env.NODE_ENV || 'production' - }, - agent: { - defaultValue: () => undefined, - message: 'should be an HTTP(s) agent', - validate: value => value === undefined || isAgent(value) - }, - onUncaughtException: { - defaultValue: () => (err, event, logger) => { - logger.error(`Uncaught exception${getContext(event)}, the process will now terminate…\n${printError(err)}`) - process.exit(1) - }, - message: 'should be a function', - validate: value => typeof value === 'function' - }, - onUnhandledRejection: { - defaultValue: () => (err, event, logger) => { - logger.error(`Unhandled rejection${getContext(event)}…\n${printError(err)}`) - }, - message: 'should be a function', - validate: value => typeof value === 'function' - } -} - -const printError = err => err && err.stack ? err.stack : inspect(err) - -const getPrefixedConsole = () => { - return ['debug', 'info', 'warn', 'error'].reduce((accum, method) => { - const consoleMethod = console[method] || console.log - accum[method] = consoleMethod.bind(console, '[bugsnag]') - return accum - }, {}) -} - -const getContext = event => - event.request && Object.keys(event.request).length - ? ` at ${event.request.httpMethod} ${event.request.path || event.request.url}` - : '' - -const isAgent = value => (typeof value === 'object' && value !== null) || typeof value === 'boolean' diff --git a/packages/node/src/config.ts b/packages/node/src/config.ts new file mode 100644 index 0000000000..9b83528b21 --- /dev/null +++ b/packages/node/src/config.ts @@ -0,0 +1,65 @@ +import { Event, LoggerConfig, schema, stringWithLength } from '@bugsnag/core' +import os from 'os' +import { inspect } from 'util' + + +import getPrefixedConsole from './get-prefixed-console' + +const config = { + appType: { + ...schema.appType, + defaultValue: () => 'node' + }, + projectRoot: { + defaultValue: () => process.cwd(), + validate: (value: unknown) => value === null || stringWithLength(value), + message: 'should be string' + }, + hostname: { + defaultValue: () => os.hostname(), + message: 'should be a string', + validate: (value: unknown) => value === null || stringWithLength(value) + }, + logger: Object.assign({}, schema.logger, { + defaultValue: () => + // set logger based on browser capability + (typeof console !== 'undefined' && typeof console.debug === 'function') + ? getPrefixedConsole() + : undefined + }), + releaseStage: { + ...schema.releaseStage, + defaultValue: () => process.env.NODE_ENV || 'production' + }, + agent: { + defaultValue: () => undefined, + message: 'should be an HTTP(s) agent', + validate: (value: unknown) => value === undefined || isAgent(value) + }, + onUncaughtException: { + defaultValue: () => (err: Error, event: Event, logger: LoggerConfig) => { + logger.error(`Uncaught exception${getContext(event)}, the process will now terminate…\n${printError(err)}`) + process.exit(1) + }, + message: 'should be a function', + validate: (value: unknown) => typeof value === 'function' + }, + onUnhandledRejection: { + defaultValue: () => (err: Error, event: Event, logger: LoggerConfig) => { + logger.error(`Unhandled rejection${getContext(event)}…\n${printError(err)}`) + }, + message: 'should be a function', + validate: (value: unknown) => typeof value === 'function' + } +} + +const printError = (err: Error) => err && err.stack ? err.stack : inspect(err) + +const getContext = (event: Event) => + event.request && Object.keys(event.request).length + ? ` at ${event.request.httpMethod} ${event.request.path || event.request.url}` + : '' + +const isAgent = (value: any) => (typeof value === 'object' && value !== null) || typeof value === 'boolean' + +export default config diff --git a/packages/node/src/get-prefixed-console.ts b/packages/node/src/get-prefixed-console.ts new file mode 100644 index 0000000000..27be40dd43 --- /dev/null +++ b/packages/node/src/get-prefixed-console.ts @@ -0,0 +1,16 @@ +type LoggerMethod = 'debug' | 'info' | 'warn' | 'error' + +const getPrefixedConsole = () => { + const logger: Record = {} + const consoleLog = console.log + const loggerMethods = ['debug', 'info', 'warn', 'error'] as const + loggerMethods.map((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/node/src/index-cjs.ts b/packages/node/src/index-cjs.ts new file mode 100644 index 0000000000..6040a2d5e8 --- /dev/null +++ b/packages/node/src/index-cjs.ts @@ -0,0 +1,7 @@ +import { Breadcrumb, Client, Event, Session } from '@bugsnag/core' + + + +import Bugsnag from './bugsnag' + +export default Object.assign(Bugsnag, { Breadcrumb, Client, Event, Session }) diff --git a/packages/node/src/index-es.ts b/packages/node/src/index-es.ts new file mode 100644 index 0000000000..e18c3171b8 --- /dev/null +++ b/packages/node/src/index-es.ts @@ -0,0 +1,4 @@ +export { default } from './bugsnag' +export type { NodeBugsnagStatic, NodeConfig } from './bugsnag' + +export * from '@bugsnag/core' diff --git a/packages/node/src/notifier.js b/packages/node/src/notifier.js deleted file mode 100644 index 4e3da4930a..0000000000 --- a/packages/node/src/notifier.js +++ /dev/null @@ -1,128 +0,0 @@ -const name = 'Bugsnag Node' -const version = '__VERSION__' -const url = 'https://github.com/bugsnag/bugsnag-js' - -const { AsyncLocalStorage } = require('async_hooks') - -const Client = require('@bugsnag/core/client') -const Event = require('@bugsnag/core/event') -const Session = require('@bugsnag/core/session') -const Breadcrumb = require('@bugsnag/core/breadcrumb') - -Event.__type = 'nodejs' - -const delivery = require('@bugsnag/delivery-node') - -// extend the base config schema with some node-specific options -const schema = { ...require('@bugsnag/core/config').schema, ...require('./config') } - -const pluginApp = require('@bugsnag/plugin-app-duration') -const pluginSurroundingCode = require('@bugsnag/plugin-node-surrounding-code') -const pluginInProject = require('@bugsnag/plugin-node-in-project') -const pluginStripProjectRoot = require('@bugsnag/plugin-strip-project-root') -const pluginServerSession = require('@bugsnag/plugin-server-session') -const pluginNodeDevice = require('@bugsnag/plugin-node-device') -const pluginNodeUncaughtException = require('@bugsnag/plugin-node-uncaught-exception') -const pluginNodeUnhandledRejection = require('@bugsnag/plugin-node-unhandled-rejection') -const pluginIntercept = require('@bugsnag/plugin-intercept') -const pluginContextualize = require('@bugsnag/plugin-contextualize') -const pluginStackframePathNormaliser = require('@bugsnag/plugin-stackframe-path-normaliser') -const pluginConsoleBreadcrumbs = require('@bugsnag/plugin-console-breadcrumbs') - -const internalPlugins = [ - pluginApp, - pluginSurroundingCode, - pluginInProject, - pluginStripProjectRoot, - pluginServerSession, - pluginNodeDevice, - pluginNodeUncaughtException, - pluginNodeUnhandledRejection, - pluginIntercept, - pluginContextualize, - pluginStackframePathNormaliser, - pluginConsoleBreadcrumbs -] - -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 bugsnag = new Client(opts, schema, internalPlugins, { name, version, url }) - - /** - * Patch all calls to the client in order to forwards them to the context client if it exists - * - * This is useful for when client methods are called later, such as in the console breadcrumbs - * plugin where we want to call `leaveBreadcrumb` on the request-scoped client, if it exists. - */ - Object.keys(Client.prototype).forEach((m) => { - const original = bugsnag[m] - bugsnag[m] = function () { - // if we are in an async context, use the client from that context - const contextClient = bugsnag._clientContext && typeof bugsnag._clientContext.getStore === 'function' ? bugsnag._clientContext.getStore() : null - const client = contextClient || bugsnag - const originalMethod = contextClient ? contextClient[m] : original - - client._depth += 1 - const ret = originalMethod.apply(client, arguments) - client._depth -= 1 - return ret - } - }) - - // Used to store and retrieve the request-scoped client which makes it easy to obtain the request-scoped client - // from anywhere in the codebase e.g. when calling Bugsnag.leaveBreadcrumb() or even within the global unhandled - // promise rejection handler. - bugsnag._clientContext = new AsyncLocalStorage() - - bugsnag._setDelivery(delivery) - - bugsnag._logger.debug('Loaded!') - - return 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 - } -} - -Object.keys(Client.prototype).forEach((m) => { - if (/^_/.test(m)) return - Bugsnag[m] = function () { - // if we are in an async context, use the client from that context - let client = Bugsnag._client - const ctx = client && client._clientContext && client._clientContext.getStore() - if (ctx) { - client = ctx - } - - if (!client) return console.error(`Bugsnag.${m}() was called before Bugsnag.start()`) - - client._depth += 1 - const ret = client[m].apply(client, arguments) - 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/node/test/integration/handled-unhandled.test.ts b/packages/node/test/integration/handled-unhandled.test.ts index 59a366e19e..7bb1781fc4 100644 --- a/packages/node/test/integration/handled-unhandled.test.ts +++ b/packages/node/test/integration/handled-unhandled.test.ts @@ -1,4 +1,4 @@ -import Bugsnag from '../../src/notifier' +import Bugsnag from '../../src/bugsnag' import https from 'https' // extend the https module type with the utilities added in mocks diff --git a/packages/node/test/notifier.test.ts b/packages/node/test/notifier.test.ts index 1fa7cc4fed..9ba99f6ee8 100644 --- a/packages/node/test/notifier.test.ts +++ b/packages/node/test/notifier.test.ts @@ -1,4 +1,4 @@ -import Bugsnag from '../src/notifier' +import Bugsnag from '../src/bugsnag' describe('node notifier', () => { beforeAll(() => { @@ -28,6 +28,8 @@ describe('node notifier', () => { Bugsnag.leaveBreadcrumb('test') expect(spy).toHaveBeenCalledWith('Bugsnag.leaveBreadcrumb() was called before Bugsnag.start()') + + spy.mockRestore() }) }) diff --git a/packages/node/tsconfig.json b/packages/node/tsconfig.json new file mode 100644 index 0000000000..1fbd9128bd --- /dev/null +++ b/packages/node/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest", "node"], + }, + "include": ["src/**/*.ts"] +} \ No newline at end of file diff --git a/packages/path-normalizer/LICENSE.txt b/packages/path-normalizer/LICENSE.txt new file mode 100644 index 0000000000..3596c5b072 --- /dev/null +++ b/packages/path-normalizer/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Bugsnag + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/path-normalizer/README.md b/packages/path-normalizer/README.md new file mode 100644 index 0000000000..2f4fc79bc0 --- /dev/null +++ b/packages/path-normalizer/README.md @@ -0,0 +1,23 @@ +# @bugsnag/path-normalizer + +A utility for normalizing file paths by making them absolute and adding trailing slashes for directories. + +## Installation + +```bash +npm install @bugsnag/path-normalizer +``` + +## Usage + +```javascript +const normalizePath = require('@bugsnag/path-normalizer') + +// Normalize a path +const normalizedPath = normalizePath('./some/relative/path') +// Returns: '/absolute/path/to/some/relative/path/' +``` + +## License + +MIT diff --git a/packages/path-normalizer/package.json b/packages/path-normalizer/package.json new file mode 100644 index 0000000000..89cdc8eb74 --- /dev/null +++ b/packages/path-normalizer/package.json @@ -0,0 +1,38 @@ +{ + "name": "@bugsnag/path-normalizer", + "version": "8.4.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "default": "./dist/index.js" + } + }, + "description": "Path normalization utility for Bugsnag", + "homepage": "https://www.bugsnag.com/", + "repository": { + "type": "git", + "url": "git@github.com:bugsnag/bugsnag-js.git" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "rollup --config rollup.config.js", + "test:types": "tsc -p tsconfig.json" + }, + "author": "Bugsnag", + "license": "MIT", + "devDependencies": { + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-typescript": "^12.1.2", + "rollup": "^4.24.0", + "typescript": "^5.7.2" + } +} diff --git a/packages/path-normalizer/rollup.config.js b/packages/path-normalizer/rollup.config.js new file mode 100644 index 0000000000..05d336978d --- /dev/null +++ b/packages/path-normalizer/rollup.config.js @@ -0,0 +1,47 @@ +/* eslint-env node */ +/* global require, module */ +const { nodeResolve } = require('@rollup/plugin-node-resolve') +const commonjs = require('@rollup/plugin-commonjs') +const typescript = require('@rollup/plugin-typescript') + +module.exports = [ + // CommonJS build + { + input: 'src/index.ts', + output: { + file: 'dist/index.js', + format: 'cjs', + exports: 'default' + }, + plugins: [ + nodeResolve({ + preferBuiltins: true + }), + commonjs(), + typescript({ + declaration: true, + declarationDir: 'dist', + rootDir: 'src' + }) + ], + external: ['path'] + }, + // ES Module build + { + input: 'src/index.ts', + output: { + file: 'dist/index.mjs', + format: 'es' + }, + plugins: [ + nodeResolve({ + preferBuiltins: true + }), + commonjs(), + typescript({ + declaration: false + }) + ], + external: ['path'] + } +] diff --git a/packages/core/lib/path-normalizer.js b/packages/path-normalizer/src/index.ts similarity index 55% rename from packages/core/lib/path-normalizer.js rename to packages/path-normalizer/src/index.ts index fc5c75bf41..592c74c106 100644 --- a/packages/core/lib/path-normalizer.js +++ b/packages/path-normalizer/src/index.ts @@ -1,5 +1,7 @@ -const { join, resolve } = require('path') +import { join, resolve } from 'path' // normalise a path to a directory, adding a trailing slash if it doesn't already // have one and resolve it to make it absolute (e.g. get rid of any ".."s) -module.exports = p => join(resolve(p), '/') +const normalizePath = (p: string) => join(resolve(p), '/') + +export default normalizePath diff --git a/packages/path-normalizer/tsconfig.json b/packages/path-normalizer/tsconfig.json new file mode 100644 index 0000000000..918896fa6f --- /dev/null +++ b/packages/path-normalizer/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["."], + "compilerOptions": { + "lib": ["es2017"], + "types": ["node"] + } +} diff --git a/packages/plugin-angular/package.json b/packages/plugin-angular/package.json index 775b638ed6..267706d9dd 100644 --- a/packages/plugin-angular/package.json +++ b/packages/plugin-angular/package.json @@ -28,7 +28,8 @@ "@angular-devkit/build-angular": "19.2.14", "@angular/cli": "^19.0.5", "@angular/common": "^19.0.0", - "@angular/compiler-cli": "^19.0.0", + "@angular/compiler": "^19.2.2", + "@angular/compiler-cli": "^19.2.2", "@angular/core": "^19.0.0", "@bugsnag/js": "^8.4.0", "ng-packagr": "^19.1.0", diff --git a/packages/plugin-app-duration/package.json b/packages/plugin-app-duration/package.json index 9af0efb32f..eefa2d4982 100644 --- a/packages/plugin-app-duration/package.json +++ b/packages/plugin-app-duration/package.json @@ -1,7 +1,15 @@ { "name": "@bugsnag/plugin-app-duration", "version": "8.4.0", - "main": "app.js", + "main": "dist/app-duration.js", + "types": "dist/types/app-duration.d.ts", + "exports": { + ".": { + "types": "./dist/types/app-duration.d.ts", + "default": "./dist/app-duration.js", + "import": "./dist/app-duration.mjs" + } + }, "description": "@bugsnag/js plugin to set app duration in browsers and node", "homepage": "https://www.bugsnag.com/", "repository": { @@ -21,5 +29,10 @@ }, "peerDependencies": { "@bugsnag/core": "^8.0.0" + }, + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" } } diff --git a/packages/plugin-app-duration/rollup.config.npm.mjs b/packages/plugin-app-duration/rollup.config.npm.mjs new file mode 100644 index 0000000000..cb88da21b5 --- /dev/null +++ b/packages/plugin-app-duration/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from '../../.rollup/index.mjs' + +export default createRollupConfig({ + input: 'src/app-duration.ts', + external: [/node_modules/] +}) diff --git a/packages/plugin-app-duration/app.js b/packages/plugin-app-duration/src/app-duration.ts similarity index 60% rename from packages/plugin-app-duration/app.js rename to packages/plugin-app-duration/src/app-duration.ts index 7d3c370bf3..3d43f82b6a 100644 --- a/packages/plugin-app-duration/app.js +++ b/packages/plugin-app-duration/src/app-duration.ts @@ -1,15 +1,19 @@ +import { Plugin } from '@bugsnag/core' + let appStart = new Date() const reset = () => { appStart = new Date() } -module.exports = { +const plugin: Plugin = { name: 'appDuration', load: client => { client.addOnError(event => { const now = new Date() - event.app.duration = now - appStart + event.app.duration = Number(now) - Number(appStart) }, true) return { reset } } } + +export default plugin diff --git a/packages/plugin-app-duration/test/app.test.ts b/packages/plugin-app-duration/test/app-duration.test.ts similarity index 91% rename from packages/plugin-app-duration/test/app.test.ts rename to packages/plugin-app-duration/test/app-duration.test.ts index 7f6911d16c..b80a419e89 100644 --- a/packages/plugin-app-duration/test/app.test.ts +++ b/packages/plugin-app-duration/test/app-duration.test.ts @@ -1,6 +1,5 @@ -import plugin from '../app' -import Client from '@bugsnag/core/client' -import Event from '@bugsnag/core/event' +import plugin from '../src/app-duration' +import { Client, Event } from '@bugsnag/core' describe('plugin-app-duration', () => { it('includes duration in event.app', done => { @@ -39,7 +38,7 @@ describe('plugin-app-duration', () => { } } - const result = plugin.load(client) + const result = plugin.load(client as unknown as Client) const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) diff --git a/packages/plugin-app-duration/tsconfig.json b/packages/plugin-app-duration/tsconfig.json new file mode 100644 index 0000000000..c76cdab7cd --- /dev/null +++ b/packages/plugin-app-duration/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "target": "ES2020" + } +} + \ No newline at end of file diff --git a/packages/plugin-aws-lambda/package.json b/packages/plugin-aws-lambda/package.json index 6c32bf6d26..491164465e 100644 --- a/packages/plugin-aws-lambda/package.json +++ b/packages/plugin-aws-lambda/package.json @@ -1,8 +1,15 @@ { "name": "@bugsnag/plugin-aws-lambda", "version": "8.4.0", - "main": "src/index.js", - "types": "types/bugsnag-plugin-aws-lambda.d.ts", + "main": "dist/index.js", + "types": "dist/types/index.d.ts", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "default": "./dist/index.js", + "import": "./dist/index.mjs" + } + }, "description": "AWS Lambda support for @bugsnag/node", "homepage": "https://www.bugsnag.com/", "repository": { @@ -13,8 +20,7 @@ "access": "public" }, "files": [ - "src", - "types" + "dist" ], "author": "Bugsnag", "license": "MIT", @@ -30,5 +36,10 @@ }, "peerDependencies": { "@bugsnag/core": "^8.0.0" + }, + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" } } diff --git a/packages/plugin-aws-lambda/rollup.config.npm.mjs b/packages/plugin-aws-lambda/rollup.config.npm.mjs new file mode 100644 index 0000000000..28566d8c04 --- /dev/null +++ b/packages/plugin-aws-lambda/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from '../../.rollup/index.mjs' + +export default createRollupConfig({ + input: 'src/index.ts', + external: ['@bugsnag/in-flight', '@bugsnag/plugin-browser-session'] +}) diff --git a/packages/plugin-aws-lambda/src/index.js b/packages/plugin-aws-lambda/src/index.ts similarity index 72% rename from packages/plugin-aws-lambda/src/index.js rename to packages/plugin-aws-lambda/src/index.ts index 688914f16b..46f9e8c43a 100644 --- a/packages/plugin-aws-lambda/src/index.js +++ b/packages/plugin-aws-lambda/src/index.ts @@ -1,6 +1,7 @@ -const bugsnagInFlight = require('@bugsnag/in-flight') -const BugsnagPluginBrowserSession = require('@bugsnag/plugin-browser-session') -const LambdaTimeoutApproaching = require('./lambda-timeout-approaching') +import { Client, Config, Plugin } from "@bugsnag/core" +import bugsnagInFlight from '@bugsnag/in-flight' +import BugsnagPluginBrowserSession from '@bugsnag/plugin-browser-session' +import LambdaTimeoutApproaching from './lambda-timeout-approaching' // JS timers use a signed 32 bit integer for the millisecond parameter. SAM's // "local invoke" has a bug that means it exceeds this amount, resulting in @@ -8,9 +9,30 @@ const LambdaTimeoutApproaching = require('./lambda-timeout-approaching') const MAX_TIMER_VALUE = Math.pow(2, 31) - 1 const SERVER_PLUGIN_NAMES = ['express', 'koa', 'restify', 'hono'] -const isServerPluginLoaded = client => SERVER_PLUGIN_NAMES.some(name => client.getPlugin(name)) +const isServerPluginLoaded = (client: Client) => SERVER_PLUGIN_NAMES.some(name => client.getPlugin(name)) -const BugsnagPluginAwsLambda = { +type AsyncHandler = (event: any, context: any) => Promise +type CallbackHandler = (event: any, context: any, callback: (err?: Error|string|null, response?: any) => void) => void + +export type BugsnagPluginAwsLambdaHandler = (handler: AsyncHandler|CallbackHandler) => AsyncHandler + +export interface BugsnagPluginAwsLambdaConfiguration { + flushTimeoutMs?: number + lambdaTimeoutNotifyMs?: number +} + +export interface BugsnagPluginAwsLambdaResult { + createHandler(configuration?: BugsnagPluginAwsLambdaConfiguration): BugsnagPluginAwsLambdaHandler +} + +// add a new call signature for the getPlugin() method that types the plugin result +declare module '@bugsnag/core' { + interface Client { + getPlugin(id: 'awsLambda'): BugsnagPluginAwsLambdaResult | undefined + } +} + +const BugsnagPluginAwsLambda: Plugin = { name: 'awsLambda', load (client) { @@ -62,7 +84,7 @@ const BugsnagPluginAwsLambda = { } } -function wrapHandler (client, flushTimeoutMs, lambdaTimeoutNotifyMs, handler) { +function wrapHandler (client: Client, flushTimeoutMs: number, lambdaTimeoutNotifyMs: number, handler: AsyncHandler | CallbackHandler): AsyncHandler { let _handler = handler if (handler.length > 2) { @@ -114,7 +136,7 @@ function wrapHandler (client, flushTimeoutMs, lambdaTimeoutNotifyMs, handler) { } try { - return await _handler(event, context) + return await (_handler as AsyncHandler)(event, context) } catch (err) { if (client._config.autoDetectErrors && client._config.enabledErrorTypes.unhandledExceptions) { const handledState = { @@ -123,8 +145,7 @@ function wrapHandler (client, flushTimeoutMs, lambdaTimeoutNotifyMs, handler) { severityReason: { type: 'unhandledException' } } - const event = client.Event.create(err, true, handledState, 'aws lambda plugin', 1) - + const event = client.Event.create(err as Error, true, handledState, 'aws lambda plugin', 1) client._notify(event) } @@ -137,14 +158,14 @@ function wrapHandler (client, flushTimeoutMs, lambdaTimeoutNotifyMs, handler) { try { await bugsnagInFlight.flush(flushTimeoutMs) } catch (err) { - client._logger.error(`Delivery may be unsuccessful: ${err.message}`) + client._logger.error(`Delivery may be unsuccessful: ${(err as Error).message}`) } } } } // Convert a handler that uses callbacks to an async handler -function promisifyHandler (handler) { +function promisifyHandler (handler: CallbackHandler): AsyncHandler { return function (event, context) { return new Promise(function (resolve, reject) { const result = handler(event, context, function (err, response) { @@ -166,13 +187,14 @@ function promisifyHandler (handler) { } } -function isPromise (value) { - return (typeof value === 'object' || typeof value === 'function') && - typeof value.then === 'function' && - typeof value.catch === 'function' +function isObject (value: unknown): value is Record { + return !!value && typeof value === 'object' && !Array.isArray(value) } -module.exports = BugsnagPluginAwsLambda +function isPromise (value: unknown): value is Promise { + return isObject(value) && + typeof value.then === 'function' && + typeof value.catch === 'function' +} -// add a default export for ESM modules without interop -module.exports.default = module.exports +export default BugsnagPluginAwsLambda diff --git a/packages/plugin-aws-lambda/src/lambda-timeout-approaching.js b/packages/plugin-aws-lambda/src/lambda-timeout-approaching.ts similarity index 50% rename from packages/plugin-aws-lambda/src/lambda-timeout-approaching.js rename to packages/plugin-aws-lambda/src/lambda-timeout-approaching.ts index ad48611bca..6557b45287 100644 --- a/packages/plugin-aws-lambda/src/lambda-timeout-approaching.js +++ b/packages/plugin-aws-lambda/src/lambda-timeout-approaching.ts @@ -1,10 +1,9 @@ -module.exports = class LambdaTimeoutApproaching extends Error { - constructor (remainingMs) { +export default class LambdaTimeoutApproaching extends Error { + constructor (remainingMs: number) { const message = `Lambda will timeout in ${remainingMs}ms` - super(message) this.name = 'LambdaTimeoutApproaching' - this.stack = [] + this.stack = undefined } } diff --git a/packages/plugin-aws-lambda/test/index.test.ts b/packages/plugin-aws-lambda/test/index.test.ts index 64827d2b29..e8d3911419 100644 --- a/packages/plugin-aws-lambda/test/index.test.ts +++ b/packages/plugin-aws-lambda/test/index.test.ts @@ -1,11 +1,10 @@ import util from 'util' import BugsnagPluginAwsLambda from '../src/' -import Client, { EventDeliveryPayload, SessionDeliveryPayload } from '@bugsnag/core/client' +import { Client, EventDeliveryPayload, SessionDeliveryPayload } from '@bugsnag/core' const createClient = (events: EventDeliveryPayload[], sessions: SessionDeliveryPayload[], config = {}) => { const client = new Client({ apiKey: 'AN_API_KEY', plugins: [BugsnagPluginAwsLambda], ...config }) - // @ts-ignore the following property is not defined on the public Event interface client.Event.__type = 'nodejs' // a flush failure won't throw as we don't want to crash apps if delivery takes diff --git a/packages/plugin-aws-lambda/test/serverless-express.test.js b/packages/plugin-aws-lambda/test/serverless-express.test.js index 0212eca34e..3dcc571fc6 100644 --- a/packages/plugin-aws-lambda/test/serverless-express.test.js +++ b/packages/plugin-aws-lambda/test/serverless-express.test.js @@ -4,8 +4,8 @@ // require the raw source files to avoid getting the built files - otherwise // we'd need to rebuild these packages to see any changes reflected in this file -const Bugsnag = require('../../node/src/notifier') -const BugsnagPluginAwsLambda = require('../../plugin-aws-lambda/src/index') +const Bugsnag = require('../../node/src/index-cjs').default +const BugsnagPluginAwsLambda = require('../../plugin-aws-lambda') const BugsnagPluginExpress = require('../../plugin-express/src/express') const serverlessExpress = require('@vendia/serverless-express') const express = require('express') diff --git a/packages/plugin-aws-lambda/tsconfig.json b/packages/plugin-aws-lambda/tsconfig.json new file mode 100644 index 0000000000..ff411e3018 --- /dev/null +++ b/packages/plugin-aws-lambda/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "target": "ES2020", + "types": ["node"] + } +} + \ No newline at end of file diff --git a/packages/plugin-aws-lambda/types/bugsnag-plugin-aws-lambda.d.ts b/packages/plugin-aws-lambda/types/bugsnag-plugin-aws-lambda.d.ts deleted file mode 100644 index 56ffcd89d4..0000000000 --- a/packages/plugin-aws-lambda/types/bugsnag-plugin-aws-lambda.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Plugin, Client } from '@bugsnag/core' - -declare const BugsnagPluginAwsLambda: Plugin -export default BugsnagPluginAwsLambda - -type AsyncHandler = (event: any, context: any) => Promise -type CallbackHandler = (event: any, context: any, callback: (err?: Error|string|null, response?: any) => void) => void - -export type BugsnagPluginAwsLambdaHandler = (handler: AsyncHandler|CallbackHandler) => AsyncHandler - -export interface BugsnagPluginAwsLambdaConfiguration { - flushTimeoutMs?: number - lambdaTimeoutNotifyMs?: number -} - -export interface BugsnagPluginAwsLambdaResult { - createHandler(configuration?: BugsnagPluginAwsLambdaConfiguration): BugsnagPluginAwsLambdaHandler -} - -// add a new call signature for the getPlugin() method that types the plugin result -declare module '@bugsnag/core' { - interface Client { - getPlugin(id: 'awsLambda'): BugsnagPluginAwsLambdaResult | undefined - } -} diff --git a/packages/plugin-browser-context/package.json b/packages/plugin-browser-context/package.json index 26a769cd44..56c9fd4827 100644 --- a/packages/plugin-browser-context/package.json +++ b/packages/plugin-browser-context/package.json @@ -1,7 +1,15 @@ { "name": "@bugsnag/plugin-browser-context", "version": "8.4.0", - "main": "context.js", + "main": "dist/context.js", + "types": "dist/types/context.d.ts", + "exports": { + ".": { + "types": "./dist/types/context.d.ts", + "default": "./dist/context.js", + "import": "./dist/context.mjs" + } + }, "description": "@bugsnag/js plugin to set event context in browsers", "homepage": "https://www.bugsnag.com/", "repository": { @@ -12,7 +20,7 @@ "access": "public" }, "files": [ - "*.js" + "dist" ], "author": "Bugsnag", "license": "MIT", @@ -21,5 +29,10 @@ }, "peerDependencies": { "@bugsnag/core": "^8.0.0" + }, + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" } } diff --git a/packages/plugin-browser-context/rollup.config.npm.mjs b/packages/plugin-browser-context/rollup.config.npm.mjs new file mode 100644 index 0000000000..85f3e6c4ae --- /dev/null +++ b/packages/plugin-browser-context/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from "../../.rollup/index.mjs"; + +export default createRollupConfig({ + input: "src/context.ts", + external: [/node_modules/] +}); diff --git a/packages/plugin-browser-context/context.js b/packages/plugin-browser-context/src/context.ts similarity index 66% rename from packages/plugin-browser-context/context.js rename to packages/plugin-browser-context/src/context.ts index 42dd661b5e..218a0478ad 100644 --- a/packages/plugin-browser-context/context.js +++ b/packages/plugin-browser-context/src/context.ts @@ -1,8 +1,9 @@ +import { Plugin } from '@bugsnag/core' /* * Sets the default context to be the current URL */ -module.exports = (win = window) => ({ - load: (client) => { +export default (win = window): Plugin => ({ + load: client => { client.addOnError(event => { if (event.context !== undefined) return event.context = win.location.pathname diff --git a/packages/plugin-browser-context/test/context.test.ts b/packages/plugin-browser-context/test/context.test.ts index a0b7414787..fa902e622b 100644 --- a/packages/plugin-browser-context/test/context.test.ts +++ b/packages/plugin-browser-context/test/context.test.ts @@ -1,6 +1,6 @@ -import plugin from '../' +import plugin from '../src/context' -import Client, { EventDeliveryPayload } from '@bugsnag/core/client' +import { Client, EventDeliveryPayload } from '@bugsnag/core' const window = { location: { diff --git a/packages/plugin-browser-context/tsconfig.json b/packages/plugin-browser-context/tsconfig.json new file mode 100644 index 0000000000..9478c51300 --- /dev/null +++ b/packages/plugin-browser-context/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "target": "ES2020" + } +} diff --git a/packages/plugin-browser-device/device.js b/packages/plugin-browser-device/device.js deleted file mode 100644 index bd1437b970..0000000000 --- a/packages/plugin-browser-device/device.js +++ /dev/null @@ -1,81 +0,0 @@ -const assign = require('@bugsnag/core/lib/es-utils/assign') -const BUGSNAG_ANONYMOUS_ID_KEY = 'bugsnag-anonymous-id' - -const getDeviceId = (win) => { - try { - const storage = win.localStorage - - let id = storage.getItem(BUGSNAG_ANONYMOUS_ID_KEY) - - // If we get an ID, make sure it looks like a valid cuid. The length can - // fluctuate slightly, so some leeway is built in - if (id && /^c[a-z0-9]{20,32}$/.test(id)) { - return id - } - - const cuid = require('@bugsnag/cuid') - id = cuid() - - storage.setItem(BUGSNAG_ANONYMOUS_ID_KEY, id) - - return id - } catch (err) { - // If localStorage is not available (e.g. because it's disabled) then give up - } -} - -/* - * Automatically detects browser device details - */ -module.exports = (nav = navigator, win = window) => ({ - load: (client) => { - const device = { - locale: nav.browserLanguage || nav.systemLanguage || nav.userLanguage || nav.language, - userAgent: nav.userAgent - } - - if (win && win.screen && win.screen.orientation && win.screen.orientation.type) { - device.orientation = win.screen.orientation.type - } else if (win && win.document) { - device.orientation = - win.document.documentElement.clientWidth > win.document.documentElement.clientHeight - ? 'landscape' - : 'portrait' - } - - if (client._config.generateAnonymousId) { - device.id = getDeviceId(win) - } - - client.addOnSession(session => { - session.device = assign({}, session.device, device) - // only set device id if collectUserIp is false - if (!client._config.collectUserIp) setDefaultUserId(session) - }) - - // add time just as the event is sent - client.addOnError((event) => { - event.device = assign({}, - event.device, - device, - { time: new Date() } - ) - if (!client._config.collectUserIp) setDefaultUserId(event) - }, true) - }, - configSchema: { - generateAnonymousId: { - validate: value => value === true || value === false, - defaultValue: () => true, - message: 'should be true|false' - } - } -}) - -const setDefaultUserId = (eventOrSession) => { - // device id is also used to populate the user id field, if it's not already set - const user = eventOrSession.getUser() - if (!user || !user.id) { - eventOrSession.setUser(eventOrSession.device.id) - } -} diff --git a/packages/plugin-browser-device/package.json b/packages/plugin-browser-device/package.json index 9c9b9aab33..d799c298f9 100644 --- a/packages/plugin-browser-device/package.json +++ b/packages/plugin-browser-device/package.json @@ -1,7 +1,15 @@ { "name": "@bugsnag/plugin-browser-device", "version": "8.4.0", - "main": "device.js", + "main": "dist/device.js", + "types": "dist/types/device.d.ts", + "exports": { + ".": { + "types": "./dist/types/device.d.ts", + "default": "./dist/device.js", + "import": "./dist/device.mjs" + } + }, "description": "@bugsnag/js plugin to set device info in browsers", "homepage": "https://www.bugsnag.com/", "repository": { @@ -24,5 +32,10 @@ }, "peerDependencies": { "@bugsnag/core": "^8.0.0" + }, + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" } } diff --git a/packages/plugin-browser-device/rollup.config.npm.mjs b/packages/plugin-browser-device/rollup.config.npm.mjs new file mode 100644 index 0000000000..7fae77a741 --- /dev/null +++ b/packages/plugin-browser-device/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from '../../.rollup/index.mjs' + +export default createRollupConfig({ + input: 'src/device.ts', + external: [/node_modules/], +}) diff --git a/packages/plugin-browser-device/src/device.ts b/packages/plugin-browser-device/src/device.ts new file mode 100644 index 0000000000..128838360c --- /dev/null +++ b/packages/plugin-browser-device/src/device.ts @@ -0,0 +1,67 @@ +import type { Config, Device, Plugin } from '@bugsnag/core' + +import setDefaultUserId from './set-default-user-id' +import getDeviceId from './get-device-id' + +// Declare deprecated navigator values +declare global { + interface Navigator { + browserLanguage?: string + systemLanguage?: string + userLanguage?: string + } +} + +interface PluginConfig extends Config { + generateAnonymousId?: boolean + collectUserIp?: boolean +} + +/* + * Automatically detects browser device details + */ +export default (nav = navigator, win: Window | null = window): Plugin => ({ + name: 'device', + load: (client) => { + const device: Device = { + locale: nav.browserLanguage || nav.systemLanguage || nav.userLanguage || nav.language, + userAgent: nav.userAgent + } + + if (win && win.screen && win.screen.orientation && win.screen.orientation.type) { + device.orientation = win.screen.orientation.type + } else if (win && win.document) { + device.orientation = + win.document.documentElement.clientWidth > win.document.documentElement.clientHeight + ? 'landscape' + : 'portrait' + } + + if (client._config.generateAnonymousId && win) { + device.id = getDeviceId(win) + } + + client.addOnSession(session => { + session.device = Object.assign({}, session.device, device) + // only set device id if collectUserIp is false + if (!client._config.collectUserIp) setDefaultUserId(session) + }) + + // add time just as the event is sent + client.addOnError((event) => { + event.device = Object.assign({}, + event.device, + device, + { time: new Date() } + ) + if (!client._config.collectUserIp) setDefaultUserId(event) + }, true) + }, + configSchema: { + generateAnonymousId: { + validate: (value: unknown): value is boolean => value === true || value === false, + defaultValue: () => true, + message: 'should be true|false' + } + } +}) diff --git a/packages/plugin-browser-device/src/get-device-id.ts b/packages/plugin-browser-device/src/get-device-id.ts new file mode 100644 index 0000000000..1bdcdbedef --- /dev/null +++ b/packages/plugin-browser-device/src/get-device-id.ts @@ -0,0 +1,27 @@ +import cuid from '@bugsnag/cuid' + +const BUGSNAG_ANONYMOUS_ID_KEY = 'bugsnag-anonymous-id' + +const getDeviceId = (win: Window) => { + try { + const storage = win.localStorage + + let id = storage.getItem(BUGSNAG_ANONYMOUS_ID_KEY) + + // If we get an ID, make sure it looks like a valid cuid. The length can + // fluctuate slightly, so some leeway is built in + if (id && /^c[a-z0-9]{20,32}$/.test(id)) { + return id + } + + id = cuid() + + storage.setItem(BUGSNAG_ANONYMOUS_ID_KEY, id) + + return id + } catch (err) { + // If localStorage is not available (e.g. because it's disabled) then give up + } +} + +export default getDeviceId diff --git a/packages/plugin-browser-device/src/set-default-user-id.ts b/packages/plugin-browser-device/src/set-default-user-id.ts new file mode 100644 index 0000000000..abc08d9af2 --- /dev/null +++ b/packages/plugin-browser-device/src/set-default-user-id.ts @@ -0,0 +1,11 @@ +import type { Event, Session } from '@bugsnag/core' + +const setDefaultUserId = (eventOrSession: Event | Session) => { + // device id is also used to populate the user id field, if it's not already set + const user = eventOrSession.getUser() + if ((!user || !user.id) && eventOrSession.device) { + eventOrSession.setUser(eventOrSession.device.id) + } +} + +export default setDefaultUserId diff --git a/packages/plugin-browser-device/test/device.test.ts b/packages/plugin-browser-device/test/device.test.ts index 819a208243..5cd18799cf 100644 --- a/packages/plugin-browser-device/test/device.test.ts +++ b/packages/plugin-browser-device/test/device.test.ts @@ -1,12 +1,6 @@ -import plugin from '../device' +import plugin from '../src/device' -import Client, { - SessionDeliveryPayload, - EventDeliveryPayload -} from '@bugsnag/core/client' -import { Device, Session } from '@bugsnag/core' -import EventWithInternals from '@bugsnag/core/event' -import { schema } from '@bugsnag/core/config' +import { Client, Device, Event, Session, SessionDeliveryPayload, EventDeliveryPayload, schema } from '@bugsnag/core' interface SessionWithDevice extends Session { device: Device } @@ -116,7 +110,7 @@ describe('plugin: device', () => { const mockDelivery = ( client: Client, - events: EventWithInternals[], + events: Event[], sessions: SessionWithDevice[] ) => { client._sessionDelegate = { @@ -164,7 +158,7 @@ describe('plugin: device', () => { [plugin(navigator)] ) - const events: EventWithInternals[] = [] + const events: Event[] = [] const sessions: SessionWithDevice[] = [] mockDelivery(client, events, sessions) @@ -188,7 +182,7 @@ describe('plugin: device', () => { [plugin(navigator)] ) - const events: EventWithInternals[] = [] + const events: Event[] = [] const sessions: SessionWithDevice[] = [] mockDelivery(client, events, sessions) @@ -210,7 +204,7 @@ describe('plugin: device', () => { [plugin(navigator)] ) - const events: EventWithInternals[] = [] + const events: Event[] = [] const sessions: SessionWithDevice[] = [] mockDelivery(client, events, sessions) @@ -241,7 +235,7 @@ describe('plugin: device', () => { [plugin(navigator)] ) - const events: EventWithInternals[] = [] + const events: Event[] = [] const sessions: SessionWithDevice[] = [] mockDelivery(client, events, sessions) @@ -285,7 +279,7 @@ describe('plugin: device', () => { [plugin(navigator)] ) - const events: EventWithInternals[] = [] + const events: Event[] = [] const sessions: SessionWithDevice[] = [] mockDelivery(client, events, sessions) @@ -312,7 +306,7 @@ describe('plugin: device', () => { [plugin(navigator)] ) - const events: EventWithInternals[] = [] + const events: Event[] = [] const sessions: SessionWithDevice[] = [] mockDelivery(client, events, sessions) @@ -343,7 +337,7 @@ describe('plugin: device', () => { [plugin(navigator)] ) - const events: EventWithInternals[] = [] + const events: Event[] = [] const sessions: SessionWithDevice[] = [] mockDelivery(client, events, sessions) @@ -374,7 +368,7 @@ describe('plugin: device', () => { [plugin(navigator)] ) - const events: EventWithInternals[] = [] + const events: Event[] = [] const sessions: SessionWithDevice[] = [] mockDelivery(client, events, sessions) @@ -399,7 +393,7 @@ describe('plugin: device', () => { [plugin(navigator)] ) - const events: EventWithInternals[] = [] + const events: Event[] = [] const sessions: SessionWithDevice[] = [] mockDelivery(client, events, sessions) @@ -425,7 +419,7 @@ describe('plugin: device', () => { [plugin(navigator)] ) - const events: EventWithInternals[] = [] + const events: Event[] = [] const sessions: SessionWithDevice[] = [] mockDelivery(client, events, sessions) @@ -451,7 +445,7 @@ describe('plugin: device', () => { [plugin(navigator)] ) - const events: EventWithInternals[] = [] + const events: Event[] = [] const sessions: SessionWithDevice[] = [] mockDelivery(client, events, sessions) @@ -480,7 +474,7 @@ describe('plugin: device', () => { }, [plugin(navigator)] ) - const events: EventWithInternals[] = [] + const events: Event[] = [] const sessions: SessionWithDevice[] = [] expect(client._cbs.e).toHaveLength(1) @@ -510,7 +504,7 @@ describe('plugin: device', () => { }, [plugin(navigator)] ) - const events: EventWithInternals[] = [] + const events: Event[] = [] const sessions: SessionWithDevice[] = [] expect(client._cbs.e).toHaveLength(1) @@ -542,7 +536,7 @@ describe('plugin: device', () => { }, [plugin(navigator)] ) - const events: EventWithInternals[] = [] + const events: Event[] = [] const sessions: SessionWithDevice[] = [] expect(client._cbs.e).toHaveLength(1) diff --git a/packages/plugin-browser-device/tsconfig.json b/packages/plugin-browser-device/tsconfig.json new file mode 100644 index 0000000000..c76cdab7cd --- /dev/null +++ b/packages/plugin-browser-device/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "target": "ES2020" + } +} + \ No newline at end of file diff --git a/packages/plugin-browser-request/package.json b/packages/plugin-browser-request/package.json index 04c424e8fd..2b0e19b35d 100644 --- a/packages/plugin-browser-request/package.json +++ b/packages/plugin-browser-request/package.json @@ -1,7 +1,15 @@ { "name": "@bugsnag/plugin-browser-request", "version": "8.4.0", - "main": "request.js", + "main": "dist/request.js", + "types": "dist/types/request.d.ts", + "exports": { + ".": { + "types": "./dist/types/request.d.ts", + "default": "./dist/request.js", + "import": "./dist/request.mjs" + } + }, "description": "@bugsnag/js plugin to set request info in browsers", "homepage": "https://www.bugsnag.com/", "repository": { @@ -12,9 +20,8 @@ "access": "public" }, "files": [ - "*.js" + "dist" ], - "scripts": {}, "author": "Bugsnag", "license": "MIT", "devDependencies": { @@ -22,5 +29,10 @@ }, "peerDependencies": { "@bugsnag/core": "^8.0.0" + }, + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" } } diff --git a/packages/plugin-browser-request/rollup.config.npm.mjs b/packages/plugin-browser-request/rollup.config.npm.mjs new file mode 100644 index 0000000000..5de2d4968e --- /dev/null +++ b/packages/plugin-browser-request/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from "../../.rollup/index.mjs"; + +export default createRollupConfig({ + input: "src/request.ts", + external: [/node_modules/], +}); diff --git a/packages/plugin-browser-request/request.js b/packages/plugin-browser-request/src/request.ts similarity index 52% rename from packages/plugin-browser-request/request.js rename to packages/plugin-browser-request/src/request.ts index 83e18d771b..490c532962 100644 --- a/packages/plugin-browser-request/request.js +++ b/packages/plugin-browser-request/src/request.ts @@ -1,13 +1,14 @@ -const assign = require('@bugsnag/core/lib/es-utils/assign') +import { Plugin } from '@bugsnag/core' + /* * Sets the event request: { url } to be the current href */ -module.exports = (win = window) => ({ +export default (win = window): Plugin => ({ load: (client) => { client.addOnError(event => { if (event.request && event.request.url) return - event.request = assign({}, event.request, { url: win.location.href }) + event.request = Object.assign({}, event.request, { url: win.location.href }) }, true) } }) diff --git a/packages/plugin-browser-request/test/request.test.ts b/packages/plugin-browser-request/test/request.test.ts index e0e225baa2..8a1dfe5d01 100644 --- a/packages/plugin-browser-request/test/request.test.ts +++ b/packages/plugin-browser-request/test/request.test.ts @@ -1,6 +1,6 @@ -import plugin from '../' +import plugin from '../src/request' -import Client, { EventDeliveryPayload } from '@bugsnag/core/client' +import { Client, EventDeliveryPayload } from '@bugsnag/core' const window = { location: { href: 'http://xyz.abc/foo/bar.html' } } as unknown as Window & typeof globalThis diff --git a/packages/plugin-browser-request/tsconfig.json b/packages/plugin-browser-request/tsconfig.json new file mode 100644 index 0000000000..9478c51300 --- /dev/null +++ b/packages/plugin-browser-request/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "target": "ES2020" + } +} diff --git a/packages/plugin-browser-session/package.json b/packages/plugin-browser-session/package.json index d0f02e612a..d851b02568 100644 --- a/packages/plugin-browser-session/package.json +++ b/packages/plugin-browser-session/package.json @@ -1,7 +1,15 @@ { "name": "@bugsnag/plugin-browser-session", "version": "8.4.0", - "main": "session.js", + "main": "dist/session.js", + "types": "dist/types/session.d.ts", + "exports": { + ".": { + "types": "./dist/types/session.d.ts", + "default": "./dist/session.js", + "import": "./dist/session.mjs" + } + }, "description": "@bugsnag/js plugin to enable session tracking in browsers", "homepage": "https://www.bugsnag.com/", "repository": { @@ -12,7 +20,7 @@ "access": "public" }, "files": [ - "*.js" + "dist" ], "author": "Bugsnag", "license": "MIT", @@ -21,5 +29,10 @@ }, "peerDependencies": { "@bugsnag/core": "^8.0.0" + }, + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" } } diff --git a/packages/plugin-browser-session/rollup.config.npm.mjs b/packages/plugin-browser-session/rollup.config.npm.mjs new file mode 100644 index 0000000000..5910a8a13c --- /dev/null +++ b/packages/plugin-browser-session/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from "../../.rollup/index.mjs"; + +export default createRollupConfig({ + input: "src/session.ts", + external: [/node_modules/], +}); diff --git a/packages/plugin-browser-session/session.js b/packages/plugin-browser-session/src/session.ts similarity index 60% rename from packages/plugin-browser-session/session.js rename to packages/plugin-browser-session/src/session.ts index fdee3c102d..eb3a045047 100644 --- a/packages/plugin-browser-session/session.js +++ b/packages/plugin-browser-session/src/session.ts @@ -1,17 +1,19 @@ -const includes = require('@bugsnag/core/lib/es-utils/includes') +import { Client, Plugin, Session, SessionDelegate } from '@bugsnag/core' -module.exports = { - load: client => { client._sessionDelegate = sessionDelegate } +const plugin: Plugin = { + load: client => { + client._sessionDelegate = sessionDelegate + } } -const sessionDelegate = { - startSession: (client, session) => { +const sessionDelegate: SessionDelegate = { + startSession: (client: Client, session: Session) => { const sessionClient = client sessionClient._session = session sessionClient._pausedSession = null // exit early if the current releaseStage is not enabled - if (sessionClient._config.enabledReleaseStages !== null && !includes(sessionClient._config.enabledReleaseStages, sessionClient._config.releaseStage)) { + if (sessionClient._config.enabledReleaseStages && sessionClient._config.enabledReleaseStages.indexOf(sessionClient._config.releaseStage) === -1) { sessionClient._logger.warn('Session not sent due to releaseStage/enabledReleaseStages configuration') return sessionClient } @@ -24,13 +26,13 @@ const sessionDelegate = { { id: session.id, startedAt: session.startedAt, - user: session._user + user: session.getUser() } ] - }) + }, () => {}) return sessionClient }, - resumeSession: (client) => { + resumeSession: (client: Client) => { // Do nothing if there's already an active session if (client._session) { return client @@ -45,10 +47,13 @@ const sessionDelegate = { } // Otherwise start a new session - return client.startSession() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return client.startSession()! }, - pauseSession: (client) => { + pauseSession: (client: Client) => { client._pausedSession = client._session client._session = null } } + +export default plugin diff --git a/packages/plugin-browser-session/test/session.test.ts b/packages/plugin-browser-session/test/session.test.ts index 70a41c2c53..2b594ccece 100644 --- a/packages/plugin-browser-session/test/session.test.ts +++ b/packages/plugin-browser-session/test/session.test.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import plugin from '../' -import Client, { EventDeliveryPayload } from '@bugsnag/core/client' -import EventWithInternals from '@bugsnag/core/event' +import plugin from '../src/session' +import { Client, EventDeliveryPayload } from '@bugsnag/core' const VALID_NOTIFIER = { name: 't', version: '0', url: 'http://' } @@ -38,7 +37,7 @@ describe('plugin: sessions', () => { } })) const sessionClient = c.startSession() - const Event = c.Event as unknown as typeof EventWithInternals + const Event = c.Event sessionClient.notify(new Error('broke')) sessionClient._notify(new Event('err', 'bad', [], { unhandled: true, severity: 'error', severityReason: { type: 'unhandledException' } })) sessionClient.notify(new Error('broke')) diff --git a/packages/plugin-browser-session/tsconfig.json b/packages/plugin-browser-session/tsconfig.json new file mode 100644 index 0000000000..a5cb75c562 --- /dev/null +++ b/packages/plugin-browser-session/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"] +} diff --git a/packages/plugin-client-ip/client-ip.js b/packages/plugin-client-ip/client-ip.js deleted file mode 100644 index 0e4b7e9de1..0000000000 --- a/packages/plugin-client-ip/client-ip.js +++ /dev/null @@ -1,25 +0,0 @@ -const assign = require('@bugsnag/core/lib/es-utils/assign') - -/* - * Prevent collection of user IPs - */ -module.exports = { - load: (client) => { - if (client._config.collectUserIp) return - - client.addOnError(event => { - // If user.id is explicitly undefined, it will be missing from the payload. It needs - // removing so that the following line replaces it - if (event._user && typeof event._user.id === 'undefined') delete event._user.id - event._user = assign({ id: '[REDACTED]' }, event._user) - event.request = assign({ clientIp: '[REDACTED]' }, event.request) - }) - }, - configSchema: { - collectUserIp: { - defaultValue: () => true, - message: 'should be true|false', - validate: value => value === true || value === false - } - } -} diff --git a/packages/plugin-client-ip/package.json b/packages/plugin-client-ip/package.json index 4a807629d2..07a63cfb2c 100644 --- a/packages/plugin-client-ip/package.json +++ b/packages/plugin-client-ip/package.json @@ -1,7 +1,15 @@ { "name": "@bugsnag/plugin-client-ip", "version": "8.4.0", - "main": "client-ip.js", + "main": "dist/client-ip.js", + "types": "dist/types/client-ip.d.ts", + "exports": { + ".": { + "types": "./dist/types/client-ip.d.ts", + "default": "./dist/client-ip.js", + "import": "./dist/client-ip.mjs" + } + }, "description": "@bugsnag/js plugin to disable client IP from error reports", "homepage": "https://www.bugsnag.com/", "repository": { @@ -12,7 +20,7 @@ "access": "public" }, "files": [ - "*.js" + "dist" ], "author": "Bugsnag", "license": "MIT", @@ -21,5 +29,10 @@ }, "peerDependencies": { "@bugsnag/core": "^8.0.0" + }, + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" } } diff --git a/packages/plugin-client-ip/rollup.config.npm.mjs b/packages/plugin-client-ip/rollup.config.npm.mjs new file mode 100644 index 0000000000..1f2a6da34a --- /dev/null +++ b/packages/plugin-client-ip/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from "../../.rollup/index.mjs"; + +export default createRollupConfig({ + input: "src/client-ip.ts", + external: [/node_modules/], +}); diff --git a/packages/plugin-client-ip/src/client-ip.ts b/packages/plugin-client-ip/src/client-ip.ts new file mode 100644 index 0000000000..2521f3a9ac --- /dev/null +++ b/packages/plugin-client-ip/src/client-ip.ts @@ -0,0 +1,44 @@ +import { Plugin } from '@bugsnag/core' + + +interface Config { + collectUserIp: boolean +} + +interface ExtendedPlugin extends Plugin { + configSchema: Record +} + +interface ValidationOption { + validate: (value: unknown) => boolean + defaultValue: () => unknown + message: string +} + +/* + * Prevent collection of user IPs + */ +const plugin: ExtendedPlugin = { + load: client => { + if ((client._config as unknown as Config).collectUserIp) return + + client.addOnError(event => { + // If user.id is explicitly undefined, it will be missing from the payload. It needs + // removing so that the following line replaces it + if (event.getUser() && typeof event.getUser().id === 'undefined') { + const _user = event.getUser() + event.setUser('[REDACTED]', _user.email, _user.name) + } + event.request = Object.assign({ clientIp: '[REDACTED]' }, event.request) + }) + }, + configSchema: { + collectUserIp: { + defaultValue: () => true, + message: 'should be true|false', + validate: (value: unknown) => value === true || value === false + } + } +} + +export default plugin diff --git a/packages/plugin-client-ip/test/client-ip.test.ts b/packages/plugin-client-ip/test/client-ip.test.ts index 04a4f50b00..a715afa08d 100644 --- a/packages/plugin-client-ip/test/client-ip.test.ts +++ b/packages/plugin-client-ip/test/client-ip.test.ts @@ -1,6 +1,6 @@ import plugin from '../' -import Client, { EventDeliveryPayload } from '@bugsnag/core/client' +import { Client, EventDeliveryPayload } from '@bugsnag/core' describe('plugin: ip', () => { it('does nothing when collectUserIp=true', () => { diff --git a/packages/plugin-client-ip/tsconfig.json b/packages/plugin-client-ip/tsconfig.json new file mode 100644 index 0000000000..a5cb75c562 --- /dev/null +++ b/packages/plugin-client-ip/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"] +} diff --git a/packages/plugin-console-breadcrumbs/console-breadcrumbs.js b/packages/plugin-console-breadcrumbs/console-breadcrumbs.js deleted file mode 100644 index f009c48247..0000000000 --- a/packages/plugin-console-breadcrumbs/console-breadcrumbs.js +++ /dev/null @@ -1,47 +0,0 @@ -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') - -/* - * Leaves breadcrumbs when console log methods are called - */ -exports.load = (client) => { - const isDev = /^(local-)?dev(elopment)?$/.test(client._config.releaseStage) - - if (isDev || !client._isBreadcrumbTypeEnabled('log')) return - - map(CONSOLE_LOG_METHODS, method => { - const original = console[method] - console[method] = (...args) => { - client.leaveBreadcrumb('Console output', reduce(args, (accum, arg, i) => { - // do the best/simplest stringification of each argument - let stringified = '[Unknown value]' - // this may fail if the input is: - // - an object whose [[Prototype]] is null (no toString) - // - an object with a broken toString or @@toPrimitive implementation - try { stringified = String(arg) } catch (e) {} - // if it stringifies to [object Object] attempt to JSON stringify - if (stringified === '[object Object]') { - // catch stringify errors and fallback to [object Object] - try { stringified = JSON.stringify(arg) } catch (e) {} - } - accum[`[${i}]`] = stringified - return accum - }, { - severity: method.indexOf('group') === 0 ? 'log' : method - }), 'log') - original.apply(console, args) - } - console[method]._restore = () => { console[method] = original } - }) -} - -if (process.env.NODE_ENV !== 'production') { - exports.destroy = () => CONSOLE_LOG_METHODS.forEach(method => { - if (typeof console[method]._restore === 'function') console[method]._restore() - }) -} - -const CONSOLE_LOG_METHODS = filter(['log', 'debug', 'info', 'warn', 'error'], method => - typeof console !== 'undefined' && typeof console[method] === 'function' -) diff --git a/packages/plugin-console-breadcrumbs/package.json b/packages/plugin-console-breadcrumbs/package.json index a0fbd8aeb3..0f70f1bd28 100644 --- a/packages/plugin-console-breadcrumbs/package.json +++ b/packages/plugin-console-breadcrumbs/package.json @@ -1,7 +1,15 @@ { "name": "@bugsnag/plugin-console-breadcrumbs", "version": "8.4.0", - "main": "console-breadcrumbs.js", + "main": "dist/console-breadcrumbs.js", + "types": "dist/types/console-breadcrumbs.d.ts", + "exports": { + ".": { + "types": "./dist/types/console-breadcrumbs.d.ts", + "default": "./dist/console-breadcrumbs.js", + "import": "./dist/console-breadcrumbs.mjs" + } + }, "description": "@bugsnag/js plugin to record console log method calls as breadcrumbs", "homepage": "https://www.bugsnag.com/", "repository": { @@ -12,7 +20,7 @@ "access": "public" }, "files": [ - "*.js" + "dist" ], "author": "Bugsnag", "license": "MIT", @@ -21,5 +29,10 @@ }, "peerDependencies": { "@bugsnag/core": "^8.0.0" + }, + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" } } diff --git a/packages/plugin-console-breadcrumbs/rollup.config.npm.mjs b/packages/plugin-console-breadcrumbs/rollup.config.npm.mjs new file mode 100644 index 0000000000..9749cd7288 --- /dev/null +++ b/packages/plugin-console-breadcrumbs/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from "../../.rollup/index.mjs"; + +export default createRollupConfig({ + input: "src/console-breadcrumbs.ts", + external: [/node_modules/], +}); diff --git a/packages/plugin-console-breadcrumbs/src/console-breadcrumbs.ts b/packages/plugin-console-breadcrumbs/src/console-breadcrumbs.ts new file mode 100644 index 0000000000..6ae759c2fb --- /dev/null +++ b/packages/plugin-console-breadcrumbs/src/console-breadcrumbs.ts @@ -0,0 +1,80 @@ +import { Client, Config, Plugin } from '@bugsnag/core' + +type ConsoleMethod = 'log' | 'debug' | 'info' | 'warn' | 'error' + +interface ClientWithInternals extends Client { + _config: Required + _isBreadcrumbTypeEnabled: (type: ConsoleMethod) => boolean +} + +type ConsoleWithRestore = Console & { + [key in ConsoleMethod]: typeof console[ConsoleMethod] & { _restore: () => void } +} + +/* + * Leaves breadcrumbs when console log methods are called + */ +const plugin: Plugin = { + load: (client) => { + const isDev = /^(local-)?dev(elopment)?$/.test((client as ClientWithInternals)._config.releaseStage) + + if (isDev || !(client as ClientWithInternals)._isBreadcrumbTypeEnabled('log')) return + + CONSOLE_LOG_METHODS.map(method => { + const original = console[method] + console[method] = (...args: any) => { + client.leaveBreadcrumb( + 'Console output', + args.reduce( + (accum: Record, arg: any, i: number) => { + // do the best/simplest stringification of each argument + let stringified = '[Unknown value]' + // this may fail if the input is: + // - an object whose [[Prototype]] is null (no toString) + // - an object with a broken toString or @@toPrimitive implementation + try { + stringified = String(arg) + } catch (e) {} + // if it stringifies to [object Object] attempt to JSON stringify + if (stringified === '[object Object]') { + // catch stringify errors and fallback to [object Object] + try { + stringified = JSON.stringify(arg) + } catch (e) {} + } + accum[`[${i}]`] = stringified + return accum + }, + { + severity: method.indexOf('group') === 0 ? 'log' : method + } + ), + 'log' + ) + original.apply(console, args) + } + (console as ConsoleWithRestore)[method]._restore = () => { + console[method] = original + } + }) + } +} + +if (process.env.NODE_ENV !== 'production') { + plugin.destroy = () => { + const consoleWithRestore = console as ConsoleWithRestore + CONSOLE_LOG_METHODS.forEach(method => { + if (typeof consoleWithRestore[method]._restore === 'function') { + consoleWithRestore[method]._restore() + } + }) + } +} + +const consoleMethod: ConsoleMethod[] = ['log', 'debug', 'info', 'warn', 'error'] +const CONSOLE_LOG_METHODS: ConsoleMethod[] = consoleMethod.filter( + method => + typeof console !== 'undefined' && typeof console[method] === 'function' +) + +export default plugin diff --git a/packages/plugin-console-breadcrumbs/test/console-breadcrumbs.test.ts b/packages/plugin-console-breadcrumbs/test/console-breadcrumbs.test.ts index a6f5799b13..521448c853 100644 --- a/packages/plugin-console-breadcrumbs/test/console-breadcrumbs.test.ts +++ b/packages/plugin-console-breadcrumbs/test/console-breadcrumbs.test.ts @@ -1,6 +1,6 @@ -import plugin from '../' +import plugin from '../src/console-breadcrumbs' -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' describe('plugin: console breadcrumbs', () => { beforeAll(() => { @@ -27,7 +27,7 @@ describe('plugin: console breadcrumbs', () => { expect(c._breadcrumbs[2].metadata['[0]']).toBe('{"foo":[1,2,3,"four"]}') expect(c._breadcrumbs[2].metadata['[1]']).toBe('{"pets":{"cat":"scratcher","dog":"pupper","rabbit":"sniffer"}}') // undo the global side effects of wrapping console.* for the rest of the tests - plugin.destroy() + plugin.destroy?.() }) it('should not throw when an object without toString is logged', () => { @@ -36,48 +36,48 @@ describe('plugin: console breadcrumbs', () => { expect(c._breadcrumbs.length).toBe(1) expect(c._breadcrumbs[0].message).toBe('Console output') expect(c._breadcrumbs[0].metadata['[0]']).toBe('[Unknown value]') - plugin.destroy() + plugin.destroy?.() }) it('should not be enabled when enabledBreadcrumbTypes=[]', () => { const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: [], plugins: [plugin] }) console.log(123) expect(c._breadcrumbs.length).toBe(0) - plugin.destroy() + plugin.destroy?.() }) it('should be enabled when enabledBreadcrumbTypes=null', () => { const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: null, plugins: [plugin] }) console.log(123) expect(c._breadcrumbs).toHaveLength(1) - plugin.destroy() + plugin.destroy?.() }) it('should be enabled when enabledBreadcrumbTypes=["log"]', () => { const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', enabledBreadcrumbTypes: ['log'], plugins: [plugin] }) console.log(123) expect(c._breadcrumbs.length).toBe(1) - plugin.destroy() + plugin.destroy?.() }) it('should be not enabled by default when releaseStage=development', () => { const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', releaseStage: 'development', plugins: [plugin] }) console.log(123) expect(c._breadcrumbs.length).toBe(0) - plugin.destroy() + plugin.destroy?.() }) it('should be not enabled by default when releaseStage=dev', () => { const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', releaseStage: 'dev', plugins: [plugin] }) console.log(123) expect(c._breadcrumbs.length).toBe(0) - plugin.destroy() + plugin.destroy?.() }) it('should be not enabled by default when releaseStage=local-dev', () => { const c = new Client({ apiKey: 'aaaa-aaaa-aaaa-aaaa', releaseStage: 'local-dev', plugins: [plugin] }) console.log(123) expect(c._breadcrumbs.length).toBe(0) - plugin.destroy() + plugin.destroy?.() }) }) diff --git a/packages/plugin-console-breadcrumbs/tsconfig.json b/packages/plugin-console-breadcrumbs/tsconfig.json new file mode 100644 index 0000000000..4fe20b6abd --- /dev/null +++ b/packages/plugin-console-breadcrumbs/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "target": "ES2020", + "types": ["node"] + } +} diff --git a/packages/plugin-contextualize/contextualize.js b/packages/plugin-contextualize/contextualize.js index 5a3671ef76..5097089475 100644 --- a/packages/plugin-contextualize/contextualize.js +++ b/packages/plugin-contextualize/contextualize.js @@ -1,14 +1,13 @@ -const { getStack } = require('@bugsnag/core/lib/node-fallback-stack') -const clone = require('@bugsnag/core/lib/clone-client') +const { cloneClient, nodeFallbackStack } = require('@bugsnag/core') module.exports = { name: 'contextualize', load: client => { const contextualize = (fn, onError) => { // capture a stacktrace in case a resulting error has nothing - const fallbackStack = getStack() + const fallbackStack = nodeFallbackStack.getStack() - const clonedClient = clone(client) + const clonedClient = cloneClient(client) // add the stacktrace to the cloned client so it can be used later // by the uncaught exception handler. Note the unhandled rejection diff --git a/packages/plugin-electron-app-breadcrumbs/test/app-breadcrumbs.test.ts b/packages/plugin-electron-app-breadcrumbs/test/app-breadcrumbs.test.ts index 84faf92af8..ddcbab6b7d 100644 --- a/packages/plugin-electron-app-breadcrumbs/test/app-breadcrumbs.test.ts +++ b/packages/plugin-electron-app-breadcrumbs/test/app-breadcrumbs.test.ts @@ -1,5 +1,4 @@ -import Breadcrumb from '@bugsnag/core/breadcrumb' -import Client from '@bugsnag/core/client' +import { Breadcrumb, Client } from '@bugsnag/core' import { makeApp, makeBrowserWindow } from '@bugsnag/electron-test-helpers' import plugin from '../' diff --git a/packages/plugin-electron-app/app.js b/packages/plugin-electron-app/app.js index d01b68dbf5..8abfd773e9 100644 --- a/packages/plugin-electron-app/app.js +++ b/packages/plugin-electron-app/app.js @@ -1,6 +1,5 @@ const native = require('bindings')('bugsnag_plugin_electron_app_bindings') -const { schema } = require('@bugsnag/core/config') -const intRange = require('@bugsnag/core/lib/validators/int-range') +const { schema, intRange } = require('@bugsnag/core') const isNativeClientEnabled = client => client._config.autoDetectErrors && client._config.enabledErrorTypes.nativeCrashes diff --git a/packages/plugin-electron-client-state-manager/test/client-state-manager.test.ts b/packages/plugin-electron-client-state-manager/test/client-state-manager.test.ts index 7d7c40794b..dd070cc68b 100644 --- a/packages/plugin-electron-client-state-manager/test/client-state-manager.test.ts +++ b/packages/plugin-electron-client-state-manager/test/client-state-manager.test.ts @@ -1,5 +1,5 @@ import stateManager from '../client-state-manager' -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' import { User } from '@bugsnag/core' const Notifier = { diff --git a/packages/plugin-electron-client-state-persistence/client-state-persistence.js b/packages/plugin-electron-client-state-persistence/client-state-persistence.js index f0ee0c3966..46279dd904 100644 --- a/packages/plugin-electron-client-state-persistence/client-state-persistence.js +++ b/packages/plugin-electron-client-state-persistence/client-state-persistence.js @@ -1,4 +1,4 @@ -const featureFlagDelegate = require('@bugsnag/core/lib/feature-flag-delegate') +const { featureFlagDelegate } = require('@bugsnag/core') const isEnabledFor = client => client._config.autoDetectErrors && client._config.enabledErrorTypes.nativeCrashes diff --git a/packages/plugin-electron-deliver-minidumps/deliver-minidumps.js b/packages/plugin-electron-deliver-minidumps/deliver-minidumps.js index d8aea81739..39b3e8972d 100644 --- a/packages/plugin-electron-deliver-minidumps/deliver-minidumps.js +++ b/packages/plugin-electron-deliver-minidumps/deliver-minidumps.js @@ -5,7 +5,7 @@ const MinidumpDeliveryLoop = require('./minidump-loop') const MinidumpQueue = require('./minidump-queue') const sendMinidumpFactory = require('./send-minidump') const NetworkStatus = require('@bugsnag/electron-network-status') -const featureFlagDelegate = require('@bugsnag/core/lib/feature-flag-delegate') +const { featureFlagDelegate } = require('@bugsnag/core') const isEnabledFor = client => client._config.autoDetectErrors && client._config.enabledErrorTypes.nativeCrashes diff --git a/packages/plugin-electron-deliver-minidumps/event-serialisation.js b/packages/plugin-electron-deliver-minidumps/event-serialisation.js index 59f38286be..d838bfcba6 100644 --- a/packages/plugin-electron-deliver-minidumps/event-serialisation.js +++ b/packages/plugin-electron-deliver-minidumps/event-serialisation.js @@ -1,6 +1,4 @@ -const Event = require('@bugsnag/core/event') -const Session = require('@bugsnag/core/session') -const Breadcrumb = require('@bugsnag/core/breadcrumb') +const { Breadcrumb, Event, Session } = require('@bugsnag/core') const supportedProperties = [ 'app', diff --git a/packages/plugin-electron-deliver-minidumps/minidump-loop.js b/packages/plugin-electron-deliver-minidumps/minidump-loop.js index 739c49359b..863535ae60 100644 --- a/packages/plugin-electron-deliver-minidumps/minidump-loop.js +++ b/packages/plugin-electron-deliver-minidumps/minidump-loop.js @@ -1,5 +1,5 @@ const { readFile } = require('fs').promises -const runSyncCallbacks = require('@bugsnag/core/lib/sync-callback-runner') +const { runSyncCallbacks } = require('@bugsnag/core') const { serialiseEvent, deserialiseEvent } = require('./event-serialisation') module.exports = class MinidumpDeliveryLoop { diff --git a/packages/plugin-electron-deliver-minidumps/package.json b/packages/plugin-electron-deliver-minidumps/package.json index 24d4665b67..c25170b296 100644 --- a/packages/plugin-electron-deliver-minidumps/package.json +++ b/packages/plugin-electron-deliver-minidumps/package.json @@ -19,6 +19,7 @@ "send-minidump.js" ], "dependencies": { + "@bugsnag/json-payload": "^8.4.0", "form-data": "^4.0.4" }, "devDependencies": { @@ -26,7 +27,6 @@ "@bugsnag/electron-network-status": "^8.4.0" }, "peerDependencies": { - "@bugsnag/core": "^8.0.0", "@bugsnag/electron-network-status": "^8.0.0" }, "author": "Bugsnag", diff --git a/packages/plugin-electron-deliver-minidumps/send-minidump.js b/packages/plugin-electron-deliver-minidumps/send-minidump.js index a49e034f29..8f8023a892 100644 --- a/packages/plugin-electron-deliver-minidumps/send-minidump.js +++ b/packages/plugin-electron-deliver-minidumps/send-minidump.js @@ -1,7 +1,7 @@ const { createReadStream } = require('fs') const { createGzip } = require('zlib') const { basename } = require('path') -const payload = require('@bugsnag/core/lib/json-payload') +const jsonPayload = require('@bugsnag/json-payload') const FormData = require('form-data') module.exports = (net, client) => { @@ -56,7 +56,7 @@ module.exports = (net, client) => { payloadVersion: '5', events: [event] } - const eventBody = payload.event(eventPayload, client._config.redactedKeys) + const eventBody = jsonPayload.event(eventPayload, client._config.redactedKeys) formData.append('event', eventBody) } diff --git a/packages/plugin-electron-deliver-minidumps/test/minidump-loop.test.ts b/packages/plugin-electron-deliver-minidumps/test/minidump-loop.test.ts index 680beed554..1e23cfd8ab 100644 --- a/packages/plugin-electron-deliver-minidumps/test/minidump-loop.test.ts +++ b/packages/plugin-electron-deliver-minidumps/test/minidump-loop.test.ts @@ -1,6 +1,5 @@ import EventEmitter from 'events' -import Session from '@bugsnag/core/session' -import Breadcrumb from '@bugsnag/core/breadcrumb' +import { Breadcrumb, Session } from '@bugsnag/core' import NetworkStatus from '@bugsnag/electron-network-status' import MinidumpDeliveryLoop from '../minidump-loop' @@ -47,8 +46,8 @@ describe('electron-minidump-delivery: minidump-loop', () => { await runDeliveryLoop() - expect(sendMinidump).toBeCalledTimes(1) - expect(minidumpQueue.remove).toBeCalledTimes(1) + expect(sendMinidump).toHaveBeenCalledTimes(1) + expect(minidumpQueue.remove).toHaveBeenCalledTimes(1) }) it('sends minidumps with no event', async () => { @@ -63,8 +62,8 @@ describe('electron-minidump-delivery: minidump-loop', () => { await runDeliveryLoop() - expect(sendMinidump).toBeCalledTimes(1) - expect(minidumpQueue.remove).toBeCalledTimes(1) + expect(sendMinidump).toHaveBeenCalledTimes(1) + expect(minidumpQueue.remove).toHaveBeenCalledTimes(1) }) }) @@ -80,8 +79,8 @@ describe('electron-minidump-delivery: minidump-loop', () => { await runDeliveryLoop(2) - expect(sendMinidump).toBeCalledTimes(0) - expect(minidumpQueue.remove).toBeCalledTimes(2) + expect(sendMinidump).toHaveBeenCalledTimes(0) + expect(minidumpQueue.remove).toHaveBeenCalledTimes(2) }) it('allows on send callback to mutate the event', async () => { @@ -122,7 +121,7 @@ describe('electron-minidump-delivery: minidump-loop', () => { await runDeliveryLoop(1) - expect(sendMinidump).toBeCalledWith('minidump-path', { + expect(sendMinidump).toHaveBeenCalledWith('minidump-path', { breadcrumbs: [ { name: 'crumby', @@ -157,7 +156,7 @@ describe('electron-minidump-delivery: minidump-loop', () => { expect(eventMinidumpPath).toBe('minidump-path') - expect(minidumpQueue.remove).toBeCalledWith({ + expect(minidumpQueue.remove).toHaveBeenCalledWith({ minidumpPath: 'minidump-path', eventPath: 'event-path' }) @@ -177,8 +176,8 @@ describe('electron-minidump-delivery: minidump-loop', () => { await runDeliveryLoop(2) - expect(sendMinidump).toBeCalledTimes(0) - expect(minidumpQueue.remove).toBeCalledTimes(2) + expect(sendMinidump).toHaveBeenCalledTimes(0) + expect(minidumpQueue.remove).toHaveBeenCalledTimes(2) // the callbacks are called twice as there are two minidumps and are called // in order of most recently added -> least recently added @@ -209,8 +208,8 @@ describe('electron-minidump-delivery: minidump-loop', () => { await runDeliveryLoop(2) - expect(sendMinidump).toBeCalledTimes(2) - expect(minidumpQueue.remove).toBeCalledTimes(2) + expect(sendMinidump).toHaveBeenCalledTimes(2) + expect(minidumpQueue.remove).toHaveBeenCalledTimes(2) expect(callbacks[3]).toHaveBeenCalledTimes(2) expect(callbacks[2]).toHaveBeenCalledTimes(2) @@ -238,8 +237,8 @@ describe('electron-minidump-delivery: minidump-loop', () => { await runDeliveryLoop(3) - expect(sendMinidump).toBeCalledTimes(2) - expect(minidumpQueue.remove).toBeCalledTimes(2) + expect(sendMinidump).toHaveBeenCalledTimes(2) + expect(minidumpQueue.remove).toHaveBeenCalledTimes(2) expect(jest.getTimerCount()).toBe(0) }) @@ -261,8 +260,8 @@ describe('electron-minidump-delivery: minidump-loop', () => { await runDeliveryLoop(2) - expect(sendMinidump).toBeCalledTimes(2) - expect(minidumpQueue.remove).toBeCalledTimes(1) + expect(sendMinidump).toHaveBeenCalledTimes(2) + expect(minidumpQueue.remove).toHaveBeenCalledTimes(1) }) describe('watchNetworkStatus', () => { @@ -283,14 +282,14 @@ describe('electron-minidump-delivery: minidump-loop', () => { // ensure that nothing is delivered while disconnected await runDeliveryLoop(1) - expect(sendMinidump).toBeCalledTimes(0) + expect(sendMinidump).toHaveBeenCalledTimes(0) // connect the network emitter.emit('MetadataUpdate', { section: 'device', values: { online: true } }, null) // check that we've started delivering minidumps await runDeliveryLoop(1) - expect(sendMinidump).toBeCalledTimes(1) + expect(sendMinidump).toHaveBeenCalledTimes(1) }) it('should stop delivery when disconnected', async () => { @@ -307,14 +306,14 @@ describe('electron-minidump-delivery: minidump-loop', () => { // ensure that the first minidump is delivered await runDeliveryLoop(1) - expect(sendMinidump).toBeCalledTimes(1) + expect(sendMinidump).toHaveBeenCalledTimes(1) // disconnect the network emitter.emit('MetadataUpdate', { section: 'device', values: { online: false } }, null) // check that no more minidumps are delivered await runDeliveryLoop(2) - expect(sendMinidump).toBeCalledTimes(1) + expect(sendMinidump).toHaveBeenCalledTimes(1) }) }) }) diff --git a/packages/plugin-electron-ipc/bugsnag-ipc-main.js b/packages/plugin-electron-ipc/bugsnag-ipc-main.js index 8d8a01a9e9..4064aedd1f 100644 --- a/packages/plugin-electron-ipc/bugsnag-ipc-main.js +++ b/packages/plugin-electron-ipc/bugsnag-ipc-main.js @@ -1,7 +1,4 @@ -const Event = require('@bugsnag/core/event') -const Breadcrumb = require('@bugsnag/core/breadcrumb') -const runCallbacks = require('@bugsnag/core/lib/callback-runner') -const featureFlagDelegate = require('@bugsnag/core/lib/feature-flag-delegate') +const { Breadcrumb, Event, featureFlagDelegate, runCallbacks } = require('@bugsnag/core') module.exports = class BugsnagIpcMain { constructor (client) { diff --git a/packages/plugin-electron-ipc/test/bugsnag-ipc-main.test.ts b/packages/plugin-electron-ipc/test/bugsnag-ipc-main.test.ts index 4abba5aa06..54bf50cb83 100644 --- a/packages/plugin-electron-ipc/test/bugsnag-ipc-main.test.ts +++ b/packages/plugin-electron-ipc/test/bugsnag-ipc-main.test.ts @@ -1,7 +1,6 @@ +import { Schema } from '../../core/dist/types/config' import BugsnagIpcMain from '../bugsnag-ipc-main' -import Client from '@bugsnag/core/client' -import InternalEvent from '@bugsnag/core/event' -import { User, Plugin, Event, FeatureFlag } from '@bugsnag/core' +import { Client, User, Plugin, Event, FeatureFlag } from '@bugsnag/core' const mockClientStateManagerPlugin = { name: 'clientStateManager', @@ -16,17 +15,20 @@ const Notifier = { url: 'https://github.com/bugsnag/bugsnag-js' } +// @ts-expect-error invalid schema expected for testing +const testSchema: Schema = {} + describe('BugsnagIpcMain', () => { describe('constructor()', () => { it('should throw if the state manager plugin is not loaded first', () => { - const client = new Client({ apiKey: '123' }, {}, [], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [], Notifier) expect(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const bugsnagIpcMain = new BugsnagIpcMain(client) }).toThrowError('Expected @bugsnag/plugin-electron-client-state-manager to be loaded first') }) it('should work when the state manager plugin is loaded first', () => { - const client = new Client({ apiKey: '123' }, {}, [mockClientStateManagerPlugin], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [mockClientStateManagerPlugin], Notifier) expect(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const bugsnagIpcMain = new BugsnagIpcMain(client) @@ -36,7 +38,7 @@ describe('BugsnagIpcMain', () => { describe('handle()', () => { it('works for updating context', () => { - const client = new Client({ apiKey: '123' }, {}, [mockClientStateManagerPlugin], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [mockClientStateManagerPlugin], Notifier) client.setContext = jest.fn() const bugsnagIpcMain = new BugsnagIpcMain(client) bugsnagIpcMain.handle({}, 'setContext', JSON.stringify('new context')) @@ -44,7 +46,7 @@ describe('BugsnagIpcMain', () => { }) it('returns the current context', () => { - const client = new Client({ apiKey: '123' }, {}, [mockClientStateManagerPlugin], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [mockClientStateManagerPlugin], Notifier) client.setContext('today') const bugsnagIpcMain = new BugsnagIpcMain(client) const event = { returnValue: undefined } @@ -70,7 +72,7 @@ describe('BugsnagIpcMain', () => { }) it('works for updating user', () => { - const client = new Client({ apiKey: '123' }, {}, [mockClientStateManagerPlugin], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [mockClientStateManagerPlugin], Notifier) client.setUser = jest.fn() const bugsnagIpcMain = new BugsnagIpcMain(client) // all fields set @@ -82,7 +84,7 @@ describe('BugsnagIpcMain', () => { }) it('returns the current user', () => { - const client = new Client({ apiKey: '123' }, {}, [mockClientStateManagerPlugin], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [mockClientStateManagerPlugin], Notifier) client.setUser('81676', undefined, 'Cal') const bugsnagIpcMain = new BugsnagIpcMain(client) const event = { returnValue: undefined } @@ -91,7 +93,7 @@ describe('BugsnagIpcMain', () => { }) it('works for adding metadata', () => { - const client = new Client({ apiKey: '123' }, {}, [mockClientStateManagerPlugin], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [mockClientStateManagerPlugin], Notifier) client.addMetadata = jest.fn() const bugsnagIpcMain = new BugsnagIpcMain(client) const stubWebContents = { /* this would be a WebContents instance */ } @@ -101,7 +103,7 @@ describe('BugsnagIpcMain', () => { }) it('works for removing metadata', () => { - const client = new Client({ apiKey: '123' }, {}, [mockClientStateManagerPlugin], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [mockClientStateManagerPlugin], Notifier) client.clearMetadata = jest.fn() const bugsnagIpcMain = new BugsnagIpcMain(client) bugsnagIpcMain.handle({}, 'clearMetadata', JSON.stringify('section')) @@ -109,7 +111,7 @@ describe('BugsnagIpcMain', () => { }) it('returns metadata content', () => { - const client = new Client({ apiKey: '123' }, {}, [mockClientStateManagerPlugin], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [mockClientStateManagerPlugin], Notifier) client.addMetadata('section', 'content', 'X') const bugsnagIpcMain = new BugsnagIpcMain(client) const event = { returnValue: undefined } @@ -123,7 +125,7 @@ describe('BugsnagIpcMain', () => { }) it('works for adding a single feature flag', () => { - const client = new Client({ apiKey: '123' }, {}, [mockClientStateManagerPlugin], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [mockClientStateManagerPlugin], Notifier) client.addFeatureFlag = jest.fn() const bugsnagIpcMain = new BugsnagIpcMain(client) @@ -135,7 +137,7 @@ describe('BugsnagIpcMain', () => { }) it('works for adding multiple feature flags', () => { - const client = new Client({ apiKey: '123' }, {}, [mockClientStateManagerPlugin], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [mockClientStateManagerPlugin], Notifier) client.addFeatureFlags = jest.fn() const bugsnagIpcMain = new BugsnagIpcMain(client) @@ -155,7 +157,7 @@ describe('BugsnagIpcMain', () => { }) it('works for clearing a single feature flag', () => { - const client = new Client({ apiKey: '123' }, {}, [mockClientStateManagerPlugin], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [mockClientStateManagerPlugin], Notifier) client.clearFeatureFlag = jest.fn() const bugsnagIpcMain = new BugsnagIpcMain(client) @@ -167,7 +169,7 @@ describe('BugsnagIpcMain', () => { }) it('works for clearing all feature flags', () => { - const client = new Client({ apiKey: '123' }, {}, [mockClientStateManagerPlugin], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [mockClientStateManagerPlugin], Notifier) client.clearFeatureFlags = jest.fn() const bugsnagIpcMain = new BugsnagIpcMain(client) @@ -179,7 +181,7 @@ describe('BugsnagIpcMain', () => { }) it('works for managing sessions', () => { - const client = new Client({ apiKey: '123' }, {}, [mockClientStateManagerPlugin], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [mockClientStateManagerPlugin], Notifier) client._sessionDelegate = { startSession: jest.fn(), resumeSession: jest.fn(), pauseSession: jest.fn() } const bugsnagIpcMain = new BugsnagIpcMain(client) // start @@ -194,7 +196,7 @@ describe('BugsnagIpcMain', () => { }) it('works for breadcrumbs', (done) => { - const client = new Client({ apiKey: '123' }, {}, [mockClientStateManagerPlugin], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [mockClientStateManagerPlugin], Notifier) client.addOnBreadcrumb(b => { expect(b.message).toBe('hi IPC') expect(b.type).toBe('manual') @@ -210,7 +212,7 @@ describe('BugsnagIpcMain', () => { }) it('works for bulk updates', done => { - const client = new Client({ apiKey: '123' }, {}, [{ + const client = new Client({ apiKey: '123' }, testSchema, [{ name: 'clientStateManager', load: () => ({ bulkUpdate: ({ context, user, metadata, features }: { context?: string, user?: User, metadata: Record, features: FeatureFlag | null[]}) => { @@ -238,13 +240,13 @@ describe('BugsnagIpcMain', () => { }) it('is resilient to unknown methods', () => { - const client = new Client({ apiKey: '123' }, {}, [mockClientStateManagerPlugin], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [mockClientStateManagerPlugin], Notifier) const bugsnagIpcMain = new BugsnagIpcMain(client) expect(() => bugsnagIpcMain.handle({}, 'explodePlease', JSON.stringify({ data: 123 }))).not.toThrowError() }) it('is resilient to bad JSON', () => { - const client = new Client({ apiKey: '123' }, {}, [mockClientStateManagerPlugin], Notifier) + const client = new Client({ apiKey: '123' }, testSchema, [mockClientStateManagerPlugin], Notifier) const bugsnagIpcMain = new BugsnagIpcMain(client) expect(() => bugsnagIpcMain.handle({}, 'leaveBreadcrumb', 'not json')).not.toThrowError() }) @@ -386,7 +388,7 @@ describe('BugsnagIpcMain', () => { client._setDelivery(client => mockDelivery) const bugsnagIpcMain = new BugsnagIpcMain(client) - const event = new InternalEvent('Error', 'Something bad happened', []) + const event = new Event('Error', 'Something bad happened', []) bugsnagIpcMain.dispatch(Object.assign({}, event)) expect(mockDelivery.sendEvent).toHaveBeenCalledWith(expect.objectContaining({ diff --git a/packages/plugin-electron-ipc/test/bugsnag-ipc-renderer.test.ts b/packages/plugin-electron-ipc/test/bugsnag-ipc-renderer.test.ts index 6cbb8043f2..f1ae7b7a4b 100644 --- a/packages/plugin-electron-ipc/test/bugsnag-ipc-renderer.test.ts +++ b/packages/plugin-electron-ipc/test/bugsnag-ipc-renderer.test.ts @@ -1,6 +1,6 @@ import BugsnagIpcRenderer from '../bugsnag-ipc-renderer' import { CHANNEL_RENDERER_TO_MAIN, CHANNEL_RENDERER_TO_MAIN_SYNC } from '../lib/constants' -import Breadcrumb from '@bugsnag/core/breadcrumb' +import { Breadcrumb } from '@bugsnag/core' import * as electron from 'electron' diff --git a/packages/plugin-electron-net-breadcrumbs/test/net-breadcrumbs.test-main.ts b/packages/plugin-electron-net-breadcrumbs/test/net-breadcrumbs.test-main.ts index 5ea25ac5b5..1c4e71dce2 100644 --- a/packages/plugin-electron-net-breadcrumbs/test/net-breadcrumbs.test-main.ts +++ b/packages/plugin-electron-net-breadcrumbs/test/net-breadcrumbs.test-main.ts @@ -1,4 +1,4 @@ -import Breadcrumb from '@bugsnag/core/breadcrumb' +import { Breadcrumb } from '@bugsnag/core' import { net } from 'electron' import { AddressInfo } from 'net' import { createServer, STATUS_CODES, Server, IncomingMessage, ServerResponse } from 'http' diff --git a/packages/plugin-electron-network-status/test/network-status.test.ts b/packages/plugin-electron-network-status/test/network-status.test.ts index df608c2971..54769f322c 100644 --- a/packages/plugin-electron-network-status/test/network-status.test.ts +++ b/packages/plugin-electron-network-status/test/network-status.test.ts @@ -1,4 +1,4 @@ -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' import plugin from '../' describe('plugin: electron network status', () => { diff --git a/packages/plugin-electron-power-monitor-breadcrumbs/test/power-monitor-breadcrumbs.test.ts b/packages/plugin-electron-power-monitor-breadcrumbs/test/power-monitor-breadcrumbs.test.ts index 73e7adbbd0..6050ac8054 100644 --- a/packages/plugin-electron-power-monitor-breadcrumbs/test/power-monitor-breadcrumbs.test.ts +++ b/packages/plugin-electron-power-monitor-breadcrumbs/test/power-monitor-breadcrumbs.test.ts @@ -1,7 +1,7 @@ import { PowerMonitorEvent } from '@bugsnag/electron-test-helpers/src/PowerMonitor' import { makePowerMonitor, makeClientForPlugin } from '@bugsnag/electron-test-helpers' import plugin from '../' -import Breadcrumb from '@bugsnag/core/breadcrumb' +import { Breadcrumb } from '@bugsnag/core' describe('plugin: electron power monitor breadcrumbs', () => { const events: Array<[PowerMonitorEvent, string]> = [ diff --git a/packages/plugin-electron-preload-error/package.json b/packages/plugin-electron-preload-error/package.json index ddf44e43a5..ef8a2501f0 100644 --- a/packages/plugin-electron-preload-error/package.json +++ b/packages/plugin-electron-preload-error/package.json @@ -15,11 +15,10 @@ "preload-error.js" ], "devDependencies": { - "@bugsnag/core": "^8.4.0", "@bugsnag/electron-test-helpers": "^8.4.0" }, - "peerDependencies": { - "@bugsnag/core": "^8.0.0" + "dependencies": { + "@bugsnag/path-normalizer": "^8.4.0" }, "author": "Bugsnag", "license": "MIT" diff --git a/packages/plugin-electron-preload-error/preload-error.js b/packages/plugin-electron-preload-error/preload-error.js index a39aaf63db..f8027824f8 100644 --- a/packages/plugin-electron-preload-error/preload-error.js +++ b/packages/plugin-electron-preload-error/preload-error.js @@ -1,4 +1,4 @@ -const normalizePath = require('@bugsnag/core/lib/path-normalizer') +const normalizePath = require('@bugsnag/path-normalizer') const handledState = { severity: 'error', diff --git a/packages/plugin-electron-process-info/test/procinfo.test.ts b/packages/plugin-electron-process-info/test/procinfo.test.ts index cca57d3133..27ece93cb3 100644 --- a/packages/plugin-electron-process-info/test/procinfo.test.ts +++ b/packages/plugin-electron-process-info/test/procinfo.test.ts @@ -1,5 +1,4 @@ -import Client, { Delivery } from '@bugsnag/core/client' -import Event from '@bugsnag/core/event' +import { Client, Event, Delivery } from '@bugsnag/core' import plugin from '../' describe('plugin: electron process info', () => { diff --git a/packages/plugin-electron-renderer-client-state-updates/test/client-state-updates.test.ts b/packages/plugin-electron-renderer-client-state-updates/test/client-state-updates.test.ts index f33243ed4c..4b51c6af1b 100644 --- a/packages/plugin-electron-renderer-client-state-updates/test/client-state-updates.test.ts +++ b/packages/plugin-electron-renderer-client-state-updates/test/client-state-updates.test.ts @@ -1,5 +1,5 @@ import clientStateUpdatesPlugin from '../client-state-updates' -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' const Notifier = { name: 'Bugsnag Electron Test', diff --git a/packages/plugin-electron-renderer-event-data/renderer-event-data.js b/packages/plugin-electron-renderer-event-data/renderer-event-data.js index 2ec0ac5398..452f82154a 100644 --- a/packages/plugin-electron-renderer-event-data/renderer-event-data.js +++ b/packages/plugin-electron-renderer-event-data/renderer-event-data.js @@ -1,5 +1,5 @@ const { stripProjectRoot } = require('@bugsnag/plugin-electron-renderer-strip-project-root') -const featureFlagDelegate = require('@bugsnag/core/lib/feature-flag-delegate') +const { featureFlagDelegate } = require('@bugsnag/core') module.exports = (BugsnagIpcRenderer = window.__bugsnag_ipc__) => ({ load: client => { diff --git a/packages/plugin-electron-renderer-event-data/test/renderer-event-data.test.ts b/packages/plugin-electron-renderer-event-data/test/renderer-event-data.test.ts index 17ee8fbec7..201f868410 100644 --- a/packages/plugin-electron-renderer-event-data/test/renderer-event-data.test.ts +++ b/packages/plugin-electron-renderer-event-data/test/renderer-event-data.test.ts @@ -1,5 +1,5 @@ import { makeClientForPlugin } from '@bugsnag/electron-test-helpers' -import Breadcrumb from '@bugsnag/core/breadcrumb' +import { Breadcrumb } from '@bugsnag/core' import plugin from '../' describe('plugin: electron renderer event data', () => { diff --git a/packages/plugin-electron-renderer-strip-project-root/test/strip-project-root.test.ts b/packages/plugin-electron-renderer-strip-project-root/test/strip-project-root.test.ts index 7562da07db..f3fe6a0ff8 100644 --- a/packages/plugin-electron-renderer-strip-project-root/test/strip-project-root.test.ts +++ b/packages/plugin-electron-renderer-strip-project-root/test/strip-project-root.test.ts @@ -1,4 +1,4 @@ -import Event from '@bugsnag/core/event' +import { Event } from '@bugsnag/core' import plugin from '..' import { makeClientForPlugin } from '@bugsnag/electron-test-helpers' diff --git a/packages/plugin-electron-screen-breadcrumbs/test/screen-breadcrumbs.test.ts b/packages/plugin-electron-screen-breadcrumbs/test/screen-breadcrumbs.test.ts index 584c51028e..69fbf58293 100644 --- a/packages/plugin-electron-screen-breadcrumbs/test/screen-breadcrumbs.test.ts +++ b/packages/plugin-electron-screen-breadcrumbs/test/screen-breadcrumbs.test.ts @@ -1,4 +1,4 @@ -import Breadcrumb from '@bugsnag/core/breadcrumb' +import { Breadcrumb } from '@bugsnag/core' import { makeClientForPlugin, makeDisplay, makeScreen } from '@bugsnag/electron-test-helpers' import plugin from '../' diff --git a/packages/plugin-electron-session/session.js b/packages/plugin-electron-session/session.js index f5440fe251..fd1cf000d7 100644 --- a/packages/plugin-electron-session/session.js +++ b/packages/plugin-electron-session/session.js @@ -1,5 +1,5 @@ const sessionDelegate = require('@bugsnag/plugin-browser-session') -const Session = require('@bugsnag/core/session') +const { Session } = require('@bugsnag/core') const SESSION_TIMEOUT_MS = 60 * 1000 diff --git a/packages/plugin-electron-session/test/session.test.ts b/packages/plugin-electron-session/test/session.test.ts index fa7abb8778..7a32d852d4 100644 --- a/packages/plugin-electron-session/test/session.test.ts +++ b/packages/plugin-electron-session/test/session.test.ts @@ -1,6 +1,4 @@ -import Client, { EventDeliveryPayload } from '@bugsnag/core/client' -import { schema as defaultSchema } from '@bugsnag/core/config' -import { SessionPayload } from '@bugsnag/core' +import { Client, EventDeliveryPayload, SessionPayload, schema as defaultSchema } from '@bugsnag/core' import { makeApp, makeBrowserWindow } from '@bugsnag/electron-test-helpers' import plugin from '../' diff --git a/packages/plugin-express/package.json b/packages/plugin-express/package.json index bdad235dd1..747566e850 100644 --- a/packages/plugin-express/package.json +++ b/packages/plugin-express/package.json @@ -19,7 +19,7 @@ "author": "Bugsnag", "license": "MIT", "peerDependencies": { - "@bugsnag/core": "^8.0.0" + "@bugsnag/core": "^8.2.0" }, "devDependencies": { "@bugsnag/core": "^8.4.0", diff --git a/packages/plugin-express/src/express.js b/packages/plugin-express/src/express.js index bbe1ad903e..5050593a41 100644 --- a/packages/plugin-express/src/express.js +++ b/packages/plugin-express/src/express.js @@ -1,5 +1,5 @@ const extractRequestInfo = require('./request-info') -const clone = require('@bugsnag/core/lib/clone-client') +const { cloneClient } = require('@bugsnag/core') const handledState = { severity: 'error', unhandled: true, @@ -14,7 +14,7 @@ module.exports = { load: client => { const requestHandler = (req, res, next) => { // clone the client to be scoped to this request. If sessions are enabled, start one - const requestClient = clone(client) + const requestClient = cloneClient(client) if (requestClient._config.autoTrackSessions) { requestClient.startSession() } diff --git a/packages/plugin-express/src/request-info.js b/packages/plugin-express/src/request-info.js index 4c955728fb..52f7cbdf13 100644 --- a/packages/plugin-express/src/request-info.js +++ b/packages/plugin-express/src/request-info.js @@ -1,4 +1,4 @@ -const extractObject = require('@bugsnag/core/lib/extract-object') +const { extractObject } = require('@bugsnag/core') module.exports = req => { const connection = req.connection diff --git a/packages/plugin-express/test/express.test.ts b/packages/plugin-express/test/express.test.ts index 97c3ab1a62..7584ef0b94 100644 --- a/packages/plugin-express/test/express.test.ts +++ b/packages/plugin-express/test/express.test.ts @@ -1,4 +1,4 @@ -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' import plugin from '../src/express' describe('plugin: express', () => { diff --git a/packages/plugin-inline-script-content/package.json b/packages/plugin-inline-script-content/package.json index 89d4eb634b..14c5e8d170 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.4.0", - "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..6b3b2d514a --- /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'] +}) 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 71% 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..c5341bccb0 100644 --- a/packages/plugin-inline-script-content/inline-script-content.js +++ b/packages/plugin-inline-script-content/src/inline-script-content.ts @@ -1,12 +1,11 @@ -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 { Client } from '@bugsnag/core' const MAX_LINE_LENGTH = 200 const MAX_SCRIPT_LENGTH = 500000 -module.exports = (doc = document, win = window) => ({ - load: (client) => { +export default (doc = document, win = window) => ({ + load: (client: Client) => { + // @ts-expect-error trackInlineScripts is a valid config option if (!client._config.trackInlineScripts) return const originalLocation = win.location.href @@ -14,6 +13,7 @@ module.exports = (doc = document, win = window) => ({ // in IE8-10 the 'interactive' state can fire too soon (before scripts have finished executing), so in those // we wait for the 'complete' state before assuming that synchronous scripts are no longer executing + // @ts-expect-error Property 'attachEvent' does not exist on type 'Document' const isOldIe = !!doc.attachEvent let DOMContentLoaded = isOldIe ? doc.readyState === 'complete' : doc.readyState !== 'loading' const getHtml = () => doc.documentElement.outerHTML @@ -28,11 +28,12 @@ module.exports = (doc = document, win = window) => ({ html = getHtml() DOMContentLoaded = true } - try { prev.apply(this, arguments) } catch (e) {} + // @ts-expect-error Argument of type 'IArguments' is not assignable to parameter of type '[ev: Event]' + try { prev?.apply(this, arguments) } catch (e) {} } - let _lastScript = null - const updateLastScript = script => { + let _lastScript: HTMLOrSVGScriptElement | null = null + const updateLastScript = (script: HTMLOrSVGScriptElement | null) => { _lastScript = script } @@ -45,7 +46,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 @@ -53,7 +54,7 @@ module.exports = (doc = document, win = window) => ({ const zeroBasedLine = lineNumber - 1 const start = Math.max(zeroBasedLine - 3, 0) const end = Math.min(zeroBasedLine + 3, htmlLines.length) - return reduce(htmlLines.slice(start, end), (accum, line, i) => { + return htmlLines.slice(start, end).reduce((accum: Record, line, i) => { accum[start + 1 + i] = line.length <= MAX_LINE_LENGTH ? line : line.substr(0, MAX_LINE_LENGTH) return accum }, {}) @@ -62,12 +63,13 @@ 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))) + // @ts-expect-error Type 'undefined' is not assignable to type 'string' + event.errors[0].stacktrace = event.errors[0].stacktrace.filter(f => !(/__trace__$/.test(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 @@ -91,38 +93,43 @@ module.exports = (doc = document, win = window) => ({ // Proxy all the timer functions whose callback is their 0th argument. // Keep a reference to the original setTimeout because we need it later - const [_setTimeout] = map([ + const [_setTimeout] = [ 'setTimeout', 'setInterval', 'setImmediate', 'requestAnimationFrame' - ], fn => + ].map(fn => __proxy(win, fn, original => - __traceOriginalScript(original, args => ({ + __traceOriginalScript(original, (args: any) => ({ get: () => args[0], - replace: fn => { args[0] = fn } + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + replace: (fn: Function) => { args[0] = fn } })) ) ) // Proxy all the host objects whose prototypes have an addEventListener function - map([ - 'EventTarget', 'Window', 'Node', 'ApplicationCache', 'AudioTrackList', 'ChannelMergerNode', - 'CryptoOperation', 'EventSource', 'FileReader', 'HTMLUnknownElement', 'IDBDatabase', + const eventListeners = ['EventTarget', 'Window', 'Node', 'ApplicationCache', 'AudioTrackList', 'ChannelMergerNode', 'CryptoOperation', 'EventSource', 'FileReader', 'HTMLUnknownElement', 'IDBDatabase', 'IDBRequest', 'IDBTransaction', 'KeyOperation', 'MediaController', 'MessagePort', 'ModalWindow', 'Notification', 'SVGElementInstance', 'Screen', 'TextTrack', 'TextTrackCue', 'TextTrackList', 'WebSocket', 'WebSocketWorker', 'Worker', 'XMLHttpRequest', 'XMLHttpRequestEventTarget', 'XMLHttpRequestUpload' - ], o => { + ] + + eventListeners.map(o => { + // @ts-expect-error Element implicitly has an 'any' type because index expression is not of type 'number' if (!win[o] || !win[o].prototype || !Object.prototype.hasOwnProperty.call(win[o].prototype, 'addEventListener')) return + // @ts-expect-error Element implicitly has an 'any' type because index expression is not of type 'number' __proxy(win[o].prototype, 'addEventListener', original => __traceOriginalScript(original, eventTargetCallbackAccessor) ) + // @ts-expect-error Element implicitly has an 'any' type because index expression is not of type 'number' __proxy(win[o].prototype, 'removeEventListener', original => __traceOriginalScript(original, eventTargetCallbackAccessor, true) ) }) - function __traceOriginalScript (fn, callbackAccessor, alsoCallOriginal = false) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + function __traceOriginalScript (fn: Function, callbackAccessor: Function, alsoCallOriginal = false) { return function () { // this is required for removeEventListener to remove anything added with // addEventListener before the functions started being wrapped by Bugsnag @@ -130,7 +137,9 @@ module.exports = (doc = document, win = window) => ({ try { const cba = callbackAccessor(args) const cb = cba.get() + // @ts-expect-error this' implicitly has type 'any' because it does not have a type annotation. if (alsoCallOriginal) fn.apply(this, args) + // @ts-expect-error this' implicitly has type 'any' because it does not have a type annotation. if (typeof cb !== 'function') return fn.apply(this, args) if (cb.__trace__) { cba.replace(cb.__trace__) @@ -159,6 +168,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 + // @ts-expect-error 'this' implicitly has type 'any' because it does not have a type annotation. if (fn.apply) return fn.apply(this, args) switch (args.length) { case 1: return fn(args[0]) @@ -170,14 +180,15 @@ module.exports = (doc = document, win = window) => ({ }, configSchema: { trackInlineScripts: { - validate: value => value === true || value === false, + validate: (value: unknown) => value === true || value === false, defaultValue: () => true, message: 'should be true|false' } } }) -function __proxy (host, name, replacer) { +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +function __proxy (host: any, name: string, replacer: (fn: Function) => Function) { const original = host[name] if (!original) return original const replacement = replacer(original) @@ -185,13 +196,13 @@ function __proxy (host, name, replacer) { return original } -function eventTargetCallbackAccessor (args) { +function eventTargetCallbackAccessor (args: any) { 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: any) { if (isEventHandlerObj) { args[1].handleEvent = fn } else { 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..af566d5d92 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,7 +1,6 @@ -import plugin from '../inline-script-content' +import plugin from '../src/inline-script-content' -import Client from '@bugsnag/core/client' -import Event from '@bugsnag/core/event' +import { Client, Event, EventDeliveryPayload } from '@bugsnag/core' describe('plugin: inline script content', () => { it('should add an onError callback which captures the HTML content if file=current url', () => { @@ -27,7 +26,7 @@ Lorem ipsum dolor sit amet. const window = { location: { href: 'https://app.bugsnag.com/errors' } } as unknown as Window &typeof globalThis const client = new Client({ apiKey: 'API_KEY_YEAH' }, undefined, [plugin(document, window)]) - const payloads = [] + const payloads: EventDeliveryPayload[] = [] expect(client._cbs.e.length).toBe(1) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload), sendSession: () => {} })) @@ -48,7 +47,7 @@ Lorem ipsum dolor sit amet. // check it installed a new onreadystatechange handler expect(document.onreadystatechange === prevHandler).toBe(false) // now check it calls the previous one - document.onreadystatechange({} as unknown as globalThis.Event) + document.onreadystatechange?.({} as unknown as globalThis.Event) expect(client).toBe(client) }) @@ -91,7 +90,7 @@ Lorem ipsum dolor sit amet. const window = { location: { href: 'https://app.bugsnag.com/errors' } } as unknown as Window &typeof globalThis const client = new Client({ apiKey: 'API_KEY_YEAH' }, undefined, [plugin(document, window)]) - const payloads = [] + const payloads: EventDeliveryPayload[] = [] expect(client._cbs.e.length).toBe(1) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload), sendSession: () => {} })) @@ -127,7 +126,7 @@ Lorem ipsum dolor sit amet. const window = { location: { href: 'https://app.bugsnag.com/errors' } } as unknown as Window &typeof globalThis const client = new Client({ apiKey: 'API_KEY_YEAH' }, undefined, [plugin(document, window)]) - const payloads = [] + const payloads: EventDeliveryPayload[] = [] expect(client._cbs.e.length).toBe(1) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload), sendSession: () => {} })) @@ -136,7 +135,7 @@ Lorem ipsum dolor sit amet. ])) expect(payloads.length).toEqual(1) expect(payloads[0].events[0].errors[0].stacktrace[0].code).toBeDefined() - const surroundingCode = payloads[0].events[0].errors[0].stacktrace[0].code + const surroundingCode = payloads[0].events[0].errors[0].stacktrace[0].code as Record Object.keys(surroundingCode).forEach(line => { expect(surroundingCode[line].length > 200).toBe(false) }) @@ -162,7 +161,7 @@ Lorem ipsum dolor sit amet. const window = { location: { href: 'https://app.bugsnag.com/errors' } } as unknown as Window &typeof globalThis const client = new Client({ apiKey: 'API_KEY_YEAH' }, undefined, [plugin(document, window)]) - const payloads = [] + const payloads: EventDeliveryPayload[] = [] expect(client._cbs.e.length).toBe(1) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload), sendSession: () => {} })) @@ -225,7 +224,7 @@ Lorem ipsum dolor sit amet. const window = { location: { href: 'https://app.bugsnag.com/errors' } } as unknown as Window &typeof globalThis const client = new Client({ apiKey: 'API_KEY_YEAH' }, undefined, [plugin(document, window)]) - const payloads = [] + const payloads: EventDeliveryPayload[] = [] expect(client._cbs.e.length).toBe(1) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload), sendSession: () => {} })) @@ -254,7 +253,7 @@ Lorem ipsum dolor sit amet. const window = { location: { href: 'https://app.bugsnag.com/errors' } } as unknown as Window &typeof globalThis const client = new Client({ apiKey: 'API_KEY_YEAH' }, undefined, [plugin(document, window)]) - const payloads = [] + const payloads: EventDeliveryPayload[] = [] expect(client._cbs.e.length).toBe(1) client._setDelivery(client => ({ sendEvent: (payload) => payloads.push(payload), sendSession: () => {} })) 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 82ee99e7bc..4e842012c5 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.4.0", - "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..9f1f9fb865 --- /dev/null +++ b/packages/plugin-interaction-breadcrumbs/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from '../../.rollup/index.mjs' + +export default createRollupConfig({ + input: 'src/interaction-breadcrumbs.ts', + external: [/node_modules/] +}) diff --git a/packages/plugin-interaction-breadcrumbs/interaction-breadcrumbs.js b/packages/plugin-interaction-breadcrumbs/src/interaction-breadcrumbs.ts similarity index 89% rename from packages/plugin-interaction-breadcrumbs/interaction-breadcrumbs.js rename to packages/plugin-interaction-breadcrumbs/src/interaction-breadcrumbs.ts index 86c7b03086..4a11993e37 100644 --- a/packages/plugin-interaction-breadcrumbs/interaction-breadcrumbs.js +++ b/packages/plugin-interaction-breadcrumbs/src/interaction-breadcrumbs.ts @@ -1,7 +1,9 @@ +import { Plugin } from '@bugsnag/core' + /* * 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 @@ -13,7 +15,7 @@ module.exports = (win = window) => ({ targetSelector = getNodeSelector(event.target, win) } catch (e) { targetText = '[hidden]' - targetSelector = '[hidden]' + targetSelector = '[hidden]'; client._logger.error('Cross domain error when tracking click event. See docs: https://tinyurl.com/yy3rn63z') } client.leaveBreadcrumb('UI click', { targetText, targetSelector }, 'user') @@ -23,7 +25,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 +43,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..5b3965340e 100644 --- a/packages/plugin-interaction-breadcrumbs/test/interaction-breadcrumbs.test.ts +++ b/packages/plugin-interaction-breadcrumbs/test/interaction-breadcrumbs.test.ts @@ -1,7 +1,6 @@ -import plugin from '../' +import plugin from '../src/interaction-breadcrumbs' -import Client from '@bugsnag/core/client' -import Breadcrumb from '@bugsnag/core/breadcrumb' +import { Breadcrumb, Client } from '@bugsnag/core' const lotsOfWhitespace = ' '.repeat(100000) const lotsOfText = 'a'.repeat(100000) @@ -54,12 +53,10 @@ describe('plugin: interaction breadcrumbs', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion document.querySelector('button')!.click() - // TODO: targetSelector should be 'BUTTON.button' but for some reason seems to be ' > HTML:nth-child(2) > BODY:nth-child(2) > DIV > BUTTON.button' - // SEE PLAT-12831 expect(c._breadcrumbs).toStrictEqual([ new Breadcrumb( 'UI click', - { targetText: 'Click me', targetSelector: expect.stringContaining('BUTTON.button') }, + { targetText: 'Click me', targetSelector: 'BUTTON.button' }, 'user', expect.any(Date) ) 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-intercept/intercept.js b/packages/plugin-intercept/intercept.js index af8bf592ab..3ff68ce004 100644 --- a/packages/plugin-intercept/intercept.js +++ b/packages/plugin-intercept/intercept.js @@ -1,4 +1,4 @@ -const { getStack, maybeUseFallbackStack } = require('@bugsnag/core/lib/node-fallback-stack') +const { nodeFallbackStack } = require('@bugsnag/core') module.exports = { name: 'intercept', @@ -10,12 +10,12 @@ module.exports = { } // capture a stacktrace in case a resulting error has nothing - const fallbackStack = getStack() + const fallbackStack = nodeFallbackStack.getStack() return (err, ...data) => { if (err) { // check if the stacktrace has no context, if so, if so append the frames we created earlier - if (err.stack) maybeUseFallbackStack(err, fallbackStack) + if (err.stack) nodeFallbackStack.maybeUseFallbackStack(err, fallbackStack) const event = client.Event.create(err, true, { severity: 'warning', unhandled: false, @@ -24,7 +24,7 @@ module.exports = { client._notify(event, onError) return } - cb(...data) // eslint-disable-line + cb(...data) } } diff --git a/packages/plugin-intercept/test/intercept.test.ts b/packages/plugin-intercept/test/intercept.test.ts index cb6f73c710..986782f623 100644 --- a/packages/plugin-intercept/test/intercept.test.ts +++ b/packages/plugin-intercept/test/intercept.test.ts @@ -1,4 +1,4 @@ -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' import plugin from '../' import fs from 'fs' diff --git a/packages/plugin-internal-callback-marker/test/internal-callback-marker.test.ts b/packages/plugin-internal-callback-marker/test/internal-callback-marker.test.ts index a74e707006..6d774ced63 100644 --- a/packages/plugin-internal-callback-marker/test/internal-callback-marker.test.ts +++ b/packages/plugin-internal-callback-marker/test/internal-callback-marker.test.ts @@ -1,5 +1,4 @@ import { FirstPlugin, LastPlugin } from '../internal-callback-marker' -import InternalClient from '@bugsnag/core/client' import { Plugin, Client } from '@bugsnag/core' describe('@bugsnag/plugin-internal-callback-marker', () => { @@ -30,7 +29,7 @@ describe('@bugsnag/plugin-internal-callback-marker', () => { } ] - const client = new InternalClient({ + const client = new Client({ apiKey: '123', onError: externalOnErrorViaConfig, plugins: externalPlugins diff --git a/packages/plugin-koa/package.json b/packages/plugin-koa/package.json index 6cb16b7427..5f033053fe 100644 --- a/packages/plugin-koa/package.json +++ b/packages/plugin-koa/package.json @@ -19,7 +19,7 @@ "author": "Bugsnag", "license": "MIT", "peerDependencies": { - "@bugsnag/core": "^8.0.0" + "@bugsnag/core": "^8.2.0" }, "devDependencies": { "@bugsnag/core": "^8.4.0", diff --git a/packages/plugin-koa/src/koa.js b/packages/plugin-koa/src/koa.js index 84aff13a3a..1a2d0733e9 100644 --- a/packages/plugin-koa/src/koa.js +++ b/packages/plugin-koa/src/koa.js @@ -1,4 +1,4 @@ -const clone = require('@bugsnag/core/lib/clone-client') +const { cloneClient } = require('@bugsnag/core') const extractRequestInfo = require('./request-info') const handledState = { @@ -15,7 +15,7 @@ module.exports = { load: client => { const requestHandler = async (ctx, next) => { // clone the client to be scoped to this request. If sessions are enabled, start one - const requestClient = clone(client) + const requestClient = cloneClient(client) if (requestClient._config.autoTrackSessions) { requestClient.startSession() } @@ -38,7 +38,7 @@ module.exports = { requestHandler.v1 = function * (next) { // clone the client to be scoped to this request. If sessions are enabled, start one - const requestClient = clone(client) + const requestClient = cloneClient(client) if (requestClient._config.autoTrackSessions) { requestClient.startSession() } diff --git a/packages/plugin-koa/test/koa.test.ts b/packages/plugin-koa/test/koa.test.ts index a4b1e1fe6c..e75e040ded 100644 --- a/packages/plugin-koa/test/koa.test.ts +++ b/packages/plugin-koa/test/koa.test.ts @@ -1,7 +1,5 @@ -import Client from '@bugsnag/core/client' import plugin from '../src/koa' -import { EventPayload } from '@bugsnag/core' -import Event from '@bugsnag/core/event' +import { Client, Event } from '@bugsnag/core' const noop = () => {} const id = (a: T) => a @@ -43,6 +41,7 @@ describe('plugin: koa', () => { client._sessionDelegate = { startSession, pauseSession, resumeSession } client._logger = logger() + // @ts-expect-error client._clientContext = { run: jest.fn() } const middleware = client.getPlugin('koa') @@ -63,6 +62,7 @@ describe('plugin: koa', () => { expect(resumeSession).not.toHaveBeenCalled() expect(context.bugsnag).toStrictEqual(expect.any(Client)) expect(context.bugsnag).not.toBe(client) + // @ts-expect-error expect(client._clientContext.run).toHaveBeenCalledWith(expect.any(Client), next) }) @@ -71,7 +71,7 @@ describe('plugin: koa', () => { client._sessionDelegate = { startSession: id, pauseSession: noop, resumeSession: id } client._logger = logger() client._setDelivery(() => ({ - sendEvent (payload: EventPayload, cb: (err: Error|null, obj: unknown) => void) { + sendEvent (payload, cb: (err: Error|null, obj: unknown) => void) { expect(payload.events).toHaveLength(1) cb(null, payload.events[0]) }, @@ -106,12 +106,14 @@ describe('plugin: koa', () => { }, ip: '1.2.3.4' } as any + // @ts-expect-error client._clientContext = { run: jest.fn() } const next = jest.fn() await middleware.requestHandler(context, next) + // @ts-expect-error expect(client._clientContext.run).toHaveBeenCalledWith(expect.any(Client), next) const event: Event = await new Promise(resolve => { @@ -157,12 +159,13 @@ describe('plugin: koa', () => { client._sessionDelegate = { startSession: id, pauseSession: noop, resumeSession: id } client._logger = logger() client._setDelivery(() => ({ - sendEvent (payload: EventPayload, cb: (err: Error|null, obj: unknown) => void) { + sendEvent (payload, cb: (err: Error|null, obj: unknown) => void) { expect(payload.events).toHaveLength(1) cb(null, payload.events[0]) }, sendSession: noop })) + // @ts-expect-error client._clientContext = { run: jest.fn() } const middleware = client.getPlugin('koa') @@ -181,6 +184,7 @@ describe('plugin: koa', () => { await middleware.requestHandler(context, next) + // @ts-expect-error expect(client._clientContext.run).toHaveBeenCalledWith(expect.any(Client), next) const event: Event = await new Promise(resolve => { @@ -222,6 +226,7 @@ describe('plugin: koa', () => { client._sessionDelegate = { startSession, pauseSession, resumeSession } client._logger = logger() + // @ts-expect-error client._clientContext = { run: jest.fn() } const middleware = client.getPlugin('koa') @@ -240,6 +245,7 @@ describe('plugin: koa', () => { expect(startSession).not.toHaveBeenCalled() expect(pauseSession).not.toHaveBeenCalled() expect(resumeSession).not.toHaveBeenCalled() + // @ts-expect-error expect(client._clientContext.run).toHaveBeenCalledWith(expect.any(Client), next) // the Client should be cloned to ensure any manually started sessions @@ -267,7 +273,7 @@ describe('plugin: koa', () => { const events: Event[] = [] client2._setDelivery(() => ({ - sendEvent (payload: EventPayload, cb: (err: Error|null, obj: unknown) => void) { + sendEvent (payload, cb: (err: Error|null, obj: unknown) => void) { expect(payload.events).toHaveLength(1) events.push(payload.events[0] as Event) }, @@ -303,7 +309,7 @@ describe('plugin: koa', () => { const events: Event[] = [] client._setDelivery(() => ({ - sendEvent (payload: EventPayload, cb: (err: Error|null, obj: unknown) => void) { + sendEvent (payload, cb: (err: Error|null, obj: unknown) => void) { expect(payload.events).toHaveLength(1) events.push(payload.events[0] as Event) }, diff --git a/packages/plugin-navigation-breadcrumbs/package.json b/packages/plugin-navigation-breadcrumbs/package.json index 813994339c..8f02d5dc63 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.4.0", - "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..82311d811d --- /dev/null +++ b/packages/plugin-navigation-breadcrumbs/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from '../../.rollup/index.mjs' + +export default createRollupConfig({ + input: 'src/navigation-breadcrumbs.ts', + external: [/node_modules/] +}) diff --git a/packages/plugin-navigation-breadcrumbs/navigation-breadcrumbs.js b/packages/plugin-navigation-breadcrumbs/src/navigation-breadcrumbs.ts similarity index 58% rename from packages/plugin-navigation-breadcrumbs/navigation-breadcrumbs.js rename to packages/plugin-navigation-breadcrumbs/src/navigation-breadcrumbs.ts index 873215dd83..b3b1f918c0 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 '@bugsnag/core' + +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,41 +58,43 @@ 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') // if throttle plugin is in use, reset the event sent count + // @ts-expect-error needs better typing to handle additional fields on the browser client 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..17090afeaa 100644 --- a/packages/plugin-navigation-breadcrumbs/test/navigation-breadcrumbs.test.ts +++ b/packages/plugin-navigation-breadcrumbs/test/navigation-breadcrumbs.test.ts @@ -1,6 +1,6 @@ -import plugin from '../navigation-breadcrumbs' +import plugin from '../src/navigation-breadcrumbs' -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' const noop = () => {} const id = (a: T) => a 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-network-breadcrumbs/package.json b/packages/plugin-network-breadcrumbs/package.json index 3650888fd7..026524a00f 100644 --- a/packages/plugin-network-breadcrumbs/package.json +++ b/packages/plugin-network-breadcrumbs/package.json @@ -1,7 +1,15 @@ { "name": "@bugsnag/plugin-network-breadcrumbs", "version": "8.4.0", - "main": "network-breadcrumbs.js", + "main": "dist/network-breadcrumbs.js", + "types": "dist/types/network-breadcrumbs.d.ts", + "exports": { + ".": { + "types": "./dist/types/network-breadcrumbs.d.ts", + "default": "./dist/network-breadcrumbs.js", + "import": "./dist/network-breadcrumbs.mjs" + } + }, "description": "@bugsnag/js plugin to record browser requests as breadcrumbs", "homepage": "https://www.bugsnag.com/", "repository": { @@ -12,7 +20,7 @@ "access": "public" }, "files": [ - "*.js" + "dist" ], "author": "Bugsnag", "license": "MIT", @@ -21,5 +29,10 @@ }, "peerDependencies": { "@bugsnag/core": "^8.0.0" + }, + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" } } diff --git a/packages/plugin-network-breadcrumbs/rollup.config.npm.mjs b/packages/plugin-network-breadcrumbs/rollup.config.npm.mjs new file mode 100644 index 0000000000..8f53a1d50e --- /dev/null +++ b/packages/plugin-network-breadcrumbs/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from "../../.rollup/index.mjs"; + +export default createRollupConfig({ + input: "src/network-breadcrumbs.ts", + external: [/node_modules/], +}); \ No newline at end of file diff --git a/packages/plugin-network-breadcrumbs/network-breadcrumbs.js b/packages/plugin-network-breadcrumbs/src/network-breadcrumbs.ts similarity index 61% rename from packages/plugin-network-breadcrumbs/network-breadcrumbs.js rename to packages/plugin-network-breadcrumbs/src/network-breadcrumbs.ts index 86997b9caa..0f2c1e1636 100644 --- a/packages/plugin-network-breadcrumbs/network-breadcrumbs.js +++ b/packages/plugin-network-breadcrumbs/src/network-breadcrumbs.ts @@ -1,19 +1,35 @@ +import { Client, Config, Logger, Plugin } from '@bugsnag/core' + const BREADCRUMB_TYPE = 'request' -const includes = require('@bugsnag/core/lib/es-utils/includes') +interface GlobalWithFetchAndXHR { + fetch: typeof fetch + XMLHttpRequest: typeof XMLHttpRequest + WeakMap: typeof WeakMap +} + +interface InternalClient extends Client { + _logger: Logger + _config: Required + _isBreadcrumbTypeEnabled: (type: string) => boolean +} + +type FetchArguments = Parameters /* * Leaves breadcrumbs when network requests occur */ -module.exports = (_ignoredUrls = [], win = window) => { - let restoreFunctions = [] - const plugin = { +export default (_ignoredUrls = [], win: GlobalWithFetchAndXHR = window): Plugin => { + let restoreFunctions: Array<() => void> = [] + const plugin: Plugin = { load: client => { - if (!client._isBreadcrumbTypeEnabled('request')) return + const internalClient = client as InternalClient + + if (!internalClient._isBreadcrumbTypeEnabled('request')) return const ignoredUrls = [ - client._config.endpoints.notify, - client._config.endpoints.sessions + internalClient._config.endpoints.notify, + internalClient._config.endpoints.sessions ].concat(_ignoredUrls) monkeyPatchXMLHttpRequest() @@ -27,12 +43,12 @@ module.exports = (_ignoredUrls = [], win = window) => { const requestHandlers = new WeakMap() const originalOpen = win.XMLHttpRequest.prototype.open - win.XMLHttpRequest.prototype.open = function open (method, url) { + win.XMLHttpRequest.prototype.open = function open (method: string, url: string | URL) { // it's possible for `this` to be `undefined`, which is not a valid key for a WeakMap if (this) { trackedRequests.set(this, { method, url }) } - originalOpen.apply(this, arguments) + originalOpen.apply(this, arguments as unknown as Parameters) } const originalSend = win.XMLHttpRequest.prototype.send @@ -59,7 +75,7 @@ module.exports = (_ignoredUrls = [], win = window) => { } } - originalSend.apply(this, arguments) + originalSend.apply(this, arguments as unknown as Parameters) } if (process.env.NODE_ENV !== 'production') { @@ -70,15 +86,15 @@ module.exports = (_ignoredUrls = [], win = window) => { } } - function handleXHRLoad (method, url, status, duration) { + function handleXHRLoad (method: string, url: string, status: number, duration: number) { if (url === undefined) { - client._logger.warn('The request URL is no longer present on this XMLHttpRequest. A breadcrumb cannot be left for this request.') + internalClient._logger.warn('The request URL is no longer present on this XMLHttpRequest. A breadcrumb cannot be left for this request.') return } // an XMLHttpRequest's URL can be an object as long as its 'toString' // returns a URL, e.g. a HTMLAnchorElement - if (typeof url === 'string' && includes(ignoredUrls, url.replace(/\?.*$/, ''))) { + if (typeof url === 'string' && ignoredUrls.indexOf(url.replace(/\?.*$/, '')) !== -1) { // don't leave a network breadcrumb from bugsnag notify calls return } @@ -90,25 +106,25 @@ module.exports = (_ignoredUrls = [], win = window) => { } if (status >= 400) { // contacted server but got an error response - client.leaveBreadcrumb('XMLHttpRequest failed', metadata, BREADCRUMB_TYPE) + internalClient.leaveBreadcrumb('XMLHttpRequest failed', metadata, BREADCRUMB_TYPE) } else { - client.leaveBreadcrumb('XMLHttpRequest succeeded', metadata, BREADCRUMB_TYPE) + internalClient.leaveBreadcrumb('XMLHttpRequest succeeded', metadata, BREADCRUMB_TYPE) } } - function handleXHRError (method, url, duration) { + function handleXHRError (method: string, url: string, duration: number) { if (url === undefined) { - client._logger.warn('The request URL is no longer present on this XMLHttpRequest. A breadcrumb cannot be left for this request.') + internalClient._logger.warn('The request URL is no longer present on this XMLHttpRequest. A breadcrumb cannot be left for this request.') return } - if (typeof url === 'string' && includes(ignoredUrls, url.replace(/\?.*$/, ''))) { + if (typeof url === 'string' && ignoredUrls.indexOf(url.replace(/\?.*$/, '')) !== -1) { // don't leave a network breadcrumb from bugsnag notify calls return } // failed to contact server - client.leaveBreadcrumb('XMLHttpRequest error', { + internalClient.leaveBreadcrumb('XMLHttpRequest error', { method: String(method), url: String(url), duration: duration @@ -120,17 +136,18 @@ module.exports = (_ignoredUrls = [], win = window) => { // only patch it if it exists and if it is not a polyfill (patching a polyfilled // fetch() results in duplicate breadcrumbs for the same request because the // implementation uses XMLHttpRequest which is also patched) + // @ts-expect-error polyfill is not defined in the Fetch API if (!('fetch' in win) || win.fetch.polyfill) return const oldFetch = win.fetch - win.fetch = function fetch () { - const urlOrRequest = arguments[0] - const options = arguments[1] + win.fetch = function fetch (...args: FetchArguments) { + const urlOrRequest = args[0] + const options = args[1] - let method - let url = null + let method: string | undefined + let url: string | URL | null = null - if (urlOrRequest && typeof urlOrRequest === 'object') { + if (urlOrRequest && typeof urlOrRequest === 'object' && 'url' in urlOrRequest) { url = urlOrRequest.url if (options && 'method' in options) { method = options.method @@ -152,13 +169,13 @@ module.exports = (_ignoredUrls = [], win = window) => { const requestStart = new Date() // pass through to native fetch - oldFetch(...arguments) + oldFetch(...args) .then(response => { - handleFetchSuccess(response, method, url, getDuration(requestStart)) + handleFetchSuccess(response, String(method), String(url), getDuration(requestStart)) resolve(response) }) .catch(error => { - handleFetchError(method, url, getDuration(requestStart)) + handleFetchError(String(method), String(url), getDuration(requestStart)) reject(error) }) }) @@ -171,23 +188,23 @@ module.exports = (_ignoredUrls = [], win = window) => { } } - const handleFetchSuccess = (response, method, url, duration) => { + const handleFetchSuccess = (response: Response, method: string, url: string, duration: number) => { const metadata = { - method: String(method), + method, status: response.status, - url: String(url), + url, duration: duration } if (response.status >= 400) { // when the request comes back with a 4xx or 5xx status it does not reject the fetch promise, - client.leaveBreadcrumb('fetch() failed', metadata, BREADCRUMB_TYPE) + internalClient.leaveBreadcrumb('fetch() failed', metadata, BREADCRUMB_TYPE) } else { - client.leaveBreadcrumb('fetch() succeeded', metadata, BREADCRUMB_TYPE) + internalClient.leaveBreadcrumb('fetch() succeeded', metadata, BREADCRUMB_TYPE) } } - const handleFetchError = (method, url, duration) => { - client.leaveBreadcrumb('fetch() error', { method: String(method), url: String(url), duration: duration }, BREADCRUMB_TYPE) + const handleFetchError = (method: string, url: string, duration: number) => { + internalClient.leaveBreadcrumb('fetch() error', { method, url, duration: duration }, BREADCRUMB_TYPE) } } } @@ -202,4 +219,4 @@ module.exports = (_ignoredUrls = [], win = window) => { return plugin } -const getDuration = (startTime) => startTime && new Date() - startTime +const getDuration = (startTime: Date): number => startTime && Number(new Date()) - Number(startTime) diff --git a/packages/plugin-network-breadcrumbs/test/network-breadcrumbs.test.ts b/packages/plugin-network-breadcrumbs/test/network-breadcrumbs.test.ts index 9cdda0fc87..f76fe1f071 100644 --- a/packages/plugin-network-breadcrumbs/test/network-breadcrumbs.test.ts +++ b/packages/plugin-network-breadcrumbs/test/network-breadcrumbs.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import plugin from '../' -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' import { Config } from '@bugsnag/core' class XMLHttpRequest { diff --git a/packages/plugin-network-breadcrumbs/tsconfig.json b/packages/plugin-network-breadcrumbs/tsconfig.json new file mode 100644 index 0000000000..7346c3eb52 --- /dev/null +++ b/packages/plugin-network-breadcrumbs/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "types": ["node"] + } +} diff --git a/packages/plugin-node-device/test/device.test.ts b/packages/plugin-node-device/test/device.test.ts index 9b47bcf14b..e3a7a939c8 100644 --- a/packages/plugin-node-device/test/device.test.ts +++ b/packages/plugin-node-device/test/device.test.ts @@ -1,8 +1,8 @@ import plugin from '../device' -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' const schema = { - ...require('@bugsnag/core/config').schema, + ...require('@bugsnag/core').schema, hostname: { defaultValue: () => 'test-machine.local', validate: () => true, diff --git a/packages/plugin-node-in-project/in-project.js b/packages/plugin-node-in-project/in-project.js index d9012bde7a..f056ea5b2f 100644 --- a/packages/plugin-node-in-project/in-project.js +++ b/packages/plugin-node-in-project/in-project.js @@ -1,4 +1,4 @@ -const normalizePath = require('@bugsnag/core/lib/path-normalizer') +const normalizePath = require('@bugsnag/path-normalizer') module.exports = { load: client => client.addOnError(event => { diff --git a/packages/plugin-node-in-project/package.json b/packages/plugin-node-in-project/package.json index 865a7af0fe..717c9de9a9 100644 --- a/packages/plugin-node-in-project/package.json +++ b/packages/plugin-node-in-project/package.json @@ -17,10 +17,7 @@ "scripts": {}, "author": "Bugsnag", "license": "MIT", - "devDependencies": { - "@bugsnag/core": "^8.4.0" - }, - "peerDependencies": { - "@bugsnag/core": "^8.0.0" + "dependencies": { + "@bugsnag/path-normalizer": "^8.4.0" } } diff --git a/packages/plugin-node-in-project/test/in-project.test.ts b/packages/plugin-node-in-project/test/in-project.test.ts index 15a32532d3..a4e38d0ea6 100644 --- a/packages/plugin-node-in-project/test/in-project.test.ts +++ b/packages/plugin-node-in-project/test/in-project.test.ts @@ -1,13 +1,12 @@ import plugin from '../' import { join } from 'path' -import Event from '@bugsnag/core/event' -import Client from '@bugsnag/core/client' -import { schema } from '@bugsnag/core/config' +import { Client, Event, schema } from '@bugsnag/core' describe('plugin: node in project', () => { it('should mark stackframes as "inProject" if it is a descendent of the "projectRoot"', done => { const client = new Client({ apiKey: 'api_key', projectRoot: '/app', plugins: [plugin] }, { ...schema, + // @ts-expect-error projectRoot: { validate: () => true, defaultValue: () => '', @@ -46,6 +45,7 @@ describe('plugin: node in project', () => { it('should mark stackframes as "out of project" if it is not a descendent of "projectRoot"', done => { const client = new Client({ apiKey: 'api_key', projectRoot: '/app', plugins: [plugin] }, { ...schema, + // @ts-expect-error projectRoot: { validate: () => true, defaultValue: () => '', @@ -84,6 +84,7 @@ describe('plugin: node in project', () => { it('should work with node_modules and node internals', done => { const client = new Client({ apiKey: 'api_key', projectRoot: '/app', plugins: [plugin] }, { ...schema, + // @ts-expect-error projectRoot: { validate: () => true, defaultValue: () => '', @@ -117,6 +118,7 @@ describe('plugin: node in project', () => { it('should tolerate stackframe.file not being a string', done => { const client = new Client({ apiKey: 'api_key', projectRoot: '/app', plugins: [plugin] }, { ...schema, + // @ts-expect-error projectRoot: { validate: () => true, defaultValue: () => '', diff --git a/packages/plugin-node-surrounding-code/test/surrounding-code.test.ts b/packages/plugin-node-surrounding-code/test/surrounding-code.test.ts index eccea05a40..352f37dd81 100644 --- a/packages/plugin-node-surrounding-code/test/surrounding-code.test.ts +++ b/packages/plugin-node-surrounding-code/test/surrounding-code.test.ts @@ -2,9 +2,7 @@ import fs from 'fs' import plugin from '../' import { join } from 'path' -import Event from '@bugsnag/core/event' -import Client from '@bugsnag/core/client' -import { schema as defaultSchema } from '@bugsnag/core/config' +import { Client, Event, schema as defaultSchema } from '@bugsnag/core' let createReadStreamCount = 0 const originalReadStream = fs.createReadStream @@ -62,6 +60,7 @@ describe('plugin: node surrounding code', () => { { apiKey: 'api_key', projectRoot: __dirname }, { ...defaultSchema, + // @ts-expect-error projectRoot: { defaultValue: () => null, validate: () => true, diff --git a/packages/plugin-node-uncaught-exception/test/uncaught-exception.test.ts b/packages/plugin-node-uncaught-exception/test/uncaught-exception.test.ts index 9ba0bf36b6..6b03da0022 100644 --- a/packages/plugin-node-uncaught-exception/test/uncaught-exception.test.ts +++ b/packages/plugin-node-uncaught-exception/test/uncaught-exception.test.ts @@ -1,7 +1,5 @@ -import Client from '@bugsnag/core/client' -import { schema } from '@bugsnag/core/config' +import { Client, Event, schema } from '@bugsnag/core' import plugin from '../' -import EventWithInternals from '@bugsnag/core/event' describe('plugin: node uncaught exception handler', () => { it('should listen to the process#uncaughtException event', () => { @@ -36,7 +34,7 @@ describe('plugin: node uncaught exception handler', () => { it('should call the configured onUncaughtException callback', done => { const c = new Client({ apiKey: 'api_key', - onUncaughtException: (err: Error, event: EventWithInternals) => { + onUncaughtException: (err: Error, event: Event) => { expect(err.message).toBe('never gonna catch me') expect(event.errors[0].errorMessage).toBe('never gonna catch me') expect(event._handledState.unhandled).toBe(true) @@ -48,6 +46,7 @@ describe('plugin: node uncaught exception handler', () => { plugins: [plugin] }, { ...schema, + // @ts-expect-error onUncaughtException: { validate: (val: unknown) => typeof val === 'function', message: 'should be a function', @@ -58,13 +57,13 @@ describe('plugin: node uncaught exception handler', () => { sendEvent: (payload, cb) => cb(), sendSession: (payload, cb) => cb() })) - process.listeners('uncaughtException')[0](new Error('never gonna catch me')) + process.listeners('uncaughtException')[0](new Error('never gonna catch me'), 'uncaughtException') }) it('should tolerate delivery errors', done => { const c = new Client({ apiKey: 'api_key', - onUncaughtException: (err: Error, event: EventWithInternals) => { + onUncaughtException: (err: Error, event: Event) => { expect(err.message).toBe('never gonna catch me') expect(event.errors[0].errorMessage).toBe('never gonna catch me') expect(event._handledState.unhandled).toBe(true) @@ -76,6 +75,7 @@ describe('plugin: node uncaught exception handler', () => { plugins: [plugin] }, { ...schema, + // @ts-expect-error onUncaughtException: { validate: (val: unknown) => typeof val === 'function', message: 'should be a function', @@ -86,6 +86,6 @@ describe('plugin: node uncaught exception handler', () => { sendEvent: (payload, cb) => cb(new Error('failed')), sendSession: (payload, cb) => cb() })) - process.listeners('uncaughtException')[0](new Error('never gonna catch me')) + process.listeners('uncaughtException')[0](new Error('never gonna catch me'), 'uncaughtException') }) }) diff --git a/packages/plugin-node-uncaught-exception/uncaught-exception.js b/packages/plugin-node-uncaught-exception/uncaught-exception.js index db10be6397..c07015cce8 100644 --- a/packages/plugin-node-uncaught-exception/uncaught-exception.js +++ b/packages/plugin-node-uncaught-exception/uncaught-exception.js @@ -1,4 +1,4 @@ -const { maybeUseFallbackStack } = require('@bugsnag/core/lib/node-fallback-stack') +const { nodeFallbackStack } = require('@bugsnag/core') let _handler module.exports = { @@ -12,7 +12,7 @@ module.exports = { // check if the stacktrace has no context, if so append the frames we created earlier // see plugin-contextualize for where this is created - if (err.stack && c.fallbackStack) maybeUseFallbackStack(err, c.fallbackStack) + if (err.stack && c.fallbackStack) nodeFallbackStack.maybeUseFallbackStack(err, c.fallbackStack) const event = c.Event.create(err, false, { severity: 'error', diff --git a/packages/plugin-node-unhandled-rejection/test/unhandled-rejection.test.ts b/packages/plugin-node-unhandled-rejection/test/unhandled-rejection.test.ts index 0500cbd643..4819ac2b2f 100644 --- a/packages/plugin-node-unhandled-rejection/test/unhandled-rejection.test.ts +++ b/packages/plugin-node-unhandled-rejection/test/unhandled-rejection.test.ts @@ -1,7 +1,5 @@ -import Client from '@bugsnag/core/client' -import { schema } from '@bugsnag/core/config' +import { Client, Event, schema } from '@bugsnag/core' import plugin from '../' -import EventWithInternals from '@bugsnag/core/event' describe('plugin: node unhandled rejection handler', () => { it('should listen to the process#unhandledRejection event', () => { @@ -36,7 +34,7 @@ describe('plugin: node unhandled rejection handler', () => { it('should call the configured onUnhandledRejection callback', done => { const c = new Client({ apiKey: 'api_key', - onUnhandledRejection: (err: Error, event: EventWithInternals) => { + onUnhandledRejection: (err: Error, event: Event) => { expect(err.message).toBe('never gonna catch me') expect(event.errors[0].errorMessage).toBe('never gonna catch me') expect(event._handledState.unhandled).toBe(true) @@ -48,6 +46,7 @@ describe('plugin: node unhandled rejection handler', () => { plugins: [plugin] }, { ...schema, + // @ts-expect-error onUnhandledRejection: { validate: (val: unknown) => typeof val === 'function', message: 'should be a function', @@ -65,7 +64,7 @@ describe('plugin: node unhandled rejection handler', () => { const c = new Client({ apiKey: 'api_key', reportUnhandledPromiseRejectionsAsHandled: true, - onUnhandledRejection: (err: Error, event: EventWithInternals) => { + onUnhandledRejection: (err: Error, event: Event) => { expect(err.message).toBe('never gonna catch me') expect(event._handledState.unhandled).toBe(false) expect(event._handledState.severity).toBe('error') @@ -76,6 +75,7 @@ describe('plugin: node unhandled rejection handler', () => { plugins: [plugin] }, { ...schema, + // @ts-expect-error onUnhandledRejection: { validate: (val: unknown) => typeof val === 'function', message: 'should be a function', @@ -92,7 +92,7 @@ describe('plugin: node unhandled rejection handler', () => { it('should tolerate delivery errors', done => { const c = new Client({ apiKey: 'api_key', - onUnhandledRejection: (err: Error, event: EventWithInternals) => { + onUnhandledRejection: (err: Error, event: Event) => { expect(err.message).toBe('never gonna catch me') expect(event.errors[0].errorMessage).toBe('never gonna catch me') expect(event._handledState.unhandled).toBe(true) @@ -104,6 +104,7 @@ describe('plugin: node unhandled rejection handler', () => { plugins: [plugin] }, { ...schema, + // @ts-expect-error onUnhandledRejection: { validate: (val: unknown) => typeof val === 'function', message: 'should be a function', diff --git a/packages/plugin-react-native-client-sync/package.json b/packages/plugin-react-native-client-sync/package.json index 2963627e0b..747381ad56 100644 --- a/packages/plugin-react-native-client-sync/package.json +++ b/packages/plugin-react-native-client-sync/package.json @@ -1,7 +1,7 @@ { "name": "@bugsnag/plugin-react-native-client-sync", "version": "8.4.0", - "main": "client-sync.js", + "main": "dist/client-sync.js", "description": "@bugsnag/react-native plugin to sync information between JS and native layer", "homepage": "https://www.bugsnag.com/", "repository": { @@ -12,17 +12,19 @@ "access": "public" }, "files": [ - "*.js" + "dist" ], "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^8.4.0" + "@bugsnag/core": "^8.4.0", + "@bugsnag/derecursify": "^8.4.0" }, "peerDependencies": { "@bugsnag/core": "^8.0.0" }, "scripts": { + "build": "rollup --config rollup.config.js", "test:types": "tsc -p tsconfig.json" } } diff --git a/packages/plugin-react-native-client-sync/rollup.config.js b/packages/plugin-react-native-client-sync/rollup.config.js new file mode 100644 index 0000000000..85c2d46294 --- /dev/null +++ b/packages/plugin-react-native-client-sync/rollup.config.js @@ -0,0 +1,21 @@ +/* eslint-env node */ +/* global require, module */ +const { nodeResolve } = require('@rollup/plugin-node-resolve') +const commonjs = require('@rollup/plugin-commonjs') +const babel = require('@rollup/plugin-babel'); + +module.exports = { + input: 'src/client-sync.js', + output: { + file: 'dist/client-sync.js', + format: 'cjs' + }, + plugins: [ + nodeResolve({ + preferBuiltins: true + }), + commonjs(), + babel({ babelHelpers: 'bundled' }) + ], + external: ['@bugsnag/core', 'react-native'] +} diff --git a/packages/plugin-react-native-client-sync/client-sync.js b/packages/plugin-react-native-client-sync/src/client-sync.js similarity index 98% rename from packages/plugin-react-native-client-sync/client-sync.js rename to packages/plugin-react-native-client-sync/src/client-sync.js index f3ac52d920..0300b0c2e1 100644 --- a/packages/plugin-react-native-client-sync/client-sync.js +++ b/packages/plugin-react-native-client-sync/src/client-sync.js @@ -1,5 +1,5 @@ const { DeviceEventEmitter, NativeEventEmitter, NativeModules, Platform } = require('react-native') -const derecursify = require('@bugsnag/core/lib/derecursify') +const { derecursify } = require('@bugsnag/derecursify') module.exports = (NativeClient) => ({ load: (client) => { diff --git a/packages/plugin-react-native-client-sync/test/client-sync.test.ts b/packages/plugin-react-native-client-sync/test/client-sync.test.ts index c5c387ce3e..8a85dff866 100644 --- a/packages/plugin-react-native-client-sync/test/client-sync.test.ts +++ b/packages/plugin-react-native-client-sync/test/client-sync.test.ts @@ -1,11 +1,6 @@ -import Client from '@bugsnag/core/client' -import plugin from '../' -import { Breadcrumb } from '@bugsnag/core' +import plugin from '../src/client-sync' +import { Client, Breadcrumb } from '@bugsnag/core' -// @types/react-native conflicts with lib dom so disable ts for -// react-native imports until a better solution is found. -// See DefinitelyTyped/DefinitelyTyped#33311 -// @ts-ignore import { DeviceEventEmitter } from 'react-native' jest.mock('react-native', () => ({ diff --git a/packages/plugin-react-native-client-sync/tsconfig.json b/packages/plugin-react-native-client-sync/tsconfig.json index 305a2e87a0..f227f15f7b 100644 --- a/packages/plugin-react-native-client-sync/tsconfig.json +++ b/packages/plugin-react-native-client-sync/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.json", "include": ["."], "compilerOptions": { - "lib": ["es2015"], + "lib": ["es2015", "esnext"], "types": ["jest", "react-native"], } } \ No newline at end of file diff --git a/packages/plugin-react-native-event-sync/test/event-sync.test.ts b/packages/plugin-react-native-event-sync/test/event-sync.test.ts index ddcf909e04..4e8c8344ba 100644 --- a/packages/plugin-react-native-event-sync/test/event-sync.test.ts +++ b/packages/plugin-react-native-event-sync/test/event-sync.test.ts @@ -1,4 +1,4 @@ -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' import plugin from '../' describe('plugin: react native event sync', () => { diff --git a/packages/plugin-react-native-global-error-handler/package.json b/packages/plugin-react-native-global-error-handler/package.json index efcdd92b69..b15aea033e 100644 --- a/packages/plugin-react-native-global-error-handler/package.json +++ b/packages/plugin-react-native-global-error-handler/package.json @@ -17,7 +17,8 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^8.4.0" + "@bugsnag/core": "^8.4.0", + "@types/react-native": "0.67.8" }, "peerDependencies": { "@bugsnag/core": "^8.0.0" diff --git a/packages/plugin-react-native-global-error-handler/test/error-handler.test.ts b/packages/plugin-react-native-global-error-handler/test/error-handler.test.ts index fc86aef8b5..26a3c01a12 100644 --- a/packages/plugin-react-native-global-error-handler/test/error-handler.test.ts +++ b/packages/plugin-react-native-global-error-handler/test/error-handler.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import plugin from '../' -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' class MockErrorUtils { _globalHandler: ((err: Error) => void) | null; diff --git a/packages/plugin-react-native-hermes/package.json b/packages/plugin-react-native-hermes/package.json index 2b50be5f37..f254e8f978 100644 --- a/packages/plugin-react-native-hermes/package.json +++ b/packages/plugin-react-native-hermes/package.json @@ -20,7 +20,8 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^8.4.0" + "@bugsnag/core": "^8.4.0", + "@types/react-native": "0.67.8" }, "peerDependencies": { "@bugsnag/core": "^8.0.0" diff --git a/packages/plugin-react-native-hermes/test/hermes.test.ts b/packages/plugin-react-native-hermes/test/hermes.test.ts index 933d526c31..e0da86fa7a 100644 --- a/packages/plugin-react-native-hermes/test/hermes.test.ts +++ b/packages/plugin-react-native-hermes/test/hermes.test.ts @@ -1,6 +1,6 @@ import plugin from '../hermes' -import Client, { EventDeliveryPayload } from '@bugsnag/core/client' +import { Client, EventDeliveryPayload } from '@bugsnag/core' describe('plugin: react native hermes', () => { it('should add an onError callback which captures device information', () => { diff --git a/packages/plugin-react-native-navigation/test/react-native-navigation.test.ts b/packages/plugin-react-native-navigation/test/react-native-navigation.test.ts index eb40da2a97..86c530715d 100644 --- a/packages/plugin-react-native-navigation/test/react-native-navigation.test.ts +++ b/packages/plugin-react-native-navigation/test/react-native-navigation.test.ts @@ -1,6 +1,5 @@ import Plugin from '../react-native-navigation' -import { Breadcrumb } from '@bugsnag/core' -import Client from '@bugsnag/core/client' +import { Breadcrumb, Client } from '@bugsnag/core' interface Event { componentId: number diff --git a/packages/plugin-react-native-orientation-breadcrumbs/package.json b/packages/plugin-react-native-orientation-breadcrumbs/package.json index e8f06fc284..c142c6557f 100644 --- a/packages/plugin-react-native-orientation-breadcrumbs/package.json +++ b/packages/plugin-react-native-orientation-breadcrumbs/package.json @@ -17,7 +17,8 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^8.4.0" + "@bugsnag/core": "^8.4.0", + "@types/react-native": "0.67.8" }, "peerDependencies": { "@bugsnag/core": "^8.0.0" diff --git a/packages/plugin-react-native-orientation-breadcrumbs/test/orientation.test.ts b/packages/plugin-react-native-orientation-breadcrumbs/test/orientation.test.ts index 99f505a8bc..84ab1e3fa8 100644 --- a/packages/plugin-react-native-orientation-breadcrumbs/test/orientation.test.ts +++ b/packages/plugin-react-native-orientation-breadcrumbs/test/orientation.test.ts @@ -1,10 +1,6 @@ -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' import plugin from '..' -// @types/react-native conflicts with lib dom so disable ts for -// react-native imports until a better solution is found. -// See DefinitelyTyped/DefinitelyTyped#33311 -// @ts-ignore import { Dimensions } from 'react-native' jest.mock('react-native', () => ({ diff --git a/packages/plugin-react-native-session/package.json b/packages/plugin-react-native-session/package.json index b00bd46e74..3c347141a9 100644 --- a/packages/plugin-react-native-session/package.json +++ b/packages/plugin-react-native-session/package.json @@ -17,7 +17,8 @@ "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^8.4.0" + "@bugsnag/core": "^8.4.0", + "@types/react-native": "0.67.8" }, "peerDependencies": { "@bugsnag/core": "^8.0.0" diff --git a/packages/plugin-react-native-session/test/session.test.ts b/packages/plugin-react-native-session/test/session.test.ts index 0cbc4176b5..dd11982e2a 100644 --- a/packages/plugin-react-native-session/test/session.test.ts +++ b/packages/plugin-react-native-session/test/session.test.ts @@ -1,5 +1,5 @@ import plugin from '../' -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' describe('plugin: react native session', () => { it('adds missing methods and forwards calls to native client', () => { diff --git a/packages/plugin-react-native-unhandled-rejection/test/rejection-handler.test.ts b/packages/plugin-react-native-unhandled-rejection/test/rejection-handler.test.ts index 609cf65878..29504080d3 100644 --- a/packages/plugin-react-native-unhandled-rejection/test/rejection-handler.test.ts +++ b/packages/plugin-react-native-unhandled-rejection/test/rejection-handler.test.ts @@ -1,5 +1,5 @@ import plugin from '../' -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' // use the promise polyfill that RN uses, otherwise the unhandled rejections in // this test go to node's process#unhandledRejection event diff --git a/packages/plugin-react-navigation/test/react-navigation.test.tsx b/packages/plugin-react-navigation/test/react-navigation.test.tsx index b16cfdf67b..52e4ed8f97 100644 --- a/packages/plugin-react-navigation/test/react-navigation.test.tsx +++ b/packages/plugin-react-navigation/test/react-navigation.test.tsx @@ -1,5 +1,5 @@ import Plugin from '../' -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' import TestRenderer from 'react-test-renderer' import * as React from 'react' import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native' diff --git a/packages/plugin-react/babel.config.js b/packages/plugin-react/babel.config.js new file mode 100644 index 0000000000..235ee4504b --- /dev/null +++ b/packages/plugin-react/babel.config.js @@ -0,0 +1,3 @@ +const babelConfig = require('../../babel.config.js') + +module.exports = babelConfig diff --git a/packages/plugin-react/package.json b/packages/plugin-react/package.json index 8f4893b12f..7270e42e05 100644 --- a/packages/plugin-react/package.json +++ b/packages/plugin-react/package.json @@ -1,10 +1,17 @@ { "name": "@bugsnag/plugin-react", "version": "8.4.0", - "main": "dist/bugsnag-react.js", "description": "React integration for @bugsnag/js", - "browser": "dist/bugsnag-react.js", - "types": "types/bugsnag-plugin-react.d.ts", + "main": "dist/index-cjs.cjs", + "types": "dist/types/index-es.d.ts", + "browser": "./dist/bugsnag-react.js", + "exports": { + ".": { + "types": "./dist/types/index-es.d.ts", + "import": "./dist/index-es.mjs", + "default": "./dist/index-cjs.cjs" + } + }, "homepage": "https://www.bugsnag.com/", "repository": { "type": "git", @@ -14,17 +21,20 @@ "access": "public" }, "files": [ - "dist", - "types" + "dist" ], "scripts": { "clean": "rm -fr dist && mkdir dist", - "build": "npm run clean && ../../bin/bundle src/index.js --standalone=BugsnagPluginReact | ../../bin/extract-source-map dist/bugsnag-react.js" + "build": "npm run clean && npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs" }, "author": "Bugsnag", "license": "MIT", "devDependencies": { - "@bugsnag/core": "^8.4.0" + "@bugsnag/core": "^8.4.0", + "@types/react-dom": "^16.9.16", + "@types/react": "^16.9.49", + "react": "^16.13.1" }, "peerDependencies": { "@bugsnag/core": "^8.0.0" diff --git a/packages/plugin-react/rollup.config.npm.mjs b/packages/plugin-react/rollup.config.npm.mjs new file mode 100644 index 0000000000..7bb6de1ad6 --- /dev/null +++ b/packages/plugin-react/rollup.config.npm.mjs @@ -0,0 +1,65 @@ +import babel from '@rollup/plugin-babel' +import commonjs from '@rollup/plugin-commonjs' +import nodeResolve from '@rollup/plugin-node-resolve' +import typescript from '@rollup/plugin-typescript' + +import createRollupConfig, { sharedOutput } from '../../.rollup/index.mjs' + +const plugins = [ + nodeResolve({ + browser: true + }), + commonjs(), + typescript({ + removeComments: true, + // don't output anything if there's a TS error + noEmitOnError: true, + compilerOptions: { + target: 'es2015' + } + }), + babel({ babelHelpers: 'bundled' }) +] + +const external = ['react'] + +export default [ + createRollupConfig({ + input: 'src/index-es.ts', + output: [ + { + ...sharedOutput, + preserveModules: false, + entryFileNames: '[name].mjs', + format: 'esm' + } + ], + external, + plugins + }), + createRollupConfig({ + input: 'src/index-cjs.ts', + output: [ + { + ...sharedOutput, + entryFileNames: '[name].cjs', + format: 'cjs' + }, + ], + external, + plugins + }), + createRollupConfig({ + input: 'src/index-umd.ts', + output: [ + { + ...sharedOutput, + entryFileNames: 'bugsnag-react.js', + format: 'umd', + name: 'BugsnagReact' + }, + ], + external, + plugins + }) +] diff --git a/packages/plugin-react/src/index-cjs.ts b/packages/plugin-react/src/index-cjs.ts new file mode 100644 index 0000000000..e1c9509d48 --- /dev/null +++ b/packages/plugin-react/src/index-cjs.ts @@ -0,0 +1,5 @@ +import BugsnagPluginReact, { formatComponentStack } from './plugin' + + + +export default Object.assign(BugsnagPluginReact, { formatComponentStack }) diff --git a/packages/plugin-react/src/index-es.ts b/packages/plugin-react/src/index-es.ts new file mode 100644 index 0000000000..0f41efac6f --- /dev/null +++ b/packages/plugin-react/src/index-es.ts @@ -0,0 +1 @@ +export { default, formatComponentStack } from './plugin' diff --git a/packages/plugin-react/src/index-umd.ts b/packages/plugin-react/src/index-umd.ts new file mode 100644 index 0000000000..e1c9509d48 --- /dev/null +++ b/packages/plugin-react/src/index-umd.ts @@ -0,0 +1,5 @@ +import BugsnagPluginReact, { formatComponentStack } from './plugin' + + + +export default Object.assign(BugsnagPluginReact, { formatComponentStack }) diff --git a/packages/plugin-react/src/index.js b/packages/plugin-react/src/index.js deleted file mode 100644 index c272709676..0000000000 --- a/packages/plugin-react/src/index.js +++ /dev/null @@ -1,93 +0,0 @@ -module.exports = class BugsnagPluginReact { - constructor (...args) { - // Fetch React from the window object, if it exists - const globalReact = typeof window !== 'undefined' && window.React - - this.name = 'react' - this.lazy = args.length === 0 && !globalReact - - if (!this.lazy) { - this.React = args[0] || globalReact - if (!this.React) throw new Error('@bugsnag/plugin-react reference to `React` was undefined') - } - } - - load (client) { - if (!this.lazy) { - const ErrorBoundary = createClass(this.React, client) - ErrorBoundary.createErrorBoundary = () => ErrorBoundary - return ErrorBoundary - } - - const BugsnagPluginReactLazyInitializer = function () { - throw new Error(`@bugsnag/plugin-react was used incorrectly. Valid usage is as follows: -Pass React to the plugin constructor - - \`Bugsnag.start({ plugins: [new BugsnagPluginReact(React)] })\` -and then call \`const ErrorBoundary = Bugsnag.getPlugin('react').createErrorBoundary()\` - -Or if React is not available until after Bugsnag has started, -construct the plugin with no arguments - \`Bugsnag.start({ plugins: [new BugsnagPluginReact()] })\`, -then pass in React when available to construct your error boundary - \`const ErrorBoundary = Bugsnag.getPlugin('react').createErrorBoundary(React)\``) - } - BugsnagPluginReactLazyInitializer.createErrorBoundary = (React) => { - if (!React) throw new Error('@bugsnag/plugin-react reference to `React` was undefined') - return createClass(React, client) - } - return BugsnagPluginReactLazyInitializer - } -} - -const formatComponentStack = str => { - const lines = str.split(/\n/g) - let ret = '' - for (let line = 0, len = lines.length; line < len; line++) { - if (lines[line].length) ret += `${ret.length ? '\n' : ''}${lines[line].trim()}` - } - return ret -} - -const createClass = (React, client) => class ErrorBoundary extends React.Component { - constructor (props) { - super(props) - this.state = { - error: null, - info: null - } - this.handleClearError = this.handleClearError.bind(this) - } - - handleClearError () { - this.setState({ error: null, info: null }) - } - - componentDidCatch (error, info) { - const { onError } = this.props - const handledState = { severity: 'error', unhandled: true, severityReason: { type: 'unhandledException' } } - const event = client.Event.create( - error, - true, - handledState, - 1 - ) - if (info && info.componentStack) info.componentStack = formatComponentStack(info.componentStack) - event.addMetadata('react', info) - client._notify(event, onError) - this.setState({ error, info }) - } - - render () { - const { error } = this.state - if (error) { - const { FallbackComponent } = this.props - if (FallbackComponent) return React.createElement(FallbackComponent, { ...this.state, clearError: this.handleClearError }) - return null - } - return this.props.children - } -} - -module.exports.formatComponentStack = formatComponentStack -module.exports.default = module.exports diff --git a/packages/plugin-react/src/plugin.ts b/packages/plugin-react/src/plugin.ts new file mode 100644 index 0000000000..618a672f1c --- /dev/null +++ b/packages/plugin-react/src/plugin.ts @@ -0,0 +1,120 @@ +import React, { ErrorInfo } from 'react' + +import { Plugin, OnErrorCallback, Client } from '@bugsnag/core' + +interface BugsnagErrorBoundaryProps { + children?: React.ReactNode | undefined + onError?: OnErrorCallback + FallbackComponent?: React.ComponentType<{ + error: Error + info: React.ErrorInfo + clearError: () => void + }> +} + +export type BugsnagErrorBoundary = React.ComponentType + +export interface BugsnagPluginReactResult { + createErrorBoundary(react?: typeof React): BugsnagErrorBoundary +} + +// add a new call signature for the getPlugin() method that types the react plugin result +declare module '@bugsnag/core' { + interface Client { + getPlugin(id: 'react'): BugsnagPluginReactResult | undefined + } +} + +export default class BugsnagPluginReact implements Plugin { + public readonly name: string; + private readonly lazy: boolean; + private readonly React?: typeof React + + constructor (react?: typeof React) { + // Fetch React from the window object, if it exists + const globalReact = typeof window !== 'undefined' && window.React + + this.name = 'react' + this.lazy = !react && !globalReact + + if (!this.lazy) { + this.React = react || globalReact as typeof React + if (!this.React) throw new Error('@bugsnag/plugin-react reference to `React` was undefined') + } + } + + load (client: Client) { + if (!this.lazy && this.React) { + const ErrorBoundary = createClass(this.React, client) + return { createErrorBoundary: () => ErrorBoundary } + } + + const BugsnagPluginReactLazyInitializer = function () { + throw new Error(`@bugsnag/plugin-react was used incorrectly. Valid usage is as follows: +Pass React to the plugin constructor + + \`Bugsnag.start({ plugins: [new BugsnagPluginReact(React)] })\` +and then call \`const ErrorBoundary = Bugsnag.getPlugin('react').createErrorBoundary()\` + +Or if React is not available until after Bugsnag has started, +construct the plugin with no arguments + \`Bugsnag.start({ plugins: [new BugsnagPluginReact()] })\`, +then pass in React when available to construct your error boundary + \`const ErrorBoundary = Bugsnag.getPlugin('react').createErrorBoundary(React)\``) + } + BugsnagPluginReactLazyInitializer.createErrorBoundary = (react: typeof React) => { + if (!react) throw new Error('@bugsnag/plugin-react reference to `React` was undefined') + return createClass(react, client) + } + return BugsnagPluginReactLazyInitializer + } +} + +export const formatComponentStack = (str: string) => { + const lines = str.split(/\n/g) + let ret = '' + for (let line = 0, len = lines.length; line < len; line++) { + if (lines[line].length) ret += `${ret.length ? '\n' : ''}${lines[line].trim()}` + } + return ret +} + +const createClass = (react: typeof React, client: Client) => class BugsnagErrorBoundary extends React.Component { + constructor (props: BugsnagErrorBoundaryProps) { + super(props) + this.state = { + errorState: null + } + this.handleClearError = this.handleClearError.bind(this) + } + + handleClearError () { + this.setState({ errorState: null }) + } + + componentDidCatch (error: Error, info: ErrorInfo) { + const { onError } = this.props + const handledState = { severity: 'error', unhandled: true, severityReason: { type: 'unhandledException' } } + const event = client.Event.create( + error, + true, + handledState, + 'react plugin', + 1 + ) + if (info && info.componentStack) info.componentStack = formatComponentStack(info.componentStack) + event.addMetadata('react', info) + client._notify(event, onError) + this.setState({ errorState: { error, info } }) + } + + render () { + const { errorState } = this.state + if (errorState) { + const { FallbackComponent } = this.props + if (FallbackComponent) return react.createElement(FallbackComponent, { ...errorState, clearError: this.handleClearError }) + return null + } + return this.props.children + } +} diff --git a/packages/plugin-react/src/test/.babelrc b/packages/plugin-react/src/test/.babelrc deleted file mode 100644 index 2b7bafa5fa..0000000000 --- a/packages/plugin-react/src/test/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@babel/preset-env", "@babel/preset-react"] -} diff --git a/packages/plugin-react/src/test/__snapshots__/index.test.tsx.snap b/packages/plugin-react/src/test/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 6370955c27..0000000000 --- a/packages/plugin-react/src/test/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`does not render FallbackComponent when no error 1`] = `"test"`; - -exports[`renders FallbackComponent on error 1`] = `"fallback"`; - -exports[`renders correctly 1`] = `"test"`; - -exports[`resets the error boundary when the FallbackComponent calls the passed clearError prop 1`] = ` - -`; diff --git a/packages/plugin-react/src/test/index.test.tsx b/packages/plugin-react/test/index.test.tsx similarity index 53% rename from packages/plugin-react/src/test/index.test.tsx rename to packages/plugin-react/test/index.test.tsx index 9be1e91b21..4f1a69492f 100644 --- a/packages/plugin-react/src/test/index.test.tsx +++ b/packages/plugin-react/test/index.test.tsx @@ -1,9 +1,12 @@ import React, { useState } from 'react' import { create, act } from 'react-test-renderer' -import BugsnagPluginReact from '..' -import Client from '@bugsnag/core/client' +import BugsnagPluginReact, { formatComponentStack } from '../src/plugin' +import { Client } from '@bugsnag/core' -const client = new Client({ apiKey: '123', plugins: [new BugsnagPluginReact(React)] }, undefined) +const client = new Client( + { apiKey: '123', plugins: [new BugsnagPluginReact(React)] }, + undefined +) client._notify = jest.fn() interface FallbackComponentProps { @@ -11,9 +14,9 @@ interface FallbackComponentProps { info: React.ErrorInfo clearError: () => void } -type FallbackComponentType = React.ComponentType +type FallbackComponentType = React.ComponentType; -// eslint-disable-next-line +// eslint-disable-next-line @typescript-eslint/no-non-null-assertion const ErrorBoundary = client.getPlugin('react')!.createErrorBoundary() beforeAll(() => { @@ -26,8 +29,7 @@ test('formatComponentStack(str)', () => { const str = ` in BadButton in ErrorBoundary` - expect(BugsnagPluginReact.formatComponentStack(str)) - .toBe('in BadButton\nin ErrorBoundary') + expect(formatComponentStack(str)).toBe('in BadButton\nin ErrorBoundary') }) const BadComponent = () => { @@ -35,7 +37,7 @@ const BadComponent = () => { } // see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20544 -const GoodComponent = (): JSX.Element => 'test' as unknown as JSX.Element +const GoodComponent = (): JSX.Element => ('test' as unknown) as JSX.Element const ComponentWithBadButton = () => { const [clicked, setClicked] = useState(false) @@ -47,60 +49,92 @@ const ComponentWithBadButton = () => { } it('renders correctly', () => { - const tree = create() - .toJSON() - expect(tree).toMatchSnapshot() + const tree = create( + + + + ).toJSON() + expect(tree).toMatchInlineSnapshot('"test"') }) it('renders correctly on error', () => { - const tree = create() - .toJSON() + const tree = create( + + + + ).toJSON() expect(tree).toBe(null) }) it('calls notify on error', () => { - create() - .toJSON() + create( + + + + ).toJSON() expect(client._notify).toHaveBeenCalledTimes(1) }) it('does not render FallbackComponent when no error', () => { - const FallbackComponent = jest.fn(() => 'fallback') as unknown as FallbackComponentType - const tree = create() - .toJSON() - expect(tree).toMatchSnapshot() + const FallbackComponent = (jest.fn( + () => 'fallback' + ) as unknown) as FallbackComponentType + const tree = create( + + + + ).toJSON() + expect(tree).toMatchInlineSnapshot('"test"') expect(FallbackComponent).toHaveBeenCalledTimes(0) }) it('renders FallbackComponent on error', () => { - const FallbackComponent = jest.fn(() => 'fallback') as unknown as FallbackComponentType - const tree = create() - .toJSON() - expect(tree).toMatchSnapshot() + const FallbackComponent = (jest.fn( + () => 'fallback' + ) as unknown) as FallbackComponentType + const tree = create( + + + + ).toJSON() + expect(tree).toMatchInlineSnapshot('"fallback"') }) it('passes the props to the FallbackComponent', () => { - const FallbackComponent = jest.fn(() => 'fallback') as unknown as FallbackComponentType - create() - expect(FallbackComponent).toBeCalledWith({ - error: expect.any(Error), - info: { componentStack: expect.any(String) }, - clearError: expect.any(Function) - }, {}) + const FallbackComponent = (jest.fn( + () => 'fallback' + ) as unknown) as FallbackComponentType + create( + + + + ) + expect(FallbackComponent).toBeCalledWith( + { + error: expect.any(Error), + info: { componentStack: expect.any(String) }, + clearError: expect.any(Function) + }, + {} + ) }) it('resets the error boundary when the FallbackComponent calls the passed clearError prop', () => { const FallbackComponent = ({ clearError }: FallbackComponentProps) => { - return ( - - ) + return } - const component = create() + const component = create( + + + + ) const instance = component.root // Trigger a render exception - const badButton = instance.findByType(ComponentWithBadButton).findByType('button') + const badButton = instance + .findByType(ComponentWithBadButton) + .findByType('button') act(() => { badButton.props.onClick() }) @@ -112,11 +146,21 @@ it('resets the error boundary when the FallbackComponent calls the passed clearE }) // expect to see ComponentWithBadButton again - expect(component.toJSON()).toMatchSnapshot() + expect(component.toJSON()).toMatchInlineSnapshot(` + + `) }) it('a bad FallbackComponent implementation does not trigger stack overflow', () => { - const BadFallbackComponentImplementation = ({ error, info, clearError }: FallbackComponentProps) => { + const BadFallbackComponentImplementation = ({ + error, + info, + clearError + }: FallbackComponentProps) => { function log (o: any) {} log(error) clearError() @@ -125,22 +169,29 @@ it('a bad FallbackComponent implementation does not trigger stack overflow', () } expect(() => { - create() + create( + + + + ) }).toThrow() }) it('it passes the onError function to the Bugsnag notify call', () => { const onError = () => {} - create() - .toJSON() - expect(client._notify).toBeCalledWith( - expect.any(client.Event), - onError - ) + create( + + + + ).toJSON() + expect(client._notify).toBeCalledWith(expect.any(client.Event), onError) }) it('supports passing reference to React when the error boundary is created', () => { - const client = new Client({ apiKey: '123', plugins: [new BugsnagPluginReact()] }, undefined) + const client = new Client( + { apiKey: '123', plugins: [new BugsnagPluginReact()] }, + undefined + ) // eslint-disable-next-line const ErrorBoundary = client.getPlugin('react')!.createErrorBoundary(React) expect(ErrorBoundary).toBeTruthy() @@ -160,7 +211,10 @@ describe('global React', () => { it('can pull React out of the window object', () => { globalReference.window.React = React - const client = new Client({ apiKey: 'API_KEYYY', plugins: [new BugsnagPluginReact()] }) + const client = new Client({ + apiKey: 'API_KEYYY', + plugins: [new BugsnagPluginReact()] + }) // eslint-disable-next-line const ErrorBoundary = client.getPlugin('react')!.createErrorBoundary() @@ -172,7 +226,10 @@ describe('global React', () => { // Delete the window object so that any unsafe check for 'window.React' will throw delete globalReference.window - const client = new Client({ apiKey: 'API_KEYYY', plugins: [new BugsnagPluginReact()] }) + const client = new Client({ + apiKey: 'API_KEYYY', + plugins: [new BugsnagPluginReact()] + }) // eslint-disable-next-line const ErrorBoundary = client.getPlugin('react')!.createErrorBoundary(React) diff --git a/packages/plugin-react/tsconfig.json b/packages/plugin-react/tsconfig.json new file mode 100644 index 0000000000..a5cb75c562 --- /dev/null +++ b/packages/plugin-react/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"] +} diff --git a/packages/plugin-react/types/bugsnag-plugin-react.d.ts b/packages/plugin-react/types/bugsnag-plugin-react.d.ts deleted file mode 100644 index 1e6825fb8d..0000000000 --- a/packages/plugin-react/types/bugsnag-plugin-react.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Plugin, OnErrorCallback } from '@bugsnag/core' -import React from 'react' - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface BugsnagPluginReact extends Plugin { } -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -declare class BugsnagPluginReact { - constructor(react?: typeof React) -} - -export type BugsnagErrorBoundary = React.ComponentType<{ - children?: React.ReactNode | undefined - onError?: OnErrorCallback - FallbackComponent?: React.ComponentType<{ - error: Error - info: React.ErrorInfo - clearError: () => void - }> -}> - -export interface BugsnagPluginReactResult { - createErrorBoundary(react?: typeof React): BugsnagErrorBoundary -} - -// add a new call signature for the getPlugin() method that types the react plugin result -declare module '@bugsnag/core' { - interface Client { - getPlugin(id: 'react'): BugsnagPluginReactResult | undefined - } -} - -export default BugsnagPluginReact diff --git a/packages/plugin-restify/package.json b/packages/plugin-restify/package.json index 8dc7ef9bc6..19f22ad447 100644 --- a/packages/plugin-restify/package.json +++ b/packages/plugin-restify/package.json @@ -19,7 +19,7 @@ "author": "Bugsnag", "license": "MIT", "peerDependencies": { - "@bugsnag/core": "^8.0.0" + "@bugsnag/core": "^8.2.0" }, "devDependencies": { "@bugsnag/core": "^8.4.0", diff --git a/packages/plugin-restify/src/request-info.js b/packages/plugin-restify/src/request-info.js index 2f9b95049e..c46af8e1e0 100644 --- a/packages/plugin-restify/src/request-info.js +++ b/packages/plugin-restify/src/request-info.js @@ -1,4 +1,4 @@ -const extractObject = require('@bugsnag/core/lib/extract-object') +const { extractObject } = require('@bugsnag/core') module.exports = req => { const connection = req.connection diff --git a/packages/plugin-restify/src/restify.js b/packages/plugin-restify/src/restify.js index ab071f9878..e32333475a 100644 --- a/packages/plugin-restify/src/restify.js +++ b/packages/plugin-restify/src/restify.js @@ -1,5 +1,5 @@ const extractRequestInfo = require('./request-info') -const clone = require('@bugsnag/core/lib/clone-client') +const { cloneClient } = require('@bugsnag/core') const handledState = { severity: 'error', unhandled: true, @@ -14,7 +14,7 @@ module.exports = { load: client => { const requestHandler = (req, res, next) => { // clone the client to be scoped to this request. If sessions are enabled, start one - const requestClient = clone(client) + const requestClient = cloneClient(client) if (requestClient._config.autoTrackSessions) { requestClient.startSession() } diff --git a/packages/plugin-restify/test/restify.test.ts b/packages/plugin-restify/test/restify.test.ts index 06790a43b1..8f5f499456 100644 --- a/packages/plugin-restify/test/restify.test.ts +++ b/packages/plugin-restify/test/restify.test.ts @@ -1,4 +1,4 @@ -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' import plugin from '../src/restify' describe('plugin: restify', () => { diff --git a/packages/plugin-server-session/session.js b/packages/plugin-server-session/session.js index 7932aee8db..012f61ae85 100644 --- a/packages/plugin-server-session/session.js +++ b/packages/plugin-server-session/session.js @@ -1,7 +1,6 @@ -const intRange = require('@bugsnag/core/lib/validators/int-range') +const { intRange, runSyncCallbacks } = require('@bugsnag/core') const SessionTracker = require('./tracker') const Backoff = require('backo') -const runSyncCallbacks = require('@bugsnag/core/lib/sync-callback-runner') module.exports = { load: (client) => { diff --git a/packages/plugin-server-session/test/session.test.ts b/packages/plugin-server-session/test/session.test.ts index 71b8443692..549873a72b 100644 --- a/packages/plugin-server-session/test/session.test.ts +++ b/packages/plugin-server-session/test/session.test.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'events' -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' import _Tracker from '../tracker' import plugin from '../session' import { Session } from '@bugsnag/core' diff --git a/packages/plugin-server-session/test/tracker.test.ts b/packages/plugin-server-session/test/tracker.test.ts index 09593991a4..4318dae044 100644 --- a/packages/plugin-server-session/test/tracker.test.ts +++ b/packages/plugin-server-session/test/tracker.test.ts @@ -1,5 +1,5 @@ import Tracker from '../tracker' -import Session from '@bugsnag/core/session' +import { Session } from '@bugsnag/core' import timekeeper from 'timekeeper' describe('session tracker', () => { diff --git a/packages/plugin-simple-throttle/package.json b/packages/plugin-simple-throttle/package.json index c70f3f73c5..87195b99e0 100644 --- a/packages/plugin-simple-throttle/package.json +++ b/packages/plugin-simple-throttle/package.json @@ -1,7 +1,15 @@ { "name": "@bugsnag/plugin-simple-throttle", "version": "8.4.0", - "main": "throttle.js", + "main": "dist/throttle.js", + "types": "dist/types/throttle.d.ts", + "exports": { + ".": { + "types": "./dist/types/throttle.d.ts", + "default": "./dist/throttle.js", + "import": "./dist/throttle.mjs" + } + }, "description": "@bugsnag/js plugin to prevent too many events from being sent", "homepage": "https://www.bugsnag.com/", "repository": { @@ -12,9 +20,8 @@ "access": "public" }, "files": [ - "*.js" + "dist" ], - "scripts": {}, "author": "Bugsnag", "license": "MIT", "devDependencies": { @@ -22,5 +29,10 @@ }, "peerDependencies": { "@bugsnag/core": "^8.0.0" + }, + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" } } diff --git a/packages/plugin-simple-throttle/rollup.config.npm.mjs b/packages/plugin-simple-throttle/rollup.config.npm.mjs new file mode 100644 index 0000000000..ef612d2a39 --- /dev/null +++ b/packages/plugin-simple-throttle/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from '../../.rollup/index.mjs' + +export default createRollupConfig({ + input: 'src/throttle.ts', + external: [/node_modules/] +}) diff --git a/packages/plugin-simple-throttle/src/throttle.ts b/packages/plugin-simple-throttle/src/throttle.ts new file mode 100644 index 0000000000..0c2f40105c --- /dev/null +++ b/packages/plugin-simple-throttle/src/throttle.ts @@ -0,0 +1,49 @@ +import { intRange, Client, Config, Plugin } from '@bugsnag/core' + +interface PluginConfig extends Config { + maxEvents: number +} +interface ThrottlePlugin extends Plugin { + configSchema: { + [key: string]: { + defaultValue: () => unknown + message: string + validate: (value: unknown) => boolean + } + } +} + +interface InternalClient extends Client { + resetEventCount: () => void +} + +/* + * Throttles and dedupes events + */ +const plugin: ThrottlePlugin = { + load: (client) => { + // track sent events for each init of the plugin + let n = 0 + + // add onError hook + client.addOnError((event) => { + // have max events been sent already? + 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++ + }) + ; + (client as InternalClient).resetEventCount = () => { n = 0 } + }, + configSchema: { + maxEvents: { + defaultValue: () => 10, + message: 'should be a positive integer ≤100', + validate: (val: unknown): val is number => intRange(1, 100)(val) + } + } +} + +export default plugin diff --git a/packages/plugin-simple-throttle/test/throttle.test.ts b/packages/plugin-simple-throttle/test/throttle.test.ts index 32589456f4..647b81efd1 100644 --- a/packages/plugin-simple-throttle/test/throttle.test.ts +++ b/packages/plugin-simple-throttle/test/throttle.test.ts @@ -1,6 +1,6 @@ -import plugin from '../' +import plugin from '../src/throttle' -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' describe('plugin: throttle', () => { const payloads = [] diff --git a/packages/plugin-simple-throttle/throttle.js b/packages/plugin-simple-throttle/throttle.js deleted file mode 100644 index 3de502ec61..0000000000 --- a/packages/plugin-simple-throttle/throttle.js +++ /dev/null @@ -1,31 +0,0 @@ -const intRange = require('@bugsnag/core/lib/validators/int-range') - -/* - * Throttles and dedupes events - */ - -module.exports = { - load: (client) => { - // track sent events for each init of the plugin - let n = 0 - - // add onError hook - client.addOnError((event) => { - // have max events been sent already? - if (n >= client._config.maxEvents) { - client._logger.warn(`Cancelling event send due to maxEvents per session limit of ${client._config.maxEvents} being reached`) - return false - } - n++ - }) - - client.resetEventCount = () => { n = 0 } - }, - configSchema: { - maxEvents: { - defaultValue: () => 10, - message: 'should be a positive integer ≤100', - validate: val => intRange(1, 100)(val) - } - } -} diff --git a/packages/plugin-simple-throttle/tsconfig.json b/packages/plugin-simple-throttle/tsconfig.json new file mode 100644 index 0000000000..9478c51300 --- /dev/null +++ b/packages/plugin-simple-throttle/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "target": "ES2020" + } +} diff --git a/packages/plugin-stackframe-path-normaliser/test/path-normaliser.test.ts b/packages/plugin-stackframe-path-normaliser/test/path-normaliser.test.ts index c44986b99f..82c3b7427d 100644 --- a/packages/plugin-stackframe-path-normaliser/test/path-normaliser.test.ts +++ b/packages/plugin-stackframe-path-normaliser/test/path-normaliser.test.ts @@ -1,6 +1,5 @@ import plugin from '../' -import Event from '@bugsnag/core/event' -import Client from '@bugsnag/core/client' +import { Client, Event } from '@bugsnag/core' describe('plugin: stackframe path normaliser', () => { it('does not change frames with no file path', done => { diff --git a/packages/plugin-strip-project-root/package.json b/packages/plugin-strip-project-root/package.json index d1db090fe8..4c8b68b64c 100644 --- a/packages/plugin-strip-project-root/package.json +++ b/packages/plugin-strip-project-root/package.json @@ -16,10 +16,7 @@ ], "author": "Bugsnag", "license": "MIT", - "devDependencies": { - "@bugsnag/core": "^8.4.0" - }, - "peerDependencies": { - "@bugsnag/core": "^8.0.0" + "dependencies": { + "@bugsnag/path-normalizer": "^8.4.0" } } diff --git a/packages/plugin-strip-project-root/strip-project-root.js b/packages/plugin-strip-project-root/strip-project-root.js index b9515b8975..edc0671d11 100644 --- a/packages/plugin-strip-project-root/strip-project-root.js +++ b/packages/plugin-strip-project-root/strip-project-root.js @@ -1,4 +1,4 @@ -const normalizePath = require('@bugsnag/core/lib/path-normalizer') +const normalizePath = require('@bugsnag/path-normalizer') module.exports = { load: client => client.addOnError(event => { diff --git a/packages/plugin-strip-project-root/test/strip-project-root.test.ts b/packages/plugin-strip-project-root/test/strip-project-root.test.ts index 4b5426b939..fefdba36a8 100644 --- a/packages/plugin-strip-project-root/test/strip-project-root.test.ts +++ b/packages/plugin-strip-project-root/test/strip-project-root.test.ts @@ -1,13 +1,12 @@ import plugin from '../' import { join } from 'path' -import Event from '@bugsnag/core/event' -import Client from '@bugsnag/core/client' -import { schema } from '@bugsnag/core/config' +import { Client, Event, schema } from '@bugsnag/core' describe('plugin: strip project root', () => { it('should remove the project root if it matches the start of the stackframe’s file', done => { const client = new Client({ apiKey: 'api_key', projectRoot: '/app', plugins: [plugin] }, { ...schema, + // @ts-expect-error projectRoot: { validate: () => true, defaultValue: () => '', @@ -46,6 +45,7 @@ describe('plugin: strip project root', () => { it('should not remove a matching substring if it is not at the start', done => { const client = new Client({ apiKey: 'api_key', projectRoot: '/app', plugins: [plugin] }, { ...schema, + // @ts-expect-error projectRoot: { validate: () => true, defaultValue: () => '', @@ -84,6 +84,7 @@ describe('plugin: strip project root', () => { it('should work with node_modules and node internals', done => { const client = new Client({ apiKey: 'api_key', projectRoot: '/app', plugins: [plugin] }, { ...schema, + // @ts-expect-error projectRoot: { validate: () => true, defaultValue: () => '', @@ -117,6 +118,7 @@ describe('plugin: strip project root', () => { it('should tolerate stackframe.file not being a string', done => { const client = new Client({ apiKey: 'api_key', projectRoot: '/app', plugins: [plugin] }, { ...schema, + // @ts-expect-error projectRoot: { validate: () => true, defaultValue: () => '', diff --git a/packages/plugin-strip-query-string/package.json b/packages/plugin-strip-query-string/package.json index 7eefe3f22f..286bee8a03 100644 --- a/packages/plugin-strip-query-string/package.json +++ b/packages/plugin-strip-query-string/package.json @@ -1,7 +1,15 @@ { "name": "@bugsnag/plugin-strip-query-string", "version": "8.4.0", - "main": "strip-query-string.js", + "main": "dist/strip-query-string.js", + "types": "dist/types/strip-query-string.d.ts", + "exports": { + ".": { + "types": "./dist/types/strip-query-string.d.ts", + "default": "./dist/strip-query-string.js", + "import": "./dist/strip-query-string.mjs" + } + }, "description": "@bugsnag/js plugin to strip query string and document fragment from stackframe filenames", "homepage": "https://www.bugsnag.com/", "repository": { @@ -12,7 +20,7 @@ "access": "public" }, "files": [ - "*.js" + "dist" ], "author": "Bugsnag", "license": "MIT", @@ -21,5 +29,10 @@ }, "peerDependencies": { "@bugsnag/core": "^8.0.0" + }, + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" } } diff --git a/packages/plugin-strip-query-string/rollup.config.npm.mjs b/packages/plugin-strip-query-string/rollup.config.npm.mjs new file mode 100644 index 0000000000..f6d44352b8 --- /dev/null +++ b/packages/plugin-strip-query-string/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from "../../.rollup/index.mjs"; + +export default createRollupConfig({ + input: "src/strip-query-string.ts", + external: [/node_modules/], +}); diff --git a/packages/plugin-strip-query-string/src/strip-query-string.ts b/packages/plugin-strip-query-string/src/strip-query-string.ts new file mode 100644 index 0000000000..735766fcd9 --- /dev/null +++ b/packages/plugin-strip-query-string/src/strip-query-string.ts @@ -0,0 +1,27 @@ +/* + * Remove query strings (and fragments) from stacktraces + */ +import { Plugin, Stackframe } from '@bugsnag/core' + +const strip = (str: any) => + typeof str === 'string' + ? str.replace(/\?.*$/, '').replace(/#.*$/, '') + : str + +interface ExtendedPlugin extends Plugin { + _strip: typeof strip +} + +const plugin: ExtendedPlugin = { + load: (client) => { + client.addOnError(event => { + const allFrames: Stackframe[] = (event.errors).reduce((accum: Stackframe[], er) => accum.concat(er.stacktrace), []) + allFrames.map(frame => { + frame.file = strip(frame.file) + }) + }) + }, + _strip: strip +} + +export default plugin diff --git a/packages/plugin-strip-query-string/strip-query-string.js b/packages/plugin-strip-query-string/strip-query-string.js deleted file mode 100644 index c5c15b6836..0000000000 --- a/packages/plugin-strip-query-string/strip-query-string.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Remove query strings (and fragments) from stacktraces - */ -const map = require('@bugsnag/core/lib/es-utils/map') -const reduce = require('@bugsnag/core/lib/es-utils/reduce') - -module.exports = { - load: (client) => { - client.addOnError(event => { - const allFrames = reduce(event.errors, (accum, er) => accum.concat(er.stacktrace), []) - map(allFrames, frame => { - frame.file = strip(frame.file) - }) - }) - } -} - -const strip = module.exports._strip = str => - typeof str === 'string' - ? str.replace(/\?.*$/, '').replace(/#.*$/, '') - : str diff --git a/packages/plugin-strip-query-string/test/strip-query-string.test.ts b/packages/plugin-strip-query-string/test/strip-query-string.test.ts index d1a8568fc1..a9f4c84fb5 100644 --- a/packages/plugin-strip-query-string/test/strip-query-string.test.ts +++ b/packages/plugin-strip-query-string/test/strip-query-string.test.ts @@ -1,7 +1,8 @@ import plugin from '../' -import Client, { EventDeliveryPayload } from '@bugsnag/core/client' -import { Stackframe } from '@bugsnag/core/types/common' +import { Client } from '@bugsnag/core' +import { Stackframe } from '@bugsnag/core' +import { EventDeliveryPayload } from '@bugsnag/core/client' describe('plugin: strip query string', () => { it('should strip querystrings and fragments from urls', () => { diff --git a/packages/plugin-strip-query-string/tsconfig.json b/packages/plugin-strip-query-string/tsconfig.json new file mode 100644 index 0000000000..9478c51300 --- /dev/null +++ b/packages/plugin-strip-query-string/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "target": "ES2020" + } +} diff --git a/packages/plugin-vue/babel.config.js b/packages/plugin-vue/babel.config.js new file mode 100644 index 0000000000..235ee4504b --- /dev/null +++ b/packages/plugin-vue/babel.config.js @@ -0,0 +1,3 @@ +const babelConfig = require('../../babel.config.js') + +module.exports = babelConfig diff --git a/packages/plugin-vue/package.json b/packages/plugin-vue/package.json index 0e2fcaf668..e54a1f28e2 100644 --- a/packages/plugin-vue/package.json +++ b/packages/plugin-vue/package.json @@ -2,9 +2,16 @@ "name": "@bugsnag/plugin-vue", "version": "8.4.0", "description": "Vue.js integration for bugsnag-js", - "main": "dist/bugsnag-vue.js", - "browser": "dist/bugsnag-vue.js", - "types": "types/bugsnag-plugin-vue.d.ts", + "main": "dist/index.cjs", + "types": "dist/types/index.d.ts", + "browser": "./dist/bugsnag-vue.js", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/index.mjs", + "default": "./dist/index.cjs" + } + }, "homepage": "https://www.bugsnag.com/", "repository": { "type": "git", @@ -14,12 +21,12 @@ "access": "public" }, "files": [ - "dist", - "types" + "dist" ], "scripts": { "clean": "rm -fr dist && mkdir dist", - "build": "npm run clean && ../../bin/bundle src/index.js --standalone=BugsnagPluginVue | ../../bin/extract-source-map dist/bugsnag-vue.js" + "build": "npm run clean && npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs" }, "author": "Bugsnag", "license": "MIT", diff --git a/packages/plugin-vue/rollup.config.npm.mjs b/packages/plugin-vue/rollup.config.npm.mjs new file mode 100644 index 0000000000..e4500c64f1 --- /dev/null +++ b/packages/plugin-vue/rollup.config.npm.mjs @@ -0,0 +1,65 @@ +import babel from '@rollup/plugin-babel' +import commonjs from '@rollup/plugin-commonjs' +import nodeResolve from '@rollup/plugin-node-resolve' +import typescript from '@rollup/plugin-typescript' + +import createRollupConfig, { sharedOutput } from '../../.rollup/index.mjs' + +const plugins = [ + nodeResolve({ + browser: true + }), + commonjs(), + typescript({ + removeComments: true, + // don't output anything if there's a TS error + noEmitOnError: true, + compilerOptions: { + target: 'es2015' + } + }), + babel({ babelHelpers: 'bundled' }) +] + +const external = [] + +export default [ + createRollupConfig({ + input: 'src/index.ts', + output: [ + { + ...sharedOutput, + preserveModules: false, + entryFileNames: '[name].mjs', + format: 'esm' + } + ], + external, + plugins + }), + createRollupConfig({ + input: 'src/index.ts', + output: [ + { + ...sharedOutput, + entryFileNames: '[name].cjs', + format: 'cjs' + } + ], + external, + plugins + }), + createRollupConfig({ + input: 'src/index.ts', + output: [ + { + ...sharedOutput, + entryFileNames: 'bugsnag-vue.js', + format: 'umd', + name: 'BugsnagPluginVue' + } + ], + external, + plugins + }) +] diff --git a/packages/plugin-vue/src/index.ts b/packages/plugin-vue/src/index.ts new file mode 100644 index 0000000000..2dfe710df4 --- /dev/null +++ b/packages/plugin-vue/src/index.ts @@ -0,0 +1 @@ +export { default } from './plugin' diff --git a/packages/plugin-vue/src/index.js b/packages/plugin-vue/src/plugin.ts similarity index 65% rename from packages/plugin-vue/src/index.js rename to packages/plugin-vue/src/plugin.ts index bb116fb1d5..5787a1b318 100644 --- a/packages/plugin-vue/src/index.js +++ b/packages/plugin-vue/src/plugin.ts @@ -1,8 +1,20 @@ -const installVue2 = require('./vue2') -const installVue = require('./vue') +import installVue2 from './vue2' +import installVue from './vue' +import { Client } from '@bugsnag/core' +import { VueApp } from './types' -module.exports = class BugsnagPluginVue { - constructor (...args) { +declare global { + interface Window { + Vue: VueApp + } +} + +export default class BugsnagPluginVue { + public readonly name: string; + private readonly lazy: boolean; + private readonly Vue?: VueApp + + constructor (...args: any[]) { // Fetch Vue from the window object, if it exists const globalVue = typeof window !== 'undefined' && window.Vue @@ -15,7 +27,7 @@ module.exports = class BugsnagPluginVue { } } - load (client) { + load (client: Client) { if (this.Vue && this.Vue.config) { installVue2(this.Vue, client) return { @@ -23,17 +35,14 @@ module.exports = class BugsnagPluginVue { } } return { - install: (app) => { + install: (app: VueApp) => { if (!app) client._logger.error(new Error('@bugsnag/plugin-vue reference to Vue `app` was undefined')) installVue(app, client) }, - installVueErrorHandler: Vue => { + installVueErrorHandler: (Vue: VueApp) => { if (!Vue) client._logger.error(new Error('@bugsnag/plugin-vue reference to `Vue` was undefined')) installVue2(Vue, client) } } } } - -// add a default export for ESM modules without interop -module.exports.default = module.exports diff --git a/packages/plugin-vue/src/types.ts b/packages/plugin-vue/src/types.ts new file mode 100644 index 0000000000..ca44b88105 --- /dev/null +++ b/packages/plugin-vue/src/types.ts @@ -0,0 +1,23 @@ +export interface VueConfig { + errorHandler?: VueErrorHandler +} + +export interface VueConstructor { + config: VueConfig +} + +export interface VueApp { + use: (plugin: { install: (app: VueApp, ...options: any[]) => any }) => void + config: VueConfig +} + +export type VueErrorHandler = (err: unknown, instance: ComponentPublicInstance, info: string) => void + +export interface ComponentPublicInstance { + $parent: ComponentPublicInstance | null + $root?: ComponentPublicInstance | null + $options: { + name?: string + propsData?: unknown + } +} diff --git a/packages/plugin-vue/src/vue.js b/packages/plugin-vue/src/vue.ts similarity index 88% rename from packages/plugin-vue/src/vue.js rename to packages/plugin-vue/src/vue.ts index 636b22de7b..e13c809278 100644 --- a/packages/plugin-vue/src/vue.js +++ b/packages/plugin-vue/src/vue.ts @@ -1,9 +1,12 @@ -module.exports = (app, client) => { +import { Client } from '@bugsnag/core' +import { ComponentPublicInstance, VueConstructor, VueErrorHandler } from './types' + +export default (app: VueConstructor, client: Client) => { const prev = app.config.errorHandler - const handler = (err, vm, info) => { + const handler: VueErrorHandler = (err, vm, info) => { const handledState = { severity: 'error', unhandled: true, severityReason: { type: 'unhandledException' } } - const event = client.Event.create(err, true, handledState, 'vue error handler', 1) + const event = client.Event.create(err as Error, true, handledState, 'vue error handler', 1) // In Vue 3.4+, the info param is a link to the Vue error docs in prod, so we need to extract the error code from it // https://github.com/vuejs/core/pull/9165/commits/c261beab2c0a26e401f2c3d5eae2e4c41de6fe4d @@ -12,20 +15,20 @@ module.exports = (app, client) => { event.addMetadata('vue', { errorInfo, - component: vm ? formatComponentName(vm, true) : undefined, + component: vm ? formatComponentName(vm) : undefined, props: (vm && vm.$options) ? vm.$options.propsData : undefined }) client._notify(event) if (typeof console !== 'undefined' && typeof console.error === 'function') console.error(err) - if (typeof prev === 'function') prev.call(this, err, vm, info) + if (typeof prev === 'function') prev(err, vm, info) } app.config.errorHandler = handler } -function formatComponentName (vm) { +function formatComponentName (vm: ComponentPublicInstance) { if (vm.$parent === null) return 'App' return (vm.$options && vm.$options.name) ? vm.$options.name : 'Anonymous' } diff --git a/packages/plugin-vue/src/vue2.js b/packages/plugin-vue/src/vue2.ts similarity index 71% rename from packages/plugin-vue/src/vue2.js rename to packages/plugin-vue/src/vue2.ts index d4c1ca3570..69ecb8829a 100644 --- a/packages/plugin-vue/src/vue2.js +++ b/packages/plugin-vue/src/vue2.ts @@ -1,9 +1,12 @@ -module.exports = (Vue, client) => { +import { Client } from '@bugsnag/core' +import { VueConstructor, VueErrorHandler } from './types' + +export default (Vue: VueConstructor, client: Client) => { const prev = Vue.config.errorHandler - const handler = (err, vm, info) => { + const handler: VueErrorHandler = (err, vm, info) => { const handledState = { severity: 'error', unhandled: true, severityReason: { type: 'unhandledException' } } - const event = client.Event.create(err, true, handledState, 'vue error handler', 1) + const event = client.Event.create(err as Error, true, handledState, 'vue error handler', 1) event.addMetadata('vue', { errorInfo: info, @@ -14,14 +17,14 @@ module.exports = (Vue, client) => { client._notify(event) if (typeof console !== 'undefined' && typeof console.error === 'function') console.error(err) - if (typeof prev === 'function') prev.call(this, err, vm, info) + if (typeof prev === 'function') prev(err, vm, info) } Vue.config.errorHandler = handler } // taken and reworked from Vue.js source -const formatComponentName = (vm, includeFile) => { +export const formatComponentName = (vm: any, includeFile?: boolean) => { if (vm.$root === vm) return '' const options = typeof vm === 'function' && vm.cid != null ? vm.options @@ -42,5 +45,5 @@ const formatComponentName = (vm, includeFile) => { } // taken and reworked from Vue.js source -const classify = module.exports.classify = str => +export const classify = (str: string) => str.replace(/(?:^|[-_])(\w)/g, c => c.toUpperCase()).replace(/[-_]/g, '') diff --git a/packages/plugin-vue/test/index.test.ts b/packages/plugin-vue/test/index.test.ts index 6964142373..8a60543935 100644 --- a/packages/plugin-vue/test/index.test.ts +++ b/packages/plugin-vue/test/index.test.ts @@ -1,5 +1,5 @@ import BugsnagVuePlugin from '../src' -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' describe('bugsnag vue', () => { beforeAll(() => { diff --git a/packages/plugin-vue/tsconfig.json b/packages/plugin-vue/tsconfig.json new file mode 100644 index 0000000000..dc15483601 --- /dev/null +++ b/packages/plugin-vue/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"] +} \ No newline at end of file diff --git a/packages/plugin-vue/types/bugsnag-plugin-vue.d.ts b/packages/plugin-vue/types/bugsnag-plugin-vue.d.ts index 979077fafe..ef1381f11b 100644 --- a/packages/plugin-vue/types/bugsnag-plugin-vue.d.ts +++ b/packages/plugin-vue/types/bugsnag-plugin-vue.d.ts @@ -15,7 +15,7 @@ interface VueApp { type VueErrorHandler = (err: any, instance: any, info: any) => void -// eslint-disable-next-line @typescript-eslint/no-empty-interface + interface BugsnagPluginVue extends Plugin { } // eslint-disable-next-line @typescript-eslint/no-extraneous-class declare class BugsnagPluginVue { diff --git a/packages/plugin-window-onerror/package.json b/packages/plugin-window-onerror/package.json index 48dd13c844..317fd5f937 100644 --- a/packages/plugin-window-onerror/package.json +++ b/packages/plugin-window-onerror/package.json @@ -1,7 +1,15 @@ { "name": "@bugsnag/plugin-window-onerror", "version": "8.4.0", - "main": "onerror.js", + "main": "dist/onerror.mjs", + "types": "dist/types/onerror.d.ts", + "exports": { + ".": { + "types": "./dist/types/onerror.d.ts", + "default": "./dist/onerror.js", + "import": "./dist/onerror.mjs" + } + }, "description": "@bugsnag/js plugin to report unhandled exceptions in browsers", "homepage": "https://www.bugsnag.com/", "repository": { @@ -12,7 +20,7 @@ "access": "public" }, "files": [ - "*.js" + "dist" ], "author": "Bugsnag", "license": "MIT", @@ -21,5 +29,10 @@ }, "peerDependencies": { "@bugsnag/core": "^8.0.0" + }, + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" } } diff --git a/packages/plugin-window-onerror/rollup.config.npm.mjs b/packages/plugin-window-onerror/rollup.config.npm.mjs new file mode 100644 index 0000000000..745232c2a8 --- /dev/null +++ b/packages/plugin-window-onerror/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from '../../.rollup/index.mjs' + +export default createRollupConfig({ + input: 'src/onerror.ts', + external: [/node_modules/], +}) diff --git a/packages/plugin-window-onerror/onerror.js b/packages/plugin-window-onerror/src/onerror.ts similarity index 74% rename from packages/plugin-window-onerror/onerror.js rename to packages/plugin-window-onerror/src/onerror.ts index 0f5652fcdc..d26f9181b1 100644 --- a/packages/plugin-window-onerror/onerror.js +++ b/packages/plugin-window-onerror/src/onerror.ts @@ -2,13 +2,16 @@ * Automatically notifies Bugsnag when window.onerror is called */ -module.exports = (win = window, component = 'window onerror') => ({ +import type { Plugin, Stackframe } from '@bugsnag/core' + +export default (win = window, component = 'window onerror'): Plugin => ({ load: (client) => { - if (!client._config.autoDetectErrors) return - if (!client._config.enabledErrorTypes.unhandledExceptions) return - function onerror (messageOrEvent, url, lineNo, charNo, error) { + if (!client._config.autoDetectErrors || !client._config.enabledErrorTypes.unhandledExceptions) return + + const prevOnError = win.onerror + function onerror (messageOrEvent: string | Event, url?: string, lineNo?: number, charNo?: number, error?: Error) { // Ignore errors with no info due to CORS settings - if (lineNo === 0 && /Script error\.?/.test(messageOrEvent)) { + if (lineNo === 0 && /Script error\.?/.test(messageOrEvent.toString())) { client._logger.warn('Ignoring cross-domain or eval script error. See docs: https://tinyurl.com/yy3rn63z') } else { // any error sent to window.onerror is unhandled and has severity=error @@ -41,11 +44,13 @@ module.exports = (win = window, component = 'window onerror') => ({ const name = messageOrEvent.type ? `Event: ${messageOrEvent.type}` : 'Error' // attempt to find a message from one of the conventional properties, but // default to empty string (the event will fill it with a placeholder) + // @ts-expect-error TODO: messageOrEvent has no message or detail property const message = messageOrEvent.message || messageOrEvent.detail || '' event = client.Event.create({ name, message }, true, handledState, component, 1) // provide the original thing onerror received – not our error-like object we passed to _notify + // @ts-expect-error originalError is readonly event.originalError = messageOrEvent // include the raw input as metadata – it might contain more info than we extracted @@ -53,17 +58,20 @@ module.exports = (win = window, component = 'window onerror') => ({ } else { // Lastly, if there was no "error" parameter this event was probably from an old // browser that doesn't support that. Instead we need to generate a stacktrace. - event = client.Event.create(messageOrEvent, true, handledState, component, 1) + event = client.Event.create(messageOrEvent as string, true, handledState, component, 1) decorateStack(event.errors[0].stacktrace, url, lineNo, charNo) } client._notify(event) } - try { prevOnError.apply(this, arguments) } catch (e) {} + if (typeof prevOnError === 'function') { + try { + prevOnError.apply(win, [messageOrEvent, url, lineNo, charNo, error]) + } catch (e) {} + } } - const prevOnError = win.onerror win.onerror = onerror } }) @@ -71,18 +79,20 @@ module.exports = (win = window, component = 'window onerror') => ({ // Sometimes the stacktrace has less information than was passed to window.onerror. // This function will augment the first stackframe with any useful info that was // received as arguments to the onerror callback. -const decorateStack = (stack, url, lineNo, charNo) => { - if (!stack[0]) stack.push({}) +const decorateStack = (stack: Stackframe[], url?: string, lineNo?: number, charNo?: number) => { + if (!stack[0]) stack.push({ file: '' }) const culprit = stack[0] if (!culprit.file && typeof url === 'string') culprit.file = url if (!culprit.lineNumber && isActualNumber(lineNo)) culprit.lineNumber = lineNo if (!culprit.columnNumber) { if (isActualNumber(charNo)) { culprit.columnNumber = charNo + // @ts-expect-error event.errorCharacter does not exist on type 'Event' (deprecated) } else if (window.event && isActualNumber(window.event.errorCharacter)) { + // @ts-expect-error event.errorCharacter does not exist on type 'Event' (deprecated) culprit.columnNumber = window.event.errorCharacter } } } -const isActualNumber = (n) => typeof n === 'number' && String.call(n) !== 'NaN' +const isActualNumber = (n: unknown): n is number => typeof n === 'number' && String.call(n) !== 'NaN' diff --git a/packages/plugin-window-onerror/test/onerror.test.ts b/packages/plugin-window-onerror/test/onerror.test.ts index e0dca3f30f..8850f71852 100644 --- a/packages/plugin-window-onerror/test/onerror.test.ts +++ b/packages/plugin-window-onerror/test/onerror.test.ts @@ -1,8 +1,8 @@ /* eslint-disable jest/no-commented-out-tests */ -import plugin from '../' +import plugin from '../src/onerror' -import Client, { EventDeliveryPayload } from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' type EnhancedWindow = Window & typeof globalThis & { onerror: OnErrorEventHandlerNonNull } diff --git a/packages/plugin-window-onerror/tsconfig.json b/packages/plugin-window-onerror/tsconfig.json new file mode 100644 index 0000000000..c76cdab7cd --- /dev/null +++ b/packages/plugin-window-onerror/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "target": "ES2020" + } +} + \ No newline at end of file diff --git a/packages/plugin-window-unhandled-rejection/package.json b/packages/plugin-window-unhandled-rejection/package.json index 6b4d570e0c..c9b1ec7a70 100644 --- a/packages/plugin-window-unhandled-rejection/package.json +++ b/packages/plugin-window-unhandled-rejection/package.json @@ -1,7 +1,15 @@ { "name": "@bugsnag/plugin-window-unhandled-rejection", "version": "8.4.0", - "main": "unhandled-rejection.js", + "main": "dist/unhandled-rejection.js", + "types": "dist/types/unhandled-rejection.d.ts", + "exports": { + ".": { + "types": "./dist/types/unhandled-rejection.d.ts", + "default": "./dist/unhandled-rejection.js", + "import": "./dist/unhandled-rejection.mjs" + } + }, "description": "@bugsnag/js plugin to report unhandled promise rejections in browsers", "homepage": "https://www.bugsnag.com/", "repository": { @@ -12,7 +20,7 @@ "access": "public" }, "files": [ - "*.js" + "dist" ], "author": "Bugsnag", "license": "MIT", @@ -21,5 +29,10 @@ }, "peerDependencies": { "@bugsnag/core": "^8.0.0" + }, + "scripts": { + "build": "npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", + "clean": "rm -rf dist/*" } } diff --git a/packages/plugin-window-unhandled-rejection/rollup.config.npm.mjs b/packages/plugin-window-unhandled-rejection/rollup.config.npm.mjs new file mode 100644 index 0000000000..804cc03cff --- /dev/null +++ b/packages/plugin-window-unhandled-rejection/rollup.config.npm.mjs @@ -0,0 +1,6 @@ +import createRollupConfig from '../../.rollup/index.mjs' + +export default createRollupConfig({ + input: 'src/unhandled-rejection.ts', + external: [/node_modules/] +}) diff --git a/packages/plugin-window-unhandled-rejection/src/fix-bluebird-stacktrace.ts b/packages/plugin-window-unhandled-rejection/src/fix-bluebird-stacktrace.ts new file mode 100644 index 0000000000..8c2d46f051 --- /dev/null +++ b/packages/plugin-window-unhandled-rejection/src/fix-bluebird-stacktrace.ts @@ -0,0 +1,29 @@ +import { Stackframe } from '@bugsnag/core' + +// The stack parser on bluebird stacks in FF get a suprious first frame: +// +// Error: derp +// b@http://localhost:5000/bluebird.html:22:24 +// a@http://localhost:5000/bluebird.html:18:9 +// @http://localhost:5000/bluebird.html:14:9 +// +// results in +// […] +// 0: Object { file: "Error: derp", method: undefined, lineNumber: undefined, … } +// 1: Object { file: "http://localhost:5000/bluebird.html", method: "b", lineNumber: 22, … } +// 2: Object { file: "http://localhost:5000/bluebird.html", method: "a", lineNumber: 18, … } +// 3: Object { file: "http://localhost:5000/bluebird.html", lineNumber: 14, columnNumber: 9, … } +// +// so the following reduce/accumulator function removes such frames +// +// Bluebird pads method names with spaces so trim that too… + +// https://github.com/petkaantonov/bluebird/blob/b7f21399816d02f979fe434585334ce901dcaf44/src/debuggability.js#L568-L571 +const fixBluebirdStacktrace = (error: PromiseRejectionEvent['reason']) => (frame: Stackframe) => { + if (frame.file === error.toString()) return + if (frame.method) { + frame.method = frame.method.replace(/^\s+/, '') + } +} + +export default fixBluebirdStacktrace diff --git a/packages/plugin-window-unhandled-rejection/src/unhandled-rejection.ts b/packages/plugin-window-unhandled-rejection/src/unhandled-rejection.ts new file mode 100644 index 0000000000..a0cad85e92 --- /dev/null +++ b/packages/plugin-window-unhandled-rejection/src/unhandled-rejection.ts @@ -0,0 +1,82 @@ +import { isError } from '@bugsnag/core' +import type { Config, Plugin } from '@bugsnag/core' +import fixBluebirdStacktrace from './fix-bluebird-stacktrace' + +type Listener = (evt: PromiseRejectionEvent) => void + +let _listener: Listener | null + +interface BluebirdPromiseRejectionEvent { + detail?: { + reason: PromiseRejectionEvent['reason'] + promise: PromiseRejectionEvent['promise'] + } +} + +/* + * Automatically notifies Bugsnag when window.onunhandledrejection is called + */ +export default (win = window): Plugin => { + const plugin: Plugin = { + load: (client) => { + const config = client._config as Required + + if (!config.autoDetectErrors || !config.enabledErrorTypes.unhandledRejections) return + const listener: Listener = (ev) => { + const bluebirdEvent = ev as BluebirdPromiseRejectionEvent + + let error = ev.reason + let isBluebird = false + + // accessing properties on evt.detail can throw errors (see #394) + try { + if (bluebirdEvent.detail && bluebirdEvent.detail.reason) { + error = bluebirdEvent.detail.reason + isBluebird = true + } + } catch (e) {} + + const event = client.Event.create(error, false, { + severity: 'error', + // Report unhandled promise rejections as handled when set by config + unhandled: !config.reportUnhandledPromiseRejectionsAsHandled, + severityReason: { type: 'unhandledPromiseRejection' } + }, 'unhandledrejection handler', 1, client._logger) + + if (isBluebird) { + event.errors[0].stacktrace.map(fixBluebirdStacktrace(error)) + } + + client._notify(event, (event) => { + if (isError(event.originalError) && !event.originalError.stack) { + event.addMetadata('unhandledRejection handler', { + [Object.prototype.toString.call(event.originalError)]: { + name: event.originalError.name, + message: event.originalError.message, + // @ts-expect-error optional error.code property + code: event.originalError.code + } + }) + } + }) + } + if (typeof win.addEventListener === 'function') { + win.addEventListener('unhandledrejection', listener) + } + _listener = listener + } + } + + if (process.env.NODE_ENV !== 'production') { + plugin.destroy = (win = window) => { + if (_listener) { + if (typeof win.removeEventListener === 'function') { + win.removeEventListener('unhandledrejection', _listener) + } + } + _listener = null + } + } + + return plugin +} diff --git a/packages/plugin-window-unhandled-rejection/test/unhandled-rejection.test.ts b/packages/plugin-window-unhandled-rejection/test/unhandled-rejection.test.ts index 608c8ca6dc..1e8b2f8887 100644 --- a/packages/plugin-window-unhandled-rejection/test/unhandled-rejection.test.ts +++ b/packages/plugin-window-unhandled-rejection/test/unhandled-rejection.test.ts @@ -1,7 +1,7 @@ /* eslint-disable jest/no-commented-out-tests */ -import plugin from '../' +import plugin from '../src/unhandled-rejection' -import Client from '@bugsnag/core/client' +import { Client } from '@bugsnag/core' describe('plugin: unhandled rejection', () => { beforeEach(() => { diff --git a/packages/plugin-window-unhandled-rejection/tsconfig.json b/packages/plugin-window-unhandled-rejection/tsconfig.json new file mode 100644 index 0000000000..7ef24300eb --- /dev/null +++ b/packages/plugin-window-unhandled-rejection/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "types": ["node"], + "target": "ES2020" + } +} + \ No newline at end of file diff --git a/packages/plugin-window-unhandled-rejection/unhandled-rejection.js b/packages/plugin-window-unhandled-rejection/unhandled-rejection.js deleted file mode 100644 index 22df627f8d..0000000000 --- a/packages/plugin-window-unhandled-rejection/unhandled-rejection.js +++ /dev/null @@ -1,99 +0,0 @@ -const map = require('@bugsnag/core/lib/es-utils/map') -const isError = require('@bugsnag/core/lib/iserror') - -let _listener -/* - * Automatically notifies Bugsnag when window.onunhandledrejection is called - */ -module.exports = (win = window) => { - const plugin = { - load: (client) => { - if (!client._config.autoDetectErrors || !client._config.enabledErrorTypes.unhandledRejections) return - const listener = evt => { - let error = evt.reason - let isBluebird = false - - // accessing properties on evt.detail can throw errors (see #394) - try { - if (evt.detail && evt.detail.reason) { - error = evt.detail.reason - isBluebird = true - } - } catch (e) {} - - // Report unhandled promise rejections as handled if the user has configured it - const unhandled = !client._config.reportUnhandledPromiseRejectionsAsHandled - - const event = client.Event.create(error, false, { - severity: 'error', - unhandled, - severityReason: { type: 'unhandledPromiseRejection' } - }, 'unhandledrejection handler', 1, client._logger) - - if (isBluebird) { - map(event.errors[0].stacktrace, fixBluebirdStacktrace(error)) - } - - client._notify(event, (event) => { - if (isError(event.originalError) && !event.originalError.stack) { - event.addMetadata('unhandledRejection handler', { - [Object.prototype.toString.call(event.originalError)]: { - name: event.originalError.name, - message: event.originalError.message, - code: event.originalError.code - } - }) - } - }) - } - if ('addEventListener' in win) { - win.addEventListener('unhandledrejection', listener) - } else { - win.onunhandledrejection = (reason, promise) => { - listener({ detail: { reason, promise } }) - } - } - _listener = listener - } - } - - if (process.env.NODE_ENV !== 'production') { - plugin.destroy = (win = window) => { - if (_listener) { - if ('addEventListener' in win) { - win.removeEventListener('unhandledrejection', _listener) - } else { - win.onunhandledrejection = null - } - } - _listener = null - } - } - - return plugin -} - -// The stack parser on bluebird stacks in FF get a suprious first frame: -// -// Error: derp -// b@http://localhost:5000/bluebird.html:22:24 -// a@http://localhost:5000/bluebird.html:18:9 -// @http://localhost:5000/bluebird.html:14:9 -// -// results in -// […] -// 0: Object { file: "Error: derp", method: undefined, lineNumber: undefined, … } -// 1: Object { file: "http://localhost:5000/bluebird.html", method: "b", lineNumber: 22, … } -// 2: Object { file: "http://localhost:5000/bluebird.html", method: "a", lineNumber: 18, … } -// 3: Object { file: "http://localhost:5000/bluebird.html", lineNumber: 14, columnNumber: 9, … } -// -// so the following reduce/accumulator function removes such frames -// -// Bluebird pads method names with spaces so trim that too… -// https://github.com/petkaantonov/bluebird/blob/b7f21399816d02f979fe434585334ce901dcaf44/src/debuggability.js#L568-L571 -const fixBluebirdStacktrace = (error) => (frame) => { - if (frame.file === error.toString()) return - if (frame.method) { - frame.method = frame.method.replace(/^\s+/, '') - } -} diff --git a/packages/react-native/package.json b/packages/react-native/package.json index a3c9323b46..84dfaf8eb9 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -48,6 +48,9 @@ "license": "MIT", "devDependencies": { "@babel/cli": "^7.0.0", + "@types/jest": "^26", + "@types/react-native": "0.67.8", + "react-native": "^0.63.4", "tslint": "^5.12.1", "typescript": "^3.3.3" }, @@ -63,7 +66,8 @@ "@bugsnag/plugin-react-native-hermes": "^8.4.0", "@bugsnag/plugin-react-native-session": "^8.4.0", "@bugsnag/plugin-react-native-unhandled-rejection": "^8.4.0", - "iserror": "^0.0.2" + "iserror": "^0.0.2", + "react-native": "^0.63.4" }, "scripts": { "prepare": "bash prepare-android-vendor.sh", diff --git a/packages/react-native/src/NativeBugsnag.ts b/packages/react-native/src/NativeBugsnag.ts index 3b098cac63..dd50022156 100644 --- a/packages/react-native/src/NativeBugsnag.ts +++ b/packages/react-native/src/NativeBugsnag.ts @@ -46,14 +46,14 @@ export interface Spec extends TurboModule { updateGroupingDiscriminator(groupingDiscriminator: string | undefined | null): void } -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions + export type ReactNativeConfiguration = { reactNativeVersion?: string engine?: string notifierVersion: string } -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions + export type NativeConfiguration = { apiKey: string autoDetectErrors?: boolean @@ -75,7 +75,7 @@ export type NativeConfiguration = { endpoints: NativeEndpointConfig } -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions + export type NativeBreadcrumb = { timestamp: string message: string @@ -83,13 +83,13 @@ export type NativeBreadcrumb = { metadata?: UnsafeObject } -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions + export type NativeEndpointConfig = { notify: string sessions: string } -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions + export type NativeEnabledErrorTypes = { anrs?: boolean ndkCrashes?: boolean @@ -97,7 +97,7 @@ export type NativeEnabledErrorTypes = { unhandledRejections?: boolean } -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions + export type NativeFeatureFlag = { name: string variant?: string diff --git a/packages/react-native/src/config.js b/packages/react-native/src/config.js index 0dd03184b2..5241474e48 100644 --- a/packages/react-native/src/config.js +++ b/packages/react-native/src/config.js @@ -1,7 +1,5 @@ -const { schema } = require('@bugsnag/core/config') -const stringWithLength = require('@bugsnag/core/lib/validators/string-with-length') +const { isError, schema, stringWithLength } = require('@bugsnag/core') const rnPackage = require('react-native/package.json') -const iserror = require('iserror') const ALLOWED_IN_JS = [ 'onError', @@ -64,7 +62,7 @@ const getPrefixedConsole = () => { accum[method] = (...args) => originalConsole[method]('[bugsnag]', ...args) } else { accum[method] = (...args) => { - if (!iserror(args[0])) { + if (!isError(args[0])) { originalConsole[method]('[bugsnag]', ...args) } else { // a raw error doesn't display nicely in react native's yellow box, diff --git a/packages/react-native/src/notifier.js b/packages/react-native/src/notifier.js index 2d28b518f4..e3009d2536 100644 --- a/packages/react-native/src/notifier.js +++ b/packages/react-native/src/notifier.js @@ -19,10 +19,7 @@ const url = 'https://github.com/bugsnag/bugsnag-js' const React = require('react') -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 { Breadcrumb, Client, Event, Session } = require('@bugsnag/core') Event.__type = 'reactnativejs' diff --git a/packages/react-native/src/test/integration/handled-unhandled.test.ts b/packages/react-native/src/test/integration/handled-unhandled.test.ts index 8feb1a3de9..6dedfc9062 100644 --- a/packages/react-native/src/test/integration/handled-unhandled.test.ts +++ b/packages/react-native/src/test/integration/handled-unhandled.test.ts @@ -37,8 +37,8 @@ jest.mock('react-native', () => { }) // @ts-ignore -import rnPromise from 'promise/setimmediate' // eslint-disable-line -// eslint-disable-next-line +import rnPromise from 'promise/setimmediate' + import Bugsnag from '../../..' declare global { diff --git a/packages/react-native/tsconfig.json b/packages/react-native/tsconfig.json index 305a2e87a0..38cbf17346 100644 --- a/packages/react-native/tsconfig.json +++ b/packages/react-native/tsconfig.json @@ -1,8 +1,16 @@ { - "extends": "../../tsconfig.json", - "include": ["."], "compilerOptions": { + "allowJs": true, + "jsx": "preserve", + "noEmit": true, + "strict": true, + "esModuleInterop": true, + "target": "es5", "lib": ["es2015"], "types": ["jest", "react-native"], - } + }, + "include": ["."], + "exclude": [ + "src/NativeBugsnag.ts" + ], } \ No newline at end of file diff --git a/packages/web-worker/babel.config.js b/packages/web-worker/babel.config.js new file mode 100644 index 0000000000..235ee4504b --- /dev/null +++ b/packages/web-worker/babel.config.js @@ -0,0 +1,3 @@ +const babelConfig = require('../../babel.config.js') + +module.exports = babelConfig diff --git a/packages/web-worker/esm.config.js b/packages/web-worker/esm.config.js deleted file mode 100644 index 822cdb0c40..0000000000 --- a/packages/web-worker/esm.config.js +++ /dev/null @@ -1,34 +0,0 @@ -const path = require('path') -const pkg = require('./package.json') -const { DefinePlugin } = require('webpack') - -module.exports = { - entry: './src/notifier.js', - mode: 'production', - devtool: 'source-map', - optimization: { - minimize: false - }, - experiments: { - outputModule: true - }, - output: { - path: path.resolve(__dirname, 'dist'), - filename: 'bugsnag.web-worker.mjs', - module: true - }, - resolve: { - extensions: ['.ts', '.js'] - }, - module: { - rules: [ - { - test: /\.ts/, - loader: 'ts-loader' - } - ] - }, - plugins: [new DefinePlugin({ - __VERSION__: JSON.stringify(pkg.version) - })] -} diff --git a/packages/web-worker/package.json b/packages/web-worker/package.json index 49746be80c..097bbb1540 100644 --- a/packages/web-worker/package.json +++ b/packages/web-worker/package.json @@ -3,9 +3,16 @@ "version": "8.4.0", "description": "BugSnag error reporter for JavaScript web workers and service workers", "homepage": "https://www.bugsnag.com/", - "main": "dist/bugsnag.web-worker.js", - "module": "dist/bugsnag.web-worker.js", - "types": "types/notifier.ts", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/types/index.d.ts", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, "repository": { "type": "git", "url": "git@github.com:bugsnag/bugsnag-js.git" @@ -14,8 +21,7 @@ "access": "public" }, "files": [ - "dist", - "types" + "dist" ], "keywords": [ "worker", @@ -28,11 +34,8 @@ ], "scripts": { "clean": "rm -fr dist && mkdir dist", - "build": "npm run clean && npm run build:dist && npm run build:dist:min", - "build:dist": "webpack", - "build:dist:min": "webpack --optimization-minimize --output-filename=bugsnag.web-worker.min.js", - "build:dist:esm": "webpack --config esm.config.js", - "build:dist:esm.min": "webpack --config esm.config.js --optimization-minimize --output-filename=bugsnag.web-worker.min.mjs", + "build": "npm run clean && npm run build:npm", + "build:npm": "rollup --config rollup.config.npm.mjs", "size": "../../bin/size dist/bugsnag.web-worker.min.js", "cdn-upload": "../../bin/cdn-upload dist/*" }, @@ -46,9 +49,18 @@ "@bugsnag/plugin-client-ip": "^8.4.0", "@bugsnag/plugin-window-onerror": "^8.4.0", "@bugsnag/plugin-window-unhandled-rejection": "^8.4.0", + "@rollup/plugin-babel": "^6.0.4", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-replace": "^6.0.2", + "@rollup/plugin-terser": "^0.4.4", + "rollup": "^4.31.0", "ts-loader": "^9.4.1", "typescript": "^4.9.3", - "webpack": "^5.75.0", - "webpack-cli": "^5.0.0" + "webpack-cli": "^5.0.0", + "webpack": "^5.75.0" + }, + "dependencies": { + "@bugsnag/core": "^8.1.1" } } diff --git a/packages/web-worker/rollup.config.npm.mjs b/packages/web-worker/rollup.config.npm.mjs new file mode 100644 index 0000000000..420b2ac159 --- /dev/null +++ b/packages/web-worker/rollup.config.npm.mjs @@ -0,0 +1,69 @@ +import babel from '@rollup/plugin-babel' +import commonjs from '@rollup/plugin-commonjs' +import nodeResolve from '@rollup/plugin-node-resolve' +import replace from '@rollup/plugin-replace' +import terser from '@rollup/plugin-terser' +import typescript from '@rollup/plugin-typescript' +import fs from 'fs' + +import createRollupConfig, { sharedOutput } from '../../.rollup/index.mjs' + +const packageJson = JSON.parse(fs.readFileSync('./package.json')) + +const plugins = [ + nodeResolve({ + browser: true + }), + commonjs(), + typescript({ + removeComments: true, + // don't output anything if there's a TS error + noEmitOnError: true, + compilerOptions: { + target: 'es2015' + } + }), + babel({ babelHelpers: 'bundled' }), + replace({ + preventAssignment: true, + values: { + 'process.env.NODE_ENV': JSON.stringify('production'), + __BUGSNAG_NOTIFIER_VERSION__: packageJson.version, + } + }) +] + +export default [ + createRollupConfig({ + input: 'src/index.ts', + output: [ + { + ...sharedOutput, + preserveModules: false, + entryFileNames: '[name].js', + format: 'esm' + } + ], + plugins + }), + createRollupConfig({ + input: 'src/index-umd.ts', + output: [ + { + ...sharedOutput, + entryFileNames: 'bugsnag.web-worker.js', + format: 'umd', + name: 'Bugsnag' + }, + { + ...sharedOutput, + entryFileNames: 'bugsnag.web-worker.min.js', + format: 'umd', + compact: true, + name: 'Bugsnag', + plugins: [terser()] + } + ], + plugins + }) +] diff --git a/packages/web-worker/src/bugsnag.ts b/packages/web-worker/src/bugsnag.ts new file mode 100644 index 0000000000..ceca75886c --- /dev/null +++ b/packages/web-worker/src/bugsnag.ts @@ -0,0 +1,100 @@ +/* eslint-env worker, serviceworker */ + +import delivery from '@bugsnag/delivery-fetch' +import pluginClientIp from '@bugsnag/plugin-client-ip' +import pluginWindowOnError from '@bugsnag/plugin-window-onerror' +import pluginWindowUnhandledRejection from '@bugsnag/plugin-window-unhandled-rejection' + +// extend the base config schema with some browser-specific options +import workerConfig from './config' +import pluginBrowserDevice from '@bugsnag/plugin-browser-device' +import pluginBrowserSession from '@bugsnag/plugin-browser-session' +import pluginPreventDiscard from './prevent-discard' +import { Client, Config, BugsnagStatic, schema as baseConfig } from '@bugsnag/core' + + +export interface WorkerConfig extends Config { + collectUserIp?: boolean + generateAnonymousId?: boolean +} + +export interface WorkerBugsnagStatic extends BugsnagStatic { + start(apiKeyOrOpts: string | WorkerConfig): Client + createClient(apiKeyOrOpts: string | WorkerConfig): Client +} + +const name = 'Bugsnag Web Worker' +const url = 'https://github.com/bugsnag/bugsnag-js' +const version = '__BUGSNAG_NOTIFIER_VERSION__' + +// extend the base config schema with some worker-specific options +const schema = Object.assign({}, baseConfig, workerConfig) + +type WorkerClient = Partial & { + _client: Client | null + createClient: (opts?: Config) => Client + start: (opts?: Config) => Client + isStarted: () => boolean +} + +const notifier: WorkerClient = { + _client: null, + // @ts-expect-error + 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 = [ + pluginBrowserDevice(navigator, null), + pluginBrowserSession, + pluginClientIp, + pluginPreventDiscard, + pluginWindowOnError(self, 'worker onerror'), + pluginWindowUnhandledRejection(self) + ] + + // configure a client with user supplied options + const bugsnag = new Client(opts, schema, internalPlugins, { name, version, url }) + + bugsnag._setDelivery(client => delivery(client, self.fetch, self)) + + bugsnag._logger.debug('Loaded!') + + return bugsnag._config.autoTrackSessions + ? bugsnag.startSession() + : bugsnag + }, + start: (opts) => { + if (notifier._client) { + notifier._client._logger.warn('Bugsnag.start() was called more than once. Ignoring.') + return notifier._client + } + notifier._client = notifier.createClient(opts) + return notifier._client + }, + isStarted: () => { + return notifier._client != null + } +} + +; +// Add client functions to notifier +(Object.getOwnPropertyNames(Client.prototype)).forEach(method => { + // skip private methods + if (/^_/.test(method) || method === 'constructor') return + // @ts-expect-error + notifier[method] = function () { + if (!notifier._client) return console.log(`Bugsnag.${method}() was called before Bugsnag.start()`) + notifier._client._depth += 1 + // @ts-expect-error + const ret = notifier._client[method].apply(notifier._client, arguments) + notifier._client._depth -= 1 + return ret + } +}) + +// @ts-expect-error +const Bugsnag = notifier as WorkerBugsnagStatic + +export default Bugsnag diff --git a/packages/web-worker/src/config.js b/packages/web-worker/src/config.js deleted file mode 100644 index e27ca52f3b..0000000000 --- a/packages/web-worker/src/config.js +++ /dev/null @@ -1,38 +0,0 @@ -/* eslint-env worker, serviceworker */ - -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 = { - appType: { - ...schema.appType, - defaultValue: () => 'workerjs' - }, - logger: assign({}, schema.logger, { - defaultValue: () => - (typeof console !== 'undefined' && typeof console.debug === 'function') - ? getPrefixedConsole() - : undefined - }), - autoTrackSessions: { - ...schema.autoTrackSessions, - defaultValue: () => false - }, - autoDetectErrors: { - ...schema.autoTrackSessions, - defaultValue: () => false - } -} - -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/web-worker/src/config.ts b/packages/web-worker/src/config.ts new file mode 100644 index 0000000000..e9553d9cf4 --- /dev/null +++ b/packages/web-worker/src/config.ts @@ -0,0 +1,26 @@ +/* eslint-env worker, serviceworker */ + +import { schema } from '@bugsnag/core' + +import getPrefixedConsole from './get-prefixed-console' + +export default { + appType: { + ...schema.appType, + defaultValue: () => 'workerjs' + }, + logger: Object.assign({}, schema.logger, { + defaultValue: () => + (typeof console !== 'undefined' && typeof console.debug === 'function') + ? getPrefixedConsole() + : undefined + }), + autoTrackSessions: { + ...schema.autoTrackSessions, + defaultValue: () => false + }, + autoDetectErrors: { + ...schema.autoTrackSessions, + defaultValue: () => false + } +} diff --git a/packages/web-worker/src/get-prefixed-console.ts b/packages/web-worker/src/get-prefixed-console.ts new file mode 100644 index 0000000000..27be40dd43 --- /dev/null +++ b/packages/web-worker/src/get-prefixed-console.ts @@ -0,0 +1,16 @@ +type LoggerMethod = 'debug' | 'info' | 'warn' | 'error' + +const getPrefixedConsole = () => { + const logger: Record = {} + const consoleLog = console.log + const loggerMethods = ['debug', 'info', 'warn', 'error'] as const + loggerMethods.map((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/web-worker/src/index-umd.ts b/packages/web-worker/src/index-umd.ts new file mode 100644 index 0000000000..6040a2d5e8 --- /dev/null +++ b/packages/web-worker/src/index-umd.ts @@ -0,0 +1,7 @@ +import { Breadcrumb, Client, Event, Session } from '@bugsnag/core' + + + +import Bugsnag from './bugsnag' + +export default Object.assign(Bugsnag, { Breadcrumb, Client, Event, Session }) diff --git a/packages/web-worker/src/index.ts b/packages/web-worker/src/index.ts new file mode 100644 index 0000000000..52a70e07e2 --- /dev/null +++ b/packages/web-worker/src/index.ts @@ -0,0 +1,4 @@ +export { default } from './bugsnag' +export type { WorkerBugsnagStatic, WorkerConfig } from './bugsnag' + +export * from '@bugsnag/core' diff --git a/packages/web-worker/src/notifier.d.ts b/packages/web-worker/src/notifier.d.ts deleted file mode 100644 index 9dc84d0477..0000000000 --- a/packages/web-worker/src/notifier.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from '../types/notifier' -export * from '../types/notifier' diff --git a/packages/web-worker/src/notifier.js b/packages/web-worker/src/notifier.js deleted file mode 100644 index cd92a9469d..0000000000 --- a/packages/web-worker/src/notifier.js +++ /dev/null @@ -1,73 +0,0 @@ -/* eslint-env worker, serviceworker */ - -import Client from '@bugsnag/core/client' -import { schema as coreSchema } from '@bugsnag/core/config' -import delivery from '@bugsnag/delivery-fetch' -import pluginClientIp from '@bugsnag/plugin-client-ip' -import pluginWindowOnError from '@bugsnag/plugin-window-onerror' -import pluginWindowUnhandledRejection from '@bugsnag/plugin-window-unhandled-rejection' -import config from './config' -import pluginBrowserDevice from '@bugsnag/plugin-browser-device' -import pluginBrowserSession from '@bugsnag/plugin-browser-session' -import pluginPreventDiscard from './prevent-discard' - -const name = 'Bugsnag Web Worker' -const url = 'https://github.com/bugsnag/bugsnag-js' -const version = __VERSION__ // eslint-disable-line no-undef - -// extend the base config schema with some worker-specific options -const schema = { ...coreSchema, ...config } - -export const Bugsnag = { - 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 = [ - pluginBrowserDevice(navigator, null), - pluginBrowserSession, - pluginClientIp, - pluginPreventDiscard, - pluginWindowOnError(self, 'worker onerror'), - pluginWindowUnhandledRejection(self) - ] - - // configure a client with user supplied options - const bugsnag = new Client(opts, schema, internalPlugins, { name, version, url }) - - bugsnag._setDelivery(client => delivery(client, undefined, self)) - - bugsnag._logger.debug('Loaded!') - - 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 - } -} - -// Add client functions to notifier -Object.getOwnPropertyNames(Client.prototype).forEach(method => { - // skip private methods - if (/^_/.test(method) || method === 'constructor') return - Bugsnag[method] = function () { - if (!Bugsnag._client) return console.log(`Bugsnag.${method}() was called before Bugsnag.start()`) - Bugsnag._client._depth += 1 - const ret = Bugsnag._client[method].apply(Bugsnag._client, arguments) - Bugsnag._client._depth -= 1 - return ret - } -}) - -export default Bugsnag diff --git a/packages/web-worker/src/prevent-discard.js b/packages/web-worker/src/prevent-discard.ts similarity index 73% rename from packages/web-worker/src/prevent-discard.js rename to packages/web-worker/src/prevent-discard.ts index b8381f07d1..8a242e90af 100644 --- a/packages/web-worker/src/prevent-discard.js +++ b/packages/web-worker/src/prevent-discard.ts @@ -1,11 +1,13 @@ /* eslint-env worker, serviceworker */ +import { Client } from '@bugsnag/core' + const extensionRegex = /^(chrome|moz|safari|safari-web)-extension:/ -module.exports = { +export default { name: 'preventDiscard', - load: client => { - client.addOnError(event => { + load: (client: Client) => { + client.addOnError((event) => { event.errors.forEach(({ stacktrace }) => { stacktrace.forEach(function (frame) { frame.file = frame.file.replace(extensionRegex, '$1_extension:') diff --git a/packages/web-worker/test/notifier.test.ts b/packages/web-worker/test/notifier.test.ts index 36d26f744b..deabf02024 100644 --- a/packages/web-worker/test/notifier.test.ts +++ b/packages/web-worker/test/notifier.test.ts @@ -1,11 +1,11 @@ -import WorkerBugsnagStatic from '../src/notifier' +import WorkerBugsnagStatic from '../src/bugsnag' const API_KEY = '030bab153e7c2349be364d23b5ae93b5' const typedGlobal: any = global function getBugsnag (): typeof WorkerBugsnagStatic { - const bugsnag = require('../src/notifier').default as typeof WorkerBugsnagStatic + const bugsnag = require('../src/bugsnag').default as typeof WorkerBugsnagStatic return bugsnag } @@ -24,7 +24,6 @@ const testConfig = { beforeAll(() => { mockFetch() - typedGlobal.__VERSION__ = '' jest.spyOn(console, 'debug').mockImplementation(() => {}) jest.spyOn(console, 'warn').mockImplementation(() => {}) }) diff --git a/packages/web-worker/tsconfig.json b/packages/web-worker/tsconfig.json new file mode 100644 index 0000000000..dc15483601 --- /dev/null +++ b/packages/web-worker/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"] +} \ No newline at end of file diff --git a/packages/web-worker/types/notifier.ts b/packages/web-worker/types/notifier.ts deleted file mode 100644 index d0d61870ae..0000000000 --- a/packages/web-worker/types/notifier.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Client, Config, BugsnagStatic } from '@bugsnag/core' - -interface WorkerConfig extends Config { - collectUserIp?: boolean - generateAnonymousId?: boolean -} - -interface WorkerBugsnagStatic extends BugsnagStatic { - start(apiKeyOrOpts: string | WorkerConfig): Client - createClient(apiKeyOrOpts: string | WorkerConfig): Client -} - -declare const Bugsnag: WorkerBugsnagStatic - -export default Bugsnag -export * from '@bugsnag/core' -export { WorkerConfig } diff --git a/packages/web-worker/webpack.config.js b/packages/web-worker/webpack.config.js deleted file mode 100644 index 4bac798ce1..0000000000 --- a/packages/web-worker/webpack.config.js +++ /dev/null @@ -1,35 +0,0 @@ -const path = require('path') -const pkg = require('./package.json') -const { DefinePlugin } = require('webpack') - -module.exports = { - entry: './src/notifier.js', - mode: 'production', - devtool: 'source-map', - optimization: { - minimize: false - }, - output: { - path: path.resolve(__dirname, 'dist'), - filename: 'bugsnag.web-worker.js', - library: { - name: 'Bugsnag', - type: 'umd', - export: 'default' - } - }, - resolve: { - extensions: ['.ts', '.js'] - }, - module: { - rules: [ - { - test: /\.ts/, - loader: 'ts-loader' - } - ] - }, - plugins: [new DefinePlugin({ - __VERSION__: JSON.stringify(pkg.version) - })] -} diff --git a/test/aws-lambda/features/fixtures/hono-app/app/package.json b/test/aws-lambda/features/fixtures/hono-app/app/package.json index 4ce62b5cbe..df75679825 100644 --- a/test/aws-lambda/features/fixtures/hono-app/app/package.json +++ b/test/aws-lambda/features/fixtures/hono-app/app/package.json @@ -7,9 +7,12 @@ "@bugsnag/delivery-node": "bugsnag-delivery-node.tgz", "@bugsnag/in-flight": "bugsnag-in-flight.tgz", "@bugsnag/js": "bugsnag-js.tgz", + "@bugsnag/json-payload": "bugsnag-json-payload.tgz", "@bugsnag/node": "bugsnag-node.tgz", + "@bugsnag/path-normalizer": "bugsnag-path-normalizer.tgz", "@bugsnag/plugin-app-duration": "bugsnag-plugin-app-duration.tgz", "@bugsnag/plugin-aws-lambda": "bugsnag-plugin-aws-lambda.tgz", + "@bugsnag/plugin-browser-session": "bugsnag-plugin-browser-session.tgz", "@bugsnag/plugin-contextualize": "bugsnag-plugin-contextualize.tgz", "@bugsnag/plugin-hono": "bugsnag-plugin-hono.tgz", "@bugsnag/plugin-intercept": "bugsnag-plugin-intercept.tgz", diff --git a/test/aws-lambda/features/fixtures/serverless-express-app/app/package.json b/test/aws-lambda/features/fixtures/serverless-express-app/app/package.json index 77b6671142..a97a63570a 100644 --- a/test/aws-lambda/features/fixtures/serverless-express-app/app/package.json +++ b/test/aws-lambda/features/fixtures/serverless-express-app/app/package.json @@ -9,9 +9,12 @@ "@bugsnag/delivery-node": "bugsnag-delivery-node.tgz", "@bugsnag/in-flight": "bugsnag-in-flight.tgz", "@bugsnag/js": "bugsnag-js.tgz", + "@bugsnag/json-payload": "bugsnag-json-payload.tgz", "@bugsnag/node": "bugsnag-node.tgz", + "@bugsnag/path-normalizer": "bugsnag-path-normalizer.tgz", "@bugsnag/plugin-app-duration": "bugsnag-plugin-app-duration.tgz", "@bugsnag/plugin-aws-lambda": "bugsnag-plugin-aws-lambda.tgz", + "@bugsnag/plugin-browser-session": "bugsnag-plugin-browser-session.tgz", "@bugsnag/plugin-contextualize": "bugsnag-plugin-contextualize.tgz", "@bugsnag/plugin-express": "bugsnag-plugin-express.tgz", "@bugsnag/plugin-intercept": "bugsnag-plugin-intercept.tgz", diff --git a/test/aws-lambda/features/fixtures/simple-app/async/package.json b/test/aws-lambda/features/fixtures/simple-app/async/package.json index a5c32db16e..ac3795909c 100644 --- a/test/aws-lambda/features/fixtures/simple-app/async/package.json +++ b/test/aws-lambda/features/fixtures/simple-app/async/package.json @@ -7,9 +7,12 @@ "@bugsnag/delivery-node": "bugsnag-delivery-node.tgz", "@bugsnag/in-flight": "bugsnag-in-flight.tgz", "@bugsnag/js": "bugsnag-js.tgz", + "@bugsnag/json-payload": "bugsnag-json-payload.tgz", "@bugsnag/node": "bugsnag-node.tgz", + "@bugsnag/path-normalizer": "bugsnag-path-normalizer.tgz", "@bugsnag/plugin-app-duration": "bugsnag-plugin-app-duration.tgz", "@bugsnag/plugin-aws-lambda": "bugsnag-plugin-aws-lambda.tgz", + "@bugsnag/plugin-browser-session": "bugsnag-plugin-browser-session.tgz", "@bugsnag/plugin-contextualize": "bugsnag-plugin-contextualize.tgz", "@bugsnag/plugin-intercept": "bugsnag-plugin-intercept.tgz", "@bugsnag/plugin-node-device": "bugsnag-plugin-node-device.tgz", diff --git a/test/aws-lambda/features/fixtures/simple-app/callback/package.json b/test/aws-lambda/features/fixtures/simple-app/callback/package.json index a5c32db16e..ac3795909c 100644 --- a/test/aws-lambda/features/fixtures/simple-app/callback/package.json +++ b/test/aws-lambda/features/fixtures/simple-app/callback/package.json @@ -7,9 +7,12 @@ "@bugsnag/delivery-node": "bugsnag-delivery-node.tgz", "@bugsnag/in-flight": "bugsnag-in-flight.tgz", "@bugsnag/js": "bugsnag-js.tgz", + "@bugsnag/json-payload": "bugsnag-json-payload.tgz", "@bugsnag/node": "bugsnag-node.tgz", + "@bugsnag/path-normalizer": "bugsnag-path-normalizer.tgz", "@bugsnag/plugin-app-duration": "bugsnag-plugin-app-duration.tgz", "@bugsnag/plugin-aws-lambda": "bugsnag-plugin-aws-lambda.tgz", + "@bugsnag/plugin-browser-session": "bugsnag-plugin-browser-session.tgz", "@bugsnag/plugin-contextualize": "bugsnag-plugin-contextualize.tgz", "@bugsnag/plugin-intercept": "bugsnag-plugin-intercept.tgz", "@bugsnag/plugin-node-device": "bugsnag-plugin-node-device.tgz", diff --git a/test/aws-lambda/features/scripts/build-fixtures b/test/aws-lambda/features/scripts/build-fixtures index f23e64e577..d340680103 100755 --- a/test/aws-lambda/features/scripts/build-fixtures +++ b/test/aws-lambda/features/scripts/build-fixtures @@ -19,9 +19,12 @@ BUGSNAG_PACKAGES = [ "delivery-node", "in-flight", "js", + "json-payload", "node", + "path-normalizer", "plugin-app-duration", "plugin-aws-lambda", + "plugin-browser-session", "plugin-contextualize", "plugin-express", "plugin-hono", @@ -32,7 +35,7 @@ BUGSNAG_PACKAGES = [ "plugin-node-uncaught-exception", "plugin-node-unhandled-rejection", "plugin-server-session", - "plugin-strip-project-root", + "plugin-strip-project-root" ] def heading(message) diff --git a/test/browser/features/cause.feature b/test/browser/features/cause.feature index 206bdbcf12..26ba3aba25 100644 --- a/test/browser/features/cause.feature +++ b/test/browser/features/cause.feature @@ -1,4 +1,4 @@ -@skip_ie_8 @skip_ie_9 @skip_ie_10 @skip_ie_11 @skip_firefox_30 @skip_safari_6 +@skip_ie_11 @skip_firefox_30 @skip_safari_6 Feature: Error.cause Scenario: Error thrown with an assigned Error cause property diff --git a/test/browser/features/fixtures/handled/webpack3/.npmrc b/test/browser/features/fixtures/handled/webpack3/.npmrc deleted file mode 100644 index e9ee3cb4d0..0000000000 --- a/test/browser/features/fixtures/handled/webpack3/.npmrc +++ /dev/null @@ -1 +0,0 @@ -legacy-peer-deps=true \ No newline at end of file diff --git a/test/browser/features/fixtures/handled/webpack3/a.html b/test/browser/features/fixtures/handled/webpack3/a.html deleted file mode 100644 index 8f9a1579ce..0000000000 --- a/test/browser/features/fixtures/handled/webpack3/a.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/test/browser/features/fixtures/handled/webpack3/b.html b/test/browser/features/fixtures/handled/webpack3/b.html deleted file mode 100644 index 1368a97048..0000000000 --- a/test/browser/features/fixtures/handled/webpack3/b.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/test/browser/features/fixtures/handled/webpack3/c.html b/test/browser/features/fixtures/handled/webpack3/c.html deleted file mode 100644 index 783438d392..0000000000 --- a/test/browser/features/fixtures/handled/webpack3/c.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - -
PENDING
- - - diff --git a/test/browser/features/fixtures/handled/webpack3/package.json b/test/browser/features/fixtures/handled/webpack3/package.json deleted file mode 100644 index 0c361d4cad..0000000000 --- a/test/browser/features/fixtures/handled/webpack3/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "bugsnag-js-fixtures-handled-webpack3", - "private": true, - "scripts": { - "build": "webpack" - }, - "dependencies": { - "es3ify-webpack-plugin": "0.0.1", - "uglifyjs-webpack-plugin": "2.0.1", - "webpack": "^3.11.0" - } -} diff --git a/test/browser/features/fixtures/handled/webpack3/src/a.js b/test/browser/features/fixtures/handled/webpack3/src/a.js deleted file mode 100644 index 4e483c6215..0000000000 --- a/test/browser/features/fixtures/handled/webpack3/src/a.js +++ /dev/null @@ -1,6 +0,0 @@ -var Bugsnag = require('@bugsnag/browser') -var config = require('./lib/config') - -Bugsnag.start(config) - -Bugsnag.notify(new Error('bad things')) diff --git a/test/browser/features/fixtures/handled/webpack3/src/b.js b/test/browser/features/fixtures/handled/webpack3/src/b.js deleted file mode 100644 index 8790ec95e1..0000000000 --- a/test/browser/features/fixtures/handled/webpack3/src/b.js +++ /dev/null @@ -1,10 +0,0 @@ -var Bugsnag = require('@bugsnag/browser') -var config = require('./lib/config') - -Bugsnag.start(config) - -try { - foo.bar() -} catch (e) { - Bugsnag.notify(e) -} diff --git a/test/browser/features/fixtures/handled/webpack3/src/c.js b/test/browser/features/fixtures/handled/webpack3/src/c.js deleted file mode 100644 index 52b3fd5c1d..0000000000 --- a/test/browser/features/fixtures/handled/webpack3/src/c.js +++ /dev/null @@ -1,14 +0,0 @@ -var Bugsnag = require('@bugsnag/browser') -var config = require('./lib/config') - -Bugsnag.start(config) - -go() - .then(function () {}) - .catch(function (e) { - Bugsnag.notify(e) - }) - -function go() { - return Promise.reject(new Error('bad things')) -} diff --git a/test/browser/features/fixtures/handled/webpack3/src/lib/config.js b/test/browser/features/fixtures/handled/webpack3/src/lib/config.js deleted file mode 100644 index 71d315c1d2..0000000000 --- a/test/browser/features/fixtures/handled/webpack3/src/lib/config.js +++ /dev/null @@ -1,7 +0,0 @@ - -var NOTIFY = decodeURIComponent(window.location.search.match(/NOTIFY=([^&]+)/)[1]) -var SESSIONS = decodeURIComponent(window.location.search.match(/SESSIONS=([^&]+)/)[1]) -var API_KEY = decodeURIComponent(window.location.search.match(/API_KEY=([^&]+)/)[1]) - -exports.apiKey = API_KEY -exports.endpoints = { notify: NOTIFY, sessions: SESSIONS } diff --git a/test/browser/features/fixtures/handled/webpack3/webpack.config.js b/test/browser/features/fixtures/handled/webpack3/webpack.config.js deleted file mode 100644 index 2bfe48512b..0000000000 --- a/test/browser/features/fixtures/handled/webpack3/webpack.config.js +++ /dev/null @@ -1,16 +0,0 @@ -const path = require('path') -const webpack = require('webpack') -const es3ifyPlugin = require('es3ify-webpack-plugin') - -module.exports = { - entry: { a: './src/a.js', b: './src/b.js', c: './src/c.js' }, - devtool: 'sourcemap', - output: { - path: path.resolve(__dirname, 'dist'), - filename: '[name].js' - }, - plugins: [ - new es3ifyPlugin(), - new webpack.optimize.UglifyJsPlugin({ compress: false, mangle: false, ie8: true }) - ] -} diff --git a/test/browser/features/fixtures/plugin_react/webpack4/package.json b/test/browser/features/fixtures/plugin_react/webpack4/package.json index 3ae2592f74..6c2b1a22b2 100644 --- a/test/browser/features/fixtures/plugin_react/webpack4/package.json +++ b/test/browser/features/fixtures/plugin_react/webpack4/package.json @@ -9,6 +9,8 @@ "@babel/preset-env": "^7.18.10", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.18.6", + "@types/react": "^16.9.49", + "@types/react-dom": "^16.9.16", "babel-loader": "^8.2.5", "react": "^16.5.0", "react-dom": "^16.5.0", diff --git a/test/browser/features/fixtures/plugin_react/webpack4/src/lib/config.ts b/test/browser/features/fixtures/plugin_react/webpack4/src/lib/config.ts index 53d2a646ce..5aca7cb069 100644 --- a/test/browser/features/fixtures/plugin_react/webpack4/src/lib/config.ts +++ b/test/browser/features/fixtures/plugin_react/webpack4/src/lib/config.ts @@ -7,4 +7,4 @@ var API_KEY = decodeURIComponent(window.location.search.match(/API_KEY=([^&]+)/) export const apiKey = API_KEY export const endpoints = { notify: NOTIFY, sessions: SESSIONS } -export const plugins = [new BugsnagReactPlugin(React)] +export const plugins = [new BugsnagReactPlugin(React as any)] diff --git a/test/browser/features/fixtures/typescript/3_8/.gitignore b/test/browser/features/fixtures/typescript/3_8/.gitignore new file mode 100644 index 0000000000..3c25e1e49c --- /dev/null +++ b/test/browser/features/fixtures/typescript/3_8/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.log diff --git a/test/browser/features/fixtures/typescript/3_8/index.html b/test/browser/features/fixtures/typescript/3_8/index.html new file mode 100644 index 0000000000..cbc4fd8e86 --- /dev/null +++ b/test/browser/features/fixtures/typescript/3_8/index.html @@ -0,0 +1,13 @@ + + + + + TypeScript 3.8 Compatibility Test + + + +

TypeScript 3.8 Compatibility Test

+

This test verifies that Bugsnag's TypeScript definitions work with TypeScript 3.8.

+ + + diff --git a/test/browser/features/fixtures/typescript/3_8/package.json b/test/browser/features/fixtures/typescript/3_8/package.json new file mode 100644 index 0000000000..4235b1fc01 --- /dev/null +++ b/test/browser/features/fixtures/typescript/3_8/package.json @@ -0,0 +1,16 @@ +{ + "name": "bugsnag-js-fixtures-typescript-3-8", + "private": true, + "scripts": { + "build": "npm run build:iife", + "build:iife": "rollup -c rollup.config.js", + "clean": "rm -rf dist" + }, + "devDependencies": { + "typescript": "~3.8.0", + "rollup": "^2.79.1", + "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-node-resolve": "^5.2.0", + "@rollup/plugin-typescript": "^8.5.0" + } +} diff --git a/test/browser/features/fixtures/typescript/3_8/rollup.config.js b/test/browser/features/fixtures/typescript/3_8/rollup.config.js new file mode 100644 index 0000000000..3dc7c1cc2e --- /dev/null +++ b/test/browser/features/fixtures/typescript/3_8/rollup.config.js @@ -0,0 +1,36 @@ +import resolve from 'rollup-plugin-node-resolve' +import commonjs from 'rollup-plugin-commonjs' +import typescript from '@rollup/plugin-typescript' + +export default { + input: `src/main.ts`, + output: { + file: `dist/bundle.js`, + format: 'iife', + name: 'BugsnagTS38Test', + globals: { + '@bugsnag/browser': 'Bugsnag' + } + }, + external: ['@bugsnag/browser'], + plugins: [ + typescript({ + tsconfig: './tsconfig.json', + sourceMap: false, + inlineSources: false, + module: 'es2015', + compilerOptions: { + types: [] + } + }), + resolve({ + browser: true, + preferBuiltins: false, + mainFields: ['browser', 'module', 'main'] + }), + commonjs({ + include: ['node_modules/**'], + exclude: ['src/**'] + }) + ] +}; diff --git a/test/browser/features/fixtures/typescript/3_8/src/lib/config.ts b/test/browser/features/fixtures/typescript/3_8/src/lib/config.ts new file mode 100644 index 0000000000..85a1247c16 --- /dev/null +++ b/test/browser/features/fixtures/typescript/3_8/src/lib/config.ts @@ -0,0 +1,5 @@ +const NOTIFY = decodeURIComponent(window.location.search.match(/NOTIFY=([^&]+)/)![1]) +const SESSIONS = decodeURIComponent(window.location.search.match(/SESSIONS=([^&]+)/)![1]) +const API_KEY = decodeURIComponent(window.location.search.match(/API_KEY=([^&]+)/)![1]) +const config = { endpoints: { notify: NOTIFY, sessions: SESSIONS }, apiKey: API_KEY } +export default config diff --git a/test/browser/features/fixtures/typescript/3_8/src/main.ts b/test/browser/features/fixtures/typescript/3_8/src/main.ts new file mode 100644 index 0000000000..0fc1f4ee83 --- /dev/null +++ b/test/browser/features/fixtures/typescript/3_8/src/main.ts @@ -0,0 +1,14 @@ +import Bugsnag from '@bugsnag/browser' +import config from './lib/config' + +// Start Bugsnag +Bugsnag.start(config) + +// Leave a breadcrumb to demonstrate breadcrumb functionality +Bugsnag.leaveBreadcrumb('TypeScript 3.8 test fixture loaded', { + timestamp: new Date().toISOString(), + version: 'TypeScript 3.8' +}) + +// Report an error to demonstrate error reporting +Bugsnag.notify(new Error('TypeScript 3.8 compatibility test error')) diff --git a/test/browser/features/fixtures/typescript/3_8/tsconfig.json b/test/browser/features/fixtures/typescript/3_8/tsconfig.json new file mode 100644 index 0000000000..63e2aab1c8 --- /dev/null +++ b/test/browser/features/fixtures/typescript/3_8/tsconfig.json @@ -0,0 +1,70 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./dist/bundle.js", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/test/browser/features/handled_errors.feature b/test/browser/features/handled_errors.feature index 82b8f682c0..fa157aabb9 100644 --- a/test/browser/features/handled_errors.feature +++ b/test/browser/features/handled_errors.feature @@ -14,7 +14,7 @@ Scenario Outline: calling notify() with Error Examples: | type | | script | - | webpack3 | + # | webpack3 | | webpack4 | | browserify | | rollup | @@ -31,7 +31,7 @@ Scenario Outline: calling notify() with Error within try/catch Examples: | type | | script | - | webpack3 | + # | webpack3 | | webpack4 | | browserify | | rollup | @@ -50,7 +50,7 @@ Scenario Outline: calling notify() with Error within Promise catch Examples: | type | | script | - | webpack3 | + # | webpack3 | | webpack4 | | browserify | | rollup | diff --git a/test/browser/features/plugin_react.feature b/test/browser/features/plugin_react.feature index 129f08c629..b3365d4948 100644 --- a/test/browser/features/plugin_react.feature +++ b/test/browser/features/plugin_react.feature @@ -1,4 +1,4 @@ -@plugin_react +@plugin_react @skip_chrome_43 Feature: React support Scenario: basic error boundary usage diff --git a/test/browser/features/support/skip.rb b/test/browser/features/support/skip.rb index da79e7d568..da61961afe 100644 --- a/test/browser/features/support/skip.rb +++ b/test/browser/features/support/skip.rb @@ -19,3 +19,12 @@ Before('@skip_http') do |_scenario| skip_this_scenario("Skipping scenario") if Maze.config.https == false end + + +["chrome", "firefox", "safari", "edge"].each do |browser| + 1.upto(1_000) do |version| + Before("@skip_#{browser}_#{version}") do + skip_this_scenario("Skipping scenario: Not supported") if Maze.config.browser == "#{browser}_#{version}" + end + end +end diff --git a/test/browser/features/typescript.feature b/test/browser/features/typescript.feature new file mode 100644 index 0000000000..7076a87d1e --- /dev/null +++ b/test/browser/features/typescript.feature @@ -0,0 +1,12 @@ +Feature: Compatibility with TypeScript + +@skip_safari_10 +Scenario Outline: TypeScript does not error + When I navigate to the test URL "/typescript//index.html" + And I wait to receive an error + Then the error is a valid browser payload for the error reporting API + And the error payload field "events.0.exceptions.0.message" equals "TypeScript 3.8 compatibility test error" + + Examples: + | version | directory | + | 3.8 | 3_8 | diff --git a/test/browser/features/web_worker.feature b/test/browser/features/web_worker.feature index 36bb588f62..d1d4ad9538 100644 --- a/test/browser/features/web_worker.feature +++ b/test/browser/features/web_worker.feature @@ -1,8 +1,5 @@ -# browsers that do not support web workers -@skip_ie_8 @skip_ie_9 - # browsers that currently throw errors in our test fixtures -@skip_ie_10 @skip_ie_11 @skip_chrome_43 @skip_edge_17 @skip_safari_10 @skip_before_ios_12 +@skip_ie_11 @skip_chrome_43 @skip_edge_17 @skip_safari_10 @skip_before_ios_12 Feature: worker notifier diff --git a/test/electron/features/support/utils/payload-matchers.js b/test/electron/features/support/utils/payload-matchers.js index f55886e0e7..40d982ee02 100644 --- a/test/electron/features/support/utils/payload-matchers.js +++ b/test/electron/features/support/utils/payload-matchers.js @@ -34,7 +34,7 @@ const compareExact = (expected, actual, path) => { /* Assert value is an expected type */ const compareType = (expected, actual, path) => { - // eslint-disable-next-line valid-typeof + if (typeof actual !== expected) { return [{ path, expected, actual, message: `Expected an item of '${expected}' type` }] } diff --git a/tsconfig.json b/tsconfig.json index 08c8cd0543..38784b57a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,77 +2,25 @@ "compilerOptions": { /* Basic Options */ "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - "lib": [ "dom", "es2015" ], /* Specify library files to be included in the compilation. */ + "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + "moduleResolution": "bundler", + "lib": [ "dom", "esnext" ], /* Specify library files to be included in the compilation. */ "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - // "declaration": true, /* Generates corresponding '.d.ts' file. */ - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./", /* Concatenate and emit output to single file. */ - // "outDir": "./", /* Redirect output structure to the directory. */ - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "removeComments": true, /* Do not emit comments to output. */ "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - - /* Strict Type-Checking Options */ "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* Enable strict null checks. */ - // "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - - /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - - /* Module Resolution Options */ - // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [ /* List of folders to include type definitions from. */ - // "node_modules/@types", - // "packages/core/types" - // ], - "types": ["jest"], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - - /* Source Map Options */ - // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "types": ["jest"], /* Type declaration files to be included in compilation. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ }, "include": [ "packages/core", "packages/delivery-node", "packages/delivery-react-native", - "packages/delivery-x-domain-request", - "packages/delivery-xml-http-request", "packages/in-flight", - "packages/plugin-app-duration", "packages/plugin-aws-lambda", - "packages/plugin-browser-context", - "packages/plugin-browser-device", "packages/plugin-contextualize", "packages/plugin-navigation-breadcrumbs", - "packages/plugin-network-breadcrumbs", "packages/plugin-server-session", "packages/plugin-react", "packages/plugin-vue", @@ -80,25 +28,17 @@ "packages/plugin-koa", "packages/plugin-restify", "packages/node", - "packages/plugin-client-ip", - "packages/plugin-window-unhandled-rejection", - "packages/plugin-window-onerror", - "packages/plugin-strip-query-string", "packages/plugin-strip-project-root", "packages/plugin-interaction-breadcrumbs", - "packages/plugin-browser-request", - "packages/plugin-simple-throttle", "packages/plugin-intercept", "packages/plugin-node-unhandled-rejection", "packages/plugin-node-in-project", "packages/plugin-node-device", "packages/plugin-node-surrounding-code", "packages/plugin-node-uncaught-exception", - "packages/plugin-console-breadcrumbs", - "packages/plugin-browser-session", "packages/browser" ], "exclude": [ - "packages/react-native/src/NativeBugsnag.ts" + "packages/**/dist" ] }