diff --git a/.craft.yml b/.craft.yml index 44d48ec03684..c28e4777a220 100644 --- a/.craft.yml +++ b/.craft.yml @@ -2,16 +2,20 @@ minVersion: '0.23.1' changelogPolicy: simple preReleaseCommand: bash scripts/craft-pre-release.sh targets: - - name: aws-lambda-layer - includeNames: /^sentry-node-serverless-\d+.\d+.\d+(-(beta|alpha)\.\d+)?\.zip$/ - layerName: SentryNodeServerlessSDK - compatibleRuntimes: - - name: node - versions: - - nodejs10.x - - nodejs12.x - - nodejs14.x - license: MIT + # + # Deactivated for now. This needs to be reactivated if the new Sentry Lambda Extension is deployed to production. + # (ask Anton Pirker if you have questions.) + # + # - name: aws-lambda-layer + # includeNames: /^sentry-node-serverless-\d+.\d+.\d+(-(beta|alpha)\.\d+)?\.zip$/ + # layerName: SentryNodeServerlessSDK + # compatibleRuntimes: + # - name: node + # versions: + # - nodejs10.x + # - nodejs12.x + # - nodejs14.x + # license: MIT - name: gcs includeNames: /.*\.js.*$/ bucket: sentry-js-sdk diff --git a/.eslintrc.js b/.eslintrc.js index a081c38c7ade..eb8e6e8faca8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,7 +10,16 @@ module.exports = { ecmaVersion: 2018, }, extends: ['@sentry-internal/sdk'], - ignorePatterns: ['coverage/**', 'build/**', 'dist/**', 'esm/**', 'examples/**', 'test/manual/**', 'types/**'], + ignorePatterns: [ + 'coverage/**', + 'build/**', + 'dist/**', + 'cjs/**', + 'esm/**', + 'examples/**', + 'test/manual/**', + 'types/**', + ], overrides: [ { files: ['*.ts', '*.tsx', '*.d.ts'], @@ -24,6 +33,12 @@ module.exports = { project: ['tsconfig.test.json'], }, }, + { + files: ['jest/**/*.ts', 'scripts/**/*.ts'], + parserOptions: { + project: ['tsconfig.dev.json'], + }, + }, { files: ['*.tsx'], rules: { @@ -33,7 +48,7 @@ module.exports = { }, }, { - files: ['scenarios/**'], + files: ['scenarios/**', 'rollup/**'], parserOptions: { sourceType: 'module', }, diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a8f026b2729f..9765416a614c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,35 +12,68 @@ on: required: false env: + DEFAULT_NODE_VERSION: '16' + HEAD_COMMIT: ${{ github.event.inputs.commit || github.sha }} CACHED_DEPENDENCY_PATHS: | ${{ github.workspace }}/node_modules ${{ github.workspace }}/packages/**/node_modules ~/.cache/ms-playwright/ + ~/.cache/mongodb-binaries/ # DEPENDENCY_CACHE_KEY: can't be set here because we don't have access to yarn.lock + # packages/utils/cjs and packages/utils/esm: Symlinks to the folders inside of `build`, needed for tests CACHED_BUILD_PATHS: | - ${{ github.workspace }}/packages/**/build - ${{ github.workspace }}/packages/**/dist - ${{ github.workspace }}/packages/**/esm + ${{ github.workspace }}/packages/*/build ${{ github.workspace }}/packages/ember/*.d.ts ${{ github.workspace }}/packages/ember/instance-initializers - ${{ github.workspace }}/packages/serverless/dist-awslambda-layer/*.zip + ${{ github.workspace }}/packages/gatsby/*.d.ts + ${{ github.workspace }}/packages/core/src/version.ts + ${{ github.workspace }}/packages/serverless + ${{ github.workspace }}/packages/utils/cjs + ${{ github.workspace }}/packages/utils/esm BUILD_CACHE_KEY: ${{ github.event.inputs.commit || github.sha }} jobs: + job_get_metadata: + name: Get Metadata + runs-on: ubuntu-latest + steps: + - name: Check out current commit + uses: actions/checkout@v2 + with: + ref: ${{ env.HEAD_COMMIT }} + # We need to check out not only the fake merge commit between the PR and the base branch which GH creates, but + # also its parents, so that we can pull the commit message from the head commit of the PR + fetch-depth: 2 + - name: Get metadata + id: get_metadata + # We need to try a number of different options for finding the head commit, because each kind of trigger event + # stores it in a different location + run: | + COMMIT_SHA=$(git rev-parse --short ${{ github.event.pull_request.head.sha || github.event.head_commit.id || env.HEAD_COMMIT }}) + echo "COMMIT_SHA=$COMMIT_SHA" >> $GITHUB_ENV + echo "COMMIT_MESSAGE=$(git log -n 1 --pretty=format:%s $COMMIT_SHA)" >> $GITHUB_ENV + outputs: + commit_label: "${{ env.COMMIT_SHA }}: ${{ env.COMMIT_MESSAGE }}" + job_install_deps: name: Install Dependencies + needs: job_get_metadata runs-on: ubuntu-latest timeout-minutes: 15 steps: - - name: Check out current commit (${{ env.HEAD_COMMIT }}) + - name: "Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})" uses: actions/checkout@v2 + with: + ref: ${{ env.HEAD_COMMIT }} - name: Set up Node uses: actions/setup-node@v1 + with: + node-version: ${{ env.DEFAULT_NODE_VERSION }} # we use a hash of yarn.lock as our cache key, because if it hasn't changed, our dependencies haven't changed, # so no need to reinstall them - name: Compute dependency cache key @@ -60,14 +93,19 @@ jobs: job_build: name: Build - needs: job_install_deps + needs: [ job_get_metadata, job_install_deps ] runs-on: ubuntu-latest timeout-minutes: 20 steps: - - name: Check out current commit (${{ env.HEAD_COMMIT }}) + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v2 + with: + ref: ${{ env.HEAD_COMMIT }} - name: Set up Node uses: actions/setup-node@v1 + with: + # ember won't build under node 16, at least not with the versions of the ember build tools we use + node-version: '14' - name: Check dependency cache uses: actions/cache@v2 with: @@ -86,31 +124,67 @@ jobs: # this file) to a constant and skip rebuilding all of the packages each time CI runs. if: steps.cache_built_packages.outputs.cache-hit == '' run: yarn build - # We are performing a `prepublishOnly` step manually because build workflow is not responsible for publishing - # the actual release. It only creates artifacts which then are uploaded and used by another workflow. - # Because of that, any `prepublishOnly` script is skipped and files it produces are not included in the tarball. - # We also cannot use `prepare` script which would be more suited, because it's run only before `pack` is called, - # and it's done from a `release` workflow and not here. - - name: Run prepublishOnly script - if: startsWith(github.ref, 'refs/heads/release/') - run: yarn prepublishOnly outputs: # this needs to be passed on, because the `needs` context only looks at direct ancestors (so steps which depend on # `job_build` can't see `job_install_deps` and what it returned) dependency_cache_key: ${{ needs.job_install_deps.outputs.dependency_cache_key }} + job_pack_aws_lambda_layer: + name: Pack and Upload AWS Lambda Layer + needs: [job_get_metadata, job_build] + runs-on: ubuntu-latest + steps: + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) + uses: actions/checkout@v2 + with: + ref: ${{ env.HEAD_COMMIT }} + - name: Set up Node + uses: actions/setup-node@v1 + with: + node-version: ${{ env.DEFAULT_NODE_VERSION }} + - name: Check dependency cache + uses: actions/cache@v2 + with: + path: ${{ env.CACHED_DEPENDENCY_PATHS }} + key: ${{ needs.job_build.outputs.dependency_cache_key }} + - name: Check build cache + uses: actions/cache@v2 + with: + path: ${{ env.CACHED_BUILD_PATHS }} + key: ${{ env.BUILD_CACHE_KEY }} + - name: Get SDK version + # `jq` reads JSON files, and `tee` pipes its input to the given location and to stdout. (Adding `-a` is the + # equivalent of using >> rather than >.) + run: | + export SDK_VERSION=$(cat packages/core/package.json | jq --raw-output '.version') + echo "SDK_VERSION=$SDK_VERSION" | tee -a $GITHUB_ENV + - name: Move dist-serverless to root directory (requirement for zipping action) + run: | + mv ./packages/serverless/build/aws/dist-serverless . + - name: Create and upload final zip file + uses: getsentry/action-build-aws-lambda-extension@v1 + with: + artifact_name: ${{ env.HEAD_COMMIT }} + zip_file_name: sentry-node-serverless-${{ env.SDK_VERSION }}.zip + build_cache_paths: ${{ env.CACHED_BUILD_PATHS }} + build_cache_key: ${{ env.BUILD_CACHE_KEY }} + job_size_check: name: Size Check - needs: job_build + needs: [job_get_metadata, job_build] timeout-minutes: 15 runs-on: ubuntu-latest + # Size Check will error out outside of the context of a PR + if: ${{ github.event_name == 'pull_request' }} steps: - - name: Check out current commit (${{ env.HEAD_COMMIT }}) + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v2 + with: + ref: ${{ env.HEAD_COMMIT }} - name: Set up Node uses: actions/setup-node@v1 with: - node-version: '12' + node-version: ${{ env.DEFAULT_NODE_VERSION }} - name: Check dependency cache uses: actions/cache@v2 with: @@ -131,14 +205,18 @@ jobs: job_lint: name: Lint - needs: job_build + needs: [job_get_metadata, job_build] timeout-minutes: 10 runs-on: ubuntu-latest steps: - - name: Check out current commit (${{ env.HEAD_COMMIT }}) + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v2 + with: + ref: ${{ env.HEAD_COMMIT }} - name: Set up Node uses: actions/setup-node@v1 + with: + node-version: ${{ env.DEFAULT_NODE_VERSION }} - name: Check dependency cache uses: actions/cache@v2 with: @@ -154,14 +232,18 @@ jobs: job_circular_dep_check: name: Circular Dependency Check - needs: job_build + needs: [job_get_metadata, job_build] timeout-minutes: 10 runs-on: ubuntu-latest steps: - - name: Check out current commit (${{ env.HEAD_COMMIT }}) + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v2 + with: + ref: ${{ env.HEAD_COMMIT }} - name: Set up Node uses: actions/setup-node@v1 + with: + node-version: ${{ env.DEFAULT_NODE_VERSION }} - name: Check dependency cache uses: actions/cache@v2 with: @@ -177,15 +259,19 @@ jobs: job_artifacts: name: Upload Artifacts - needs: job_build + needs: [job_get_metadata, job_build] runs-on: ubuntu-latest # Build artifacts are only needed for releasing workflow. if: startsWith(github.ref, 'refs/heads/release/') steps: - - name: Check out current commit (${{ github.sha }}) + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v2 + with: + ref: ${{ env.HEAD_COMMIT }} - name: Set up Node uses: actions/setup-node@v1 + with: + node-version: ${{ env.DEFAULT_NODE_VERSION }} - name: Check dependency cache uses: actions/cache@v2 with: @@ -207,20 +293,21 @@ jobs: ${{ github.workspace }}/packages/integrations/build/bundles/** ${{ github.workspace }}/packages/tracing/build/bundles/** ${{ github.workspace }}/packages/**/*.tgz - ${{ github.workspace }}/packages/serverless/dist-awslambda-layer/*.zip job_unit_test: name: Test (Node ${{ matrix.node }}) - needs: job_build + needs: [job_get_metadata, job_build] continue-on-error: true timeout-minutes: 30 runs-on: ubuntu-latest strategy: matrix: - node: [6, 8, 10, 12, 14, 16] + node: [8, 10, 12, 14, 16, 18] steps: - - name: Check out current commit (${{ env.HEAD_COMMIT }}) + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v2 + with: + ref: ${{ env.HEAD_COMMIT }} - name: Set up Node uses: actions/setup-node@v1 with: @@ -238,22 +325,26 @@ jobs: - name: Run tests env: NODE_VERSION: ${{ matrix.node }} - run: yarn test-ci + run: | + [[ $NODE_VERSION == 8 ]] && yarn add --dev --ignore-engines --ignore-scripts --ignore-workspace-root-check ts-node@8.x + yarn test-ci - name: Compute test coverage uses: codecov/codecov-action@v1 job_nextjs_integration_test: name: Test @sentry/nextjs on (Node ${{ matrix.node }}) - needs: job_build + needs: [job_get_metadata, job_build] continue-on-error: true timeout-minutes: 30 runs-on: ubuntu-latest strategy: matrix: - node: [10, 12, 14, 16] + node: [10, 12, 14, 16, 18] steps: - - name: Check out current commit (${{ env.HEAD_COMMIT }}) + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v2 + with: + ref: ${{ env.HEAD_COMMIT }} - name: Set up Node uses: actions/setup-node@v1 with: @@ -279,18 +370,19 @@ jobs: # separate job allows them to run in parallel with the other tests. job_ember_tests: name: Test @sentry/ember - needs: job_build + needs: [job_get_metadata, job_build] continue-on-error: true timeout-minutes: 30 runs-on: ubuntu-latest steps: - - name: Check out current commit (${{ env.HEAD_COMMIT }}) + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v2 - # TODO: removing `fetch-depth` below seems to have no effect, and the commit which added it had no description, - # so it's not clear why it's necessary. That said, right now ember tests are xfail, so it's a little hard to - # tell if it's safe to remove. Once ember tests are fixed, let's try again with it turned off, and if all goes - # well, we can pull it out. with: + ref: ${{ env.HEAD_COMMIT }} + # TODO: removing `fetch-depth` below seems to have no effect, and the commit which added it had no description, + # so it's not clear why it's necessary. That said, right now ember tests are xfail, so it's a little hard to + # tell if it's safe to remove. Once ember tests are fixed, let's try again with it turned off, and if all goes + # well, we can pull it out. fetch-depth: 0 - name: Set up Node uses: actions/setup-node@v1 @@ -317,7 +409,7 @@ jobs: job_browser_playwright_tests: name: Playwright - ${{ (matrix.tracing_only && 'Browser + Tracing') || 'Browser' }} (${{ matrix.bundle }}) - needs: job_build + needs: [job_get_metadata, job_build] runs-on: ubuntu-latest strategy: matrix: @@ -339,12 +431,14 @@ jobs: - bundle: cjs tracing_only: false steps: - - name: Check out current commit (${{ env.HEAD_COMMIT }}) + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v2 + with: + ref: ${{ env.HEAD_COMMIT }} - name: Set up Node uses: actions/setup-node@v1 with: - node-version: '16' + node-version: ${{ env.DEFAULT_NODE_VERSION }} - name: Check dependency cache uses: actions/cache@v2 with: @@ -366,7 +460,7 @@ jobs: job_browser_integration_tests: name: Old Browser Integration Tests (${{ matrix.browser }}) - needs: job_build + needs: [job_get_metadata, job_build] runs-on: ubuntu-latest timeout-minutes: 10 continue-on-error: true @@ -377,10 +471,14 @@ jobs: - FirefoxHeadless - WebkitHeadless steps: - - name: Check out current commit (${{ env.HEAD_COMMIT }}) + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v2 + with: + ref: ${{ env.HEAD_COMMIT }} - name: Set up Node uses: actions/setup-node@v1 + with: + node-version: ${{ env.DEFAULT_NODE_VERSION }} - name: Check dependency cache uses: actions/cache@v2 with: @@ -401,15 +499,19 @@ jobs: job_browser_build_tests: name: Browser Build Tests - needs: job_build + needs: [job_get_metadata, job_build] runs-on: ubuntu-latest timeout-minutes: 5 continue-on-error: true steps: - - name: Check out current commit (${ env.HEAD_COMMIT }}) + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v2 + with: + ref: ${{ env.HEAD_COMMIT }} - name: Set up Node uses: actions/setup-node@v1 + with: + node-version: ${{ env.DEFAULT_NODE_VERSION }} - name: Check dependency cache uses: actions/cache@v2 with: @@ -431,16 +533,18 @@ jobs: job_node_integration_tests: name: Node SDK Integration Tests (${{ matrix.node }}) - needs: job_build + needs: [job_get_metadata, job_build] runs-on: ubuntu-latest timeout-minutes: 10 continue-on-error: true strategy: matrix: - node: [10, 12, 14, 16] + node: [10, 12, 14, 16, 18] steps: - - name: Check out current commit (${{ github.sha }}) + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v2 + with: + ref: ${{ env.HEAD_COMMIT }} - name: Set up Node uses: actions/setup-node@v1 with: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 00b74bf93f88..facd3fa52abf 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -53,7 +53,7 @@ jobs: uses: github/codeql-action/autobuild@v1 # â„šī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl + # 📚 https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml new file mode 100644 index 000000000000..b331974711f3 --- /dev/null +++ b/.github/workflows/enforce-license-compliance.yml @@ -0,0 +1,16 @@ +name: Enforce License Compliance + +on: + push: + branches: [master, main, release/*] + pull_request: + branches: [master, main] + +jobs: + enforce-license-compliance: + runs-on: ubuntu-latest + steps: + - name: 'Enforce License Compliance' + uses: getsentry/action-enforce-license-compliance@main + with: + fossa_api_key: ${{ secrets.FOSSA_API_KEY }} diff --git a/.gitignore b/.gitignore index 01098a94ef9f..bc3f92fa4202 100644 --- a/.gitignore +++ b/.gitignore @@ -4,14 +4,23 @@ packages/*/package-lock.json package-lock.json # build and test +# SDK builds build/ -packages/*/dist/ -packages/*/esm/ +# various integration test builds +dist/ coverage/ scratch/ +*.d.ts +*.js.map *.pyc *.tsbuildinfo -scenarios/*/dist/ +# side effects of running AWS lambda layer zip action locally +dist-serverless/ +sentry-node-serverless-*.zip +# transpiled transformers +jest/transformers/*.js +# node tarballs +packages/*/sentry-*.tgz # logs yarn-error.log diff --git a/.npmignore b/.npmignore index 4ab7cc4f278f..cb864514088e 100644 --- a/.npmignore +++ b/.npmignore @@ -3,9 +3,6 @@ * -# TODO remove bundles (which in the tarball are inside `build`) in v7 -!/build/**/* - -!/dist/**/* +!/cjs/**/* !/esm/**/* !/types/**/* diff --git a/.size-limit.js b/.size-limit.js index 8c51fdb70b84..a8df3eb88e83 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -1,25 +1,25 @@ module.exports = [ { name: '@sentry/browser - ES5 CDN Bundle (gzipped + minified)', - path: 'packages/browser/build/bundles/bundle.min.js', + path: 'packages/browser/build/bundles/bundle.es5.min.js', gzip: true, limit: '100 KB', }, { name: '@sentry/browser - ES5 CDN Bundle (minified)', - path: 'packages/browser/build/bundles/bundle.min.js', + path: 'packages/browser/build/bundles/bundle.es5.min.js', gzip: false, limit: '120 KB', }, { name: '@sentry/browser - ES6 CDN Bundle (gzipped + minified)', - path: 'packages/browser/build/bundles/bundle.es6.min.js', + path: 'packages/browser/build/bundles/bundle.min.js', gzip: true, limit: '100 KB', }, { name: '@sentry/browser - ES6 CDN Bundle (minified)', - path: 'packages/browser/build/bundles/bundle.es6.min.js', + path: 'packages/browser/build/bundles/bundle.min.js', gzip: false, limit: '120 KB', }, @@ -53,13 +53,13 @@ module.exports = [ }, { name: '@sentry/browser + @sentry/tracing - ES5 CDN Bundle (gzipped + minified)', - path: 'packages/tracing/build/bundles/bundle.tracing.min.js', + path: 'packages/tracing/build/bundles/bundle.tracing.es5.min.js', gzip: true, limit: '100 KB', }, { name: '@sentry/browser + @sentry/tracing - ES6 CDN Bundle (gzipped + minified)', - path: 'packages/tracing/build/bundles/bundle.tracing.es6.min.js', + path: 'packages/tracing/build/bundles/bundle.tracing.min.js', gzip: true, limit: '100 KB', }, diff --git a/.vscode/launch.json b/.vscode/launch.json index 9670683310d6..207c63ceefce 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,19 +5,34 @@ "version": "0.2.0", "inputs": [ { - "id": "getPackageName", - "type": "command", - "command": "shellCommand.execute", - "args": { - "command": "echo '${file}' | sed s/'.*sentry-javascript\\/packages\\/'// | grep --extended-regexp --only-matching --max-count 1 '[^\\/]+' | head -1", - "cwd": "${workspaceFolder}" , - // normally `input` commands bring up a selector for the user, but given that there should only be one - // choice here, this lets us skip the prompt - "useSingleResult": true - } + "id": "getPackageName", + "type": "command", + "command": "shellCommand.execute", + "args": { + "command": "echo '${file}' | sed s/'.*sentry-javascript\\/packages\\/'// | grep --extended-regexp --only-matching --max-count 1 '[^\\/]+' | head -1", + "cwd": "${workspaceFolder}", + // normally `input` commands bring up a selector for the user, but given that there should only be one + // choice here, this lets us skip the prompt + "useSingleResult": true + } } ], "configurations": [ + // Debug the ts-node script in the currently active window + { + "name": "Debug ts-node script (open file)", + "type": "pwa-node", + "cwd": "${workspaceFolder}/packages/${input:getPackageName}", + "request": "launch", + "runtimeExecutable": "yarn", + "runtimeArgs": ["ts-node", "-P", "${workspaceFolder}/tsconfig.dev.json", "${file}"], + "skipFiles": ["/**"], + "outFiles": ["${workspaceFolder}/**/*.js", "!**/node_modules/**"], + "sourceMaps": true, + "smartStep": true, + "internalConsoleOptions": "openOnSessionStart", + "outputCapture": "std" + }, // Run rollup using the config file which is in the currently active tab. { "name": "Debug rollup (config from open file)", @@ -25,18 +40,9 @@ "cwd": "${workspaceFolder}/packages/${input:getPackageName}", "request": "launch", "runtimeExecutable": "yarn", - "runtimeArgs": [ - "rollup", - "-c", - "${file}" - ], - "skipFiles": [ - "/**" - ], - "outFiles": [ - "${workspaceFolder}/**/*.js", - "!**/node_modules/**" - ], + "runtimeArgs": ["rollup", "-c", "${file}"], + "skipFiles": ["/**"], + "outFiles": ["${workspaceFolder}/**/*.js", "!**/node_modules/**"], "sourceMaps": true, "smartStep": true, "internalConsoleOptions": "openOnSessionStart", @@ -56,9 +62,6 @@ // this runs one test at a time, rather than running them in parallel (necessary for debugging so that you know // you're hitting a single test's breakpoints, in order) "--runInBand", - // TODO: when we unify jest config, we may need to change this - "--config", - "${workspaceFolder}/packages/${input:getPackageName}/package.json", // coverage messes up the source maps "--coverage", "false", @@ -67,14 +70,13 @@ ], "sourceMaps": true, "smartStep": true, - // otherwise it goes to the VSCode debug terminal, which prints the test output or console logs (depending on - // "outputCapture" option here; default is to show console logs), but not both + // otherwise it goes to the VSCode debug terminal, which prints the test output or console logs (depending on + // "outputCapture" option here; default is to show console logs), but not both "console": "integratedTerminal", // since we're not using it, don't automatically switch to it - "internalConsoleOptions": "neverOpen", + "internalConsoleOptions": "neverOpen" }, - // @sentry/nextjs - Run a specific integration test file // Must have test file in currently active tab when hitting the play button, and must already have run `yarn` in test app directory { @@ -105,18 +107,17 @@ // this controls which files are sourcemapped "outFiles": [ // our SDK code - "${workspaceFolder}/**/dist/**/*.js", + "${workspaceFolder}/**/cjs/**/*.js", // the built test app "${workspaceFolder}/packages/nextjs/test/integration/.next/**/*.js", "!**/node_modules/**" ], "resolveSourceMapLocations": [ - "${workspaceFolder}/**/dist/**", + "${workspaceFolder}/**/cjs/**", "${workspaceFolder}/packages/nextjs/test/integration/.next/**", "!**/node_modules/**" ], "internalConsoleOptions": "openOnSessionStart" - - }, + } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 3897e7bd58b4..63c97cf378dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,113 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.0.0 + +Version 7 of the Sentry JavaScript SDK brings a variety of features and fixes including bundle size and performance improvements, brand new integrations, support for the attachments API, and key bug fixes. + +This release does not change or remove any top level public API methods (`captureException`, `captureMessage`), and only requires changes to certain configuration options or custom clients/integrations/transports. + +**Note: The v7 version of the JavaScript SDK requires a self-hosted version of Sentry 20.6.0 or higher. If you are using a version of [self-hosted Sentry](https://develop.sentry.dev/self-hosted/) (aka onpremise) older than `20.6.0` then you will need to [upgrade](https://develop.sentry.dev/self-hosted/releases/).** + +For detailed overview of all the changes, please see our [v7 migration guide](./MIGRATION.md#upgrading-from-6x-to-7x). + +### Breaking Changes + +If you are a regular consumer of the Sentry JavaScript SDK you only need to focus on the general items. The internal breaking changes are aimed at libraries that build on top of and extend the JavaScript SDK (like [`@sentry/electron`](https://github.com/getsentry/sentry-electron/) or [`@sentry/react-native`](https://github.com/getsentry/sentry-react-native/)). + +#### General + +- [Updated CommonJS distributions to use ES6 by default](./MIGRATION.md#moving-to-es6-for-commonjs-files). If you need to support Internet Explorer 11 or old Node.js versions, we recommend using a preprocessing tool like [Babel](https://babeljs.io/) to convert Sentry packages to ES5. (#5005) +- Default `bundle.min.js` to ES6 instead of ES5. [ES5 bundles are still available at `bundle.es5.min.js`](./MIGRATION.md#renaming-of-cdn-bundles). (#4958) +- Updated build system to use TypeScript 3.8.3 (#4895) +- Deprecated `Severity` enum for bundle size reasons. [Please use string literals instead](./MIGRATION.md#severity-severitylevel-and-severitylevels). (#4926) +- Removed `critical` Severity level. (#5032) +- `whitelistUrls` and `blacklistUrls` have been renamed to `allowUrls` and `denyUrls` in the `Sentry.init()` options. (#4850) +- `BaseClient` and it's child classes now require `transport`, `stackParser`, and `integrations` to be [explicitly passed in](./MIGRATION.md#explicit-client-options). This was done to improve tree-shakability. (#4927) +- Updated package distribution structure and stopped distributing CDN bundles through `@sentry/*` npm packages. [See details in our migration docs.](./MIGRATION.md#restructuring-of-package-content). (#4900) (#4901) +- [Simplified `Transport` API](./MIGRATION.md#transport-changes). This means [custom transports will have to be adjusted accordingly.](./MIGRATION.md#custom-transports). +- Updated how [Node Transport Options are passed down](./MIGRATION.md#node-transport-changes). +- Start propogating [`baggage` HTTP header](https://www.w3.org/TR/baggage/) alongside `sentry-trace` header to [propogate additional tracing related information.](./MIGRATION.md#propagation-of-baggage-header). (#5133) +- Renamed `registerRequestInstrumentation` export to `instrumentOutgoingRequests` in `@sentry/tracing`. (#4859) +- Renamed `UserAgent` integration to `HttpContext`. (#5027) +- Replaced `BrowserTracing` integration's `maxTransactionDuration` option with `finalTimeout` option in the `@sentry/tracing` package and reset `idleTimeout` based on activities count. This should improve accuracy of web-vitals like LCP by 20-30%. (#5044) +- [Updated `@sentry/angular` to be compiled by the angular compiler](./MIGRATION.md#sentry-angular-sdk-changes). (#4641) +- Made tracing package treeshakable (#5166) + +- Removed support for [Node v6](./MIGRATION.md#dropping-support-for-nodejs-v6). (#4851) +- Removed `@sentry/minimal` package in favour of using [`@sentry/hub`](./MIGRATION.md#removal-of-sentryminimal). (#4971) +- Removed support for Opera browser pre v15 (#4923) +- Removed `ignoreSentryErrors` option from AWS lambda SDK. Errors originating from the SDK will now *always* be caught internally. (#4994) +- Removed `Integrations.BrowserTracing` export from `@sentry/nextjs`. Please import `BrowserTracing` from `@sentry/nextjs` directly. +- Removed static `id` property from `BrowserTracing` integration. +- Removed `SDK_NAME` export from `@sentry/browser`, `@sentry/node`, `@sentry/tracing` and `@sentry/vue` packages. (#5040) +- Removed `Angular`, `Ember`, and `Vue` integrations from `@sentry/integrations` [in favour of the explicit framework packages: `@sentry/angular`, `@sentry/ember`, and `@sentry/vue`](./MIGRATION.md#removal-of-old-platform-integrations-from-sentryintegrations-package). (#4893) +- Removed [enums `Status`, `RequestSessionStatus`, and `SessionStatus`.](./MIGRATION.md#removed-enums). Deprecated [enums `SpanStatus` and `Severity`](./MIGRATION.md#deprecated-enums). This was done to save on bundle size. (#4891) (#4889) (#4890) +- Removed support for deprecated `@sentry/apm` package. (#4845) +- Removed deprecated `user` field from DSN interface. `publicKey` should be used instead. (#4864) +- Removed deprecated `getActiveDomain` method and `DomainAsCarrier` type from `@sentry/hub`. (#4858) +- Removed `eventStatusFromHttpCode` to save on bundle size. +- Removed usage of deprecated `event.stacktrace` field. (#4885) +- Removed raven-node backward-compat code (#4942) +- Removed `showReportDialog` method on `BrowserClient` (#4973) +- Removed deprecated `startSpan` and `child` methods (#4849) +- Removed deprecated `frameContextLines` options (#4884) +- Removed `Sentry` from window in the Gatsby SDK (#4857) + +#### Internal + +- Removed support for the store endpoint (#4969) +- Made hint callback argument non-optional (#5141) +- Switched to using new transports internally (#4943) +- [Removed `API` class from `@sentry/core`.](./MIGRATION.md#removing-the-api-class-from-sentrycore). (#4848) +- [Refactored `Session` class to use a more functional approach.](./MIGRATION.md#session-changes). (#5054) +- Removed `Backend` class in favour of moving functionality into the `Client` class (for more details, see [#4911](https://github.com/getsentry/sentry-javascript/pull/4911) and [#4919](https://github.com/getsentry/sentry-javascript/pull/4919)). +- Removed forget async utility function (#4941) +- Removed tslint from `@sentry-internal/typescript` (#4940) +- Removed `_invokeClient` function from `@sentry/hub` (#4972) +- Removed top level eventbuilder exports (#4887) +- Added baggage API helpers in `@sentry/utils` (#5066) + +### Other Changes + +#### Features + +- feat(tracing): Add Prisma ORM integration. (#4931) +- feat(react): Add react-router-v6 integration (#5042) +- feat: Add attachments API (#5004) + +- feat: Add `name` field to `EventProcessor` (#4932) +- feat: Expose configurable stack parser (#4902) +- feat(tracing): Make `setMeasurement` public API (#4933) +- feat(tracing): Add GB unit to device memory tag value (#4935) +- feat: Export browser integrations individually (#5028) +- feat(core): Send Baggage in Envelope Header (#5104) + +#### Fixes + +- fix(browser): Fix memory leak in `addEventListener` instrumentation (#5147) +- fix(build): Fix express import in `gcpfunction` (#5097) +- fix(ember): Export sha hashes of injected scripts (#5089) +- fix(hub): Add missing parameter to captureException docstring (#5001) +- fix(integrations): Mark ExtraErrorData as already normalized (#5053) +- fix(serverless): Adjust v6 Lambda layer hotfix for v7 (#5006) +- fix(tracing): Adjust sideEffects package.json entry for v7 (#4987) +- fix(tracing): Remove isInstanceOf check in Hub constructor (#5046) +- fix(tracing): Don't use `querySelector` when not available (#5160) +- fix(nextjs): Update webpack-plugin and change how cli binary is detected. This should reduce bundle size of NextJS applications. (#4988) +- fix(utils): Fix infinite recursion in `dropUndefinedKeys` (#5163) + +#### Misc + +- feat(build): Vendor polyfills injected during build (#5051) +- ref(build): Use rollup to build AWS lambda layer (#5146) +- ref(core): Make event processing log warnings instead of errors (#5010) +- ref(node): Allow node stack parser to work in browser context (#5135) +- ref(serverless): Point DSN to relay in lambda extension (#5126) +- ref(serverless): Do not throw on flush error (#5090) +- ref(utils): Clean up dangerous type casts in object helper file (#5047) +- ref(utils): Add logic to enable skipping of normalization (#5052) + ## 6.19.7 - fix(react): Add children prop type to ErrorBoundary component (#4966) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ca43b81a28f..7aee8627a350 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Contributing diff --git a/MIGRATION.md b/MIGRATION.md index b3acaa6695e8..aef8293fbc9a 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,3 +1,377 @@ +# Upgrading from 6.x to 7.x + +The main goal of version 7 is to reduce bundle size. This version is breaking because we removed deprecated APIs, upgraded our build tooling, and restructured npm package contents. +Below we will outline all the breaking changes you should consider when upgrading. + +**TL;DR** If you only use basic features of Sentry, or you simply copy & pasted the setup examples from our docs, here's what changed for you: +- If you installed additional Sentry packages, such as`@sentry/tracing` alongside your Sentry SDK (e.g. `@sentry/react` or `@sentry/node`), make sure to upgrade all of them to version 7. +- Our CDN bundles are now ES6 - you will need to [reconfigure your script tags](#renaming-of-cdn-bundles) if you want to keep supporting ES5 and IE11 on the new SDK version. +- Distributed CommonJS files will be ES6. Use a transpiler if you need to support old node versions. +- We bumped the TypeScript version we generate our types with to 3.8.3. Please check if your TypeScript projects using TypeScript version 3.7 or lower still compile. Otherwise, upgrade your TypeScript version. +- `whitelistUrls` and `blacklistUrls` have been renamed to `allowUrls` and `denyUrls` in the `Sentry.init()` options. +- The `UserAgent` integration is now called `HttpContext`. +- If you are using Performance Monitoring and with tracing enabled, you might have to [make adjustments to +your server's CORS settings](#-propagation-of-baggage-header) + +## Dropping Support for Node.js v6 + +Node.js version 6 has reached end of life in April 2019. For Sentry JavaScript SDK version 7, we will no longer be supporting version 6 of Node.js. + +As far as SDK development goes, dropping support means no longer running integration tests for Node.js version 6, and also no longer handling edge cases specific to version 6. +Running the new SDK version on Node.js v6 is therefore highly discouraged. + +## Removal of `@sentry/minimal` + +The `@sentry/minimal` package was deleted and it's functionality was moved to `@sentry/hub`. All exports from `@sentry/minimal` should be avaliable in `@sentry/hub` other than `_callOnClient` function which was removed. + +```ts +// New in v7: +import { + addBreadcrumb, + captureException, + configureScope, + setTag, +} from '@sentry/hub'; + +// Before: +import { + addBreadcrumb, + captureException, + configureScope, + setTag, +} from '@sentry/minimal'; +``` + +## Explicit Client Options + +In v7, we've updated the `Client` to have options seperate from the options passed into `Sentry.init`. This means that constructing a client now requires 3 options: `integrations`, `transport` and `stackParser`. These can be customized as you see fit. + +```ts +import { BrowserClient, defaultStackParser, defaultIntegrations, makeFetchTransport } from '@sentry/browser'; + +// New in v7: +const client = new BrowserClient({ + transport: makeFetchTransport, + stackParser: defaultStackParser, + integrations: defaultIntegrations, +}); + +// Before: +const client = new BrowserClient(); +``` + +Since you now explicitly pass in the dependencies of the client, you can also tree-shake out dependencies that you do not use this way. For example, you can tree-shake out the SDK's default integrations and only use the ones that you want like so: + +```ts +import { + BrowserClient, + Breadcrumbs, + Dedupe, + defaultStackParser, + GlobalHandlers, + Integrations, + makeFetchTransport, + LinkedErrors, +} from '@sentry/browser'; + +// New in v7: +const client = new BrowserClient({ + transport: makeFetchTransport, + stackParser: defaultStackParser, + integrations: [new Breadcrumbs(), new GlobalHandlers(), new LinkedErrors(), new Dedupe()], +}); +``` + +## Removal Of Old Platform Integrations From `@sentry/integrations` Package + +The following classes will be removed from the `@sentry/integrations` package and can no longer be used: + +- `Angular` +- `Ember` +- `Vue` + +These classes have been superseded and were moved into their own packages, `@sentry/angular`, `@sentry/ember`, and `@sentry/vue` in a previous version. +Refer to those packages if you want to integrate Sentry into your Angular, Ember, or Vue application. + +## Moving To ES6 For CommonJS Files + +From version 7 onwards, the CommonJS files in Sentry JavaScript SDK packages will use ES6. + +If you need to support Internet Explorer 11 or old Node.js versions, we recommend using a preprocessing tool like [Babel](https://babeljs.io/) to convert Sentry packages to ES5. + +## Renaming Of CDN Bundles + +CDN bundles will be ES6 by default. Files that followed the naming scheme `bundle.es6.min.js` were renamed to `bundle.min.js` and any bundles using ES5 (files without `.es6`) turned into `bundle.es5.min.js`. + +See our [docs on CDN bundles](https://docs.sentry.io/platforms/javascript/install/cdn/) for more information. + +## Restructuring Of Package Content + +Up until v6.x, we have published our packages on npm with the following structure: + +- `build` folder contained CDN bundles +- `dist` folder contained CommonJS files and TypeScript declarations +- `esm` folder contained ESM files and TypeScript declarations + +Moving forward the JavaScript SDK packages will generally have the following structure: + +- `cjs` folder contains CommonJS files +- `esm` folder contains ESM files +- `types` folder contains TypeScript declarations + +**CDN bundles of version 7 or higher will no longer be distributed through our npm package.** +This means that most third-party CDNs like [unpkg](https://unpkg.com/) or [jsDelivr](https://www.jsdelivr.com/) will also not provide them. + +If you depend on any specific files in a Sentry JavaScript npm package, you will most likely need to update their references. +For example, imports on `@sentry/browser/dist/client` will become `@sentry/browser/cjs/client`. +However, directly importing from specific files is discouraged. + +## Removing the `API` class from `@sentry/core` + +The internal `API` class was removed in favor of using client options explicitly. + +```js +// New in v7: +import { + initAPIDetails, + getEnvelopeEndpointWithUrlEncodedAuth, + getStoreEndpointWithUrlEncodedAuth, +} from '@sentry/core'; + +const client = getCurrentHub().getClient(); +const dsn = client.getDsn(); +const options = client.getOptions(); +const envelopeEndpoint = getEnvelopeEndpointWithUrlEncodedAuth(dsn, options.tunnel); + +// Before: +import { API } from '@sentry/core'; + +const api = new API(dsn, metadata, tunnel); +const dsn = api.getDsn(); +const storeEndpoint = api.getStoreEndpointWithUrlEncodedAuth(); +const envelopeEndpoint = api.getEnvelopeEndpointWithUrlEncodedAuth(); +``` + +## Transport Changes + +The `Transport` API was simplified and some functionality (e.g. APIDetails and client reports) was refactored and moved +to the Client. To send data to Sentry, we switched from the previously used [Store endpoint](https://develop.sentry.dev/sdk/store/) to the [Envelopes endpoint](https://develop.sentry.dev/sdk/envelopes/). + +This example shows the new v7 and the v6 Transport API: + +```js +// New in v7: +export interface Transport { + /* Sends an envelope to the Envelope endpoint in Sentry */ + send(request: Envelope): PromiseLike; + /* Waits for all events to be sent or the timeout to expire, whichever comes first */ + flush(timeout?: number): PromiseLike; +} + +// Before: +export interface Transport { + /* Sends the event to the Store endpoint in Sentry */ + sendEvent(event: Event): PromiseLike; + /* Sends the session to the Envelope endpoint in Sentry */ + sendSession?(session: Session | SessionAggregates): PromiseLike; + /* Waits for all events to be sent or the timeout to expire, whichever comes first */ + close(timeout?: number): PromiseLike; + /* Increment the counter for the specific client outcome */ + recordLostEvent?(type: Outcome, category: SentryRequestType): void; +} +``` + +### Custom Transports +If you rely on a custom transport, you will need to make some adjustments to how it is created when migrating +to v7. Note that we changed our transports from a class-based to a functional approach, meaning that +the previously class-based transports are now created via functions. This also means that custom transports +are now passed by specifying a factory function in the `Sentry.init` options object instead passing the custom +transport's class. + +The following example shows how to create a custom transport in v7 vs. how it was done in v6: + +```js +// New in v7: +import { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; +import { createTransport } from '@sentry/core'; + +export function makeMyCustomTransport(options: BaseTransportOptions): Transport { + function makeRequest(request: TransportRequest): PromiseLike { + // this is where your sending logic goes + const myCustomRequest = { + body: request.body, + url: options.url + }; + // you define how `sendMyCustomRequest` works + return sendMyCustomRequest(myCustomRequest).then(response => ({ + headers: { + 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), + 'retry-after': response.headers.get('Retry-After'), + }, + })); + } + + // `createTransport` takes care of rate limiting and flushing + return createTransport({ bufferSize: options.bufferSize }, makeRequest); +} + +Sentry.init({ + dsn: '...', + transport: makeMyCustomTransport, // this function will be called when the client is initialized + ... +}) + +// Before: +class MyCustomTransport extends BaseTransport { + constructor(options: TransportOptions) { + // initialize your transport here + super(options); + } + + public sendEvent(event: Event): PromiseLike { + // this is where your sending logic goes + // `url` is decoded from dsn in BaseTransport + const myCustomRequest = createMyCustomRequestFromEvent(event, this.url); + return sendMyCustomRequest(myCustomRequest).then(() => resolve({status: 'success'})); + } + + public sendSession(session: Session): PromiseLike {...} + // ... +} + +Sentry.init({ + dsn: '...', + transport: MyCustomTransport, // the constructor was called when the client was initialized + ... +}) +``` + +Overall, the new way of transport creation allows you to create your custom sending implementation +without having to deal with the conversion of events or sessions to envelopes. +We recommend calling using the `createTransport` function from `@sentry/core` as demonstrated in the example above which, besides creating the `Transport` +object with your custom logic, will also take care of rate limiting and flushing. + +For a complete v7 transport implementation, take a look at our [browser fetch transport](https://github.com/getsentry/sentry-javascript/blob/ebc938a03d6efe7d0c4bbcb47714e84c9a566a9c/packages/browser/src/transports/fetch.ts#L1-L34). + +### Node Transport Changes + +To clean up the options interface, we now require users to pass down transport related options under the `transportOptions` key. The options that +were changed were `caCerts`, `httpProxy`, and `httpsProxy`. In addition, `httpProxy` and `httpsProxy` were unified to a single option under +the `transportOptions` key, `proxy`. + +```ts +// New in v7: +Sentry.init({ + dsn: '...', + transportOptions: { + caCerts: getMyCaCert(), + proxy: 'http://example.com', + }, +}); + +// Before: +Sentry.init({ + dsn: '...', + caCerts: getMyCaCert(), + httpsProxy: 'http://example.com', +}) +``` + +## Enum Changes + +Given that enums have a high bundle-size impact, our long term goal is to eventually remove all enums from the SDK in +favor of string literals. + +### Removed Enums +* The previously deprecated enum `Status` was removed (see [#4891](https://github.com/getsentry/sentry-javascript/pull/4891)). +* The previously deprecated internal-only enum `RequestSessionStatus` was removed (see + [#4889](https://github.com/getsentry/sentry-javascript/pull/4889)) in favor of string literals. +* The previously deprecated internal-only enum `SessionStatus` was removed (see + [#4890](https://github.com/getsentry/sentry-javascript/pull/4890)) in favor of string literals. + +### Deprecated Enums +The two enums `SpanStatus`, and `Severity` remain deprecated, as we decided to limit the number of high-impact breaking +changes in v7. They will be removed in the next major release which is why we strongly recommend moving to the +corresponding string literals. Here's how to adjust [`Severity`](#severity-severitylevel-and-severitylevels) and +[`SpanStatus`](#spanstatus). + +## Session Changes + +Note: These changes are not relevant for the majority of Sentry users but if you are building an +SDK on top of the Javascript SDK, you might need to make some adaptions. +The internal `Session` class was refactored and replaced with a more functional approach in +[#5054](https://github.com/getsentry/sentry-javascript/pull/5054). +Instead of the class, we now export a `Session` interface from `@sentry/types` and three utility functions +to create and update a `Session` object from `@sentry/hub`. +This short example shows what has changed and how to deal with the new functions: + +```js +// New in v7: +import { makeSession, updateSession, closeSession } from '@sentry/hub'; + +const session = makeSession({ release: 'v1.0' }); +updateSession(session, { environment: 'prod' }); +closeSession(session, 'ok'); + +// Before: +import { Session } from '@sentry/hub'; + +const session = new Session({ release: 'v1.0' }); +session.update({ environment: 'prod' }); +session.close('ok'); +``` + +## Propagation of Baggage Header + +We introduced a new way of propagating tracing and transaction-related information between services. This +change adds the [`baggage` HTTP header](https://www.w3.org/TR/baggage/) to outgoing requests if the instrumentation of requests is enabled. Since this adds a header to your HTTP requests, you might need +to adjust your Server's CORS settings to allow this additional header. Take a look at the [Sentry docs](https://docs.sentry.io/platforms/javascript/performance/connect-services/#navigation-and-other-xhr-requests) +for more in-depth instructions what to change. + +## General API Changes + +For our efforts to reduce bundle size of the SDK we had to remove and refactor parts of the package which introduced a few changes to the API: + +- Remove support for deprecated `@sentry/apm` package. `@sentry/tracing` should be used instead. +- Remove deprecated `user` field from DSN. `publicKey` should be used instead. +- Remove deprecated `whitelistUrls` and `blacklistUrls` options from `Sentry.init`. They have been superseded by `allowUrls` and `denyUrls` specifically. See [our docs page on inclusive language](https://develop.sentry.dev/inclusion/) for more details. +- Gatsby SDK: Remove `Sentry` from `window` object. +- Remove deprecated `Status`, `SessionStatus`, and `RequestSessionStatus` enums. These were only part of an internal API. If you are using these enums, we encourage you to to look at [b177690d](https://github.com/getsentry/sentry-javascript/commit/b177690d89640aef2587039113c614672c07d2be), [5fc3147d](https://github.com/getsentry/sentry-javascript/commit/5fc3147dfaaf1a856d5923e4ba409479e87273be), and [f99bdd16](https://github.com/getsentry/sentry-javascript/commit/f99bdd16539bf6fac14eccf1a974a4988d586b28) to to see the changes we've made to our code as result. We generally recommend using string literals instead of the removed enums. +- Remove 'critical' severity. +- Remove deprecated `getActiveDomain` method and `DomainAsCarrier` type from `@sentry/hub`. +- Rename `registerRequestInstrumentation` to `instrumentOutgoingRequests` in `@sentry/tracing`. +- Remove `Backend` and port its functionality into `Client` (see + [#4911](https://github.com/getsentry/sentry-javascript/pull/4911) and + [#4919](https://github.com/getsentry/sentry-javascript/pull/4919)). `Backend` was an unnecessary abstraction which is + not present in other Sentry SDKs. For the sake of reducing complexity, increasing consistency with other Sentry SDKs and + decreasing bundle-size, `Backend` was removed. +- Remove support for Opera browser pre v15. +- Rename `UserAgent` integration to `HttpContext`. (see [#5027](https://github.com/getsentry/sentry-javascript/pull/5027)) +- Remove `SDK_NAME` export from `@sentry/browser`, `@sentry/node`, `@sentry/tracing` and `@sentry/vue` packages. +- Removed `eventStatusFromHttpCode` to save on bundle size. +- Replace `BrowserTracing` `maxTransactionDuration` option with `finalTimeout` option +- Removed `ignoreSentryErrors` option from AWS lambda SDK. Errors originating from the SDK will now *always* be caught internally. +- Removed `Integrations.BrowserTracing` export from `@sentry/nextjs`. Please import `BrowserTracing` from `@sentry/nextjs` directly. +- Removed static `id` property from `BrowserTracing` integration. +- Removed usage of deprecated `event.stacktrace` field + +## Sentry Angular SDK Changes + +The Sentry Angular SDK (`@sentry/angular`) is now compiled with the Angular compiler (see [#4641](https://github.com/getsentry/sentry-javascript/pull/4641)). This change was necessary to fix a long-lasting bug in the SDK (see [#3282](https://github.com/getsentry/sentry-javascript/issues/3282)): `TraceDirective` and `TraceModule` can now be used again without risking an application compiler error or having to disable AOT compilation. + +### Angular Version Compatibility + +As in v6, we continue to list Angular 10-13 in our peer dependencies, meaning that these are the Angular versions we officially support. +If you are using v7 with Angular <10 in your project and you experience problems, we recommend staying on the latest 6.x +version until you can upgrade your Angular version. As v7 of our SDK is compiled with the Angular 10 compiler and we upgraded +our Typescript version, the SDK will work with Angular 10 and above. Tests have shown that Angular 9 seems to work as well +(use at your own risk) but we recommend upgrading to a more recent Angular version. + +### Import Changes + +Due to the compiler change, our NPM package structure changed as well as it now conforms to the [Angular Package Format v10](https://docs.google.com/document/d/1uh2D6XqaGh2yjjXwfF4SrJqWl1MBhMPntlNBBsk6rbw/edit). +In case you're importing from specific paths other than `@sentry/angular` you will have to adjust these paths. As an example, `import ... from '@sentry/angular/esm/injex.js'` should be changed to `import ... from '@sentry/angular/esm2015/index.js'`. Generally, we strongly recommend only importing from `@sentry/angular`. + # Upgrading from 6.17.x to 6.18.0 Version 6.18.0 deprecates the `frameContextLines` top-level option for the Node SDK. This option will be removed in an upcoming major version. To migrate off of the top-level option, pass it instead to the new `ContextLines` integration. diff --git a/README.md b/README.md index 84e46f6b362a..503d198e15d2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

_Bad software is everywhere, and we're tired of it. Sentry is on a mission to help developers write better software faster, so we can get back to enjoying technology. If you want to join us [**Check out our open positions**](https://sentry.io/careers/)_ @@ -104,8 +103,6 @@ below: extensions for Performance Monitoring / Tracing - [`@sentry/hub`](https://github.com/getsentry/sentry-javascript/tree/master/packages/hub): Global state management of SDKs -- [`@sentry/minimal`](https://github.com/getsentry/sentry-javascript/tree/master/packages/minimal): Minimal SDK for - library authors to add Sentry support - [`@sentry/core`](https://github.com/getsentry/sentry-javascript/tree/master/packages/core): The base for all JavaScript SDKs with interfaces, type definitions and base classes. - [`@sentry/utils`](https://github.com/getsentry/sentry-javascript/tree/master/packages/utils): A set of helpers and diff --git a/jest/jest.config.js b/jest/jest.config.js new file mode 100644 index 000000000000..8b7b7f1f5b7c --- /dev/null +++ b/jest/jest.config.js @@ -0,0 +1,17 @@ +module.exports = { + rootDir: process.cwd(), + collectCoverage: true, + transform: { + '^.+\\.ts$': 'ts-jest', + '^.+\\.tsx$': 'ts-jest', + }, + coverageDirectory: '/coverage', + moduleFileExtensions: ['js', 'ts', 'tsx'], + testMatch: ['/**/*.test.ts', '/**/*.test.tsx'], + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.test.json', + }, + }, + testPathIgnorePatterns: ['/build/', '/node_modules/'], +}; diff --git a/jest/transformers/constReplacer.ts b/jest/transformers/constReplacer.ts new file mode 100644 index 000000000000..725aa9ac48c8 --- /dev/null +++ b/jest/transformers/constReplacer.ts @@ -0,0 +1,84 @@ +/** + * This is a transformer which `ts-jest` applies during the compilation process, which switches all of the `const`s out + * for `var`s. Unlike in our package builds, where we do the same substiution for bundle size reasons, here we do it + * because otherwise `const global = getGlobalObject()` throws an error about redifining `global`. (This didn't used to + * be a problem because our down-compilation did the `const`-`var` substitution for us, but now that we're ES6-only, we + * have to do it ourselves.) + * + * Note: If you ever have to change this, and are testing it locally in the process, be sure to call + * `yarn jest --clearCache` + * before each test run, as transformation results are cached between runs. + */ + +import { + createVariableDeclarationList, + getCombinedNodeFlags, + isVariableDeclarationList, + Node, + NodeFlags, + SourceFile, + TransformationContext, + Transformer, + TransformerFactory, + visitEachChild, + visitNode, + VisitResult, +} from 'typescript'; + +// These can be anything - they're just used to construct a cache key for the transformer returned by the factory below. +// This really only matters when you're testing the transformer itself, as changing these values gives you a quick way +// to invalidate the cache and ensure that changes you've made to the code here are immediately picked up on and used. +export const name = 'const-to-var'; +export const version = '1.0'; + +/** + * Check whether the given AST node represents a `const` token. + * + * This function comes from the TS compiler, and is copied here to get around the fact that it's not exported by the + * `typescript` package. + * + * @param node The node to check + * @returns A boolean indicating if the node is a `const` token. + */ +function isVarConst(node: Node): boolean { + // eslint-disable-next-line no-bitwise + return !!(getCombinedNodeFlags(node) & NodeFlags.Const); +} + +/** + * Return a set of nested factory functions, which ultimately creates an AST-node visitor function, which can modify + * each visited node as it sees fit, and uses it to walk the AST, returning the results. + * + * In our case, we're modifying all `const` variable declarations to use `var` instead. + */ +export function factory(): TransformerFactory { + // Create the transformer + function transformerFactory(context: TransformationContext): Transformer { + // Create a visitor function and use it to walk the AST + function transformer(sourceFile: SourceFile): SourceFile { + // This visitor function can either return a node, in which case the subtree rooted at the returned node is + // substituted for the subtree rooted at the visited node, or can use the recursive `visitEachChild` function + // provided by TS to continue traversing the tree. + function visitor(node: Node): VisitResult { + // If we've found a `const` declaration, return a `var` declaration in its place + if (isVariableDeclarationList(node) && isVarConst(node)) { + // A declaration list with a `None` flag defaults to using `var` + return createVariableDeclarationList(node.declarations, NodeFlags.None); + } + + // This wasn't a node we're interested in, so keep walking down the tree. + return visitEachChild(node, visitor, context); + } + + // Having defined our visitor, pass it to the TS-provided `visitNode` function, which will use it to walk the AST, + // and return the results of that walk. + return visitNode(sourceFile, visitor); + } + + // Back in the transformer factory, return the transformer we just created + return transformer; + } + + // Finally, we're back in `factory`, and can return the whole nested system + return transformerFactory; +} diff --git a/lerna.json b/lerna.json index 251bcc4fb2bc..5249c4e4ee7b 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "lerna": "3.4.0", - "version": "6.19.7", + "version": "7.0.0-rc.0", "packages": "packages/*", "npmClient": "yarn", "useWorkspaces": true diff --git a/package.json b/package.json index 14d73feddc03..09848d739f99 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,27 @@ { "private": true, "scripts": { - "build": "node ./scripts/verify-packages-versions.js && lerna run --stream --concurrency 1 --sort build", - "build:bundle": "lerna run --parallel build:bundle", - "build:cjs": "lerna run --stream --concurrency 1 --sort build:cjs", - "build:dev": "lerna run --stream --concurrency 1 --sort build:dev", - "build:dev:filter": "lerna run --stream --concurrency 1 --sort build:dev --include-filtered-dependencies --include-filtered-dependents --scope", - "build:es5": "yarn lerna run --stream --concurrency 1 --sort build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "lerna run --stream --concurrency 1 --sort build:esm", - "build:types": "lerna run --stream --concurrency 1 --sort build:types", + "build": "node ./scripts/verify-packages-versions.js && yarn run-p build:rollup build:types build:bundle && yarn build:extras", + "build:bundle": "yarn ts-node scripts/ensure-bundle-deps.ts && yarn lerna run --parallel build:bundle", + "build:dev": "run-p build:types build:rollup", + "build:extras": "lerna run --parallel build:extras", + "build:rollup": "lerna run --parallel build:rollup", + "build:types": "lerna run --stream build:types", "build:watch": "lerna run --parallel build:watch", "build:dev:watch": "lerna run --parallel build:dev:watch", "build:types:watch": "ts-node scripts/build-types-watch.ts", "build:npm": "lerna run --parallel build:npm", "circularDepCheck": "lerna run --parallel circularDepCheck", - "clean": "lerna run --parallel clean && lerna clean --yes", + "clean": "run-p clean:build clean:caches", + "clean:build": "lerna run --parallel clean", + "clean:caches": "yarn rimraf eslintcache && yarn jest --clearCache", + "clean:deps": "lerna clean --yes && rm -rf node_modules && yarn", + "clean:all": "run-p clean:build clean:caches clean:deps", "codecov": "codecov", "fix": "lerna run --parallel fix", - "link:yarn": "lerna run --stream --concurrency 1 link:yarn", + "link:yarn": "lerna exec --parallel yarn link", "lint": "lerna run --parallel lint", "lint:eslint": "lerna run --parallel lint:eslint", - "prepublishOnly": "lerna run --stream --concurrency 1 prepublishOnly", "postpublish": "make publish-docs && lerna run --stream --concurrency 1 postpublish", "test": "lerna run --ignore @sentry-internal/browser-integration-tests --ignore @sentry-internal/node-integration-tests --stream --concurrency 1 --sort test", "test-ci": "ts-node ./scripts/test.ts" @@ -40,7 +41,6 @@ "packages/hub", "packages/integration-tests", "packages/integrations", - "packages/minimal", "packages/nextjs", "packages/node", "packages/node-integration-tests", @@ -58,18 +58,23 @@ "@rollup/plugin-commonjs": "^21.0.1", "@rollup/plugin-node-resolve": "^13.1.3", "@rollup/plugin-replace": "^3.0.1", + "@rollup/plugin-sucrase": "^4.0.3", "@size-limit/preset-small-lib": "^4.5.5", "@strictsoftware/typedoc-plugin-monorepo": "^0.3.1", "@types/chai": "^4.1.3", - "@types/jest": "^24.0.11", + "@types/jest": "^27.4.1", + "@types/jsdom": "^16.2.3", "@types/mocha": "^5.2.0", "@types/node": "~10.17.0", "@types/sinon": "^7.0.11", + "acorn": "^8.7.0", "chai": "^4.1.2", "codecov": "^3.6.5", "deepmerge": "^4.2.2", "eslint": "7.32.0", - "jest": "^24.9.0", + "jest": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jsdom": "^19.0.0", "karma-browserstack-launcher": "^1.5.1", "karma-firefox-launcher": "^1.1.0", "lerna": "3.13.4", @@ -77,23 +82,24 @@ "mocha": "^6.1.4", "npm-run-all": "^4.1.5", "prettier": "2.5.1", + "recast": "^0.20.5", "replace-in-file": "^4.0.0", "rimraf": "^3.0.2", "rollup": "^2.67.1", "rollup-plugin-license": "^2.6.1", + "rollup-plugin-re": "^1.0.7", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.31.2", "sinon": "^7.3.2", "size-limit": "^4.5.5", - "ts-jest": "^24.3.0", - "ts-node": "^8.10.2", + "ts-jest": "^27.1.4", + "ts-node": "^10.7.0", "tslib": "^2.3.1", "typedoc": "^0.18.0", - "typescript": "3.7.5" + "typescript": "3.8.3" }, "resolutions": { - "**/agent-base": "5", - "**/jest-environment-node": "24" + "**/agent-base": "5" }, "version": "0.0.0", "dependencies": {} diff --git a/packages/angular/.npmignore b/packages/angular/.npmignore index 8904efca5aea..75ee79933841 100644 --- a/packages/angular/.npmignore +++ b/packages/angular/.npmignore @@ -1,4 +1,4 @@ * -!/dist/**/* +!/cjs/**/* !/esm/**/* !/build/types/**/* diff --git a/packages/angular/README.md b/packages/angular/README.md index d42a32344082..dd288b4951a4 100644 --- a/packages/angular/README.md +++ b/packages/angular/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Official Sentry SDK for Angular @@ -12,6 +11,10 @@ - [Official SDK Docs](https://docs.sentry.io/platforms/javascript/angular/) - [TypeDoc](http://getsentry.github.io/sentry-javascript/) +## Angular Version Compatibility + +The latest version of this SDK officially supports Angular 10-13. If you are using an older version of Angular and experience problems with the Angular SDK, we recommend downgrading the SDK to version 6.x. + ## General This package is a wrapper around `@sentry/browser`, with added functionality related to Angular. All methods available diff --git a/packages/angular/angular.json b/packages/angular/angular.json new file mode 100644 index 000000000000..f771c0e78df0 --- /dev/null +++ b/packages/angular/angular.json @@ -0,0 +1,27 @@ +/* To learn more about this file see: https://angular.io/guide/workspace-config */ +{ + "$schema": "../../node_modules/@angular/cli/lib/config/schema.json", + "version": 1, // version of angular.json + "projects": { + "sentry-angular": { + "projectType": "library", + "root": ".", + "sourceRoot": "src", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "tsConfig": "tsconfig.ngc.json", + "project": "ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "tsconfig.ngc.json" + } + } + } + } + } + }, + "defaultProject": "sentry-angular" +} diff --git a/packages/angular/ng-package.json b/packages/angular/ng-package.json new file mode 100644 index 000000000000..88df70c1c7bd --- /dev/null +++ b/packages/angular/ng-package.json @@ -0,0 +1,13 @@ +{ + "$schema": "node_modules/ng-packagr/ng-package.schema.json", + "dest": "build", + "lib": { + "entryFile": "src/index.ts", + "umdModuleIds": { + "@sentry/browser": "Sentry", + "@sentry/utils": "Sentry.util" + } + }, + "whitelistedNonPeerDependencies": ["@sentry/browser", "@sentry/utils", "@sentry/types", "tslib"], + "assets": ["README.md", "LICENSE"] +} diff --git a/packages/angular/package.json b/packages/angular/package.json index ea99649e13eb..18c73fbc8422 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,57 +1,55 @@ { "name": "@sentry/angular", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Official Sentry SDK for Angular", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular", "author": "Sentry", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=8" }, - "main": "dist/index.js", - "module": "esm/index.js", - "types": "build/types/index.d.ts", + "main": "build/bundles/sentry-angular.umd.js", + "module": "build/fesm2015/sentry-angular.js", "publishConfig": { "access": "public" }, "peerDependencies": { "@angular/common": "10.x || 11.x || 12.x || 13.x", "@angular/core": "10.x || 11.x || 12.x || 13.x", - "@angular/router": "10.x || 11.x || 12.x || 13.x" + "@angular/router": "10.x || 11.x || 12.x || 13.x", + "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "6.19.7", - "@sentry/types": "6.19.7", - "@sentry/utils": "6.19.7", - "rxjs": "^6.6.0", - "tslib": "^1.9.3" + "@sentry/browser": "7.0.0-rc.0", + "@sentry/types": "7.0.0-rc.0", + "@sentry/utils": "7.0.0-rc.0", + "tslib": "^2.0.0" }, "devDependencies": { - "@angular/common": "^10.0.3", - "@angular/core": "^10.0.3", - "@angular/router": "^10.0.3" + "@angular-devkit/build-angular": "~0.1002.4", + "@angular/cli": "^10.2.4", + "@angular/common": "~10.2.5", + "@angular/compiler": "^10.2.5", + "@angular/compiler-cli": "~10.2.5", + "@angular/core": "~10.2.5", + "@angular/router": "~10.2.5", + "ng-packagr": "^10.1.0", + "typescript": "~4.0.2" }, "scripts": { - "build": "run-p build:cjs build:esm build:types", - "build:cjs": "tsc -p tsconfig.cjs.json", + "build": "yarn build:ngc", + "build:ngc": "ng build --prod", "build:dev": "run-s build", - "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "tsc -p tsconfig.esm.json", - "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", - "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", - "build:dev:watch": "run-s build:watch", - "build:es5:watch": "yarn build:cjs:watch # *** backwards compatibility - remove in v7 ***", - "build:esm:watch": "tsc -p tsconfig.esm.json --watch", - "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "npm pack", + "build:extras": "yarn build", + "build:watch": "run-p build:ngc:watch", + "build:ngc:watch": "ng build --prod --watch", + "build:npm": "npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf dist esm build coverage", + "clean": "rimraf build coverage sentry-angular-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/*.ts\"", - "link:yarn": "yarn link", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"" diff --git a/packages/angular/src/constants.ts b/packages/angular/src/constants.ts index a0786e7b516c..9e98b366d841 100644 --- a/packages/angular/src/constants.ts +++ b/packages/angular/src/constants.ts @@ -3,3 +3,8 @@ export const ANGULAR_ROUTING_OP = 'ui.angular.routing'; export const ANGULAR_INIT_OP = 'ui.angular.init'; export const ANGULAR_OP = 'ui.angular'; + +/** + * Minimum Angular version this SDK supports + */ +export const ANGULAR_MINIMUM_VERSION = 10; diff --git a/packages/angular/src/errorhandler.ts b/packages/angular/src/errorhandler.ts index 4e9c0f7ed037..54449071ee89 100644 --- a/packages/angular/src/errorhandler.ts +++ b/packages/angular/src/errorhandler.ts @@ -1,5 +1,5 @@ import { HttpErrorResponse } from '@angular/common/http'; -import { ErrorHandler as AngularErrorHandler, Injectable } from '@angular/core'; +import { ErrorHandler as AngularErrorHandler, Inject, Injectable } from '@angular/core'; import * as Sentry from '@sentry/browser'; import { runOutsideAngular } from './zone'; @@ -26,7 +26,7 @@ export interface ErrorHandlerOptions { class SentryErrorHandler implements AngularErrorHandler { protected readonly _options: ErrorHandlerOptions; - public constructor(options?: ErrorHandlerOptions) { + public constructor(@Inject('errorHandlerOptions') options?: ErrorHandlerOptions) { this._options = { logErrors: true, ...options, diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index 4b282610a16c..b6f188a35d14 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -1,7 +1,9 @@ +export type { ErrorHandlerOptions } from './errorhandler'; + export * from '@sentry/browser'; export { init } from './sdk'; -export { createErrorHandler, ErrorHandlerOptions, SentryErrorHandler } from './errorhandler'; +export { createErrorHandler, SentryErrorHandler } from './errorhandler'; export { getActiveTransaction, // TODO `instrumentAngularRouting` is just an alias for `routingInstrumentation`; deprecate the latter at some point diff --git a/packages/angular/src/sdk.ts b/packages/angular/src/sdk.ts index 09267d1f2082..bcce7c85ccdb 100644 --- a/packages/angular/src/sdk.ts +++ b/packages/angular/src/sdk.ts @@ -1,4 +1,9 @@ +import { VERSION } from '@angular/core'; import { BrowserOptions, init as browserInit, SDK_VERSION } from '@sentry/browser'; +import { logger } from '@sentry/utils'; + +import { ANGULAR_MINIMUM_VERSION } from './constants'; +import { IS_DEBUG_BUILD } from './flags'; /** * Inits the Angular SDK @@ -15,5 +20,20 @@ export function init(options: BrowserOptions): void { ], version: SDK_VERSION, }; + checkAngularVersion(); browserInit(options); } + +function checkAngularVersion(): void { + if (VERSION && VERSION.major) { + const major = parseInt(VERSION.major, 10); + if (major < ANGULAR_MINIMUM_VERSION) { + IS_DEBUG_BUILD && + logger.warn( + `The Sentry SDK does not officially support Angular ${major}.`, + `This version of the Sentry SDK supports Angular ${ANGULAR_MINIMUM_VERSION} and above.`, + 'Please consider upgrading your Angular version or downgrading the Sentry SDK.', + ); + } + } +} diff --git a/packages/angular/tsconfig.cjs.json b/packages/angular/tsconfig.cjs.json deleted file mode 100644 index abd80f77e1ff..000000000000 --- a/packages/angular/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "dist" - } -} diff --git a/packages/angular/tsconfig.esm.json b/packages/angular/tsconfig.esm.json deleted file mode 100644 index b6ee3fa615c0..000000000000 --- a/packages/angular/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "esm" - } -} diff --git a/packages/angular/tsconfig.ngc.json b/packages/angular/tsconfig.ngc.json new file mode 100644 index 000000000000..9dd04ce14239 --- /dev/null +++ b/packages/angular/tsconfig.ngc.json @@ -0,0 +1,28 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +// This tsconfig is used when building @sentry/angular with the Angular +// compiler and `ng-packagr`. It configures a production build conforming +// to the Angular Package Format (APF). +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "target": "es2015", + "lib": ["dom", "es2015"], + "baseUrl": "./" + }, + "angularCompilerOptions": { + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "enableResourceInlining": true, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true, + // As per Angular 10, the recommendation from the library creation guide + // is to disable compilation for the Ivy rendering engine in production builds + // to ensure compatibility with Angular 10. + // For Angular 11-13 applications, ngcc and the Angular linker convert the compiled JS + // at application compile time into an Ivy-compatible version which is then further used in + // the build process. This ensures compatibility with newer Angular versions than the one + // that was used to initially compile the library (Angular 10 in our case). + "enableIvy": false + } +} diff --git a/packages/angular/tsconfig.types.json b/packages/angular/tsconfig.types.json deleted file mode 100644 index 65455f66bd75..000000000000 --- a/packages/angular/tsconfig.types.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "declaration": true, - "declarationMap": true, - "emitDeclarationOnly": true, - "outDir": "build/types" - } -} diff --git a/packages/browser/README.md b/packages/browser/README.md index 486505c970b8..63c3ab561a6a 100644 --- a/packages/browser/README.md +++ b/packages/browser/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Official Sentry SDK for Browsers diff --git a/packages/browser/jest.config.js b/packages/browser/jest.config.js new file mode 100644 index 000000000000..f9cd8056a454 --- /dev/null +++ b/packages/browser/jest.config.js @@ -0,0 +1,7 @@ +const baseConfig = require('../../jest/jest.config.js'); + +module.exports = { + ...baseConfig, + testEnvironment: 'jsdom', + testMatch: ['/test/unit/**/*.test.ts'], +}; diff --git a/packages/browser/package.json b/packages/browser/package.json index 3ac30884c1f8..571e55071f82 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,24 +1,24 @@ { "name": "@sentry/browser", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Official Sentry SDK for browsers", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser", "author": "Sentry", "license": "BSD-3-Clause", "engines": { - "node": ">=6" + "node": ">=8" }, - "main": "build/npm/dist/index.js", + "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", "types": "build/npm/types/index.d.ts", "publishConfig": { "access": "public" }, "dependencies": { - "@sentry/core": "6.19.7", - "@sentry/types": "6.19.7", - "@sentry/utils": "6.19.7", + "@sentry/core": "7.0.0-rc.0", + "@sentry/types": "7.0.0-rc.0", + "@sentry/utils": "7.0.0-rc.0", "tslib": "^1.9.3" }, "devDependencies": { @@ -26,7 +26,6 @@ "btoa": "^1.2.1", "chai": "^4.1.2", "chokidar": "^3.0.2", - "jsdom": "^15.0.0", "karma": "^6.3.16", "karma-chai": "^0.1.0", "karma-chrome-launcher": "^2.2.0", @@ -44,27 +43,22 @@ "webpack": "^4.30.0" }, "scripts": { - "build": "run-p build:cjs build:esm build:bundle build:types", - "build:bundle": "rollup --config", - "build:cjs": "tsc -p tsconfig.cjs.json", - "build:dev": "run-p build:cjs build:esm build:types", - "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "tsc -p tsconfig.esm.json", + "build": "run-p build:rollup build:bundle build:types", + "build:bundle": "yarn ts-node ../../scripts/ensure-bundle-deps.ts && yarn rollup --config rollup.bundle.config.js", + "build:dev": "run-p build:rollup build:types", + "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:cjs:watch build:esm:watch build:bundle:watch build:types:watch", - "build:bundle:watch": "rollup --config --watch", - "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", - "build:es5:watch": "yarn build:cjs:watch # *** backwards compatibility - remove in v7 ***", - "build:dev:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", - "build:esm:watch": "tsc -p tsconfig.esm.json --watch", + "build:watch": "run-p build:rollup:watch build:bundle:watch build:types:watch", + "build:bundle:watch": "rollup --config rollup.bundle.config.js --watch", + "build:dev:watch": "run-p build:rollup:watch build:types:watch", + "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:npm": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage .rpt2_cache", + "clean": "rimraf build coverage .rpt2_cache sentry-browser-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/*.ts\"", - "link:yarn": "yarn link", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", @@ -72,11 +66,11 @@ "size:check:es5": "cat build/bundles/bundle.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print \"ES5: \",$1,\"kB\";}'", "size:check:es6": "cat build/bundles/bundle.es6.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print \"ES6: \",$1,\"kB\";}'", "test": "run-s test:unit", - "test:unit": "jest --config test/unit/jest.config.js", + "test:unit": "jest", "test:integration": "test/integration/run.js", "test:integration:checkbrowsers": "node scripts/checkbrowsers.js", "test:package": "node test/package/npm-build.js && rm test/package/tmp.js", - "test:unit:watch": "jest --config test/unit/jest.config.js --watch", + "test:unit:watch": "jest --watch", "test:integration:watch": "test/integration/run.js --watch" }, "volta": { diff --git a/packages/browser/rollup.bundle.config.js b/packages/browser/rollup.bundle.config.js new file mode 100644 index 000000000000..23f9bb474c94 --- /dev/null +++ b/packages/browser/rollup.bundle.config.js @@ -0,0 +1,17 @@ +import { makeBaseBundleConfig, makeBundleConfigVariants } from '../../rollup/index.js'; + +const builds = []; + +['es5', 'es6'].forEach(jsVersion => { + const baseBundleConfig = makeBaseBundleConfig({ + bundleType: 'standalone', + entrypoints: ['src/index.ts'], + jsVersion, + licenseTitle: '@sentry/browser', + outputFileBase: () => `bundles/bundle${jsVersion === 'es5' ? '.es5' : ''}`, + }); + + builds.push(...makeBundleConfigVariants(baseBundleConfig)); +}); + +export default builds; diff --git a/packages/browser/rollup.config.js b/packages/browser/rollup.config.js deleted file mode 100644 index d49892da07be..000000000000 --- a/packages/browser/rollup.config.js +++ /dev/null @@ -1,17 +0,0 @@ -import { makeBaseBundleConfig, makeConfigVariants } from '../../rollup.config'; - -const builds = []; - -['es5', 'es6'].forEach(jsVersion => { - const baseBundleConfig = makeBaseBundleConfig({ - input: 'src/index.ts', - isAddOn: false, - jsVersion, - licenseTitle: '@sentry/browser', - outputFileBase: `bundles/bundle${jsVersion === 'es6' ? '.es6' : ''}`, - }); - - builds.push(...makeConfigVariants(baseBundleConfig)); -}); - -export default builds; diff --git a/packages/browser/rollup.npm.config.js b/packages/browser/rollup.npm.config.js new file mode 100644 index 000000000000..4ffa8b9396d8 --- /dev/null +++ b/packages/browser/rollup.npm.config.js @@ -0,0 +1,8 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + // packages with bundles have a different build directory structure + hasBundles: true, + }), +); diff --git a/packages/browser/src/backend.ts b/packages/browser/src/backend.ts deleted file mode 100644 index 4cdef3d11047..000000000000 --- a/packages/browser/src/backend.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { BaseBackend, getEnvelopeEndpointWithUrlEncodedAuth, initAPIDetails } from '@sentry/core'; -import { Event, EventHint, Options, Severity, Transport, TransportOptions } from '@sentry/types'; -import { supportsFetch } from '@sentry/utils'; - -import { eventFromException, eventFromMessage } from './eventbuilder'; -import { FetchTransport, makeNewFetchTransport, makeNewXHRTransport, XHRTransport } from './transports'; - -/** - * Configuration options for the Sentry Browser SDK. - * @see BrowserClient for more information. - */ -export interface BrowserOptions extends Options { - /** - * A pattern for error URLs which should exclusively be sent to Sentry. - * This is the opposite of {@link Options.denyUrls}. - * By default, all errors will be sent. - */ - allowUrls?: Array; - - /** - * A pattern for error URLs which should not be sent to Sentry. - * To allow certain errors instead, use {@link Options.allowUrls}. - * By default, all errors will be sent. - */ - denyUrls?: Array; - - /** @deprecated use {@link Options.allowUrls} instead. */ - whitelistUrls?: Array; - - /** @deprecated use {@link Options.denyUrls} instead. */ - blacklistUrls?: Array; -} - -/** - * The Sentry Browser SDK Backend. - * @hidden - */ -export class BrowserBackend extends BaseBackend { - /** - * @inheritDoc - */ - public eventFromException(exception: unknown, hint?: EventHint): PromiseLike { - return eventFromException(exception, hint, this._options.attachStacktrace); - } - /** - * @inheritDoc - */ - public eventFromMessage(message: string, level: Severity = Severity.Info, hint?: EventHint): PromiseLike { - return eventFromMessage(message, level, hint, this._options.attachStacktrace); - } - - /** - * @inheritDoc - */ - protected _setupTransport(): Transport { - if (!this._options.dsn) { - // We return the noop transport here in case there is no Dsn. - return super._setupTransport(); - } - - const transportOptions: TransportOptions = { - ...this._options.transportOptions, - dsn: this._options.dsn, - tunnel: this._options.tunnel, - sendClientReports: this._options.sendClientReports, - _metadata: this._options._metadata, - }; - - const api = initAPIDetails(transportOptions.dsn, transportOptions._metadata, transportOptions.tunnel); - const url = getEnvelopeEndpointWithUrlEncodedAuth(api.dsn, api.tunnel); - - if (this._options.transport) { - return new this._options.transport(transportOptions); - } - if (supportsFetch()) { - const requestOptions: RequestInit = { ...transportOptions.fetchParameters }; - this._newTransport = makeNewFetchTransport({ requestOptions, url }); - return new FetchTransport(transportOptions); - } - - this._newTransport = makeNewXHRTransport({ - url, - headers: transportOptions.headers, - }); - return new XHRTransport(transportOptions); - } -} diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 75aab49db77b..9916ebd9c984 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -1,11 +1,50 @@ -import { BaseClient, Scope, SDK_VERSION } from '@sentry/core'; -import { Event, EventHint } from '@sentry/types'; -import { getGlobalObject, logger } from '@sentry/utils'; +import { BaseClient, getCurrentHub, getEnvelopeEndpointWithUrlEncodedAuth, Scope, SDK_VERSION } from '@sentry/core'; +import { ClientOptions, Event, EventHint, Options, Severity, SeverityLevel } from '@sentry/types'; +import { + createClientReportEnvelope, + dsnToString, + getEventDescription, + getGlobalObject, + logger, + serializeEnvelope, +} from '@sentry/utils'; -import { BrowserBackend, BrowserOptions } from './backend'; +import { eventFromException, eventFromMessage } from './eventbuilder'; import { IS_DEBUG_BUILD } from './flags'; -import { injectReportDialog, ReportDialogOptions } from './helpers'; import { Breadcrumbs } from './integrations'; +import { BREADCRUMB_INTEGRATION_ID } from './integrations/breadcrumbs'; +import { BrowserTransportOptions } from './transports/types'; +import { sendReport } from './transports/utils'; + +const globalObject = getGlobalObject(); + +export interface BaseBrowserOptions { + /** + * A pattern for error URLs which should exclusively be sent to Sentry. + * This is the opposite of {@link Options.denyUrls}. + * By default, all errors will be sent. + */ + allowUrls?: Array; + + /** + * A pattern for error URLs which should not be sent to Sentry. + * To allow certain errors instead, use {@link Options.allowUrls}. + * By default, all errors will be sent. + */ + denyUrls?: Array; +} + +/** + * Configuration options for the Sentry Browser SDK. + * @see @sentry/types Options for more information. + */ +export interface BrowserOptions extends Options, BaseBrowserOptions {} + +/** + * Configuration options for the Sentry Browser SDK Client class + * @see BrowserClient for more information. + */ +export interface BrowserClientOptions extends ClientOptions, BaseBrowserOptions {} /** * The Sentry Browser SDK Client. @@ -13,13 +52,13 @@ import { Breadcrumbs } from './integrations'; * @see BrowserOptions for documentation on configuration options. * @see SentryClient for usage documentation. */ -export class BrowserClient extends BaseClient { +export class BrowserClient extends BaseClient { /** * Creates a new Browser SDK instance. * * @param options Configuration options for this SDK. */ - public constructor(options: BrowserOptions = {}) { + public constructor(options: BrowserClientOptions) { options._metadata = options._metadata || {}; options._metadata.sdk = options._metadata.sdk || { name: 'sentry.javascript.browser', @@ -32,48 +71,104 @@ export class BrowserClient extends BaseClient { version: SDK_VERSION, }; - super(BrowserBackend, options); + super(options); + + if (options.sendClientReports && globalObject.document) { + globalObject.document.addEventListener('visibilitychange', () => { + if (globalObject.document.visibilityState === 'hidden') { + this._flushOutcomes(); + } + }); + } } /** - * Show a report dialog to the user to send feedback to a specific event. - * - * @param options Set individual options for the dialog + * @inheritDoc */ - public showReportDialog(options: ReportDialogOptions = {}): void { - // doesn't work without a document (React Native) - const document = getGlobalObject().document; - if (!document) { - return; - } + public eventFromException(exception: unknown, hint?: EventHint): PromiseLike { + return eventFromException(this._options.stackParser, exception, hint, this._options.attachStacktrace); + } - if (!this._isEnabled()) { - IS_DEBUG_BUILD && logger.error('Trying to call showReportDialog with Sentry Client disabled'); - return; + /** + * @inheritDoc + */ + public eventFromMessage( + message: string, + // eslint-disable-next-line deprecation/deprecation + level: Severity | SeverityLevel = 'info', + hint?: EventHint, + ): PromiseLike { + return eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace); + } + + /** + * @inheritDoc + */ + public sendEvent(event: Event, hint?: EventHint): void { + // We only want to add the sentry event breadcrumb when the user has the breadcrumb integration installed and + // activated its `sentry` option. + // We also do not want to use the `Breadcrumbs` class here directly, because we do not want it to be included in + // bundles, if it is not used by the SDK. + // This all sadly is a bit ugly, but we currently don't have a "pre-send" hook on the integrations so we do it this + // way for now. + const breadcrumbIntegration = this.getIntegrationById(BREADCRUMB_INTEGRATION_ID) as Breadcrumbs | null; + if ( + breadcrumbIntegration && + // We check for definedness of `options`, even though it is not strictly necessary, because that access to + // `.sentry` below does not throw, in case users provided their own integration with id "Breadcrumbs" that does + // not have an`options` field + breadcrumbIntegration.options && + breadcrumbIntegration.options.sentry + ) { + getCurrentHub().addBreadcrumb( + { + category: `sentry.${event.type === 'transaction' ? 'transaction' : 'event'}`, + event_id: event.event_id, + level: event.level, + message: getEventDescription(event), + }, + { + event, + }, + ); } - injectReportDialog({ - ...options, - dsn: options.dsn || this.getDsn(), - }); + super.sendEvent(event, hint); } /** * @inheritDoc */ - protected _prepareEvent(event: Event, scope?: Scope, hint?: EventHint): PromiseLike { + protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { event.platform = event.platform || 'javascript'; - return super._prepareEvent(event, scope, hint); + return super._prepareEvent(event, hint, scope); } /** - * @inheritDoc + * Sends client reports as an envelope. */ - protected _sendEvent(event: Event): void { - const integration = this.getIntegration(Breadcrumbs); - if (integration) { - integration.addSentryBreadcrumb(event); + private _flushOutcomes(): void { + const outcomes = this._clearOutcomes(); + + if (outcomes.length === 0) { + IS_DEBUG_BUILD && logger.log('No outcomes to send'); + return; + } + + if (!this._dsn) { + IS_DEBUG_BUILD && logger.log('No dsn provided, will not send outcomes'); + return; + } + + IS_DEBUG_BUILD && logger.log('Sending outcomes:', outcomes); + + const url = getEnvelopeEndpointWithUrlEncodedAuth(this._dsn, this._options.tunnel); + const envelope = createClientReportEnvelope(outcomes, this._options.tunnel && dsnToString(this._dsn)); + + try { + sendReport(url, serializeEnvelope(envelope)); + } catch (e) { + IS_DEBUG_BUILD && logger.error(e); } - super._sendEvent(event); } } diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index f999eb2854ca..2324fcd8b296 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -1,8 +1,7 @@ -import { Event, EventHint, Exception, Severity, StackFrame } from '@sentry/types'; +import { Event, EventHint, Exception, Severity, SeverityLevel, StackFrame, StackParser } from '@sentry/types'; import { addExceptionMechanism, addExceptionTypeValue, - createStackParser, extractExceptionKeysForMessage, isDOMError, isDOMException, @@ -14,22 +13,12 @@ import { resolvedSyncPromise, } from '@sentry/utils'; -import { - chromeStackParser, - geckoStackParser, - opera10StackParser, - opera11StackParser, - winjsStackParser, -} from './stack-parsers'; - /** - * This function creates an exception from an TraceKitStackTrace - * @param stacktrace TraceKitStackTrace that will be converted to an exception - * @hidden + * This function creates an exception from a JavaScript Error */ -export function exceptionFromError(ex: Error): Exception { +export function exceptionFromError(stackParser: StackParser, ex: Error): Exception { // Get the frames first since Opera can lose the stack if we touch anything else first - const frames = parseStackFrames(ex); + const frames = parseStackFrames(stackParser, ex); const exception: Exception = { type: ex && ex.name, @@ -51,6 +40,7 @@ export function exceptionFromError(ex: Error): Exception { * @hidden */ export function eventFromPlainObject( + stackParser: StackParser, exception: Record, syntheticException?: Error, isUnhandledRejection?: boolean, @@ -72,9 +62,10 @@ export function eventFromPlainObject( }; if (syntheticException) { - const frames = parseStackFrames(syntheticException); + const frames = parseStackFrames(stackParser, syntheticException); if (frames.length) { - event.stacktrace = { frames }; + // event.exception.values[0] has been set above + (event.exception as { values: Exception[] }).values[0].stacktrace = { frames }; } } @@ -84,16 +75,19 @@ export function eventFromPlainObject( /** * @hidden */ -export function eventFromError(ex: Error): Event { +export function eventFromError(stackParser: StackParser, ex: Error): Event { return { exception: { - values: [exceptionFromError(ex)], + values: [exceptionFromError(stackParser, ex)], }, }; } /** Parses stack frames from an error */ -export function parseStackFrames(ex: Error & { framesToPop?: number; stacktrace?: string }): StackFrame[] { +export function parseStackFrames( + stackParser: StackParser, + ex: Error & { framesToPop?: number; stacktrace?: string }, +): StackFrame[] { // Access and store the stacktrace property before doing ANYTHING // else to it because Opera is not very good at providing it // reliably in other circumstances. @@ -102,13 +96,7 @@ export function parseStackFrames(ex: Error & { framesToPop?: number; stacktrace? const popSize = getPopSize(ex); try { - return createStackParser( - opera10StackParser, - opera11StackParser, - chromeStackParser, - winjsStackParser, - geckoStackParser, - )(stacktrace, popSize); + return stackParser(stacktrace, popSize); } catch (e) { // no-empty } @@ -154,14 +142,15 @@ function extractMessage(ex: Error & { message: { error?: Error } }): string { * @hidden */ export function eventFromException( + stackParser: StackParser, exception: unknown, hint?: EventHint, attachStacktrace?: boolean, ): PromiseLike { const syntheticException = (hint && hint.syntheticException) || undefined; - const event = eventFromUnknownInput(exception, syntheticException, attachStacktrace); + const event = eventFromUnknownInput(stackParser, exception, syntheticException, attachStacktrace); addExceptionMechanism(event); // defaults to { type: 'generic', handled: true } - event.level = Severity.Error; + event.level = 'error'; if (hint && hint.event_id) { event.event_id = hint.event_id; } @@ -173,13 +162,15 @@ export function eventFromException( * @hidden */ export function eventFromMessage( + stackParser: StackParser, message: string, - level: Severity = Severity.Info, + // eslint-disable-next-line deprecation/deprecation + level: Severity | SeverityLevel = 'info', hint?: EventHint, attachStacktrace?: boolean, ): PromiseLike { const syntheticException = (hint && hint.syntheticException) || undefined; - const event = eventFromString(message, syntheticException, attachStacktrace); + const event = eventFromString(stackParser, message, syntheticException, attachStacktrace); event.level = level; if (hint && hint.event_id) { event.event_id = hint.event_id; @@ -191,6 +182,7 @@ export function eventFromMessage( * @hidden */ export function eventFromUnknownInput( + stackParser: StackParser, exception: unknown, syntheticException?: Error, attachStacktrace?: boolean, @@ -201,7 +193,7 @@ export function eventFromUnknownInput( if (isErrorEvent(exception as ErrorEvent) && (exception as ErrorEvent).error) { // If it is an ErrorEvent with `error` property, extract it to get actual Error const errorEvent = exception as ErrorEvent; - return eventFromError(errorEvent.error as Error); + return eventFromError(stackParser, errorEvent.error as Error); } // If it is a `DOMError` (which is a legacy API, but still supported in some browsers) then we just extract the name @@ -215,11 +207,11 @@ export function eventFromUnknownInput( const domException = exception as DOMException; if ('stack' in (exception as Error)) { - event = eventFromError(exception as Error); + event = eventFromError(stackParser, exception as Error); } else { const name = domException.name || (isDOMError(domException) ? 'DOMError' : 'DOMException'); const message = domException.message ? `${name}: ${domException.message}` : name; - event = eventFromString(message, syntheticException, attachStacktrace); + event = eventFromString(stackParser, message, syntheticException, attachStacktrace); addExceptionTypeValue(event, message); } if ('code' in domException) { @@ -230,14 +222,14 @@ export function eventFromUnknownInput( } if (isError(exception)) { // we have a real Error object, do nothing - return eventFromError(exception); + return eventFromError(stackParser, exception); } if (isPlainObject(exception) || isEvent(exception)) { // If it's a plain object or an instance of `Event` (the built-in JS kind, not this SDK's `Event` type), serialize // it manually. This will allow us to group events based on top-level keys which is much better than creating a new // group on any key/value change. const objectException = exception as Record; - event = eventFromPlainObject(objectException, syntheticException, isUnhandledRejection); + event = eventFromPlainObject(stackParser, objectException, syntheticException, isUnhandledRejection); addExceptionMechanism(event, { synthetic: true, }); @@ -253,7 +245,7 @@ export function eventFromUnknownInput( // - a plain Object // // So bail out and capture it as a simple message: - event = eventFromString(exception as string, syntheticException, attachStacktrace); + event = eventFromString(stackParser, exception as string, syntheticException, attachStacktrace); addExceptionTypeValue(event, `${exception}`, undefined); addExceptionMechanism(event, { synthetic: true, @@ -265,15 +257,22 @@ export function eventFromUnknownInput( /** * @hidden */ -export function eventFromString(input: string, syntheticException?: Error, attachStacktrace?: boolean): Event { +export function eventFromString( + stackParser: StackParser, + input: string, + syntheticException?: Error, + attachStacktrace?: boolean, +): Event { const event: Event = { message: input, }; if (attachStacktrace && syntheticException) { - const frames = parseStackFrames(syntheticException); + const frames = parseStackFrames(stackParser, syntheticException); if (frames.length) { - event.stacktrace = { frames }; + event.exception = { + values: [{ value: input, stacktrace: { frames } }], + }; } } diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 86ace0abbdd5..fd8ff9842a22 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -1,21 +1,23 @@ -export { +export type { Breadcrumb, BreadcrumbHint, Request, SdkInfo, Event, EventHint, - EventStatus, Exception, - Response, + // eslint-disable-next-line deprecation/deprecation Severity, + SeverityLevel, StackFrame, Stacktrace, Thread, User, + Session, } from '@sentry/types'; -export { SeverityLevel } from '@sentry/utils'; +export type { BrowserOptions } from './client'; +export type { ReportDialogOptions } from './helpers'; export { addGlobalEventProcessor, @@ -24,12 +26,12 @@ export { captureEvent, captureMessage, configureScope, + createTransport, getHubFromCarrier, getCurrentHub, Hub, makeMain, Scope, - Session, startTransaction, SDK_VERSION, setContext, @@ -39,11 +41,20 @@ export { setTags, setUser, withScope, + FunctionToString, + InboundFilters, } from '@sentry/core'; -export { BrowserOptions } from './backend'; export { BrowserClient } from './client'; -export { injectReportDialog, ReportDialogOptions } from './helpers'; -export { eventFromException, eventFromMessage } from './eventbuilder'; +export { makeFetchTransport, makeXHRTransport } from './transports'; +export { + defaultStackParser, + defaultStackLineParsers, + chromeStackLineParser, + geckoStackLineParser, + opera10StackLineParser, + opera11StackLineParser, + winjsStackLineParser, +} from './stack-parsers'; export { defaultIntegrations, forceLoad, init, lastEventId, onLoad, showReportDialog, flush, close, wrap } from './sdk'; -export { SDK_NAME } from './version'; +export { GlobalHandlers, TryCatch, Breadcrumbs, LinkedErrors, HttpContext, Dedupe } from './integrations'; diff --git a/packages/browser/src/helpers.ts b/packages/browser/src/helpers.ts index d0b3f8f5c9aa..870756066df4 100644 --- a/packages/browser/src/helpers.ts +++ b/packages/browser/src/helpers.ts @@ -1,18 +1,13 @@ -import { captureException, getReportDialogEndpoint, withScope } from '@sentry/core'; +import { captureException, withScope } from '@sentry/core'; import { DsnLike, Event as SentryEvent, Mechanism, Scope, WrappedFunction } from '@sentry/types'; import { addExceptionMechanism, addExceptionTypeValue, addNonEnumerableProperty, - getGlobalObject, getOriginalFunction, - logger, markFunctionWrapped, } from '@sentry/utils'; -import { IS_DEBUG_BUILD } from './flags'; - -const global = getGlobalObject(); let ignoreOnError: number = 0; /** @@ -37,7 +32,8 @@ export function ignoreNextOnError(): void { * Instruments the given function and sends an event to Sentry every time the * function throws an exception. * - * @param fn A function to wrap. + * @param fn A function to wrap. It is generally safe to pass an unbound function, because the returned wrapper always + * has a correct `this` context. * @returns The wrapped function. * @hidden */ @@ -80,8 +76,8 @@ export function wrap( } /* eslint-disable prefer-rest-params */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const sentryWrapped: WrappedFunction = function (this: any): void { + // It is important that `sentryWrapped` is not an arrow function to preserve the context of `this` + const sentryWrapped: WrappedFunction = function (this: unknown): void { const args = Array.prototype.slice.call(arguments); try { @@ -182,38 +178,3 @@ export interface ReportDialogOptions { /** Callback after reportDialog showed up */ onLoad?(): void; } - -/** - * Injects the Report Dialog script - * @hidden - */ -export function injectReportDialog(options: ReportDialogOptions = {}): void { - if (!global.document) { - return; - } - - if (!options.eventId) { - IS_DEBUG_BUILD && logger.error('Missing eventId option in showReportDialog call'); - return; - } - - if (!options.dsn) { - IS_DEBUG_BUILD && logger.error('Missing dsn option in showReportDialog call'); - return; - } - - const script = global.document.createElement('script'); - script.async = true; - script.src = getReportDialogEndpoint(options.dsn, options); - - if (options.onLoad) { - // eslint-disable-next-line @typescript-eslint/unbound-method - script.onload = options.onLoad; - } - - const injectionPoint = global.document.head || global.document.body; - - if (injectionPoint) { - injectionPoint.appendChild(script); - } -} diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 0b94c020592f..ab754f6a192f 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -4,7 +4,6 @@ import { Integrations as CoreIntegrations } from '@sentry/core'; import { getGlobalObject } from '@sentry/utils'; import * as BrowserIntegrations from './integrations'; -import * as Transports from './transports'; let windowIntegrations = {}; @@ -20,4 +19,4 @@ const INTEGRATIONS = { ...BrowserIntegrations, }; -export { INTEGRATIONS as Integrations, Transports }; +export { INTEGRATIONS as Integrations }; diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 15237f599b15..c633bf5d139c 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -1,15 +1,14 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable max-lines */ import { getCurrentHub } from '@sentry/core'; -import { Event, Integration, Severity } from '@sentry/types'; +import { Integration } from '@sentry/types'; import { addInstrumentationHandler, - getEventDescription, getGlobalObject, htmlTreeAsString, parseUrl, safeJoin, - severityFromString, + severityLevelFromString, } from '@sentry/utils'; /** JSDoc */ @@ -22,6 +21,8 @@ interface BreadcrumbsOptions { xhr: boolean; } +export const BREADCRUMB_INTEGRATION_ID = 'Breadcrumbs'; + /** * Default Breadcrumbs instrumentations * TODO: Deprecated - with v6, this will be renamed to `Instrument` @@ -30,21 +31,24 @@ export class Breadcrumbs implements Integration { /** * @inheritDoc */ - public static id: string = 'Breadcrumbs'; + public static id: string = BREADCRUMB_INTEGRATION_ID; /** * @inheritDoc */ public name: string = Breadcrumbs.id; - /** JSDoc */ - private readonly _options: BreadcrumbsOptions; + /** + * Options of the breadcrumbs integration. + */ + // This field is public, because we use it in the browser client to check if the `sentry` option is enabled. + public readonly options: Readonly; /** * @inheritDoc */ public constructor(options?: Partial) { - this._options = { + this.options = { console: true, dom: true, fetch: true, @@ -55,26 +59,6 @@ export class Breadcrumbs implements Integration { }; } - /** - * Create a breadcrumb of `sentry` from the events themselves - */ - public addSentryBreadcrumb(event: Event): void { - if (!this._options.sentry) { - return; - } - getCurrentHub().addBreadcrumb( - { - category: `sentry.${event.type === 'transaction' ? 'transaction' : 'event'}`, - event_id: event.event_id, - level: event.level, - message: getEventDescription(event), - }, - { - event, - }, - ); - } - /** * Instrument browser built-ins w/ breadcrumb capturing * - Console API @@ -84,19 +68,19 @@ export class Breadcrumbs implements Integration { * - History API */ public setupOnce(): void { - if (this._options.console) { + if (this.options.console) { addInstrumentationHandler('console', _consoleBreadcrumb); } - if (this._options.dom) { - addInstrumentationHandler('dom', _domBreadcrumb(this._options.dom)); + if (this.options.dom) { + addInstrumentationHandler('dom', _domBreadcrumb(this.options.dom)); } - if (this._options.xhr) { + if (this.options.xhr) { addInstrumentationHandler('xhr', _xhrBreadcrumb); } - if (this._options.fetch) { + if (this.options.fetch) { addInstrumentationHandler('fetch', _fetchBreadcrumb); } - if (this._options.history) { + if (this.options.history) { addInstrumentationHandler('history', _historyBreadcrumb); } } @@ -157,7 +141,7 @@ function _consoleBreadcrumb(handlerData: { [key: string]: any }): void { arguments: handlerData.args, logger: 'console', }, - level: severityFromString(handlerData.level), + level: severityLevelFromString(handlerData.level), message: safeJoin(handlerData.args, ' '), }; @@ -230,7 +214,7 @@ function _fetchBreadcrumb(handlerData: { [key: string]: any }): void { { category: 'fetch', data: handlerData.fetchData, - level: Severity.Error, + level: 'error', type: 'http', }, { diff --git a/packages/browser/src/integrations/dedupe.ts b/packages/browser/src/integrations/dedupe.ts index 24e01d2f1226..48aa8411f268 100644 --- a/packages/browser/src/integrations/dedupe.ts +++ b/packages/browser/src/integrations/dedupe.ts @@ -24,7 +24,7 @@ export class Dedupe implements Integration { * @inheritDoc */ public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor((currentEvent: Event) => { + const eventProcessor: EventProcessor = currentEvent => { const self = getCurrentHub().getIntegration(Dedupe); if (self) { // Juuust in case something goes wrong @@ -40,7 +40,10 @@ export class Dedupe implements Integration { return (self._previousEvent = currentEvent); } return currentEvent; - }); + }; + + eventProcessor.id = this.name; + addGlobalEventProcessor(eventProcessor); } } @@ -198,8 +201,6 @@ function _getFramesFromEvent(event: Event): StackFrame[] | undefined { } catch (_oO) { return undefined; } - } else if (event.stacktrace) { - return event.stacktrace.frames; } return undefined; } diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index 19ee594afaf3..9452bf8f7103 100644 --- a/packages/browser/src/integrations/globalhandlers.ts +++ b/packages/browser/src/integrations/globalhandlers.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { getCurrentHub } from '@sentry/core'; -import { Event, EventHint, Hub, Integration, Primitive, Severity } from '@sentry/types'; +import { Event, EventHint, Hub, Integration, Primitive, StackParser } from '@sentry/types'; import { addExceptionMechanism, addInstrumentationHandler, @@ -11,6 +11,7 @@ import { logger, } from '@sentry/utils'; +import { BrowserClient } from '../client'; import { eventFromUnknownInput } from '../eventbuilder'; import { IS_DEBUG_BUILD } from '../flags'; import { shouldIgnoreOnError } from '../helpers'; @@ -79,7 +80,7 @@ function _installGlobalOnErrorHandler(): void { 'error', // eslint-disable-next-line @typescript-eslint/no-explicit-any (data: { msg: any; url: any; line: any; column: any; error: any }) => { - const [hub, attachStacktrace] = getHubAndAttachStacktrace(); + const [hub, stackParser, attachStacktrace] = getHubAndOptions(); if (!hub.getIntegration(GlobalHandlers)) { return; } @@ -92,13 +93,13 @@ function _installGlobalOnErrorHandler(): void { error === undefined && isString(msg) ? _eventFromIncompleteOnError(msg, url, line, column) : _enhanceEventWithInitialFrame( - eventFromUnknownInput(error || msg, undefined, attachStacktrace, false), + eventFromUnknownInput(stackParser, error || msg, undefined, attachStacktrace, false), url, line, column, ); - event.level = Severity.Error; + event.level = 'error'; addMechanismAndCapture(hub, error, event, 'onerror'); }, @@ -111,7 +112,7 @@ function _installGlobalOnUnhandledRejectionHandler(): void { 'unhandledrejection', // eslint-disable-next-line @typescript-eslint/no-explicit-any (e: any) => { - const [hub, attachStacktrace] = getHubAndAttachStacktrace(); + const [hub, stackParser, attachStacktrace] = getHubAndOptions(); if (!hub.getIntegration(GlobalHandlers)) { return; } @@ -142,9 +143,9 @@ function _installGlobalOnUnhandledRejectionHandler(): void { const event = isPrimitive(error) ? _eventFromRejectionWithPrimitive(error) - : eventFromUnknownInput(error, undefined, attachStacktrace, true); + : eventFromUnknownInput(stackParser, error, undefined, attachStacktrace, true); - event.level = Severity.Error; + event.level = 'error'; addMechanismAndCapture(hub, error, event, 'onunhandledrejection'); return; @@ -250,9 +251,12 @@ function addMechanismAndCapture(hub: Hub, error: EventHint['originalException'], }); } -function getHubAndAttachStacktrace(): [Hub, boolean | undefined] { +function getHubAndOptions(): [Hub, StackParser, boolean | undefined] { const hub = getCurrentHub(); - const client = hub.getClient(); - const attachStacktrace = client && client.getOptions().attachStacktrace; - return [hub, attachStacktrace]; + const client = hub.getClient(); + const options = (client && client.getOptions()) || { + stackParser: () => [], + attachStacktrace: false, + }; + return [hub, options.stackParser, options.attachStacktrace]; } diff --git a/packages/browser/src/integrations/useragent.ts b/packages/browser/src/integrations/httpcontext.ts similarity index 81% rename from packages/browser/src/integrations/useragent.ts rename to packages/browser/src/integrations/httpcontext.ts index 1160320f9d93..2c7d5aa4e841 100644 --- a/packages/browser/src/integrations/useragent.ts +++ b/packages/browser/src/integrations/httpcontext.ts @@ -4,24 +4,24 @@ import { getGlobalObject } from '@sentry/utils'; const global = getGlobalObject(); -/** UserAgent */ -export class UserAgent implements Integration { +/** HttpContext integration collects information about HTTP request headers */ +export class HttpContext implements Integration { /** * @inheritDoc */ - public static id: string = 'UserAgent'; + public static id: string = 'HttpContext'; /** * @inheritDoc */ - public name: string = UserAgent.id; + public name: string = HttpContext.id; /** * @inheritDoc */ public setupOnce(): void { addGlobalEventProcessor((event: Event) => { - if (getCurrentHub().getIntegration(UserAgent)) { + if (getCurrentHub().getIntegration(HttpContext)) { // if none of the information we want exists, don't bother if (!global.navigator && !global.location && !global.document) { return event; diff --git a/packages/browser/src/integrations/index.ts b/packages/browser/src/integrations/index.ts index a354fdc883e9..e029422f363c 100644 --- a/packages/browser/src/integrations/index.ts +++ b/packages/browser/src/integrations/index.ts @@ -2,5 +2,5 @@ export { GlobalHandlers } from './globalhandlers'; export { TryCatch } from './trycatch'; export { Breadcrumbs } from './breadcrumbs'; export { LinkedErrors } from './linkederrors'; -export { UserAgent } from './useragent'; +export { HttpContext } from './httpcontext'; export { Dedupe } from './dedupe'; diff --git a/packages/browser/src/integrations/linkederrors.ts b/packages/browser/src/integrations/linkederrors.ts index fa197b48e681..8fa4413f5a83 100644 --- a/packages/browser/src/integrations/linkederrors.ts +++ b/packages/browser/src/integrations/linkederrors.ts @@ -1,7 +1,8 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; -import { Event, EventHint, Exception, ExtendedError, Integration } from '@sentry/types'; +import { Event, EventHint, Exception, ExtendedError, Integration, StackParser } from '@sentry/types'; import { isInstanceOf } from '@sentry/utils'; +import { BrowserClient } from '../client'; import { exceptionFromError } from '../eventbuilder'; const DEFAULT_KEY = 'cause'; @@ -46,9 +47,13 @@ export class LinkedErrors implements Integration { * @inheritDoc */ public setupOnce(): void { + const client = getCurrentHub().getClient(); + if (!client) { + return; + } addGlobalEventProcessor((event: Event, hint?: EventHint) => { const self = getCurrentHub().getIntegration(LinkedErrors); - return self ? _handler(self._key, self._limit, event, hint) : event; + return self ? _handler(client.getOptions().stackParser, self._key, self._limit, event, hint) : event; }); } } @@ -56,11 +61,17 @@ export class LinkedErrors implements Integration { /** * @inheritDoc */ -export function _handler(key: string, limit: number, event: Event, hint?: EventHint): Event | null { +export function _handler( + parser: StackParser, + key: string, + limit: number, + event: Event, + hint?: EventHint, +): Event | null { if (!event.exception || !event.exception.values || !hint || !isInstanceOf(hint.originalException, Error)) { return event; } - const linkedErrors = _walkErrorTree(limit, hint.originalException as ExtendedError, key); + const linkedErrors = _walkErrorTree(parser, limit, hint.originalException as ExtendedError, key); event.exception.values = [...linkedErrors, ...event.exception.values]; return event; } @@ -68,10 +79,16 @@ export function _handler(key: string, limit: number, event: Event, hint?: EventH /** * JSDOC */ -export function _walkErrorTree(limit: number, error: ExtendedError, key: string, stack: Exception[] = []): Exception[] { +export function _walkErrorTree( + parser: StackParser, + limit: number, + error: ExtendedError, + key: string, + stack: Exception[] = [], +): Exception[] { if (!isInstanceOf(error[key], Error) || stack.length + 1 >= limit) { return stack; } - const exception = exceptionFromError(error[key]); - return _walkErrorTree(limit, error[key], key, [exception, ...stack]); + const exception = exceptionFromError(parser, error[key]); + return _walkErrorTree(parser, limit, error[key], key, [exception, ...stack]); } diff --git a/packages/browser/src/integrations/trycatch.ts b/packages/browser/src/integrations/trycatch.ts index 84391d238b65..b6b293d70c37 100644 --- a/packages/browser/src/integrations/trycatch.ts +++ b/packages/browser/src/integrations/trycatch.ts @@ -208,7 +208,13 @@ function _wrapEventTarget(target: string): void { ): (eventName: string, fn: EventListenerObject, capture?: boolean, secure?: boolean) => void { try { if (typeof fn.handleEvent === 'function') { - fn.handleEvent = wrap(fn.handleEvent.bind(fn), { + // ESlint disable explanation: + // First, it is generally safe to call `wrap` with an unbound function. Furthermore, using `.bind()` would + // introduce a bug here, because bind returns a new function that doesn't have our + // flags(like __sentry_original__) attached. `wrap` checks for those flags to avoid unnecessary wrapping. + // Without those flags, every call to addEventListener wraps the function again, causing a memory leak. + // eslint-disable-next-line @typescript-eslint/unbound-method + fn.handleEvent = wrap(fn.handleEvent, { mechanism: { data: { function: 'handleEvent', diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 3f05a646c96f..584742b26f69 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -1,12 +1,26 @@ -import { getCurrentHub, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; -import { Hub } from '@sentry/types'; -import { addInstrumentationHandler, getGlobalObject, logger, resolvedSyncPromise } from '@sentry/utils'; +import { + getCurrentHub, + getIntegrationsToSetup, + getReportDialogEndpoint, + Hub, + initAndBind, + Integrations as CoreIntegrations, +} from '@sentry/core'; +import { + addInstrumentationHandler, + getGlobalObject, + logger, + resolvedSyncPromise, + stackParserFromStackParserOptions, + supportsFetch, +} from '@sentry/utils'; -import { BrowserOptions } from './backend'; -import { BrowserClient } from './client'; +import { BrowserClient, BrowserClientOptions, BrowserOptions } from './client'; import { IS_DEBUG_BUILD } from './flags'; import { ReportDialogOptions, wrap as internalWrap } from './helpers'; -import { Breadcrumbs, Dedupe, GlobalHandlers, LinkedErrors, TryCatch, UserAgent } from './integrations'; +import { Breadcrumbs, Dedupe, GlobalHandlers, HttpContext, LinkedErrors, TryCatch } from './integrations'; +import { defaultStackParser } from './stack-parsers'; +import { makeFetchTransport, makeXHRTransport } from './transports'; export const defaultIntegrations = [ new CoreIntegrations.InboundFilters(), @@ -16,7 +30,7 @@ export const defaultIntegrations = [ new GlobalHandlers(), new LinkedErrors(), new Dedupe(), - new UserAgent(), + new HttpContext(), ]; /** @@ -94,7 +108,14 @@ export function init(options: BrowserOptions = {}): void { options.sendClientReports = true; } - initAndBind(BrowserClient, options); + const clientOptions: BrowserClientOptions = { + ...options, + stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), + integrations: getIntegrationsToSetup(options), + transport: options.transport || (supportsFetch() ? makeFetchTransport : makeXHRTransport), + }; + + initAndBind(BrowserClient, clientOptions); if (options.autoSessionTracking) { startSessionTracking(); @@ -106,9 +127,21 @@ export function init(options: BrowserOptions = {}): void { * * @param options Everything is optional, we try to fetch all info need from the global scope. */ -export function showReportDialog(options: ReportDialogOptions = {}): void { - const hub = getCurrentHub(); - const scope = hub.getScope(); +export function showReportDialog(options: ReportDialogOptions = {}, hub: Hub = getCurrentHub()): void { + // doesn't work without a document (React Native) + const global = getGlobalObject(); + if (!global.document) { + IS_DEBUG_BUILD && logger.error('Global document not defined in showReportDialog call'); + return; + } + + const { client, scope } = hub.getStackTop(); + const dsn = options.dsn || (client && client.getDsn()); + if (!dsn) { + IS_DEBUG_BUILD && logger.error('DSN not configured for showReportDialog call'); + return; + } + if (scope) { options.user = { ...scope.getUser(), @@ -119,9 +152,21 @@ export function showReportDialog(options: ReportDialogOptions = {}): void { if (!options.eventId) { options.eventId = hub.lastEventId(); } - const client = hub.getClient(); - if (client) { - client.showReportDialog(options); + + const script = global.document.createElement('script'); + script.async = true; + script.src = getReportDialogEndpoint(dsn, options); + + if (options.onLoad) { + // eslint-disable-next-line @typescript-eslint/unbound-method + script.onload = options.onLoad; + } + + const injectionPoint = global.document.head || global.document.body; + if (injectionPoint) { + injectionPoint.appendChild(script); + } else { + IS_DEBUG_BUILD && logger.error('Not injecting report dialog. No injection point found in HTML'); } } diff --git a/packages/browser/src/stack-parsers.ts b/packages/browser/src/stack-parsers.ts index f54c1df803e9..0d01878ae7fe 100644 --- a/packages/browser/src/stack-parsers.ts +++ b/packages/browser/src/stack-parsers.ts @@ -1,5 +1,5 @@ -import { StackFrame } from '@sentry/types'; -import { StackLineParser, StackLineParserFn } from '@sentry/utils'; +import { StackFrame, StackLineParser, StackLineParserFn } from '@sentry/types'; +import { createStackParser } from '@sentry/utils'; // global reference to slice const UNKNOWN_FUNCTION = '?'; @@ -61,7 +61,7 @@ const chrome: StackLineParserFn = line => { return; }; -export const chromeStackParser: StackLineParser = [CHROME_PRIORITY, chrome]; +export const chromeStackLineParser: StackLineParser = [CHROME_PRIORITY, chrome]; // gecko regex: `(?:bundle|\d+\.js)`: `bundle` is for react native, `\d+\.js` also but specifically for ram bundles because it // generates filenames without a prefix like `file://` the filenames in the stacktrace are just 42.js @@ -97,7 +97,7 @@ const gecko: StackLineParserFn = line => { return; }; -export const geckoStackParser: StackLineParser = [GECKO_PRIORITY, gecko]; +export const geckoStackLineParser: StackLineParser = [GECKO_PRIORITY, gecko]; const winjsRegex = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i; @@ -110,7 +110,7 @@ const winjs: StackLineParserFn = line => { : undefined; }; -export const winjsStackParser: StackLineParser = [WINJS_PRIORITY, winjs]; +export const winjsStackLineParser: StackLineParser = [WINJS_PRIORITY, winjs]; const opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i; @@ -119,7 +119,7 @@ const opera10: StackLineParserFn = line => { return parts ? createFrame(parts[2], parts[3] || UNKNOWN_FUNCTION, +parts[1]) : undefined; }; -export const opera10StackParser: StackLineParser = [OPERA10_PRIORITY, opera10]; +export const opera10StackLineParser: StackLineParser = [OPERA10_PRIORITY, opera10]; const opera11Regex = / line (\d+), column (\d+)\s*(?:in (?:]+)>|([^)]+))\(.*\))? in (.*):\s*$/i; @@ -129,7 +129,11 @@ const opera11: StackLineParserFn = line => { return parts ? createFrame(parts[5], parts[3] || parts[4] || UNKNOWN_FUNCTION, +parts[1], +parts[2]) : undefined; }; -export const opera11StackParser: StackLineParser = [OPERA11_PRIORITY, opera11]; +export const opera11StackLineParser: StackLineParser = [OPERA11_PRIORITY, opera11]; + +export const defaultStackLineParsers = [chromeStackLineParser, geckoStackLineParser, winjsStackLineParser]; + +export const defaultStackParser = createStackParser(...defaultStackLineParsers); /** * Safari web extensions, starting version unknown, can produce "frames-only" stacktraces. diff --git a/packages/browser/src/transports/base.ts b/packages/browser/src/transports/base.ts deleted file mode 100644 index c7785b4eb7c5..000000000000 --- a/packages/browser/src/transports/base.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { - APIDetails, - eventToSentryRequest, - getEnvelopeEndpointWithUrlEncodedAuth, - getStoreEndpointWithUrlEncodedAuth, - initAPIDetails, - sessionToSentryRequest, -} from '@sentry/core'; -import { - ClientReport, - Event, - Outcome, - Response as SentryResponse, - SentryRequest, - SentryRequestType, - Session, - Transport, - TransportOptions, -} from '@sentry/types'; -import { - createClientReportEnvelope, - disabledUntil, - dsnToString, - eventStatusFromHttpCode, - getGlobalObject, - isRateLimited, - logger, - makePromiseBuffer, - PromiseBuffer, - RateLimits, - serializeEnvelope, - updateRateLimits, -} from '@sentry/utils'; - -import { IS_DEBUG_BUILD } from '../flags'; -import { sendReport } from './utils'; - -function requestTypeToCategory(ty: SentryRequestType): string { - const tyStr = ty as string; - return tyStr === 'event' ? 'error' : tyStr; -} - -const global = getGlobalObject(); - -/** Base Transport class implementation */ -export abstract class BaseTransport implements Transport { - /** - * @deprecated - */ - public url: string; - - /** Helper to get Sentry API endpoints. */ - protected readonly _api: APIDetails; - - /** A simple buffer holding all requests. */ - protected readonly _buffer: PromiseBuffer = makePromiseBuffer(30); - - /** Locks transport after receiving rate limits in a response */ - protected _rateLimits: RateLimits = {}; - - protected _outcomes: { [key: string]: number } = {}; - - public constructor(public options: TransportOptions) { - this._api = initAPIDetails(options.dsn, options._metadata, options.tunnel); - // eslint-disable-next-line deprecation/deprecation - this.url = getStoreEndpointWithUrlEncodedAuth(this._api.dsn); - - if (this.options.sendClientReports && global.document) { - global.document.addEventListener('visibilitychange', () => { - if (global.document.visibilityState === 'hidden') { - this._flushOutcomes(); - } - }); - } - } - - /** - * @inheritDoc - */ - public sendEvent(event: Event): PromiseLike { - return this._sendRequest(eventToSentryRequest(event, this._api), event); - } - - /** - * @inheritDoc - */ - public sendSession(session: Session): PromiseLike { - return this._sendRequest(sessionToSentryRequest(session, this._api), session); - } - - /** - * @inheritDoc - */ - public close(timeout?: number): PromiseLike { - return this._buffer.drain(timeout); - } - - /** - * @inheritDoc - */ - public recordLostEvent(reason: Outcome, category: SentryRequestType): void { - if (!this.options.sendClientReports) { - return; - } - // We want to track each category (event, transaction, session) separately - // but still keep the distinction between different type of outcomes. - // We could use nested maps, but it's much easier to read and type this way. - // A correct type for map-based implementation if we want to go that route - // would be `Partial>>>` - const key = `${requestTypeToCategory(category)}:${reason}`; - IS_DEBUG_BUILD && logger.log(`Adding outcome: ${key}`); - this._outcomes[key] = (this._outcomes[key] ?? 0) + 1; - } - - /** - * Send outcomes as an envelope - */ - protected _flushOutcomes(): void { - if (!this.options.sendClientReports) { - return; - } - - const outcomes = this._outcomes; - this._outcomes = {}; - - // Nothing to send - if (!Object.keys(outcomes).length) { - IS_DEBUG_BUILD && logger.log('No outcomes to flush'); - return; - } - - IS_DEBUG_BUILD && logger.log(`Flushing outcomes:\n${JSON.stringify(outcomes, null, 2)}`); - - const url = getEnvelopeEndpointWithUrlEncodedAuth(this._api.dsn, this._api.tunnel); - - const discardedEvents = Object.keys(outcomes).map(key => { - const [category, reason] = key.split(':'); - return { - reason, - category, - quantity: outcomes[key], - }; - // TODO: Improve types on discarded_events to get rid of cast - }) as ClientReport['discarded_events']; - const envelope = createClientReportEnvelope(discardedEvents, this._api.tunnel && dsnToString(this._api.dsn)); - - try { - sendReport(url, serializeEnvelope(envelope)); - } catch (e) { - IS_DEBUG_BUILD && logger.error(e); - } - } - - /** - * Handle Sentry repsonse for promise-based transports. - */ - protected _handleResponse({ - requestType, - response, - headers, - resolve, - reject, - }: { - requestType: SentryRequestType; - response: Response | XMLHttpRequest; - headers: Record; - resolve: (value?: SentryResponse | PromiseLike | null | undefined) => void; - reject: (reason?: unknown) => void; - }): void { - const status = eventStatusFromHttpCode(response.status); - - this._rateLimits = updateRateLimits(this._rateLimits, headers); - // eslint-disable-next-line deprecation/deprecation - if (this._isRateLimited(requestType)) { - IS_DEBUG_BUILD && - // eslint-disable-next-line deprecation/deprecation - logger.warn(`Too many ${requestType} requests, backing off until: ${this._disabledUntil(requestType)}`); - } - - if (status === 'success') { - resolve({ status }); - return; - } - - reject(response); - } - - /** - * Gets the time that given category is disabled until for rate limiting - * - * @deprecated Please use `disabledUntil` from @sentry/utils - */ - protected _disabledUntil(requestType: SentryRequestType): Date { - const category = requestTypeToCategory(requestType); - return new Date(disabledUntil(this._rateLimits, category)); - } - - /** - * Checks if a category is rate limited - * - * @deprecated Please use `isRateLimited` from @sentry/utils - */ - protected _isRateLimited(requestType: SentryRequestType): boolean { - const category = requestTypeToCategory(requestType); - return isRateLimited(this._rateLimits, category); - } - - protected abstract _sendRequest( - sentryRequest: SentryRequest, - originalPayload: Event | Session, - ): PromiseLike; -} diff --git a/packages/browser/src/transports/fetch.ts b/packages/browser/src/transports/fetch.ts index cddf1b53c1ae..2d7bea31ae6a 100644 --- a/packages/browser/src/transports/fetch.ts +++ b/packages/browser/src/transports/fetch.ts @@ -1,86 +1,33 @@ -import { Event, Response, SentryRequest, Session, TransportOptions } from '@sentry/types'; -import { SentryError, supportsReferrerPolicy, SyncPromise } from '@sentry/utils'; +import { createTransport } from '@sentry/core'; +import { Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; -import { BaseTransport } from './base'; +import { BrowserTransportOptions } from './types'; import { FetchImpl, getNativeFetchImplementation } from './utils'; -/** `fetch` based transport */ -export class FetchTransport extends BaseTransport { - /** - * Fetch API reference which always points to native browser implementation. - */ - private _fetch: typeof fetch; - - public constructor(options: TransportOptions, fetchImpl: FetchImpl = getNativeFetchImplementation()) { - super(options); - this._fetch = fetchImpl; - } - - /** - * @param sentryRequest Prepared SentryRequest to be delivered - * @param originalPayload Original payload used to create SentryRequest - */ - protected _sendRequest(sentryRequest: SentryRequest, originalPayload: Event | Session): PromiseLike { - // eslint-disable-next-line deprecation/deprecation - if (this._isRateLimited(sentryRequest.type)) { - this.recordLostEvent('ratelimit_backoff', sentryRequest.type); - - return Promise.reject({ - event: originalPayload, - type: sentryRequest.type, - // eslint-disable-next-line deprecation/deprecation - reason: `Transport for ${sentryRequest.type} requests locked till ${this._disabledUntil( - sentryRequest.type, - )} due to too many requests.`, - status: 429, - }); - } - - const options: RequestInit = { - body: sentryRequest.body, +/** + * Creates a Transport that uses the Fetch API to send events to Sentry. + */ +export function makeFetchTransport( + options: BrowserTransportOptions, + nativeFetch: FetchImpl = getNativeFetchImplementation(), +): Transport { + function makeRequest(request: TransportRequest): PromiseLike { + const requestOptions: RequestInit = { + body: request.body, method: 'POST', - // Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default' - // (see https://caniuse.com/#feat=referrer-policy), - // it doesn't. And it throws an exception instead of ignoring this parameter... - // REF: https://github.com/getsentry/raven-js/issues/1233 - referrerPolicy: (supportsReferrerPolicy() ? 'origin' : '') as ReferrerPolicy, + referrerPolicy: 'origin', + headers: options.headers, + ...options.fetchOptions, }; - if (this.options.fetchParameters !== undefined) { - Object.assign(options, this.options.fetchParameters); - } - if (this.options.headers !== undefined) { - options.headers = this.options.headers; - } - return this._buffer - .add( - () => - new SyncPromise((resolve, reject) => { - void this._fetch(sentryRequest.url, options) - .then(response => { - const headers = { - 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), - 'retry-after': response.headers.get('Retry-After'), - }; - this._handleResponse({ - requestType: sentryRequest.type, - response, - headers, - resolve, - reject, - }); - }) - .catch(reject); - }), - ) - .then(undefined, reason => { - // It's either buffer rejection or any other xhr/fetch error, which are treated as NetworkError. - if (reason instanceof SentryError) { - this.recordLostEvent('queue_overflow', sentryRequest.type); - } else { - this.recordLostEvent('network_error', sentryRequest.type); - } - throw reason; - }); + return nativeFetch(options.url, requestOptions).then(response => ({ + statusCode: response.status, + headers: { + 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), + 'retry-after': response.headers.get('Retry-After'), + }, + })); } + + return createTransport(options, makeRequest); } diff --git a/packages/browser/src/transports/index.ts b/packages/browser/src/transports/index.ts index 287e14e0ac50..c30287e3e616 100644 --- a/packages/browser/src/transports/index.ts +++ b/packages/browser/src/transports/index.ts @@ -1,6 +1,2 @@ -export { BaseTransport } from './base'; -export { FetchTransport } from './fetch'; -export { XHRTransport } from './xhr'; - -export { makeNewFetchTransport } from './new-fetch'; -export { makeNewXHRTransport } from './new-xhr'; +export { makeFetchTransport } from './fetch'; +export { makeXHRTransport } from './xhr'; diff --git a/packages/browser/src/transports/new-fetch.ts b/packages/browser/src/transports/new-fetch.ts deleted file mode 100644 index 47a85b517e77..000000000000 --- a/packages/browser/src/transports/new-fetch.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - BaseTransportOptions, - createTransport, - NewTransport, - TransportMakeRequestResponse, - TransportRequest, -} from '@sentry/core'; - -import { FetchImpl, getNativeFetchImplementation } from './utils'; - -export interface FetchTransportOptions extends BaseTransportOptions { - requestOptions?: RequestInit; -} - -/** - * Creates a Transport that uses the Fetch API to send events to Sentry. - */ -export function makeNewFetchTransport( - options: FetchTransportOptions, - nativeFetch: FetchImpl = getNativeFetchImplementation(), -): NewTransport { - function makeRequest(request: TransportRequest): PromiseLike { - const requestOptions: RequestInit = { - body: request.body, - method: 'POST', - referrerPolicy: 'origin', - ...options.requestOptions, - }; - - return nativeFetch(options.url, requestOptions).then(response => { - return response.text().then(body => ({ - body, - headers: { - 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), - 'retry-after': response.headers.get('Retry-After'), - }, - reason: response.statusText, - statusCode: response.status, - })); - }); - } - - return createTransport({ bufferSize: options.bufferSize }, makeRequest); -} diff --git a/packages/browser/src/transports/new-xhr.ts b/packages/browser/src/transports/new-xhr.ts deleted file mode 100644 index cd19b1de0cd4..000000000000 --- a/packages/browser/src/transports/new-xhr.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { - BaseTransportOptions, - createTransport, - NewTransport, - TransportMakeRequestResponse, - TransportRequest, -} from '@sentry/core'; -import { SyncPromise } from '@sentry/utils'; - -/** - * The DONE ready state for XmlHttpRequest - * - * Defining it here as a constant b/c XMLHttpRequest.DONE is not always defined - * (e.g. during testing, it is `undefined`) - * - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState} - */ -const XHR_READYSTATE_DONE = 4; - -export interface XHRTransportOptions extends BaseTransportOptions { - headers?: { [key: string]: string }; -} - -/** - * Creates a Transport that uses the XMLHttpRequest API to send events to Sentry. - */ -export function makeNewXHRTransport(options: XHRTransportOptions): NewTransport { - function makeRequest(request: TransportRequest): PromiseLike { - return new SyncPromise((resolve, _reject) => { - const xhr = new XMLHttpRequest(); - - xhr.onreadystatechange = (): void => { - if (xhr.readyState === XHR_READYSTATE_DONE) { - const response = { - body: xhr.response, - headers: { - 'x-sentry-rate-limits': xhr.getResponseHeader('X-Sentry-Rate-Limits'), - 'retry-after': xhr.getResponseHeader('Retry-After'), - }, - reason: xhr.statusText, - statusCode: xhr.status, - }; - resolve(response); - } - }; - - xhr.open('POST', options.url); - - for (const header in options.headers) { - if (Object.prototype.hasOwnProperty.call(options.headers, header)) { - xhr.setRequestHeader(header, options.headers[header]); - } - } - - xhr.send(request.body); - }); - } - - return createTransport({ bufferSize: options.bufferSize }, makeRequest); -} diff --git a/packages/browser/src/transports/types.ts b/packages/browser/src/transports/types.ts new file mode 100644 index 000000000000..4f4b7ef2c98a --- /dev/null +++ b/packages/browser/src/transports/types.ts @@ -0,0 +1,8 @@ +import { BaseTransportOptions } from '@sentry/types'; + +export interface BrowserTransportOptions extends BaseTransportOptions { + /** Fetch API init parameters. Used by the FetchTransport */ + fetchOptions?: RequestInit; + /** Custom headers for the transport. Used by the XHRTransport and FetchTransport */ + headers?: { [key: string]: string }; +} diff --git a/packages/browser/src/transports/utils.ts b/packages/browser/src/transports/utils.ts index 5fc614d6b86f..bd17ec097c28 100644 --- a/packages/browser/src/transports/utils.ts +++ b/packages/browser/src/transports/utils.ts @@ -1,4 +1,4 @@ -import { forget, getGlobalObject, isNativeFetch, logger, supportsFetch } from '@sentry/utils'; +import { getGlobalObject, isNativeFetch, logger, supportsFetch } from '@sentry/utils'; import { IS_DEBUG_BUILD } from '../flags'; @@ -86,25 +86,21 @@ export function getNativeFetchImplementation(): FetchImpl { * @param url report endpoint * @param body report payload */ -export function sendReport(url: string, body: string): void { +export function sendReport(url: string, body: string | Uint8Array): void { const isRealNavigator = Object.prototype.toString.call(global && global.navigator) === '[object Navigator]'; const hasSendBeacon = isRealNavigator && typeof global.navigator.sendBeacon === 'function'; if (hasSendBeacon) { // Prevent illegal invocations - https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch const sendBeacon = global.navigator.sendBeacon.bind(global.navigator); - return sendBeacon(url, body); - } - - if (supportsFetch()) { + sendBeacon(url, body); + } else if (supportsFetch()) { const fetch = getNativeFetchImplementation(); - return forget( - fetch(url, { - body, - method: 'POST', - credentials: 'omit', - keepalive: true, - }), - ); + fetch(url, { + body, + method: 'POST', + credentials: 'omit', + keepalive: true, + }).then(null, error => logger.error(error)); } } diff --git a/packages/browser/src/transports/xhr.ts b/packages/browser/src/transports/xhr.ts index 5da7de258bf6..b10ca4e2b7ca 100644 --- a/packages/browser/src/transports/xhr.ts +++ b/packages/browser/src/transports/xhr.ts @@ -1,63 +1,52 @@ -import { Event, Response, SentryRequest, Session } from '@sentry/types'; -import { SentryError, SyncPromise } from '@sentry/utils'; - -import { BaseTransport } from './base'; - -/** `XHR` based transport */ -export class XHRTransport extends BaseTransport { - /** - * @param sentryRequest Prepared SentryRequest to be delivered - * @param originalPayload Original payload used to create SentryRequest - */ - protected _sendRequest(sentryRequest: SentryRequest, originalPayload: Event | Session): PromiseLike { - // eslint-disable-next-line deprecation/deprecation - if (this._isRateLimited(sentryRequest.type)) { - this.recordLostEvent('ratelimit_backoff', sentryRequest.type); - - return Promise.reject({ - event: originalPayload, - type: sentryRequest.type, - // eslint-disable-next-line deprecation/deprecation - reason: `Transport for ${sentryRequest.type} requests locked till ${this._disabledUntil( - sentryRequest.type, - )} due to too many requests.`, - status: 429, - }); - } - - return this._buffer - .add( - () => - new SyncPromise((resolve, reject) => { - const request = new XMLHttpRequest(); - - request.onreadystatechange = (): void => { - if (request.readyState === 4) { - const headers = { - 'x-sentry-rate-limits': request.getResponseHeader('X-Sentry-Rate-Limits'), - 'retry-after': request.getResponseHeader('Retry-After'), - }; - this._handleResponse({ requestType: sentryRequest.type, response: request, headers, resolve, reject }); - } - }; - - request.open('POST', sentryRequest.url); - for (const header in this.options.headers) { - if (Object.prototype.hasOwnProperty.call(this.options.headers, header)) { - request.setRequestHeader(header, this.options.headers[header]); - } - } - request.send(sentryRequest.body); - }), - ) - .then(undefined, reason => { - // It's either buffer rejection or any other xhr/fetch error, which are treated as NetworkError. - if (reason instanceof SentryError) { - this.recordLostEvent('queue_overflow', sentryRequest.type); - } else { - this.recordLostEvent('network_error', sentryRequest.type); +import { createTransport } from '@sentry/core'; +import { Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; +import { SyncPromise } from '@sentry/utils'; + +import { BrowserTransportOptions } from './types'; + +/** + * The DONE ready state for XmlHttpRequest + * + * Defining it here as a constant b/c XMLHttpRequest.DONE is not always defined + * (e.g. during testing, it is `undefined`) + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState} + */ +const XHR_READYSTATE_DONE = 4; + +/** + * Creates a Transport that uses the XMLHttpRequest API to send events to Sentry. + */ +export function makeXHRTransport(options: BrowserTransportOptions): Transport { + function makeRequest(request: TransportRequest): PromiseLike { + return new SyncPromise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + + xhr.onerror = reject; + + xhr.onreadystatechange = (): void => { + if (xhr.readyState === XHR_READYSTATE_DONE) { + resolve({ + statusCode: xhr.status, + headers: { + 'x-sentry-rate-limits': xhr.getResponseHeader('X-Sentry-Rate-Limits'), + 'retry-after': xhr.getResponseHeader('Retry-After'), + }, + }); } - throw reason; - }); + }; + + xhr.open('POST', options.url); + + for (const header in options.headers) { + if (Object.prototype.hasOwnProperty.call(options.headers, header)) { + xhr.setRequestHeader(header, options.headers[header]); + } + } + + xhr.send(request.body); + }); } + + return createTransport(options, makeRequest); } diff --git a/packages/browser/src/version.ts b/packages/browser/src/version.ts deleted file mode 100644 index 462be137a6ca..000000000000 --- a/packages/browser/src/version.ts +++ /dev/null @@ -1,2 +0,0 @@ -// TODO: Remove in the next major release and rely only on @sentry/core SDK_VERSION and SdkInfo metadata -export const SDK_NAME = 'sentry.javascript.browser'; diff --git a/packages/browser/test/integration/debugging.md b/packages/browser/test/integration/debugging.md new file mode 100644 index 000000000000..f97abe998aed --- /dev/null +++ b/packages/browser/test/integration/debugging.md @@ -0,0 +1,29 @@ +### Debugging Hints + +These tests are hard to debug, because the testing system is somewhat complex, straightforward debugging doesn't work (see below), and the output of most `console.log` calls gets swallowed. Here, future debugger, are some tips to make it easier, to hopefully save you the hour(s) of trial and error it took to figure them out. + +- `suites/shell.js`: + - Remove the loader options from the `variants` array. + - Delete all of the placeholders of the form `{{ suites/something.js }}` except for the one you're interested in. It's not enough to comment them out, because they'll still exist in the file and get replaced by the test runner. Don't forget the one at the bottom of the file. + +- `suites/helpers.js`: + - Add `sandbox.contentWindow.console.log = (...args) => console.log(...args);` just before the return in `createSandbox()`. This will make it so that `console.log` statements come through to the terminal. (Yes, Karma theoretically has settings for that, but they don't seem to work. See https://github.com/karma-runner/karma-mocha/issues/47.) + +- `suites.yourTestFile.js`: + - Use `it.only` to only run the single test you're interested in. + +- Repo-level `rollup/bundleHelpers.js`: + - Comment out all bundle variants except whichever one `run.js` is turning into `artifacts/sdk.js`. + +- Browser-package-level `rollup.bundle.config.js`: + - Build only one of `es5` and `es6`. + +- Run `build:bundle:watch` in a separate terminal tab, so that when you add `console.log`s to the SDK, they get picked up. + +- Don't bother trying to copy one of our standard VSCode debug profiles, because it won't work, except to debug the testing system itself. (It will pause the node process running the tests, not the headless browser in which the tests themselves run.) + +- To make karma do verbose logging, run `export DEBUG=1`. To turn it off, run `unset DEBUG`. + +- To make the testing system do verbose logging, run `yarn test:integration --debug`. + +- To see exactly the files which are being run, comment out `rmdir('artifacts');` near the bottom of `run.js`. diff --git a/packages/browser/test/integration/suites/api.js b/packages/browser/test/integration/suites/api.js index f51928c5bed5..be5de29abd0e 100644 --- a/packages/browser/test/integration/suites/api.js +++ b/packages/browser/test/integration/suites/api.js @@ -65,8 +65,8 @@ describe('API', function () { return runInSandbox(sandbox, function () { throwNonError(); }).then(function (summary) { - assert.isAtLeast(summary.events[0].stacktrace.frames.length, 1); - assert.isAtMost(summary.events[0].stacktrace.frames.length, 3); + assert.isAtLeast(summary.events[0].exception.values[0].stacktrace.frames.length, 1); + assert.isAtMost(summary.events[0].exception.values[0].stacktrace.frames.length, 3); }); }); diff --git a/packages/browser/test/integration/suites/breadcrumbs.js b/packages/browser/test/integration/suites/breadcrumbs.js index 434bcacc8b4a..5e5c2973efc7 100644 --- a/packages/browser/test/integration/suites/breadcrumbs.js +++ b/packages/browser/test/integration/suites/breadcrumbs.js @@ -234,8 +234,6 @@ describe('breadcrumbs', function () { assert.equal(summary.breadcrumbHints.length, 1); assert.equal(summary.breadcrumbHints[0].name, 'click'); assert.equal(summary.breadcrumbHints[0].event.target.tagName, 'INPUT'); - // There should be no expection, if there is one it means we threw it - assert.isUndefined(summary.events[0].exception); } }); }); @@ -265,9 +263,6 @@ describe('breadcrumbs', function () { assert.equal(summary.breadcrumbs[1].category, 'ui.input'); assert.equal(summary.breadcrumbs[1].message, 'body > form#foo-form > input[name="foo"]'); - - // There should be no expection, if there is one it means we threw it - assert.isUndefined(summary.events[0].exception); } }); }); @@ -288,8 +283,6 @@ describe('breadcrumbs', function () { // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs assert.lengthOf(summary.events, 1); } else { - // There should be no expection, if there is one it means we threw it - assert.isUndefined(summary.events[0].exception); assert.equal(summary.breadcrumbs.length, 0); } }); @@ -309,8 +302,6 @@ describe('breadcrumbs', function () { // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs assert.lengthOf(summary.events, 1); } else { - // There should be no expection, if there is one it means we threw it - assert.isUndefined(summary.events[0].exception); assert.equal(summary.breadcrumbs.length, 0); } }); @@ -472,7 +463,6 @@ describe('breadcrumbs', function () { assert.equal(summary.breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); assert.equal(summary.breadcrumbHints[0].global, false); assert.equal(summary.breadcrumbHints[1].global, false); - assert.isUndefined(summary.events[0].exception); } }); }); @@ -507,7 +497,6 @@ describe('breadcrumbs', function () { assert.equal(summary.breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); assert.equal(summary.breadcrumbHints[0].global, false); assert.equal(summary.breadcrumbHints[1].global, false); - assert.isUndefined(summary.events[0].exception); } }); }); @@ -538,7 +527,6 @@ describe('breadcrumbs', function () { assert.equal(summary.breadcrumbs[1].message, 'body > form#foo-form > div.contenteditable'); assert.equal(summary.breadcrumbHints[0].global, false); assert.equal(summary.breadcrumbHints[1].global, false); - assert.isUndefined(summary.events[0].exception); } }); }); @@ -706,7 +694,6 @@ describe('breadcrumbs', function () { assert.equal(summary.breadcrumbs.length, 2); assert.equal(summary.breadcrumbHints[0].global, true); assert.equal(summary.breadcrumbHints[1].global, true); - assert.isUndefined(summary.events[0].exception); } }); }); diff --git a/packages/browser/test/integration/suites/shell.js b/packages/browser/test/integration/suites/shell.js index 0b594f3d2eff..2ba7e2bd8c79 100644 --- a/packages/browser/test/integration/suites/shell.js +++ b/packages/browser/test/integration/suites/shell.js @@ -20,7 +20,7 @@ function runVariant(variant) { }); /** - * This part will be replaced by the test runner + * The test runner will replace each of these placeholders with the contents of the corresponding file. */ {{ suites/config.js }} // prettier-ignore {{ suites/api.js }} // prettier-ignore diff --git a/packages/browser/test/package/test-code.js b/packages/browser/test/package/test-code.js index e05a667db274..3a3811eebb89 100644 --- a/packages/browser/test/package/test-code.js +++ b/packages/browser/test/package/test-code.js @@ -1,6 +1,6 @@ /* eslint-disable no-console */ -const Sentry = require('../../build/npm/dist/index.js'); -const Integrations = require('../../../integrations/build/npm/dist/dedupe.js'); +const Sentry = require('../../build/npm/cjs/index.js'); +const Integrations = require('../../../integrations/build/npm/cjs/dedupe.js'); // Init Sentry.init({ diff --git a/packages/browser/test/unit/backend.test.ts b/packages/browser/test/unit/backend.test.ts deleted file mode 100644 index 71ffec4d75b1..000000000000 --- a/packages/browser/test/unit/backend.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { BrowserBackend } from '../../src/backend'; - -let backend: BrowserBackend; - -describe('BrowserBackend', () => { - describe('sendEvent()', () => { - it('should use NoopTransport', () => { - backend = new BrowserBackend({}); - expect(backend.getTransport().constructor.name).toBe('NoopTransport'); - }); - }); -}); diff --git a/packages/browser/test/unit/helper/browser-client-options.ts b/packages/browser/test/unit/helper/browser-client-options.ts new file mode 100644 index 000000000000..9bdaf2518d40 --- /dev/null +++ b/packages/browser/test/unit/helper/browser-client-options.ts @@ -0,0 +1,13 @@ +import { createTransport } from '@sentry/core'; +import { resolvedSyncPromise } from '@sentry/utils'; + +import { BrowserClientOptions } from '../../../src/client'; + +export function getDefaultBrowserClientOptions(options: Partial = {}): BrowserClientOptions { + return { + integrations: [], + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})), + stackParser: () => [], + ...options, + }; +} diff --git a/packages/browser/test/unit/index.test.ts b/packages/browser/test/unit/index.test.ts index 83c0053d3b66..37fa972512a1 100644 --- a/packages/browser/test/unit/index.test.ts +++ b/packages/browser/test/unit/index.test.ts @@ -1,4 +1,4 @@ -import { SDK_VERSION } from '@sentry/core'; +import { getReportDialogEndpoint, SDK_VERSION } from '@sentry/core'; import { addBreadcrumb, @@ -16,13 +16,22 @@ import { showReportDialog, wrap, } from '../../src'; -import { SimpleTransport } from './mocks/simpletransport'; +import { getDefaultBrowserClientOptions } from './helper/browser-client-options'; +import { makeSimpleTransport } from './mocks/simpletransport'; const dsn = 'https://53039209a22b4ec1bcc296a3c9fdecd6@sentry.io/4291'; // eslint-disable-next-line no-var declare var global: any; +jest.mock('@sentry/core', () => { + const original = jest.requireActual('@sentry/core'); + return { + ...original, + getReportDialogEndpoint: jest.fn(), + }; +}); + describe('SentryBrowser', () => { const beforeSend = jest.fn(); @@ -30,7 +39,7 @@ describe('SentryBrowser', () => { init({ beforeSend, dsn, - transport: SimpleTransport, + transport: makeSimpleTransport, }); }); @@ -73,15 +82,14 @@ describe('SentryBrowser', () => { }); describe('showReportDialog', () => { + beforeEach(() => { + (getReportDialogEndpoint as jest.Mock).mockReset(); + }); + describe('user', () => { const EX_USER = { email: 'test@example.com' }; - const client = new BrowserClient({ dsn }); - const reportDialogSpy = jest.spyOn(client, 'showReportDialog'); - - beforeEach(() => { - reportDialogSpy.mockReset(); - }); - + const options = getDefaultBrowserClientOptions({ dsn }); + const client = new BrowserClient(options); it('uses the user on the scope', () => { configureScope(scope => { scope.setUser(EX_USER); @@ -90,8 +98,11 @@ describe('SentryBrowser', () => { showReportDialog(); - expect(reportDialogSpy).toBeCalled(); - expect(reportDialogSpy.mock.calls[0][0]!.user!.email).toBe(EX_USER.email); + expect(getReportDialogEndpoint).toHaveBeenCalledTimes(1); + expect(getReportDialogEndpoint).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ user: { email: EX_USER.email } }), + ); }); it('prioritizes options user over scope user', () => { @@ -103,8 +114,11 @@ describe('SentryBrowser', () => { const DIALOG_OPTION_USER = { email: 'option@example.com' }; showReportDialog({ user: DIALOG_OPTION_USER }); - expect(reportDialogSpy).toBeCalled(); - expect(reportDialogSpy.mock.calls[0][0]!.user!.email).toBe(DIALOG_OPTION_USER.email); + expect(getReportDialogEndpoint).toHaveBeenCalledTimes(1); + expect(getReportDialogEndpoint).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ user: { email: DIALOG_OPTION_USER.email } }), + ); }); }); }); @@ -139,44 +153,41 @@ describe('SentryBrowser', () => { }); it('should capture a message', done => { - getCurrentHub().bindClient( - new BrowserClient({ - beforeSend: (event: Event): Event | null => { - expect(event.message).toBe('test'); - expect(event.exception).toBeUndefined(); - done(); - return event; - }, - dsn, - }), - ); + const options = getDefaultBrowserClientOptions({ + beforeSend: (event: Event): Event | null => { + expect(event.message).toBe('test'); + expect(event.exception).toBeUndefined(); + done(); + return event; + }, + dsn, + }); + getCurrentHub().bindClient(new BrowserClient(options)); captureMessage('test'); }); it('should capture an event', done => { - getCurrentHub().bindClient( - new BrowserClient({ - beforeSend: (event: Event): Event | null => { - expect(event.message).toBe('event'); - expect(event.exception).toBeUndefined(); - done(); - return event; - }, - dsn, - }), - ); + const options = getDefaultBrowserClientOptions({ + beforeSend: (event: Event): Event | null => { + expect(event.message).toBe('event'); + expect(event.exception).toBeUndefined(); + done(); + return event; + }, + dsn, + }); + getCurrentHub().bindClient(new BrowserClient(options)); captureEvent({ message: 'event' }); }); it('should not dedupe an event on bound client', async () => { const localBeforeSend = jest.fn(); - getCurrentHub().bindClient( - new BrowserClient({ - beforeSend: localBeforeSend, - dsn, - integrations: [], - }), - ); + const options = getDefaultBrowserClientOptions({ + beforeSend: localBeforeSend, + dsn, + integrations: [], + }); + getCurrentHub().bindClient(new BrowserClient(options)); captureMessage('event222'); captureMessage('event222'); @@ -188,13 +199,12 @@ describe('SentryBrowser', () => { it('should use inboundfilter rules of bound client', async () => { const localBeforeSend = jest.fn(); - getCurrentHub().bindClient( - new BrowserClient({ - beforeSend: localBeforeSend, - dsn, - integrations: [new Integrations.InboundFilters({ ignoreErrors: ['capture'] })], - }), - ); + const options = getDefaultBrowserClientOptions({ + beforeSend: localBeforeSend, + dsn, + integrations: [new Integrations.InboundFilters({ ignoreErrors: ['capture'] })], + }); + getCurrentHub().bindClient(new BrowserClient(options)); captureMessage('capture'); @@ -246,18 +256,19 @@ describe('SentryBrowser initialization', () => { it('should set SDK data when Sentry.init() is called', () => { init({ dsn }); - const sdkData = (getCurrentHub().getClient() as any)._backend._transport._api.metadata?.sdk; + const sdkData = (getCurrentHub().getClient() as any).getOptions()._metadata.sdk; - expect(sdkData.name).toBe('sentry.javascript.browser'); - expect(sdkData.packages[0].name).toBe('npm:@sentry/browser'); - expect(sdkData.packages[0].version).toBe(SDK_VERSION); - expect(sdkData.version).toBe(SDK_VERSION); + expect(sdkData?.name).toBe('sentry.javascript.browser'); + expect(sdkData?.packages[0].name).toBe('npm:@sentry/browser'); + expect(sdkData?.packages[0].version).toBe(SDK_VERSION); + expect(sdkData?.version).toBe(SDK_VERSION); }); it('should set SDK data when instantiating a client directly', () => { - const client = new BrowserClient({ dsn }); + const options = getDefaultBrowserClientOptions({ dsn }); + const client = new BrowserClient(options); - const sdkData = (client as any)._backend._transport._api.metadata?.sdk; + const sdkData = client.getOptions()._metadata?.sdk as any; expect(sdkData.name).toBe('sentry.javascript.browser'); expect(sdkData.packages[0].name).toBe('npm:@sentry/browser'); @@ -285,7 +296,7 @@ describe('SentryBrowser initialization', () => { }, }); - const sdkData = (getCurrentHub().getClient() as any)._backend._transport._api.metadata?.sdk; + const sdkData = (getCurrentHub().getClient() as any).getOptions()._metadata?.sdk; expect(sdkData.name).toBe('sentry.javascript.angular'); expect(sdkData.packages[0].name).toBe('npm:@sentry/angular'); @@ -297,17 +308,16 @@ describe('SentryBrowser initialization', () => { describe('wrap()', () => { it('should wrap and call function while capturing error', done => { - getCurrentHub().bindClient( - new BrowserClient({ - beforeSend: (event: Event): Event | null => { - expect(event.exception!.values![0].type).toBe('TypeError'); - expect(event.exception!.values![0].value).toBe('mkey'); - done(); - return null; - }, - dsn, - }), - ); + const options = getDefaultBrowserClientOptions({ + beforeSend: (event: Event): Event | null => { + expect(event.exception!.values![0].type).toBe('TypeError'); + expect(event.exception!.values![0].value).toBe('mkey'); + done(); + return null; + }, + dsn, + }); + getCurrentHub().bindClient(new BrowserClient(options)); try { wrap(() => { diff --git a/packages/browser/test/unit/integrations/linkederrors.test.ts b/packages/browser/test/unit/integrations/linkederrors.test.ts index 1531aa6f77ed..1a80a86093b2 100644 --- a/packages/browser/test/unit/integrations/linkederrors.test.ts +++ b/packages/browser/test/unit/integrations/linkederrors.test.ts @@ -1,7 +1,15 @@ -import { ExtendedError } from '@sentry/types'; +import { Event as SentryEvent, Exception, ExtendedError } from '@sentry/types'; -import { BrowserBackend } from '../../../src/backend'; +import { BrowserClient } from '../../../src/client'; import * as LinkedErrorsModule from '../../../src/integrations/linkederrors'; +import { defaultStackParser as parser } from '../../../src/stack-parsers'; +import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; + +type EventWithException = SentryEvent & { + exception: { + values: Exception[]; + }; +}; describe('LinkedErrors', () => { describe('handler', () => { @@ -9,7 +17,7 @@ describe('LinkedErrors', () => { const event = { message: 'foo', }; - const result = LinkedErrorsModule._handler('cause', 5, event); + const result = LinkedErrorsModule._handler(parser, 'cause', 5, event); expect(result).toEqual(event); }); @@ -20,7 +28,7 @@ describe('LinkedErrors', () => { }, message: 'foo', }; - const result = LinkedErrorsModule._handler('cause', 5, event); + const result = LinkedErrorsModule._handler(parser, 'cause', 5, event); expect(result).toEqual(event); }); @@ -34,11 +42,12 @@ describe('LinkedErrors', () => { one.cause = two; const originalException = one; - const backend = new BrowserBackend({}); - return backend.eventFromException(originalException).then(event => { - const result = LinkedErrorsModule._handler('cause', 5, event, { + const options = getDefaultBrowserClientOptions({ stackParser: parser }); + const client = new BrowserClient(options); + return client.eventFromException(originalException).then(event => { + const result = LinkedErrorsModule._handler(parser, 'cause', 5, event, { originalException, - }); + }) as EventWithException; // It shouldn't include root exception, as it's already processed in the event by the main error handler expect(result.exception.values.length).toBe(3); @@ -64,11 +73,12 @@ describe('LinkedErrors', () => { one.reason = two; const originalException = one; - const backend = new BrowserBackend({}); - return backend.eventFromException(originalException).then(event => { - const result = LinkedErrorsModule._handler('reason', 5, event, { + const options = getDefaultBrowserClientOptions({ stackParser: parser }); + const client = new BrowserClient(options); + return client.eventFromException(originalException).then(event => { + const result = LinkedErrorsModule._handler(parser, 'reason', 5, event, { originalException, - }); + }) as EventWithException; expect(result.exception.values.length).toBe(3); expect(result.exception.values[0].type).toBe('SyntaxError'); @@ -90,12 +100,13 @@ describe('LinkedErrors', () => { one.cause = two; two.cause = three; - const backend = new BrowserBackend({}); + const options = getDefaultBrowserClientOptions({ stackParser: parser }); + const client = new BrowserClient(options); const originalException = one; - return backend.eventFromException(originalException).then(event => { - const result = LinkedErrorsModule._handler('cause', 2, event, { + return client.eventFromException(originalException).then(event => { + const result = LinkedErrorsModule._handler(parser, 'cause', 2, event, { originalException, - }); + }) as EventWithException; expect(result.exception.values.length).toBe(2); expect(result.exception.values[0].type).toBe('TypeError'); diff --git a/packages/browser/test/unit/jest.config.js b/packages/browser/test/unit/jest.config.js deleted file mode 100644 index 5495646bc33f..000000000000 --- a/packages/browser/test/unit/jest.config.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - transform: { - '^.+\\.ts$': 'ts-jest', - }, - collectCoverage: true, - coverageDirectory: '../../coverage', - moduleFileExtensions: ['js', 'ts'], - testEnvironment: 'jsdom', - testMatch: ['**/*.test.ts'], - globals: { - 'ts-jest': { - tsConfig: '../../tsconfig.json', - diagnostics: false, - }, - }, -}; diff --git a/packages/browser/test/unit/mocks/simpletransport.ts b/packages/browser/test/unit/mocks/simpletransport.ts index 7d0b0e9498df..cbad94fb310d 100644 --- a/packages/browser/test/unit/mocks/simpletransport.ts +++ b/packages/browser/test/unit/mocks/simpletransport.ts @@ -1,14 +1,6 @@ -import { eventStatusFromHttpCode, resolvedSyncPromise } from '@sentry/utils'; +import { createTransport } from '@sentry/core'; +import { resolvedSyncPromise } from '@sentry/utils'; -import { Event, Response } from '../../../src'; -import { BaseTransport } from '../../../src/transports'; - -export class SimpleTransport extends BaseTransport { - public sendEvent(_: Event): PromiseLike { - return this._buffer.add(() => - resolvedSyncPromise({ - status: eventStatusFromHttpCode(200), - }), - ); - } +export function makeSimpleTransport() { + return createTransport({ recordDroppedEvent: () => undefined }, () => resolvedSyncPromise({})); } diff --git a/packages/browser/test/unit/sdk.test.ts b/packages/browser/test/unit/sdk.test.ts new file mode 100644 index 000000000000..a2df869fe90d --- /dev/null +++ b/packages/browser/test/unit/sdk.test.ts @@ -0,0 +1,126 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import { Scope } from '@sentry/core'; +import { createTransport } from '@sentry/core'; +import { MockIntegration } from '@sentry/core/test/lib/sdk.test'; +import { Client, Integration } from '@sentry/types'; +import { resolvedSyncPromise } from '@sentry/utils'; + +import { BrowserOptions } from '../../src'; +import { init } from '../../src/sdk'; +// eslint-disable-next-line no-var +declare var global: any; + +const PUBLIC_DSN = 'https://username@domain/123'; + +function getDefaultBrowserOptions(options: Partial = {}): BrowserOptions { + return { + integrations: [], + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})), + stackParser: () => [], + ...options, + }; +} + +jest.mock('@sentry/hub', () => { + const original = jest.requireActual('@sentry/hub'); + return { + ...original, + getCurrentHub(): { + bindClient(client: Client): boolean; + getClient(): boolean; + getScope(): Scope; + } { + return { + getClient(): boolean { + return false; + }, + getScope(): Scope { + return new Scope(); + }, + bindClient(client: Client): boolean { + client.setupIntegrations(); + return true; + }, + }; + }, + }; +}); + +describe('init', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + test('installs default integrations', () => { + const DEFAULT_INTEGRATIONS: Integration[] = [ + new MockIntegration('MockIntegration 0.1'), + new MockIntegration('MockIntegration 0.2'), + ]; + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, defaultIntegrations: DEFAULT_INTEGRATIONS }); + + init(options); + + expect(DEFAULT_INTEGRATIONS[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + }); + + test("doesn't install default integrations if told not to", () => { + const DEFAULT_INTEGRATIONS: Integration[] = [ + new MockIntegration('MockIntegration 0.3'), + new MockIntegration('MockIntegration 0.4'), + ]; + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, defaultIntegrations: false }); + init(options); + + expect(DEFAULT_INTEGRATIONS[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + expect(DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + }); + + it('installs merged default integrations, with overrides provided through options', () => { + const DEFAULT_INTEGRATIONS = [ + new MockIntegration('MockIntegration 1.1'), + new MockIntegration('MockIntegration 1.2'), + ]; + + const integrations = [new MockIntegration('MockIntegration 1.1'), new MockIntegration('MockIntegration 1.3')]; + const options = getDefaultBrowserOptions({ + dsn: PUBLIC_DSN, + defaultIntegrations: DEFAULT_INTEGRATIONS, + integrations, + }); + + init(options); + // 'MockIntegration 1' should be overridden by the one with the same name provided through options + expect(DEFAULT_INTEGRATIONS[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + expect(DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(integrations[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(integrations[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + }); + + it('installs integrations returned from a callback function', () => { + const DEFAULT_INTEGRATIONS = [ + new MockIntegration('MockIntegration 2.1'), + new MockIntegration('MockIntegration 2.2'), + ]; + + const newIntegration = new MockIntegration('MockIntegration 2.3'); + const options = getDefaultBrowserOptions({ + defaultIntegrations: DEFAULT_INTEGRATIONS, + dsn: PUBLIC_DSN, + integrations: (integrations: Integration[]) => { + const t = integrations.slice(0, 1).concat(newIntegration); + return t; + }, + }); + + init(options); + + expect(DEFAULT_INTEGRATIONS[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(newIntegration.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + }); +}); diff --git a/packages/browser/test/unit/tracekit/chromium.test.ts b/packages/browser/test/unit/tracekit/chromium.test.ts index e40abffd8d1f..183ffaa7eb97 100644 --- a/packages/browser/test/unit/tracekit/chromium.test.ts +++ b/packages/browser/test/unit/tracekit/chromium.test.ts @@ -1,9 +1,10 @@ import { exceptionFromError } from '../../../src/eventbuilder'; +import { defaultStackParser as parser } from '../../../src/stack-parsers'; describe('Tracekit - Chrome Tests', () => { it('should parse Chrome error with no location', () => { const NO_LOCATION = { message: 'foo', name: 'bar', stack: 'error\n at Array.forEach (native)' }; - const ex = exceptionFromError(NO_LOCATION); + const ex = exceptionFromError(parser, NO_LOCATION); expect(ex).toEqual({ value: 'foo', @@ -25,7 +26,7 @@ describe('Tracekit - Chrome Tests', () => { ' at http://path/to/file.js:24:4', }; - const ex = exceptionFromError(CHROME_15); + const ex = exceptionFromError(parser, CHROME_15); expect(ex).toEqual({ value: "Object # has no method 'undef'", @@ -52,7 +53,7 @@ describe('Tracekit - Chrome Tests', () => { ' at I.e.fn.(anonymous function) [as index] (http://localhost:8080/file.js:10:3651)', }; - const ex = exceptionFromError(CHROME_36); + const ex = exceptionFromError(parser, CHROME_36); expect(ex).toEqual({ value: 'Default error', @@ -98,7 +99,7 @@ describe('Tracekit - Chrome Tests', () => { ' at TESTTESTTEST.proxiedMethod(webpack:///./~/react-proxy/modules/createPrototypeProxy.js?:44:30)', }; - const ex = exceptionFromError(CHROME_XX_WEBPACK); + const ex = exceptionFromError(parser, CHROME_XX_WEBPACK); expect(ex).toEqual({ value: "Cannot read property 'error' of undefined", @@ -151,7 +152,7 @@ describe('Tracekit - Chrome Tests', () => { 'at http://localhost:8080/file.js:31:13\n', }; - const ex = exceptionFromError(CHROME_48_EVAL); + const ex = exceptionFromError(parser, CHROME_48_EVAL); expect(ex).toEqual({ value: 'message string', @@ -183,7 +184,7 @@ describe('Tracekit - Chrome Tests', () => { ' at n.handle (blob:http%3A//localhost%3A8080/abfc40e9-4742-44ed-9dcd-af8f99a29379:7:2863)', }; - const ex = exceptionFromError(CHROME_48_BLOB); + const ex = exceptionFromError(parser, CHROME_48_BLOB); expect(ex).toEqual({ value: 'Error: test', @@ -246,7 +247,7 @@ describe('Tracekit - Chrome Tests', () => { at examplescheme://examplehost/cd351f7250857e22ceaa.worker.js:70179:15`, }; - const ex = exceptionFromError(CHROMIUM_EMBEDDED_FRAMEWORK_CUSTOM_SCHEME); + const ex = exceptionFromError(parser, CHROMIUM_EMBEDDED_FRAMEWORK_CUSTOM_SCHEME); expect(ex).toEqual({ value: 'message string', @@ -276,7 +277,7 @@ describe('Tracekit - Chrome Tests', () => { at http://localhost:5000/test:24:7`, }; - const ex = exceptionFromError(CHROME73_NATIVE_CODE_EXCEPTION); + const ex = exceptionFromError(parser, CHROME73_NATIVE_CODE_EXCEPTION); expect(ex).toEqual({ value: 'test', @@ -309,7 +310,7 @@ describe('Tracekit - Chrome Tests', () => { at http://localhost:5000/:50:19`, }; - const ex = exceptionFromError(CHROME73_EVAL_EXCEPTION); + const ex = exceptionFromError(parser, CHROME73_EVAL_EXCEPTION); expect(ex).toEqual({ value: 'bad', @@ -342,7 +343,7 @@ describe('Tracekit - Chrome Tests', () => { at Global code (http://localhost:5000/test:24:7)`, }; - const ex = exceptionFromError(EDGE44_NATIVE_CODE_EXCEPTION); + const ex = exceptionFromError(parser, EDGE44_NATIVE_CODE_EXCEPTION); expect(ex).toEqual({ value: 'test', @@ -375,7 +376,7 @@ describe('Tracekit - Chrome Tests', () => { at Anonymous function (http://localhost:5000/:50:8)`, }; - const ex = exceptionFromError(EDGE44_EVAL_EXCEPTION); + const ex = exceptionFromError(parser, EDGE44_EVAL_EXCEPTION); expect(ex).toEqual({ value: 'aha', @@ -411,7 +412,7 @@ describe('Tracekit - Chrome Tests', () => { at TESTTESTTEST.someMethod (C:\\Users\\user\\path\\to\\file.js:295:108)`, }; - const ex = exceptionFromError(CHROME_ELECTRON_RENDERER); + const ex = exceptionFromError(parser, CHROME_ELECTRON_RENDERER); expect(ex).toEqual({ value: "Cannot read property 'error' of undefined", diff --git a/packages/browser/test/unit/tracekit/firefox.test.ts b/packages/browser/test/unit/tracekit/firefox.test.ts index a14fae1e38cc..a9f8ee2d50cb 100644 --- a/packages/browser/test/unit/tracekit/firefox.test.ts +++ b/packages/browser/test/unit/tracekit/firefox.test.ts @@ -1,4 +1,5 @@ import { exceptionFromError } from '../../../src/eventbuilder'; +import { defaultStackParser as parser } from '../../../src/stack-parsers'; describe('Tracekit - Firefox Tests', () => { it('should parse Firefox 3 error', () => { @@ -18,7 +19,7 @@ describe('Tracekit - Firefox Tests', () => { '', }; - const ex = exceptionFromError(FIREFOX_3); + const ex = exceptionFromError(parser, FIREFOX_3); expect(ex).toEqual({ value: 'this.undef is not a function', @@ -54,7 +55,7 @@ describe('Tracekit - Firefox Tests', () => { '', }; - const ex = exceptionFromError(FIREFOX_7); + const ex = exceptionFromError(parser, FIREFOX_7); expect(ex).toEqual({ value: 'bar', @@ -86,7 +87,7 @@ describe('Tracekit - Firefox Tests', () => { lineNumber: 48, }; - const ex = exceptionFromError(FIREFOX_14); + const ex = exceptionFromError(parser, FIREFOX_14); expect(ex).toEqual({ value: 'x is null', @@ -115,7 +116,7 @@ describe('Tracekit - Firefox Tests', () => { columnNumber: 12, }; - const ex = exceptionFromError(FIREFOX_31); + const ex = exceptionFromError(parser, FIREFOX_31); expect(ex).toEqual({ value: 'Default error', @@ -150,7 +151,7 @@ describe('Tracekit - Firefox Tests', () => { result: 2147500037, }; - const ex = exceptionFromError(FIREFOX_44_NS_EXCEPTION); + const ex = exceptionFromError(parser, FIREFOX_44_NS_EXCEPTION); expect(ex).toEqual({ value: 'No error message', @@ -185,7 +186,7 @@ describe('Tracekit - Firefox Tests', () => { name: 'TypeError', }; - const ex = exceptionFromError(FIREFOX_50_RESOURCE_URL); + const ex = exceptionFromError(parser, FIREFOX_50_RESOURCE_URL); expect(ex).toEqual({ value: 'this.props.raw[this.state.dataSource].rows is undefined', @@ -233,7 +234,7 @@ describe('Tracekit - Firefox Tests', () => { '@http://localhost:8080/file.js:33:9', }; - const ex = exceptionFromError(FIREFOX_43_EVAL); + const ex = exceptionFromError(parser, FIREFOX_43_EVAL); expect(ex).toEqual({ value: 'message string', @@ -259,7 +260,7 @@ describe('Tracekit - Firefox Tests', () => { @http://localhost:5000/test:24:7`, }; - const stacktrace = exceptionFromError(FIREFOX66_NATIVE_CODE_EXCEPTION); + const stacktrace = exceptionFromError(parser, FIREFOX66_NATIVE_CODE_EXCEPTION); expect(stacktrace).toEqual({ value: 'test', @@ -289,7 +290,7 @@ describe('Tracekit - Firefox Tests', () => { @http://localhost:5000/:50:19`, }; - const stacktrace = exceptionFromError(FIREFOX66_EVAL_EXCEPTION); + const stacktrace = exceptionFromError(parser, FIREFOX66_EVAL_EXCEPTION); expect(stacktrace).toEqual({ value: 'aha', diff --git a/packages/browser/test/unit/tracekit/ie.test.ts b/packages/browser/test/unit/tracekit/ie.test.ts index cfd60ab2e6c4..544542b0dcaf 100644 --- a/packages/browser/test/unit/tracekit/ie.test.ts +++ b/packages/browser/test/unit/tracekit/ie.test.ts @@ -1,4 +1,5 @@ import { exceptionFromError } from '../../../src/eventbuilder'; +import { defaultStackParser as parser } from '../../../src/stack-parsers'; describe('Tracekit - IE Tests', () => { it('should parse IE 10 error', () => { @@ -14,7 +15,7 @@ describe('Tracekit - IE Tests', () => { number: -2146823281, }; - const ex = exceptionFromError(IE_10); + const ex = exceptionFromError(parser, IE_10); // TODO: func should be normalized expect(ex).toEqual({ @@ -43,7 +44,7 @@ describe('Tracekit - IE Tests', () => { number: -2146823281, }; - const ex = exceptionFromError(IE_11); + const ex = exceptionFromError(parser, IE_11); // TODO: func should be normalized expect(ex).toEqual({ @@ -72,7 +73,7 @@ describe('Tracekit - IE Tests', () => { number: -2146823279, }; - const ex = exceptionFromError(IE_11_EVAL); + const ex = exceptionFromError(parser, IE_11_EVAL); expect(ex).toEqual({ value: "'getExceptionProps' is undefined", diff --git a/packages/browser/test/unit/tracekit/misc.test.ts b/packages/browser/test/unit/tracekit/misc.test.ts index 3aa59754cc9a..e9db457ea196 100644 --- a/packages/browser/test/unit/tracekit/misc.test.ts +++ b/packages/browser/test/unit/tracekit/misc.test.ts @@ -1,4 +1,5 @@ import { exceptionFromError } from '../../../src/eventbuilder'; +import { defaultStackParser as parser } from '../../../src/stack-parsers'; describe('Tracekit - Misc Tests', () => { it('should parse PhantomJS 1.19 error', () => { @@ -11,7 +12,7 @@ describe('Tracekit - Misc Tests', () => { ' at foo (http://path/to/file.js:4283)\n' + ' at http://path/to/file.js:4287', }; - const ex = exceptionFromError(PHANTOMJS_1_19); + const ex = exceptionFromError(parser, PHANTOMJS_1_19); expect(ex).toEqual({ value: 'bar', diff --git a/packages/browser/test/unit/tracekit/opera.test.ts b/packages/browser/test/unit/tracekit/opera.test.ts index 472c4a55e2ca..e86855dc172a 100644 --- a/packages/browser/test/unit/tracekit/opera.test.ts +++ b/packages/browser/test/unit/tracekit/opera.test.ts @@ -1,4 +1,10 @@ +import { createStackParser } from '@sentry/utils'; + import { exceptionFromError } from '../../../src/eventbuilder'; +import { defaultStackParser, opera10StackLineParser, opera11StackLineParser } from '../../../src/stack-parsers'; + +const operaParser = createStackParser(opera10StackLineParser, opera11StackLineParser); +const chromiumParser = defaultStackParser; describe('Tracekit - Opera Tests', () => { it('should parse Opera 10 error', () => { @@ -24,7 +30,7 @@ describe('Tracekit - Opera Tests', () => { '', }; - const ex = exceptionFromError(OPERA_10); + const ex = exceptionFromError(operaParser, OPERA_10); expect(ex).toEqual({ value: 'Statement on line 42: Type mismatch (usually non-object value supplied where object required)', @@ -70,7 +76,7 @@ describe('Tracekit - Opera Tests', () => { ' foo();', }; - const ex = exceptionFromError(OPERA_11); + const ex = exceptionFromError(operaParser, OPERA_11); expect(ex).toEqual({ value: "'this.undef' is not a function", @@ -107,7 +113,7 @@ describe('Tracekit - Opera Tests', () => { ' dumpException3();', }; - const ex = exceptionFromError(OPERA_12); + const ex = exceptionFromError(operaParser, OPERA_12); expect(ex).toEqual({ value: "Cannot convert 'x' to object", @@ -151,7 +157,7 @@ describe('Tracekit - Opera Tests', () => { ' at bar (http://path/to/file.js:108:168)', }; - const ex = exceptionFromError(OPERA_25); + const ex = exceptionFromError(chromiumParser, OPERA_25); expect(ex).toEqual({ value: "Cannot read property 'undef' of null", diff --git a/packages/browser/test/unit/tracekit/react-native.test.ts b/packages/browser/test/unit/tracekit/react-native.test.ts index 6935acd615fd..9a74e46007b1 100644 --- a/packages/browser/test/unit/tracekit/react-native.test.ts +++ b/packages/browser/test/unit/tracekit/react-native.test.ts @@ -1,4 +1,5 @@ import { exceptionFromError } from '../../../src/eventbuilder'; +import { defaultStackParser as parser } from '../../../src/stack-parsers'; describe('Tracekit - React Native Tests', () => { it('should parse exceptions for react-native-v8', () => { @@ -14,7 +15,7 @@ describe('Tracekit - React Native Tests', () => { at Object.y(index.android.bundle:93:571) at P(index.android.bundle:93:714)`, }; - const stacktrace = exceptionFromError(REACT_NATIVE_V8_EXCEPTION); + const stacktrace = exceptionFromError(parser, REACT_NATIVE_V8_EXCEPTION); expect(stacktrace).toEqual({ value: 'Manually triggered crash to test Sentry reporting', @@ -61,7 +62,7 @@ describe('Tracekit - React Native Tests', () => { p@/data/user/0/com.sentrytest/files/.expo-internal/bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3:96:385 forEach@[native code]`, }; - const stacktrace = exceptionFromError(REACT_NATIVE_EXPO_EXCEPTION); + const stacktrace = exceptionFromError(parser, REACT_NATIVE_EXPO_EXCEPTION); expect(stacktrace).toEqual({ value: 'Test Error Expo', @@ -122,7 +123,7 @@ describe('Tracekit - React Native Tests', () => { 'at this(/home/username/sample-workspace/sampleapp.collect.react/node_modules/react-native/Libraries/Renderer/src/renderers/native/ReactNativeBaseComponent.js:74:41)\n', }; - const ex = exceptionFromError(ANDROID_REACT_NATIVE); + const ex = exceptionFromError(parser, ANDROID_REACT_NATIVE); expect(ex).toEqual({ value: 'Error: test', @@ -241,7 +242,7 @@ describe('Tracekit - React Native Tests', () => { '[native code]', }; - const ex = exceptionFromError(ANDROID_REACT_NATIVE_PROD); + const ex = exceptionFromError(parser, ANDROID_REACT_NATIVE_PROD); expect(ex).toEqual({ value: 'Error: test', @@ -352,7 +353,7 @@ describe('Tracekit - React Native Tests', () => { 'at value (address at index.android.bundle:1:32776)\n' + 'at value (address at index.android.bundle:1:31561)', }; - const ex = exceptionFromError(ANDROID_REACT_NATIVE_HERMES); + const ex = exceptionFromError(parser, ANDROID_REACT_NATIVE_HERMES); expect(ex).toEqual({ value: 'Error: lets throw!', diff --git a/packages/browser/test/unit/tracekit/react.test.ts b/packages/browser/test/unit/tracekit/react.test.ts index dba60cceab4f..d949a4dee0eb 100644 --- a/packages/browser/test/unit/tracekit/react.test.ts +++ b/packages/browser/test/unit/tracekit/react.test.ts @@ -1,4 +1,5 @@ import { exceptionFromError } from '../../../src/eventbuilder'; +import { defaultStackParser as parser } from '../../../src/stack-parsers'; describe('Tracekit - React Tests', () => { it('should correctly parse Invariant Violation errors and use framesToPop to drop info message', () => { @@ -14,7 +15,7 @@ describe('Tracekit - React Tests', () => { at f (http://localhost:5000/:1:980)`, }; - const ex = exceptionFromError(REACT_INVARIANT_VIOLATION_EXCEPTION); + const ex = exceptionFromError(parser, REACT_INVARIANT_VIOLATION_EXCEPTION); expect(ex).toEqual({ value: @@ -61,7 +62,7 @@ describe('Tracekit - React Tests', () => { at f (http://localhost:5000/:1:980)`, }; - const ex = exceptionFromError(REACT_PRODUCTION_ERROR); + const ex = exceptionFromError(parser, REACT_PRODUCTION_ERROR); expect(ex).toEqual({ value: @@ -109,7 +110,7 @@ describe('Tracekit - React Tests', () => { at f (http://localhost:5000/:1:980)`, }; - const ex = exceptionFromError(REACT_PRODUCTION_ERROR); + const ex = exceptionFromError(parser, REACT_PRODUCTION_ERROR); expect(ex).toEqual({ value: diff --git a/packages/browser/test/unit/tracekit/safari.test.ts b/packages/browser/test/unit/tracekit/safari.test.ts index 6342899e1ca4..657ffc7daecc 100644 --- a/packages/browser/test/unit/tracekit/safari.test.ts +++ b/packages/browser/test/unit/tracekit/safari.test.ts @@ -1,4 +1,5 @@ import { exceptionFromError } from '../../../src/eventbuilder'; +import { defaultStackParser as parser } from '../../../src/stack-parsers'; describe('Tracekit - Safari Tests', () => { it('should parse Safari 6 error', () => { @@ -14,7 +15,7 @@ describe('Tracekit - Safari Tests', () => { sourceURL: 'http://path/to/file.js', }; - const stackFrames = exceptionFromError(SAFARI_6); + const stackFrames = exceptionFromError(parser, SAFARI_6); expect(stackFrames).toEqual({ value: "'null' is not an object (evaluating 'x.undef')", @@ -40,7 +41,7 @@ describe('Tracekit - Safari Tests', () => { sourceURL: 'http://path/to/file.js', }; - const stackFrames = exceptionFromError(SAFARI_7); + const stackFrames = exceptionFromError(parser, SAFARI_7); expect(stackFrames).toEqual({ value: "'null' is not an object (evaluating 'x.undef')", @@ -66,7 +67,7 @@ describe('Tracekit - Safari Tests', () => { sourceURL: 'http://path/to/file.js', }; - const stackFrames = exceptionFromError(SAFARI_8); + const stackFrames = exceptionFromError(parser, SAFARI_8); expect(stackFrames).toEqual({ value: "null is not an object (evaluating 'x.undef')", @@ -96,7 +97,7 @@ describe('Tracekit - Safari Tests', () => { column: 18, }; - const stackFrames = exceptionFromError(SAFARI_8_EVAL); + const stackFrames = exceptionFromError(parser, SAFARI_8_EVAL); expect(stackFrames).toEqual({ value: "Can't find variable: getExceptionProps", @@ -121,7 +122,7 @@ describe('Tracekit - Safari Tests', () => { at safari-extension:(//3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/topee-content.js:3313:26)`, }; - const ex = exceptionFromError(SAFARI_EXTENSION_EXCEPTION); + const ex = exceptionFromError(parser, SAFARI_EXTENSION_EXCEPTION); expect(ex).toEqual({ value: 'wat', @@ -155,7 +156,7 @@ describe('Tracekit - Safari Tests', () => { safari-extension://com.grammarly.safari.extension.ext2-W8F64X92K3/ee7759dd/Grammarly.js:2:1588410 promiseReactionJob@[native code]`, }; - const ex = exceptionFromError(SAFARI_EXTENSION_EXCEPTION); + const ex = exceptionFromError(parser, SAFARI_EXTENSION_EXCEPTION); expect(ex).toEqual({ value: "undefined is not an object (evaluating 'e.groups.includes')", @@ -191,7 +192,7 @@ describe('Tracekit - Safari Tests', () => { at safari-web-extension:(//3284871F-A480-4FFC-8BC4-3F362C752446/2665fee0/topee-content.js:3313:26)`, }; - const ex = exceptionFromError(SAFARI_WEB_EXTENSION_EXCEPTION); + const ex = exceptionFromError(parser, SAFARI_WEB_EXTENSION_EXCEPTION); expect(ex).toEqual({ value: 'wat', @@ -225,7 +226,7 @@ describe('Tracekit - Safari Tests', () => { safari-web-extension://46434E60-F5BD-48A4-80C8-A422C5D16897/scripts/content-script.js:29:56027 promiseReactionJob@[native code]`, }; - const ex = exceptionFromError(SAFARI_EXTENSION_EXCEPTION); + const ex = exceptionFromError(parser, SAFARI_EXTENSION_EXCEPTION); expect(ex).toEqual({ value: "undefined is not an object (evaluating 'e.groups.includes')", @@ -263,7 +264,7 @@ describe('Tracekit - Safari Tests', () => { global code@http://localhost:5000/test:24:10`, }; - const ex = exceptionFromError(SAFARI12_NATIVE_CODE_EXCEPTION); + const ex = exceptionFromError(parser, SAFARI12_NATIVE_CODE_EXCEPTION); expect(ex).toEqual({ value: 'test', @@ -297,7 +298,7 @@ describe('Tracekit - Safari Tests', () => { http://localhost:5000/:50:29`, }; - const ex = exceptionFromError(SAFARI12_EVAL_EXCEPTION); + const ex = exceptionFromError(parser, SAFARI12_EVAL_EXCEPTION); expect(ex).toEqual({ value: 'aha', diff --git a/packages/browser/test/unit/transports/base.test.ts b/packages/browser/test/unit/transports/base.test.ts deleted file mode 100644 index 2f9fc26d07be..000000000000 --- a/packages/browser/test/unit/transports/base.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { BaseTransport } from '../../../src/transports/base'; - -const testDsn = 'https://123@sentry.io/42'; -const envelopeEndpoint = 'https://sentry.io/api/42/envelope/?sentry_key=123&sentry_version=7'; - -class SimpleTransport extends BaseTransport {} - -describe('BaseTransport', () => { - describe('Client Reports', () => { - const sendBeaconSpy = jest.fn(); - let visibilityState: string; - - beforeAll(() => { - navigator.sendBeacon = sendBeaconSpy; - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return visibilityState; - }, - }); - jest.spyOn(Date, 'now').mockImplementation(() => 12345); - }); - - beforeEach(() => { - sendBeaconSpy.mockClear(); - }); - - it('attaches visibilitychange handler if sendClientReport is set to true', () => { - const eventListenerSpy = jest.spyOn(document, 'addEventListener'); - new SimpleTransport({ dsn: testDsn, sendClientReports: true }); - expect(eventListenerSpy.mock.calls[0][0]).toBe('visibilitychange'); - eventListenerSpy.mockRestore(); - }); - - it('doesnt attach visibilitychange handler if sendClientReport is set to false', () => { - const eventListenerSpy = jest.spyOn(document, 'addEventListener'); - new SimpleTransport({ dsn: testDsn, sendClientReports: false }); - expect(eventListenerSpy).not.toHaveBeenCalled(); - eventListenerSpy.mockRestore(); - }); - - it('sends beacon request when there are outcomes captured and visibility changed to `hidden`', () => { - const transport = new SimpleTransport({ dsn: testDsn, sendClientReports: true }); - - transport.recordLostEvent('before_send', 'event'); - - visibilityState = 'hidden'; - document.dispatchEvent(new Event('visibilitychange')); - - const outcomes = [{ reason: 'before_send', category: 'error', quantity: 1 }]; - - expect(sendBeaconSpy).toHaveBeenCalledWith( - envelopeEndpoint, - `{}\n{"type":"client_report"}\n{"timestamp":12.345,"discarded_events":${JSON.stringify(outcomes)}}`, - ); - }); - - it('doesnt send beacon request when there are outcomes captured, but visibility state did not change to `hidden`', () => { - const transport = new SimpleTransport({ dsn: testDsn, sendClientReports: true }); - transport.recordLostEvent('before_send', 'event'); - - visibilityState = 'visible'; - document.dispatchEvent(new Event('visibilitychange')); - - expect(sendBeaconSpy).not.toHaveBeenCalled(); - }); - - it('correctly serializes request with different categories/reasons pairs', () => { - const transport = new SimpleTransport({ dsn: testDsn, sendClientReports: true }); - - transport.recordLostEvent('before_send', 'event'); - transport.recordLostEvent('before_send', 'event'); - transport.recordLostEvent('sample_rate', 'transaction'); - transport.recordLostEvent('network_error', 'session'); - transport.recordLostEvent('network_error', 'session'); - transport.recordLostEvent('ratelimit_backoff', 'event'); - - visibilityState = 'hidden'; - document.dispatchEvent(new Event('visibilitychange')); - - const outcomes = [ - { reason: 'before_send', category: 'error', quantity: 2 }, - { reason: 'sample_rate', category: 'transaction', quantity: 1 }, - { reason: 'network_error', category: 'session', quantity: 2 }, - { reason: 'ratelimit_backoff', category: 'error', quantity: 1 }, - ]; - - expect(sendBeaconSpy).toHaveBeenCalledWith( - envelopeEndpoint, - `{}\n{"type":"client_report"}\n{"timestamp":12.345,"discarded_events":${JSON.stringify(outcomes)}}`, - ); - }); - - it('attaches DSN to envelope header if tunnel is configured', () => { - const tunnel = 'https://hello.com/world'; - const transport = new SimpleTransport({ dsn: testDsn, sendClientReports: true, tunnel }); - - transport.recordLostEvent('before_send', 'event'); - - visibilityState = 'hidden'; - document.dispatchEvent(new Event('visibilitychange')); - - const outcomes = [{ reason: 'before_send', category: 'error', quantity: 1 }]; - - expect(sendBeaconSpy).toHaveBeenCalledWith( - tunnel, - `{"dsn":"${testDsn}"}\n{"type":"client_report"}\n{"timestamp":12.345,"discarded_events":${JSON.stringify( - outcomes, - )}}`, - ); - }); - }); - - it('doesnt provide sendEvent() implementation', () => { - expect.assertions(1); - const transport = new SimpleTransport({ dsn: testDsn }); - - try { - void transport.sendEvent({}); - } catch (e) { - expect(e).toBeDefined(); - } - }); - - it('has correct endpoint url', () => { - const transport = new SimpleTransport({ dsn: testDsn }); - // eslint-disable-next-line deprecation/deprecation - expect(transport.url).toBe('https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7'); - }); -}); diff --git a/packages/browser/test/unit/transports/fetch.test.ts b/packages/browser/test/unit/transports/fetch.test.ts index f7af4e38349c..87ad77266106 100644 --- a/packages/browser/test/unit/transports/fetch.test.ts +++ b/packages/browser/test/unit/transports/fetch.test.ts @@ -1,515 +1,100 @@ -import { SentryError } from '@sentry/utils'; - -import { Event, Response, Transports } from '../../../src'; - -const testDsn = 'https://123@sentry.io/42'; -const storeUrl = 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7'; -const tunnel = 'https://hello.com/world'; -const eventPayload: Event = { - event_id: '1337', -}; -const transactionPayload: Event = { - event_id: '42', - type: 'transaction', +import { EventEnvelope, EventItem } from '@sentry/types'; +import { createEnvelope, serializeEnvelope } from '@sentry/utils'; +import { TextEncoder } from 'util'; + +import { makeFetchTransport } from '../../../src/transports/fetch'; +import { BrowserTransportOptions } from '../../../src/transports/types'; +import { FetchImpl } from '../../../src/transports/utils'; + +const DEFAULT_FETCH_TRANSPORT_OPTIONS: BrowserTransportOptions = { + url: 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7', + recordDroppedEvent: () => undefined, + textEncoder: new TextEncoder(), }; -const fetch = jest.fn(); -let transport: Transports.BaseTransport; - -// eslint-disable-next-line no-var -declare var window: any; - -jest.mock('@sentry/utils', () => { - return { - ...jest.requireActual('@sentry/utils'), - supportsReferrerPolicy(): boolean { - return true; - }, - }; -}); - -describe('FetchTransport', () => { - beforeEach(() => { - window.fetch = fetch; - window.Headers = class Headers { - headers: { [key: string]: string } = {}; - get(key: string) { - return this.headers[key]; - } - set(key: string, value: string) { - this.headers[key] = value; - } - }; - transport = new Transports.FetchTransport({ dsn: testDsn }, window.fetch); +const ERROR_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ + [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, +]); + +class Headers { + headers: { [key: string]: string } = {}; + get(key: string) { + return this.headers[key] || null; + } + set(key: string, value: string) { + this.headers[key] = value; + } +} + +describe('NewFetchTransport', () => { + it('calls fetch with the given URL', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + headers: new Headers(), + status: 200, + text: () => Promise.resolve({}), + }), + ) as unknown as FetchImpl; + const transport = makeFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch); + + expect(mockFetch).toHaveBeenCalledTimes(0); + await transport.send(ERROR_ENVELOPE); + expect(mockFetch).toHaveBeenCalledTimes(1); + + expect(mockFetch).toHaveBeenLastCalledWith(DEFAULT_FETCH_TRANSPORT_OPTIONS.url, { + body: serializeEnvelope(ERROR_ENVELOPE, new TextEncoder()), + method: 'POST', + referrerPolicy: 'origin', + }); }); - afterEach(() => { - fetch.mockRestore(); - }); + it('sets rate limit headers', async () => { + const headers = { + get: jest.fn(), + }; - it('inherits composeEndpointUrl() implementation', () => { - // eslint-disable-next-line deprecation/deprecation - expect(transport.url).toBe(storeUrl); + const mockFetch = jest.fn(() => + Promise.resolve({ + headers, + status: 200, + text: () => Promise.resolve({}), + }), + ) as unknown as FetchImpl; + const transport = makeFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch); + + expect(headers.get).toHaveBeenCalledTimes(0); + await transport.send(ERROR_ENVELOPE); + + expect(headers.get).toHaveBeenCalledTimes(2); + expect(headers.get).toHaveBeenCalledWith('X-Sentry-Rate-Limits'); + expect(headers.get).toHaveBeenCalledWith('Retry-After'); }); - describe('sendEvent()', () => { - it('sends a request to Sentry servers', async () => { - const response = { status: 200, headers: new Headers() }; - - window.fetch.mockImplementation(() => Promise.resolve(response)); - - const res = await transport.sendEvent(eventPayload); - - expect((res as Response).status).toBe('success'); - expect(fetch).toHaveBeenCalledWith(storeUrl, { - body: JSON.stringify(eventPayload), - method: 'POST', - referrerPolicy: 'origin', - }); - }); - - it('sends a request to tunnel if configured', async () => { - transport = new Transports.FetchTransport({ dsn: testDsn, tunnel }, window.fetch); - window.fetch.mockImplementation(() => Promise.resolve({ status: 200, headers: new Headers() })); - - await transport.sendEvent(eventPayload); - - expect(fetch.mock.calls[0][0]).toBe(tunnel); - }); - - it('rejects with non-200 status code', async () => { - const response = { status: 403, headers: new Headers() }; - - window.fetch.mockImplementation(() => Promise.resolve(response)); - - try { - await transport.sendEvent(eventPayload); - } catch (res) { - expect((res as Response).status).toBe(403); - expect(fetch).toHaveBeenCalledWith(storeUrl, { - body: JSON.stringify(eventPayload), - method: 'POST', - referrerPolicy: 'origin', - }); - } - }); - - it('pass the error to rejection when fetch fails', async () => { - const response = { status: 403, headers: new Headers() }; - - window.fetch.mockImplementation(() => Promise.reject(response)); - - try { - await transport.sendEvent(eventPayload); - } catch (res) { - expect(res).toBe(response); - } - }); - - it('should record dropped event when fetch fails', async () => { - const response = { status: 403, headers: new Headers() }; - - window.fetch.mockImplementation(() => Promise.reject(response)); - - const spy = jest.spyOn(transport, 'recordLostEvent'); - - try { - await transport.sendEvent(eventPayload); - } catch (_) { - expect(spy).toHaveBeenCalledWith('network_error', 'event'); - } - }); - - it('should record dropped event when queue buffer overflows', async () => { - // @ts-ignore private method - jest.spyOn(transport._buffer, 'add').mockRejectedValue(new SentryError('Buffer Full')); - const spy = jest.spyOn(transport, 'recordLostEvent'); - - try { - await transport.sendEvent(transactionPayload); - } catch (_) { - expect(spy).toHaveBeenCalledWith('queue_overflow', 'transaction'); - } - }); - - it('passes in headers', async () => { - transport = new Transports.FetchTransport( - { - dsn: testDsn, - headers: { - Accept: 'application/json', - }, - }, - window.fetch, - ); - const response = { status: 200, headers: new Headers() }; - - window.fetch.mockImplementation(() => Promise.resolve(response)); - - const res = await transport.sendEvent(eventPayload); - - expect((res as Response).status).toBe('success'); - expect(fetch).toHaveBeenCalledWith(storeUrl, { - body: JSON.stringify(eventPayload), - headers: { - Accept: 'application/json', - }, - method: 'POST', - referrerPolicy: 'origin', - }); - }); - - it('passes in fetch parameters', async () => { - transport = new Transports.FetchTransport( - { - dsn: testDsn, - fetchParameters: { - credentials: 'include', - }, - }, - window.fetch, - ); - const response = { status: 200, headers: new Headers() }; - - window.fetch.mockImplementation(() => Promise.resolve(response)); - - const res = await transport.sendEvent(eventPayload); - - expect((res as Response).status).toBe('success'); - expect(fetch).toHaveBeenCalledWith(storeUrl, { - body: JSON.stringify(eventPayload), - credentials: 'include', - method: 'POST', - referrerPolicy: 'origin', - }); - }); - - describe('Rate-limiting', () => { - it('back-off using Retry-After header', async () => { - const retryAfterSeconds = 10; - const beforeLimit = Date.now(); - const withinLimit = beforeLimit + (retryAfterSeconds / 2) * 1000; - const afterLimit = beforeLimit + retryAfterSeconds * 1000; - - jest - .spyOn(Date, 'now') - // 1st event - updateRateLimits - false - .mockImplementationOnce(() => beforeLimit) - // 1st event - _handleRateLimit - .mockImplementationOnce(() => beforeLimit) - // 1st event - _handleRateLimit - .mockImplementationOnce(() => beforeLimit) - // 2nd event - _isRateLimited - true - .mockImplementationOnce(() => withinLimit) - // 3rd event - _isRateLimited - false - .mockImplementationOnce(() => afterLimit) - // 3rd event - _handleRateLimit - .mockImplementationOnce(() => afterLimit); - - const headers = new Headers(); - headers.set('Retry-After', `${retryAfterSeconds}`); - window.fetch.mockImplementation(() => Promise.resolve({ status: 429, headers })); - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBeUndefined(); - expect(fetch).toHaveBeenCalled(); - } - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBe( - `Transport for event requests locked till ${new Date(afterLimit)} due to too many requests.`, - ); - expect(fetch).toHaveBeenCalled(); - } - - window.fetch.mockImplementation(() => Promise.resolve({ status: 200, headers: new Headers() })); - - const eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toBe('success'); - expect(fetch).toHaveBeenCalledTimes(2); - }); - - it('back-off using X-Sentry-Rate-Limits with single category', async () => { - const retryAfterSeconds = 10; - const beforeLimit = Date.now(); - const withinLimit = beforeLimit + (retryAfterSeconds / 2) * 1000; - const afterLimit = beforeLimit + retryAfterSeconds * 1000; - - jest - .spyOn(Date, 'now') - // 1st event - _isRateLimited - false - .mockImplementationOnce(() => beforeLimit) - // 1st event - _handleRateLimit - .mockImplementationOnce(() => beforeLimit) - // 1st event - _isRateLimited - .mockImplementationOnce(() => beforeLimit) - // 1st event - _handleRateLimit - .mockImplementationOnce(() => beforeLimit) - // 2nd event - _isRateLimited - false (different category) - .mockImplementationOnce(() => withinLimit) - // 2nd event - _handleRateLimit - .mockImplementationOnce(() => withinLimit) - // 3rd event - _isRateLimited - true - .mockImplementationOnce(() => withinLimit) - // 4th event - _isRateLimited - false - .mockImplementationOnce(() => afterLimit) - // 4th event - _handleRateLimit - .mockImplementationOnce(() => afterLimit); - - const headers = new Headers(); - headers.set('X-Sentry-Rate-Limits', `${retryAfterSeconds}:error:scope`); - window.fetch.mockImplementation(() => Promise.resolve({ status: 429, headers })); - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBeUndefined(); - expect(fetch).toHaveBeenCalled(); - } - - window.fetch.mockImplementation(() => Promise.resolve({ status: 200, headers: new Headers() })); - - const transactionRes = await transport.sendEvent(transactionPayload); - expect(transactionRes.status).toBe('success'); - expect(fetch).toHaveBeenCalledTimes(2); - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBe( - `Transport for event requests locked till ${new Date(afterLimit)} due to too many requests.`, - ); - expect(fetch).toHaveBeenCalledTimes(2); - } - - const eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toBe('success'); - expect(fetch).toHaveBeenCalledTimes(3); - }); - - it('back-off using X-Sentry-Rate-Limits with multiple categories', async () => { - const retryAfterSeconds = 10; - const beforeLimit = Date.now(); - const withinLimit = beforeLimit + (retryAfterSeconds / 2) * 1000; - const afterLimit = beforeLimit + retryAfterSeconds * 1000; - - jest - .spyOn(Date, 'now') - // 1st event - _isRateLimited - false - .mockImplementationOnce(() => beforeLimit) - // 1st event - updateRateLimits - .mockImplementationOnce(() => beforeLimit) - // 1st event - _isRateLimited - .mockImplementationOnce(() => beforeLimit) - // 2nd event - _isRateLimited - true (event category) - .mockImplementationOnce(() => withinLimit) - // 3rd event - _isRateLimited - true (transaction category) - .mockImplementationOnce(() => withinLimit) - // 4th event - _isRateLimited - false (event category) - .mockImplementationOnce(() => afterLimit) - // 4th event - _handleRateLimit - .mockImplementationOnce(() => afterLimit) - // 5th event - _isRateLimited - false (transaction category) - .mockImplementationOnce(() => afterLimit) - // 5th event - _handleRateLimit - .mockImplementationOnce(() => afterLimit); - - const headers = new Headers(); - headers.set('X-Sentry-Rate-Limits', `${retryAfterSeconds}:error;transaction:scope`); - window.fetch.mockImplementation(() => Promise.resolve({ status: 429, headers })); - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBeUndefined(); - expect(fetch).toHaveBeenCalled(); - } - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBe( - `Transport for event requests locked till ${new Date(afterLimit)} due to too many requests.`, - ); - expect(fetch).toHaveBeenCalled(); - } - - try { - await transport.sendEvent(transactionPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBe( - `Transport for transaction requests locked till ${new Date(afterLimit)} due to too many requests.`, - ); - expect(fetch).toHaveBeenCalled(); - } - - window.fetch.mockImplementation(() => Promise.resolve({ status: 200, headers: new Headers() })); - - const eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toBe('success'); - expect(fetch).toHaveBeenCalledTimes(2); - - const transactionRes = await transport.sendEvent(transactionPayload); - expect(transactionRes.status).toBe('success'); - expect(fetch).toHaveBeenCalledTimes(3); - }); - - it('back-off using X-Sentry-Rate-Limits with missing categories should lock them all', async () => { - const retryAfterSeconds = 10; - const beforeLimit = Date.now(); - const withinLimit = beforeLimit + (retryAfterSeconds / 2) * 1000; - const afterLimit = beforeLimit + retryAfterSeconds * 1000; - - jest - .spyOn(Date, 'now') - // 1st event - _isRateLimited - false - .mockImplementationOnce(() => beforeLimit) - // 1st event - _handleRateLimit - .mockImplementationOnce(() => beforeLimit) - // 1st event - _isRateLimited - .mockImplementationOnce(() => beforeLimit) - // 2nd event - _isRateLimited - true (event category) - .mockImplementationOnce(() => withinLimit) - // 3rd event - _isRateLimited - true (transaction category) - .mockImplementationOnce(() => withinLimit) - // 4th event - _isRateLimited - false (event category) - .mockImplementationOnce(() => afterLimit) - // 4th event - _handleRateLimit - .mockImplementationOnce(() => afterLimit) - // 5th event - _isRateLimited - false (transaction category) - .mockImplementationOnce(() => afterLimit) - // 5th event - _handleRateLimit - .mockImplementationOnce(() => afterLimit); - - const headers = new Headers(); - headers.set('X-Sentry-Rate-Limits', `${retryAfterSeconds}::scope`); - window.fetch.mockImplementation(() => Promise.resolve({ status: 429, headers })); - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBeUndefined(); - expect(fetch).toHaveBeenCalled(); - } - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBe( - `Transport for event requests locked till ${new Date(afterLimit)} due to too many requests.`, - ); - expect(fetch).toHaveBeenCalled(); - } - - try { - await transport.sendEvent(transactionPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBe( - `Transport for transaction requests locked till ${new Date(afterLimit)} due to too many requests.`, - ); - expect(fetch).toHaveBeenCalled(); - } - - window.fetch.mockImplementation(() => Promise.resolve({ status: 200, headers: new Headers() })); - - const eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toBe('success'); - expect(fetch).toHaveBeenCalledTimes(2); - - const transactionRes = await transport.sendEvent(transactionPayload); - expect(transactionRes.status).toBe('success'); - expect(fetch).toHaveBeenCalledTimes(3); - }); - - it('back-off using X-Sentry-Rate-Limits should also trigger for 200 responses', async () => { - const retryAfterSeconds = 10; - const beforeLimit = Date.now(); - const withinLimit = beforeLimit + (retryAfterSeconds / 2) * 1000; - const afterLimit = beforeLimit + retryAfterSeconds * 1000; - - jest - .spyOn(Date, 'now') - // 1st event - _isRateLimited - false - .mockImplementationOnce(() => beforeLimit) - // 1st event - _handleRateLimit - .mockImplementationOnce(() => beforeLimit) - // 1st event - _isRateLimited - .mockImplementationOnce(() => beforeLimit) - // 2nd event - _isRateLimited - true - .mockImplementationOnce(() => withinLimit) - // 3rd event - _isRateLimited - false - .mockImplementationOnce(() => afterLimit) - // 3rd event - _handleRateLimit - .mockImplementationOnce(() => afterLimit); - - const headers = new Headers(); - headers.set('X-Sentry-Rate-Limits', `${retryAfterSeconds}:error;transaction:scope`); - window.fetch.mockImplementation(() => Promise.resolve({ status: 200, headers })); - - let eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toBe('success'); - expect(fetch).toHaveBeenCalled(); - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBe( - `Transport for event requests locked till ${new Date(afterLimit)} due to too many requests.`, - ); - expect(fetch).toHaveBeenCalled(); - } - - window.fetch.mockImplementation(() => Promise.resolve({ status: 200, headers: new Headers() })); - - eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toBe('success'); - expect(fetch).toHaveBeenCalledTimes(2); - }); - - it('should record dropped event', async () => { - // @ts-ignore private method - jest.spyOn(transport, '_isRateLimited').mockReturnValue(true); - - const spy = jest.spyOn(transport, 'recordLostEvent'); + it('allows for custom options to be passed in', async () => { + const mockFetch = jest.fn(() => + Promise.resolve({ + headers: new Headers(), + status: 200, + text: () => Promise.resolve({}), + }), + ) as unknown as FetchImpl; + + const REQUEST_OPTIONS: RequestInit = { + referrerPolicy: 'strict-origin', + keepalive: true, + referrer: 'http://example.org', + }; - try { - await transport.sendEvent(eventPayload); - } catch (_) { - expect(spy).toHaveBeenCalledWith('ratelimit_backoff', 'event'); - } + const transport = makeFetchTransport( + { ...DEFAULT_FETCH_TRANSPORT_OPTIONS, fetchOptions: REQUEST_OPTIONS }, + mockFetch, + ); - try { - await transport.sendEvent(transactionPayload); - } catch (_) { - expect(spy).toHaveBeenCalledWith('ratelimit_backoff', 'transaction'); - } - }); + await transport.send(ERROR_ENVELOPE); + expect(mockFetch).toHaveBeenLastCalledWith(DEFAULT_FETCH_TRANSPORT_OPTIONS.url, { + body: serializeEnvelope(ERROR_ENVELOPE, new TextEncoder()), + method: 'POST', + ...REQUEST_OPTIONS, }); }); }); diff --git a/packages/browser/test/unit/transports/new-fetch.test.ts b/packages/browser/test/unit/transports/new-fetch.test.ts deleted file mode 100644 index e1030be07204..000000000000 --- a/packages/browser/test/unit/transports/new-fetch.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { EventEnvelope, EventItem } from '@sentry/types'; -import { createEnvelope, serializeEnvelope } from '@sentry/utils'; - -import { FetchTransportOptions, makeNewFetchTransport } from '../../../src/transports/new-fetch'; -import { FetchImpl } from '../../../src/transports/utils'; - -const DEFAULT_FETCH_TRANSPORT_OPTIONS: FetchTransportOptions = { - url: 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7', -}; - -const ERROR_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ - [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, -]); - -class Headers { - headers: { [key: string]: string } = {}; - get(key: string) { - return this.headers[key] || null; - } - set(key: string, value: string) { - this.headers[key] = value; - } -} - -describe('NewFetchTransport', () => { - it('calls fetch with the given URL', async () => { - const mockFetch = jest.fn(() => - Promise.resolve({ - headers: new Headers(), - status: 200, - text: () => Promise.resolve({}), - }), - ) as unknown as FetchImpl; - const transport = makeNewFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch); - - expect(mockFetch).toHaveBeenCalledTimes(0); - const res = await transport.send(ERROR_ENVELOPE); - expect(mockFetch).toHaveBeenCalledTimes(1); - - expect(res.status).toBe('success'); - - expect(mockFetch).toHaveBeenLastCalledWith(DEFAULT_FETCH_TRANSPORT_OPTIONS.url, { - body: serializeEnvelope(ERROR_ENVELOPE), - method: 'POST', - referrerPolicy: 'origin', - }); - }); - - it('sets rate limit headers', async () => { - const headers = { - get: jest.fn(), - }; - - const mockFetch = jest.fn(() => - Promise.resolve({ - headers, - status: 200, - text: () => Promise.resolve({}), - }), - ) as unknown as FetchImpl; - const transport = makeNewFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch); - - expect(headers.get).toHaveBeenCalledTimes(0); - await transport.send(ERROR_ENVELOPE); - - expect(headers.get).toHaveBeenCalledTimes(2); - expect(headers.get).toHaveBeenCalledWith('X-Sentry-Rate-Limits'); - expect(headers.get).toHaveBeenCalledWith('Retry-After'); - }); - - it('allows for custom options to be passed in', async () => { - const mockFetch = jest.fn(() => - Promise.resolve({ - headers: new Headers(), - status: 200, - text: () => Promise.resolve({}), - }), - ) as unknown as FetchImpl; - - const REQUEST_OPTIONS: RequestInit = { - referrerPolicy: 'strict-origin', - keepalive: true, - referrer: 'http://example.org', - }; - - const transport = makeNewFetchTransport( - { ...DEFAULT_FETCH_TRANSPORT_OPTIONS, requestOptions: REQUEST_OPTIONS }, - mockFetch, - ); - - await transport.send(ERROR_ENVELOPE); - expect(mockFetch).toHaveBeenLastCalledWith(DEFAULT_FETCH_TRANSPORT_OPTIONS.url, { - body: serializeEnvelope(ERROR_ENVELOPE), - method: 'POST', - ...REQUEST_OPTIONS, - }); - }); -}); diff --git a/packages/browser/test/unit/transports/new-xhr.test.ts b/packages/browser/test/unit/transports/new-xhr.test.ts deleted file mode 100644 index 5b3bbda313c7..000000000000 --- a/packages/browser/test/unit/transports/new-xhr.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { EventEnvelope, EventItem } from '@sentry/types'; -import { createEnvelope, serializeEnvelope } from '@sentry/utils'; - -import { makeNewXHRTransport, XHRTransportOptions } from '../../../src/transports/new-xhr'; - -const DEFAULT_XHR_TRANSPORT_OPTIONS: XHRTransportOptions = { - url: 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7', -}; - -const ERROR_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ - [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, -]); - -function createXHRMock() { - const retryAfterSeconds = 10; - - const xhrMock: Partial = { - open: jest.fn(), - send: jest.fn(), - setRequestHeader: jest.fn(), - readyState: 4, - status: 200, - response: 'Hello World!', - onreadystatechange: () => {}, - getResponseHeader: jest.fn((header: string) => { - switch (header) { - case 'Retry-After': - return '10'; - case `${retryAfterSeconds}`: - return; - default: - return `${retryAfterSeconds}:error:scope`; - } - }), - }; - - // casting `window` as `any` because XMLHttpRequest is missing in Window (TS-only) - jest.spyOn(window as any, 'XMLHttpRequest').mockImplementation(() => xhrMock as XMLHttpRequest); - - return xhrMock; -} - -describe('NewXHRTransport', () => { - const xhrMock: Partial = createXHRMock(); - - afterEach(() => { - jest.clearAllMocks(); - }); - - afterAll(() => { - jest.restoreAllMocks(); - }); - - it('makes an XHR request to the given URL', async () => { - const transport = makeNewXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS); - expect(xhrMock.open).toHaveBeenCalledTimes(0); - expect(xhrMock.setRequestHeader).toHaveBeenCalledTimes(0); - expect(xhrMock.send).toHaveBeenCalledTimes(0); - - await Promise.all([transport.send(ERROR_ENVELOPE), (xhrMock as XMLHttpRequest).onreadystatechange(null)]); - - expect(xhrMock.open).toHaveBeenCalledTimes(1); - expect(xhrMock.open).toHaveBeenCalledWith('POST', DEFAULT_XHR_TRANSPORT_OPTIONS.url); - expect(xhrMock.send).toHaveBeenCalledTimes(1); - expect(xhrMock.send).toHaveBeenCalledWith(serializeEnvelope(ERROR_ENVELOPE)); - }); - - it('returns the correct response', async () => { - const transport = makeNewXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS); - - const [res] = await Promise.all([ - transport.send(ERROR_ENVELOPE), - (xhrMock as XMLHttpRequest).onreadystatechange(null), - ]); - - expect(res).toBeDefined(); - expect(res.status).toEqual('success'); - }); - - it('sets rate limit response headers', async () => { - const transport = makeNewXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS); - - await Promise.all([transport.send(ERROR_ENVELOPE), (xhrMock as XMLHttpRequest).onreadystatechange(null)]); - - expect(xhrMock.getResponseHeader).toHaveBeenCalledTimes(2); - expect(xhrMock.getResponseHeader).toHaveBeenCalledWith('X-Sentry-Rate-Limits'); - expect(xhrMock.getResponseHeader).toHaveBeenCalledWith('Retry-After'); - }); - - it('sets custom request headers', async () => { - const headers = { - referrerPolicy: 'strict-origin', - keepalive: 'true', - referrer: 'http://example.org', - }; - const options: XHRTransportOptions = { - ...DEFAULT_XHR_TRANSPORT_OPTIONS, - headers, - }; - - const transport = makeNewXHRTransport(options); - await Promise.all([transport.send(ERROR_ENVELOPE), (xhrMock as XMLHttpRequest).onreadystatechange(null)]); - - expect(xhrMock.setRequestHeader).toHaveBeenCalledTimes(3); - expect(xhrMock.setRequestHeader).toHaveBeenCalledWith('referrerPolicy', headers.referrerPolicy); - expect(xhrMock.setRequestHeader).toHaveBeenCalledWith('keepalive', headers.keepalive); - expect(xhrMock.setRequestHeader).toHaveBeenCalledWith('referrer', headers.referrer); - }); -}); diff --git a/packages/browser/test/unit/transports/xhr.test.ts b/packages/browser/test/unit/transports/xhr.test.ts index 2a0f43d89815..117edce8d2ea 100644 --- a/packages/browser/test/unit/transports/xhr.test.ts +++ b/packages/browser/test/unit/transports/xhr.test.ts @@ -1,441 +1,101 @@ -import { SentryError } from '@sentry/utils'; -import { fakeServer, SinonFakeServer } from 'sinon'; +import { EventEnvelope, EventItem } from '@sentry/types'; +import { createEnvelope, serializeEnvelope } from '@sentry/utils'; +import { TextEncoder } from 'util'; -import { Event, Response, Transports } from '../../../src'; +import { BrowserTransportOptions } from '../../../src/transports/types'; +import { makeXHRTransport } from '../../../src/transports/xhr'; -const testDsn = 'https://123@sentry.io/42'; -const storeUrl = 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7'; -const envelopeUrl = 'https://sentry.io/api/42/envelope/?sentry_key=123&sentry_version=7'; -const tunnel = 'https://hello.com/world'; -const eventPayload: Event = { - event_id: '1337', +const DEFAULT_XHR_TRANSPORT_OPTIONS: BrowserTransportOptions = { + url: 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7', + recordDroppedEvent: () => undefined, + textEncoder: new TextEncoder(), }; -const transactionPayload: Event = { - event_id: '42', - type: 'transaction', -}; - -let server: SinonFakeServer; -let transport: Transports.BaseTransport; - -describe('XHRTransport', () => { - beforeEach(() => { - server = fakeServer.create(); - server.respondImmediately = true; - transport = new Transports.XHRTransport({ dsn: testDsn }); - }); - - afterEach(() => { - server.restore(); - }); - - it('inherits composeEndpointUrl() implementation', () => { - // eslint-disable-next-line deprecation/deprecation - expect(transport.url).toBe(storeUrl); - }); - - describe('sendEvent()', () => { - it('sends a request to Sentry servers', async () => { - server.respondWith('POST', storeUrl, [200, {}, '']); - - const res = await transport.sendEvent(eventPayload); - - expect((res as Response).status).toBe('success'); - const request = server.requests[0]; - expect(server.requests.length).toBe(1); - expect(request.method).toBe('POST'); - expect(JSON.parse(request.requestBody)).toEqual(eventPayload); - }); - - it('sends a request to tunnel if configured', async () => { - transport = new Transports.XHRTransport({ dsn: testDsn, tunnel }); - server.respondWith('POST', tunnel, [200, {}, '']); - - await transport.sendEvent(eventPayload); - - expect(server.requests[0].url).toBe(tunnel); - }); - - it('rejects with non-200 status code', async () => { - server.respondWith('POST', storeUrl, [403, {}, '']); - try { - await transport.sendEvent(eventPayload); - } catch (res) { - expect((res as Response).status).toBe(403); - const request = server.requests[0]; - expect(server.requests.length).toBe(1); - expect(request.method).toBe('POST'); - expect(JSON.parse(request.requestBody)).toEqual(eventPayload); +const ERROR_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ + [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, +]); + +function createXHRMock() { + const retryAfterSeconds = 10; + + const xhrMock: Partial = { + open: jest.fn(), + send: jest.fn(), + setRequestHeader: jest.fn(), + readyState: 4, + status: 200, + response: 'Hello World!', + onreadystatechange: () => {}, + getResponseHeader: jest.fn((header: string) => { + switch (header) { + case 'Retry-After': + return '10'; + case `${retryAfterSeconds}`: + return null; + default: + return `${retryAfterSeconds}:error:scope`; } - }); - - it('should record dropped event when request fails', async () => { - server.respondWith('POST', storeUrl, [403, {}, '']); - - const spy = jest.spyOn(transport, 'recordLostEvent'); - - try { - await transport.sendEvent(eventPayload); - } catch (_) { - expect(spy).toHaveBeenCalledWith('network_error', 'event'); - } - }); - - it('should record dropped event when queue buffer overflows', async () => { - // @ts-ignore private method - jest.spyOn(transport._buffer, 'add').mockRejectedValue(new SentryError('Buffer Full')); - const spy = jest.spyOn(transport, 'recordLostEvent'); - - try { - await transport.sendEvent(transactionPayload); - } catch (_) { - expect(spy).toHaveBeenCalledWith('queue_overflow', 'transaction'); - } - }); - - it('passes in headers', async () => { - transport = new Transports.XHRTransport({ - dsn: testDsn, - headers: { - Accept: 'application/json', - }, - }); - - server.respondWith('POST', storeUrl, [200, {}, '']); - const res = await transport.sendEvent(eventPayload); - const request = server.requests[0]; - - expect((res as Response).status).toBe('success'); - const requestHeaders: { [key: string]: string } = request.requestHeaders as { [key: string]: string }; - expect(requestHeaders['Accept']).toBe('application/json'); - }); - - describe('Rate-limiting', () => { - it('back-off using Retry-After header', async () => { - const retryAfterSeconds = 10; - const beforeLimit = Date.now(); - const withinLimit = beforeLimit + (retryAfterSeconds / 2) * 1000; - const afterLimit = beforeLimit + retryAfterSeconds * 1000; - - server.respondWith('POST', storeUrl, [429, { 'Retry-After': `${retryAfterSeconds}` }, '']); - - jest - .spyOn(Date, 'now') - // 1st event - _isRateLimited - false - .mockImplementationOnce(() => beforeLimit) - // 1st event - _handleRateLimit - .mockImplementationOnce(() => beforeLimit) - // 1st event - _handleRateLimit - .mockImplementationOnce(() => beforeLimit) - // 2nd event - _isRateLimited - true - .mockImplementationOnce(() => withinLimit) - // 3rd event - _isRateLimited - false - .mockImplementationOnce(() => afterLimit) - // 3rd event - _handleRateLimit - .mockImplementationOnce(() => afterLimit); - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBeUndefined(); - expect(server.requests.length).toBe(1); - } - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBe( - `Transport for event requests locked till ${new Date(afterLimit)} due to too many requests.`, - ); - expect(server.requests.length).toBe(1); - } - - server.respondWith('POST', storeUrl, [200, {}, '']); + }), + }; - const eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toBe('success'); - expect(server.requests.length).toBe(2); - }); + // casting `window` as `any` because XMLHttpRequest is missing in Window (TS-only) + jest.spyOn(window as any, 'XMLHttpRequest').mockImplementation(() => xhrMock as XMLHttpRequest); - it('back-off using X-Sentry-Rate-Limits with single category', async () => { - const retryAfterSeconds = 10; - const beforeLimit = Date.now(); - const withinLimit = beforeLimit + (retryAfterSeconds / 2) * 1000; - const afterLimit = beforeLimit + retryAfterSeconds * 1000; + return xhrMock; +} - server.respondWith('POST', storeUrl, [429, { 'X-Sentry-Rate-Limits': `${retryAfterSeconds}:error:scope` }, '']); - server.respondWith('POST', envelopeUrl, [200, {}, '']); +describe('NewXHRTransport', () => { + const xhrMock: Partial = createXHRMock(); - jest - .spyOn(Date, 'now') - // 1st event - _isRateLimited - false - .mockImplementationOnce(() => beforeLimit) - // 1st event - _handleRateLimit - .mockImplementationOnce(() => beforeLimit) - // 1st event - _isRateLimited - .mockImplementationOnce(() => beforeLimit) - // 1st event - _handleRateLimit - .mockImplementationOnce(() => beforeLimit) - // 2nd event - _isRateLimited - false (different category) - .mockImplementationOnce(() => withinLimit) - // 2nd event - _handleRateLimit - .mockImplementationOnce(() => withinLimit) - // 3rd event - _isRateLimited - true - .mockImplementationOnce(() => withinLimit) - // 4th event - _isRateLimited - false - .mockImplementationOnce(() => afterLimit) - // 4th event - _handleRateLimit - .mockImplementationOnce(() => afterLimit); - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBeUndefined(); - expect(server.requests.length).toBe(1); - } - - const transactionRes = await transport.sendEvent(transactionPayload); - expect(transactionRes.status).toBe('success'); - expect(server.requests.length).toBe(2); - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBe( - `Transport for event requests locked till ${new Date(afterLimit)} due to too many requests.`, - ); - expect(server.requests.length).toBe(2); - } - - server.respondWith('POST', storeUrl, [200, {}, '']); - - const eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toBe('success'); - expect(server.requests.length).toBe(3); - }); - - it('back-off using X-Sentry-Rate-Limits with multiple categories', async () => { - const retryAfterSeconds = 10; - const beforeLimit = Date.now(); - const withinLimit = beforeLimit + (retryAfterSeconds / 2) * 1000; - const afterLimit = beforeLimit + retryAfterSeconds * 1000; - - server.respondWith('POST', storeUrl, [ - 429, - { 'X-Sentry-Rate-Limits': `${retryAfterSeconds}:error;transaction:scope` }, - '', - ]); - server.respondWith('POST', envelopeUrl, [200, {}, '']); - - jest - .spyOn(Date, 'now') - // 1st event - _isRateLimited - false - .mockImplementationOnce(() => beforeLimit) - // 1st event - _handleRateLimit - .mockImplementationOnce(() => beforeLimit) - // 1st event - _isRateLimited - .mockImplementationOnce(() => beforeLimit) - // 2nd event - _isRateLimited - true (event category) - .mockImplementationOnce(() => withinLimit) - // 3rd event - _isRateLimited - true (transaction category) - .mockImplementationOnce(() => withinLimit) - // 4th event - _isRateLimited - false (event category) - .mockImplementationOnce(() => afterLimit) - // 4th event - _handleRateLimit - .mockImplementationOnce(() => afterLimit) - // 5th event - _isRateLimited - false (transaction category) - .mockImplementationOnce(() => afterLimit) - // 5th event - _handleRateLimit - .mockImplementationOnce(() => afterLimit); - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBeUndefined(); - expect(server.requests.length).toBe(1); - } - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBe( - `Transport for event requests locked till ${new Date(afterLimit)} due to too many requests.`, - ); - expect(server.requests.length).toBe(1); - } - - try { - await transport.sendEvent(transactionPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBe( - `Transport for transaction requests locked till ${new Date(afterLimit)} due to too many requests.`, - ); - expect(server.requests.length).toBe(1); - } - - server.respondWith('POST', storeUrl, [200, {}, '']); - server.respondWith('POST', envelopeUrl, [200, {}, '']); - - const eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toBe('success'); - expect(server.requests.length).toBe(2); - - const transactionRes = await transport.sendEvent(transactionPayload); - expect(transactionRes.status).toBe('success'); - expect(server.requests.length).toBe(3); - }); - - it('back-off using X-Sentry-Rate-Limits with missing categories should lock them all', async () => { - const retryAfterSeconds = 10; - const beforeLimit = Date.now(); - const withinLimit = beforeLimit + (retryAfterSeconds / 2) * 1000; - const afterLimit = beforeLimit + retryAfterSeconds * 1000; - - server.respondWith('POST', storeUrl, [429, { 'X-Sentry-Rate-Limits': `${retryAfterSeconds}::scope` }, '']); - server.respondWith('POST', envelopeUrl, [200, {}, '']); - - jest - .spyOn(Date, 'now') - // 1st event - _isRateLimited - false - .mockImplementationOnce(() => beforeLimit) - // 1st event - _handleRateLimit - .mockImplementationOnce(() => beforeLimit) - // 1st event - _isRateLimited - .mockImplementationOnce(() => beforeLimit) - // 2nd event - _isRateLimited - true (event category) - .mockImplementationOnce(() => withinLimit) - // 3rd event - _isRateLimited - true (transaction category) - .mockImplementationOnce(() => withinLimit) - // 4th event - _isRateLimited - false (event category) - .mockImplementationOnce(() => afterLimit) - // 4th event - _handleRateLimit - .mockImplementationOnce(() => afterLimit) - // 5th event - _isRateLimited - false (transaction category) - .mockImplementationOnce(() => afterLimit) - // 5th event - _handleRateLimit - .mockImplementationOnce(() => afterLimit); - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBeUndefined(); - expect(server.requests.length).toBe(1); - } - - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBe( - `Transport for event requests locked till ${new Date(afterLimit)} due to too many requests.`, - ); - expect(server.requests.length).toBe(1); - } - - try { - await transport.sendEvent(transactionPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBe( - `Transport for transaction requests locked till ${new Date(afterLimit)} due to too many requests.`, - ); - expect(server.requests.length).toBe(1); - } - - server.respondWith('POST', storeUrl, [200, {}, '']); - server.respondWith('POST', envelopeUrl, [200, {}, '']); - - const eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toBe('success'); - expect(server.requests.length).toBe(2); - - const transactionRes = await transport.sendEvent(transactionPayload); - expect(transactionRes.status).toBe('success'); - expect(server.requests.length).toBe(3); - }); - - it('back-off using X-Sentry-Rate-Limits should also trigger for 200 responses', async () => { - const retryAfterSeconds = 10; - const beforeLimit = Date.now(); - const withinLimit = beforeLimit + (retryAfterSeconds / 2) * 1000; - const afterLimit = beforeLimit + retryAfterSeconds * 1000; - - server.respondWith('POST', storeUrl, [200, { 'X-Sentry-Rate-Limits': `${retryAfterSeconds}:error:scope` }, '']); - - jest - .spyOn(Date, 'now') - // 1st event - _isRateLimited - false - .mockImplementationOnce(() => beforeLimit) - // 1st event - _handleRateLimit - .mockImplementationOnce(() => beforeLimit) - // 1st event - _handleRateLimit - .mockImplementationOnce(() => beforeLimit) - // 2nd event - _isRateLimited - true - .mockImplementationOnce(() => withinLimit) - // 3rd event - _isRateLimited - false - .mockImplementationOnce(() => afterLimit) - // 3rd event - _handleRateLimit - .mockImplementationOnce(() => afterLimit); + afterEach(() => { + jest.clearAllMocks(); + }); - let eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toBe('success'); - expect(server.requests.length).toBe(1); + afterAll(() => { + jest.restoreAllMocks(); + }); - try { - await transport.sendEvent(eventPayload); - throw new Error('unreachable!'); - } catch (res) { - expect((res as Response).status).toBe(429); - expect((res as Response).reason).toBe( - `Transport for event requests locked till ${new Date(afterLimit)} due to too many requests.`, - ); - expect(server.requests.length).toBe(1); - } + it('makes an XHR request to the given URL', async () => { + const transport = makeXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS); + expect(xhrMock.open).toHaveBeenCalledTimes(0); + expect(xhrMock.setRequestHeader).toHaveBeenCalledTimes(0); + expect(xhrMock.send).toHaveBeenCalledTimes(0); - server.respondWith('POST', storeUrl, [200, {}, '']); + await Promise.all([transport.send(ERROR_ENVELOPE), (xhrMock as XMLHttpRequest).onreadystatechange!({} as Event)]); - eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toBe('success'); - expect(server.requests.length).toBe(2); - }); + expect(xhrMock.open).toHaveBeenCalledTimes(1); + expect(xhrMock.open).toHaveBeenCalledWith('POST', DEFAULT_XHR_TRANSPORT_OPTIONS.url); + expect(xhrMock.send).toHaveBeenCalledTimes(1); + expect(xhrMock.send).toHaveBeenCalledWith(serializeEnvelope(ERROR_ENVELOPE, new TextEncoder())); + }); - it('should record dropped event', async () => { - // @ts-ignore private method - jest.spyOn(transport, '_isRateLimited').mockReturnValue(true); + it('sets rate limit response headers', async () => { + const transport = makeXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS); - const spy = jest.spyOn(transport, 'recordLostEvent'); + await Promise.all([transport.send(ERROR_ENVELOPE), (xhrMock as XMLHttpRequest).onreadystatechange!({} as Event)]); - try { - await transport.sendEvent(eventPayload); - } catch (_) { - expect(spy).toHaveBeenCalledWith('ratelimit_backoff', 'event'); - } + expect(xhrMock.getResponseHeader).toHaveBeenCalledTimes(2); + expect(xhrMock.getResponseHeader).toHaveBeenCalledWith('X-Sentry-Rate-Limits'); + expect(xhrMock.getResponseHeader).toHaveBeenCalledWith('Retry-After'); + }); - try { - await transport.sendEvent(transactionPayload); - } catch (_) { - expect(spy).toHaveBeenCalledWith('ratelimit_backoff', 'transaction'); - } - }); - }); + it('sets custom request headers', async () => { + const headers = { + referrerPolicy: 'strict-origin', + keepalive: 'true', + referrer: 'http://example.org', + }; + const options: BrowserTransportOptions = { + ...DEFAULT_XHR_TRANSPORT_OPTIONS, + headers, + }; + + const transport = makeXHRTransport(options); + await Promise.all([transport.send(ERROR_ENVELOPE), (xhrMock as XMLHttpRequest).onreadystatechange!({} as Event)]); + + expect(xhrMock.setRequestHeader).toHaveBeenCalledTimes(3); + expect(xhrMock.setRequestHeader).toHaveBeenCalledWith('referrerPolicy', headers.referrerPolicy); + expect(xhrMock.setRequestHeader).toHaveBeenCalledWith('keepalive', headers.keepalive); + expect(xhrMock.setRequestHeader).toHaveBeenCalledWith('referrer', headers.referrer); }); }); diff --git a/packages/browser/tsconfig.cjs.json b/packages/browser/tsconfig.cjs.json deleted file mode 100644 index 6782dae5e453..000000000000 --- a/packages/browser/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "build/npm/dist" - } -} diff --git a/packages/browser/tsconfig.esm.json b/packages/browser/tsconfig.esm.json deleted file mode 100644 index feffe52ca581..000000000000 --- a/packages/browser/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "build/npm/esm" - } -} diff --git a/packages/core/README.md b/packages/core/README.md index b228fc669a80..0226b9e41be3 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Sentry JavaScript SDK Core diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js new file mode 100644 index 000000000000..24f49ab59a4c --- /dev/null +++ b/packages/core/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest/jest.config.js'); diff --git a/packages/core/package.json b/packages/core/package.json index 7a1e0a8f38d6..451937a7f2fd 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,47 +1,41 @@ { "name": "@sentry/core", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Base implementation for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/core", "author": "Sentry", "license": "BSD-3-Clause", "engines": { - "node": ">=6" + "node": ">=8" }, - "main": "build/dist/index.js", + "main": "build/cjs/index.js", "module": "build/esm/index.js", "types": "build/types/index.d.ts", "publishConfig": { "access": "public" }, "dependencies": { - "@sentry/hub": "6.19.7", - "@sentry/minimal": "6.19.7", - "@sentry/types": "6.19.7", - "@sentry/utils": "6.19.7", + "@sentry/hub": "7.0.0-rc.0", + "@sentry/types": "7.0.0-rc.0", + "@sentry/utils": "7.0.0-rc.0", "tslib": "^1.9.3" }, "scripts": { - "build": "run-p build:cjs build:esm build:types", - "build:cjs": "tsc -p tsconfig.cjs.json", + "build": "run-p build:rollup build:types", "build:dev": "run-s build", - "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "tsc -p tsconfig.esm.json", + "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", - "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", + "build:watch": "run-p build:rollup:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:es5:watch": "yarn build:cjs:watch # *** backwards compatibility - remove in v7 ***", - "build:esm:watch": "tsc -p tsconfig.esm.json --watch", + "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf dist esm build coverage", + "clean": "rimraf build coverage sentry-core-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/*.ts\"", - "link:yarn": "yarn link", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", @@ -52,25 +46,5 @@ "volta": { "extends": "../../package.json" }, - "jest": { - "collectCoverage": true, - "transform": { - "^.+\\.ts$": "ts-jest" - }, - "moduleFileExtensions": [ - "js", - "ts" - ], - "testEnvironment": "node", - "testMatch": [ - "**/*.test.ts" - ], - "globals": { - "ts-jest": { - "tsConfig": "./tsconfig.json", - "diagnostics": false - } - } - }, "sideEffects": false } diff --git a/packages/core/rollup.npm.config.js b/packages/core/rollup.npm.config.js new file mode 100644 index 000000000000..5a62b528ef44 --- /dev/null +++ b/packages/core/rollup.npm.config.js @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/core/src/api.ts b/packages/core/src/api.ts index bace0ad07591..31375593fd4a 100644 --- a/packages/core/src/api.ts +++ b/packages/core/src/api.ts @@ -1,99 +1,8 @@ -import { DsnComponents, DsnLike, SdkMetadata } from '@sentry/types'; +import { DsnComponents, DsnLike } from '@sentry/types'; import { dsnToString, makeDsn, urlEncode } from '@sentry/utils'; const SENTRY_API_VERSION = '7'; -/** - * Stores details about a Sentry SDK - */ -export interface APIDetails { - /** The DSN as passed to Sentry.init() */ - initDsn: DsnLike; - /** Metadata about the SDK (name, version, etc) for inclusion in envelope headers */ - metadata: SdkMetadata; - /** The internally used Dsn object. */ - readonly dsn: DsnComponents; - /** The envelope tunnel to use. */ - readonly tunnel?: string; -} - -/** - * Helper class to provide urls, headers and metadata that can be used to form - * different types of requests to Sentry endpoints. - * Supports both envelopes and regular event requests. - * - * @deprecated Please use APIDetails - **/ -export class API { - /** The DSN as passed to Sentry.init() */ - public dsn: DsnLike; - - /** Metadata about the SDK (name, version, etc) for inclusion in envelope headers */ - public metadata: SdkMetadata; - - /** The internally used Dsn object. */ - private readonly _dsnObject: DsnComponents; - - /** The envelope tunnel to use. */ - private readonly _tunnel?: string; - - /** Create a new instance of API */ - public constructor(dsn: DsnLike, metadata: SdkMetadata = {}, tunnel?: string) { - this.dsn = dsn; - this._dsnObject = makeDsn(dsn); - this.metadata = metadata; - this._tunnel = tunnel; - } - - /** Returns the Dsn object. */ - public getDsn(): DsnComponents { - return this._dsnObject; - } - - /** Does this transport force envelopes? */ - public forceEnvelope(): boolean { - return !!this._tunnel; - } - - /** Returns the prefix to construct Sentry ingestion API endpoints. */ - public getBaseApiEndpoint(): string { - return getBaseApiEndpoint(this._dsnObject); - } - - /** Returns the store endpoint URL. */ - public getStoreEndpoint(): string { - return getStoreEndpoint(this._dsnObject); - } - - /** - * Returns the store endpoint URL with auth in the query string. - * - * Sending auth as part of the query string and not as custom HTTP headers avoids CORS preflight requests. - */ - public getStoreEndpointWithUrlEncodedAuth(): string { - return getStoreEndpointWithUrlEncodedAuth(this._dsnObject); - } - - /** - * Returns the envelope endpoint URL with auth in the query string. - * - * Sending auth as part of the query string and not as custom HTTP headers avoids CORS preflight requests. - */ - public getEnvelopeEndpointWithUrlEncodedAuth(): string { - return getEnvelopeEndpointWithUrlEncodedAuth(this._dsnObject, this._tunnel); - } -} - -/** Initializes API Details */ -export function initAPIDetails(dsn: DsnLike, metadata?: SdkMetadata, tunnel?: string): APIDetails { - return { - initDsn: dsn, - metadata: metadata || {}, - dsn: makeDsn(dsn), - tunnel, - } as APIDetails; -} - /** Returns the prefix to construct Sentry ingestion API endpoints. */ function getBaseApiEndpoint(dsn: DsnComponents): string { const protocol = dsn.protocol ? `${dsn.protocol}:` : ''; @@ -102,8 +11,8 @@ function getBaseApiEndpoint(dsn: DsnComponents): string { } /** Returns the ingest API endpoint for target. */ -function _getIngestEndpoint(dsn: DsnComponents, target: 'store' | 'envelope'): string { - return `${getBaseApiEndpoint(dsn)}${dsn.projectId}/${target}/`; +function _getIngestEndpoint(dsn: DsnComponents): string { + return `${getBaseApiEndpoint(dsn)}${dsn.projectId}/envelope/`; } /** Returns a URL-encoded string with auth config suitable for a query string. */ @@ -116,54 +25,13 @@ function _encodedAuth(dsn: DsnComponents): string { }); } -/** Returns the store endpoint URL. */ -function getStoreEndpoint(dsn: DsnComponents): string { - return _getIngestEndpoint(dsn, 'store'); -} - -/** - * Returns the store endpoint URL with auth in the query string. - * - * Sending auth as part of the query string and not as custom HTTP headers avoids CORS preflight requests. - */ -export function getStoreEndpointWithUrlEncodedAuth(dsn: DsnComponents): string { - return `${getStoreEndpoint(dsn)}?${_encodedAuth(dsn)}`; -} - -/** Returns the envelope endpoint URL. */ -function _getEnvelopeEndpoint(dsn: DsnComponents): string { - return _getIngestEndpoint(dsn, 'envelope'); -} - /** * Returns the envelope endpoint URL with auth in the query string. * * Sending auth as part of the query string and not as custom HTTP headers avoids CORS preflight requests. */ export function getEnvelopeEndpointWithUrlEncodedAuth(dsn: DsnComponents, tunnel?: string): string { - return tunnel ? tunnel : `${_getEnvelopeEndpoint(dsn)}?${_encodedAuth(dsn)}`; -} - -/** - * Returns an object that can be used in request headers. - * This is needed for node and the old /store endpoint in sentry - */ -export function getRequestHeaders( - dsn: DsnComponents, - clientName: string, - clientVersion: string, -): { [key: string]: string } { - // CHANGE THIS to use metadata but keep clientName and clientVersion compatible - const header = [`Sentry sentry_version=${SENTRY_API_VERSION}`]; - header.push(`sentry_client=${clientName}/${clientVersion}`); - header.push(`sentry_key=${dsn.publicKey}`); - if (dsn.pass) { - header.push(`sentry_secret=${dsn.pass}`); - } - return { - 'Content-Type': 'application/json', - 'X-Sentry-Auth': header.join(', '), - }; + return tunnel ? tunnel : `${_getIngestEndpoint(dsn)}?${_encodedAuth(dsn)}`; } /** Returns the url to the report dialog endpoint. */ @@ -185,14 +53,15 @@ export function getReportDialogEndpoint( } if (key === 'user') { - if (!dialogOptions.user) { + const user = dialogOptions.user; + if (!user) { continue; } - if (dialogOptions.user.name) { - encodedOptions += `&name=${encodeURIComponent(dialogOptions.user.name)}`; + if (user.name) { + encodedOptions += `&name=${encodeURIComponent(user.name)}`; } - if (dialogOptions.user.email) { - encodedOptions += `&email=${encodeURIComponent(dialogOptions.user.email)}`; + if (user.email) { + encodedOptions += `&email=${encodeURIComponent(user.email)}`; } } else { encodedOptions += `&${encodeURIComponent(key)}=${encodeURIComponent(dialogOptions[key] as string)}`; diff --git a/packages/core/src/basebackend.ts b/packages/core/src/basebackend.ts deleted file mode 100644 index 92bdd4e7f65c..000000000000 --- a/packages/core/src/basebackend.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { Event, EventHint, Options, Session, Severity, Transport } from '@sentry/types'; -import { logger, SentryError } from '@sentry/utils'; - -import { initAPIDetails } from './api'; -import { IS_DEBUG_BUILD } from './flags'; -import { createEventEnvelope, createSessionEnvelope } from './request'; -import { NewTransport } from './transports/base'; -import { NoopTransport } from './transports/noop'; - -/** - * Internal platform-dependent Sentry SDK Backend. - * - * While {@link Client} contains business logic specific to an SDK, the - * Backend offers platform specific implementations for low-level operations. - * These are persisting and loading information, sending events, and hooking - * into the environment. - * - * Backends receive a handle to the Client in their constructor. When a - * Backend automatically generates events, it must pass them to - * the Client for validation and processing first. - * - * Usually, the Client will be of corresponding type, e.g. NodeBackend - * receives NodeClient. However, higher-level SDKs can choose to instantiate - * multiple Backends and delegate tasks between them. In this case, an event - * generated by one backend might very well be sent by another one. - * - * The client also provides access to options via {@link Client.getOptions}. - * @hidden - */ -export interface Backend { - /** Creates an {@link Event} from all inputs to `captureException` and non-primitive inputs to `captureMessage`. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - eventFromException(exception: any, hint?: EventHint): PromiseLike; - - /** Creates an {@link Event} from primitive inputs to `captureMessage`. */ - eventFromMessage(message: string, level?: Severity, hint?: EventHint): PromiseLike; - - /** Submits the event to Sentry */ - sendEvent(event: Event): void; - - /** Submits the session to Sentry */ - sendSession(session: Session): void; - - /** - * Returns the transport that is used by the backend. - * Please note that the transport gets lazy initialized so it will only be there once the first event has been sent. - * - * @returns The transport. - */ - getTransport(): Transport; -} - -/** - * A class object that can instantiate Backend objects. - * @hidden - */ -export type BackendClass = new (options: O) => B; - -/** - * This is the base implemention of a Backend. - * @hidden - */ -export abstract class BaseBackend implements Backend { - /** Options passed to the SDK. */ - protected readonly _options: O; - - /** Cached transport used internally. */ - protected _transport: Transport; - - /** New v7 Transport that is initialized alongside the old one */ - protected _newTransport?: NewTransport; - - /** Creates a new backend instance. */ - public constructor(options: O) { - this._options = options; - if (!this._options.dsn) { - IS_DEBUG_BUILD && logger.warn('No DSN provided, backend will not do anything.'); - } - this._transport = this._setupTransport(); - } - - /** - * @inheritDoc - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types - public eventFromException(_exception: any, _hint?: EventHint): PromiseLike { - throw new SentryError('Backend has to implement `eventFromException` method'); - } - - /** - * @inheritDoc - */ - public eventFromMessage(_message: string, _level?: Severity, _hint?: EventHint): PromiseLike { - throw new SentryError('Backend has to implement `eventFromMessage` method'); - } - - /** - * @inheritDoc - */ - public sendEvent(event: Event): void { - // TODO(v7): Remove the if-else - if ( - this._newTransport && - this._options.dsn && - this._options._experiments && - this._options._experiments.newTransport - ) { - const api = initAPIDetails(this._options.dsn, this._options._metadata, this._options.tunnel); - const env = createEventEnvelope(event, api); - void this._newTransport.send(env).then(null, reason => { - IS_DEBUG_BUILD && logger.error('Error while sending event:', reason); - }); - } else { - void this._transport.sendEvent(event).then(null, reason => { - IS_DEBUG_BUILD && logger.error('Error while sending event:', reason); - }); - } - } - - /** - * @inheritDoc - */ - public sendSession(session: Session): void { - if (!this._transport.sendSession) { - IS_DEBUG_BUILD && logger.warn("Dropping session because custom transport doesn't implement sendSession"); - return; - } - - // TODO(v7): Remove the if-else - if ( - this._newTransport && - this._options.dsn && - this._options._experiments && - this._options._experiments.newTransport - ) { - const api = initAPIDetails(this._options.dsn, this._options._metadata, this._options.tunnel); - const [env] = createSessionEnvelope(session, api); - void this._newTransport.send(env).then(null, reason => { - IS_DEBUG_BUILD && logger.error('Error while sending session:', reason); - }); - } else { - void this._transport.sendSession(session).then(null, reason => { - IS_DEBUG_BUILD && logger.error('Error while sending session:', reason); - }); - } - } - - /** - * @inheritDoc - */ - public getTransport(): Transport { - return this._transport; - } - - /** - * Sets up the transport so it can be used later to send requests. - */ - protected _setupTransport(): Transport { - return new NoopTransport(); - } -} diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index b7fef7f2164f..1c8de1472a49 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -1,18 +1,27 @@ /* eslint-disable max-lines */ -import { Scope, Session } from '@sentry/hub'; +import { Scope, updateSession } from '@sentry/hub'; import { Client, + ClientOptions, + DataCategory, DsnComponents, + Envelope, Event, + EventDropReason, EventHint, Integration, IntegrationClass, - Options, + Outcome, + Session, + SessionAggregates, Severity, + SeverityLevel, Transport, } from '@sentry/types'; import { + addItemToEnvelope, checkOrSetAlreadyCaught, + createAttachmentEnvelopeItem, dateTimestampInSeconds, isPlainObject, isPrimitive, @@ -28,7 +37,8 @@ import { uuid4, } from '@sentry/utils'; -import { Backend, BackendClass } from './basebackend'; +import { getEnvelopeEndpointWithUrlEncodedAuth } from './api'; +import { createEventEnvelope, createSessionEnvelope } from './envelope'; import { IS_DEBUG_BUILD } from './flags'; import { IntegrationIndex, setupIntegrations } from './integration'; @@ -37,17 +47,16 @@ const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been ca /** * Base implementation for all JavaScript SDK clients. * - * Call the constructor with the corresponding backend constructor and options + * Call the constructor with the corresponding options * specific to the client subclass. To access these options later, use - * {@link Client.getOptions}. Also, the Backend instance is available via - * {@link Client.getBackend}. + * {@link Client.getOptions}. * * If a Dsn is specified in the options, it will be parsed and stored. Use * {@link Client.getDsn} to retrieve the Dsn at any moment. In case the Dsn is * invalid, the constructor will throw a {@link SentryException}. Note that * without a valid Dsn, the SDK will not send any events to Sentry. * - * Before sending an event via the backend, it is passed through + * Before sending an event, it is passed through * {@link BaseClient._prepareEvent} to add SDK information and scope data * (breadcrumbs and context). To add more custom information, override this * method and extend the resulting prepared event. @@ -58,46 +67,52 @@ const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been ca * {@link Client.addBreadcrumb}. * * @example - * class NodeClient extends BaseClient { + * class NodeClient extends BaseClient { * public constructor(options: NodeOptions) { - * super(NodeBackend, options); + * super(options); * } * * // ... * } */ -export abstract class BaseClient implements Client { - /** - * The backend used to physically interact in the environment. Usually, this - * will correspond to the client. When composing SDKs, however, the Backend - * from the root SDK will be used. - */ - protected readonly _backend: B; - +export abstract class BaseClient implements Client { /** Options passed to the SDK. */ protected readonly _options: O; /** The client Dsn, if specified in options. Without this Dsn, the SDK will be disabled. */ protected readonly _dsn?: DsnComponents; - /** Array of used integrations. */ + protected readonly _transport?: Transport; + + /** Array of set up integrations. */ protected _integrations: IntegrationIndex = {}; + /** Indicates whether this client's integrations have been set up. */ + protected _integrationsInitialized: boolean = false; + /** Number of calls being processed */ protected _numProcessing: number = 0; + /** Holds flushable */ + private _outcomes: { [key: string]: number } = {}; + /** * Initializes this client instance. * - * @param backendClass A constructor function to create the backend. * @param options Options for the client. */ - protected constructor(backendClass: BackendClass, options: O) { - this._backend = new backendClass(options); + protected constructor(options: O) { this._options = options; - if (options.dsn) { this._dsn = makeDsn(options.dsn); + const url = getEnvelopeEndpointWithUrlEncodedAuth(this._dsn, options.tunnel); + this._transport = options.transport({ + recordDroppedEvent: this.recordDroppedEvent.bind(this), + ...options.transportOptions, + url, + }); + } else { + IS_DEBUG_BUILD && logger.warn('No DSN provided, client will not do anything.'); } } @@ -115,8 +130,7 @@ export abstract class BaseClient implement let eventId: string | undefined = hint && hint.event_id; this._process( - this._getBackend() - .eventFromException(exception, hint) + this.eventFromException(exception, hint) .then(event => this._captureEvent(event, hint, scope)) .then(result => { eventId = result; @@ -129,12 +143,18 @@ export abstract class BaseClient implement /** * @inheritDoc */ - public captureMessage(message: string, level?: Severity, hint?: EventHint, scope?: Scope): string | undefined { + public captureMessage( + message: string, + // eslint-disable-next-line deprecation/deprecation + level?: Severity | SeverityLevel, + hint?: EventHint, + scope?: Scope, + ): string | undefined { let eventId: string | undefined = hint && hint.event_id; const promisedEvent = isPrimitive(message) - ? this._getBackend().eventFromMessage(String(message), level, hint) - : this._getBackend().eventFromException(message, hint); + ? this.eventFromMessage(String(message), level, hint) + : this.eventFromException(message, hint); this._process( promisedEvent @@ -180,9 +200,9 @@ export abstract class BaseClient implement if (!(typeof session.release === 'string')) { IS_DEBUG_BUILD && logger.warn('Discarded session because of missing or non-string release'); } else { - this._sendSession(session); + this.sendSession(session); // After sending, we set init false to indicate it's not the first occurrence - session.update({ init: false }); + updateSession(session, { init: false }); } } @@ -203,19 +223,22 @@ export abstract class BaseClient implement /** * @inheritDoc */ - public getTransport(): Transport { - return this._getBackend().getTransport(); + public getTransport(): Transport | undefined { + return this._transport; } /** * @inheritDoc */ public flush(timeout?: number): PromiseLike { - return this._isClientDoneProcessing(timeout).then(clientFinished => { - return this.getTransport() - .close(timeout) - .then(transportFlushed => clientFinished && transportFlushed); - }); + const transport = this._transport; + if (transport) { + return this._isClientDoneProcessing(timeout).then(clientFinished => { + return transport.flush(timeout).then(transportFlushed => clientFinished && transportFlushed); + }); + } else { + return resolvedSyncPromise(true); + } } /** @@ -232,11 +255,21 @@ export abstract class BaseClient implement * Sets up the integrations */ public setupIntegrations(): void { - if (this._isEnabled() && !this._integrations.initialized) { - this._integrations = setupIntegrations(this._options); + if (this._isEnabled() && !this._integrationsInitialized) { + this._integrations = setupIntegrations(this._options.integrations); + this._integrationsInitialized = true; } } + /** + * Gets an installed integration by its `id`. + * + * @returns The installed integration or `undefined` if no integration with that `id` was installed. + */ + public getIntegrationById(integrationId: string): Integration | undefined { + return this._integrations[integrationId]; + } + /** * @inheritDoc */ @@ -249,6 +282,53 @@ export abstract class BaseClient implement } } + /** + * @inheritDoc + */ + public sendEvent(event: Event, hint: EventHint = {}): void { + if (this._dsn) { + let env = createEventEnvelope(event, this._dsn, this._options._metadata, this._options.tunnel); + + for (const attachment of hint.attachments || []) { + env = addItemToEnvelope( + env, + createAttachmentEnvelopeItem(attachment, this._options.transportOptions?.textEncoder), + ); + } + + this._sendEnvelope(env); + } + } + + /** + * @inheritDoc + */ + public sendSession(session: Session | SessionAggregates): void { + if (this._dsn) { + const env = createSessionEnvelope(session, this._dsn, this._options._metadata, this._options.tunnel); + this._sendEnvelope(env); + } + } + + /** + * @inheritDoc + */ + public recordDroppedEvent(reason: EventDropReason, category: DataCategory): void { + if (this._options.sendClientReports) { + // We want to track each category (error, transaction, session) separately + // but still keep the distinction between different type of outcomes. + // We could use nested maps, but it's much easier to read and type this way. + // A correct type for map-based implementation if we want to go that route + // would be `Partial>>>` + // With typescript 4.1 we could even use template literal types + const key = `${reason}:${category}`; + IS_DEBUG_BUILD && logger.log(`Adding outcome: "${key}"`); + + // The following works because undefined + 1 === NaN and NaN is falsy + this._outcomes[key] = this._outcomes[key] + 1 || 1; + } + } + /** Updates existing session based on the provided event */ protected _updateSessionFromEvent(session: Session, event: Event): void { let crashed = false; @@ -274,7 +354,7 @@ export abstract class BaseClient implement const shouldUpdateAndSend = (sessionNonTerminal && session.errors === 0) || (sessionNonTerminal && crashed); if (shouldUpdateAndSend) { - session.update({ + updateSession(session, { ...(crashed && { status: 'crashed' }), errors: session.errors || Number(errored || crashed), }); @@ -282,11 +362,6 @@ export abstract class BaseClient implement } } - /** Deliver captured session to Sentry */ - protected _sendSession(session: Session): void { - this._getBackend().sendSession(session); - } - /** * Determine if the client is finished processing. Returns a promise because it will wait `timeout` ms before saying * "no" (resolving to `false`) in order to give the client a chance to potentially finish first. @@ -317,11 +392,6 @@ export abstract class BaseClient implement }); } - /** Returns the current backend. */ - protected _getBackend(): B { - return this._backend; - } - /** Determines whether this SDK is enabled and a valid Dsn is present. */ protected _isEnabled(): boolean { return this.getOptions().enabled !== false && this._dsn !== undefined; @@ -341,11 +411,11 @@ export abstract class BaseClient implement * @param scope A scope containing event metadata. * @returns A new event with more information. */ - protected _prepareEvent(event: Event, scope?: Scope, hint?: EventHint): PromiseLike { + protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { const { normalizeDepth = 3, normalizeMaxBreadth = 1_000 } = this.getOptions(); const prepared: Event = { ...event, - event_id: event.event_id || (hint && hint.event_id ? hint.event_id : uuid4()), + event_id: event.event_id || hint.event_id || uuid4(), timestamp: event.timestamp || dateTimestampInSeconds(), }; @@ -355,7 +425,7 @@ export abstract class BaseClient implement // If we have scope given to us, use it as the base for further modifications. // This allows us to prevent unnecessary copying of data if `captureContext` is not provided. let finalScope = scope; - if (hint && hint.captureContext) { + if (hint.captureContext) { finalScope = Scope.clone(finalScope).update(hint.captureContext); } @@ -365,6 +435,13 @@ export abstract class BaseClient implement // This should be the last thing called, since we want that // {@link Hub.addEventProcessor} gets the finished prepared event. if (finalScope) { + // Collect attachments from the hint and scope + const attachments = [...(hint.attachments || []), ...finalScope.getAttachments()]; + + if (attachments.length) { + hint.attachments = attachments; + } + // In case we have a hub we reassign it. result = finalScope.applyToEvent(prepared, hint); } @@ -486,27 +563,19 @@ export abstract class BaseClient implement } } - /** - * Tells the backend to send this event - * @param event The Sentry event to send - */ - protected _sendEvent(event: Event): void { - this._getBackend().sendEvent(event); - } - /** * Processes the event and logs an error in case of rejection * @param event * @param hint * @param scope */ - protected _captureEvent(event: Event, hint?: EventHint, scope?: Scope): PromiseLike { + protected _captureEvent(event: Event, hint: EventHint = {}, scope?: Scope): PromiseLike { return this._processEvent(event, hint, scope).then( finalEvent => { return finalEvent.event_id; }, reason => { - IS_DEBUG_BUILD && logger.error(reason); + IS_DEBUG_BUILD && logger.warn(reason); return undefined; }, ); @@ -525,19 +594,8 @@ export abstract class BaseClient implement * @param scope A scope containing event metadata. * @returns A SyncPromise that resolves with the event or rejects in case event was/will not be send. */ - protected _processEvent(event: Event, hint?: EventHint, scope?: Scope): PromiseLike { - // eslint-disable-next-line @typescript-eslint/unbound-method + protected _processEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { const { beforeSend, sampleRate } = this.getOptions(); - const transport = this.getTransport(); - - type RecordLostEvent = NonNullable; - type RecordLostEventParams = Parameters; - - function recordLostEvent(outcome: RecordLostEventParams[0], category: RecordLostEventParams[1]): void { - if (transport.recordLostEvent) { - transport.recordLostEvent(outcome, category); - } - } if (!this._isEnabled()) { return rejectedSyncPromise(new SentryError('SDK not enabled, will not capture event.')); @@ -548,7 +606,7 @@ export abstract class BaseClient implement // 0.0 === 0% events are sent // Sampling for transaction happens somewhere else if (!isTransaction && typeof sampleRate === 'number' && Math.random() > sampleRate) { - recordLostEvent('sample_rate', 'event'); + this.recordDroppedEvent('sample_rate', 'error'); return rejectedSyncPromise( new SentryError( `Discarding event because it's not included in the random sample (sampling rate = ${sampleRate})`, @@ -556,14 +614,14 @@ export abstract class BaseClient implement ); } - return this._prepareEvent(event, scope, hint) + return this._prepareEvent(event, hint, scope) .then(prepared => { if (prepared === null) { - recordLostEvent('event_processor', event.type || 'event'); + this.recordDroppedEvent('event_processor', event.type || 'error'); throw new SentryError('An event processor returned null, will not send event.'); } - const isInternalException = hint && hint.data && (hint.data as { __sentry__: boolean }).__sentry__ === true; + const isInternalException = hint.data && (hint.data as { __sentry__: boolean }).__sentry__ === true; if (isInternalException || isTransaction || !beforeSend) { return prepared; } @@ -573,16 +631,16 @@ export abstract class BaseClient implement }) .then(processedEvent => { if (processedEvent === null) { - recordLostEvent('before_send', event.type || 'event'); + this.recordDroppedEvent('before_send', event.type || 'error'); throw new SentryError('`beforeSend` returned `null`, will not send event.'); } - const session = scope && scope.getSession && scope.getSession(); + const session = scope && scope.getSession(); if (!isTransaction && session) { this._updateSessionFromEvent(session, processedEvent); } - this._sendEvent(processedEvent); + this.sendEvent(processedEvent, hint); return processedEvent; }) .then(null, reason => { @@ -618,6 +676,51 @@ export abstract class BaseClient implement }, ); } + + /** + * @inheritdoc + */ + protected _sendEnvelope(envelope: Envelope): void { + if (this._transport && this._dsn) { + this._transport.send(envelope).then(null, reason => { + IS_DEBUG_BUILD && logger.error('Error while sending event:', reason); + }); + } else { + IS_DEBUG_BUILD && logger.error('Transport disabled'); + } + } + + /** + * Clears outcomes on this client and returns them. + */ + protected _clearOutcomes(): Outcome[] { + const outcomes = this._outcomes; + this._outcomes = {}; + return Object.keys(outcomes).map(key => { + const [reason, category] = key.split(':') as [EventDropReason, DataCategory]; + return { + reason, + category, + quantity: outcomes[key], + }; + }); + } + + /** + * @inheritDoc + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + public abstract eventFromException(_exception: any, _hint?: EventHint): PromiseLike; + + /** + * @inheritDoc + */ + public abstract eventFromMessage( + _message: string, + // eslint-disable-next-line deprecation/deprecation + _level?: Severity | SeverityLevel, + _hint?: EventHint, + ): PromiseLike; } /** diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts new file mode 100644 index 000000000000..0fb974d66bc7 --- /dev/null +++ b/packages/core/src/envelope.ts @@ -0,0 +1,147 @@ +import { + DsnComponents, + Event, + EventEnvelope, + EventEnvelopeHeaders, + EventItem, + SdkInfo, + SdkMetadata, + Session, + SessionAggregates, + SessionEnvelope, + SessionItem, +} from '@sentry/types'; +import { createEnvelope, dropUndefinedKeys, dsnToString } from '@sentry/utils'; + +/** Extract sdk info from from the API metadata */ +function getSdkMetadataForEnvelopeHeader(metadata?: SdkMetadata): SdkInfo | undefined { + if (!metadata || !metadata.sdk) { + return; + } + const { name, version } = metadata.sdk; + return { name, version }; +} + +/** + * Apply SdkInfo (name, version, packages, integrations) to the corresponding event key. + * Merge with existing data if any. + **/ +function enhanceEventWithSdkInfo(event: Event, sdkInfo?: SdkInfo): Event { + if (!sdkInfo) { + return event; + } + event.sdk = event.sdk || {}; + event.sdk.name = event.sdk.name || sdkInfo.name; + event.sdk.version = event.sdk.version || sdkInfo.version; + event.sdk.integrations = [...(event.sdk.integrations || []), ...(sdkInfo.integrations || [])]; + event.sdk.packages = [...(event.sdk.packages || []), ...(sdkInfo.packages || [])]; + return event; +} + +/** Creates an envelope from a Session */ +export function createSessionEnvelope( + session: Session | SessionAggregates, + dsn: DsnComponents, + metadata?: SdkMetadata, + tunnel?: string, +): SessionEnvelope { + const sdkInfo = getSdkMetadataForEnvelopeHeader(metadata); + const envelopeHeaders = { + sent_at: new Date().toISOString(), + ...(sdkInfo && { sdk: sdkInfo }), + ...(!!tunnel && { dsn: dsnToString(dsn) }), + }; + + const envelopeItem: SessionItem = + 'aggregates' in session ? [{ type: 'sessions' }, session] : [{ type: 'session' }, session]; + + return createEnvelope(envelopeHeaders, [envelopeItem]); +} + +/** + * Create an Envelope from an event. + */ +export function createEventEnvelope( + event: Event, + dsn: DsnComponents, + metadata?: SdkMetadata, + tunnel?: string, +): EventEnvelope { + const sdkInfo = getSdkMetadataForEnvelopeHeader(metadata); + const eventType = event.type || 'event'; + + const { transactionSampling } = event.sdkProcessingMetadata || {}; + const { method: samplingMethod, rate: sampleRate } = transactionSampling || {}; + + // TODO: Below is a temporary hack in order to debug a serialization error - see + // https://github.com/getsentry/sentry-javascript/issues/2809, + // https://github.com/getsentry/sentry-javascript/pull/4425, and + // https://github.com/getsentry/sentry-javascript/pull/4574. + // + // TL; DR: even though we normalize all events (which should prevent this), something is causing `JSON.stringify` to + // throw a circular reference error. + // + // When it's time to remove it: + // 1. Delete everything between here and where the request object `req` is created, EXCEPT the line deleting + // `sdkProcessingMetadata` + // 2. Restore the original version of the request body, which is commented out + // 3. Search for either of the PR URLs above and pull out the companion hacks in the browser playwright tests and the + // baseClient tests in this package + enhanceEventWithSdkInfo(event, metadata && metadata.sdk); + event.tags = event.tags || {}; + event.extra = event.extra || {}; + + // In theory, all events should be marked as having gone through normalization and so + // we should never set this tag/extra data + if (!(event.sdkProcessingMetadata && event.sdkProcessingMetadata.baseClientNormalized)) { + event.tags.skippedNormalization = true; + event.extra.normalizeDepth = event.sdkProcessingMetadata ? event.sdkProcessingMetadata.normalizeDepth : 'unset'; + } + + // prevent this data from being sent to sentry + // TODO: This is NOT part of the hack - DO NOT DELETE + delete event.sdkProcessingMetadata; + + const envelopeHeaders = createEventEnvelopeHeaders(event, sdkInfo, tunnel, dsn); + + const eventItem: EventItem = [ + { + type: eventType, + sample_rates: [{ id: samplingMethod, rate: sampleRate }], + }, + event, + ]; + return createEnvelope(envelopeHeaders, [eventItem]); +} + +function createEventEnvelopeHeaders( + event: Event, + sdkInfo: SdkInfo | undefined, + tunnel: string | undefined, + dsn: DsnComponents, +): EventEnvelopeHeaders { + return { + event_id: event.event_id as string, + sent_at: new Date().toISOString(), + ...(sdkInfo && { sdk: sdkInfo }), + ...(!!tunnel && { dsn: dsnToString(dsn) }), + ...(event.type === 'transaction' && + event.contexts && + event.contexts.trace && { + // TODO: Grab this from baggage + trace: dropUndefinedKeys({ + // Trace context must be defined for transactions + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + trace_id: event.contexts!.trace.trace_id as string, + environment: event.environment, + release: event.release, + transaction: event.transaction, + user: event.user && { + id: event.user.id, + segment: event.user.segment, + }, + public_key: dsn.publicKey, + }), + }), + }; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f09d804d3bed..0d1a686b8e5e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,3 +1,5 @@ +export type { ClientClass } from './sdk'; + export { addBreadcrumb, captureException, @@ -12,32 +14,20 @@ export { setTags, setUser, withScope, -} from '@sentry/minimal'; -export { addGlobalEventProcessor, getCurrentHub, getHubFromCarrier, Hub, makeMain, Scope, Session } from '@sentry/hub'; -export { - // eslint-disable-next-line deprecation/deprecation - API, - APIDetails, - getEnvelopeEndpointWithUrlEncodedAuth, - getStoreEndpointWithUrlEncodedAuth, - getRequestHeaders, - initAPIDetails, - getReportDialogEndpoint, -} from './api'; + addGlobalEventProcessor, + getCurrentHub, + getHubFromCarrier, + Hub, + makeMain, + Scope, +} from '@sentry/hub'; +export { getEnvelopeEndpointWithUrlEncodedAuth, getReportDialogEndpoint } from './api'; export { BaseClient } from './baseclient'; -export { BackendClass, BaseBackend } from './basebackend'; -export { eventToSentryRequest, sessionToSentryRequest } from './request'; -export { initAndBind, ClientClass } from './sdk'; -export { NoopTransport } from './transports/noop'; -export { - BaseTransportOptions, - createTransport, - NewTransport, - TransportMakeRequestResponse, - TransportRequest, - TransportRequestExecutor, -} from './transports/base'; +export { initAndBind } from './sdk'; +export { createTransport } from './transports/base'; export { SDK_VERSION } from './version'; +export { getIntegrationsToSetup } from './integration'; +export { FunctionToString, InboundFilters } from './integrations'; import * as Integrations from './integrations'; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index cc694c815289..e45f4a7c09d7 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -1,6 +1,6 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/hub'; import { Integration, Options } from '@sentry/types'; -import { addNonEnumerableProperty, logger } from '@sentry/utils'; +import { logger } from '@sentry/utils'; import { IS_DEBUG_BUILD } from './flags'; @@ -9,7 +9,7 @@ export const installedIntegrations: string[] = []; /** Map of integrations assigned to a client */ export type IntegrationIndex = { [key: string]: Integration; -} & { initialized?: boolean }; +}; /** * @private @@ -54,31 +54,24 @@ export function getIntegrationsToSetup(options: Options): Integration[] { return integrations; } -/** Setup given integration */ -export function setupIntegration(integration: Integration): void { - if (installedIntegrations.indexOf(integration.name) !== -1) { - return; - } - integration.setupOnce(addGlobalEventProcessor, getCurrentHub); - installedIntegrations.push(integration.name); - IS_DEBUG_BUILD && logger.log(`Integration installed: ${integration.name}`); -} - /** * Given a list of integration instances this installs them all. When `withDefaults` is set to `true` then all default * integrations are added unless they were already provided before. * @param integrations array of integration instances * @param withDefault should enable default integrations */ -export function setupIntegrations(options: O): IntegrationIndex { - const integrations: IntegrationIndex = {}; - getIntegrationsToSetup(options).forEach(integration => { - integrations[integration.name] = integration; - setupIntegration(integration); +export function setupIntegrations(integrations: Integration[]): IntegrationIndex { + const integrationIndex: IntegrationIndex = {}; + + integrations.forEach(integration => { + integrationIndex[integration.name] = integration; + + if (installedIntegrations.indexOf(integration.name) === -1) { + integration.setupOnce(addGlobalEventProcessor, getCurrentHub); + installedIntegrations.push(integration.name); + IS_DEBUG_BUILD && logger.log(`Integration installed: ${integration.name}`); + } }); - // set the `initialized` flag so we don't run through the process again unecessarily; use `Object.defineProperty` - // because by default it creates a property which is nonenumerable, which we want since `initialized` shouldn't be - // considered a member of the index the way the actual integrations are - addNonEnumerableProperty(integrations, 'initialized', true); - return integrations; + + return integrationIndex; } diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index ff6c616c442d..0e9dc859a3f8 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -13,11 +13,6 @@ export interface InboundFiltersOptions { denyUrls: Array; ignoreErrors: Array; ignoreInternal: boolean; - - /** @deprecated use {@link InboundFiltersOptions.allowUrls} instead. */ - whitelistUrls: Array; - /** @deprecated use {@link InboundFiltersOptions.denyUrls} instead. */ - blacklistUrls: Array; } /** Inbound filters configurable by the user */ @@ -38,7 +33,7 @@ export class InboundFilters implements Integration { * @inheritDoc */ public setupOnce(addGlobalEventProcessor: (processor: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor((event: Event) => { + const eventProcess: EventProcessor = (event: Event) => { const hub = getCurrentHub(); if (hub) { const self = hub.getIntegration(InboundFilters); @@ -50,7 +45,10 @@ export class InboundFilters implements Integration { } } return event; - }); + }; + + eventProcess.id = this.name; + addGlobalEventProcessor(eventProcess); } } @@ -60,22 +58,8 @@ export function _mergeOptions( clientOptions: Partial = {}, ): Partial { return { - allowUrls: [ - // eslint-disable-next-line deprecation/deprecation - ...(internalOptions.whitelistUrls || []), - ...(internalOptions.allowUrls || []), - // eslint-disable-next-line deprecation/deprecation - ...(clientOptions.whitelistUrls || []), - ...(clientOptions.allowUrls || []), - ], - denyUrls: [ - // eslint-disable-next-line deprecation/deprecation - ...(internalOptions.blacklistUrls || []), - ...(internalOptions.denyUrls || []), - // eslint-disable-next-line deprecation/deprecation - ...(clientOptions.blacklistUrls || []), - ...(clientOptions.denyUrls || []), - ], + allowUrls: [...(internalOptions.allowUrls || []), ...(clientOptions.allowUrls || [])], + denyUrls: [...(internalOptions.denyUrls || []), ...(clientOptions.denyUrls || [])], ignoreErrors: [ ...(internalOptions.ignoreErrors || []), ...(clientOptions.ignoreErrors || []), @@ -189,9 +173,6 @@ function _getLastValidUrl(frames: StackFrame[] = []): string | null { function _getEventFilterUrl(event: Event): string | null { try { - if (event.stacktrace) { - return _getLastValidUrl(event.stacktrace.frames); - } let frames; try { // @ts-ignore we only care about frames if the whole thing here is defined diff --git a/packages/core/src/request.ts b/packages/core/src/request.ts deleted file mode 100644 index 2d65006272dd..000000000000 --- a/packages/core/src/request.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { - Event, - EventEnvelope, - EventItem, - SdkInfo, - SentryRequest, - SentryRequestType, - Session, - SessionAggregates, - SessionEnvelope, - SessionItem, -} from '@sentry/types'; -import { createEnvelope, dsnToString, normalize, serializeEnvelope } from '@sentry/utils'; - -import { APIDetails, getEnvelopeEndpointWithUrlEncodedAuth, getStoreEndpointWithUrlEncodedAuth } from './api'; - -/** Extract sdk info from from the API metadata */ -function getSdkMetadataForEnvelopeHeader(api: APIDetails): SdkInfo | undefined { - if (!api.metadata || !api.metadata.sdk) { - return; - } - const { name, version } = api.metadata.sdk; - return { name, version }; -} - -/** - * Apply SdkInfo (name, version, packages, integrations) to the corresponding event key. - * Merge with existing data if any. - **/ -function enhanceEventWithSdkInfo(event: Event, sdkInfo?: SdkInfo): Event { - if (!sdkInfo) { - return event; - } - event.sdk = event.sdk || {}; - event.sdk.name = event.sdk.name || sdkInfo.name; - event.sdk.version = event.sdk.version || sdkInfo.version; - event.sdk.integrations = [...(event.sdk.integrations || []), ...(sdkInfo.integrations || [])]; - event.sdk.packages = [...(event.sdk.packages || []), ...(sdkInfo.packages || [])]; - return event; -} - -/** Creates an envelope from a Session */ -export function createSessionEnvelope( - session: Session | SessionAggregates, - api: APIDetails, -): [SessionEnvelope, SentryRequestType] { - const sdkInfo = getSdkMetadataForEnvelopeHeader(api); - const envelopeHeaders = { - sent_at: new Date().toISOString(), - ...(sdkInfo && { sdk: sdkInfo }), - ...(!!api.tunnel && { dsn: dsnToString(api.dsn) }), - }; - - // I know this is hacky but we don't want to add `sessions` to request type since it's never rate limited - const type = 'aggregates' in session ? ('sessions' as SentryRequestType) : 'session'; - - // TODO (v7) Have to cast type because envelope items do not accept a `SentryRequestType` - const envelopeItem = [{ type } as { type: 'session' | 'sessions' }, session] as SessionItem; - const envelope = createEnvelope(envelopeHeaders, [envelopeItem]); - - return [envelope, type]; -} - -/** Creates a SentryRequest from a Session. */ -export function sessionToSentryRequest(session: Session | SessionAggregates, api: APIDetails): SentryRequest { - const [envelope, type] = createSessionEnvelope(session, api); - return { - body: serializeEnvelope(envelope), - type, - url: getEnvelopeEndpointWithUrlEncodedAuth(api.dsn, api.tunnel), - }; -} - -/** - * Create an Envelope from an event. Note that this is duplicated from below, - * but on purpose as this will be refactored in v7. - */ -export function createEventEnvelope(event: Event, api: APIDetails): EventEnvelope { - const sdkInfo = getSdkMetadataForEnvelopeHeader(api); - const eventType = event.type || 'event'; - - const { transactionSampling } = event.sdkProcessingMetadata || {}; - const { method: samplingMethod, rate: sampleRate } = transactionSampling || {}; - - // TODO: Below is a temporary hack in order to debug a serialization error - see - // https://github.com/getsentry/sentry-javascript/issues/2809, - // https://github.com/getsentry/sentry-javascript/pull/4425, and - // https://github.com/getsentry/sentry-javascript/pull/4574. - // - // TL; DR: even though we normalize all events (which should prevent this), something is causing `JSON.stringify` to - // throw a circular reference error. - // - // When it's time to remove it: - // 1. Delete everything between here and where the request object `req` is created, EXCEPT the line deleting - // `sdkProcessingMetadata` - // 2. Restore the original version of the request body, which is commented out - // 3. Search for either of the PR URLs above and pull out the companion hacks in the browser playwright tests and the - // baseClient tests in this package - enhanceEventWithSdkInfo(event, api.metadata.sdk); - event.tags = event.tags || {}; - event.extra = event.extra || {}; - - // In theory, all events should be marked as having gone through normalization and so - // we should never set this tag/extra data - if (!(event.sdkProcessingMetadata && event.sdkProcessingMetadata.baseClientNormalized)) { - event.tags.skippedNormalization = true; - event.extra.normalizeDepth = event.sdkProcessingMetadata ? event.sdkProcessingMetadata.normalizeDepth : 'unset'; - } - - // prevent this data from being sent to sentry - // TODO: This is NOT part of the hack - DO NOT DELETE - delete event.sdkProcessingMetadata; - - const envelopeHeaders = { - event_id: event.event_id as string, - sent_at: new Date().toISOString(), - ...(sdkInfo && { sdk: sdkInfo }), - ...(!!api.tunnel && { dsn: dsnToString(api.dsn) }), - }; - const eventItem: EventItem = [ - { - type: eventType, - sample_rates: [{ id: samplingMethod, rate: sampleRate }], - }, - event, - ]; - return createEnvelope(envelopeHeaders, [eventItem]); -} - -/** Creates a SentryRequest from an event. */ -export function eventToSentryRequest(event: Event, api: APIDetails): SentryRequest { - const sdkInfo = getSdkMetadataForEnvelopeHeader(api); - const eventType = event.type || 'event'; - const useEnvelope = eventType === 'transaction' || !!api.tunnel; - - const { transactionSampling } = event.sdkProcessingMetadata || {}; - const { method: samplingMethod, rate: sampleRate } = transactionSampling || {}; - - // TODO: Below is a temporary hack in order to debug a serialization error - see - // https://github.com/getsentry/sentry-javascript/issues/2809, - // https://github.com/getsentry/sentry-javascript/pull/4425, and - // https://github.com/getsentry/sentry-javascript/pull/4574. - // - // TL; DR: even though we normalize all events (which should prevent this), something is causing `JSON.stringify` to - // throw a circular reference error. - // - // When it's time to remove it: - // 1. Delete everything between here and where the request object `req` is created, EXCEPT the line deleting - // `sdkProcessingMetadata` - // 2. Restore the original version of the request body, which is commented out - // 3. Search for either of the PR URLs above and pull out the companion hacks in the browser playwright tests and the - // baseClient tests in this package - enhanceEventWithSdkInfo(event, api.metadata.sdk); - event.tags = event.tags || {}; - event.extra = event.extra || {}; - - // In theory, all events should be marked as having gone through normalization and so - // we should never set this tag/extra data - if (!(event.sdkProcessingMetadata && event.sdkProcessingMetadata.baseClientNormalized)) { - event.tags.skippedNormalization = true; - event.extra.normalizeDepth = event.sdkProcessingMetadata ? event.sdkProcessingMetadata.normalizeDepth : 'unset'; - } - - // prevent this data from being sent to sentry - // TODO: This is NOT part of the hack - DO NOT DELETE - delete event.sdkProcessingMetadata; - - let body; - try { - // 99.9% of events should get through just fine - no change in behavior for them - body = JSON.stringify(event); - } catch (err) { - // Record data about the error without replacing original event data, then force renormalization - event.tags.JSONStringifyError = true; - event.extra.JSONStringifyError = err; - try { - body = JSON.stringify(normalize(event)); - } catch (newErr) { - // At this point even renormalization hasn't worked, meaning something about the event data has gone very wrong. - // Time to cut our losses and record only the new error. With luck, even in the problematic cases we're trying to - // debug with this hack, we won't ever land here. - const innerErr = newErr as Error; - body = JSON.stringify({ - message: 'JSON.stringify error after renormalization', - // setting `extra: { innerErr }` here for some reason results in an empty object, so unpack manually - extra: { message: innerErr.message, stack: innerErr.stack }, - }); - } - } - - const req: SentryRequest = { - // this is the relevant line of code before the hack was added, to make it easy to undo said hack once we've solved - // the mystery - // body: JSON.stringify(sdkInfo ? enhanceEventWithSdkInfo(event, api.metadata.sdk) : event), - body, - type: eventType, - url: useEnvelope - ? getEnvelopeEndpointWithUrlEncodedAuth(api.dsn, api.tunnel) - : getStoreEndpointWithUrlEncodedAuth(api.dsn), - }; - - // https://develop.sentry.dev/sdk/envelopes/ - - // Since we don't need to manipulate envelopes nor store them, there is no - // exported concept of an Envelope with operations including serialization and - // deserialization. Instead, we only implement a minimal subset of the spec to - // serialize events inline here. - if (useEnvelope) { - const envelopeHeaders = { - event_id: event.event_id as string, - sent_at: new Date().toISOString(), - ...(sdkInfo && { sdk: sdkInfo }), - ...(!!api.tunnel && { dsn: dsnToString(api.dsn) }), - }; - const eventItem: EventItem = [ - { - type: eventType, - sample_rates: [{ id: samplingMethod, rate: sampleRate }], - }, - req.body, - ]; - const envelope = createEnvelope(envelopeHeaders, [eventItem]); - req.body = serializeEnvelope(envelope); - } - - return req; -} diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index 381a54839a0a..b549163a1ecc 100644 --- a/packages/core/src/sdk.ts +++ b/packages/core/src/sdk.ts @@ -1,11 +1,11 @@ import { getCurrentHub } from '@sentry/hub'; -import { Client, Options } from '@sentry/types'; +import { Client, ClientOptions } from '@sentry/types'; import { logger } from '@sentry/utils'; import { IS_DEBUG_BUILD } from './flags'; /** A class object that can instantiate Client objects. */ -export type ClientClass = new (options: O) => F; +export type ClientClass = new (options: O) => F; /** * Internal function to create a new SDK client instance. The client is @@ -14,7 +14,10 @@ export type ClientClass = new (options: O) * @param clientClass The client class to instantiate. * @param options Options to pass to the client. */ -export function initAndBind(clientClass: ClientClass, options: O): void { +export function initAndBind( + clientClass: ClientClass, + options: O, +): void { if (options.debug === true) { if (IS_DEBUG_BUILD) { logger.enable(); @@ -29,6 +32,7 @@ export function initAndBind(clientClass: Cl if (scope) { scope.update(options.initialScope); } + const client = new clientClass(options); hub.bindClient(client); } diff --git a/packages/core/src/transports/base.ts b/packages/core/src/transports/base.ts index 663a4c3c686f..fc83fb6b2855 100644 --- a/packages/core/src/transports/base.ts +++ b/packages/core/src/transports/base.ts @@ -1,83 +1,32 @@ -import { Envelope, EventStatus } from '@sentry/types'; import { - disabledUntil, - eventStatusFromHttpCode, - getEnvelopeType, + Envelope, + EnvelopeItem, + EventDropReason, + InternalBaseTransportOptions, + Transport, + TransportRequestExecutor, +} from '@sentry/types'; +import { + createEnvelope, + envelopeItemTypeToDataCategory, + forEachEnvelopeItem, isRateLimited, + logger, makePromiseBuffer, PromiseBuffer, RateLimits, - rejectedSyncPromise, resolvedSyncPromise, + SentryError, serializeEnvelope, updateRateLimits, } from '@sentry/utils'; -export const ERROR_TRANSPORT_CATEGORY = 'error'; - -export const TRANSACTION_TRANSPORT_CATEGORY = 'transaction'; - -export const ATTACHMENT_TRANSPORT_CATEGORY = 'attachment'; - -export const SESSION_TRANSPORT_CATEGORY = 'session'; - -type TransportCategory = - | typeof ERROR_TRANSPORT_CATEGORY - | typeof TRANSACTION_TRANSPORT_CATEGORY - | typeof ATTACHMENT_TRANSPORT_CATEGORY - | typeof SESSION_TRANSPORT_CATEGORY; - -export type TransportRequest = { - body: string; - category: TransportCategory; -}; - -export type TransportMakeRequestResponse = { - body?: string; - headers?: { - [key: string]: string | null; - 'x-sentry-rate-limits': string | null; - 'retry-after': string | null; - }; - reason?: string; - statusCode: number; -}; - -export type TransportResponse = { - status: EventStatus; - reason?: string; -}; - -interface InternalBaseTransportOptions { - bufferSize?: number; -} - -export interface BaseTransportOptions extends InternalBaseTransportOptions { - // url to send the event - // transport does not care about dsn specific - client should take care of - // parsing and figuring that out - url: string; -} - -// TODO: Move into Browser Transport -export interface BrowserTransportOptions extends BaseTransportOptions { - // options to pass into fetch request - fetchParams: Record; - headers?: Record; - sendClientReports?: boolean; -} - -export interface NewTransport { - send(request: Envelope): PromiseLike; - flush(timeout?: number): PromiseLike; -} - -export type TransportRequestExecutor = (request: TransportRequest) => PromiseLike; +import { IS_DEBUG_BUILD } from '../flags'; export const DEFAULT_TRANSPORT_BUFFER_SIZE = 30; /** - * Creates a `NewTransport` + * Creates an instance of a Sentry `Transport` * * @param options * @param makeRequest @@ -85,47 +34,68 @@ export const DEFAULT_TRANSPORT_BUFFER_SIZE = 30; export function createTransport( options: InternalBaseTransportOptions, makeRequest: TransportRequestExecutor, - buffer: PromiseBuffer = makePromiseBuffer(options.bufferSize || DEFAULT_TRANSPORT_BUFFER_SIZE), -): NewTransport { + buffer: PromiseBuffer = makePromiseBuffer(options.bufferSize || DEFAULT_TRANSPORT_BUFFER_SIZE), +): Transport { let rateLimits: RateLimits = {}; const flush = (timeout?: number): PromiseLike => buffer.drain(timeout); - function send(envelope: Envelope): PromiseLike { - const envCategory = getEnvelopeType(envelope); - const category = envCategory === 'event' ? 'error' : (envCategory as TransportCategory); - const request: TransportRequest = { - category, - body: serializeEnvelope(envelope), - }; - - // Don't add to buffer if transport is already rate-limited - if (isRateLimited(rateLimits, category)) { - return rejectedSyncPromise({ - status: 'rate_limit', - reason: getRateLimitReason(rateLimits, category), - }); + function send(envelope: Envelope): PromiseLike { + const filteredEnvelopeItems: EnvelopeItem[] = []; + + // Drop rate limited items from envelope + forEachEnvelopeItem(envelope, (item, type) => { + const envelopeItemDataCategory = envelopeItemTypeToDataCategory(type); + if (isRateLimited(rateLimits, envelopeItemDataCategory)) { + options.recordDroppedEvent('ratelimit_backoff', envelopeItemDataCategory); + } else { + filteredEnvelopeItems.push(item); + } + }); + + // Skip sending if envelope is empty after filtering out rate limited events + if (filteredEnvelopeItems.length === 0) { + return resolvedSyncPromise(); } - const requestTask = (): PromiseLike => - makeRequest(request).then(({ body, headers, reason, statusCode }): PromiseLike => { - const status = eventStatusFromHttpCode(statusCode); - if (headers) { - rateLimits = updateRateLimits(rateLimits, headers); - } - if (status === 'success') { - return resolvedSyncPromise({ status, reason }); - } - return rejectedSyncPromise({ - status, - reason: - reason || - body || - (status === 'rate_limit' ? getRateLimitReason(rateLimits, category) : 'Unknown transport error'), - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const filteredEnvelope: Envelope = createEnvelope(envelope[0], filteredEnvelopeItems as any); + + // Creates client report for each item in an envelope + const recordEnvelopeLoss = (reason: EventDropReason): void => { + forEachEnvelopeItem(filteredEnvelope, (_, type) => { + options.recordDroppedEvent(reason, envelopeItemTypeToDataCategory(type)); }); + }; - return buffer.add(requestTask); + const requestTask = (): PromiseLike => + makeRequest({ body: serializeEnvelope(filteredEnvelope, options.textEncoder) }).then( + response => { + // We don't want to throw on NOK responses, but we want to at least log them + if (response.statusCode !== undefined && (response.statusCode < 200 || response.statusCode >= 300)) { + IS_DEBUG_BUILD && logger.warn(`Sentry responded with status code ${response.statusCode} to sent event.`); + } + + rateLimits = updateRateLimits(rateLimits, response); + }, + error => { + IS_DEBUG_BUILD && logger.error('Failed while sending event:', error); + recordEnvelopeLoss('network_error'); + }, + ); + + return buffer.add(requestTask).then( + result => result, + error => { + if (error instanceof SentryError) { + IS_DEBUG_BUILD && logger.error('Skipped sending event due to full buffer'); + recordEnvelopeLoss('queue_overflow'); + return resolvedSyncPromise(); + } else { + throw error; + } + }, + ); } return { @@ -133,9 +103,3 @@ export function createTransport( flush, }; } - -function getRateLimitReason(rateLimits: RateLimits, category: TransportCategory): string { - return `Too many ${category} requests, backing off until: ${new Date( - disabledUntil(rateLimits, category), - ).toISOString()}`; -} diff --git a/packages/core/src/transports/noop.ts b/packages/core/src/transports/noop.ts deleted file mode 100644 index 8ea66c7b8a4e..000000000000 --- a/packages/core/src/transports/noop.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Event, Response, Transport } from '@sentry/types'; -import { resolvedSyncPromise } from '@sentry/utils'; - -/** Noop transport */ -export class NoopTransport implements Transport { - /** - * @inheritDoc - */ - public sendEvent(_: Event): PromiseLike { - return resolvedSyncPromise({ - reason: 'NoopTransport: Event has been skipped because no Dsn is configured.', - status: 'skipped', - }); - } - - /** - * @inheritDoc - */ - public close(_?: number): PromiseLike { - return resolvedSyncPromise(true); - } -} diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index b774b10aabdc..6a67171e89be 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -1 +1 @@ -export const SDK_VERSION = '6.19.7'; +export const SDK_VERSION = '7.0.0-rc.0'; diff --git a/packages/core/test/lib/api.test.ts b/packages/core/test/lib/api.test.ts index 14b44aed9602..0a5ba52196e7 100644 --- a/packages/core/test/lib/api.test.ts +++ b/packages/core/test/lib/api.test.ts @@ -1,41 +1,20 @@ /* eslint-disable deprecation/deprecation */ import { makeDsn } from '@sentry/utils'; -import { API, getReportDialogEndpoint, getRequestHeaders } from '../../src/api'; +import { getEnvelopeEndpointWithUrlEncodedAuth, getReportDialogEndpoint } from '../../src/api'; const ingestDsn = 'https://abc@xxxx.ingest.sentry.io:1234/subpath/123'; const dsnPublic = 'https://abc@sentry.io:1234/subpath/123'; -const legacyDsn = 'https://abc:123@sentry.io:1234/subpath/123'; const tunnel = 'https://hello.com/world'; -describe('API', () => { - test('getStoreEndpoint', () => { - expect(new API(dsnPublic).getStoreEndpointWithUrlEncodedAuth()).toEqual( - 'https://sentry.io:1234/subpath/api/123/store/?sentry_key=abc&sentry_version=7', - ); - expect(new API(dsnPublic).getStoreEndpoint()).toEqual('https://sentry.io:1234/subpath/api/123/store/'); - expect(new API(ingestDsn).getStoreEndpoint()).toEqual('https://xxxx.ingest.sentry.io:1234/subpath/api/123/store/'); - }); +const dsnPublicComponents = makeDsn(dsnPublic); +describe('API', () => { test('getEnvelopeEndpoint', () => { - expect(new API(dsnPublic).getEnvelopeEndpointWithUrlEncodedAuth()).toEqual( + expect(getEnvelopeEndpointWithUrlEncodedAuth(dsnPublicComponents)).toEqual( 'https://sentry.io:1234/subpath/api/123/envelope/?sentry_key=abc&sentry_version=7', ); - expect(new API(dsnPublic, {}, tunnel).getEnvelopeEndpointWithUrlEncodedAuth()).toEqual(tunnel); - }); - - test('getRequestHeaders', () => { - expect(getRequestHeaders(makeDsn(dsnPublic), 'a', '1.0')).toMatchObject({ - 'Content-Type': 'application/json', - 'X-Sentry-Auth': expect.stringMatching(/^Sentry sentry_version=\d, sentry_client=a\/1\.0, sentry_key=abc$/), - }); - - expect(getRequestHeaders(makeDsn(legacyDsn), 'a', '1.0')).toMatchObject({ - 'Content-Type': 'application/json', - 'X-Sentry-Auth': expect.stringMatching( - /^Sentry sentry_version=\d, sentry_client=a\/1\.0, sentry_key=abc, sentry_secret=123$/, - ), - }); + expect(getEnvelopeEndpointWithUrlEncodedAuth(dsnPublicComponents, tunnel)).toEqual(tunnel); }); describe('getReportDialogEndpoint', () => { @@ -117,14 +96,4 @@ describe('API', () => { }, ); }); - - test('getDsn', () => { - expect(new API(dsnPublic).getDsn().host).toEqual(makeDsn(dsnPublic).host); - expect(new API(dsnPublic).getDsn().path).toEqual(makeDsn(dsnPublic).path); - expect(new API(dsnPublic).getDsn().pass).toEqual(makeDsn(dsnPublic).pass); - expect(new API(dsnPublic).getDsn().port).toEqual(makeDsn(dsnPublic).port); - expect(new API(dsnPublic).getDsn().protocol).toEqual(makeDsn(dsnPublic).protocol); - expect(new API(dsnPublic).getDsn().projectId).toEqual(makeDsn(dsnPublic).projectId); - expect(new API(dsnPublic).getDsn().publicKey).toEqual(makeDsn(dsnPublic).publicKey); - }); }); diff --git a/packages/core/test/lib/attachments.test.ts b/packages/core/test/lib/attachments.test.ts new file mode 100644 index 000000000000..610518225793 --- /dev/null +++ b/packages/core/test/lib/attachments.test.ts @@ -0,0 +1,38 @@ +import { parseEnvelope } from '@sentry/utils/test/testutils'; +import { TextEncoder } from 'util'; + +import { createTransport } from '../../src/transports/base'; +import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; + +describe('Attachments', () => { + beforeEach(() => { + TestClient.sendEventCalled = undefined; + TestClient.instance = undefined; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('actually end up in envelope', async () => { + expect.assertions(4); + + const options = getDefaultTestClientOptions({ + dsn: 'https://username@domain/123', + enableSend: true, + transport: () => + createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, async req => { + const [, items] = parseEnvelope(req.body); + expect(items.length).toEqual(2); + // Second envelope item should be the attachment + expect(items[1][0]).toEqual({ type: 'attachment', length: 50000, filename: 'empty.bin' }); + expect(items[1][1]).toBeInstanceOf(Uint8Array); + expect((items[1][1] as Uint8Array).length).toEqual(50_000); + return {}; + }), + }); + + const client = new TestClient(options); + client.captureEvent({}, { attachments: [{ filename: 'empty.bin', data: new Uint8Array(50_000) }] }); + }); +}); diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index 5c978b0ee530..7a5431d1f9cb 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -1,18 +1,17 @@ -import { Hub, Scope, Session } from '@sentry/hub'; -import { Event, Span, Transport } from '@sentry/types'; +import { Hub, makeSession, Scope } from '@sentry/hub'; +import { Event, Span } from '@sentry/types'; import { dsnToString, logger, SentryError, SyncPromise } from '@sentry/utils'; import * as integrationModule from '../../src/integration'; -import { TestBackend } from '../mocks/backend'; -import { TestClient } from '../mocks/client'; +import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; import { TestIntegration } from '../mocks/integration'; -import { FakeTransport } from '../mocks/transport'; +import { makeFakeTransport } from '../mocks/transport'; const PUBLIC_DSN = 'https://username@domain/123'; // eslint-disable-next-line no-var declare var global: any; -const backendEventFromException = jest.spyOn(TestBackend.prototype, 'eventFromException'); +const clientEventFromException = jest.spyOn(TestClient.prototype, 'eventFromException'); const clientProcess = jest.spyOn(TestClient.prototype as any, '_process'); jest.mock('@sentry/utils', () => { @@ -55,8 +54,8 @@ jest.mock('@sentry/utils', () => { describe('BaseClient', () => { beforeEach(() => { - TestBackend.sendEventCalled = undefined; - TestBackend.instance = undefined; + TestClient.sendEventCalled = undefined; + TestClient.instance = undefined; }); afterEach(() => { @@ -67,14 +66,16 @@ describe('BaseClient', () => { test('returns the Dsn', () => { expect.assertions(1); - const client = new TestClient({ dsn: PUBLIC_DSN }); - expect(dsnToString(client.getDsn())).toBe(PUBLIC_DSN); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); + expect(dsnToString(client.getDsn()!)).toBe(PUBLIC_DSN); }); test('allows missing Dsn', () => { expect.assertions(1); - const client = new TestClient({}); + const options = getDefaultTestClientOptions(); + const client = new TestClient(options); expect(client.getDsn()).toBeUndefined(); }); @@ -82,7 +83,8 @@ describe('BaseClient', () => { test('throws with invalid Dsn', () => { expect.assertions(1); - expect(() => new TestClient({ dsn: 'abc' })).toThrow(SentryError); + const options = getDefaultTestClientOptions({ dsn: 'abc' }); + expect(() => new TestClient(options)).toThrow(SentryError); }); }); @@ -90,7 +92,7 @@ describe('BaseClient', () => { test('returns the options', () => { expect.assertions(1); - const options = { dsn: PUBLIC_DSN, test: true }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, test: true }); const client = new TestClient(options); expect(client.getOptions()).toEqual(options); @@ -98,14 +100,13 @@ describe('BaseClient', () => { }); describe('getTransport()', () => { - test('returns the transport from backend', () => { - expect.assertions(2); + test('returns undefined when no dsn is set', () => { + expect.assertions(1); - const options = { dsn: PUBLIC_DSN, transport: FakeTransport }; + const options = getDefaultTestClientOptions({}); const client = new TestClient(options); - expect(client.getTransport()).toBeInstanceOf(FakeTransport); - expect(TestBackend.instance!.getTransport()).toBe(client.getTransport()); + expect(client.getTransport()).toBeUndefined(); }); }); @@ -113,7 +114,8 @@ describe('BaseClient', () => { test('adds a breadcrumb', () => { expect.assertions(1); - const client = new TestClient({}); + const options = getDefaultTestClientOptions({}); + const client = new TestClient(options); const scope = new Scope(); const hub = new Hub(client, scope); @@ -126,7 +128,8 @@ describe('BaseClient', () => { test('adds a timestamp to new breadcrumbs', () => { expect.assertions(1); - const client = new TestClient({}); + const options = getDefaultTestClientOptions({}); + const client = new TestClient(options); const scope = new Scope(); const hub = new Hub(client, scope); @@ -139,7 +142,8 @@ describe('BaseClient', () => { test('discards breadcrumbs beyond maxBreadcrumbs', () => { expect.assertions(2); - const client = new TestClient({ maxBreadcrumbs: 1 }); + const options = getDefaultTestClientOptions({ maxBreadcrumbs: 1 }); + const client = new TestClient(options); const scope = new Scope(); const hub = new Hub(client, scope); @@ -153,7 +157,8 @@ describe('BaseClient', () => { test('allows concurrent updates', () => { expect.assertions(1); - const client = new TestClient({}); + const options = getDefaultTestClientOptions({}); + const client = new TestClient(options); const scope = new Scope(); const hub = new Hub(client, scope); @@ -167,7 +172,8 @@ describe('BaseClient', () => { expect.assertions(1); const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); - const client = new TestClient({ beforeBreadcrumb }); + const options = getDefaultTestClientOptions({ beforeBreadcrumb }); + const client = new TestClient(options); const scope = new Scope(); const hub = new Hub(client, scope); @@ -180,7 +186,8 @@ describe('BaseClient', () => { expect.assertions(1); const beforeBreadcrumb = jest.fn(() => ({ message: 'changed' })); - const client = new TestClient({ beforeBreadcrumb }); + const options = getDefaultTestClientOptions({ beforeBreadcrumb }); + const client = new TestClient(options); const scope = new Scope(); const hub = new Hub(client, scope); @@ -193,7 +200,8 @@ describe('BaseClient', () => { expect.assertions(1); const beforeBreadcrumb = jest.fn(() => null); - const client = new TestClient({ beforeBreadcrumb }); + const options = getDefaultTestClientOptions({ beforeBreadcrumb }); + const client = new TestClient(options); const scope = new Scope(); const hub = new Hub(client, scope); @@ -206,7 +214,8 @@ describe('BaseClient', () => { expect.assertions(2); const beforeBreadcrumb = jest.fn((breadcrumb, hint) => ({ ...breadcrumb, data: hint.data })); - const client = new TestClient({ beforeBreadcrumb }); + const options = getDefaultTestClientOptions({ beforeBreadcrumb }); + const client = new TestClient(options); const scope = new Scope(); const hub = new Hub(client, scope); @@ -219,11 +228,12 @@ describe('BaseClient', () => { describe('captureException', () => { test('captures and sends exceptions', () => { - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); client.captureException(new Error('test exception')); - expect(TestBackend.instance!.event).toEqual( + expect(TestClient.instance!.event).toEqual( expect.objectContaining({ environment: 'production', event_id: '42', @@ -241,7 +251,8 @@ describe('BaseClient', () => { }); test('allows for providing explicit scope', () => { - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); const scope = new Scope(); scope.setExtra('foo', 'wat'); @@ -257,7 +268,7 @@ describe('BaseClient', () => { scope, ); - expect(TestBackend.instance!.event).toEqual( + expect(TestClient.instance!.event).toEqual( expect.objectContaining({ extra: { bar: 'wat', @@ -268,7 +279,8 @@ describe('BaseClient', () => { }); test('allows for clearing data from existing scope if explicit one does so in a callback function', () => { - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); const scope = new Scope(); scope.setExtra('foo', 'wat'); @@ -284,7 +296,7 @@ describe('BaseClient', () => { scope, ); - expect(TestBackend.instance!.event).toEqual( + expect(TestClient.instance!.event).toEqual( expect.objectContaining({ extra: { bar: 'wat', @@ -302,29 +314,31 @@ describe('BaseClient', () => { // already-seen check to work . Any primitive which is passed without being wrapped will be captured each time it // is encountered, so this test doesn't apply. ])("doesn't capture the same exception twice - %s", (_name: string, thrown: any) => { - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); expect(thrown.__sentry_captured__).toBeUndefined(); client.captureException(thrown); expect(thrown.__sentry_captured__).toBe(true); - expect(backendEventFromException).toHaveBeenCalledTimes(1); + expect(clientEventFromException).toHaveBeenCalledTimes(1); client.captureException(thrown); // `captureException` should bail right away this second time around and not get as far as calling this again - expect(backendEventFromException).toHaveBeenCalledTimes(1); + expect(clientEventFromException).toHaveBeenCalledTimes(1); }); }); describe('captureMessage', () => { test('captures and sends messages', () => { - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); client.captureMessage('test message'); - expect(TestBackend.instance!.event).toEqual( + expect(TestClient.instance!.event).toEqual( expect.objectContaining({ environment: 'production', event_id: '42', @@ -336,8 +350,9 @@ describe('BaseClient', () => { }); test('should call eventFromException if input to captureMessage is not a primitive', () => { - const client = new TestClient({ dsn: PUBLIC_DSN }); - const spy = jest.spyOn(TestBackend.instance!, 'eventFromException'); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); + const spy = jest.spyOn(TestClient.instance!, 'eventFromException'); client.captureMessage('foo'); client.captureMessage(null as any); @@ -354,7 +369,8 @@ describe('BaseClient', () => { }); test('allows for providing explicit scope', () => { - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); const scope = new Scope(); scope.setExtra('foo', 'wat'); @@ -371,7 +387,7 @@ describe('BaseClient', () => { scope, ); - expect(TestBackend.instance!.event).toEqual( + expect(TestClient.instance!.event).toEqual( expect.objectContaining({ extra: { bar: 'wat', @@ -387,23 +403,25 @@ describe('BaseClient', () => { test('skips when disabled', () => { expect.assertions(1); - const client = new TestClient({ enabled: false, dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ enabled: false, dsn: PUBLIC_DSN }); + const client = new TestClient(options); const scope = new Scope(); client.captureEvent({}, undefined, scope); - expect(TestBackend.instance!.event).toBeUndefined(); + expect(TestClient.instance!.event).toBeUndefined(); }); test('skips without a Dsn', () => { expect.assertions(1); - const client = new TestClient({}); + const options = getDefaultTestClientOptions({}); + const client = new TestClient(options); const scope = new Scope(); client.captureEvent({}, undefined, scope); - expect(TestBackend.instance!.event).toBeUndefined(); + expect(TestClient.instance!.event).toBeUndefined(); }); test.each([ @@ -415,10 +433,11 @@ describe('BaseClient', () => { // already-seen check to work . Any primitive which is passed without being wrapped will be captured each time it // is encountered, so this test doesn't apply. ])("doesn't capture an event wrapping the same exception twice - %s", (_name: string, thrown: any) => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); // Note: this is the same test as in `describe(captureException)`, except with the exception already wrapped in a // hint and accompanying an event. Duplicated here because some methods skip `captureException` and go straight to // `captureEvent`. - const client = new TestClient({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); const event = { exception: { values: [{ type: 'Error', message: 'Will I get caught twice?' }] } }; const hint = { originalException: thrown }; @@ -438,13 +457,14 @@ describe('BaseClient', () => { test('sends an event', () => { expect.assertions(2); - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); const scope = new Scope(); client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!.message).toBe('message'); - expect(TestBackend.instance!.event).toEqual( + expect(TestClient.instance!.event!.message).toBe('message'); + expect(TestClient.instance!.event).toEqual( expect.objectContaining({ environment: 'production', event_id: '42', @@ -457,13 +477,14 @@ describe('BaseClient', () => { test('does not overwrite existing timestamp', () => { expect.assertions(2); - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); const scope = new Scope(); client.captureEvent({ message: 'message', timestamp: 1234 }, undefined, scope); - expect(TestBackend.instance!.event!.message).toBe('message'); - expect(TestBackend.instance!.event).toEqual( + expect(TestClient.instance!.event!.message).toBe('message'); + expect(TestClient.instance!.event).toEqual( expect.objectContaining({ environment: 'production', event_id: '42', @@ -476,12 +497,13 @@ describe('BaseClient', () => { test('adds event_id from hint if available', () => { expect.assertions(1); - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); const scope = new Scope(); client.captureEvent({ message: 'message' }, { event_id: 'wat' }, scope); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ environment: 'production', event_id: 'wat', @@ -494,14 +516,13 @@ describe('BaseClient', () => { test('sets default environment to `production` if none provided', () => { expect.assertions(1); - const client = new TestClient({ - dsn: PUBLIC_DSN, - }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); const scope = new Scope(); client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ environment: 'production', event_id: '42', @@ -514,15 +535,13 @@ describe('BaseClient', () => { test('adds the configured environment', () => { expect.assertions(1); - const client = new TestClient({ - dsn: PUBLIC_DSN, - environment: 'env', - }); + const options = getDefaultTestClientOptions({ environment: 'env', dsn: PUBLIC_DSN }); + const client = new TestClient(options); const scope = new Scope(); client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ environment: 'env', event_id: '42', @@ -535,15 +554,13 @@ describe('BaseClient', () => { test('allows for environment to be explicitly set to falsy value', () => { expect.assertions(1); - const client = new TestClient({ - dsn: PUBLIC_DSN, - environment: undefined, - }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, environment: undefined }); + const client = new TestClient(options); const scope = new Scope(); client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ environment: undefined, event_id: '42', @@ -556,15 +573,13 @@ describe('BaseClient', () => { test('adds the configured release', () => { expect.assertions(1); - const client = new TestClient({ - dsn: PUBLIC_DSN, - release: 'v1.0.0', - }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, release: 'v1.0.0' }); + const client = new TestClient(options); const scope = new Scope(); client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ environment: 'production', event_id: '42', @@ -578,22 +593,24 @@ describe('BaseClient', () => { test('adds breadcrumbs', () => { expect.assertions(4); - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); const scope = new Scope(); scope.addBreadcrumb({ message: 'breadcrumb' }, 100); client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!).toHaveProperty('event_id', '42'); - expect(TestBackend.instance!.event!).toHaveProperty('message', 'message'); - expect(TestBackend.instance!.event!).toHaveProperty('breadcrumbs'); - expect(TestBackend.instance!.event!.breadcrumbs![0]).toHaveProperty('message', 'breadcrumb'); + expect(TestClient.instance!.event!).toHaveProperty('event_id', '42'); + expect(TestClient.instance!.event!).toHaveProperty('message', 'message'); + expect(TestClient.instance!.event!).toHaveProperty('breadcrumbs'); + expect(TestClient.instance!.event!.breadcrumbs![0]).toHaveProperty('message', 'breadcrumb'); }); test('limits previously saved breadcrumbs', () => { expect.assertions(2); - const client = new TestClient({ dsn: PUBLIC_DSN, maxBreadcrumbs: 1 }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, maxBreadcrumbs: 1 }); + const client = new TestClient(options); const scope = new Scope(); const hub = new Hub(client, scope); hub.addBreadcrumb({ message: '1' }); @@ -601,14 +618,15 @@ describe('BaseClient', () => { client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!.breadcrumbs).toHaveLength(1); - expect(TestBackend.instance!.event!.breadcrumbs![0].message).toEqual('2'); + expect(TestClient.instance!.event!.breadcrumbs).toHaveLength(1); + expect(TestClient.instance!.event!.breadcrumbs![0].message).toEqual('2'); }); test('adds context data', () => { expect.assertions(1); - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); const scope = new Scope(); scope.setExtra('b', 'b'); scope.setTag('a', 'a'); @@ -616,7 +634,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ environment: 'production', event_id: '42', @@ -632,13 +650,14 @@ describe('BaseClient', () => { test('adds fingerprint', () => { expect.assertions(1); - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); const scope = new Scope(); scope.setFingerprint(['abcd']); client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ environment: 'production', event_id: '42', @@ -650,12 +669,13 @@ describe('BaseClient', () => { }); test('adds installed integrations to sdk info', () => { - const client = new TestClient({ dsn: PUBLIC_DSN, integrations: [new TestIntegration()] }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, integrations: [new TestIntegration()] }); + const client = new TestClient(options); client.setupIntegrations(); client.captureEvent({ message: 'message' }); - expect(TestBackend.instance!.event!.sdk).toEqual({ + expect(TestClient.instance!.event!.sdk).toEqual({ integrations: ['TestIntegration'], }); }); @@ -663,7 +683,8 @@ describe('BaseClient', () => { test('normalizes event with default depth of 3', () => { expect.assertions(1); - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); const fourLevelsObject = { a: { b: { @@ -698,7 +719,7 @@ describe('BaseClient', () => { user: fourLevelsObject, }); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ breadcrumbs: [normalizedBreadcrumb, normalizedBreadcrumb, normalizedBreadcrumb], contexts: normalizedObject, @@ -714,10 +735,8 @@ describe('BaseClient', () => { test('normalization respects `normalizeDepth` option', () => { expect.assertions(1); - const client = new TestClient({ - dsn: PUBLIC_DSN, - normalizeDepth: 2, - }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, normalizeDepth: 2 }); + const client = new TestClient(options); const fourLevelsObject = { a: { b: { @@ -749,7 +768,7 @@ describe('BaseClient', () => { user: fourLevelsObject, }); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ breadcrumbs: [normalizedBreadcrumb, normalizedBreadcrumb, normalizedBreadcrumb], contexts: normalizedObject, @@ -765,10 +784,8 @@ describe('BaseClient', () => { test('skips normalization when `normalizeDepth: 0`', () => { expect.assertions(1); - const client = new TestClient({ - dsn: PUBLIC_DSN, - normalizeDepth: 0, - }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, normalizeDepth: 0 }); + const client = new TestClient(options); const fourLevelsObject = { a: { b: { @@ -805,7 +822,7 @@ describe('BaseClient', () => { user: fourLevelsObject, }); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ breadcrumbs: [normalizedBreadcrumb, normalizedBreadcrumb, normalizedBreadcrumb], contexts: normalizedObject, @@ -821,7 +838,8 @@ describe('BaseClient', () => { test('normalization applies to Transaction and Span consistently', () => { expect.assertions(1); - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); const transaction: Event = { contexts: { trace: { @@ -871,7 +889,7 @@ describe('BaseClient', () => { // event. The code can be restored to its original form (the commented-out line below) once that hack is // removed. See https://github.com/getsentry/sentry-javascript/pull/4425 and // https://github.com/getsentry/sentry-javascript/pull/4574 - const capturedEvent = TestBackend.instance!.event!; + const capturedEvent = TestClient.instance!.event!; if (capturedEvent.sdkProcessingMetadata?.normalizeDepth) { if (Object.keys(capturedEvent.sdkProcessingMetadata).length === 1) { delete capturedEvent.sdkProcessingMetadata; @@ -888,44 +906,47 @@ describe('BaseClient', () => { } expect(capturedEvent).toEqual(normalizedTransaction); - // expect(TestBackend.instance!.event!).toEqual(normalizedTransaction); + // expect(TestClient.instance!.event!).toEqual(normalizedTransaction); }); test('calls beforeSend and uses original event without any changes', () => { expect.assertions(1); const beforeSend = jest.fn(event => event); - const client = new TestClient({ dsn: PUBLIC_DSN, beforeSend }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); + const client = new TestClient(options); client.captureEvent({ message: 'hello' }); - expect(TestBackend.instance!.event!.message).toBe('hello'); + expect(TestClient.instance!.event!.message).toBe('hello'); }); test('calls beforeSend and uses the new one', () => { expect.assertions(1); const beforeSend = jest.fn(() => ({ message: 'changed1' })); - const client = new TestClient({ dsn: PUBLIC_DSN, beforeSend }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); + const client = new TestClient(options); client.captureEvent({ message: 'hello' }); - expect(TestBackend.instance!.event!.message).toBe('changed1'); + expect(TestClient.instance!.event!.message).toBe('changed1'); }); test('calls beforeSend and discards the event', () => { expect.assertions(3); const beforeSend = jest.fn(() => null); - const client = new TestClient({ dsn: PUBLIC_DSN, beforeSend }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); + const client = new TestClient(options); const captureExceptionSpy = jest.spyOn(client, 'captureException'); - const loggerErrorSpy = jest.spyOn(logger, 'error'); + const loggerWarnSpy = jest.spyOn(logger, 'warn'); client.captureEvent({ message: 'hello' }); - expect(TestBackend.instance!.event).toBeUndefined(); + expect(TestClient.instance!.event).toBeUndefined(); expect(captureExceptionSpy).not.toBeCalled(); - expect(loggerErrorSpy).toBeCalledWith(new SentryError('`beforeSend` returned `null`, will not send event.')); + expect(loggerWarnSpy).toBeCalledWith(new SentryError('`beforeSend` returned `null`, will not send event.')); }); test('calls beforeSend and log info about invalid return value', () => { @@ -935,13 +956,14 @@ describe('BaseClient', () => { for (const val of invalidValues) { const beforeSend = jest.fn(() => val); // @ts-ignore we need to test regular-js behavior - const client = new TestClient({ dsn: PUBLIC_DSN, beforeSend }); - const loggerErrorSpy = jest.spyOn(logger, 'error'); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); + const client = new TestClient(options); + const loggerWarnSpy = jest.spyOn(logger, 'warn'); client.captureEvent({ message: 'hello' }); - expect(TestBackend.instance!.event).toBeUndefined(); - expect(loggerErrorSpy).toBeCalledWith( + expect(TestClient.instance!.event).toBeUndefined(); + expect(loggerWarnSpy).toBeCalledWith( new SentryError('`beforeSend` method has to return `null` or a valid event.'), ); } @@ -959,12 +981,13 @@ describe('BaseClient', () => { }, 1); }), ); - const client = new TestClient({ dsn: PUBLIC_DSN, beforeSend }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); + const client = new TestClient(options); client.captureEvent({ message: 'hello' }); jest.runOnlyPendingTimers(); - TestBackend.sendEventCalled = (event: Event) => { + TestClient.sendEventCalled = (event: Event) => { expect(event.message).toBe('hello'); }; @@ -987,12 +1010,13 @@ describe('BaseClient', () => { }, 1); }), ); - const client = new TestClient({ dsn: PUBLIC_DSN, beforeSend }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); + const client = new TestClient(options); client.captureEvent({ message: 'hello' }); jest.runOnlyPendingTimers(); - TestBackend.sendEventCalled = (event: Event) => { + TestClient.sendEventCalled = (event: Event) => { expect(event.message).toBe('changed2'); }; @@ -1015,91 +1039,86 @@ describe('BaseClient', () => { }); }), ); - const client = new TestClient({ dsn: PUBLIC_DSN, beforeSend }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); + const client = new TestClient(options); client.captureEvent({ message: 'hello' }); jest.runAllTimers(); - expect(TestBackend.instance!.event).toBeUndefined(); + expect(TestClient.instance!.event).toBeUndefined(); }); test('beforeSend gets access to a hint as a second argument', () => { expect.assertions(2); const beforeSend = jest.fn((event, hint) => ({ ...event, data: hint.data })); - const client = new TestClient({ dsn: PUBLIC_DSN, beforeSend }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); + const client = new TestClient(options); client.captureEvent({ message: 'hello' }, { data: 'someRandomThing' }); - expect(TestBackend.instance!.event!.message).toBe('hello'); - expect((TestBackend.instance!.event! as any).data).toBe('someRandomThing'); + expect(TestClient.instance!.event!.message).toBe('hello'); + expect((TestClient.instance!.event! as any).data).toBe('someRandomThing'); }); test('beforeSend records dropped events', () => { expect.assertions(1); - const client = new TestClient({ - dsn: PUBLIC_DSN, - beforeSend() { - return null; - }, - }); - const recordLostEventSpy = jest.fn(); - jest.spyOn(client, 'getTransport').mockImplementationOnce( - () => - ({ - recordLostEvent: recordLostEventSpy, - } as any as Transport), + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + beforeSend() { + return null; + }, + }), ); + const recordLostEventSpy = jest.spyOn(client, 'recordDroppedEvent'); + client.captureEvent({ message: 'hello' }, {}); - expect(recordLostEventSpy).toHaveBeenCalledWith('before_send', 'event'); + expect(recordLostEventSpy).toHaveBeenCalledWith('before_send', 'error'); }); test('eventProcessor can drop the even when it returns null', () => { expect.assertions(3); - const client = new TestClient({ dsn: PUBLIC_DSN }); + const client = new TestClient(getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); const captureExceptionSpy = jest.spyOn(client, 'captureException'); - const loggerErrorSpy = jest.spyOn(logger, 'error'); + const loggerWarnSpy = jest.spyOn(logger, 'warn'); const scope = new Scope(); scope.addEventProcessor(() => null); client.captureEvent({ message: 'hello' }, {}, scope); - expect(TestBackend.instance!.event).toBeUndefined(); + expect(TestClient.instance!.event).toBeUndefined(); expect(captureExceptionSpy).not.toBeCalled(); - expect(loggerErrorSpy).toBeCalledWith(new SentryError('An event processor returned null, will not send event.')); + expect(loggerWarnSpy).toBeCalledWith(new SentryError('An event processor returned null, will not send event.')); }); test('eventProcessor records dropped events', () => { expect.assertions(1); - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); - const recordLostEventSpy = jest.fn(); - jest.spyOn(client, 'getTransport').mockImplementationOnce( - () => - ({ - recordLostEvent: recordLostEventSpy, - } as any as Transport), - ); + const recordLostEventSpy = jest.spyOn(client, 'recordDroppedEvent'); const scope = new Scope(); scope.addEventProcessor(() => null); client.captureEvent({ message: 'hello' }, {}, scope); - expect(recordLostEventSpy).toHaveBeenCalledWith('event_processor', 'event'); + expect(recordLostEventSpy).toHaveBeenCalledWith('event_processor', 'error'); }); test('eventProcessor sends an event and logs when it crashes', () => { expect.assertions(3); - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); const captureExceptionSpy = jest.spyOn(client, 'captureException'); - const loggerErrorSpy = jest.spyOn(logger, 'error'); + const loggerWarnSpy = jest.spyOn(logger, 'warn'); const scope = new Scope(); const exception = new Error('sorry'); scope.addEventProcessor(() => { @@ -1108,14 +1127,14 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }, {}, scope); - expect(TestBackend.instance!.event!.exception!.values![0]).toStrictEqual({ type: 'Error', value: 'sorry' }); + expect(TestClient.instance!.event!.exception!.values![0]).toStrictEqual({ type: 'Error', value: 'sorry' }); expect(captureExceptionSpy).toBeCalledWith(exception, { data: { __sentry__: true, }, originalException: exception, }); - expect(loggerErrorSpy).toBeCalledWith( + expect(loggerWarnSpy).toBeCalledWith( new SentryError( `Event processing pipeline threw an error, original event will not be sent. Details have been sent as a new event.\nReason: ${exception}`, ), @@ -1125,21 +1144,13 @@ describe('BaseClient', () => { test('records events dropped due to sampleRate', () => { expect.assertions(1); - const client = new TestClient({ - dsn: PUBLIC_DSN, - sampleRate: 0, - }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, sampleRate: 0 }); + const client = new TestClient(options); - const recordLostEventSpy = jest.fn(); - jest.spyOn(client, 'getTransport').mockImplementationOnce( - () => - ({ - recordLostEvent: recordLostEventSpy, - } as any as Transport), - ); + const recordLostEventSpy = jest.spyOn(client, 'recordDroppedEvent'); client.captureEvent({ message: 'hello' }, {}); - expect(recordLostEventSpy).toHaveBeenCalledWith('sample_rate', 'event'); + expect(recordLostEventSpy).toHaveBeenCalledWith('sample_rate', 'error'); }); }); @@ -1151,10 +1162,8 @@ describe('BaseClient', () => { test('setup each one of them on setupIntegration call', () => { expect.assertions(2); - const client = new TestClient({ - dsn: PUBLIC_DSN, - integrations: [new TestIntegration()], - }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, integrations: [new TestIntegration()] }); + const client = new TestClient(options); client.setupIntegrations(); expect(Object.keys((client as any)._integrations).length).toBe(1); @@ -1164,9 +1173,8 @@ describe('BaseClient', () => { test('skips installation if DSN is not provided', () => { expect.assertions(2); - const client = new TestClient({ - integrations: [new TestIntegration()], - }); + const options = getDefaultTestClientOptions({ integrations: [new TestIntegration()] }); + const client = new TestClient(options); client.setupIntegrations(); expect(Object.keys((client as any)._integrations).length).toBe(0); @@ -1176,11 +1184,12 @@ describe('BaseClient', () => { test('skips installation if enabled is set to false', () => { expect.assertions(2); - const client = new TestClient({ + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enabled: false, integrations: [new TestIntegration()], }); + const client = new TestClient(options); client.setupIntegrations(); expect(Object.keys((client as any)._integrations).length).toBe(0); @@ -1190,10 +1199,8 @@ describe('BaseClient', () => { test('skips installation if integrations are already installed', () => { expect.assertions(4); - const client = new TestClient({ - dsn: PUBLIC_DSN, - integrations: [new TestIntegration()], - }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, integrations: [new TestIntegration()] }); + const client = new TestClient(options); // note: not the `Client` method `setupIntegrations`, but the free-standing function which that method calls const setupIntegrationsHelper = jest.spyOn(integrationModule, 'setupIntegrations'); @@ -1214,62 +1221,60 @@ describe('BaseClient', () => { describe('flush/close', () => { test('flush', async () => { jest.useRealTimers(); - expect.assertions(5); + expect.assertions(4); - const client = new TestClient({ - dsn: PUBLIC_DSN, - enableSend: true, - transport: FakeTransport, - }); + const { makeTransport, getSendCalled, getSentCount, delay } = makeFakeTransport(1); - const delay = 1; - const transportInstance = client.getTransport() as FakeTransport; - transportInstance.delay = delay; + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + enableSend: true, + transport: makeTransport, + }), + ); client.captureMessage('test'); - expect(transportInstance).toBeInstanceOf(FakeTransport); - expect(transportInstance.sendCalled).toEqual(1); - expect(transportInstance.sentCount).toEqual(0); + expect(getSendCalled()).toEqual(1); + expect(getSentCount()).toEqual(0); await client.flush(delay); - expect(transportInstance.sentCount).toEqual(1); - expect(transportInstance.sendCalled).toEqual(1); + expect(getSentCount()).toEqual(1); + expect(getSendCalled()).toEqual(1); }); test('flush with some events being processed async', async () => { jest.useRealTimers(); - expect.assertions(5); + expect.assertions(4); - const client = new TestClient({ - dsn: PUBLIC_DSN, - enableSend: true, - transport: FakeTransport, - }); + const { makeTransport, getSendCalled, getSentCount, delay } = makeFakeTransport(300); + + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + enableSend: true, + transport: makeTransport, + }), + ); - const delay = 300; - const spy = jest.spyOn(TestBackend.instance!, 'eventFromMessage'); + const spy = jest.spyOn(TestClient.instance!, 'eventFromMessage'); spy.mockImplementationOnce( (message, level) => new SyncPromise(resolve => { setTimeout(() => resolve({ message, level }), 150); }), ); - const transportInstance = client.getTransport() as FakeTransport; - transportInstance.delay = delay; - client.captureMessage('test async'); client.captureMessage('test non-async'); - expect(transportInstance).toBeInstanceOf(FakeTransport); - expect(transportInstance.sendCalled).toEqual(1); - expect(transportInstance.sentCount).toEqual(0); + expect(getSendCalled()).toEqual(1); + expect(getSentCount()).toEqual(0); await client.flush(delay); - expect(transportInstance.sentCount).toEqual(2); - expect(transportInstance.sendCalled).toEqual(2); + expect(getSentCount()).toEqual(2); + expect(getSendCalled()).toEqual(2); spy.mockRestore(); }); @@ -1278,16 +1283,15 @@ describe('BaseClient', () => { jest.useRealTimers(); expect.assertions(2); - const client = new TestClient({ - dsn: PUBLIC_DSN, - enableSend: true, - transport: FakeTransport, - }); - - const delay = 1; - const transportInstance = client.getTransport() as FakeTransport; - transportInstance.delay = delay; + const { makeTransport, delay } = makeFakeTransport(300); + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + enableSend: true, + transport: makeTransport, + }), + ); expect(client.captureMessage('test')).toBeTruthy(); await client.close(delay); @@ -1300,7 +1304,8 @@ describe('BaseClient', () => { jest.useRealTimers(); expect.assertions(3); - const client = new TestClient({ dsn: PUBLIC_DSN }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); return Promise.all([ client.flush(1).then(() => { @@ -1317,26 +1322,93 @@ describe('BaseClient', () => { }); describe('captureSession()', () => { - test('sends sessions to the backend', () => { + test('sends sessions to the client', () => { expect.assertions(1); - const client = new TestClient({ dsn: PUBLIC_DSN }); - const session = new Session({ release: 'test' }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); + const session = makeSession({ release: 'test' }); client.captureSession(session); - expect(TestBackend.instance!.session).toEqual(session); + expect(TestClient.instance!.session).toEqual(session); }); test('skips when disabled', () => { expect.assertions(1); - const client = new TestClient({ enabled: false, dsn: PUBLIC_DSN }); - const session = new Session({ release: 'test' }); + const options = getDefaultTestClientOptions({ enabled: false, dsn: PUBLIC_DSN }); + const client = new TestClient(options); + const session = makeSession({ release: 'test' }); client.captureSession(session); - expect(TestBackend.instance!.session).toBeUndefined(); + expect(TestClient.instance!.session).toBeUndefined(); + }); + }); + + describe('recordDroppedEvent()/_clearOutcomes()', () => { + test('record and return outcomes', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); + + client.recordDroppedEvent('ratelimit_backoff', 'error'); + client.recordDroppedEvent('ratelimit_backoff', 'error'); + client.recordDroppedEvent('network_error', 'transaction'); + client.recordDroppedEvent('network_error', 'transaction'); + client.recordDroppedEvent('before_send', 'session'); + client.recordDroppedEvent('event_processor', 'attachment'); + client.recordDroppedEvent('network_error', 'transaction'); + + const clearedOutcomes = client._clearOutcomes(); + + expect(clearedOutcomes).toEqual( + expect.arrayContaining([ + { + reason: 'ratelimit_backoff', + category: 'error', + quantity: 2, + }, + { + reason: 'network_error', + category: 'transaction', + quantity: 3, + }, + { + reason: 'before_send', + category: 'session', + quantity: 1, + }, + { + reason: 'event_processor', + category: 'attachment', + quantity: 1, + }, + ]), + ); + }); + + test('to clear outcomes', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); + + client.recordDroppedEvent('ratelimit_backoff', 'error'); + client.recordDroppedEvent('ratelimit_backoff', 'error'); + client.recordDroppedEvent('event_processor', 'attachment'); + + const clearedOutcomes1 = client._clearOutcomes(); + expect(clearedOutcomes1.length).toEqual(2); + + const clearedOutcomes2 = client._clearOutcomes(); + expect(clearedOutcomes2.length).toEqual(0); + + client.recordDroppedEvent('network_error', 'attachment'); + + const clearedOutcomes3 = client._clearOutcomes(); + expect(clearedOutcomes3.length).toEqual(1); + + const clearedOutcomes4 = client._clearOutcomes(); + expect(clearedOutcomes4.length).toEqual(0); }); }); }); diff --git a/packages/core/test/lib/envelope.test.ts b/packages/core/test/lib/envelope.test.ts new file mode 100644 index 000000000000..13dda0e276d4 --- /dev/null +++ b/packages/core/test/lib/envelope.test.ts @@ -0,0 +1,92 @@ +import { DsnComponents, Event, EventTraceContext } from '@sentry/types'; + +import { createEventEnvelope } from '../../src/envelope'; + +const testDsn: DsnComponents = { protocol: 'https', projectId: 'abc', host: 'testry.io', publicKey: 'pubKey123' }; + +describe('createEventEnvelope', () => { + describe('trace header', () => { + it("doesn't add trace header if event is not a transaction", () => { + const event: Event = {}; + const envelopeHeaders = createEventEnvelope(event, testDsn)[0]; + + expect(envelopeHeaders).toBeDefined(); + expect(envelopeHeaders.trace).toBeUndefined(); + }); + + it('adds minimal trace data if event is a transaction and no other baggage-related data is available', () => { + const event: Event = { + type: 'transaction', + contexts: { + trace: { + trace_id: '1234', + }, + }, + }; + const envelopeHeaders = createEventEnvelope(event, testDsn)[0]; + + expect(envelopeHeaders).toBeDefined(); + expect(envelopeHeaders.trace).toEqual({ trace_id: '1234', public_key: 'pubKey123' }); + }); + + const testTable: Array<[string, Event, EventTraceContext]> = [ + [ + 'adds only baggage item', + { + type: 'transaction', + release: '1.0.0', + contexts: { + trace: { + trace_id: '1234', + }, + }, + }, + { release: '1.0.0', trace_id: '1234', public_key: 'pubKey123' }, + ], + [ + 'adds two baggage items', + { + type: 'transaction', + release: '1.0.0', + environment: 'prod', + contexts: { + trace: { + trace_id: '1234', + }, + }, + }, + { release: '1.0.0', environment: 'prod', trace_id: '1234', public_key: 'pubKey123' }, + ], + [ + 'adds all baggageitems', + { + type: 'transaction', + release: '1.0.0', + environment: 'prod', + user: { id: 'bob', segment: 'segmentA' }, + transaction: 'TX', + contexts: { + trace: { + trace_id: '1234', + }, + }, + }, + { + release: '1.0.0', + environment: 'prod', + user: { id: 'bob', segment: 'segmentA' }, + transaction: 'TX', + trace_id: '1234', + public_key: 'pubKey123', + }, + ], + ]; + it.each(testTable)('%s', (_: string, event, trace) => { + const envelopeHeaders = createEventEnvelope(event, testDsn)[0]; + + expect(envelopeHeaders).toBeDefined(); + expect(envelopeHeaders.trace).toBeDefined(); + expect(envelopeHeaders.trace).toEqual(trace); + }); + }); +}); diff --git a/packages/core/test/lib/hint.test.ts b/packages/core/test/lib/hint.test.ts new file mode 100644 index 000000000000..03b755a63c71 --- /dev/null +++ b/packages/core/test/lib/hint.test.ts @@ -0,0 +1,96 @@ +import { captureEvent, configureScope } from '@sentry/hub'; +import { getGlobalObject } from '@sentry/utils'; + +import { initAndBind } from '../../src/sdk'; +import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; +import { AddAttachmentTestIntegration } from '../mocks/integration'; + +const PUBLIC_DSN = 'https://username@domain/123'; +const sendEvent = jest.spyOn(TestClient.prototype, 'sendEvent'); + +describe('Hint', () => { + beforeEach(() => { + TestClient.sendEventCalled = undefined; + TestClient.instance = undefined; + }); + + afterEach(() => { + jest.clearAllMocks(); + delete getGlobalObject().__SENTRY__; + }); + + describe('attachments', () => { + test('can be mutated in beforeSend', () => { + expect.assertions(1); + + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + beforeSend: (event, hint) => { + hint.attachments = [...(hint.attachments || []), { filename: 'another.file', data: 'more text' }]; + return event; + }, + }); + + const client = new TestClient(options); + client.captureEvent({}); + + const [, hint] = sendEvent.mock.calls[0]; + expect(hint).toEqual({ attachments: [{ filename: 'another.file', data: 'more text' }] }); + }); + + test('gets passed through to beforeSend and can be further mutated', () => { + expect.assertions(1); + + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + beforeSend: (event, hint) => { + hint.attachments = [...(hint.attachments || []), { filename: 'another.file', data: 'more text' }]; + return event; + }, + }); + + const client = new TestClient(options); + client.captureEvent({}, { attachments: [{ filename: 'some-file.txt', data: 'Hello' }] }); + + const [, hint] = sendEvent.mock.calls[0]; + expect(hint).toEqual({ + attachments: [ + { filename: 'some-file.txt', data: 'Hello' }, + { filename: 'another.file', data: 'more text' }, + ], + }); + }); + + test('can be mutated by an integration via event processor', () => { + expect.assertions(1); + + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + integrations: [new AddAttachmentTestIntegration()], + }); + + initAndBind(TestClient, options); + captureEvent({}); + + const [, hint] = sendEvent.mock.calls[0]; + expect(hint?.attachments).toEqual([{ filename: 'integration.file', data: 'great content!' }]); + }); + + test('get copied from scope to hint', () => { + expect.assertions(1); + + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + initAndBind(TestClient, options); + + configureScope(scope => scope.addAttachment({ filename: 'scope.file', data: 'great content!' })); + + captureEvent({}, { attachments: [{ filename: 'some-file.txt', data: 'Hello' }] }); + + const [, hint] = sendEvent.mock.calls[0]; + expect(hint?.attachments).toEqual([ + { filename: 'some-file.txt', data: 'Hello' }, + { filename: 'scope.file', data: 'great content!' }, + ]); + }); + }); +}); diff --git a/packages/core/test/lib/integrations/inboundfilters.test.ts b/packages/core/test/lib/integrations/inboundfilters.test.ts index fd6530cd75e3..8170e68d9cd5 100644 --- a/packages/core/test/lib/integrations/inboundfilters.test.ts +++ b/packages/core/test/lib/integrations/inboundfilters.test.ts @@ -1,4 +1,4 @@ -import { EventProcessor } from '@sentry/types'; +import { Event, EventProcessor } from '@sentry/types'; import { InboundFilters, InboundFiltersOptions } from '../../../src/integrations/inboundfilters'; @@ -52,50 +52,68 @@ function createInboundFiltersEventProcessor( // Fixtures -const MESSAGE_EVENT = { +const MESSAGE_EVENT: Event = { message: 'captureMessage', }; -const MESSAGE_EVENT_2 = { +const MESSAGE_EVENT_2: Event = { message: 'captureMessageSomething', }; -const MESSAGE_EVENT_WITH_STACKTRACE = { +const MESSAGE_EVENT_WITH_STACKTRACE: Event = { message: 'wat', - stacktrace: { - // Frames are always in the reverse order, as this is how Sentry expect them to come. - // Frame that crashed is the last one, the one from awesome-analytics - frames: [ - { filename: 'https://our-side.com/js/bundle.js' }, - { filename: 'https://our-side.com/js/bundle.js' }, - { filename: 'https://awesome-analytics.io/some/file.js' }, + exception: { + values: [ + { + stacktrace: { + // Frames are always in the reverse order, as this is how Sentry expect them to come. + // Frame that crashed is the last one, the one from awesome-analytics + frames: [ + { filename: 'https://our-side.com/js/bundle.js' }, + { filename: 'https://our-side.com/js/bundle.js' }, + { filename: 'https://awesome-analytics.io/some/file.js' }, + ], + }, + }, ], }, }; -const MESSAGE_EVENT_WITH_ANON_LAST_FRAME = { +const MESSAGE_EVENT_WITH_ANON_LAST_FRAME: Event = { message: 'any', - stacktrace: { - frames: [ - { filename: 'https://our-side.com/js/bundle.js' }, - { filename: 'https://awesome-analytics.io/some/file.js' }, - { filename: '' }, + exception: { + values: [ + { + stacktrace: { + frames: [ + { filename: 'https://our-side.com/js/bundle.js' }, + { filename: 'https://awesome-analytics.io/some/file.js' }, + { filename: '' }, + ], + }, + }, ], }, }; -const MESSAGE_EVENT_WITH_NATIVE_LAST_FRAME = { +const MESSAGE_EVENT_WITH_NATIVE_LAST_FRAME: Event = { message: 'any', - stacktrace: { - frames: [ - { filename: 'https://our-side.com/js/bundle.js' }, - { filename: 'https://awesome-analytics.io/some/file.js' }, - { filename: '[native code]' }, + exception: { + values: [ + { + stacktrace: { + frames: [ + { filename: 'https://our-side.com/js/bundle.js' }, + { filename: 'https://awesome-analytics.io/some/file.js' }, + { filename: '[native code]' }, + ], + }, + }, ], }, }; -const EXCEPTION_EVENT = { +const EXCEPTION_EVENT: Event = { exception: { values: [ { @@ -106,7 +124,7 @@ const EXCEPTION_EVENT = { }, }; -const EXCEPTION_EVENT_WITH_FRAMES = { +const EXCEPTION_EVENT_WITH_FRAMES: Event = { exception: { values: [ { @@ -124,7 +142,7 @@ const EXCEPTION_EVENT_WITH_FRAMES = { }, }; -const SENTRY_EVENT = { +const SENTRY_EVENT: Event = { exception: { values: [ { @@ -135,7 +153,7 @@ const SENTRY_EVENT = { }, }; -const SCRIPT_ERROR_EVENT = { +const SCRIPT_ERROR_EVENT: Event = { exception: { values: [ { @@ -146,9 +164,15 @@ const SCRIPT_ERROR_EVENT = { }, }; -const MALFORMED_EVENT = { - stacktrace: { - frames: undefined, +const MALFORMED_EVENT: Event = { + exception: { + values: [ + { + stacktrace: { + frames: undefined, + }, + }, + ], }, }; @@ -156,16 +180,16 @@ describe('InboundFilters', () => { describe('_isSentryError', () => { it('should work as expected', () => { const eventProcessor = createInboundFiltersEventProcessor(); - expect(eventProcessor(MESSAGE_EVENT)).toBe(MESSAGE_EVENT); - expect(eventProcessor(EXCEPTION_EVENT)).toBe(EXCEPTION_EVENT); - expect(eventProcessor(SENTRY_EVENT)).toBe(null); + expect(eventProcessor(MESSAGE_EVENT, {})).toBe(MESSAGE_EVENT); + expect(eventProcessor(EXCEPTION_EVENT, {})).toBe(EXCEPTION_EVENT); + expect(eventProcessor(SENTRY_EVENT, {})).toBe(null); }); it('should be configurable', () => { const eventProcessor = createInboundFiltersEventProcessor({ ignoreInternal: false }); - expect(eventProcessor(MESSAGE_EVENT)).toBe(MESSAGE_EVENT); - expect(eventProcessor(EXCEPTION_EVENT)).toBe(EXCEPTION_EVENT); - expect(eventProcessor(SENTRY_EVENT)).toBe(SENTRY_EVENT); + expect(eventProcessor(MESSAGE_EVENT, {})).toBe(MESSAGE_EVENT); + expect(eventProcessor(EXCEPTION_EVENT, {})).toBe(EXCEPTION_EVENT); + expect(eventProcessor(SENTRY_EVENT, {})).toBe(SENTRY_EVENT); }); }); @@ -174,29 +198,29 @@ describe('InboundFilters', () => { const eventProcessor = createInboundFiltersEventProcessor({ ignoreErrors: ['capture'], }); - expect(eventProcessor(MESSAGE_EVENT)).toBe(null); + expect(eventProcessor(MESSAGE_EVENT, {})).toBe(null); }); it('string filter with exact match', () => { const eventProcessor = createInboundFiltersEventProcessor({ ignoreErrors: ['captureMessage'], }); - expect(eventProcessor(MESSAGE_EVENT)).toBe(null); + expect(eventProcessor(MESSAGE_EVENT, {})).toBe(null); }); it('regexp filter with partial match', () => { const eventProcessor = createInboundFiltersEventProcessor({ ignoreErrors: [/capture/], }); - expect(eventProcessor(MESSAGE_EVENT)).toBe(null); + expect(eventProcessor(MESSAGE_EVENT, {})).toBe(null); }); it('regexp filter with exact match', () => { const eventProcessor = createInboundFiltersEventProcessor({ ignoreErrors: [/^captureMessage$/], }); - expect(eventProcessor(MESSAGE_EVENT)).toBe(null); - expect(eventProcessor(MESSAGE_EVENT_2)).toBe(MESSAGE_EVENT_2); + expect(eventProcessor(MESSAGE_EVENT, {})).toBe(null); + expect(eventProcessor(MESSAGE_EVENT_2, {})).toBe(MESSAGE_EVENT_2); }); it('prefers message when both message and exception are available', () => { @@ -207,20 +231,20 @@ describe('InboundFilters', () => { ...EXCEPTION_EVENT, ...MESSAGE_EVENT, }; - expect(eventProcessor(event)).toBe(null); + expect(eventProcessor(event, {})).toBe(null); }); it('can use multiple filters', () => { const eventProcessor = createInboundFiltersEventProcessor({ ignoreErrors: ['captureMessage', /SyntaxError/], }); - expect(eventProcessor(MESSAGE_EVENT)).toBe(null); - expect(eventProcessor(EXCEPTION_EVENT)).toBe(null); + expect(eventProcessor(MESSAGE_EVENT, {})).toBe(null); + expect(eventProcessor(EXCEPTION_EVENT, {})).toBe(null); }); it('uses default filters', () => { const eventProcessor = createInboundFiltersEventProcessor(); - expect(eventProcessor(SCRIPT_ERROR_EVENT)).toBe(null); + expect(eventProcessor(SCRIPT_ERROR_EVENT, {})).toBe(null); }); describe('on exception', () => { @@ -228,21 +252,21 @@ describe('InboundFilters', () => { const eventProcessor = createInboundFiltersEventProcessor({ ignoreErrors: ['SyntaxError: unidentified ? at line 1337'], }); - expect(eventProcessor(EXCEPTION_EVENT)).toBe(null); + expect(eventProcessor(EXCEPTION_EVENT, {})).toBe(null); }); it('can match on exception value', () => { const eventProcessor = createInboundFiltersEventProcessor({ ignoreErrors: [/unidentified \?/], }); - expect(eventProcessor(EXCEPTION_EVENT)).toBe(null); + expect(eventProcessor(EXCEPTION_EVENT, {})).toBe(null); }); it('can match on exception type', () => { const eventProcessor = createInboundFiltersEventProcessor({ ignoreErrors: [/^SyntaxError/], }); - expect(eventProcessor(EXCEPTION_EVENT)).toBe(null); + expect(eventProcessor(EXCEPTION_EVENT, {})).toBe(null); }); }); }); @@ -252,7 +276,7 @@ describe('InboundFilters', () => { const eventProcessorDeny = createInboundFiltersEventProcessor({ denyUrls: ['https://awesome-analytics.io'], }); - expect(eventProcessorDeny(MESSAGE_EVENT_WITH_STACKTRACE)).toBe(null); + expect(eventProcessorDeny(MESSAGE_EVENT_WITH_STACKTRACE, {})).toBe(null); }); it('should allow denyUrls to take precedence', () => { @@ -260,70 +284,70 @@ describe('InboundFilters', () => { allowUrls: ['https://awesome-analytics.io'], denyUrls: ['https://awesome-analytics.io'], }); - expect(eventProcessorBoth(MESSAGE_EVENT_WITH_STACKTRACE)).toBe(null); + expect(eventProcessorBoth(MESSAGE_EVENT_WITH_STACKTRACE, {})).toBe(null); }); it('should filter captured message based on its stack trace using regexp filter', () => { const eventProcessorDeny = createInboundFiltersEventProcessor({ denyUrls: [/awesome-analytics\.io/], }); - expect(eventProcessorDeny(MESSAGE_EVENT_WITH_STACKTRACE)).toBe(null); + expect(eventProcessorDeny(MESSAGE_EVENT_WITH_STACKTRACE, {})).toBe(null); }); it('should not filter captured messages with no stacktraces', () => { const eventProcessor = createInboundFiltersEventProcessor({ denyUrls: ['https://awesome-analytics.io'], }); - expect(eventProcessor(MESSAGE_EVENT)).toBe(MESSAGE_EVENT); + expect(eventProcessor(MESSAGE_EVENT, {})).toBe(MESSAGE_EVENT); }); it('should filter captured exception based on its stack trace using string filter', () => { const eventProcessor = createInboundFiltersEventProcessor({ denyUrls: ['https://awesome-analytics.io'], }); - expect(eventProcessor(EXCEPTION_EVENT_WITH_FRAMES)).toBe(null); + expect(eventProcessor(EXCEPTION_EVENT_WITH_FRAMES, {})).toBe(null); }); it('should filter captured exception based on its stack trace using regexp filter', () => { const eventProcessor = createInboundFiltersEventProcessor({ denyUrls: [/awesome-analytics\.io/], }); - expect(eventProcessor(EXCEPTION_EVENT_WITH_FRAMES)).toBe(null); + expect(eventProcessor(EXCEPTION_EVENT_WITH_FRAMES, {})).toBe(null); }); it("should not filter events that don't match the filtered values", () => { const eventProcessor = createInboundFiltersEventProcessor({ denyUrls: ['some-other-domain.com'], }); - expect(eventProcessor(EXCEPTION_EVENT_WITH_FRAMES)).toBe(EXCEPTION_EVENT_WITH_FRAMES); + expect(eventProcessor(EXCEPTION_EVENT_WITH_FRAMES, {})).toBe(EXCEPTION_EVENT_WITH_FRAMES); }); it('should be able to use multiple filters', () => { const eventProcessor = createInboundFiltersEventProcessor({ denyUrls: ['some-other-domain.com', /awesome-analytics\.io/], }); - expect(eventProcessor(EXCEPTION_EVENT_WITH_FRAMES)).toBe(null); + expect(eventProcessor(EXCEPTION_EVENT_WITH_FRAMES, {})).toBe(null); }); it('should not fail with malformed event event', () => { const eventProcessor = createInboundFiltersEventProcessor({ denyUrls: ['https://awesome-analytics.io'], }); - expect(eventProcessor(MALFORMED_EVENT)).toBe(MALFORMED_EVENT); + expect(eventProcessor(MALFORMED_EVENT, {})).toBe(MALFORMED_EVENT); }); it('should search for script names when there is an anonymous callback at the last frame', () => { const eventProcessor = createInboundFiltersEventProcessor({ denyUrls: ['https://awesome-analytics.io/some/file.js'], }); - expect(eventProcessor(MESSAGE_EVENT_WITH_ANON_LAST_FRAME)).toBe(null); + expect(eventProcessor(MESSAGE_EVENT_WITH_ANON_LAST_FRAME, {})).toBe(null); }); it('should search for script names when the last frame is from native code', () => { const eventProcessor = createInboundFiltersEventProcessor({ denyUrls: ['https://awesome-analytics.io/some/file.js'], }); - expect(eventProcessor(MESSAGE_EVENT_WITH_NATIVE_LAST_FRAME)).toBe(null); + expect(eventProcessor(MESSAGE_EVENT_WITH_NATIVE_LAST_FRAME, {})).toBe(null); }); }); }); diff --git a/packages/core/test/lib/request.test.ts b/packages/core/test/lib/request.test.ts deleted file mode 100644 index c740611a5996..000000000000 --- a/packages/core/test/lib/request.test.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { Event, SentryRequest } from '@sentry/types'; - -import { initAPIDetails } from '../../src/api'; -import { eventToSentryRequest, sessionToSentryRequest } from '../../src/request'; - -const ingestDsn = 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012'; -const storeUrl = - 'https://squirrelchasers.ingest.sentry.io/api/12312012/store/?sentry_key=dogsarebadatkeepingsecrets&sentry_version=7'; -const envelopeUrl = - 'https://squirrelchasers.ingest.sentry.io/api/12312012/envelope/?sentry_key=dogsarebadatkeepingsecrets&sentry_version=7'; -const tunnel = 'https://hello.com/world'; - -const api = initAPIDetails(ingestDsn, { - sdk: { - integrations: ['AWSLambda'], - name: 'sentry.javascript.browser', - version: '12.31.12', - packages: [{ name: 'npm:@sentry/browser', version: '12.31.12' }], - }, -}); - -function parseEnvelopeRequest(request: SentryRequest): any { - const [envelopeHeaderString, itemHeaderString, eventString] = request.body.split('\n'); - - return { - envelopeHeader: JSON.parse(envelopeHeaderString), - itemHeader: JSON.parse(itemHeaderString), - event: JSON.parse(eventString), - }; -} - -describe('eventToSentryRequest', () => { - let event: Event; - - beforeEach(() => { - event = { - contexts: { trace: { trace_id: '1231201211212012', span_id: '12261980', op: 'pageload' } }, - environment: 'dogpark', - event_id: '0908201304152013', - release: 'off.leash.park', - spans: [], - transaction: '/dogs/are/great/', - type: 'transaction', - user: { id: '1121', username: 'CharlieDog', ip_address: '11.21.20.12' }, - sdkProcessingMetadata: {}, - }; - }); - - it('adds transaction sampling information to item header', () => { - event.sdkProcessingMetadata = { transactionSampling: { method: 'client_rate', rate: 0.1121 } }; - - const result = eventToSentryRequest(event, api); - const envelope = parseEnvelopeRequest(result); - - expect(envelope.itemHeader).toEqual( - expect.objectContaining({ - sample_rates: [{ id: 'client_rate', rate: 0.1121 }], - }), - ); - }); - - it('adds sdk info to envelope header', () => { - const result = eventToSentryRequest(event, api); - const envelope = parseEnvelopeRequest(result); - - expect(envelope.envelopeHeader).toEqual( - expect.objectContaining({ sdk: { name: 'sentry.javascript.browser', version: '12.31.12' } }), - ); - }); - - it('adds sdk info to event body', () => { - const result = eventToSentryRequest(event, api); - const envelope = parseEnvelopeRequest(result); - - expect(envelope.event).toEqual( - expect.objectContaining({ - sdk: { - integrations: ['AWSLambda'], - name: 'sentry.javascript.browser', - version: '12.31.12', - packages: [{ name: 'npm:@sentry/browser', version: '12.31.12' }], - }, - }), - ); - }); - - it('merges existing sdk info if one is present on the event body', () => { - event.sdk = { - integrations: ['Clojure'], - name: 'foo', - packages: [{ name: 'npm:@sentry/clj', version: '12.31.12' }], - version: '1337', - }; - - const result = eventToSentryRequest(event, api); - const envelope = parseEnvelopeRequest(result); - - expect(envelope.event).toEqual( - expect.objectContaining({ - sdk: { - integrations: ['Clojure', 'AWSLambda'], - name: 'foo', - packages: [ - { name: 'npm:@sentry/clj', version: '12.31.12' }, - { name: 'npm:@sentry/browser', version: '12.31.12' }, - ], - version: '1337', - }, - }), - ); - }); - - it('uses tunnel as the url if it is configured', () => { - const tunnelRequest = eventToSentryRequest(event, initAPIDetails(ingestDsn, {}, tunnel)); - expect(tunnelRequest.url).toEqual(tunnel); - - const defaultRequest = eventToSentryRequest(event, initAPIDetails(ingestDsn, {})); - expect(defaultRequest.url).toEqual(envelopeUrl); - }); - - it('adds dsn to envelope header if tunnel is configured', () => { - const result = eventToSentryRequest(event, initAPIDetails(ingestDsn, {}, tunnel)); - const envelope = parseEnvelopeRequest(result); - - expect(envelope.envelopeHeader).toEqual( - expect.objectContaining({ - dsn: ingestDsn, - }), - ); - }); - - it('adds default "event" item type to item header if tunnel is configured', () => { - delete event.type; - - const result = eventToSentryRequest(event, initAPIDetails(ingestDsn, {}, tunnel)); - const envelope = parseEnvelopeRequest(result); - - expect(envelope.itemHeader).toEqual( - expect.objectContaining({ - type: 'event', - }), - ); - }); - - it('removes processing metadata before serializing event', () => { - event.sdkProcessingMetadata = { dogs: 'are great!' }; - - const result = eventToSentryRequest(event, api); - const envelope = parseEnvelopeRequest(result); - - expect(envelope.event.processingMetadata).toBeUndefined(); - }); - - it("doesn't depend on optional event fields for success ", () => { - // all event fields are optional - const emptyEvent = {}; - - const result = eventToSentryRequest(emptyEvent, api); - expect(result).toEqual({ - // The body isn't empty because SDK info gets added in `eventToSentryRequest`. (The specifics of that SDK info are - // tested elsewhere.) - body: expect.any(String), - type: 'event', - url: storeUrl, - }); - }); -}); - -describe('sessionToSentryRequest', () => { - it('test envelope creation for aggregateSessions', () => { - const aggregatedSession = { - attrs: { release: '1.0.x', environment: 'prod' }, - aggregates: [{ started: '2021-04-08T12:18:00.000Z', exited: 2 }], - }; - const result = sessionToSentryRequest(aggregatedSession, api); - - const [envelopeHeaderString, itemHeaderString, sessionString] = result.body.split('\n'); - - expect(JSON.parse(envelopeHeaderString)).toEqual( - expect.objectContaining({ - sdk: { name: 'sentry.javascript.browser', version: '12.31.12' }, - }), - ); - expect(JSON.parse(itemHeaderString)).toEqual( - expect.objectContaining({ - type: 'sessions', - }), - ); - expect(JSON.parse(sessionString)).toEqual( - expect.objectContaining({ - attrs: { release: '1.0.x', environment: 'prod' }, - aggregates: [{ started: '2021-04-08T12:18:00.000Z', exited: 2 }], - }), - ); - }); - - it('uses tunnel as the url if it is configured', () => { - const tunnelRequest = sessionToSentryRequest({ aggregates: [] }, initAPIDetails(ingestDsn, {}, tunnel)); - expect(tunnelRequest.url).toEqual(tunnel); - - const defaultRequest = sessionToSentryRequest({ aggregates: [] }, initAPIDetails(ingestDsn, {})); - expect(defaultRequest.url).toEqual(envelopeUrl); - }); - - it('adds dsn to envelope header if tunnel is configured', () => { - const result = sessionToSentryRequest({ aggregates: [] }, initAPIDetails(ingestDsn, {}, tunnel)); - const envelope = parseEnvelopeRequest(result); - - expect(envelope.envelopeHeader).toEqual( - expect.objectContaining({ - dsn: ingestDsn, - }), - ); - }); -}); diff --git a/packages/core/test/lib/sdk.test.ts b/packages/core/test/lib/sdk.test.ts index 04f704d0698e..d2857a2d83bd 100644 --- a/packages/core/test/lib/sdk.test.ts +++ b/packages/core/test/lib/sdk.test.ts @@ -3,7 +3,7 @@ import { Client, Integration } from '@sentry/types'; import { installedIntegrations } from '../../src/integration'; import { initAndBind } from '../../src/sdk'; -import { TestClient } from '../mocks/client'; +import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; // eslint-disable-next-line no-var declare var global: any; @@ -35,7 +35,7 @@ jest.mock('@sentry/hub', () => { }; }); -class MockIntegration implements Integration { +export class MockIntegration implements Integration { public name: string; public setupOnce: () => void = jest.fn(); public constructor(name: string) { @@ -50,68 +50,15 @@ describe('SDK', () => { }); describe('initAndBind', () => { - test('installs default integrations', () => { - const DEFAULT_INTEGRATIONS: Integration[] = [ - new MockIntegration('MockIntegration 1'), - new MockIntegration('MockIntegration 2'), - ]; - initAndBind(TestClient, { dsn: PUBLIC_DSN, defaultIntegrations: DEFAULT_INTEGRATIONS }); - expect((DEFAULT_INTEGRATIONS[0].setupOnce as jest.Mock).mock.calls.length).toBe(1); - expect((DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).mock.calls.length).toBe(1); - }); - - test("doesn't install default integrations if told not to", () => { - const DEFAULT_INTEGRATIONS: Integration[] = [ - new MockIntegration('MockIntegration 1'), - new MockIntegration('MockIntegration 2'), - ]; - initAndBind(TestClient, { dsn: PUBLIC_DSN, defaultIntegrations: false }); - expect((DEFAULT_INTEGRATIONS[0].setupOnce as jest.Mock).mock.calls.length).toBe(0); - expect((DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).mock.calls.length).toBe(0); - }); - test('installs integrations provided through options', () => { const integrations: Integration[] = [ new MockIntegration('MockIntegration 1'), new MockIntegration('MockIntegration 2'), ]; - initAndBind(TestClient, { dsn: PUBLIC_DSN, integrations }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, integrations }); + initAndBind(TestClient, options); expect((integrations[0].setupOnce as jest.Mock).mock.calls.length).toBe(1); expect((integrations[1].setupOnce as jest.Mock).mock.calls.length).toBe(1); }); - - test('installs merged default integrations, with overrides provided through options', () => { - const DEFAULT_INTEGRATIONS: Integration[] = [ - new MockIntegration('MockIntegration 1'), - new MockIntegration('MockIntegration 2'), - ]; - const integrations: Integration[] = [ - new MockIntegration('MockIntegration 1'), - new MockIntegration('MockIntegration 3'), - ]; - initAndBind(TestClient, { dsn: PUBLIC_DSN, defaultIntegrations: DEFAULT_INTEGRATIONS, integrations }); - // 'MockIntegration 1' should be overridden by the one with the same name provided through options - expect((DEFAULT_INTEGRATIONS[0].setupOnce as jest.Mock).mock.calls.length).toBe(0); - expect((DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).mock.calls.length).toBe(1); - expect((integrations[0].setupOnce as jest.Mock).mock.calls.length).toBe(1); - expect((integrations[1].setupOnce as jest.Mock).mock.calls.length).toBe(1); - }); - - test('installs integrations returned from a callback function', () => { - const DEFAULT_INTEGRATIONS: Integration[] = [ - new MockIntegration('MockIntegration 1'), - new MockIntegration('MockIntegration 2'), - ]; - const newIntegration = new MockIntegration('MockIntegration 3'); - initAndBind(TestClient, { - // Take only the first one and add a new one to it - defaultIntegrations: DEFAULT_INTEGRATIONS, - dsn: PUBLIC_DSN, - integrations: (integrations: Integration[]) => integrations.slice(0, 1).concat(newIntegration), - }); - expect((DEFAULT_INTEGRATIONS[0].setupOnce as jest.Mock).mock.calls.length).toBe(1); - expect((newIntegration.setupOnce as jest.Mock).mock.calls.length).toBe(1); - expect((DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).mock.calls.length).toBe(0); - }); }); }); diff --git a/packages/core/test/lib/transports/base.test.ts b/packages/core/test/lib/transports/base.test.ts index 2257a67165b1..94d83751a21d 100644 --- a/packages/core/test/lib/transports/base.test.ts +++ b/packages/core/test/lib/transports/base.test.ts @@ -1,14 +1,8 @@ -import { EventEnvelope, EventItem } from '@sentry/types'; +import { EventEnvelope, EventItem, TransportMakeRequestResponse } from '@sentry/types'; import { createEnvelope, PromiseBuffer, resolvedSyncPromise, serializeEnvelope } from '@sentry/utils'; +import { TextEncoder } from 'util'; -import { - createTransport, - ERROR_TRANSPORT_CATEGORY, - NewTransport, - TRANSACTION_TRANSPORT_CATEGORY, - TransportMakeRequestResponse, - TransportResponse, -} from '../../../src/transports/base'; +import { createTransport } from '../../../src/transports/base'; const ERROR_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, @@ -19,14 +13,19 @@ const TRANSACTION_ENVELOPE = createEnvelope( [[{ type: 'transaction' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem], ); +const transportOptions = { + recordDroppedEvent: () => undefined, // noop + textEncoder: new TextEncoder(), +}; + describe('createTransport', () => { it('flushes the buffer', async () => { - const mockBuffer: PromiseBuffer = { + const mockBuffer: PromiseBuffer = { $: [], add: jest.fn(), drain: jest.fn(), }; - const transport = createTransport({}, _ => resolvedSyncPromise({ statusCode: 200 }), mockBuffer); + const transport = createTransport(transportOptions, _ => resolvedSyncPromise({}), mockBuffer); /* eslint-disable @typescript-eslint/unbound-method */ expect(mockBuffer.drain).toHaveBeenCalledTimes(0); await transport.flush(1000); @@ -37,42 +36,25 @@ describe('createTransport', () => { describe('send', () => { it('constructs a request to send to Sentry', async () => { - const transport = createTransport({}, req => { - expect(req.category).toEqual(ERROR_TRANSPORT_CATEGORY); - expect(req.body).toEqual(serializeEnvelope(ERROR_ENVELOPE)); - return resolvedSyncPromise({ statusCode: 200, reason: 'OK' }); + expect.assertions(1); + const transport = createTransport(transportOptions, req => { + expect(req.body).toEqual(serializeEnvelope(ERROR_ENVELOPE, new TextEncoder())); + return resolvedSyncPromise({}); }); - const res = await transport.send(ERROR_ENVELOPE); - expect(res.status).toBe('success'); - expect(res.reason).toBe('OK'); + await transport.send(ERROR_ENVELOPE); }); - it('returns an error if request failed', async () => { - const transport = createTransport({}, req => { - expect(req.category).toEqual(ERROR_TRANSPORT_CATEGORY); - expect(req.body).toEqual(serializeEnvelope(ERROR_ENVELOPE)); - return resolvedSyncPromise({ statusCode: 400, reason: 'Bad Request' }); - }); - try { - await transport.send(ERROR_ENVELOPE); - } catch (res) { - expect(res.status).toBe('invalid'); - expect(res.reason).toBe('Bad Request'); - } - }); + it('does throw if request fails', async () => { + expect.assertions(2); - it('returns a default reason if reason not provided and request failed', async () => { - const transport = createTransport({}, req => { - expect(req.category).toEqual(TRANSACTION_TRANSPORT_CATEGORY); - expect(req.body).toEqual(serializeEnvelope(TRANSACTION_ENVELOPE)); - return resolvedSyncPromise({ statusCode: 500 }); + const transport = createTransport(transportOptions, req => { + expect(req.body).toEqual(serializeEnvelope(ERROR_ENVELOPE, new TextEncoder())); + throw new Error(); }); - try { - await transport.send(TRANSACTION_ENVELOPE); - } catch (res) { - expect(res.status).toBe('failed'); - expect(res.reason).toBe('Unknown transport error'); - } + + expect(() => { + void transport.send(ERROR_ENVELOPE); + }).toThrow(); }); describe('Rate-limiting', () => { @@ -89,278 +71,211 @@ describe('createTransport', () => { return { retryAfterSeconds, beforeLimit, withinLimit, afterLimit }; } - function createTestTransport( - initialTransportResponse: TransportMakeRequestResponse, - ): [NewTransport, (res: TransportMakeRequestResponse) => void] { + function createTestTransport(initialTransportResponse: TransportMakeRequestResponse) { let transportResponse: TransportMakeRequestResponse = initialTransportResponse; function setTransportResponse(res: TransportMakeRequestResponse) { transportResponse = res; } - const transport = createTransport({}, _ => { + const mockRequestExecutor = jest.fn(_ => { return resolvedSyncPromise(transportResponse); }); - return [transport, setTransportResponse]; + const mockRecordDroppedEventCallback = jest.fn(); + + const transport = createTransport( + { recordDroppedEvent: mockRecordDroppedEventCallback, textEncoder: new TextEncoder() }, + mockRequestExecutor, + ); + + return [transport, setTransportResponse, mockRequestExecutor, mockRecordDroppedEventCallback] as const; } - it('back-off using Retry-After header', async () => { + it('back-off _after_ Retry-After header was received', async () => { const { retryAfterSeconds, beforeLimit, withinLimit, afterLimit } = setRateLimitTimes(); + const [transport, setTransportResponse, requestExecutor, recordDroppedEventCallback] = createTestTransport({}); - jest - .spyOn(Date, 'now') - // 1st event - isRateLimited - FALSE - .mockImplementationOnce(() => beforeLimit) - // 1st event - updateRateLimits - .mockImplementationOnce(() => beforeLimit) - // 2nd event - isRateLimited - TRUE - .mockImplementationOnce(() => withinLimit) - // 3rd event - isRateLimited - FALSE - .mockImplementationOnce(() => afterLimit) - // 3rd event - updateRateLimits - .mockImplementationOnce(() => afterLimit); - - const [transport, setTransportResponse] = createTestTransport({ + const dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => beforeLimit); + + await transport.send(ERROR_ENVELOPE); + expect(requestExecutor).toHaveBeenCalledTimes(1); + expect(recordDroppedEventCallback).not.toHaveBeenCalled(); + requestExecutor.mockClear(); + recordDroppedEventCallback.mockClear(); + + setTransportResponse({ headers: { 'x-sentry-rate-limits': null, 'retry-after': `${retryAfterSeconds}`, }, - statusCode: 429, }); - try { - await transport.send(ERROR_ENVELOPE); - } catch (res) { - expect(res.status).toBe('rate_limit'); - expect(res.reason).toBe(`Too many error requests, backing off until: ${new Date(afterLimit).toISOString()}`); - } + await transport.send(ERROR_ENVELOPE); + expect(requestExecutor).toHaveBeenCalledTimes(1); + expect(recordDroppedEventCallback).not.toHaveBeenCalled(); + requestExecutor.mockClear(); + recordDroppedEventCallback.mockClear(); - setTransportResponse({ statusCode: 200 }); + // act like were in the rate limited period + dateNowSpy.mockImplementation(() => withinLimit); - try { - await transport.send(ERROR_ENVELOPE); - } catch (res) { - expect(res.status).toBe('rate_limit'); - expect(res.reason).toBe(`Too many error requests, backing off until: ${new Date(afterLimit).toISOString()}`); - } + await transport.send(ERROR_ENVELOPE); + expect(requestExecutor).not.toHaveBeenCalled(); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error'); + requestExecutor.mockClear(); + recordDroppedEventCallback.mockClear(); - const res = await transport.send(ERROR_ENVELOPE); - expect(res.status).toBe('success'); + // act like it's after the rate limited period + dateNowSpy.mockImplementation(() => afterLimit); + + await transport.send(ERROR_ENVELOPE); + expect(requestExecutor).toHaveBeenCalledTimes(1); + expect(recordDroppedEventCallback).not.toHaveBeenCalled(); }); it('back-off using X-Sentry-Rate-Limits with single category', async () => { const { retryAfterSeconds, beforeLimit, withinLimit, afterLimit } = setRateLimitTimes(); - - jest - .spyOn(Date, 'now') - // 1st event - isRateLimited - FALSE - .mockImplementationOnce(() => beforeLimit) - // 1st event - updateRateLimits - .mockImplementationOnce(() => beforeLimit) - // 2nd event - isRateLimited - FALSE (different category) - .mockImplementationOnce(() => withinLimit) - // 3rd event - isRateLimited - TRUE - .mockImplementationOnce(() => withinLimit) - // 4th event - isRateLimited - FALSE - .mockImplementationOnce(() => afterLimit) - // 4th event - updateRateLimits - .mockImplementationOnce(() => afterLimit); - - const [transport, setTransportResponse] = createTestTransport({ + const [transport, setTransportResponse, requestExecutor, recordDroppedEventCallback] = createTestTransport({ headers: { 'x-sentry-rate-limits': `${retryAfterSeconds}:error:scope`, 'retry-after': null, }, - statusCode: 429, }); - try { - await transport.send(ERROR_ENVELOPE); - } catch (res) { - expect(res.status).toBe('rate_limit'); - expect(res.reason).toBe(`Too many error requests, backing off until: ${new Date(afterLimit).toISOString()}`); - } + const dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => beforeLimit); + + await transport.send(ERROR_ENVELOPE); + expect(requestExecutor).toHaveBeenCalledTimes(1); + expect(recordDroppedEventCallback).not.toHaveBeenCalled(); + requestExecutor.mockClear(); + recordDroppedEventCallback.mockClear(); - setTransportResponse({ statusCode: 200 }); + setTransportResponse({}); - try { - await transport.send(TRANSACTION_ENVELOPE); - } catch (res) { - expect(res.status).toBe('rate_limit'); - expect(res.reason).toBe(`Too many error requests, backing off until: ${new Date(afterLimit).toISOString()}`); - } + // act like were in the rate limited period + dateNowSpy.mockImplementation(() => withinLimit); - try { - await transport.send(ERROR_ENVELOPE); - } catch (res) { - expect(res.status).toBe('rate_limit'); - expect(res.reason).toBe(`Too many error requests, backing off until: ${new Date(afterLimit).toISOString()}`); - } + await transport.send(TRANSACTION_ENVELOPE); // Transaction envelope should be sent + expect(requestExecutor).toHaveBeenCalledTimes(1); + expect(recordDroppedEventCallback).not.toHaveBeenCalled(); + requestExecutor.mockClear(); + recordDroppedEventCallback.mockClear(); + + await transport.send(ERROR_ENVELOPE); // Error envelope should not be sent because of pending rate limit + expect(requestExecutor).not.toHaveBeenCalled(); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error'); + requestExecutor.mockClear(); + recordDroppedEventCallback.mockClear(); + + // act like it's after the rate limited period + dateNowSpy.mockImplementation(() => afterLimit); + + await transport.send(TRANSACTION_ENVELOPE); + expect(requestExecutor).toHaveBeenCalledTimes(1); + expect(recordDroppedEventCallback).not.toHaveBeenCalled(); + requestExecutor.mockClear(); + recordDroppedEventCallback.mockClear(); - const res = await transport.send(TRANSACTION_ENVELOPE); - expect(res.status).toBe('success'); + await transport.send(ERROR_ENVELOPE); + expect(requestExecutor).toHaveBeenCalledTimes(1); + expect(recordDroppedEventCallback).not.toHaveBeenCalled(); }); it('back-off using X-Sentry-Rate-Limits with multiple categories', async () => { const { retryAfterSeconds, beforeLimit, withinLimit, afterLimit } = setRateLimitTimes(); - - jest - .spyOn(Date, 'now') - // 1st event - isRateLimited - FALSE - .mockImplementationOnce(() => beforeLimit) - // 1st event - updateRateLimits - .mockImplementationOnce(() => beforeLimit) - // 2nd event - isRateLimited - TRUE (different category) - .mockImplementationOnce(() => withinLimit) - // 3rd event - isRateLimited - TRUE - .mockImplementationOnce(() => withinLimit) - // 4th event - isRateLimited - FALSE - .mockImplementationOnce(() => afterLimit) - // 4th event - updateRateLimits - .mockImplementationOnce(() => afterLimit); - - const [transport, setTransportResponse] = createTestTransport({ + const [transport, setTransportResponse, requestExecutor, recordDroppedEventCallback] = createTestTransport({ headers: { 'x-sentry-rate-limits': `${retryAfterSeconds}:error;transaction:scope`, 'retry-after': null, }, - statusCode: 429, }); - try { - await transport.send(ERROR_ENVELOPE); - } catch (res) { - expect(res.status).toBe('rate_limit'); - expect(res.reason).toBe(`Too many error requests, backing off until: ${new Date(afterLimit).toISOString()}`); - } + const dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => beforeLimit); - try { - await transport.send(ERROR_ENVELOPE); - } catch (res) { - expect(res.status).toBe('rate_limit'); - expect(res.reason).toBe(`Too many error requests, backing off until: ${new Date(afterLimit).toISOString()}`); - } + await transport.send(ERROR_ENVELOPE); + expect(requestExecutor).toHaveBeenCalledTimes(1); + expect(recordDroppedEventCallback).not.toHaveBeenCalled(); + requestExecutor.mockClear(); + recordDroppedEventCallback.mockClear(); - try { - await transport.send(TRANSACTION_ENVELOPE); - } catch (res) { - expect(res.status).toBe('rate_limit'); - expect(res.reason).toBe( - `Too many transaction requests, backing off until: ${new Date(afterLimit).toISOString()}`, - ); - } + setTransportResponse({}); + + // act like were in the rate limited period + dateNowSpy.mockImplementation(() => withinLimit); - setTransportResponse({ statusCode: 200 }); + await transport.send(TRANSACTION_ENVELOPE); // Transaction envelope should not be sent because of pending rate limit + expect(requestExecutor).not.toHaveBeenCalled(); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'transaction'); + requestExecutor.mockClear(); + recordDroppedEventCallback.mockClear(); - const eventRes = await transport.send(ERROR_ENVELOPE); - expect(eventRes.status).toBe('success'); + await transport.send(ERROR_ENVELOPE); // Error envelope should not be sent because of pending rate limit + expect(requestExecutor).not.toHaveBeenCalled(); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error'); + requestExecutor.mockClear(); + recordDroppedEventCallback.mockClear(); - const transactionRes = await transport.send(TRANSACTION_ENVELOPE); - expect(transactionRes.status).toBe('success'); + // act like it's after the rate limited period + dateNowSpy.mockImplementation(() => afterLimit); + + await transport.send(TRANSACTION_ENVELOPE); + expect(requestExecutor).toHaveBeenCalledTimes(1); + expect(recordDroppedEventCallback).not.toHaveBeenCalled(); + requestExecutor.mockClear(); + recordDroppedEventCallback.mockClear(); + + await transport.send(ERROR_ENVELOPE); + expect(requestExecutor).toHaveBeenCalledTimes(1); + expect(recordDroppedEventCallback).not.toHaveBeenCalled(); }); it('back-off using X-Sentry-Rate-Limits with missing categories should lock them all', async () => { const { retryAfterSeconds, beforeLimit, withinLimit, afterLimit } = setRateLimitTimes(); - - jest - .spyOn(Date, 'now') - // 1st event - isRateLimited - false - .mockImplementationOnce(() => beforeLimit) - // 1st event - updateRateLimits - .mockImplementationOnce(() => beforeLimit) - // 2nd event - isRateLimited - true (event category) - .mockImplementationOnce(() => withinLimit) - // 3rd event - isRateLimited - true (transaction category) - .mockImplementationOnce(() => withinLimit) - // 4th event - isRateLimited - false (event category) - .mockImplementationOnce(() => afterLimit) - // 4th event - updateRateLimits - .mockImplementationOnce(() => afterLimit) - // 5th event - isRateLimited - false (transaction category) - .mockImplementationOnce(() => afterLimit) - // 5th event - updateRateLimits - .mockImplementationOnce(() => afterLimit); - - const [transport, setTransportResponse] = createTestTransport({ + const [transport, setTransportResponse, requestExecutor, recordDroppedEventCallback] = createTestTransport({ headers: { 'x-sentry-rate-limits': `${retryAfterSeconds}::scope`, 'retry-after': null, }, - statusCode: 429, }); - try { - await transport.send(ERROR_ENVELOPE); - } catch (res) { - expect(res.status).toBe('rate_limit'); - expect(res.reason).toBe(`Too many error requests, backing off until: ${new Date(afterLimit).toISOString()}`); - } - - try { - await transport.send(ERROR_ENVELOPE); - } catch (res) { - expect(res.status).toBe('rate_limit'); - expect(res.reason).toBe(`Too many error requests, backing off until: ${new Date(afterLimit).toISOString()}`); - } + const dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => beforeLimit); - try { - await transport.send(TRANSACTION_ENVELOPE); - } catch (res) { - expect(res.status).toBe('rate_limit'); - expect(res.reason).toBe( - `Too many transaction requests, backing off until: ${new Date(afterLimit).toISOString()}`, - ); - } + await transport.send(ERROR_ENVELOPE); + expect(requestExecutor).toHaveBeenCalledTimes(1); + expect(recordDroppedEventCallback).not.toHaveBeenCalled(); + requestExecutor.mockClear(); + recordDroppedEventCallback.mockClear(); - setTransportResponse({ statusCode: 200 }); + setTransportResponse({}); - const eventRes = await transport.send(ERROR_ENVELOPE); - expect(eventRes.status).toBe('success'); + // act like were in the rate limited period + dateNowSpy.mockImplementation(() => withinLimit); - const transactionRes = await transport.send(TRANSACTION_ENVELOPE); - expect(transactionRes.status).toBe('success'); - }); + await transport.send(TRANSACTION_ENVELOPE); // Transaction envelope should not be sent because of pending rate limit + expect(requestExecutor).not.toHaveBeenCalled(); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'transaction'); + requestExecutor.mockClear(); + recordDroppedEventCallback.mockClear(); - it('back-off using X-Sentry-Rate-Limits should also trigger for 200 responses', async () => { - const { retryAfterSeconds, beforeLimit, withinLimit, afterLimit } = setRateLimitTimes(); + await transport.send(ERROR_ENVELOPE); // Error envelope should not be sent because of pending rate limit + expect(requestExecutor).not.toHaveBeenCalled(); + expect(recordDroppedEventCallback).toHaveBeenCalledWith('ratelimit_backoff', 'error'); + requestExecutor.mockClear(); + recordDroppedEventCallback.mockClear(); - jest - .spyOn(Date, 'now') - // 1st event - isRateLimited - FALSE - .mockImplementationOnce(() => beforeLimit) - // 1st event - updateRateLimits - .mockImplementationOnce(() => beforeLimit) - // 2nd event - isRateLimited - TRUE - .mockImplementationOnce(() => withinLimit) - // 3rd event - isRateLimited - FALSE - .mockImplementationOnce(() => afterLimit) - // 3rd event - updateRateLimits - .mockImplementationOnce(() => afterLimit); - - const [transport] = createTestTransport({ - headers: { - 'x-sentry-rate-limits': `${retryAfterSeconds}:error;transaction:scope`, - 'retry-after': null, - }, - statusCode: 200, - }); + // act like it's after the rate limited period + dateNowSpy.mockImplementation(() => afterLimit); - try { - await transport.send(ERROR_ENVELOPE); - } catch (res) { - expect(res.status).toBe('rate_limit'); - expect(res.reason).toBe(`Too many error requests, backing off until: ${new Date(afterLimit).toISOString()}`); - } + await transport.send(TRANSACTION_ENVELOPE); + expect(requestExecutor).toHaveBeenCalledTimes(1); + expect(recordDroppedEventCallback).not.toHaveBeenCalled(); + requestExecutor.mockClear(); + recordDroppedEventCallback.mockClear(); - try { - await transport.send(TRANSACTION_ENVELOPE); - } catch (res) { - expect(res.status).toBe('rate_limit'); - expect(res.reason).toBe( - `Too many transaction requests, backing off until: ${new Date(afterLimit).toISOString()}`, - ); - } + await transport.send(ERROR_ENVELOPE); + expect(requestExecutor).toHaveBeenCalledTimes(1); + expect(recordDroppedEventCallback).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/core/test/mocks/backend.ts b/packages/core/test/mocks/backend.ts deleted file mode 100644 index 48ddb0d9cc0c..000000000000 --- a/packages/core/test/mocks/backend.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Session } from '@sentry/hub'; -import { Event, Options, Severity, Transport } from '@sentry/types'; -import { resolvedSyncPromise } from '@sentry/utils'; - -import { BaseBackend } from '../../src/basebackend'; - -export interface TestOptions extends Options { - test?: boolean; - mockInstallFailure?: boolean; - enableSend?: boolean; -} - -export class TestBackend extends BaseBackend { - public static instance?: TestBackend; - public static sendEventCalled?: (event: Event) => void; - - public event?: Event; - public session?: Session; - - public constructor(protected readonly _options: TestOptions) { - super(_options); - TestBackend.instance = this; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types - public eventFromException(exception: any): PromiseLike { - return resolvedSyncPromise({ - exception: { - values: [ - { - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ - type: exception.name, - value: exception.message, - /* eslint-enable @typescript-eslint/no-unsafe-member-access */ - }, - ], - }, - }); - } - - public eventFromMessage(message: string, level: Severity = Severity.Info): PromiseLike { - return resolvedSyncPromise({ message, level }); - } - - public sendEvent(event: Event): void { - this.event = event; - if (this._options.enableSend) { - super.sendEvent(event); - return; - } - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - TestBackend.sendEventCalled && TestBackend.sendEventCalled(event); - } - - public sendSession(session: Session): void { - this.session = session; - } - - protected _setupTransport(): Transport { - if (!this._options.dsn) { - // We return the noop transport here in case there is no Dsn. - return super._setupTransport(); - } - - const transportOptions = this._options.transportOptions - ? this._options.transportOptions - : { dsn: this._options.dsn }; - - if (this._options.transport) { - return new this._options.transport(transportOptions); - } - - return super._setupTransport(); - } -} diff --git a/packages/core/test/mocks/client.ts b/packages/core/test/mocks/client.ts index 810328818eae..62dfa10219da 100644 --- a/packages/core/test/mocks/client.ts +++ b/packages/core/test/mocks/client.ts @@ -1,16 +1,92 @@ +import { ClientOptions, Event, EventHint, Integration, Outcome, Session, Severity, SeverityLevel } from '@sentry/types'; +import { resolvedSyncPromise } from '@sentry/utils'; +import { TextEncoder } from 'util'; + import { BaseClient } from '../../src/baseclient'; import { initAndBind } from '../../src/sdk'; -import { TestBackend, TestOptions } from './backend'; +import { createTransport } from '../../src/transports/base'; + +export function getDefaultTestClientOptions(options: Partial = {}): TestClientOptions { + return { + integrations: [], + sendClientReports: true, + transportOptions: { textEncoder: new TextEncoder() }, + transport: () => + createTransport( + { + recordDroppedEvent: () => undefined, + textEncoder: new TextEncoder(), + }, // noop + _ => resolvedSyncPromise({}), + ), + stackParser: () => [], + ...options, + }; +} + +export interface TestClientOptions extends ClientOptions { + test?: boolean; + mockInstallFailure?: boolean; + enableSend?: boolean; + defaultIntegrations?: Integration[] | false; +} -export class TestClient extends BaseClient { +export class TestClient extends BaseClient { public static instance?: TestClient; + public static sendEventCalled?: (event: Event) => void; + + public event?: Event; + public session?: Session; - public constructor(options: TestOptions) { - super(TestBackend, options); + public constructor(options: TestClientOptions) { + super(options); TestClient.instance = this; } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + public eventFromException(exception: any): PromiseLike { + return resolvedSyncPromise({ + exception: { + values: [ + { + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + type: exception.name, + value: exception.message, + /* eslint-enable @typescript-eslint/no-unsafe-member-access */ + }, + ], + }, + }); + } + + public eventFromMessage( + message: string, + // eslint-disable-next-line deprecation/deprecation + level: Severity | SeverityLevel = 'info', + ): PromiseLike { + return resolvedSyncPromise({ message, level }); + } + + public sendEvent(event: Event, hint?: EventHint): void { + this.event = event; + if (this._options.enableSend) { + super.sendEvent(event, hint); + return; + } + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + TestClient.sendEventCalled && TestClient.sendEventCalled(event); + } + + public sendSession(session: Session): void { + this.session = session; + } + + // Public proxy for protected method + public _clearOutcomes(): Outcome[] { + return super._clearOutcomes(); + } } -export function init(options: TestOptions): void { +export function init(options: TestClientOptions): void { initAndBind(TestClient, options); } diff --git a/packages/core/test/mocks/integration.ts b/packages/core/test/mocks/integration.ts index 465fac64576a..868446665949 100644 --- a/packages/core/test/mocks/integration.ts +++ b/packages/core/test/mocks/integration.ts @@ -1,6 +1,5 @@ -import { getCurrentHub } from '@sentry/hub'; -import { configureScope } from '@sentry/minimal'; -import { Event, Integration } from '@sentry/types'; +import { configureScope, getCurrentHub } from '@sentry/hub'; +import { Event, EventProcessor, Integration } from '@sentry/types'; export class TestIntegration implements Integration { public static id: string = 'TestIntegration'; @@ -8,14 +7,31 @@ export class TestIntegration implements Integration { public name: string = 'TestIntegration'; public setupOnce(): void { + const eventProcessor: EventProcessor = (event: Event) => { + if (!getCurrentHub().getIntegration(TestIntegration)) { + return event; + } + + return null; + }; + + eventProcessor.id = this.name; + configureScope(scope => { - scope.addEventProcessor((event: Event) => { - if (!getCurrentHub().getIntegration(TestIntegration)) { - return event; - } + scope.addEventProcessor(eventProcessor); + }); + } +} + +export class AddAttachmentTestIntegration implements Integration { + public static id: string = 'AddAttachmentTestIntegration'; + + public name: string = 'AddAttachmentTestIntegration'; - return null; - }); + public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void { + addGlobalEventProcessor((event, hint) => { + hint.attachments = [...(hint.attachments || []), { filename: 'integration.file', data: 'great content!' }]; + return event; }); } } diff --git a/packages/core/test/mocks/transport.ts b/packages/core/test/mocks/transport.ts index 1037fada987d..90d35e2a0247 100644 --- a/packages/core/test/mocks/transport.ts +++ b/packages/core/test/mocks/transport.ts @@ -1,31 +1,23 @@ -import { Event, Response, Transport } from '@sentry/types'; -import { makePromiseBuffer, PromiseBuffer, SyncPromise } from '@sentry/utils'; +import { SyncPromise } from '@sentry/utils'; +import { TextEncoder } from 'util'; + +import { createTransport } from '../../src/transports/base'; async function sleep(delay: number): Promise { return new SyncPromise(resolve => setTimeout(resolve, delay)); } -export class FakeTransport implements Transport { - public sendCalled: number = 0; - public sentCount: number = 0; - public delay: number = 2000; - - /** A simple buffer holding all requests. */ - protected readonly _buffer: PromiseBuffer = makePromiseBuffer(9999); - - public sendEvent(_event: Event): PromiseLike { - this.sendCalled += 1; - return this._buffer.add( - () => - new SyncPromise(async res => { - await sleep(this.delay); - this.sentCount += 1; - res({ status: 'success' }); - }), - ); - } - - public close(timeout?: number): PromiseLike { - return this._buffer.drain(timeout); - } +export function makeFakeTransport(delay: number = 2000) { + let sendCalled = 0; + let sentCount = 0; + const makeTransport = () => + createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, () => { + sendCalled += 1; + return new SyncPromise(async res => { + await sleep(delay); + sentCount += 1; + res({}); + }); + }); + return { makeTransport, getSendCalled: () => sendCalled, getSentCount: () => sentCount, delay }; } diff --git a/packages/core/tsconfig.cjs.json b/packages/core/tsconfig.cjs.json deleted file mode 100644 index e3a918fc70af..000000000000 --- a/packages/core/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "build/dist" - } -} diff --git a/packages/core/tsconfig.esm.json b/packages/core/tsconfig.esm.json deleted file mode 100644 index 0b86c52918cc..000000000000 --- a/packages/core/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "build/esm" - } -} diff --git a/packages/ember/README.md b/packages/ember/README.md index a141ec8c2431..0b801a071ad4 100644 --- a/packages/ember/README.md +++ b/packages/ember/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Official Sentry SDK for Ember.js diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index 5bb3024f7df6..8f992f8716cc 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -380,7 +380,22 @@ export async function instrumentForPerformance(appInstance: ApplicationInstance) }), ]; - if (isTesting() && Sentry.getCurrentHub()?.getIntegration(tracing.Integrations.BrowserTracing)) { + class FakeBrowserTracingClass { + static id = tracing.BROWSER_TRACING_INTEGRATION_ID; + public name = FakeBrowserTracingClass.id; + setupOnce() { + // noop - We're just faking this class for a lookup + } + } + + if ( + isTesting() && + Sentry.getCurrentHub()?.getIntegration( + // This is a temporary hack because the BrowserTracing integration cannot have a static `id` field for tree + // shaking reasons. However, `getIntegration` needs that field. + FakeBrowserTracingClass, + ) + ) { // Initializers are called more than once in tests, causing the integrations to not be setup correctly. return; } diff --git a/packages/ember/index.js b/packages/ember/index.js index c51410e9348c..130d5f33ed5f 100644 --- a/packages/ember/index.js +++ b/packages/ember/index.js @@ -1,10 +1,21 @@ 'use strict'; const fs = require('fs'); +const crypto = require('crypto'); function readSnippet(fileName) { - return ``; + return fs.readFileSync(`${__dirname}/vendor/${fileName}`, 'utf8'); } +function hashSha256base64(string) { + return crypto.createHash('sha256').update(string).digest('base64'); +} + +const initialLoadHeadSnippet = readSnippet('initial-load-head.js'); +const initialLoadBodySnippet = readSnippet('initial-load-body.js'); + +const initialLoadHeadSnippetHash = hashSha256base64(initialLoadHeadSnippet); +const initialLoadBodySnippetHash = hashSha256base64(initialLoadBodySnippet); + module.exports = { name: require('./package').name, options: { @@ -24,16 +35,18 @@ module.exports = { contentFor(type, config) { const addonConfig = config['@sentry/ember'] || {}; - const { disablePerformance, disableInitialLoadInstrumentation } = addonConfig; + if (disablePerformance || disableInitialLoadInstrumentation) { return; } + if (type === 'head') { - return readSnippet('initial-load-head.js'); - } - if (type === 'body-footer') { - return readSnippet('initial-load-body.js'); + return ``; + } else if (type === 'body-footer') { + return ``; } }, + + injectedScriptHashes: [initialLoadHeadSnippetHash, initialLoadBodySnippetHash], }; diff --git a/packages/ember/package.json b/packages/ember/package.json index c4e58973cd53..559f6be775fb 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/ember", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Official Sentry SDK for Ember.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember", @@ -18,8 +18,9 @@ }, "scripts": { "build": "ember build --environment=production", + "build:extras": "yarn build", "build:npm": "ember ts:precompile && npm pack && ember ts:clean", - "link:yarn": "yarn link", + "clean": "yarn rimraf sentry-ember-*.tgz", "lint": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*", "lint:hbs": "ember-template-lint .", "lint:js": "eslint . --cache --cache-location '../../eslintcache/'", @@ -29,10 +30,10 @@ }, "dependencies": { "@embroider/macros": "~0.47.2", - "@sentry/browser": "6.19.7", - "@sentry/tracing": "6.19.7", - "@sentry/types": "6.19.7", - "@sentry/utils": "6.19.7", + "@sentry/browser": "7.0.0-rc.0", + "@sentry/tracing": "7.0.0-rc.0", + "@sentry/types": "7.0.0-rc.0", + "@sentry/utils": "7.0.0-rc.0", "ember-auto-import": "~1.12.1 || ~2.2.0", "ember-cli-babel": "~7.26.6", "ember-cli-htmlbars": "^6.0.1", diff --git a/packages/ember/tests/dummy/app/app.js b/packages/ember/tests/dummy/app/app.js index 25a843863e69..3479bd41f315 100644 --- a/packages/ember/tests/dummy/app/app.js +++ b/packages/ember/tests/dummy/app/app.js @@ -4,25 +4,7 @@ import loadInitializers from 'ember-load-initializers'; import config from './config/environment'; import * as Sentry from '@sentry/ember'; -import { Transports } from '@sentry/browser'; -import Ember from 'ember'; - -class TestFetchTransport extends Transports.FetchTransport { - sendEvent(event) { - if (Ember.testing) { - if (!window._sentryTestEvents) { - window._sentryTestEvents = []; - } - window._sentryTestEvents.push(event); - return Promise.resolve(); - } - return super.sendEvent(event); - } -} - -Sentry.init({ - transport: TestFetchTransport, -}); +Sentry.init(); export default class App extends Application { modulePrefix = config.modulePrefix; diff --git a/packages/ember/tests/test-helper.js b/packages/ember/tests/test-helper.js index eae0acb0b24e..da7c45929c8f 100644 --- a/packages/ember/tests/test-helper.js +++ b/packages/ember/tests/test-helper.js @@ -10,30 +10,17 @@ import Application from '../app'; import config from '../config/environment'; import { setApplication } from '@ember/test-helpers'; import { start } from 'ember-qunit'; -import { Transports } from '@sentry/browser'; import Ember from 'ember'; -import { getConfig } from '@embroider/macros'; - -function getSentryConfig() { - return getConfig('@sentry/ember').sentryConfig; -} - -export class TestFetchTransport extends Transports.FetchTransport { - sendEvent(event) { - if (Ember.testing) { - if (!window._sentryTestEvents) { - window._sentryTestEvents = []; - } - window._sentryTestEvents.push(event); - return Promise.resolve(); +Sentry.addGlobalEventProcessor((event) => { + if (Ember.testing) { + if (!window._sentryTestEvents) { + window._sentryTestEvents = []; } - return super.sendEvent(event); + window._sentryTestEvents.push(event); } -} - -const sentryConfig = getSentryConfig(); -sentryConfig.sentry['transport'] = TestFetchTransport; + return event; +}); setApplication(Application.create(config.APP)); diff --git a/packages/eslint-config-sdk/README.md b/packages/eslint-config-sdk/README.md index 7af88bd47989..28d113bdfbcd 100644 --- a/packages/eslint-config-sdk/README.md +++ b/packages/eslint-config-sdk/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Official Sentry SDK eslint config diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index b9a467c926d2..c742e3b78e3e 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-config-sdk", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Official Sentry SDK eslint config", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-config-sdk", @@ -12,15 +12,15 @@ "sentry" ], "engines": { - "node": ">=6" + "node": ">=8" }, "main": "src/index.js", "publishConfig": { "access": "public" }, "dependencies": { - "@sentry-internal/eslint-plugin-sdk": "6.19.7", - "@sentry-internal/typescript": "6.19.7", + "@sentry-internal/eslint-plugin-sdk": "7.0.0-rc.0", + "@sentry-internal/typescript": "7.0.0-rc.0", "@typescript-eslint/eslint-plugin": "^3.9.0", "@typescript-eslint/parser": "^3.9.0", "eslint-config-prettier": "^6.11.0", @@ -36,7 +36,7 @@ "eslint": "7.32.0" }, "scripts": { - "link:yarn": "yarn link", + "clean": "yarn rimraf sentry-internal-eslint-config-sdk-*.tgz", "lint": "prettier --check \"**/*.js\"", "fix": "prettier --write \"**/*.js\"", "build:npm": "npm pack", diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index 633b31552efa..0682c3510655 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-plugin-sdk", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Official Sentry SDK eslint plugin", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-plugin-sdk", @@ -12,7 +12,7 @@ "sentry" ], "engines": { - "node": ">=6" + "node": ">=8" }, "main": "src/index.js", "publishConfig": { @@ -25,7 +25,7 @@ "mocha": "^6.2.0" }, "scripts": { - "link:yarn": "yarn link", + "clean": "yarn rimraf sentry-internal-eslint-plugin-sdk-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test}/**/*.js\"", diff --git a/packages/gatsby/.eslintrc.js b/packages/gatsby/.eslintrc.js index ab610a82206a..88a6a96293ee 100644 --- a/packages/gatsby/.eslintrc.js +++ b/packages/gatsby/.eslintrc.js @@ -6,8 +6,15 @@ module.exports = { parserOptions: { jsx: true, }, - // ignoring the package-specific prepack script here b/c it is not - // covered by a `tsconfig` which makes eslint throw an error - ignorePatterns: ['scripts/prepack.ts'], + // ignore these because they're not covered by a `tsconfig`, which makes eslint throw an error + ignorePatterns: ['gatsby-browser.d.ts', 'gatsby-node.d.ts'], + overrides: [ + { + files: ['scripts/**/*.ts'], + parserOptions: { + project: ['../../tsconfig.dev.json'], + }, + }, + ], extends: ['../../.eslintrc.js'], }; diff --git a/packages/gatsby/.npmignore b/packages/gatsby/.npmignore index b8bb08da7374..35348e6a718d 100644 --- a/packages/gatsby/.npmignore +++ b/packages/gatsby/.npmignore @@ -3,10 +3,12 @@ * -!/dist/**/* +!/cjs/**/* !/esm/**/* !/types/**/* # Gatsby specific !gatsby-browser.js !gatsby-node.js +!gatsby-browser.d.ts +!gatsby-node.d.ts diff --git a/packages/gatsby/README.md b/packages/gatsby/README.md index 85f3e70dee85..22e181c5c70f 100644 --- a/packages/gatsby/README.md +++ b/packages/gatsby/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Official Sentry SDK for GatsbyJS diff --git a/packages/gatsby/gatsby-browser.js b/packages/gatsby/gatsby-browser.js index 5cf1e3d26f80..598ce44ade08 100644 --- a/packages/gatsby/gatsby-browser.js +++ b/packages/gatsby/gatsby-browser.js @@ -6,12 +6,11 @@ exports.onClientEntry = function (_, pluginParams) { const areOptionsDefined = areSentryOptionsDefined(pluginParams); if (isIntialized) { - window.Sentry = Sentry; // For backwards compatibility if (areOptionsDefined) { console.warn( 'Sentry Logger [Warn]: The SDK was initialized in the Sentry config file, but options were found in the Gatsby config. ' + - 'These have been ignored, merge them to the Sentry config if you want to use them.\n' + - 'Learn more about the Gatsby SDK on https://docs.sentry.io/platforms/javascript/guides/gatsby/', + 'These have been ignored, merge them to the Sentry config if you want to use them.\n' + + 'Learn more about the Gatsby SDK on https://docs.sentry.io/platforms/javascript/guides/gatsby/', ); } return; @@ -20,7 +19,7 @@ exports.onClientEntry = function (_, pluginParams) { if (!areOptionsDefined) { console.error( 'Sentry Logger [Error]: No config for the Gatsby SDK was found.\n' + - 'Learn how to configure it on https://docs.sentry.io/platforms/javascript/guides/gatsby/', + 'Learn how to configure it on https://docs.sentry.io/platforms/javascript/guides/gatsby/', ); return; } @@ -32,7 +31,6 @@ exports.onClientEntry = function (_, pluginParams) { dsn: __SENTRY_DSN__, ...pluginParams, }); - window.Sentry = Sentry; // For backwards compatibility }; function isSentryInitialized() { diff --git a/packages/gatsby/jest.config.js b/packages/gatsby/jest.config.js new file mode 100644 index 000000000000..cc7d8162cd59 --- /dev/null +++ b/packages/gatsby/jest.config.js @@ -0,0 +1,7 @@ +const baseConfig = require('../../jest/jest.config.js'); + +module.exports = { + ...baseConfig, + setupFiles: ['/test/setEnvVars.ts'], + testEnvironment: 'jsdom', +}; diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 1151153161fb..dfed5b0d6352 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/gatsby", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Official Sentry SDK for Gatsby.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby", @@ -11,82 +11,54 @@ "gatsby-plugin" ], "engines": { - "node": ">=6" + "node": ">=8" }, - "main": "build/dist/index.js", + "main": "build/cjs/index.js", "module": "build/esm/index.js", "types": "build/types/index.d.ts", "publishConfig": { "access": "public" }, "dependencies": { - "@sentry/react": "6.19.7", - "@sentry/tracing": "6.19.7", - "@sentry/webpack-plugin": "1.18.8" + "@sentry/react": "7.0.0-rc.0", + "@sentry/tracing": "7.0.0-rc.0", + "@sentry/types": "7.0.0-rc.0", + "@sentry/utils": "7.0.0-rc.0", + "@sentry/webpack-plugin": "1.18.9" }, "peerDependencies": { "gatsby": "^2.0.0 || ^3.0.0 || ^4.0.0", "react": "15.x || 16.x || 17.x || 18.x" }, "devDependencies": { - "@sentry/types": "6.19.7", "@testing-library/react": "^13.0.0", "react": "^18.0.0" }, "scripts": { - "build": "run-p build:cjs build:esm build:types", - "build:cjs": "tsc -p tsconfig.cjs.json", + "build": "run-p build:rollup build:types && yarn build:extras", "build:dev": "run-s build", - "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "tsc -p tsconfig.esm.json", + "build:extras": "yarn build:plugin", + "build:plugin": "tsc -p tsconfig.plugin.json", + "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", - "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", + "build:watch": "run-p build:rollup:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:es5:watch": "yarn build:cjs:watch # *** backwards compatibility - remove in v7 ***", - "build:esm:watch": "tsc -p tsconfig.esm.json --watch", + "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf dist esm build coverage", + "clean": "rimraf build coverage *.d.ts sentry-gatsby-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/*.ts\"", - "link:yarn": "yarn link", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", - "test": "jest", - "test:watch": "jest --watch" + "test": "yarn ts-node scripts/pretest.ts && yarn jest", + "test:watch": "yarn ts-node scripts/pretest.ts && yarn jest --watch" }, "volta": { "extends": "../../package.json" }, - "jest": { - "collectCoverage": true, - "transform": { - "^.+\\.ts$": "ts-jest", - "^.+\\.tsx$": "ts-jest" - }, - "moduleFileExtensions": [ - "js", - "ts", - "tsx" - ], - "testEnvironment": "jsdom", - "testMatch": [ - "**/*.test.ts", - "**/*.test.tsx" - ], - "globals": { - "ts-jest": { - "tsConfig": "./tsconfig.json", - "diagnostics": false - } - }, - "setupFiles": [ - "/test/setEnvVars.ts" - ] - }, "sideEffects": false } diff --git a/packages/gatsby/rollup.npm.config.js b/packages/gatsby/rollup.npm.config.js new file mode 100644 index 000000000000..5a62b528ef44 --- /dev/null +++ b/packages/gatsby/rollup.npm.config.js @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/gatsby/scripts/prepack.ts b/packages/gatsby/scripts/prepack.ts index 5aa95909d70c..c103875f93d3 100644 --- a/packages/gatsby/scripts/prepack.ts +++ b/packages/gatsby/scripts/prepack.ts @@ -6,20 +6,25 @@ import * as fs from 'fs'; import * as path from 'path'; -const PACKAGE_ASSETS = ['gatsby-browser.js', 'gatsby-node.js']; +const PACKAGE_ASSETS = ['gatsby-browser.js', 'gatsby-browser.d.ts', 'gatsby-node.js', 'gatsby-node.d.ts']; export function prepack(buildDir: string): boolean { // copy package-specific assets to build dir return PACKAGE_ASSETS.every(asset => { const assetPath = path.resolve(asset); + const destinationPath = path.resolve(buildDir, asset); try { if (!fs.existsSync(assetPath)) { - console.error(`Asset ${asset} does not exist.`); + console.error(`\nERROR: Asset '${asset}' does not exist.`); return false; } - fs.copyFileSync(assetPath, path.resolve(buildDir, asset)); + console.log(`Copying ${path.basename(asset)} to ${path.relative('../..', destinationPath)}.`); + fs.copyFileSync(assetPath, destinationPath); } catch (error) { - console.error(`Error while copying ${asset} to ${buildDir}`); + console.error( + `\nERROR: Error while copying ${path.basename(asset)} to ${path.relative('../..', destinationPath)}:\n`, + error, + ); return false; } return true; diff --git a/packages/gatsby/scripts/pretest.ts b/packages/gatsby/scripts/pretest.ts new file mode 100644 index 000000000000..834301f7c38a --- /dev/null +++ b/packages/gatsby/scripts/pretest.ts @@ -0,0 +1,14 @@ +import { execSync } from 'child_process'; +import * as fs from 'fs'; + +function ensurePluginTypes(): void { + if (!fs.existsSync('gatsby-browser.d.ts') || !fs.existsSync('gatsby-node.d.ts')) { + // eslint-disable-next-line no-console + console.warn( + '\nWARNING: Missing types for gatsby plugin files. Types will be created before running gatsby tests.', + ); + execSync('yarn build:plugin'); + } +} + +ensurePluginTypes(); diff --git a/packages/gatsby/test/gatsby-browser.test.ts b/packages/gatsby/test/gatsby-browser.test.ts index a3c98524a2fd..cfeb9d227a88 100644 --- a/packages/gatsby/test/gatsby-browser.test.ts +++ b/packages/gatsby/test/gatsby-browser.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-explicit-any */ -const { onClientEntry } = require('../gatsby-browser'); +import { onClientEntry } from '../gatsby-browser'; (global as any).__SENTRY_RELEASE__ = '683f3a6ab819d47d23abfca9a914c81f0524d35b'; (global as any).__SENTRY_DSN__ = 'https://examplePublicKey@o0.ingest.sentry.io/0'; @@ -36,10 +36,6 @@ describe('onClientEntry', () => { tracingAddExtensionMethods = jest.fn(); }); - afterEach(() => { - (window as any).Sentry = undefined; - }); - it.each([ [{}, ['dsn', 'release']], [{ key: 'value' }, ['dsn', 'release', 'key']], @@ -54,7 +50,6 @@ describe('onClientEntry', () => { describe('inits Sentry once', () => { afterEach(() => { - delete (window as any).Sentry; delete (window as any).__SENTRY__; (global.console.warn as jest.Mock).mockClear(); (global.console.error as jest.Mock).mockClear(); @@ -78,7 +73,6 @@ describe('onClientEntry', () => { // eslint-disable-next-line no-console expect(console.error).not.toHaveBeenCalled(); expect(sentryInit).not.toHaveBeenCalled(); - expect((window as any).Sentry).toBeDefined(); }); it('initialized in injected config, with pluginParams', () => { @@ -94,7 +88,6 @@ describe('onClientEntry', () => { // eslint-disable-next-line no-console expect(console.error).not.toHaveBeenCalled(); expect(sentryInit).not.toHaveBeenCalled(); - expect((window as any).Sentry).toBeDefined(); }); it('not initialized in injected config, without pluginParams', () => { @@ -108,7 +101,6 @@ describe('onClientEntry', () => { Learn how to configure it on https://docs.sentry.io/platforms/javascript/guides/gatsby/", ] `); - expect((window as any).Sentry).not.toBeDefined(); }); it('not initialized in injected config, with pluginParams', () => { @@ -125,7 +117,6 @@ describe('onClientEntry', () => { "release": "release", } `); - expect((window as any).Sentry).toBeDefined(); }); }); @@ -162,9 +153,8 @@ describe('onClientEntry', () => { // Run this last to check for any test side effects it('does not run if plugin params are undefined', () => { - onClientEntry(); + onClientEntry(undefined, undefined); expect(sentryInit).toHaveBeenCalledTimes(0); - expect((window as any).Sentry).toBeUndefined(); expect(tracingAddExtensionMethods).toHaveBeenCalledTimes(0); }); }); diff --git a/packages/gatsby/test/gatsby-node.test.ts b/packages/gatsby/test/gatsby-node.test.ts index f09f8e5be781..8880c133b90d 100644 --- a/packages/gatsby/test/gatsby-node.test.ts +++ b/packages/gatsby/test/gatsby-node.test.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-explicit-any */ -const { onCreateWebpackConfig } = require('../gatsby-node'); +import { onCreateWebpackConfig } from '../gatsby-node'; describe('onCreateWebpackConfig', () => { it('sets a webpack config', () => { @@ -12,7 +12,9 @@ describe('onCreateWebpackConfig', () => { setWebpackConfig: jest.fn(), }; - onCreateWebpackConfig({ plugins, actions }); + const getConfig = jest.fn(); + + onCreateWebpackConfig({ plugins, actions, getConfig }); expect(plugins.define).toHaveBeenCalledTimes(1); expect(plugins.define).toHaveBeenLastCalledWith({ diff --git a/packages/gatsby/test/integration.test.tsx b/packages/gatsby/test/integration.test.tsx index e758aa90a79d..3fd06dc21990 100644 --- a/packages/gatsby/test/integration.test.tsx +++ b/packages/gatsby/test/integration.test.tsx @@ -3,12 +3,16 @@ import { render } from '@testing-library/react'; import { useEffect } from 'react'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import * as React from 'react'; +import { TextDecoder,TextEncoder } from 'util'; import { onClientEntry } from '../gatsby-browser'; +import * as Sentry from '../src'; beforeAll(() => { (global as any).__SENTRY_RELEASE__ = '683f3a6ab819d47d23abfca9a914c81f0524d35b'; (global as any).__SENTRY_DSN__ = 'https://examplePublicKey@o0.ingest.sentry.io/0'; + (global as any).TextEncoder = TextEncoder; + (global as any).TextDecoder = TextDecoder; }); describe('useEffect', () => { @@ -28,7 +32,7 @@ describe('useEffect', () => { function TestComponent() { useEffect(() => { const error = new Error('testing 123'); - (window as any).Sentry.captureException(error); + Sentry.captureException(error); }); return
Hello
; diff --git a/packages/gatsby/test/setEnvVars.ts b/packages/gatsby/test/setEnvVars.ts index c97579e924e7..bc9d45b9c84a 100644 --- a/packages/gatsby/test/setEnvVars.ts +++ b/packages/gatsby/test/setEnvVars.ts @@ -1,2 +1,6 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access process.env.SENTRY_RELEASE = '14abbb1678a2eb59d1a171ea33d630dd6c6eee70'; + +// This file needs to have an import or an export to count as a module, which is necessary when using +// the `isolatedModules` tsconfig option. +export const _ = ''; diff --git a/packages/gatsby/tsconfig.cjs.json b/packages/gatsby/tsconfig.cjs.json deleted file mode 100644 index e3a918fc70af..000000000000 --- a/packages/gatsby/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "build/dist" - } -} diff --git a/packages/gatsby/tsconfig.esm.json b/packages/gatsby/tsconfig.esm.json deleted file mode 100644 index 0b86c52918cc..000000000000 --- a/packages/gatsby/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "build/esm" - } -} diff --git a/packages/gatsby/tsconfig.json b/packages/gatsby/tsconfig.json index f074f990a911..b2c40b91a630 100644 --- a/packages/gatsby/tsconfig.json +++ b/packages/gatsby/tsconfig.json @@ -5,7 +5,6 @@ "compilerOptions": { // package-specific options - "esModuleInterop": true, "jsx": "react" } } diff --git a/packages/gatsby/tsconfig.plugin.json b/packages/gatsby/tsconfig.plugin.json new file mode 100644 index 000000000000..8a755642dd8b --- /dev/null +++ b/packages/gatsby/tsconfig.plugin.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + + "include": ["gatsby-browser.js", "gatsby-node.js"], + + "compilerOptions": { + // should include all types from `./tsconfig.json` plus types for all test frameworks used + // "types": ["node", "jest"] + "declaration": true, + "declarationMap": false, + "emitDeclarationOnly": true, + "allowJs": true, + "skipLibCheck": true + + // other package-specific, plugin-specific options + } +} diff --git a/packages/hub/README.md b/packages/hub/README.md index 7a4da6390db0..ed2e5a20d950 100644 --- a/packages/hub/README.md +++ b/packages/hub/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Sentry JavaScript SDK Hub diff --git a/packages/hub/jest.config.js b/packages/hub/jest.config.js new file mode 100644 index 000000000000..24f49ab59a4c --- /dev/null +++ b/packages/hub/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest/jest.config.js'); diff --git a/packages/hub/package.json b/packages/hub/package.json index f194b7c7513d..1f5b488ad26f 100644 --- a/packages/hub/package.json +++ b/packages/hub/package.json @@ -1,45 +1,40 @@ { "name": "@sentry/hub", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Sentry hub which handles global state managment.", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/hub", "author": "Sentry", "license": "BSD-3-Clause", "engines": { - "node": ">=6" + "node": ">=8" }, - "main": "build/dist/index.js", + "main": "build/cjs/index.js", "module": "build/esm/index.js", "types": "build/types/index.d.ts", "publishConfig": { "access": "public" }, "dependencies": { - "@sentry/types": "6.19.7", - "@sentry/utils": "6.19.7", + "@sentry/types": "7.0.0-rc.0", + "@sentry/utils": "7.0.0-rc.0", "tslib": "^1.9.3" }, "scripts": { - "build": "run-p build:cjs build:esm build:types", - "build:cjs": "tsc -p tsconfig.cjs.json", + "build": "run-p build:rollup build:types", "build:dev": "run-s build", - "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "tsc -p tsconfig.esm.json", + "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", - "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", + "build:watch": "run-p build:rollup:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:es5:watch": "yarn build:cjs:watch # *** backwards compatibility - remove in v7 ***", - "build:esm:watch": "tsc -p tsconfig.esm.json --watch", + "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf dist esm coverage", + "clean": "rimraf build coverage sentry-hub-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/*.ts\"", - "link:yarn": "yarn link", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test}/**/*.ts\"", @@ -49,25 +44,5 @@ "volta": { "extends": "../../package.json" }, - "jest": { - "collectCoverage": true, - "transform": { - "^.+\\.ts$": "ts-jest" - }, - "moduleFileExtensions": [ - "js", - "ts" - ], - "testEnvironment": "node", - "testMatch": [ - "**/*.test.ts" - ], - "globals": { - "ts-jest": { - "tsConfig": "./tsconfig.json", - "diagnostics": false - } - } - }, "sideEffects": false } diff --git a/packages/hub/rollup.npm.config.js b/packages/hub/rollup.npm.config.js new file mode 100644 index 000000000000..5a62b528ef44 --- /dev/null +++ b/packages/hub/rollup.npm.config.js @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/minimal/src/index.ts b/packages/hub/src/exports.ts similarity index 59% rename from packages/minimal/src/index.ts rename to packages/hub/src/exports.ts index 00a1cdd91d44..528da6289d3e 100644 --- a/packages/minimal/src/index.ts +++ b/packages/hub/src/exports.ts @@ -1,48 +1,38 @@ -import { getCurrentHub, Hub, Scope } from '@sentry/hub'; import { Breadcrumb, CaptureContext, CustomSamplingContext, Event, + EventHint, Extra, Extras, Primitive, Severity, - Transaction, + SeverityLevel, TransactionContext, User, } from '@sentry/types'; -/** - * This calls a function on the current hub. - * @param method function to call on hub. - * @param args to pass to function. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function callOnHub(method: string, ...args: any[]): T { - const hub = getCurrentHub(); - if (hub && hub[method as keyof Hub]) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (hub[method as keyof Hub] as any)(...args); - } - throw new Error(`No hub defined or ${method} was not found on the hub, please open a bug report.`); -} +import { getCurrentHub, Hub } from './hub'; +import { Scope } from './scope'; + +// Note: All functions in this file are typed with a return value of `ReturnType`, +// where HUB_FUNCTION is some method on the Hub class. +// +// This is done to make sure the top level SDK methods stay in sync with the hub methods. +// Although every method here has an explicit return type, some of them (that map to void returns) do not +// contain `return` keywords. This is done to save on bundle size, as `return` is not minifiable. /** * Captures an exception event and sends it to Sentry. * * @param exception An exception-like object. + * @param captureContext Additional scope data to apply to exception event. * @returns The generated eventId. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types -export function captureException(exception: any, captureContext?: CaptureContext): string { - const syntheticException = new Error('Sentry syntheticException'); - - return callOnHub('captureException', exception, { - captureContext, - originalException: exception, - syntheticException, - }); +export function captureException(exception: any, captureContext?: CaptureContext): ReturnType { + return getCurrentHub().captureException(exception, { captureContext }); } /** @@ -52,19 +42,16 @@ export function captureException(exception: any, captureContext?: CaptureContext * @param Severity Define the level of the message. * @returns The generated eventId. */ -export function captureMessage(message: string, captureContext?: CaptureContext | Severity): string { - const syntheticException = new Error(message); - +export function captureMessage( + message: string, + // eslint-disable-next-line deprecation/deprecation + captureContext?: CaptureContext | Severity | SeverityLevel, +): ReturnType { // This is necessary to provide explicit scopes upgrade, without changing the original // arity of the `captureMessage(message, level)` method. const level = typeof captureContext === 'string' ? captureContext : undefined; const context = typeof captureContext !== 'string' ? { captureContext } : undefined; - - return callOnHub('captureMessage', message, level, { - originalException: message, - syntheticException, - ...context, - }); + return getCurrentHub().captureMessage(message, level, context); } /** @@ -73,16 +60,16 @@ export function captureMessage(message: string, captureContext?: CaptureContext * @param event The event to send to Sentry. * @returns The generated eventId. */ -export function captureEvent(event: Event): string { - return callOnHub('captureEvent', event); +export function captureEvent(event: Event, hint?: EventHint): ReturnType { + return getCurrentHub().captureEvent(event, hint); } /** * Callback to set context information onto the scope. * @param callback Callback function that receives Scope. */ -export function configureScope(callback: (scope: Scope) => void): void { - callOnHub('configureScope', callback); +export function configureScope(callback: (scope: Scope) => void): ReturnType { + getCurrentHub().configureScope(callback); } /** @@ -93,8 +80,8 @@ export function configureScope(callback: (scope: Scope) => void): void { * * @param breadcrumb The breadcrumb to record. */ -export function addBreadcrumb(breadcrumb: Breadcrumb): void { - callOnHub('addBreadcrumb', breadcrumb); +export function addBreadcrumb(breadcrumb: Breadcrumb): ReturnType { + getCurrentHub().addBreadcrumb(breadcrumb); } /** @@ -103,33 +90,33 @@ export function addBreadcrumb(breadcrumb: Breadcrumb): void { * @param context Any kind of data. This data will be normalized. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function setContext(name: string, context: { [key: string]: any } | null): void { - callOnHub('setContext', name, context); +export function setContext(name: string, context: { [key: string]: any } | null): ReturnType { + getCurrentHub().setContext(name, context); } /** * Set an object that will be merged sent as extra data with the event. * @param extras Extras object to merge into current context. */ -export function setExtras(extras: Extras): void { - callOnHub('setExtras', extras); +export function setExtras(extras: Extras): ReturnType { + getCurrentHub().setExtras(extras); } /** - * Set an object that will be merged sent as tags data with the event. - * @param tags Tags context object to merge into current context. + * Set key:value that will be sent as extra data with the event. + * @param key String of extra + * @param extra Any kind of data. This data will be normalized. */ -export function setTags(tags: { [key: string]: Primitive }): void { - callOnHub('setTags', tags); +export function setExtra(key: string, extra: Extra): ReturnType { + getCurrentHub().setExtra(key, extra); } /** - * Set key:value that will be sent as extra data with the event. - * @param key String of extra - * @param extra Any kind of data. This data will be normalized. + * Set an object that will be merged sent as tags data with the event. + * @param tags Tags context object to merge into current context. */ -export function setExtra(key: string, extra: Extra): void { - callOnHub('setExtra', key, extra); +export function setTags(tags: { [key: string]: Primitive }): ReturnType { + getCurrentHub().setTags(tags); } /** @@ -140,8 +127,8 @@ export function setExtra(key: string, extra: Extra): void { * @param key String key of tag * @param value Value of tag */ -export function setTag(key: string, value: Primitive): void { - callOnHub('setTag', key, value); +export function setTag(key: string, value: Primitive): ReturnType { + getCurrentHub().setTag(key, value); } /** @@ -149,8 +136,8 @@ export function setTag(key: string, value: Primitive): void { * * @param user User context object to be set in the current context. Pass `null` to unset the user. */ -export function setUser(user: User | null): void { - callOnHub('setUser', user); +export function setUser(user: User | null): ReturnType { + getCurrentHub().setUser(user); } /** @@ -166,23 +153,8 @@ export function setUser(user: User | null): void { * * @param callback that will be enclosed into push/popScope. */ -export function withScope(callback: (scope: Scope) => void): void { - callOnHub('withScope', callback); -} - -/** - * Calls a function on the latest client. Use this with caution, it's meant as - * in "internal" helper so we don't need to expose every possible function in - * the shim. It is not guaranteed that the client actually implements the - * function. - * - * @param method The method to call on the client/client. - * @param args Arguments to pass to the client/fontend. - * @hidden - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function _callOnClient(method: string, ...args: any[]): void { - callOnHub('_invokeClient', method, ...args); +export function withScope(callback: (scope: Scope) => void): ReturnType { + getCurrentHub().withScope(callback); } /** @@ -205,6 +177,6 @@ export function _callOnClient(method: string, ...args: any[]): void { export function startTransaction( context: TransactionContext, customSamplingContext?: CustomSamplingContext, -): Transaction { - return callOnHub('startTransaction', { ...context }, customSamplingContext); +): ReturnType { + return getCurrentHub().startTransaction({ ...context }, customSamplingContext); } diff --git a/packages/hub/src/hub.ts b/packages/hub/src/hub.ts index 67761953abfa..7f591e44c2e8 100644 --- a/packages/hub/src/hub.ts +++ b/packages/hub/src/hub.ts @@ -12,10 +12,10 @@ import { Integration, IntegrationClass, Primitive, + Session, SessionContext, Severity, - Span, - SpanContext, + SeverityLevel, Transaction, TransactionContext, User, @@ -32,7 +32,7 @@ import { import { IS_DEBUG_BUILD } from './flags'; import { Scope } from './scope'; -import { Session } from './session'; +import { closeSession, makeSession, updateSession } from './session'; /** * API compatibility version of this hub. @@ -82,15 +82,6 @@ export interface Carrier { }; } -/** - * @hidden - * @deprecated Can be removed once `Hub.getActiveDomain` is removed. - */ -export interface DomainAsCarrier extends Carrier { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - members: { [key: string]: any }[]; -} - /** * @inheritDoc */ @@ -195,28 +186,18 @@ export class Hub implements HubInterface { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types public captureException(exception: any, hint?: EventHint): string { const eventId = (this._lastEventId = hint && hint.event_id ? hint.event_id : uuid4()); - let finalHint = hint; - - // If there's no explicit hint provided, mimic the same thing that would happen - // in the minimal itself to create a consistent behavior. - // We don't do this in the client, as it's the lowest level API, and doing this, - // would prevent user from having full control over direct calls. - if (!hint) { - let syntheticException: Error; - try { - throw new Error('Sentry syntheticException'); - } catch (exception) { - syntheticException = exception as Error; - } - finalHint = { - originalException: exception, - syntheticException, - }; - } - - this._invokeClient('captureException', exception, { - ...finalHint, - event_id: eventId, + const syntheticException = new Error('Sentry syntheticException'); + this._withClient((client, scope) => { + client.captureException( + exception, + { + originalException: exception, + syntheticException, + ...hint, + event_id: eventId, + }, + scope, + ); }); return eventId; } @@ -224,30 +205,26 @@ export class Hub implements HubInterface { /** * @inheritDoc */ - public captureMessage(message: string, level?: Severity, hint?: EventHint): string { + public captureMessage( + message: string, + // eslint-disable-next-line deprecation/deprecation + level?: Severity | SeverityLevel, + hint?: EventHint, + ): string { const eventId = (this._lastEventId = hint && hint.event_id ? hint.event_id : uuid4()); - let finalHint = hint; - - // If there's no explicit hint provided, mimic the same thing that would happen - // in the minimal itself to create a consistent behavior. - // We don't do this in the client, as it's the lowest level API, and doing this, - // would prevent user from having full control over direct calls. - if (!hint) { - let syntheticException: Error; - try { - throw new Error(message); - } catch (exception) { - syntheticException = exception as Error; - } - finalHint = { - originalException: message, - syntheticException, - }; - } - - this._invokeClient('captureMessage', message, level, { - ...finalHint, - event_id: eventId, + const syntheticException = new Error(message); + this._withClient((client, scope) => { + client.captureMessage( + message, + level, + { + originalException: message, + syntheticException, + ...hint, + event_id: eventId, + }, + scope, + ); }); return eventId; } @@ -261,9 +238,8 @@ export class Hub implements HubInterface { this._lastEventId = eventId; } - this._invokeClient('captureEvent', event, { - ...hint, - event_id: eventId, + this._withClient((client, scope) => { + client.captureEvent(event, { ...hint, event_id: eventId }, scope); }); return eventId; } @@ -385,13 +361,6 @@ export class Hub implements HubInterface { } } - /** - * @inheritDoc - */ - public startSpan(context: SpanContext): Span { - return this._callExtensionMethod('startSpan', context); - } - /** * @inheritDoc */ @@ -427,7 +396,7 @@ export class Hub implements HubInterface { const scope = layer && layer.scope; const session = scope && scope.getSession(); if (session) { - session.close(); + closeSession(session); } this._sendSessionUpdate(); @@ -448,7 +417,7 @@ export class Hub implements HubInterface { const global = getGlobalObject<{ navigator?: { userAgent?: string } }>(); const { userAgent } = global.navigator || {}; - const session = new Session({ + const session = makeSession({ release, environment, ...(scope && { user: scope.getUser() }), @@ -460,7 +429,7 @@ export class Hub implements HubInterface { // End existing session if there's one const currentSession = scope.getSession && scope.getSession(); if (currentSession && currentSession.status === 'ok') { - currentSession.update({ status: 'exited' }); + updateSession(currentSession, { status: 'exited' }); } this.endSession(); @@ -478,7 +447,7 @@ export class Hub implements HubInterface { const { scope, client } = this.getStackTop(); if (!scope) return; - const session = scope.getSession && scope.getSession(); + const session = scope.getSession(); if (session) { if (client && client.captureSession) { client.captureSession(session); @@ -492,12 +461,10 @@ export class Hub implements HubInterface { * @param method The method to call on the client. * @param args Arguments to pass to the client function. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private _invokeClient(method: M, ...args: any[]): void { + private _withClient(callback: (client: Client, scope: Scope | undefined) => void): void { const { scope, client } = this.getStackTop(); - if (client && client[method]) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - (client as any)[method](...args, scope); + if (client) { + callback(client, scope); } } @@ -568,20 +535,6 @@ export function getCurrentHub(): Hub { return getHubFromCarrier(registry); } -/** - * Returns the active domain, if one exists - * @deprecated No longer used; remove in v7 - * @returns The domain, or undefined if there is no active domain - */ -// eslint-disable-next-line deprecation/deprecation -export function getActiveDomain(): DomainAsCarrier | undefined { - IS_DEBUG_BUILD && logger.warn('Function `getActiveDomain` is deprecated and will be removed in a future version.'); - - const sentry = getMainCarrier().__SENTRY__; - - return sentry && sentry.extensions && sentry.extensions.domain && sentry.extensions.domain.active; -} - /** * Try to read the hub from an active domain, and fallback to the registry if one doesn't exist * @returns discovered hub diff --git a/packages/hub/src/index.ts b/packages/hub/src/index.ts index 9c0a77625dc2..16c4f3680dd8 100644 --- a/packages/hub/src/index.ts +++ b/packages/hub/src/index.ts @@ -1,17 +1,21 @@ +export type { Carrier, Layer } from './hub'; + export { addGlobalEventProcessor, Scope } from './scope'; -export { Session } from './session'; +export { closeSession, makeSession, updateSession } from './session'; export { SessionFlusher } from './sessionflusher'; +export { getCurrentHub, getHubFromCarrier, getMainCarrier, Hub, makeMain, setHubOnCarrier } from './hub'; export { - // eslint-disable-next-line deprecation/deprecation - getActiveDomain, - getCurrentHub, - getHubFromCarrier, - getMainCarrier, - Hub, - makeMain, - setHubOnCarrier, - Carrier, - // eslint-disable-next-line deprecation/deprecation - DomainAsCarrier, - Layer, -} from './hub'; + addBreadcrumb, + captureException, + captureEvent, + captureMessage, + configureScope, + startTransaction, + setContext, + setExtra, + setExtras, + setTag, + setTags, + setUser, + withScope, +} from './exports'; diff --git a/packages/hub/src/scope.ts b/packages/hub/src/scope.ts index 9243cd409b66..98f80745004c 100644 --- a/packages/hub/src/scope.ts +++ b/packages/hub/src/scope.ts @@ -1,5 +1,6 @@ /* eslint-disable max-lines */ import { + Attachment, Breadcrumb, CaptureContext, Context, @@ -13,14 +14,24 @@ import { RequestSession, Scope as ScopeInterface, ScopeContext, + Session, Severity, + SeverityLevel, Span, Transaction, User, } from '@sentry/types'; -import { dateTimestampInSeconds, getGlobalSingleton, isPlainObject, isThenable, SyncPromise } from '@sentry/utils'; +import { + dateTimestampInSeconds, + getGlobalSingleton, + isPlainObject, + isThenable, + logger, + SyncPromise, +} from '@sentry/utils'; -import { Session } from './session'; +import { IS_DEBUG_BUILD } from './flags'; +import { updateSession } from './session'; /** * Absolute maximum number of breadcrumbs added to an event. @@ -61,7 +72,8 @@ export class Scope implements ScopeInterface { protected _fingerprint?: string[]; /** Severity */ - protected _level?: Severity; + // eslint-disable-next-line deprecation/deprecation + protected _level?: Severity | SeverityLevel; /** Transaction Name */ protected _transactionName?: string; @@ -75,6 +87,9 @@ export class Scope implements ScopeInterface { /** Request Mode Session Status */ protected _requestSession?: RequestSession; + /** Attachments */ + protected _attachments: Attachment[] = []; + /** * A place to stash data which is needed at some point in the SDK's event processing pipeline but which shouldn't get * sent to Sentry @@ -100,6 +115,7 @@ export class Scope implements ScopeInterface { newScope._fingerprint = scope._fingerprint; newScope._eventProcessors = [...scope._eventProcessors]; newScope._requestSession = scope._requestSession; + newScope._attachments = [...scope._attachments]; } return newScope; } @@ -126,7 +142,7 @@ export class Scope implements ScopeInterface { public setUser(user: User | null): this { this._user = user || {}; if (this._session) { - this._session.update({ user }); + updateSession(this._session, { user }); } this._notifyScopeListeners(); return this; @@ -208,7 +224,10 @@ export class Scope implements ScopeInterface { /** * @inheritDoc */ - public setLevel(level: Severity): this { + public setLevel( + // eslint-disable-next-line deprecation/deprecation + level: Severity | SeverityLevel, + ): this { this._level = level; this._notifyScopeListeners(); return this; @@ -223,14 +242,6 @@ export class Scope implements ScopeInterface { return this; } - /** - * Can be removed in major version. - * @deprecated in favor of {@link this.setTransactionName} - */ - public setTransaction(name?: string): this { - return this.setTransactionName(name); - } - /** * @inheritDoc */ @@ -360,6 +371,7 @@ export class Scope implements ScopeInterface { this._span = undefined; this._session = undefined; this._notifyScopeListeners(); + this._attachments = []; return this; } @@ -393,6 +405,29 @@ export class Scope implements ScopeInterface { return this; } + /** + * @inheritDoc + */ + public addAttachment(attachment: Attachment): this { + this._attachments.push(attachment); + return this; + } + + /** + * @inheritDoc + */ + public getAttachments(): Attachment[] { + return this._attachments; + } + + /** + * @inheritDoc + */ + public clearAttachments(): this { + this._attachments = []; + return this; + } + /** * Applies the current context and fingerprint to the event. * Note that breadcrumbs will be added by the client. @@ -401,7 +436,7 @@ export class Scope implements ScopeInterface { * @param hint May contain additional information about the original exception. * @hidden */ - public applyToEvent(event: Event, hint?: EventHint): PromiseLike { + public applyToEvent(event: Event, hint: EventHint = {}): PromiseLike { if (this._extra && Object.keys(this._extra).length) { event.extra = { ...this._extra, ...event.extra }; } @@ -456,7 +491,7 @@ export class Scope implements ScopeInterface { protected _notifyEventProcessors( processors: EventProcessor[], event: Event | null, - hint?: EventHint, + hint: EventHint, index: number = 0, ): PromiseLike { return new SyncPromise((resolve, reject) => { @@ -465,6 +500,12 @@ export class Scope implements ScopeInterface { resolve(event); } else { const result = processor({ ...event }, hint) as Event | null; + + IS_DEBUG_BUILD && + processor.id && + result === null && + logger.log(`Event processor "${processor.id}" dropped event`); + if (isThenable(result)) { void result .then(final => this._notifyEventProcessors(processors, final, hint, index + 1).then(resolve)) diff --git a/packages/hub/src/session.ts b/packages/hub/src/session.ts index 3206ef9306dc..c8cba3e30a04 100644 --- a/packages/hub/src/session.ts +++ b/packages/hub/src/session.ts @@ -1,136 +1,153 @@ -import { Session as SessionInterface, SessionContext, SessionStatus } from '@sentry/types'; +import { SerializedSession, Session, SessionContext, SessionStatus } from '@sentry/types'; import { dropUndefinedKeys, timestampInSeconds, uuid4 } from '@sentry/utils'; /** - * @inheritdoc + * Creates a new `Session` object by setting certain default parameters. If optional @param context + * is passed, the passed properties are applied to the session object. + * + * @param context (optional) additional properties to be applied to the returned session object + * + * @returns a new `Session` object */ -export class Session implements SessionInterface { - public userAgent?: string; - public errors: number = 0; - public release?: string; - public sid: string = uuid4(); - public did?: string; - public timestamp: number; - public started: number; - public duration?: number = 0; - public status: SessionStatus = 'ok'; - public environment?: string; - public ipAddress?: string; - public init: boolean = true; - public ignoreDuration: boolean = false; +export function makeSession(context?: Omit): Session { + // Both timestamp and started are in seconds since the UNIX epoch. + const startingTime = timestampInSeconds(); - public constructor(context?: Omit) { - // Both timestamp and started are in seconds since the UNIX epoch. - const startingTime = timestampInSeconds(); - this.timestamp = startingTime; - this.started = startingTime; - if (context) { - this.update(context); - } + const session: Session = { + sid: uuid4(), + init: true, + timestamp: startingTime, + started: startingTime, + duration: 0, + status: 'ok', + errors: 0, + ignoreDuration: false, + toJSON: () => sessionToJSON(session), + }; + + if (context) { + updateSession(session, context); } - /** JSDoc */ - // eslint-disable-next-line complexity - public update(context: SessionContext = {}): void { - if (context.user) { - if (!this.ipAddress && context.user.ip_address) { - this.ipAddress = context.user.ip_address; - } + return session; +} - if (!this.did && !context.did) { - this.did = context.user.id || context.user.email || context.user.username; - } +/** + * Updates a session object with the properties passed in the context. + * + * Note that this function mutates the passed object and returns void. + * (Had to do this instead of returning a new and updated session because closing and sending a session + * makes an update to the session after it was passed to the sending logic. + * @see BaseClient.captureSession ) + * + * @param session the `Session` to update + * @param context the `SessionContext` holding the properties that should be updated in @param session + */ +// eslint-disable-next-line complexity +export function updateSession(session: Session, context: SessionContext = {}): void { + if (context.user) { + if (!session.ipAddress && context.user.ip_address) { + session.ipAddress = context.user.ip_address; } - this.timestamp = context.timestamp || timestampInSeconds(); - if (context.ignoreDuration) { - this.ignoreDuration = context.ignoreDuration; - } - if (context.sid) { - // Good enough uuid validation. — Kamil - this.sid = context.sid.length === 32 ? context.sid : uuid4(); - } - if (context.init !== undefined) { - this.init = context.init; - } - if (!this.did && context.did) { - this.did = `${context.did}`; - } - if (typeof context.started === 'number') { - this.started = context.started; - } - if (this.ignoreDuration) { - this.duration = undefined; - } else if (typeof context.duration === 'number') { - this.duration = context.duration; - } else { - const duration = this.timestamp - this.started; - this.duration = duration >= 0 ? duration : 0; - } - if (context.release) { - this.release = context.release; - } - if (context.environment) { - this.environment = context.environment; - } - if (!this.ipAddress && context.ipAddress) { - this.ipAddress = context.ipAddress; - } - if (!this.userAgent && context.userAgent) { - this.userAgent = context.userAgent; - } - if (typeof context.errors === 'number') { - this.errors = context.errors; - } - if (context.status) { - this.status = context.status; + if (!session.did && !context.did) { + session.did = context.user.id || context.user.email || context.user.username; } } - /** JSDoc */ - public close(status?: Exclude): void { - if (status) { - this.update({ status }); - } else if (this.status === 'ok') { - this.update({ status: 'exited' }); - } else { - this.update(); - } + session.timestamp = context.timestamp || timestampInSeconds(); + + if (context.ignoreDuration) { + session.ignoreDuration = context.ignoreDuration; + } + if (context.sid) { + // Good enough uuid validation. — Kamil + session.sid = context.sid.length === 32 ? context.sid : uuid4(); } + if (context.init !== undefined) { + session.init = context.init; + } + if (!session.did && context.did) { + session.did = `${context.did}`; + } + if (typeof context.started === 'number') { + session.started = context.started; + } + if (session.ignoreDuration) { + session.duration = undefined; + } else if (typeof context.duration === 'number') { + session.duration = context.duration; + } else { + const duration = session.timestamp - session.started; + session.duration = duration >= 0 ? duration : 0; + } + if (context.release) { + session.release = context.release; + } + if (context.environment) { + session.environment = context.environment; + } + if (!session.ipAddress && context.ipAddress) { + session.ipAddress = context.ipAddress; + } + if (!session.userAgent && context.userAgent) { + session.userAgent = context.userAgent; + } + if (typeof context.errors === 'number') { + session.errors = context.errors; + } + if (context.status) { + session.status = context.status; + } +} - /** JSDoc */ - public toJSON(): { - init: boolean; - sid: string; - did?: string; - timestamp: string; - started: string; - duration?: number; - status: SessionStatus; - errors: number; - attrs?: { - release?: string; - environment?: string; - user_agent?: string; - ip_address?: string; - }; - } { - return dropUndefinedKeys({ - sid: `${this.sid}`, - init: this.init, - // Make sure that sec is converted to ms for date constructor - started: new Date(this.started * 1000).toISOString(), - timestamp: new Date(this.timestamp * 1000).toISOString(), - status: this.status, - errors: this.errors, - did: typeof this.did === 'number' || typeof this.did === 'string' ? `${this.did}` : undefined, - duration: this.duration, - attrs: { - release: this.release, - environment: this.environment, - ip_address: this.ipAddress, - user_agent: this.userAgent, - }, - }); +/** + * Closes a session by setting its status and updating the session object with it. + * Internally calls `updateSession` to update the passed session object. + * + * Note that this function mutates the passed session (@see updateSession for explanation). + * + * @param session the `Session` object to be closed + * @param status the `SessionStatus` with which the session was closed. If you don't pass a status, + * this function will keep the previously set status, unless it was `'ok'` in which case + * it is changed to `'exited'`. + */ +export function closeSession(session: Session, status?: Exclude): void { + let context = {}; + if (status) { + context = { status }; + } else if (session.status === 'ok') { + context = { status: 'exited' }; } + + updateSession(session, context); +} + +/** + * Serializes a passed session object to a JSON object with a slightly different structure. + * This is necessary because the Sentry backend requires a slightly different schema of a session + * than the one the JS SDKs use internally. + * + * @param session the session to be converted + * + * @returns a JSON object of the passed session + */ +function sessionToJSON(session: Session): SerializedSession { + return dropUndefinedKeys({ + sid: `${session.sid}`, + init: session.init, + // Make sure that sec is converted to ms for date constructor + started: new Date(session.started * 1000).toISOString(), + timestamp: new Date(session.timestamp * 1000).toISOString(), + status: session.status, + errors: session.errors, + did: typeof session.did === 'number' || typeof session.did === 'string' ? `${session.did}` : undefined, + duration: session.duration, + attrs: { + release: session.release, + environment: session.environment, + ip_address: session.ipAddress, + user_agent: session.userAgent, + }, + }); } diff --git a/packages/hub/src/sessionflusher.ts b/packages/hub/src/sessionflusher.ts index fcc4386c70d8..7b2bda98c4da 100644 --- a/packages/hub/src/sessionflusher.ts +++ b/packages/hub/src/sessionflusher.ts @@ -1,13 +1,6 @@ -import { - AggregationCounts, - RequestSessionStatus, - SessionAggregates, - SessionFlusherLike, - Transport, -} from '@sentry/types'; -import { dropUndefinedKeys, logger } from '@sentry/utils'; +import { AggregationCounts, Client, RequestSessionStatus, SessionAggregates, SessionFlusherLike } from '@sentry/types'; +import { dropUndefinedKeys } from '@sentry/utils'; -import { IS_DEBUG_BUILD } from './flags'; import { getCurrentHub } from './hub'; type ReleaseHealthAttributes = { @@ -24,34 +17,23 @@ export class SessionFlusher implements SessionFlusherLike { private _sessionAttrs: ReleaseHealthAttributes; private _intervalId: ReturnType; private _isEnabled: boolean = true; - private _transport: Transport; + private _client: Client; - public constructor(transport: Transport, attrs: ReleaseHealthAttributes) { - this._transport = transport; + public constructor(client: Client, attrs: ReleaseHealthAttributes) { + this._client = client; // Call to setInterval, so that flush is called every 60 seconds this._intervalId = setInterval(() => this.flush(), this.flushTimeout * 1000); this._sessionAttrs = attrs; } - /** Sends session aggregates to Transport */ - public sendSessionAggregates(sessionAggregates: SessionAggregates): void { - if (!this._transport.sendSession) { - IS_DEBUG_BUILD && logger.warn("Dropping session because custom transport doesn't implement sendSession"); - return; - } - void this._transport.sendSession(sessionAggregates).then(null, reason => { - IS_DEBUG_BUILD && logger.error('Error while sending session:', reason); - }); - } - - /** Checks if `pendingAggregates` has entries, and if it does flushes them by calling `sendSessions` */ + /** Checks if `pendingAggregates` has entries, and if it does flushes them by calling `sendSession` */ public flush(): void { const sessionAggregates = this.getSessionAggregates(); if (sessionAggregates.aggregates.length === 0) { return; } this._pendingAggregates = {}; - this.sendSessionAggregates(sessionAggregates); + this._client.sendSession(sessionAggregates); } /** Massages the entries in `pendingAggregates` and returns aggregated sessions */ diff --git a/packages/minimal/test/lib/minimal.test.ts b/packages/hub/test/exports.test.ts similarity index 91% rename from packages/minimal/test/lib/minimal.test.ts rename to packages/hub/test/exports.test.ts index 911c19ad1f68..91b7450b66eb 100644 --- a/packages/minimal/test/lib/minimal.test.ts +++ b/packages/hub/test/exports.test.ts @@ -1,8 +1,5 @@ -import { getCurrentHub, getHubFromCarrier, Scope } from '@sentry/hub'; -import { Severity } from '@sentry/types'; - +import { getCurrentHub, getHubFromCarrier, Scope } from '../src'; import { - _callOnClient, captureEvent, captureException, captureMessage, @@ -14,13 +11,30 @@ import { setTags, setUser, withScope, -} from '../../src'; -import { init, TestClient, TestClient2 } from '../mocks/client'; +} from '../src/exports'; + +export class TestClient { + public static instance?: TestClient; + + public constructor(public options: Record) { + TestClient.instance = this; + } + + public mySecretPublicMethod(str: string): string { + return `secret: ${str}`; + } +} + +export class TestClient2 {} + +export function init(options: Record): void { + getCurrentHub().bindClient(new TestClient(options) as any); +} // eslint-disable-next-line no-var declare var global: any; -describe('Minimal', () => { +describe('Top Level API', () => { beforeEach(() => { global.__SENTRY__ = { hub: undefined, @@ -165,8 +179,8 @@ describe('Minimal', () => { const client: any = new TestClient({}); const scope = getCurrentHub().pushScope(); getCurrentHub().bindClient(client); - scope.setLevel(Severity.Warning); - expect(global.__SENTRY__.hub._stack[1].scope._level).toEqual(Severity.Warning); + scope.setLevel('warning'); + expect(global.__SENTRY__.hub._stack[1].scope._level).toEqual('warning'); }); }); @@ -197,17 +211,6 @@ describe('Minimal', () => { expect(getCurrentHub().getClient()).toBe(TestClient.instance); }); - test('Calls function on the client', done => { - const s = jest.spyOn(TestClient.prototype, 'mySecretPublicMethod'); - getCurrentHub().withScope(() => { - getCurrentHub().bindClient(new TestClient({}) as any); - _callOnClient('mySecretPublicMethod', 'test'); - expect(s.mock.calls[0][0]).toBe('test'); - s.mockRestore(); - done(); - }); - }); - test('does not throw an error when pushing different clients', () => { init({}); expect(() => { @@ -245,16 +248,16 @@ describe('Minimal', () => { test('withScope', () => { withScope(scope => { - scope.setLevel(Severity.Warning); + scope.setLevel('warning'); scope.setFingerprint(['1']); withScope(scope2 => { - scope2.setLevel(Severity.Info); + scope2.setLevel('info'); scope2.setFingerprint(['2']); withScope(scope3 => { scope3.clear(); - expect(global.__SENTRY__.hub._stack[1].scope._level).toEqual(Severity.Warning); + expect(global.__SENTRY__.hub._stack[1].scope._level).toEqual('warning'); expect(global.__SENTRY__.hub._stack[1].scope._fingerprint).toEqual(['1']); - expect(global.__SENTRY__.hub._stack[2].scope._level).toEqual(Severity.Info); + expect(global.__SENTRY__.hub._stack[2].scope._level).toEqual('info'); expect(global.__SENTRY__.hub._stack[2].scope._fingerprint).toEqual(['2']); expect(global.__SENTRY__.hub._stack[3].scope._level).toBeUndefined(); }); diff --git a/packages/hub/test/global.test.ts b/packages/hub/test/global.test.ts index ec63679d5592..8dc32acb7fc7 100644 --- a/packages/hub/test/global.test.ts +++ b/packages/hub/test/global.test.ts @@ -1,5 +1,9 @@ +import { getGlobalObject } from '@sentry/utils'; + import { getCurrentHub, getHubFromCarrier, Hub } from '../src'; +const global = getGlobalObject(); + describe('global', () => { test('getGlobalHub', () => { expect(getCurrentHub()).toBeTruthy(); diff --git a/packages/hub/test/hub.test.ts b/packages/hub/test/hub.test.ts index 98c8dcea0636..443f56b19e11 100644 --- a/packages/hub/test/hub.test.ts +++ b/packages/hub/test/hub.test.ts @@ -1,4 +1,5 @@ -import { Event } from '@sentry/types'; +/* eslint-disable @typescript-eslint/unbound-method */ +import { Client, Event } from '@sentry/types'; import { getCurrentHub, Hub, Scope } from '../src'; @@ -15,7 +16,18 @@ function makeClient() { getIntegration: jest.fn(), setupIntegrations: jest.fn(), captureMessage: jest.fn(), - }; + } as unknown as Client; +} + +/** + * Return an array containing the arguments passed to the given mocked or spied-upon function. + * + * By default, the args passed to the first call of the function are returned, but it is also possible to retrieve the + * nth call by passing `callIndex`. If the function wasn't called, an error message is returned instead. + */ +function getPassedArgs(mock: (...args: any[]) => any, callIndex: number = 0): any[] { + const asMock = mock as jest.MockedFunction<(...args: any[]) => any>; + return asMock.mock.calls[callIndex] || ["Error: Function wasn't called."]; } describe('Hub', () => { @@ -40,13 +52,6 @@ describe('Hub', () => { expect(hub.getStack()).toHaveLength(1); }); - test("don't invoke client sync with wrong func", () => { - const hub = new Hub(clientFn); - // @ts-ignore we want to able to call private method - hub._invokeClient('funca', true); - expect(clientFn).not.toHaveBeenCalled(); - }); - test('isOlderThan', () => { const hub = new Hub(); expect(hub.isOlderThan(0)).toBeFalsy(); @@ -102,7 +107,7 @@ describe('Hub', () => { }); }); - test('inherit processors', () => { + test('inherit processors', async () => { expect.assertions(1); const event: Event = { extra: { b: 3 }, @@ -210,34 +215,45 @@ describe('Hub', () => { test('simple', () => { const testClient = makeClient(); const hub = new Hub(testClient); + hub.captureException('a'); - expect(testClient.captureException).toHaveBeenCalled(); - expect(testClient.captureException.mock.calls[0][0]).toBe('a'); + const args = getPassedArgs(testClient.captureException); + + expect(args[0]).toBe('a'); }); test('should set event_id in hint', () => { const testClient = makeClient(); const hub = new Hub(testClient); + hub.captureException('a'); - expect(testClient.captureException.mock.calls[0][1].event_id).toBeTruthy(); + const args = getPassedArgs(testClient.captureException); + + expect(args[1].event_id).toBeTruthy(); }); test('should keep event_id from hint', () => { const testClient = makeClient(); const hub = new Hub(testClient); const id = Math.random().toString(); + hub.captureException('a', { event_id: id }); - expect(testClient.captureException.mock.calls[0][1].event_id).toBe(id); + const args = getPassedArgs(testClient.captureException); + + expect(args[1].event_id).toBe(id); }); test('should generate hint if not provided in the call', () => { const testClient = makeClient(); const hub = new Hub(testClient); const ex = new Error('foo'); + hub.captureException(ex); - expect(testClient.captureException.mock.calls[0][1].originalException).toBe(ex); - expect(testClient.captureException.mock.calls[0][1].syntheticException).toBeInstanceOf(Error); - expect(testClient.captureException.mock.calls[0][1].syntheticException.message).toBe('Sentry syntheticException'); + const args = getPassedArgs(testClient.captureException); + + expect(args[1].originalException).toBe(ex); + expect(args[1].syntheticException).toBeInstanceOf(Error); + expect(args[1].syntheticException.message).toBe('Sentry syntheticException'); }); }); @@ -245,32 +261,44 @@ describe('Hub', () => { test('simple', () => { const testClient = makeClient(); const hub = new Hub(testClient); + hub.captureMessage('a'); - expect(testClient.captureMessage.mock.calls[0][0]).toBe('a'); + const args = getPassedArgs(testClient.captureMessage); + + expect(args[0]).toBe('a'); }); test('should set event_id in hint', () => { const testClient = makeClient(); const hub = new Hub(testClient); + hub.captureMessage('a'); - expect(testClient.captureMessage.mock.calls[0][2].event_id).toBeTruthy(); + const args = getPassedArgs(testClient.captureMessage); + + expect(args[2].event_id).toBeTruthy(); }); test('should keep event_id from hint', () => { const testClient = makeClient(); const hub = new Hub(testClient); const id = Math.random().toString(); + hub.captureMessage('a', undefined, { event_id: id }); - expect(testClient.captureMessage.mock.calls[0][2].event_id).toBe(id); + const args = getPassedArgs(testClient.captureMessage); + + expect(args[2].event_id).toBe(id); }); test('should generate hint if not provided in the call', () => { const testClient = makeClient(); const hub = new Hub(testClient); + hub.captureMessage('foo'); - expect(testClient.captureMessage.mock.calls[0][2].originalException).toBe('foo'); - expect(testClient.captureMessage.mock.calls[0][2].syntheticException).toBeInstanceOf(Error); - expect(testClient.captureMessage.mock.calls[0][2].syntheticException.message).toBe('foo'); + const args = getPassedArgs(testClient.captureMessage); + + expect(args[2].originalException).toBe('foo'); + expect(args[2].syntheticException).toBeInstanceOf(Error); + expect(args[2].syntheticException.message).toBe('foo'); }); }); @@ -281,8 +309,11 @@ describe('Hub', () => { }; const testClient = makeClient(); const hub = new Hub(testClient); + hub.captureEvent(event); - expect(testClient.captureEvent.mock.calls[0][0]).toBe(event); + const args = getPassedArgs(testClient.captureEvent); + + expect(args[0]).toBe(event); }); test('should set event_id in hint', () => { @@ -291,8 +322,11 @@ describe('Hub', () => { }; const testClient = makeClient(); const hub = new Hub(testClient); + hub.captureEvent(event); - expect(testClient.captureEvent.mock.calls[0][1].event_id).toBeTruthy(); + const args = getPassedArgs(testClient.captureEvent); + + expect(args[1].event_id).toBeTruthy(); }); test('should keep event_id from hint', () => { @@ -302,8 +336,11 @@ describe('Hub', () => { const testClient = makeClient(); const hub = new Hub(testClient); const id = Math.random().toString(); + hub.captureEvent(event, { event_id: id }); - expect(testClient.captureEvent.mock.calls[0][1].event_id).toBe(id); + const args = getPassedArgs(testClient.captureEvent); + + expect(args[1].event_id).toBe(id); }); test('sets lastEventId', () => { @@ -312,8 +349,11 @@ describe('Hub', () => { }; const testClient = makeClient(); const hub = new Hub(testClient); + hub.captureEvent(event); - expect(testClient.captureEvent.mock.calls[0][1].event_id).toEqual(hub.lastEventId()); + const args = getPassedArgs(testClient.captureEvent); + + expect(args[1].event_id).toEqual(hub.lastEventId()); }); test('transactions do not set lastEventId', () => { @@ -323,8 +363,11 @@ describe('Hub', () => { }; const testClient = makeClient(); const hub = new Hub(testClient); + hub.captureEvent(event); - expect(testClient.captureEvent.mock.calls[0][1].event_id).not.toEqual(hub.lastEventId()); + const args = getPassedArgs(testClient.captureEvent); + + expect(args[1].event_id).not.toEqual(hub.lastEventId()); }); }); diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts index 62f2e112f7fe..1bad52db6d9f 100644 --- a/packages/hub/test/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -1,4 +1,4 @@ -import { Event, EventHint, Severity } from '@sentry/types'; +import { Event, EventHint, RequestSessionStatus } from '@sentry/types'; import { getGlobalObject } from '@sentry/utils'; import { addGlobalEventProcessor, Scope } from '../src'; @@ -85,8 +85,8 @@ describe('Scope', () => { test('setLevel', () => { const scope = new Scope(); - scope.setLevel(Severity.Critical); - expect((scope as any)._level).toEqual(Severity.Critical); + scope.setLevel('fatal'); + expect((scope as any)._level).toEqual('fatal'); }); test('setTransactionName', () => { @@ -137,8 +137,8 @@ describe('Scope', () => { test('chaining', () => { const scope = new Scope(); - scope.setLevel(Severity.Critical).setUser({ id: '1' }); - expect((scope as any)._level).toEqual(Severity.Critical); + scope.setLevel('fatal').setUser({ id: '1' }); + expect((scope as any)._level).toEqual('fatal'); expect((scope as any)._user).toEqual({ id: '1' }); }); }); @@ -194,7 +194,7 @@ describe('Scope', () => { }); describe('applyToEvent', () => { - test('basic usage', () => { + test('basic usage', async () => { expect.assertions(9); const scope = new Scope(); @@ -202,7 +202,7 @@ describe('Scope', () => { scope.setTag('a', 'b'); scope.setUser({ id: '1' }); scope.setFingerprint(['abcd']); - scope.setLevel(Severity.Warning); + scope.setLevel('warning'); scope.setTransactionName('/abc'); scope.addBreadcrumb({ message: 'test' }); scope.setContext('os', { id: '1' }); @@ -222,7 +222,7 @@ describe('Scope', () => { }); }); - test('merge with existing event data', () => { + test('merge with existing event data', async () => { expect.assertions(8); const scope = new Scope(); scope.setExtra('a', 2); @@ -291,18 +291,18 @@ describe('Scope', () => { }); }); - test('scope level should have priority over event level', () => { + test('scope level should have priority over event level', async () => { expect.assertions(1); const scope = new Scope(); - scope.setLevel(Severity.Warning); + scope.setLevel('warning'); const event: Event = {}; - event.level = Severity.Critical; + event.level = 'fatal'; return scope.applyToEvent(event).then(processedEvent => { - expect(processedEvent!.level).toEqual(Severity.Warning); + expect(processedEvent!.level).toEqual('warning'); }); }); - test('scope transaction should have priority over event transaction', () => { + test('scope transaction should have priority over event transaction', async () => { expect.assertions(1); const scope = new Scope(); scope.setTransactionName('/abc'); @@ -410,7 +410,7 @@ describe('Scope', () => { scope.setContext('foo', { id: '1' }); scope.setContext('bar', { id: '2' }); scope.setUser({ id: '1337' }); - scope.setLevel(Severity.Info); + scope.setLevel('info'); scope.setFingerprint(['foo']); scope.setRequestSession({ status: 'ok' }); }); @@ -458,7 +458,7 @@ describe('Scope', () => { localScope.setContext('bar', { id: '3' }); localScope.setContext('baz', { id: '4' }); localScope.setUser({ id: '42' }); - localScope.setLevel(Severity.Warning); + localScope.setLevel('warning'); localScope.setFingerprint(['bar']); (localScope as any)._requestSession = { status: 'ok' }; @@ -511,10 +511,10 @@ describe('Scope', () => { contexts: { bar: { id: '3' }, baz: { id: '4' } }, extra: { bar: '3', baz: '4' }, fingerprint: ['bar'], - level: 'warning', + level: 'warning' as const, tags: { bar: '3', baz: '4' }, user: { id: '42' }, - requestSession: { status: 'errored' }, + requestSession: { status: 'errored' as RequestSessionStatus }, }; const updatedScope = scope.update(localAttributes) as any; @@ -541,7 +541,7 @@ describe('Scope', () => { }); describe('addEventProcessor', () => { - test('should allow for basic event manipulation', () => { + test('should allow for basic event manipulation', async () => { expect.assertions(3); const event: Event = { extra: { b: 3 }, @@ -566,7 +566,7 @@ describe('Scope', () => { }); }); - test('should work alongside global event processors', () => { + test('should work alongside global event processors', async () => { expect.assertions(3); const event: Event = { extra: { b: 3 }, @@ -667,7 +667,7 @@ describe('Scope', () => { }); }); - test('should drop an event when any of processors return null', () => { + test('should drop an event when any of processors return null', async () => { expect.assertions(1); const event: Event = { extra: { b: 3 }, @@ -680,7 +680,7 @@ describe('Scope', () => { }); }); - test('should have an access to the EventHint', () => { + test('should have an access to the EventHint', async () => { expect.assertions(3); const event: Event = { extra: { b: 3 }, diff --git a/packages/hub/test/session.test.ts b/packages/hub/test/session.test.ts index f25e5ad4189b..f57267fd11c0 100644 --- a/packages/hub/test/session.test.ts +++ b/packages/hub/test/session.test.ts @@ -1,11 +1,12 @@ import { SessionContext } from '@sentry/types'; import { timestampInSeconds } from '@sentry/utils'; -import { Session } from '../src/session'; +import { closeSession, makeSession, updateSession } from '../src/session'; describe('Session', () => { it('initializes with the proper defaults', () => { - const session = new Session().toJSON(); + const newSession = makeSession(); + const session = newSession.toJSON(); // Grab current year to check if we are converting from sec -> ms correctly const currentYear = new Date(timestampInSeconds() * 1000).toISOString().slice(0, 4); @@ -82,36 +83,39 @@ describe('Session', () => { // the out variable in a test explicitly refers to it. const DEFAULT_OUT = { duration: expect.any(Number), timestamp: expect.any(String) }; - const session = new Session(); + const session = makeSession(); const initSessionProps = session.toJSON(); - session.update(test[1]); - expect(session.toJSON()).toEqual({ ...initSessionProps, ...DEFAULT_OUT, ...test[2] }); + updateSession(session, test[1]); + const updatedSessionProps = session.toJSON(); + + expect(updatedSessionProps).toEqual({ ...initSessionProps, ...DEFAULT_OUT, ...test[2] }); }); }); describe('close', () => { it('exits a normal session', () => { - const session = new Session(); + const session = makeSession(); expect(session.status).toEqual('ok'); - session.close(); + + closeSession(session); expect(session.status).toEqual('exited'); }); it('updates session status when give status', () => { - const session = new Session(); + const session = makeSession(); expect(session.status).toEqual('ok'); - session.close('abnormal'); + closeSession(session, 'abnormal'); expect(session.status).toEqual('abnormal'); }); it('only changes status ok to exited', () => { - const session = new Session(); - session.update({ status: 'crashed' }); + const session = makeSession(); + updateSession(session, { status: 'crashed' }); expect(session.status).toEqual('crashed'); - session.close(); + closeSession(session, 'crashed'); expect(session.status).toEqual('crashed'); }); }); diff --git a/packages/hub/test/sessionflusher.test.ts b/packages/hub/test/sessionflusher.test.ts index 3c7dc9782615..58ce3ee374ce 100644 --- a/packages/hub/test/sessionflusher.test.ts +++ b/packages/hub/test/sessionflusher.test.ts @@ -1,21 +1,17 @@ +import { Client } from '@sentry/types'; + import { SessionFlusher } from '../src/sessionflusher'; describe('Session Flusher', () => { let sendSession: jest.Mock; - let transport: { - sendEvent: jest.Mock; - sendSession: jest.Mock; - close: jest.Mock; - }; + let mockClient: Client; beforeEach(() => { jest.useFakeTimers(); sendSession = jest.fn(() => Promise.resolve({ status: 'success' })); - transport = { - sendEvent: jest.fn(), + mockClient = { sendSession, - close: jest.fn(), - }; + } as unknown as Client; }); afterEach(() => { @@ -23,7 +19,7 @@ describe('Session Flusher', () => { }); test('test incrementSessionStatusCount updates the internal SessionFlusher state', () => { - const flusher = new SessionFlusher(transport, { release: '1.0.0', environment: 'dev' }); + const flusher = new SessionFlusher(mockClient, { release: '1.0.0', environment: 'dev' }); const date = new Date('2021-04-08T12:18:23.043Z'); let count = (flusher as any)._incrementSessionStatusCount('ok', date); @@ -46,7 +42,7 @@ describe('Session Flusher', () => { }); test('test undefined attributes are excluded, on incrementSessionStatusCount call', () => { - const flusher = new SessionFlusher(transport, { release: '1.0.0' }); + const flusher = new SessionFlusher(mockClient, { release: '1.0.0' }); const date = new Date('2021-04-08T12:18:23.043Z'); (flusher as any)._incrementSessionStatusCount('ok', date); @@ -59,7 +55,7 @@ describe('Session Flusher', () => { }); test('flush is called every 60 seconds after initialisation of an instance of SessionFlusher', () => { - const flusher = new SessionFlusher(transport, { release: '1.0.0', environment: 'dev' }); + const flusher = new SessionFlusher(mockClient, { release: '1.0.0', environment: 'dev' }); const flusherFlushFunc = jest.spyOn(flusher, 'flush'); jest.advanceTimersByTime(59000); expect(flusherFlushFunc).toHaveBeenCalledTimes(0); @@ -72,7 +68,7 @@ describe('Session Flusher', () => { }); test('sendSessions is called on flush if sessions were captured', () => { - const flusher = new SessionFlusher(transport, { release: '1.0.0', environment: 'dev' }); + const flusher = new SessionFlusher(mockClient, { release: '1.0.0', environment: 'dev' }); const flusherFlushFunc = jest.spyOn(flusher, 'flush'); const date = new Date('2021-04-08T12:18:23.043Z'); (flusher as any)._incrementSessionStatusCount('ok', date); @@ -92,7 +88,7 @@ describe('Session Flusher', () => { }); test('sendSessions is not called on flush if no sessions were captured', () => { - const flusher = new SessionFlusher(transport, { release: '1.0.0', environment: 'dev' }); + const flusher = new SessionFlusher(mockClient, { release: '1.0.0', environment: 'dev' }); const flusherFlushFunc = jest.spyOn(flusher, 'flush'); expect(sendSession).toHaveBeenCalledTimes(0); @@ -102,13 +98,13 @@ describe('Session Flusher', () => { }); test('calling close on SessionFlusher should disable SessionFlusher', () => { - const flusher = new SessionFlusher(transport, { release: '1.0.x' }); + const flusher = new SessionFlusher(mockClient, { release: '1.0.x' }); flusher.close(); expect((flusher as any)._isEnabled).toEqual(false); }); test('calling close on SessionFlusher will force call flush', () => { - const flusher = new SessionFlusher(transport, { release: '1.0.x' }); + const flusher = new SessionFlusher(mockClient, { release: '1.0.x' }); const flusherFlushFunc = jest.spyOn(flusher, 'flush'); const date = new Date('2021-04-08T12:18:23.043Z'); (flusher as any)._incrementSessionStatusCount('ok', date); diff --git a/packages/hub/tsconfig.cjs.json b/packages/hub/tsconfig.cjs.json deleted file mode 100644 index e3a918fc70af..000000000000 --- a/packages/hub/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "build/dist" - } -} diff --git a/packages/hub/tsconfig.esm.json b/packages/hub/tsconfig.esm.json deleted file mode 100644 index 0b86c52918cc..000000000000 --- a/packages/hub/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "build/esm" - } -} diff --git a/packages/integration-tests/.gitignore b/packages/integration-tests/.gitignore deleted file mode 100644 index 1521c8b7652b..000000000000 --- a/packages/integration-tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 3cec389dfaf3..fc69c3e4a72f 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-integration-tests", - "version": "6.19.7", + "version": "7.0.0-rc.0", "main": "index.js", "license": "MIT", "engines": { diff --git a/packages/integration-tests/suites/new-transports/fetch-captureException/subject.js b/packages/integration-tests/suites/new-transports/fetch-captureException/subject.js deleted file mode 100644 index 9cc217bdb087..000000000000 --- a/packages/integration-tests/suites/new-transports/fetch-captureException/subject.js +++ /dev/null @@ -1 +0,0 @@ -Sentry.captureException(new Error('this is an error')); diff --git a/packages/integration-tests/suites/new-transports/fetch-captureException/test.ts b/packages/integration-tests/suites/new-transports/fetch-captureException/test.ts deleted file mode 100644 index cb92e50e2dc5..000000000000 --- a/packages/integration-tests/suites/new-transports/fetch-captureException/test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; - -import { sentryTest } from '../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; - -sentryTest('should capture an error with the new fetch transport', async ({ getLocalTestPath, page }) => { - const url = await getLocalTestPath({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.exception?.values).toHaveLength(1); - expect(eventData.exception?.values?.[0]).toMatchObject({ - type: 'Error', - value: 'this is an error', - mechanism: { - type: 'generic', - handled: true, - }, - }); -}); diff --git a/packages/integration-tests/suites/new-transports/fetch-startTransaction/subject.js b/packages/integration-tests/suites/new-transports/fetch-startTransaction/subject.js deleted file mode 100644 index 78c7c33c654c..000000000000 --- a/packages/integration-tests/suites/new-transports/fetch-startTransaction/subject.js +++ /dev/null @@ -1,2 +0,0 @@ -const transaction = Sentry.startTransaction({ name: 'test_transaction_1' }); -transaction.finish(); diff --git a/packages/integration-tests/suites/new-transports/fetch-startTransaction/test.ts b/packages/integration-tests/suites/new-transports/fetch-startTransaction/test.ts deleted file mode 100644 index 8daef2e06b54..000000000000 --- a/packages/integration-tests/suites/new-transports/fetch-startTransaction/test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; - -import { sentryTest } from '../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; - -sentryTest('should report a transaction with the new fetch transport', async ({ getLocalTestPath, page }) => { - const url = await getLocalTestPath({ testDir: __dirname }); - const transaction = await getFirstSentryEnvelopeRequest(page, url); - - expect(transaction.transaction).toBe('test_transaction_1'); - expect(transaction.spans).toBeDefined(); -}); diff --git a/packages/integration-tests/suites/new-transports/init.js b/packages/integration-tests/suites/new-transports/init.js deleted file mode 100644 index 6cc8110c0475..000000000000 --- a/packages/integration-tests/suites/new-transports/init.js +++ /dev/null @@ -1,13 +0,0 @@ -import * as Sentry from '@sentry/browser'; -// eslint-disable-next-line no-unused-vars -import * as _ from '@sentry/tracing'; - -window.Sentry = Sentry; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - _experiments: { - newTransport: true, - }, - tracesSampleRate: 1.0, -}); diff --git a/packages/integration-tests/suites/new-transports/xhr-captureException/subject.js b/packages/integration-tests/suites/new-transports/xhr-captureException/subject.js deleted file mode 100644 index e42102004dad..000000000000 --- a/packages/integration-tests/suites/new-transports/xhr-captureException/subject.js +++ /dev/null @@ -1,4 +0,0 @@ -// deactivate fetch s.t. the SDK falls back to XHR transport -window.fetch = undefined; - -Sentry.captureException(new Error('this is an error')); diff --git a/packages/integration-tests/suites/new-transports/xhr-captureException/test.ts b/packages/integration-tests/suites/new-transports/xhr-captureException/test.ts deleted file mode 100644 index cb92e50e2dc5..000000000000 --- a/packages/integration-tests/suites/new-transports/xhr-captureException/test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; - -import { sentryTest } from '../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; - -sentryTest('should capture an error with the new fetch transport', async ({ getLocalTestPath, page }) => { - const url = await getLocalTestPath({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.exception?.values).toHaveLength(1); - expect(eventData.exception?.values?.[0]).toMatchObject({ - type: 'Error', - value: 'this is an error', - mechanism: { - type: 'generic', - handled: true, - }, - }); -}); diff --git a/packages/integration-tests/suites/new-transports/xhr-startTransaction/subject.js b/packages/integration-tests/suites/new-transports/xhr-startTransaction/subject.js deleted file mode 100644 index 444e095ed3a1..000000000000 --- a/packages/integration-tests/suites/new-transports/xhr-startTransaction/subject.js +++ /dev/null @@ -1,5 +0,0 @@ -// deactivate fetch s.t. the SDK falls back to XHR transport -window.fetch = undefined; - -const transaction = Sentry.startTransaction({ name: 'test_transaction_1' }); -transaction.finish(); diff --git a/packages/integration-tests/suites/new-transports/xhr-startTransaction/test.ts b/packages/integration-tests/suites/new-transports/xhr-startTransaction/test.ts deleted file mode 100644 index 59ddfb00c6a1..000000000000 --- a/packages/integration-tests/suites/new-transports/xhr-startTransaction/test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; - -import { sentryTest } from '../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; - -sentryTest('should report a transaction with the new XHR transport', async ({ getLocalTestPath, page }) => { - const url = await getLocalTestPath({ testDir: __dirname }); - const transaction = await getFirstSentryEnvelopeRequest(page, url); - - expect(transaction.transaction).toBe('test_transaction_1'); - expect(transaction.spans).toBeDefined(); -}); diff --git a/packages/integration-tests/suites/public-api/addBreadcrumb/empty_obj/test.ts b/packages/integration-tests/suites/public-api/addBreadcrumb/empty_obj/test.ts index e6e2b9a8f4dd..3fea4283b71e 100644 --- a/packages/integration-tests/suites/public-api/addBreadcrumb/empty_obj/test.ts +++ b/packages/integration-tests/suites/public-api/addBreadcrumb/empty_obj/test.ts @@ -1,14 +1,15 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest( 'should add an empty breadcrumb initialized with a timestamp, when an empty object is given', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.breadcrumbs).toHaveLength(1); expect(eventData.breadcrumbs?.[0]).toMatchObject({ diff --git a/packages/integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts b/packages/integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts index c7bca64aafd5..d864be4f9073 100644 --- a/packages/integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts +++ b/packages/integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should add multiple breadcrumbs', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.breadcrumbs).toHaveLength(2); expect(eventData.breadcrumbs?.[0]).toMatchObject({ diff --git a/packages/integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts b/packages/integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts index 98e36a254076..224d4dba0932 100644 --- a/packages/integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts +++ b/packages/integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should add a simple breadcrumb', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.breadcrumbs).toHaveLength(1); expect(eventData.breadcrumbs?.[0]).toMatchObject({ diff --git a/packages/integration-tests/suites/public-api/addBreadcrumb/undefined_arg/test.ts b/packages/integration-tests/suites/public-api/addBreadcrumb/undefined_arg/test.ts index b41f527c58ed..5e5ec669a7dc 100644 --- a/packages/integration-tests/suites/public-api/addBreadcrumb/undefined_arg/test.ts +++ b/packages/integration-tests/suites/public-api/addBreadcrumb/undefined_arg/test.ts @@ -1,14 +1,15 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest( 'should add an empty breadcrumb initialized with a timestamp, when no argument is given', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.breadcrumbs).toHaveLength(1); expect(eventData.breadcrumbs?.[0]).toMatchObject({ diff --git a/packages/integration-tests/suites/public-api/captureException/empty_obj/test.ts b/packages/integration-tests/suites/public-api/captureException/empty_obj/test.ts index 2606b2984d08..a41fdcc6a6e1 100644 --- a/packages/integration-tests/suites/public-api/captureException/empty_obj/test.ts +++ b/packages/integration-tests/suites/public-api/captureException/empty_obj/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should capture an empty object', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.exception?.values).toHaveLength(1); expect(eventData.exception?.values?.[0]).toMatchObject({ diff --git a/packages/integration-tests/suites/public-api/captureException/simple_error/test.ts b/packages/integration-tests/suites/public-api/captureException/simple_error/test.ts index f52e951c20c6..49627e826726 100644 --- a/packages/integration-tests/suites/public-api/captureException/simple_error/test.ts +++ b/packages/integration-tests/suites/public-api/captureException/simple_error/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should capture a simple error with message', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.exception?.values).toHaveLength(1); expect(eventData.exception?.values?.[0]).toMatchObject({ diff --git a/packages/integration-tests/suites/public-api/captureException/undefined_arg/test.ts b/packages/integration-tests/suites/public-api/captureException/undefined_arg/test.ts index 021af6f922f3..52e2ef5c21f8 100644 --- a/packages/integration-tests/suites/public-api/captureException/undefined_arg/test.ts +++ b/packages/integration-tests/suites/public-api/captureException/undefined_arg/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should capture an undefined error when no arguments are provided', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.exception?.values).toHaveLength(1); expect(eventData.exception?.values?.[0]).toMatchObject({ diff --git a/packages/integration-tests/suites/public-api/captureMessage/simple_message/test.ts b/packages/integration-tests/suites/public-api/captureMessage/simple_message/test.ts index 7b4b68f228d6..cfd5580653ac 100644 --- a/packages/integration-tests/suites/public-api/captureMessage/simple_message/test.ts +++ b/packages/integration-tests/suites/public-api/captureMessage/simple_message/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should capture a simple message string', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.message).toBe('foo'); expect(eventData.level).toBe('info'); diff --git a/packages/integration-tests/suites/public-api/captureMessage/with_level/subject.js b/packages/integration-tests/suites/public-api/captureMessage/with_level/subject.js index 3d7368c95fef..0d12e7115a7e 100644 --- a/packages/integration-tests/suites/public-api/captureMessage/with_level/subject.js +++ b/packages/integration-tests/suites/public-api/captureMessage/with_level/subject.js @@ -3,5 +3,4 @@ Sentry.captureMessage('info_message', 'info'); Sentry.captureMessage('warning_message', 'warning'); Sentry.captureMessage('error_message', 'error'); Sentry.captureMessage('fatal_message', 'fatal'); -Sentry.captureMessage('critical_message', 'critical'); Sentry.captureMessage('log_message', 'log'); diff --git a/packages/integration-tests/suites/public-api/captureMessage/with_level/test.ts b/packages/integration-tests/suites/public-api/captureMessage/with_level/test.ts index ba8bb18d729a..da17ff07a77e 100644 --- a/packages/integration-tests/suites/public-api/captureMessage/with_level/test.ts +++ b/packages/integration-tests/suites/public-api/captureMessage/with_level/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getMultipleSentryRequests } from '../../../../utils/helpers'; +import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; sentryTest('should capture with different severity levels', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const events = await getMultipleSentryRequests(page, 7, url); + const events = await getMultipleSentryEnvelopeRequests(page, 6, { url }); expect(events[0].message).toBe('debug_message'); expect(events[0].level).toBe('debug'); @@ -23,9 +24,6 @@ sentryTest('should capture with different severity levels', async ({ getLocalTes expect(events[4].message).toBe('fatal_message'); expect(events[4].level).toBe('fatal'); - expect(events[5].message).toBe('critical_message'); - expect(events[5].level).toBe('critical'); - - expect(events[6].message).toBe('log_message'); - expect(events[6].level).toBe('log'); + expect(events[5].message).toBe('log_message'); + expect(events[5].level).toBe('log'); }); diff --git a/packages/integration-tests/suites/public-api/configureScope/clear_scope/test.ts b/packages/integration-tests/suites/public-api/configureScope/clear_scope/test.ts index 4bbb0faf4c56..8626cf1adf66 100644 --- a/packages/integration-tests/suites/public-api/configureScope/clear_scope/test.ts +++ b/packages/integration-tests/suites/public-api/configureScope/clear_scope/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should clear previously set properties of a scope', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); // TODO: This is to compensate for a temporary debugging hack which adds data the tests aren't anticipating to the // event. The code can be restored to its original form (the commented-out line below) once that hack is diff --git a/packages/integration-tests/suites/public-api/configureScope/set_properties/test.ts b/packages/integration-tests/suites/public-api/configureScope/set_properties/test.ts index d4001c317d05..992dd7c31043 100644 --- a/packages/integration-tests/suites/public-api/configureScope/set_properties/test.ts +++ b/packages/integration-tests/suites/public-api/configureScope/set_properties/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should set different properties of a scope', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.message).toBe('configured_scope'); expect(eventData.user).toMatchObject({ id: 'baz' }); diff --git a/packages/integration-tests/suites/public-api/instrumentation/eventListener-instrumentation-behaviour/subject.js b/packages/integration-tests/suites/public-api/instrumentation/eventListener-instrumentation-behaviour/subject.js new file mode 100644 index 000000000000..32f1d09378b0 --- /dev/null +++ b/packages/integration-tests/suites/public-api/instrumentation/eventListener-instrumentation-behaviour/subject.js @@ -0,0 +1,21 @@ +// Simple function event listener +const functionListener = () => { + functionListenerCallback(); +}; + +// Attach event listener twice +window.addEventListener('click', functionListener); +window.addEventListener('click', functionListener); + +// Event listener that has handleEvent() method: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#listener +class EventHandlerClass { + handleEvent() { + objectListenerCallback(); + } +} + +const objectListener = new EventHandlerClass(); + +// Attach event listener twice +window.addEventListener('click', objectListener); +window.addEventListener('click', objectListener); diff --git a/packages/integration-tests/suites/public-api/instrumentation/eventListener-instrumentation-behaviour/test.ts b/packages/integration-tests/suites/public-api/instrumentation/eventListener-instrumentation-behaviour/test.ts new file mode 100644 index 000000000000..b6d2e6fa9231 --- /dev/null +++ b/packages/integration-tests/suites/public-api/instrumentation/eventListener-instrumentation-behaviour/test.ts @@ -0,0 +1,28 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; + +sentryTest( + 'Event listener instrumentation should attach the same event listener only once', + async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + await page.goto(url); + + let functionListenerCalls = 0; + await page.exposeFunction('functionListenerCallback', () => { + functionListenerCalls = functionListenerCalls + 1; + }); + + let objectListenerCalls = 0; + await page.exposeFunction('objectListenerCallback', () => { + objectListenerCalls = objectListenerCalls + 1; + }); + + // Trigger event listeners twice + await page.evaluate('document.body.click()'); + await page.evaluate('document.body.click()'); + + expect(functionListenerCalls).toBe(2); + expect(objectListenerCalls).toBe(2); + }, +); diff --git a/packages/integration-tests/suites/public-api/instrumentation/eventListener-this-preservation/subject.js b/packages/integration-tests/suites/public-api/instrumentation/eventListener-this-preservation/subject.js new file mode 100644 index 000000000000..7f5d2cc290e1 --- /dev/null +++ b/packages/integration-tests/suites/public-api/instrumentation/eventListener-this-preservation/subject.js @@ -0,0 +1,24 @@ +const btn = document.createElement('button'); +btn.id = 'btn'; +document.body.appendChild(btn); + +const functionListener = function () { + functionCallback(this.constructor.name); +}; + +class EventHandlerClass { + handleEvent() { + classInstanceCallback(this.constructor.name); + } +} +const objectListener = new EventHandlerClass(); + +// Attach event listeners a few times for good measure + +btn.addEventListener('click', functionListener); +btn.addEventListener('click', functionListener); +btn.addEventListener('click', functionListener); + +btn.addEventListener('click', objectListener); +btn.addEventListener('click', objectListener); +btn.addEventListener('click', objectListener); diff --git a/packages/integration-tests/suites/public-api/instrumentation/eventListener-this-preservation/test.ts b/packages/integration-tests/suites/public-api/instrumentation/eventListener-this-preservation/test.ts new file mode 100644 index 000000000000..e6304858eed4 --- /dev/null +++ b/packages/integration-tests/suites/public-api/instrumentation/eventListener-this-preservation/test.ts @@ -0,0 +1,24 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; + +sentryTest('Event listener instrumentation preserves "this" context', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + await page.goto(url); + + let assertions = 0; + + await page.exposeFunction('functionCallback', (thisInstanceName: unknown) => { + expect(thisInstanceName).toBe('HTMLButtonElement'); + assertions = assertions + 1; + }); + + await page.exposeFunction('classInstanceCallback', (thisInstanceName: unknown) => { + expect(thisInstanceName).toBe('EventHandlerClass'); + assertions = assertions + 1; + }); + + await page.evaluate('document.getElementById("btn").click()'); + + expect(assertions).toBe(2); +}); diff --git a/packages/integration-tests/suites/public-api/instrumentation/eventListener-wrapping/subject.js b/packages/integration-tests/suites/public-api/instrumentation/eventListener-wrapping/subject.js new file mode 100644 index 000000000000..289068737dc2 --- /dev/null +++ b/packages/integration-tests/suites/public-api/instrumentation/eventListener-wrapping/subject.js @@ -0,0 +1,18 @@ +// Simple function event listener +const functionListener = () => { + reportFunctionListenerStackHeight(new Error().stack.split('\n').length); +}; + +// Event listener that has handleEvent() method: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#listener +class EventHandlerClass { + handleEvent() { + reportObjectListenerStackHeight(new Error().stack.split('\n').length); + } +} + +const objectListener = new EventHandlerClass(); + +window.attachListeners = function () { + window.addEventListener('click', functionListener); + window.addEventListener('click', objectListener); +}; diff --git a/packages/integration-tests/suites/public-api/instrumentation/eventListener-wrapping/test.ts b/packages/integration-tests/suites/public-api/instrumentation/eventListener-wrapping/test.ts new file mode 100644 index 000000000000..cf77132c98df --- /dev/null +++ b/packages/integration-tests/suites/public-api/instrumentation/eventListener-wrapping/test.ts @@ -0,0 +1,38 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; + +sentryTest( + 'Event listener instrumentation should not wrap event listeners multiple times', + async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + await page.goto(url); + + const functionListenerStackHeights: number[] = []; + const objectListenerStackHeights: number[] = []; + + await page.exposeFunction('reportFunctionListenerStackHeight', (height: number) => { + functionListenerStackHeights.push(height); + }); + + await page.exposeFunction('reportObjectListenerStackHeight', (height: number) => { + objectListenerStackHeights.push(height); + }); + + // Attach initial listeners + await page.evaluate('window.attachListeners()'); + await page.evaluate('document.body.click()'); + + await page.evaluate('window.attachListeners()'); + await page.evaluate('window.attachListeners()'); + await page.evaluate('window.attachListeners()'); + await page.evaluate('document.body.click()'); + + expect(functionListenerStackHeights).toHaveLength(2); + expect(objectListenerStackHeights).toHaveLength(2); + + // check if all error stack traces are the same height + expect(functionListenerStackHeights.every((val, _i, arr) => val === arr[0])).toBeTruthy(); + expect(objectListenerStackHeights.every((val, _i, arr) => val === arr[0])).toBeTruthy(); + }, +); diff --git a/packages/integration-tests/suites/public-api/instrumentation/eventListener/subject.js b/packages/integration-tests/suites/public-api/instrumentation/eventListener/subject.js new file mode 100644 index 000000000000..a5e052885a4a --- /dev/null +++ b/packages/integration-tests/suites/public-api/instrumentation/eventListener/subject.js @@ -0,0 +1,5 @@ +window.addEventListener('click', () => { + throw new Error('event_listener_error'); +}); + +document.body.click(); diff --git a/packages/integration-tests/suites/public-api/instrumentation/eventListener/test.ts b/packages/integration-tests/suites/public-api/instrumentation/eventListener/test.ts new file mode 100644 index 000000000000..71eb3e7023b3 --- /dev/null +++ b/packages/integration-tests/suites/public-api/instrumentation/eventListener/test.ts @@ -0,0 +1,27 @@ +import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest( + 'Event listener instrumentation should capture an error thrown in an event handler', + async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.exception?.values).toHaveLength(1); + expect(eventData.exception?.values?.[0]).toMatchObject({ + type: 'Error', + value: 'event_listener_error', + mechanism: { + type: 'instrument', + handled: true, + }, + stacktrace: { + frames: expect.any(Array), + }, + }); + }, +); diff --git a/packages/integration-tests/suites/public-api/instrumentation/init.js b/packages/integration-tests/suites/public-api/instrumentation/init.js new file mode 100644 index 000000000000..d8c94f36fdd0 --- /dev/null +++ b/packages/integration-tests/suites/public-api/instrumentation/init.js @@ -0,0 +1,7 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', +}); diff --git a/packages/integration-tests/suites/public-api/setContext/multiple_contexts/test.ts b/packages/integration-tests/suites/public-api/setContext/multiple_contexts/test.ts index 96eecdd38662..6d00519cbad4 100644 --- a/packages/integration-tests/suites/public-api/setContext/multiple_contexts/test.ts +++ b/packages/integration-tests/suites/public-api/setContext/multiple_contexts/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should record multiple contexts', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.message).toBe('multiple_contexts'); expect(eventData.contexts).toMatchObject({ diff --git a/packages/integration-tests/suites/public-api/setContext/non_serializable_context/test.ts b/packages/integration-tests/suites/public-api/setContext/non_serializable_context/test.ts index fe67bdaff3e7..54fea2c68908 100644 --- a/packages/integration-tests/suites/public-api/setContext/non_serializable_context/test.ts +++ b/packages/integration-tests/suites/public-api/setContext/non_serializable_context/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should normalize non-serializable context', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.contexts?.non_serializable).toMatchObject({}); expect(eventData.message).toBe('non_serializable'); diff --git a/packages/integration-tests/suites/public-api/setContext/simple_context/test.ts b/packages/integration-tests/suites/public-api/setContext/simple_context/test.ts index 05f534888796..a39f838f5b18 100644 --- a/packages/integration-tests/suites/public-api/setContext/simple_context/test.ts +++ b/packages/integration-tests/suites/public-api/setContext/simple_context/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should set a simple context', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.message).toBe('simple_context_object'); expect(eventData.contexts).toMatchObject({ diff --git a/packages/integration-tests/suites/public-api/setExtra/multiple_extras/test.ts b/packages/integration-tests/suites/public-api/setExtra/multiple_extras/test.ts index 13d8aa83d9c4..82a2b4ce21e5 100644 --- a/packages/integration-tests/suites/public-api/setExtra/multiple_extras/test.ts +++ b/packages/integration-tests/suites/public-api/setExtra/multiple_extras/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should record multiple extras of different types', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.message).toBe('multiple_extras'); expect(eventData.extra).toMatchObject({ extra_1: { foo: 'bar', baz: { qux: 'quux' } }, extra_2: false }); diff --git a/packages/integration-tests/suites/public-api/setExtra/non_serializable_extra/test.ts b/packages/integration-tests/suites/public-api/setExtra/non_serializable_extra/test.ts index eaa9d342e4e8..168bfc88e2c5 100644 --- a/packages/integration-tests/suites/public-api/setExtra/non_serializable_extra/test.ts +++ b/packages/integration-tests/suites/public-api/setExtra/non_serializable_extra/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should normalize non-serializable extra', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.message).toBe('non_serializable'); expect(eventData.extra).toMatchObject({}); diff --git a/packages/integration-tests/suites/public-api/setExtra/simple_extra/test.ts b/packages/integration-tests/suites/public-api/setExtra/simple_extra/test.ts index 352b01191e6e..95a6184e95a9 100644 --- a/packages/integration-tests/suites/public-api/setExtra/simple_extra/test.ts +++ b/packages/integration-tests/suites/public-api/setExtra/simple_extra/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should record a simple extra object', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.message).toBe('simple_extra'); expect(eventData.extra).toMatchObject({ simple_extra: { foo: 'bar', baz: { qux: 'quux' } } }); diff --git a/packages/integration-tests/suites/public-api/setExtras/consecutive_calls/test.ts b/packages/integration-tests/suites/public-api/setExtras/consecutive_calls/test.ts index 319afc32255b..641325affa34 100644 --- a/packages/integration-tests/suites/public-api/setExtras/consecutive_calls/test.ts +++ b/packages/integration-tests/suites/public-api/setExtras/consecutive_calls/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should set extras from multiple consecutive calls', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.message).toBe('consecutive_calls'); expect(eventData.extra).toMatchObject({ extra: [], Infinity: 2, null: null, obj: { foo: ['bar', 'baz', 1] } }); diff --git a/packages/integration-tests/suites/public-api/setExtras/multiple_extras/test.ts b/packages/integration-tests/suites/public-api/setExtras/multiple_extras/test.ts index 07a43458e94b..1e238739d8a1 100644 --- a/packages/integration-tests/suites/public-api/setExtras/multiple_extras/test.ts +++ b/packages/integration-tests/suites/public-api/setExtras/multiple_extras/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should record an extras object', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.message).toBe('multiple_extras'); expect(eventData.extra).toMatchObject({ diff --git a/packages/integration-tests/suites/public-api/setTag/with_non_primitives/test.ts b/packages/integration-tests/suites/public-api/setTag/with_non_primitives/test.ts index 56843d8f6652..e4a1f9b19bd4 100644 --- a/packages/integration-tests/suites/public-api/setTag/with_non_primitives/test.ts +++ b/packages/integration-tests/suites/public-api/setTag/with_non_primitives/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should not accept non-primitive tags', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.message).toBe('non_primitives'); expect(eventData.tags).toMatchObject({}); diff --git a/packages/integration-tests/suites/public-api/setTag/with_primitives/test.ts b/packages/integration-tests/suites/public-api/setTag/with_primitives/test.ts index b3c24a8cd2c9..ba2b648ad913 100644 --- a/packages/integration-tests/suites/public-api/setTag/with_primitives/test.ts +++ b/packages/integration-tests/suites/public-api/setTag/with_primitives/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should set primitive tags', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.message).toBe('primitive_tags'); expect(eventData.tags).toMatchObject({ diff --git a/packages/integration-tests/suites/public-api/setTags/with_non_primitives/test.ts b/packages/integration-tests/suites/public-api/setTags/with_non_primitives/test.ts index 56843d8f6652..e4a1f9b19bd4 100644 --- a/packages/integration-tests/suites/public-api/setTags/with_non_primitives/test.ts +++ b/packages/integration-tests/suites/public-api/setTags/with_non_primitives/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should not accept non-primitive tags', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.message).toBe('non_primitives'); expect(eventData.tags).toMatchObject({}); diff --git a/packages/integration-tests/suites/public-api/setTags/with_primitives/test.ts b/packages/integration-tests/suites/public-api/setTags/with_primitives/test.ts index b3c24a8cd2c9..ba2b648ad913 100644 --- a/packages/integration-tests/suites/public-api/setTags/with_primitives/test.ts +++ b/packages/integration-tests/suites/public-api/setTags/with_primitives/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getSentryRequest } from '../../../../utils/helpers'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest('should set primitive tags', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getSentryRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); expect(eventData.message).toBe('primitive_tags'); expect(eventData.tags).toMatchObject({ diff --git a/packages/integration-tests/suites/public-api/setUser/unset_user/test.ts b/packages/integration-tests/suites/public-api/setUser/unset_user/test.ts index 045b79597b72..2aed7beb60aa 100644 --- a/packages/integration-tests/suites/public-api/setUser/unset_user/test.ts +++ b/packages/integration-tests/suites/public-api/setUser/unset_user/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getMultipleSentryRequests } from '../../../../utils/helpers'; +import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; sentryTest('should unset user', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getMultipleSentryRequests(page, 3, url); + const eventData = await getMultipleSentryEnvelopeRequests(page, 3, { url }); expect(eventData[0].message).toBe('no_user'); expect(eventData[0].user).toBeUndefined(); diff --git a/packages/integration-tests/suites/public-api/setUser/update_user/test.ts b/packages/integration-tests/suites/public-api/setUser/update_user/test.ts index 1520655c8363..fa846f0221c2 100644 --- a/packages/integration-tests/suites/public-api/setUser/update_user/test.ts +++ b/packages/integration-tests/suites/public-api/setUser/update_user/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getMultipleSentryRequests } from '../../../../utils/helpers'; +import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; sentryTest('should update user', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getMultipleSentryRequests(page, 2, url); + const eventData = await getMultipleSentryEnvelopeRequests(page, 2, { url }); expect(eventData[0].message).toBe('first_user'); expect(eventData[0].user).toMatchObject({ diff --git a/packages/integration-tests/suites/public-api/startTransaction/setMeasurement/subject.js b/packages/integration-tests/suites/public-api/startTransaction/setMeasurement/subject.js new file mode 100644 index 000000000000..036e86201b18 --- /dev/null +++ b/packages/integration-tests/suites/public-api/startTransaction/setMeasurement/subject.js @@ -0,0 +1,8 @@ +const transaction = Sentry.startTransaction({ name: 'some_transaction' }); + +transaction.setMeasurement('metric.foo', 42, 'ms'); +transaction.setMeasurement('metric.bar', 1337, 'nanoseconds'); +transaction.setMeasurement('metric.baz', 99, 's'); +transaction.setMeasurement('metric.baz', 1); + +transaction.finish(); diff --git a/packages/integration-tests/suites/public-api/startTransaction/setMeasurement/test.ts b/packages/integration-tests/suites/public-api/startTransaction/setMeasurement/test.ts new file mode 100644 index 000000000000..e91231093bf3 --- /dev/null +++ b/packages/integration-tests/suites/public-api/startTransaction/setMeasurement/test.ts @@ -0,0 +1,18 @@ +import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest('should attach measurement to transaction', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + const event = await getFirstSentryEnvelopeRequest(page, url); + + expect(event.measurements?.['metric.foo'].value).toBe(42); + expect(event.measurements?.['metric.bar'].value).toBe(1337); + expect(event.measurements?.['metric.baz'].value).toBe(1); + + expect(event.measurements?.['metric.foo'].unit).toBe('ms'); + expect(event.measurements?.['metric.bar'].unit).toBe('nanoseconds'); + expect(event.measurements?.['metric.baz'].unit).toBe(''); +}); diff --git a/packages/integration-tests/suites/public-api/withScope/nested_scopes/test.ts b/packages/integration-tests/suites/public-api/withScope/nested_scopes/test.ts index 7175eb10ae52..1cc024e799fc 100644 --- a/packages/integration-tests/suites/public-api/withScope/nested_scopes/test.ts +++ b/packages/integration-tests/suites/public-api/withScope/nested_scopes/test.ts @@ -1,12 +1,13 @@ import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getMultipleSentryRequests } from '../../../../utils/helpers'; +import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; sentryTest('should allow nested scoping', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getMultipleSentryRequests(page, 5, url); + const eventData = await getMultipleSentryEnvelopeRequests(page, 5, { url }); expect(eventData[0].message).toBe('root_before'); expect(eventData[0].user).toMatchObject({ id: 'qux' }); diff --git a/packages/integration-tests/suites/tracing/baggage/init.js b/packages/integration-tests/suites/tracing/baggage/init.js new file mode 100644 index 000000000000..5cd0764d4da5 --- /dev/null +++ b/packages/integration-tests/suites/tracing/baggage/init.js @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/browser'; +import { Integrations } from '@sentry/tracing'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new Integrations.BrowserTracing({ tracingOrigins: [/.*/] })], + environment: 'production', + tracesSampleRate: 1, +}); + +Sentry.configureScope(scope => { + scope.setUser({ id: 'user123', segment: 'segmentB' }); + scope.setTransactionName('testTransactionBaggage'); +}); diff --git a/packages/integration-tests/suites/tracing/baggage/test.ts b/packages/integration-tests/suites/tracing/baggage/test.ts new file mode 100644 index 000000000000..5dcb8e82bcf1 --- /dev/null +++ b/packages/integration-tests/suites/tracing/baggage/test.ts @@ -0,0 +1,23 @@ +import { expect } from '@playwright/test'; +import { EventEnvelopeHeaders } from '@sentry/types'; + +import { sentryTest } from '../../../utils/fixtures'; +import { envelopeHeaderRequestParser, getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; + +sentryTest('should send trace context data in transaction envelope header', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const envHeader = await getFirstSentryEnvelopeRequest(page, url, envelopeHeaderRequestParser); + + expect(envHeader.trace).toBeDefined(); + expect(envHeader.trace).toMatchObject({ + environment: 'production', + transaction: 'testTransactionBaggage', + user: { + id: 'user123', + segment: 'segmentB', + }, + public_key: 'public', + trace_id: expect.any(String), + }); +}); diff --git a/packages/integration-tests/suites/tracing/browsertracing/backgroundtab-custom/test.ts b/packages/integration-tests/suites/tracing/browsertracing/backgroundtab-custom/test.ts index a21cff2f1831..8a50d643e0c9 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/backgroundtab-custom/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/backgroundtab-custom/test.ts @@ -36,5 +36,5 @@ sentryTest('should finish a custom transaction when the page goes background', a expect(id_before).toBe(id_after); expect(name_after).toBe(name_before); expect(status_after).toBe('cancelled'); - expect(tags_after).toStrictEqual({ finishReason: 'documentHidden', visibilitychange: 'document.hidden' }); + expect(tags_after).toStrictEqual({ visibilitychange: 'document.hidden' }); }); diff --git a/packages/integration-tests/suites/tracing/browsertracing/backgroundtab-pageload/test.ts b/packages/integration-tests/suites/tracing/browsertracing/backgroundtab-pageload/test.ts index 42787cbb4819..149eb3e87a33 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/backgroundtab-pageload/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/backgroundtab-pageload/test.ts @@ -9,7 +9,7 @@ sentryTest('should finish pageload transaction when the page goes background', a await page.goto(url); - page.click('#go-background'); + void page.click('#go-background'); const pageloadTransaction = await getFirstSentryEnvelopeRequest(page); @@ -17,6 +17,5 @@ sentryTest('should finish pageload transaction when the page goes background', a expect(pageloadTransaction.contexts?.trace.status).toBe('cancelled'); expect(pageloadTransaction.contexts?.trace.tags).toMatchObject({ visibilitychange: 'document.hidden', - finishReason: 'documentHidden', }); }); diff --git a/packages/integration-tests/suites/tracing/browsertracing/meta/template.html b/packages/integration-tests/suites/tracing/browsertracing/meta/template.html index 0afff8864522..60c6c062c7f3 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/meta/template.html +++ b/packages/integration-tests/suites/tracing/browsertracing/meta/template.html @@ -2,5 +2,6 @@ + diff --git a/packages/integration-tests/suites/tracing/browsertracing/meta/test.ts b/packages/integration-tests/suites/tracing/browsertracing/meta/test.ts index 263fd9257186..d2d44ae74050 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/meta/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/meta/test.ts @@ -1,8 +1,8 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import { Event, EventEnvelopeHeaders } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; +import { envelopeHeaderRequestParser, getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest( 'should create a pageload transaction based on `sentry-trace` ', @@ -21,6 +21,20 @@ sentryTest( }, ); +// TODO this we can't really test until we actually propagate sentry- entries in baggage +// skipping for now but this must be adjusted later on +sentryTest.skip( + 'should pick up `baggage` tag and propagate the content in transaction', + async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const envHeader = await getFirstSentryEnvelopeRequest(page, url, envelopeHeaderRequestParser); + + expect(envHeader.trace).toBeDefined(); + expect(envHeader.trace).toEqual('{version:2.1.12}'); + }, +); + sentryTest( "should create a navigation that's not influenced by `sentry-trace` ", async ({ getLocalTestPath, page }) => { diff --git a/packages/integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts b/packages/integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts index ed3a86676867..0eaa60a75031 100644 --- a/packages/integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts +++ b/packages/integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts @@ -12,9 +12,9 @@ sentryTest('should capture a FID vital.', async ({ browserName, getLocalTestPath const url = await getLocalTestPath({ testDir: __dirname }); - page.goto(url); + await page.goto(url); // To trigger FID - page.click('#fid-btn'); + await page.click('#fid-btn'); const eventData = await getFirstSentryEnvelopeRequest(page); diff --git a/packages/integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts b/packages/integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts index 2eb554013280..32f86d3c54d1 100644 --- a/packages/integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts +++ b/packages/integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts @@ -14,10 +14,10 @@ sentryTest('should capture a LCP vital with element details.', async ({ browserN ); const url = await getLocalTestPath({ testDir: __dirname }); - page.goto(url); + await page.goto(url); // Force closure of LCP listener. - page.click('body'); + await page.click('body'); const eventData = await getFirstSentryEnvelopeRequest(page); expect(eventData.measurements).toBeDefined(); diff --git a/packages/integration-tests/suites/tracing/request/fetch/test.ts b/packages/integration-tests/suites/tracing/request/fetch/test.ts index 77db6f8c18aa..88bbeaf2c00d 100644 --- a/packages/integration-tests/suites/tracing/request/fetch/test.ts +++ b/packages/integration-tests/suites/tracing/request/fetch/test.ts @@ -2,24 +2,36 @@ import { expect, Request } from '@playwright/test'; import { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; +import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; sentryTest('should create spans for multiple fetch requests', async ({ getLocalTestPath, page }) => { const url = await getLocalTestPath({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client'); + // Because we fetch from http://example.com, fetch will throw a CORS error in firefox and webkit. + // Chromium does not throw for cors errors. + // This means that we will intercept a dynamic amount of envelopes here. + + // We will wait 500ms for all envelopes to be sent. Generally, in all browsers, the last sent + // envelope contains tracing data. + + // If we are on FF or webkit: + // 1st envelope contains CORS error + // 2nd envelope contains the tracing data we want to check here + const envelopes = await getMultipleSentryEnvelopeRequests(page, 2, { url, timeout: 10000 }); + const tracingEvent = envelopes[envelopes.length - 1]; // last envelope contains tracing data on all browsers + + const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client'); expect(requestSpans).toHaveLength(3); requestSpans?.forEach((span, index) => expect(span).toMatchObject({ description: `GET http://example.com/${index}`, - parent_span_id: eventData.contexts?.trace.span_id, + parent_span_id: tracingEvent.contexts?.trace.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), timestamp: expect.any(Number), - trace_id: eventData.contexts?.trace.trace_id, + trace_id: tracingEvent.contexts?.trace.trace_id, }), ); }); diff --git a/packages/integration-tests/utils/generatePlugin.ts b/packages/integration-tests/utils/generatePlugin.ts index 5163f81a668a..62a91b31dc9c 100644 --- a/packages/integration-tests/utils/generatePlugin.ts +++ b/packages/integration-tests/utils/generatePlugin.ts @@ -17,20 +17,20 @@ const useBundle = bundleKey && !useCompiledModule; const BUNDLE_PATHS: Record> = { browser: { - cjs: 'build/npm/dist/index.js', + cjs: 'build/npm/cjs/index.js', esm: 'build/npm/esm/index.js', - bundle_es5: 'build/bundles/bundle.js', - bundle_es5_min: 'build/bundles/bundle.min.js', - bundle_es6: 'build/bundles/bundle.es6.js', - bundle_es6_min: 'build/bundles/bundle.es6.min.js', + bundle_es5: 'build/bundles/bundle.es5.js', + bundle_es5_min: 'build/bundles/bundle.es5.min.js', + bundle_es6: 'build/bundles/bundle.js', + bundle_es6_min: 'build/bundles/bundle.min.js', }, tracing: { - cjs: 'build/npm/dist/index.js', + cjs: 'build/npm/cjs/index.js', esm: 'build/npm/esm/index.js', - bundle_es5: 'build/bundles/bundle.tracing.js', - bundle_es5_min: 'build/bundles/bundle.tracing.min.js', - bundle_es6: 'build/bundles/bundle.tracing.es6.js', - bundle_es6_min: 'build/bundles/bundle.tracing.es6.min.js', + bundle_es5: 'build/bundles/bundle.tracing.es5.js', + bundle_es5_min: 'build/bundles/bundle.tracing.es5.min.js', + bundle_es6: 'build/bundles/bundle.tracing.js', + bundle_es6_min: 'build/bundles/bundle.tracing.min.js', }, }; diff --git a/packages/integration-tests/utils/helpers.ts b/packages/integration-tests/utils/helpers.ts index 5218dc02c27c..34b512ec9e01 100644 --- a/packages/integration-tests/utils/helpers.ts +++ b/packages/integration-tests/utils/helpers.ts @@ -1,10 +1,8 @@ import { Page, Request } from '@playwright/test'; -import { Event } from '@sentry/types'; +import { Event, EventEnvelopeHeaders } from '@sentry/types'; -const storeUrlRegex = /\.sentry\.io\/api\/\d+\/store\//; const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//; -const storeRequestParser = (request: Request | null): Event => JSON.parse((request && request.postData()) || ''); const envelopeRequestParser = (request: Request | null): Event => { // https://develop.sentry.dev/sdk/envelopes/ const envelope = request?.postData() || ''; @@ -13,6 +11,14 @@ const envelopeRequestParser = (request: Request | null): Event => { return envelope.split('\n').map(line => JSON.parse(line))[2]; }; +export const envelopeHeaderRequestParser = (request: Request | null): EventEnvelopeHeaders => { + // https://develop.sentry.dev/sdk/envelopes/ + const envelope = request?.postData() || ''; + + // First row of the envelop is the event payload. + return envelope.split('\n').map(line => JSON.parse(line))[0]; +}; + /** * Run script at the given path inside the test environment. * @@ -24,17 +30,6 @@ async function runScriptInSandbox(page: Page, path: string): Promise { await page.addScriptTag({ path }); } -/** - * Wait and get Sentry's request sending the event at the given URL, or the current page - * - * @param {Page} page - * @param {string} [url] - * @return {*} {Promise} - */ -async function getSentryRequest(page: Page, url?: string): Promise { - return (await getMultipleSentryRequests(page, 1, url))[0]; -} - /** * Get Sentry events at the given URL, or the current page. * @@ -52,27 +47,26 @@ async function getSentryEvents(page: Page, url?: string): Promise> } /** - * Wait and get multiple requests matching urlRgx at the given URL, or the current page - * - * @param {Page} page - * @param {number} count - * @param {RegExp} urlRgx - * @param {(req: Request) => Event} requestParser - * @param {string} [url] - * @return {*} {Promise} + * Waits until a number of requests matching urlRgx at the given URL arrive. + * If the timout option is configured, this function will abort waiting, even if it hasn't reveived the configured + * amount of requests, and returns all the events recieved up to that point in time. */ async function getMultipleRequests( page: Page, count: number, urlRgx: RegExp, requestParser: (req: Request) => Event, - url?: string, + options?: { + url?: string; + timeout?: number; + }, ): Promise { const requests: Promise = new Promise((resolve, reject) => { let reqCount = count; const requestData: Event[] = []; + let timeoutId: NodeJS.Timeout | undefined = undefined; - page.on('request', request => { + function requestHandler(request: Request): void { if (urlRgx.test(request.url())) { try { reqCount -= 1; @@ -98,47 +92,49 @@ async function getMultipleRequests( // requestData.push(requestParser(request)); if (reqCount === 0) { + if (timeoutId) { + clearTimeout(timeoutId); + } + page.off('request', requestHandler); resolve(requestData); } } catch (err) { reject(err); } } - }); + } + + page.on('request', requestHandler); + + if (options?.timeout) { + timeoutId = setTimeout(() => { + resolve(requestData); + }, options.timeout); + } }); - if (url) { - await page.goto(url); + if (options?.url) { + await page.goto(options.url); } return requests; } -/** - * Wait and get multiple event requests at the given URL, or the current page - * - * @param {Page} page - * @param {number} count - * @param {string} [url] - * @return {*} {Promise} - */ -async function getMultipleSentryRequests(page: Page, count: number, url?: string): Promise { - return getMultipleRequests(page, count, storeUrlRegex, storeRequestParser, url); -} - /** * Wait and get multiple envelope requests at the given URL, or the current page - * - * @template T - * @param {Page} page - * @param {number} count - * @param {string} [url] - * @return {*} {Promise} */ -async function getMultipleSentryEnvelopeRequests(page: Page, count: number, url?: string): Promise { +async function getMultipleSentryEnvelopeRequests( + page: Page, + count: number, + options?: { + url?: string; + timeout?: number; + }, + requestParser: (req: Request) => T = envelopeRequestParser as (req: Request) => T, +): Promise { // TODO: This is not currently checking the type of envelope, just casting for now. // We can update this to include optional type-guarding when we have types for Envelope. - return getMultipleRequests(page, count, envelopeUrlRegex, envelopeRequestParser, url) as Promise; + return getMultipleRequests(page, count, envelopeUrlRegex, requestParser, options) as Promise; } /** @@ -149,8 +145,12 @@ async function getMultipleSentryEnvelopeRequests(page: Page, count: number, u * @param {string} [url] * @return {*} {Promise} */ -async function getFirstSentryEnvelopeRequest(page: Page, url?: string): Promise { - return (await getMultipleSentryEnvelopeRequests(page, 1, url))[0]; +async function getFirstSentryEnvelopeRequest( + page: Page, + url?: string, + requestParser: (req: Request) => T = envelopeRequestParser as (req: Request) => T, +): Promise { + return (await getMultipleSentryEnvelopeRequests(page, 1, { url }, requestParser))[0]; } /** @@ -172,10 +172,8 @@ async function injectScriptAndGetEvents(page: Page, url: string, scriptPath: str export { runScriptInSandbox, - getMultipleSentryRequests, getMultipleSentryEnvelopeRequests, getFirstSentryEnvelopeRequest, - getSentryRequest, getSentryEvents, injectScriptAndGetEvents, }; diff --git a/packages/integrations/README.md b/packages/integrations/README.md index 1a666b201983..042a01139d58 100644 --- a/packages/integrations/README.md +++ b/packages/integrations/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Sentry JavaScript SDK Integrations diff --git a/packages/integrations/jest.config.js b/packages/integrations/jest.config.js new file mode 100644 index 000000000000..24f49ab59a4c --- /dev/null +++ b/packages/integrations/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest/jest.config.js'); diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 335ca2f3b9ab..491cbe9c35c8 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -1,23 +1,23 @@ { "name": "@sentry/integrations", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Pluggable integrations that can be used to enhance JS SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/integrations", "author": "Sentry", "license": "BSD-3-Clause", "engines": { - "node": ">=6" + "node": ">=8" }, "publishConfig": { "access": "public" }, - "main": "build/npm/dist/index.js", + "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", "types": "build/npm/types/index.d.ts", "dependencies": { - "@sentry/types": "6.19.7", - "@sentry/utils": "6.19.7", + "@sentry/types": "7.0.0-rc.0", + "@sentry/utils": "7.0.0-rc.0", "localforage": "^1.8.1", "tslib": "^1.9.3" }, @@ -25,26 +25,21 @@ "chai": "^4.1.2" }, "scripts": { - "build": "run-p build:cjs build:esm build:types build:bundle", - "build:bundle": "bash scripts/buildBundles.sh", - "build:cjs": "tsc -p tsconfig.cjs.json", - "build:dev": "run-p build:cjs build:esm build:types", - "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "tsc -p tsconfig.esm.json", + "build": "run-p build:rollup build:types build:bundle", + "build:bundle": "yarn ts-node ../../scripts/ensure-bundle-deps.ts && bash scripts/buildBundles.sh", + "build:dev": "run-p build:rollup build:types", + "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", - "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", + "build:watch": "run-p build:rollup:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:es5:watch": "yarn build:cjs:watch # *** backwards compatibility - remove in v7 ***", - "build:esm:watch": "tsc -p tsconfig.esm.json --watch", + "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:npm": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf dist esm build coverage .rpt2_cache", + "clean": "rimraf build coverage .rpt2_cache sentry-integrations-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/*.ts\"", - "link:yarn": "yarn link", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", @@ -54,25 +49,5 @@ "volta": { "extends": "../../package.json" }, - "jest": { - "collectCoverage": true, - "transform": { - "^.+\\.ts$": "ts-jest" - }, - "moduleFileExtensions": [ - "js", - "ts" - ], - "testEnvironment": "node", - "testMatch": [ - "**/*.test.ts" - ], - "globals": { - "ts-jest": { - "tsConfig": "./tsconfig.json", - "diagnostics": false - } - } - }, "sideEffects": false } diff --git a/packages/integrations/rollup.config.js b/packages/integrations/rollup.bundle.config.js similarity index 64% rename from packages/integrations/rollup.config.js rename to packages/integrations/rollup.bundle.config.js index 7b23d698dfa2..8f6e21de1937 100644 --- a/packages/integrations/rollup.config.js +++ b/packages/integrations/rollup.bundle.config.js @@ -1,6 +1,6 @@ import commonjs from '@rollup/plugin-commonjs'; -import { insertAt, makeBaseBundleConfig, makeConfigVariants } from '../../rollup.config'; +import { insertAt, makeBaseBundleConfig, makeBundleConfigVariants } from '../../rollup/index.js'; const builds = []; @@ -8,17 +8,17 @@ const file = process.env.INTEGRATION_FILE; const jsVersion = process.env.JS_VERSION; const baseBundleConfig = makeBaseBundleConfig({ - input: `src/${file}`, - isAddOn: true, + bundleType: 'addon', + entrypoints: [`src/${file}`], jsVersion, licenseTitle: '@sentry/integrations', - outputFileBase: `bundles/${file.replace('.ts', '')}${jsVersion === 'ES6' ? '.es6' : ''}`, + outputFileBase: ({ name: entrypoint }) => `bundles/${entrypoint}${jsVersion === 'ES5' ? '.es5' : ''}`, }); // TODO We only need `commonjs` for localforage (used in the offline plugin). Once that's fixed, this can come out. baseBundleConfig.plugins = insertAt(baseBundleConfig.plugins, -2, commonjs()); // this makes non-minified, minified, and minified-with-debug-logging versions of each bundle -builds.push(...makeConfigVariants(baseBundleConfig)); +builds.push(...makeBundleConfigVariants(baseBundleConfig)); export default builds; diff --git a/packages/integrations/rollup.npm.config.js b/packages/integrations/rollup.npm.config.js new file mode 100644 index 000000000000..4ffa8b9396d8 --- /dev/null +++ b/packages/integrations/rollup.npm.config.js @@ -0,0 +1,8 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + // packages with bundles have a different build directory structure + hasBundles: true, + }), +); diff --git a/packages/integrations/scripts/buildBundles.sh b/packages/integrations/scripts/buildBundles.sh index 88c4887b0605..2b75b83ee670 100644 --- a/packages/integrations/scripts/buildBundles.sh +++ b/packages/integrations/scripts/buildBundles.sh @@ -3,14 +3,15 @@ for filepath in ./src/*; do file=$(basename $filepath) - # the index file is only there for the purposes of npm builds - for the CDN we create a separate bundle for each - # integration - so we can skip it here - if [[ $file == "index.ts" ]]; then + # The index file is only there for the purposes of npm builds (for the CDN we create a separate bundle for each + # integration) and the flags file is just a helper for including or not including debug logging, whose contents gets + # incorporated into each of the individual integration bundles, so we can skip them both here. + if [[ $file == "index.ts" || $file == "flags.ts" ]]; then continue fi # run the build for each integration - INTEGRATION_FILE=$file JS_VERSION=$js_version yarn --silent rollup -c rollup.config.js + INTEGRATION_FILE=$file JS_VERSION=$js_version yarn --silent rollup --config rollup.bundle.config.js done done diff --git a/packages/integrations/src/angular.ts b/packages/integrations/src/angular.ts deleted file mode 100644 index dc71042700ff..000000000000 --- a/packages/integrations/src/angular.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { Event, EventProcessor, Hub, Integration } from '@sentry/types'; -import { getGlobalObject, logger } from '@sentry/utils'; - -import { IS_DEBUG_BUILD } from './flags'; - -// See https://github.com/angular/angular.js/blob/v1.4.7/src/minErr.js -const angularPattern = /^\[((?:[$a-zA-Z0-9]+:)?(?:[$a-zA-Z0-9]+))\] (.*?)\n?(\S+)$/; - -/** - * AngularJS integration - * - * Provides an $exceptionHandler for AngularJS - */ -export class Angular implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'AngularJS'; - - /** - * moduleName used in Angular's DI resolution algorithm - */ - public static moduleName: string = 'ngSentry'; - - /** - * @inheritDoc - */ - public name: string = Angular.id; - - /** - * Angular's instance - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private readonly _angular: any; - - /** - * ngSentry module instance - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private readonly _module: any; - - /** - * Returns current hub. - */ - private _getCurrentHub?: () => Hub; - - /** - * @inheritDoc - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public constructor(options: { angular?: any } = {}) { - IS_DEBUG_BUILD && logger.log('You are still using the Angular integration, consider moving to @sentry/angular'); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - this._angular = options.angular || getGlobalObject().angular; - - if (!this._angular) { - IS_DEBUG_BUILD && logger.error('AngularIntegration is missing an Angular instance'); - return; - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - this._module = this._angular.module(Angular.moduleName, []); - } - - /** - * @inheritDoc - */ - public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - if (!this._module) { - return; - } - - this._getCurrentHub = getCurrentHub; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - this._module.config([ - '$provide', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ($provide: any): void => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - $provide.decorator('$exceptionHandler', ['$delegate', this._$exceptionHandlerDecorator.bind(this)]); - }, - ]); - } - - /** - * Angular's exceptionHandler for Sentry integration - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private _$exceptionHandlerDecorator($delegate: any): any { - return (exception: Error, cause?: string): void => { - const hub = this._getCurrentHub && this._getCurrentHub(); - - if (hub && hub.getIntegration(Angular)) { - hub.withScope(scope => { - if (cause) { - scope.setExtra('cause', cause); - } - - scope.addEventProcessor((event: Event) => { - const ex = event.exception && event.exception.values && event.exception.values[0]; - - if (ex) { - const matches = angularPattern.exec(ex.value || ''); - - if (matches) { - // This type now becomes something like: $rootScope:inprog - ex.type = matches[1]; - ex.value = matches[2]; - event.message = `${ex.type}: ${ex.value}`; - // auto set a new tag specifically for the angular error url - event.extra = { - ...event.extra, - angularDocs: matches[3].substr(0, 250), - }; - } - } - - return event; - }); - - hub.captureException(exception); - }); - } - $delegate(exception, cause); - }; - } -} diff --git a/packages/integrations/src/captureconsole.ts b/packages/integrations/src/captureconsole.ts index 7224df50f286..7243520c0661 100644 --- a/packages/integrations/src/captureconsole.ts +++ b/packages/integrations/src/captureconsole.ts @@ -1,5 +1,5 @@ import { EventProcessor, Hub, Integration } from '@sentry/types'; -import { CONSOLE_LEVELS, fill, getGlobalObject, safeJoin, severityFromString } from '@sentry/utils'; +import { CONSOLE_LEVELS, fill, getGlobalObject, safeJoin, severityLevelFromString } from '@sentry/utils'; const global = getGlobalObject(); @@ -48,7 +48,7 @@ export class CaptureConsole implements Integration { if (hub.getIntegration(CaptureConsole)) { hub.withScope(scope => { - scope.setLevel(severityFromString(level)); + scope.setLevel(severityLevelFromString(level)); scope.setExtra('arguments', args); scope.addEventProcessor(event => { event.logger = 'console'; diff --git a/packages/integrations/src/debug.ts b/packages/integrations/src/debug.ts index baf981947b6c..efa1beba35c9 100644 --- a/packages/integrations/src/debug.ts +++ b/packages/integrations/src/debug.ts @@ -37,7 +37,7 @@ export class Debug implements Integration { * @inheritDoc */ public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor((event: Event, hint?: EventHint) => { + addGlobalEventProcessor((event: Event, hint: EventHint) => { const self = getCurrentHub().getIntegration(Debug); if (self) { if (self._options.debugger) { @@ -49,12 +49,12 @@ export class Debug implements Integration { consoleSandbox(() => { if (self._options.stringify) { console.log(JSON.stringify(event, null, 2)); - if (hint) { + if (Object.keys(hint).length) { console.log(JSON.stringify(hint, null, 2)); } } else { console.log(event); - if (hint) { + if (Object.keys(hint).length) { console.log(hint); } } diff --git a/packages/integrations/src/dedupe.ts b/packages/integrations/src/dedupe.ts index 2ce72a6b636c..a29223f6832b 100644 --- a/packages/integrations/src/dedupe.ts +++ b/packages/integrations/src/dedupe.ts @@ -24,7 +24,7 @@ export class Dedupe implements Integration { * @inheritDoc */ public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor((currentEvent: Event) => { + const eventProcessor: EventProcessor = currentEvent => { const self = getCurrentHub().getIntegration(Dedupe); if (self) { // Juuust in case something goes wrong @@ -40,7 +40,10 @@ export class Dedupe implements Integration { return (self._previousEvent = currentEvent); } return currentEvent; - }); + }; + + eventProcessor.id = this.name; + addGlobalEventProcessor(eventProcessor); } } @@ -198,8 +201,6 @@ function _getFramesFromEvent(event: Event): StackFrame[] | undefined { } catch (_oO) { return undefined; } - } else if (event.stacktrace) { - return event.stacktrace.frames; } return undefined; } diff --git a/packages/integrations/src/ember.ts b/packages/integrations/src/ember.ts deleted file mode 100644 index 81bbd1a0036b..000000000000 --- a/packages/integrations/src/ember.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { EventProcessor, Hub, Integration } from '@sentry/types'; -import { getGlobalObject, isInstanceOf, logger } from '@sentry/utils'; - -import { IS_DEBUG_BUILD } from './flags'; - -/** JSDoc */ -export class Ember implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Ember'; - - /** - * @inheritDoc - */ - public name: string = Ember.id; - - /** - * @inheritDoc - */ - // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any - private readonly _Ember: any; - - /** - * @inheritDoc - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public constructor(options: { Ember?: any } = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - this._Ember = options.Ember || getGlobalObject().Ember; - } - - /** - * @inheritDoc - */ - public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - if (!this._Ember) { - IS_DEBUG_BUILD && logger.error('EmberIntegration is missing an Ember instance'); - return; - } - - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ - const oldOnError = this._Ember.onerror; - - this._Ember.onerror = (error: Error): void => { - if (getCurrentHub().getIntegration(Ember)) { - getCurrentHub().captureException(error, { originalException: error }); - } - - if (typeof oldOnError === 'function') { - oldOnError.call(this._Ember, error); - } else if (this._Ember.testing) { - throw error; - } - }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this._Ember.RSVP.on('error', (reason: unknown): void => { - if (getCurrentHub().getIntegration(Ember)) { - getCurrentHub().withScope(scope => { - if (isInstanceOf(reason, Error)) { - scope.setExtra('context', 'Unhandled Promise error detected'); - getCurrentHub().captureException(reason, { originalException: reason as Error }); - } else { - scope.setExtra('reason', reason); - getCurrentHub().captureMessage('Unhandled Promise error detected'); - } - }); - } - }); - } - /* eslint-enable @typescript-eslint/no-unsafe-member-access */ -} diff --git a/packages/integrations/src/extraerrordata.ts b/packages/integrations/src/extraerrordata.ts index aad7210a85b2..0dfb8f39bbfc 100644 --- a/packages/integrations/src/extraerrordata.ts +++ b/packages/integrations/src/extraerrordata.ts @@ -1,5 +1,5 @@ -import { Event, EventHint, EventProcessor, ExtendedError, Hub, Integration } from '@sentry/types'; -import { isError, isPlainObject, logger, normalize } from '@sentry/utils'; +import { Contexts, Event, EventHint, EventProcessor, ExtendedError, Hub, Integration } from '@sentry/types'; +import { addNonEnumerableProperty, isError, isPlainObject, logger, normalize } from '@sentry/utils'; import { IS_DEBUG_BUILD } from './flags'; @@ -37,7 +37,7 @@ export class ExtraErrorData implements Integration { * @inheritDoc */ public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor((event: Event, hint?: EventHint) => { + addGlobalEventProcessor((event: Event, hint: EventHint) => { const self = getCurrentHub().getIntegration(ExtraErrorData); if (!self) { return event; @@ -49,27 +49,26 @@ export class ExtraErrorData implements Integration { /** * Attaches extracted information from the Error object to extra field in the Event */ - public enhanceEventWithErrorData(event: Event, hint?: EventHint): Event { - if (!hint || !hint.originalException || !isError(hint.originalException)) { + public enhanceEventWithErrorData(event: Event, hint: EventHint = {}): Event { + if (!hint.originalException || !isError(hint.originalException)) { return event; } - const name = (hint.originalException as ExtendedError).name || hint.originalException.constructor.name; + const exceptionName = (hint.originalException as ExtendedError).name || hint.originalException.constructor.name; const errorData = this._extractErrorData(hint.originalException as ExtendedError); if (errorData) { - let contexts = { + const contexts: Contexts = { ...event.contexts, }; const normalizedErrorData = normalize(errorData, this._options.depth); + if (isPlainObject(normalizedErrorData)) { - contexts = { - ...event.contexts, - [name]: { - ...normalizedErrorData, - }, - }; + // We mark the error data as "already normalized" here, because we don't want other normalization procedures to + // potentially truncate the data we just already normalized, with a certain depth setting. + addNonEnumerableProperty(normalizedErrorData, '__sentry_skip_normalization__', true); + contexts[exceptionName] = normalizedErrorData; } return { diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts index f1ba52e92026..9a2573ee5a44 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -1,12 +1,9 @@ -export { Angular } from './angular'; export { CaptureConsole } from './captureconsole'; export { Debug } from './debug'; export { Dedupe } from './dedupe'; -export { Ember } from './ember'; export { ExtraErrorData } from './extraerrordata'; export { Offline } from './offline'; export { ReportingObserver } from './reportingobserver'; export { RewriteFrames } from './rewriteframes'; export { SessionTiming } from './sessiontiming'; export { Transaction } from './transaction'; -export { Vue } from './vue'; diff --git a/packages/integrations/src/offline.ts b/packages/integrations/src/offline.ts index 0d2da0841805..78d8ead05dd4 100644 --- a/packages/integrations/src/offline.ts +++ b/packages/integrations/src/offline.ts @@ -80,10 +80,12 @@ export class Offline implements Integration { }); } - addGlobalEventProcessor((event: Event) => { + const eventProcessor: EventProcessor = event => { if (this.hub && this.hub.getIntegration(Offline)) { // cache if we are positively offline if ('navigator' in this.global && 'onLine' in this.global.navigator && !this.global.navigator.onLine) { + IS_DEBUG_BUILD && logger.log('Event dropped due to being a offline - caching instead'); + void this._cacheEvent(event) .then((_event: Event): Promise => this._enforceMaxEvents()) .catch((_error): void => { @@ -96,7 +98,10 @@ export class Offline implements Integration { } return event; - }); + }; + + eventProcessor.id = this.name; + addGlobalEventProcessor(eventProcessor); // if online now, send any events stored in a previous offline session if ('navigator' in this.global && 'onLine' in this.global.navigator && this.global.navigator.onLine) { diff --git a/packages/integrations/src/rewriteframes.ts b/packages/integrations/src/rewriteframes.ts index 9eb95e54f6d3..7b1129e45032 100644 --- a/packages/integrations/src/rewriteframes.ts +++ b/packages/integrations/src/rewriteframes.ts @@ -61,10 +61,6 @@ export class RewriteFrames implements Integration { processedEvent = this._processExceptionsEvent(processedEvent); } - if (originalEvent.stacktrace) { - processedEvent = this._processStacktraceEvent(processedEvent); - } - return processedEvent; } @@ -110,18 +106,6 @@ export class RewriteFrames implements Integration { } } - /** JSDoc */ - private _processStacktraceEvent(event: Event): Event { - try { - return { - ...event, - stacktrace: this._processStacktrace(event.stacktrace), - }; - } catch (_oO) { - return event; - } - } - /** JSDoc */ private _processStacktrace(stacktrace?: Stacktrace): Stacktrace { return { diff --git a/packages/integrations/src/vue.ts b/packages/integrations/src/vue.ts deleted file mode 100644 index 1b1cf3caa63a..000000000000 --- a/packages/integrations/src/vue.ts +++ /dev/null @@ -1,457 +0,0 @@ -/* eslint-disable max-lines */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { EventProcessor, Hub, Integration, IntegrationClass, Scope, Span, Transaction } from '@sentry/types'; -import { basename, getGlobalObject, logger, timestampWithMs } from '@sentry/utils'; - -import { IS_DEBUG_BUILD } from './flags'; - -/** - * Used to extract Tracing integration from the current client, - * without the need to import `Tracing` itself from the @sentry/apm package. - * @deprecated as @sentry/tracing should be used over @sentry/apm. - */ -const TRACING_GETTER = { - id: 'Tracing', -} as any as IntegrationClass; - -/** - * Used to extract BrowserTracing integration from @sentry/tracing - */ -const BROWSER_TRACING_GETTER = { - id: 'BrowserTracing', -} as any as IntegrationClass; - -const VUE_OP = 'ui.vue'; - -/** Global Vue object limited to the methods/attributes we require */ -interface VueInstance { - config: { - errorHandler?(error: Error, vm?: ViewModel, info?: string): void; - }; - util?: { - warn(...input: any): void; - }; - mixin(hooks: { [key: string]: () => void }): void; -} - -/** Representation of Vue component internals */ -interface ViewModel { - [key: string]: any; - // eslint-disable-next-line @typescript-eslint/ban-types - $root: object; - $options: { - [key: string]: any; - name?: string; - propsData?: { [key: string]: any }; - _componentTag?: string; - __file?: string; - $_sentryPerfHook?: boolean; - }; - $once(hook: string, cb: () => void): void; -} - -/** Vue Integration configuration */ -interface IntegrationOptions { - /** Vue instance to be used inside the integration */ - Vue: VueInstance; - - /** - * When set to `false`, Sentry will suppress reporting of all props data - * from your Vue components for privacy concerns. - */ - attachProps: boolean; - /** - * When set to `true`, original Vue's `logError` will be called as well. - * https://github.com/vuejs/vue/blob/c2b1cfe9ccd08835f2d99f6ce60f67b4de55187f/src/core/util/error.js#L38-L48 - */ - logErrors: boolean; - - /** - * When set to `true`, enables tracking of components lifecycle performance. - * It requires `Tracing` integration to be also enabled. - */ - tracing: boolean; - - /** {@link TracingOptions} */ - tracingOptions: TracingOptions; -} - -/** Vue specific configuration for Tracing Integration */ -interface TracingOptions { - /** - * Decides whether to track components by hooking into its lifecycle methods. - * Can be either set to `boolean` to enable/disable tracking for all of them. - * Or to an array of specific component names (case-sensitive). - */ - trackComponents: boolean | string[]; - /** How long to wait until the tracked root activity is marked as finished and sent of to Sentry */ - timeout: number; - /** - * List of hooks to keep track of during component lifecycle. - * Available hooks: 'activate' | 'create' | 'destroy' | 'mount' | 'update' - * Based on https://vuejs.org/v2/api/#Options-Lifecycle-Hooks - */ - hooks: Operation[]; -} - -/** Optional metadata attached to Sentry Event */ -interface Metadata { - [key: string]: any; - componentName?: string; - propsData?: { [key: string]: any }; - lifecycleHook?: string; -} - -// https://vuejs.org/v2/api/#Options-Lifecycle-Hooks -type Hook = - | 'activated' - | 'beforeCreate' - | 'beforeDestroy' - | 'beforeMount' - | 'beforeUpdate' - | 'created' - | 'deactivated' - | 'destroyed' - | 'mounted' - | 'updated'; - -type Operation = 'activate' | 'create' | 'destroy' | 'mount' | 'update'; - -// Mappings from operation to corresponding lifecycle hook. -const HOOKS: { [key in Operation]: Hook[] } = { - activate: ['activated', 'deactivated'], - create: ['beforeCreate', 'created'], - destroy: ['beforeDestroy', 'destroyed'], - mount: ['beforeMount', 'mounted'], - update: ['beforeUpdate', 'updated'], -}; - -const COMPONENT_NAME_REGEXP = /(?:^|[-_/])(\w)/g; -const ROOT_COMPONENT_NAME = 'root'; -const ANONYMOUS_COMPONENT_NAME = 'anonymous component'; - -/** JSDoc */ -export class Vue implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Vue'; - - /** - * @inheritDoc - */ - public name: string = Vue.id; - - private readonly _options: IntegrationOptions; - - /** - * Cache holding already processed component names - */ - private readonly _componentsCache: { [key: string]: string } = {}; - private _rootSpan?: Span; - private _rootSpanTimer?: ReturnType; - private _tracingActivity?: number; - - /** - * @inheritDoc - */ - public constructor( - options: Partial & { tracingOptions: Partial }>, - ) { - IS_DEBUG_BUILD && logger.log('You are still using the Vue.js integration, consider moving to @sentry/vue'); - this._options = { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - Vue: getGlobalObject().Vue, - attachProps: true, - logErrors: false, - tracing: false, - ...options, - tracingOptions: { - hooks: ['mount', 'update'], - timeout: 2000, - trackComponents: false, - ...options.tracingOptions, - }, - }; - } - - /** - * @inheritDoc - */ - public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - if (!this._options.Vue) { - IS_DEBUG_BUILD && logger.error('Vue integration is missing a Vue instance'); - return; - } - - this._attachErrorHandler(getCurrentHub); - - if (this._options.tracing) { - this._startTracing(getCurrentHub); - } - } - - /** - * Extract component name from the ViewModel - */ - private _getComponentName(vm: ViewModel): string { - // Such level of granularity is most likely not necessary, but better safe than sorry. — Kamil - if (!vm) { - return ANONYMOUS_COMPONENT_NAME; - } - - if (vm.$root === vm) { - return ROOT_COMPONENT_NAME; - } - - if (!vm.$options) { - return ANONYMOUS_COMPONENT_NAME; - } - - if (vm.$options.name) { - return vm.$options.name; - } - - if (vm.$options._componentTag) { - return vm.$options._componentTag; - } - - // injected by vue-loader - if (vm.$options.__file) { - const unifiedFile = vm.$options.__file.replace(/^[a-zA-Z]:/, '').replace(/\\/g, '/'); - const filename = basename(unifiedFile, '.vue'); - return ( - this._componentsCache[filename] || - (this._componentsCache[filename] = filename.replace(COMPONENT_NAME_REGEXP, (_, c: string) => - c ? c.toUpperCase() : '', - )) - ); - } - - return ANONYMOUS_COMPONENT_NAME; - } - - /** Keep it as attribute function, to keep correct `this` binding inside the hooks callbacks */ - // eslint-disable-next-line @typescript-eslint/typedef - private readonly _applyTracingHooks = (vm: ViewModel, getCurrentHub: () => Hub): void => { - // Don't attach twice, just in case - if (vm.$options.$_sentryPerfHook) { - return; - } - vm.$options.$_sentryPerfHook = true; - - const name = this._getComponentName(vm); - const rootMount = name === ROOT_COMPONENT_NAME; - const spans: { [key: string]: Span } = {}; - - // Render hook starts after once event is emitted, - // but it ends before the second event of the same type. - // - // Because of this, we start measuring inside the first event, - // but finish it before it triggers, to skip the event emitter timing itself. - const rootHandler = (hook: Hook): void => { - const now = timestampWithMs(); - - // On the first handler call (before), it'll be undefined, as `$once` will add it in the future. - // However, on the second call (after), it'll be already in place. - if (this._rootSpan) { - this._finishRootSpan(now, getCurrentHub); - } else { - vm.$once(`hook:${hook}`, () => { - // Create an activity on the first event call. There'll be no second call, as rootSpan will be in place, - // thus new event handler won't be attached. - - // We do this whole dance with `TRACING_GETTER` to prevent `@sentry/apm` from becoming a peerDependency. - // We also need to ask for the `.constructor`, as `pushActivity` and `popActivity` are static, not instance methods. - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ - // eslint-disable-next-line deprecation/deprecation - const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER); - if (tracingIntegration) { - this._tracingActivity = (tracingIntegration as any).constructor.pushActivity('Vue Application Render'); - const transaction = (tracingIntegration as any).constructor.getTransaction(); - if (transaction) { - this._rootSpan = transaction.startChild({ - description: 'Application Render', - op: VUE_OP, - }); - } - // Use functionality from @sentry/tracing - } else { - const activeTransaction = getActiveTransaction(getCurrentHub()); - if (activeTransaction) { - this._rootSpan = activeTransaction.startChild({ - description: 'Application Render', - op: VUE_OP, - }); - } - } - /* eslint-enable @typescript-eslint/no-unsafe-member-access */ - }); - } - }; - - const childHandler = (hook: Hook, operation: Operation): void => { - // Skip components that we don't want to track to minimize the noise and give a more granular control to the user - const shouldTrack = Array.isArray(this._options.tracingOptions.trackComponents) - ? this._options.tracingOptions.trackComponents.indexOf(name) > -1 - : this._options.tracingOptions.trackComponents; - - if (!this._rootSpan || !shouldTrack) { - return; - } - - const now = timestampWithMs(); - const span = spans[operation]; - - // On the first handler call (before), it'll be undefined, as `$once` will add it in the future. - // However, on the second call (after), it'll be already in place. - if (span) { - span.finish(); - this._finishRootSpan(now, getCurrentHub); - } else { - vm.$once(`hook:${hook}`, () => { - if (this._rootSpan) { - spans[operation] = this._rootSpan.startChild({ - description: `Vue <${name}>`, - op: `${VUE_OP}.${operation}`, - }); - } - }); - } - }; - - // Each component has it's own scope, so all activities are only related to one of them - this._options.tracingOptions.hooks.forEach(operation => { - // Retrieve corresponding hooks from Vue lifecycle. - // eg. mount => ['beforeMount', 'mounted'] - const internalHooks = HOOKS[operation]; - - if (!internalHooks) { - IS_DEBUG_BUILD && logger.warn(`Unknown hook: ${operation}`); - return; - } - - internalHooks.forEach(internalHook => { - const handler = rootMount - ? rootHandler.bind(this, internalHook) - : childHandler.bind(this, internalHook, operation); - const currentValue = vm.$options[internalHook]; - - if (Array.isArray(currentValue)) { - vm.$options[internalHook] = [handler, ...currentValue]; - } else if (typeof currentValue === 'function') { - vm.$options[internalHook] = [handler, currentValue]; - } else { - vm.$options[internalHook] = [handler]; - } - }); - }); - }; - - /** Finish top-level span and activity with a debounce configured using `timeout` option */ - private _finishRootSpan(timestamp: number, getCurrentHub: () => Hub): void { - if (this._rootSpanTimer) { - clearTimeout(this._rootSpanTimer); - } - - this._rootSpanTimer = setTimeout(() => { - if (this._tracingActivity) { - // We do this whole dance with `TRACING_GETTER` to prevent `@sentry/apm` from becoming a peerDependency. - // We also need to ask for the `.constructor`, as `pushActivity` and `popActivity` are static, not instance methods. - // eslint-disable-next-line deprecation/deprecation - const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER); - if (tracingIntegration) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (tracingIntegration as any).constructor.popActivity(this._tracingActivity); - } - } - - // We should always finish the span, only should pop activity if using @sentry/apm - if (this._rootSpan) { - this._rootSpan.finish(timestamp); - } - }, this._options.tracingOptions.timeout); - } - - /** Inject configured tracing hooks into Vue's component lifecycles */ - private _startTracing(getCurrentHub: () => Hub): void { - const applyTracingHooks = this._applyTracingHooks; - - this._options.Vue.mixin({ - beforeCreate(this: ViewModel): void { - // eslint-disable-next-line deprecation/deprecation - if (getCurrentHub().getIntegration(TRACING_GETTER) || getCurrentHub().getIntegration(BROWSER_TRACING_GETTER)) { - // `this` points to currently rendered component - applyTracingHooks(this, getCurrentHub); - } else { - IS_DEBUG_BUILD && - logger.error('Vue integration has tracing enabled, but Tracing integration is not configured'); - } - }, - }); - } - - /** Inject Sentry's handler into owns Vue's error handler */ - private _attachErrorHandler(getCurrentHub: () => Hub): void { - // eslint-disable-next-line @typescript-eslint/unbound-method - const currentErrorHandler = this._options.Vue.config.errorHandler; - - this._options.Vue.config.errorHandler = (error: Error, vm?: ViewModel, info?: string): void => { - const metadata: Metadata = {}; - - if (vm) { - try { - metadata.componentName = this._getComponentName(vm); - - if (this._options.attachProps) { - metadata.propsData = vm.$options.propsData; - } - } catch (_oO) { - IS_DEBUG_BUILD && logger.warn('Unable to extract metadata from Vue component.'); - } - } - - if (info) { - metadata.lifecycleHook = info; - } - - if (getCurrentHub().getIntegration(Vue)) { - // Capture exception in the next event loop, to make sure that all breadcrumbs are recorded in time. - setTimeout(() => { - getCurrentHub().withScope(scope => { - scope.setContext('vue', metadata); - getCurrentHub().captureException(error); - }); - }); - } - - if (typeof currentErrorHandler === 'function') { - currentErrorHandler.call(this._options.Vue, error, vm, info); - } - - if (this._options.logErrors) { - if (this._options.Vue.util) { - this._options.Vue.util.warn(`Error in ${info}: "${error && error.toString()}"`, vm); - } - // eslint-disable-next-line no-console - console.error(error); - } - }; - } -} - -interface HubType extends Hub { - getScope?(): Scope | undefined; -} - -/** Grabs active transaction off scope */ -export function getActiveTransaction(hub: HubType): T | undefined { - if (hub && hub.getScope) { - const scope = hub.getScope() as Scope; - if (scope) { - return scope.getTransaction() as T | undefined; - } - } - - return undefined; -} diff --git a/packages/integrations/test/captureconsole.test.ts b/packages/integrations/test/captureconsole.test.ts index bb6cce8110d3..f7979497ba6b 100644 --- a/packages/integrations/test/captureconsole.test.ts +++ b/packages/integrations/test/captureconsole.test.ts @@ -1,4 +1,5 @@ -import { Event, Integration } from '@sentry/types'; +/* eslint-disable @typescript-eslint/unbound-method */ +import { Event, Hub, Integration } from '@sentry/types'; import { CaptureConsole } from '../src/captureconsole'; @@ -16,15 +17,30 @@ const mockHub = { captureException: jest.fn(), }; -const getMockHubWithIntegration = (integration: Integration) => ({ - ...mockHub, - getIntegration: jest.fn(() => integration), -}); +const mockConsole = { + debug: jest.fn(), + log: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + assert: jest.fn(), + info: jest.fn(), +}; + +const getMockHubWithIntegration = (integration: Integration) => + ({ + ...mockHub, + getIntegration: jest.fn(() => integration), + } as unknown as Hub); // We're using this to un-monkey patch the console after each test. const originalConsole = Object.assign({}, global.console); describe('CaptureConsole setup', () => { + beforeEach(() => { + // this suppresses output to the terminal running the tests, but doesn't interfere with our wrapping + Object.assign(global.console, mockConsole); + }); + afterEach(() => { jest.clearAllMocks(); @@ -32,56 +48,67 @@ describe('CaptureConsole setup', () => { Object.assign(global.console, originalConsole); }); - it('should patch user-configured console levels', () => { - const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'warn'] }); - captureConsoleIntegration.setupOnce( - () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, - ); + describe('monkeypatching', () => { + beforeEach(() => { + // for these tests only, we don't want to use the mock console, because we're testing for equality to methods from + // the original, so undo the global `beforeEach()` + Object.assign(global.console, originalConsole); + }); - expect(global.console.error).toBe(originalConsole.error); // not monkey patched - expect(global.console.log).not.toBe(originalConsole.log); // monkey patched - expect(global.console.warn).not.toBe(originalConsole.warn); // monkey patched - }); + it('should patch user-configured console levels', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'warn'] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration), + ); - it('should fall back to default console levels if none are provided', () => { - const captureConsoleIntegration = new CaptureConsole(); - captureConsoleIntegration.setupOnce( - () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, - ); + expect(global.console.error).toBe(originalConsole.error); // not monkey patched + expect(global.console.log).not.toBe(originalConsole.log); // monkey patched + expect(global.console.warn).not.toBe(originalConsole.warn); // monkey patched + }); - // expect a set of defined console levels to have been monkey patched - expect(global.console.debug).not.toBe(originalConsole.debug); - expect(global.console.info).not.toBe(originalConsole.info); - expect(global.console.warn).not.toBe(originalConsole.warn); - expect(global.console.error).not.toBe(originalConsole.error); - expect(global.console.log).not.toBe(originalConsole.log); - expect(global.console.assert).not.toBe(originalConsole.assert); + it('should fall back to default console levels if none are provided', () => { + const captureConsoleIntegration = new CaptureConsole(); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration), + ); - // any other fields should not have been patched - expect(global.console.trace).toBe(originalConsole.trace); - expect(global.console.table).toBe(originalConsole.table); - }); + // expect a set of defined console levels to have been monkey patched + expect(global.console.debug).not.toBe(originalConsole.debug); + expect(global.console.info).not.toBe(originalConsole.info); + expect(global.console.warn).not.toBe(originalConsole.warn); + expect(global.console.error).not.toBe(originalConsole.error); + expect(global.console.log).not.toBe(originalConsole.log); + expect(global.console.assert).not.toBe(originalConsole.assert); + + // any other fields should not have been patched + expect(global.console.trace).toBe(originalConsole.trace); + expect(global.console.table).toBe(originalConsole.table); + }); + + it('should not wrap any functions with an empty levels option', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: [] }); + captureConsoleIntegration.setupOnce( + () => undefined, + () => getMockHubWithIntegration(captureConsoleIntegration), + ); - it('should not wrap any functions with an empty levels option', () => { - const captureConsoleIntegration = new CaptureConsole({ levels: [] }); - captureConsoleIntegration.setupOnce( - () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, - ); + // expect the default set of console levels not to have been monkey patched + expect(global.console.debug).toBe(originalConsole.debug); + expect(global.console.info).toBe(originalConsole.info); + expect(global.console.warn).toBe(originalConsole.warn); + expect(global.console.error).toBe(originalConsole.error); + expect(global.console.log).toBe(originalConsole.log); + expect(global.console.assert).toBe(originalConsole.assert); - // expect the default set of console levels not to have been monkey patched - expect(global.console.debug).toBe(originalConsole.debug); - expect(global.console.info).toBe(originalConsole.info); - expect(global.console.warn).toBe(originalConsole.warn); - expect(global.console.error).toBe(originalConsole.error); - expect(global.console.log).toBe(originalConsole.log); - expect(global.console.assert).toBe(originalConsole.assert); + // suppress output from the logging we're about to do + global.console.log = global.console.info = jest.fn(); - // expect no message to be captured with console.log - global.console.log('some message'); - expect(mockHub.captureMessage).not.toHaveBeenCalled(); + // expect no message to be captured with console.log + global.console.log('some message'); + expect(mockHub.captureMessage).not.toHaveBeenCalled(); + }); }); it('setup should fail gracefully when console is not available', () => { @@ -93,7 +120,7 @@ describe('CaptureConsole setup', () => { const captureConsoleIntegration = new CaptureConsole(); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, + () => getMockHubWithIntegration(captureConsoleIntegration), ); }).not.toThrow(); @@ -105,7 +132,7 @@ describe('CaptureConsole setup', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['error'] }); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, + () => getMockHubWithIntegration(captureConsoleIntegration), ); // call a wrapped function @@ -119,7 +146,7 @@ describe('CaptureConsole setup', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, + () => getMockHubWithIntegration(captureConsoleIntegration), ); // call a wrapped function @@ -135,7 +162,7 @@ describe('CaptureConsole setup', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, + () => getMockHubWithIntegration(captureConsoleIntegration), ); // call a wrapped function @@ -154,7 +181,7 @@ describe('CaptureConsole setup', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['assert'] }); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, + () => getMockHubWithIntegration(captureConsoleIntegration), ); global.console.assert(1 + 1 === 3); @@ -168,7 +195,7 @@ describe('CaptureConsole setup', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['assert'] }); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, + () => getMockHubWithIntegration(captureConsoleIntegration), ); global.console.assert(1 + 1 === 3, 'expression is false'); @@ -182,7 +209,7 @@ describe('CaptureConsole setup', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['assert'] }); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, + () => getMockHubWithIntegration(captureConsoleIntegration), ); global.console.assert(1 + 1 === 2); @@ -192,7 +219,7 @@ describe('CaptureConsole setup', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['error'] }); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, + () => getMockHubWithIntegration(captureConsoleIntegration), ); const someError = new Error('some error'); @@ -206,7 +233,7 @@ describe('CaptureConsole setup', () => { const captureConsoleIntegration = new CaptureConsole(); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, + () => getMockHubWithIntegration(captureConsoleIntegration), ); const someError = new Error('some error'); @@ -220,7 +247,7 @@ describe('CaptureConsole setup', () => { const captureConsoleIntegration = new CaptureConsole(); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, + () => getMockHubWithIntegration(captureConsoleIntegration), ); global.console.error('some message'); @@ -233,7 +260,7 @@ describe('CaptureConsole setup', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['error'] }); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, + () => getMockHubWithIntegration(captureConsoleIntegration), ); global.console.error('some non-error message'); @@ -247,7 +274,7 @@ describe('CaptureConsole setup', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['info'] }); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, + () => getMockHubWithIntegration(captureConsoleIntegration), ); global.console.info('some message'); @@ -265,7 +292,7 @@ describe('CaptureConsole setup', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, + () => getMockHubWithIntegration(captureConsoleIntegration), ); global.console.log('some message 1', 'some message 2'); @@ -281,11 +308,11 @@ describe('CaptureConsole setup', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'someNonExistingLevel', 'error'] }); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, + () => getMockHubWithIntegration(captureConsoleIntegration), ); // The provided level should not be created - expect(global.console['someNonExistingLevel']).toBeUndefined(); + expect((global.console as any)['someNonExistingLevel']).toBeUndefined(); // Ohter levels should be wrapped as expected expect(global.console.log).not.toBe(originalConsole.log); @@ -296,7 +323,7 @@ describe('CaptureConsole setup', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'error'] }); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(null) as any, // simulate not having the integration registered + () => getMockHubWithIntegration(null as any), // simulate not having the integration registered ); // Console should be wrapped @@ -310,12 +337,12 @@ describe('CaptureConsole setup', () => { it("should not crash when the original console methods don't exist at time of invocation", () => { const originalConsoleLog = global.console.log; - global.console.log = undefined; // don't `delete` here, otherwise `fill` won't wrap the function + global.console.log = undefined as any; // don't `delete` here, otherwise `fill` won't wrap the function const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration) as any, + () => getMockHubWithIntegration(captureConsoleIntegration), ); expect(() => { diff --git a/packages/integrations/test/debug.test.ts b/packages/integrations/test/debug.test.ts index 5c2f84f5d1e6..eed7e52d509e 100644 --- a/packages/integrations/test/debug.test.ts +++ b/packages/integrations/test/debug.test.ts @@ -8,6 +8,7 @@ const mockGetCurrentHub = (getIntegrationResult: Integration) => ({ // Replace console log with a mock so we can check for invocations const mockConsoleLog = jest.fn(); +// eslint-disable-next-line @typescript-eslint/unbound-method const originalConsoleLog = global.console.log; global.console.log = mockConsoleLog; @@ -26,7 +27,7 @@ describe('Debug integration setup should register an event processor that', () = const captureEventProcessor = (eventProcessor: EventProcessor) => { const testEvent = { event_id: 'some event' }; - void eventProcessor(testEvent); + void eventProcessor(testEvent, {}); expect(mockConsoleLog).toHaveBeenCalledTimes(1); expect(mockConsoleLog).toBeCalledWith(testEvent); }; @@ -54,7 +55,7 @@ describe('Debug integration setup should register an event processor that', () = const captureEventProcessor = (eventProcessor: EventProcessor) => { const testEvent = { event_id: 'some event' }; - void eventProcessor(testEvent); + void eventProcessor(testEvent, {}); expect(mockConsoleLog).toHaveBeenCalledTimes(1); expect(mockConsoleLog).toBeCalledWith(JSON.stringify(testEvent, null, 2)); }; diff --git a/packages/integrations/test/dedupe.test.ts b/packages/integrations/test/dedupe.test.ts index 46cac4d06320..8bc354ffa620 100644 --- a/packages/integrations/test/dedupe.test.ts +++ b/packages/integrations/test/dedupe.test.ts @@ -1,31 +1,48 @@ +import { Event as SentryEvent, Exception, StackFrame, Stacktrace } from '@sentry/types'; + import { _shouldDropEvent } from '../src/dedupe'; +type EventWithException = SentryEvent & { + exception: { + values: ExceptionWithStacktrace[]; + }; +}; +type ExceptionWithStacktrace = Exception & { stacktrace: StacktraceWithFrames }; +type StacktraceWithFrames = Stacktrace & { frames: StackFrame[] }; + /** JSDoc */ function clone(data: T): T { return JSON.parse(JSON.stringify(data)); } -const messageEvent = { +const messageEvent: EventWithException = { fingerprint: ['MrSnuffles'], message: 'PickleRick', - stacktrace: { - frames: [ - { - colno: 1, - filename: 'filename.js', - function: 'function', - lineno: 1, - }, + exception: { + values: [ { - colno: 2, - filename: 'filename.js', - function: 'function', - lineno: 2, + value: 'PickleRick', + stacktrace: { + frames: [ + { + colno: 1, + filename: 'filename.js', + function: 'function', + lineno: 1, + }, + { + colno: 2, + filename: 'filename.js', + function: 'function', + lineno: 2, + }, + ], + }, }, ], }, }; -const exceptionEvent = { +const exceptionEvent: EventWithException = { exception: { values: [ { @@ -64,13 +81,14 @@ describe('Dedupe', () => { const eventA = clone(messageEvent); const eventB = clone(messageEvent); eventB.message = 'EvilMorty'; + eventB.exception.values[0].value = 'EvilMorty'; expect(_shouldDropEvent(eventA, eventB)).toBe(false); }); it('should not drop if events have same messages, but different stacktraces', () => { const eventA = clone(messageEvent); const eventB = clone(messageEvent); - eventB.stacktrace.frames[0].colno = 1337; + eventB.exception.values[0].stacktrace.frames[0].colno = 1337; expect(_shouldDropEvent(eventA, eventB)).toBe(false); }); diff --git a/packages/integrations/test/extraerrordata.test.ts b/packages/integrations/test/extraerrordata.test.ts index ed76a2dc75ca..68e38720f761 100644 --- a/packages/integrations/test/extraerrordata.test.ts +++ b/packages/integrations/test/extraerrordata.test.ts @@ -71,7 +71,7 @@ describe('ExtraErrorData()', () => { event = { // @ts-ignore Allow contexts on event contexts: { - foo: 42, + foo: { bar: 42 }, }, }; const error = new TypeError('foo') as ExtendedError; @@ -85,7 +85,7 @@ describe('ExtraErrorData()', () => { TypeError: { baz: 42, }, - foo: 42, + foo: { bar: 42 }, }); }); diff --git a/packages/integrations/test/offline.test.ts b/packages/integrations/test/offline.test.ts index 1f728d835abd..7bd77c047182 100644 --- a/packages/integrations/test/offline.test.ts +++ b/packages/integrations/test/offline.test.ts @@ -169,7 +169,7 @@ function initIntegration(options: { maxStoredEvents?: number } = {}): void { jest.spyOn(utils, 'getGlobalObject').mockImplementation( () => ({ - addEventListener: (_windowEvent, callback) => { + addEventListener: (_windowEvent: any, callback: any) => { eventListeners.push(callback); }, navigator: { @@ -202,7 +202,7 @@ function processEventListeners(): void { function processEvents(): void { eventProcessors.forEach(processor => { events.forEach(event => { - processor(event) as Event | null; + processor(event, {}) as Event | null; }); }); } diff --git a/packages/integrations/test/reportingobserver.test.ts b/packages/integrations/test/reportingobserver.test.ts index 1d6417e8ccc7..91547d5572f8 100644 --- a/packages/integrations/test/reportingobserver.test.ts +++ b/packages/integrations/test/reportingobserver.test.ts @@ -1,4 +1,4 @@ -import { Integration } from '@sentry/types'; +import { Hub, Integration } from '@sentry/types'; import { ReportingObserver } from '../src/reportingobserver'; @@ -13,10 +13,11 @@ const mockHub = { captureMessage: jest.fn(), }; -const getMockHubWithIntegration = (integration: Integration) => ({ - ...mockHub, - getIntegration: jest.fn(() => integration), -}); +const getMockHubWithIntegration = (integration: Integration) => + ({ + ...mockHub, + getIntegration: jest.fn(() => integration), + } as unknown as Hub); const mockReportingObserverConstructor = jest.fn(); const mockObserve = jest.fn(); @@ -49,7 +50,7 @@ describe('ReportingObserver', () => { expect(() => { reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(null) as any, + () => getMockHubWithIntegration(null as any), ); }).not.toThrow(); @@ -61,7 +62,7 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration) as any, + () => getMockHubWithIntegration(reportingObserverIntegration), ); expect(mockReportingObserverConstructor).toHaveBeenCalledTimes(1); @@ -75,7 +76,7 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver({ types: ['crash'] }); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration) as any, + () => getMockHubWithIntegration(reportingObserverIntegration), ); expect(mockReportingObserverConstructor).toHaveBeenCalledTimes(1); @@ -89,7 +90,7 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration) as any, + () => getMockHubWithIntegration(reportingObserverIntegration), ); expect(mockReportingObserverConstructor).toHaveBeenCalledTimes(1); @@ -103,7 +104,7 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration) as any, + () => getMockHubWithIntegration(reportingObserverIntegration), ); expect(mockObserve).toHaveBeenCalledTimes(1); @@ -115,7 +116,7 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(null) as any, + () => getMockHubWithIntegration(null as any), ); expect(() => { @@ -129,7 +130,7 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration) as any, + () => getMockHubWithIntegration(reportingObserverIntegration), ); reportingObserverIntegration.handler([ @@ -144,7 +145,7 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration) as any, + () => getMockHubWithIntegration(reportingObserverIntegration), ); reportingObserverIntegration.handler([ @@ -160,7 +161,7 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration) as any, + () => getMockHubWithIntegration(reportingObserverIntegration), ); const report1 = { type: 'crash', url: 'some url 1', body: { crashId: 'id1' } } as const; @@ -176,7 +177,7 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration) as any, + () => getMockHubWithIntegration(reportingObserverIntegration), ); reportingObserverIntegration.handler([{ type: 'crash', url: 'some url' }]); @@ -188,7 +189,7 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration) as any, + () => getMockHubWithIntegration(reportingObserverIntegration), ); const report = { @@ -207,7 +208,7 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration) as any, + () => getMockHubWithIntegration(reportingObserverIntegration), ); const report = { @@ -225,7 +226,7 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration) as any, + () => getMockHubWithIntegration(reportingObserverIntegration), ); const report = { @@ -243,7 +244,7 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration) as any, + () => getMockHubWithIntegration(reportingObserverIntegration), ); const report = { @@ -260,7 +261,7 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration) as any, + () => getMockHubWithIntegration(reportingObserverIntegration), ); const report = { type: 'crash', url: 'some url', body: { crashId: '', reason: '' } } as const; @@ -274,7 +275,7 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration) as any, + () => getMockHubWithIntegration(reportingObserverIntegration), ); const report = { @@ -292,7 +293,7 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration) as any, + () => getMockHubWithIntegration(reportingObserverIntegration), ); const report = { diff --git a/packages/integrations/test/rewriteframes.test.ts b/packages/integrations/test/rewriteframes.test.ts index 92346d99ebcb..bbe0a157e44a 100644 --- a/packages/integrations/test/rewriteframes.test.ts +++ b/packages/integrations/test/rewriteframes.test.ts @@ -3,18 +3,12 @@ import { Event, StackFrame } from '@sentry/types'; import { RewriteFrames } from '../src/rewriteframes'; let rewriteFrames: RewriteFrames; -let messageEvent: Event; let exceptionEvent: Event; let windowsExceptionEvent: Event; let multipleStacktracesEvent: Event; describe('RewriteFrames', () => { beforeEach(() => { - messageEvent = { - stacktrace: { - frames: [{ filename: '/www/src/app/file1.js' }, { filename: '/www/src/app/mo\\dule/file2.js' }], - }, - }; exceptionEvent = { exception: { values: [ @@ -65,12 +59,6 @@ describe('RewriteFrames', () => { rewriteFrames = new RewriteFrames(); }); - it('transforms messageEvent frames', () => { - const event = rewriteFrames.process(messageEvent); - expect(event.stacktrace!.frames![0].filename).toEqual('app:///file1.js'); - expect(event.stacktrace!.frames![1].filename).toEqual('app:///file2.js'); - }); - it('transforms exceptionEvent frames', () => { const event = rewriteFrames.process(exceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///file1.js'); @@ -85,12 +73,6 @@ describe('RewriteFrames', () => { }); }); - it('transforms messageEvent frames', () => { - const event = rewriteFrames.process(messageEvent); - expect(event.stacktrace!.frames![0].filename).toEqual('foobar/file1.js'); - expect(event.stacktrace!.frames![1].filename).toEqual('foobar/file2.js'); - }); - it('transforms exceptionEvent frames', () => { const event = rewriteFrames.process(exceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('foobar/file1.js'); @@ -117,12 +99,6 @@ describe('RewriteFrames', () => { }); }); - it('transforms messageEvent frames', () => { - const event = rewriteFrames.process(messageEvent); - expect(event.stacktrace!.frames![0].filename).toEqual('app:///src/app/file1.js'); - expect(event.stacktrace!.frames![1].filename).toEqual('app:///src/app/mo\\dule/file2.js'); - }); - it('transforms exceptionEvent frames', () => { const event = rewriteFrames.process(exceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///src/app/file1.js'); @@ -146,14 +122,6 @@ describe('RewriteFrames', () => { }); }); - it('transforms messageEvent frames', () => { - const event = rewriteFrames.process(messageEvent); - expect(event.stacktrace!.frames![0].filename).toEqual('/www/src/app/file1.js'); - expect(event.stacktrace!.frames![0].function).toEqual('whoops'); - expect(event.stacktrace!.frames![1].filename).toEqual('/www/src/app/mo\\dule/file2.js'); - expect(event.stacktrace!.frames![1].function).toEqual('whoops'); - }); - it('transforms exceptionEvent frames', () => { const event = rewriteFrames.process(exceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('/www/src/app/file1.js'); diff --git a/packages/integrations/tsconfig.cjs.json b/packages/integrations/tsconfig.cjs.json deleted file mode 100644 index 6782dae5e453..000000000000 --- a/packages/integrations/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "build/npm/dist" - } -} diff --git a/packages/integrations/tsconfig.esm.json b/packages/integrations/tsconfig.esm.json deleted file mode 100644 index feffe52ca581..000000000000 --- a/packages/integrations/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "build/npm/esm" - } -} diff --git a/packages/integrations/tsconfig.test.json b/packages/integrations/tsconfig.test.json index af7e36ec0eda..87f6afa06b86 100644 --- a/packages/integrations/tsconfig.test.json +++ b/packages/integrations/tsconfig.test.json @@ -5,7 +5,7 @@ "compilerOptions": { // should include all types from `./tsconfig.json` plus types for all test frameworks used - "types": ["jest"] + "types": ["node", "jest"] // other package-specific, test-specific options } diff --git a/packages/minimal/.eslintrc.js b/packages/minimal/.eslintrc.js deleted file mode 100644 index 5a2cc7f1ec08..000000000000 --- a/packages/minimal/.eslintrc.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - extends: ['../../.eslintrc.js'], -}; diff --git a/packages/minimal/.npmignore b/packages/minimal/.npmignore deleted file mode 100644 index 4ab7cc4f278f..000000000000 --- a/packages/minimal/.npmignore +++ /dev/null @@ -1,11 +0,0 @@ -# The paths in this file are specified so that they align with the file structure in `./build` after this file is copied -# into it by the prepack script `scripts/prepack.ts`. - -* - -# TODO remove bundles (which in the tarball are inside `build`) in v7 -!/build/**/* - -!/dist/**/* -!/esm/**/* -!/types/**/* diff --git a/packages/minimal/LICENSE b/packages/minimal/LICENSE deleted file mode 100644 index 8b42db873c95..000000000000 --- a/packages/minimal/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2019, Sentry -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/minimal/README.md b/packages/minimal/README.md deleted file mode 100644 index 4204f58846d4..000000000000 --- a/packages/minimal/README.md +++ /dev/null @@ -1,63 +0,0 @@ -

- - - -
-

- -# Sentry JavaScript SDK Minimal - -[![npm version](https://img.shields.io/npm/v/@sentry/minimal.svg)](https://www.npmjs.com/package/@sentry/minimal) -[![npm dm](https://img.shields.io/npm/dm/@sentry/minimal.svg)](https://www.npmjs.com/package/@sentry/minimal) -[![npm dt](https://img.shields.io/npm/dt/@sentry/minimal.svg)](https://www.npmjs.com/package/@sentry/minimal) -[![typedoc](https://img.shields.io/badge/docs-typedoc-blue.svg)](http://getsentry.github.io/sentry-javascript/) - -## Links - -- [Official SDK Docs](https://docs.sentry.io/quickstart/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) - -## General - -A minimal Sentry SDK that uses a configured client when embedded into an application. It allows library authors add -support for a Sentry SDK without having to bundle the entire SDK or being dependent on a specific platform. If the user -is using Sentry in their application and your library uses `@sentry/minimal`, the user receives all -breadcrumbs/messages/events you added to your libraries codebase. - -## Usage - -To use the minimal, you do not have to initialize an SDK. This should be handled by the user of your library. Instead, -directly use the exported functions of `@sentry/minimal` to add breadcrumbs or capture events: - -```javascript -import * as Sentry from '@sentry/minimal'; - -// Add a breadcrumb for future events -Sentry.addBreadcrumb({ - message: 'My Breadcrumb', - // ... -}); - -// Capture exceptions, messages or manual events -Sentry.captureMessage('Hello, world!'); -Sentry.captureException(new Error('Good bye')); -Sentry.captureEvent({ - message: 'Manual', - stacktrace: [ - // ... - ], -}); -``` - -Note that while strictly possible, it is discouraged to interfere with the event context. If for some reason your -library needs to inject context information, beware that this might override the user's context values: - -```javascript -// Set user information, as well as tags and further extras -Sentry.configureScope(scope => { - scope.setExtra('battery', 0.7); - scope.setTag('user_mode', 'admin'); - scope.setUser({ id: '4711' }); - // scope.clear(); -}); -``` diff --git a/packages/minimal/package.json b/packages/minimal/package.json deleted file mode 100644 index 2ae39401d7b2..000000000000 --- a/packages/minimal/package.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "name": "@sentry/minimal", - "version": "6.19.7", - "description": "Sentry minimal library that can be used in other packages", - "repository": "git://github.com/getsentry/sentry-javascript.git", - "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/minimal", - "author": "Sentry", - "license": "BSD-3-Clause", - "engines": { - "node": ">=6" - }, - "main": "build/dist/index.js", - "module": "build/esm/index.js", - "types": "build/types/index.d.ts", - "publishConfig": { - "access": "public" - }, - "dependencies": { - "@sentry/hub": "6.19.7", - "@sentry/types": "6.19.7", - "tslib": "^1.9.3" - }, - "scripts": { - "build": "run-p build:cjs build:esm build:types", - "build:cjs": "tsc -p tsconfig.cjs.json", - "build:dev": "run-s build", - "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "tsc -p tsconfig.esm.json", - "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", - "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", - "build:dev:watch": "run-s build:watch", - "build:es5:watch": "yarn build:cjs:watch # *** backwards compatibility - remove in v7 ***", - "build:esm:watch": "tsc -p tsconfig.esm.json --watch", - "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", - "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf dist esm build coverage", - "fix": "run-s fix:eslint fix:prettier", - "fix:eslint": "eslint . --format stylish --fix", - "fix:prettier": "prettier --write \"{src,test,scripts}/**/*.ts\"", - "link:yarn": "yarn link", - "lint": "run-s lint:prettier lint:eslint", - "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", - "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", - "test": "jest", - "test:watch": "jest --watch" - }, - "volta": { - "extends": "../../package.json" - }, - "jest": { - "collectCoverage": true, - "transform": { - "^.+\\.ts$": "ts-jest" - }, - "moduleFileExtensions": [ - "js", - "ts" - ], - "testEnvironment": "node", - "testMatch": [ - "**/*.test.ts" - ], - "globals": { - "ts-jest": { - "tsConfig": "./tsconfig.json", - "diagnostics": false - } - } - }, - "sideEffects": false -} diff --git a/packages/minimal/test/mocks/client.ts b/packages/minimal/test/mocks/client.ts deleted file mode 100644 index 813fd694c38a..000000000000 --- a/packages/minimal/test/mocks/client.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { getCurrentHub } from '@sentry/hub'; - -export class TestClient { - public static instance?: TestClient; - - public constructor(public options: Record) { - TestClient.instance = this; - } - - public mySecretPublicMethod(str: string): string { - return `secret: ${str}`; - } -} - -export class TestClient2 {} - -export function init(options: Record): void { - getCurrentHub().bindClient(new TestClient(options) as any); -} diff --git a/packages/minimal/tsconfig.cjs.json b/packages/minimal/tsconfig.cjs.json deleted file mode 100644 index e3a918fc70af..000000000000 --- a/packages/minimal/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "build/dist" - } -} diff --git a/packages/minimal/tsconfig.esm.json b/packages/minimal/tsconfig.esm.json deleted file mode 100644 index 0b86c52918cc..000000000000 --- a/packages/minimal/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "build/esm" - } -} diff --git a/packages/minimal/tsconfig.json b/packages/minimal/tsconfig.json deleted file mode 100644 index bf45a09f2d71..000000000000 --- a/packages/minimal/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.json", - - "include": ["src/**/*"], - - "compilerOptions": { - // package-specific options - } -} diff --git a/packages/minimal/tsconfig.types.json b/packages/minimal/tsconfig.types.json deleted file mode 100644 index 65455f66bd75..000000000000 --- a/packages/minimal/tsconfig.types.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "declaration": true, - "declarationMap": true, - "emitDeclarationOnly": true, - "outDir": "build/types" - } -} diff --git a/packages/nextjs/README.md b/packages/nextjs/README.md index 19660295f666..3f8147d1d9a6 100644 --- a/packages/nextjs/README.md +++ b/packages/nextjs/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Official Sentry SDK for Next.js diff --git a/packages/nextjs/jest.config.js b/packages/nextjs/jest.config.js new file mode 100644 index 000000000000..24f49ab59a4c --- /dev/null +++ b/packages/nextjs/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest/jest.config.js'); diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 760833113817..9acd4f1e3332 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,15 +1,15 @@ { "name": "@sentry/nextjs", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Official Sentry SDK for Next.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs", "author": "Sentry", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=8" }, - "main": "build/dist/index.server.js", + "main": "build/cjs/index.server.js", "module": "build/esm/index.server.js", "browser": "build/esm/index.client.js", "types": "build/types/index.server.d.ts", @@ -17,18 +17,18 @@ "access": "public" }, "dependencies": { - "@sentry/core": "6.19.7", - "@sentry/hub": "6.19.7", - "@sentry/integrations": "6.19.7", - "@sentry/node": "6.19.7", - "@sentry/react": "6.19.7", - "@sentry/tracing": "6.19.7", - "@sentry/utils": "6.19.7", - "@sentry/webpack-plugin": "1.18.8", + "@sentry/core": "7.0.0-rc.0", + "@sentry/hub": "7.0.0-rc.0", + "@sentry/integrations": "7.0.0-rc.0", + "@sentry/node": "7.0.0-rc.0", + "@sentry/react": "7.0.0-rc.0", + "@sentry/tracing": "7.0.0-rc.0", + "@sentry/types": "7.0.0-rc.0", + "@sentry/utils": "7.0.0-rc.0", + "@sentry/webpack-plugin": "1.18.9", "tslib": "^1.9.3" }, "devDependencies": { - "@sentry/types": "6.19.7", "@types/webpack": "^4.41.31", "next": "10.1.3" }, @@ -43,25 +43,20 @@ } }, "scripts": { - "build": "run-p build:cjs build:esm build:types", - "build:cjs": "tsc -p tsconfig.cjs.json", + "build": "run-p build:rollup build:types", "build:dev": "run-s build", - "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "tsc -p tsconfig.esm.json", + "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", - "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", + "build:watch": "run-p build:rollup:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:es5:watch": "yarn build:cjs:watch # *** backwards compatibility - remove in v7 ***", - "build:esm:watch": "tsc -p tsconfig.esm.json --watch", + "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.client.ts && madge --circular --exclude 'config/types\\.ts' src/index.server.ts # see https://github.com/pahen/madge/issues/306", - "clean": "rimraf dist esm build coverage *.js *.js.map *.d.ts", + "clean": "rimraf build coverage sentry-nextjs-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/*.ts\"", - "link:yarn": "yarn link", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", @@ -76,28 +71,8 @@ "volta": { "extends": "../../package.json" }, - "jest": { - "collectCoverage": true, - "transform": { - "^.+\\.ts$": "ts-jest" - }, - "moduleFileExtensions": [ - "js", - "ts" - ], - "testEnvironment": "node", - "testMatch": [ - "**/*.test.ts" - ], - "globals": { - "ts-jest": { - "tsConfig": "./tsconfig.json", - "diagnostics": false - } - } - }, "sideEffects": [ - "./dist/index.server.js", + "./cjs/index.server.js", "./esm/index.server.js", "./src/index.server.ts" ] diff --git a/packages/nextjs/rollup.npm.config.js b/packages/nextjs/rollup.npm.config.js new file mode 100644 index 000000000000..b0ecb2b94b05 --- /dev/null +++ b/packages/nextjs/rollup.npm.config.js @@ -0,0 +1,12 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + // We need to include `instrumentServer.ts` separately because it's only conditionally required, and so rollup + // doesn't automatically include it when calculating the module dependency tree. + entrypoints: ['src/index.server.ts', 'src/index.client.ts', 'src/utils/instrumentServer.ts'], + // prevent this nextjs code from ending up in our built package (this doesn't happen automatially because the name + // doesn't match an SDK dependency) + packageSpecificConfig: { external: ['next/router'] }, + }), +); diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index d98d7ec3503c..568a23cf5d88 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -325,6 +325,12 @@ export function getWebpackPluginOptions( return { ...defaultPluginOptions, ...userPluginOptions }; } +/** + * NOTE: `eval` usage is a workaround for @vercel/nft detecting the binary itself as the hard dependency + * and effectively always including it in the bundle, which is not what we want. + * ref: https://github.com/getsentry/sentry-javascript/issues/3865 + * ref: https://github.com/vercel/nft/issues/203 + */ function ensureCLIBinaryExists(): boolean { - return fs.existsSync(path.join(require.resolve('@sentry/cli'), '../../sentry-cli')); + return eval("fs.existsSync(path.join(require.resolve('@sentry/cli'), '../../sentry-cli'))"); } diff --git a/packages/nextjs/src/index.client.ts b/packages/nextjs/src/index.client.ts index 817f8ad3ca56..b5993fb734ab 100644 --- a/packages/nextjs/src/index.client.ts +++ b/packages/nextjs/src/index.client.ts @@ -1,5 +1,6 @@ -import { configureScope, init as reactInit, Integrations as BrowserIntegrations } from '@sentry/react'; +import { configureScope, init as reactInit, Integrations } from '@sentry/react'; import { BrowserTracing, defaultRequestInstrumentationOptions } from '@sentry/tracing'; +import { EventProcessor } from '@sentry/types'; import { nextRouterInstrumentation } from './performance/client'; import { buildMetadata } from './utils/metadata'; @@ -9,12 +10,8 @@ import { addIntegration, UserIntegrations } from './utils/userIntegrations'; export * from '@sentry/react'; export { nextRouterInstrumentation } from './performance/client'; -export const Integrations = { ...BrowserIntegrations, BrowserTracing }; +export { Integrations }; -// This is already exported as part of `Integrations` above (and for the moment will remain so for -// backwards compatibility), but that interferes with treeshaking, so we also export it separately -// here. -// // Previously we expected users to import `BrowserTracing` like this: // // import { Integrations } from '@sentry/nextjs'; @@ -27,33 +24,44 @@ export const Integrations = { ...BrowserIntegrations, BrowserTracing }; // const instance = new BrowserTracing(); export { BrowserTracing }; +// Treeshakable guard to remove all code related to tracing +declare const __SENTRY_TRACING__: boolean; + /** Inits the Sentry NextJS SDK on the browser with the React SDK. */ export function init(options: NextjsOptions): void { buildMetadata(options, ['nextjs', 'react']); options.environment = options.environment || process.env.NODE_ENV; - // Only add BrowserTracing if a tracesSampleRate or tracesSampler is set - const integrations = - options.tracesSampleRate === undefined && options.tracesSampler === undefined - ? options.integrations - : createClientIntegrations(options.integrations); + let integrations = options.integrations; + + // Guard below evaluates to true unless __SENTRY_TRACING__ is text-replaced with "false" + if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) { + // Only add BrowserTracing if a tracesSampleRate or tracesSampler is set + if (options.tracesSampleRate !== undefined || options.tracesSampler !== undefined) { + integrations = createClientIntegrations(options.integrations); + } + } reactInit({ ...options, integrations, }); + configureScope(scope => { scope.setTag('runtime', 'browser'); - scope.addEventProcessor(event => (event.type === 'transaction' && event.transaction === '/404' ? null : event)); + const filterTransactions: EventProcessor = event => + event.type === 'transaction' && event.transaction === '/404' ? null : event; + filterTransactions.id = 'NextClient404Filter'; + scope.addEventProcessor(filterTransactions); }); } -const defaultBrowserTracingIntegration = new BrowserTracing({ - tracingOrigins: [...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/], - routingInstrumentation: nextRouterInstrumentation, -}); - function createClientIntegrations(integrations?: UserIntegrations): UserIntegrations { + const defaultBrowserTracingIntegration = new BrowserTracing({ + tracingOrigins: [...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/], + routingInstrumentation: nextRouterInstrumentation, + }); + if (integrations) { return addIntegration(defaultBrowserTracingIntegration, integrations, { BrowserTracing: { keyPath: 'options.routingInstrumentation', value: nextRouterInstrumentation }, diff --git a/packages/nextjs/src/index.server.ts b/packages/nextjs/src/index.server.ts index a57ee89443a5..8d34a989264a 100644 --- a/packages/nextjs/src/index.server.ts +++ b/packages/nextjs/src/index.server.ts @@ -2,7 +2,7 @@ import { Carrier, getHubFromCarrier, getMainCarrier } from '@sentry/hub'; import { RewriteFrames } from '@sentry/integrations'; import { configureScope, getCurrentHub, init as nodeInit, Integrations } from '@sentry/node'; import { hasTracingEnabled } from '@sentry/tracing'; -import { Event } from '@sentry/types'; +import { EventProcessor } from '@sentry/types'; import { escapeStringForRegex, logger } from '@sentry/utils'; import * as domainModule from 'domain'; import * as path from 'path'; @@ -71,6 +71,12 @@ export function init(options: NextjsOptions): void { nodeInit(options); + const filterTransactions: EventProcessor = event => { + return event.type === 'transaction' && event.transaction === '/404' ? null : event; + }; + + filterTransactions.id = 'NextServer404Filter'; + configureScope(scope => { scope.setTag('runtime', 'node'); if (isVercel) { @@ -131,12 +137,8 @@ function addServerIntegrations(options: NextjsOptions): void { } } -function filterTransactions(event: Event): Event | null { - return event.type === 'transaction' && event.transaction === '/404' ? null : event; -} - +export type { SentryWebpackPluginOptions } from './config/types'; export { withSentryConfig } from './config'; -export { SentryWebpackPluginOptions } from './config/types'; export { withSentry } from './utils/withSentry'; // Wrap various server methods to enable error monitoring and tracing. (Note: This only happens for non-Vercel diff --git a/packages/nextjs/src/utils/instrumentServer.ts b/packages/nextjs/src/utils/instrumentServer.ts index cba8e0ab9423..bf4adc9ec8b3 100644 --- a/packages/nextjs/src/utils/instrumentServer.ts +++ b/packages/nextjs/src/utils/instrumentServer.ts @@ -8,7 +8,14 @@ import { startTransaction, } from '@sentry/node'; import { extractTraceparentData, getActiveTransaction, hasTracingEnabled } from '@sentry/tracing'; -import { addExceptionMechanism, fill, isString, logger, stripUrlQueryAndFragment } from '@sentry/utils'; +import { + addExceptionMechanism, + fill, + isString, + logger, + parseBaggageString, + stripUrlQueryAndFragment, +} from '@sentry/utils'; import * as domain from 'domain'; import * as http from 'http'; import { default as createNextServer } from 'next'; @@ -252,6 +259,9 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler { IS_DEBUG_BUILD && logger.log(`[Tracing] Continuing trace ${traceparentData?.traceId}.`); } + const baggage = + nextReq.headers && isString(nextReq.headers.baggage) && parseBaggageString(nextReq.headers.baggage); + // pull off query string, if any const reqPath = stripUrlQueryAndFragment(nextReq.url); @@ -265,6 +275,7 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler { op: 'http.server', metadata: { requestPath: reqPath }, ...traceparentData, + ...(baggage && { metadata: { baggage: baggage } }), }, // Extra context passed to the `tracesSampler` (Note: We're combining `nextReq` and `req` this way in order // to not break people's `tracesSampler` functions, even though the format of `nextReq` has changed (see diff --git a/packages/nextjs/src/utils/withSentry.ts b/packages/nextjs/src/utils/withSentry.ts index 00e76ec40b69..742d9f4c9280 100644 --- a/packages/nextjs/src/utils/withSentry.ts +++ b/packages/nextjs/src/utils/withSentry.ts @@ -1,7 +1,14 @@ import { captureException, flush, getCurrentHub, Handlers, startTransaction } from '@sentry/node'; import { extractTraceparentData, hasTracingEnabled } from '@sentry/tracing'; import { Transaction } from '@sentry/types'; -import { addExceptionMechanism, isString, logger, objectify, stripUrlQueryAndFragment } from '@sentry/utils'; +import { + addExceptionMechanism, + isString, + logger, + objectify, + parseBaggageString, + stripUrlQueryAndFragment, +} from '@sentry/utils'; import * as domain from 'domain'; import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'; @@ -48,6 +55,8 @@ export const withSentry = (origHandler: NextApiHandler): WrappedNextApiHandler = IS_DEBUG_BUILD && logger.log(`[Tracing] Continuing trace ${traceparentData?.traceId}.`); } + const baggage = req.headers && isString(req.headers.baggage) && parseBaggageString(req.headers.baggage); + const url = `${req.url}`; // pull off query string, if any let reqPath = stripUrlQueryAndFragment(url); @@ -66,6 +75,7 @@ export const withSentry = (origHandler: NextApiHandler): WrappedNextApiHandler = name: `${reqMethod}${reqPath}`, op: 'http.server', ...traceparentData, + ...(baggage && { metadata: { baggage: baggage } }), }, // extra context passed to the `tracesSampler` { request: req }, diff --git a/packages/nextjs/test/index.client.test.ts b/packages/nextjs/test/index.client.test.ts index 44edf997ef50..a143591ad8f0 100644 --- a/packages/nextjs/test/index.client.test.ts +++ b/packages/nextjs/test/index.client.test.ts @@ -14,7 +14,7 @@ const global = getGlobalObject(); const reactInit = jest.spyOn(SentryReact, 'init'); const captureEvent = jest.spyOn(BaseClient.prototype, 'captureEvent'); -const logError = jest.spyOn(logger, 'error'); +const logWarn = jest.spyOn(logger, 'warn'); describe('Client init()', () => { afterEach(() => { @@ -68,14 +68,14 @@ describe('Client init()', () => { tracesSampleRate: 1.0, }); const hub = getCurrentHub(); - const sendEvent = jest.spyOn(hub.getClient()!.getTransport!(), 'sendEvent'); + const transportSend = jest.spyOn(hub.getClient()!.getTransport()!, 'send'); const transaction = hub.startTransaction({ name: '/404' }); transaction.finish(); - expect(sendEvent).not.toHaveBeenCalled(); + expect(transportSend).not.toHaveBeenCalled(); expect(captureEvent.mock.results[0].value).toBeUndefined(); - expect(logError).toHaveBeenCalledWith(new SentryError('An event processor returned null, will not send event.')); + expect(logWarn).toHaveBeenCalledWith(new SentryError('An event processor returned null, will not send event.')); }); describe('integrations', () => { diff --git a/packages/nextjs/test/index.server.test.ts b/packages/nextjs/test/index.server.test.ts index e4e359a10624..d926d8bbd57e 100644 --- a/packages/nextjs/test/index.server.test.ts +++ b/packages/nextjs/test/index.server.test.ts @@ -16,7 +16,7 @@ const global = getGlobalObject(); (global as typeof global & { __rewriteFramesDistDir__: string }).__rewriteFramesDistDir__ = '.next'; const nodeInit = jest.spyOn(SentryNode, 'init'); -const logError = jest.spyOn(logger, 'error'); +const logWarn = jest.spyOn(logger, 'warn'); describe('Server init()', () => { afterEach(() => { @@ -95,7 +95,7 @@ describe('Server init()', () => { tracesSampleRate: 1.0, }); const hub = getCurrentHub(); - const sendEvent = jest.spyOn(hub.getClient()!.getTransport!(), 'sendEvent'); + const transportSend = jest.spyOn(hub.getClient()!.getTransport()!, 'send'); const transaction = hub.startTransaction({ name: '/404' }); transaction.finish(); @@ -103,8 +103,8 @@ describe('Server init()', () => { // We need to flush because the event processor pipeline is async whereas transaction.finish() is sync. await SentryNode.flush(); - expect(sendEvent).not.toHaveBeenCalled(); - expect(logError).toHaveBeenCalledWith(new SentryError('An event processor returned null, will not send event.')); + expect(transportSend).not.toHaveBeenCalled(); + expect(logWarn).toHaveBeenCalledWith(new SentryError('An event processor returned null, will not send event.')); }); it("initializes both global hub and domain hub when there's an active domain", () => { diff --git a/packages/nextjs/test/integration/package.json b/packages/nextjs/test/integration/package.json index 5523ee933d20..2d8588dbf1a3 100644 --- a/packages/nextjs/test/integration/package.json +++ b/packages/nextjs/test/integration/package.json @@ -28,7 +28,6 @@ "@sentry/core": "file:../../../core", "@sentry/hub": "file:../../../hub", "@sentry/integrations": "file:../../../integrations", - "@sentry/minimal": "file:../../../minimal", "@sentry/node": "file:../../../node", "@sentry/react": "file:../../../react", "@sentry/tracing": "file:../../../tracing", diff --git a/packages/nextjs/test/integration/test/utils/client.js b/packages/nextjs/test/integration/test/utils/client.js index d2c11837db3d..76d88832c863 100644 --- a/packages/nextjs/test/integration/test/utils/client.js +++ b/packages/nextjs/test/integration/test/utils/client.js @@ -20,7 +20,7 @@ const createRequestInterceptor = env => { } if (isEventRequest(request)) { - logIf(process.env.LOG_REQUESTS, 'Intercepted Event', extractEventFromRequest(request), env.argv.depth); + logIf(process.env.LOG_REQUESTS, 'Intercepted Event', extractEnvelopeFromRequest(request), env.argv.depth); env.requests.events.push(request); } else if (isSessionRequest(request)) { logIf(process.env.LOG_REQUESTS, 'Intercepted Session', extractEnvelopeFromRequest(request), env.argv.depth); @@ -38,14 +38,14 @@ const isSentryRequest = request => { return /sentry.io\/api/.test(request.url()); }; -const isEventRequest = request => { - return /sentry.io\/api\/\d+\/store/.test(request.url()); -}; - const isEnvelopeRequest = request => { return /sentry.io\/api\/\d+\/envelope/.test(request.url()); }; +const isEventRequest = request => { + return isEnvelopeRequest(request) && extractEnvelopeFromRequest(request).itemHeader.type === 'event'; +}; + const isSessionRequest = request => { return isEnvelopeRequest(request) && extractEnvelopeFromRequest(request).itemHeader.type === 'session'; }; @@ -54,21 +54,25 @@ const isTransactionRequest = request => { return isEnvelopeRequest(request) && extractEnvelopeFromRequest(request).itemHeader.type === 'transaction'; }; -const expectEvent = (request, expectedEvent) => { +const expectEvent = (request, expectedItem) => { if (!request) throw new Error('Event missing'); - return assertObjectMatches(extractEventFromRequest(request), expectedEvent); + const { itemHeader, item } = extractEnvelopeFromRequest(request); + strictEqual(itemHeader.type, 'event'); + assertObjectMatches(item, expectedItem); }; const expectSession = (request, expectedItem) => { if (!request) throw new Error('Session missing'); const { itemHeader, item } = extractEnvelopeFromRequest(request); - return itemHeader.type === 'session' && assertObjectMatches(item, expectedItem); + strictEqual(itemHeader.type, 'session'); + assertObjectMatches(item, expectedItem); }; const expectTransaction = (request, expectedItem) => { if (!request) throw new Error('Transaction missing'); const { itemHeader, item } = extractEnvelopeFromRequest(request); - return itemHeader.type === 'transaction' && assertObjectMatches(item, expectedItem); + strictEqual(itemHeader.type, 'transaction'); + assertObjectMatches(item, expectedItem); }; const expectRequestCount = (requests, expectedCount, timeout = 100) => { @@ -89,10 +93,6 @@ const expectRequestCount = (requests, expectedCount, timeout = 100) => { }); }; -const extractEventFromRequest = request => { - return JSON.parse(request.postData()); -}; - const extractEnvelopeFromRequest = request => { return parseEnvelope(request.postData()); }; @@ -111,8 +111,6 @@ const assertObjectMatches = (actual, expected) => { strictEqual(actual[key], expectedValue); } } - - return true; }; module.exports = { @@ -122,7 +120,6 @@ module.exports = { expectSession, expectTransaction, extractEnvelopeFromRequest, - extractEventFromRequest, isEnvelopeRequest, isEventRequest, isSentryRequest, diff --git a/packages/nextjs/test/integration/test/utils/server.js b/packages/nextjs/test/integration/test/utils/server.js index 8844c8cc1799..8c38446e3f41 100644 --- a/packages/nextjs/test/integration/test/utils/server.js +++ b/packages/nextjs/test/integration/test/utils/server.js @@ -35,15 +35,17 @@ const getAsync = (url, rewrap = false) => { const interceptEventRequest = (expectedEvent, argv, testName = '') => { return nock('https://dsn.ingest.sentry.io') - .post('/api/1337/store/', body => { + .post('/api/1337/envelope/', body => { + const { envelopeHeader, itemHeader, item } = parseEnvelope(body); logIf( process.env.LOG_REQUESTS, '\nIntercepted Event' + (testName.length ? ` (from test \`${testName}\`)` : ''), - body, + { envelopeHeader, itemHeader, item }, argv.depth, ); - return objectMatches(body, expectedEvent); + return itemHeader.type === 'event' && objectMatches(item, expectedEvent); }) + .query(true) // accept any query params - used for sentry_key param used by the envelope endpoint .reply(200); }; @@ -59,6 +61,7 @@ const interceptSessionRequest = (expectedItem, argv, testName = '') => { ); return itemHeader.type === 'session' && objectMatches(item, expectedItem); }) + .query(true) // accept any query params - used for sentry_key param used by the envelope endpoint .reply(200); }; @@ -74,6 +77,7 @@ const interceptTracingRequest = (expectedItem, argv, testName = '') => { ); return itemHeader.type === 'transaction' && objectMatches(item, expectedItem); }) + .query(true) // accept any query params - used for sentry_key param used by the envelope endpoint .reply(200); }; diff --git a/packages/nextjs/test/run-integration-tests.sh b/packages/nextjs/test/run-integration-tests.sh index 4f31ce75cd2b..715273afb42c 100755 --- a/packages/nextjs/test/run-integration-tests.sh +++ b/packages/nextjs/test/run-integration-tests.sh @@ -78,6 +78,19 @@ for NEXTJS_VERSION in 10 11 12; do WEBPACK_VERSION=5 || WEBPACK_VERSION=4 + # Node v18 only with Webpack 5 and above + # https://github.com/webpack/webpack/issues/14532#issuecomment-947513562 + # Context: https://github.com/vercel/next.js/issues/30078#issuecomment-947338268 + if [ "$NODE_MAJOR" -gt "17" ] && [ "$WEBPACK_VERSION" -eq "4" ]; then + echo "[nextjs$NEXTJS_VERSION | webpack@$WEBPACK_VERSION] Node $NODE_MAJOR not compatible with Webpack $WEBPACK_VERSION" + exit 0 + fi + if [ "$NODE_MAJOR" -gt "17" ] && [ "$NEXTJS_VERSION" -eq "10" ]; then + echo "[nextjs$NEXTJS_VERSION | webpack@$WEBPACK_VERSION] Node $NODE_MAJOR not compatible with Webpack $WEBPACK_VERSION" + exit 0 + fi + + # next 10 defaults to webpack 4 and next 11 defaults to webpack 5, but each can use either based on settings if [ "$NEXTJS_VERSION" -eq "10" ]; then sed "s/%RUN_WEBPACK_5%/$RUN_WEBPACK_5/g" next.config.js diff --git a/packages/nextjs/tsconfig.cjs.json b/packages/nextjs/tsconfig.cjs.json deleted file mode 100644 index e3a918fc70af..000000000000 --- a/packages/nextjs/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "build/dist" - } -} diff --git a/packages/nextjs/tsconfig.esm.json b/packages/nextjs/tsconfig.esm.json deleted file mode 100644 index 0b86c52918cc..000000000000 --- a/packages/nextjs/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "build/esm" - } -} diff --git a/packages/nextjs/vercel/install-sentry-from-branch.sh b/packages/nextjs/vercel/install-sentry-from-branch.sh index af79d10ea04e..84a0f59eb7c3 100644 --- a/packages/nextjs/vercel/install-sentry-from-branch.sh +++ b/packages/nextjs/vercel/install-sentry-from-branch.sh @@ -27,11 +27,8 @@ yarn --prod false echo " " echo "BUILDING SDK" -# We need to build es5 versions because `next.config.js` calls `require` on the SDK (to get `withSentryConfig`) and -# therefore it looks for `dist/index.js` -yarn build:cjs -# We need to build esm versions because that's what `next` actually uses when it builds the app -yarn build:esm +# build types, cjs, and esm +yarn build:dev # Set all packages in the repo to point to their siblings as file dependencies. That way, when we install the local copy # of @sentry/nextjs, it'll pull the local copy of each of its @sentry/* dependents. This mimics what Lerna does with @@ -51,7 +48,7 @@ PACKAGE_NAMES=$(ls $PACKAGES_DIR) for package in ${PACKAGE_NAMES[@]}; do # Within a given package.json file, search for each of the other packages in turn, and if found, make the replacement for package_dep in ${PACKAGE_NAMES[@]}; do - sed -Ei /"@sentry\/${package_dep}"/s/"[0-9]+\.[0-9]+\.[0-9]+"/"file:${ESCAPED_PACKAGES_DIR}\/${package_dep}"/ ${PACKAGES_DIR}/${package}/package.json + sed -Ei /"@sentry\/${package_dep}"/s/"[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)\.[0-9]+)?"/"file:${ESCAPED_PACKAGES_DIR}\/${package_dep}"/ ${PACKAGES_DIR}/${package}/package.json done done diff --git a/packages/node-integration-tests/.eslintrc.js b/packages/node-integration-tests/.eslintrc.js index fdcde4fa0f14..5a3ecdd7617a 100644 --- a/packages/node-integration-tests/.eslintrc.js +++ b/packages/node-integration-tests/.eslintrc.js @@ -4,7 +4,20 @@ module.exports = { jest: true, }, extends: ['../../.eslintrc.js'], - parserOptions: { - sourceType: 'module', - }, + overrides: [ + { + files: ['utils/**/*.ts'], + parserOptions: { + project: ['tsconfig.json'], + sourceType: 'module', + }, + }, + { + files: ['suites/**/*.ts'], + parserOptions: { + project: ['tsconfig.test.json'], + sourceType: 'module', + }, + }, + ], }; diff --git a/packages/node-integration-tests/jest.config.js b/packages/node-integration-tests/jest.config.js index 8edea137a75b..9a2862dd9b8d 100644 --- a/packages/node-integration-tests/jest.config.js +++ b/packages/node-integration-tests/jest.config.js @@ -1,10 +1,7 @@ -const config = { - transform: { - '^.+\\.ts$': 'ts-jest', - }, - testEnvironment: 'node', +const baseConfig = require('../../jest/jest.config.js'); + +module.exports = { + globalSetup: '/utils/setup-tests.ts', + ...baseConfig, testMatch: ['**/test.ts'], - moduleFileExtensions: ['js', 'ts'], }; - -module.exports = config; diff --git a/packages/node-integration-tests/package.json b/packages/node-integration-tests/package.json index e6014bd9a367..2ce973eceb4b 100644 --- a/packages/node-integration-tests/package.json +++ b/packages/node-integration-tests/package.json @@ -1,30 +1,40 @@ { "name": "@sentry-internal/node-integration-tests", - "version": "6.19.7", + "version": "7.0.0-rc.0", "license": "MIT", "engines": { "node": ">=10" }, "private": true, "scripts": { + "clean": "rimraf -g **/node_modules", + "prisma:init": "(cd suites/tracing/prisma-orm && ts-node ./setup.ts)", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{suites,utils}/**/*.ts\"", "type-check": "tsc", - "test": "jest --detectOpenHandles --runInBand --forceExit", + "pretest": "run-s --silent prisma:init", + "test": "jest --runInBand --forceExit", "test:watch": "yarn test --watch" }, "dependencies": { + "@prisma/client": "^3.12.0", "@types/mongodb": "^3.6.20", "@types/mysql": "^2.15.21", "@types/pg": "^8.6.5", "cors": "^2.8.5", "express": "^4.17.3", "mongodb": "^3.7.3", - "mongodb-memory-server": "^7.6.3", + "mongodb-memory-server-global": "^7.6.3", "mysql": "^2.18.1", "nock": "^13.1.0", "pg": "^8.7.3", "portfinder": "^1.0.28" + }, + "config": { + "mongodbMemoryServer": { + "preferGlobalPath": true, + "runtimeDownload": false + } } } diff --git a/packages/node-integration-tests/suites/express/handle-error/test.ts b/packages/node-integration-tests/suites/express/handle-error/test.ts index 66cc1ec2d9ae..9ae4586f6510 100644 --- a/packages/node-integration-tests/suites/express/handle-error/test.ts +++ b/packages/node-integration-tests/suites/express/handle-error/test.ts @@ -1,12 +1,12 @@ -import { assertSentryEvent, getEventRequest, runServer } from '../../../utils/index'; +import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../utils/index'; test('should capture and send Express controller error.', async () => { const url = await runServer(__dirname, `${__dirname}/server.ts`); - const event = await getEventRequest(`${url}/express`); + const event = await getEnvelopeRequest(`${url}/express`); - expect((event as any).exception.values[0].stacktrace.frames.length).toBeGreaterThan(0); + expect((event[2] as any).exception.values[0].stacktrace.frames.length).toBeGreaterThan(0); - assertSentryEvent(event, { + assertSentryEvent(event[2] as any, { exception: { values: [ { diff --git a/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts b/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts new file mode 100644 index 000000000000..81dddb7c1f97 --- /dev/null +++ b/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts @@ -0,0 +1,52 @@ +import * as path from 'path'; + +import { getAPIResponse, runServer } from '../../../../utils/index'; +import { TestAPIResponse } from '../server'; + +test('Should assign `baggage` header which contains 3rd party trace baggage data of an outgoing request.', async () => { + const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`); + + const response = (await getAPIResponse(new URL(`${url}/express`), { + baggage: 'foo=bar,bar=baz', + })) as TestAPIResponse; + + expect(response).toBeDefined(); + expect(response).toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + baggage: expect.stringContaining('foo=bar,bar=baz'), + }, + }); +}); + +test('Should assign `baggage` header which contains sentry trace baggage data of an outgoing request.', async () => { + const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`); + + const response = (await getAPIResponse(new URL(`${url}/express`), { + baggage: 'sentry-version=1.0.0,sentry-environment=production', + })) as TestAPIResponse; + + expect(response).toBeDefined(); + expect(response).toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + baggage: expect.stringContaining('sentry-version=1.0.0,sentry-environment=production'), + }, + }); +}); + +test('Should assign `baggage` header which contains sentry and 3rd party trace baggage data of an outgoing request.', async () => { + const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`); + + const response = (await getAPIResponse(new URL(`${url}/express`), { + baggage: 'sentry-version=1.0.0,sentry-environment=production,dogs=great', + })) as TestAPIResponse; + + expect(response).toBeDefined(); + expect(response).toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + baggage: expect.stringContaining('dogs=great,sentry-version=1.0.0,sentry-environment=production'), + }, + }); +}); diff --git a/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts b/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts new file mode 100644 index 000000000000..70b6cc19edee --- /dev/null +++ b/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts @@ -0,0 +1,19 @@ +import * as path from 'path'; + +import { getAPIResponse, runServer } from '../../../../utils/index'; +import { TestAPIResponse } from '../server'; + +test('should attach a `baggage` header to an outgoing request.', async () => { + const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`); + + const response = (await getAPIResponse(new URL(`${url}/express`))) as TestAPIResponse; + + expect(response).toBeDefined(); + expect(response).toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + // TODO this is currently still empty but eventually it should contain sentry data + baggage: expect.stringMatching(''), + }, + }); +}); diff --git a/packages/node-integration-tests/suites/express/sentry-trace/server.ts b/packages/node-integration-tests/suites/express/sentry-trace/server.ts index 172c2edd3900..632d9b8338fb 100644 --- a/packages/node-integration-tests/suites/express/sentry-trace/server.ts +++ b/packages/node-integration-tests/suites/express/sentry-trace/server.ts @@ -6,6 +6,8 @@ import http from 'http'; const app = express(); +export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; + Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', diff --git a/packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts b/packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts index 5ee98789c299..47382b3fdb4b 100644 --- a/packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts +++ b/packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts @@ -1,14 +1,15 @@ import { TRACEPARENT_REGEXP } from '@sentry/utils'; +import * as path from 'path'; import { getAPIResponse, runServer } from '../../../../utils/index'; -import path = require('path'); +import { TestAPIResponse } from '../server'; test('Should assign `sentry-trace` header which sets parent trace id of an outgoing request.', async () => { const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`); - const response = await getAPIResponse(new URL(`${url}/express`), { + const response = (await getAPIResponse(new URL(`${url}/express`), { 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0', - }); + })) as TestAPIResponse; expect(response).toBeDefined(); expect(response).toMatchObject({ diff --git a/packages/node-integration-tests/suites/express/sentry-trace/trace-header-out/test.ts b/packages/node-integration-tests/suites/express/sentry-trace/trace-header-out/test.ts index ca7eb56fd61c..788a3f7086ab 100644 --- a/packages/node-integration-tests/suites/express/sentry-trace/trace-header-out/test.ts +++ b/packages/node-integration-tests/suites/express/sentry-trace/trace-header-out/test.ts @@ -1,12 +1,13 @@ import { TRACEPARENT_REGEXP } from '@sentry/utils'; +import * as path from 'path'; import { getAPIResponse, runServer } from '../../../../utils/index'; -import path = require('path'); +import { TestAPIResponse } from '../server'; test('should attach a `sentry-trace` header to an outgoing request.', async () => { const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`); - const response = await getAPIResponse(new URL(`${url}/express`)); + const response = (await getAPIResponse(new URL(`${url}/express`))) as TestAPIResponse; expect(response).toBeDefined(); expect(response).toMatchObject({ diff --git a/packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/test.ts b/packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/test.ts index dd9a8ca4fe87..be9c0b6c15b6 100644 --- a/packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/test.ts +++ b/packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/test.ts @@ -1,10 +1,13 @@ -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should add an empty breadcrumb, when an empty object is given', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); + const errorEnvelope = envelopes[1]; - assertSentryEvent(requestBody, { + expect(errorEnvelope).toHaveLength(3); + + assertSentryEvent(errorEnvelope[2], { message: 'test-empty-obj', }); }); diff --git a/packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts b/packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts index a94acf718b4b..b9ae05edefac 100644 --- a/packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts +++ b/packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts @@ -8,7 +8,7 @@ Sentry.init({ Sentry.addBreadcrumb({ category: 'foo', message: 'bar', - level: Sentry.Severity.Critical, + level: 'fatal', }); Sentry.addBreadcrumb({ diff --git a/packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts b/packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts index b643a187bc6f..38ef745cb28e 100644 --- a/packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts +++ b/packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts @@ -1,16 +1,16 @@ -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should add multiple breadcrumbs', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); - assertSentryEvent(requestBody, { + assertSentryEvent(envelopes[1][2], { message: 'test_multi_breadcrumbs', breadcrumbs: [ { category: 'foo', message: 'bar', - level: 'critical', + level: 'fatal', }, { category: 'qux', diff --git a/packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts b/packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts index 108b79c26963..94ebd23dfaa5 100644 --- a/packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts +++ b/packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts @@ -8,7 +8,7 @@ Sentry.init({ Sentry.addBreadcrumb({ category: 'foo', message: 'bar', - level: Sentry.Severity.Critical, + level: 'fatal', }); Sentry.captureMessage('test_simple'); diff --git a/packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts b/packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts index 8bc0ee26fea2..d5fc2ef8df73 100644 --- a/packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts +++ b/packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts @@ -1,16 +1,16 @@ -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should add a simple breadcrumb', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); - assertSentryEvent(requestBody, { + assertSentryEvent(envelopes[1][2], { message: 'test_simple', breadcrumbs: [ { category: 'foo', message: 'bar', - level: 'critical', + level: 'fatal', }, ], }); diff --git a/packages/node-integration-tests/suites/public-api/captureException/catched-error/test.ts b/packages/node-integration-tests/suites/public-api/captureException/catched-error/test.ts index f6fee1d8b819..2070199f77d9 100644 --- a/packages/node-integration-tests/suites/public-api/captureException/catched-error/test.ts +++ b/packages/node-integration-tests/suites/public-api/captureException/catched-error/test.ts @@ -1,10 +1,11 @@ -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should work inside catch block', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); + const errorEnvelope = envelopes[1]; - assertSentryEvent(requestBody, { + assertSentryEvent(errorEnvelope[2], { exception: { values: [ { diff --git a/packages/node-integration-tests/suites/public-api/captureException/empty-obj/test.ts b/packages/node-integration-tests/suites/public-api/captureException/empty-obj/test.ts index fb6ca293a27f..0df21996f08c 100644 --- a/packages/node-integration-tests/suites/public-api/captureException/empty-obj/test.ts +++ b/packages/node-integration-tests/suites/public-api/captureException/empty-obj/test.ts @@ -1,10 +1,11 @@ -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should capture an empty object', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); + const errorEnvelope = envelopes[1]; - assertSentryEvent(requestBody, { + assertSentryEvent(errorEnvelope[2], { exception: { values: [ { diff --git a/packages/node-integration-tests/suites/public-api/captureException/new-transport/scenario.ts b/packages/node-integration-tests/suites/public-api/captureException/new-transport/scenario.ts deleted file mode 100644 index a03dea7cbce5..000000000000 --- a/packages/node-integration-tests/suites/public-api/captureException/new-transport/scenario.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as Sentry from '@sentry/node'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - _experiments: { - newTransport: true, // use new transport - }, -}); - -Sentry.captureException(new Error('test_simple_error')); diff --git a/packages/node-integration-tests/suites/public-api/captureException/new-transport/test.ts b/packages/node-integration-tests/suites/public-api/captureException/new-transport/test.ts deleted file mode 100644 index 0424ac121dcd..000000000000 --- a/packages/node-integration-tests/suites/public-api/captureException/new-transport/test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { getMultipleEnvelopeRequest, runServer } from '../../../../utils'; - -test('should correctly send envelope', async () => { - const url = await runServer(__dirname); - const envelopes = await getMultipleEnvelopeRequest(url, 2); - - const errorEnvelope = envelopes[1]; - - expect(errorEnvelope).toHaveLength(3); - expect(errorEnvelope[2]).toMatchObject({ - exception: { - values: [ - { - type: 'Error', - value: 'test_simple_error', - }, - ], - }, - release: '1.0', - event_id: expect.any(String), - timestamp: expect.any(Number), - }); -}); diff --git a/packages/node-integration-tests/suites/public-api/captureException/simple-error/test.ts b/packages/node-integration-tests/suites/public-api/captureException/simple-error/test.ts index 8553cb1be0d7..66ca0410377a 100644 --- a/packages/node-integration-tests/suites/public-api/captureException/simple-error/test.ts +++ b/packages/node-integration-tests/suites/public-api/captureException/simple-error/test.ts @@ -1,10 +1,11 @@ -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should capture a simple error with message', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); + const errorEnvelope = envelopes[1]; - assertSentryEvent(requestBody, { + assertSentryEvent(errorEnvelope[2], { exception: { values: [ { diff --git a/packages/node-integration-tests/suites/public-api/captureMessage/simple_message/test.ts b/packages/node-integration-tests/suites/public-api/captureMessage/simple_message/test.ts index 69fa8c27764b..2ecc0e86720d 100644 --- a/packages/node-integration-tests/suites/public-api/captureMessage/simple_message/test.ts +++ b/packages/node-integration-tests/suites/public-api/captureMessage/simple_message/test.ts @@ -1,10 +1,11 @@ -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should capture a simple message string', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); + const errorEnvelope = envelopes[1]; - assertSentryEvent(requestBody, { + assertSentryEvent(errorEnvelope[2], { message: 'Message', level: 'info', }); diff --git a/packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts b/packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts index 61f8e2532ee6..be175d36e816 100644 --- a/packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts +++ b/packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts @@ -5,10 +5,9 @@ Sentry.init({ release: '1.0', }); -Sentry.captureMessage('debug_message', Sentry.Severity.Debug); -Sentry.captureMessage('info_message', Sentry.Severity.Info); -Sentry.captureMessage('warning_message', Sentry.Severity.Warning); -Sentry.captureMessage('error_message', Sentry.Severity.Error); -Sentry.captureMessage('fatal_message', Sentry.Severity.Fatal); -Sentry.captureMessage('critical_message', Sentry.Severity.Critical); -Sentry.captureMessage('log_message', Sentry.Severity.Log); +Sentry.captureMessage('debug_message', 'debug'); +Sentry.captureMessage('info_message', 'info'); +Sentry.captureMessage('warning_message', 'warning'); +Sentry.captureMessage('error_message', 'error'); +Sentry.captureMessage('fatal_message', 'fatal'); +Sentry.captureMessage('log_message', 'log'); diff --git a/packages/node-integration-tests/suites/public-api/captureMessage/with_level/test.ts b/packages/node-integration-tests/suites/public-api/captureMessage/with_level/test.ts index e43c13db3711..18bd395b148d 100644 --- a/packages/node-integration-tests/suites/public-api/captureMessage/with_level/test.ts +++ b/packages/node-integration-tests/suites/public-api/captureMessage/with_level/test.ts @@ -1,40 +1,35 @@ -import { assertSentryEvent, getMultipleEventRequests, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should capture with different severity levels', async () => { const url = await runServer(__dirname); - const events = await getMultipleEventRequests(url, 7); + const envelopes = await getMultipleEnvelopeRequest(url, 12); - assertSentryEvent(events[0], { + assertSentryEvent(envelopes[1][2], { message: 'debug_message', level: 'debug', }); - assertSentryEvent(events[1], { + assertSentryEvent(envelopes[3][2], { message: 'info_message', level: 'info', }); - assertSentryEvent(events[2], { + assertSentryEvent(envelopes[5][2], { message: 'warning_message', level: 'warning', }); - assertSentryEvent(events[3], { + assertSentryEvent(envelopes[7][2], { message: 'error_message', level: 'error', }); - assertSentryEvent(events[4], { + assertSentryEvent(envelopes[9][2], { message: 'fatal_message', level: 'fatal', }); - assertSentryEvent(events[5], { - message: 'critical_message', - level: 'critical', - }); - - assertSentryEvent(events[6], { + assertSentryEvent(envelopes[11][2], { message: 'log_message', level: 'log', }); diff --git a/packages/node-integration-tests/suites/public-api/configureScope/clear_scope/test.ts b/packages/node-integration-tests/suites/public-api/configureScope/clear_scope/test.ts index 97437a4e7e90..e1cf213e2e4c 100644 --- a/packages/node-integration-tests/suites/public-api/configureScope/clear_scope/test.ts +++ b/packages/node-integration-tests/suites/public-api/configureScope/clear_scope/test.ts @@ -1,16 +1,16 @@ import { Event } from '@sentry/node'; -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../../utils'; test('should clear previously set properties of a scope', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelope = await getEnvelopeRequest(url); - assertSentryEvent(requestBody, { + assertSentryEvent(envelope[2], { message: 'cleared_scope', tags: {}, extra: {}, }); - expect((requestBody as Event).user).not.toBeDefined(); + expect((envelope[2] as Event).user).not.toBeDefined(); }); diff --git a/packages/node-integration-tests/suites/public-api/configureScope/set_properties/test.ts b/packages/node-integration-tests/suites/public-api/configureScope/set_properties/test.ts index 6e482197470c..17d7c9b8df42 100644 --- a/packages/node-integration-tests/suites/public-api/configureScope/set_properties/test.ts +++ b/packages/node-integration-tests/suites/public-api/configureScope/set_properties/test.ts @@ -1,10 +1,11 @@ -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should set different properties of a scope', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); + const errorEnvelope = envelopes[1]; - assertSentryEvent(requestBody, { + assertSentryEvent(errorEnvelope[2], { message: 'configured_scope', tags: { foo: 'bar', diff --git a/packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts b/packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts index 46d3de6bc2bf..f1c8981fad9a 100644 --- a/packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts +++ b/packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts @@ -1,12 +1,13 @@ import { Event } from '@sentry/node'; -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should record multiple contexts', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); + const errorEnvelope = envelopes[1]; - assertSentryEvent(requestBody, { + assertSentryEvent(errorEnvelope[2], { message: 'multiple_contexts', contexts: { context_1: { @@ -17,5 +18,5 @@ test('should record multiple contexts', async () => { }, }); - expect((requestBody as Event).contexts?.context_3).not.toBeDefined(); + expect((errorEnvelope[2] as Event).contexts?.context_3).not.toBeDefined(); }); diff --git a/packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts b/packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts index 2ea859de480c..26b5fe8c7025 100644 --- a/packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts +++ b/packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts @@ -1,15 +1,16 @@ import { Event } from '@sentry/node'; -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should normalize non-serializable context', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); + const errorEnvelope = envelopes[1]; - assertSentryEvent(requestBody, { + assertSentryEvent(errorEnvelope[2], { message: 'non_serializable', contexts: {}, }); - expect((requestBody as Event).contexts?.context_3).not.toBeDefined(); + expect((errorEnvelope[2] as Event).contexts?.context_3).not.toBeDefined(); }); diff --git a/packages/node-integration-tests/suites/public-api/setContext/simple-context/test.ts b/packages/node-integration-tests/suites/public-api/setContext/simple-context/test.ts index cdabe34d8680..362afb9e55e4 100644 --- a/packages/node-integration-tests/suites/public-api/setContext/simple-context/test.ts +++ b/packages/node-integration-tests/suites/public-api/setContext/simple-context/test.ts @@ -1,12 +1,13 @@ import { Event } from '@sentry/node'; -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should set a simple context', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); + const errorEnvelope = envelopes[1]; - assertSentryEvent(requestBody, { + assertSentryEvent(errorEnvelope[2], { message: 'simple_context_object', contexts: { foo: { @@ -15,5 +16,5 @@ test('should set a simple context', async () => { }, }); - expect((requestBody as Event).contexts?.context_3).not.toBeDefined(); + expect((errorEnvelope[2] as Event).contexts?.context_3).not.toBeDefined(); }); diff --git a/packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts b/packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts index 0bc530b13cef..428ebd7f45c4 100644 --- a/packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts +++ b/packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts @@ -1,10 +1,11 @@ -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should record multiple extras of different types', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); + const errorEnvelope = envelopes[1]; - assertSentryEvent(requestBody, { + assertSentryEvent(errorEnvelope[2], { message: 'multiple_extras', extra: { extra_1: { foo: 'bar', baz: { qux: 'quux' } }, diff --git a/packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts b/packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts index 24005031781b..3cd2ca078eeb 100644 --- a/packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts +++ b/packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts @@ -1,10 +1,11 @@ -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should normalize non-serializable extra', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); + const errorEnvelope = envelopes[1]; - assertSentryEvent(requestBody, { + assertSentryEvent(errorEnvelope[2], { message: 'non_serializable', extra: {}, }); diff --git a/packages/node-integration-tests/suites/public-api/setExtra/simple-extra/test.ts b/packages/node-integration-tests/suites/public-api/setExtra/simple-extra/test.ts index 23c0cbc6ad42..33bfe641bfa3 100644 --- a/packages/node-integration-tests/suites/public-api/setExtra/simple-extra/test.ts +++ b/packages/node-integration-tests/suites/public-api/setExtra/simple-extra/test.ts @@ -1,10 +1,11 @@ -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should set a simple extra', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); + const errorEnvelope = envelopes[1]; - assertSentryEvent(requestBody, { + assertSentryEvent(errorEnvelope[2], { message: 'simple_extra', extra: { foo: { diff --git a/packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts b/packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts index 5126cde1a58f..464324c97fdf 100644 --- a/packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts +++ b/packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts @@ -1,10 +1,11 @@ -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should set extras from multiple consecutive calls', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); + const errorEnvelope = envelopes[1]; - assertSentryEvent(requestBody, { + assertSentryEvent(errorEnvelope[2], { message: 'consecutive_calls', extra: { extra: [], Infinity: 2, null: 0, obj: { foo: ['bar', 'baz', 1] } }, }); diff --git a/packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts b/packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts index f1c653307e45..d23c8e815a06 100644 --- a/packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts +++ b/packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts @@ -1,10 +1,11 @@ -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should record an extras object', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); + const errorEnvelope = envelopes[1]; - assertSentryEvent(requestBody, { + assertSentryEvent(errorEnvelope[2], { message: 'multiple_extras', extra: { extra_1: [1, ['foo'], 'bar'], diff --git a/packages/node-integration-tests/suites/public-api/setTag/with-primitives/test.ts b/packages/node-integration-tests/suites/public-api/setTag/with-primitives/test.ts index c6b94aca64d1..88d78b70c655 100644 --- a/packages/node-integration-tests/suites/public-api/setTag/with-primitives/test.ts +++ b/packages/node-integration-tests/suites/public-api/setTag/with-primitives/test.ts @@ -1,10 +1,11 @@ -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should set primitive tags', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); + const envelope = envelopes[1]; - assertSentryEvent(requestBody, { + assertSentryEvent(envelope[2], { message: 'primitive_tags', tags: { tag_1: 'foo', diff --git a/packages/node-integration-tests/suites/public-api/setTags/with-primitives/test.ts b/packages/node-integration-tests/suites/public-api/setTags/with-primitives/test.ts index c6b94aca64d1..88d78b70c655 100644 --- a/packages/node-integration-tests/suites/public-api/setTags/with-primitives/test.ts +++ b/packages/node-integration-tests/suites/public-api/setTags/with-primitives/test.ts @@ -1,10 +1,11 @@ -import { assertSentryEvent, getEventRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should set primitive tags', async () => { const url = await runServer(__dirname); - const requestBody = await getEventRequest(url); + const envelopes = await getMultipleEnvelopeRequest(url, 2); + const envelope = envelopes[1]; - assertSentryEvent(requestBody, { + assertSentryEvent(envelope[2], { message: 'primitive_tags', tags: { tag_1: 'foo', diff --git a/packages/node-integration-tests/suites/public-api/setUser/unset_user/test.ts b/packages/node-integration-tests/suites/public-api/setUser/unset_user/test.ts index 8c804f3864b8..736ed1fdf38c 100644 --- a/packages/node-integration-tests/suites/public-api/setUser/unset_user/test.ts +++ b/packages/node-integration-tests/suites/public-api/setUser/unset_user/test.ts @@ -1,18 +1,18 @@ import { Event } from '@sentry/node'; -import { assertSentryEvent, getMultipleEventRequests, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should unset user', async () => { const url = await runServer(__dirname); - const events = await getMultipleEventRequests(url, 3); + const envelopes = await getMultipleEnvelopeRequest(url, 6); - assertSentryEvent(events[0], { + assertSentryEvent(envelopes[1][2], { message: 'no_user', }); - expect((events[0] as Event).user).not.toBeDefined(); + expect((envelopes[0][2] as Event).user).not.toBeDefined(); - assertSentryEvent(events[1], { + assertSentryEvent(envelopes[3][2], { message: 'user', user: { id: 'foo', @@ -21,9 +21,9 @@ test('should unset user', async () => { }, }); - assertSentryEvent(events[2], { + assertSentryEvent(envelopes[5][2], { message: 'unset_user', }); - expect((events[2] as Event).user).not.toBeDefined(); + expect((envelopes[2][2] as Event).user).not.toBeDefined(); }); diff --git a/packages/node-integration-tests/suites/public-api/setUser/update_user/test.ts b/packages/node-integration-tests/suites/public-api/setUser/update_user/test.ts index 3f84a390cd05..27c7de89fe45 100644 --- a/packages/node-integration-tests/suites/public-api/setUser/update_user/test.ts +++ b/packages/node-integration-tests/suites/public-api/setUser/update_user/test.ts @@ -1,10 +1,10 @@ -import { assertSentryEvent, getMultipleEventRequests, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should update user', async () => { const url = await runServer(__dirname); - const events = await getMultipleEventRequests(url, 2); + const envelopes = await getMultipleEnvelopeRequest(url, 4); - assertSentryEvent(events[0], { + assertSentryEvent(envelopes[1][2], { message: 'first_user', user: { id: 'foo', @@ -12,7 +12,7 @@ test('should update user', async () => { }, }); - assertSentryEvent(events[1], { + assertSentryEvent(envelopes[3][2], { message: 'second_user', user: { id: 'baz', diff --git a/packages/node-integration-tests/suites/public-api/startTransaction/new-transport/scenario.ts b/packages/node-integration-tests/suites/public-api/startTransaction/new-transport/scenario.ts deleted file mode 100644 index 82ae5e905410..000000000000 --- a/packages/node-integration-tests/suites/public-api/startTransaction/new-transport/scenario.ts +++ /dev/null @@ -1,17 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import '@sentry/tracing'; - -import * as Sentry from '@sentry/node'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - _experiments: { - newTransport: true, // use new transport - }, -}); - -const transaction = Sentry.startTransaction({ name: 'test_transaction_1' }); - -transaction.finish(); diff --git a/packages/node-integration-tests/suites/public-api/startTransaction/new-transport/test.ts b/packages/node-integration-tests/suites/public-api/startTransaction/new-transport/test.ts deleted file mode 100644 index 62b8d8fb4402..000000000000 --- a/packages/node-integration-tests/suites/public-api/startTransaction/new-transport/test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { assertSentryTransaction, getEnvelopeRequest, runServer } from '../../../../utils'; - -test('should send a manually started transaction when @sentry/tracing is imported using unnamed import', async () => { - const url = await runServer(__dirname); - const envelope = await getEnvelopeRequest(url); - - expect(envelope).toHaveLength(3); - - assertSentryTransaction(envelope[2], { - transaction: 'test_transaction_1', - }); -}); diff --git a/packages/node-integration-tests/suites/public-api/withScope/nested-scopes/test.ts b/packages/node-integration-tests/suites/public-api/withScope/nested-scopes/test.ts index 03a219470b0f..57047696cf0c 100644 --- a/packages/node-integration-tests/suites/public-api/withScope/nested-scopes/test.ts +++ b/packages/node-integration-tests/suites/public-api/withScope/nested-scopes/test.ts @@ -1,12 +1,12 @@ import { Event } from '@sentry/node'; -import { assertSentryEvent, getMultipleEventRequests, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should allow nested scoping', async () => { const url = await runServer(__dirname); - const events = await getMultipleEventRequests(url, 5); + const envelopes = await getMultipleEnvelopeRequest(url, 10); - assertSentryEvent(events[0], { + assertSentryEvent(envelopes[1][2], { message: 'root_before', user: { id: 'qux', @@ -14,7 +14,7 @@ test('should allow nested scoping', async () => { tags: {}, }); - assertSentryEvent(events[1], { + assertSentryEvent(envelopes[3][2], { message: 'outer_before', user: { id: 'qux', @@ -24,7 +24,7 @@ test('should allow nested scoping', async () => { }, }); - assertSentryEvent(events[2], { + assertSentryEvent(envelopes[5][2], { message: 'inner', tags: { foo: false, @@ -32,9 +32,9 @@ test('should allow nested scoping', async () => { }, }); - expect((events[2] as Event).user).toBeUndefined(); + expect((envelopes[4][2] as Event).user).toBeUndefined(); - assertSentryEvent(events[3], { + assertSentryEvent(envelopes[7][2], { message: 'outer_after', user: { id: 'baz', @@ -44,7 +44,7 @@ test('should allow nested scoping', async () => { }, }); - assertSentryEvent(events[4], { + assertSentryEvent(envelopes[9][2], { message: 'root_after', user: { id: 'qux', diff --git a/packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts b/packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts index 59bb88d4aae7..13051fd9c47a 100644 --- a/packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts +++ b/packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts @@ -1,17 +1,17 @@ import path from 'path'; -import { getEnvelopeRequest, runServer } from '../../../utils'; +import { getMultipleEnvelopeRequest, runServer } from '../../../utils'; test('should aggregate successful and crashed sessions', async () => { const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`); - const envelope = await Promise.race([ - getEnvelopeRequest(`${url}/success`), - getEnvelopeRequest(`${url}/error_unhandled`), - getEnvelopeRequest(`${url}/success_slow`), + const envelopes = await Promise.race([ + getMultipleEnvelopeRequest(`${url}/success`, 2), + getMultipleEnvelopeRequest(`${url}/error_unhandled`, 2), + getMultipleEnvelopeRequest(`${url}/success_next`, 2), ]); + const envelope = envelopes[1]; - expect(envelope).toHaveLength(3); expect(envelope[0]).toMatchObject({ sent_at: expect.any(String), sdk: { diff --git a/packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts b/packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts index e26cafa8a586..0f3130cd9b09 100644 --- a/packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts +++ b/packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts @@ -1,18 +1,19 @@ import path from 'path'; -import { getEnvelopeRequest, runServer } from '../../../utils'; +import { getMultipleEnvelopeRequest, runServer } from '../../../utils'; test('should aggregate successful, crashed and erroneous sessions', async () => { const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`); - const envelope = await Promise.race([ - getEnvelopeRequest(`${url}/success_slow`), - getEnvelopeRequest(`${url}/error_handled`), - getEnvelopeRequest(`${url}/error_unhandled`), + const envelopes = await Promise.race([ + getMultipleEnvelopeRequest(`${url}/success`, 3), + getMultipleEnvelopeRequest(`${url}/error_handled`, 3), + getMultipleEnvelopeRequest(`${url}/error_unhandled`, 3), ]); - expect(envelope).toHaveLength(3); - expect(envelope[0]).toMatchObject({ + expect(envelopes).toHaveLength(3); + const aggregateSessionEnvelope = envelopes[2]; + expect(aggregateSessionEnvelope[0]).toMatchObject({ sent_at: expect.any(String), sdk: { name: 'sentry.javascript.node', @@ -20,11 +21,11 @@ test('should aggregate successful, crashed and erroneous sessions', async () => }, }); - expect(envelope[1]).toMatchObject({ + expect(aggregateSessionEnvelope[1]).toMatchObject({ type: 'sessions', }); - expect(envelope[2]).toMatchObject({ + expect(aggregateSessionEnvelope[2]).toMatchObject({ aggregates: [ { started: expect.any(String), diff --git a/packages/node-integration-tests/suites/sessions/server.ts b/packages/node-integration-tests/suites/sessions/server.ts index 89b9a0e521ea..a718c07d16c1 100644 --- a/packages/node-integration-tests/suites/sessions/server.ts +++ b/packages/node-integration-tests/suites/sessions/server.ts @@ -24,8 +24,7 @@ clearInterval(flusherIntervalId); // @ts-ignore: need access to `_intervalId` flusherIntervalId = flusher?._intervalId = setInterval(() => flusher?.flush(), 1000); -// @ts-ignore: need access to `_intervalId` again -setTimeout(() => clearInterval(flusherIntervalId), 3000); +setTimeout(() => clearInterval(flusherIntervalId), 2000); app.get('/test/success', (_req, res) => { res.send('Success!'); diff --git a/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts b/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts index d9c8afd2e9df..870553f8b0b4 100644 --- a/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts +++ b/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts @@ -3,6 +3,9 @@ import '@sentry/tracing'; import * as Sentry from '@sentry/node'; import { MongoClient } from 'mongodb'; +// suppress logging of the mongo download +global.console.log = () => null; + Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', diff --git a/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/test.ts b/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/test.ts index d118a03261a5..76cadd1518dd 100644 --- a/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/test.ts +++ b/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/test.ts @@ -1,14 +1,17 @@ -import { MongoMemoryServer } from 'mongodb-memory-server'; +import { MongoMemoryServer } from 'mongodb-memory-server-global'; import { assertSentryTransaction, conditionalTest, getEnvelopeRequest, runServer } from '../../../../utils'; +// This test can take longer. +jest.setTimeout(15000); + conditionalTest({ min: 12 })('MongoDB Test', () => { let mongoServer: MongoMemoryServer; beforeAll(async () => { mongoServer = await MongoMemoryServer.create(); process.env.MONGO_URL = mongoServer.getUri(); - }, 30000); + }, 10000); afterAll(async () => { await mongoServer.stop(); diff --git a/packages/node-integration-tests/suites/tracing/prisma-orm/docker-compose.yml b/packages/node-integration-tests/suites/tracing/prisma-orm/docker-compose.yml new file mode 100644 index 000000000000..45caa4bb3179 --- /dev/null +++ b/packages/node-integration-tests/suites/tracing/prisma-orm/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.9' + +services: + db: + image: postgres:13 + restart: always + container_name: integration-tests-prisma + ports: + - '5433:5432' + environment: + POSTGRES_USER: prisma + POSTGRES_PASSWORD: prisma + POSTGRES_DB: tests diff --git a/packages/node-integration-tests/suites/tracing/prisma-orm/package.json b/packages/node-integration-tests/suites/tracing/prisma-orm/package.json new file mode 100644 index 000000000000..f8b24d7d0465 --- /dev/null +++ b/packages/node-integration-tests/suites/tracing/prisma-orm/package.json @@ -0,0 +1,22 @@ +{ + "name": "sentry-prisma-test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "engines": { + "node": ">=12" + }, + "scripts": { + "db-up": "docker-compose up -d", + "generate": "prisma generate", + "migrate": "prisma migrate dev -n sentry-test", + "setup": "run-s --silent db-up generate migrate" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@prisma/client": "3.12.0", + "prisma": "^3.12.0" + } +} diff --git a/packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/migration_lock.toml b/packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000000..fbffa92c2bb7 --- /dev/null +++ b/packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/sentry_test/migration.sql b/packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/sentry_test/migration.sql new file mode 100644 index 000000000000..8619aaceb2b0 --- /dev/null +++ b/packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/sentry_test/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "email" TEXT NOT NULL, + "name" TEXT, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/packages/node-integration-tests/suites/tracing/prisma-orm/prisma/schema.prisma b/packages/node-integration-tests/suites/tracing/prisma-orm/prisma/schema.prisma new file mode 100644 index 000000000000..4363c97738ee --- /dev/null +++ b/packages/node-integration-tests/suites/tracing/prisma-orm/prisma/schema.prisma @@ -0,0 +1,15 @@ +datasource db { + url = "postgresql://prisma:prisma@localhost:5433/tests" + provider = "postgresql" +} + +generator client { + provider = "prisma-client-js" +} + +model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + email String @unique + name String? +} diff --git a/packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts b/packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts new file mode 100644 index 000000000000..047166a9e136 --- /dev/null +++ b/packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts @@ -0,0 +1,48 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { PrismaClient } from '@prisma/client'; +import * as Sentry from '@sentry/node'; +import * as Tracing from '@sentry/tracing'; +import { randomBytes } from 'crypto'; + +const client = new PrismaClient(); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + integrations: [new Tracing.Integrations.Prisma({ client })], +}); + +async function run(): Promise { + const transaction = Sentry.startTransaction({ + name: 'Test Transaction', + op: 'transaction', + }); + + Sentry.configureScope(scope => { + scope.setSpan(transaction); + }); + + try { + await client.user.create({ + data: { + name: 'Tilda', + email: `tilda_${randomBytes(4).toString('hex')}@sentry.io`, + }, + }); + + await client.user.findMany(); + + await client.user.deleteMany({ + where: { + email: { + contains: 'sentry.io', + }, + }, + }); + } finally { + if (transaction) transaction.finish(); + } +} + +void run(); diff --git a/packages/node-integration-tests/suites/tracing/prisma-orm/setup.ts b/packages/node-integration-tests/suites/tracing/prisma-orm/setup.ts new file mode 100755 index 000000000000..3c40d12f7337 --- /dev/null +++ b/packages/node-integration-tests/suites/tracing/prisma-orm/setup.ts @@ -0,0 +1,16 @@ +import { parseSemver } from '@sentry/utils'; +import { execSync } from 'child_process'; + +const NODE_VERSION = parseSemver(process.versions.node); + +if (NODE_VERSION.major && NODE_VERSION.major < 12) { + // eslint-disable-next-line no-console + console.warn(`Skipping Prisma tests on Node: ${NODE_VERSION.major}`); + process.exit(0); +} + +try { + execSync('yarn && yarn setup'); +} catch (_) { + process.exit(1); +} diff --git a/packages/node-integration-tests/suites/tracing/prisma-orm/test.ts b/packages/node-integration-tests/suites/tracing/prisma-orm/test.ts new file mode 100644 index 000000000000..91e6f39da889 --- /dev/null +++ b/packages/node-integration-tests/suites/tracing/prisma-orm/test.ts @@ -0,0 +1,17 @@ +import { assertSentryTransaction, conditionalTest, getEnvelopeRequest, runServer } from '../../../utils'; + +conditionalTest({ min: 12 })('Prisma ORM Integration', () => { + test('should instrument Prisma client for tracing.', async () => { + const url = await runServer(__dirname); + const envelope = await getEnvelopeRequest(url); + + assertSentryTransaction(envelope[2], { + transaction: 'Test Transaction', + spans: [ + { description: 'User create', op: 'db.prisma' }, + { description: 'User findMany', op: 'db.prisma' }, + { description: 'User deleteMany', op: 'db.prisma' }, + ], + }); + }); +}); diff --git a/packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock b/packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock new file mode 100644 index 000000000000..d228adebd621 --- /dev/null +++ b/packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock @@ -0,0 +1,27 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@prisma/client@3.12.0": + version "3.12.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.12.0.tgz#a0eb49ffea5c128dd11dffb896d7139a60073d12" + integrity sha512-4NEQjUcWja/NVBvfuDFscWSk1/rXg3+wj+TSkqXCb1tKlx/bsUE00rxsvOvGg7VZ6lw1JFpGkwjwmsOIc4zvQw== + dependencies: + "@prisma/engines-version" "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" + +"@prisma/engines-version@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980": + version "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz#829ca3d9d0d92555f44644606d4edfd45b2f5886" + integrity sha512-o+jo8d7ZEiVpcpNWUDh3fj2uPQpBxl79XE9ih9nkogJbhw6P33274SHnqheedZ7PyvPIK/mvU8MLNYgetgXPYw== + +"@prisma/engines@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980": + version "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz#e52e364084c4d05278f62768047b788665e64a45" + integrity sha512-zULjkN8yhzS7B3yeEz4aIym4E2w1ChrV12i14pht3ePFufvsAvBSoZ+tuXMvfSoNTgBS5E4bolRzLbMmbwkkMQ== + +prisma@^3.12.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.12.0.tgz#9675e0e72407122759d3eadcb6d27cdccd3497bd" + integrity sha512-ltCMZAx1i0i9xuPM692Srj8McC665h6E5RqJom999sjtVSccHSD8Z+HSdBN2183h9PJKvC5dapkn78dd0NWMBg== + dependencies: + "@prisma/engines" "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" diff --git a/packages/node-integration-tests/tsconfig.json b/packages/node-integration-tests/tsconfig.json index 87d045dbc42d..782d8f9c517f 100644 --- a/packages/node-integration-tests/tsconfig.json +++ b/packages/node-integration-tests/tsconfig.json @@ -1,9 +1,11 @@ { "extends": "../../tsconfig.json", + + "include": ["utils/**/*.ts"], + "compilerOptions": { + // package-specific options "esModuleInterop": true, - "types": ["jest", "node"] - }, - "include": ["**/*.ts", "jest.config.js"], - "exclude": ["node_modules"] + "types": ["node"] + } } diff --git a/packages/minimal/tsconfig.test.json b/packages/node-integration-tests/tsconfig.test.json similarity index 88% rename from packages/minimal/tsconfig.test.json rename to packages/node-integration-tests/tsconfig.test.json index 87f6afa06b86..5a37b90c4fe2 100644 --- a/packages/minimal/tsconfig.test.json +++ b/packages/node-integration-tests/tsconfig.test.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", - "include": ["test/**/*"], + "include": ["suites/**/*.ts"], "compilerOptions": { // should include all types from `./tsconfig.json` plus types for all test frameworks used diff --git a/packages/node-integration-tests/utils/index.ts b/packages/node-integration-tests/utils/index.ts index d652f63185b0..872c9d72521d 100644 --- a/packages/node-integration-tests/utils/index.ts +++ b/packages/node-integration-tests/utils/index.ts @@ -65,42 +65,6 @@ export const parseEnvelope = (body: string): Array> => { return body.split('\n').map(e => JSON.parse(e)); }; -/** - * Intercepts and extracts multiple requests containing a Sentry Event - * - * @param {string} url - * @param {number} count - * @return {*} {Promise>>} - */ -export const getMultipleEventRequests = async (url: string, count: number): Promise>> => { - const events: Record[] = []; - - return new Promise(resolve => { - nock('https://dsn.ingest.sentry.io') - .post('/api/1337/store/', body => { - events.push(body); - - if (events.length === count) { - resolve(events); - } - return true; - }) - .times(7) - .reply(200); - http.get(url); - }); -}; - -/** - * Intercepts and extracts a single request containing a Sentry Event - * - * @param {string} url - * @return {*} {Promise>} - */ -export const getEventRequest = async (url: string): Promise> => { - return (await getMultipleEventRequests(url, 1))[0]; -}; - /** * Intercepts and extracts up to a number of requests containing Sentry envelopes. * @@ -139,8 +103,8 @@ export const getMultipleEnvelopeRequest = async (url: string, count: number): Pr * @param {Record} [headers] * @return {*} {Promise} */ -export const getAPIResponse = async (url: URL, headers?: Record): Promise => { - return await new Promise(resolve => { +export const getAPIResponse = async (url: URL, headers?: Record): Promise => { + return new Promise(resolve => { http.get( headers ? ({ diff --git a/packages/node-integration-tests/utils/setup-tests.ts b/packages/node-integration-tests/utils/setup-tests.ts new file mode 100644 index 000000000000..6f7bb2bec369 --- /dev/null +++ b/packages/node-integration-tests/utils/setup-tests.ts @@ -0,0 +1,12 @@ +import EventEmitter from 'events'; + +const setup = async (): Promise => { + // Node warns about a potential memory leak + // when more than 10 event listeners are assigned inside a single thread. + // Initializing Sentry for each test triggers these warnings after 10th test inside Jest thread. + // As we know that it's not a memory leak and number of listeners are limited to the number of tests, + // removing the limit on listener count here. + EventEmitter.defaultMaxListeners = 0; +}; + +export default setup; diff --git a/packages/node/.npmignore b/packages/node/.npmignore deleted file mode 100644 index 4ab7cc4f278f..000000000000 --- a/packages/node/.npmignore +++ /dev/null @@ -1,11 +0,0 @@ -# The paths in this file are specified so that they align with the file structure in `./build` after this file is copied -# into it by the prepack script `scripts/prepack.ts`. - -* - -# TODO remove bundles (which in the tarball are inside `build`) in v7 -!/build/**/* - -!/dist/**/* -!/esm/**/* -!/types/**/* diff --git a/packages/node/README.md b/packages/node/README.md index f59c831b0055..fee8104478ac 100644 --- a/packages/node/README.md +++ b/packages/node/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Official Sentry SDK for NodeJS diff --git a/packages/node/jest.config.js b/packages/node/jest.config.js new file mode 100644 index 000000000000..24f49ab59a4c --- /dev/null +++ b/packages/node/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest/jest.config.js'); diff --git a/packages/node/package.json b/packages/node/package.json index f7c3e63b5f48..77b0826e374e 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,25 +1,25 @@ { "name": "@sentry/node", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Official Sentry SDK for Node.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node", "author": "Sentry", "license": "BSD-3-Clause", "engines": { - "node": ">=6" + "node": ">=8" }, - "main": "build/dist/index.js", + "main": "build/cjs/index.js", "module": "build/esm/index.js", "types": "build/types/index.d.ts", "publishConfig": { "access": "public" }, "dependencies": { - "@sentry/core": "6.19.7", - "@sentry/hub": "6.19.7", - "@sentry/types": "6.19.7", - "@sentry/utils": "6.19.7", + "@sentry/core": "7.0.0-rc.0", + "@sentry/hub": "7.0.0-rc.0", + "@sentry/types": "7.0.0-rc.0", + "@sentry/utils": "7.0.0-rc.0", "cookie": "^0.4.1", "https-proxy-agent": "^5.0.0", "lru_map": "^0.3.3", @@ -34,25 +34,20 @@ "nock": "^13.0.5" }, "scripts": { - "build": "run-p build:cjs build:esm build:types", - "build:cjs": "tsc -p tsconfig.cjs.json", + "build": "run-p build:rollup build:types", "build:dev": "run-s build", - "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "tsc -p tsconfig.esm.json", + "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", - "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", + "build:watch": "run-p build:rollup:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:es5:watch": "yarn build:cjs:watch # *** backwards compatibility - remove in v7 ***", - "build:esm:watch": "tsc -p tsconfig.esm.json --watch", + "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf dist esm build coverage", + "clean": "rimraf build coverage sentry-node-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/*.ts\"", - "link:yarn": "yarn link", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", @@ -60,30 +55,10 @@ "test:express": "node test/manual/express-scope-separation/start.js", "test:jest": "jest", "test:release-health": "node test/manual/release-health/runner.js", - "test:webpack": "cd test/manual/webpack-domain/ && yarn && node npm-build.js", + "test:webpack": "cd test/manual/webpack-domain/ && yarn --silent && node npm-build.js", "test:watch": "jest --watch" }, "volta": { "extends": "../../package.json" - }, - "jest": { - "collectCoverage": true, - "transform": { - "^.+\\.ts$": "ts-jest" - }, - "moduleFileExtensions": [ - "js", - "ts" - ], - "testEnvironment": "node", - "testMatch": [ - "**/*.test.ts" - ], - "globals": { - "ts-jest": { - "tsConfig": "./tsconfig.json", - "diagnostics": false - } - } } } diff --git a/packages/node/rollup.npm.config.js b/packages/node/rollup.npm.config.js new file mode 100644 index 000000000000..5a62b528ef44 --- /dev/null +++ b/packages/node/rollup.npm.config.js @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/node/src/backend.ts b/packages/node/src/backend.ts deleted file mode 100644 index f319673ebc18..000000000000 --- a/packages/node/src/backend.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { BaseBackend, getEnvelopeEndpointWithUrlEncodedAuth, initAPIDetails } from '@sentry/core'; -import { Event, EventHint, Severity, Transport, TransportOptions } from '@sentry/types'; -import { makeDsn, resolvedSyncPromise } from '@sentry/utils'; - -import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; -import { HTTPSTransport, HTTPTransport, makeNodeTransport } from './transports'; -import { NodeOptions } from './types'; - -/** - * The Sentry Node SDK Backend. - * @hidden - */ -export class NodeBackend extends BaseBackend { - /** - * @inheritDoc - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types - public eventFromException(exception: any, hint?: EventHint): PromiseLike { - return resolvedSyncPromise(eventFromUnknownInput(exception, hint)); - } - - /** - * @inheritDoc - */ - public eventFromMessage(message: string, level: Severity = Severity.Info, hint?: EventHint): PromiseLike { - return resolvedSyncPromise(eventFromMessage(message, level, hint, this._options.attachStacktrace)); - } - - /** - * @inheritDoc - */ - protected _setupTransport(): Transport { - if (!this._options.dsn) { - // We return the noop transport here in case there is no Dsn. - return super._setupTransport(); - } - - const dsn = makeDsn(this._options.dsn); - - const transportOptions: TransportOptions = { - ...this._options.transportOptions, - ...(this._options.httpProxy && { httpProxy: this._options.httpProxy }), - ...(this._options.httpsProxy && { httpsProxy: this._options.httpsProxy }), - ...(this._options.caCerts && { caCerts: this._options.caCerts }), - dsn: this._options.dsn, - tunnel: this._options.tunnel, - _metadata: this._options._metadata, - }; - - if (this._options.transport) { - return new this._options.transport(transportOptions); - } - - const api = initAPIDetails(transportOptions.dsn, transportOptions._metadata, transportOptions.tunnel); - const url = getEnvelopeEndpointWithUrlEncodedAuth(api.dsn, api.tunnel); - - this._newTransport = makeNodeTransport({ - url, - headers: transportOptions.headers, - proxy: transportOptions.httpProxy, - caCerts: transportOptions.caCerts, - }); - - if (dsn.protocol === 'http') { - return new HTTPTransport(transportOptions); - } - return new HTTPSTransport(transportOptions); - } -} diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts index dde7f8941b74..9ebad827d3e4 100644 --- a/packages/node/src/client.ts +++ b/packages/node/src/client.ts @@ -1,26 +1,27 @@ import { BaseClient, Scope, SDK_VERSION } from '@sentry/core'; import { SessionFlusher } from '@sentry/hub'; -import { Event, EventHint } from '@sentry/types'; -import { logger } from '@sentry/utils'; +import { Event, EventHint, Severity, SeverityLevel } from '@sentry/types'; +import { logger, resolvedSyncPromise } from '@sentry/utils'; +import { TextEncoder } from 'util'; -import { NodeBackend } from './backend'; +import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; import { IS_DEBUG_BUILD } from './flags'; -import { NodeOptions } from './types'; +import { NodeClientOptions } from './types'; /** * The Sentry Node SDK Client. * - * @see NodeOptions for documentation on configuration options. + * @see NodeClientOptions for documentation on configuration options. * @see SentryClient for usage documentation. */ -export class NodeClient extends BaseClient { +export class NodeClient extends BaseClient { protected _sessionFlusher: SessionFlusher | undefined; /** * Creates a new Node SDK instance. * @param options Configuration options for this SDK. */ - public constructor(options: NodeOptions) { + public constructor(options: NodeClientOptions) { options._metadata = options._metadata || {}; options._metadata.sdk = options._metadata.sdk || { name: 'sentry.javascript.node', @@ -33,7 +34,13 @@ export class NodeClient extends BaseClient { version: SDK_VERSION, }; - super(NodeBackend, options); + // Until node supports global TextEncoder in all versions we support, we are forced to pass it from util + options.transportOptions = { + textEncoder: new TextEncoder(), + ...options.transportOptions, + }; + + super(options); } /** @@ -99,7 +106,7 @@ export class NodeClient extends BaseClient { if (!release) { IS_DEBUG_BUILD && logger.warn('Cannot initialise an instance of SessionFlusher if no release is provided!'); } else { - this._sessionFlusher = new SessionFlusher(this.getTransport(), { + this._sessionFlusher = new SessionFlusher(this, { release, environment, }); @@ -109,12 +116,34 @@ export class NodeClient extends BaseClient { /** * @inheritDoc */ - protected _prepareEvent(event: Event, scope?: Scope, hint?: EventHint): PromiseLike { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + public eventFromException(exception: any, hint?: EventHint): PromiseLike { + return resolvedSyncPromise(eventFromUnknownInput(this._options.stackParser, exception, hint)); + } + + /** + * @inheritDoc + */ + public eventFromMessage( + message: string, + // eslint-disable-next-line deprecation/deprecation + level: Severity | SeverityLevel = 'info', + hint?: EventHint, + ): PromiseLike { + return resolvedSyncPromise( + eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace), + ); + } + + /** + * @inheritDoc + */ + protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { event.platform = event.platform || 'node'; if (this.getOptions().serverName) { event.server_name = this.getOptions().serverName; } - return super._prepareEvent(event, scope, hint); + return super._prepareEvent(event, hint, scope); } /** diff --git a/packages/node/src/eventbuilder.ts b/packages/node/src/eventbuilder.ts index 0376f21a59c8..eead089793a0 100644 --- a/packages/node/src/eventbuilder.ts +++ b/packages/node/src/eventbuilder.ts @@ -1,34 +1,40 @@ import { getCurrentHub } from '@sentry/hub'; -import { Event, EventHint, Exception, Mechanism, Severity, StackFrame } from '@sentry/types'; +import { + Event, + EventHint, + Exception, + Mechanism, + Severity, + SeverityLevel, + StackFrame, + StackParser, +} from '@sentry/types'; import { addExceptionMechanism, addExceptionTypeValue, - createStackParser, extractExceptionKeysForMessage, isError, isPlainObject, normalizeToSize, } from '@sentry/utils'; -import { nodeStackParser } from './stack-parser'; - /** * Extracts stack frames from the error.stack string */ -export function parseStackFrames(error: Error): StackFrame[] { - return createStackParser(nodeStackParser)(error.stack || '', 1); +export function parseStackFrames(stackParser: StackParser, error: Error): StackFrame[] { + return stackParser(error.stack || '', 1); } /** * Extracts stack frames from the error and builds a Sentry Exception */ -export function exceptionFromError(error: Error): Exception { +export function exceptionFromError(stackParser: StackParser, error: Error): Exception { const exception: Exception = { type: error.name || error.constructor.name, value: error.message, }; - const frames = parseStackFrames(error); + const frames = parseStackFrames(stackParser, error); if (frames.length) { exception.stacktrace = { frames }; } @@ -40,7 +46,7 @@ export function exceptionFromError(error: Error): Exception { * Builds and Event from a Exception * @hidden */ -export function eventFromUnknownInput(exception: unknown, hint?: EventHint): Event { +export function eventFromUnknownInput(stackParser: StackParser, exception: unknown, hint?: EventHint): Event { // eslint-disable-next-line @typescript-eslint/no-explicit-any let ex: unknown = exception; const providedMechanism: Mechanism | undefined = @@ -73,7 +79,7 @@ export function eventFromUnknownInput(exception: unknown, hint?: EventHint): Eve const event = { exception: { - values: [exceptionFromError(ex as Error)], + values: [exceptionFromError(stackParser, ex as Error)], }, }; @@ -91,8 +97,10 @@ export function eventFromUnknownInput(exception: unknown, hint?: EventHint): Eve * @hidden */ export function eventFromMessage( + stackParser: StackParser, message: string, - level: Severity = Severity.Info, + // eslint-disable-next-line deprecation/deprecation + level: Severity | SeverityLevel = 'info', hint?: EventHint, attachStacktrace?: boolean, ): Event { @@ -103,9 +111,16 @@ export function eventFromMessage( }; if (attachStacktrace && hint && hint.syntheticException) { - const frames = parseStackFrames(hint.syntheticException); + const frames = parseStackFrames(stackParser, hint.syntheticException); if (frames.length) { - event.stacktrace = { frames }; + event.exception = { + values: [ + { + value: message, + stacktrace: { frames }, + }, + ], + }; } } diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 451ef14e9223..27dbeb3fc9cf 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -8,6 +8,7 @@ import { isString, logger, normalize, + parseBaggageString, stripUrlQueryAndFragment, } from '@sentry/utils'; import * as cookie from 'cookie'; @@ -61,16 +62,16 @@ export function tracingHandler(): ( next: (error?: any) => void, ): void { // If there is a trace header set, we extract the data from it (parentSpanId, traceId, and sampling decision) - let traceparentData; - if (req.headers && isString(req.headers['sentry-trace'])) { - traceparentData = extractTraceparentData(req.headers['sentry-trace']); - } + const traceparentData = + req.headers && isString(req.headers['sentry-trace']) && extractTraceparentData(req.headers['sentry-trace']); + const baggage = req.headers && isString(req.headers.baggage) && parseBaggageString(req.headers.baggage); const transaction = startTransaction( { name: extractExpressTransactionName(req, { path: true, method: true }), op: 'http.server', ...traceparentData, + ...(baggage && { metadata: { baggage: baggage } }), }, // extra context passed to the tracesSampler { request: extractRequestData(req) }, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 9f65c3f1a1a8..96e2383c6ec0 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -1,21 +1,22 @@ -export { +export type { Breadcrumb, BreadcrumbHint, Request, SdkInfo, Event, EventHint, - EventStatus, Exception, - Response, + Session, + // eslint-disable-next-line deprecation/deprecation Severity, + SeverityLevel, StackFrame, Stacktrace, Thread, User, } from '@sentry/types'; -export { SeverityLevel } from '@sentry/utils'; +export type { NodeOptions } from './types'; export { addGlobalEventProcessor, @@ -24,12 +25,12 @@ export { captureEvent, captureMessage, configureScope, + createTransport, getHubFromCarrier, getCurrentHub, Hub, makeMain, Scope, - Session, startTransaction, SDK_VERSION, setContext, @@ -41,12 +42,10 @@ export { withScope, } from '@sentry/core'; -export { NodeOptions } from './types'; -export { NodeBackend } from './backend'; export { NodeClient } from './client'; -export { defaultIntegrations, init, lastEventId, flush, close, getSentryRelease } from './sdk'; +export { makeNodeTransport } from './transports'; +export { defaultIntegrations, init, defaultStackParser, lastEventId, flush, close, getSentryRelease } from './sdk'; export { deepReadDirSync } from './utils'; -export { SDK_NAME } from './version'; import { Integrations as CoreIntegrations } from '@sentry/core'; import { getMainCarrier } from '@sentry/hub'; @@ -54,14 +53,13 @@ import * as domain from 'domain'; import * as Handlers from './handlers'; import * as NodeIntegrations from './integrations'; -import * as Transports from './transports'; const INTEGRATIONS = { ...CoreIntegrations, ...NodeIntegrations, }; -export { INTEGRATIONS as Integrations, Transports, Handlers }; +export { INTEGRATIONS as Integrations, Handlers }; // We need to patch domain on the global __SENTRY__ object to make it work for node in cross-platform packages like // @sentry/hub. If we don't do this, browser bundlers will have troubles resolving `require('domain')`. diff --git a/packages/node/src/integrations/console.ts b/packages/node/src/integrations/console.ts index a9542837fbcc..9b032c75cced 100644 --- a/packages/node/src/integrations/console.ts +++ b/packages/node/src/integrations/console.ts @@ -1,6 +1,6 @@ import { getCurrentHub } from '@sentry/core'; import { Integration } from '@sentry/types'; -import { fill, severityFromString } from '@sentry/utils'; +import { fill, severityLevelFromString } from '@sentry/utils'; import * as util from 'util'; /** Console module integration */ @@ -30,7 +30,7 @@ export class Console implements Integration { */ function createConsoleWrapper(level: string): (originalConsoleMethod: () => void) => void { return function consoleWrapper(originalConsoleMethod: () => void): () => void { - const sentryLevel = severityFromString(level); + const sentryLevel = severityLevelFromString(level); /* eslint-disable prefer-rest-params */ return function (this: typeof console): void { diff --git a/packages/node/src/integrations/contextlines.ts b/packages/node/src/integrations/contextlines.ts index e18768b2fe3d..cad196342f28 100644 --- a/packages/node/src/integrations/contextlines.ts +++ b/packages/node/src/integrations/contextlines.ts @@ -1,11 +1,8 @@ -import { getCurrentHub } from '@sentry/core'; import { Event, EventProcessor, Integration, StackFrame } from '@sentry/types'; import { addContextToFrame } from '@sentry/utils'; import { readFile } from 'fs'; import { LRUMap } from 'lru_map'; -import { NodeClient } from '../client'; - const FILE_CONTENT_CACHE = new LRUMap(100); const DEFAULT_LINES_OF_CONTEXT = 7; @@ -53,16 +50,6 @@ export class ContextLines implements Integration { /** Get's the number of context lines to add */ private get _contextLines(): number { - // This is only here to copy frameContextLines from init options if it hasn't - // been set via this integrations constructor. - // - // TODO: Remove on next major! - if (this._options.frameContextLines === undefined) { - const initOptions = getCurrentHub().getClient()?.getOptions(); - // eslint-disable-next-line deprecation/deprecation - this._options.frameContextLines = initOptions?.frameContextLines; - } - return this._options.frameContextLines !== undefined ? this._options.frameContextLines : DEFAULT_LINES_OF_CONTEXT; } diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 19a4e6ba1423..074c81ffe842 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -1,6 +1,6 @@ import { getCurrentHub } from '@sentry/core'; import { Integration, Span } from '@sentry/types'; -import { fill, logger, parseSemver } from '@sentry/utils'; +import { fill, logger, mergeAndSerializeBaggage, parseSemver } from '@sentry/utils'; import * as http from 'http'; import * as https from 'https'; @@ -123,7 +123,14 @@ function _createWrappedRequestMethodFactory( logger.log( `[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to ${requestUrl}: `, ); - requestOptions.headers = { ...requestOptions.headers, 'sentry-trace': sentryTraceHeader }; + + const headerBaggageString = requestOptions.headers && (requestOptions.headers.baggage as string); + + requestOptions.headers = { + ...requestOptions.headers, + 'sentry-trace': sentryTraceHeader, + baggage: mergeAndSerializeBaggage(span.getBaggage(), headerBaggageString), + }; } } diff --git a/packages/node/src/integrations/linkederrors.ts b/packages/node/src/integrations/linkederrors.ts index 701400594971..2817517513db 100644 --- a/packages/node/src/integrations/linkederrors.ts +++ b/packages/node/src/integrations/linkederrors.ts @@ -1,7 +1,8 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; -import { Event, EventHint, Exception, ExtendedError, Integration } from '@sentry/types'; +import { Event, EventHint, Exception, ExtendedError, Integration, StackParser } from '@sentry/types'; import { isInstanceOf, resolvedSyncPromise, SyncPromise } from '@sentry/utils'; +import { NodeClient } from '../client'; import { exceptionFromError } from '../eventbuilder'; import { ContextLines } from './contextlines'; @@ -42,11 +43,12 @@ export class LinkedErrors implements Integration { * @inheritDoc */ public setupOnce(): void { - addGlobalEventProcessor((event: Event, hint?: EventHint) => { - const self = getCurrentHub().getIntegration(LinkedErrors); - if (self) { - const handler = self._handler && self._handler.bind(self); - return typeof handler === 'function' ? handler(event, hint) : event; + addGlobalEventProcessor(async (event: Event, hint: EventHint) => { + const hub = getCurrentHub(); + const self = hub.getIntegration(LinkedErrors); + const client = hub.getClient(); + if (client && self) { + await self._handler(client.getOptions().stackParser, event, hint); } return event; }); @@ -55,13 +57,13 @@ export class LinkedErrors implements Integration { /** * @inheritDoc */ - private _handler(event: Event, hint?: EventHint): PromiseLike { - if (!event.exception || !event.exception.values || !hint || !isInstanceOf(hint.originalException, Error)) { + private _handler(stackParser: StackParser, event: Event, hint: EventHint): PromiseLike { + if (!event.exception || !event.exception.values || !isInstanceOf(hint.originalException, Error)) { return resolvedSyncPromise(event); } return new SyncPromise(resolve => { - void this._walkErrorTree(hint.originalException as Error, this._key) + void this._walkErrorTree(stackParser, hint.originalException as Error, this._key) .then((linkedErrors: Exception[]) => { if (event && event.exception && event.exception.values) { event.exception.values = [...linkedErrors, ...event.exception.values]; @@ -77,12 +79,17 @@ export class LinkedErrors implements Integration { /** * @inheritDoc */ - private async _walkErrorTree(error: ExtendedError, key: string, stack: Exception[] = []): Promise { + private async _walkErrorTree( + stackParser: StackParser, + error: ExtendedError, + key: string, + stack: Exception[] = [], + ): Promise { if (!isInstanceOf(error[key], Error) || stack.length + 1 >= this._limit) { return Promise.resolve(stack); } - const exception = exceptionFromError(error[key]); + const exception = exceptionFromError(stackParser, error[key]); // If the ContextLines integration is enabled, we add source code context to linked errors // because we can't guarantee the order that integrations are run. @@ -92,7 +99,7 @@ export class LinkedErrors implements Integration { } return new Promise((resolve, reject) => { - void this._walkErrorTree(error[key], key, [exception, ...stack]) + void this._walkErrorTree(stackParser, error[key], key, [exception, ...stack]) .then(resolve) .then(null, () => { reject(); diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index 9c8d33913315..dfff26fa0675 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -1,5 +1,5 @@ import { getCurrentHub, Scope } from '@sentry/core'; -import { Integration, Severity } from '@sentry/types'; +import { Integration } from '@sentry/types'; import { logger } from '@sentry/utils'; import { NodeClient } from '../client'; @@ -78,7 +78,7 @@ export class OnUncaughtException implements Integration { if (hub.getIntegration(OnUncaughtException)) { hub.withScope((scope: Scope) => { - scope.setLevel(Severity.Fatal); + scope.setLevel('fatal'); hub.captureException(error, { originalException: error, data: { mechanism: { handled: false, type: 'onuncaughtexception' } }, diff --git a/packages/node/src/integrations/onunhandledrejection.ts b/packages/node/src/integrations/onunhandledrejection.ts index 19f733b1f908..2ba9a3d8f205 100644 --- a/packages/node/src/integrations/onunhandledrejection.ts +++ b/packages/node/src/integrations/onunhandledrejection.ts @@ -46,36 +46,15 @@ export class OnUnhandledRejection implements Integration { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any public sendUnhandledPromise(reason: any, promise: any): void { const hub = getCurrentHub(); - - if (!hub.getIntegration(OnUnhandledRejection)) { - this._handleRejection(reason); - return; - } - - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ - const context = (promise.domain && promise.domain.sentryContext) || {}; - - hub.withScope((scope: Scope) => { - scope.setExtra('unhandledPromiseRejection', true); - - // Preserve backwards compatibility with raven-node for now - if (context.user) { - scope.setUser(context.user); - } - if (context.tags) { - scope.setTags(context.tags); - } - if (context.extra) { - scope.setExtras(context.extra); - } - - hub.captureException(reason, { - originalException: promise, - data: { mechanism: { handled: false, type: 'onunhandledrejection' } }, + if (hub.getIntegration(OnUnhandledRejection)) { + hub.withScope((scope: Scope) => { + scope.setExtra('unhandledPromiseRejection', true); + hub.captureException(reason, { + originalException: promise, + data: { mechanism: { handled: false, type: 'onunhandledrejection' } }, + }); }); - }); - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ - + } this._handleRejection(reason); } diff --git a/packages/node/src/integrations/utils/errorhandling.ts b/packages/node/src/integrations/utils/errorhandling.ts index aa79eebcaee8..587840d040b5 100644 --- a/packages/node/src/integrations/utils/errorhandling.ts +++ b/packages/node/src/integrations/utils/errorhandling.ts @@ -1,5 +1,5 @@ import { getCurrentHub } from '@sentry/core'; -import { forget, logger } from '@sentry/utils'; +import { logger } from '@sentry/utils'; import { NodeClient } from '../../client'; import { IS_DEBUG_BUILD } from '../../flags'; @@ -24,12 +24,13 @@ export function logAndExitProcess(error: Error): void { const timeout = (options && options.shutdownTimeout && options.shutdownTimeout > 0 && options.shutdownTimeout) || DEFAULT_SHUTDOWN_TIMEOUT; - forget( - client.close(timeout).then((result: boolean) => { + client.close(timeout).then( + (result: boolean) => { if (!result) { IS_DEBUG_BUILD && logger.warn('We reached the timeout for emptying the request buffer, still exiting now!'); } global.process.exit(1); - }), + }, + error => logger.error(error), ); } diff --git a/packages/node/src/module.ts b/packages/node/src/module.ts new file mode 100644 index 000000000000..15682d16d121 --- /dev/null +++ b/packages/node/src/module.ts @@ -0,0 +1,36 @@ +import { basename, dirname } from '@sentry/utils'; + +/** Gets the module from a filename */ +export function getModule(filename: string | undefined): string | undefined { + if (!filename) { + return; + } + + // We could use optional chaining here but webpack does like that mixed with require + const base = `${ + (require && require.main && require.main.filename && dirname(require.main.filename)) || global.process.cwd() + }/`; + + // It's specifically a module + const file = basename(filename, '.js'); + + const path = dirname(filename); + let n = path.lastIndexOf('/node_modules/'); + if (n > -1) { + // /node_modules/ is 14 chars + return `${path.substr(n + 14).replace(/\//g, '.')}:${file}`; + } + // Let's see if it's a part of the main module + // To be a part of main module, it has to share the same base + n = `${path}/`.lastIndexOf(base, 0); + + if (n === 0) { + let moduleName = path.substr(base.length).replace(/\//g, '.'); + if (moduleName) { + moduleName += ':'; + } + moduleName += file; + return moduleName; + } + return file; +} diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index 7dd660a44048..e83320103cb6 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -1,13 +1,16 @@ -import { getCurrentHub, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; +import { getCurrentHub, getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; import { getMainCarrier, setHubOnCarrier } from '@sentry/hub'; -import { SessionStatus } from '@sentry/types'; -import { getGlobalObject, logger } from '@sentry/utils'; +import { SessionStatus, StackParser } from '@sentry/types'; +import { createStackParser, getGlobalObject, logger, stackParserFromStackParserOptions } from '@sentry/utils'; import * as domain from 'domain'; import { NodeClient } from './client'; import { IS_DEBUG_BUILD } from './flags'; import { Console, ContextLines, Http, LinkedErrors, OnUncaughtException, OnUnhandledRejection } from './integrations'; -import { NodeOptions } from './types'; +import { getModule } from './module'; +import { nodeStackLineParser } from './stack-parser'; +import { makeNodeTransport } from './transports'; +import { NodeClientOptions, NodeOptions } from './types'; export const defaultIntegrations = [ // Common @@ -125,7 +128,15 @@ export function init(options: NodeOptions = {}): void { setHubOnCarrier(carrier, getCurrentHub()); } - initAndBind(NodeClient, options); + // TODO(v7): Refactor this to reduce the logic above + const clientOptions: NodeClientOptions = { + ...options, + stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), + integrations: getIntegrationsToSetup(options), + transport: options.transport || makeNodeTransport, + }; + + initAndBind(NodeClient, clientOptions); if (options.autoSessionTracking) { startSessionTracking(); @@ -182,7 +193,7 @@ export function isAutoSessionTrackingEnabled(client?: NodeClient): boolean { if (client === undefined) { return false; } - const clientOptions: NodeOptions = client && client.getOptions(); + const clientOptions = client && client.getOptions(); if (clientOptions && clientOptions.autoSessionTracking !== undefined) { return clientOptions.autoSessionTracking; } @@ -222,6 +233,9 @@ export function getSentryRelease(fallback?: string): string | undefined { ); } +/** Node.js stack parser */ +export const defaultStackParser: StackParser = createStackParser(nodeStackLineParser(getModule)); + /** * Enable automatic Session Tracking for the node process. */ diff --git a/packages/node/src/stack-parser.ts b/packages/node/src/stack-parser.ts index a37e38001da3..7fd8a5ee3abd 100644 --- a/packages/node/src/stack-parser.ts +++ b/packages/node/src/stack-parser.ts @@ -1,116 +1,89 @@ -import { basename, dirname, StackLineParser, StackLineParserFn } from '@sentry/utils'; - -/** Gets the module */ -function getModule(filename: string | undefined): string | undefined { - if (!filename) { - return; - } - - // We could use optional chaining here but webpack does like that mixed with require - const base = `${ - (require && require.main && require.main.filename && dirname(require.main.filename)) || global.process.cwd() - }/`; - - // It's specifically a module - const file = basename(filename, '.js'); - - const path = dirname(filename); - let n = path.lastIndexOf('/node_modules/'); - if (n > -1) { - // /node_modules/ is 14 chars - return `${path.substr(n + 14).replace(/\//g, '.')}:${file}`; - } - // Let's see if it's a part of the main module - // To be a part of main module, it has to share the same base - n = `${path}/`.lastIndexOf(base, 0); - - if (n === 0) { - let moduleName = path.substr(base.length).replace(/\//g, '.'); - if (moduleName) { - moduleName += ':'; - } - moduleName += file; - return moduleName; - } - return file; -} +import { StackLineParser, StackLineParserFn } from '@sentry/types'; const FILENAME_MATCH = /^\s*[-]{4,}$/; const FULL_MATCH = /at (?:async )?(?:(.+?)\s+\()?(?:(.+?):(\d+)(?::(\d+))?|([^)]+))\)?/; +type GetModuleFn = (filename: string | undefined) => string | undefined; + // eslint-disable-next-line complexity -const node: StackLineParserFn = (line: string) => { - if (line.match(FILENAME_MATCH)) { - return { - filename: line, - }; - } +function node(getModule?: GetModuleFn): StackLineParserFn { + // eslint-disable-next-line complexity + return (line: string) => { + if (line.match(FILENAME_MATCH)) { + return { + filename: line, + }; + } - const lineMatch = line.match(FULL_MATCH); - if (!lineMatch) { - return undefined; - } + const lineMatch = line.match(FULL_MATCH); + if (!lineMatch) { + return undefined; + } - let object: string | undefined; - let method: string | undefined; - let functionName: string | undefined; - let typeName: string | undefined; - let methodName: string | undefined; + let object: string | undefined; + let method: string | undefined; + let functionName: string | undefined; + let typeName: string | undefined; + let methodName: string | undefined; - if (lineMatch[1]) { - functionName = lineMatch[1]; + if (lineMatch[1]) { + functionName = lineMatch[1]; - let methodStart = functionName.lastIndexOf('.'); - if (functionName[methodStart - 1] === '.') { - // eslint-disable-next-line no-plusplus - methodStart--; - } + let methodStart = functionName.lastIndexOf('.'); + if (functionName[methodStart - 1] === '.') { + // eslint-disable-next-line no-plusplus + methodStart--; + } - if (methodStart > 0) { - object = functionName.substr(0, methodStart); - method = functionName.substr(methodStart + 1); - const objectEnd = object.indexOf('.Module'); - if (objectEnd > 0) { - functionName = functionName.substr(objectEnd + 1); - object = object.substr(0, objectEnd); + if (methodStart > 0) { + object = functionName.substr(0, methodStart); + method = functionName.substr(methodStart + 1); + const objectEnd = object.indexOf('.Module'); + if (objectEnd > 0) { + functionName = functionName.substr(objectEnd + 1); + object = object.substr(0, objectEnd); + } } + typeName = undefined; } - typeName = undefined; - } - if (method) { - typeName = object; - methodName = method; - } + if (method) { + typeName = object; + methodName = method; + } - if (method === '') { - methodName = undefined; - functionName = undefined; - } + if (method === '') { + methodName = undefined; + functionName = undefined; + } - if (functionName === undefined) { - methodName = methodName || ''; - functionName = typeName ? `${typeName}.${methodName}` : methodName; - } + if (functionName === undefined) { + methodName = methodName || ''; + functionName = typeName ? `${typeName}.${methodName}` : methodName; + } - const filename = lineMatch[2]?.startsWith('file://') ? lineMatch[2].substr(7) : lineMatch[2]; - const isNative = lineMatch[5] === 'native'; - const isInternal = - isNative || (filename && !filename.startsWith('/') && !filename.startsWith('.') && filename.indexOf(':\\') !== 1); + const filename = lineMatch[2]?.startsWith('file://') ? lineMatch[2].substr(7) : lineMatch[2]; + const isNative = lineMatch[5] === 'native'; + const isInternal = + isNative || (filename && !filename.startsWith('/') && !filename.startsWith('.') && filename.indexOf(':\\') !== 1); - // in_app is all that's not an internal Node function or a module within node_modules - // note that isNative appears to return true even for node core libraries - // see https://github.com/getsentry/raven-node/issues/176 - const in_app = !isInternal && filename !== undefined && !filename.includes('node_modules/'); + // in_app is all that's not an internal Node function or a module within node_modules + // note that isNative appears to return true even for node core libraries + // see https://github.com/getsentry/raven-node/issues/176 + const in_app = !isInternal && filename !== undefined && !filename.includes('node_modules/'); - return { - filename, - module: getModule(filename), - function: functionName, - lineno: parseInt(lineMatch[3], 10) || undefined, - colno: parseInt(lineMatch[4], 10) || undefined, - in_app, + return { + filename, + module: getModule?.(filename), + function: functionName, + lineno: parseInt(lineMatch[3], 10) || undefined, + colno: parseInt(lineMatch[4], 10) || undefined, + in_app, + }; }; -}; +} -export const nodeStackParser: StackLineParser = [90, node]; +/** Node.js stack line parser */ +export function nodeStackLineParser(getModule?: GetModuleFn): StackLineParser { + return [90, node(getModule)]; +} diff --git a/packages/node/src/transports/base/index.ts b/packages/node/src/transports/base/index.ts deleted file mode 100644 index 0cbe39c42b2b..000000000000 --- a/packages/node/src/transports/base/index.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { APIDetails, getRequestHeaders, initAPIDetails, SDK_VERSION } from '@sentry/core'; -import { - DsnProtocol, - Event, - Response, - SentryRequest, - SentryRequestType, - Session, - SessionAggregates, - Transport, - TransportOptions, -} from '@sentry/types'; -import { - eventStatusFromHttpCode, - logger, - makePromiseBuffer, - parseRetryAfterHeader, - PromiseBuffer, - SentryError, -} from '@sentry/utils'; -import * as fs from 'fs'; -import * as http from 'http'; -import * as https from 'https'; -import { URL } from 'url'; - -import { IS_DEBUG_BUILD } from '../../flags'; -import { SDK_NAME } from '../../version'; -import { HTTPModule } from './http-module'; - -export type URLParts = Pick; -export type UrlParser = (url: string) => URLParts; - -const CATEGORY_MAPPING: { - [key in SentryRequestType]: string; -} = { - event: 'error', - transaction: 'transaction', - session: 'session', - attachment: 'attachment', -}; - -/** Base Transport class implementation */ -export abstract class BaseTransport implements Transport { - /** The Agent used for corresponding transport */ - public module?: HTTPModule; - - /** The Agent used for corresponding transport */ - public client?: http.Agent | https.Agent; - - /** API object */ - protected _api: APIDetails; - - /** A simple buffer holding all requests. */ - protected readonly _buffer: PromiseBuffer = makePromiseBuffer(30); - - /** Locks transport after receiving rate limits in a response */ - protected readonly _rateLimits: Record = {}; - - /** Create instance and set this.dsn */ - public constructor(public options: TransportOptions) { - // eslint-disable-next-line deprecation/deprecation - this._api = initAPIDetails(options.dsn, options._metadata, options.tunnel); - } - - /** Default function used to parse URLs */ - public urlParser: UrlParser = url => new URL(url); - - /** - * @inheritDoc - */ - public sendEvent(_: Event): PromiseLike { - throw new SentryError('Transport Class has to implement `sendEvent` method.'); - } - - /** - * @inheritDoc - */ - public close(timeout?: number): PromiseLike { - return this._buffer.drain(timeout); - } - - /** - * Extracts proxy settings from client options and env variables. - * - * Honors `no_proxy` env variable with the highest priority to allow for hosts exclusion. - * - * An order of priority for available protocols is: - * `http` => `options.httpProxy` | `process.env.http_proxy` - * `https` => `options.httpsProxy` | `options.httpProxy` | `process.env.https_proxy` | `process.env.http_proxy` - */ - protected _getProxy(protocol: DsnProtocol): string | undefined { - const { no_proxy, http_proxy, https_proxy } = process.env; - const { httpProxy, httpsProxy } = this.options; - const proxy = protocol === 'http' ? httpProxy || http_proxy : httpsProxy || httpProxy || https_proxy || http_proxy; - - if (!no_proxy) { - return proxy; - } - - const { host, port } = this._api.dsn; - for (const np of no_proxy.split(',')) { - if (host.endsWith(np) || `${host}:${port}`.endsWith(np)) { - return; - } - } - - return proxy; - } - - /** Returns a build request option object used by request */ - protected _getRequestOptions(urlParts: URLParts): http.RequestOptions | https.RequestOptions { - const headers = { - ...getRequestHeaders(this._api.dsn, SDK_NAME, SDK_VERSION), - ...this.options.headers, - }; - const { hostname, pathname, port, protocol } = urlParts; - // See https://github.com/nodejs/node/blob/38146e717fed2fabe3aacb6540d839475e0ce1c6/lib/internal/url.js#L1268-L1290 - // We ignore the query string on purpose - const path = `${pathname}`; - - return { - agent: this.client, - headers, - hostname, - method: 'POST', - path, - port, - protocol, - ...(this.options.caCerts && { - ca: fs.readFileSync(this.options.caCerts), - }), - }; - } - - /** - * Gets the time that given category is disabled until for rate limiting - */ - protected _disabledUntil(requestType: SentryRequestType): Date { - const category = CATEGORY_MAPPING[requestType]; - return this._rateLimits[category] || this._rateLimits.all; - } - - /** - * Checks if a category is rate limited - */ - protected _isRateLimited(requestType: SentryRequestType): boolean { - return this._disabledUntil(requestType) > new Date(Date.now()); - } - - /** - * Sets internal _rateLimits from incoming headers. Returns true if headers contains a non-empty rate limiting header. - */ - protected _handleRateLimit(headers: Record): boolean { - const now = Date.now(); - const rlHeader = headers['x-sentry-rate-limits']; - const raHeader = headers['retry-after']; - - if (rlHeader) { - // rate limit headers are of the form - //
,
,.. - // where each
is of the form - // : : : - // where - // is a delay in ms - // is the event type(s) (error, transaction, etc) being rate limited and is of the form - // ;;... - // is what's being limited (org, project, or key) - ignored by SDK - // is an arbitrary string like "org_quota" - ignored by SDK - for (const limit of rlHeader.trim().split(',')) { - const parameters = limit.split(':', 2); - const headerDelay = parseInt(parameters[0], 10); - const delay = (!isNaN(headerDelay) ? headerDelay : 60) * 1000; // 60sec default - for (const category of (parameters[1] && parameters[1].split(';')) || ['all']) { - // categoriesAllowed is added here to ensure we are only storing rate limits for categories we support in this - // sdk and any categories that are not supported will not be added redundantly to the rateLimits object - const categoriesAllowed = [ - ...(Object.keys(CATEGORY_MAPPING) as [SentryRequestType]).map(k => CATEGORY_MAPPING[k]), - 'all', - ]; - if (categoriesAllowed.includes(category)) this._rateLimits[category] = new Date(now + delay); - } - } - return true; - } else if (raHeader) { - this._rateLimits.all = new Date(now + parseRetryAfterHeader(raHeader, now)); - return true; - } - return false; - } - - /** JSDoc */ - protected async _send( - sentryRequest: SentryRequest, - originalPayload?: Event | Session | SessionAggregates, - ): Promise { - if (!this.module) { - throw new SentryError('No module available'); - } - if (originalPayload && this._isRateLimited(sentryRequest.type)) { - return Promise.reject({ - payload: originalPayload, - type: sentryRequest.type, - reason: `Transport for ${sentryRequest.type} requests locked till ${this._disabledUntil( - sentryRequest.type, - )} due to too many requests.`, - status: 429, - }); - } - - return this._buffer.add( - () => - new Promise((resolve, reject) => { - if (!this.module) { - throw new SentryError('No module available'); - } - const options = this._getRequestOptions(this.urlParser(sentryRequest.url)); - const req = this.module.request(options, res => { - const statusCode = res.statusCode || 500; - const status = eventStatusFromHttpCode(statusCode); - - res.setEncoding('utf8'); - - /** - * "Key-value pairs of header names and values. Header names are lower-cased." - * https://nodejs.org/api/http.html#http_message_headers - */ - let retryAfterHeader = res.headers ? res.headers['retry-after'] : ''; - retryAfterHeader = (Array.isArray(retryAfterHeader) ? retryAfterHeader[0] : retryAfterHeader) as string; - - let rlHeader = res.headers ? res.headers['x-sentry-rate-limits'] : ''; - rlHeader = (Array.isArray(rlHeader) ? rlHeader[0] : rlHeader) as string; - - const headers = { - 'x-sentry-rate-limits': rlHeader, - 'retry-after': retryAfterHeader, - }; - - const limited = this._handleRateLimit(headers); - if (limited) - IS_DEBUG_BUILD && - logger.warn( - `Too many ${sentryRequest.type} requests, backing off until: ${this._disabledUntil( - sentryRequest.type, - )}`, - ); - - if (status === 'success') { - resolve({ status }); - } else { - let rejectionMessage = `HTTP Error (${statusCode})`; - if (res.headers && res.headers['x-sentry-error']) { - rejectionMessage += `: ${res.headers['x-sentry-error']}`; - } - reject(new SentryError(rejectionMessage)); - } - - // Force the socket to drain - res.on('data', () => { - // Drain - }); - res.on('end', () => { - // Drain - }); - }); - req.on('error', reject); - req.end(sentryRequest.body); - }), - ); - } -} diff --git a/packages/node/src/transports/base/http-module.ts b/packages/node/src/transports/http-module.ts similarity index 97% rename from packages/node/src/transports/base/http-module.ts rename to packages/node/src/transports/http-module.ts index 0189d4971e4b..3d21faf2fc34 100644 --- a/packages/node/src/transports/base/http-module.ts +++ b/packages/node/src/transports/http-module.ts @@ -20,7 +20,7 @@ export interface HTTPModuleRequestIncomingMessage { * Some transports work in a special Javascript environment where http.IncomingMessage is not available. */ export interface HTTPModuleClientRequest { - end(chunk: string): void; + end(chunk: string | Uint8Array): void; on(event: 'error', listener: () => void): void; } diff --git a/packages/node/src/transports/http.ts b/packages/node/src/transports/http.ts index a9234d830876..64bf52070eb9 100644 --- a/packages/node/src/transports/http.ts +++ b/packages/node/src/transports/http.ts @@ -1,32 +1,129 @@ -import { eventToSentryRequest, sessionToSentryRequest } from '@sentry/core'; -import { Event, Response, Session, SessionAggregates, TransportOptions } from '@sentry/types'; +import { createTransport } from '@sentry/core'; +import { + BaseTransportOptions, + Transport, + TransportMakeRequestResponse, + TransportRequest, + TransportRequestExecutor, +} from '@sentry/types'; import * as http from 'http'; +import * as https from 'https'; +import { URL } from 'url'; -import { BaseTransport } from './base'; - -/** Node http module transport */ -export class HTTPTransport extends BaseTransport { - /** Create a new instance and set this.agent */ - public constructor(public options: TransportOptions) { - super(options); - const proxy = this._getProxy('http'); - this.module = http; - this.client = proxy - ? (new (require('https-proxy-agent'))(proxy) as http.Agent) - : new http.Agent({ keepAlive: false, maxSockets: 30, timeout: 2000 }); - } +import { HTTPModule } from './http-module'; - /** - * @inheritDoc - */ - public sendEvent(event: Event): Promise { - return this._send(eventToSentryRequest(event, this._api), event); - } +export interface NodeTransportOptions extends BaseTransportOptions { + /** Define custom headers */ + headers?: Record; + /** Set a proxy that should be used for outbound requests. */ + proxy?: string; + /** HTTPS proxy CA certificates */ + caCerts?: string | Buffer | Array; + /** Custom HTTP module. Defaults to the native 'http' and 'https' modules. */ + httpModule?: HTTPModule; +} + +/** + * Creates a Transport that uses native the native 'http' and 'https' modules to send events to Sentry. + */ +export function makeNodeTransport(options: NodeTransportOptions): Transport { + const urlSegments = new URL(options.url); + const isHttps = urlSegments.protocol === 'https:'; + + // Proxy prioritization: http => `options.proxy` | `process.env.http_proxy` + // Proxy prioritization: https => `options.proxy` | `process.env.https_proxy` | `process.env.http_proxy` + const proxy = applyNoProxyOption( + urlSegments, + options.proxy || (isHttps ? process.env.https_proxy : undefined) || process.env.http_proxy, + ); + + const nativeHttpModule = isHttps ? https : http; + + // TODO(v7): Evaluate if we can set keepAlive to true. This would involve testing for memory leaks in older node + // versions(>= 8) as they had memory leaks when using it: #2555 + const agent = proxy + ? (new (require('https-proxy-agent'))(proxy) as http.Agent) + : new nativeHttpModule.Agent({ keepAlive: false, maxSockets: 30, timeout: 2000 }); + + const requestExecutor = createRequestExecutor(options, options.httpModule ?? nativeHttpModule, agent); + return createTransport(options, requestExecutor); +} - /** - * @inheritDoc - */ - public sendSession(session: Session | SessionAggregates): PromiseLike { - return this._send(sessionToSentryRequest(session, this._api), session); +/** + * Honors the `no_proxy` env variable with the highest priority to allow for hosts exclusion. + * + * @param transportUrl The URL the transport intends to send events to. + * @param proxy The client configured proxy. + * @returns A proxy the transport should use. + */ +function applyNoProxyOption(transportUrlSegments: URL, proxy: string | undefined): string | undefined { + const { no_proxy } = process.env; + + const urlIsExemptFromProxy = + no_proxy && + no_proxy + .split(',') + .some( + exemption => transportUrlSegments.host.endsWith(exemption) || transportUrlSegments.hostname.endsWith(exemption), + ); + + if (urlIsExemptFromProxy) { + return undefined; + } else { + return proxy; } } + +/** + * Creates a RequestExecutor to be used with `createTransport`. + */ +function createRequestExecutor( + options: NodeTransportOptions, + httpModule: HTTPModule, + agent: http.Agent, +): TransportRequestExecutor { + const { hostname, pathname, port, protocol, search } = new URL(options.url); + return function makeRequest(request: TransportRequest): Promise { + return new Promise((resolve, reject) => { + const req = httpModule.request( + { + method: 'POST', + agent, + headers: options.headers, + hostname, + path: `${pathname}${search}`, + port, + protocol, + ca: options.caCerts, + }, + res => { + res.on('data', () => { + // Drain socket + }); + + res.on('end', () => { + // Drain socket + }); + + res.setEncoding('utf8'); + + // "Key-value pairs of header names and values. Header names are lower-cased." + // https://nodejs.org/api/http.html#http_message_headers + const retryAfterHeader = res.headers['retry-after'] ?? null; + const rateLimitsHeader = res.headers['x-sentry-rate-limits'] ?? null; + + resolve({ + statusCode: res.statusCode, + headers: { + 'retry-after': retryAfterHeader, + 'x-sentry-rate-limits': Array.isArray(rateLimitsHeader) ? rateLimitsHeader[0] : rateLimitsHeader, + }, + }); + }, + ); + + req.on('error', reject); + req.end(request.body); + }); + }; +} diff --git a/packages/node/src/transports/https.ts b/packages/node/src/transports/https.ts deleted file mode 100644 index d6c312608504..000000000000 --- a/packages/node/src/transports/https.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { eventToSentryRequest, sessionToSentryRequest } from '@sentry/core'; -import { Event, Response, Session, SessionAggregates, TransportOptions } from '@sentry/types'; -import * as https from 'https'; - -import { BaseTransport } from './base'; - -/** Node https module transport */ -export class HTTPSTransport extends BaseTransport { - /** Create a new instance and set this.agent */ - public constructor(public options: TransportOptions) { - super(options); - const proxy = this._getProxy('https'); - this.module = https; - this.client = proxy - ? (new (require('https-proxy-agent'))(proxy) as https.Agent) - : new https.Agent({ keepAlive: false, maxSockets: 30, timeout: 2000 }); - } - - /** - * @inheritDoc - */ - public sendEvent(event: Event): Promise { - return this._send(eventToSentryRequest(event, this._api), event); - } - - /** - * @inheritDoc - */ - public sendSession(session: Session | SessionAggregates): PromiseLike { - return this._send(sessionToSentryRequest(session, this._api), session); - } -} diff --git a/packages/node/src/transports/index.ts b/packages/node/src/transports/index.ts index 2cabeee08d5f..ba59ba8878a4 100644 --- a/packages/node/src/transports/index.ts +++ b/packages/node/src/transports/index.ts @@ -1,4 +1,3 @@ -export { BaseTransport } from './base'; -export { HTTPTransport } from './http'; -export { HTTPSTransport } from './https'; -export { makeNodeTransport, NodeTransportOptions } from './new'; +export type { NodeTransportOptions } from './http'; + +export { makeNodeTransport } from './http'; diff --git a/packages/node/src/transports/new.ts b/packages/node/src/transports/new.ts deleted file mode 100644 index b30dc3ffe36f..000000000000 --- a/packages/node/src/transports/new.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { - BaseTransportOptions, - createTransport, - NewTransport, - TransportMakeRequestResponse, - TransportRequest, - TransportRequestExecutor, -} from '@sentry/core'; -import { eventStatusFromHttpCode } from '@sentry/utils'; -import * as http from 'http'; -import * as https from 'https'; -import { URL } from 'url'; - -import { HTTPModule } from './base/http-module'; - -// TODO(v7): -// - Rename this file "transport.ts" -// - Move this file one folder upwards -// - Delete "transports" folder -// OR -// - Split this file up and leave it in the transports folder - -export interface NodeTransportOptions extends BaseTransportOptions { - /** Define custom headers */ - headers?: Record; - /** Set a proxy that should be used for outbound requests. */ - proxy?: string; - /** HTTPS proxy CA certificates */ - caCerts?: string | Buffer | Array; - /** Custom HTTP module. Defaults to the native 'http' and 'https' modules. */ - httpModule?: HTTPModule; -} - -/** - * Creates a Transport that uses native the native 'http' and 'https' modules to send events to Sentry. - */ -export function makeNodeTransport(options: NodeTransportOptions): NewTransport { - const urlSegments = new URL(options.url); - const isHttps = urlSegments.protocol === 'https:'; - - // Proxy prioritization: http => `options.proxy` | `process.env.http_proxy` - // Proxy prioritization: https => `options.proxy` | `process.env.https_proxy` | `process.env.http_proxy` - const proxy = applyNoProxyOption( - urlSegments, - options.proxy || (isHttps ? process.env.https_proxy : undefined) || process.env.http_proxy, - ); - - const nativeHttpModule = isHttps ? https : http; - - // TODO(v7): Evaluate if we can set keepAlive to true. This would involve testing for memory leaks in older node - // versions(>= 8) as they had memory leaks when using it: #2555 - const agent = proxy - ? (new (require('https-proxy-agent'))(proxy) as http.Agent) - : new nativeHttpModule.Agent({ keepAlive: false, maxSockets: 30, timeout: 2000 }); - - const requestExecutor = createRequestExecutor(options, options.httpModule ?? nativeHttpModule, agent); - return createTransport({ bufferSize: options.bufferSize }, requestExecutor); -} - -/** - * Honors the `no_proxy` env variable with the highest priority to allow for hosts exclusion. - * - * @param transportUrl The URL the transport intends to send events to. - * @param proxy The client configured proxy. - * @returns A proxy the transport should use. - */ -function applyNoProxyOption(transportUrlSegments: URL, proxy: string | undefined): string | undefined { - const { no_proxy } = process.env; - - const urlIsExemptFromProxy = - no_proxy && - no_proxy - .split(',') - .some( - exemption => transportUrlSegments.host.endsWith(exemption) || transportUrlSegments.hostname.endsWith(exemption), - ); - - if (urlIsExemptFromProxy) { - return undefined; - } else { - return proxy; - } -} - -/** - * Creates a RequestExecutor to be used with `createTransport`. - */ -function createRequestExecutor( - options: NodeTransportOptions, - httpModule: HTTPModule, - agent: http.Agent, -): TransportRequestExecutor { - const { hostname, pathname, port, protocol, search } = new URL(options.url); - - return function makeRequest(request: TransportRequest): Promise { - return new Promise((resolve, reject) => { - const req = httpModule.request( - { - method: 'POST', - agent, - headers: options.headers, - hostname, - path: `${pathname}${search}`, - port, - protocol, - ca: options.caCerts, - }, - res => { - res.on('data', () => { - // Drain socket - }); - - res.on('end', () => { - // Drain socket - }); - - const statusCode = res.statusCode ?? 500; - const status = eventStatusFromHttpCode(statusCode); - - res.setEncoding('utf8'); - - // "Key-value pairs of header names and values. Header names are lower-cased." - // https://nodejs.org/api/http.html#http_message_headers - const retryAfterHeader = res.headers['retry-after'] ?? null; - const rateLimitsHeader = res.headers['x-sentry-rate-limits'] ?? null; - - resolve({ - headers: { - 'retry-after': retryAfterHeader, - 'x-sentry-rate-limits': Array.isArray(rateLimitsHeader) ? rateLimitsHeader[0] : rateLimitsHeader, - }, - reason: status, - statusCode: statusCode, - }); - }, - ); - - req.on('error', reject); - req.end(request.body); - }); - }; -} diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index 6c6651bc7b88..57b19dcd1820 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -1,40 +1,23 @@ -import { Options } from '@sentry/types'; +import { ClientOptions, Options } from '@sentry/types'; -/** - * Configuration options for the Sentry Node SDK. - * @see NodeClient for more information. - */ -export interface NodeOptions extends Options { +import { NodeTransportOptions } from './transports'; + +export interface BaseNodeOptions { /** Sets an optional server name (device name) */ serverName?: string; - /** Maximum time in milliseconds to wait to drain the request queue, before the process is allowed to exit. */ - shutdownTimeout?: number; - - /** Set a HTTP proxy that should be used for outbound requests. */ - httpProxy?: string; - - /** Set a HTTPS proxy that should be used for outbound requests. */ - httpsProxy?: string; - - /** HTTPS proxy certificates path */ - caCerts?: string; - - /** - * Sets the number of context lines for each frame when loading a file. - * - * @deprecated Context lines configuration has moved to the `ContextLines` integration, and can be used like this: - * - * ``` - * init({ - * dsn: '__DSN__', - * integrations: [new ContextLines({ frameContextLines: 10 })] - * }) - * ``` - * - * */ - frameContextLines?: number; - /** Callback that is executed when a fatal global error occurs. */ onFatalError?(error: Error): void; } + +/** + * Configuration options for the Sentry Node SDK + * @see @sentry/types Options for more information. + */ +export interface NodeOptions extends Options, BaseNodeOptions {} + +/** + * Configuration options for the Sentry Node SDK Client class + * @see NodeClient for more information. + */ +export interface NodeClientOptions extends ClientOptions, BaseNodeOptions {} diff --git a/packages/node/src/version.ts b/packages/node/src/version.ts deleted file mode 100644 index 8cfd1070cec5..000000000000 --- a/packages/node/src/version.ts +++ /dev/null @@ -1,2 +0,0 @@ -// TODO: Remove in the next major release and rely only on @sentry/core SDK_VERSION and SdkMetadata -export const SDK_NAME = 'sentry.javascript.node'; diff --git a/packages/node/test/client.test.ts b/packages/node/test/client.test.ts index f18f66c716af..2aafb8a2544e 100644 --- a/packages/node/test/client.test.ts +++ b/packages/node/test/client.test.ts @@ -1,6 +1,7 @@ import { Scope, SessionFlusher } from '@sentry/hub'; import { NodeClient } from '../src'; +import { getDefaultNodeClientOptions } from './helper/node-client-options'; const PUBLIC_DSN = 'https://username@domain/123'; @@ -14,7 +15,8 @@ describe('NodeClient', () => { describe('captureException', () => { test('when autoSessionTracking is enabled, and requestHandler is not used -> requestStatus should not be set', () => { - client = new NodeClient({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }); + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }); + client = new NodeClient(options); const scope = new Scope(); scope.setRequestSession({ status: 'ok' }); @@ -24,7 +26,8 @@ describe('NodeClient', () => { expect(requestSession!.status).toEqual('ok'); }); test('when autoSessionTracking is disabled -> requestStatus should not be set', () => { - client = new NodeClient({ dsn: PUBLIC_DSN, autoSessionTracking: false, release: '1.4' }); + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: false, release: '1.4' }); + client = new NodeClient(options); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) client.initSessionFlusher(); @@ -38,7 +41,8 @@ describe('NodeClient', () => { expect(requestSession!.status).toEqual('ok'); }); test('when autoSessionTracking is enabled + requestSession status is Crashed -> requestStatus should not be overridden', () => { - client = new NodeClient({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }); + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }); + client = new NodeClient(options); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) client.initSessionFlusher(); @@ -52,7 +56,8 @@ describe('NodeClient', () => { expect(requestSession!.status).toEqual('crashed'); }); test('when autoSessionTracking is enabled + error occurs within request bounds -> requestStatus should be set to Errored', () => { - client = new NodeClient({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }); + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }); + client = new NodeClient(options); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) client.initSessionFlusher(); @@ -66,7 +71,8 @@ describe('NodeClient', () => { expect(requestSession!.status).toEqual('errored'); }); test('when autoSessionTracking is enabled + error occurs outside of request bounds -> requestStatus should not be set to Errored', () => { - client = new NodeClient({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }); + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }); + client = new NodeClient(options); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) client.initSessionFlusher(); @@ -82,7 +88,8 @@ describe('NodeClient', () => { describe('captureEvent()', () => { test('If autoSessionTracking is disabled, requestSession status should not be set', () => { - client = new NodeClient({ dsn: PUBLIC_DSN, autoSessionTracking: false, release: '1.4' }); + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: false, release: '1.4' }); + client = new NodeClient(options); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) client.initSessionFlusher(); @@ -100,7 +107,8 @@ describe('NodeClient', () => { }); test('When captureEvent is called with an exception, requestSession status should be set to Errored', () => { - client = new NodeClient({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '2.2' }); + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '2.2' }); + client = new NodeClient(options); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) client.initSessionFlusher(); @@ -115,7 +123,8 @@ describe('NodeClient', () => { }); test('When captureEvent is called without an exception, requestSession status should not be set to Errored', () => { - client = new NodeClient({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '2.2' }); + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '2.2' }); + client = new NodeClient(options); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) client.initSessionFlusher(); @@ -130,7 +139,8 @@ describe('NodeClient', () => { }); test('When captureEvent is called with an exception but outside of a request, then requestStatus should not be set', () => { - client = new NodeClient({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '2.2' }); + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '2.2' }); + client = new NodeClient(options); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) client.initSessionFlusher(); @@ -147,7 +157,8 @@ describe('NodeClient', () => { }); test('When captureEvent is called with a transaction, then requestSession status should not be set', () => { - client = new NodeClient({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.3' }); + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.3' }); + client = new NodeClient(options); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) client.initSessionFlusher(); @@ -161,7 +172,8 @@ describe('NodeClient', () => { }); test('When captureEvent is called with an exception but requestHandler is not used, then requestSession status should not be set', () => { - client = new NodeClient({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.3' }); + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.3' }); + client = new NodeClient(options); const scope = new Scope(); scope.setRequestSession({ status: 'ok' }); @@ -180,11 +192,12 @@ describe('NodeClient', () => { describe('flush/close', () => { test('client close function disables _sessionFlusher', async () => { jest.useRealTimers(); - const client = new NodeClient({ + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.1', }); + const client = new NodeClient(options); client.initSessionFlusher(); // Clearing interval is important here to ensure that the flush function later on is called by the `client.close()` // not due to the interval running every 60s diff --git a/packages/node/test/context-lines.test.ts b/packages/node/test/context-lines.test.ts index 821c95d7b6b0..2b0fef8206b8 100644 --- a/packages/node/test/context-lines.test.ts +++ b/packages/node/test/context-lines.test.ts @@ -3,6 +3,7 @@ import * as fs from 'fs'; import { parseStackFrames } from '../src/eventbuilder'; import { ContextLines, resetFileContentCache } from '../src/integrations/contextlines'; +import { defaultStackParser } from '../src/sdk'; import { getError } from './helper/error'; describe('ContextLines', () => { @@ -27,7 +28,7 @@ describe('ContextLines', () => { test('parseStack with same file', async () => { expect.assertions(1); - const frames = parseStackFrames(new Error('test')); + const frames = parseStackFrames(defaultStackParser, new Error('test')); await addContext(Array.from(frames)); @@ -57,12 +58,12 @@ describe('ContextLines', () => { test('parseStack with adding different file', async () => { expect.assertions(1); - const frames = parseStackFrames(new Error('test')); + const frames = parseStackFrames(defaultStackParser, new Error('test')); await addContext(frames); const numCalls = readFileSpy.mock.calls.length; - const parsedFrames = parseStackFrames(getError()); + const parsedFrames = parseStackFrames(defaultStackParser, getError()); await addContext(parsedFrames); const newErrorCalls = readFileSpy.mock.calls.length; @@ -100,7 +101,7 @@ describe('ContextLines', () => { contextLines = new ContextLines({ frameContextLines: 0 }); expect.assertions(1); - const frames = parseStackFrames(new Error('test')); + const frames = parseStackFrames(defaultStackParser, new Error('test')); await addContext(frames); expect(readFileSpy).toHaveBeenCalledTimes(0); diff --git a/packages/node/test/domain.test.ts b/packages/node/test/domain.test.ts index 2d70fe1275e2..fa6bb94db292 100644 --- a/packages/node/test/domain.test.ts +++ b/packages/node/test/domain.test.ts @@ -4,8 +4,9 @@ import * as domain from 'domain'; // We need this import here to patch domain on the global object import * as Sentry from '../src'; -// eslint-disable-next-line no-console -console.log(Sentry.SDK_NAME); +// TODO This is here because if we don't use the `Sentry` object, the 'concurrent domain hubs' test will fail. Is this a +// product of treeshaking? +Sentry.getCurrentHub(); describe('domains', () => { test('without domain', () => { diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index f0959de22132..8cee24f678fd 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -1,8 +1,8 @@ import * as sentryCore from '@sentry/core'; -import { Hub } from '@sentry/hub'; import * as sentryHub from '@sentry/hub'; +import { Hub } from '@sentry/hub'; import { Transaction } from '@sentry/tracing'; -import { Runtime } from '@sentry/types'; +import { Baggage, Runtime } from '@sentry/types'; import { SentryError } from '@sentry/utils'; import * as http from 'http'; import * as net from 'net'; @@ -18,6 +18,7 @@ import { tracingHandler, } from '../src/handlers'; import * as SDK from '../src/sdk'; +import { getDefaultNodeClientOptions } from './helper/node-client-options'; describe('parseRequest', () => { let mockReq: { [key: string]: any }; @@ -223,7 +224,8 @@ describe('requestHandler', () => { }); it('autoSessionTracking is enabled, sets requestSession status to ok, when handling a request', () => { - client = new NodeClient({ autoSessionTracking: true, release: '1.2' }); + const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '1.2' }); + client = new NodeClient(options); const hub = new Hub(client); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); @@ -235,7 +237,8 @@ describe('requestHandler', () => { }); it('autoSessionTracking is disabled, does not set requestSession, when handling a request', () => { - client = new NodeClient({ autoSessionTracking: false, release: '1.2' }); + const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '1.2' }); + client = new NodeClient(options); const hub = new Hub(client); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); @@ -247,7 +250,8 @@ describe('requestHandler', () => { }); it('autoSessionTracking is enabled, calls _captureRequestSession, on response finish', done => { - client = new NodeClient({ autoSessionTracking: true, release: '1.2' }); + const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '1.2' }); + client = new NodeClient(options); const hub = new Hub(client); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); @@ -267,7 +271,8 @@ describe('requestHandler', () => { }); it('autoSessionTracking is disabled, does not call _captureRequestSession, on response finish', done => { - client = new NodeClient({ autoSessionTracking: false, release: '1.2' }); + const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '1.2' }); + client = new NodeClient(options); const hub = new Hub(client); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); @@ -363,33 +368,67 @@ describe('tracingHandler', () => { expect(transaction.traceId).toEqual('12312012123120121231201212312012'); expect(transaction.parentSpanId).toEqual('1121201211212012'); expect(transaction.sampled).toEqual(false); + expect(transaction.metadata?.baggage).toBeUndefined(); }); - it('extracts request data for sampling context', () => { - const tracesSampler = jest.fn(); - const hub = new Hub(new NodeClient({ tracesSampler })); - // we need to mock both of these because the tracing handler relies on `@sentry/core` while the sampler relies on - // `@sentry/hub`, and mocking breaks the link between the two - jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); - jest.spyOn(sentryHub, 'getCurrentHub').mockReturnValue(hub); + it("pulls parent's data from tracing and baggage headers on the request", () => { + req.headers = { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0', + baggage: 'sentry-version=1.0,sentry-environment=production', + }; sentryTracingMiddleware(req, res, next); - expect(tracesSampler).toHaveBeenCalledWith( - expect.objectContaining({ - request: { - headers, - method, - url: `http://${hostname}${path}?${queryString}`, - cookies: { favorite: 'zukes' }, - query_string: queryString, - }, - }), - ); + const transaction = (res as any).__sentry_transaction; + + // since we have no tracesSampler defined, the default behavior (inherit if possible) applies + expect(transaction.traceId).toEqual('12312012123120121231201212312012'); + expect(transaction.parentSpanId).toEqual('1121201211212012'); + expect(transaction.sampled).toEqual(false); + expect(transaction.metadata?.baggage).toBeDefined(); + expect(transaction.metadata?.baggage).toEqual([{ version: '1.0', environment: 'production' }, ''] as Baggage); + }); + + it("pulls parent's baggage (sentry + third party entries) headers on the request", () => { + req.headers = { + baggage: 'sentry-version=1.0,sentry-environment=production,dogs=great,cats=boring', + }; + + sentryTracingMiddleware(req, res, next); + + const transaction = (res as any).__sentry_transaction; + + expect(transaction.metadata?.baggage).toBeDefined(); + expect(transaction.metadata?.baggage).toEqual([ + { version: '1.0', environment: 'production' }, + 'dogs=great,cats=boring', + ] as Baggage); + }); + + it('extracts request data for sampling context', () => { + const tracesSampler = jest.fn(); + const options = getDefaultNodeClientOptions({ tracesSampler }); + const hub = new Hub(new NodeClient(options)); + hub.run(() => { + sentryTracingMiddleware(req, res, next); + + expect(tracesSampler).toHaveBeenCalledWith( + expect.objectContaining({ + request: { + headers, + method, + url: `http://${hostname}${path}?${queryString}`, + cookies: { favorite: 'zukes' }, + query_string: queryString, + }, + }), + ); + }); }); it('puts its transaction on the scope', () => { - const hub = new Hub(new NodeClient({ tracesSampleRate: 1.0 })); + const options = getDefaultNodeClientOptions({ tracesSampleRate: 1.0 }); + const hub = new Hub(new NodeClient(options)); // we need to mock both of these because the tracing handler relies on `@sentry/core` while the sampler relies on // `@sentry/hub`, and mocking breaks the link between the two jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); @@ -720,7 +759,8 @@ describe('errorHandler()', () => { jest.restoreAllMocks(); }); it('when autoSessionTracking is disabled, does not set requestSession status on Crash', () => { - client = new NodeClient({ autoSessionTracking: false, release: '3.3' }); + const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '3.3' }); + client = new NodeClient(options); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) client.initSessionFlusher(); @@ -739,7 +779,8 @@ describe('errorHandler()', () => { }); it('autoSessionTracking is enabled + requestHandler is not used -> does not set requestSession status on Crash', () => { - client = new NodeClient({ autoSessionTracking: false, release: '3.3' }); + const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '3.3' }); + client = new NodeClient(options); const scope = sentryCore.getCurrentHub().getScope(); const hub = new Hub(client); @@ -755,7 +796,8 @@ describe('errorHandler()', () => { }); it('when autoSessionTracking is enabled, should set requestSession status to Crashed when an unhandled error occurs within the bounds of a request', () => { - client = new NodeClient({ autoSessionTracking: true, release: '1.1' }); + const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '1.1' }); + client = new NodeClient(options); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) client.initSessionFlusher(); @@ -763,17 +805,18 @@ describe('errorHandler()', () => { const hub = new Hub(client, scope); jest.spyOn(client, '_captureRequestSession'); - jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); - jest.spyOn(sentryHub, 'getCurrentHub').mockReturnValue(hub); - scope?.setRequestSession({ status: 'ok' }); - sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, next); - const requestSession = scope?.getRequestSession(); - expect(requestSession).toEqual({ status: 'crashed' }); + hub.run(() => { + scope?.setRequestSession({ status: 'ok' }); + sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, next); + const requestSession = scope?.getRequestSession(); + expect(requestSession).toEqual({ status: 'crashed' }); + }); }); it('when autoSessionTracking is enabled, should not set requestSession status on Crash when it occurs outside the bounds of a request', () => { - client = new NodeClient({ autoSessionTracking: true, release: '2.2' }); + const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '2.2' }); + client = new NodeClient(options); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) client.initSessionFlusher(); diff --git a/packages/node/test/helper/node-client-options.ts b/packages/node/test/helper/node-client-options.ts new file mode 100644 index 000000000000..8831af6a2896 --- /dev/null +++ b/packages/node/test/helper/node-client-options.ts @@ -0,0 +1,13 @@ +import { createTransport } from '@sentry/core'; +import { resolvedSyncPromise } from '@sentry/utils'; + +import { NodeClientOptions } from '../../src/types'; + +export function getDefaultNodeClientOptions(options: Partial = {}): NodeClientOptions { + return { + integrations: [], + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})), + stackParser: () => [], + ...options, + }; +} diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index 0af38dae80fb..7b127637778c 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -1,6 +1,6 @@ import { initAndBind, SDK_VERSION } from '@sentry/core'; import { getMainCarrier } from '@sentry/hub'; -import { Integration } from '@sentry/types'; +import { EventHint, Integration } from '@sentry/types'; import * as domain from 'domain'; import { @@ -15,8 +15,9 @@ import { NodeClient, Scope, } from '../src'; -import { NodeBackend } from '../src/backend'; import { ContextLines, LinkedErrors } from '../src/integrations'; +import { defaultStackParser } from '../src/sdk'; +import { getDefaultNodeClientOptions } from './helper/node-client-options'; jest.mock('@sentry/core', () => { const original = jest.requireActual('@sentry/core'); @@ -75,10 +76,10 @@ describe('SentryNode', () => { }); describe('breadcrumbs', () => { - let s: jest.SpyInstance; + let s: jest.SpyInstance; beforeEach(() => { - s = jest.spyOn(NodeBackend.prototype, 'sendEvent').mockImplementation(async () => Promise.resolve({ code: 200 })); + s = jest.spyOn(NodeClient.prototype, 'sendEvent').mockImplementation(async () => Promise.resolve({ code: 200 })); }); afterEach(() => { @@ -86,7 +87,7 @@ describe('SentryNode', () => { }); test('record auto breadcrumbs', done => { - const client = new NodeClient({ + const options = getDefaultNodeClientOptions({ beforeSend: (event: Event) => { // TODO: It should be 3, but we don't capture a breadcrumb // for our own captureMessage/captureException calls yet @@ -95,7 +96,9 @@ describe('SentryNode', () => { return null; }, dsn, + stackParser: defaultStackParser, }); + const client = new NodeClient(options); getCurrentHub().bindClient(client); addBreadcrumb({ message: 'test1' }); addBreadcrumb({ message: 'test2' }); @@ -104,10 +107,10 @@ describe('SentryNode', () => { }); describe('capture', () => { - let s: jest.SpyInstance; + let s: jest.SpyInstance; beforeEach(() => { - s = jest.spyOn(NodeBackend.prototype, 'sendEvent').mockImplementation(async () => Promise.resolve({ code: 200 })); + s = jest.spyOn(NodeClient.prototype, 'sendEvent').mockImplementation(async () => Promise.resolve({ code: 200 })); }); afterEach(() => { @@ -116,21 +119,21 @@ describe('SentryNode', () => { test('capture an exception', done => { expect.assertions(6); - getCurrentHub().bindClient( - new NodeClient({ - beforeSend: (event: Event) => { - expect(event.tags).toEqual({ test: '1' }); - expect(event.exception).not.toBeUndefined(); - expect(event.exception!.values![0]).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![2]).not.toBeUndefined(); - expect(event.exception!.values![0].value).toEqual('test'); - done(); - return null; - }, - dsn, - }), - ); + const options = getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + beforeSend: (event: Event) => { + expect(event.tags).toEqual({ test: '1' }); + expect(event.exception).not.toBeUndefined(); + expect(event.exception!.values![0]).not.toBeUndefined(); + expect(event.exception!.values![0].stacktrace!).not.toBeUndefined(); + expect(event.exception!.values![0].stacktrace!.frames![2]).not.toBeUndefined(); + expect(event.exception!.values![0].value).toEqual('test'); + done(); + return null; + }, + dsn, + }); + getCurrentHub().bindClient(new NodeClient(options)); configureScope((scope: Scope) => { scope.setTag('test', '1'); }); @@ -143,21 +146,21 @@ describe('SentryNode', () => { test('capture a string exception', done => { expect.assertions(6); - getCurrentHub().bindClient( - new NodeClient({ - beforeSend: (event: Event) => { - expect(event.tags).toEqual({ test: '1' }); - expect(event.exception).not.toBeUndefined(); - expect(event.exception!.values![0]).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![2]).not.toBeUndefined(); - expect(event.exception!.values![0].value).toEqual('test string exception'); - done(); - return null; - }, - dsn, - }), - ); + const options = getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + beforeSend: (event: Event) => { + expect(event.tags).toEqual({ test: '1' }); + expect(event.exception).not.toBeUndefined(); + expect(event.exception!.values![0]).not.toBeUndefined(); + expect(event.exception!.values![0].stacktrace!).not.toBeUndefined(); + expect(event.exception!.values![0].stacktrace!.frames![2]).not.toBeUndefined(); + expect(event.exception!.values![0].value).toEqual('test string exception'); + done(); + return null; + }, + dsn, + }); + getCurrentHub().bindClient(new NodeClient(options)); configureScope((scope: Scope) => { scope.setTag('test', '1'); }); @@ -170,25 +173,25 @@ describe('SentryNode', () => { test('capture an exception with pre/post context', done => { expect.assertions(10); - getCurrentHub().bindClient( - new NodeClient({ - beforeSend: (event: Event) => { - expect(event.tags).toEqual({ test: '1' }); - expect(event.exception).not.toBeUndefined(); - expect(event.exception!.values![0]).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![1]).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![1].pre_context).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![1].post_context).not.toBeUndefined(); - expect(event.exception!.values![0].type).toBe('Error'); - expect(event.exception!.values![0].value).toBe('test'); - expect(event.exception!.values![0].stacktrace).toBeTruthy(); - done(); - return null; - }, - dsn, - }), - ); + const options = getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + beforeSend: (event: Event) => { + expect(event.tags).toEqual({ test: '1' }); + expect(event.exception).not.toBeUndefined(); + expect(event.exception!.values![0]).not.toBeUndefined(); + expect(event.exception!.values![0].stacktrace!).not.toBeUndefined(); + expect(event.exception!.values![0].stacktrace!.frames![1]).not.toBeUndefined(); + expect(event.exception!.values![0].stacktrace!.frames![1].pre_context).not.toBeUndefined(); + expect(event.exception!.values![0].stacktrace!.frames![1].post_context).not.toBeUndefined(); + expect(event.exception!.values![0].type).toBe('Error'); + expect(event.exception!.values![0].value).toBe('test'); + expect(event.exception!.values![0].stacktrace).toBeTruthy(); + done(); + return null; + }, + dsn, + }); + getCurrentHub().bindClient(new NodeClient(options)); configureScope((scope: Scope) => { scope.setTag('test', '1'); }); @@ -201,32 +204,32 @@ describe('SentryNode', () => { test('capture a linked exception with pre/post context', done => { expect.assertions(15); - getCurrentHub().bindClient( - new NodeClient({ - integrations: [new ContextLines(), new LinkedErrors()], - beforeSend: (event: Event) => { - expect(event.exception).not.toBeUndefined(); - expect(event.exception!.values![1]).not.toBeUndefined(); - expect(event.exception!.values![1].stacktrace!).not.toBeUndefined(); - expect(event.exception!.values![1].stacktrace!.frames![1]).not.toBeUndefined(); - expect(event.exception!.values![1].stacktrace!.frames![1].pre_context).not.toBeUndefined(); - expect(event.exception!.values![1].stacktrace!.frames![1].post_context).not.toBeUndefined(); - expect(event.exception!.values![1].type).toBe('Error'); - expect(event.exception!.values![1].value).toBe('test'); - - expect(event.exception!.values![0]).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![1]).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![1].pre_context).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![1].post_context).not.toBeUndefined(); - expect(event.exception!.values![0].type).toBe('Error'); - expect(event.exception!.values![0].value).toBe('cause'); - done(); - return null; - }, - dsn, - }), - ); + const options = getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + integrations: [new ContextLines(), new LinkedErrors()], + beforeSend: (event: Event) => { + expect(event.exception).not.toBeUndefined(); + expect(event.exception!.values![1]).not.toBeUndefined(); + expect(event.exception!.values![1].stacktrace!).not.toBeUndefined(); + expect(event.exception!.values![1].stacktrace!.frames![1]).not.toBeUndefined(); + expect(event.exception!.values![1].stacktrace!.frames![1].pre_context).not.toBeUndefined(); + expect(event.exception!.values![1].stacktrace!.frames![1].post_context).not.toBeUndefined(); + expect(event.exception!.values![1].type).toBe('Error'); + expect(event.exception!.values![1].value).toBe('test'); + + expect(event.exception!.values![0]).not.toBeUndefined(); + expect(event.exception!.values![0].stacktrace!).not.toBeUndefined(); + expect(event.exception!.values![0].stacktrace!.frames![1]).not.toBeUndefined(); + expect(event.exception!.values![0].stacktrace!.frames![1].pre_context).not.toBeUndefined(); + expect(event.exception!.values![0].stacktrace!.frames![1].post_context).not.toBeUndefined(); + expect(event.exception!.values![0].type).toBe('Error'); + expect(event.exception!.values![0].value).toBe('cause'); + done(); + return null; + }, + dsn, + }); + getCurrentHub().bindClient(new NodeClient(options)); try { throw new Error('test'); } catch (e) { @@ -241,40 +244,41 @@ describe('SentryNode', () => { test('capture a message', done => { expect.assertions(2); - getCurrentHub().bindClient( - new NodeClient({ - beforeSend: (event: Event) => { - expect(event.message).toBe('test'); - expect(event.exception).toBeUndefined(); - done(); - return null; - }, - dsn, - }), - ); + const options = getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + beforeSend: (event: Event) => { + expect(event.message).toBe('test'); + expect(event.exception).toBeUndefined(); + done(); + return null; + }, + dsn, + }); + getCurrentHub().bindClient(new NodeClient(options)); captureMessage('test'); }); test('capture an event', done => { expect.assertions(2); - getCurrentHub().bindClient( - new NodeClient({ - beforeSend: (event: Event) => { - expect(event.message).toBe('test event'); - expect(event.exception).toBeUndefined(); - done(); - return null; - }, - dsn, - }), - ); + const options = getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + beforeSend: (event: Event) => { + expect(event.message).toBe('test event'); + expect(event.exception).toBeUndefined(); + done(); + return null; + }, + dsn, + }); + getCurrentHub().bindClient(new NodeClient(options)); captureEvent({ message: 'test event' }); }); test('capture an event in a domain', done => { const d = domain.create(); - const client = new NodeClient({ + const options = getDefaultNodeClientOptions({ + stackParser: defaultStackParser, beforeSend: (event: Event) => { expect(event.message).toBe('test domain'); expect(event.exception).toBeUndefined(); @@ -283,6 +287,7 @@ describe('SentryNode', () => { }, dsn, }); + const client = new NodeClient(options); d.run(() => { getCurrentHub().bindClient(client); @@ -293,20 +298,19 @@ describe('SentryNode', () => { test('stacktrace order', done => { expect.assertions(1); - getCurrentHub().bindClient( - new NodeClient({ - beforeSend: (event: Event) => { - expect( - event.exception!.values![0].stacktrace!.frames![ - event.exception!.values![0].stacktrace!.frames!.length - 1 - ].function, - ).toEqual('testy'); - done(); - return null; - }, - dsn, - }), - ); + const options = getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + beforeSend: (event: Event) => { + expect( + event.exception!.values![0].stacktrace!.frames![event.exception!.values![0].stacktrace!.frames!.length - 1] + .function, + ).toEqual('testy'); + done(); + return null; + }, + dsn, + }); + getCurrentHub().bindClient(new NodeClient(options)); try { // @ts-ignore allow function declarations in strict mode // eslint-disable-next-line no-inner-declarations @@ -360,7 +364,7 @@ describe('SentryNode initialization', () => { init({ dsn }); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const sdkData = (getCurrentHub().getClient() as any)._backend._transport._api.metadata?.sdk; + const sdkData = (getCurrentHub().getClient() as any).getOptions()._metadata.sdk; expect(sdkData.name).toEqual('sentry.javascript.node'); expect(sdkData.packages[0].name).toEqual('npm:@sentry/node'); @@ -369,10 +373,11 @@ describe('SentryNode initialization', () => { }); it('should set SDK data when instantiating a client directly', () => { - const client = new NodeClient({ dsn }); + const options = getDefaultNodeClientOptions({ dsn }); + const client = new NodeClient(options); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const sdkData = (client as any)._backend._transport._api.metadata?.sdk; + const sdkData = (client as any).getOptions()._metadata.sdk; expect(sdkData.name).toEqual('sentry.javascript.node'); expect(sdkData.packages[0].name).toEqual('npm:@sentry/node'); @@ -401,7 +406,7 @@ describe('SentryNode initialization', () => { }); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const sdkData = (getCurrentHub().getClient() as any)._backend._transport._api.metadata?.sdk; + const sdkData = (getCurrentHub().getClient() as any).getOptions()._metadata.sdk; expect(sdkData.name).toEqual('sentry.javascript.serverless'); expect(sdkData.packages[0].name).toEqual('npm:@sentry/serverless'); diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index c7a9c3e0a621..50d26e92452a 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -1,6 +1,6 @@ import * as sentryCore from '@sentry/core'; -import { Hub } from '@sentry/hub'; import * as hubModule from '@sentry/hub'; +import { Hub } from '@sentry/hub'; import { addExtensionMethods, Span, TRACEPARENT_REGEXP, Transaction } from '@sentry/tracing'; import { parseSemver } from '@sentry/utils'; import * as http from 'http'; @@ -11,18 +11,18 @@ import * as nock from 'nock'; import { Breadcrumb } from '../../src'; import { NodeClient } from '../../src/client'; import { Http as HttpIntegration } from '../../src/integrations/http'; +import { getDefaultNodeClientOptions } from '../helper/node-client-options'; const NODE_VERSION = parseSemver(process.versions.node); describe('tracing', () => { function createTransactionOnScope() { - const hub = new Hub( - new NodeClient({ - dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', - tracesSampleRate: 1.0, - integrations: [new HttpIntegration({ tracing: true })], - }), - ); + const options = getDefaultNodeClientOptions({ + dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', + tracesSampleRate: 1.0, + integrations: [new HttpIntegration({ tracing: true })], + }); + const hub = new Hub(new NodeClient(options)); addExtensionMethods(); // we need to mock both of these because the tracing handler relies on `@sentry/core` while the sampler relies on @@ -44,9 +44,7 @@ describe('tracing', () => { http.get('http://dogs.are.great/'); - // TODO: For some reason in node 6 two request spans are appearing. Once we stop testing against it, this can go - // back to being `toEqual()`. - expect(spans.length).toBeGreaterThanOrEqual(2); + expect(spans.length).toEqual(2); // our span is at index 1 because the transaction itself is at index 0 expect(spans[1].description).toEqual('GET http://dogs.are.great/'); @@ -88,6 +86,43 @@ describe('tracing', () => { expect(sentryTraceHeader).not.toBeDefined(); }); + + it('attaches the baggage header to outgoing non-sentry requests', async () => { + nock('http://dogs.are.great').get('/').reply(200); + + createTransactionOnScope(); + + const request = http.get('http://dogs.are.great/'); + const baggageHeader = request.getHeader('baggage') as string; + + expect(baggageHeader).toBeDefined(); + // this might change once we actually add our baggage data to the header + expect(baggageHeader).toEqual(''); + }); + + it('propagates 3rd party baggage header data to outgoing non-sentry requests', async () => { + nock('http://dogs.are.great').get('/').reply(200); + + createTransactionOnScope(); + + const request = http.get({ host: 'http://dogs.are.great/', headers: { baggage: 'dog=great' } }); + const baggageHeader = request.getHeader('baggage') as string; + + expect(baggageHeader).toBeDefined(); + // this might change once we actually add our baggage data to the header + expect(baggageHeader).toEqual('dog=great'); + }); + + it("doesn't attach the sentry-trace header to outgoing sentry requests", () => { + nock('http://squirrelchasers.ingest.sentry.io').get('/api/12312012/store/').reply(200); + + createTransactionOnScope(); + + const request = http.get('http://squirrelchasers.ingest.sentry.io/api/12312012/store/'); + const baggage = request.getHeader('baggage'); + + expect(baggage).not.toBeDefined(); + }); }); describe('default protocols', () => { @@ -99,18 +134,17 @@ describe('default protocols', () => { const p = new Promise(r => { resolve = r; }); - hub.bindClient( - new NodeClient({ - dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', - integrations: [new HttpIntegration({ breadcrumbs: true })], - beforeBreadcrumb: (b: Breadcrumb) => { - if ((b.data?.url as string).includes(key)) { - resolve(b); - } - return b; - }, - }), - ); + const options = getDefaultNodeClientOptions({ + dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', + integrations: [new HttpIntegration({ breadcrumbs: true })], + beforeBreadcrumb: (b: Breadcrumb) => { + if ((b.data?.url as string).includes(key)) { + resolve(b); + } + return b; + }, + }); + hub.bindClient(new NodeClient(options)); return p; } @@ -171,7 +205,7 @@ describe('default protocols', () => { let nockProtocol = 'https'; const proxy = 'http://:3128'; - const agent = new HttpsProxyAgent(proxy); + const agent = HttpsProxyAgent(proxy); if (NODE_VERSION.major && NODE_VERSION.major < 9) { nockProtocol = 'http'; diff --git a/packages/node/test/integrations/linkederrors.test.ts b/packages/node/test/integrations/linkederrors.test.ts index 7dcaf077e4c6..390a847eea34 100644 --- a/packages/node/test/integrations/linkederrors.test.ts +++ b/packages/node/test/integrations/linkederrors.test.ts @@ -1,8 +1,9 @@ import { ExtendedError } from '@sentry/types'; -import { Event } from '../../src'; -import { NodeBackend } from '../../src/backend'; +import { Event, NodeClient } from '../../src'; import { LinkedErrors } from '../../src/integrations/linkederrors'; +import { defaultStackParser as stackParser } from '../../src/sdk'; +import { getDefaultNodeClientOptions } from '../helper/node-client-options'; let linkedErrors: any; @@ -18,7 +19,7 @@ describe('LinkedErrors', () => { const event = { message: 'foo', }; - return linkedErrors._handler(event).then((result: any) => { + return linkedErrors._handler(stackParser, event, {}).then((result: any) => { expect(spy.mock.calls.length).toEqual(0); expect(result).toEqual(event); }); @@ -28,13 +29,14 @@ describe('LinkedErrors', () => { expect.assertions(2); const spy = jest.spyOn(linkedErrors, '_walkErrorTree'); const one = new Error('originalException'); - const backend = new NodeBackend({}); + const options = getDefaultNodeClientOptions({ stackParser }); + const client = new NodeClient(options); let event: Event | undefined; - return backend + return client .eventFromException(one) .then(eventFromException => { event = eventFromException; - return linkedErrors._handler(eventFromException); + return linkedErrors._handler(stackParser, eventFromException, {}); }) .then(result => { expect(spy.mock.calls.length).toEqual(0); @@ -51,10 +53,11 @@ describe('LinkedErrors', () => { }), ); const one = new Error('originalException'); - const backend = new NodeBackend({}); - return backend.eventFromException(one).then(event => + const options = getDefaultNodeClientOptions({ stackParser }); + const client = new NodeClient(options); + return client.eventFromException(one).then(event => linkedErrors - ._handler(event, { + ._handler(stackParser, event, { originalException: one, }) .then((_: any) => { @@ -71,10 +74,11 @@ describe('LinkedErrors', () => { one.cause = two; two.cause = three; - const backend = new NodeBackend({}); - return backend.eventFromException(one).then(event => + const options = getDefaultNodeClientOptions({ stackParser }); + const client = new NodeClient(options); + return client.eventFromException(one).then(event => linkedErrors - ._handler(event, { + ._handler(stackParser, event, { originalException: one, }) .then((result: any) => { @@ -104,10 +108,11 @@ describe('LinkedErrors', () => { one.reason = two; two.reason = three; - const backend = new NodeBackend({}); - return backend.eventFromException(one).then(event => + const options = getDefaultNodeClientOptions({ stackParser }); + const client = new NodeClient(options); + return client.eventFromException(one).then(event => linkedErrors - ._handler(event, { + ._handler(stackParser, event, { originalException: one, }) .then((result: any) => { @@ -137,10 +142,11 @@ describe('LinkedErrors', () => { one.cause = two; two.cause = three; - const backend = new NodeBackend({}); - return backend.eventFromException(one).then(event => + const options = getDefaultNodeClientOptions({ stackParser }); + const client = new NodeClient(options); + return client.eventFromException(one).then(event => linkedErrors - ._handler(event, { + ._handler(stackParser, event, { originalException: one, }) .then((result: any) => { diff --git a/packages/node/test/manual/apm-transaction/main.js b/packages/node/test/manual/apm-transaction/main.js index e34088b4eaaf..0584177e8b10 100644 --- a/packages/node/test/manual/apm-transaction/main.js +++ b/packages/node/test/manual/apm-transaction/main.js @@ -1,7 +1,7 @@ const http = require('http'); const express = require('express'); const app = express(); -const Sentry = require('../../../build/dist'); +const Sentry = require('../../../build/cjs'); Sentry.init({ debug: true, diff --git a/packages/node/test/manual/colorize.js b/packages/node/test/manual/colorize.js new file mode 100644 index 000000000000..632d20aba4f7 --- /dev/null +++ b/packages/node/test/manual/colorize.js @@ -0,0 +1,16 @@ +const COLOR_RESET = '\x1b[0m'; +const COLORS = { + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', +}; + +function colorize(str, color) { + if (!(color in COLORS)) { + throw new Error(`Unknown color. Available colors: ${Object.keys(COLORS).join(', ')}`); + } + + return `${COLORS[color]}${str}${COLOR_RESET}`; +} + +module.exports = { colorize }; diff --git a/packages/node/test/manual/express-scope-separation/start.js b/packages/node/test/manual/express-scope-separation/start.js index 2e89fa14f4ec..81ca392a4407 100644 --- a/packages/node/test/manual/express-scope-separation/start.js +++ b/packages/node/test/manual/express-scope-separation/start.js @@ -1,36 +1,41 @@ const http = require('http'); const express = require('express'); const app = express(); -const Sentry = require('../../../build/dist'); +const Sentry = require('../../../build/cjs'); +const { colorize } = require('../colorize'); +const { TextEncoder } = require('util'); + +// don't log the test errors we're going to throw, so at a quick glance it doesn't look like the test itself has failed +global.console.error = () => null; function assertTags(actual, expected) { if (JSON.stringify(actual) !== JSON.stringify(expected)) { - console.error('FAILED: Scope contains incorrect tags'); + console.log(colorize('FAILED: Scope contains incorrect tags\n', 'red')); process.exit(1); } } let remaining = 3; -class DummyTransport { - sendEvent(event) { +function makeDummyTransport() { + return Sentry.createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, req => { --remaining; if (!remaining) { - console.error('SUCCESS: All scopes contain correct tags'); + console.log(colorize('PASSED: All scopes contain correct tags\n', 'green')); server.close(); process.exit(0); } return Promise.resolve({ - status: 'success', + statusCode: 200, }); - } + }); } Sentry.init({ dsn: 'http://test@example.com/1337', - transport: DummyTransport, + transport: makeDummyTransport, beforeSend(event) { if (event.transaction === 'GET /foo') { assertTags(event.tags, { diff --git a/packages/node/test/manual/memory-leak/context-memory.js b/packages/node/test/manual/memory-leak/context-memory.js index 9c63343c1fd8..6c124d542ce5 100644 --- a/packages/node/test/manual/memory-leak/context-memory.js +++ b/packages/node/test/manual/memory-leak/context-memory.js @@ -1,4 +1,4 @@ -const Sentry = require('../../../build/dist'); +const Sentry = require('../../../build/cjs'); Sentry.init({ dsn: 'https://public@app.getsentry.com/12345' }); diff --git a/packages/node/test/manual/memory-leak/express-patient.js b/packages/node/test/manual/memory-leak/express-patient.js index 90be8be575d4..7a677a442972 100644 --- a/packages/node/test/manual/memory-leak/express-patient.js +++ b/packages/node/test/manual/memory-leak/express-patient.js @@ -1,4 +1,4 @@ -const Sentry = require('../../../build/dist'); +const Sentry = require('../../../build/cjs'); Sentry.init({ dsn: 'https://public@app.getsentry.com/12345' }); diff --git a/packages/node/test/manual/release-health/runner.js b/packages/node/test/manual/release-health/runner.js index ecbcf7fe15bc..c8ecab182859 100644 --- a/packages/node/test/manual/release-health/runner.js +++ b/packages/node/test/manual/release-health/runner.js @@ -1,21 +1,7 @@ const fs = require('fs'); const path = require('path'); const { spawn } = require('child_process'); - -const COLOR_RESET = '\x1b[0m'; -const COLORS = { - green: '\x1b[32m', - red: '\x1b[31m', - yellow: '\x1b[33m', -}; - -const colorize = (str, color) => { - if (!(color in COLORS)) { - throw new Error(`Unknown color. Available colors: ${Object.keys(COLORS).join(', ')}`); - } - - return `${COLORS[color]}${str}${COLOR_RESET}`; -}; +const { colorize } = require('../colorize'); const scenariosDirs = ['session-aggregates', 'single-session']; const scenarios = []; diff --git a/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js b/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js index cedd795e9a57..300870d21fac 100644 --- a/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js +++ b/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js @@ -1,8 +1,9 @@ const http = require('http'); const express = require('express'); const app = express(); -const Sentry = require('../../../../build/dist'); -const { assertSessions, BaseDummyTransport } = require('../test-utils'); +const Sentry = require('../../../../build/cjs'); +const { assertSessions } = require('../test-utils'); +const { TextEncoder } = require('util'); function cleanUpAndExitSuccessfully() { server.close(); @@ -27,9 +28,13 @@ function assertSessionAggregates(session, expected) { assertSessions(session, expected); } -class DummyTransport extends BaseDummyTransport { - sendSession(session) { - assertSessionAggregates(session, { +function makeDummyTransport() { + return Sentry.createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, req => { + const sessionEnv = req.body + .split('\n') + .filter(l => !!l) + .map(e => JSON.parse(e)); + assertSessionAggregates(sessionEnv[2], { attrs: { release: '1.1' }, aggregates: [{ crashed: 2, errored: 1, exited: 1 }], }); @@ -37,15 +42,15 @@ class DummyTransport extends BaseDummyTransport { cleanUpAndExitSuccessfully(); return Promise.resolve({ - status: 'success', + statusCode: 200, }); - } + }); } Sentry.init({ dsn: 'http://test@example.com/1337', release: '1.1', - transport: DummyTransport, + transport: makeDummyTransport, autoSessionTracking: true, }); /** diff --git a/packages/node/test/manual/release-health/single-session/caught-exception-errored-session.js b/packages/node/test/manual/release-health/single-session/caught-exception-errored-session.js index 9f538a1ba993..0cecc8ee75e4 100644 --- a/packages/node/test/manual/release-health/single-session/caught-exception-errored-session.js +++ b/packages/node/test/manual/release-health/single-session/caught-exception-errored-session.js @@ -1,10 +1,6 @@ -const Sentry = require('../../../../build/dist'); -const { - assertSessions, - constructStrippedSessionObject, - BaseDummyTransport, - validateSessionCountFunction, -} = require('../test-utils'); +const Sentry = require('../../../../build/cjs'); +const { assertSessions, constructStrippedSessionObject, validateSessionCountFunction } = require('../test-utils'); +const { TextEncoder } = require('util'); const sessionCounts = { sessionCounter: 0, @@ -13,36 +9,46 @@ const sessionCounts = { validateSessionCountFunction(sessionCounts); -class DummyTransport extends BaseDummyTransport { - sendSession(session) { - sessionCounts.sessionCounter++; - - if (sessionCounts.sessionCounter === 1) { - assertSessions(constructStrippedSessionObject(session), { - init: true, - status: 'ok', - errors: 1, - release: '1.1', - }); - } +function makeDummyTransport() { + return Sentry.createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, req => { + const payload = req.body + .split('\n') + .filter(l => !!l) + .map(e => JSON.parse(e)); + const isSessionPayload = payload[1].type === 'session'; + + if (isSessionPayload) { + sessionCounts.sessionCounter++; + + if (sessionCounts.sessionCounter === 1) { + assertSessions(constructStrippedSessionObject(payload[2]), { + init: true, + status: 'ok', + errors: 1, + release: '1.1', + }); + } - if (sessionCounts.sessionCounter === 2) { - assertSessions(constructStrippedSessionObject(session), { - init: false, - status: 'exited', - errors: 1, - release: '1.1', - }); + if (sessionCounts.sessionCounter === 2) { + assertSessions(constructStrippedSessionObject(payload[2]), { + init: false, + status: 'exited', + errors: 1, + release: '1.1', + }); + } } - return super.sendSession(session); - } + return Promise.resolve({ + statusCode: 200, + }); + }); } Sentry.init({ dsn: 'http://test@example.com/1337', release: '1.1', - transport: DummyTransport, + transport: makeDummyTransport, autoSessionTracking: true, }); diff --git a/packages/node/test/manual/release-health/single-session/errors-in-session-capped-to-one.js b/packages/node/test/manual/release-health/single-session/errors-in-session-capped-to-one.js index 65c2bf05ef7f..18ec6fefdc78 100644 --- a/packages/node/test/manual/release-health/single-session/errors-in-session-capped-to-one.js +++ b/packages/node/test/manual/release-health/single-session/errors-in-session-capped-to-one.js @@ -1,10 +1,6 @@ -const Sentry = require('../../../../build/dist'); -const { - assertSessions, - constructStrippedSessionObject, - BaseDummyTransport, - validateSessionCountFunction, -} = require('../test-utils'); +const Sentry = require('../../../../build/cjs'); +const { assertSessions, constructStrippedSessionObject, validateSessionCountFunction } = require('../test-utils'); +const { TextEncoder } = require('util'); const sessionCounts = { sessionCounter: 0, @@ -13,36 +9,46 @@ const sessionCounts = { validateSessionCountFunction(sessionCounts); -class DummyTransport extends BaseDummyTransport { - sendSession(session) { - sessionCounts.sessionCounter++; - - if (sessionCounts.sessionCounter === 1) { - assertSessions(constructStrippedSessionObject(session), { - init: true, - status: 'ok', - errors: 1, - release: '1.1', - }); - } +function makeDummyTransport() { + return Sentry.createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, req => { + const payload = req.body + .split('\n') + .filter(l => !!l) + .map(e => JSON.parse(e)); + const isSessionPayload = payload[1].type === 'session'; + + if (isSessionPayload) { + sessionCounts.sessionCounter++; + + if (sessionCounts.sessionCounter === 1) { + assertSessions(constructStrippedSessionObject(payload[2]), { + init: true, + status: 'ok', + errors: 1, + release: '1.1', + }); + } - if (sessionCounts.sessionCounter === 2) { - assertSessions(constructStrippedSessionObject(session), { - init: false, - status: 'exited', - errors: 1, - release: '1.1', - }); + if (sessionCounts.sessionCounter === 2) { + assertSessions(constructStrippedSessionObject(payload[2]), { + init: false, + status: 'exited', + errors: 1, + release: '1.1', + }); + } } - return super.sendSession(session); - } + return Promise.resolve({ + statusCode: 200, + }); + }); } Sentry.init({ dsn: 'http://test@example.com/1337', release: '1.1', - transport: DummyTransport, + transport: makeDummyTransport, autoSessionTracking: true, }); /** diff --git a/packages/node/test/manual/release-health/single-session/healthy-session.js b/packages/node/test/manual/release-health/single-session/healthy-session.js index 05712ae7dcc2..11c3092dfcad 100644 --- a/packages/node/test/manual/release-health/single-session/healthy-session.js +++ b/packages/node/test/manual/release-health/single-session/healthy-session.js @@ -1,10 +1,6 @@ -const Sentry = require('../../../../build/dist'); -const { - assertSessions, - constructStrippedSessionObject, - BaseDummyTransport, - validateSessionCountFunction, -} = require('../test-utils'); +const Sentry = require('../../../../build/cjs'); +const { assertSessions, constructStrippedSessionObject, validateSessionCountFunction } = require('../test-utils'); +const { TextEncoder } = require('util'); const sessionCounts = { sessionCounter: 0, @@ -13,25 +9,32 @@ const sessionCounts = { validateSessionCountFunction(sessionCounts); -class DummyTransport extends BaseDummyTransport { - sendSession(session) { +function makeDummyTransport() { + return Sentry.createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, req => { sessionCounts.sessionCounter++; - assertSessions(constructStrippedSessionObject(session), { + const sessionEnv = req.body + .split('\n') + .filter(l => !!l) + .map(e => JSON.parse(e)); + + assertSessions(constructStrippedSessionObject(sessionEnv[2]), { init: true, status: 'exited', errors: 0, release: '1.1', }); - return super.sendSession(session); - } + return Promise.resolve({ + statusCode: 200, + }); + }); } Sentry.init({ dsn: 'http://test@example.com/1337', release: '1.1', - transport: DummyTransport, + transport: makeDummyTransport, autoSessionTracking: true, }); diff --git a/packages/node/test/manual/release-health/single-session/terminal-state-sessions-sent-once.js b/packages/node/test/manual/release-health/single-session/terminal-state-sessions-sent-once.js index 1fe58a972569..571af19d0f94 100644 --- a/packages/node/test/manual/release-health/single-session/terminal-state-sessions-sent-once.js +++ b/packages/node/test/manual/release-health/single-session/terminal-state-sessions-sent-once.js @@ -1,10 +1,6 @@ -const Sentry = require('../../../../build/dist'); -const { - assertSessions, - constructStrippedSessionObject, - BaseDummyTransport, - validateSessionCountFunction, -} = require('../test-utils'); +const Sentry = require('../../../../build/cjs'); +const { assertSessions, constructStrippedSessionObject, validateSessionCountFunction } = require('../test-utils'); +const { TextEncoder } = require('util'); const sessionCounts = { sessionCounter: 0, @@ -13,25 +9,35 @@ const sessionCounts = { validateSessionCountFunction(sessionCounts); -class DummyTransport extends BaseDummyTransport { - sendSession(session) { - sessionCounts.sessionCounter++; - - assertSessions(constructStrippedSessionObject(session), { - init: true, - status: 'crashed', - errors: 1, - release: '1.1', +function makeDummyTransport() { + return Sentry.createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, req => { + const payload = req.body + .split('\n') + .filter(l => !!l) + .map(e => JSON.parse(e)); + const isSessionPayload = payload[1].type === 'session'; + + if (isSessionPayload) { + sessionCounts.sessionCounter++; + + assertSessions(constructStrippedSessionObject(payload[2]), { + init: true, + status: 'crashed', + errors: 1, + release: '1.1', + }); + } + + return Promise.resolve({ + statusCode: 200, }); - - return super.sendSession(session); - } + }); } Sentry.init({ dsn: 'http://test@example.com/1337', release: '1.1', - transport: DummyTransport, + transport: makeDummyTransport, autoSessionTracking: true, }); diff --git a/packages/node/test/manual/release-health/single-session/uncaught-exception-crashed-session.js b/packages/node/test/manual/release-health/single-session/uncaught-exception-crashed-session.js index 8007b7d0ea4c..1759c1cc2d0f 100644 --- a/packages/node/test/manual/release-health/single-session/uncaught-exception-crashed-session.js +++ b/packages/node/test/manual/release-health/single-session/uncaught-exception-crashed-session.js @@ -1,24 +1,33 @@ -const Sentry = require('../../../../build/dist'); -const { assertSessions, constructStrippedSessionObject, BaseDummyTransport } = require('../test-utils'); +const Sentry = require('../../../../build/cjs'); +const { assertSessions, constructStrippedSessionObject } = require('../test-utils'); +const { TextEncoder } = require('util'); -class DummyTransport extends BaseDummyTransport { - sendSession(session) { - assertSessions(constructStrippedSessionObject(session), { - init: true, - status: 'crashed', - errors: 1, - release: '1.1', - }); +function makeDummyTransport() { + return Sentry.createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, req => { + if (req.category === 'session') { + sessionCounts.sessionCounter++; + const sessionEnv = req.body + .split('\n') + .filter(l => !!l) + .map(e => JSON.parse(e)); + + assertSessions(constructStrippedSessionObject(sessionEnv[2]), { + init: true, + status: 'crashed', + errors: 1, + release: '1.1', + }); + } // We need to explicitly exit process early here to allow for 0 exit code process.exit(0); - } + }); } Sentry.init({ dsn: 'http://test@example.com/1337', release: '1.1', - transport: DummyTransport, + transport: makeDummyTransport, autoSessionTracking: true, }); diff --git a/packages/node/test/manual/release-health/single-session/unhandled-rejection-crashed-session.js b/packages/node/test/manual/release-health/single-session/unhandled-rejection-crashed-session.js index f24c557c878a..cc8cf921c5d0 100644 --- a/packages/node/test/manual/release-health/single-session/unhandled-rejection-crashed-session.js +++ b/packages/node/test/manual/release-health/single-session/unhandled-rejection-crashed-session.js @@ -1,10 +1,6 @@ -const Sentry = require('../../../../build/dist'); -const { - assertSessions, - constructStrippedSessionObject, - BaseDummyTransport, - validateSessionCountFunction, -} = require('../test-utils'); +const Sentry = require('../../../../build/cjs'); +const { assertSessions, constructStrippedSessionObject, validateSessionCountFunction } = require('../test-utils'); +const { TextEncoder } = require('util'); const sessionCounts = { sessionCounter: 0, @@ -13,25 +9,35 @@ const sessionCounts = { validateSessionCountFunction(sessionCounts); -class DummyTransport extends BaseDummyTransport { - sendSession(session) { - sessionCounts.sessionCounter++; +function makeDummyTransport() { + return Sentry.createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, req => { + const payload = req.body + .split('\n') + .filter(l => !!l) + .map(e => JSON.parse(e)); + const isSessionPayload = payload[1].type === 'session'; - assertSessions(constructStrippedSessionObject(session), { - init: true, - status: 'crashed', - errors: 1, - release: '1.1', - }); + if (isSessionPayload) { + sessionCounts.sessionCounter++; + + assertSessions(constructStrippedSessionObject(payload[2]), { + init: true, + status: 'crashed', + errors: 1, + release: '1.1', + }); + } - return super.sendSession(session); - } + return Promise.resolve({ + statusCode: 200, + }); + }); } Sentry.init({ dsn: 'http://test@example.com/1337', release: '1.1', - transport: DummyTransport, + transport: makeDummyTransport, autoSessionTracking: true, }); diff --git a/packages/node/test/manual/release-health/test-utils.js b/packages/node/test/manual/release-health/test-utils.js index 668923e74dd3..2b3f88a440b9 100644 --- a/packages/node/test/manual/release-health/test-utils.js +++ b/packages/node/test/manual/release-health/test-utils.js @@ -8,26 +8,10 @@ function assertSessions(actual, expected) { } function constructStrippedSessionObject(actual) { - const { init, status, errors, release, did } = actual; + const { init, status, errors, attrs: { release }, did } = actual; return { init, status, errors, release, did }; } -class BaseDummyTransport { - sendEvent(event) { - return Promise.resolve({ - status: 'success', - }); - } - sendSession(session) { - return Promise.resolve({ - status: 'success', - }); - } - close(timeout) { - return Promise.resolve(true); - } -} - function validateSessionCountFunction(sessionCounts) { process.on('exit', () => { const { sessionCounter, expectedSessions } = sessionCounts; @@ -38,4 +22,4 @@ function validateSessionCountFunction(sessionCounts) { }); } -module.exports = { assertSessions, constructStrippedSessionObject, BaseDummyTransport, validateSessionCountFunction }; +module.exports = { assertSessions, constructStrippedSessionObject, validateSessionCountFunction }; diff --git a/packages/node/test/manual/webpack-domain/.gitignore b/packages/node/test/manual/webpack-domain/.gitignore deleted file mode 100644 index 1521c8b7652b..000000000000 --- a/packages/node/test/manual/webpack-domain/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist diff --git a/packages/node/test/manual/webpack-domain/index.js b/packages/node/test/manual/webpack-domain/index.js index e0479e0fec78..001ba1fa8c78 100644 --- a/packages/node/test/manual/webpack-domain/index.js +++ b/packages/node/test/manual/webpack-domain/index.js @@ -1,35 +1,37 @@ -const Sentry = require('../../../build/dist'); +const Sentry = require('../../../build/cjs'); +const { colorize } = require('../colorize'); +const { TextEncoder } = require('util'); let remaining = 2; -class DummyTransport { - sendEvent(event) { +function makeDummyTransport() { + return Sentry.createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, req => { --remaining; if (!remaining) { - console.error('SUCCESS: Webpack Node Domain test OK!'); + console.log(colorize('PASSED: Webpack Node Domain test OK!\n', 'green')); process.exit(0); } return Promise.resolve({ status: 'success', }); - } + }); } Sentry.init({ dsn: 'https://a@example.com/1', - transport: DummyTransport, + transport: makeDummyTransport, beforeSend(event) { if (event.message === 'inside') { if (event.tags.a !== 'x' && event.tags.b !== 'c') { - console.error('FAILED: Scope contains incorrect tags'); + console.log(colorize('FAILED: Scope contains incorrect tags\n', 'red')); process.exit(1); } } if (event.message === 'outside') { if (event.tags.a !== 'b') { - console.error('FAILED: Scope contains incorrect tags'); + console.log(colorize('FAILED: Scope contains incorrect tags\n', 'red')); process.exit(1); } } diff --git a/packages/node/test/manual/webpack-domain/npm-build.js b/packages/node/test/manual/webpack-domain/npm-build.js index 923d437212c5..03db46e84955 100644 --- a/packages/node/test/manual/webpack-domain/npm-build.js +++ b/packages/node/test/manual/webpack-domain/npm-build.js @@ -2,6 +2,11 @@ const path = require('path'); const webpack = require('webpack'); const { execSync } = require('child_process'); +// Webpack test does not work in Node 18 and above. +if (Number(process.versions.node.split('.')[0]) >= 18) { + return; +} + // prettier-ignore webpack( { @@ -13,7 +18,7 @@ webpack( target: 'node', mode: 'development', }, - function(err, stats) { + function (err, stats) { if (err) { console.error(err.stack || err); if (err.details) { @@ -39,7 +44,7 @@ webpack( function runTests() { try { - execSync('node ' + path.resolve(__dirname, 'dist', 'bundle.js')); + execSync('node ' + path.resolve(__dirname, 'dist', 'bundle.js'), { stdio: 'inherit' }); } catch (_) { process.exit(1); } diff --git a/packages/node/test/onunhandledrejection.test.ts b/packages/node/test/onunhandledrejection.test.ts index 5d8d498079e5..012e6b37643f 100644 --- a/packages/node/test/onunhandledrejection.test.ts +++ b/packages/node/test/onunhandledrejection.test.ts @@ -1,8 +1,11 @@ -import { Scope } from '@sentry/core'; import { Hub } from '@sentry/hub'; import { OnUnhandledRejection } from '../src/integrations/onunhandledrejection'; +// don't log the test errors we're going to throw, so at a quick glance it doesn't look like the test itself has failed +global.console.warn = () => null; +global.console.error = () => null; + jest.mock('@sentry/hub', () => { // we just want to short-circuit it, so dont worry about types const original = jest.requireActual('@sentry/hub'); @@ -35,10 +38,6 @@ describe('unhandled promises', () => { }; const captureException = jest.spyOn(Hub.prototype, 'captureException'); - const setUser = jest.spyOn(Scope.prototype, 'setUser'); - const setExtra = jest.spyOn(Scope.prototype, 'setExtra'); - const setExtras = jest.spyOn(Scope.prototype, 'setExtras'); - const setTags = jest.spyOn(Scope.prototype, 'setTags'); integration.sendUnhandledPromise('bla', promise); @@ -46,10 +45,5 @@ describe('unhandled promises', () => { mechanism: { handled: false, type: 'onunhandledrejection' }, }); expect(captureException.mock.calls[0][0]).toBe('bla'); - expect(setUser.mock.calls[0][0]).toEqual({ id: 1 }); - expect(setExtra.mock.calls[0]).toEqual(['unhandledPromiseRejection', true]); - - expect(setExtras.mock.calls[0]).toEqual([{ extra: '1' }]); - expect(setTags.mock.calls[0]).toEqual([{ tag: '2' }]); }); }); diff --git a/packages/node/test/sdk.test.ts b/packages/node/test/sdk.test.ts new file mode 100644 index 000000000000..48e5accee439 --- /dev/null +++ b/packages/node/test/sdk.test.ts @@ -0,0 +1,92 @@ +import { Integration } from '@sentry/types'; + +import { init } from '../src/sdk'; +import * as sdk from '../src/sdk'; + +// eslint-disable-next-line no-var +declare var global: any; + +const PUBLIC_DSN = 'https://username@domain/123'; + +class MockIntegration implements Integration { + public name: string; + public setupOnce: jest.Mock = jest.fn(); + public constructor(name: string) { + this.name = name; + } +} + +const defaultIntegrationsBackup = sdk.defaultIntegrations; + +describe('init()', () => { + beforeEach(() => { + global.__SENTRY__ = {}; + }); + + afterEach(() => { + // @ts-ignore - Reset the default integrations of node sdk to original + sdk.defaultIntegrations = defaultIntegrationsBackup; + }); + + it("doesn't install default integrations if told not to", () => { + const mockDefaultIntegrations = [ + new MockIntegration('Mock integration 1.1'), + new MockIntegration('Mock integration 1.2'), + ]; + + // @ts-ignore - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly + sdk.defaultIntegrations = mockDefaultIntegrations; + + init({ dsn: PUBLIC_DSN, defaultIntegrations: false }); + + expect(mockDefaultIntegrations[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + expect(mockDefaultIntegrations[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + }); + + it('installs merged default integrations, with overrides provided through options', () => { + const mockDefaultIntegrations = [ + new MockIntegration('Some mock integration 2.1'), + new MockIntegration('Some mock integration 2.2'), + ]; + + // @ts-ignore - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly + sdk.defaultIntegrations = mockDefaultIntegrations; + + const mockIntegrations = [ + new MockIntegration('Some mock integration 2.1'), + new MockIntegration('Some mock integration 2.3'), + ]; + + init({ dsn: PUBLIC_DSN, integrations: mockIntegrations }); + + expect(mockDefaultIntegrations[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + expect(mockDefaultIntegrations[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(mockIntegrations[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(mockIntegrations[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + }); + + it('installs integrations returned from a callback function', () => { + const mockDefaultIntegrations = [ + new MockIntegration('Some mock integration 3.1'), + new MockIntegration('Some mock integration 3.2'), + ]; + + // @ts-ignore - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly + sdk.defaultIntegrations = mockDefaultIntegrations; + + const newIntegration = new MockIntegration('Some mock integration 3.3'); + + init({ + dsn: PUBLIC_DSN, + integrations: integrations => { + const newIntegrations = [...integrations]; + newIntegrations[1] = newIntegration; + return newIntegrations; + }, + }); + + expect(mockDefaultIntegrations[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(mockDefaultIntegrations[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + expect(newIntegration.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/node/test/stacktrace.test.ts b/packages/node/test/stacktrace.test.ts index 656ba1a69a9b..e84c436364ed 100644 --- a/packages/node/test/stacktrace.test.ts +++ b/packages/node/test/stacktrace.test.ts @@ -11,6 +11,7 @@ */ import { parseStackFrames } from '../src/eventbuilder'; +import { defaultStackParser as stackParser } from '../src/sdk'; function testBasic() { return new Error('something went wrong'); @@ -26,17 +27,17 @@ function evalWrapper() { describe('Stack parsing', () => { test('test basic error', () => { - const frames = parseStackFrames(testBasic()); + const frames = parseStackFrames(stackParser, testBasic()); const last = frames.length - 1; expect(frames[last].filename).toEqual(__filename); expect(frames[last].function).toEqual('testBasic'); - expect(frames[last].lineno).toEqual(16); + expect(frames[last].lineno).toEqual(17); expect(frames[last].colno).toEqual(10); }); test('test error with wrapper', () => { - const frames = parseStackFrames(testWrapper()); + const frames = parseStackFrames(stackParser, testWrapper()); const last = frames.length - 1; expect(frames[last].function).toEqual('testBasic'); @@ -44,7 +45,7 @@ describe('Stack parsing', () => { }); test('test error with eval wrapper', () => { - const frames = parseStackFrames(evalWrapper()); + const frames = parseStackFrames(stackParser, evalWrapper()); const last = frames.length - 1; expect(frames[last].function).toEqual('testBasic'); @@ -59,7 +60,7 @@ describe('Stack parsing', () => { ' at [object Object].global.every [as _onTimeout] (/Users/hoitz/develop/test.coffee:36:3)\n' + ' at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)\n'; - const frames = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { @@ -83,7 +84,7 @@ describe('Stack parsing', () => { test('parses undefined stack', () => { const err = { stack: undefined }; - const trace = parseStackFrames(err as Error); + const trace = parseStackFrames(stackParser, err as Error); expect(trace).toEqual([]); }); @@ -97,7 +98,7 @@ describe('Stack parsing', () => { 'oh no' + ' at TestCase.run (/Users/felix/code/node-fast-or-slow/lib/test_case.js:61:8)\n'; - const frames = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { @@ -126,7 +127,7 @@ describe('Stack parsing', () => { ' at Test.fn (/Users/felix/code/node-fast-or-slow/test/fast/example/test-example.js:6)\n' + ' at Test.run (/Users/felix/code/node-fast-or-slow/lib/test.js:45)'; - const frames = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { @@ -157,7 +158,7 @@ describe('Stack parsing', () => { ' at Array.0 (native)\n' + ' at EventEmitter._tickCallback (node.js:126:26)'; - const frames = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { @@ -212,7 +213,7 @@ describe('Stack parsing', () => { const err = new Error(); err.stack = 'AssertionError: true == false\n' + ' at /Users/felix/code/node-fast-or-slow/lib/test_case.js:80:10'; - const frames = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { @@ -232,7 +233,7 @@ describe('Stack parsing', () => { 'AssertionError: true == false\nAnd some more shit\n' + ' at /Users/felix/code/node-fast-or-slow/lib/test_case.js:80:10'; - const frames = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { @@ -252,7 +253,7 @@ describe('Stack parsing', () => { 'AssertionError: expected [] to be arguments\n' + ' at Assertion.prop.(anonymous function) (/Users/den/Projects/should.js/lib/should.js:60:14)\n'; - const frames = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { @@ -273,7 +274,7 @@ describe('Stack parsing', () => { ' at Test.run (/Users/felix (something)/code/node-fast-or-slow/lib/test.js:45:10)\n' + ' at TestCase.run (/Users/felix (something)/code/node-fast-or-slow/lib/test_case.js:61:8)\n'; - const frames = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { @@ -309,7 +310,7 @@ describe('Stack parsing', () => { ' at async onBatch (/code/node_modules/kafkajs/src/consumer/runner.js:326:9)\n' + ' at async /code/node_modules/kafkajs/src/consumer/runner.js:376:15\n'; - const frames = parseStackFrames(err); + const frames = parseStackFrames(stackParser, err); expect(frames).toEqual([ { diff --git a/packages/node/test/transports/custom/index.test.ts b/packages/node/test/transports/custom/index.test.ts deleted file mode 100644 index 88295bbc7e9b..000000000000 --- a/packages/node/test/transports/custom/index.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { CustomUrlTransport } from './transports'; - -describe('Custom transport', () => { - describe('URL parser support', () => { - const noop = () => null; - const sampleDsn = 'https://username@sentry.tld/path/1'; - - test('use URL parser for sendEvent() method', async () => { - const urlParser = jest.fn(); - const transport = new CustomUrlTransport({ dsn: sampleDsn }, urlParser); - await transport.sendEvent({}).catch(noop); - - expect(urlParser).toHaveBeenCalled(); - }); - - test('use URL parser for sendSession() method', async () => { - const urlParser = jest.fn(); - const transport = new CustomUrlTransport({ dsn: sampleDsn }, urlParser); - await transport.sendSession({ aggregates: [] }).then(noop, noop); - - expect(urlParser).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/node/test/transports/custom/transports.ts b/packages/node/test/transports/custom/transports.ts deleted file mode 100644 index e3305fbf8a20..000000000000 --- a/packages/node/test/transports/custom/transports.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { TransportOptions } from '@sentry/types'; - -import { HTTPTransport } from '../../../src/transports'; -import { UrlParser } from '../../../src/transports/base'; - -export class CustomUrlTransport extends HTTPTransport { - public constructor(public options: TransportOptions, urlParser: UrlParser) { - super(options); - this.urlParser = urlParser; - } -} diff --git a/packages/node/test/transports/http.test.ts b/packages/node/test/transports/http.test.ts index e2e837fee2d3..789baaf871ab 100644 --- a/packages/node/test/transports/http.test.ts +++ b/packages/node/test/transports/http.test.ts @@ -1,710 +1,339 @@ -import { Session } from '@sentry/hub'; -import { Event, SessionAggregates, TransportOptions } from '@sentry/types'; -import { SentryError } from '@sentry/utils'; +import { createTransport } from '@sentry/core'; +import { EventEnvelope, EventItem } from '@sentry/types'; +import { createEnvelope, serializeEnvelope } from '@sentry/utils'; import * as http from 'http'; -import * as HttpsProxyAgent from 'https-proxy-agent'; +import { TextEncoder } from 'util'; -import { HTTPTransport } from '../../src/transports/http'; +import { makeNodeTransport } from '../../src/transports'; -const mockSetEncoding = jest.fn(); -const dsn = 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622'; -const storePath = '/mysubpath/api/50622/store/'; -const envelopePath = '/mysubpath/api/50622/envelope/'; -const tunnel = 'https://hello.com/world'; -const eventPayload: Event = { - event_id: '1337', -}; -const transactionPayload: Event = { - event_id: '42', - type: 'transaction', -}; -const sessionPayload: Session = { - environment: 'test', - release: '1.0', - sid: '353463243253453254', - errors: 0, - started: Date.now(), - timestamp: Date.now(), - init: true, - duration: 0, - status: 'exited', - update: jest.fn(), - close: jest.fn(), - toJSON: jest.fn(), -}; -const sessionsPayload: SessionAggregates = { - attrs: { environment: 'test', release: '1.0' }, - aggregates: [{ started: '2021-03-17T16:00:00.000Z', exited: 1 }], -}; -let mockReturnCode = 200; -let mockHeaders = {}; - -function createTransport(options: TransportOptions): HTTPTransport { - const transport = new HTTPTransport(options); - transport.module = { - request: jest.fn().mockImplementation((_options: any, callback: any) => ({ - end: () => { - callback({ - headers: mockHeaders, - setEncoding: mockSetEncoding, - statusCode: mockReturnCode, - }); - }, - on: jest.fn(), - })), +jest.mock('@sentry/core', () => { + const actualCore = jest.requireActual('@sentry/core'); + return { + ...actualCore, + createTransport: jest.fn().mockImplementation(actualCore.createTransport), }; - return transport; -} +}); -function assertBasicOptions(options: any, useEnvelope: boolean = false): void { - expect(options.headers['X-Sentry-Auth']).toContain('sentry_version'); - expect(options.headers['X-Sentry-Auth']).toContain('sentry_client'); - expect(options.headers['X-Sentry-Auth']).toContain('sentry_key'); - expect(options.port).toEqual('8989'); - expect(options.path).toEqual(useEnvelope ? envelopePath : storePath); - expect(options.hostname).toEqual('sentry.io'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const httpProxyAgent = require('https-proxy-agent'); +jest.mock('https-proxy-agent', () => { + return jest.fn().mockImplementation(() => new http.Agent({ keepAlive: false, maxSockets: 30, timeout: 2000 })); +}); + +const SUCCESS = 200; +const RATE_LIMIT = 429; +const INVALID = 400; +const FAILED = 500; + +interface TestServerOptions { + statusCode: number; + responseHeaders?: Record; } -describe('HTTPTransport', () => { - beforeEach(() => { - mockReturnCode = 200; - mockHeaders = {}; - jest.clearAllMocks(); - }); +let testServer: http.Server | undefined; - test('send 200', async () => { - const transport = createTransport({ dsn }); - await transport.sendEvent({ - message: 'test', +function setupTestServer( + options: TestServerOptions, + requestInspector?: (req: http.IncomingMessage, body: string) => void, +) { + testServer = http.createServer((req, res) => { + let body = ''; + + req.on('data', data => { + body += data; }); - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions); - expect(mockSetEncoding).toHaveBeenCalled(); - }); + req.on('end', () => { + requestInspector?.(req, body); + }); - test('send 400', async () => { - mockReturnCode = 400; - const transport = createTransport({ dsn }); + res.writeHead(options.statusCode, options.responseHeaders); + res.end(); - try { - await transport.sendEvent({ - message: 'test', - }); - } catch (e) { - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions); - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`)); - } + // also terminate socket because keepalive hangs connection a bit + res.connection.end(); }); - test('send 200 session', async () => { - const transport = createTransport({ dsn }); - await transport.sendSession(new Session()); + testServer.listen(18099); - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions, true); - expect(mockSetEncoding).toHaveBeenCalled(); + return new Promise(resolve => { + testServer?.on('listening', resolve); }); +} - test('send 400 session', async () => { - mockReturnCode = 400; - const transport = createTransport({ dsn }); +const TEST_SERVER_URL = 'http://localhost:18099'; - try { - await transport.sendSession(new Session()); - } catch (e) { - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions, true); - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`)); - } - }); +const EVENT_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ + [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, +]); - test('send 200 request mode sessions', async () => { - const transport = createTransport({ dsn }); - await transport.sendSession(sessionsPayload); +const SERIALIZED_EVENT_ENVELOPE = serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()); - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions, true); - expect(mockSetEncoding).toHaveBeenCalled(); - }); +const defaultOptions = { + url: TEST_SERVER_URL, + recordDroppedEvent: () => undefined, + textEncoder: new TextEncoder(), +}; - test('send 400 request mode session', async () => { - mockReturnCode = 400; - const transport = createTransport({ dsn }); +describe('makeNewHttpTransport()', () => { + afterEach(() => { + jest.clearAllMocks(); - try { - await transport.sendSession(sessionsPayload); - } catch (e) { - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions, true); - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`)); + if (testServer) { + testServer.close(); } }); - test('send x-sentry-error header', async () => { - mockReturnCode = 429; - mockHeaders = { - 'x-sentry-error': 'test-failed', - }; - const transport = createTransport({ dsn }); - - try { - await transport.sendEvent({ - message: 'test', + describe('.send()', () => { + it('should correctly send envelope to server', async () => { + await setupTestServer({ statusCode: SUCCESS }, (req, body) => { + expect(req.method).toBe('POST'); + expect(body).toBe(SERIALIZED_EVENT_ENVELOPE); }); - } catch (e) { - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions); - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode}): test-failed`)); - } - }); - test('sends a request to tunnel if configured', async () => { - const transport = createTransport({ dsn, tunnel }); - - await transport.sendEvent({ - message: 'test', + const transport = makeNodeTransport(defaultOptions); + await transport.send(EVENT_ENVELOPE); }); - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - expect(requestOptions.protocol).toEqual('https:'); - expect(requestOptions.hostname).toEqual('hello.com'); - expect(requestOptions.path).toEqual('/world'); - }); - - test('back-off using retry-after header', async () => { - const retryAfterSeconds = 10; - mockReturnCode = 429; - mockHeaders = { - 'retry-after': retryAfterSeconds, - }; - const transport = createTransport({ dsn }); - - const now = Date.now(); - const mock = jest - .spyOn(Date, 'now') - // Check for first event - .mockReturnValueOnce(now) - // Setting disabledUntil - .mockReturnValueOnce(now) - // Check for second event - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // Check for third event - .mockReturnValueOnce(now + retryAfterSeconds * 1000); - - try { - await transport.sendEvent({ message: 'test' }); - } catch (e) { - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`)); - } - - try { - await transport.sendEvent({ message: 'test' }); - } catch (e) { - expect(e.status).toEqual(429); - expect(e.reason).toEqual( - `Transport for event requests locked till ${new Date( - now + retryAfterSeconds * 1000, - )} due to too many requests.`, - ); - expect(e.payload.message).toEqual('test'); - expect(e.type).toEqual('event'); - } - - try { - await transport.sendEvent({ message: 'test' }); - } catch (e) { - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`)); - } + it('should correctly send user-provided headers to server', async () => { + await setupTestServer({ statusCode: SUCCESS }, req => { + expect(req.headers).toEqual( + expect.objectContaining({ + // node http module lower-cases incoming headers + 'x-some-custom-header-1': 'value1', + 'x-some-custom-header-2': 'value2', + }), + ); + }); - mock.mockRestore(); - }); + const transport = makeNodeTransport({ + ...defaultOptions, + headers: { + 'X-Some-Custom-Header-1': 'value1', + 'X-Some-Custom-Header-2': 'value2', + }, + }); - test('back-off using x-sentry-rate-limits with bogus headers and missing categories should just lock them all', async () => { - const retryAfterSeconds = 60; - mockReturnCode = 429; - mockHeaders = { - 'x-sentry-rate-limits': 'sgthrthewhertht', - }; - const transport = createTransport({ dsn }); - const now = Date.now(); - const mock = jest - .spyOn(Date, 'now') - // 1st event - _isRateLimited - false - .mockReturnValueOnce(now) - // 1st event - _handleRateLimit - .mockReturnValueOnce(now) - // 2nd event - _isRateLimited - true (event category) - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // 3rd event - _isRateLimited - true (transaction category) - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // 4th event - _isRateLimited - false (event category) - .mockReturnValueOnce(now + retryAfterSeconds * 1000) - // 4th event - _handleRateLimit - .mockReturnValueOnce(now + retryAfterSeconds * 1000) - // 5th event - _isRateLimited - false (transaction category) - .mockReturnValueOnce(now + retryAfterSeconds * 1000) - // 5th event - _handleRateLimit - .mockReturnValueOnce(now + retryAfterSeconds * 1000); - - try { - await transport.sendEvent(eventPayload); - } catch (e) { - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`)); - } + await transport.send(EVENT_ENVELOPE); + }); - try { - await transport.sendEvent(eventPayload); - } catch (e) { - expect(e.status).toEqual(429); - expect(e.reason).toEqual( - `Transport for event requests locked till ${new Date( - now + retryAfterSeconds * 1000, - )} due to too many requests.`, - ); - expect(e.payload).toEqual(eventPayload); - expect(e.type).toEqual('event'); - } + it.each([RATE_LIMIT, INVALID, FAILED])( + 'should resolve on bad server response (status %i)', + async serverStatusCode => { + await setupTestServer({ statusCode: serverStatusCode }); - try { - await transport.sendEvent(transactionPayload); - } catch (e) { - expect(e.status).toEqual(429); - expect(e.reason).toEqual( - `Transport for transaction requests locked till ${new Date( - now + retryAfterSeconds * 1000, - )} due to too many requests.`, - ); - expect(e.payload).toEqual(transactionPayload); - expect(e.type).toEqual('transaction'); - } + const transport = makeNodeTransport(defaultOptions); - mockHeaders = {}; - mockReturnCode = 200; + await expect(transport.send(EVENT_ENVELOPE)).resolves.toBeUndefined(); + }, + ); + + it('should resolve when server responds with rate limit header and status code 200', async () => { + await setupTestServer({ + statusCode: SUCCESS, + responseHeaders: { + 'Retry-After': '2700', + 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', + }, + }); - const eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toEqual('success'); + const transport = makeNodeTransport(defaultOptions); + await expect(transport.send(EVENT_ENVELOPE)).resolves.toBeUndefined(); + }); - const transactionRes = await transport.sendEvent(transactionPayload); - expect(transactionRes.status).toEqual('success'); + it('should resolve when server responds with rate limit header and status code 200', async () => { + await setupTestServer({ + statusCode: SUCCESS, + responseHeaders: { + 'Retry-After': '2700', + 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', + }, + }); - mock.mockRestore(); + const transport = makeNodeTransport(defaultOptions); + await transport.send(EVENT_ENVELOPE); + }); }); - test('back-off using x-sentry-rate-limits with single category', async () => { - const retryAfterSeconds = 10; - mockReturnCode = 429; - mockHeaders = { - 'x-sentry-rate-limits': `${retryAfterSeconds}:error:scope`, - }; - const transport = createTransport({ dsn }); - const now = Date.now(); - const mock = jest - .spyOn(Date, 'now') - // 1st event - _isRateLimited - false - .mockReturnValueOnce(now) - // 1st event - _handleRateLimit - .mockReturnValueOnce(now) - // 2nd event - _isRateLimited - false (different category) - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // 2nd event - _handleRateLimit - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // 3rd event - _isRateLimited - false (different category - sessions) - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // 3rd event - _handleRateLimit - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // 4th event - _isRateLimited - true - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // 5th event - _isRateLimited - false - .mockReturnValueOnce(now + retryAfterSeconds * 1000) - // 5th event - _handleRateLimit - .mockReturnValueOnce(now + retryAfterSeconds * 1000); - - try { - await transport.sendEvent(eventPayload); - } catch (e) { - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`)); - } - - mockHeaders = {}; - mockReturnCode = 200; - - const transactionRes = await transport.sendEvent(transactionPayload); - expect(transactionRes.status).toEqual('success'); - - const sessionsRes = await transport.sendSession(sessionPayload); - expect(sessionsRes.status).toEqual('success'); - - try { - await transport.sendEvent(eventPayload); - } catch (e) { - expect(e.status).toEqual(429); - expect(e.reason).toEqual( - `Transport for event requests locked till ${new Date( - now + retryAfterSeconds * 1000, - )} due to too many requests.`, - ); - expect(e.payload).toEqual(eventPayload); - expect(e.type).toEqual('event'); - } - - const eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toEqual('success'); - - mock.mockRestore(); - }); + describe('proxy', () => { + it('can be configured through option', () => { + makeNodeTransport({ + ...defaultOptions, + url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', + proxy: 'http://example.com', + }); - test('back-off using x-sentry-rate-limits with multiple category', async () => { - const retryAfterSeconds = 10; - mockReturnCode = 429; - mockHeaders = { - 'x-sentry-rate-limits': `${retryAfterSeconds}:error;transaction;session:scope`, - }; - const transport = createTransport({ dsn }); - const now = Date.now(); - const mock = jest - .spyOn(Date, 'now') - // 1st event - _isRateLimited - false - .mockReturnValueOnce(now) - // 1st event - _handleRateLimit - .mockReturnValueOnce(now) - // 2nd event - _isRateLimited - true (event category) - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // 3rd event - _isRateLimited - true (sessions category) - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // 4th event - _isRateLimited - true (transactions category) - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // 5th event - _isRateLimited - false (event category) - .mockReturnValueOnce(now + retryAfterSeconds * 1000) - // 5th event - _handleRateLimit - .mockReturnValueOnce(now + retryAfterSeconds * 1000) - // 6th event - _isRateLimited - false (sessions category) - .mockReturnValueOnce(now + retryAfterSeconds * 1000) - // 6th event - handleRateLimit - .mockReturnValueOnce(now + retryAfterSeconds * 1000) - // 7th event - _isRateLimited - false (transaction category) - .mockReturnValueOnce(now + retryAfterSeconds * 1000) - // 7th event - handleRateLimit - .mockReturnValueOnce(now + retryAfterSeconds * 1000); - - try { - await transport.sendEvent(eventPayload); - } catch (e) { - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`)); - } + expect(httpProxyAgent).toHaveBeenCalledTimes(1); + expect(httpProxyAgent).toHaveBeenCalledWith('http://example.com'); + }); - try { - await transport.sendEvent(eventPayload); - } catch (e) { - expect(e.status).toEqual(429); - expect(e.reason).toEqual( - `Transport for event requests locked till ${new Date( - now + retryAfterSeconds * 1000, - )} due to too many requests.`, - ); - expect(e.payload).toEqual(eventPayload); - expect(e.type).toEqual('event'); - } + it('can be configured through env variables option', () => { + process.env.http_proxy = 'http://example.com'; + makeNodeTransport({ + ...defaultOptions, + url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', + }); - try { - await transport.sendSession(sessionPayload); - } catch (e) { - expect(e.status).toEqual(429); - expect(e.reason).toEqual( - `Transport for session requests locked till ${new Date( - now + retryAfterSeconds * 1000, - )} due to too many requests.`, - ); - expect(e.payload.environment).toEqual(sessionPayload.environment); - expect(e.payload.release).toEqual(sessionPayload.release); - expect(e.payload.sid).toEqual(sessionPayload.sid); - expect(e.type).toEqual('session'); - } + expect(httpProxyAgent).toHaveBeenCalledTimes(1); + expect(httpProxyAgent).toHaveBeenCalledWith('http://example.com'); + delete process.env.http_proxy; + }); - try { - await transport.sendEvent(transactionPayload); - } catch (e) { - expect(e.status).toEqual(429); - expect(e.reason).toEqual( - `Transport for transaction requests locked till ${new Date( - now + retryAfterSeconds * 1000, - )} due to too many requests.`, - ); - expect(e.payload).toEqual(transactionPayload); - expect(e.type).toEqual('transaction'); - } + it('client options have priority over env variables', () => { + process.env.http_proxy = 'http://foo.com'; + makeNodeTransport({ + ...defaultOptions, + url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', + proxy: 'http://bar.com', + }); - mockHeaders = {}; - mockReturnCode = 200; + expect(httpProxyAgent).toHaveBeenCalledTimes(1); + expect(httpProxyAgent).toHaveBeenCalledWith('http://bar.com'); + delete process.env.http_proxy; + }); - const eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toEqual('success'); + it('no_proxy allows for skipping specific hosts', () => { + process.env.no_proxy = 'sentry.io'; + makeNodeTransport({ + ...defaultOptions, + url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', + proxy: 'http://example.com', + }); - const sessionsRes = await transport.sendSession(sessionPayload); - expect(sessionsRes.status).toEqual('success'); + expect(httpProxyAgent).not.toHaveBeenCalled(); - const transactionRes = await transport.sendEvent(transactionPayload); - expect(transactionRes.status).toEqual('success'); + delete process.env.no_proxy; + }); - mock.mockRestore(); - }); + it('no_proxy works with a port', () => { + process.env.http_proxy = 'http://example.com:8080'; + process.env.no_proxy = 'sentry.io:8989'; - test('back-off using x-sentry-rate-limits with missing categories should lock them all', async () => { - const retryAfterSeconds = 10; - mockReturnCode = 429; - mockHeaders = { - 'x-sentry-rate-limits': `${retryAfterSeconds}::scope`, - }; - const transport = createTransport({ dsn }); - const now = Date.now(); - const mock = jest - .spyOn(Date, 'now') - // 1st event - _isRateLimited - false - .mockReturnValueOnce(now) - // 1st event - _handleRateLimit - .mockReturnValueOnce(now) - // 2nd event - _isRateLimited - true (event category) - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // 3rd event - _isRateLimited - true (transaction category) - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // 4th event - _isRateLimited - false (event category) - .mockReturnValueOnce(now + retryAfterSeconds * 1000) - // 4th event - _handleRateLimit - .mockReturnValueOnce(now + retryAfterSeconds * 1000) - // 5th event - _isRateLimited - false (transaction category) - .mockReturnValueOnce(now + retryAfterSeconds * 1000) - // 5th event - _handleRateLimit - .mockReturnValueOnce(now + retryAfterSeconds * 1000); - - try { - await transport.sendEvent(eventPayload); - } catch (e) { - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`)); - } + makeNodeTransport({ + ...defaultOptions, + url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', + }); - try { - await transport.sendEvent(eventPayload); - } catch (e) { - expect(e.status).toEqual(429); - expect(e.reason).toEqual( - `Transport for event requests locked till ${new Date( - now + retryAfterSeconds * 1000, - )} due to too many requests.`, - ); - expect(e.payload).toEqual(eventPayload); - expect(e.type).toEqual('event'); - } + expect(httpProxyAgent).not.toHaveBeenCalled(); - try { - await transport.sendEvent(transactionPayload); - } catch (e) { - expect(e.status).toEqual(429); - expect(e.reason).toEqual( - `Transport for transaction requests locked till ${new Date( - now + retryAfterSeconds * 1000, - )} due to too many requests.`, - ); - expect(e.payload).toEqual(transactionPayload); - expect(e.type).toEqual('transaction'); - } + delete process.env.no_proxy; + delete process.env.http_proxy; + }); - mockHeaders = {}; - mockReturnCode = 200; + it('no_proxy works with multiple comma-separated hosts', () => { + process.env.http_proxy = 'http://example.com:8080'; + process.env.no_proxy = 'example.com,sentry.io,wat.com:1337'; - const eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toEqual('success'); + makeNodeTransport({ + ...defaultOptions, + url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', + }); - const transactionRes = await transport.sendEvent(transactionPayload); - expect(transactionRes.status).toEqual('success'); + expect(httpProxyAgent).not.toHaveBeenCalled(); - mock.mockRestore(); + delete process.env.no_proxy; + delete process.env.http_proxy; + }); }); - test('back-off using x-sentry-rate-limits with bogus categories should be dropped', async () => { - const retryAfterSeconds = 10; - mockReturnCode = 429; - mockHeaders = { - 'x-sentry-rate-limits': `${retryAfterSeconds}:error;safegreg;eqwerw:scope`, - }; - const transport = createTransport({ dsn }); - const now = Date.now(); - const mock = jest - .spyOn(Date, 'now') - // 1st event - _isRateLimited - false - .mockReturnValueOnce(now) - // 1st event - _handleRateLimit - .mockReturnValueOnce(now) - // 2nd event - _isRateLimited - true (event category) - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // 3rd event - _isRateLimited - false (transaction category) - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // 3rd Event - _handleRateLimit - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // 4th event - _isRateLimited - false (event category) - .mockReturnValueOnce(now + retryAfterSeconds * 1000) - // 4th event - _handleRateLimit - .mockReturnValueOnce(now + retryAfterSeconds * 1000); - - try { - await transport.sendEvent(eventPayload); - } catch (e) { - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`)); - } - - try { - await transport.sendEvent(eventPayload); - } catch (e) { - expect(e.status).toEqual(429); - expect(e.reason).toEqual( - `Transport for event requests locked till ${new Date( - now + retryAfterSeconds * 1000, - )} due to too many requests.`, - ); - expect(e.payload).toEqual(eventPayload); - expect(e.type).toEqual('event'); - } - - mockHeaders = {}; - mockReturnCode = 200; + it('should register TransportRequestExecutor that returns the correct object from server response (rate limit)', async () => { + await setupTestServer({ + statusCode: RATE_LIMIT, + responseHeaders: {}, + }); - const transactionRes = await transport.sendEvent(transactionPayload); - expect(transactionRes.status).toEqual('success'); + makeNodeTransport(defaultOptions); + const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - const eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toEqual('success'); + const executorResult = registeredRequestExecutor({ + body: serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()), + category: 'error', + }); - mock.mockRestore(); + await expect(executorResult).resolves.toEqual( + expect.objectContaining({ + statusCode: RATE_LIMIT, + }), + ); }); - test('back-off using x-sentry-rate-limits should also trigger for 200 responses', async () => { - const retryAfterSeconds = 10; - mockReturnCode = 200; - mockHeaders = { - 'x-sentry-rate-limits': `${retryAfterSeconds}:error;transaction:scope`, - }; - const transport = createTransport({ dsn }); - const now = Date.now(); - const mock = jest - .spyOn(Date, 'now') - // 1st event - _isRateLimited - false - .mockReturnValueOnce(now) - // 1st event - _handleRateLimit - .mockReturnValueOnce(now) - // 2nd event - _isRateLimited - true - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // 3rd event - _isRateLimited - false - .mockReturnValueOnce(now + retryAfterSeconds * 1000) - // 3rd event - _handleRateLimit - .mockReturnValueOnce(now + retryAfterSeconds * 1000); - - let eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toEqual('success'); - - try { - await transport.sendEvent(eventPayload); - } catch (e) { - expect(e.status).toEqual(429); - expect(e.reason).toEqual( - `Transport for event requests locked till ${new Date( - now + retryAfterSeconds * 1000, - )} due to too many requests.`, - ); - expect(e.payload).toEqual(eventPayload); - expect(e.type).toEqual('event'); - } + it('should register TransportRequestExecutor that returns the correct object from server response (OK)', async () => { + await setupTestServer({ + statusCode: SUCCESS, + }); - mockReturnCode = 200; - mockHeaders = {}; + makeNodeTransport(defaultOptions); + const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toEqual('success'); + const executorResult = registeredRequestExecutor({ + body: serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()), + category: 'error', + }); - mock.mockRestore(); + await expect(executorResult).resolves.toEqual( + expect.objectContaining({ + statusCode: SUCCESS, + headers: { + 'retry-after': null, + 'x-sentry-rate-limits': null, + }, + }), + ); }); - test('transport options', async () => { - mockReturnCode = 200; - const transport = createTransport({ - dsn, - headers: { - a: 'b', + it('should register TransportRequestExecutor that returns the correct object from server response (OK with rate-limit headers)', async () => { + await setupTestServer({ + statusCode: SUCCESS, + responseHeaders: { + 'Retry-After': '2700', + 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', }, }); - await transport.sendEvent({ - message: 'test', - }); - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions); - expect(requestOptions.headers).toEqual(expect.objectContaining({ a: 'b' })); - }); + makeNodeTransport(defaultOptions); + const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - describe('proxy', () => { - test('can be configured through client option', async () => { - const transport = createTransport({ - dsn, - httpProxy: 'http://example.com:8080', - }); - const client = transport.client as unknown as { proxy: Record; secureProxy: boolean }; - expect(client).toBeInstanceOf(HttpsProxyAgent); - expect(client.secureProxy).toEqual(false); - expect(client.proxy).toEqual(expect.objectContaining({ protocol: 'http:', port: 8080, host: 'example.com' })); + const executorResult = registeredRequestExecutor({ + body: serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()), + category: 'error', }); - test('can be configured through env variables option', async () => { - process.env.http_proxy = 'http://example.com:8080'; - const transport = createTransport({ - dsn, - httpProxy: 'http://example.com:8080', - }); - const client = transport.client as unknown as { proxy: Record; secureProxy: boolean }; - expect(client).toBeInstanceOf(HttpsProxyAgent); - expect(client.secureProxy).toEqual(false); - expect(client.proxy).toEqual(expect.objectContaining({ protocol: 'http:', port: 8080, host: 'example.com' })); - delete process.env.http_proxy; - }); + await expect(executorResult).resolves.toEqual( + expect.objectContaining({ + statusCode: SUCCESS, + headers: { + 'retry-after': '2700', + 'x-sentry-rate-limits': '60::organization, 2700::organization', + }, + }), + ); + }); - test('client options have priority over env variables', async () => { - process.env.http_proxy = 'http://env-example.com:8080'; - const transport = createTransport({ - dsn, - httpProxy: 'http://example.com:8080', - }); - const client = transport.client as unknown as { proxy: Record; secureProxy: boolean }; - expect(client).toBeInstanceOf(HttpsProxyAgent); - expect(client.secureProxy).toEqual(false); - expect(client.proxy).toEqual(expect.objectContaining({ protocol: 'http:', port: 8080, host: 'example.com' })); - delete process.env.http_proxy; + it('should register TransportRequestExecutor that returns the correct object from server response (NOK with rate-limit headers)', async () => { + await setupTestServer({ + statusCode: RATE_LIMIT, + responseHeaders: { + 'Retry-After': '2700', + 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', + }, }); - test('no_proxy allows for skipping specific hosts', async () => { - process.env.no_proxy = 'sentry.io'; - const transport = createTransport({ - dsn, - httpProxy: 'http://example.com:8080', - }); - expect(transport.client).toBeInstanceOf(http.Agent); - }); + makeNodeTransport(defaultOptions); + const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - test('no_proxy works with a port', async () => { - process.env.http_proxy = 'http://example.com:8080'; - process.env.no_proxy = 'sentry.io:8989'; - const transport = createTransport({ - dsn, - }); - expect(transport.client).toBeInstanceOf(http.Agent); - delete process.env.http_proxy; + const executorResult = registeredRequestExecutor({ + body: serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()), + category: 'error', }); - test('no_proxy works with multiple comma-separated hosts', async () => { - process.env.http_proxy = 'http://example.com:8080'; - process.env.no_proxy = 'example.com,sentry.io,wat.com:1337'; - const transport = createTransport({ - dsn, - }); - expect(transport.client).toBeInstanceOf(http.Agent); - delete process.env.http_proxy; - }); + await expect(executorResult).resolves.toEqual( + expect.objectContaining({ + statusCode: RATE_LIMIT, + headers: { + 'retry-after': '2700', + 'x-sentry-rate-limits': '60::organization, 2700::organization', + }, + }), + ); }); }); diff --git a/packages/node/test/transports/https.test.ts b/packages/node/test/transports/https.test.ts index 8d7f4dd9aa65..cf7051b54fe4 100644 --- a/packages/node/test/transports/https.test.ts +++ b/packages/node/test/transports/https.test.ts @@ -1,332 +1,392 @@ -import { Session } from '@sentry/hub'; -import { SessionAggregates, TransportOptions } from '@sentry/types'; -import { SentryError } from '@sentry/utils'; +import { createTransport } from '@sentry/core'; +import { EventEnvelope, EventItem } from '@sentry/types'; +import { createEnvelope, serializeEnvelope } from '@sentry/utils'; +import * as http from 'http'; import * as https from 'https'; -import * as HttpsProxyAgent from 'https-proxy-agent'; - -import { HTTPSTransport } from '../../src/transports/https'; - -const mockSetEncoding = jest.fn(); -const dsn = 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622'; -const storePath = '/mysubpath/api/50622/store/'; -const envelopePath = '/mysubpath/api/50622/envelope/'; -const tunnel = 'https://hello.com/world'; -const sessionsPayload: SessionAggregates = { - attrs: { environment: 'test', release: '1.0' }, - aggregates: [{ started: '2021-03-17T16:00:00.000Z', exited: 1 }], -}; -let mockReturnCode = 200; -let mockHeaders = {}; - -jest.mock('fs', () => ({ - readFileSync(): string { - return 'mockedCert'; - }, -})); - -function createTransport(options: TransportOptions): HTTPSTransport { - const transport = new HTTPSTransport(options); - transport.module = { - request: jest.fn().mockImplementation((_options: any, callback: any) => ({ - end: () => { - callback({ - headers: mockHeaders, - setEncoding: mockSetEncoding, - statusCode: mockReturnCode, - }); - }, - on: jest.fn(), - })), +import { TextEncoder } from 'util'; + +import { makeNodeTransport } from '../../src/transports'; +import { HTTPModule, HTTPModuleRequestIncomingMessage } from '../../src/transports/http-module'; +import testServerCerts from './test-server-certs'; + +jest.mock('@sentry/core', () => { + const actualCore = jest.requireActual('@sentry/core'); + return { + ...actualCore, + createTransport: jest.fn().mockImplementation(actualCore.createTransport), }; - return transport; -} +}); -function assertBasicOptions(options: any, useEnvelope: boolean = false): void { - expect(options.headers['X-Sentry-Auth']).toContain('sentry_version'); - expect(options.headers['X-Sentry-Auth']).toContain('sentry_client'); - expect(options.headers['X-Sentry-Auth']).toContain('sentry_key'); - expect(options.port).toEqual('8989'); - expect(options.path).toEqual(useEnvelope ? envelopePath : storePath); - expect(options.hostname).toEqual('sentry.io'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const httpProxyAgent = require('https-proxy-agent'); +jest.mock('https-proxy-agent', () => { + return jest.fn().mockImplementation(() => new http.Agent({ keepAlive: false, maxSockets: 30, timeout: 2000 })); +}); + +const SUCCESS = 200; +const RATE_LIMIT = 429; +const INVALID = 400; +const FAILED = 500; + +interface TestServerOptions { + statusCode: number; + responseHeaders?: Record; } -describe('HTTPSTransport', () => { - beforeEach(() => { - mockReturnCode = 200; - mockHeaders = {}; - jest.clearAllMocks(); - }); +let testServer: http.Server | undefined; + +function setupTestServer( + options: TestServerOptions, + requestInspector?: (req: http.IncomingMessage, body: string) => void, +) { + testServer = https.createServer(testServerCerts, (req, res) => { + let body = ''; - test('send 200', async () => { - const transport = createTransport({ dsn }); - await transport.sendEvent({ - message: 'test', + req.on('data', data => { + body += data; }); - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions); - expect(mockSetEncoding).toHaveBeenCalled(); - }); + req.on('end', () => { + requestInspector?.(req, body); + }); - test('send 400', async () => { - mockReturnCode = 400; - const transport = createTransport({ dsn }); + res.writeHead(options.statusCode, options.responseHeaders); + res.end(); - try { - await transport.sendEvent({ - message: 'test', - }); - } catch (e) { - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions); - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`)); - } + // also terminate socket because keepalive hangs connection a bit + res.connection.end(); }); - test('send 200 session', async () => { - const transport = createTransport({ dsn }); - await transport.sendSession(new Session()); + testServer.listen(8099); - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions, true); - expect(mockSetEncoding).toHaveBeenCalled(); + return new Promise(resolve => { + testServer?.on('listening', resolve); }); +} - test('send 400 session', async () => { - mockReturnCode = 400; - const transport = createTransport({ dsn }); - - try { - await transport.sendSession(new Session()); - } catch (e) { - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions, true); - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`)); - } - }); +const TEST_SERVER_URL = 'https://localhost:8099'; - test('send 200 request mode session', async () => { - const transport = createTransport({ dsn }); - await transport.sendSession(sessionsPayload); +const EVENT_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ + [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, +]); - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions, true); - expect(mockSetEncoding).toHaveBeenCalled(); - }); +const SERIALIZED_EVENT_ENVELOPE = serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()); - test('send 400 request mode session', async () => { - mockReturnCode = 400; - const transport = createTransport({ dsn }); +const unsafeHttpsModule: HTTPModule = { + request: jest + .fn() + .mockImplementation((options: https.RequestOptions, callback?: (res: HTTPModuleRequestIncomingMessage) => void) => { + return https.request({ ...options, rejectUnauthorized: false }, callback); + }), +}; - try { - await transport.sendSession(sessionsPayload); - } catch (e) { - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions, true); - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`)); - } - }); +const defaultOptions = { + httpModule: unsafeHttpsModule, + url: TEST_SERVER_URL, + recordDroppedEvent: () => undefined, // noop + textEncoder: new TextEncoder(), +}; - test('send x-sentry-error header', async () => { - mockReturnCode = 429; - mockHeaders = { - 'x-sentry-error': 'test-failed', - }; - const transport = createTransport({ dsn }); +describe('makeNewHttpsTransport()', () => { + afterEach(() => { + jest.clearAllMocks(); - try { - await transport.sendEvent({ - message: 'test', - }); - } catch (e) { - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions); - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode}): test-failed`)); + if (testServer) { + testServer.close(); } }); - test('sends a request to tunnel if configured', async () => { - const transport = createTransport({ dsn, tunnel }); + describe('.send()', () => { + it('should correctly send envelope to server', async () => { + await setupTestServer({ statusCode: SUCCESS }, (req, body) => { + expect(req.method).toBe('POST'); + expect(body).toBe(SERIALIZED_EVENT_ENVELOPE); + }); - await transport.sendEvent({ - message: 'test', + const transport = makeNodeTransport(defaultOptions); + await transport.send(EVENT_ENVELOPE); }); - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - expect(requestOptions.protocol).toEqual('https:'); - expect(requestOptions.hostname).toEqual('hello.com'); - expect(requestOptions.path).toEqual('/world'); - }); - - test('back-off using retry-after header', async () => { - const retryAfterSeconds = 10; - mockReturnCode = 429; - mockHeaders = { - 'retry-after': retryAfterSeconds, - }; - const transport = createTransport({ dsn }); - - const now = Date.now(); - const mock = jest - .spyOn(Date, 'now') - // Check for first event - .mockReturnValueOnce(now) - // Setting disabledUntil - .mockReturnValueOnce(now) - // Check for second event - .mockReturnValueOnce(now + (retryAfterSeconds / 2) * 1000) - // Check for third event - .mockReturnValueOnce(now + retryAfterSeconds * 1000); - - try { - await transport.sendEvent({ message: 'test' }); - } catch (e) { - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`)); - } + it('should correctly send user-provided headers to server', async () => { + await setupTestServer({ statusCode: SUCCESS }, req => { + expect(req.headers).toEqual( + expect.objectContaining({ + // node http module lower-cases incoming headers + 'x-some-custom-header-1': 'value1', + 'x-some-custom-header-2': 'value2', + }), + ); + }); - try { - await transport.sendEvent({ message: 'test' }); - } catch (e) { - expect(e.status).toEqual(429); - expect(e.reason).toEqual( - `Transport for event requests locked till ${new Date( - now + retryAfterSeconds * 1000, - )} due to too many requests.`, - ); - expect(e.payload.message).toEqual('test'); - expect(e.type).toEqual('event'); - } + const transport = makeNodeTransport({ + ...defaultOptions, + headers: { + 'X-Some-Custom-Header-1': 'value1', + 'X-Some-Custom-Header-2': 'value2', + }, + }); - try { - await transport.sendEvent({ message: 'test' }); - } catch (e) { - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`)); - } + await transport.send(EVENT_ENVELOPE); + }); - mock.mockRestore(); - }); + it.each([RATE_LIMIT, INVALID, FAILED])( + 'should resolve on bad server response (status %i)', + async serverStatusCode => { + await setupTestServer({ statusCode: serverStatusCode }); - test('transport options', async () => { - mockReturnCode = 200; - const transport = createTransport({ - dsn, - headers: { - a: 'b', + const transport = makeNodeTransport(defaultOptions); + expect(() => { + expect(transport.send(EVENT_ENVELOPE)); + }).not.toThrow(); }, + ); + + it('should resolve when server responds with rate limit header and status code 200', async () => { + await setupTestServer({ + statusCode: SUCCESS, + responseHeaders: { + 'Retry-After': '2700', + 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', + }, + }); + + const transport = makeNodeTransport(defaultOptions); + await expect(transport.send(EVENT_ENVELOPE)).resolves.toBeUndefined(); }); - await transport.sendEvent({ - message: 'test', + + it('should resolve when server responds with rate limit header and status code 200', async () => { + await setupTestServer({ + statusCode: SUCCESS, + responseHeaders: { + 'Retry-After': '2700', + 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', + }, + }); + + const transport = makeNodeTransport(defaultOptions); + await expect(transport.send(EVENT_ENVELOPE)).resolves.toBeUndefined(); }); - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions); - expect(requestOptions.headers).toEqual(expect.objectContaining({ a: 'b' })); - }); + it('should use `caCerts` option', async () => { + await setupTestServer({ statusCode: SUCCESS }); - describe('proxy', () => { - test('can be configured through client option', async () => { - const transport = createTransport({ - dsn, - httpsProxy: 'https://example.com:8080', + const transport = makeNodeTransport({ + ...defaultOptions, + httpModule: unsafeHttpsModule, + url: TEST_SERVER_URL, + caCerts: 'some cert', }); - const client = transport.client as unknown as { proxy: Record; secureProxy: boolean }; - expect(client).toBeInstanceOf(HttpsProxyAgent); - expect(client.secureProxy).toEqual(true); - expect(client.proxy).toEqual(expect.objectContaining({ protocol: 'https:', port: 8080, host: 'example.com' })); + + await transport.send(EVENT_ENVELOPE); + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(unsafeHttpsModule.request).toHaveBeenCalledWith( + expect.objectContaining({ + ca: 'some cert', + }), + expect.anything(), + ); }); + }); - test('can be configured through env variables option', async () => { - process.env.https_proxy = 'https://example.com:8080'; - const transport = createTransport({ - dsn, - httpsProxy: 'https://example.com:8080', + describe('proxy', () => { + it('can be configured through option', () => { + makeNodeTransport({ + ...defaultOptions, + httpModule: unsafeHttpsModule, + url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', + proxy: 'https://example.com', }); - const client = transport.client as unknown as { proxy: Record; secureProxy: boolean }; - expect(client).toBeInstanceOf(HttpsProxyAgent); - expect(client.secureProxy).toEqual(true); - expect(client.proxy).toEqual(expect.objectContaining({ protocol: 'https:', port: 8080, host: 'example.com' })); - delete process.env.https_proxy; + + expect(httpProxyAgent).toHaveBeenCalledTimes(1); + expect(httpProxyAgent).toHaveBeenCalledWith('https://example.com'); }); - test('https proxies have priority in client option', async () => { - const transport = createTransport({ - dsn, - httpProxy: 'http://unsecure-example.com:8080', - httpsProxy: 'https://example.com:8080', + it('can be configured through env variables option (http)', () => { + process.env.http_proxy = 'https://example.com'; + makeNodeTransport({ + ...defaultOptions, + httpModule: unsafeHttpsModule, + url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', }); - const client = transport.client as unknown as { proxy: Record; secureProxy: boolean }; - expect(client).toBeInstanceOf(HttpsProxyAgent); - expect(client.secureProxy).toEqual(true); - expect(client.proxy).toEqual(expect.objectContaining({ protocol: 'https:', port: 8080, host: 'example.com' })); + + expect(httpProxyAgent).toHaveBeenCalledTimes(1); + expect(httpProxyAgent).toHaveBeenCalledWith('https://example.com'); + delete process.env.http_proxy; }); - test('https proxies have priority in env variables', async () => { - process.env.http_proxy = 'http://unsecure-example.com:8080'; - process.env.https_proxy = 'https://example.com:8080'; - const transport = createTransport({ - dsn, + it('can be configured through env variables option (https)', () => { + process.env.https_proxy = 'https://example.com'; + makeNodeTransport({ + ...defaultOptions, + httpModule: unsafeHttpsModule, + url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', }); - const client = transport.client as unknown as { proxy: Record; secureProxy: boolean }; - expect(client).toBeInstanceOf(HttpsProxyAgent); - expect(client.secureProxy).toEqual(true); - expect(client.proxy).toEqual(expect.objectContaining({ protocol: 'https:', port: 8080, host: 'example.com' })); - delete process.env.http_proxy; + + expect(httpProxyAgent).toHaveBeenCalledTimes(1); + expect(httpProxyAgent).toHaveBeenCalledWith('https://example.com'); delete process.env.https_proxy; }); - test('client options have priority over env variables', async () => { - process.env.https_proxy = 'https://env-example.com:8080'; - const transport = createTransport({ - dsn, - httpsProxy: 'https://example.com:8080', + it('client options have priority over env variables', () => { + process.env.https_proxy = 'https://foo.com'; + makeNodeTransport({ + ...defaultOptions, + httpModule: unsafeHttpsModule, + url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', + proxy: 'https://bar.com', }); - const client = transport.client as unknown as { proxy: Record; secureProxy: boolean }; - expect(client).toBeInstanceOf(HttpsProxyAgent); - expect(client.secureProxy).toEqual(true); - expect(client.proxy).toEqual(expect.objectContaining({ protocol: 'https:', port: 8080, host: 'example.com' })); + + expect(httpProxyAgent).toHaveBeenCalledTimes(1); + expect(httpProxyAgent).toHaveBeenCalledWith('https://bar.com'); delete process.env.https_proxy; }); - test('no_proxy allows for skipping specific hosts', async () => { + it('no_proxy allows for skipping specific hosts', () => { process.env.no_proxy = 'sentry.io'; - const transport = createTransport({ - dsn, - httpsProxy: 'https://example.com:8080', + makeNodeTransport({ + ...defaultOptions, + httpModule: unsafeHttpsModule, + url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', + proxy: 'https://example.com', }); - expect(transport.client).toBeInstanceOf(https.Agent); + + expect(httpProxyAgent).not.toHaveBeenCalled(); + + delete process.env.no_proxy; }); - test('no_proxy works with a port', async () => { - process.env.https_proxy = 'https://example.com:8080'; + it('no_proxy works with a port', () => { + process.env.http_proxy = 'https://example.com:8080'; process.env.no_proxy = 'sentry.io:8989'; - const transport = createTransport({ - dsn, + + makeNodeTransport({ + ...defaultOptions, + httpModule: unsafeHttpsModule, + url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', }); - expect(transport.client).toBeInstanceOf(https.Agent); - delete process.env.https_proxy; + + expect(httpProxyAgent).not.toHaveBeenCalled(); + + delete process.env.no_proxy; + delete process.env.http_proxy; }); - test('no_proxy works with multiple comma-separated hosts', async () => { + it('no_proxy works with multiple comma-separated hosts', () => { process.env.http_proxy = 'https://example.com:8080'; process.env.no_proxy = 'example.com,sentry.io,wat.com:1337'; - const transport = createTransport({ - dsn, + + makeNodeTransport({ + ...defaultOptions, + httpModule: unsafeHttpsModule, + url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', }); - expect(transport.client).toBeInstanceOf(https.Agent); - delete process.env.https_proxy; + + expect(httpProxyAgent).not.toHaveBeenCalled(); + + delete process.env.no_proxy; + delete process.env.http_proxy; }); + }); - test('can configure tls certificate through client option', async () => { - mockReturnCode = 200; - const transport = createTransport({ - caCerts: './some/path.pem', - dsn, - }); - await transport.sendEvent({ - message: 'test', - }); - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - expect(requestOptions.ca).toEqual('mockedCert'); + it('should register TransportRequestExecutor that returns the correct object from server response (rate limit)', async () => { + await setupTestServer({ + statusCode: RATE_LIMIT, + responseHeaders: {}, + }); + + makeNodeTransport(defaultOptions); + const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; + + const executorResult = registeredRequestExecutor({ + body: serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()), + category: 'error', + }); + + await expect(executorResult).resolves.toEqual( + expect.objectContaining({ + statusCode: RATE_LIMIT, + }), + ); + }); + + it('should register TransportRequestExecutor that returns the correct object from server response (OK)', async () => { + await setupTestServer({ + statusCode: SUCCESS, + }); + + makeNodeTransport(defaultOptions); + const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; + + const executorResult = registeredRequestExecutor({ + body: serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()), + category: 'error', + }); + + await expect(executorResult).resolves.toEqual( + expect.objectContaining({ + statusCode: SUCCESS, + headers: { + 'retry-after': null, + 'x-sentry-rate-limits': null, + }, + }), + ); + }); + + it('should register TransportRequestExecutor that returns the correct object from server response (OK with rate-limit headers)', async () => { + await setupTestServer({ + statusCode: SUCCESS, + responseHeaders: { + 'Retry-After': '2700', + 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', + }, }); + + makeNodeTransport(defaultOptions); + const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; + + const executorResult = registeredRequestExecutor({ + body: serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()), + category: 'error', + }); + + await expect(executorResult).resolves.toEqual( + expect.objectContaining({ + statusCode: SUCCESS, + headers: { + 'retry-after': '2700', + 'x-sentry-rate-limits': '60::organization, 2700::organization', + }, + }), + ); + }); + + it('should register TransportRequestExecutor that returns the correct object from server response (NOK with rate-limit headers)', async () => { + await setupTestServer({ + statusCode: RATE_LIMIT, + responseHeaders: { + 'Retry-After': '2700', + 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', + }, + }); + + makeNodeTransport(defaultOptions); + const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; + + const executorResult = registeredRequestExecutor({ + body: serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()), + category: 'error', + }); + + await expect(executorResult).resolves.toEqual( + expect.objectContaining({ + statusCode: RATE_LIMIT, + headers: { + 'retry-after': '2700', + 'x-sentry-rate-limits': '60::organization, 2700::organization', + }, + }), + ); }); }); diff --git a/packages/node/test/transports/new/http.test.ts b/packages/node/test/transports/new/http.test.ts deleted file mode 100644 index b3ce46d5a542..000000000000 --- a/packages/node/test/transports/new/http.test.ts +++ /dev/null @@ -1,347 +0,0 @@ -import { createTransport } from '@sentry/core'; -import { EventEnvelope, EventItem } from '@sentry/types'; -import { createEnvelope, serializeEnvelope } from '@sentry/utils'; -import * as http from 'http'; - -// TODO(v7): We're renaming the imported file so this needs to be changed as well -import { makeNodeTransport } from '../../../src/transports/new'; - -jest.mock('@sentry/core', () => { - const actualCore = jest.requireActual('@sentry/core'); - return { - ...actualCore, - createTransport: jest.fn().mockImplementation(actualCore.createTransport), - }; -}); - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const httpProxyAgent = require('https-proxy-agent'); -jest.mock('https-proxy-agent', () => { - return jest.fn().mockImplementation(() => new http.Agent({ keepAlive: false, maxSockets: 30, timeout: 2000 })); -}); - -const SUCCESS = 200; -const RATE_LIMIT = 429; -const INVALID = 400; -const FAILED = 500; - -interface TestServerOptions { - statusCode: number; - responseHeaders?: Record; -} - -let testServer: http.Server | undefined; - -function setupTestServer( - options: TestServerOptions, - requestInspector?: (req: http.IncomingMessage, body: string) => void, -) { - testServer = http.createServer((req, res) => { - let body = ''; - - req.on('data', data => { - body += data; - }); - - req.on('end', () => { - requestInspector?.(req, body); - }); - - res.writeHead(options.statusCode, options.responseHeaders); - res.end(); - - // also terminate socket because keepalive hangs connection a bit - res.connection.end(); - }); - - testServer.listen(18099); - - return new Promise(resolve => { - testServer?.on('listening', resolve); - }); -} - -const TEST_SERVER_URL = 'http://localhost:18099'; - -const EVENT_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ - [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, -]); - -const SERIALIZED_EVENT_ENVELOPE = serializeEnvelope(EVENT_ENVELOPE); - -describe('makeNewHttpTransport()', () => { - afterEach(() => { - jest.clearAllMocks(); - - if (testServer) { - testServer.close(); - } - }); - - describe('.send()', () => { - it('should correctly return successful server response', async () => { - await setupTestServer({ statusCode: SUCCESS }); - - const transport = makeNodeTransport({ url: TEST_SERVER_URL }); - const transportResponse = await transport.send(EVENT_ENVELOPE); - - expect(transportResponse).toEqual(expect.objectContaining({ status: 'success' })); - }); - - it('should correctly send envelope to server', async () => { - await setupTestServer({ statusCode: SUCCESS }, (req, body) => { - expect(req.method).toBe('POST'); - expect(body).toBe(SERIALIZED_EVENT_ENVELOPE); - }); - - const transport = makeNodeTransport({ url: TEST_SERVER_URL }); - await transport.send(EVENT_ENVELOPE); - }); - - it('should correctly send user-provided headers to server', async () => { - await setupTestServer({ statusCode: SUCCESS }, req => { - expect(req.headers).toEqual( - expect.objectContaining({ - // node http module lower-cases incoming headers - 'x-some-custom-header-1': 'value1', - 'x-some-custom-header-2': 'value2', - }), - ); - }); - - const transport = makeNodeTransport({ - url: TEST_SERVER_URL, - headers: { - 'X-Some-Custom-Header-1': 'value1', - 'X-Some-Custom-Header-2': 'value2', - }, - }); - - await transport.send(EVENT_ENVELOPE); - }); - - it.each([ - [RATE_LIMIT, 'rate_limit'], - [INVALID, 'invalid'], - [FAILED, 'failed'], - ])('should correctly reject bad server response (status %i)', async (serverStatusCode, expectedStatus) => { - await setupTestServer({ statusCode: serverStatusCode }); - - const transport = makeNodeTransport({ url: TEST_SERVER_URL }); - await expect(transport.send(EVENT_ENVELOPE)).rejects.toEqual(expect.objectContaining({ status: expectedStatus })); - }); - - it('should resolve when server responds with rate limit header and status code 200', async () => { - await setupTestServer({ - statusCode: SUCCESS, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', - }, - }); - - const transport = makeNodeTransport({ url: TEST_SERVER_URL }); - const transportResponse = await transport.send(EVENT_ENVELOPE); - - expect(transportResponse).toEqual(expect.objectContaining({ status: 'success' })); - }); - - it('should resolve when server responds with rate limit header and status code 200', async () => { - await setupTestServer({ - statusCode: SUCCESS, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', - }, - }); - - const transport = makeNodeTransport({ url: TEST_SERVER_URL }); - const transportResponse = await transport.send(EVENT_ENVELOPE); - - expect(transportResponse).toEqual(expect.objectContaining({ status: 'success' })); - }); - }); - - describe('proxy', () => { - it('can be configured through option', () => { - makeNodeTransport({ - url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - proxy: 'http://example.com', - }); - - expect(httpProxyAgent).toHaveBeenCalledTimes(1); - expect(httpProxyAgent).toHaveBeenCalledWith('http://example.com'); - }); - - it('can be configured through env variables option', () => { - process.env.http_proxy = 'http://example.com'; - makeNodeTransport({ - url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - }); - - expect(httpProxyAgent).toHaveBeenCalledTimes(1); - expect(httpProxyAgent).toHaveBeenCalledWith('http://example.com'); - delete process.env.http_proxy; - }); - - it('client options have priority over env variables', () => { - process.env.http_proxy = 'http://foo.com'; - makeNodeTransport({ - url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - proxy: 'http://bar.com', - }); - - expect(httpProxyAgent).toHaveBeenCalledTimes(1); - expect(httpProxyAgent).toHaveBeenCalledWith('http://bar.com'); - delete process.env.http_proxy; - }); - - it('no_proxy allows for skipping specific hosts', () => { - process.env.no_proxy = 'sentry.io'; - makeNodeTransport({ - url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - proxy: 'http://example.com', - }); - - expect(httpProxyAgent).not.toHaveBeenCalled(); - - delete process.env.no_proxy; - }); - - it('no_proxy works with a port', () => { - process.env.http_proxy = 'http://example.com:8080'; - process.env.no_proxy = 'sentry.io:8989'; - - makeNodeTransport({ - url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - }); - - expect(httpProxyAgent).not.toHaveBeenCalled(); - - delete process.env.no_proxy; - delete process.env.http_proxy; - }); - - it('no_proxy works with multiple comma-separated hosts', () => { - process.env.http_proxy = 'http://example.com:8080'; - process.env.no_proxy = 'example.com,sentry.io,wat.com:1337'; - - makeNodeTransport({ - url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - }); - - expect(httpProxyAgent).not.toHaveBeenCalled(); - - delete process.env.no_proxy; - delete process.env.http_proxy; - }); - }); - - it('should register TransportRequestExecutor that returns the correct object from server response (rate limit)', async () => { - await setupTestServer({ - statusCode: RATE_LIMIT, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', - }, - }); - - makeNodeTransport({ url: TEST_SERVER_URL }); - const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - - const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE), - category: 'error', - }); - - await expect(executorResult).resolves.toEqual( - expect.objectContaining({ - headers: { - 'retry-after': '2700', - 'x-sentry-rate-limits': '60::organization, 2700::organization', - }, - statusCode: RATE_LIMIT, - }), - ); - }); - - it('should register TransportRequestExecutor that returns the correct object from server response (OK)', async () => { - await setupTestServer({ - statusCode: SUCCESS, - }); - - makeNodeTransport({ url: TEST_SERVER_URL }); - const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - - const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE), - category: 'error', - }); - - await expect(executorResult).resolves.toEqual( - expect.objectContaining({ - headers: { - 'retry-after': null, - 'x-sentry-rate-limits': null, - }, - statusCode: SUCCESS, - }), - ); - }); - - it('should register TransportRequestExecutor that returns the correct object from server response (OK with rate-limit headers)', async () => { - await setupTestServer({ - statusCode: SUCCESS, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', - }, - }); - - makeNodeTransport({ url: TEST_SERVER_URL }); - const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - - const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE), - category: 'error', - }); - - await expect(executorResult).resolves.toEqual( - expect.objectContaining({ - headers: { - 'retry-after': '2700', - 'x-sentry-rate-limits': '60::organization, 2700::organization', - }, - statusCode: SUCCESS, - }), - ); - }); - - it('should register TransportRequestExecutor that returns the correct object from server response (NOK with rate-limit headers)', async () => { - await setupTestServer({ - statusCode: RATE_LIMIT, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', - }, - }); - - makeNodeTransport({ url: TEST_SERVER_URL }); - const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - - const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE), - category: 'error', - }); - - await expect(executorResult).resolves.toEqual( - expect.objectContaining({ - headers: { - 'retry-after': '2700', - 'x-sentry-rate-limits': '60::organization, 2700::organization', - }, - statusCode: RATE_LIMIT, - }), - ); - }); -}); diff --git a/packages/node/test/transports/new/https.test.ts b/packages/node/test/transports/new/https.test.ts deleted file mode 100644 index 7784e16c65df..000000000000 --- a/packages/node/test/transports/new/https.test.ts +++ /dev/null @@ -1,397 +0,0 @@ -import { createTransport } from '@sentry/core'; -import { EventEnvelope, EventItem } from '@sentry/types'; -import { createEnvelope, serializeEnvelope } from '@sentry/utils'; -import * as http from 'http'; -import * as https from 'https'; - -import { HTTPModule, HTTPModuleRequestIncomingMessage } from '../../../src/transports/base/http-module'; -// TODO(v7): We're renaming the imported file so this needs to be changed as well -import { makeNodeTransport } from '../../../src/transports/new'; -import testServerCerts from './test-server-certs'; - -jest.mock('@sentry/core', () => { - const actualCore = jest.requireActual('@sentry/core'); - return { - ...actualCore, - createTransport: jest.fn().mockImplementation(actualCore.createTransport), - }; -}); - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const httpProxyAgent = require('https-proxy-agent'); -jest.mock('https-proxy-agent', () => { - return jest.fn().mockImplementation(() => new http.Agent({ keepAlive: false, maxSockets: 30, timeout: 2000 })); -}); - -const SUCCESS = 200; -const RATE_LIMIT = 429; -const INVALID = 400; -const FAILED = 500; - -interface TestServerOptions { - statusCode: number; - responseHeaders?: Record; -} - -let testServer: http.Server | undefined; - -function setupTestServer( - options: TestServerOptions, - requestInspector?: (req: http.IncomingMessage, body: string) => void, -) { - testServer = https.createServer(testServerCerts, (req, res) => { - let body = ''; - - req.on('data', data => { - body += data; - }); - - req.on('end', () => { - requestInspector?.(req, body); - }); - - res.writeHead(options.statusCode, options.responseHeaders); - res.end(); - - // also terminate socket because keepalive hangs connection a bit - res.connection.end(); - }); - - testServer.listen(8099); - - return new Promise(resolve => { - testServer?.on('listening', resolve); - }); -} - -const TEST_SERVER_URL = 'https://localhost:8099'; - -const EVENT_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ - [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, -]); - -const SERIALIZED_EVENT_ENVELOPE = serializeEnvelope(EVENT_ENVELOPE); - -const unsafeHttpsModule: HTTPModule = { - request: jest - .fn() - .mockImplementation((options: https.RequestOptions, callback?: (res: HTTPModuleRequestIncomingMessage) => void) => { - return https.request({ ...options, rejectUnauthorized: false }, callback); - }), -}; - -describe('makeNewHttpsTransport()', () => { - afterEach(() => { - jest.clearAllMocks(); - - if (testServer) { - testServer.close(); - } - }); - - describe('.send()', () => { - it('should correctly return successful server response', async () => { - await setupTestServer({ statusCode: SUCCESS }); - - const transport = makeNodeTransport({ httpModule: unsafeHttpsModule, url: TEST_SERVER_URL }); - const transportResponse = await transport.send(EVENT_ENVELOPE); - - expect(transportResponse).toEqual(expect.objectContaining({ status: 'success' })); - }); - - it('should correctly send envelope to server', async () => { - await setupTestServer({ statusCode: SUCCESS }, (req, body) => { - expect(req.method).toBe('POST'); - expect(body).toBe(SERIALIZED_EVENT_ENVELOPE); - }); - - const transport = makeNodeTransport({ httpModule: unsafeHttpsModule, url: TEST_SERVER_URL }); - await transport.send(EVENT_ENVELOPE); - }); - - it('should correctly send user-provided headers to server', async () => { - await setupTestServer({ statusCode: SUCCESS }, req => { - expect(req.headers).toEqual( - expect.objectContaining({ - // node http module lower-cases incoming headers - 'x-some-custom-header-1': 'value1', - 'x-some-custom-header-2': 'value2', - }), - ); - }); - - const transport = makeNodeTransport({ - httpModule: unsafeHttpsModule, - url: TEST_SERVER_URL, - headers: { - 'X-Some-Custom-Header-1': 'value1', - 'X-Some-Custom-Header-2': 'value2', - }, - }); - - await transport.send(EVENT_ENVELOPE); - }); - - it.each([ - [RATE_LIMIT, 'rate_limit'], - [INVALID, 'invalid'], - [FAILED, 'failed'], - ])('should correctly reject bad server response (status %i)', async (serverStatusCode, expectedStatus) => { - await setupTestServer({ statusCode: serverStatusCode }); - - const transport = makeNodeTransport({ httpModule: unsafeHttpsModule, url: TEST_SERVER_URL }); - await expect(transport.send(EVENT_ENVELOPE)).rejects.toEqual(expect.objectContaining({ status: expectedStatus })); - }); - - it('should resolve when server responds with rate limit header and status code 200', async () => { - await setupTestServer({ - statusCode: SUCCESS, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', - }, - }); - - const transport = makeNodeTransport({ httpModule: unsafeHttpsModule, url: TEST_SERVER_URL }); - const transportResponse = await transport.send(EVENT_ENVELOPE); - - expect(transportResponse).toEqual(expect.objectContaining({ status: 'success' })); - }); - - it('should resolve when server responds with rate limit header and status code 200', async () => { - await setupTestServer({ - statusCode: SUCCESS, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', - }, - }); - - const transport = makeNodeTransport({ httpModule: unsafeHttpsModule, url: TEST_SERVER_URL }); - const transportResponse = await transport.send(EVENT_ENVELOPE); - - expect(transportResponse).toEqual(expect.objectContaining({ status: 'success' })); - }); - - it('should use `caCerts` option', async () => { - await setupTestServer({ statusCode: SUCCESS }); - - const transport = makeNodeTransport({ - httpModule: unsafeHttpsModule, - url: TEST_SERVER_URL, - caCerts: 'some cert', - }); - - await transport.send(EVENT_ENVELOPE); - - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(unsafeHttpsModule.request).toHaveBeenCalledWith( - expect.objectContaining({ - ca: 'some cert', - }), - expect.anything(), - ); - }); - }); - - describe('proxy', () => { - it('can be configured through option', () => { - makeNodeTransport({ - httpModule: unsafeHttpsModule, - url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - proxy: 'https://example.com', - }); - - expect(httpProxyAgent).toHaveBeenCalledTimes(1); - expect(httpProxyAgent).toHaveBeenCalledWith('https://example.com'); - }); - - it('can be configured through env variables option (http)', () => { - process.env.http_proxy = 'https://example.com'; - makeNodeTransport({ - httpModule: unsafeHttpsModule, - url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - }); - - expect(httpProxyAgent).toHaveBeenCalledTimes(1); - expect(httpProxyAgent).toHaveBeenCalledWith('https://example.com'); - delete process.env.http_proxy; - }); - - it('can be configured through env variables option (https)', () => { - process.env.https_proxy = 'https://example.com'; - makeNodeTransport({ - httpModule: unsafeHttpsModule, - url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - }); - - expect(httpProxyAgent).toHaveBeenCalledTimes(1); - expect(httpProxyAgent).toHaveBeenCalledWith('https://example.com'); - delete process.env.https_proxy; - }); - - it('client options have priority over env variables', () => { - process.env.https_proxy = 'https://foo.com'; - makeNodeTransport({ - httpModule: unsafeHttpsModule, - url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - proxy: 'https://bar.com', - }); - - expect(httpProxyAgent).toHaveBeenCalledTimes(1); - expect(httpProxyAgent).toHaveBeenCalledWith('https://bar.com'); - delete process.env.https_proxy; - }); - - it('no_proxy allows for skipping specific hosts', () => { - process.env.no_proxy = 'sentry.io'; - makeNodeTransport({ - httpModule: unsafeHttpsModule, - url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - proxy: 'https://example.com', - }); - - expect(httpProxyAgent).not.toHaveBeenCalled(); - - delete process.env.no_proxy; - }); - - it('no_proxy works with a port', () => { - process.env.http_proxy = 'https://example.com:8080'; - process.env.no_proxy = 'sentry.io:8989'; - - makeNodeTransport({ - httpModule: unsafeHttpsModule, - url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - }); - - expect(httpProxyAgent).not.toHaveBeenCalled(); - - delete process.env.no_proxy; - delete process.env.http_proxy; - }); - - it('no_proxy works with multiple comma-separated hosts', () => { - process.env.http_proxy = 'https://example.com:8080'; - process.env.no_proxy = 'example.com,sentry.io,wat.com:1337'; - - makeNodeTransport({ - httpModule: unsafeHttpsModule, - url: 'https://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', - }); - - expect(httpProxyAgent).not.toHaveBeenCalled(); - - delete process.env.no_proxy; - delete process.env.http_proxy; - }); - }); - - it('should register TransportRequestExecutor that returns the correct object from server response (rate limit)', async () => { - await setupTestServer({ - statusCode: RATE_LIMIT, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', - }, - }); - - makeNodeTransport({ httpModule: unsafeHttpsModule, url: TEST_SERVER_URL }); - const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - - const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE), - category: 'error', - }); - - await expect(executorResult).resolves.toEqual( - expect.objectContaining({ - headers: { - 'retry-after': '2700', - 'x-sentry-rate-limits': '60::organization, 2700::organization', - }, - statusCode: RATE_LIMIT, - }), - ); - }); - - it('should register TransportRequestExecutor that returns the correct object from server response (OK)', async () => { - await setupTestServer({ - statusCode: SUCCESS, - }); - - makeNodeTransport({ httpModule: unsafeHttpsModule, url: TEST_SERVER_URL }); - const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - - const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE), - category: 'error', - }); - - await expect(executorResult).resolves.toEqual( - expect.objectContaining({ - headers: { - 'retry-after': null, - 'x-sentry-rate-limits': null, - }, - statusCode: SUCCESS, - }), - ); - }); - - it('should register TransportRequestExecutor that returns the correct object from server response (OK with rate-limit headers)', async () => { - await setupTestServer({ - statusCode: SUCCESS, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', - }, - }); - - makeNodeTransport({ httpModule: unsafeHttpsModule, url: TEST_SERVER_URL }); - const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - - const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE), - category: 'error', - }); - - await expect(executorResult).resolves.toEqual( - expect.objectContaining({ - headers: { - 'retry-after': '2700', - 'x-sentry-rate-limits': '60::organization, 2700::organization', - }, - statusCode: SUCCESS, - }), - ); - }); - - it('should register TransportRequestExecutor that returns the correct object from server response (NOK with rate-limit headers)', async () => { - await setupTestServer({ - statusCode: RATE_LIMIT, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', - }, - }); - - makeNodeTransport({ httpModule: unsafeHttpsModule, url: TEST_SERVER_URL }); - const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; - - const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE), - category: 'error', - }); - - await expect(executorResult).resolves.toEqual( - expect.objectContaining({ - headers: { - 'retry-after': '2700', - 'x-sentry-rate-limits': '60::organization, 2700::organization', - }, - statusCode: RATE_LIMIT, - }), - ); - }); -}); diff --git a/packages/node/test/transports/new/test-server-certs.ts b/packages/node/test/transports/test-server-certs.ts similarity index 100% rename from packages/node/test/transports/new/test-server-certs.ts rename to packages/node/test/transports/test-server-certs.ts diff --git a/packages/node/tsconfig.cjs.json b/packages/node/tsconfig.cjs.json deleted file mode 100644 index e3a918fc70af..000000000000 --- a/packages/node/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "build/dist" - } -} diff --git a/packages/node/tsconfig.esm.json b/packages/node/tsconfig.esm.json deleted file mode 100644 index 0b86c52918cc..000000000000 --- a/packages/node/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "build/esm" - } -} diff --git a/packages/react/README.md b/packages/react/README.md index e5aea3323b14..761083801ca1 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Official Sentry SDK for ReactJS diff --git a/packages/react/jest.config.js b/packages/react/jest.config.js new file mode 100644 index 000000000000..cd02790794a7 --- /dev/null +++ b/packages/react/jest.config.js @@ -0,0 +1,6 @@ +const baseConfig = require('../../jest/jest.config.js'); + +module.exports = { + ...baseConfig, + testEnvironment: 'jsdom', +}; diff --git a/packages/react/package.json b/packages/react/package.json index ace7013232e1..33b5beea3a4e 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,25 +1,24 @@ { "name": "@sentry/react", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Official Sentry SDK for React.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react", "author": "Sentry", "license": "BSD-3-Clause", "engines": { - "node": ">=6" + "node": ">=8" }, - "main": "build/dist/index.js", + "main": "build/cjs/index.js", "module": "build/esm/index.js", "types": "build/types/index.d.ts", "publishConfig": { "access": "public" }, "dependencies": { - "@sentry/browser": "6.19.7", - "@sentry/minimal": "6.19.7", - "@sentry/types": "6.19.7", - "@sentry/utils": "6.19.7", + "@sentry/browser": "7.0.0-rc.0", + "@sentry/types": "7.0.0-rc.0", + "@sentry/utils": "7.0.0-rc.0", "hoist-non-react-statics": "^3.3.2", "tslib": "^1.9.3" }, @@ -40,34 +39,29 @@ "eslint-plugin-react-hooks": "^4.0.8", "history-4": "npm:history@4.6.0", "history-5": "npm:history@4.9.0", - "jsdom": "^16.2.2", "react": "^18.0.0", "react-dom": "^18.0.0", "react-router-3": "npm:react-router@3.2.0", "react-router-4": "npm:react-router@4.1.0", "react-router-5": "npm:react-router@5.0.0", + "react-router-6": "npm:react-router@6.3.0", "redux": "^4.0.5" }, "scripts": { - "build": "run-p build:cjs build:esm build:types", - "build:cjs": "tsc -p tsconfig.cjs.json", + "build": "run-p build:rollup build:types", "build:dev": "run-s build", - "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "tsc -p tsconfig.esm.json", + "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", - "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", + "build:watch": "run-p build:rollup:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:es5:watch": "yarn build:cjs:watch # *** backwards compatibility - remove in v7 ***", - "build:esm:watch": "tsc -p tsconfig.esm.json --watch", + "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf dist esm build coverage", + "clean": "rimraf build coverage sentry-react-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/*.ts\"", - "link:yarn": "yarn link", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", @@ -77,28 +71,5 @@ "volta": { "extends": "../../package.json" }, - "jest": { - "collectCoverage": true, - "transform": { - "^.+\\.ts$": "ts-jest", - "^.+\\.tsx$": "ts-jest" - }, - "moduleFileExtensions": [ - "js", - "ts", - "tsx" - ], - "testEnvironment": "jsdom", - "testMatch": [ - "**/*.test.ts", - "**/*.test.tsx" - ], - "globals": { - "ts-jest": { - "tsConfig": "./tsconfig.json", - "diagnostics": false - } - } - }, "sideEffects": false } diff --git a/packages/react/rollup.npm.config.js b/packages/react/rollup.npm.config.js new file mode 100644 index 000000000000..ebe81bb263c6 --- /dev/null +++ b/packages/react/rollup.npm.config.js @@ -0,0 +1,7 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + esModuleInterop: true, + }), +); diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 306f90e4f943..150f4571ef85 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -6,3 +6,4 @@ export { ErrorBoundary, withErrorBoundary } from './errorboundary'; export { createReduxEnhancer } from './redux'; export { reactRouterV3Instrumentation } from './reactrouterv3'; export { reactRouterV4Instrumentation, reactRouterV5Instrumentation, withSentryRouting } from './reactrouter'; +export { reactRouterV6Instrumentation, withSentryReactRouterV6Routing } from './reactrouterv6'; diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index acf764347c69..c52d503eb92b 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { getCurrentHub, Hub } from '@sentry/browser'; -import { Integration, IntegrationClass, Span, Transaction } from '@sentry/types'; +import { Span, Transaction } from '@sentry/types'; import { timestampWithMs } from '@sentry/utils'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; @@ -10,66 +10,6 @@ import { REACT_MOUNT_OP, REACT_RENDER_OP, REACT_UPDATE_OP } from './constants'; export const UNKNOWN_COMPONENT = 'unknown'; -const TRACING_GETTER = ({ - id: 'Tracing', -} as any) as IntegrationClass; - -let globalTracingIntegration: Integration | null = null; -/** @deprecated remove when @sentry/apm no longer used */ -const getTracingIntegration = (): Integration | null => { - if (globalTracingIntegration) { - return globalTracingIntegration; - } - - globalTracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER); - return globalTracingIntegration; -}; - -/** - * pushActivity creates an new react activity. - * Is a no-op if Tracing integration is not valid - * @param name displayName of component that started activity - * @deprecated remove when @sentry/apm no longer used - */ -function pushActivity(name: string, op: string): number | null { - if (globalTracingIntegration === null) { - return null; - } - - return (globalTracingIntegration as any).constructor.pushActivity(name, { - description: `<${name}>`, - op, - }); -} - -/** - * popActivity removes a React activity. - * Is a no-op if Tracing integration is not valid. - * @param activity id of activity that is being popped - * @deprecated remove when @sentry/apm no longer used - */ -function popActivity(activity: number | null): void { - if (activity === null || globalTracingIntegration === null) { - return; - } - - (globalTracingIntegration as any).constructor.popActivity(activity); -} - -/** - * Obtain a span given an activity id. - * Is a no-op if Tracing integration is not valid. - * @param activity activity id associated with obtained span - * @deprecated remove when @sentry/apm no longer used - */ -function getActivitySpan(activity: number | null): Span | undefined { - if (activity === null || globalTracingIntegration === null) { - return undefined; - } - - return (globalTracingIntegration as any).constructor.getActivitySpan(activity) as Span | undefined; -} - export type ProfilerProps = { // The name of the component being profiled. name: string; @@ -95,9 +35,6 @@ class Profiler extends React.Component { */ protected _mountSpan: Span | undefined = undefined; - // The activity representing how long it takes to mount a component. - private _mountActivity: number | null = null; - // eslint-disable-next-line @typescript-eslint/member-ordering public static defaultProps: Partial = { disabled: false, @@ -113,19 +50,12 @@ class Profiler extends React.Component { return; } - // If they are using @sentry/apm, we need to push/pop activities - // eslint-disable-next-line deprecation/deprecation - if (getTracingIntegration()) { - // eslint-disable-next-line deprecation/deprecation - this._mountActivity = pushActivity(name, REACT_MOUNT_OP); - } else { - const activeTransaction = getActiveTransaction(); - if (activeTransaction) { - this._mountSpan = activeTransaction.startChild({ - description: `<${name}>`, - op: REACT_MOUNT_OP, - }); - } + const activeTransaction = getActiveTransaction(); + if (activeTransaction) { + this._mountSpan = activeTransaction.startChild({ + description: `<${name}>`, + op: REACT_MOUNT_OP, + }); } } @@ -133,12 +63,6 @@ class Profiler extends React.Component { public componentDidMount(): void { if (this._mountSpan) { this._mountSpan.finish(); - } else { - // eslint-disable-next-line deprecation/deprecation - this._mountSpan = getActivitySpan(this._mountActivity); - // eslint-disable-next-line deprecation/deprecation - popActivity(this._mountActivity); - this._mountActivity = null; } } diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx new file mode 100644 index 000000000000..a793f8824e1d --- /dev/null +++ b/packages/react/src/reactrouterv6.tsx @@ -0,0 +1,180 @@ +// Inspired from Donnie McNeal's solution: +// https://gist.github.com/wontondon/e8c4bdf2888875e4c755712e99279536 + +import { Transaction, TransactionContext } from '@sentry/types'; +import { getGlobalObject, logger } from '@sentry/utils'; +import hoistNonReactStatics from 'hoist-non-react-statics'; +import React from 'react'; + +import { IS_DEBUG_BUILD } from './flags'; +import { Action, Location } from './types'; + +interface RouteObject { + caseSensitive?: boolean; + children?: RouteObject[]; + element?: React.ReactNode; + index?: boolean; + path?: string; +} + +type Params = { + readonly [key in Key]: string | undefined; +}; + +interface RouteMatch { + params: Params; + pathname: string; + route: RouteObject; +} + +type UseEffect = (cb: () => void, deps: unknown[]) => void; +type UseLocation = () => Location; +type UseNavigationType = () => Action; +type CreateRoutesFromChildren = (children: JSX.Element[]) => RouteObject[]; +type MatchRoutes = (routes: RouteObject[], location: Location) => RouteMatch[] | null; + +let activeTransaction: Transaction | undefined; + +let _useEffect: UseEffect; +let _useLocation: UseLocation; +let _useNavigationType: UseNavigationType; +let _createRoutesFromChildren: CreateRoutesFromChildren; +let _matchRoutes: MatchRoutes; +let _customStartTransaction: (context: TransactionContext) => Transaction | undefined; +let _startTransactionOnLocationChange: boolean; + +const global = getGlobalObject(); + +const SENTRY_TAGS = { + 'routing.instrumentation': 'react-router-v6', +}; + +function getInitPathName(): string | undefined { + if (global && global.location) { + return global.location.pathname; + } + + return undefined; +} + +export function reactRouterV6Instrumentation( + useEffect: UseEffect, + useLocation: UseLocation, + useNavigationType: UseNavigationType, + createRoutesFromChildren: CreateRoutesFromChildren, + matchRoutes: MatchRoutes, +) { + return ( + customStartTransaction: (context: TransactionContext) => Transaction | undefined, + startTransactionOnPageLoad = true, + startTransactionOnLocationChange = true, + ): void => { + const initPathName = getInitPathName(); + if (startTransactionOnPageLoad && initPathName) { + activeTransaction = customStartTransaction({ + name: initPathName, + op: 'pageload', + tags: SENTRY_TAGS, + }); + } + + _useEffect = useEffect; + _useLocation = useLocation; + _useNavigationType = useNavigationType; + _matchRoutes = matchRoutes; + _createRoutesFromChildren = createRoutesFromChildren; + + _customStartTransaction = customStartTransaction; + _startTransactionOnLocationChange = startTransactionOnLocationChange; + }; +} + +const getTransactionName = (routes: RouteObject[], location: Location, matchRoutes: MatchRoutes): string => { + if (!routes || routes.length === 0 || !matchRoutes) { + return location.pathname; + } + + const branches = matchRoutes(routes, location); + + if (branches) { + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let x = 0; x < branches.length; x++) { + if (branches[x].route && branches[x].route.path && branches[x].pathname === location.pathname) { + return branches[x].route.path || location.pathname; + } + } + } + + return location.pathname; +}; + +export function withSentryReactRouterV6Routing

, R extends React.FC

>(Routes: R): R { + if ( + !_useEffect || + !_useLocation || + !_useNavigationType || + !_createRoutesFromChildren || + !_matchRoutes || + !_customStartTransaction + ) { + IS_DEBUG_BUILD && + logger.warn('reactRouterV6Instrumentation was unable to wrap Routes because of one or more missing parameters.'); + + return Routes; + } + + let isBaseLocation: boolean = false; + let routes: RouteObject[]; + + const SentryRoutes: React.FC

= (props: P) => { + const location = _useLocation(); + const navigationType = _useNavigationType(); + + _useEffect(() => { + // Performance concern: + // This is repeated when is rendered. + routes = _createRoutesFromChildren(props.children); + isBaseLocation = true; + + if (activeTransaction) { + activeTransaction.setName(getTransactionName(routes, location, _matchRoutes)); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.children]); + + _useEffect(() => { + if (isBaseLocation) { + if (activeTransaction) { + activeTransaction.finish(); + } + + return; + } + + if (_startTransactionOnLocationChange && (navigationType === 'PUSH' || navigationType === 'POP')) { + if (activeTransaction) { + activeTransaction.finish(); + } + + activeTransaction = _customStartTransaction({ + name: getTransactionName(routes, location, _matchRoutes), + op: 'navigation', + tags: SENTRY_TAGS, + }); + } + }, [props.children, location, navigationType, isBaseLocation]); + + isBaseLocation = false; + + // @ts-ignore Setting more specific React Component typing for `R` generic above + // will break advanced type inference done by react router params + return ; + }; + + hoistNonReactStatics(SentryRoutes, Routes); + + // @ts-ignore Setting more specific React Component typing for `R` generic above + // will break advanced type inference done by react router params + return SentryRoutes; +} diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index e1d0f42c3046..eb67a0a05a7f 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { configureScope } from '@sentry/minimal'; +import { configureScope } from '@sentry/browser'; import { Scope } from '@sentry/types'; interface Action { diff --git a/packages/react/test/reactrouterv3.test.tsx b/packages/react/test/reactrouterv3.test.tsx index e36a8d20d9ab..1faf7c7ce6f0 100644 --- a/packages/react/test/reactrouterv3.test.tsx +++ b/packages/react/test/reactrouterv3.test.tsx @@ -58,7 +58,9 @@ describe('React Router V3', () => { instrumentation(mockStartTransaction); render({routes}); - history.push('/about'); + act(() => { + history.push('/about'); + }); expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/about', @@ -66,7 +68,9 @@ describe('React Router V3', () => { tags: { from: '/', 'routing.instrumentation': 'react-router-v3' }, }); - history.push('/features'); + act(() => { + history.push('/features'); + }); expect(mockStartTransaction).toHaveBeenCalledTimes(3); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/features', @@ -87,7 +91,9 @@ describe('React Router V3', () => { instrumentation(mockStartTransaction); render({routes}); - history.replace('hello'); + act(() => { + history.replace('hello'); + }); expect(mockStartTransaction).toHaveBeenCalledTimes(1); }); @@ -98,7 +104,9 @@ describe('React Router V3', () => { render({routes}); expect(mockStartTransaction).toHaveBeenCalledTimes(1); - history.push('/features'); + act(() => { + history.push('/features'); + }); expect(mockFinish).toHaveBeenCalledTimes(1); expect(mockStartTransaction).toHaveBeenCalledTimes(2); }); diff --git a/packages/react/test/reactrouterv4.test.tsx b/packages/react/test/reactrouterv4.test.tsx index 89caf7b398ee..60f617516482 100644 --- a/packages/react/test/reactrouterv4.test.tsx +++ b/packages/react/test/reactrouterv4.test.tsx @@ -58,7 +58,9 @@ describe('React Router v4', () => { , ); - history.push('/about'); + act(() => { + history.push('/about'); + }); expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/about', @@ -66,7 +68,9 @@ describe('React Router v4', () => { tags: { 'routing.instrumentation': 'react-router-v4' }, }); - history.push('/features'); + act(() => { + history.push('/features'); + }); expect(mockStartTransaction).toHaveBeenCalledTimes(3); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/features', @@ -101,7 +105,9 @@ describe('React Router v4', () => { , ); - history.replace('hello'); + act(() => { + history.replace('hello'); + }); expect(mockStartTransaction).toHaveBeenCalledTimes(1); }); @@ -118,7 +124,9 @@ describe('React Router v4', () => { ); expect(mockStartTransaction).toHaveBeenCalledTimes(1); - history.push('/features'); + act(() => { + history.push('/features'); + }); expect(mockFinish).toHaveBeenCalledTimes(1); expect(mockStartTransaction).toHaveBeenCalledTimes(2); }); @@ -237,7 +245,9 @@ describe('React Router v4', () => { , ); - history.push('/organizations/1234/v1/758'); + act(() => { + history.push('/organizations/1234/v1/758'); + }); expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/organizations/:orgid/v1/:teamid', @@ -245,7 +255,9 @@ describe('React Router v4', () => { tags: { 'routing.instrumentation': 'react-router-v4' }, }); - history.push('/organizations/1234'); + act(() => { + history.push('/organizations/1234'); + }); expect(mockStartTransaction).toHaveBeenCalledTimes(3); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/organizations/:orgid', diff --git a/packages/react/test/reactrouterv5.test.tsx b/packages/react/test/reactrouterv5.test.tsx index 5d9cd66c9898..daa5a4fa5554 100644 --- a/packages/react/test/reactrouterv5.test.tsx +++ b/packages/react/test/reactrouterv5.test.tsx @@ -1,4 +1,4 @@ -import { act,render } from '@testing-library/react'; +import { act, render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; import * as React from 'react'; import { matchPath, Route, Router, Switch } from 'react-router-5'; @@ -58,7 +58,9 @@ describe('React Router v5', () => { , ); - history.push('/about'); + act(() => { + history.push('/about'); + }); expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/about', @@ -66,7 +68,9 @@ describe('React Router v5', () => { tags: { 'routing.instrumentation': 'react-router-v5' }, }); - history.push('/features'); + act(() => { + history.push('/features'); + }); expect(mockStartTransaction).toHaveBeenCalledTimes(3); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/features', @@ -101,7 +105,9 @@ describe('React Router v5', () => { , ); - history.replace('hello'); + act(() => { + history.replace('hello'); + }); expect(mockStartTransaction).toHaveBeenCalledTimes(1); }); @@ -118,7 +124,9 @@ describe('React Router v5', () => { ); expect(mockStartTransaction).toHaveBeenCalledTimes(1); - history.push('/features'); + act(() => { + history.push('/features'); + }); expect(mockFinish).toHaveBeenCalledTimes(1); expect(mockStartTransaction).toHaveBeenCalledTimes(2); }); @@ -238,7 +246,9 @@ describe('React Router v5', () => { , ); - history.push('/organizations/1234/v1/758'); + act(() => { + history.push('/organizations/1234/v1/758'); + }); expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/organizations/:orgid/v1/:teamid', @@ -246,7 +256,9 @@ describe('React Router v5', () => { tags: { 'routing.instrumentation': 'react-router-v5' }, }); - history.push('/organizations/1234'); + act(() => { + history.push('/organizations/1234'); + }); expect(mockStartTransaction).toHaveBeenCalledTimes(3); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/organizations/:orgid', diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx new file mode 100644 index 000000000000..5e30d96a83a9 --- /dev/null +++ b/packages/react/test/reactrouterv6.test.tsx @@ -0,0 +1,190 @@ +import { render } from '@testing-library/react'; +import * as React from 'react'; +import { + createRoutesFromChildren, + matchPath, + matchRoutes, + MemoryRouter, + Navigate, + Route, + Routes, + useLocation, + useNavigationType, +} from 'react-router-6'; + +import { reactRouterV6Instrumentation } from '../src'; +import { withSentryReactRouterV6Routing } from '../src/reactrouterv6'; + +describe('React Router v6', () => { + function createInstrumentation(_opts?: { + startTransactionOnPageLoad?: boolean; + startTransactionOnLocationChange?: boolean; + }): [jest.Mock, { mockSetName: jest.Mock; mockFinish: jest.Mock }] { + const options = { + matchPath: _opts ? matchPath : undefined, + startTransactionOnLocationChange: true, + startTransactionOnPageLoad: true, + ..._opts, + }; + const mockFinish = jest.fn(); + const mockSetName = jest.fn(); + const mockStartTransaction = jest.fn().mockReturnValue({ setName: mockSetName, finish: mockFinish }); + + reactRouterV6Instrumentation( + React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + )(mockStartTransaction, options.startTransactionOnPageLoad, options.startTransactionOnLocationChange); + return [mockStartTransaction, { mockSetName, mockFinish }]; + } + + it('starts a pageload transaction', () => { + const [mockStartTransaction] = createInstrumentation(); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + Home} /> + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(1); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/', + op: 'pageload', + tags: { 'routing.instrumentation': 'react-router-v6' }, + }); + }); + + it('skips pageload transaction with `startTransactionOnPageLoad: false`', () => { + const [mockStartTransaction] = createInstrumentation({ startTransactionOnPageLoad: false }); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + Home} /> + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(0); + }); + + it('skips navigation transaction, with `startTransactionOnLocationChange: false`', () => { + const [mockStartTransaction] = createInstrumentation({ startTransactionOnLocationChange: false }); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + About} /> + } /> + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(1); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/', + op: 'pageload', + tags: { 'routing.instrumentation': 'react-router-v6' }, + }); + }); + + it('starts a navigation transaction', () => { + const [mockStartTransaction] = createInstrumentation(); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + About} /> + } /> + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/about', + op: 'navigation', + tags: { 'routing.instrumentation': 'react-router-v6' }, + }); + }); + + it('works with nested routes', () => { + const [mockStartTransaction] = createInstrumentation(); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + About}> + us} /> + + } /> + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/about/us', + op: 'navigation', + tags: { 'routing.instrumentation': 'react-router-v6' }, + }); + }); + + it('works with paramaterized paths', () => { + const [mockStartTransaction] = createInstrumentation(); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + About}> + page} /> + + } /> + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/about/:page', + op: 'navigation', + tags: { 'routing.instrumentation': 'react-router-v6' }, + }); + }); + + it('works with paths with multiple parameters', () => { + const [mockStartTransaction] = createInstrumentation(); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + Stores}> + Store}> + Product} /> + + + } /> + + , + ); + + expect(mockStartTransaction).toHaveBeenCalledTimes(2); + expect(mockStartTransaction).toHaveBeenLastCalledWith({ + name: '/stores/:storeId/products/:productId', + op: 'navigation', + tags: { 'routing.instrumentation': 'react-router-v6' }, + }); + }); +}); diff --git a/packages/react/test/redux.test.ts b/packages/react/test/redux.test.ts index 55729ddc860b..9c75bc944d91 100644 --- a/packages/react/test/redux.test.ts +++ b/packages/react/test/redux.test.ts @@ -1,4 +1,4 @@ -import * as Sentry from '@sentry/minimal'; +import * as Sentry from '@sentry/browser'; import { Scope } from '@sentry/types'; import * as Redux from 'redux'; @@ -7,7 +7,8 @@ import { createReduxEnhancer } from '../src/redux'; const mockAddBreadcrumb = jest.fn(); const mockSetContext = jest.fn(); -jest.mock('@sentry/minimal', () => ({ +jest.mock('@sentry/browser', () => ({ + ...jest.requireActual('@sentry/browser'), configureScope: (callback: (scope: any) => Partial) => callback({ addBreadcrumb: mockAddBreadcrumb, diff --git a/packages/react/tsconfig.cjs.json b/packages/react/tsconfig.cjs.json deleted file mode 100644 index e3a918fc70af..000000000000 --- a/packages/react/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "build/dist" - } -} diff --git a/packages/react/tsconfig.esm.json b/packages/react/tsconfig.esm.json deleted file mode 100644 index 0b86c52918cc..000000000000 --- a/packages/react/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "build/esm" - } -} diff --git a/packages/serverless/.eslintrc.js b/packages/serverless/.eslintrc.js index ce28fd3a0514..b341e7ca3056 100644 --- a/packages/serverless/.eslintrc.js +++ b/packages/serverless/.eslintrc.js @@ -6,4 +6,18 @@ module.exports = { rules: { '@sentry-internal/sdk/no-async-await': 'off', }, + overrides: [ + { + files: ['scripts/**/*.ts'], + parserOptions: { + project: ['../../tsconfig.dev.json'], + }, + }, + { + files: ['test/**'], + parserOptions: { + sourceType: 'module', + }, + }, + ], }; diff --git a/packages/serverless/.gitignore b/packages/serverless/.gitignore deleted file mode 100644 index c94757f34037..000000000000 --- a/packages/serverless/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist-awslambda-layer/ diff --git a/packages/serverless/README.md b/packages/serverless/README.md index 4b99dde9b424..c0ac6f7659aa 100644 --- a/packages/serverless/README.md +++ b/packages/serverless/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Official Sentry SDK for Serverless environments @@ -60,7 +59,7 @@ Another and much simpler way to integrate Sentry to your AWS Lambda function is 1. Choose Layers -> Add Layer. 2. Specify an ARN: `arn:aws:lambda:us-west-1:TODO:layer:TODO:VERSION`. 3. Go to Environment variables and add: - - `NODE_OPTIONS`: `-r @sentry/serverless/dist/awslambda-auto`. + - `NODE_OPTIONS`: `-r @sentry/serverless/cjs/awslambda-auto`. - `SENTRY_DSN`: `your dsn`. - `SENTRY_TRACES_SAMPLE_RATE`: a number between 0 and 1 representing the chance a transaction is sent to Sentry. For more information, see [docs](https://docs.sentry.io/platforms/node/guides/aws-lambda/configuration/options/#tracesSampleRate). diff --git a/packages/serverless/jest.config.js b/packages/serverless/jest.config.js new file mode 100644 index 000000000000..24f49ab59a4c --- /dev/null +++ b/packages/serverless/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest/jest.config.js'); diff --git a/packages/serverless/package.json b/packages/serverless/package.json index 0200a629e706..c2133f07297f 100644 --- a/packages/serverless/package.json +++ b/packages/serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/serverless", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Official Sentry SDK for various serverless solutions", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/serverless", @@ -9,18 +9,17 @@ "engines": { "node": ">=10" }, - "main": "build/dist/index.js", - "module": "build/esm/index.js", - "types": "build/types/index.d.ts", + "main": "build/npm/cjs/index.js", + "module": "build/npm/esm/index.js", + "types": "build/npm/types/index.d.ts", "publishConfig": { "access": "public" }, "dependencies": { - "@sentry/minimal": "6.19.7", - "@sentry/node": "6.19.7", - "@sentry/tracing": "6.19.7", - "@sentry/types": "6.19.7", - "@sentry/utils": "6.19.7", + "@sentry/node": "7.0.0-rc.0", + "@sentry/tracing": "7.0.0-rc.0", + "@sentry/types": "7.0.0-rc.0", + "@sentry/utils": "7.0.0-rc.0", "@types/aws-lambda": "^8.10.62", "@types/express": "^4.17.2", "tslib": "^1.9.3" @@ -39,26 +38,23 @@ "read-pkg": "^5.2.0" }, "scripts": { - "build": "run-p build:cjs build:esm build:types && yarn build:awslambda-layer", - "build:awslambda-layer": "node scripts/build-awslambda-layer.js", - "build:cjs": "tsc -p tsconfig.cjs.json", - "build:dev": "run-p build:cjs build:esm build:types", - "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "tsc -p tsconfig.esm.json", + "build": "run-p build:rollup build:types build:bundle && yarn build:extras", + "build:awslambda-layer": "echo 'WARNING: AWS lambda layer build emporarily moved to \\`build:bundle\\`.'", + "build:bundle": "yarn ts-node scripts/buildLambdaLayer.ts", + "build:dev": "run-p build:rollup build:types", + "build:extras": "yarn build:awslambda-layer", + "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", - "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", + "build:watch": "run-p build:rollup:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:es5:watch": "yarn build:cjs:watch # *** backwards compatibility - remove in v7 ***", - "build:esm:watch": "tsc -p tsconfig.esm.json --watch", + "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "build:npm": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf dist esm build dist-awslambda-layer coverage", + "clean": "rimraf build dist-awslambda-layer coverage sentry-serverless-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/*.ts\"", - "link:yarn": "yarn link", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", @@ -68,25 +64,5 @@ "volta": { "extends": "../../package.json" }, - "sideEffects": false, - "jest": { - "collectCoverage": true, - "transform": { - "^.+\\.ts$": "ts-jest" - }, - "moduleFileExtensions": [ - "js", - "ts" - ], - "testEnvironment": "node", - "testMatch": [ - "**/*.test.ts" - ], - "globals": { - "ts-jest": { - "tsConfig": "./tsconfig.json", - "diagnostics": false - } - } - } + "sideEffects": false } diff --git a/packages/serverless/rollup.aws.config.js b/packages/serverless/rollup.aws.config.js new file mode 100644 index 000000000000..23c10b757f33 --- /dev/null +++ b/packages/serverless/rollup.aws.config.js @@ -0,0 +1,44 @@ +import { makeBaseBundleConfig, makeBundleConfigVariants, makeBaseNPMConfig } from '../../rollup/index.js'; + +export default [ + // The SDK + ...makeBundleConfigVariants( + makeBaseBundleConfig({ + // this automatically sets it to be CJS + bundleType: 'node', + entrypoints: ['src/index.awslambda.ts'], + jsVersion: 'es6', + licenseTitle: '@sentry/serverless', + outputFileBase: () => 'index', + packageSpecificConfig: { + output: { + dir: 'build/aws/dist-serverless/nodejs/node_modules/@sentry/serverless/build/npm/cjs', + sourcemap: false, + }, + }, + }), + // We only need one copy of the SDK, and we pick the minified one because there's a cap on how big a lambda function + // plus its dependencies can be, and we might as well take up as little of that space as is necessary. We'll rename + // it to be `index.js` in the build script, since it's standing in for the index file of the npm package. + { variants: ['.min.js'] }, + ), + + // This builds a wrapper file, which our lambda layer integration automatically sets up to run as soon as node + // launches (via the `NODE_OPTIONS="-r @sentry/serverless/dist/awslambda-auto"` variable). Note the inclusion in this + // path of the legacy `dist` folder; for backwards compatibility, in the build script we'll copy the file there. + makeBaseNPMConfig({ + entrypoints: ['src/awslambda-auto.ts'], + packageSpecificConfig: { + // Normally `makeNPMConfigVariants` sets both of these values for us, but we don't actually want the ESM variant, + // and the directory structure is different than normal, so we have to do it ourselves. + output: { + format: 'cjs', + dir: 'build/aws/dist-serverless/nodejs/node_modules/@sentry/serverless/build/npm/cjs', + sourcemap: false, + }, + // We only want `awslambda-auto.js`, not the modules that it imports, because they're all included in the bundle + // we generate above + external: ['./index'], + }, + }), +]; diff --git a/packages/serverless/rollup.npm.config.js b/packages/serverless/rollup.npm.config.js new file mode 100644 index 000000000000..4e9641d5879e --- /dev/null +++ b/packages/serverless/rollup.npm.config.js @@ -0,0 +1,12 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + // TODO: `awslambda-auto.ts` is a file which the lambda layer uses to automatically init the SDK. Does it need to be + // in the npm package? Is it possible that some people are using it themselves in the same way the layer uses it (in + // which case removing it would be a breaking change)? Should it stay here or not? + entrypoints: ['src/index.ts', 'src/awslambda-auto.ts'], + // packages with bundles have a different build directory structure + hasBundles: true, + }), +); diff --git a/packages/serverless/scripts/build-awslambda-layer.js b/packages/serverless/scripts/build-awslambda-layer.js deleted file mode 100644 index 6031883e60b7..000000000000 --- a/packages/serverless/scripts/build-awslambda-layer.js +++ /dev/null @@ -1,178 +0,0 @@ -/* eslint-disable no-console */ -const path = require('path'); -const process = require('process'); -const fs = require('fs'); -const childProcess = require('child_process'); - -const findUp = require('find-up'); -const packList = require('npm-packlist'); -const readPkg = require('read-pkg'); - -const serverlessPackage = require('../package.json'); - -if (!process.env.GITHUB_ACTIONS) { - console.log('Skipping build-awslambda-layer script in local environment.'); - process.exit(0); -} - -// AWS Lambda layer are being uploaded as zip archive, whose content is then being unpacked to the /opt -// directory in the lambda environment. -// -// So this script does the following: it builds a 'dist-awslambda-layer/nodejs/node_modules/@sentry/serverless' -// directory with a special index.js and with all necessary @sentry packages symlinked as node_modules. -// Then, this directory is compressed with zip. -// -// The tricky part about it is that one cannot just symlink the entire package directories into node_modules because -// all the src/ contents and other unnecessary files will end up in the zip archive. So, we need to symlink only -// individual files from package and it must be only those of them that are distributable. -// There exists a `npm-packlist` library for such purpose. So we need to traverse all the dependencies, -// execute `npm-packlist` on them and symlink the files into 'dist-awslambda-layer/.../@sentry/serverless/node_modules'. -// I didn't find any way to achieve this goal using standard command-line tools so I have to write this script. -// -// Another, and much simpler way to assemble such zip bundle is install all the dependencies from npm registry and -// just bundle the entire node_modules. -// It's easier and looks more stable but it's inconvenient if one wants build a zip bundle out of current source tree. -// -// And yet another way is to bundle everything with webpack into a single file. I tried and it seems to be error-prone -// so I think it's better to have a classic package directory with node_modules file structure. - -/** Recursively traverse all the dependencies and collect all the info to the map */ -async function collectPackages(cwd, packages = {}) { - const packageJson = await readPkg({ cwd }); - - packages[packageJson.name] = { cwd, packageJson }; - - if (!packageJson.dependencies) { - return packages; - } - - await Promise.all( - Object.keys(packageJson.dependencies).map(async dep => { - // We are interested only in 'external' dependencies which are strictly upper than current directory. - // Internal deps aka local node_modules folder of each package is handled differently. - const searchPath = path.resolve(cwd, '..'); - const depPath = fs.realpathSync( - await findUp(path.join('node_modules', dep), { type: 'directory', cwd: searchPath }), - ); - if (packages[dep]) { - if (packages[dep].cwd != depPath) { - throw new Error(`${packageJson.name}'s dependency ${dep} maps to both ${packages[dep].cwd} and ${depPath}`); - } - return; - } - await collectPackages(depPath, packages); - }), - ); - - return packages; -} - -async function main() { - const workDir = path.resolve(__dirname, '..'); // packages/serverless directory - const distRequirements = path.resolve(workDir, 'build', 'dist'); - if (!fs.existsSync(distRequirements)) { - console.log(`The path ${distRequirements} must exist.`); - return; - } - const packages = await collectPackages(workDir); - - const dist = path.resolve(workDir, 'dist-awslambda-layer'); - const destRootRelative = 'nodejs/node_modules/@sentry/serverless'; - const destRoot = path.resolve(dist, destRootRelative); - const destModulesRoot = path.resolve(destRoot, 'node_modules'); - - try { - // Setting `force: true` ignores exceptions when paths don't exist. - fs.rmSync(destRoot, { force: true, recursive: true, maxRetries: 1 }); - fs.mkdirSync(destRoot, { recursive: true }); - } catch (error) { - // Ignore errors. - } - - await Promise.all( - Object.entries(packages).map(async ([name, pkg]) => { - const isRoot = name == serverlessPackage.name; - const destPath = isRoot ? destRoot : path.resolve(destModulesRoot, name); - - // Scan over the distributable files of the module and symlink each of them. - const sourceFiles = await packList({ path: pkg.cwd }); - await Promise.all( - sourceFiles.map(async filename => { - const sourceFilename = path.resolve(pkg.cwd, filename); - const destFilename = path.resolve(destPath, filename); - - try { - fs.mkdirSync(path.dirname(destFilename), { recursive: true }); - fs.symlinkSync(sourceFilename, destFilename); - } catch (error) { - // Ignore errors. - } - }), - ); - - const sourceModulesRoot = path.resolve(pkg.cwd, 'node_modules'); - // `fs.constants.F_OK` indicates whether the file is visible to the current process, but it doesn't check - // its permissions. For more information, refer to https://nodejs.org/api/fs.html#fs_file_access_constants. - try { - fs.accessSync(path.resolve(sourceModulesRoot), fs.constants.F_OK); - } catch (error) { - return; - } - - // Scan over local node_modules folder of the package and symlink its non-dev dependencies. - const sourceModules = fs.readdirSync(sourceModulesRoot); - await Promise.all( - sourceModules.map(async sourceModule => { - if (!pkg.packageJson.dependencies || !pkg.packageJson.dependencies[sourceModule]) { - return; - } - - const sourceModulePath = path.resolve(sourceModulesRoot, sourceModule); - const destModulePath = path.resolve(destPath, 'node_modules', sourceModule); - - try { - fs.mkdirSync(path.dirname(destModulePath), { recursive: true }); - fs.symlinkSync(sourceModulePath, destModulePath); - } catch (error) { - // Ignore errors. - } - }), - ); - }), - ); - - const version = serverlessPackage.version; - const zipFilename = `sentry-node-serverless-${version}.zip`; - - try { - fs.symlinkSync(path.resolve(destRoot, 'build', 'dist'), path.resolve(destRoot, 'dist')); - fs.symlinkSync(path.resolve(destRoot, 'build', 'esm'), path.resolve(destRoot, 'esm')); - } catch (error) { - console.error(error); - } - - try { - fs.unlinkSync(path.resolve(dist, zipFilename)); - } catch (error) { - // If the ZIP file hasn't been previously created (e.g. running this script for the first time), - // `unlinkSync` will try to delete a non-existing file. This error is ignored. - } - - try { - childProcess.execSync(`zip -r ${zipFilename} ${destRootRelative}`, { cwd: dist }); - } catch (error) { - // The child process timed out or had non-zero exit code. - // The error contains the entire result from `childProcess.spawnSync`. - console.log(error); - } -} - -main().then( - () => { - process.exit(0); - }, - err => { - console.error(err); - process.exit(-1); - }, -); diff --git a/packages/serverless/scripts/buildLambdaLayer.ts b/packages/serverless/scripts/buildLambdaLayer.ts new file mode 100644 index 000000000000..1a04aafde8aa --- /dev/null +++ b/packages/serverless/scripts/buildLambdaLayer.ts @@ -0,0 +1,62 @@ +/* eslint-disable no-console */ +import * as childProcess from 'child_process'; +import * as fs from 'fs'; +import * as rimraf from 'rimraf'; + +import { ensureBundleBuildPrereqs } from '../../../scripts/ensure-bundle-deps'; + +/** + * Run the given shell command, piping the shell process's `stdin`, `stdout`, and `stderr` to that of the current + * process. Returns contents of `stdout`. + */ +function run(cmd: string, options?: childProcess.ExecSyncOptions): string { + return String(childProcess.execSync(cmd, { stdio: 'inherit', ...options })); +} + +async function buildLambdaLayer(): Promise { + // Create the main SDK bundle + await ensureBundleBuildPrereqs({ + dependencies: ['@sentry/utils', '@sentry/hub', '@sentry/core', '@sentry/tracing', '@sentry/node'], + }); + run('yarn rollup --config rollup.aws.config.js'); + + // We build a minified bundle, but it's standing in for the regular `index.js` file listed in `package.json`'s `main` + // property, so we have to rename it so it's findable. + fs.renameSync( + 'build/aws/dist-serverless/nodejs/node_modules/@sentry/serverless/build/npm/cjs/index.min.js', + 'build/aws/dist-serverless/nodejs/node_modules/@sentry/serverless/build/npm/cjs/index.js', + ); + + // We're creating a bundle for the SDK, but still using it in a Node context, so we need to copy in `package.json`, + // purely for its `main` property. + console.log('Copying `package.json` into lambda layer.'); + fs.copyFileSync('package.json', 'build/aws/dist-serverless/nodejs/node_modules/@sentry/serverless/package.json'); + + // The layer also includes `awslambda-auto.js`, a helper file which calls `Sentry.init()` and wraps the lambda + // handler. It gets run when Node is launched inside the lambda, using the environment variable + // + // `NODE_OPTIONS="-r @sentry/serverless/dist/awslambda-auto"`. + // + // (The`-r` is what runs the script on startup.) The `dist` directory is no longer where we emit our built code, so + // for backwards compatibility, we create a symlink. + console.log('Creating symlink for `awslambda-auto.js` in legacy `dist` directory.'); + fsForceMkdirSync('build/aws/dist-serverless/nodejs/node_modules/@sentry/serverless/dist'); + fs.symlinkSync( + '../build/npm/cjs/awslambda-auto.js', + 'build/aws/dist-serverless/nodejs/node_modules/@sentry/serverless/dist/awslambda-auto.js', + ); +} + +void buildLambdaLayer(); + +/** + * Make a directory synchronously, overwriting the old directory if necessary. + * + * This is what `fs.mkdirSync(path, { force: true })` would be, if it existed. Primarily useful for local building and + * testing, where scripts are often run more than once (and so the directory you're trying to create may already be + * there), but also harmless when used in CI. + */ +function fsForceMkdirSync(path: string): void { + rimraf.sync(path); + fs.mkdirSync(path); +} diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index 3702ffc6a1fd..a2825b1e1e2e 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -11,7 +11,7 @@ import { } from '@sentry/node'; import { extractTraceparentData } from '@sentry/tracing'; import { Integration } from '@sentry/types'; -import { isString, logger, SentryError } from '@sentry/utils'; +import { extensionRelayDSN, isString, logger, parseBaggageString } from '@sentry/utils'; // NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil // eslint-disable-next-line import/no-unresolved import { Context, Handler } from 'aws-lambda'; @@ -54,7 +54,6 @@ export interface WrapperOptions { * @default false */ captureAllSettledReasons: boolean; - ignoreSentryErrors: boolean; } export const defaultIntegrations: Integration[] = [...Sentry.defaultIntegrations, new AWSServices({ optional: true })]; @@ -80,6 +79,8 @@ export function init(options: Sentry.NodeOptions = {}): void { version: Sentry.SDK_VERSION, }; + options.dsn = extensionRelayDSN(options.dsn); + Sentry.init(options); Sentry.addGlobalEventProcessor(serverlessEventProcessor); } @@ -226,7 +227,6 @@ export function wrapHandler( captureTimeoutWarning: true, timeoutWarningLimit: 500, captureAllSettledReasons: false, - ignoreSentryErrors: false, ...wrapOptions, }; let timeoutWarningTimer: NodeJS.Timeout; @@ -277,9 +277,9 @@ export function wrapHandler( timeoutWarningTimer = setTimeout(() => { withScope(scope => { scope.setTag('timeout', humanReadableTimeout); - captureMessage(`Possible function timeout: ${context.functionName}`, Sentry.Severity.Warning); + captureMessage(`Possible function timeout: ${context.functionName}`, 'warning'); }); - }, timeoutWarningDelay); + }, timeoutWarningDelay) as unknown as NodeJS.Timeout; } // Applying `sentry-trace` to context @@ -288,10 +288,17 @@ export function wrapHandler( if (eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace'])) { traceparentData = extractTraceparentData(eventWithHeaders.headers['sentry-trace']); } + + const baggage = + eventWithHeaders.headers && + isString(eventWithHeaders.headers.baggage) && + parseBaggageString(eventWithHeaders.headers.baggage); + const transaction = startTransaction({ name: context.functionName, op: 'awslambda.handler', ...traceparentData, + ...(baggage && { metadata: { baggage: baggage } }), }); const hub = getCurrentHub(); @@ -318,11 +325,7 @@ export function wrapHandler( transaction.finish(); hub.popScope(); await flush(options.flushTimeout).catch(e => { - if (options.ignoreSentryErrors && e instanceof SentryError) { - IS_DEBUG_BUILD && logger.error(e); - return; - } - throw e; + IS_DEBUG_BUILD && logger.error(e); }); } return rv; diff --git a/packages/serverless/src/gcpfunction/cloud_events.ts b/packages/serverless/src/gcpfunction/cloud_events.ts index f4c79c410d9c..ae993128139f 100644 --- a/packages/serverless/src/gcpfunction/cloud_events.ts +++ b/packages/serverless/src/gcpfunction/cloud_events.ts @@ -61,11 +61,11 @@ function _wrapCloudEventFunction( transaction.finish(); void flush(options.flushTimeout) - .then(() => { - callback(...args); - }) .then(null, e => { IS_DEBUG_BUILD && logger.error(e); + }) + .then(() => { + callback(...args); }); }); diff --git a/packages/serverless/src/gcpfunction/events.ts b/packages/serverless/src/gcpfunction/events.ts index e47efc1571eb..4b29a26270e4 100644 --- a/packages/serverless/src/gcpfunction/events.ts +++ b/packages/serverless/src/gcpfunction/events.ts @@ -56,11 +56,11 @@ function _wrapEventFunction( transaction.finish(); void flush(options.flushTimeout) - .then(() => { - callback(...args); - }) .then(null, e => { IS_DEBUG_BUILD && logger.error(e); + }) + .then(() => { + callback(...args); }); }); diff --git a/packages/serverless/src/gcpfunction/general.ts b/packages/serverless/src/gcpfunction/general.ts index becad86b5b0f..9c5f95615506 100644 --- a/packages/serverless/src/gcpfunction/general.ts +++ b/packages/serverless/src/gcpfunction/general.ts @@ -1,6 +1,6 @@ import { Scope } from '@sentry/node'; import { Context as SentryContext } from '@sentry/types'; -import { Request, Response } from 'express'; // eslint-disable-line import/no-extraneous-dependencies +import type { Request, Response } from 'express'; import { hostname } from 'os'; export interface HttpFunction { @@ -62,4 +62,4 @@ export function configureScopeWithContext(scope: Scope, context: Context): void scope.setContext('gcp.function.context', { ...context } as SentryContext); } -export { Request, Response }; +export type { Request, Response }; diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts index 607656fb3dbd..26c32b4a21cb 100644 --- a/packages/serverless/src/gcpfunction/http.ts +++ b/packages/serverless/src/gcpfunction/http.ts @@ -1,6 +1,6 @@ import { captureException, flush, getCurrentHub, Handlers, startTransaction } from '@sentry/node'; import { extractTraceparentData } from '@sentry/tracing'; -import { isString, logger, stripUrlQueryAndFragment } from '@sentry/utils'; +import { isString, logger, parseBaggageString, stripUrlQueryAndFragment } from '@sentry/utils'; import { IS_DEBUG_BUILD } from '../flags'; import { domainify, getActiveDomain, proxyFunction } from './../utils'; @@ -56,10 +56,17 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial void), encoding?: string | (() => void), cb?: () => void): void { + res.end = function (chunk?: any | (() => void), encoding?: string | (() => void), cb?: () => void): any { transaction.setHttpStatus(res.statusCode); transaction.finish(); void flush(options.flushTimeout) - .then(() => { - _end.call(this, chunk, encoding, cb); - }) .then(null, e => { IS_DEBUG_BUILD && logger.error(e); + }) + .then(() => { + _end.call(this, chunk, encoding, cb); }); }; diff --git a/packages/serverless/src/index.awslambda.ts b/packages/serverless/src/index.awslambda.ts new file mode 100644 index 000000000000..c097591ab9dc --- /dev/null +++ b/packages/serverless/src/index.awslambda.ts @@ -0,0 +1,8 @@ +/** This file is used as the entrypoint for the lambda layer bundle, and is not included in the npm package. */ + +// https://medium.com/unsplash/named-namespace-imports-7345212bbffb +import * as AWSLambda from './awslambda'; +export { AWSLambda }; + +export * from './awsservices'; +export * from '@sentry/node'; diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index ab0c38d2f08a..078eff338ea9 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -1,4 +1,3 @@ -import { SentryError } from '@sentry/utils'; // NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil // eslint-disable-next-line import/no-unresolved import { Callback, Handler } from 'aws-lambda'; @@ -178,44 +177,6 @@ describe('AWSLambda', () => { expect(Sentry.captureException).toHaveBeenNthCalledWith(2, error2); expect(Sentry.captureException).toBeCalledTimes(2); }); - - test('ignoreSentryErrors - with successful handler', async () => { - const sentryError = new SentryError('HTTP Error (429)'); - jest.spyOn(Sentry, 'flush').mockRejectedValueOnce(sentryError); - - const handledError = new Error('handled error, and we want to monitor it'); - const expectedSuccessStatus = { status: 'success', reason: 'we handled error, so success' }; - const handler = () => { - Sentry.captureException(handledError); - return Promise.resolve(expectedSuccessStatus); - }; - const wrappedHandlerWithoutIgnoringSentryErrors = wrapHandler(handler, { ignoreSentryErrors: false }); - const wrappedHandlerWithIgnoringSentryErrors = wrapHandler(handler, { ignoreSentryErrors: true }); - - await expect(wrappedHandlerWithoutIgnoringSentryErrors(fakeEvent, fakeContext, fakeCallback)).rejects.toThrow( - sentryError, - ); - await expect(wrappedHandlerWithIgnoringSentryErrors(fakeEvent, fakeContext, fakeCallback)).resolves.toBe( - expectedSuccessStatus, - ); - }); - - test('ignoreSentryErrors - with failed handler', async () => { - const sentryError = new SentryError('HTTP Error (429)'); - jest.spyOn(Sentry, 'flush').mockRejectedValueOnce(sentryError); - - const criticalUnhandledError = new Error('critical unhandled error '); - const handler = () => Promise.reject(criticalUnhandledError); - const wrappedHandlerWithoutIgnoringSentryErrors = wrapHandler(handler, { ignoreSentryErrors: false }); - const wrappedHandlerWithIgnoringSentryErrors = wrapHandler(handler, { ignoreSentryErrors: true }); - - await expect(wrappedHandlerWithoutIgnoringSentryErrors(fakeEvent, fakeContext, fakeCallback)).rejects.toThrow( - sentryError, - ); - await expect(wrappedHandlerWithIgnoringSentryErrors(fakeEvent, fakeContext, fakeCallback)).rejects.toThrow( - criticalUnhandledError, - ); - }); }); describe('wrapHandler() on sync handler', () => { @@ -268,6 +229,40 @@ describe('AWSLambda', () => { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); }); + test('incoming trace headers are correctly parsed and used', async () => { + expect.assertions(1); + + fakeEvent.headers = { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0', + baggage: 'sentry-release=2.12.1,maisey=silly,charlie=goofy', + }; + + const handler: Handler = (_event, _context, callback) => { + expect(Sentry.startTransaction).toBeCalledWith( + expect.objectContaining({ + parentSpanId: '1121201211212012', + parentSampled: false, + op: 'awslambda.handler', + name: 'functionName', + traceId: '12312012123120121231201212312012', + metadata: { + baggage: [ + { + release: '2.12.1', + }, + 'maisey=silly,charlie=goofy', + ], + }, + }), + ); + + callback(undefined, { its: 'fine' }); + }; + + const wrappedHandler = wrapHandler(handler); + await wrappedHandler(fakeEvent, fakeContext, fakeCallback); + }); + test('capture error', async () => { expect.assertions(10); @@ -345,6 +340,21 @@ describe('AWSLambda', () => { expect(Sentry.flush).toBeCalled(); } }); + + test('should not throw when flush rejects', async () => { + const handler: Handler = async () => { + // Friendly handler with no errors :) + return 'some string'; + }; + + const wrappedHandler = wrapHandler(handler); + + jest.spyOn(Sentry, 'flush').mockImplementationOnce(async () => { + throw new Error(); + }); + + await expect(wrappedHandler(fakeEvent, fakeContext, fakeCallback)).resolves.toBe('some string'); + }); }); describe('wrapHandler() on async handler with a callback method (aka incorrect usage)', () => { diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index 0a77742a33c6..89b6aa084b9b 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -120,6 +120,39 @@ describe('GCPFunction', () => { expect(Sentry.flush).toBeCalledWith(2000); }); + test('incoming trace headers are correctly parsed and used', async () => { + expect.assertions(1); + + const handler: HttpFunction = (_req, res) => { + res.statusCode = 200; + res.end(); + }; + const wrappedHandler = wrapHttpFunction(handler); + const traceHeaders = { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0', + baggage: 'sentry-release=2.12.1,maisey=silly,charlie=goofy', + }; + await handleHttp(wrappedHandler, traceHeaders); + + expect(Sentry.startTransaction).toBeCalledWith( + expect.objectContaining({ + name: 'POST /path', + op: 'gcp.function.http', + traceId: '12312012123120121231201212312012', + parentSpanId: '1121201211212012', + parentSampled: false, + metadata: { + baggage: [ + { + release: '2.12.1', + }, + 'maisey=silly,charlie=goofy', + ], + }, + }), + ); + }); + test('capture error', async () => { expect.assertions(5); @@ -148,6 +181,34 @@ describe('GCPFunction', () => { expect(Sentry.fakeTransaction.finish).toBeCalled(); expect(Sentry.flush).toBeCalled(); }); + + test('should not throw when flush rejects', async () => { + expect.assertions(2); + + const handler: HttpFunction = async (_req, res) => { + res.statusCode = 200; + res.end(); + }; + + const wrappedHandler = wrapHttpFunction(handler); + + const request = { + method: 'POST', + url: '/path?q=query', + headers: { host: 'hostname', 'content-type': 'application/json' }, + body: { foo: 'bar' }, + } as Request; + + const mockEnd = jest.fn(); + const response = { end: mockEnd } as unknown as Response; + + jest.spyOn(Sentry, 'flush').mockImplementationOnce(async () => { + throw new Error(); + }); + + await expect(wrappedHandler(request, response)).resolves.toBeUndefined(); + expect(mockEnd).toHaveBeenCalledTimes(1); + }); }); test('wrapHttpFunction request data', async () => { diff --git a/packages/serverless/test/google-cloud-http.test.ts b/packages/serverless/test/google-cloud-http.test.ts index fced84730b00..7d785462a4d9 100644 --- a/packages/serverless/test/google-cloud-http.test.ts +++ b/packages/serverless/test/google-cloud-http.test.ts @@ -62,6 +62,7 @@ describe('GoogleCloudHttp tracing', () => { op: 'gcloud.http.bigquery', description: 'POST /jobs', }); + // @ts-ignore see "Why @ts-ignore" note expect(Sentry.fakeTransaction.startChild).toBeCalledWith({ op: 'gcloud.http.bigquery', description: expect.stringMatching(new RegExp('^GET /queries/.+')), diff --git a/packages/serverless/tsconfig.cjs.json b/packages/serverless/tsconfig.cjs.json deleted file mode 100644 index e3a918fc70af..000000000000 --- a/packages/serverless/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "build/dist" - } -} diff --git a/packages/serverless/tsconfig.esm.json b/packages/serverless/tsconfig.esm.json deleted file mode 100644 index 0b86c52918cc..000000000000 --- a/packages/serverless/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "build/esm" - } -} diff --git a/packages/serverless/tsconfig.types.json b/packages/serverless/tsconfig.types.json index 65455f66bd75..4c51bd21e64b 100644 --- a/packages/serverless/tsconfig.types.json +++ b/packages/serverless/tsconfig.types.json @@ -1,10 +1,14 @@ { "extends": "./tsconfig.json", + // We don't ship this in the npm package (it exists purely for controlling what ends up in the AWS lambda layer), so + // no need to build types for it + "exclude": ["src/index.awslambda.ts"], + "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "outDir": "build/types" + "outDir": "build/npm/types" } } diff --git a/packages/tracing/.npmignore b/packages/tracing/.npmignore deleted file mode 100644 index 4ab7cc4f278f..000000000000 --- a/packages/tracing/.npmignore +++ /dev/null @@ -1,11 +0,0 @@ -# The paths in this file are specified so that they align with the file structure in `./build` after this file is copied -# into it by the prepack script `scripts/prepack.ts`. - -* - -# TODO remove bundles (which in the tarball are inside `build`) in v7 -!/build/**/* - -!/dist/**/* -!/esm/**/* -!/types/**/* diff --git a/packages/tracing/README.md b/packages/tracing/README.md index 5454433f73b8..d37633c9decd 100644 --- a/packages/tracing/README.md +++ b/packages/tracing/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Sentry Tracing Extensions diff --git a/packages/tracing/jest.config.js b/packages/tracing/jest.config.js new file mode 100644 index 000000000000..24f49ab59a4c --- /dev/null +++ b/packages/tracing/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest/jest.config.js'); diff --git a/packages/tracing/package.json b/packages/tracing/package.json index 64f3e83b9cea..ca22615d94e7 100644 --- a/packages/tracing/package.json +++ b/packages/tracing/package.json @@ -1,55 +1,49 @@ { "name": "@sentry/tracing", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Extensions for Sentry AM", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tracing", "author": "Sentry", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=8" }, - "main": "build/npm/dist/index.js", + "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", "types": "build/npm/types/index.d.ts", "publishConfig": { "access": "public" }, "dependencies": { - "@sentry/hub": "6.19.7", - "@sentry/minimal": "6.19.7", - "@sentry/types": "6.19.7", - "@sentry/utils": "6.19.7", + "@sentry/hub": "7.0.0-rc.0", + "@sentry/types": "7.0.0-rc.0", + "@sentry/utils": "7.0.0-rc.0", "tslib": "^1.9.3" }, "devDependencies": { - "@sentry/browser": "6.19.7", - "@types/express": "^4.17.1", - "@types/jsdom": "^16.2.3", - "jsdom": "^16.2.2" + "@sentry/browser": "7.0.0-rc.0", + "@types/express": "^4.17.1" }, "scripts": { - "build": "run-p build:cjs build:esm build:types build:bundle && ts-node ../../scripts/prepack.ts --bundles #necessary for integration tests", - "build:bundle": "rollup --config", - "build:cjs": "tsc -p tsconfig.cjs.json", - "build:dev": "run-p build:cjs build:esm build:types", - "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "tsc -p tsconfig.esm.json", + "build": "run-p build:rollup build:types build:bundle && yarn build:extras #necessary for integration tests", + "build:bundle": "yarn ts-node ../../scripts/ensure-bundle-deps.ts && yarn rollup --config rollup.bundle.config.js", + "build:dev": "run-p build:rollup build:types", + "build:extras": "yarn build:prepack", + "build:prepack": "ts-node ../../scripts/prepack.ts --bundles", + "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:cjs:watch build:esm:watch build:bundle:watch build:types:watch", - "build:bundle:watch": "rollup --config --watch", - "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", - "build:dev:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", - "build:es5:watch": "yarn build:cjs:watch # *** backwards compatibility - remove in v7 ***", - "build:esm:watch": "tsc -p tsconfig.esm.json --watch", + "build:watch": "run-p build:rollup:watch build:bundle:watch build:types:watch", + "build:bundle:watch": "rollup --config rollup.bundle.config.js --watch", + "build:dev:watch": "run-p build:rollup:watch build:types:watch", + "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:npm": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", - "clean": "rimraf dist esm build coverage", + "clean": "rimraf build coverage sentry-tracing-*.tgz", "circularDepCheck": "madge --circular src/index.ts", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/*.ts\"", - "link:yarn": "yarn link", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", @@ -59,30 +53,10 @@ "volta": { "extends": "../../package.json" }, - "jest": { - "collectCoverage": true, - "transform": { - "^.+\\.ts$": "ts-jest" - }, - "moduleFileExtensions": [ - "js", - "ts" - ], - "testEnvironment": "node", - "testMatch": [ - "**/*.test.ts" - ], - "globals": { - "ts-jest": { - "tsConfig": "./tsconfig.json", - "diagnostics": false - } - } - }, "sideEffects": [ - "./dist/index.js", + "./cjs/index.js", "./esm/index.js", - "./build/npm/dist/index.js", + "./build/npm/cjs/index.js", "./build/npm/esm/index.js", "./src/index.ts" ] diff --git a/packages/tracing/rollup.bundle.config.js b/packages/tracing/rollup.bundle.config.js new file mode 100644 index 000000000000..0d5f4fcc8867 --- /dev/null +++ b/packages/tracing/rollup.bundle.config.js @@ -0,0 +1,17 @@ +import { makeBaseBundleConfig, makeBundleConfigVariants } from '../../rollup/index.js'; + +const builds = []; + +['es5', 'es6'].forEach(jsVersion => { + const baseBundleConfig = makeBaseBundleConfig({ + bundleType: 'standalone', + entrypoints: ['src/index.bundle.ts'], + jsVersion, + licenseTitle: '@sentry/tracing & @sentry/browser', + outputFileBase: () => `bundles/bundle.tracing${jsVersion === 'es5' ? '.es5' : ''}`, + }); + + builds.push(...makeBundleConfigVariants(baseBundleConfig)); +}); + +export default builds; diff --git a/packages/tracing/rollup.config.js b/packages/tracing/rollup.config.js deleted file mode 100644 index 529ddea29a1b..000000000000 --- a/packages/tracing/rollup.config.js +++ /dev/null @@ -1,17 +0,0 @@ -import { makeBaseBundleConfig, makeConfigVariants } from '../../rollup.config'; - -const builds = []; - -['es5', 'es6'].forEach(jsVersion => { - const baseBundleConfig = makeBaseBundleConfig({ - input: 'src/index.bundle.ts', - isAddOn: false, - jsVersion, - licenseTitle: '@sentry/tracing & @sentry/browser', - outputFileBase: `bundles/bundle.tracing${jsVersion === 'es6' ? '.es6' : ''}`, - }); - - builds.push(...makeConfigVariants(baseBundleConfig)); -}); - -export default builds; diff --git a/packages/tracing/rollup.npm.config.js b/packages/tracing/rollup.npm.config.js new file mode 100644 index 000000000000..4ffa8b9396d8 --- /dev/null +++ b/packages/tracing/rollup.npm.config.js @@ -0,0 +1,8 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + // packages with bundles have a different build directory structure + hasBundles: true, + }), +); diff --git a/packages/tracing/src/browser/backgroundtab.ts b/packages/tracing/src/browser/backgroundtab.ts index 2448247188bd..64bb06d2d70f 100644 --- a/packages/tracing/src/browser/backgroundtab.ts +++ b/packages/tracing/src/browser/backgroundtab.ts @@ -1,6 +1,5 @@ import { getGlobalObject, logger } from '@sentry/utils'; -import { FINISH_REASON_TAG, IDLE_TRANSACTION_FINISH_REASONS } from '../constants'; import { IS_DEBUG_BUILD } from '../flags'; import { IdleTransaction } from '../idletransaction'; import { SpanStatusType } from '../span'; @@ -29,7 +28,6 @@ export function registerBackgroundTabDetection(): void { activeTransaction.setStatus(statusType); } activeTransaction.setTag('visibilitychange', 'document.hidden'); - activeTransaction.setTag(FINISH_REASON_TAG, IDLE_TRANSACTION_FINISH_REASONS[2]); activeTransaction.finish(); } }); diff --git a/packages/tracing/src/browser/browsertracing.ts b/packages/tracing/src/browser/browsertracing.ts index e9d02db6d7d7..8995775a8a41 100644 --- a/packages/tracing/src/browser/browsertracing.ts +++ b/packages/tracing/src/browser/browsertracing.ts @@ -1,13 +1,13 @@ import { Hub } from '@sentry/hub'; import { EventProcessor, Integration, Transaction, TransactionContext } from '@sentry/types'; -import { getGlobalObject, logger } from '@sentry/utils'; +import { getGlobalObject, logger, parseBaggageString } from '@sentry/utils'; import { IS_DEBUG_BUILD } from '../flags'; import { startIdleTransaction } from '../hubextensions'; -import { DEFAULT_IDLE_TIMEOUT, IdleTransaction } from '../idletransaction'; -import { extractTraceparentData, secToMs } from '../utils'; +import { DEFAULT_FINAL_TIMEOUT, DEFAULT_IDLE_TIMEOUT } from '../idletransaction'; +import { extractTraceparentData } from '../utils'; import { registerBackgroundTabDetection } from './backgroundtab'; -import { MetricsInstrumentation } from './metrics'; +import { addPerformanceEntries, startTrackingWebVitals } from './metrics'; import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests, @@ -15,19 +15,31 @@ import { } from './request'; import { instrumentRoutingWithDefaults } from './router'; -export const DEFAULT_MAX_TRANSACTION_DURATION_SECONDS = 600; +export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing'; /** Options for Browser Tracing integration */ export interface BrowserTracingOptions extends RequestInstrumentationOptions { /** - * The time to wait in ms until the transaction will be finished. The transaction will use the end timestamp of - * the last finished span as the endtime for the transaction. + * The time to wait in ms until the transaction will be finished during an idle state. An idle state is defined + * by a moment where there are no in-progress spans. + * + * The transaction will use the end timestamp of the last finished span as the endtime for the transaction. + * If there are still active spans when this the `idleTimeout` is set, the `idleTimeout` will get reset. * Time is in ms. * * Default: 1000 */ idleTimeout: number; + /** + * The max duration for a transaction. If a transaction duration hits the `finalTimeout` value, it + * will be finished. + * Time is in ms. + * + * Default: 30000 + */ + finalTimeout: number; + /** * Flag to enable/disable creation of `navigation` transaction on history changes. * @@ -42,15 +54,6 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions { */ startTransactionOnPageLoad: boolean; - /** - * The maximum duration of a transaction before it will be marked as "deadline_exceeded". - * If you never want to mark a transaction set it to 0. - * Time is in seconds. - * - * Default: 600 - */ - maxTransactionDuration: number; - /** * Flag Transactions where tabs moved to background with "cancelled". Browser background tab timing is * not suited towards doing precise measurements of operations. By default, we recommend that this option @@ -94,8 +97,8 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions { const DEFAULT_BROWSER_TRACING_OPTIONS = { idleTimeout: DEFAULT_IDLE_TIMEOUT, + finalTimeout: DEFAULT_FINAL_TIMEOUT, markBackgroundTransactions: true, - maxTransactionDuration: DEFAULT_MAX_TRANSACTION_DURATION_SECONDS, routingInstrumentation: instrumentRoutingWithDefaults, startTransactionOnLocationChange: true, startTransactionOnPageLoad: true, @@ -110,10 +113,10 @@ const DEFAULT_BROWSER_TRACING_OPTIONS = { * any routing library. This integration uses {@see IdleTransaction} to create transactions. */ export class BrowserTracing implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'BrowserTracing'; + // This class currently doesn't have a static `id` field like the other integration classes, because it prevented + // @sentry/tracing from being treeshaken. Tree shakers do not like static fields, because they behave like side effects. + // TODO: Come up with a better plan, than using static fields on integration classes, and use that plan on all + // integrations. /** Browser Tracing integration options */ public options: BrowserTracingOptions; @@ -121,22 +124,16 @@ export class BrowserTracing implements Integration { /** * @inheritDoc */ - public name: string = BrowserTracing.id; + public name: string = BROWSER_TRACING_INTEGRATION_ID; private _getCurrentHub?: () => Hub; - private readonly _metrics: MetricsInstrumentation; - private readonly _emitOptionsWarning?: boolean; - /** Store configured idle timeout so that it can be added as a tag to transactions */ - private _configuredIdleTimeout: BrowserTracingOptions['idleTimeout'] | undefined = undefined; - public constructor(_options?: Partial) { let tracingOrigins = defaultRequestInstrumentationOptions.tracingOrigins; // NOTE: Logger doesn't work in constructors, as it's initialized after integrations instances if (_options) { - this._configuredIdleTimeout = _options.idleTimeout; if (_options.tracingOrigins && Array.isArray(_options.tracingOrigins) && _options.tracingOrigins.length !== 0) { tracingOrigins = _options.tracingOrigins; } else { @@ -151,7 +148,7 @@ export class BrowserTracing implements Integration { }; const { _metricOptions } = this.options; - this._metrics = new MetricsInstrumentation(_metricOptions && _metricOptions._reportAllChanges); + startTrackingWebVitals(_metricOptions && _metricOptions._reportAllChanges); } /** @@ -205,9 +202,9 @@ export class BrowserTracing implements Integration { } // eslint-disable-next-line @typescript-eslint/unbound-method - const { beforeNavigate, idleTimeout, maxTransactionDuration } = this.options; + const { beforeNavigate, idleTimeout, finalTimeout } = this.options; - const parentContextFromHeader = context.op === 'pageload' ? getHeaderContext() : undefined; + const parentContextFromHeader = context.op === 'pageload' ? extractTraceDataFromMetaTags() : undefined; const expandedContext = { ...context, @@ -233,29 +230,39 @@ export class BrowserTracing implements Integration { hub, finalContext, idleTimeout, + finalTimeout, true, { location }, // for use in the tracesSampler ); - idleTransaction.registerBeforeFinishCallback((transaction, endTimestamp) => { - this._metrics.addPerformanceEntries(transaction); - adjustTransactionDuration(secToMs(maxTransactionDuration), transaction, endTimestamp); + idleTransaction.registerBeforeFinishCallback(transaction => { + addPerformanceEntries(transaction); + transaction.setTag( + 'sentry_reportAllChanges', + Boolean(this.options._metricOptions && this.options._metricOptions._reportAllChanges), + ); }); - idleTransaction.setTag('idleTimeout', this._configuredIdleTimeout); - return idleTransaction as Transaction; } } /** - * Gets transaction context from a sentry-trace meta. - * - * @returns Transaction context data from the header or undefined if there's no header or the header is malformed + * Gets transaction context data from `sentry-trace` and `baggage` tags. + * @returns Transaction context data or undefined neither tag exists or has valid data */ -export function getHeaderContext(): Partial | undefined { - const header = getMetaContent('sentry-trace'); - if (header) { - return extractTraceparentData(header); +export function extractTraceDataFromMetaTags(): Partial | undefined { + const sentrytraceValue = getMetaContent('sentry-trace'); + const baggageValue = getMetaContent('baggage'); + + const sentrytraceData = sentrytraceValue ? extractTraceparentData(sentrytraceValue) : undefined; + const baggage = baggageValue ? parseBaggageString(baggageValue) : undefined; + + // TODO more extensive checks for baggage validity/emptyness? + if (sentrytraceData || baggage) { + return { + ...(sentrytraceData && sentrytraceData), + ...(baggage && { metadata: { baggage } }), + }; } return undefined; @@ -263,16 +270,13 @@ export function getHeaderContext(): Partial | undefined { /** Returns the value of a meta tag */ export function getMetaContent(metaName: string): string | null { - const el = getGlobalObject().document.querySelector(`meta[name=${metaName}]`); - return el ? el.getAttribute('content') : null; -} - -/** Adjusts transaction value based on max transaction duration */ -function adjustTransactionDuration(maxDuration: number, transaction: IdleTransaction, endTimestamp: number): void { - const diff = endTimestamp - transaction.startTimestamp; - const isOutdatedTransaction = endTimestamp && (diff > maxDuration || diff < 0); - if (isOutdatedTransaction) { - transaction.setStatus('deadline_exceeded'); - transaction.setTag('maxTransactionDurationExceeded', 'true'); + const globalObject = getGlobalObject(); + + // DOM/querySelector is not available in all environments + if (globalObject.document && globalObject.document.querySelector) { + const el = globalObject.document.querySelector(`meta[name=${metaName}]`); + return el ? el.getAttribute('content') : null; + } else { + return null; } } diff --git a/packages/tracing/src/browser/index.ts b/packages/tracing/src/browser/index.ts index dd022fe2b8ec..dcf4a08270fd 100644 --- a/packages/tracing/src/browser/index.ts +++ b/packages/tracing/src/browser/index.ts @@ -1,6 +1,4 @@ -export { BrowserTracing } from './browsertracing'; -export { - instrumentOutgoingRequests, - RequestInstrumentationOptions, - defaultRequestInstrumentationOptions, -} from './request'; +export type { RequestInstrumentationOptions } from './request'; + +export { BrowserTracing, BROWSER_TRACING_INTEGRATION_ID } from './browsertracing'; +export { instrumentOutgoingRequests, defaultRequestInstrumentationOptions } from './request'; diff --git a/packages/tracing/src/browser/metrics.ts b/packages/tracing/src/browser/metrics.ts deleted file mode 100644 index 8386b608f247..000000000000 --- a/packages/tracing/src/browser/metrics.ts +++ /dev/null @@ -1,433 +0,0 @@ -/* eslint-disable max-lines */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Measurements, SpanContext } from '@sentry/types'; -import { browserPerformanceTimeOrigin, getGlobalObject, htmlTreeAsString, isNodeEnv, logger } from '@sentry/utils'; - -import { IS_DEBUG_BUILD } from '../flags'; -import { Span } from '../span'; -import { Transaction } from '../transaction'; -import { msToSec } from '../utils'; -import { getCLS, LayoutShift } from './web-vitals/getCLS'; -import { getFID } from './web-vitals/getFID'; -import { getLCP, LargestContentfulPaint } from './web-vitals/getLCP'; -import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher'; -import { NavigatorDeviceMemory, NavigatorNetworkInformation } from './web-vitals/types'; - -const global = getGlobalObject(); - -/** Class tracking metrics */ -export class MetricsInstrumentation { - private _measurements: Measurements = {}; - - private _performanceCursor: number = 0; - private _lcpEntry: LargestContentfulPaint | undefined; - private _clsEntry: LayoutShift | undefined; - - public constructor(private _reportAllChanges: boolean = false) { - if (!isNodeEnv() && global && global.performance && global.document) { - if (global.performance.mark) { - global.performance.mark('sentry-tracing-init'); - } - - this._trackCLS(); - this._trackLCP(); - this._trackFID(); - } - } - - /** Add performance related spans to a transaction */ - public addPerformanceEntries(transaction: Transaction): void { - if (!global || !global.performance || !global.performance.getEntries || !browserPerformanceTimeOrigin) { - // Gatekeeper if performance API not available - return; - } - - IS_DEBUG_BUILD && logger.log('[Tracing] Adding & adjusting spans using Performance API'); - - const timeOrigin = msToSec(browserPerformanceTimeOrigin); - - let responseStartTimestamp: number | undefined; - let requestStartTimestamp: number | undefined; - - global.performance - .getEntries() - .slice(this._performanceCursor) - .forEach((entry: Record) => { - const startTime = msToSec(entry.startTime as number); - const duration = msToSec(entry.duration as number); - - if (transaction.op === 'navigation' && timeOrigin + startTime < transaction.startTimestamp) { - return; - } - - switch (entry.entryType) { - case 'navigation': { - addNavigationSpans(transaction, entry, timeOrigin); - responseStartTimestamp = timeOrigin + msToSec(entry.responseStart as number); - requestStartTimestamp = timeOrigin + msToSec(entry.requestStart as number); - break; - } - case 'mark': - case 'paint': - case 'measure': { - const startTimestamp = addMeasureSpans(transaction, entry, startTime, duration, timeOrigin); - // capture web vitals - - const firstHidden = getVisibilityWatcher(); - // Only report if the page wasn't hidden prior to the web vital. - const shouldRecord = entry.startTime < firstHidden.firstHiddenTime; - - if (entry.name === 'first-paint' && shouldRecord) { - IS_DEBUG_BUILD && logger.log('[Measurements] Adding FP'); - this._measurements['fp'] = { value: entry.startTime }; - this._measurements['mark.fp'] = { value: startTimestamp }; - } - - if (entry.name === 'first-contentful-paint' && shouldRecord) { - IS_DEBUG_BUILD && logger.log('[Measurements] Adding FCP'); - this._measurements['fcp'] = { value: entry.startTime }; - this._measurements['mark.fcp'] = { value: startTimestamp }; - } - - break; - } - case 'resource': { - const resourceName = (entry.name as string).replace(global.location.origin, ''); - addResourceSpans(transaction, entry, resourceName, startTime, duration, timeOrigin); - break; - } - default: - // Ignore other entry types. - } - }); - - this._performanceCursor = Math.max(performance.getEntries().length - 1, 0); - - this._trackNavigator(transaction); - - // Measurements are only available for pageload transactions - if (transaction.op === 'pageload') { - // normalize applicable web vital values to be relative to transaction.startTimestamp - - const timeOrigin = msToSec(browserPerformanceTimeOrigin); - - // Generate TTFB (Time to First Byte), which measured as the time between the beginning of the transaction and the - // start of the response in milliseconds - if (typeof responseStartTimestamp === 'number') { - IS_DEBUG_BUILD && logger.log('[Measurements] Adding TTFB'); - this._measurements['ttfb'] = { value: (responseStartTimestamp - transaction.startTimestamp) * 1000 }; - - if (typeof requestStartTimestamp === 'number' && requestStartTimestamp <= responseStartTimestamp) { - // Capture the time spent making the request and receiving the first byte of the response. - // This is the time between the start of the request and the start of the response in milliseconds. - this._measurements['ttfb.requestTime'] = { value: (responseStartTimestamp - requestStartTimestamp) * 1000 }; - } - } - - ['fcp', 'fp', 'lcp'].forEach(name => { - if (!this._measurements[name] || timeOrigin >= transaction.startTimestamp) { - return; - } - - // The web vitals, fcp, fp, lcp, and ttfb, all measure relative to timeOrigin. - // Unfortunately, timeOrigin is not captured within the transaction span data, so these web vitals will need - // to be adjusted to be relative to transaction.startTimestamp. - - const oldValue = this._measurements[name].value; - const measurementTimestamp = timeOrigin + msToSec(oldValue); - // normalizedValue should be in milliseconds - const normalizedValue = Math.abs((measurementTimestamp - transaction.startTimestamp) * 1000); - - const delta = normalizedValue - oldValue; - IS_DEBUG_BUILD && - logger.log(`[Measurements] Normalized ${name} from ${oldValue} to ${normalizedValue} (${delta})`); - - this._measurements[name].value = normalizedValue; - }); - - if (this._measurements['mark.fid'] && this._measurements['fid']) { - // create span for FID - - _startChild(transaction, { - description: 'first input delay', - endTimestamp: this._measurements['mark.fid'].value + msToSec(this._measurements['fid'].value), - op: 'web.vitals', - startTimestamp: this._measurements['mark.fid'].value, - }); - } - - // If FCP is not recorded we should not record the cls value - // according to the new definition of CLS. - if (!('fcp' in this._measurements)) { - delete this._measurements.cls; - } - - transaction.setMeasurements(this._measurements); - tagMetricInfo(transaction, this._lcpEntry, this._clsEntry); - transaction.setTag('sentry_reportAllChanges', this._reportAllChanges); - } - } - - /** - * Capture the information of the user agent. - */ - private _trackNavigator(transaction: Transaction): void { - const navigator = global.navigator as null | (Navigator & NavigatorNetworkInformation & NavigatorDeviceMemory); - if (!navigator) { - return; - } - - // track network connectivity - const connection = navigator.connection; - if (connection) { - if (connection.effectiveType) { - transaction.setTag('effectiveConnectionType', connection.effectiveType); - } - - if (connection.type) { - transaction.setTag('connectionType', connection.type); - } - - if (isMeasurementValue(connection.rtt)) { - this._measurements['connection.rtt'] = { value: connection.rtt as number }; - } - - if (isMeasurementValue(connection.downlink)) { - this._measurements['connection.downlink'] = { value: connection.downlink as number }; - } - } - - if (isMeasurementValue(navigator.deviceMemory)) { - transaction.setTag('deviceMemory', String(navigator.deviceMemory)); - } - - if (isMeasurementValue(navigator.hardwareConcurrency)) { - transaction.setTag('hardwareConcurrency', String(navigator.hardwareConcurrency)); - } - } - - /** Starts tracking the Cumulative Layout Shift on the current page. */ - private _trackCLS(): void { - // See: - // https://web.dev/evolving-cls/ - // https://web.dev/cls-web-tooling/ - getCLS(metric => { - const entry = metric.entries.pop(); - if (!entry) { - return; - } - - IS_DEBUG_BUILD && logger.log('[Measurements] Adding CLS'); - this._measurements['cls'] = { value: metric.value }; - this._clsEntry = entry as LayoutShift; - }); - } - - /** Starts tracking the Largest Contentful Paint on the current page. */ - private _trackLCP(): void { - getLCP(metric => { - const entry = metric.entries.pop(); - if (!entry) { - return; - } - - const timeOrigin = msToSec(browserPerformanceTimeOrigin as number); - const startTime = msToSec(entry.startTime); - IS_DEBUG_BUILD && logger.log('[Measurements] Adding LCP'); - this._measurements['lcp'] = { value: metric.value }; - this._measurements['mark.lcp'] = { value: timeOrigin + startTime }; - this._lcpEntry = entry as LargestContentfulPaint; - }, this._reportAllChanges); - } - - /** Starts tracking the First Input Delay on the current page. */ - private _trackFID(): void { - getFID(metric => { - const entry = metric.entries.pop(); - if (!entry) { - return; - } - - const timeOrigin = msToSec(browserPerformanceTimeOrigin as number); - const startTime = msToSec(entry.startTime); - IS_DEBUG_BUILD && logger.log('[Measurements] Adding FID'); - this._measurements['fid'] = { value: metric.value }; - this._measurements['mark.fid'] = { value: timeOrigin + startTime }; - }); - } -} - -/** Instrument navigation entries */ -function addNavigationSpans(transaction: Transaction, entry: Record, timeOrigin: number): void { - ['unloadEvent', 'redirect', 'domContentLoadedEvent', 'loadEvent', 'connect'].forEach(event => { - addPerformanceNavigationTiming(transaction, entry, event, timeOrigin); - }); - addPerformanceNavigationTiming(transaction, entry, 'secureConnection', timeOrigin, 'TLS/SSL', 'connectEnd'); - addPerformanceNavigationTiming(transaction, entry, 'fetch', timeOrigin, 'cache', 'domainLookupStart'); - addPerformanceNavigationTiming(transaction, entry, 'domainLookup', timeOrigin, 'DNS'); - addRequest(transaction, entry, timeOrigin); -} - -/** Create measure related spans */ -function addMeasureSpans( - transaction: Transaction, - entry: Record, - startTime: number, - duration: number, - timeOrigin: number, -): number { - const measureStartTimestamp = timeOrigin + startTime; - const measureEndTimestamp = measureStartTimestamp + duration; - - _startChild(transaction, { - description: entry.name as string, - endTimestamp: measureEndTimestamp, - op: entry.entryType as string, - startTimestamp: measureStartTimestamp, - }); - - return measureStartTimestamp; -} - -export interface ResourceEntry extends Record { - initiatorType?: string; - transferSize?: number; - encodedBodySize?: number; - decodedBodySize?: number; -} - -/** Create resource-related spans */ -export function addResourceSpans( - transaction: Transaction, - entry: ResourceEntry, - resourceName: string, - startTime: number, - duration: number, - timeOrigin: number, -): void { - // we already instrument based on fetch and xhr, so we don't need to - // duplicate spans here. - if (entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch') { - return; - } - - const data: Record = {}; - if ('transferSize' in entry) { - data['Transfer Size'] = entry.transferSize; - } - if ('encodedBodySize' in entry) { - data['Encoded Body Size'] = entry.encodedBodySize; - } - if ('decodedBodySize' in entry) { - data['Decoded Body Size'] = entry.decodedBodySize; - } - - const startTimestamp = timeOrigin + startTime; - const endTimestamp = startTimestamp + duration; - - _startChild(transaction, { - description: resourceName, - endTimestamp, - op: entry.initiatorType ? `resource.${entry.initiatorType}` : 'resource', - startTimestamp, - data, - }); -} - -/** Create performance navigation related spans */ -function addPerformanceNavigationTiming( - transaction: Transaction, - entry: Record, - event: string, - timeOrigin: number, - description?: string, - eventEnd?: string, -): void { - const end = eventEnd ? (entry[eventEnd] as number | undefined) : (entry[`${event}End`] as number | undefined); - const start = entry[`${event}Start`] as number | undefined; - if (!start || !end) { - return; - } - _startChild(transaction, { - op: 'browser', - description: description ?? event, - startTimestamp: timeOrigin + msToSec(start), - endTimestamp: timeOrigin + msToSec(end), - }); -} - -/** Create request and response related spans */ -function addRequest(transaction: Transaction, entry: Record, timeOrigin: number): void { - _startChild(transaction, { - op: 'browser', - description: 'request', - startTimestamp: timeOrigin + msToSec(entry.requestStart as number), - endTimestamp: timeOrigin + msToSec(entry.responseEnd as number), - }); - - _startChild(transaction, { - op: 'browser', - description: 'response', - startTimestamp: timeOrigin + msToSec(entry.responseStart as number), - endTimestamp: timeOrigin + msToSec(entry.responseEnd as number), - }); -} - -/** - * Helper function to start child on transactions. This function will make sure that the transaction will - * use the start timestamp of the created child span if it is earlier than the transactions actual - * start timestamp. - */ -export function _startChild(transaction: Transaction, { startTimestamp, ...ctx }: SpanContext): Span { - if (startTimestamp && transaction.startTimestamp > startTimestamp) { - transaction.startTimestamp = startTimestamp; - } - - return transaction.startChild({ - startTimestamp, - ...ctx, - }); -} - -/** - * Checks if a given value is a valid measurement value. - */ -function isMeasurementValue(value: any): boolean { - return typeof value === 'number' && isFinite(value); -} - -/** Add LCP / CLS data to transaction to allow debugging */ -function tagMetricInfo( - transaction: Transaction, - lcpEntry: MetricsInstrumentation['_lcpEntry'], - clsEntry: MetricsInstrumentation['_clsEntry'], -): void { - if (lcpEntry) { - IS_DEBUG_BUILD && logger.log('[Measurements] Adding LCP Data'); - - // Capture Properties of the LCP element that contributes to the LCP. - - if (lcpEntry.element) { - transaction.setTag('lcp.element', htmlTreeAsString(lcpEntry.element)); - } - - if (lcpEntry.id) { - transaction.setTag('lcp.id', lcpEntry.id); - } - - if (lcpEntry.url) { - // Trim URL to the first 200 characters. - transaction.setTag('lcp.url', lcpEntry.url.trim().slice(0, 200)); - } - - transaction.setTag('lcp.size', lcpEntry.size); - } - - // See: https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift - if (clsEntry && clsEntry.sources) { - IS_DEBUG_BUILD && logger.log('[Measurements] Adding CLS Data'); - clsEntry.sources.forEach((source, index) => - transaction.setTag(`cls.source.${index + 1}`, htmlTreeAsString(source.node)), - ); - } -} diff --git a/packages/tracing/src/browser/metrics/index.ts b/packages/tracing/src/browser/metrics/index.ts new file mode 100644 index 000000000000..48438ab7b719 --- /dev/null +++ b/packages/tracing/src/browser/metrics/index.ts @@ -0,0 +1,421 @@ +/* eslint-disable max-lines */ +import { Measurements } from '@sentry/types'; +import { browserPerformanceTimeOrigin, getGlobalObject, htmlTreeAsString, isNodeEnv, logger } from '@sentry/utils'; + +import { IS_DEBUG_BUILD } from '../../flags'; +import { Transaction } from '../../transaction'; +import { msToSec } from '../../utils'; +import { getCLS, LayoutShift } from '../web-vitals/getCLS'; +import { getFID } from '../web-vitals/getFID'; +import { getLCP, LargestContentfulPaint } from '../web-vitals/getLCP'; +import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher'; +import { NavigatorDeviceMemory, NavigatorNetworkInformation } from '../web-vitals/types'; +import { _startChild, isMeasurementValue } from './utils'; + +const global = getGlobalObject(); + +function getBrowserPerformanceAPI(): false | Performance { + return !isNodeEnv() && global && global.document && global.performance; +} + +let _performanceCursor: number = 0; + +let _measurements: Measurements = {}; +let _lcpEntry: LargestContentfulPaint | undefined; +let _clsEntry: LayoutShift | undefined; + +/** + * Start tracking web vitals + */ +export function startTrackingWebVitals(reportAllChanges: boolean = false): void { + const performance = getBrowserPerformanceAPI(); + if (performance && browserPerformanceTimeOrigin) { + if (performance.mark) { + global.performance.mark('sentry-tracing-init'); + } + _trackCLS(); + _trackLCP(reportAllChanges); + _trackFID(); + } +} + +/** Starts tracking the Cumulative Layout Shift on the current page. */ +function _trackCLS(): void { + // See: + // https://web.dev/evolving-cls/ + // https://web.dev/cls-web-tooling/ + getCLS(metric => { + const entry = metric.entries.pop(); + if (!entry) { + return; + } + + IS_DEBUG_BUILD && logger.log('[Measurements] Adding CLS'); + _measurements['cls'] = { value: metric.value, unit: 'millisecond' }; + _clsEntry = entry as LayoutShift; + }); +} + +/** Starts tracking the Largest Contentful Paint on the current page. */ +function _trackLCP(reportAllChanges: boolean): void { + getLCP(metric => { + const entry = metric.entries.pop(); + if (!entry) { + return; + } + + const timeOrigin = msToSec(browserPerformanceTimeOrigin as number); + const startTime = msToSec(entry.startTime); + IS_DEBUG_BUILD && logger.log('[Measurements] Adding LCP'); + _measurements['lcp'] = { value: metric.value, unit: 'millisecond' }; + _measurements['mark.lcp'] = { value: timeOrigin + startTime, unit: 'second' }; + _lcpEntry = entry as LargestContentfulPaint; + }, reportAllChanges); +} + +/** Starts tracking the First Input Delay on the current page. */ +function _trackFID(): void { + getFID(metric => { + const entry = metric.entries.pop(); + if (!entry) { + return; + } + + const timeOrigin = msToSec(browserPerformanceTimeOrigin as number); + const startTime = msToSec(entry.startTime); + IS_DEBUG_BUILD && logger.log('[Measurements] Adding FID'); + _measurements['fid'] = { value: metric.value, unit: 'millisecond' }; + _measurements['mark.fid'] = { value: timeOrigin + startTime, unit: 'second' }; + }); +} + +/** Add performance related spans to a transaction */ +export function addPerformanceEntries(transaction: Transaction): void { + const performance = getBrowserPerformanceAPI(); + if (!performance || !global.performance.getEntries || !browserPerformanceTimeOrigin) { + // Gatekeeper if performance API not available + return; + } + + IS_DEBUG_BUILD && logger.log('[Tracing] Adding & adjusting spans using Performance API'); + const timeOrigin = msToSec(browserPerformanceTimeOrigin); + + const performanceEntries = performance.getEntries(); + + let responseStartTimestamp: number | undefined; + let requestStartTimestamp: number | undefined; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + performanceEntries.slice(_performanceCursor).forEach((entry: Record) => { + const startTime = msToSec(entry.startTime); + const duration = msToSec(entry.duration); + + if (transaction.op === 'navigation' && timeOrigin + startTime < transaction.startTimestamp) { + return; + } + + switch (entry.entryType) { + case 'navigation': { + _addNavigationSpans(transaction, entry, timeOrigin); + responseStartTimestamp = timeOrigin + msToSec(entry.responseStart); + requestStartTimestamp = timeOrigin + msToSec(entry.requestStart); + break; + } + case 'mark': + case 'paint': + case 'measure': { + const startTimestamp = _addMeasureSpans(transaction, entry, startTime, duration, timeOrigin); + + // capture web vitals + const firstHidden = getVisibilityWatcher(); + // Only report if the page wasn't hidden prior to the web vital. + const shouldRecord = entry.startTime < firstHidden.firstHiddenTime; + + if (entry.name === 'first-paint' && shouldRecord) { + IS_DEBUG_BUILD && logger.log('[Measurements] Adding FP'); + _measurements['fp'] = { value: entry.startTime, unit: 'millisecond' }; + _measurements['mark.fp'] = { value: startTimestamp, unit: 'second' }; + } + if (entry.name === 'first-contentful-paint' && shouldRecord) { + IS_DEBUG_BUILD && logger.log('[Measurements] Adding FCP'); + _measurements['fcp'] = { value: entry.startTime, unit: 'millisecond' }; + _measurements['mark.fcp'] = { value: startTimestamp, unit: 'second' }; + } + break; + } + case 'resource': { + const resourceName = (entry.name as string).replace(global.location.origin, ''); + _addResourceSpans(transaction, entry, resourceName, startTime, duration, timeOrigin); + break; + } + default: + // Ignore other entry types. + } + }); + + _performanceCursor = Math.max(performanceEntries.length - 1, 0); + + _trackNavigator(transaction); + + // Measurements are only available for pageload transactions + if (transaction.op === 'pageload') { + // Generate TTFB (Time to First Byte), which measured as the time between the beginning of the transaction and the + // start of the response in milliseconds + if (typeof responseStartTimestamp === 'number') { + IS_DEBUG_BUILD && logger.log('[Measurements] Adding TTFB'); + _measurements['ttfb'] = { + value: (responseStartTimestamp - transaction.startTimestamp) * 1000, + unit: 'millisecond', + }; + + if (typeof requestStartTimestamp === 'number' && requestStartTimestamp <= responseStartTimestamp) { + // Capture the time spent making the request and receiving the first byte of the response. + // This is the time between the start of the request and the start of the response in milliseconds. + _measurements['ttfb.requestTime'] = { + value: (responseStartTimestamp - requestStartTimestamp) * 1000, + unit: 'second', + }; + } + } + + ['fcp', 'fp', 'lcp'].forEach(name => { + if (!_measurements[name] || timeOrigin >= transaction.startTimestamp) { + return; + } + // The web vitals, fcp, fp, lcp, and ttfb, all measure relative to timeOrigin. + // Unfortunately, timeOrigin is not captured within the transaction span data, so these web vitals will need + // to be adjusted to be relative to transaction.startTimestamp. + const oldValue = _measurements[name].value; + const measurementTimestamp = timeOrigin + msToSec(oldValue); + + // normalizedValue should be in milliseconds + const normalizedValue = Math.abs((measurementTimestamp - transaction.startTimestamp) * 1000); + const delta = normalizedValue - oldValue; + + IS_DEBUG_BUILD && + logger.log(`[Measurements] Normalized ${name} from ${oldValue} to ${normalizedValue} (${delta})`); + _measurements[name].value = normalizedValue; + }); + + if (_measurements['mark.fid'] && _measurements['fid']) { + // create span for FID + _startChild(transaction, { + description: 'first input delay', + endTimestamp: _measurements['mark.fid'].value + msToSec(_measurements['fid'].value), + op: 'web.vitals', + startTimestamp: _measurements['mark.fid'].value, + }); + } + + // If FCP is not recorded we should not record the cls value + // according to the new definition of CLS. + if (!('fcp' in _measurements)) { + delete _measurements.cls; + } + + Object.keys(_measurements).forEach(measurementName => { + transaction.setMeasurement( + measurementName, + _measurements[measurementName].value, + _measurements[measurementName].unit, + ); + }); + + _tagMetricInfo(transaction); + } + + _lcpEntry = undefined; + _clsEntry = undefined; + _measurements = {}; +} + +/** Create measure related spans */ +export function _addMeasureSpans( + transaction: Transaction, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + entry: Record, + startTime: number, + duration: number, + timeOrigin: number, +): number { + const measureStartTimestamp = timeOrigin + startTime; + const measureEndTimestamp = measureStartTimestamp + duration; + + _startChild(transaction, { + description: entry.name as string, + endTimestamp: measureEndTimestamp, + op: entry.entryType as string, + startTimestamp: measureStartTimestamp, + }); + + return measureStartTimestamp; +} + +/** Instrument navigation entries */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function _addNavigationSpans(transaction: Transaction, entry: Record, timeOrigin: number): void { + ['unloadEvent', 'redirect', 'domContentLoadedEvent', 'loadEvent', 'connect'].forEach(event => { + _addPerformanceNavigationTiming(transaction, entry, event, timeOrigin); + }); + _addPerformanceNavigationTiming(transaction, entry, 'secureConnection', timeOrigin, 'TLS/SSL', 'connectEnd'); + _addPerformanceNavigationTiming(transaction, entry, 'fetch', timeOrigin, 'cache', 'domainLookupStart'); + _addPerformanceNavigationTiming(transaction, entry, 'domainLookup', timeOrigin, 'DNS'); + _addRequest(transaction, entry, timeOrigin); +} + +/** Create performance navigation related spans */ +function _addPerformanceNavigationTiming( + transaction: Transaction, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + entry: Record, + event: string, + timeOrigin: number, + description?: string, + eventEnd?: string, +): void { + const end = eventEnd ? (entry[eventEnd] as number | undefined) : (entry[`${event}End`] as number | undefined); + const start = entry[`${event}Start`] as number | undefined; + if (!start || !end) { + return; + } + _startChild(transaction, { + op: 'browser', + description: description ?? event, + startTimestamp: timeOrigin + msToSec(start), + endTimestamp: timeOrigin + msToSec(end), + }); +} + +/** Create request and response related spans */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function _addRequest(transaction: Transaction, entry: Record, timeOrigin: number): void { + _startChild(transaction, { + op: 'browser', + description: 'request', + startTimestamp: timeOrigin + msToSec(entry.requestStart as number), + endTimestamp: timeOrigin + msToSec(entry.responseEnd as number), + }); + + _startChild(transaction, { + op: 'browser', + description: 'response', + startTimestamp: timeOrigin + msToSec(entry.responseStart as number), + endTimestamp: timeOrigin + msToSec(entry.responseEnd as number), + }); +} + +export interface ResourceEntry extends Record { + initiatorType?: string; + transferSize?: number; + encodedBodySize?: number; + decodedBodySize?: number; +} + +/** Create resource-related spans */ +export function _addResourceSpans( + transaction: Transaction, + entry: ResourceEntry, + resourceName: string, + startTime: number, + duration: number, + timeOrigin: number, +): void { + // we already instrument based on fetch and xhr, so we don't need to + // duplicate spans here. + if (entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch') { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data: Record = {}; + if ('transferSize' in entry) { + data['Transfer Size'] = entry.transferSize; + } + if ('encodedBodySize' in entry) { + data['Encoded Body Size'] = entry.encodedBodySize; + } + if ('decodedBodySize' in entry) { + data['Decoded Body Size'] = entry.decodedBodySize; + } + + const startTimestamp = timeOrigin + startTime; + const endTimestamp = startTimestamp + duration; + + _startChild(transaction, { + description: resourceName, + endTimestamp, + op: entry.initiatorType ? `resource.${entry.initiatorType}` : 'resource', + startTimestamp, + data, + }); +} + +/** + * Capture the information of the user agent. + */ +function _trackNavigator(transaction: Transaction): void { + const navigator = global.navigator as null | (Navigator & NavigatorNetworkInformation & NavigatorDeviceMemory); + if (!navigator) { + return; + } + + // track network connectivity + const connection = navigator.connection; + if (connection) { + if (connection.effectiveType) { + transaction.setTag('effectiveConnectionType', connection.effectiveType); + } + + if (connection.type) { + transaction.setTag('connectionType', connection.type); + } + + if (isMeasurementValue(connection.rtt)) { + _measurements['connection.rtt'] = { value: connection.rtt, unit: 'millisecond' }; + } + + if (isMeasurementValue(connection.downlink)) { + _measurements['connection.downlink'] = { value: connection.downlink, unit: '' }; // unit is empty string for now, while relay doesn't support download speed units + } + } + + if (isMeasurementValue(navigator.deviceMemory)) { + transaction.setTag('deviceMemory', `${navigator.deviceMemory} GB`); + } + + if (isMeasurementValue(navigator.hardwareConcurrency)) { + transaction.setTag('hardwareConcurrency', String(navigator.hardwareConcurrency)); + } +} + +/** Add LCP / CLS data to transaction to allow debugging */ +function _tagMetricInfo(transaction: Transaction): void { + if (_lcpEntry) { + IS_DEBUG_BUILD && logger.log('[Measurements] Adding LCP Data'); + + // Capture Properties of the LCP element that contributes to the LCP. + + if (_lcpEntry.element) { + transaction.setTag('lcp.element', htmlTreeAsString(_lcpEntry.element)); + } + + if (_lcpEntry.id) { + transaction.setTag('lcp.id', _lcpEntry.id); + } + + if (_lcpEntry.url) { + // Trim URL to the first 200 characters. + transaction.setTag('lcp.url', _lcpEntry.url.trim().slice(0, 200)); + } + + transaction.setTag('lcp.size', _lcpEntry.size); + } + + // See: https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift + if (_clsEntry && _clsEntry.sources) { + IS_DEBUG_BUILD && logger.log('[Measurements] Adding CLS Data'); + _clsEntry.sources.forEach((source, index) => + transaction.setTag(`cls.source.${index + 1}`, htmlTreeAsString(source.node)), + ); + } +} diff --git a/packages/tracing/src/browser/metrics/utils.ts b/packages/tracing/src/browser/metrics/utils.ts new file mode 100644 index 000000000000..4894dbd3ec8e --- /dev/null +++ b/packages/tracing/src/browser/metrics/utils.ts @@ -0,0 +1,26 @@ +import { Span, SpanContext } from '@sentry/types'; + +import { Transaction } from '../../transaction'; + +/** + * Checks if a given value is a valid measurement value. + */ +export function isMeasurementValue(value: unknown): value is number { + return typeof value === 'number' && isFinite(value); +} + +/** + * Helper function to start child on transactions. This function will make sure that the transaction will + * use the start timestamp of the created child span if it is earlier than the transactions actual + * start timestamp. + */ +export function _startChild(transaction: Transaction, { startTimestamp, ...ctx }: SpanContext): Span { + if (startTimestamp && transaction.startTimestamp > startTimestamp) { + transaction.startTimestamp = startTimestamp; + } + + return transaction.startChild({ + startTimestamp, + ...ctx, + }); +} diff --git a/packages/tracing/src/browser/request.ts b/packages/tracing/src/browser/request.ts index 9a71a7387bed..b45775edd05f 100644 --- a/packages/tracing/src/browser/request.ts +++ b/packages/tracing/src/browser/request.ts @@ -1,4 +1,11 @@ -import { addInstrumentationHandler, isInstanceOf, isMatchingPattern } from '@sentry/utils'; +/* eslint-disable max-lines */ +import { + addInstrumentationHandler, + BAGGAGE_HEADER_NAME, + isInstanceOf, + isMatchingPattern, + mergeAndSerializeBaggage, +} from '@sentry/utils'; import { Span } from '../span'; import { getActiveTransaction, hasTracingEnabled } from '../utils'; @@ -70,12 +77,24 @@ export interface XHRData { }; __sentry_xhr_span_id__?: string; setRequestHeader?: (key: string, val: string) => void; + getRequestHeader?: (key: string) => string; __sentry_own_request__?: boolean; }; startTimestamp: number; endTimestamp?: number; } +type PolymorphicRequestHeaders = + | Record + | Array<[string, string]> + // the below is not preicsely the Header type used in Request, but it'll pass duck-typing + | { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; + append: (key: string, value: string) => void; + get: (key: string) => string; + }; + export const defaultRequestInstrumentationOptions: RequestInstrumentationOptions = { traceFetch: true, traceXHR: true, @@ -179,25 +198,48 @@ export function fetchCallback( const request = (handlerData.args[0] = handlerData.args[0] as string | Request); // eslint-disable-next-line @typescript-eslint/no-explicit-any const options = (handlerData.args[1] = (handlerData.args[1] as { [key: string]: any }) || {}); - let headers = options.headers; - if (isInstanceOf(request, Request)) { - headers = (request as Request).headers; - } - if (headers) { + options.headers = addTracingHeaders(request, span, options); + } +} + +function addTracingHeaders( + request: string | Request, + span: Span, + options: { [key: string]: any }, +): PolymorphicRequestHeaders { + let headers = options.headers; + + if (isInstanceOf(request, Request)) { + headers = (request as Request).headers; + } + const incomingBaggage = span.getBaggage(); + + if (headers) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (typeof headers.append === 'function') { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (typeof headers.append === 'function') { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - headers.append('sentry-trace', span.toTraceparent()); - } else if (Array.isArray(headers)) { - headers = [...headers, ['sentry-trace', span.toTraceparent()]]; - } else { - headers = { ...headers, 'sentry-trace': span.toTraceparent() }; - } + headers.append('sentry-trace', span.toTraceparent()); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + headers.append(BAGGAGE_HEADER_NAME, mergeAndSerializeBaggage(incomingBaggage, headers.get(BAGGAGE_HEADER_NAME))); + } else if (Array.isArray(headers)) { + const [, headerBaggageString] = headers.find(([key, _]) => key === BAGGAGE_HEADER_NAME); + headers = [ + ...headers, + ['sentry-trace', span.toTraceparent()], + [BAGGAGE_HEADER_NAME, mergeAndSerializeBaggage(incomingBaggage, headerBaggageString)], + ]; } else { - headers = { 'sentry-trace': span.toTraceparent() }; + headers = { + ...headers, + 'sentry-trace': span.toTraceparent(), + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + baggage: mergeAndSerializeBaggage(incomingBaggage, headers.baggage), + }; } - options.headers = headers; + } else { + headers = { 'sentry-trace': span.toTraceparent(), baggage: mergeAndSerializeBaggage(incomingBaggage) }; } + return headers; } /** @@ -254,6 +296,14 @@ export function xhrCallback( if (handlerData.xhr.setRequestHeader) { try { handlerData.xhr.setRequestHeader('sentry-trace', span.toTraceparent()); + + const headerBaggageString = + handlerData.xhr.getRequestHeader && handlerData.xhr.getRequestHeader(BAGGAGE_HEADER_NAME); + + handlerData.xhr.setRequestHeader( + BAGGAGE_HEADER_NAME, + mergeAndSerializeBaggage(span.getBaggage(), headerBaggageString), + ); } catch (_) { // Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED. } diff --git a/packages/tracing/src/constants.ts b/packages/tracing/src/constants.ts deleted file mode 100644 index 3f01982c3614..000000000000 --- a/packages/tracing/src/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Store finish reasons in tuple to save on bundle size -// Readonly type should enforce that this is not mutated. -export const FINISH_REASON_TAG = 'finishReason'; - -export const IDLE_TRANSACTION_FINISH_REASONS = ['heartbeatFailed', 'idleTimeout', 'documentHidden'] as const; diff --git a/packages/tracing/src/hubextensions.ts b/packages/tracing/src/hubextensions.ts index bd38b270cc22..dbcb4fb66029 100644 --- a/packages/tracing/src/hubextensions.ts +++ b/packages/tracing/src/hubextensions.ts @@ -1,5 +1,7 @@ +/* eslint-disable max-lines */ import { getMainCarrier, Hub } from '@sentry/hub'; import { + ClientOptions, CustomSamplingContext, Integration, IntegrationClass, @@ -41,7 +43,11 @@ function traceHeaders(this: Hub): { [key: string]: string } { * * @returns The given transaction with its `sampled` value set */ -function sample(transaction: T, options: Options, samplingContext: SamplingContext): T { +function sample( + transaction: T, + options: Pick, + samplingContext: SamplingContext, +): T { // nothing to do if tracing is not enabled if (!hasTracingEnabled(options)) { transaction.sampled = false; @@ -171,7 +177,7 @@ function _startTransaction( customSamplingContext?: CustomSamplingContext, ): Transaction { const client = this.getClient(); - const options = (client && client.getOptions()) || {}; + const options: Partial = (client && client.getOptions()) || {}; let transaction = new Transaction(transactionContext, this); transaction = sample(transaction, options, { @@ -191,14 +197,15 @@ function _startTransaction( export function startIdleTransaction( hub: Hub, transactionContext: TransactionContext, - idleTimeout?: number, + idleTimeout: number, + finalTimeout: number, onScope?: boolean, customSamplingContext?: CustomSamplingContext, ): IdleTransaction { const client = hub.getClient(); - const options = (client && client.getOptions()) || {}; + const options: Partial = (client && client.getOptions()) || {}; - let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, onScope); + let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, finalTimeout, onScope); transaction = sample(transaction, options, { parentSampled: transactionContext.parentSampled, transactionContext, diff --git a/packages/tracing/src/idletransaction.ts b/packages/tracing/src/idletransaction.ts index 0d292eebba65..9353f552fcc9 100644 --- a/packages/tracing/src/idletransaction.ts +++ b/packages/tracing/src/idletransaction.ts @@ -1,13 +1,14 @@ +/* eslint-disable max-lines */ import { Hub } from '@sentry/hub'; import { TransactionContext } from '@sentry/types'; import { logger, timestampWithMs } from '@sentry/utils'; -import { FINISH_REASON_TAG, IDLE_TRANSACTION_FINISH_REASONS } from './constants'; import { IS_DEBUG_BUILD } from './flags'; import { Span, SpanRecorder } from './span'; import { Transaction } from './transaction'; export const DEFAULT_IDLE_TIMEOUT = 1000; +export const DEFAULT_FINAL_TIMEOUT = 30000; export const HEARTBEAT_INTERVAL = 5000; /** @@ -17,7 +18,7 @@ export class IdleTransactionSpanRecorder extends SpanRecorder { public constructor( private readonly _pushActivity: (id: string) => void, private readonly _popActivity: (id: string) => void, - public transactionSpanId: string = '', + public transactionSpanId: string, maxlen?: number, ) { super(maxlen); @@ -69,25 +70,28 @@ export class IdleTransaction extends Transaction { private readonly _beforeFinishCallbacks: BeforeFinishCallback[] = []; /** - * If a transaction is created and no activities are added, we want to make sure that - * it times out properly. This is cleared and not used when activities are added. + * Timer that tracks Transaction idleTimeout */ - private _initTimeout: ReturnType | undefined; + private _idleTimeoutID: ReturnType | undefined; public constructor( transactionContext: TransactionContext, - private readonly _idleHub?: Hub, + private readonly _idleHub: Hub, /** - * The time to wait in ms until the idle transaction will be finished. - * @default 1000 + * The time to wait in ms until the idle transaction will be finished. This timer is started each time + * there are no active spans on this transaction. */ private readonly _idleTimeout: number = DEFAULT_IDLE_TIMEOUT, + /** + * The final value in ms that a transaction cannot exceed + */ + private readonly _finalTimeout: number = DEFAULT_FINAL_TIMEOUT, // Whether or not the transaction should put itself on the scope when it starts and pop itself off when it ends private readonly _onScope: boolean = false, ) { super(transactionContext, _idleHub); - if (_idleHub && _onScope) { + if (_onScope) { // There should only be one active transaction on the scope clearActiveTransaction(_idleHub); @@ -97,11 +101,13 @@ export class IdleTransaction extends Transaction { _idleHub.configureScope(scope => scope.setSpan(this)); } - this._initTimeout = setTimeout(() => { + this._startIdleTimeout(); + setTimeout(() => { if (!this._finished) { + this.setStatus('deadline_exceeded'); this.finish(); } - }, this._idleTimeout); + }, this._finalTimeout); } /** {@inheritDoc} */ @@ -193,15 +199,34 @@ export class IdleTransaction extends Transaction { this.spanRecorder.add(this); } + /** + * Cancels the existing idletimeout, if there is one + */ + private _cancelIdleTimeout(): void { + if (this._idleTimeoutID) { + clearTimeout(this._idleTimeoutID); + this._idleTimeoutID = undefined; + } + } + + /** + * Creates an idletimeout + */ + private _startIdleTimeout(endTimestamp?: Parameters[0]): void { + this._cancelIdleTimeout(); + this._idleTimeoutID = setTimeout(() => { + if (!this._finished && Object.keys(this.activities).length === 0) { + this.finish(endTimestamp); + } + }, this._idleTimeout); + } + /** * Start tracking a specific activity. * @param spanId The span id that represents the activity */ private _pushActivity(spanId: string): void { - if (this._initTimeout) { - clearTimeout(this._initTimeout); - this._initTimeout = undefined; - } + this._cancelIdleTimeout(); IS_DEBUG_BUILD && logger.log(`[Tracing] pushActivity: ${spanId}`); this.activities[spanId] = true; IS_DEBUG_BUILD && logger.log('[Tracing] new activities count', Object.keys(this.activities).length); @@ -220,17 +245,10 @@ export class IdleTransaction extends Transaction { } if (Object.keys(this.activities).length === 0) { - const timeout = this._idleTimeout; // We need to add the timeout here to have the real endtimestamp of the transaction // Remember timestampWithMs is in seconds, timeout is in ms - const end = timestampWithMs() + timeout / 1000; - - setTimeout(() => { - if (!this._finished) { - this.setTag(FINISH_REASON_TAG, IDLE_TRANSACTION_FINISH_REASONS[1]); - this.finish(end); - } - }, timeout); + const endTimestamp = timestampWithMs() + this._idleTimeout / 1000; + this._startIdleTimeout(endTimestamp); } } @@ -257,7 +275,6 @@ export class IdleTransaction extends Transaction { if (this._heartbeatCounter >= 3) { IS_DEBUG_BUILD && logger.log('[Tracing] Transaction finished because of no change for 3 heart beats'); this.setStatus('deadline_exceeded'); - this.setTag(FINISH_REASON_TAG, IDLE_TRANSACTION_FINISH_REASONS[0]); this.finish(); } else { this._pingHeartbeat(); @@ -278,14 +295,12 @@ export class IdleTransaction extends Transaction { /** * Reset transaction on scope to `undefined` */ -function clearActiveTransaction(hub?: Hub): void { - if (hub) { - const scope = hub.getScope(); - if (scope) { - const transaction = scope.getTransaction(); - if (transaction) { - scope.setSpan(undefined); - } +function clearActiveTransaction(hub: Hub): void { + const scope = hub.getScope(); + if (scope) { + const transaction = scope.getTransaction(); + if (transaction) { + scope.setSpan(undefined); } } } diff --git a/packages/tracing/src/index.bundle.ts b/packages/tracing/src/index.bundle.ts index 3349dfbd06da..bb55da2cd273 100644 --- a/packages/tracing/src/index.bundle.ts +++ b/packages/tracing/src/index.bundle.ts @@ -1,19 +1,19 @@ -export { +export type { Breadcrumb, Request, SdkInfo, Event, - EventStatus, Exception, - Response, + // eslint-disable-next-line deprecation/deprecation Severity, + SeverityLevel, StackFrame, Stacktrace, Thread, User, } from '@sentry/types'; -export { SeverityLevel } from '@sentry/utils'; +export type { BrowserOptions, ReportDialogOptions } from '@sentry/browser'; export { addGlobalEventProcessor, @@ -33,12 +33,12 @@ export { setTags, setUser, startTransaction, - Transports, + makeFetchTransport, + makeXHRTransport, withScope, } from '@sentry/browser'; -export { BrowserOptions } from '@sentry/browser'; -export { BrowserClient, ReportDialogOptions } from '@sentry/browser'; +export { BrowserClient } from '@sentry/browser'; export { defaultIntegrations, forceLoad, @@ -50,7 +50,7 @@ export { close, wrap, } from '@sentry/browser'; -export { SDK_NAME, SDK_VERSION } from '@sentry/browser'; +export { SDK_VERSION } from '@sentry/browser'; import { Integrations as BrowserIntegrations } from '@sentry/browser'; import { getGlobalObject } from '@sentry/utils'; diff --git a/packages/tracing/src/index.ts b/packages/tracing/src/index.ts index 89e3b3ba92d5..14d80fb7b4cc 100644 --- a/packages/tracing/src/index.ts +++ b/packages/tracing/src/index.ts @@ -1,6 +1,9 @@ import { addExtensionMethods } from './hubextensions'; import * as Integrations from './integrations'; +export type { RequestInstrumentationOptions } from './browser'; +export type { SpanStatusType } from './span'; + export { Integrations }; // This is already exported as part of `Integrations` above (and for the moment will remain so for @@ -19,23 +22,24 @@ export { Integrations }; // const instance = new BrowserTracing(); // // For an example of of the new usage of BrowserTracing, see @sentry/nextjs index.client.ts -export { BrowserTracing } from './browser'; +export { BrowserTracing, BROWSER_TRACING_INTEGRATION_ID } from './browser'; -export { Span, SpanStatusType, spanStatusfromHttpCode } from './span'; +export { Span, spanStatusfromHttpCode } from './span'; // eslint-disable-next-line deprecation/deprecation export { SpanStatus } from './spanstatus'; export { Transaction } from './transaction'; -export { - // TODO deprecate old name in v7 - instrumentOutgoingRequests as registerRequestInstrumentation, - RequestInstrumentationOptions, - defaultRequestInstrumentationOptions, -} from './browser'; +export { instrumentOutgoingRequests, defaultRequestInstrumentationOptions } from './browser'; export { IdleTransaction } from './idletransaction'; export { startIdleTransaction } from './hubextensions'; -// We are patching the global object with our hub extension methods -addExtensionMethods(); +// Treeshakable guard to remove all code related to tracing +declare const __SENTRY_TRACING__: boolean; + +// Guard for tree +if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) { + // We are patching the global object with our hub extension methods + addExtensionMethods(); +} export { addExtensionMethods }; diff --git a/packages/tracing/src/integrations/index.ts b/packages/tracing/src/integrations/index.ts index b35eff6c495f..6fb86c1afc78 100644 --- a/packages/tracing/src/integrations/index.ts +++ b/packages/tracing/src/integrations/index.ts @@ -2,6 +2,7 @@ export { Express } from './node/express'; export { Postgres } from './node/postgres'; export { Mysql } from './node/mysql'; export { Mongo } from './node/mongo'; +export { Prisma } from './node/prisma'; // TODO(v7): Remove this export // Please see `src/index.ts` for more details. diff --git a/packages/tracing/src/integrations/node/prisma.ts b/packages/tracing/src/integrations/node/prisma.ts new file mode 100644 index 000000000000..3aa7a07f4da3 --- /dev/null +++ b/packages/tracing/src/integrations/node/prisma.ts @@ -0,0 +1,109 @@ +import { Hub } from '@sentry/hub'; +import { EventProcessor, Integration } from '@sentry/types'; +import { isThenable, logger } from '@sentry/utils'; + +import { IS_DEBUG_BUILD } from '../../flags'; + +type PrismaAction = + | 'findUnique' + | 'findMany' + | 'findFirst' + | 'create' + | 'createMany' + | 'update' + | 'updateMany' + | 'upsert' + | 'delete' + | 'deleteMany' + | 'executeRaw' + | 'queryRaw' + | 'aggregate' + | 'count' + | 'runCommandRaw'; + +interface PrismaMiddlewareParams { + model?: unknown; + action: PrismaAction; + args: unknown; + dataPath: string[]; + runInTransaction: boolean; +} + +type PrismaMiddleware = ( + params: PrismaMiddlewareParams, + next: (params: PrismaMiddlewareParams) => Promise, +) => Promise; + +interface PrismaClient { + $use: (cb: PrismaMiddleware) => void; +} + +function isValidPrismaClient(possibleClient: unknown): possibleClient is PrismaClient { + return possibleClient && !!(possibleClient as PrismaClient)['$use']; +} + +/** Tracing integration for @prisma/client package */ +export class Prisma implements Integration { + /** + * @inheritDoc + */ + public static id: string = 'Prisma'; + + /** + * @inheritDoc + */ + public name: string = Prisma.id; + + /** + * Prisma ORM Client Instance + */ + private readonly _client?: PrismaClient; + + /** + * @inheritDoc + */ + public constructor(options: { client?: unknown } = {}) { + if (isValidPrismaClient(options.client)) { + this._client = options.client; + } else { + logger.warn( + `Unsupported Prisma client provided to PrismaIntegration. Provided client: ${JSON.stringify(options.client)}`, + ); + } + } + + /** + * @inheritDoc + */ + public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { + if (!this._client) { + IS_DEBUG_BUILD && logger.error('PrismaIntegration is missing a Prisma Client Instance'); + return; + } + + this._client.$use((params, next: (params: PrismaMiddlewareParams) => Promise) => { + const scope = getCurrentHub().getScope(); + const parentSpan = scope?.getSpan(); + + const action = params.action; + const model = params.model; + + const span = parentSpan?.startChild({ + description: model ? `${model} ${action}` : action, + op: 'db.prisma', + }); + + const rv = next(params); + + if (isThenable(rv)) { + return rv.then((res: unknown) => { + span?.finish(); + return res; + }); + } + + span?.finish(); + return rv; + }); + } +} diff --git a/packages/tracing/src/span.ts b/packages/tracing/src/span.ts index 990f56ce981c..68cba79eb7cc 100644 --- a/packages/tracing/src/span.ts +++ b/packages/tracing/src/span.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { Primitive, Span as SpanInterface, SpanContext, Transaction } from '@sentry/types'; +import { Baggage, Primitive, Span as SpanInterface, SpanContext, Transaction } from '@sentry/types'; import { dropUndefinedKeys, timestampWithMs, uuid4 } from '@sentry/utils'; /** @@ -149,16 +149,6 @@ export class Span implements SpanInterface { } } - /** - * @inheritDoc - * @deprecated - */ - public child( - spanContext?: Pick>, - ): Span { - return this.startChild(spanContext); - } - /** * @inheritDoc */ @@ -308,6 +298,13 @@ export class Span implements SpanInterface { }); } + /** + * @inheritdoc + */ + public getBaggage(): Baggage | undefined { + return this.transaction && this.transaction.metadata.baggage; + } + /** * @inheritDoc */ diff --git a/packages/tracing/src/transaction.ts b/packages/tracing/src/transaction.ts index 6cbda0e04223..fb6cd9d02b6c 100644 --- a/packages/tracing/src/transaction.ts +++ b/packages/tracing/src/transaction.ts @@ -6,7 +6,7 @@ import { TransactionContext, TransactionMetadata, } from '@sentry/types'; -import { dropUndefinedKeys, isInstanceOf, logger } from '@sentry/utils'; +import { dropUndefinedKeys, logger } from '@sentry/utils'; import { IS_DEBUG_BUILD } from './flags'; import { Span as SpanClass, SpanRecorder } from './span'; @@ -22,7 +22,7 @@ export class Transaction extends SpanClass implements TransactionInterface { /** * The reference to the current hub. */ - private readonly _hub: Hub = getCurrentHub() as unknown as Hub; + private readonly _hub: Hub; private _trimEnd?: boolean; @@ -36,9 +36,7 @@ export class Transaction extends SpanClass implements TransactionInterface { public constructor(transactionContext: TransactionContext, hub?: Hub) { super(transactionContext); - if (isInstanceOf(hub, Hub)) { - this._hub = hub as Hub; - } + this._hub = hub || getCurrentHub(); this.name = transactionContext.name || ''; @@ -68,11 +66,10 @@ export class Transaction extends SpanClass implements TransactionInterface { } /** - * Set observed measurements for this transaction. - * @hidden + * @inheritDoc */ - public setMeasurements(measurements: Measurements): void { - this._measurements = { ...measurements }; + public setMeasurement(name: string, value: number, unit: string = ''): void { + this._measurements[name] = { value, unit }; } /** @@ -105,10 +102,10 @@ export class Transaction extends SpanClass implements TransactionInterface { IS_DEBUG_BUILD && logger.log('[Tracing] Discarding transaction because its trace was not chosen to be sampled.'); const client = this._hub.getClient(); - const transport = client && client.getTransport && client.getTransport(); - if (transport && transport.recordLostEvent) { - transport.recordLostEvent('sample_rate', 'transaction'); + if (client) { + client.recordDroppedEvent('sample_rate', 'transaction'); } + return undefined; } diff --git a/packages/tracing/src/utils.ts b/packages/tracing/src/utils.ts index 63598c00ac58..9d999df486fb 100644 --- a/packages/tracing/src/utils.ts +++ b/packages/tracing/src/utils.ts @@ -20,7 +20,9 @@ export { TRACEPARENT_REGEXP, extractTraceparentData } from '@sentry/utils'; * * Tracing is enabled when at least one of `tracesSampleRate` and `tracesSampler` is defined in the SDK config. */ -export function hasTracingEnabled(maybeOptions?: Options | undefined): boolean { +export function hasTracingEnabled( + maybeOptions?: Pick | undefined, +): boolean { const client = getCurrentHub().getClient(); const options = maybeOptions || (client && client.getOptions()); return !!options && ('tracesSampleRate' in options || 'tracesSampler' in options); diff --git a/packages/tracing/test/browser/backgroundtab.test.ts b/packages/tracing/test/browser/backgroundtab.test.ts index e46c79695d20..3e332033d74d 100644 --- a/packages/tracing/test/browser/backgroundtab.test.ts +++ b/packages/tracing/test/browser/backgroundtab.test.ts @@ -4,6 +4,7 @@ import { JSDOM } from 'jsdom'; import { registerBackgroundTabDetection } from '../../src/browser/backgroundtab'; import { addExtensionMethods } from '../../src/hubextensions'; +import { getDefaultBrowserClientOptions } from '../testutils'; describe('registerBackgroundTabDetection', () => { let events: Record = {}; @@ -13,7 +14,8 @@ describe('registerBackgroundTabDetection', () => { // @ts-ignore need to override global document global.document = dom.window.document; - hub = new Hub(new BrowserClient({ tracesSampleRate: 1 })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); + hub = new Hub(new BrowserClient(options)); makeMain(hub); // If we do not add extension methods, invoking hub.startTransaction returns undefined diff --git a/packages/tracing/test/browser/browsertracing.test.ts b/packages/tracing/test/browser/browsertracing.test.ts index 8db3cc10e41e..6fbcc5b6f6d8 100644 --- a/packages/tracing/test/browser/browsertracing.test.ts +++ b/packages/tracing/test/browser/browsertracing.test.ts @@ -1,21 +1,21 @@ import { BrowserClient } from '@sentry/browser'; import { Hub, makeMain } from '@sentry/hub'; -import { getGlobalObject } from '@sentry/utils'; +import { BaggageObj } from '@sentry/types'; +import { getGlobalObject, InstrumentHandlerCallback, InstrumentHandlerType } from '@sentry/utils'; import { JSDOM } from 'jsdom'; import { BrowserTracing, BrowserTracingOptions, - DEFAULT_MAX_TRANSACTION_DURATION_SECONDS, - getHeaderContext, + extractTraceDataFromMetaTags, getMetaContent, } from '../../src/browser/browsertracing'; -import { MetricsInstrumentation } from '../../src/browser/metrics'; import { defaultRequestInstrumentationOptions } from '../../src/browser/request'; import { instrumentRoutingWithDefaults } from '../../src/browser/router'; import * as hubExtensions from '../../src/hubextensions'; -import { DEFAULT_IDLE_TIMEOUT, IdleTransaction } from '../../src/idletransaction'; -import { getActiveTransaction, secToMs } from '../../src/utils'; +import { DEFAULT_FINAL_TIMEOUT, DEFAULT_IDLE_TIMEOUT, IdleTransaction } from '../../src/idletransaction'; +import { getActiveTransaction } from '../../src/utils'; +import { getDefaultBrowserClientOptions } from '../testutils'; let mockChangeHistory: ({ to, from }: { to: string; from?: string }) => void = () => undefined; @@ -23,7 +23,7 @@ jest.mock('@sentry/utils', () => { const actual = jest.requireActual('@sentry/utils'); return { ...actual, - addInstrumentationHandler: (type, callback): void => { + addInstrumentationHandler: (type: InstrumentHandlerType, callback: InstrumentHandlerCallback): void => { if (type === 'history') { // rather than actually add the navigation-change handler, grab a reference to it, so we can trigger it manually mockChangeHistory = callback; @@ -51,7 +51,8 @@ describe('BrowserTracing', () => { let hub: Hub; beforeEach(() => { jest.useFakeTimers(); - hub = new Hub(new BrowserClient({ tracesSampleRate: 1 })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); + hub = new Hub(new BrowserClient(options)); makeMain(hub); document.head.innerHTML = ''; @@ -83,8 +84,8 @@ describe('BrowserTracing', () => { expect(browserTracing.options).toEqual({ idleTimeout: DEFAULT_IDLE_TIMEOUT, + finalTimeout: DEFAULT_FINAL_TIMEOUT, markBackgroundTransactions: true, - maxTransactionDuration: DEFAULT_MAX_TRANSACTION_DURATION_SECONDS, routingInstrumentation: instrumentRoutingWithDefaults, startTransactionOnLocationChange: true, startTransactionOnPageLoad: true, @@ -214,7 +215,8 @@ describe('BrowserTracing', () => { it('sets transaction context from sentry-trace header', () => { const name = 'sentry-trace'; const content = '126de09502ae4e0fb26c6967190756a4-b6e54397b12a2a0f-1'; - document.head.innerHTML = ``; + document.head.innerHTML = + `` + ''; const startIdleTransaction = jest.spyOn(hubExtensions, 'startIdleTransaction'); createBrowserTracing(true, { routingInstrumentation: customInstrumentRouting }); @@ -225,8 +227,12 @@ describe('BrowserTracing', () => { traceId: '126de09502ae4e0fb26c6967190756a4', parentSpanId: 'b6e54397b12a2a0f', parentSampled: true, + metadata: { + baggage: [{ release: '2.1.14' }, 'foo=bar'], + }, }), expect.any(Number), + expect.any(Number), expect.any(Boolean), expect.any(Object), ); @@ -245,8 +251,6 @@ describe('BrowserTracing', () => { expect(mockFinish).toHaveBeenCalledTimes(0); jest.advanceTimersByTime(DEFAULT_IDLE_TIMEOUT); expect(mockFinish).toHaveBeenCalledTimes(1); - - expect(transaction.tags).toEqual({ finishReason: 'idleTimeout', idleTimeout: undefined }); }); it('can be a custom value', () => { @@ -261,44 +265,6 @@ describe('BrowserTracing', () => { expect(mockFinish).toHaveBeenCalledTimes(0); jest.advanceTimersByTime(2000); expect(mockFinish).toHaveBeenCalledTimes(1); - - expect(transaction.tags).toEqual({ finishReason: 'idleTimeout', idleTimeout: 2000 }); - }); - }); - - describe('maxTransactionDuration', () => { - it('cancels a transaction if exceeded', () => { - createBrowserTracing(true, { routingInstrumentation: customInstrumentRouting }); - const transaction = getActiveTransaction(hub) as IdleTransaction; - transaction.finish(transaction.startTimestamp + secToMs(DEFAULT_MAX_TRANSACTION_DURATION_SECONDS) + 1); - - expect(transaction.status).toBe('deadline_exceeded'); - expect(transaction.tags.maxTransactionDurationExceeded).toBeDefined(); - }); - - it('does not cancel a transaction if not exceeded', () => { - createBrowserTracing(true, { routingInstrumentation: customInstrumentRouting }); - const transaction = getActiveTransaction(hub) as IdleTransaction; - transaction.finish(transaction.startTimestamp + secToMs(DEFAULT_MAX_TRANSACTION_DURATION_SECONDS)); - - expect(transaction.status).toBe(undefined); - expect(transaction.tags.maxTransactionDurationExceeded).not.toBeDefined(); - }); - - it('can have a custom value', () => { - const customMaxTransactionDuration = 700; - // Test to make sure default duration is less than tested custom value. - expect(DEFAULT_MAX_TRANSACTION_DURATION_SECONDS < customMaxTransactionDuration).toBe(true); - createBrowserTracing(true, { - maxTransactionDuration: customMaxTransactionDuration, - routingInstrumentation: customInstrumentRouting, - }); - const transaction = getActiveTransaction(hub) as IdleTransaction; - - transaction.finish(transaction.startTimestamp + secToMs(customMaxTransactionDuration)); - - expect(transaction.status).toBe(undefined); - expect(transaction.tags.maxTransactionDurationExceeded).not.toBeDefined(); }); }); }); @@ -360,7 +326,7 @@ describe('BrowserTracing', () => { }); }); - describe('sentry-trace element', () => { + describe('sentry-trace and baggage elements', () => { describe('getMetaContent', () => { it('finds the specified tag and extracts the value', () => { const name = 'sentry-trace'; @@ -390,12 +356,12 @@ describe('BrowserTracing', () => { }); }); - describe('getHeaderContext', () => { + describe('extractTraceDataFromMetaTags()', () => { it('correctly parses a valid sentry-trace meta header', () => { document.head.innerHTML = ''; - const headerContext = getHeaderContext(); + const headerContext = extractTraceDataFromMetaTags(); expect(headerContext).toBeDefined(); expect(headerContext!.traceId).toEqual('12312012123120121231201212312012'); @@ -403,54 +369,93 @@ describe('BrowserTracing', () => { expect(headerContext!.parentSampled).toEqual(false); }); - it('returns undefined if the header is malformed', () => { + it('correctly parses a valid baggage meta header', () => { + document.head.innerHTML = ''; + + const headerContext = extractTraceDataFromMetaTags(); + + expect(headerContext).toBeDefined(); + expect(headerContext?.metadata?.baggage).toBeDefined(); + const baggage = headerContext?.metadata?.baggage; + expect(baggage && baggage[0]).toBeDefined(); + expect(baggage && baggage[0]).toEqual({ + release: '2.1.12', + } as BaggageObj); + expect(baggage && baggage[1]).toBeDefined(); + expect(baggage && baggage[1]).toEqual('foo=bar'); + }); + + it('returns undefined if the sentry-trace header is malformed', () => { document.head.innerHTML = ''; - const headerContext = getHeaderContext(); + const headerContext = extractTraceDataFromMetaTags(); expect(headerContext).toBeUndefined(); }); + it('does not crash if the baggage header is malformed', () => { + document.head.innerHTML = ''; + + const headerContext = extractTraceDataFromMetaTags(); + + // TODO currently this creates invalid baggage. This must be adressed in a follow-up PR + expect(headerContext).toBeDefined(); + expect(headerContext?.metadata?.baggage).toBeDefined(); + const baggage = headerContext?.metadata?.baggage; + expect(baggage && baggage[0]).toBeDefined(); + expect(baggage && baggage[1]).toBeDefined(); + }); + it("returns undefined if the header isn't there", () => { document.head.innerHTML = ''; - const headerContext = getHeaderContext(); + const headerContext = extractTraceDataFromMetaTags(); expect(headerContext).toBeUndefined(); }); }); describe('using the data', () => { - it('uses the data for pageload transactions', () => { + it('uses the tracing data for pageload transactions', () => { // make sampled false here, so we can see that it's being used rather than the tracesSampleRate-dictated one document.head.innerHTML = - ''; + '' + + ''; // pageload transactions are created as part of the BrowserTracing integration's initialization createBrowserTracing(true); const transaction = getActiveTransaction(hub) as IdleTransaction; + const baggage = transaction.getBaggage()!; expect(transaction).toBeDefined(); expect(transaction.op).toBe('pageload'); expect(transaction.traceId).toEqual('12312012123120121231201212312012'); expect(transaction.parentSpanId).toEqual('1121201211212012'); expect(transaction.sampled).toBe(false); + expect(baggage).toBeDefined(); + expect(baggage[0]).toBeDefined(); + expect(baggage[0]).toEqual({ release: '2.1.14' }); + expect(baggage[1]).toBeDefined(); + expect(baggage[1]).toEqual('foo=bar'); }); it('ignores the data for navigation transactions', () => { mockChangeHistory = () => undefined; document.head.innerHTML = - ''; + '' + + ''; createBrowserTracing(true); mockChangeHistory({ to: 'here', from: 'there' }); const transaction = getActiveTransaction(hub) as IdleTransaction; + const baggage = transaction.getBaggage()!; expect(transaction).toBeDefined(); expect(transaction.op).toBe('navigation'); expect(transaction.traceId).not.toEqual('12312012123120121231201212312012'); expect(transaction.parentSpanId).toBeUndefined(); + expect(baggage).toBeUndefined(); }); }); }); @@ -472,7 +477,8 @@ describe('BrowserTracing', () => { getGlobalObject().location = dogParkLocation as any; const tracesSampler = jest.fn(); - hub.bindClient(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + hub.bindClient(new BrowserClient(options)); // setting up the BrowserTracing integration automatically starts a pageload transaction createBrowserTracing(true); @@ -488,7 +494,8 @@ describe('BrowserTracing', () => { getGlobalObject().location = dogParkLocation as any; const tracesSampler = jest.fn(); - hub.bindClient(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + hub.bindClient(new BrowserClient(options)); // setting up the BrowserTracing integration normally automatically starts a pageload transaction, but that's not // what we're testing here createBrowserTracing(true, { startTransactionOnPageLoad: false }); @@ -502,29 +509,4 @@ describe('BrowserTracing', () => { ); }); }); - - describe('metrics', () => { - beforeEach(() => { - // @ts-ignore mock clear - MetricsInstrumentation.mockClear(); - }); - - it('creates metrics instrumentation', () => { - createBrowserTracing(true, {}); - - expect(MetricsInstrumentation).toHaveBeenCalledTimes(1); - expect(MetricsInstrumentation).toHaveBeenLastCalledWith(undefined); - }); - - it('creates metrics instrumentation with custom options', () => { - createBrowserTracing(true, { - _metricOptions: { - _reportAllChanges: true, - }, - }); - - expect(MetricsInstrumentation).toHaveBeenCalledTimes(1); - expect(MetricsInstrumentation).toHaveBeenLastCalledWith(true); - }); - }); }); diff --git a/packages/tracing/test/browser/metrics.test.ts b/packages/tracing/test/browser/metrics.test.ts deleted file mode 100644 index 7133f7975221..000000000000 --- a/packages/tracing/test/browser/metrics.test.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { Span, Transaction } from '../../src'; -import { - _startChild, - addResourceSpans, - DEFAULT_METRICS_INSTR_OPTIONS, - MetricsInstrumentation, - ResourceEntry, -} from '../../src/browser/metrics'; -import { addDOMPropertiesToGlobal } from '../testutils'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any, no-var -declare var global: any; - -describe('_startChild()', () => { - it('creates a span with given properties', () => { - const transaction = new Transaction({ name: 'test' }); - const span = _startChild(transaction, { - description: 'evaluation', - op: 'script', - }); - - expect(span).toBeInstanceOf(Span); - expect(span.description).toBe('evaluation'); - expect(span.op).toBe('script'); - }); - - it('adjusts the start timestamp if child span starts before transaction', () => { - const transaction = new Transaction({ name: 'test', startTimestamp: 123 }); - const span = _startChild(transaction, { - description: 'script.js', - op: 'resource', - startTimestamp: 100, - }); - - expect(transaction.startTimestamp).toEqual(span.startTimestamp); - expect(transaction.startTimestamp).toEqual(100); - }); - - it('does not adjust start timestamp if child span starts after transaction', () => { - const transaction = new Transaction({ name: 'test', startTimestamp: 123 }); - const span = _startChild(transaction, { - description: 'script.js', - op: 'resource', - startTimestamp: 150, - }); - - expect(transaction.startTimestamp).not.toEqual(span.startTimestamp); - expect(transaction.startTimestamp).toEqual(123); - }); -}); - -describe('addResourceSpans', () => { - const transaction = new Transaction({ name: 'hello' }); - beforeEach(() => { - transaction.startChild = jest.fn(); - }); - - // We already track xhr, we don't need to use - it('does not create spans for xmlhttprequest', () => { - const entry: ResourceEntry = { - initiatorType: 'xmlhttprequest', - transferSize: 256, - encodedBodySize: 256, - decodedBodySize: 256, - }; - addResourceSpans(transaction, entry, '/assets/to/me', 123, 456, 100); - - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(transaction.startChild).toHaveBeenCalledTimes(0); - }); - - it('does not create spans for fetch', () => { - const entry: ResourceEntry = { - initiatorType: 'fetch', - transferSize: 256, - encodedBodySize: 256, - decodedBodySize: 256, - }; - addResourceSpans(transaction, entry, '/assets/to/me', 123, 456, 100); - - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(transaction.startChild).toHaveBeenCalledTimes(0); - }); - - it('creates spans for resource spans', () => { - const entry: ResourceEntry = { - initiatorType: 'css', - transferSize: 256, - encodedBodySize: 456, - decodedBodySize: 593, - }; - - const timeOrigin = 100; - const startTime = 23; - const duration = 356; - - addResourceSpans(transaction, entry, '/assets/to/css', startTime, duration, timeOrigin); - - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(transaction.startChild).toHaveBeenLastCalledWith({ - data: { - ['Decoded Body Size']: entry.decodedBodySize, - ['Encoded Body Size']: entry.encodedBodySize, - ['Transfer Size']: entry.transferSize, - }, - description: '/assets/to/css', - endTimestamp: timeOrigin + startTime + duration, - op: 'resource.css', - startTimestamp: timeOrigin + startTime, - }); - }); - - it('creates a variety of resource spans', () => { - const table = [ - { - initiatorType: undefined, - op: 'resource', - }, - { - initiatorType: '', - op: 'resource', - }, - { - initiatorType: 'css', - op: 'resource.css', - }, - { - initiatorType: 'image', - op: 'resource.image', - }, - { - initiatorType: 'script', - op: 'resource.script', - }, - ]; - - for (const { initiatorType, op } of table) { - const entry: ResourceEntry = { - initiatorType, - }; - addResourceSpans(transaction, entry, '/assets/to/me', 123, 234, 465); - - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(transaction.startChild).toHaveBeenLastCalledWith( - expect.objectContaining({ - op, - }), - ); - } - }); - - it('allows for enter size of 0', () => { - const entry: ResourceEntry = { - initiatorType: 'css', - transferSize: 0, - encodedBodySize: 0, - decodedBodySize: 0, - }; - - addResourceSpans(transaction, entry, '/assets/to/css', 100, 23, 345); - - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(transaction.startChild).toHaveBeenLastCalledWith( - expect.objectContaining({ - data: { - ['Decoded Body Size']: entry.decodedBodySize, - ['Encoded Body Size']: entry.encodedBodySize, - ['Transfer Size']: entry.transferSize, - }, - }), - ); - }); -}); - -describe('MetricsInstrumentation', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('does not initialize trackers when on node', () => { - const trackers = ['_trackCLS', '_trackLCP', '_trackFID'].map(tracker => - jest.spyOn(MetricsInstrumentation.prototype as any, tracker), - ); - - new MetricsInstrumentation(DEFAULT_METRICS_INSTR_OPTIONS); - - trackers.forEach(tracker => expect(tracker).not.toBeCalled()); - }); - - it('initializes trackers when not on node and `global.performance` and `global.document` are available.', () => { - addDOMPropertiesToGlobal(['performance', 'document', 'addEventListener', 'window']); - - const backup = global.process; - global.process = undefined; - - const trackers = ['_trackCLS', '_trackLCP', '_trackFID'].map(tracker => - jest.spyOn(MetricsInstrumentation.prototype as any, tracker), - ); - new MetricsInstrumentation(DEFAULT_METRICS_INSTR_OPTIONS); - global.process = backup; - - trackers.forEach(tracker => expect(tracker).toBeCalled()); - }); - - it('does not initialize trackers when not on node but `global.document` is not available (in worker)', () => { - // window not necessary for this test, but it is here to exercise that it is absence of document that is checked - addDOMPropertiesToGlobal(['performance', 'addEventListener', 'window']); - - const processBackup = global.process; - global.process = undefined; - const documentBackup = global.document; - global.document = undefined; - - const trackers = ['_trackCLS', '_trackLCP', '_trackFID'].map(tracker => - jest.spyOn(MetricsInstrumentation.prototype as any, tracker), - ); - new MetricsInstrumentation(); - global.process = processBackup; - global.document = documentBackup; - - trackers.forEach(tracker => expect(tracker).not.toBeCalled()); - }); -}); diff --git a/packages/tracing/test/browser/metrics/index.test.ts b/packages/tracing/test/browser/metrics/index.test.ts new file mode 100644 index 000000000000..b47ebd92a8f3 --- /dev/null +++ b/packages/tracing/test/browser/metrics/index.test.ts @@ -0,0 +1,162 @@ +import { Transaction } from '../../../src'; +import { _addMeasureSpans, _addResourceSpans, ResourceEntry } from '../../../src/browser/metrics'; + +describe('_addMeasureSpans', () => { + const transaction = new Transaction({ op: 'pageload', name: '/' }); + beforeEach(() => { + transaction.startChild = jest.fn(); + }); + + it('adds measure spans to a transaction', () => { + const entry: Omit = { + entryType: 'measure', + name: 'measure-1', + duration: 10, + startTime: 12, + }; + + const timeOrigin = 100; + const startTime = 23; + const duration = 356; + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(transaction.startChild).toHaveBeenCalledTimes(0); + _addMeasureSpans(transaction, entry, startTime, duration, timeOrigin); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(transaction.startChild).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(transaction.startChild).toHaveBeenLastCalledWith({ + description: 'measure-1', + startTimestamp: timeOrigin + startTime, + endTimestamp: timeOrigin + startTime + duration, + op: 'measure', + }); + }); +}); + +describe('_addResourceSpans', () => { + const transaction = new Transaction({ op: 'pageload', name: '/' }); + beforeEach(() => { + transaction.startChild = jest.fn(); + }); + + // We already track xhr, we don't need to use + it('does not create spans for xmlhttprequest', () => { + const entry: ResourceEntry = { + initiatorType: 'xmlhttprequest', + transferSize: 256, + encodedBodySize: 256, + decodedBodySize: 256, + }; + _addResourceSpans(transaction, entry, '/assets/to/me', 123, 456, 100); + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(transaction.startChild).toHaveBeenCalledTimes(0); + }); + + it('does not create spans for fetch', () => { + const entry: ResourceEntry = { + initiatorType: 'fetch', + transferSize: 256, + encodedBodySize: 256, + decodedBodySize: 256, + }; + _addResourceSpans(transaction, entry, '/assets/to/me', 123, 456, 100); + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(transaction.startChild).toHaveBeenCalledTimes(0); + }); + + it('creates spans for resource spans', () => { + const entry: ResourceEntry = { + initiatorType: 'css', + transferSize: 256, + encodedBodySize: 456, + decodedBodySize: 593, + }; + + const timeOrigin = 100; + const startTime = 23; + const duration = 356; + + _addResourceSpans(transaction, entry, '/assets/to/css', startTime, duration, timeOrigin); + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(transaction.startChild).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(transaction.startChild).toHaveBeenLastCalledWith({ + data: { + ['Decoded Body Size']: entry.decodedBodySize, + ['Encoded Body Size']: entry.encodedBodySize, + ['Transfer Size']: entry.transferSize, + }, + description: '/assets/to/css', + endTimestamp: timeOrigin + startTime + duration, + op: 'resource.css', + startTimestamp: timeOrigin + startTime, + }); + }); + + it('creates a variety of resource spans', () => { + const table = [ + { + initiatorType: undefined, + op: 'resource', + }, + { + initiatorType: '', + op: 'resource', + }, + { + initiatorType: 'css', + op: 'resource.css', + }, + { + initiatorType: 'image', + op: 'resource.image', + }, + { + initiatorType: 'script', + op: 'resource.script', + }, + ]; + + for (const { initiatorType, op } of table) { + const entry: ResourceEntry = { + initiatorType, + }; + _addResourceSpans(transaction, entry, '/assets/to/me', 123, 234, 465); + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(transaction.startChild).toHaveBeenLastCalledWith( + expect.objectContaining({ + op, + }), + ); + } + }); + + it('allows for enter size of 0', () => { + const entry: ResourceEntry = { + initiatorType: 'css', + transferSize: 0, + encodedBodySize: 0, + decodedBodySize: 0, + }; + + _addResourceSpans(transaction, entry, '/assets/to/css', 100, 23, 345); + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(transaction.startChild).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(transaction.startChild).toHaveBeenLastCalledWith( + expect.objectContaining({ + data: { + ['Decoded Body Size']: entry.decodedBodySize, + ['Encoded Body Size']: entry.encodedBodySize, + ['Transfer Size']: entry.transferSize, + }, + }), + ); + }); +}); diff --git a/packages/tracing/test/browser/metrics/utils.test.ts b/packages/tracing/test/browser/metrics/utils.test.ts new file mode 100644 index 000000000000..25f03c11af0b --- /dev/null +++ b/packages/tracing/test/browser/metrics/utils.test.ts @@ -0,0 +1,40 @@ +import { Span, Transaction } from '../../../src'; +import { _startChild } from '../../../src/browser/metrics/utils'; + +describe('_startChild()', () => { + it('creates a span with given properties', () => { + const transaction = new Transaction({ name: 'test' }); + const span = _startChild(transaction, { + description: 'evaluation', + op: 'script', + }); + + expect(span).toBeInstanceOf(Span); + expect(span.description).toBe('evaluation'); + expect(span.op).toBe('script'); + }); + + it('adjusts the start timestamp if child span starts before transaction', () => { + const transaction = new Transaction({ name: 'test', startTimestamp: 123 }); + const span = _startChild(transaction, { + description: 'script.js', + op: 'resource', + startTimestamp: 100, + }); + + expect(transaction.startTimestamp).toEqual(span.startTimestamp); + expect(transaction.startTimestamp).toEqual(100); + }); + + it('does not adjust start timestamp if child span starts after transaction', () => { + const transaction = new Transaction({ name: 'test', startTimestamp: 123 }); + const span = _startChild(transaction, { + description: 'script.js', + op: 'resource', + startTimestamp: 150, + }); + + expect(transaction.startTimestamp).not.toEqual(span.startTimestamp); + expect(transaction.startTimestamp).toEqual(123); + }); +}); diff --git a/packages/tracing/test/browser/request.test.ts b/packages/tracing/test/browser/request.test.ts index 2ba1c906818a..e77a0ae15c36 100644 --- a/packages/tracing/test/browser/request.test.ts +++ b/packages/tracing/test/browser/request.test.ts @@ -3,9 +3,10 @@ import { Hub, makeMain } from '@sentry/hub'; import * as utils from '@sentry/utils'; import { Span, spanStatusfromHttpCode, Transaction } from '../../src'; -import { fetchCallback, FetchData, instrumentOutgoingRequests, xhrCallback, XHRData } from '../../src/browser/request'; +import { fetchCallback, FetchData, instrumentOutgoingRequests, xhrCallback } from '../../src/browser/request'; import { addExtensionMethods } from '../../src/hubextensions'; import * as tracingUtils from '../../src/utils'; +import { getDefaultBrowserClientOptions } from '../testutils'; beforeAll(() => { addExtensionMethods(); @@ -55,7 +56,7 @@ describe('callbacks', () => { fetchData: { url: 'http://dogs.are.great/', method: 'GET' }, startTimestamp, }; - const xhrHandlerData: XHRData = { + const xhrHandlerData = { xhr: { __sentry_xhr__: { method: 'GET', @@ -72,7 +73,8 @@ describe('callbacks', () => { }; beforeAll(() => { - hub = new Hub(new BrowserClient({ tracesSampleRate: 1 })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); + hub = new Hub(new BrowserClient(options)); makeMain(hub); }); @@ -128,17 +130,17 @@ describe('callbacks', () => { // triggered by request being sent fetchCallback(fetchHandlerData, alwaysCreateSpan, spans); - const newSpan = transaction.spanRecorder?.spans[1]; + const newSpan = transaction.spanRecorder?.spans[1] as Span; expect(newSpan).toBeDefined(); expect(newSpan).toBeInstanceOf(Span); - expect(newSpan!.data).toEqual({ + expect(newSpan.data).toEqual({ method: 'GET', type: 'fetch', url: 'http://dogs.are.great/', }); - expect(newSpan!.description).toBe('GET http://dogs.are.great/'); - expect(newSpan!.op).toBe('http.client'); + expect(newSpan.description).toBe('GET http://dogs.are.great/'); + expect(newSpan.op).toBe('http.client'); expect(fetchHandlerData.fetchData?.__span).toBeDefined(); const postRequestFetchHandlerData = { @@ -149,7 +151,7 @@ describe('callbacks', () => { // triggered by response coming back fetchCallback(postRequestFetchHandlerData, alwaysCreateSpan, spans); - expect(newSpan!.endTimestamp).toBeDefined(); + expect(newSpan.endTimestamp).toBeDefined(); }); it('sets response status on finish', () => { @@ -158,7 +160,7 @@ describe('callbacks', () => { // triggered by request being sent fetchCallback(fetchHandlerData, alwaysCreateSpan, spans); - const newSpan = transaction.spanRecorder?.spans[1]; + const newSpan = transaction.spanRecorder?.spans[1] as Span; expect(newSpan).toBeDefined(); @@ -171,7 +173,7 @@ describe('callbacks', () => { // triggered by response coming back fetchCallback(postRequestFetchHandlerData, alwaysCreateSpan, spans); - expect(newSpan!.status).toBe(spanStatusfromHttpCode(404)); + expect(newSpan.status).toBe(spanStatusfromHttpCode(404)); }); it('ignores response with no associated span', () => { @@ -236,18 +238,18 @@ describe('callbacks', () => { // triggered by request being sent xhrCallback(xhrHandlerData, alwaysCreateSpan, spans); - const newSpan = transaction.spanRecorder?.spans[1]; + const newSpan = transaction.spanRecorder?.spans[1] as Span; expect(newSpan).toBeInstanceOf(Span); - expect(newSpan!.data).toEqual({ + expect(newSpan.data).toEqual({ method: 'GET', type: 'xhr', url: 'http://dogs.are.great/', }); - expect(newSpan!.description).toBe('GET http://dogs.are.great/'); - expect(newSpan!.op).toBe('http.client'); - expect(xhrHandlerData.xhr!.__sentry_xhr_span_id__).toBeDefined(); - expect(xhrHandlerData.xhr!.__sentry_xhr_span_id__).toEqual(newSpan?.spanId); + expect(newSpan.description).toBe('GET http://dogs.are.great/'); + expect(newSpan.op).toBe('http.client'); + expect(xhrHandlerData.xhr.__sentry_xhr_span_id__).toBeDefined(); + expect(xhrHandlerData.xhr.__sentry_xhr_span_id__).toEqual(newSpan?.spanId); const postRequestXHRHandlerData = { ...xhrHandlerData, @@ -257,7 +259,7 @@ describe('callbacks', () => { // triggered by response coming back xhrCallback(postRequestXHRHandlerData, alwaysCreateSpan, spans); - expect(newSpan!.endTimestamp).toBeDefined(); + expect(newSpan.endTimestamp).toBeDefined(); }); it('sets response status on finish', () => { @@ -266,7 +268,7 @@ describe('callbacks', () => { // triggered by request being sent xhrCallback(xhrHandlerData, alwaysCreateSpan, spans); - const newSpan = transaction.spanRecorder?.spans[1]; + const newSpan = transaction.spanRecorder?.spans[1] as Span; expect(newSpan).toBeDefined(); @@ -274,12 +276,12 @@ describe('callbacks', () => { ...xhrHandlerData, endTimestamp, }; - postRequestXHRHandlerData.xhr!.__sentry_xhr__!.status_code = 404; + postRequestXHRHandlerData.xhr.__sentry_xhr__.status_code = 404; // triggered by response coming back xhrCallback(postRequestXHRHandlerData, alwaysCreateSpan, spans); - expect(newSpan!.status).toBe(spanStatusfromHttpCode(404)); + expect(newSpan.status).toBe(spanStatusfromHttpCode(404)); }); it('ignores response with no associated span', () => { diff --git a/packages/tracing/test/browser/router.test.ts b/packages/tracing/test/browser/router.test.ts index e340a81222fe..9d1ae86f4e01 100644 --- a/packages/tracing/test/browser/router.test.ts +++ b/packages/tracing/test/browser/router.test.ts @@ -1,3 +1,4 @@ +import { InstrumentHandlerCallback, InstrumentHandlerType } from '@sentry/utils'; import { JSDOM } from 'jsdom'; import { instrumentRoutingWithDefaults } from '../../src/browser/router'; @@ -8,7 +9,7 @@ jest.mock('@sentry/utils', () => { const actual = jest.requireActual('@sentry/utils'); return { ...actual, - addInstrumentationHandler: (type, callback): void => { + addInstrumentationHandler: (type: InstrumentHandlerType, callback: InstrumentHandlerCallback): void => { addInstrumentationHandlerType = type; mockChangeHistory = callback; }, diff --git a/packages/tracing/test/errors.test.ts b/packages/tracing/test/errors.test.ts index 43583b352ef1..6d3a3fa50768 100644 --- a/packages/tracing/test/errors.test.ts +++ b/packages/tracing/test/errors.test.ts @@ -1,17 +1,19 @@ import { BrowserClient } from '@sentry/browser'; import { Hub, makeMain } from '@sentry/hub'; +import { InstrumentHandlerCallback, InstrumentHandlerType } from '@sentry/utils'; import { registerErrorInstrumentation } from '../src/errors'; import { _addTracingExtensions } from '../src/hubextensions'; +import { getDefaultBrowserClientOptions } from './testutils'; const mockAddInstrumentationHandler = jest.fn(); -let mockErrorCallback: () => void = () => undefined; -let mockUnhandledRejectionCallback: () => void = () => undefined; +let mockErrorCallback: InstrumentHandlerCallback = () => undefined; +let mockUnhandledRejectionCallback: InstrumentHandlerCallback = () => undefined; jest.mock('@sentry/utils', () => { const actual = jest.requireActual('@sentry/utils'); return { ...actual, - addInstrumentationHandler: (type, callback) => { + addInstrumentationHandler: (type: InstrumentHandlerType, callback: InstrumentHandlerCallback) => { if (type === 'error') { mockErrorCallback = callback; } @@ -33,7 +35,8 @@ describe('registerErrorHandlers()', () => { let hub: Hub; beforeEach(() => { mockAddInstrumentationHandler.mockClear(); - hub = new Hub(new BrowserClient({ tracesSampleRate: 1 })); + const options = getDefaultBrowserClientOptions(); + hub = new Hub(new BrowserClient(options)); makeMain(hub); }); @@ -53,10 +56,10 @@ describe('registerErrorHandlers()', () => { const transaction = hub.startTransaction({ name: 'test' }); expect(transaction.status).toBe(undefined); - mockErrorCallback(); + mockErrorCallback({}); expect(transaction.status).toBe(undefined); - mockUnhandledRejectionCallback(); + mockUnhandledRejectionCallback({}); expect(transaction.status).toBe(undefined); transaction.finish(); }); @@ -66,7 +69,7 @@ describe('registerErrorHandlers()', () => { const transaction = hub.startTransaction({ name: 'test' }); hub.configureScope(scope => scope.setSpan(transaction)); - mockErrorCallback(); + mockErrorCallback({}); expect(transaction.status).toBe('internal_error'); transaction.finish(); @@ -77,7 +80,7 @@ describe('registerErrorHandlers()', () => { const transaction = hub.startTransaction({ name: 'test' }); hub.configureScope(scope => scope.setSpan(transaction)); - mockUnhandledRejectionCallback(); + mockUnhandledRejectionCallback({}); expect(transaction.status).toBe('internal_error'); transaction.finish(); }); diff --git a/packages/tracing/test/hub.test.ts b/packages/tracing/test/hub.test.ts index 5e4c8a806cac..5fe7d84066ab 100644 --- a/packages/tracing/test/hub.test.ts +++ b/packages/tracing/test/hub.test.ts @@ -8,7 +8,12 @@ import { BrowserTracing } from '../src/browser/browsertracing'; import { addExtensionMethods } from '../src/hubextensions'; import { Transaction } from '../src/transaction'; import { extractTraceparentData, TRACEPARENT_REGEXP } from '../src/utils'; -import { addDOMPropertiesToGlobal, getSymbolObjectKeyByName, testOnlyIfNodeVersionAtLeast } from './testutils'; +import { + addDOMPropertiesToGlobal, + getDefaultBrowserClientOptions, + getSymbolObjectKeyByName, + testOnlyIfNodeVersionAtLeast, +} from './testutils'; addExtensionMethods(); @@ -32,7 +37,8 @@ describe('Hub', () => { describe('getTransaction()', () => { it('should find a transaction which has been set on the scope if sampled = true', () => { - const hub = new Hub(new BrowserClient({ tracesSampleRate: 1 })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); transaction.sampled = true; @@ -45,7 +51,8 @@ describe('Hub', () => { }); it('should find a transaction which has been set on the scope if sampled = false', () => { - const hub = new Hub(new BrowserClient({ tracesSampleRate: 1 })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark', sampled: false }); @@ -57,7 +64,8 @@ describe('Hub', () => { }); it("should not find an open transaction if it's not on the scope", () => { - const hub = new Hub(new BrowserClient({ tracesSampleRate: 1 })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -69,7 +77,8 @@ describe('Hub', () => { describe('default sample context', () => { it('should add transaction context data to default sample context', () => { const tracesSampler = jest.fn(); - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transactionContext = { @@ -85,7 +94,8 @@ describe('Hub', () => { it("should add parent's sampling decision to default sample context", () => { const tracesSampler = jest.fn(); - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const parentSamplingDecsion = false; @@ -103,8 +113,9 @@ describe('Hub', () => { describe('sample()', () => { it('should set sampled = false when tracing is disabled', () => { + const options = getDefaultBrowserClientOptions({}); // neither tracesSampleRate nor tracesSampler is defined -> tracing disabled - const hub = new Hub(new BrowserClient({})); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -112,7 +123,8 @@ describe('Hub', () => { }); it('should set sampled = false if tracesSampleRate is 0', () => { - const hub = new Hub(new BrowserClient({ tracesSampleRate: 0 })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 0 }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -120,7 +132,8 @@ describe('Hub', () => { }); it('should set sampled = true if tracesSampleRate is 1', () => { - const hub = new Hub(new BrowserClient({ tracesSampleRate: 1 })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -128,7 +141,8 @@ describe('Hub', () => { }); it('should set sampled = true if tracesSampleRate is 1 (without global hub)', () => { - const hub = new Hub(new BrowserClient({ tracesSampleRate: 1 })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); + const hub = new Hub(new BrowserClient(options)); const transaction = hub.startTransaction({ name: 'dogpark' }); expect(transaction.sampled).toBe(true); @@ -136,7 +150,8 @@ describe('Hub', () => { it("should call tracesSampler if it's defined", () => { const tracesSampler = jest.fn(); - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -145,7 +160,8 @@ describe('Hub', () => { it('should set sampled = false if tracesSampler returns 0', () => { const tracesSampler = jest.fn().mockReturnValue(0); - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -155,7 +171,8 @@ describe('Hub', () => { it('should set sampled = true if tracesSampler returns 1', () => { const tracesSampler = jest.fn().mockReturnValue(1); - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -165,7 +182,8 @@ describe('Hub', () => { it('should set sampled = true if tracesSampler returns 1 (without global hub)', () => { const tracesSampler = jest.fn().mockReturnValue(1); - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); const transaction = hub.startTransaction({ name: 'dogpark' }); expect(tracesSampler).toHaveBeenCalled(); @@ -175,7 +193,8 @@ describe('Hub', () => { it('should not try to override explicitly set positive sampling decision', () => { // so that the decision otherwise would be false const tracesSampler = jest.fn().mockReturnValue(0); - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark', sampled: true }); @@ -185,7 +204,8 @@ describe('Hub', () => { it('should not try to override explicitly set negative sampling decision', () => { // so that the decision otherwise would be true const tracesSampler = jest.fn().mockReturnValue(1); - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark', sampled: false }); @@ -195,7 +215,8 @@ describe('Hub', () => { it('should prefer tracesSampler to tracesSampleRate', () => { // make the two options do opposite things to prove precedence const tracesSampler = jest.fn().mockReturnValue(true); - const hub = new Hub(new BrowserClient({ tracesSampleRate: 0, tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 0, tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -205,7 +226,8 @@ describe('Hub', () => { it('should tolerate tracesSampler returning a boolean', () => { const tracesSampler = jest.fn().mockReturnValue(true); - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -215,7 +237,8 @@ describe('Hub', () => { it('should record sampling method when sampling decision is explicitly set', () => { const tracesSampler = jest.fn().mockReturnValue(0.1121); - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); hub.startTransaction({ name: 'dogpark', sampled: true }); @@ -226,7 +249,8 @@ describe('Hub', () => { it('should record sampling method and rate when sampling decision comes from tracesSampler', () => { const tracesSampler = jest.fn().mockReturnValue(0.1121); - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -236,7 +260,8 @@ describe('Hub', () => { }); it('should record sampling method when sampling decision is inherited', () => { - const hub = new Hub(new BrowserClient({ tracesSampleRate: 0.1121 })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 0.1121 }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); hub.startTransaction({ name: 'dogpark', parentSampled: true }); @@ -246,7 +271,8 @@ describe('Hub', () => { }); it('should record sampling method and rate when sampling decision comes from traceSampleRate', () => { - const hub = new Hub(new BrowserClient({ tracesSampleRate: 0.1121 })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 0.1121 }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -258,7 +284,8 @@ describe('Hub', () => { describe('isValidSampleRate()', () => { it("should reject tracesSampleRates which aren't numbers or booleans", () => { - const hub = new Hub(new BrowserClient({ tracesSampleRate: 'dogs!' as any })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 'dogs!' as any }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -266,7 +293,8 @@ describe('Hub', () => { }); it('should reject tracesSampleRates which are NaN', () => { - const hub = new Hub(new BrowserClient({ tracesSampleRate: 'dogs!' as any })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 'dogs!' as any }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -275,7 +303,8 @@ describe('Hub', () => { // the rate might be a boolean, but for our purposes, false is equivalent to 0 and true is equivalent to 1 it('should reject tracesSampleRates less than 0', () => { - const hub = new Hub(new BrowserClient({ tracesSampleRate: -26 })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: -26 }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -284,7 +313,8 @@ describe('Hub', () => { // the rate might be a boolean, but for our purposes, false is equivalent to 0 and true is equivalent to 1 it('should reject tracesSampleRates greater than 1', () => { - const hub = new Hub(new BrowserClient({ tracesSampleRate: 26 })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 26 }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -293,7 +323,8 @@ describe('Hub', () => { it("should reject tracesSampler return values which aren't numbers or booleans", () => { const tracesSampler = jest.fn().mockReturnValue('dogs!'); - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -302,7 +333,8 @@ describe('Hub', () => { it('should reject tracesSampler return values which are NaN', () => { const tracesSampler = jest.fn().mockReturnValue(NaN); - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -312,7 +344,8 @@ describe('Hub', () => { // the rate might be a boolean, but for our purposes, false is equivalent to 0 and true is equivalent to 1 it('should reject tracesSampler return values less than 0', () => { const tracesSampler = jest.fn().mockReturnValue(-12); - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -322,7 +355,8 @@ describe('Hub', () => { // the rate might be a boolean, but for our purposes, false is equivalent to 0 and true is equivalent to 1 it('should reject tracesSampler return values greater than 1', () => { const tracesSampler = jest.fn().mockReturnValue(31); - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -331,7 +365,8 @@ describe('Hub', () => { }); it('should drop transactions with sampled = false', () => { - const client = new BrowserClient({ tracesSampleRate: 0 }); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 0 }); + const client = new BrowserClient(options); jest.spyOn(client, 'captureEvent'); const hub = new Hub(client); @@ -348,7 +383,8 @@ describe('Hub', () => { describe('sampling inheritance', () => { it('should propagate sampling decision to child spans', () => { - const hub = new Hub(new BrowserClient({ tracesSampleRate: Math.random() })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: Math.random() }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); const child = transaction.startChild({ op: 'ball.chase' }); @@ -360,13 +396,12 @@ describe('Hub', () => { testOnlyIfNodeVersionAtLeast(10)( 'should propagate positive sampling decision to child transactions in XHR header', async () => { - const hub = new Hub( - new BrowserClient({ - dsn: 'https://1231@dogs.are.great/1121', - tracesSampleRate: 1, - integrations: [new BrowserTracing()], - }), - ); + const options = getDefaultBrowserClientOptions({ + dsn: 'https://1231@dogs.are.great/1121', + tracesSampleRate: 1, + integrations: [new BrowserTracing()], + }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -402,13 +437,12 @@ describe('Hub', () => { testOnlyIfNodeVersionAtLeast(10)( 'should propagate negative sampling decision to child transactions in XHR header', async () => { - const hub = new Hub( - new BrowserClient({ - dsn: 'https://1231@dogs.are.great/1121', - tracesSampleRate: 1, - integrations: [new BrowserTracing()], - }), - ); + const options = getDefaultBrowserClientOptions({ + dsn: 'https://1231@dogs.are.great/1121', + tracesSampleRate: 1, + integrations: [new BrowserTracing()], + }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark', sampled: false }); @@ -453,7 +487,8 @@ describe('Hub', () => { // sample rate), so make parent's decision the opposite to prove that inheritance takes precedence over // tracesSampleRate mathRandom.mockReturnValueOnce(1); - const hub = new Hub(new BrowserClient({ tracesSampleRate: 0.5 })); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 0.5 }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const parentSamplingDecsion = true; @@ -467,9 +502,10 @@ describe('Hub', () => { }); it("should inherit parent's negative sampling decision if tracesSampler is undefined", () => { + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); // tracesSampleRate = 1 means every transaction should end up with sampled = true, so make parent's decision the // opposite to prove that inheritance takes precedence over tracesSampleRate - const hub = new Hub(new BrowserClient({ tracesSampleRate: 1 })); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const parentSamplingDecsion = false; @@ -488,7 +524,8 @@ describe('Hub', () => { const tracesSampler = () => true; const parentSamplingDecsion = false; - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transaction = hub.startTransaction({ @@ -506,7 +543,8 @@ describe('Hub', () => { const tracesSampler = () => false; const parentSamplingDecsion = true; - const hub = new Hub(new BrowserClient({ tracesSampler })); + const options = getDefaultBrowserClientOptions({ tracesSampler }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); const transaction = hub.startTransaction({ diff --git a/packages/tracing/test/idletransaction.test.ts b/packages/tracing/test/idletransaction.test.ts index b60fec726c54..956de96b5ea5 100644 --- a/packages/tracing/test/idletransaction.test.ts +++ b/packages/tracing/test/idletransaction.test.ts @@ -1,26 +1,27 @@ -import { BrowserClient, Transports } from '@sentry/browser'; +import { BrowserClient } from '@sentry/browser'; import { Hub } from '@sentry/hub'; import { + DEFAULT_FINAL_TIMEOUT, DEFAULT_IDLE_TIMEOUT, HEARTBEAT_INTERVAL, IdleTransaction, IdleTransactionSpanRecorder, } from '../src/idletransaction'; import { Span } from '../src/span'; - -export class SimpleTransport extends Transports.BaseTransport {} +import { getDefaultBrowserClientOptions } from './testutils'; const dsn = 'https://123@sentry.io/42'; let hub: Hub; beforeEach(() => { - hub = new Hub(new BrowserClient({ dsn, tracesSampleRate: 1, transport: SimpleTransport })); + const options = getDefaultBrowserClientOptions({ dsn, tracesSampleRate: 1 }); + hub = new Hub(new BrowserClient(options)); }); describe('IdleTransaction', () => { describe('onScope', () => { it('sets the transaction on the scope on creation if onScope is true', () => { - const transaction = new IdleTransaction({ name: 'foo' }, hub, DEFAULT_IDLE_TIMEOUT, true); + const transaction = new IdleTransaction({ name: 'foo' }, hub, DEFAULT_IDLE_TIMEOUT, DEFAULT_FINAL_TIMEOUT, true); transaction.initSpanRecorder(10); hub.configureScope(s => { @@ -29,7 +30,7 @@ describe('IdleTransaction', () => { }); it('does not set the transaction on the scope on creation if onScope is falsey', () => { - const transaction = new IdleTransaction({ name: 'foo' }, hub, DEFAULT_IDLE_TIMEOUT); + const transaction = new IdleTransaction({ name: 'foo' }, hub); transaction.initSpanRecorder(10); hub.configureScope(s => { @@ -38,7 +39,7 @@ describe('IdleTransaction', () => { }); it('removes sampled transaction from scope on finish if onScope is true', () => { - const transaction = new IdleTransaction({ name: 'foo' }, hub, DEFAULT_IDLE_TIMEOUT, true); + const transaction = new IdleTransaction({ name: 'foo' }, hub, DEFAULT_IDLE_TIMEOUT, DEFAULT_FINAL_TIMEOUT, true); transaction.initSpanRecorder(10); transaction.finish(); @@ -50,7 +51,13 @@ describe('IdleTransaction', () => { }); it('removes unsampled transaction from scope on finish if onScope is true', () => { - const transaction = new IdleTransaction({ name: 'foo', sampled: false }, hub, DEFAULT_IDLE_TIMEOUT, true); + const transaction = new IdleTransaction( + { name: 'foo', sampled: false }, + hub, + DEFAULT_IDLE_TIMEOUT, + DEFAULT_FINAL_TIMEOUT, + true, + ); transaction.finish(); jest.runAllTimers(); @@ -66,7 +73,7 @@ describe('IdleTransaction', () => { }); it('push and pops activities', () => { - const transaction = new IdleTransaction({ name: 'foo' }, hub, DEFAULT_IDLE_TIMEOUT); + const transaction = new IdleTransaction({ name: 'foo' }, hub); const mockFinish = jest.spyOn(transaction, 'finish'); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); @@ -84,7 +91,7 @@ describe('IdleTransaction', () => { }); it('does not push activities if a span already has an end timestamp', () => { - const transaction = new IdleTransaction({ name: 'foo' }, hub, DEFAULT_IDLE_TIMEOUT); + const transaction = new IdleTransaction({ name: 'foo' }, hub); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); @@ -93,7 +100,7 @@ describe('IdleTransaction', () => { }); it('does not finish if there are still active activities', () => { - const transaction = new IdleTransaction({ name: 'foo' }, hub, DEFAULT_IDLE_TIMEOUT); + const transaction = new IdleTransaction({ name: 'foo' }, hub); const mockFinish = jest.spyOn(transaction, 'finish'); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); @@ -103,7 +110,7 @@ describe('IdleTransaction', () => { expect(transaction.activities).toMatchObject({ [span.spanId]: true, [childSpan.spanId]: true }); span.finish(); - jest.runOnlyPendingTimers(); + jest.advanceTimersByTime(DEFAULT_IDLE_TIMEOUT + 1); expect(mockFinish).toHaveBeenCalledTimes(0); expect(transaction.activities).toMatchObject({ [childSpan.spanId]: true }); @@ -112,7 +119,7 @@ describe('IdleTransaction', () => { it('calls beforeFinish callback before finishing', () => { const mockCallback1 = jest.fn(); const mockCallback2 = jest.fn(); - const transaction = new IdleTransaction({ name: 'foo' }, hub, DEFAULT_IDLE_TIMEOUT); + const transaction = new IdleTransaction({ name: 'foo' }, hub); transaction.initSpanRecorder(10); transaction.registerBeforeFinishCallback(mockCallback1); transaction.registerBeforeFinishCallback(mockCallback2); @@ -131,7 +138,7 @@ describe('IdleTransaction', () => { }); it('filters spans on finish', () => { - const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, DEFAULT_IDLE_TIMEOUT); + const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub); transaction.initSpanRecorder(10); // regular child - should be kept @@ -166,19 +173,19 @@ describe('IdleTransaction', () => { it('should record dropped transactions', async () => { const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234, sampled: false }, hub, 1000); - const transport = hub.getClient()?.getTransport(); + const client = hub.getClient()!; - const spy = jest.spyOn(transport!, 'recordLostEvent'); + const recordDroppedEventSpy = jest.spyOn(client, 'recordDroppedEvent'); transaction.initSpanRecorder(10); transaction.finish(transaction.startTimestamp + 10); - expect(spy).toHaveBeenCalledWith('sample_rate', 'transaction'); + expect(recordDroppedEventSpy).toHaveBeenCalledWith('sample_rate', 'transaction'); }); - describe('_initTimeout', () => { + describe('_idleTimeout', () => { it('finishes if no activities are added to the transaction', () => { - const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, DEFAULT_IDLE_TIMEOUT); + const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub); transaction.initSpanRecorder(10); jest.advanceTimersByTime(DEFAULT_IDLE_TIMEOUT); @@ -186,13 +193,49 @@ describe('IdleTransaction', () => { }); it('does not finish if a activity is started', () => { - const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, DEFAULT_IDLE_TIMEOUT); + const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub); transaction.initSpanRecorder(10); transaction.startChild({}); jest.advanceTimersByTime(DEFAULT_IDLE_TIMEOUT); expect(transaction.endTimestamp).toBeUndefined(); }); + + it('does not finish when idleTimeout is not exceed after last activity finished', () => { + const idleTimeout = 10; + const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); + transaction.initSpanRecorder(10); + + const span = transaction.startChild({}); + span.finish(); + + jest.advanceTimersByTime(2); + + const span2 = transaction.startChild({}); + span2.finish(); + + jest.advanceTimersByTime(8); + + expect(transaction.endTimestamp).toBeUndefined(); + }); + + it('finish when idleTimeout is exceeded after last activity finished', () => { + const idleTimeout = 10; + const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); + transaction.initSpanRecorder(10); + + const span = transaction.startChild({}); + span.finish(); + + jest.advanceTimersByTime(2); + + const span2 = transaction.startChild({}); + span2.finish(); + + jest.advanceTimersByTime(10); + + expect(transaction.endTimestamp).toBeDefined(); + }); }); describe('heartbeat', () => { @@ -229,20 +272,20 @@ describe('IdleTransaction', () => { transaction.startChild({}); // Beat 1 - jest.runOnlyPendingTimers(); + jest.advanceTimersByTime(HEARTBEAT_INTERVAL); expect(mockFinish).toHaveBeenCalledTimes(0); // Beat 2 - jest.runOnlyPendingTimers(); + jest.advanceTimersByTime(HEARTBEAT_INTERVAL); expect(mockFinish).toHaveBeenCalledTimes(0); // Beat 3 - jest.runOnlyPendingTimers(); + jest.advanceTimersByTime(HEARTBEAT_INTERVAL); expect(mockFinish).toHaveBeenCalledTimes(1); }); it('resets after new activities are added', () => { - const transaction = new IdleTransaction({ name: 'foo' }, hub, DEFAULT_IDLE_TIMEOUT); + const transaction = new IdleTransaction({ name: 'foo' }, hub, DEFAULT_IDLE_TIMEOUT, 50000); const mockFinish = jest.spyOn(transaction, 'finish'); transaction.initSpanRecorder(10); @@ -250,42 +293,42 @@ describe('IdleTransaction', () => { transaction.startChild({}); // Beat 1 - jest.runOnlyPendingTimers(); + jest.advanceTimersByTime(HEARTBEAT_INTERVAL); expect(mockFinish).toHaveBeenCalledTimes(0); const span = transaction.startChild(); // push activity // Beat 1 - jest.runOnlyPendingTimers(); + jest.advanceTimersByTime(HEARTBEAT_INTERVAL); expect(mockFinish).toHaveBeenCalledTimes(0); // Beat 2 - jest.runOnlyPendingTimers(); + jest.advanceTimersByTime(HEARTBEAT_INTERVAL); expect(mockFinish).toHaveBeenCalledTimes(0); transaction.startChild(); // push activity transaction.startChild(); // push activity // Beat 1 - jest.runOnlyPendingTimers(); + jest.advanceTimersByTime(HEARTBEAT_INTERVAL); expect(mockFinish).toHaveBeenCalledTimes(0); // Beat 2 - jest.runOnlyPendingTimers(); + jest.advanceTimersByTime(HEARTBEAT_INTERVAL); expect(mockFinish).toHaveBeenCalledTimes(0); span.finish(); // pop activity // Beat 1 - jest.runOnlyPendingTimers(); + jest.advanceTimersByTime(HEARTBEAT_INTERVAL); expect(mockFinish).toHaveBeenCalledTimes(0); // Beat 2 - jest.runOnlyPendingTimers(); + jest.advanceTimersByTime(HEARTBEAT_INTERVAL); expect(mockFinish).toHaveBeenCalledTimes(0); // Beat 3 - jest.runOnlyPendingTimers(); + jest.advanceTimersByTime(HEARTBEAT_INTERVAL); expect(mockFinish).toHaveBeenCalledTimes(1); // Heartbeat does not keep going after finish has been called @@ -299,7 +342,7 @@ describe('IdleTransactionSpanRecorder', () => { it('pushes and pops activities', () => { const mockPushActivity = jest.fn(); const mockPopActivity = jest.fn(); - const spanRecorder = new IdleTransactionSpanRecorder(mockPushActivity, mockPopActivity, undefined, 10); + const spanRecorder = new IdleTransactionSpanRecorder(mockPushActivity, mockPopActivity, '', 10); expect(mockPushActivity).toHaveBeenCalledTimes(0); expect(mockPopActivity).toHaveBeenCalledTimes(0); @@ -322,7 +365,7 @@ describe('IdleTransactionSpanRecorder', () => { it('does not push activities if a span has a timestamp', () => { const mockPushActivity = jest.fn(); const mockPopActivity = jest.fn(); - const spanRecorder = new IdleTransactionSpanRecorder(mockPushActivity, mockPopActivity, undefined, 10); + const spanRecorder = new IdleTransactionSpanRecorder(mockPushActivity, mockPopActivity, '', 10); const span = new Span({ sampled: true, startTimestamp: 765, endTimestamp: 345 }); spanRecorder.add(span); @@ -334,7 +377,7 @@ describe('IdleTransactionSpanRecorder', () => { const mockPushActivity = jest.fn(); const mockPopActivity = jest.fn(); - const transaction = new IdleTransaction({ name: 'foo' }, hub, DEFAULT_IDLE_TIMEOUT); + const transaction = new IdleTransaction({ name: 'foo' }, hub); const spanRecorder = new IdleTransactionSpanRecorder(mockPushActivity, mockPopActivity, transaction.spanId, 10); spanRecorder.add(transaction); diff --git a/packages/tracing/test/index.bundle.test.ts b/packages/tracing/test/index.bundle.test.ts index 319daa826ce6..91f643128d21 100644 --- a/packages/tracing/test/index.bundle.test.ts +++ b/packages/tracing/test/index.bundle.test.ts @@ -3,7 +3,14 @@ import { Integrations } from '../src/index.bundle'; describe('Integrations export', () => { it('is exported correctly', () => { Object.keys(Integrations).forEach(key => { - expect(Integrations[key as keyof typeof Integrations].id).toStrictEqual(expect.any(String)); + // Skip BrowserTracing because it doesn't have a static id field. + if (key === 'BrowserTracing') { + return; + } + + expect(Integrations[key as keyof Omit].id).toStrictEqual( + expect.any(String), + ); }); }); }); diff --git a/packages/tracing/test/index.test.ts b/packages/tracing/test/index.test.ts index 8837e2063cc7..c01b8cad0b54 100644 --- a/packages/tracing/test/index.test.ts +++ b/packages/tracing/test/index.test.ts @@ -12,7 +12,14 @@ describe('index', () => { describe('Integrations', () => { it('is exported correctly', () => { Object.keys(Integrations).forEach(key => { - expect(Integrations[key as keyof typeof Integrations].id).toStrictEqual(expect.any(String)); + // Skip BrowserTracing because it doesn't have a static id field. + if (key === 'BrowserTracing') { + return; + } + + expect(Integrations[key as keyof Omit].id).toStrictEqual( + expect.any(String), + ); }); }); diff --git a/packages/tracing/test/integrations/node/prisma.test.ts b/packages/tracing/test/integrations/node/prisma.test.ts new file mode 100644 index 000000000000..501101dbce6f --- /dev/null +++ b/packages/tracing/test/integrations/node/prisma.test.ts @@ -0,0 +1,61 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import { Hub, Scope } from '@sentry/hub'; + +import { Prisma } from '../../../src/integrations/node/prisma'; +import { Span } from '../../../src/span'; + +type PrismaMiddleware = (params: unknown, next: (params?: unknown) => Promise) => Promise; + +class PrismaClient { + public user: { create: () => Promise | undefined } = { + create: () => this._middleware?.({ action: 'create', model: 'user' }, () => Promise.resolve('result')), + }; + + private _middleware?: PrismaMiddleware; + + constructor() { + this._middleware = undefined; + } + + public $use(cb: PrismaMiddleware) { + this._middleware = cb; + } +} + +describe('setupOnce', function () { + const Client: PrismaClient = new PrismaClient(); + + let scope = new Scope(); + let parentSpan: Span; + let childSpan: Span; + + beforeAll(() => { + // @ts-ignore, not to export PrismaClient types from integration source + new Prisma({ client: Client }).setupOnce( + () => undefined, + () => new Hub(undefined, scope), + ); + }); + + beforeEach(() => { + scope = new Scope(); + parentSpan = new Span(); + childSpan = parentSpan.startChild(); + jest.spyOn(scope, 'getSpan').mockReturnValueOnce(parentSpan); + jest.spyOn(parentSpan, 'startChild').mockReturnValueOnce(childSpan); + jest.spyOn(childSpan, 'finish'); + }); + + it('should add middleware with $use method correctly', done => { + void Client.user.create()?.then(res => { + expect(res).toBe('result'); + expect(scope.getSpan).toBeCalled(); + expect(parentSpan.startChild).toBeCalledWith({ + description: 'user create', + op: 'db.prisma', + }); + expect(childSpan.finish).toBeCalled(); + done(); + }); + }); +}); diff --git a/packages/tracing/test/span.test.ts b/packages/tracing/test/span.test.ts index 4b0887f2dda7..27df400ffc9e 100644 --- a/packages/tracing/test/span.test.ts +++ b/packages/tracing/test/span.test.ts @@ -3,13 +3,15 @@ import { Hub, makeMain, Scope } from '@sentry/hub'; import { Span, Transaction } from '../src'; import { TRACEPARENT_REGEXP } from '../src/utils'; +import { getDefaultBrowserClientOptions } from './testutils'; describe('Span', () => { let hub: Hub; beforeEach(() => { const myScope = new Scope(); - hub = new Hub(new BrowserClient({ tracesSampleRate: 1 }), myScope); + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); + hub = new Hub(new BrowserClient(options), myScope); makeMain(hub); }); @@ -216,12 +218,11 @@ describe('Span', () => { }); test('maxSpans correctly limits number of spans', () => { - const _hub = new Hub( - new BrowserClient({ - _experiments: { maxSpans: 3 }, - tracesSampleRate: 1, - }), - ); + const options = getDefaultBrowserClientOptions({ + _experiments: { maxSpans: 3 }, + tracesSampleRate: 1, + }); + const _hub = new Hub(new BrowserClient(options)); const spy = jest.spyOn(_hub as any, 'captureEvent') as any; const transaction = _hub.startTransaction({ name: 'test' }); for (let i = 0; i < 10; i++) { @@ -233,11 +234,10 @@ describe('Span', () => { }); test('no span recorder created if transaction.sampled is false', () => { - const _hub = new Hub( - new BrowserClient({ - tracesSampleRate: 1, - }), - ); + const options = getDefaultBrowserClientOptions({ + tracesSampleRate: 1, + }); + const _hub = new Hub(new BrowserClient(options)); const spy = jest.spyOn(_hub as any, 'captureEvent') as any; const transaction = _hub.startTransaction({ name: 'test', sampled: false }); for (let i = 0; i < 10; i++) { diff --git a/packages/tracing/test/testutils.ts b/packages/tracing/test/testutils.ts index a8f42f84b9b8..5b4594c64913 100644 --- a/packages/tracing/test/testutils.ts +++ b/packages/tracing/test/testutils.ts @@ -1,4 +1,6 @@ -import { getGlobalObject } from '@sentry/utils'; +import { createTransport } from '@sentry/browser'; +import { ClientOptions } from '@sentry/types'; +import { getGlobalObject, resolvedSyncPromise } from '@sentry/utils'; import { JSDOM } from 'jsdom'; /** @@ -56,3 +58,12 @@ export const testOnlyIfNodeVersionAtLeast = (minVersion: number): jest.It => { return it; }; + +export function getDefaultBrowserClientOptions(options: Partial = {}): ClientOptions { + return { + integrations: [], + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})), + stackParser: () => [], + ...options, + }; +} diff --git a/packages/tracing/tsconfig.cjs.json b/packages/tracing/tsconfig.cjs.json deleted file mode 100644 index 6782dae5e453..000000000000 --- a/packages/tracing/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "build/npm/dist" - } -} diff --git a/packages/tracing/tsconfig.esm.json b/packages/tracing/tsconfig.esm.json deleted file mode 100644 index feffe52ca581..000000000000 --- a/packages/tracing/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "build/npm/esm" - } -} diff --git a/packages/tracing/tsconfig.types.json b/packages/tracing/tsconfig.types.json index 374fd9bc9364..1df465fa6534 100644 --- a/packages/tracing/tsconfig.types.json +++ b/packages/tracing/tsconfig.types.json @@ -1,6 +1,12 @@ { "extends": "./tsconfig.json", + // We don't need types for this because we don't ship it in our npm bundle. Skipping it here also lets us get around + // the fact that it introduces a dependency on `@sentry/browser` which doesn't exist anywhere else in the SDK, which + // then prevents us from building that and this at the same time when doing a parallellized build from the repo root + // level. + "exclude": ["src/index.bundle.ts"], + "compilerOptions": { "declaration": true, "declarationMap": true, diff --git a/packages/types/.npmignore b/packages/types/.npmignore deleted file mode 100644 index 4ab7cc4f278f..000000000000 --- a/packages/types/.npmignore +++ /dev/null @@ -1,11 +0,0 @@ -# The paths in this file are specified so that they align with the file structure in `./build` after this file is copied -# into it by the prepack script `scripts/prepack.ts`. - -* - -# TODO remove bundles (which in the tarball are inside `build`) in v7 -!/build/**/* - -!/dist/**/* -!/esm/**/* -!/types/**/* diff --git a/packages/types/README.md b/packages/types/README.md index 6d0a1322cffa..c5870f59ba41 100644 --- a/packages/types/README.md +++ b/packages/types/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Sentry JavaScript SDK Types diff --git a/packages/types/package.json b/packages/types/package.json index 84c43bb53198..cb5fcb05967b 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,36 +1,31 @@ { "name": "@sentry/types", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Types for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/types", "author": "Sentry", "license": "BSD-3-Clause", "engines": { - "node": ">=6" + "node": ">=8" }, - "main": "build/dist/index.js", + "main": "build/cjs/index.js", "module": "build/esm/index.js", "types": "build/types/index.d.ts", "publishConfig": { "access": "public" }, "scripts": { - "build": "run-p build:cjs build:esm build:types", - "build:cjs": "tsc -p tsconfig.cjs.json", + "build": "run-p build:rollup build:types", "build:dev": "run-s build", - "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "tsc -p tsconfig.esm.json", + "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", - "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", + "build:watch": "run-p build:rollup:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:es5:watch": "yarn build:cjs:watch # *** backwards compatibility - remove in v7 ***", - "build:esm:watch": "tsc -p tsconfig.esm.json --watch", + "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", - "clean": "rimraf dist esm build", - "link:yarn": "yarn link", + "clean": "rimraf build sentry-types-*.tgz", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", diff --git a/packages/types/rollup.npm.config.js b/packages/types/rollup.npm.config.js new file mode 100644 index 000000000000..5a62b528ef44 --- /dev/null +++ b/packages/types/rollup.npm.config.js @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/types/src/attachment.ts b/packages/types/src/attachment.ts new file mode 100644 index 000000000000..55cc795732ea --- /dev/null +++ b/packages/types/src/attachment.ts @@ -0,0 +1,6 @@ +export interface Attachment { + data: string | Uint8Array; + filename: string; + contentType?: string; + attachmentType?: string; +} diff --git a/packages/types/src/baggage.ts b/packages/types/src/baggage.ts new file mode 100644 index 000000000000..58fc5f294347 --- /dev/null +++ b/packages/types/src/baggage.ts @@ -0,0 +1,17 @@ +export type AllowedBaggageKeys = 'environment' | 'release' | 'userid' | 'transaction' | 'usersegment'; +export type BaggageObj = Partial & Record>; + +/** + * The baggage data structure represents key,value pairs based on the baggage + * spec: https://www.w3.org/TR/baggage + * + * It is expected that users interact with baggage using the helpers methods: + * `createBaggage`, `getBaggageValue`, and `setBaggageValue`. + * + * Internally, the baggage data structure is a tuple of length 2, separating baggage values + * based on if they are related to Sentry or not. If the baggage values are + * set/used by sentry, they will be stored in an object to be easily accessed. + * If they are not, they are kept as a string to be only accessed when serialized + * at baggage propagation time. + */ +export type Baggage = [BaggageObj, string]; diff --git a/packages/types/src/breadcrumb.ts b/packages/types/src/breadcrumb.ts index cab5ac68b3e1..34fe2dfcd16a 100644 --- a/packages/types/src/breadcrumb.ts +++ b/packages/types/src/breadcrumb.ts @@ -1,9 +1,10 @@ -import { Severity } from './severity'; +import { Severity, SeverityLevel } from './severity'; /** JSDoc */ export interface Breadcrumb { type?: string; - level?: Severity; + // eslint-disable-next-line deprecation/deprecation + level?: Severity | SeverityLevel; event_id?: string; category?: string; message?: string; diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 69f7298105b2..b09557ccba75 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -1,10 +1,12 @@ +import { EventDropReason } from './clientreport'; +import { DataCategory } from './datacategory'; import { DsnComponents } from './dsn'; import { Event, EventHint } from './event'; import { Integration, IntegrationClass } from './integration'; -import { Options } from './options'; +import { ClientOptions } from './options'; import { Scope } from './scope'; -import { Session } from './session'; -import { Severity } from './severity'; +import { Session, SessionAggregates } from './session'; +import { Severity, SeverityLevel } from './severity'; import { Transport } from './transport'; /** @@ -16,7 +18,7 @@ import { Transport } from './transport'; * there will only be one instance during runtime. * */ -export interface Client { +export interface Client { /** * Captures an exception event and sends it to Sentry. * @@ -36,7 +38,13 @@ export interface Client { * @param scope An optional scope containing event metadata. * @returns The event id */ - captureMessage(message: string, level?: Severity, hint?: EventHint, scope?: Scope): string | undefined; + captureMessage( + message: string, + // eslint-disable-next-line deprecation/deprecation + level?: Severity | SeverityLevel, + hint?: EventHint, + scope?: Scope, + ): string | undefined; /** * Captures a manually created event and sends it to Sentry. @@ -48,7 +56,8 @@ export interface Client { */ captureEvent(event: Event, hint?: EventHint, scope?: Scope): string | undefined; - /** Captures a session + /** + * Captures a session * * @param session Session to be delivered */ @@ -60,8 +69,13 @@ export interface Client { /** Returns the current options. */ getOptions(): O; - /** Returns clients transport. */ - getTransport?(): Transport; + /** + * Returns the transport that is used by the client. + * Please note that the transport gets lazy initialized so it will only be there once the first event has been sent. + * + * @returns The transport. + */ + getTransport(): Transport | undefined; /** * Flush the event queue and set the client to `enabled = false`. See {@link Client.flush}. @@ -88,4 +102,30 @@ export interface Client { /** This is an internal function to setup all integrations that should run on the client */ setupIntegrations(): void; + + /** Creates an {@link Event} from all inputs to `captureException` and non-primitive inputs to `captureMessage`. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + eventFromException(exception: any, hint?: EventHint): PromiseLike; + + /** Creates an {@link Event} from primitive inputs to `captureMessage`. */ + eventFromMessage( + message: string, + // eslint-disable-next-line deprecation/deprecation + level?: Severity | SeverityLevel, + hint?: EventHint, + ): PromiseLike; + + /** Submits the event to Sentry */ + sendEvent(event: Event, hint?: EventHint): void; + + /** Submits the session to Sentry */ + sendSession(session: Session | SessionAggregates): void; + + /** + * Record on the client that an event got dropped (ie, an event that will not be sent to sentry). + * + * @param reason The reason why the event got dropped. + * @param category The data category of the dropped event. + */ + recordDroppedEvent(reason: EventDropReason, category: DataCategory): void; } diff --git a/packages/types/src/clientreport.ts b/packages/types/src/clientreport.ts index 22c590d0cc64..79b3c2a4454c 100644 --- a/packages/types/src/clientreport.ts +++ b/packages/types/src/clientreport.ts @@ -1,7 +1,20 @@ -import { SentryRequestType } from './request'; -import { Outcome } from './transport'; +import { DataCategory } from './datacategory'; + +export type EventDropReason = + | 'before_send' + | 'event_processor' + | 'network_error' + | 'queue_overflow' + | 'ratelimit_backoff' + | 'sample_rate'; + +export type Outcome = { + reason: EventDropReason; + category: DataCategory; + quantity: number; +}; export type ClientReport = { timestamp: number; - discarded_events: Array<{ reason: Outcome; category: SentryRequestType; quantity: number }>; + discarded_events: Outcome[]; }; diff --git a/packages/types/src/datacategory.ts b/packages/types/src/datacategory.ts new file mode 100644 index 000000000000..dee9fc16d270 --- /dev/null +++ b/packages/types/src/datacategory.ts @@ -0,0 +1,20 @@ +// This type is used in various places like Client Reports and Rate Limit Categories +// See: +// - https://develop.sentry.dev/sdk/rate-limiting/#definitions +// - https://github.com/getsentry/relay/blob/10874b587bb676bd6d50ad42d507216513660082/relay-common/src/constants.rs#L97-L113 +// - https://develop.sentry.dev/sdk/client-reports/#envelope-item-payload under `discarded_events` +export type DataCategory = + // Reserved and only used in edgecases, unlikely to be ever actually used + | 'default' + // Error events + | 'error' + // Transaction type event + | 'transaction' + // Events with `event_type` csp, hpkp, expectct, expectstaple + | 'security' + // Attachment bytes stored (unused for rate limiting + | 'attachment' + // Session update events + | 'session' + // SDK internal event, like client_reports + | 'internal'; diff --git a/packages/types/src/dsn.ts b/packages/types/src/dsn.ts index b21130802903..761c52889caf 100644 --- a/packages/types/src/dsn.ts +++ b/packages/types/src/dsn.ts @@ -5,8 +5,6 @@ export type DsnProtocol = 'http' | 'https'; export interface DsnComponents { /** Protocol used to connect to Sentry. */ protocol: DsnProtocol; - /** Public authorization key (deprecated, renamed to publicKey). */ - user?: string; /** Public authorization key. */ publicKey?: string; /** Private authorization key (deprecated, optional). */ diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index 8e5091dbbfa9..14df524e37b8 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -1,12 +1,35 @@ import { ClientReport } from './clientreport'; +import { DsnComponents } from './dsn'; import { Event } from './event'; import { SdkInfo } from './sdkinfo'; import { Session, SessionAggregates } from './session'; -import { TransactionSamplingMethod } from './transaction'; +import { Transaction, TransactionSamplingMethod } from './transaction'; import { UserFeedback } from './user'; // Based on: https://develop.sentry.dev/sdk/envelopes/ +// Based on https://github.com/getsentry/relay/blob/b23b8d3b2360a54aaa4d19ecae0231201f31df5e/relay-sampling/src/lib.rs#L685-L707 +export type EventTraceContext = { + trace_id: Transaction['traceId']; + public_key: DsnComponents['publicKey']; + release?: string; + user?: { + id?: string; + segment?: string; + }; + environment?: string; + transaction?: string; +}; + +export type EnvelopeItemType = + | 'client_report' + | 'user_report' + | 'session' + | 'sessions' + | 'transaction' + | 'attachment' + | 'event'; + export type BaseEnvelopeHeaders = { [key: string]: unknown; dsn?: string; @@ -15,7 +38,7 @@ export type BaseEnvelopeHeaders = { export type BaseEnvelopeItemHeaders = { [key: string]: unknown; - type: string; + type: EnvelopeItemType; length?: number; }; @@ -30,24 +53,27 @@ type EventItemHeaders = { type: 'event' | 'transaction'; sample_rates?: [{ id?: TransactionSamplingMethod; rate?: number }]; }; -type AttachmentItemHeaders = { type: 'attachment'; filename: string }; +type AttachmentItemHeaders = { + type: 'attachment'; + length: number; + filename: string; + content_type?: string; + attachment_type?: string; +}; type UserFeedbackItemHeaders = { type: 'user_report' }; type SessionItemHeaders = { type: 'session' }; type SessionAggregatesItemHeaders = { type: 'sessions' }; type ClientReportItemHeaders = { type: 'client_report' }; -// TODO(v7): Remove the string union from `Event | string` -// We have to allow this hack for now as we pre-serialize events because we support -// both store and envelope endpoints. -export type EventItem = BaseEnvelopeItem; -export type AttachmentItem = BaseEnvelopeItem; +export type EventItem = BaseEnvelopeItem; +export type AttachmentItem = BaseEnvelopeItem; export type UserFeedbackItem = BaseEnvelopeItem; export type SessionItem = | BaseEnvelopeItem | BaseEnvelopeItem; export type ClientReportItem = BaseEnvelopeItem; -type EventEnvelopeHeaders = { event_id: string; sent_at: string }; +export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: EventTraceContext }; type SessionEnvelopeHeaders = { sent_at: string }; type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders; @@ -56,3 +82,4 @@ export type SessionEnvelope = BaseEnvelope; export type ClientReportEnvelope = BaseEnvelope; export type Envelope = EventEnvelope | SessionEnvelope | ClientReportEnvelope; +export type EnvelopeItem = Envelope[1][number]; diff --git a/packages/types/src/event.ts b/packages/types/src/event.ts index 0aa146f43848..c769feee65ff 100644 --- a/packages/types/src/event.ts +++ b/packages/types/src/event.ts @@ -1,3 +1,4 @@ +import { Attachment } from './attachment'; import { Breadcrumb } from './breadcrumb'; import { Contexts } from './context'; import { DebugMeta } from './debugMeta'; @@ -7,9 +8,8 @@ import { Primitive } from './misc'; import { Request } from './request'; import { CaptureContext } from './scope'; import { SdkInfo } from './sdkinfo'; -import { Severity } from './severity'; +import { Severity, SeverityLevel } from './severity'; import { Span } from './span'; -import { Stacktrace } from './stacktrace'; import { Measurements } from './transaction'; import { User } from './user'; @@ -19,7 +19,8 @@ export interface Event { message?: string; timestamp?: number; start_timestamp?: number; - level?: Severity; + // eslint-disable-next-line deprecation/deprecation + level?: Severity | SeverityLevel; platform?: string; logger?: string; server_name?: string; @@ -34,7 +35,6 @@ export interface Event { exception?: { values?: Exception[]; }; - stacktrace?: Stacktrace; breadcrumbs?: Breadcrumb[]; contexts?: Contexts; tags?: { [key: string]: Primitive }; @@ -57,5 +57,6 @@ export interface EventHint { captureContext?: CaptureContext; syntheticException?: Error | null; originalException?: Error | string | null; + attachments?: Attachment[]; data?: any; } diff --git a/packages/types/src/eventprocessor.ts b/packages/types/src/eventprocessor.ts index 13727ae2e797..5be90a755367 100644 --- a/packages/types/src/eventprocessor.ts +++ b/packages/types/src/eventprocessor.ts @@ -6,4 +6,7 @@ import { Event, EventHint } from './event'; * Returning a PromiseLike will work just fine, but better be sure that you know what you are doing. * Event processing will be deferred until your Promise is resolved. */ -export type EventProcessor = (event: Event, hint?: EventHint) => PromiseLike | Event | null; +export interface EventProcessor { + id?: string; // This field can't be named "name" because functions already have this field natively + (event: Event, hint: EventHint): PromiseLike | Event | null; +} diff --git a/packages/types/src/eventstatus.ts b/packages/types/src/eventstatus.ts deleted file mode 100644 index 498f0f45b3dd..000000000000 --- a/packages/types/src/eventstatus.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type EventStatus = - /** The status could not be determined. */ - | 'unknown' - /** The event was skipped due to configuration or callbacks. */ - | 'skipped' - /** The event was sent to Sentry successfully. */ - | 'rate_limit' - /** The client is currently rate limited and will try again later. */ - | 'invalid' - /** The event could not be processed. */ - | 'failed' - /** A server-side error occurred during submission. */ - | 'success'; diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index c80ea1b51686..a34d4372e08d 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -5,9 +5,8 @@ import { Extra, Extras } from './extra'; import { Integration, IntegrationClass } from './integration'; import { Primitive } from './misc'; import { Scope } from './scope'; -import { Session, SessionContext } from './session'; -import { Severity } from './severity'; -import { Span, SpanContext } from './span'; +import { Session } from './session'; +import { Severity, SeverityLevel } from './severity'; import { CustomSamplingContext, Transaction, TransactionContext } from './transaction'; import { User } from './user'; @@ -88,7 +87,12 @@ export interface Hub { * @param hint May contain additional information about the original exception. * @returns The generated eventId. */ - captureMessage(message: string, level?: Severity, hint?: EventHint): string; + captureMessage( + message: string, + // eslint-disable-next-line deprecation/deprecation + level?: Severity | SeverityLevel, + hint?: EventHint, + ): string; /** * Captures a manually created event and sends it to Sentry. @@ -180,11 +184,6 @@ export interface Hub { /** Returns all trace headers that are currently on the top scope. */ traceHeaders(): { [key: string]: string }; - /** - * @deprecated No longer does anything. Use use {@link Transaction.startChild} instead. - */ - startSpan(context: SpanContext): Span; - /** * Starts a new `Transaction` and returns it. This is the entry point to manual tracing instrumentation. * @@ -216,7 +215,7 @@ export interface Hub { * * @returns The session which was just started */ - startSession(context?: SessionContext): Session; + startSession(context?: Session): Session; /** * Ends the session that lives on the current scope and sends it to Sentry diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 13651b7cd78e..3032d7d6cdec 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,41 +1,47 @@ -export { Breadcrumb, BreadcrumbHint } from './breadcrumb'; -export { Client } from './client'; -export { ClientReport } from './clientreport'; -export { Context, Contexts } from './context'; -export { DsnComponents, DsnLike, DsnProtocol } from './dsn'; -export { DebugImage, DebugImageType, DebugMeta } from './debugMeta'; -export { +export type { Attachment } from './attachment'; +export type { AllowedBaggageKeys, Baggage, BaggageObj } from './baggage'; +export type { Breadcrumb, BreadcrumbHint } from './breadcrumb'; +export type { Client } from './client'; +export type { ClientReport, Outcome, EventDropReason } from './clientreport'; +export type { Context, Contexts } from './context'; +export type { DataCategory } from './datacategory'; +export type { DsnComponents, DsnLike, DsnProtocol } from './dsn'; +export type { DebugImage, DebugImageType, DebugMeta } from './debugMeta'; +export type { AttachmentItem, BaseEnvelopeHeaders, BaseEnvelopeItemHeaders, ClientReportEnvelope, ClientReportItem, Envelope, + EnvelopeItemType, + EnvelopeItem, EventEnvelope, + EventEnvelopeHeaders, EventItem, + EventTraceContext, SessionEnvelope, SessionItem, UserFeedbackItem, } from './envelope'; -export { ExtendedError } from './error'; -export { Event, EventHint } from './event'; -export { EventStatus } from './eventstatus'; -export { EventProcessor } from './eventprocessor'; -export { Exception } from './exception'; -export { Extra, Extras } from './extra'; -export { Hub } from './hub'; -export { Integration, IntegrationClass } from './integration'; -export { Mechanism } from './mechanism'; -export { ExtractedNodeRequestData, Primitive, WorkerLocation } from './misc'; -export { Options } from './options'; -export { Package } from './package'; -export { QueryParams, Request, SentryRequest, SentryRequestType } from './request'; -export { Response } from './response'; -export { Runtime } from './runtime'; -export { CaptureContext, Scope, ScopeContext } from './scope'; -export { SdkInfo } from './sdkinfo'; -export { SdkMetadata } from './sdkmetadata'; -export { +export type { ExtendedError } from './error'; +export type { Event, EventHint } from './event'; +export type { EventProcessor } from './eventprocessor'; +export type { Exception } from './exception'; +export type { Extra, Extras } from './extra'; +export type { Hub } from './hub'; +export type { Integration, IntegrationClass } from './integration'; +export type { Mechanism } from './mechanism'; +export type { ExtractedNodeRequestData, Primitive, WorkerLocation } from './misc'; +export type { ClientOptions, Options } from './options'; +export type { Package } from './package'; +export type { PolymorphicEvent } from './polymorphics'; +export type { QueryParams, Request } from './request'; +export type { Runtime } from './runtime'; +export type { CaptureContext, Scope, ScopeContext } from './scope'; +export type { SdkInfo } from './sdkinfo'; +export type { SdkMetadata } from './sdkmetadata'; +export type { SessionAggregates, AggregationCounts, Session, @@ -44,14 +50,15 @@ export { RequestSession, RequestSessionStatus, SessionFlusherLike, + SerializedSession, } from './session'; -export { Severity } from './severity'; -export { SeverityLevel, SeverityLevels } from './severity'; -export { Span, SpanContext } from './span'; -export { StackFrame } from './stackframe'; -export { Stacktrace } from './stacktrace'; -export { +// eslint-disable-next-line deprecation/deprecation +export type { Severity, SeverityLevel } from './severity'; +export type { Span, SpanContext } from './span'; +export type { StackFrame } from './stackframe'; +export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace'; +export type { CustomSamplingContext, Measurements, SamplingContext, @@ -61,7 +68,14 @@ export { TransactionMetadata, TransactionSamplingMethod, } from './transaction'; -export { Thread } from './thread'; -export { Outcome, Transport, TransportOptions, TransportClass } from './transport'; -export { User, UserFeedback } from './user'; -export { WrappedFunction } from './wrappedfunction'; +export type { Thread } from './thread'; +export type { + Transport, + TransportRequest, + TransportMakeRequestResponse, + InternalBaseTransportOptions, + BaseTransportOptions, + TransportRequestExecutor, +} from './transport'; +export type { User, UserFeedback } from './user'; +export type { WrappedFunction } from './wrappedfunction'; diff --git a/packages/types/src/options.ts b/packages/types/src/options.ts index 3fefd41ca137..b8d346f2973e 100644 --- a/packages/types/src/options.ts +++ b/packages/types/src/options.ts @@ -3,11 +3,11 @@ import { Event, EventHint } from './event'; import { Integration } from './integration'; import { CaptureContext } from './scope'; import { SdkMetadata } from './sdkmetadata'; +import { StackLineParser, StackParser } from './stacktrace'; import { SamplingContext } from './transaction'; -import { Transport, TransportClass, TransportOptions } from './transport'; +import { BaseTransportOptions, Transport } from './transport'; -/** Base configuration options for every SDK. */ -export interface Options { +export interface ClientOptions { /** * Enable debug functionality in the SDK itself */ @@ -19,6 +19,21 @@ export interface Options { */ enabled?: boolean; + /** Attaches stacktraces to pure capture message / log integrations */ + attachStacktrace?: boolean; + + /** + * A flag enabling Sessions Tracking feature. + * By default, Sessions Tracking is enabled. + */ + autoSessionTracking?: boolean; + + /** + * Send SDK Client Reports. + * By default, Client Reports are enabled. + */ + sendClientReports?: boolean; + /** * The Dsn used to connect to Sentry and identify the project. If omitted, the * SDK will not send any data to Sentry. @@ -26,54 +41,55 @@ export interface Options { dsn?: string; /** - * If this is set to false, default integrations will not be added, otherwise this will internally be set to the - * recommended default integrations. - * TODO: We should consider changing this to `boolean | Integration[]` + * The release identifier used when uploading respective source maps. Specify + * this value to allow Sentry to resolve the correct source maps when + * processing events. */ - defaultIntegrations?: false | Integration[]; + release?: string; + + /** The current environment of your application (e.g. "production"). */ + environment?: string; + + /** Sets the distribution for all events */ + dist?: string; /** * List of integrations that should be installed after SDK was initialized. - * Accepts either a list of integrations or a function that receives - * default integrations and returns a new, updated list. */ - integrations?: Integration[] | ((integrations: Integration[]) => Integration[]); + integrations: Integration[]; /** - * A pattern for error messages which should not be sent to Sentry. - * By default, all errors will be sent. + * A function that takes transport options and returns the Transport object which is used to send events to Sentry. + * The function is invoked internally when the client is initialized. */ - ignoreErrors?: Array; + transport: (transportOptions: TO) => Transport; /** - * Transport object that should be used to send events to Sentry + * A stack parser implementation + * By default, a stack parser is supplied for all supported platforms */ - transport?: TransportClass; + stackParser: StackParser; /** * Options for the default transport that the SDK uses. */ - transportOptions?: TransportOptions; + transportOptions?: Partial; /** - * A URL to an envelope tunnel endpoint. An envelope tunnel is an HTTP endpoint - * that accepts Sentry envelopes for forwarding. This can be used to force data - * through a custom server independent of the type of data. + * Sample rate to determine trace sampling. + * + * 0.0 = 0% chance of a given trace being sent (send no traces) 1.0 = 100% chance of a given trace being sent (send + * all traces) + * + * Tracing is enabled if either this or `tracesSampler` is defined. If both are defined, `tracesSampleRate` is + * ignored. */ - tunnel?: string; + tracesSampleRate?: number; /** - * The release identifier used when uploading respective source maps. Specify - * this value to allow Sentry to resolve the correct source maps when - * processing events. + * Initial data to populate scope. */ - release?: string; - - /** The current environment of your application (e.g. "production"). */ - environment?: string; - - /** Sets the distribution for all events */ - dist?: string; + initialScope?: CaptureContext; /** * The maximum number of breadcrumbs sent with events. Defaults to 100. @@ -81,12 +97,14 @@ export interface Options { */ maxBreadcrumbs?: number; - /** A global sample rate to apply to all events (0 - 1). */ + /** + * A global sample rate to apply to all events. + * + * 0.0 = 0% chance of a given event being sent (send no events) 1.0 = 100% chance of a given event being sent (send + * all events) + */ sampleRate?: number; - /** Attaches stacktraces to pure capture message / log integrations */ - attachStacktrace?: boolean; - /** Maximum number of chars a single value can have before it will be truncated. */ maxValueLength?: number; @@ -122,37 +140,22 @@ export interface Options { shutdownTimeout?: number; /** - * Sample rate to determine trace sampling. - * - * 0.0 = 0% chance of a given trace being sent (send no traces) 1.0 = 100% chance of a given trace being sent (send - * all traces) - * - * Tracing is enabled if either this or `tracesSampler` is defined. If both are defined, `tracesSampleRate` is - * ignored. - */ - tracesSampleRate?: number; - - /** - * A flag enabling Sessions Tracking feature. - * By default, Sessions Tracking is enabled. - */ - autoSessionTracking?: boolean; - - /** - * Send SDK Client Reports. - * By default, Client Reports are enabled. + * A pattern for error messages which should not be sent to Sentry. + * By default, all errors will be sent. */ - sendClientReports?: boolean; + ignoreErrors?: Array; /** - * Initial data to populate scope. + * A URL to an envelope tunnel endpoint. An envelope tunnel is an HTTP endpoint + * that accepts Sentry envelopes for forwarding. This can be used to force data + * through a custom server independent of the type of data. */ - initialScope?: CaptureContext; + tunnel?: string; /** * Set of metadata about the SDK that can be internally used to enhance envelopes and events, * and provide additional data about every request. - * */ + */ _metadata?: SdkMetadata; /** @@ -188,7 +191,7 @@ export interface Options { * @param hint May contain additional information about the original exception. * @returns A new event that will be sent | null. */ - beforeSend?: (event: Event, hint?: EventHint) => PromiseLike | Event | null; + beforeSend?: (event: Event, hint: EventHint) => PromiseLike | Event | null; /** * A callback invoked when adding a breadcrumb, allowing to optionally modify @@ -203,3 +206,33 @@ export interface Options { */ beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | null; } + +/** Base configuration options for every SDK. */ +export interface Options + extends Omit>, 'integrations' | 'transport' | 'stackParser'> { + /** + * If this is set to false, default integrations will not be added, otherwise this will internally be set to the + * recommended default integrations. + */ + defaultIntegrations?: false | Integration[]; + + /** + * List of integrations that should be installed after SDK was initialized. + * Accepts either a list of integrations or a function that receives + * default integrations and returns a new, updated list. + */ + integrations?: Integration[] | ((integrations: Integration[]) => Integration[]); + + /** + * A function that takes transport options and returns the Transport object which is used to send events to Sentry. + * The function is invoked internally during SDK initialization. + * By default, the SDK initializes its default transports. + */ + transport?: (transportOptions: TO) => Transport; + + /** + * A stack parser implementation or an array of stack line parsers + * By default, a stack parser is supplied for all supported browsers + */ + stackParser?: StackParser | StackLineParser[]; +} diff --git a/packages/types/src/polymorphics.ts b/packages/types/src/polymorphics.ts new file mode 100644 index 000000000000..0274aedfb2b4 --- /dev/null +++ b/packages/types/src/polymorphics.ts @@ -0,0 +1,11 @@ +/** + * Event-like interface that's usable in browser and node. + * + * Property availability taken from https://developer.mozilla.org/en-US/docs/Web/API/Event#browser_compatibility + */ +export interface PolymorphicEvent { + [key: string]: unknown; + readonly type: string; + readonly target?: unknown; + readonly currentTarget?: unknown; +} diff --git a/packages/types/src/request.ts b/packages/types/src/request.ts index 052de1b41c08..c06b29525a84 100644 --- a/packages/types/src/request.ts +++ b/packages/types/src/request.ts @@ -1,14 +1,3 @@ -/** Possible SentryRequest types that can be used to make a distinction between Sentry features */ -// NOTE(kamil): It would be nice if we make it a valid enum instead -export type SentryRequestType = 'event' | 'transaction' | 'session' | 'attachment'; - -/** A generic client request. */ -export interface SentryRequest { - body: string; - type: SentryRequestType; - url: string; -} - /** Request data included in an event as sent to Sentry */ export interface Request { url?: string; diff --git a/packages/types/src/requestsessionstatus.ts b/packages/types/src/requestsessionstatus.ts deleted file mode 100644 index b6b7ab9ee659..000000000000 --- a/packages/types/src/requestsessionstatus.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** JSDoc - * @deprecated Use string literals - if you require type casting, cast to RequestSessionStatus type - */ -export enum RequestSessionStatus { - /** JSDoc */ - Ok = 'ok', - /** JSDoc */ - Errored = 'errored', - /** JSDoc */ - Crashed = 'crashed', -} diff --git a/packages/types/src/response.ts b/packages/types/src/response.ts deleted file mode 100644 index 4add517f4870..000000000000 --- a/packages/types/src/response.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Event, EventType } from './event'; -import { EventStatus } from './eventstatus'; -import { Session } from './session'; - -/** JSDoc */ -export interface Response { - status: EventStatus; - event?: Event | Session; - type?: EventType; - reason?: string; -} diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index b46c47ce0759..a2a14ffd4664 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -1,10 +1,11 @@ +import { Attachment } from './attachment'; import { Breadcrumb } from './breadcrumb'; import { Context, Contexts } from './context'; import { EventProcessor } from './eventprocessor'; import { Extra, Extras } from './extra'; import { Primitive } from './misc'; import { RequestSession, Session } from './session'; -import { Severity } from './severity'; +import { Severity, SeverityLevel } from './severity'; import { Span } from './span'; import { Transaction } from './transaction'; import { User } from './user'; @@ -15,7 +16,8 @@ export type CaptureContext = Scope | Partial | ((scope: Scope) => /** JSDocs */ export interface ScopeContext { user: User; - level: Severity; + // eslint-disable-next-line deprecation/deprecation + level: Severity | SeverityLevel; extra: Extras; contexts: Contexts; tags: { [key: string]: Primitive }; @@ -79,9 +81,12 @@ export interface Scope { /** * Sets the level on the scope for future events. - * @param level string {@link Severity} + * @param level string {@link SeverityLevel} */ - setLevel(level: Severity): this; + setLevel( + // eslint-disable-next-line deprecation/deprecation + level: Severity | SeverityLevel, + ): this; /** * Sets the transaction name on the scope for future events. @@ -154,4 +159,20 @@ export interface Scope { * Clears all currently set Breadcrumbs. */ clearBreadcrumbs(): this; + + /** + * Adds an attachment to the scope + * @param attachment Attachment options + */ + addAttachment(attachment: Attachment): this; + + /** + * Returns an array of attachments on the scope + */ + getAttachments(): Attachment[]; + + /** + * Clears attachments from the scope + */ + clearAttachments(): this; } diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 552dda002531..35d5e0840a3e 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -1,60 +1,39 @@ import { User } from './user'; -/** - * @inheritdoc - */ -export interface Session extends SessionContext { - /** JSDoc */ - update(context?: SessionContext): void; - - /** JSDoc */ - close(status?: SessionStatus): void; - - /** JSDoc */ - toJSON(): { - init: boolean; - sid: string; - did?: string; - timestamp: string; - started: string; - duration?: number; - status: SessionStatus; - errors: number; - attrs?: { - release?: string; - environment?: string; - user_agent?: string; - ip_address?: string; - }; - }; -} - export interface RequestSession { status?: RequestSessionStatus; } -/** - * Session Context - */ -export interface SessionContext { - sid?: string; +export interface Session { + sid: string; did?: string; - init?: boolean; + init: boolean; // seconds since the UNIX epoch - timestamp?: number; + timestamp: number; // seconds since the UNIX epoch - started?: number; + started: number; duration?: number; - status?: SessionStatus; + status: SessionStatus; release?: string; environment?: string; userAgent?: string; ipAddress?: string; - errors?: number; + errors: number; user?: User | null; - ignoreDuration?: boolean; + ignoreDuration: boolean; + + /** + * Overrides default JSON serialization of the Session because + * the Sentry servers expect a slightly different schema of a session + * which is described in the interface @see SerializedSession in this file. + * + * @return a Sentry-backend conforming JSON object of the session + */ + toJSON(): SerializedSession; } +export type SessionContext = Partial; + export type SessionStatus = 'ok' | 'exited' | 'crashed' | 'abnormal'; export type RequestSessionStatus = 'ok' | 'errored' | 'crashed'; @@ -74,9 +53,6 @@ export interface SessionFlusherLike { */ incrementSessionStatusCount(): void; - /** Submits the aggregates request mode sessions to Sentry */ - sendSessionAggregates(sessionAggregates: SessionAggregates): void; - /** Empties Aggregate Buckets and Sends them to Transport Buffer */ flush(): void; @@ -90,3 +66,20 @@ export interface AggregationCounts { exited?: number; crashed?: number; } + +export interface SerializedSession { + init: boolean; + sid: string; + did?: string; + timestamp: string; + started: string; + duration?: number; + status: SessionStatus; + errors: number; + attrs?: { + release?: string; + environment?: string; + user_agent?: string; + ip_address?: string; + }; +} diff --git a/packages/types/src/sessionstatus.ts b/packages/types/src/sessionstatus.ts deleted file mode 100644 index 339b4ea2f5e6..000000000000 --- a/packages/types/src/sessionstatus.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** JSDoc - * @deprecated Use string literals - if you require type casting, cast to SessionStatus type - */ -export enum SessionStatus { - /** JSDoc */ - Ok = 'ok', - /** JSDoc */ - Exited = 'exited', - /** JSDoc */ - Crashed = 'crashed', - /** JSDoc */ - Abnormal = 'abnormal', -} diff --git a/packages/types/src/severity.ts b/packages/types/src/severity.ts index 513c63c7dadb..de13f2a02b99 100644 --- a/packages/types/src/severity.ts +++ b/packages/types/src/severity.ts @@ -1,5 +1,6 @@ /** - * TODO(v7): Remove this enum and replace with SeverityLevel + * @deprecated Please use a `SeverityLevel` string instead of the `Severity` enum. Acceptable values are 'fatal', + * 'error', 'warning', 'log', 'info', and 'debug'. */ export enum Severity { /** JSDoc */ @@ -14,11 +15,8 @@ export enum Severity { Info = 'info', /** JSDoc */ Debug = 'debug', - /** JSDoc */ - Critical = 'critical', } -// TODO: in v7, these can disappear, because they now also exist in `@sentry/utils`. (Having them there rather than here -// is nice because then it enforces the idea that only types are exported from `@sentry/types`.) -export const SeverityLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug', 'critical'] as const; -export type SeverityLevel = typeof SeverityLevels[number]; +// Note: If this is ever changed, the `validSeverityLevels` array in `@sentry/utils` needs to be changed, also. (See +// note there for why we can't derive one from the other.) +export type SeverityLevel = 'fatal' | 'error' | 'warning' | 'log' | 'info' | 'debug'; diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 09b69454835e..120d754cc329 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -1,3 +1,4 @@ +import { Baggage } from './baggage'; import { Primitive } from './misc'; import { Transaction } from './transaction'; @@ -128,14 +129,6 @@ export interface Span extends SpanContext { */ setHttpStatus(httpStatus: number): this; - /** - * Use {@link startChild} - * @deprecated - */ - child( - spanContext?: Pick>, - ): Span; - /** * Creates a new `Span` while setting the current `Span.id` as `parentSpanId`. * Also the `sampled` decision will be inherited. @@ -182,4 +175,7 @@ export interface Span extends SpanContext { timestamp?: number; trace_id: string; }; + + /** return the baggage for dynamic sampling and trace propagation */ + getBaggage(): Baggage | undefined; } diff --git a/packages/types/src/stacktrace.ts b/packages/types/src/stacktrace.ts index 120a1b471af6..ae2f350f716b 100644 --- a/packages/types/src/stacktrace.ts +++ b/packages/types/src/stacktrace.ts @@ -5,3 +5,7 @@ export interface Stacktrace { frames?: StackFrame[]; frames_omitted?: [number, number]; } + +export type StackParser = (stack: string, skipFirst?: number) => StackFrame[]; +export type StackLineParserFn = (line: string) => StackFrame | undefined; +export type StackLineParser = [number, StackLineParserFn]; diff --git a/packages/types/src/status.ts b/packages/types/src/status.ts deleted file mode 100644 index a80cd00b97e7..000000000000 --- a/packages/types/src/status.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** JSDoc - * @deprecated Use string literals - if you require type casting, cast to EventStatus type - */ -export enum Status { - /** The status could not be determined. */ - Unknown = 'unknown', - /** The event was skipped due to configuration or callbacks. */ - Skipped = 'skipped', - /** The event was sent to Sentry successfully. */ - Success = 'success', - /** The client is currently rate limited and will try again later. */ - RateLimit = 'rate_limit', - /** The event could not be processed. */ - Invalid = 'invalid', - /** A server-side error occurred during submission. */ - Failed = 'failed', -} diff --git a/packages/types/src/transaction.ts b/packages/types/src/transaction.ts index d2a8744cd335..b87fc2b151cf 100644 --- a/packages/types/src/transaction.ts +++ b/packages/types/src/transaction.ts @@ -1,6 +1,6 @@ +import { Baggage } from './baggage'; import { ExtractedNodeRequestData, Primitive, WorkerLocation } from './misc'; import { Span, SpanContext } from './span'; - /** * Interface holding Transaction-specific properties */ @@ -73,6 +73,15 @@ export interface Transaction extends TransactionContext, Span { */ setName(name: string): void; + /** + * Set observed measurement for this transaction. + * + * @param name Name of the measurement + * @param value Value of the measurement + * @param unit Unit of the measurement. (Defaults to an empty string) + */ + setMeasurement(name: string, value: number, unit: string): void; + /** Returns the current transaction properties as a `TransactionContext` */ toContext(): TransactionContext; @@ -115,18 +124,15 @@ export interface SamplingContext extends CustomSamplingContext { request?: ExtractedNodeRequestData; } -export type Measurements = Record; +export type Measurements = Record; export type TransactionSamplingMethod = 'explicitly_set' | 'client_sampler' | 'client_rate' | 'inheritance'; export interface TransactionMetadata { transactionSampling?: { rate?: number; method: TransactionSamplingMethod }; - /** The two halves (sentry and third-party) of a transaction's tracestate header, used for dynamic sampling */ - tracestate?: { - sentry?: string; - thirdparty?: string; - }; + /** The baggage object of a transaction's baggage header, used for dynamic sampling */ + baggage?: Baggage; /** For transactions tracing server-side request handling, the path of the request being tracked. */ requestPath?: string; diff --git a/packages/types/src/transport.ts b/packages/types/src/transport.ts index a1ba2983b1a3..6d358dc7ddc9 100644 --- a/packages/types/src/transport.ts +++ b/packages/types/src/transport.ts @@ -1,74 +1,41 @@ -import { DsnLike } from './dsn'; -import { Event } from './event'; -import { SentryRequestType } from './request'; -import { Response } from './response'; -import { SdkMetadata } from './sdkmetadata'; -import { Session, SessionAggregates } from './session'; +import { EventDropReason } from './clientreport'; +import { DataCategory } from './datacategory'; +import { Envelope } from './envelope'; -export type Outcome = - | 'before_send' - | 'event_processor' - | 'network_error' - | 'queue_overflow' - | 'ratelimit_backoff' - | 'sample_rate'; +export type TransportRequest = { + body: string | Uint8Array; +}; -/** Transport used sending data to Sentry */ -export interface Transport { - /** - * Sends the event to the Store endpoint in Sentry. - * - * @param event Event that should be sent to Sentry. - */ - sendEvent(event: Event): PromiseLike; - - /** - * Sends the session to the Envelope endpoint in Sentry. - * - * @param session Session that should be sent to Sentry | Session Aggregates that should be sent to Sentry. - */ - sendSession?(session: Session | SessionAggregates): PromiseLike; +export type TransportMakeRequestResponse = { + statusCode?: number; + headers?: { + [key: string]: string | null; + 'x-sentry-rate-limits': string | null; + 'retry-after': string | null; + }; +}; - /** - * Wait for all events to be sent or the timeout to expire, whichever comes first. - * - * @param timeout Maximum time in ms the transport should wait for events to be flushed. Omitting this parameter will - * cause the transport to wait until all events are sent before resolving the promise. - * @returns A promise that will resolve with `true` if all events are sent before the timeout, or `false` if there are - * still events in the queue when the timeout is reached. - */ - close(timeout?: number): PromiseLike; +// Combination of global TextEncoder and Node require('util').TextEncoder +interface TextEncoderInternal extends TextEncoderCommon { + encode(input?: string): Uint8Array; +} - /** - * Increment the counter for the specific client outcome - */ - recordLostEvent?(type: Outcome, category: SentryRequestType): void; +export interface InternalBaseTransportOptions { + bufferSize?: number; + recordDroppedEvent: (reason: EventDropReason, dataCategory: DataCategory) => void; + textEncoder?: TextEncoderInternal; } -/** JSDoc */ -export type TransportClass = new (options: TransportOptions) => T; +export interface BaseTransportOptions extends InternalBaseTransportOptions { + // url to send the event + // transport does not care about dsn specific - client should take care of + // parsing and figuring that out + url: string; +} -/** JSDoc */ -export interface TransportOptions { - /** Sentry DSN */ - dsn: DsnLike; - /** Define custom headers */ - headers?: { [key: string]: string }; - /** Set a HTTP proxy that should be used for outbound requests. */ - httpProxy?: string; - /** Set a HTTPS proxy that should be used for outbound requests. */ - httpsProxy?: string; - /** HTTPS proxy certificates path */ - caCerts?: string; - /** Fetch API init parameters */ - fetchParameters?: { [key: string]: string }; - /** The envelope tunnel to use. */ - tunnel?: string; - /** Send SDK Client Reports. Enabled by default. */ - sendClientReports?: boolean; - /** - * Set of metadata about the SDK that can be internally used to enhance envelopes and events, - * and provide additional data about every request. - * */ - _metadata?: SdkMetadata; +export interface Transport { + send(request: Envelope): PromiseLike; + flush(timeout?: number): PromiseLike; } + +export type TransportRequestExecutor = (request: TransportRequest) => PromiseLike; diff --git a/packages/types/src/user.ts b/packages/types/src/user.ts index 0f27df252cf6..0295c1432727 100644 --- a/packages/types/src/user.ts +++ b/packages/types/src/user.ts @@ -5,6 +5,7 @@ export interface User { ip_address?: string; email?: string; username?: string; + segment?: string; } export interface UserFeedback { diff --git a/packages/types/tsconfig.cjs.json b/packages/types/tsconfig.cjs.json deleted file mode 100644 index e3a918fc70af..000000000000 --- a/packages/types/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "build/dist" - } -} diff --git a/packages/types/tsconfig.esm.json b/packages/types/tsconfig.esm.json deleted file mode 100644 index 0b86c52918cc..000000000000 --- a/packages/types/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "build/esm" - } -} diff --git a/packages/typescript/.npmignore b/packages/typescript/.npmignore index d59f27ed8132..457274275b7c 100644 --- a/packages/typescript/.npmignore +++ b/packages/typescript/.npmignore @@ -1,3 +1,2 @@ * !/tsconfig.json -!/tslint.json diff --git a/packages/typescript/README.md b/packages/typescript/README.md index e06503ddf511..fc8d7e2f570d 100644 --- a/packages/typescript/README.md +++ b/packages/typescript/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Sentry TypeScript Configuration @@ -36,14 +35,6 @@ npm install --save-dev @sentry-internal/typescript Add the following config files to your project's root directory: -**tslint.json**: - -```json -{ - "extends": "@sentry-internal/typescript/tslint" -} -``` - **tsconfig.json**: ```json diff --git a/packages/typescript/package.json b/packages/typescript/package.json index 2156c40a9c2f..af30f54a0b02 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/typescript", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Typescript configuration used at Sentry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/typescript", @@ -10,16 +10,11 @@ "publishConfig": { "access": "public" }, - "dependencies": { - "tslint-config-prettier": "^1.18.0", - "tslint-consistent-codestyle": "^1.15.1" - }, "peerDependencies": { - "tslint": "5.16.0", - "typescript": "3.7.5" + "typescript": "3.8.3" }, "scripts": { - "link:yarn": "yarn link", + "clean": "yarn rimraf sentry-internal-typescript-*.tgz", "build:npm": "npm pack" }, "volta": { diff --git a/packages/typescript/tsconfig.json b/packages/typescript/tsconfig.json index d6858094b969..6ffd79b4ccc7 100644 --- a/packages/typescript/tsconfig.json +++ b/packages/typescript/tsconfig.json @@ -6,32 +6,20 @@ "downlevelIteration": true, "importHelpers": true, "inlineSources": true, + "isolatedModules": true, "lib": ["es6", "dom"], - // "module": "commonjs", // implied by "target" : "es5" "moduleResolution": "node", "noEmitHelpers": true, + "noErrorTruncation": true, "noFallthroughCasesInSwitch": true, - "noImplicitAny": true, "noImplicitReturns": true, - "noImplicitThis": true, "noImplicitUseStrict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "plugins": [ - { - "name": "typescript-tslint-plugin", - "configFile": "./tslint.json", - "alwaysShowRuleFailuresAsWarnings": false, - "ignoreDefinitionFiles": true, - "mockTypeScriptVersion": false, - "suppressWhileTypeErrorsPresent": false - } - ], "preserveWatchOutput": true, - "pretty": true, "sourceMap": true, "strict": true, "strictBindCallApply": false, - "target": "es5" + "target": "es6" } } diff --git a/packages/typescript/tslint.json b/packages/typescript/tslint.json deleted file mode 100644 index a739adabaf6a..000000000000 --- a/packages/typescript/tslint.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "extends": ["tslint:all", "tslint-config-prettier", "tslint-consistent-codestyle"], - "rules": { - // This rule has side effects and must be disabled - "no-unused-variable": false, - - "no-submodule-imports": false, - "no-null-keyword": false, - // We don't want these - "newline-before-return": false, - "no-any": false, - "no-magic-numbers": false, - "no-parameter-properties": false, - "no-require-imports": false, - "prefer-function-over-method": [false], - "strict-boolean-expressions": false, - "no-inferrable-types": false, - "ban-ts-ignore": false, - "increment-decrement": false, - "promise-function-async": false, - "ban-types": [true, ["async", "Use Promises instead, as async/await adds a lot to bundle size."]], - // These are too strict in tslint:all - "comment-format": [true, "check-space"], - "completed-docs": [ - true, - { - "classes": { - "tags": { - "content": {}, - "existence": ["inheritDoc", "hidden"] - } - }, - "enums": { - "tags": { - "content": {}, - "existence": ["inheritDoc", "hidden"] - } - }, - "enum-members": { - "tags": { - "content": {}, - "existence": ["inheritDoc", "hidden"] - } - }, - "functions": { - "tags": { - "content": {}, - "existence": ["inheritDoc", "hidden"] - } - }, - "interfaces": { - "tags": { - "content": {}, - "existence": ["inheritDoc", "hidden"] - } - }, - "methods": { - "tags": { - "content": {}, - "existence": ["inheritDoc", "hidden"] - } - }, - "properties": { - "tags": { - "content": {}, - "existence": ["inheritDoc", "hidden"] - }, - "locations": "instance" - } - } - ], - "interface-name": [true, "never-prefix"], - "member-ordering": [true, "statics-first"], - "no-console": [true, "log"], - "only-arrow-functions": [true, "allow-named-functions"], - "typedef": [true, "call-signature", "parameter", "property-declaration", "member-variable-declaration"], - "variable-name": [true, "check-format", "allow-leading-underscore", "ban-keywords"], - "naming-convention": [ - true, - // This config will apply to properties AND methods. If you only need it for properties, use "property" instead of - // "member". - { "type": "member", "modifiers": "protected", "leadingUnderscore": "require" }, - { "type": "member", "modifiers": "private", "leadingUnderscore": "require" } - ], - // we cannot use Promises as they are not IE10-11 compatibile, so we had to create our own implementation - "await-promise": [true, "PromiseLike"] - } -} diff --git a/packages/utils/.eslintrc.js b/packages/utils/.eslintrc.js index 5a2cc7f1ec08..35c6aab563f5 100644 --- a/packages/utils/.eslintrc.js +++ b/packages/utils/.eslintrc.js @@ -1,3 +1,19 @@ module.exports = { extends: ['../../.eslintrc.js'], + overrides: [ + { + files: ['scripts/**/*.ts'], + parserOptions: { + project: ['../../tsconfig.dev.json'], + }, + }, + { + files: ['test/**'], + parserOptions: { + sourceType: 'module', + }, + }, + ], + // symlinks to the folders inside of `build`, created to simulate what's in the npm package + ignorePatterns: ['cjs/**', 'esm/**'], }; diff --git a/packages/utils/.gitignore b/packages/utils/.gitignore index 5c5f1c199d85..4b89517c06c8 100644 --- a/packages/utils/.gitignore +++ b/packages/utils/.gitignore @@ -1,5 +1,6 @@ -*.js.map -*.d.ts -*.js -!test/types/* -!.eslintrc.js +# symlinks to the folders in `build`, needed for tests +cjs +esm + +# needed so we can test our versions of polyfills against Sucrase and Rollup's originals +!test/buildPolyfills/originals.d.ts diff --git a/packages/utils/.npmignore b/packages/utils/.npmignore deleted file mode 100644 index 4ab7cc4f278f..000000000000 --- a/packages/utils/.npmignore +++ /dev/null @@ -1,11 +0,0 @@ -# The paths in this file are specified so that they align with the file structure in `./build` after this file is copied -# into it by the prepack script `scripts/prepack.ts`. - -* - -# TODO remove bundles (which in the tarball are inside `build`) in v7 -!/build/**/* - -!/dist/**/* -!/esm/**/* -!/types/**/* diff --git a/packages/utils/README.md b/packages/utils/README.md index 185fc3778f44..b2dac05757dc 100644 --- a/packages/utils/README.md +++ b/packages/utils/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Sentry JavaScript SDK Utilities diff --git a/packages/utils/jest.config.js b/packages/utils/jest.config.js new file mode 100644 index 000000000000..f9b7ccfa4502 --- /dev/null +++ b/packages/utils/jest.config.js @@ -0,0 +1,9 @@ +const baseConfig = require('../../jest/jest.config.js'); + +module.exports = { + ...baseConfig, + transform: { + '^.+\\.ts$': 'ts-jest', + '^.+\\.js$': 'ts-jest', + }, +}; diff --git a/packages/utils/package.json b/packages/utils/package.json index c104c67ba5ff..af3dbedd4340 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,48 +1,44 @@ { "name": "@sentry/utils", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Utilities for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/utils", "author": "Sentry", "license": "BSD-3-Clause", "engines": { - "node": ">=6" + "node": ">=8" }, - "main": "build/dist/index.js", + "main": "build/cjs/index.js", "module": "build/esm/index.js", "types": "build/types/index.d.ts", "publishConfig": { "access": "public" }, "dependencies": { - "@sentry/types": "6.19.7", + "@sentry/types": "7.0.0-rc.0", "tslib": "^1.9.3" }, "devDependencies": { - "chai": "^4.1.2", - "jsdom": "^16.2.2" + "@types/array.prototype.flat": "^1.2.1", + "array.prototype.flat": "^1.3.0", + "chai": "^4.1.2" }, "scripts": { - "build": "run-p build:cjs build:esm build:types", - "build:cjs": "tsc -p tsconfig.cjs.json", + "build": "run-p build:rollup build:types", "build:dev": "run-s build", - "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "tsc -p tsconfig.esm.json", + "build:rollup": "yarn ts-node scripts/buildRollup.ts", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", - "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", + "build:watch": "run-p build:rollup:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:es5:watch": "yarn build:cjs:watch # *** backwards compatibility - remove in v7 ***", - "build:esm:watch": "tsc -p tsconfig.esm.json --watch", + "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf dist esm build coverage *.js *.js.map *.d.ts", + "clean": "rimraf build coverage cjs esm sentry-utils-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/*.ts\"", - "link:yarn": "yarn link", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", @@ -53,25 +49,5 @@ "volta": { "extends": "../../package.json" }, - "jest": { - "collectCoverage": true, - "transform": { - "^.+\\.ts$": "ts-jest" - }, - "moduleFileExtensions": [ - "js", - "ts" - ], - "testEnvironment": "node", - "testMatch": [ - "**/*.test.ts" - ], - "globals": { - "ts-jest": { - "tsConfig": "./tsconfig.json", - "diagnostics": false - } - } - }, "sideEffects": false } diff --git a/packages/utils/rollup.npm.config.js b/packages/utils/rollup.npm.config.js new file mode 100644 index 000000000000..dcf32469a4e4 --- /dev/null +++ b/packages/utils/rollup.npm.config.js @@ -0,0 +1,9 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + // We build the polyfills separately because they're not included in the top-level exports of the package, in order + // to keep them out of the public API. + entrypoints: ['src/index.ts', 'src/buildPolyfills/index.ts'], + }), +); diff --git a/packages/utils/scripts/buildRollup.ts b/packages/utils/scripts/buildRollup.ts new file mode 100644 index 000000000000..f48b0b23d6d9 --- /dev/null +++ b/packages/utils/scripts/buildRollup.ts @@ -0,0 +1,30 @@ +import * as childProcess from 'child_process'; +import * as fs from 'fs'; + +/** + * Run the given shell command, piping the shell process's `stdin`, `stdout`, and `stderr` to that of the current + * process. Returns contents of `stdout`. + */ +function run(cmd: string, options?: childProcess.ExecSyncOptions): string | Buffer { + return childProcess.execSync(cmd, { stdio: 'inherit', ...options }); +} + +run('yarn rollup -c rollup.npm.config.js'); + +// We want to distribute the README because it contains the MIT license blurb from Sucrase and Rollup +fs.copyFileSync('src/buildPolyfills/README.md', 'build/cjs/buildPolyfills/README.md'); +fs.copyFileSync('src/buildPolyfills/README.md', 'build/esm/buildPolyfills/README.md'); + +// Because we import our polyfills from `@sentry/utils/cjs/buildPolyfills` and `@sentry/utils/esm/buildPolyfills` rather +// than straight from `@sentry/utils` (so as to avoid having them in the package's public API), when tests run, they'll +// expect to find `cjs` and `esm` at the root level of the repo. +try { + fs.symlinkSync('build/cjs', 'cjs'); +} catch (oO) { + // if we get here, it's because the symlink already exists, so we're good +} +try { + fs.symlinkSync('build/esm', 'esm'); +} catch (oO) { + // same as above +} diff --git a/packages/utils/src/async.ts b/packages/utils/src/async.ts deleted file mode 100644 index e811fe25c4a6..000000000000 --- a/packages/utils/src/async.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Consumes the promise and logs the error when it rejects. - * @param promise A promise to forget. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function forget(promise: PromiseLike): void { - void promise.then(null, e => { - // TODO: Use a better logging mechanism - // eslint-disable-next-line no-console - console.error(e); - }); -} diff --git a/packages/utils/src/baggage.ts b/packages/utils/src/baggage.ts new file mode 100644 index 000000000000..bc2f6568ff1f --- /dev/null +++ b/packages/utils/src/baggage.ts @@ -0,0 +1,117 @@ +import { Baggage, BaggageObj } from '@sentry/types'; + +import { IS_DEBUG_BUILD } from './flags'; +import { logger } from './logger'; + +export const BAGGAGE_HEADER_NAME = 'baggage'; + +export const SENTRY_BAGGAGE_KEY_PREFIX = 'sentry-'; + +export const SENTRY_BAGGAGE_KEY_PREFIX_REGEX = /^sentry-/; + +/** + * Max length of a serialized baggage string + * + * https://www.w3.org/TR/baggage/#limits + */ +export const MAX_BAGGAGE_STRING_LENGTH = 8192; + +/** Create an instance of Baggage */ +export function createBaggage(initItems: BaggageObj, baggageString: string = ''): Baggage { + return [{ ...initItems }, baggageString]; +} + +/** Get a value from baggage */ +export function getBaggageValue(baggage: Baggage, key: keyof BaggageObj): BaggageObj[keyof BaggageObj] { + return baggage[0][key]; +} + +/** Add a value to baggage */ +export function setBaggageValue(baggage: Baggage, key: keyof BaggageObj, value: BaggageObj[keyof BaggageObj]): void { + baggage[0][key] = value; +} + +/** Check if the baggage object (i.e. the first element in the tuple) is empty */ +export function isBaggageEmpty(baggage: Baggage): boolean { + return Object.keys(baggage[0]).length === 0; +} + +/** Returns Sentry specific baggage values */ +export function getSentryBaggageItems(baggage: Baggage): BaggageObj { + return baggage[0]; +} + +/** + * Returns 3rd party baggage string of @param baggage + * @param baggage + */ +export function getThirdPartyBaggage(baggage: Baggage): string { + return baggage[1]; +} + +/** Serialize a baggage object */ +export function serializeBaggage(baggage: Baggage): string { + return Object.keys(baggage[0]).reduce((prev, key: keyof BaggageObj) => { + const val = baggage[0][key] as string; + const baggageEntry = `${SENTRY_BAGGAGE_KEY_PREFIX}${encodeURIComponent(key)}=${encodeURIComponent(val)}`; + const newVal = prev === '' ? baggageEntry : `${prev},${baggageEntry}`; + if (newVal.length > MAX_BAGGAGE_STRING_LENGTH) { + IS_DEBUG_BUILD && + logger.warn(`Not adding key: ${key} with val: ${val} to baggage due to exceeding baggage size limits.`); + return prev; + } else { + return newVal; + } + }, baggage[1]); +} + +/** Parse a baggage header to a string */ +export function parseBaggageString(inputBaggageString: string): Baggage { + return inputBaggageString.split(',').reduce( + ([baggageObj, baggageString], curr) => { + const [key, val] = curr.split('='); + if (SENTRY_BAGGAGE_KEY_PREFIX_REGEX.test(key)) { + const baggageKey = decodeURIComponent(key.split('-')[1]); + return [ + { + ...baggageObj, + [baggageKey]: decodeURIComponent(val), + }, + baggageString, + ]; + } else { + return [baggageObj, baggageString === '' ? curr : `${baggageString},${curr}`]; + } + }, + [{}, ''], + ); +} + +/** + * Merges the baggage header we saved from the incoming request (or meta tag) with + * a possibly created or modified baggage header by a third party that's been added + * to the outgoing request header. + * + * In case @param headerBaggageString exists, we can safely add the the 3rd party part of @param headerBaggage + * with our @param incomingBaggage. This is possible because if we modified anything beforehand, + * it would only affect parts of the sentry baggage (@see Baggage interface). + * + * @param incomingBaggage the baggage header of the incoming request that might contain sentry entries + * @param headerBaggageString possibly existing baggage header string added from a third party to request headers + * + * @return a merged and serialized baggage string to be propagated with the outgoing request + */ +export function mergeAndSerializeBaggage(incomingBaggage?: Baggage, headerBaggageString?: string): string { + if (!incomingBaggage && !headerBaggageString) { + return ''; + } + + const headerBaggage = (headerBaggageString && parseBaggageString(headerBaggageString)) || undefined; + const thirdPartyHeaderBaggage = headerBaggage && getThirdPartyBaggage(headerBaggage); + + const finalBaggage = createBaggage( + (incomingBaggage && incomingBaggage[0]) || {}, + thirdPartyHeaderBaggage || (incomingBaggage && incomingBaggage[1]) || '', + ); + return serializeBaggage(finalBaggage); +} diff --git a/packages/utils/src/buildPolyfills/README.md b/packages/utils/src/buildPolyfills/README.md new file mode 100644 index 000000000000..8171e8583a96 --- /dev/null +++ b/packages/utils/src/buildPolyfills/README.md @@ -0,0 +1,15 @@ +## Build Polyfills + +This is a collection of syntax and import/export polyfills either copied directly from or heavily inspired by those used by [Rollup](https://github.com/rollup/rollup) and [Sucrase](https://github.com/alangpierce/sucrase). When either tool uses one of these polyfills during a build, it injects the function source code into each file needing the function, which can lead to a great deal of duplication. For our builds, we have therefore implemented something similar to [`tsc`'s `importHelpers` behavior](https://www.typescriptlang.org/tsconfig#importHelpers): Instead of leaving the polyfills injected in multiple places, we instead replace each injected function with an `import` or `require` statement, pulling from the CJS or ESM builds as appropriate. (In other words, the injected `import` statements import from `@sentry/utils/esm/buildPolyfills` and the injected `require` statements pull from `@sentry/utils/cjs/buildPolyfills/`. Because these functions should never be part of the public API, they're not exported from the package directly.) + +Note that not all polyfills are currently used by the SDK, but all are included here for future compatitibility, should they ever be needed. Also, since we're never going to be calling these directly from within another TS file, their types are fairly generic. In some cases testing required more specific types, which can be found in the test files. + +-------- + +_Code from both Rollup and Sucrase is used under the MIT license, copyright 2017 and 2012-2018, respectively._ + +_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/utils/src/buildPolyfills/_asyncNullishCoalesce.ts b/packages/utils/src/buildPolyfills/_asyncNullishCoalesce.ts new file mode 100644 index 000000000000..c1b075ee5222 --- /dev/null +++ b/packages/utils/src/buildPolyfills/_asyncNullishCoalesce.ts @@ -0,0 +1,30 @@ +// adapted from Sucrase (https://github.com/alangpierce/sucrase) + +import { _nullishCoalesce } from './_nullishCoalesce'; + +/** + * Polyfill for the nullish coalescing operator (`??`), when used in situations where at least one of the values is the + * result of an async operation. + * + * Note that the RHS is wrapped in a function so that if it's a computed value, that evaluation won't happen unless the + * LHS evaluates to a nullish value, to mimic the operator's short-circuiting behavior. + * + * Adapted from Sucrase (https://github.com/alangpierce/sucrase) + * + * @param lhs The value of the expression to the left of the `??` + * @param rhsFn A function returning the value of the expression to the right of the `??` + * @returns The LHS value, unless it's `null` or `undefined`, in which case, the RHS value + */ +// eslint-disable-next-line @sentry-internal/sdk/no-async-await +export async function _asyncNullishCoalesce(lhs: unknown, rhsFn: () => unknown): Promise { + return _nullishCoalesce(lhs, rhsFn); +} + +// Sucrase version: +// async function _asyncNullishCoalesce(lhs, rhsFn) { +// if (lhs != null) { +// return lhs; +// } else { +// return await rhsFn(); +// } +// } diff --git a/packages/utils/src/buildPolyfills/_asyncOptionalChain.ts b/packages/utils/src/buildPolyfills/_asyncOptionalChain.ts new file mode 100644 index 000000000000..10a10aa03f3a --- /dev/null +++ b/packages/utils/src/buildPolyfills/_asyncOptionalChain.ts @@ -0,0 +1,59 @@ +import { GenericFunction } from './types'; + +/** + * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, + * descriptors, and functions, for situations in which at least one part of the expression is async. + * + * Adapted from Sucrase (https://github.com/alangpierce/sucrase) See + * https://github.com/alangpierce/sucrase/blob/265887868966917f3b924ce38dfad01fbab1329f/src/transformers/OptionalChainingNullishTransformer.ts#L15 + * + * @param ops Array result of expression conversion + * @returns The value of the expression + */ +// eslint-disable-next-line @sentry-internal/sdk/no-async-await +export async function _asyncOptionalChain(ops: unknown[]): Promise { + let lastAccessLHS: unknown = undefined; + let value = ops[0]; + let i = 1; + while (i < ops.length) { + const op = ops[i] as string; + const fn = ops[i + 1] as (intermediateValue: unknown) => Promise; + i += 2; + // by checking for loose equality to `null`, we catch both `null` and `undefined` + if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { + // really we're meaning to return `undefined` as an actual value here, but it saves bytes not to write it + return; + } + if (op === 'access' || op === 'optionalAccess') { + lastAccessLHS = value; + value = await fn(value); + } else if (op === 'call' || op === 'optionalCall') { + value = await fn((...args: unknown[]) => (value as GenericFunction).call(lastAccessLHS, ...args)); + lastAccessLHS = undefined; + } + } + return value; +} + +// Sucrase version: +// async function _asyncOptionalChain(ops) { +// let lastAccessLHS = undefined; +// let value = ops[0]; +// let i = 1; +// while (i < ops.length) { +// const op = ops[i]; +// const fn = ops[i + 1]; +// i += 2; +// if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { +// return undefined; +// } +// if (op === 'access' || op === 'optionalAccess') { +// lastAccessLHS = value; +// value = await fn(value); +// } else if (op === 'call' || op === 'optionalCall') { +// value = await fn((...args) => value.call(lastAccessLHS, ...args)); +// lastAccessLHS = undefined; +// } +// } +// return value; +// } diff --git a/packages/utils/src/buildPolyfills/_asyncOptionalChainDelete.ts b/packages/utils/src/buildPolyfills/_asyncOptionalChainDelete.ts new file mode 100644 index 000000000000..bf0260337919 --- /dev/null +++ b/packages/utils/src/buildPolyfills/_asyncOptionalChainDelete.ts @@ -0,0 +1,28 @@ +import { _asyncOptionalChain } from './_asyncOptionalChain'; + +/** + * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, + * descriptors, and functions, in cases where the value of the expression is to be deleted. + * + * Adapted from Sucrase (https://github.com/alangpierce/sucrase) See + * https://github.com/alangpierce/sucrase/blob/265887868966917f3b924ce38dfad01fbab1329f/src/transformers/OptionalChainingNullishTransformer.ts#L15 + * + * @param ops Array result of expression conversion + * @returns The return value of the `delete` operator: `true`, unless the deletion target is an own, non-configurable + * property (one which can't be deleted or turned into an accessor, and whose enumerability can't be changed), in which + * case `false`. + */ +// eslint-disable-next-line @sentry-internal/sdk/no-async-await +export async function _asyncOptionalChainDelete(ops: unknown[]): Promise { + const result = (await _asyncOptionalChain(ops)) as Promise; + // If `result` is `null`, it means we didn't get to the end of the chain and so nothing was deleted (in which case, + // return `true` since that's what `delete` does when it no-ops). If it's non-null, we know the delete happened, in + // which case we return whatever the `delete` returned, which will be a boolean. + return result == null ? true : (result as Promise); +} + +// Sucrase version: +// async function asyncOptionalChainDelete(ops) { +// const result = await ASYNC_OPTIONAL_CHAIN_NAME(ops); +// return result == null ? true : result; +// } diff --git a/packages/utils/src/buildPolyfills/_createNamedExportFrom.ts b/packages/utils/src/buildPolyfills/_createNamedExportFrom.ts new file mode 100644 index 000000000000..0c632e586aba --- /dev/null +++ b/packages/utils/src/buildPolyfills/_createNamedExportFrom.ts @@ -0,0 +1,21 @@ +import { GenericObject } from './types'; + +declare const exports: GenericObject; + +/** + * Copy a property from the given object into `exports`, under the given name. + * + * Adapted from Sucrase (https://github.com/alangpierce/sucrase) + * + * @param obj The object containing the property to copy. + * @param localName The name under which to export the property + * @param importedName The name under which the property lives in `obj` + */ +export function _createNamedExportFrom(obj: GenericObject, localName: string, importedName: string): void { + exports[localName] = obj[importedName]; +} + +// Sucrase version: +// function _createNamedExportFrom(obj, localName, importedName) { +// Object.defineProperty(exports, localName, {enumerable: true, get: () => obj[importedName]}); +// } diff --git a/packages/utils/src/buildPolyfills/_createStarExport.ts b/packages/utils/src/buildPolyfills/_createStarExport.ts new file mode 100644 index 000000000000..f4f36c8c041a --- /dev/null +++ b/packages/utils/src/buildPolyfills/_createStarExport.ts @@ -0,0 +1,28 @@ +import { GenericObject } from './types'; + +declare const exports: GenericObject; + +/** + * Copy properties from an object into `exports`. + * + * Adapted from Sucrase (https://github.com/alangpierce/sucrase) + * + * @param obj The object containing the properties to copy. + */ +export function _createStarExport(obj: GenericObject): void { + Object.keys(obj) + .filter(key => key !== 'default' && key !== '__esModule' && !(key in exports)) + .forEach(key => (exports[key] = obj[key])); +} + +// Sucrase version: +// function _createStarExport(obj) { +// Object.keys(obj) +// .filter(key => key !== 'default' && key !== '__esModule') +// .forEach(key => { +// if (exports.hasOwnProperty(key)) { +// return; +// } +// Object.defineProperty(exports, key, { enumerable: true, get: () => obj[key] }); +// }); +// } diff --git a/packages/utils/src/buildPolyfills/_interopDefault.ts b/packages/utils/src/buildPolyfills/_interopDefault.ts new file mode 100644 index 000000000000..5bed0ef4e3f1 --- /dev/null +++ b/packages/utils/src/buildPolyfills/_interopDefault.ts @@ -0,0 +1,18 @@ +import { RequireResult } from './types'; + +/** + * Unwraps a module if it has been wrapped in an object under the key `default`. + * + * Adapted from Rollup (https://github.com/rollup/rollup) + * + * @param requireResult The result of calling `require` on a module + * @returns The full module, unwrapped if necessary. + */ +export function _interopDefault(requireResult: RequireResult): RequireResult { + return requireResult.__esModule ? (requireResult.default as RequireResult) : requireResult; +} + +// Rollup version: +// function _interopDefault(e) { +// return e && e.__esModule ? e['default'] : e; +// } diff --git a/packages/utils/src/buildPolyfills/_interopNamespace.ts b/packages/utils/src/buildPolyfills/_interopNamespace.ts new file mode 100644 index 000000000000..2211e21accfa --- /dev/null +++ b/packages/utils/src/buildPolyfills/_interopNamespace.ts @@ -0,0 +1,26 @@ +import { RequireResult } from './types'; + +/** + * Adds a self-referential `default` property to CJS modules which aren't the result of transpilation from ESM modules. + * + * Adapted from Rollup (https://github.com/rollup/rollup) + * + * @param requireResult The result of calling `require` on a module + * @returns Either `requireResult` or a copy of `requireResult` with an added self-referential `default` property + */ +export function _interopNamespace(requireResult: RequireResult): RequireResult { + return requireResult.__esModule ? requireResult : { ...requireResult, default: requireResult }; +} + +// Rollup version (with `output.externalLiveBindings` and `output.freeze` both set to false) +// function _interopNamespace(e) { +// if (e && e.__esModule) return e; +// var n = Object.create(null); +// if (e) { +// for (var k in e) { +// n[k] = e[k]; +// } +// } +// n["default"] = e; +// return n; +// } diff --git a/packages/utils/src/buildPolyfills/_interopNamespaceDefaultOnly.ts b/packages/utils/src/buildPolyfills/_interopNamespaceDefaultOnly.ts new file mode 100644 index 000000000000..66785a79e92f --- /dev/null +++ b/packages/utils/src/buildPolyfills/_interopNamespaceDefaultOnly.ts @@ -0,0 +1,24 @@ +import { RequireResult } from './types'; + +/** + * Wrap a module in an object, as the value under the key `default`. + * + * Adapted from Rollup (https://github.com/rollup/rollup) + * + * @param requireResult The result of calling `require` on a module + * @returns An object containing the key-value pair (`default`, `requireResult`) + */ +export function _interopNamespaceDefaultOnly(requireResult: RequireResult): RequireResult { + return { + __proto__: null, + default: requireResult, + }; +} + +// Rollup version +// function _interopNamespaceDefaultOnly(e) { +// return { +// __proto__: null, +// 'default': e +// }; +// } diff --git a/packages/utils/src/buildPolyfills/_interopRequireDefault.ts b/packages/utils/src/buildPolyfills/_interopRequireDefault.ts new file mode 100644 index 000000000000..9d9a7767cb7c --- /dev/null +++ b/packages/utils/src/buildPolyfills/_interopRequireDefault.ts @@ -0,0 +1,18 @@ +import { RequireResult } from './types'; + +/** + * Wraps modules which aren't the result of transpiling an ESM module in an object under the key `default` + * + * Adapted from Sucrase (https://github.com/alangpierce/sucrase) + * + * @param requireResult The result of calling `require` on a module + * @returns `requireResult` or `requireResult` wrapped in an object, keyed as `default` + */ +export function _interopRequireDefault(requireResult: RequireResult): RequireResult { + return requireResult.__esModule ? requireResult : { default: requireResult }; +} + +// Sucrase version +// function _interopRequireDefault(obj) { +// return obj && obj.__esModule ? obj : { default: obj }; +// } diff --git a/packages/utils/src/buildPolyfills/_interopRequireWildcard.ts b/packages/utils/src/buildPolyfills/_interopRequireWildcard.ts new file mode 100644 index 000000000000..411939ab68d6 --- /dev/null +++ b/packages/utils/src/buildPolyfills/_interopRequireWildcard.ts @@ -0,0 +1,31 @@ +import { RequireResult } from './types'; + +/** + * Adds a `default` property to CJS modules which aren't the result of transpilation from ESM modules. + * + * Adapted from Sucrase (https://github.com/alangpierce/sucrase) + * + * @param requireResult The result of calling `require` on a module + * @returns Either `requireResult` or a copy of `requireResult` with an added self-referential `default` property + */ +export function _interopRequireWildcard(requireResult: RequireResult): RequireResult { + return requireResult.__esModule ? requireResult : { ...requireResult, default: requireResult }; +} + +// Sucrase version +// function _interopRequireWildcard(obj) { +// if (obj && obj.__esModule) { +// return obj; +// } else { +// var newObj = {}; +// if (obj != null) { +// for (var key in obj) { +// if (Object.prototype.hasOwnProperty.call(obj, key)) { +// newObj[key] = obj[key]; +// } +// } +// } +// newObj.default = obj; +// return newObj; +// } +// } diff --git a/packages/utils/src/buildPolyfills/_nullishCoalesce.ts b/packages/utils/src/buildPolyfills/_nullishCoalesce.ts new file mode 100644 index 000000000000..70ba98a2bd8f --- /dev/null +++ b/packages/utils/src/buildPolyfills/_nullishCoalesce.ts @@ -0,0 +1,25 @@ +/** + * Polyfill for the nullish coalescing operator (`??`). + * + * Note that the RHS is wrapped in a function so that if it's a computed value, that evaluation won't happen unless the + * LHS evaluates to a nullish value, to mimic the operator's short-circuiting behavior. + * + * Adapted from Sucrase (https://github.com/alangpierce/sucrase) + * + * @param lhs The value of the expression to the left of the `??` + * @param rhsFn A function returning the value of the expression to the right of the `??` + * @returns The LHS value, unless it's `null` or `undefined`, in which case, the RHS value + */ +export function _nullishCoalesce(lhs: unknown, rhsFn: () => unknown): unknown { + // by checking for loose equality to `null`, we catch both `null` and `undefined` + return lhs != null ? lhs : rhsFn(); +} + +// Sucrase version: +// function _nullishCoalesce(lhs, rhsFn) { +// if (lhs != null) { +// return lhs; +// } else { +// return rhsFn(); +// } +// } diff --git a/packages/utils/src/buildPolyfills/_optionalChain.ts b/packages/utils/src/buildPolyfills/_optionalChain.ts new file mode 100644 index 000000000000..452c6ac110b0 --- /dev/null +++ b/packages/utils/src/buildPolyfills/_optionalChain.ts @@ -0,0 +1,58 @@ +import { GenericFunction } from './types'; + +/** + * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, + * descriptors, and functions. + * + * Adapted from Sucrase (https://github.com/alangpierce/sucrase) + * See https://github.com/alangpierce/sucrase/blob/265887868966917f3b924ce38dfad01fbab1329f/src/transformers/OptionalChainingNullishTransformer.ts#L15 + * + * @param ops Array result of expression conversion + * @returns The value of the expression + */ +export function _optionalChain(ops: unknown[]): unknown { + let lastAccessLHS: unknown = undefined; + let value = ops[0]; + let i = 1; + while (i < ops.length) { + const op = ops[i] as string; + const fn = ops[i + 1] as (intermediateValue: unknown) => unknown; + i += 2; + // by checking for loose equality to `null`, we catch both `null` and `undefined` + if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { + // really we're meaning to return `undefined` as an actual value here, but it saves bytes not to write it + return; + } + if (op === 'access' || op === 'optionalAccess') { + lastAccessLHS = value; + value = fn(value); + } else if (op === 'call' || op === 'optionalCall') { + value = fn((...args: unknown[]) => (value as GenericFunction).call(lastAccessLHS, ...args)); + lastAccessLHS = undefined; + } + } + return value; +} + +// Sucrase version +// function _optionalChain(ops) { +// let lastAccessLHS = undefined; +// let value = ops[0]; +// let i = 1; +// while (i < ops.length) { +// const op = ops[i]; +// const fn = ops[i + 1]; +// i += 2; +// if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { +// return undefined; +// } +// if (op === 'access' || op === 'optionalAccess') { +// lastAccessLHS = value; +// value = fn(value); +// } else if (op === 'call' || op === 'optionalCall') { +// value = fn((...args) => value.call(lastAccessLHS, ...args)); +// lastAccessLHS = undefined; +// } +// } +// return value; +// } diff --git a/packages/utils/src/buildPolyfills/_optionalChainDelete.ts b/packages/utils/src/buildPolyfills/_optionalChainDelete.ts new file mode 100644 index 000000000000..61147bedc5b5 --- /dev/null +++ b/packages/utils/src/buildPolyfills/_optionalChainDelete.ts @@ -0,0 +1,28 @@ +import { _optionalChain } from './_optionalChain'; + +/** + * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, + * descriptors, and functions, in cases where the value of the expression is to be deleted. + * + * Adapted from Sucrase (https://github.com/alangpierce/sucrase) See + * https://github.com/alangpierce/sucrase/blob/265887868966917f3b924ce38dfad01fbab1329f/src/transformers/OptionalChainingNullishTransformer.ts#L15 + * + * @param ops Array result of expression conversion + * @returns The return value of the `delete` operator: `true`, unless the deletion target is an own, non-configurable + * property (one which can't be deleted or turned into an accessor, and whose enumerability can't be changed), in which + * case `false`. + */ +export function _optionalChainDelete(ops: unknown[]): boolean { + const result = _optionalChain(ops) as boolean | null; + // If `result` is `null`, it means we didn't get to the end of the chain and so nothing was deleted (in which case, + // return `true` since that's what `delete` does when it no-ops). If it's non-null, we know the delete happened, in + // which case we return whatever the `delete` returned, which will be a boolean. + return result == null ? true : result; +} + +// Sucrase version: +// function _optionalChainDelete(ops) { +// const result = _optionalChain(ops); +// // by checking for loose equality to `null`, we catch both `null` and `undefined` +// return result == null ? true : result; +// } diff --git a/packages/utils/src/buildPolyfills/index.ts b/packages/utils/src/buildPolyfills/index.ts new file mode 100644 index 000000000000..9717453e98fa --- /dev/null +++ b/packages/utils/src/buildPolyfills/index.ts @@ -0,0 +1,13 @@ +export { _asyncNullishCoalesce } from './_asyncNullishCoalesce'; +export { _asyncOptionalChain } from './_asyncOptionalChain'; +export { _asyncOptionalChainDelete } from './_asyncOptionalChainDelete'; +export { _createNamedExportFrom } from './_createNamedExportFrom'; +export { _createStarExport } from './_createStarExport'; +export { _interopDefault } from './_interopDefault'; +export { _interopNamespace } from './_interopNamespace'; +export { _interopNamespaceDefaultOnly } from './_interopNamespaceDefaultOnly'; +export { _interopRequireDefault } from './_interopRequireDefault'; +export { _interopRequireWildcard } from './_interopRequireWildcard'; +export { _nullishCoalesce } from './_nullishCoalesce'; +export { _optionalChain } from './_optionalChain'; +export { _optionalChainDelete } from './_optionalChainDelete'; diff --git a/packages/utils/src/buildPolyfills/types.ts b/packages/utils/src/buildPolyfills/types.ts new file mode 100644 index 000000000000..41f1a8a31ee5 --- /dev/null +++ b/packages/utils/src/buildPolyfills/types.ts @@ -0,0 +1,7 @@ +import { Primitive } from '@sentry/types'; + +export type GenericObject = { [key: string]: Value }; +export type GenericFunction = (...args: unknown[]) => Value; +export type Value = Primitive | GenericFunction | GenericObject; + +export type RequireResult = GenericObject | (GenericFunction & GenericObject); diff --git a/packages/utils/src/dsn.ts b/packages/utils/src/dsn.ts index 0b58a7974821..2c43c2934fb4 100644 --- a/packages/utils/src/dsn.ts +++ b/packages/utils/src/dsn.ts @@ -27,6 +27,12 @@ export function dsnToString(dsn: DsnComponents, withPassword: boolean = false): ); } +/** + * Parses a Dsn from a given string. + * + * @param str A Dsn as string + * @returns Dsn as DsnComponents + */ function dsnFromString(str: string): DsnComponents { const match = DSN_REGEX.exec(str); @@ -55,13 +61,7 @@ function dsnFromString(str: string): DsnComponents { } function dsnFromComponents(components: DsnComponents): DsnComponents { - // TODO this is for backwards compatibility, and can be removed in a future version - if ('user' in components && !('publicKey' in components)) { - components.publicKey = components.user; - } - return { - user: components.publicKey || '', protocol: components.protocol, publicKey: components.publicKey || '', pass: components.pass || '', @@ -104,8 +104,27 @@ function validateDsn(dsn: DsnComponents): boolean | void { /** The Sentry Dsn, identifying a Sentry instance and project. */ export function makeDsn(from: DsnLike): DsnComponents { const components = typeof from === 'string' ? dsnFromString(from) : dsnFromComponents(from); - validateDsn(components); - return components; } + +/** + * Changes a Dsn to point to the `relay` server running in the Lambda Extension. + * + * This is only used by the serverless integration for AWS Lambda. + * + * @param originalDsn The original Dsn of the customer. + * @returns Dsn pointing to Lambda extension. + */ +export function extensionRelayDSN(originalDsn: string | undefined): string | undefined { + if (originalDsn === undefined) { + return undefined; + } + + const dsn = dsnFromString(originalDsn); + dsn.host = 'localhost'; + dsn.port = '3000'; + dsn.protocol = 'http'; + + return dsnToString(dsn); +} diff --git a/packages/utils/src/enums.ts b/packages/utils/src/enums.ts deleted file mode 100644 index 998540a6677f..000000000000 --- a/packages/utils/src/enums.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const SeverityLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug', 'critical'] as const; -export type SeverityLevel = typeof SeverityLevels[number]; diff --git a/packages/utils/src/envelope.ts b/packages/utils/src/envelope.ts index 0000e6007aee..1bc69915345d 100644 --- a/packages/utils/src/envelope.ts +++ b/packages/utils/src/envelope.ts @@ -1,6 +1,6 @@ -import { Envelope } from '@sentry/types'; +import { Attachment, AttachmentItem, DataCategory, Envelope, EnvelopeItem, EnvelopeItemType } from '@sentry/types'; -import { isPrimitive } from './is'; +import { dropUndefinedKeys } from './object'; /** * Creates an envelope. @@ -22,29 +22,103 @@ export function addItemToEnvelope(envelope: E, newItem: E[1] } /** - * Get the type of the envelope. Grabs the type from the first envelope item. + * Convenience function to loop through the items and item types of an envelope. + * (This function was mostly created because working with envelope types is painful at the moment) */ -export function getEnvelopeType(envelope: E): string { - const [, [[firstItemHeader]]] = envelope; - return firstItemHeader.type; +export function forEachEnvelopeItem( + envelope: Envelope, + callback: (envelopeItem: E[1][number], envelopeItemType: E[1][number][0]['type']) => void, +): void { + const envelopeItems = envelope[1]; + envelopeItems.forEach((envelopeItem: EnvelopeItem) => { + const envelopeItemType = envelopeItem[0].type; + callback(envelopeItem, envelopeItemType); + }); +} + +// Combination of global TextEncoder and Node require('util').TextEncoder +interface TextEncoderInternal extends TextEncoderCommon { + encode(input?: string): Uint8Array; +} + +function encodeUTF8(input: string, textEncoder?: TextEncoderInternal): Uint8Array { + const utf8 = textEncoder || new TextEncoder(); + return utf8.encode(input); } /** - * Serializes an envelope into a string. + * Serializes an envelope. */ -export function serializeEnvelope(envelope: Envelope): string { - const [headers, items] = envelope; - const serializedHeaders = JSON.stringify(headers); - - // Have to cast items to any here since Envelope is a union type - // Fixed in Typescript 4.2 - // TODO: Remove any[] cast when we upgrade to TS 4.2 - // https://github.com/microsoft/TypeScript/issues/36390 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (items as any[]).reduce((acc, item: typeof items[number]) => { - const [itemHeaders, payload] = item; - // We do not serialize payloads that are primitives - const serializedPayload = isPrimitive(payload) ? String(payload) : JSON.stringify(payload); - return `${acc}\n${JSON.stringify(itemHeaders)}\n${serializedPayload}`; - }, serializedHeaders); +export function serializeEnvelope(envelope: Envelope, textEncoder?: TextEncoderInternal): string | Uint8Array { + const [envHeaders, items] = envelope; + + // Initially we construct our envelope as a string and only convert to binary chunks if we encounter binary data + let parts: string | Uint8Array[] = JSON.stringify(envHeaders); + + function append(next: string | Uint8Array): void { + if (typeof parts === 'string') { + parts = typeof next === 'string' ? parts + next : [encodeUTF8(parts, textEncoder), next]; + } else { + parts.push(typeof next === 'string' ? encodeUTF8(next, textEncoder) : next); + } + } + + for (const item of items) { + const [itemHeaders, payload] = item as typeof items[number]; + append(`\n${JSON.stringify(itemHeaders)}\n`); + append(typeof payload === 'string' || payload instanceof Uint8Array ? payload : JSON.stringify(payload)); + } + + return typeof parts === 'string' ? parts : concatBuffers(parts); +} + +function concatBuffers(buffers: Uint8Array[]): Uint8Array { + const totalLength = buffers.reduce((acc, buf) => acc + buf.length, 0); + + const merged = new Uint8Array(totalLength); + let offset = 0; + for (const buffer of buffers) { + merged.set(buffer, offset); + offset += buffer.length; + } + + return merged; +} + +/** + * Creates attachment envelope items + */ +export function createAttachmentEnvelopeItem( + attachment: Attachment, + textEncoder?: TextEncoderInternal, +): AttachmentItem { + const buffer = typeof attachment.data === 'string' ? encodeUTF8(attachment.data, textEncoder) : attachment.data; + + return [ + dropUndefinedKeys({ + type: 'attachment', + length: buffer.length, + filename: attachment.filename, + content_type: attachment.contentType, + attachment_type: attachment.attachmentType, + }), + buffer, + ]; +} + +const ITEM_TYPE_TO_DATA_CATEGORY_MAP: Record = { + session: 'session', + sessions: 'session', + attachment: 'attachment', + transaction: 'transaction', + event: 'error', + client_report: 'internal', + user_report: 'default', +}; + +/** + * Maps the type of an envelope item to a data category. + */ +export function envelopeItemTypeToDataCategory(type: EnvelopeItemType): DataCategory { + return ITEM_TYPE_TO_DATA_CATEGORY_MAP[type]; } diff --git a/packages/utils/src/error.ts b/packages/utils/src/error.ts index f913d80a7438..30b35c115d14 100644 --- a/packages/utils/src/error.ts +++ b/packages/utils/src/error.ts @@ -1,5 +1,3 @@ -import { setPrototypeOf } from './polyfill'; - /** An error emitted by Sentry SDKs and related utilities. */ export class SentryError extends Error { /** Display name of this error instance. */ @@ -9,6 +7,6 @@ export class SentryError extends Error { super(message); this.name = new.target.prototype.constructor.name; - setPrototypeOf(this, new.target.prototype); + Object.setPrototypeOf(this, new.target.prototype); } } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index e4567790f7d3..fd627db6f2e5 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,7 +1,5 @@ -export * from './async'; export * from './browser'; export * from './dsn'; -export * from './enums'; export * from './error'; export * from './global'; export * from './instrument'; @@ -16,7 +14,6 @@ export * from './path'; export * from './promisebuffer'; export * from './severity'; export * from './stacktrace'; -export * from './status'; export * from './string'; export * from './supports'; export * from './syncpromise'; @@ -26,3 +23,4 @@ export * from './env'; export * from './envelope'; export * from './clientreport'; export * from './ratelimit'; +export * from './baggage'; diff --git a/packages/utils/src/instrument.ts b/packages/utils/src/instrument.ts index 30fcef290c19..a77e0c8222f3 100644 --- a/packages/utils/src/instrument.ts +++ b/packages/utils/src/instrument.ts @@ -13,7 +13,7 @@ import { supportsHistory, supportsNativeFetch } from './supports'; const global = getGlobalObject(); -type InstrumentHandlerType = +export type InstrumentHandlerType = | 'console' | 'dom' | 'fetch' @@ -22,7 +22,7 @@ type InstrumentHandlerType = | 'xhr' | 'error' | 'unhandledrejection'; -type InstrumentHandlerCallback = (data: any) => void; +export type InstrumentHandlerCallback = (data: any) => void; /** * Instrument native APIs to call handlers that can be used to create breadcrumbs, APM spans etc. diff --git a/packages/utils/src/is.ts b/packages/utils/src/is.ts index 4981359968c9..81ae28349a0a 100644 --- a/packages/utils/src/is.ts +++ b/packages/utils/src/is.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { Primitive } from '@sentry/types'; +import { PolymorphicEvent, Primitive } from '@sentry/types'; // eslint-disable-next-line @typescript-eslint/unbound-method const objectToString = Object.prototype.toString; @@ -101,7 +101,7 @@ export function isPlainObject(wat: unknown): wat is Record { * @param wat A value to be checked. * @returns A boolean representing the result. */ -export function isEvent(wat: unknown): boolean { +export function isEvent(wat: unknown): wat is PolymorphicEvent { return typeof Event !== 'undefined' && isInstanceOf(wat, Event); } diff --git a/packages/utils/src/normalize.ts b/packages/utils/src/normalize.ts index 51566a2125a7..c88ab00b510f 100644 --- a/packages/utils/src/normalize.ts +++ b/packages/utils/src/normalize.ts @@ -1,6 +1,6 @@ import { Primitive } from '@sentry/types'; -import { isError, isEvent, isNaN, isSyntheticEvent } from './is'; +import { isNaN, isSyntheticEvent } from './is'; import { memoBuilder, MemoFunc } from './memo'; import { convertToPlainObject } from './object'; import { getFunctionName } from './stacktrace'; @@ -31,9 +31,10 @@ type ObjOrArray = { [key: string]: T }; * object in the normallized output.. * @returns A normalized version of the object, or `"**non-serializable**"` if any errors are thrown during normalization. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function normalize(input: unknown, depth: number = +Infinity, maxProperties: number = +Infinity): any { try { - // since we're at the outermost level, there is no key + // since we're at the outermost level, we don't provide a key return visit('', input, depth, maxProperties); } catch (err) { return { ERROR: `**non-serializable** (${err})` }; @@ -42,6 +43,7 @@ export function normalize(input: unknown, depth: number = +Infinity, maxProperti /** JSDoc */ export function normalizeToSize( + // eslint-disable-next-line @typescript-eslint/no-explicit-any object: { [key: string]: any }, // Default Node.js REPL depth depth: number = 3, @@ -98,6 +100,15 @@ function visit( return stringified; } + // From here on, we can assert that `value` is either an object or an array. + + // Do not normalize objects that we know have already been normalized. As a general rule, the + // "__sentry_skip_normalization__" property should only be used sparingly and only should only be set on objects that + // have already been normalized. + if ((value as ObjOrArray)['__sentry_skip_normalization__']) { + return value as ObjOrArray; + } + // We're also done if we've reached the max depth if (depth === 0) { // At this point we know `serialized` is a string of the form `"[object XXXX]"`. Clean it up so it's just `"[XXXX]"`. @@ -117,7 +128,7 @@ function visit( // Before we begin, convert`Error` and`Event` instances into plain objects, since some of each of their relevant // properties are non-enumerable and otherwise would get missed. - const visitable = (isError(value) || isEvent(value) ? convertToPlainObject(value) : value) as ObjOrArray; + const visitable = convertToPlainObject(value as ObjOrArray); for (const visitKey in visitable) { // Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration. @@ -232,6 +243,7 @@ function utf8Length(value: string): number { } /** Calculates bytes size of input object */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any function jsonSize(value: any): number { return utf8Length(JSON.stringify(value)); } diff --git a/packages/utils/src/object.ts b/packages/utils/src/object.ts index cc401d2fbdfe..5957214ecc86 100644 --- a/packages/utils/src/object.ts +++ b/packages/utils/src/object.ts @@ -1,9 +1,10 @@ /* eslint-disable max-lines */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { ExtendedError, WrappedFunction } from '@sentry/types'; +import { WrappedFunction } from '@sentry/types'; import { htmlTreeAsString } from './browser'; import { isElement, isError, isEvent, isInstanceOf, isPlainObject, isPrimitive } from './is'; +import { memoBuilder, MemoFunc } from './memo'; import { truncate } from './string'; /** @@ -92,50 +93,59 @@ export function urlEncode(object: { [key: string]: any }): string { } /** - * Transforms any object into an object literal with all its attributes - * attached to it. + * Transforms any `Error` or `Event` into a plain object with all of their enumerable properties, and some of their + * non-enumerable properties attached. * * @param value Initial source that we have to transform in order for it to be usable by the serializer + * @returns An Event or Error turned into an object - or the value argurment itself, when value is neither an Event nor + * an Error. */ -export function convertToPlainObject(value: unknown): { - [key: string]: unknown; -} { - let newObj = value as { - [key: string]: unknown; - }; - +export function convertToPlainObject( + value: V, +): + | { + [ownProps: string]: unknown; + type: string; + target: string; + currentTarget: string; + detail?: unknown; + } + | { + [ownProps: string]: unknown; + message: string; + name: string; + stack?: string; + } + | V { if (isError(value)) { - newObj = { + return { message: value.message, name: value.name, stack: value.stack, - ...getOwnProperties(value as ExtendedError), + ...getOwnProperties(value), }; } else if (isEvent(value)) { - /** - * Event-like interface that's usable in browser and node - */ - interface SimpleEvent { - [key: string]: unknown; + const newObj: { + [ownProps: string]: unknown; type: string; - target?: unknown; - currentTarget?: unknown; - } - - const event = value as SimpleEvent; - - newObj = { - type: event.type, - target: serializeEventTarget(event.target), - currentTarget: serializeEventTarget(event.currentTarget), - ...getOwnProperties(event), + target: string; + currentTarget: string; + detail?: unknown; + } = { + type: value.type, + target: serializeEventTarget(value.target), + currentTarget: serializeEventTarget(value.currentTarget), + ...getOwnProperties(value), }; if (typeof CustomEvent !== 'undefined' && isInstanceOf(value, CustomEvent)) { - newObj.detail = event.detail; + newObj.detail = value.detail; } + + return newObj; + } else { + return value; } - return newObj; } /** Creates a string representation of the target of an `Event` object */ @@ -148,14 +158,18 @@ function serializeEventTarget(target: unknown): string { } /** Filters out all but an object's own properties */ -function getOwnProperties(obj: { [key: string]: unknown }): { [key: string]: unknown } { - const extractedProps: { [key: string]: unknown } = {}; - for (const property in obj) { - if (Object.prototype.hasOwnProperty.call(obj, property)) { - extractedProps[property] = obj[property]; +function getOwnProperties(obj: unknown): { [key: string]: unknown } { + if (typeof obj === 'object' && obj !== null) { + const extractedProps: { [key: string]: unknown } = {}; + for (const property in obj) { + if (Object.prototype.hasOwnProperty.call(obj, property)) { + extractedProps[property] = (obj as Record)[property]; + } } + return extractedProps; + } else { + return {}; } - return extractedProps; } /** @@ -163,8 +177,7 @@ function getOwnProperties(obj: { [key: string]: unknown }): { [key: string]: unk * and truncated list that will be used inside the event message. * eg. `Non-error exception captured with keys: foo, bar, baz` */ -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function extractExceptionKeysForMessage(exception: any, maxLength: number = 40): string { +export function extractExceptionKeysForMessage(exception: Record, maxLength: number = 40): string { const keys = Object.keys(convertToPlainObject(exception)); keys.sort(); @@ -193,20 +206,37 @@ export function extractExceptionKeysForMessage(exception: any, maxLength: number /** * Given any object, return the new object with removed keys that value was `undefined`. * Works recursively on objects and arrays. + * + * Attention: This function keeps circular references in the returned object. */ export function dropUndefinedKeys(val: T): T { + // This function just proxies `_dropUndefinedKeys` to keep the `memoBuilder` out of this function's API + return _dropUndefinedKeys(val, memoBuilder()); +} + +function _dropUndefinedKeys(val: T, memo: MemoFunc): T { + const [memoize] = memo; // we don't need unmemoize because we don't need to visit nodes twice + if (isPlainObject(val)) { + if (memoize(val)) { + return val; + } const rv: { [key: string]: any } = {}; for (const key of Object.keys(val)) { if (typeof val[key] !== 'undefined') { - rv[key] = dropUndefinedKeys(val[key]); + rv[key] = _dropUndefinedKeys(val[key], memo); } } return rv as T; } if (Array.isArray(val)) { - return (val as any[]).map(dropUndefinedKeys) as any; + if (memoize(val)) { + return val; + } + return (val as any[]).map(item => { + return _dropUndefinedKeys(item, memo); + }) as any; } return val; diff --git a/packages/utils/src/polyfill.ts b/packages/utils/src/polyfill.ts deleted file mode 100644 index a11b47fc55e1..000000000000 --- a/packages/utils/src/polyfill.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const setPrototypeOf = - Object.setPrototypeOf || ({ __proto__: [] } instanceof Array ? setProtoOf : mixinProperties); - -/** - * setPrototypeOf polyfill using __proto__ - */ -// eslint-disable-next-line @typescript-eslint/ban-types -function setProtoOf(obj: TTarget, proto: TProto): TTarget & TProto { - // @ts-ignore __proto__ does not exist on obj - obj.__proto__ = proto; - return obj as TTarget & TProto; -} - -/** - * setPrototypeOf polyfill using mixin - */ -// eslint-disable-next-line @typescript-eslint/ban-types -function mixinProperties(obj: TTarget, proto: TProto): TTarget & TProto { - for (const prop in proto) { - if (!Object.prototype.hasOwnProperty.call(obj, prop)) { - // @ts-ignore typescript complains about indexing so we remove - obj[prop] = proto[prop]; - } - } - - return obj as TTarget & TProto; -} diff --git a/packages/utils/src/ratelimit.ts b/packages/utils/src/ratelimit.ts index 59906c0abdaf..78720c888576 100644 --- a/packages/utils/src/ratelimit.ts +++ b/packages/utils/src/ratelimit.ts @@ -1,4 +1,6 @@ -// Keeping the key broad until we add the new transports +import { TransportMakeRequestResponse } from '@sentry/types'; + +// Intentionally keeping the key broad, as we don't know for sure what rate limit headers get returned from backend export type RateLimits = Record; export const DEFAULT_RETRY_AFTER = 60 * 1000; // 60 seconds @@ -43,7 +45,7 @@ export function isRateLimited(limits: RateLimits, category: string, now: number */ export function updateRateLimits( limits: RateLimits, - headers: Record, + { statusCode, headers }: TransportMakeRequestResponse, now: number = Date.now(), ): RateLimits { const updatedRateLimits: RateLimits = { @@ -52,8 +54,8 @@ export function updateRateLimits( // "The name is case-insensitive." // https://developer.mozilla.org/en-US/docs/Web/API/Headers/get - const rateLimitHeader = headers['x-sentry-rate-limits']; - const retryAfterHeader = headers['retry-after']; + const rateLimitHeader = headers && headers['x-sentry-rate-limits']; + const retryAfterHeader = headers && headers['retry-after']; if (rateLimitHeader) { /** @@ -69,19 +71,21 @@ export function updateRateLimits( * is an arbitrary string like "org_quota" - ignored by SDK */ for (const limit of rateLimitHeader.trim().split(',')) { - const parameters = limit.split(':', 2); - const headerDelay = parseInt(parameters[0], 10); + const [retryAfter, categories] = limit.split(':', 2); + const headerDelay = parseInt(retryAfter, 10); const delay = (!isNaN(headerDelay) ? headerDelay : 60) * 1000; // 60sec default - if (!parameters[1]) { + if (!categories) { updatedRateLimits.all = now + delay; } else { - for (const category of parameters[1].split(';')) { + for (const category of categories.split(';')) { updatedRateLimits[category] = now + delay; } } } } else if (retryAfterHeader) { updatedRateLimits.all = now + parseRetryAfterHeader(retryAfterHeader, now); + } else if (statusCode === 429) { + updatedRateLimits.all = now + 60 * 1000; } return updatedRateLimits; diff --git a/packages/utils/src/severity.ts b/packages/utils/src/severity.ts index 034f7dcbeb99..c40f0c2a6004 100644 --- a/packages/utils/src/severity.ts +++ b/packages/utils/src/severity.ts @@ -1,20 +1,36 @@ -import { Severity } from '@sentry/types'; +/* eslint-disable deprecation/deprecation */ +import { Severity, SeverityLevel } from '@sentry/types'; -import { SeverityLevel, SeverityLevels } from './enums'; +// Note: Ideally the `SeverityLevel` type would be derived from `validSeverityLevels`, but that would mean either +// +// a) moving `validSeverityLevels` to `@sentry/types`, +// b) moving the`SeverityLevel` type here, or +// c) importing `validSeverityLevels` from here into `@sentry/types`. +// +// Option A would make `@sentry/types` a runtime dependency of `@sentry/utils` (not good), and options B and C would +// create a circular dependency between `@sentry/types` and `@sentry/utils` (also not good). So a TODO accompanying the +// type, reminding anyone who changes it to change this list also, will have to do. + +export const validSeverityLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug']; -function isSupportedSeverity(level: string): level is Severity { - return SeverityLevels.indexOf(level as SeverityLevel) !== -1; -} /** - * Converts a string-based level into a {@link Severity}. + * Converts a string-based level into a member of the deprecated {@link Severity} enum. * - * @param level string representation of Severity + * @deprecated `severityFromString` is deprecated. Please use `severityLevelFromString` instead. + * + * @param level String representation of Severity * @returns Severity */ -export function severityFromString(level: SeverityLevel | string): Severity { - if (level === 'warn') return Severity.Warning; - if (isSupportedSeverity(level)) { - return level; - } - return Severity.Log; +export function severityFromString(level: Severity | SeverityLevel | string): Severity { + return severityLevelFromString(level) as Severity; +} + +/** + * Converts a string-based level into a `SeverityLevel`, normalizing it along the way. + * + * @param level String representation of desired `SeverityLevel`. + * @returns The `SeverityLevel` corresponding to the given string, or 'log' if the string isn't a valid level. + */ +export function severityLevelFromString(level: SeverityLevel | string): SeverityLevel { + return (level === 'warn' ? 'warning' : validSeverityLevels.includes(level) ? level : 'log') as SeverityLevel; } diff --git a/packages/utils/src/stacktrace.ts b/packages/utils/src/stacktrace.ts index 7c9dddeee298..0949a6194526 100644 --- a/packages/utils/src/stacktrace.ts +++ b/packages/utils/src/stacktrace.ts @@ -1,11 +1,7 @@ -import { StackFrame } from '@sentry/types'; +import { StackFrame, StackLineParser, StackParser } from '@sentry/types'; const STACKTRACE_LIMIT = 50; -export type StackParser = (stack: string, skipFirst?: number) => StackFrame[]; -export type StackLineParserFn = (line: string) => StackFrame | undefined; -export type StackLineParser = [number, StackLineParserFn]; - /** * Creates a stack parser with the supplied line parsers * @@ -34,6 +30,19 @@ export function createStackParser(...parsers: StackLineParser[]): StackParser { }; } +/** + * Gets a stack parser implementation from Options.stackParser + * @see Options + * + * If options contains an array of line parsers, it is converted into a parser + */ +export function stackParserFromStackParserOptions(stackParser: StackParser | StackLineParser[]): StackParser { + if (Array.isArray(stackParser)) { + return createStackParser(...stackParser); + } + return stackParser; +} + /** * @hidden */ diff --git a/packages/utils/src/status.ts b/packages/utils/src/status.ts deleted file mode 100644 index 381a0971b66f..000000000000 --- a/packages/utils/src/status.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { EventStatus } from '@sentry/types'; -/** - * Converts an HTTP status code to sentry status {@link EventStatus}. - * - * @param code number HTTP status code - * @returns EventStatus - */ -export function eventStatusFromHttpCode(code: number): EventStatus { - if (code >= 200 && code < 300) { - return 'success'; - } - - if (code === 429) { - return 'rate_limit'; - } - - if (code >= 400 && code < 500) { - return 'invalid'; - } - - if (code >= 500) { - return 'failed'; - } - - return 'unknown'; -} diff --git a/packages/utils/src/syncpromise.ts b/packages/utils/src/syncpromise.ts index c824d1b09063..ec5cd9e9faee 100644 --- a/packages/utils/src/syncpromise.ts +++ b/packages/utils/src/syncpromise.ts @@ -14,13 +14,17 @@ const enum States { REJECTED = 2, } +// Overloads so we can call resolvedSyncPromise without arguments and generic argument +export function resolvedSyncPromise(): PromiseLike; +export function resolvedSyncPromise(value: T | PromiseLike): PromiseLike; + /** * Creates a resolved sync promise. * * @param value the value to resolve the promise with * @returns the resolved sync promise */ -export function resolvedSyncPromise(value: T | PromiseLike): PromiseLike { +export function resolvedSyncPromise(value?: T | PromiseLike): PromiseLike { return new SyncPromise(resolve => { resolve(value); }); diff --git a/packages/utils/test/async.test.ts b/packages/utils/test/async.test.ts deleted file mode 100644 index d4edbeca7129..000000000000 --- a/packages/utils/test/async.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { forget } from '../src/async'; - -describe('forget', () => { - const console = { - error: jest.fn(), - log: jest.fn(), - }; - - beforeEach(() => { - global.console = console as any as Console; - }); - - test('logs rejections to console.error', done => { - const error = new Error(); - forget(Promise.reject(error)); - - setImmediate(() => { - expect(console.error).toHaveBeenCalledWith(error); - done(); - }); - }); -}); diff --git a/packages/utils/test/baggage.test.ts b/packages/utils/test/baggage.test.ts new file mode 100644 index 000000000000..07ccdaaedd48 --- /dev/null +++ b/packages/utils/test/baggage.test.ts @@ -0,0 +1,132 @@ +import { + createBaggage, + getBaggageValue, + isBaggageEmpty, + mergeAndSerializeBaggage, + parseBaggageString, + serializeBaggage, + setBaggageValue, +} from '../src/baggage'; + +describe('Baggage', () => { + describe('createBaggage', () => { + it.each([ + ['creates an empty baggage instance', {}, [{}, '']], + [ + 'creates a baggage instance with initial values', + { environment: 'production', anyKey: 'anyValue' }, + [{ environment: 'production', anyKey: 'anyValue' }, ''], + ], + ])('%s', (_: string, input, output) => { + expect(createBaggage(input)).toEqual(output); + }); + }); + + describe('getBaggageValue', () => { + it.each([ + [ + 'gets a baggage item', + createBaggage({ environment: 'production', anyKey: 'anyValue' }), + 'environment', + 'production', + ], + ['finds undefined items', createBaggage({}), 'environment', undefined], + ])('%s', (_: string, baggage, key, value) => { + expect(getBaggageValue(baggage, key)).toEqual(value); + }); + }); + + describe('setBaggageValue', () => { + it.each([ + ['sets a baggage item', createBaggage({}), 'environment', 'production'], + ['overwrites a baggage item', createBaggage({ environment: 'development' }), 'environment', 'production'], + ])('%s', (_: string, baggage, key, value) => { + setBaggageValue(baggage, key, value); + expect(getBaggageValue(baggage, key)).toEqual(value); + }); + }); + + describe('serializeBaggage', () => { + it.each([ + ['serializes empty baggage', createBaggage({}), ''], + [ + 'serializes baggage with a single value', + createBaggage({ environment: 'production' }), + 'sentry-environment=production', + ], + [ + 'serializes baggage with multiple values', + createBaggage({ environment: 'production', release: '10.0.2' }), + 'sentry-environment=production,sentry-release=10.0.2', + ], + [ + 'keeps non-sentry prefixed baggage items', + createBaggage( + { environment: 'production', release: '10.0.2' }, + 'userId=alice,serverNode=DF%2028,isProduction=false', + ), + 'userId=alice,serverNode=DF%2028,isProduction=false,sentry-environment=production,sentry-release=10.0.2', + ], + [ + 'can only use non-sentry prefixed baggage items', + createBaggage({}, 'userId=alice,serverNode=DF%2028,isProduction=false'), + 'userId=alice,serverNode=DF%2028,isProduction=false', + ], + ])('%s', (_: string, baggage, serializedBaggage) => { + expect(serializeBaggage(baggage)).toEqual(serializedBaggage); + }); + }); + + describe('parseBaggageString', () => { + it.each([ + ['parses an empty string', '', createBaggage({})], + [ + 'parses sentry values into baggage', + 'sentry-environment=production,sentry-release=10.0.2', + createBaggage({ environment: 'production', release: '10.0.2' }), + ], + [ + 'parses arbitrary baggage headers', + 'userId=alice,serverNode=DF%2028,isProduction=false,sentry-environment=production,sentry-release=10.0.2', + createBaggage( + { environment: 'production', release: '10.0.2' }, + 'userId=alice,serverNode=DF%2028,isProduction=false', + ), + ], + ])('%s', (_: string, baggageString, baggage) => { + expect(parseBaggageString(baggageString)).toEqual(baggage); + }); + }); + + describe('isBaggageEmpty', () => { + it.each([ + ['returns true if the modifyable part of baggage is empty', createBaggage({}), true], + ['returns false if the modifyable part of baggage is not empty', createBaggage({ release: '10.0.2' }), false], + ])('%s', (_: string, baggage, outcome) => { + expect(isBaggageEmpty(baggage)).toEqual(outcome); + }); + }); + + describe('mergeAndSerializeBaggage', () => { + it.each([ + [ + 'returns original baggage when there is no additional baggage', + createBaggage({ release: '1.1.1', userid: '1234' }, 'foo=bar'), + undefined, + 'foo=bar,sentry-release=1.1.1,sentry-userid=1234', + ], + [ + 'returns merged baggage when there is a 3rd party header added', + createBaggage({ release: '1.1.1', userid: '1234' }, 'foo=bar'), + 'bar=baz,key=value', + 'bar=baz,key=value,sentry-release=1.1.1,sentry-userid=1234', + ], + ['returns merged baggage original baggage is empty', createBaggage({}), 'bar=baz,key=value', 'bar=baz,key=value'], + ['returns empty string when original and 3rd party baggage are empty', createBaggage({}), '', ''], + ['returns merged baggage original baggage is undefined', undefined, 'bar=baz,key=value', 'bar=baz,key=value'], + ['returns empty string when both params are undefined', undefined, undefined, ''], + ])('%s', (_: string, baggage, headerBaggageString, outcome) => { + expect(mergeAndSerializeBaggage(baggage, headerBaggageString)).toEqual(outcome); + }); + }); +}); diff --git a/packages/utils/test/buildPolyfills/interop.test.ts b/packages/utils/test/buildPolyfills/interop.test.ts new file mode 100644 index 000000000000..917d62daee2f --- /dev/null +++ b/packages/utils/test/buildPolyfills/interop.test.ts @@ -0,0 +1,180 @@ +import { + _interopDefault, + _interopNamespace, + _interopNamespaceDefaultOnly, + _interopRequireDefault, + _interopRequireWildcard, +} from '../../src/buildPolyfills'; +import { RequireResult } from '../../src/buildPolyfills/types'; +import { + _interopDefault as _interopDefaultOrig, + _interopNamespace as _interopNamespaceOrig, + _interopNamespaceDefaultOnly as _interopNamespaceDefaultOnlyOrig, + _interopRequireDefault as _interopRequireDefaultOrig, + _interopRequireWildcard as _interopRequireWildcardOrig, +} from './originals'; + +// This file tests five different functions against a range of test cases. Though the inputs are the same for each +// function's test cases, the expected output differs. The testcases for each function are therefore built from separate +// collections of expected inputs and expected outputs. Further, for readability purposes, the tests labels have also +// been split into their own object. It's also worth noting that in real life, there are some test-case/function +// pairings which would never happen, but by testing all combinations, we're guaranteed to have tested the ones which +// show up in the wild. + +const dogStr = 'dogs are great!'; +const dogFunc = () => dogStr; +const dogAdjectives = { maisey: 'silly', charlie: 'goofy' }; + +const withESModuleFlag = { __esModule: true, ...dogAdjectives }; +const withESModuleFlagAndDefault = { __esModule: true, default: dogFunc, ...dogAdjectives }; +const namedExports = { ...dogAdjectives }; +const withNonEnumerableProp = { ...dogAdjectives }; +// Properties added using `Object.defineProperty` are non-enumerable by default +Object.defineProperty(withNonEnumerableProp, 'hiddenProp', { value: 'shhhhhhhh' }); +const withDefaultExport = { default: dogFunc, ...dogAdjectives }; +const withOnlyDefaultExport = { default: dogFunc }; +const exportsEquals = dogFunc as RequireResult; +const exportsEqualsWithDefault = dogFunc as RequireResult; +exportsEqualsWithDefault.default = exportsEqualsWithDefault; + +const mockRequireResults: Record = { + withESModuleFlag, + withESModuleFlagAndDefault, + namedExports, + withNonEnumerableProp, + withDefaultExport, + withOnlyDefaultExport, + exportsEquals: exportsEquals, + exportsEqualsWithDefault: exportsEqualsWithDefault as unknown as RequireResult, +}; + +const testLabels: Record = { + withESModuleFlag: 'module with `__esModule` flag', + withESModuleFlagAndDefault: 'module with `__esModule` flag and default export', + namedExports: 'module with named exports', + withNonEnumerableProp: 'module with named exports and non-enumerable prop', + withDefaultExport: 'module with default export', + withOnlyDefaultExport: 'module with only default export', + exportsEquals: 'module using `exports =`', + exportsEqualsWithDefault: 'module using `exports =` with default export', +}; + +function makeTestCases(expectedOutputs: Record): Array<[string, RequireResult, RequireResult]> { + return Object.keys(mockRequireResults).map(key => [testLabels[key], mockRequireResults[key], expectedOutputs[key]]); +} + +describe('_interopNamespace', () => { + describe('returns the same result as the original', () => { + const expectedOutputs: Record = { + withESModuleFlag: withESModuleFlag, + withESModuleFlagAndDefault: withESModuleFlagAndDefault, + namedExports: { ...namedExports, default: namedExports }, + withNonEnumerableProp: { + ...withNonEnumerableProp, + default: withNonEnumerableProp, + }, + withDefaultExport: { ...withDefaultExport, default: withDefaultExport }, + withOnlyDefaultExport: { default: withOnlyDefaultExport }, + exportsEquals: { default: exportsEquals }, + exportsEqualsWithDefault: { default: exportsEqualsWithDefault }, + }; + + const testCases = makeTestCases(expectedOutputs); + + it.each(testCases)('%s', (_, requireResult, expectedOutput) => { + expect(_interopNamespace(requireResult)).toEqual(_interopNamespaceOrig(requireResult)); + expect(_interopNamespace(requireResult)).toEqual(expectedOutput); + }); + }); +}); + +describe('_interopNamespaceDefaultOnly', () => { + describe('returns the same result as the original', () => { + const expectedOutputs: Record = { + withESModuleFlag: { default: withESModuleFlag }, + withESModuleFlagAndDefault: { default: withESModuleFlagAndDefault }, + namedExports: { default: namedExports }, + withNonEnumerableProp: { default: withNonEnumerableProp }, + withDefaultExport: { default: withDefaultExport }, + withOnlyDefaultExport: { default: withOnlyDefaultExport }, + exportsEquals: { default: exportsEquals }, + exportsEqualsWithDefault: { default: exportsEqualsWithDefault }, + }; + + const testCases = makeTestCases(expectedOutputs); + + it.each(testCases)('%s', (_, requireResult, expectedOutput) => { + expect(_interopNamespaceDefaultOnly(requireResult)).toEqual(_interopNamespaceDefaultOnlyOrig(requireResult)); + expect(_interopNamespaceDefaultOnly(requireResult)).toEqual(expectedOutput); + }); + }); +}); + +describe('_interopRequireWildcard', () => { + describe('returns the same result as the original', () => { + const expectedOutputs: Record = { + withESModuleFlag: withESModuleFlag, + withESModuleFlagAndDefault: withESModuleFlagAndDefault, + namedExports: { ...namedExports, default: namedExports }, + withNonEnumerableProp: { + ...withNonEnumerableProp, + default: withNonEnumerableProp, + }, + withDefaultExport: { ...withDefaultExport, default: withDefaultExport }, + withOnlyDefaultExport: { default: withOnlyDefaultExport }, + exportsEquals: { default: exportsEquals }, + exportsEqualsWithDefault: { default: exportsEqualsWithDefault }, + }; + + const testCases = makeTestCases(expectedOutputs); + + it.each(testCases)('%s', (_, requireResult, expectedOutput) => { + expect(_interopRequireWildcard(requireResult)).toEqual(_interopRequireWildcardOrig(requireResult)); + expect(_interopRequireWildcard(requireResult)).toEqual(expectedOutput); + }); + }); +}); + +describe('_interopDefault', () => { + describe('returns the same result as the original', () => { + const expectedOutputs: Record = { + withESModuleFlag: undefined as unknown as RequireResult, + withESModuleFlagAndDefault: withESModuleFlagAndDefault.default as RequireResult, + namedExports: namedExports, + withNonEnumerableProp: withNonEnumerableProp, + withDefaultExport: withDefaultExport, + withOnlyDefaultExport: withOnlyDefaultExport, + exportsEquals: exportsEquals, + exportsEqualsWithDefault: exportsEqualsWithDefault, + }; + + const testCases = makeTestCases(expectedOutputs); + + it.each(testCases)('%s', (_, requireResult, expectedOutput) => { + expect(_interopDefault(requireResult)).toEqual(_interopDefaultOrig(requireResult)); + expect(_interopDefault(requireResult)).toEqual(expectedOutput); + }); + }); +}); + +describe('_interopRequireDefault', () => { + describe('returns the same result as the original', () => { + const expectedOutputs: Record = { + withESModuleFlag: withESModuleFlag, + withESModuleFlagAndDefault: withESModuleFlagAndDefault, + namedExports: { default: namedExports }, + withNonEnumerableProp: { default: withNonEnumerableProp }, + withDefaultExport: { default: withDefaultExport }, + withOnlyDefaultExport: { default: withOnlyDefaultExport }, + exportsEquals: { default: exportsEquals }, + exportsEqualsWithDefault: { default: exportsEqualsWithDefault }, + }; + + const testCases = makeTestCases(expectedOutputs); + + it.each(testCases)('%s', (_, requireResult, expectedOutput) => { + expect(_interopRequireDefault(requireResult)).toEqual(_interopRequireDefaultOrig(requireResult)); + expect(_interopRequireDefault(requireResult)).toEqual(expectedOutput); + }); + }); +}); diff --git a/packages/utils/test/buildPolyfills/nullishCoalesce.test.ts b/packages/utils/test/buildPolyfills/nullishCoalesce.test.ts new file mode 100644 index 000000000000..55fd5ee9c996 --- /dev/null +++ b/packages/utils/test/buildPolyfills/nullishCoalesce.test.ts @@ -0,0 +1,29 @@ +import { _nullishCoalesce } from '../../src/buildPolyfills'; +import { Value } from '../../src/buildPolyfills/types'; +import { _nullishCoalesce as _nullishCoalesceOrig } from './originals'; + +const dogStr = 'dogs are great!'; +const dogFunc = () => dogStr; +const dogAdjectives = { maisey: 'silly', charlie: 'goofy' }; +const dogAdjectiveFunc = () => dogAdjectives; + +describe('_nullishCoalesce', () => { + describe('returns the same result as the original', () => { + const testCases: Array<[string, Value, () => Value, Value]> = [ + ['null LHS', null, dogFunc, dogStr], + ['undefined LHS', undefined, dogFunc, dogStr], + ['false LHS', false, dogFunc, false], + ['zero LHS', 0, dogFunc, 0], + ['empty string LHS', '', dogFunc, ''], + ['true LHS', true, dogFunc, true], + ['truthy primitive LHS', 12312012, dogFunc, 12312012], + ['truthy object LHS', dogAdjectives, dogFunc, dogAdjectives], + ['truthy function LHS', dogAdjectiveFunc, dogFunc, dogAdjectiveFunc], + ]; + + it.each(testCases)('%s', (_, lhs, rhs, expectedValue) => { + expect(_nullishCoalesce(lhs, rhs)).toEqual(_nullishCoalesceOrig(lhs, rhs)); + expect(_nullishCoalesce(lhs, rhs)).toEqual(expectedValue); + }); + }); +}); diff --git a/packages/utils/test/buildPolyfills/optionalChain.test.ts b/packages/utils/test/buildPolyfills/optionalChain.test.ts new file mode 100644 index 000000000000..4fec5ee1dc63 --- /dev/null +++ b/packages/utils/test/buildPolyfills/optionalChain.test.ts @@ -0,0 +1,87 @@ +import { default as arrayFlat } from 'array.prototype.flat'; + +import { _optionalChain } from '../../src/buildPolyfills'; +import { GenericFunction, GenericObject, Value } from '../../src/buildPolyfills/types'; +import { _optionalChain as _optionalChainOrig } from './originals'; + +// Older versions of Node don't have `Array.prototype.flat`, which crashes these tests. On newer versions that do have +// it, this is a no-op. +arrayFlat.shim(); + +type OperationType = 'access' | 'call' | 'optionalAccess' | 'optionalCall'; +type OperationExecutor = + | ((intermediateValue: GenericObject) => Value) + | ((intermediateValue: GenericFunction) => Value); +type Operation = [OperationType, OperationExecutor]; + +const truthyObject = { maisey: 'silly', charlie: 'goofy' }; +const nullishObject = null; +const truthyFunc = (): GenericObject => truthyObject; +const nullishFunc = undefined; +const truthyReturn = (): GenericObject => truthyObject; +const nullishReturn = (): null => nullishObject; + +// The polyfill being tested here works under the assumption that the original code containing the optional chain has +// been transformed into an array of values, labels, and functions. For example, `truthyObject?.charlie` will have been +// transformed into `_optionalChain([truthyObject, 'optionalAccess', _ => _.charlie])`. We are not testing the +// transformation here, only what the polyfill does with the already-transformed inputs. + +describe('_optionalChain', () => { + describe('returns the same result as the original', () => { + // In these test cases, the array passed to `_optionalChain` has been broken up into the first entry followed by an + // array of pairs of subsequent elements, because this seemed the easiest way to express the type, which is really + // + // [Value, OperationType, Value => Value, OperationType, Value => Value, OperationType, Value => Value, ...]. + // + // (In other words, `[A, B, C, D, E]` has become `A, [[B, C], [D, E]]`, and these are then the second and third + // entries in each test case.) We then undo this wrapping before passing the data to our functions. + const testCases: Array<[string, Value, Operation[], Value]> = [ + ['truthyObject?.charlie', truthyObject, [['optionalAccess', (_: GenericObject) => _.charlie]], 'goofy'], + ['nullishObject?.maisey', nullishObject, [['optionalAccess', (_: GenericObject) => _.maisey]], undefined], + [ + 'truthyFunc?.().maisey', + truthyFunc, + [ + ['optionalCall', (_: GenericFunction) => _()], + ['access', (_: GenericObject) => _.maisey], + ], + 'silly', + ], + [ + 'nullishFunc?.().charlie', + nullishFunc, + [ + ['optionalCall', (_: GenericFunction) => _()], + ['access', (_: GenericObject) => _.charlie], + ], + undefined, + ], + [ + 'truthyReturn()?.maisey', + truthyReturn, + [ + ['call', (_: GenericFunction) => _()], + ['optionalAccess', (_: GenericObject) => _.maisey], + ], + 'silly', + ], + [ + 'nullishReturn()?.charlie', + nullishReturn, + [ + ['call', (_: GenericFunction) => _()], + ['optionalAccess', (_: GenericObject) => _.charlie], + ], + undefined, + ], + ]; + + it.each(testCases)('%s', (_, initialChainComponent, operations, expectedValue) => { + // `operations` is flattened and spread in order to undo the wrapping done in the test cases for TS purposes. + expect(_optionalChain([initialChainComponent, ...operations.flat()])).toEqual( + _optionalChainOrig([initialChainComponent, ...operations.flat()]), + ); + expect(_optionalChain([initialChainComponent, ...operations.flat()])).toEqual(expectedValue); + }); + }); +}); diff --git a/packages/utils/test/buildPolyfills/originals.d.ts b/packages/utils/test/buildPolyfills/originals.d.ts new file mode 100644 index 000000000000..323d6f26e93c --- /dev/null +++ b/packages/utils/test/buildPolyfills/originals.d.ts @@ -0,0 +1,23 @@ +// NOTE: Unlike other types files, this is NOT auto-generated by our build scripts. Since these functions are the +// originals of functions we adapted from Rollup and Sucrase, there's no reason they should ever change, but if they do, +// this file needs to be regenerated, by running +// `yarn tsc --allowJs --skipLibCheck --declaration --emitDeclarationOnly test/buildPolyfills/originals.js` +// from within the `utils` package. Keep in mind that running that command will clobber this note, so make sure to copy +// it before you regenerate the types, so you can add it back in.) + +export function _asyncNullishCoalesce(lhs: any, rhsFn: any): Promise; +export function _asyncOptionalChain(ops: any): Promise; +export function _asyncOptionalChainDelete(ops: any): Promise; +export function _createNamedExportFrom(obj: any, localName: any, importedName: any): void; +export function _createStarExport(obj: any): void; +export function _interopDefault(e: any): any; +export function _interopNamespace(e: any): any; +export function _interopNamespaceDefaultOnly(e: any): { + __proto__: any; + default: any; +}; +export function _interopRequireDefault(obj: any): any; +export function _interopRequireWildcard(obj: any): any; +export function _nullishCoalesce(lhs: any, rhsFn: any): any; +export function _optionalChain(ops: any): any; +export function _optionalChainDelete(ops: any): any; diff --git a/packages/utils/test/buildPolyfills/originals.js b/packages/utils/test/buildPolyfills/originals.js new file mode 100644 index 000000000000..d3dcb22e8082 --- /dev/null +++ b/packages/utils/test/buildPolyfills/originals.js @@ -0,0 +1,147 @@ +// Originals of the buildPolyfills from Sucrase and Rollup we use (which we have adapted in various ways), preserved here for testing, to prove that +// the modified versions do the same thing the originals do. + +// From Sucrase +export async function _asyncNullishCoalesce(lhs, rhsFn) { + if (lhs != null) { + return lhs; + } else { + return await rhsFn(); + } +} + +// From Sucrase +export async function _asyncOptionalChain(ops) { + let lastAccessLHS = undefined; + let value = ops[0]; + let i = 1; + while (i < ops.length) { + const op = ops[i]; + const fn = ops[i + 1]; + i += 2; + if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { + return undefined; + } + if (op === 'access' || op === 'optionalAccess') { + lastAccessLHS = value; + value = await fn(value); + } else if (op === 'call' || op === 'optionalCall') { + value = await fn((...args) => value.call(lastAccessLHS, ...args)); + lastAccessLHS = undefined; + } + } + return value; +} + +// From Sucrase +export async function _asyncOptionalChainDelete(ops) { + const result = await _asyncOptionalChain(ops); + // by checking for loose equality to `null`, we catch both `null` and `undefined` + return result == null ? true : result; +} + +// From Sucrase +export function _createNamedExportFrom(obj, localName, importedName) { + Object.defineProperty(exports, localName, { enumerable: true, get: () => obj[importedName] }); +} + +// From Sucrase +export function _createStarExport(obj) { + Object.keys(obj) + .filter(key => key !== 'default' && key !== '__esModule') + .forEach(key => { + // eslint-disable-next-line no-prototype-builtins + if (exports.hasOwnProperty(key)) { + return; + } + Object.defineProperty(exports, key, { enumerable: true, get: () => obj[key] }); + }); +} + +// From Rollup +export function _interopDefault(e) { + return e && e.__esModule ? e['default'] : e; +} + +// From Rollup +export function _interopNamespace(e) { + if (e && e.__esModule) return e; + var n = Object.create(null); + if (e) { + // eslint-disable-next-line guard-for-in + for (var k in e) { + n[k] = e[k]; + } + } + n['default'] = e; + return n; +} + +export function _interopNamespaceDefaultOnly(e) { + return { + __proto__: null, + default: e, + }; +} + +// From Sucrase +export function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { default: obj }; +} + +// From Sucrase +export function _interopRequireWildcard(obj) { + if (obj && obj.__esModule) { + return obj; + } else { + var newObj = {}; + if (obj != null) { + for (var key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + newObj[key] = obj[key]; + } + } + } + newObj.default = obj; + return newObj; + } +} + +// From Sucrase +export function _nullishCoalesce(lhs, rhsFn) { + if (lhs != null) { + return lhs; + } else { + return rhsFn(); + } +} + +// From Sucrase +export function _optionalChain(ops) { + let lastAccessLHS = undefined; + let value = ops[0]; + let i = 1; + while (i < ops.length) { + const op = ops[i]; + const fn = ops[i + 1]; + i += 2; + if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { + return undefined; + } + if (op === 'access' || op === 'optionalAccess') { + lastAccessLHS = value; + value = fn(value); + } else if (op === 'call' || op === 'optionalCall') { + value = fn((...args) => value.call(lastAccessLHS, ...args)); + lastAccessLHS = undefined; + } + } + return value; +} + +// From Sucrase +export function _optionalChainDelete(ops) { + const result = _optionalChain(ops); + // by checking for loose equality to `null`, we catch both `null` and `undefined` + return result == null ? true : result; +} diff --git a/packages/utils/test/clientreport.test.ts b/packages/utils/test/clientreport.test.ts index 8d98291c3a14..442df49c6edf 100644 --- a/packages/utils/test/clientreport.test.ts +++ b/packages/utils/test/clientreport.test.ts @@ -1,12 +1,14 @@ import { ClientReport } from '@sentry/types'; +import { TextEncoder } from 'util'; import { createClientReportEnvelope } from '../src/clientreport'; import { serializeEnvelope } from '../src/envelope'; +import { parseEnvelope } from './testutils'; -const DEFAULT_DISCARDED_EVENTS: Array = [ +const DEFAULT_DISCARDED_EVENTS: ClientReport['discarded_events'] = [ { reason: 'before_send', - category: 'event', + category: 'error', quantity: 30, }, { @@ -41,11 +43,21 @@ describe('createClientReportEnvelope', () => { it('serializes an envelope', () => { const env = createClientReportEnvelope(DEFAULT_DISCARDED_EVENTS, MOCK_DSN, 123456); - const serializedEnv = serializeEnvelope(env); - expect(serializedEnv).toMatchInlineSnapshot(` - "{\\"dsn\\":\\"https://public@example.com/1\\"} - {\\"type\\":\\"client_report\\"} - {\\"timestamp\\":123456,\\"discarded_events\\":[{\\"reason\\":\\"before_send\\",\\"category\\":\\"event\\",\\"quantity\\":30},{\\"reason\\":\\"network_error\\",\\"category\\":\\"transaction\\",\\"quantity\\":23}]}" - `); + + const [headers, items] = parseEnvelope(serializeEnvelope(env, new TextEncoder())); + + expect(headers).toEqual({ dsn: 'https://public@example.com/1' }); + expect(items).toEqual([ + [ + { type: 'client_report' }, + { + timestamp: 123456, + discarded_events: [ + { reason: 'before_send', category: 'error', quantity: 30 }, + { reason: 'network_error', category: 'transaction', quantity: 23 }, + ], + }, + ], + ]); }); }); diff --git a/packages/utils/test/envelope.test.ts b/packages/utils/test/envelope.test.ts index dab2b92d5f47..af63b5bfb9bf 100644 --- a/packages/utils/test/envelope.test.ts +++ b/packages/utils/test/envelope.test.ts @@ -1,6 +1,7 @@ import { EventEnvelope } from '@sentry/types'; +import { TextEncoder } from 'util'; -import { addItemToEnvelope, createEnvelope, getEnvelopeType, serializeEnvelope } from '../src/envelope'; +import { addItemToEnvelope, createEnvelope, forEachEnvelopeItem, serializeEnvelope } from '../src/envelope'; import { parseEnvelope } from './testutils'; describe('envelope', () => { @@ -20,38 +21,85 @@ describe('envelope', () => { describe('serializeEnvelope()', () => { it('serializes an envelope', () => { const env = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, []); - expect(serializeEnvelope(env)).toMatchInlineSnapshot( - '"{\\"event_id\\":\\"aa3ff046696b4bc6b609ce6d28fde9e2\\",\\"sent_at\\":\\"123\\"}"', + const serializedEnvelope = serializeEnvelope(env, new TextEncoder()); + expect(typeof serializedEnvelope).toBe('string'); + + const [headers] = parseEnvelope(serializedEnvelope); + expect(headers).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }); + }); + + it('serializes an envelope with attachments', () => { + const items: EventEnvelope[1] = [ + [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }], + [{ type: 'attachment', filename: 'bar.txt', length: 6 }, Uint8Array.from([1, 2, 3, 4, 5, 6])], + [{ type: 'attachment', filename: 'foo.txt', length: 6 }, Uint8Array.from([7, 8, 9, 10, 11, 12])], + ]; + + const env = createEnvelope( + { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, + items, ); + + expect.assertions(6); + + const serializedEnvelope = serializeEnvelope(env, new TextEncoder()); + expect(serializedEnvelope).toBeInstanceOf(Uint8Array); + + const [parsedHeaders, parsedItems] = parseEnvelope(serializedEnvelope); + expect(parsedHeaders).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }); + expect(parsedItems).toHaveLength(3); + expect(items[0]).toEqual([{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }]); + expect(items[1]).toEqual([ + { type: 'attachment', filename: 'bar.txt', length: 6 }, + Uint8Array.from([1, 2, 3, 4, 5, 6]), + ]); + expect(items[2]).toEqual([ + { type: 'attachment', filename: 'foo.txt', length: 6 }, + Uint8Array.from([7, 8, 9, 10, 11, 12]), + ]); }); }); describe('addItemToEnvelope()', () => { it('adds an item to an envelope', () => { const env = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, []); - const parsedEnvelope = parseEnvelope(serializeEnvelope(env)); - expect(parsedEnvelope).toHaveLength(1); - expect(parsedEnvelope[0]).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }); + let [envHeaders, items] = parseEnvelope(serializeEnvelope(env, new TextEncoder())); + expect(items).toHaveLength(0); + expect(envHeaders).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }); const newEnv = addItemToEnvelope(env, [ { type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }, ]); - const parsedNewEnvelope = parseEnvelope(serializeEnvelope(newEnv)); - expect(parsedNewEnvelope).toHaveLength(3); - expect(parsedNewEnvelope[0]).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }); - expect(parsedNewEnvelope[1]).toEqual({ type: 'event' }); - expect(parsedNewEnvelope[2]).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }); + + [envHeaders, items] = parseEnvelope(serializeEnvelope(newEnv, new TextEncoder())); + expect(envHeaders).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }); + expect(items).toHaveLength(1); + expect(items[0]).toEqual([{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }]); }); }); - describe('getEnvelopeType', () => { - it('returns the type of the envelope', () => { - const env = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ + describe('forEachEnvelopeItem', () => { + it('loops through an envelope', () => { + const items: EventEnvelope[1] = [ [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }], - [{ type: 'attachment', filename: 'me.txt' }, '123456'], - ]); - expect(getEnvelopeType(env)).toEqual('event'); + [{ type: 'attachment', filename: 'bar.txt', length: 6 }, Uint8Array.from([1, 2, 3, 4, 5, 6])], + [{ type: 'attachment', filename: 'foo.txt', length: 6 }, Uint8Array.from([7, 8, 9, 10, 11, 12])], + ]; + + const env = createEnvelope( + { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, + items, + ); + + expect.assertions(6); + + let iteration = 0; + forEachEnvelopeItem(env, (item, type) => { + expect(item).toBe(items[iteration]); + expect(type).toBe(items[iteration][0].type); + iteration = iteration + 1; + }); }); }); }); diff --git a/packages/utils/test/normalize.test.ts b/packages/utils/test/normalize.test.ts index a9702d133897..e5d01de4e962 100644 --- a/packages/utils/test/normalize.test.ts +++ b/packages/utils/test/normalize.test.ts @@ -4,8 +4,8 @@ import * as isModule from '../src/is'; import { normalize } from '../src/normalize'; +import { addNonEnumerableProperty } from '../src/object'; import * as stacktraceModule from '../src/stacktrace'; -import { testOnlyIfNodeVersionAtLeast } from './testutils'; describe('normalize()', () => { describe('acts as a pass-through for simple-cases', () => { @@ -48,7 +48,7 @@ describe('normalize()', () => { }); }); - testOnlyIfNodeVersionAtLeast(8)('extracts data from `Event` objects', () => { + describe('extracts data from `Event` objects', () => { const isElement = jest.spyOn(isModule, 'isElement').mockReturnValue(true); const getAttribute = () => undefined; @@ -505,4 +505,52 @@ describe('normalize()', () => { qux: '[Function: qux]', }); }); + + describe('skips normalizing objects marked with a non-enumerable property __sentry_skip_normalization__', () => { + test('by leaving non-serializable values intact', () => { + const someFun = () => undefined; + const alreadyNormalizedObj = { + nan: NaN, + fun: someFun, + }; + + addNonEnumerableProperty(alreadyNormalizedObj, '__sentry_skip_normalization__', true); + + const result = normalize(alreadyNormalizedObj); + expect(result).toEqual({ + nan: NaN, + fun: someFun, + }); + }); + + test('by ignoring normalization depth', () => { + const alreadyNormalizedObj = { + three: { + more: { + layers: '!', + }, + }, + }; + + addNonEnumerableProperty(alreadyNormalizedObj, '__sentry_skip_normalization__', true); + + const obj = { + foo: { + bar: { + baz: alreadyNormalizedObj, + boo: { + bam: { + pow: 'poof', + }, + }, + }, + }, + }; + + const result = normalize(obj, 4); + + expect(result?.foo?.bar?.baz?.three?.more?.layers).toBe('!'); + expect(result?.foo?.bar?.boo?.bam?.pow).not.toBe('poof'); + }); + }); }); diff --git a/packages/utils/test/object.test.ts b/packages/utils/test/object.test.ts index e4fc4428ae04..65131fa0cd94 100644 --- a/packages/utils/test/object.test.ts +++ b/packages/utils/test/object.test.ts @@ -199,6 +199,47 @@ describe('dropUndefinedKeys()', () => { }, }); }); + + test('objects with circular reference', () => { + const dog: any = { + food: undefined, + }; + + const human = { + brain: undefined, + pets: dog, + }; + + const rat = { + scares: human, + weight: '4kg', + }; + + dog.chases = rat; + + expect(dropUndefinedKeys(human)).toStrictEqual({ + pets: { + chases: rat, + }, + }); + }); + + test('arrays with circular reference', () => { + const egg: any[] = []; + + const chicken = { + food: undefined, + weight: '1kg', + lays: egg, + }; + + egg[0] = chicken; + + expect(dropUndefinedKeys(chicken)).toStrictEqual({ + lays: egg, + weight: '1kg', + }); + }); }); describe('objectify()', () => { diff --git a/packages/utils/test/ratelimit.test.ts b/packages/utils/test/ratelimit.test.ts index fec1b3413ae7..52a04683563f 100644 --- a/packages/utils/test/ratelimit.test.ts +++ b/packages/utils/test/ratelimit.test.ts @@ -70,27 +70,30 @@ describe('updateRateLimits()', () => { test('should update the `all` category based on `retry-after` header ', () => { const rateLimits: RateLimits = {}; const headers = { + 'x-sentry-rate-limits': null, 'retry-after': '42', }; - const updatedRateLimits = updateRateLimits(rateLimits, headers, 0); + const updatedRateLimits = updateRateLimits(rateLimits, { headers }, 0); expect(updatedRateLimits.all).toEqual(42 * 1000); }); test('should update a single category based on `x-sentry-rate-limits` header', () => { const rateLimits: RateLimits = {}; const headers = { + 'retry-after': null, 'x-sentry-rate-limits': '13:error', }; - const updatedRateLimits = updateRateLimits(rateLimits, headers, 0); + const updatedRateLimits = updateRateLimits(rateLimits, { headers }, 0); expect(updatedRateLimits.error).toEqual(13 * 1000); }); test('should update multiple categories based on `x-sentry-rate-limits` header', () => { const rateLimits: RateLimits = {}; const headers = { + 'retry-after': null, 'x-sentry-rate-limits': '13:error;transaction', }; - const updatedRateLimits = updateRateLimits(rateLimits, headers, 0); + const updatedRateLimits = updateRateLimits(rateLimits, { headers }, 0); expect(updatedRateLimits.error).toEqual(13 * 1000); expect(updatedRateLimits.transaction).toEqual(13 * 1000); }); @@ -98,9 +101,10 @@ describe('updateRateLimits()', () => { test('should update multiple categories with different values based on multi `x-sentry-rate-limits` header', () => { const rateLimits: RateLimits = {}; const headers = { + 'retry-after': null, 'x-sentry-rate-limits': '13:error,15:transaction', }; - const updatedRateLimits = updateRateLimits(rateLimits, headers, 0); + const updatedRateLimits = updateRateLimits(rateLimits, { headers }, 0); expect(updatedRateLimits.error).toEqual(13 * 1000); expect(updatedRateLimits.transaction).toEqual(15 * 1000); }); @@ -108,9 +112,10 @@ describe('updateRateLimits()', () => { test('should use last entry from multi `x-sentry-rate-limits` header for a given category', () => { const rateLimits: RateLimits = {}; const headers = { + 'retry-after': null, 'x-sentry-rate-limits': '13:error,15:transaction;error', }; - const updatedRateLimits = updateRateLimits(rateLimits, headers, 0); + const updatedRateLimits = updateRateLimits(rateLimits, { headers }, 0); expect(updatedRateLimits.error).toEqual(15 * 1000); expect(updatedRateLimits.transaction).toEqual(15 * 1000); }); @@ -118,18 +123,20 @@ describe('updateRateLimits()', () => { test('should fallback to `all` if `x-sentry-rate-limits` header is missing a category', () => { const rateLimits: RateLimits = {}; const headers = { + 'retry-after': null, 'x-sentry-rate-limits': '13', }; - const updatedRateLimits = updateRateLimits(rateLimits, headers, 0); + const updatedRateLimits = updateRateLimits(rateLimits, { headers }, 0); expect(updatedRateLimits.all).toEqual(13 * 1000); }); test('should use 60s default if delay in `x-sentry-rate-limits` header is malformed', () => { const rateLimits: RateLimits = {}; const headers = { + 'retry-after': null, 'x-sentry-rate-limits': 'x', }; - const updatedRateLimits = updateRateLimits(rateLimits, headers, 0); + const updatedRateLimits = updateRateLimits(rateLimits, { headers }, 0); expect(updatedRateLimits.all).toEqual(60 * 1000); }); @@ -138,9 +145,10 @@ describe('updateRateLimits()', () => { error: 1337, }; const headers = { + 'retry-after': null, 'x-sentry-rate-limits': '13:transaction', }; - const updatedRateLimits = updateRateLimits(rateLimits, headers, 0); + const updatedRateLimits = updateRateLimits(rateLimits, { headers }, 0); expect(updatedRateLimits.error).toEqual(1337); expect(updatedRateLimits.transaction).toEqual(13 * 1000); }); @@ -151,8 +159,31 @@ describe('updateRateLimits()', () => { 'retry-after': '42', 'x-sentry-rate-limits': '13:error', }; - const updatedRateLimits = updateRateLimits(rateLimits, headers, 0); + const updatedRateLimits = updateRateLimits(rateLimits, { headers }, 0); expect(updatedRateLimits.error).toEqual(13 * 1000); expect(updatedRateLimits.all).toBeUndefined(); }); + + test('should apply a global rate limit of 60s when no headers are provided on a 429 status code', () => { + const rateLimits: RateLimits = {}; + const updatedRateLimits = updateRateLimits(rateLimits, { statusCode: 429 }, 0); + expect(updatedRateLimits.all).toBe(60_000); + }); + + test('should not apply a global rate limit specific headers are provided on a 429 status code', () => { + const rateLimits: RateLimits = {}; + const headers = { + 'retry-after': null, + 'x-sentry-rate-limits': '13:error', + }; + const updatedRateLimits = updateRateLimits(rateLimits, { statusCode: 429, headers }, 0); + expect(updatedRateLimits.error).toEqual(13 * 1000); + expect(updatedRateLimits.all).toBeUndefined(); + }); + + test('should not apply a default rate limit on a non-429 status code', () => { + const rateLimits: RateLimits = {}; + const updatedRateLimits = updateRateLimits(rateLimits, { statusCode: 200 }, 0); + expect(updatedRateLimits).toEqual(rateLimits); + }); }); diff --git a/packages/utils/test/severity.test.ts b/packages/utils/test/severity.test.ts index 7b41c92a2082..51f66e815288 100644 --- a/packages/utils/test/severity.test.ts +++ b/packages/utils/test/severity.test.ts @@ -1,23 +1,17 @@ -import { SeverityLevels } from '../src/enums'; -import { severityFromString } from '../src/severity'; +import { severityLevelFromString, validSeverityLevels } from '../src/severity'; -describe('severityFromString()', () => { - describe('normalize warn and warning', () => { - test('handles warn and warning', () => { - expect(severityFromString('warn')).toBe('warning'); - expect(severityFromString('warning')).toBe('warning'); - }); - test('handles warn and warning', () => { - expect(severityFromString('warn')).toBe('warning'); - expect(severityFromString('warning')).toBe('warning'); - }); +describe('severityLevelFromString()', () => { + test("converts 'warn' to 'warning'", () => { + expect(severityLevelFromString('warn')).toBe('warning'); }); - describe('default to log', () => { - expect(severityFromString('foo')).toBe('log'); + + test('defaults to log', () => { + expect(severityLevelFromString('foo')).toBe('log'); }); - describe('allows ', () => { - for (const level of SeverityLevels) { - expect(severityFromString(level)).toBe(level); + + test('acts as a pass-through for valid level strings', () => { + for (const level of validSeverityLevels) { + expect(severityLevelFromString(level)).toBe(level); } }); }); diff --git a/packages/utils/test/syncpromise.test.ts b/packages/utils/test/syncpromise.test.ts index 6164c666d967..cdb0a7c6e0e7 100644 --- a/packages/utils/test/syncpromise.test.ts +++ b/packages/utils/test/syncpromise.test.ts @@ -1,7 +1,7 @@ import { rejectedSyncPromise, resolvedSyncPromise, SyncPromise } from '../src/syncpromise'; describe('SyncPromise', () => { - test('simple', () => { + test('simple', async () => { expect.assertions(1); return new SyncPromise(resolve => { @@ -11,7 +11,7 @@ describe('SyncPromise', () => { }); }); - test('simple chaining', () => { + test('simple chaining', async () => { expect.assertions(1); return new SyncPromise(resolve => { @@ -94,7 +94,7 @@ describe('SyncPromise', () => { ); }); - test('simple static', () => { + test('simple static', async () => { expect.assertions(1); const p = resolvedSyncPromise(10); @@ -103,7 +103,7 @@ describe('SyncPromise', () => { }); }); - test('using new Promise internally', () => { + test('using new Promise internally', async () => { expect.assertions(2); return new SyncPromise(done => { @@ -120,7 +120,7 @@ describe('SyncPromise', () => { }); }); - test('with setTimeout', () => { + test('with setTimeout', async () => { jest.useFakeTimers(); expect.assertions(1); @@ -175,7 +175,7 @@ describe('SyncPromise', () => { expect(qp).toHaveProperty('_value'); }); - test('multiple then returning undefined', () => { + test('multiple then returning undefined', async () => { expect.assertions(3); return new SyncPromise(resolve => { @@ -192,7 +192,7 @@ describe('SyncPromise', () => { }); }); - test('multiple then returning different values', () => { + test('multiple then returning different values', async () => { expect.assertions(3); return new SyncPromise(resolve => { @@ -211,7 +211,7 @@ describe('SyncPromise', () => { }); }); - test('multiple then returning different SyncPromise', () => { + test('multiple then returning different SyncPromise', async () => { expect.assertions(2); return new SyncPromise(resolve => { @@ -228,7 +228,7 @@ describe('SyncPromise', () => { }); }); - test('reject immediatly and do not call then', () => { + test('reject immediatly and do not call then', async () => { expect.assertions(1); return new SyncPromise((_, reject) => { @@ -242,7 +242,7 @@ describe('SyncPromise', () => { }); }); - test('reject', () => { + test('reject', async () => { expect.assertions(1); return new SyncPromise((_, reject) => { @@ -252,7 +252,7 @@ describe('SyncPromise', () => { }); }); - test('rejecting after first then', () => { + test('rejecting after first then', async () => { expect.assertions(2); return new SyncPromise(resolve => { diff --git a/packages/utils/test/testutils.ts b/packages/utils/test/testutils.ts index aa3c5485eec1..b708d77064eb 100644 --- a/packages/utils/test/testutils.ts +++ b/packages/utils/test/testutils.ts @@ -1,3 +1,6 @@ +import { BaseEnvelopeHeaders, BaseEnvelopeItemHeaders, Envelope } from '@sentry/types'; +import { TextDecoder, TextEncoder } from 'util'; + export const testOnlyIfNodeVersionAtLeast = (minVersion: number): jest.It => { const currentNodeVersion = process.env.NODE_VERSION; @@ -12,6 +15,55 @@ export const testOnlyIfNodeVersionAtLeast = (minVersion: number): jest.It => { return it; }; -export function parseEnvelope(env: string): Array> { - return env.split('\n').map(e => JSON.parse(e)); +/** + * A naive binary envelope parser + */ +export function parseEnvelope(env: string | Uint8Array): Envelope { + let buf = typeof env === 'string' ? new TextEncoder().encode(env) : env; + + let envelopeHeaders: BaseEnvelopeHeaders | undefined; + let lastItemHeader: BaseEnvelopeItemHeaders | undefined; + const items: [any, any][] = []; + + let binaryLength = 0; + while (buf.length) { + // Next length is either the binary length from the previous header + // or the next newline character + let i = binaryLength || buf.indexOf(0xa); + + // If no newline was found, assume this is the last block + if (i < 0) { + i = buf.length; + } + + // If we read out a length in the previous header, assume binary + if (binaryLength > 0) { + const bin = buf.slice(0, binaryLength); + binaryLength = 0; + items.push([lastItemHeader, bin]); + } else { + const json = JSON.parse(new TextDecoder().decode(buf.slice(0, i + 1))); + + if (typeof json.length === 'number') { + binaryLength = json.length; + } + + // First json is always the envelope headers + if (!envelopeHeaders) { + envelopeHeaders = json; + } else { + // If there is a type property, assume this is an item header + if ('type' in json) { + lastItemHeader = json; + } else { + items.push([lastItemHeader, json]); + } + } + } + + // Replace the buffer with the previous block and newline removed + buf = buf.slice(i + 1); + } + + return [envelopeHeaders as BaseEnvelopeHeaders, items]; } diff --git a/packages/utils/test/types/index.js b/packages/utils/test/types/index.js index a8f9e9360b68..7b6936691197 100644 --- a/packages/utils/test/types/index.js +++ b/packages/utils/test/types/index.js @@ -3,7 +3,7 @@ const path = require('path'); const testStrings = ['/// ']; -const paths = [path.join('./build/dist'), path.join('./build/esm')]; +const paths = [path.join('./build/cjs'), path.join('./build/esm')]; paths.forEach(dir => { if (!fs.existsSync(dir)) { diff --git a/packages/utils/tsconfig.cjs.json b/packages/utils/tsconfig.cjs.json deleted file mode 100644 index e3a918fc70af..000000000000 --- a/packages/utils/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "build/dist" - } -} diff --git a/packages/utils/tsconfig.esm.json b/packages/utils/tsconfig.esm.json deleted file mode 100644 index 0b86c52918cc..000000000000 --- a/packages/utils/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "build/esm" - } -} diff --git a/packages/utils/tsconfig.test.json b/packages/utils/tsconfig.test.json index 87f6afa06b86..0f755903fa64 100644 --- a/packages/utils/tsconfig.test.json +++ b/packages/utils/tsconfig.test.json @@ -5,8 +5,19 @@ "compilerOptions": { // should include all types from `./tsconfig.json` plus types for all test frameworks used - "types": ["node", "jest"] + "types": ["node", "jest"], // other package-specific, test-specific options + // this is necessary in order to be able to handle the buildPolyfills `originals.js` which is used for testing + "allowJs": true, + + // `es2020` is the recommended `lib` and `target` for Node 14 + // see https://github.com/tsconfig/bases/blob/main/bases/node14.json + "lib": ["dom", "es2020"], + "module": "commonjs", + "target": "es2020", + + // so we don't have to worry about how libraries we use export things + "esModuleInterop": true } } diff --git a/packages/vue/README.md b/packages/vue/README.md index 6c2d5072b1a5..c8fd91af2b24 100644 --- a/packages/vue/README.md +++ b/packages/vue/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Official Sentry SDK for Vue.js diff --git a/packages/vue/jest.config.js b/packages/vue/jest.config.js new file mode 100644 index 000000000000..cd02790794a7 --- /dev/null +++ b/packages/vue/jest.config.js @@ -0,0 +1,6 @@ +const baseConfig = require('../../jest/jest.config.js'); + +module.exports = { + ...baseConfig, + testEnvironment: 'jsdom', +}; diff --git a/packages/vue/package.json b/packages/vue/package.json index f14ae53120cd..dc518d7a67a4 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,56 +1,47 @@ { "name": "@sentry/vue", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Official Sentry SDK for Vue.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vue", "author": "Sentry", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=8" }, - "main": "build/dist/index.js", + "main": "build/cjs/index.js", "module": "build/esm/index.js", "types": "build/types/index.d.ts", "publishConfig": { "access": "public" }, "dependencies": { - "@sentry/browser": "6.19.7", - "@sentry/core": "6.19.7", - "@sentry/minimal": "6.19.7", - "@sentry/types": "6.19.7", - "@sentry/utils": "6.19.7", + "@sentry/browser": "7.0.0-rc.0", + "@sentry/core": "7.0.0-rc.0", + "@sentry/types": "7.0.0-rc.0", + "@sentry/utils": "7.0.0-rc.0", "tslib": "^1.9.3" }, "peerDependencies": { "vue": "2.x || 3.x" }, - "devDependencies": { - "jsdom": "^16.2.2" - }, "scripts": { - "build": "run-p build:cjs build:esm build:types", - "build:bundle": "rollup --config", - "build:cjs": "tsc -p tsconfig.cjs.json", - "build:dev": "run-p build:cjs build:esm build:types", - "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "tsc -p tsconfig.esm.json", + "build": "run-p build:rollup build:types", + "build:bundle": "yarn ts-node ../../scripts/ensure-bundle-deps.ts && yarn rollup --config rollup.bundle.config.js", + "build:dev": "run-p build:rollup build:types", + "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", + "build:watch": "run-p build:rollup:watch build:types:watch", "build:bundle:watch": "rollup --config --watch", - "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", - "build:dev:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", - "build:es5:watch": "yarn build:cjs:watch # *** backwards compatibility - remove in v7 ***", - "build:esm:watch": "tsc -p tsconfig.esm.json --watch", + "build:dev:watch": "run-p build:rollup:watch build:types:watch", + "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf dist esm build coverage", + "clean": "rimraf build coverage sentry-vue-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/*.ts\"", - "link:yarn": "yarn link", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", @@ -60,25 +51,5 @@ "volta": { "extends": "../../package.json" }, - "jest": { - "collectCoverage": true, - "transform": { - "^.+\\.ts$": "ts-jest" - }, - "moduleFileExtensions": [ - "js", - "ts" - ], - "testEnvironment": "jsdom", - "testMatch": [ - "**/*.test.ts" - ], - "globals": { - "ts-jest": { - "tsConfig": "./tsconfig.json", - "diagnostics": false - } - } - }, "sideEffects": false } diff --git a/packages/vue/rollup.bundle.config.js b/packages/vue/rollup.bundle.config.js new file mode 100644 index 000000000000..4df9b0d5b614 --- /dev/null +++ b/packages/vue/rollup.bundle.config.js @@ -0,0 +1,11 @@ +import { makeBaseBundleConfig, makeBundleConfigVariants } from '../../rollup/index.js'; + +const baseBundleConfig = makeBaseBundleConfig({ + bundleType: 'standalone', + entrypoints: ['src/index.bundle.ts'], + jsVersion: 'es6', + licenseTitle: '@sentry/vue', + outputFileBase: () => 'bundle.vue', +}); + +export default makeBundleConfigVariants(baseBundleConfig); diff --git a/packages/vue/rollup.config.js b/packages/vue/rollup.config.js deleted file mode 100644 index 2185b2b716c5..000000000000 --- a/packages/vue/rollup.config.js +++ /dev/null @@ -1,11 +0,0 @@ -import { makeBaseBundleConfig, makeConfigVariants } from '../../rollup.config'; - -const baseBundleConfig = makeBaseBundleConfig({ - input: 'src/index.bundle.ts', - isAddOn: false, - jsVersion: 'es5', - licenseTitle: '@sentry/vue', - outputFileBase: 'bundle.vue', -}); - -export default makeConfigVariants(baseBundleConfig); diff --git a/packages/vue/rollup.npm.config.js b/packages/vue/rollup.npm.config.js new file mode 100644 index 000000000000..5a62b528ef44 --- /dev/null +++ b/packages/vue/rollup.npm.config.js @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/vue/src/index.bundle.ts b/packages/vue/src/index.bundle.ts index 3b1c401a9af9..57059e55e80c 100644 --- a/packages/vue/src/index.bundle.ts +++ b/packages/vue/src/index.bundle.ts @@ -1,22 +1,20 @@ -export { +export type { Breadcrumb, Request, SdkInfo, Event, - EventStatus, Exception, - Response, + SeverityLevel, StackFrame, Stacktrace, Thread, User, } from '@sentry/types'; -export { SeverityLevel } from '@sentry/utils'; +export type { BrowserOptions, ReportDialogOptions } from '@sentry/browser'; export { BrowserClient, - BrowserOptions, defaultIntegrations, forceLoad, lastEventId, @@ -25,7 +23,6 @@ export { flush, close, wrap, - ReportDialogOptions, addGlobalEventProcessor, addBreadcrumb, captureException, @@ -43,9 +40,9 @@ export { setTags, setUser, startTransaction, - Transports, + makeFetchTransport, + makeXHRTransport, withScope, - SDK_NAME, SDK_VERSION, } from '@sentry/browser'; diff --git a/packages/vue/tsconfig.cjs.json b/packages/vue/tsconfig.cjs.json deleted file mode 100644 index e3a918fc70af..000000000000 --- a/packages/vue/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "build/dist" - } -} diff --git a/packages/vue/tsconfig.esm.json b/packages/vue/tsconfig.esm.json deleted file mode 100644 index 0b86c52918cc..000000000000 --- a/packages/vue/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "build/esm" - } -} diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json index e5ec1017893d..bf45a09f2d71 100644 --- a/packages/vue/tsconfig.json +++ b/packages/vue/tsconfig.json @@ -5,6 +5,5 @@ "compilerOptions": { // package-specific options - "esModuleInterop": true, } } diff --git a/packages/wasm/.gitignore b/packages/wasm/.gitignore deleted file mode 100644 index 22f01289c6db..000000000000 --- a/packages/wasm/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -*.js.map -*.d.ts -!test/*.js -!.eslintrc.js diff --git a/packages/wasm/README.md b/packages/wasm/README.md index c1189e294d20..099bcb1e8f20 100644 --- a/packages/wasm/README.md +++ b/packages/wasm/README.md @@ -1,8 +1,7 @@

- - + + Sentry -

# Sentry JavaScript WebAssembly Support diff --git a/packages/wasm/jest.config.js b/packages/wasm/jest.config.js new file mode 100644 index 000000000000..2762fa4ef2ff --- /dev/null +++ b/packages/wasm/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: 'jest-puppeteer', +}; diff --git a/packages/wasm/package.json b/packages/wasm/package.json index c4e7ecd270a3..d92887004264 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -1,23 +1,24 @@ { "name": "@sentry/wasm", - "version": "6.19.7", + "version": "7.0.0-rc.0", "description": "Support for WASM.", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/wasm", "author": "Sentry", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=8" }, - "main": "build/npm/dist/index.js", + "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", "types": "build/npm/types/index.d.ts", "publishConfig": { "access": "public" }, "dependencies": { - "@sentry/browser": "6.19.7", - "@sentry/types": "6.19.7", + "@sentry/browser": "7.0.0-rc.0", + "@sentry/types": "7.0.0-rc.0", + "@sentry/utils": "7.0.0-rc.0", "tslib": "^1.9.3" }, "devDependencies": { @@ -29,38 +30,30 @@ "puppeteer": "^5.5.0" }, "scripts": { - "build": "run-p build:cjs build:esm build:bundle build:types", - "build:bundle": "rollup --config", - "build:cjs": "tsc -p tsconfig.cjs.json", - "build:dev": "run-p build:cjs build:esm build:types", - "build:es5": "yarn build:cjs # *** backwards compatibility - remove in v7 ***", - "build:esm": "tsc -p tsconfig.esm.json", + "build": "run-p build:rollup build:bundle build:types", + "build:bundle": "yarn ts-node ../../scripts/ensure-bundle-deps.ts && yarn rollup --config rollup.bundle.config.js", + "build:dev": "run-p build:rollup build:types", + "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:cjs:watch build:esm:watch build:bundle:watch build:types:watch", - "build:bundle:watch": "rollup --config --watch", - "build:cjs:watch": "tsc -p tsconfig.cjs.json --watch", - "build:dev:watch": "run-p build:cjs:watch build:esm:watch build:types:watch", - "build:es5:watch": "yarn build:cjs:watch # *** backwards compatibility - remove in v7 ***", - "build:esm:watch": "tsc -p tsconfig.esm.json --watch", + "build:watch": "run-p build:rollup:watch build:bundle:watch build:types:watch", + "build:bundle:watch": "rollup --config rollup.bundle.config.js --watch", + "build:dev:watch": "run-p build:rollup:watch build:types:watch", + "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts --bundles --skipBundleCopy && npm pack ./build/npm", + "build:npm": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf dist esm build coverage *.js.map *.d.ts", + "clean": "rimraf build coverage sentry-wasm-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/*.ts\"", - "link:yarn": "yarn link", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", - "test": "node test/scripts/ensure-browser-bundle.js && cross-env PORT=1337 jest", + "test": "node test/scripts/ensure-bundles.js && cross-env PORT=1337 jest", "test:watch": "jest --watch" }, "volta": { "extends": "../../package.json" }, - "jest": { - "preset": "jest-puppeteer" - }, "sideEffects": false } diff --git a/packages/wasm/rollup.bundle.config.js b/packages/wasm/rollup.bundle.config.js new file mode 100644 index 000000000000..2c97176f0dee --- /dev/null +++ b/packages/wasm/rollup.bundle.config.js @@ -0,0 +1,11 @@ +import { makeBaseBundleConfig, makeBundleConfigVariants } from '../../rollup/index.js'; + +const baseBundleConfig = makeBaseBundleConfig({ + bundleType: 'addon', + entrypoints: ['src/index.ts'], + jsVersion: 'es6', + licenseTitle: '@sentry/wasm', + outputFileBase: () => 'bundles/wasm', +}); + +export default makeBundleConfigVariants(baseBundleConfig); diff --git a/packages/wasm/rollup.config.js b/packages/wasm/rollup.config.js deleted file mode 100644 index edccdbd9287a..000000000000 --- a/packages/wasm/rollup.config.js +++ /dev/null @@ -1,11 +0,0 @@ -import { makeBaseBundleConfig, makeConfigVariants } from '../../rollup.config'; - -const baseBundleConfig = makeBaseBundleConfig({ - input: 'src/index.ts', - isAddOn: true, - jsVersion: 'es5', - licenseTitle: '@sentry/wasm', - outputFileBase: 'bundles/wasm', -}); - -export default makeConfigVariants(baseBundleConfig); diff --git a/packages/wasm/rollup.npm.config.js b/packages/wasm/rollup.npm.config.js new file mode 100644 index 000000000000..4ffa8b9396d8 --- /dev/null +++ b/packages/wasm/rollup.npm.config.js @@ -0,0 +1,8 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + // packages with bundles have a different build directory structure + hasBundles: true, + }), +); diff --git a/packages/wasm/src/index.ts b/packages/wasm/src/index.ts index 14252305fa76..cc5e678ee123 100644 --- a/packages/wasm/src/index.ts +++ b/packages/wasm/src/index.ts @@ -58,9 +58,6 @@ export class Wasm implements Integration { } }); } - if (event.stacktrace?.frames) { - haveWasm = haveWasm || patchFrames(event.stacktrace.frames); - } if (haveWasm) { event.debug_meta = event.debug_meta || {}; diff --git a/packages/wasm/test/scripts/ensure-browser-bundle.js b/packages/wasm/test/scripts/ensure-bundles.js similarity index 65% rename from packages/wasm/test/scripts/ensure-browser-bundle.js rename to packages/wasm/test/scripts/ensure-bundles.js index 2261eccaf84a..baa4f1e2a41c 100644 --- a/packages/wasm/test/scripts/ensure-browser-bundle.js +++ b/packages/wasm/test/scripts/ensure-bundles.js @@ -11,4 +11,13 @@ function ensureBrowserBundle() { } } +function ensureWasmBundle() { + if (!fs.existsSync('build/bundles/wasm.js')) { + // eslint-disable-next-line no-console + console.warn('\nWARNING: Missing wasm bundle. Bundle will be created before running wasm integration tests.'); + execSync('yarn build:bundle'); + } +} + ensureBrowserBundle(); +ensureWasmBundle(); diff --git a/packages/wasm/tsconfig.cjs.json b/packages/wasm/tsconfig.cjs.json deleted file mode 100644 index 6782dae5e453..000000000000 --- a/packages/wasm/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "build/npm/dist" - } -} diff --git a/packages/wasm/tsconfig.esm.json b/packages/wasm/tsconfig.esm.json deleted file mode 100644 index feffe52ca581..000000000000 --- a/packages/wasm/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "build/npm/esm" - } -} diff --git a/packages/wasm/tsconfig.json b/packages/wasm/tsconfig.json index e5ec1017893d..bf45a09f2d71 100644 --- a/packages/wasm/tsconfig.json +++ b/packages/wasm/tsconfig.json @@ -5,6 +5,5 @@ "compilerOptions": { // package-specific options - "esModuleInterop": true, } } diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index 041f269f1095..000000000000 --- a/rollup.config.js +++ /dev/null @@ -1,270 +0,0 @@ -/** - * Code for generating config used by individual packages' Rollup configs - */ - -import assert from 'assert'; - -import deepMerge from 'deepmerge'; -import license from 'rollup-plugin-license'; -import resolve from '@rollup/plugin-node-resolve'; -import replace from '@rollup/plugin-replace'; -import { terser } from 'rollup-plugin-terser'; -import typescript from 'rollup-plugin-typescript2'; - -Error.stackTraceLimit = Infinity; - -/** - * Helper functions to compensate for the fact that JS can't handle negative array indices very well - * - * TODO `insertAt` is only exported so the integrations config can inject the `commonjs` plugin, for localforage (used - * in the offline plugin). Once that's fixed to no longer be necessary, this can stop being exported. - */ -const getLastElement = array => { - return array[array.length - 1]; -}; -export const insertAt = (arr, index, ...insertees) => { - const newArr = [...arr]; - // Add 1 to the array length so that the inserted element ends up in the right spot with respect to the length of the - // new array (which will be one element longer), rather than that of the current array - const destinationIndex = index >= 0 ? index : arr.length + 1 + index; - newArr.splice(destinationIndex, 0, ...insertees); - return newArr; -}; - -/** - * Create a plugin to add an identification banner to the top of stand-alone bundles. - * - * @param title The title to use for the SDK, if not the package name - * @returns An instance of the `rollup-plugin-license` plugin - */ -function makeLicensePlugin(title) { - const commitHash = require('child_process').execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim(); - - return license({ - banner: { - content: `/*! <%= data.title %> <%= pkg.version %> (${commitHash}) | https://github.com/getsentry/sentry-javascript */`, - data: { title }, - }, - }); -} - -function makeIsDebugBuildPlugin(includeDebugging) { - return replace({ - // __SENTRY_DEBUG__ should be save to replace in any case, so no checks for assignments necessary - preventAssignment: false, - values: { - __SENTRY_DEBUG__: includeDebugging, - }, - }); -} - -// `terser` options reference: https://github.com/terser/terser#api-reference -// `rollup-plugin-terser` options reference: https://github.com/TrySound/rollup-plugin-terser#options -export const terserPlugin = terser({ - mangle: { - // captureExceptions and captureMessage are public API methods and they don't need to be listed here - // as mangler doesn't touch user-facing thing, however sentryWrapped is not, and it would be mangled into a minified version. - // We need those full names to correctly detect our internal frames for stripping. - // I listed all of them here just for the clarity sake, as they are all used in the frames manipulation process. - reserved: ['captureException', 'captureMessage', 'sentryWrapped'], - properties: { - regex: /^_[^_]/, - reserved: ['_experiments'], - }, - }, - output: { - comments: false, - }, -}); - -export function makeBaseBundleConfig(options) { - const { input, isAddOn, jsVersion, licenseTitle, outputFileBase } = options; - - const baseTSPluginOptions = { - tsconfig: 'tsconfig.esm.json', - tsconfigOverride: { - compilerOptions: { - declaration: false, - declarationMap: false, - paths: { - '@sentry/browser': ['../browser/src'], - '@sentry/core': ['../core/src'], - '@sentry/hub': ['../hub/src'], - '@sentry/minimal': ['../minimal/src'], - '@sentry/types': ['../types/src'], - '@sentry/utils': ['../utils/src'], - }, - baseUrl: '.', - }, - }, - include: ['*.ts+(|x)', '**/*.ts+(|x)', '../**/*.ts+(|x)'], - // the typescript plugin doesn't handle concurrency very well, so clean the cache between builds - // (see https://github.com/ezolenko/rollup-plugin-typescript2/issues/15) - clean: true, - // TODO: For the moment, the above issue seems to have stopped spamming the build with (non-blocking) errors, as it - // was originally. If it starts again, this will suppress that output. If we get to the end of the bundle revamp and - // it still seems okay, we can take this out entirely. - // verbosity: 0, - }; - - const typescriptPluginES5 = typescript( - deepMerge(baseTSPluginOptions, { - tsconfigOverride: { - compilerOptions: { - target: 'es5', - }, - }, - }), - ); - - const typescriptPluginES6 = typescript( - deepMerge(baseTSPluginOptions, { - tsconfigOverride: { - compilerOptions: { - target: 'es6', - }, - }, - }), - ); - - const nodeResolvePlugin = resolve(); - - const markAsBrowserBuildPlugin = replace({ - // don't replace `__placeholder__` where it's followed immediately by a single `=` (to prevent ending up - // with something of the form `let "replacementValue" = "some assigned value"`, which would cause a - // syntax error) - preventAssignment: true, - // the replacement to make - values: { - __SENTRY_BROWSER_BUNDLE__: true, - }, - }); - - const licensePlugin = makeLicensePlugin(licenseTitle); - - // used by `@sentry/browser`, `@sentry/tracing`, and `@sentry/vue` (bundles which are a full SDK in and of themselves) - const standAloneBundleConfig = { - output: { - format: 'iife', - name: 'Sentry', - }, - context: 'window', - }; - - // used by `@sentry/integrations` and `@sentry/wasm` (bundles which need to be combined with a stand-alone SDK bundle) - const addOnBundleConfig = { - // These output settings are designed to mimic an IIFE. We don't use Rollup's `iife` format because we don't want to - // attach this code to a new global variable, but rather inject it into the existing SDK's `Integrations` object. - output: { - format: 'cjs', - - // code to add before the CJS wrapper - banner: '(function (__window) {', - - // code to add just inside the CJS wrapper, before any of the wrapped code - intro: 'var exports = {};', - - // code to add after all of the wrapped code, but still inside the CJS wrapper - outro: () => - [ - '', - " // Add this module's exports to the global `Sentry.Integrations`", - ' __window.Sentry = __window.Sentry || {};', - ' __window.Sentry.Integrations = __window.Sentry.Integrations || {};', - ' for (var key in exports) {', - ' if (Object.prototype.hasOwnProperty.call(exports, key)) {', - ' __window.Sentry.Integrations[key] = exports[key];', - ' }', - ' }', - ].join('\n'), - - // code to add after the CJS wrapper - footer: '}(window));', - }, - }; - - // used by all bundles - const sharedBundleConfig = { - input, - output: { - // a file extension will be added to this base value when we specify either a minified or non-minified build - file: `build/${outputFileBase}`, - sourcemap: true, - strict: false, - esModule: false, - }, - plugins: [ - jsVersion.toLowerCase() === 'es5' ? typescriptPluginES5 : typescriptPluginES6, - markAsBrowserBuildPlugin, - nodeResolvePlugin, - licensePlugin, - ], - treeshake: 'smallest', - }; - - return deepMerge(sharedBundleConfig, isAddOn ? addOnBundleConfig : standAloneBundleConfig); -} - -/** - * Takes the CDN rollup config for a given package and produces three versions of it: - * - non-minified, including debug logging, - * - minified, including debug logging, - * - minified, with debug logging stripped - * - * @param baseConfig The rollup config shared by the entire package - * @returns An array of versions of that config - */ -export function makeConfigVariants(baseConfig) { - const configVariants = []; - - const { plugins } = baseConfig; - const includeDebuggingPlugin = makeIsDebugBuildPlugin(true); - const stripDebuggingPlugin = makeIsDebugBuildPlugin(false); - - // The license plugin has to be last, so it ends up after terser. Otherwise, terser will remove the license banner. - assert( - getLastElement(plugins).name === 'rollup-plugin-license', - `Last plugin in given options should be \`rollup-plugin-license\`. Found ${getLastElement(plugins).name}`, - ); - - // The additional options to use for each variant we're going to create - const variantSpecificConfigs = [ - { - output: { - file: `${baseConfig.output.file}.js`, - }, - plugins: insertAt(plugins, -2, includeDebuggingPlugin), - }, - // This variant isn't particularly helpful for an SDK user, as it strips logging while making no other minification - // changes, so by default we don't create it. It is however very useful when debugging rollup's treeshaking, so it's - // left here for that purpose. - // { - // output: { file: `${baseConfig.output.file}.no-debug.js`, - // }, - // plugins: insertAt(plugins, -2, stripDebuggingPlugin), - // }, - { - output: { - file: `${baseConfig.output.file}.min.js`, - }, - plugins: insertAt(plugins, -2, stripDebuggingPlugin, terserPlugin), - }, - { - output: { - file: `${baseConfig.output.file}.debug.min.js`, - }, - plugins: insertAt(plugins, -2, includeDebuggingPlugin, terserPlugin), - }, - ]; - - variantSpecificConfigs.forEach(variant => { - const mergedConfig = deepMerge(baseConfig, variant, { - // this makes it so that instead of concatenating the `plugin` properties of the two objects, the first value is - // just overwritten by the second value - arrayMerge: (first, second) => second, - }); - configVariants.push(mergedConfig); - }); - - return configVariants; -} diff --git a/rollup/bundleHelpers.js b/rollup/bundleHelpers.js new file mode 100644 index 000000000000..a406ae6905de --- /dev/null +++ b/rollup/bundleHelpers.js @@ -0,0 +1,174 @@ +/** + * Rollup config docs: https://rollupjs.org/guide/en/#big-list-of-options + */ + +import { builtinModules } from 'module'; + +import deepMerge from 'deepmerge'; + +import { + makeBrowserBuildPlugin, + makeCommonJSPlugin, + makeIsDebugBuildPlugin, + makeLicensePlugin, + makeNodeResolvePlugin, + makeRemoveBlankLinesPlugin, + makeRemoveESLintCommentsPlugin, + makeSucrasePlugin, + makeTerserPlugin, + makeTSPlugin, +} from './plugins/index.js'; +import { mergePlugins } from './utils'; + +const BUNDLE_VARIANTS = ['.js', '.min.js', '.debug.min.js']; + +export function makeBaseBundleConfig(options) { + const { bundleType, entrypoints, jsVersion, licenseTitle, outputFileBase, packageSpecificConfig } = options; + + const nodeResolvePlugin = makeNodeResolvePlugin(); + const sucrasePlugin = makeSucrasePlugin(); + const removeBlankLinesPlugin = makeRemoveBlankLinesPlugin(); + const removeESLintCommentsPlugin = makeRemoveESLintCommentsPlugin(); + const markAsBrowserBuildPlugin = makeBrowserBuildPlugin(true); + const licensePlugin = makeLicensePlugin(licenseTitle); + const tsPlugin = makeTSPlugin(jsVersion.toLowerCase()); + + // The `commonjs` plugin is the `esModuleInterop` of the bundling world. When used with `transformMixedEsModules`, it + // will include all dependencies, imported or required, in the final bundle. (Without it, CJS modules aren't included + // at all, and without `transformMixedEsModules`, they're only included if they're imported, not if they're required.) + const commonJSPlugin = makeCommonJSPlugin({ transformMixedEsModules: true }); + + // used by `@sentry/browser`, `@sentry/tracing`, and `@sentry/vue` (bundles which are a full SDK in and of themselves) + const standAloneBundleConfig = { + output: { + format: 'iife', + name: 'Sentry', + }, + context: 'window', + plugins: [markAsBrowserBuildPlugin], + }; + + // used by `@sentry/integrations` and `@sentry/wasm` (bundles which need to be combined with a stand-alone SDK bundle) + const addOnBundleConfig = { + // These output settings are designed to mimic an IIFE. We don't use Rollup's `iife` format because we don't want to + // attach this code to a new global variable, but rather inject it into the existing SDK's `Integrations` object. + output: { + format: 'cjs', + + // code to add before the CJS wrapper + banner: '(function (__window) {', + + // code to add just inside the CJS wrapper, before any of the wrapped code + intro: 'var exports = {};', + + // code to add after all of the wrapped code, but still inside the CJS wrapper + outro: () => + [ + '', + " // Add this module's exports to the global `Sentry.Integrations`", + ' __window.Sentry = __window.Sentry || {};', + ' __window.Sentry.Integrations = __window.Sentry.Integrations || {};', + ' for (var key in exports) {', + ' if (Object.prototype.hasOwnProperty.call(exports, key)) {', + ' __window.Sentry.Integrations[key] = exports[key];', + ' }', + ' }', + ].join('\n'), + + // code to add after the CJS wrapper + footer: '}(window));', + }, + plugins: [markAsBrowserBuildPlugin], + }; + + // used by `@sentry/serverless`, when creating the lambda layer + const nodeBundleConfig = { + output: { + format: 'cjs', + }, + plugins: [commonJSPlugin], + // Don't bundle any of Node's core modules + external: builtinModules, + }; + + // used by all bundles + const sharedBundleConfig = { + input: entrypoints, + output: { + // a file extension will be added to this base value when we specify either a minified or non-minified build + entryFileNames: outputFileBase, + dir: 'build', + sourcemap: true, + strict: false, + esModule: false, + }, + plugins: + jsVersion === 'es5' + ? [tsPlugin, nodeResolvePlugin, licensePlugin] + : [sucrasePlugin, removeBlankLinesPlugin, removeESLintCommentsPlugin, nodeResolvePlugin, licensePlugin], + treeshake: 'smallest', + }; + + const bundleTypeConfigMap = { + standalone: standAloneBundleConfig, + addon: addOnBundleConfig, + node: nodeBundleConfig, + }; + + return deepMerge.all([sharedBundleConfig, bundleTypeConfigMap[bundleType], packageSpecificConfig || {}], { + // Plugins have to be in the correct order or everything breaks, so when merging we have to manually re-order them + customMerge: key => (key === 'plugins' ? mergePlugins : undefined), + }); +} + +/** + * Takes the CDN rollup config for a given package and produces three versions of it: + * - non-minified, including debug logging, + * - minified, including debug logging, + * - minified, with debug logging stripped + * + * @param baseConfig The rollup config shared by the entire package + * @returns An array of versions of that config + */ +export function makeBundleConfigVariants(baseConfig, options = {}) { + const { variants = BUNDLE_VARIANTS } = options; + + const includeDebuggingPlugin = makeIsDebugBuildPlugin(true); + const stripDebuggingPlugin = makeIsDebugBuildPlugin(false); + const terserPlugin = makeTerserPlugin(); + + // The additional options to use for each variant we're going to create. + const variantSpecificConfigMap = { + '.js': { + output: { + entryFileNames: chunkInfo => `${baseConfig.output.entryFileNames(chunkInfo)}.js`, + }, + plugins: [includeDebuggingPlugin], + }, + + '.min.js': { + output: { + entryFileNames: chunkInfo => `${baseConfig.output.entryFileNames(chunkInfo)}.min.js`, + }, + plugins: [stripDebuggingPlugin, terserPlugin], + }, + + '.debug.min.js': { + output: { + entryFileNames: chunkInfo => `${baseConfig.output.entryFileNames(chunkInfo)}.debug.min.js`, + }, + plugins: [terserPlugin], + }, + }; + + return variants.map(variant => { + if (!BUNDLE_VARIANTS.includes(variant)) { + throw new Error(`Unknown bundle variant requested: ${variant}`); + } + return deepMerge(baseConfig, variantSpecificConfigMap[variant], { + // Merge the plugin arrays and make sure the end result is in the correct order. Everything else can use the + // default merge strategy. + customMerge: key => (key === 'plugins' ? mergePlugins : undefined), + }); + }); +} diff --git a/rollup/index.js b/rollup/index.js new file mode 100644 index 000000000000..2ae4712165ad --- /dev/null +++ b/rollup/index.js @@ -0,0 +1,9 @@ +Error.stackTraceLimit = Infinity; + +// TODO Is this necessary? +import * as plugins from './plugins/index.js'; +export { plugins }; + +export * from './bundleHelpers.js'; +export * from './npmHelpers.js'; +export { insertAt } from './utils.js'; diff --git a/rollup/npmHelpers.js b/rollup/npmHelpers.js new file mode 100644 index 000000000000..c0b983dac158 --- /dev/null +++ b/rollup/npmHelpers.js @@ -0,0 +1,117 @@ +/** + * Rollup config docs: https://rollupjs.org/guide/en/#big-list-of-options + */ + +import { builtinModules } from 'module'; +import * as path from 'path'; + +import deepMerge from 'deepmerge'; + +import { + makeConstToVarPlugin, + makeExtractPolyfillsPlugin, + makeNodeResolvePlugin, + makeRemoveBlankLinesPlugin, + makeRemoveESLintCommentsPlugin, + makeSucrasePlugin, +} from './plugins/index.js'; +import { mergePlugins } from './utils'; + +const packageDotJSON = require(path.resolve(process.cwd(), './package.json')); + +export function makeBaseNPMConfig(options = {}) { + const { + entrypoints = ['src/index.ts'], + esModuleInterop = false, + hasBundles = false, + packageSpecificConfig = {}, + } = options; + + const nodeResolvePlugin = makeNodeResolvePlugin(); + const sucrasePlugin = makeSucrasePlugin(); + const constToVarPlugin = makeConstToVarPlugin(); + const removeESLintCommentsPlugin = makeRemoveESLintCommentsPlugin(); + const removeBlankLinesPlugin = makeRemoveBlankLinesPlugin(); + const extractPolyfillsPlugin = makeExtractPolyfillsPlugin(); + + const defaultBaseConfig = { + input: entrypoints, + + output: { + // an appropriately-named directory will be added to this base value when we specify either a cjs or esm build + dir: hasBundles ? 'build/npm' : 'build', + + sourcemap: true, + + // output individual files rather than one big bundle + preserveModules: true, + + // any wrappers or helper functions generated by rollup can use ES6 features + generatedCode: 'es2015', + + // don't add `"use strict"` to the top of cjs files + strict: false, + + // do TS-3.8-style exports + // exports.dogs = are.great + // rather than TS-3.9-style exports + // Object.defineProperty(exports, 'dogs', { + // enumerable: true, + // get: () => are.great, + // }); + externalLiveBindings: false, + + // Don't call `Object.freeze` on the results of `import * as someModule from '...'` + // (We don't need it, so why waste the bytes?) + freeze: false, + + // Equivalent to `esModuleInterop` in tsconfig. + // Controls whether rollup emits helpers to handle special cases where turning + // `import * as dogs from 'dogs'` + // into + // `const dogs = require('dogs')` + // doesn't work. + // + // `auto` -> emit helpers + // `esModule` -> don't emit helpers + interop: esModuleInterop ? 'auto' : 'esModule', + }, + + plugins: [ + nodeResolvePlugin, + sucrasePlugin, + constToVarPlugin, + removeESLintCommentsPlugin, + removeBlankLinesPlugin, + extractPolyfillsPlugin, + ], + + // don't include imported modules from outside the package in the final output + external: [ + ...builtinModules, + ...Object.keys(packageDotJSON.dependencies || {}), + ...Object.keys(packageDotJSON.devDependencies || {}), + ...Object.keys(packageDotJSON.peerDependencies || {}), + ], + + // TODO `'smallest'` will get rid of `isDebugBuild()` by evaluating it and inlining the result and then treeshaking + // from there. The current setting (false) prevents this, in case we want to leave it there for users to use in + // their own bundling. That said, we don't yet know for sure that that works, so come back to this. + // treeshake: 'smallest', + treeshake: false, + }; + + return deepMerge(defaultBaseConfig, packageSpecificConfig, { + // Plugins have to be in the correct order or everything breaks, so when merging we have to manually re-order them + customMerge: key => (key === 'plugins' ? mergePlugins : undefined), + }); +} + +export function makeNPMConfigVariants(baseConfig) { + const variantSpecificConfigs = [ + { output: { format: 'cjs', dir: path.join(baseConfig.output.dir, 'cjs') } }, + { output: { format: 'esm', dir: path.join(baseConfig.output.dir, 'esm') } }, + ]; + + return variantSpecificConfigs.map(variant => deepMerge(baseConfig, variant)); +} diff --git a/rollup/plugins/bundlePlugins.js b/rollup/plugins/bundlePlugins.js new file mode 100644 index 000000000000..59f3fec01cd1 --- /dev/null +++ b/rollup/plugins/bundlePlugins.js @@ -0,0 +1,154 @@ +/** + * CommonJS plugin docs: https://github.com/rollup/plugins/tree/master/packages/commonjs + * License plugin docs: https://github.com/mjeanroy/rollup-plugin-license + * Replace plugin docs: https://github.com/rollup/plugins/tree/master/packages/replace + * Resolve plugin docs: https://github.com/rollup/plugins/tree/master/packages/node-resolve + * Terser plugin docs: https://github.com/TrySound/rollup-plugin-terser#options + * Terser docs: https://github.com/terser/terser#api-reference + * Typescript plugin docs: https://github.com/ezolenko/rollup-plugin-typescript2 + */ + +import commonjs from '@rollup/plugin-commonjs'; +import deepMerge from 'deepmerge'; +import license from 'rollup-plugin-license'; +import resolve from '@rollup/plugin-node-resolve'; +import replace from '@rollup/plugin-replace'; +import { terser } from 'rollup-plugin-terser'; +import typescript from 'rollup-plugin-typescript2'; + +/** + * Create a plugin to add an identification banner to the top of stand-alone bundles. + * + * @param title The title to use for the SDK, if not the package name + * @returns An instance of the `rollup-plugin-license` plugin + */ +export function makeLicensePlugin(title) { + const commitHash = require('child_process').execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim(); + + const plugin = license({ + banner: { + content: `/*! <%= data.title %> <%= pkg.version %> (${commitHash}) | https://github.com/getsentry/sentry-javascript */`, + data: { title }, + }, + }); + + // give it a nicer name for later, when we'll need to sort the plugins + plugin.name = 'license'; + + return plugin; +} + +/** + * Create a plugin to set the value of the `__SENTRY_DEBUG__` magic string. + * + * @param includeDebugging Whether or not the resulting build should include log statements + * @returns An instance of the `replace` plugin to do the replacement of the magic string with `true` or 'false` + */ +export function makeIsDebugBuildPlugin(includeDebugging) { + return replace({ + // __SENTRY_DEBUG__ should be save to replace in any case, so no checks for assignments necessary + preventAssignment: false, + values: { + __SENTRY_DEBUG__: includeDebugging, + }, + }); +} + +/** + * Create a plugin to set the value of the `__SENTRY_BROWSER_BUNDLE__` magic string. + * + * @param isBrowserBuild Whether or not the resulting build will be run in the browser + * @returns An instance of the `replace` plugin to do the replacement of the magic string with `true` or 'false` + */ +export function makeBrowserBuildPlugin(isBrowserBuild) { + return replace({ + // TODO This will be the default in the next version of the `replace` plugin + preventAssignment: true, + values: { + __SENTRY_BROWSER_BUNDLE__: isBrowserBuild, + }, + }); +} + +// `terser` options reference: https://github.com/terser/terser#api-reference +// `rollup-plugin-terser` options reference: https://github.com/TrySound/rollup-plugin-terser#options + +/** + * Create a plugin to perform minification using `terser`. + * + * @returns An instance of the `terser` plugin + */ +export function makeTerserPlugin() { + return terser({ + mangle: { + // `captureException` and `captureMessage` are public API methods and they don't need to be listed here, as the + // mangler won't touch user-facing things, but `sentryWrapped` is not user-facing, and would be mangled during + // minification. (We need it in its original form to correctly detect our internal frames for stripping.) All three + // are all listed here just for the clarity's sake, as they are all used in the frames manipulation process. + reserved: ['captureException', 'captureMessage', 'sentryWrapped'], + properties: { + // allow mangling of private field names... + regex: /^_[^_]/, + // ...except for `_experiments`, which we want to remain usable from the outside + reserved: ['_experiments'], + }, + }, + output: { + comments: false, + }, + }); +} + +/** + * Create a TypeScript plugin, which will down-compile if necessary, based on the given JS version. + * + * @param jsVersion Either `es5` or `es6` + * @returns An instance of the `typescript` plugin + */ +export function makeTSPlugin(jsVersion) { + const baseTSPluginOptions = { + tsconfig: 'tsconfig.json', + tsconfigOverride: { + compilerOptions: { + declaration: false, + declarationMap: false, + paths: { + '@sentry/browser': ['../browser/src'], + '@sentry/core': ['../core/src'], + '@sentry/hub': ['../hub/src'], + '@sentry/types': ['../types/src'], + '@sentry/utils': ['../utils/src'], + }, + baseUrl: '.', + }, + }, + include: ['*.ts+(|x)', '**/*.ts+(|x)', '../**/*.ts+(|x)'], + // the typescript plugin doesn't handle concurrency very well, so clean the cache between builds + // (see https://github.com/ezolenko/rollup-plugin-typescript2/issues/15) + clean: true, + // TODO: For the moment, the above issue seems to have stopped spamming the build with (non-blocking) errors, as it + // was originally. If it starts again, this will suppress that output. If we get to the end of the bundle revamp and + // it still seems okay, we can take this out entirely. + // verbosity: 0, + }; + + const plugin = typescript( + deepMerge(baseTSPluginOptions, { + tsconfigOverride: { + compilerOptions: { + target: jsVersion, + }, + }, + }), + ); + + // give it a nicer name for later, when we'll need to sort the plugins + plugin.name = 'typescript'; + + return plugin; +} + +// We don't pass these plugins any options which need to be calculated or changed by us, so no need to wrap them in +// another factory function, as they are themselves already factory functions. +export { resolve as makeNodeResolvePlugin }; +export { commonjs as makeCommonJSPlugin }; diff --git a/rollup/plugins/extractPolyfillsPlugin.js b/rollup/plugins/extractPolyfillsPlugin.js new file mode 100644 index 000000000000..e7b83b23dd35 --- /dev/null +++ b/rollup/plugins/extractPolyfillsPlugin.js @@ -0,0 +1,216 @@ +import * as path from 'path'; + +import * as recast from 'recast'; +import * as acornParser from 'recast/parsers/acorn'; + +const POLYFILL_NAMES = new Set([ + '_asyncNullishCoalesce', + '_asyncOptionalChain', + '_asyncOptionalChainDelete', + '_createNamedExportFrom', + '_createStarExport', + '_interopDefault', // rollup's version + '_interopNamespace', // rollup's version + '_interopNamespaceDefaultOnly', + '_interopRequireDefault', // sucrase's version + '_interopRequireWildcard', // sucrase's version + '_nullishCoalesce', + '_optionalChain', + '_optionalChainDelete', +]); + +/** + * Create a plugin which will replace function definitions of any of the above funcions with an `import` or `require` + * statement pulling them in from a central source. Mimics tsc's `importHelpers` option. + */ +export function makeExtractPolyfillsPlugin() { + let moduleFormat; + + // For more on the hooks used in this plugin, see https://rollupjs.org/guide/en/#output-generation-hooks + return { + name: 'extractPolyfills', + + // Figure out which build we're currently in (esm or cjs) + outputOptions(options) { + moduleFormat = options.format; + }, + + // This runs after both the sucrase transpilation (which happens in the `transform` hook) and rollup's own + // esm-i-fying or cjs-i-fying work (which happens right before `renderChunk`), in other words, after all polyfills + // will have been injected + renderChunk(code, chunk) { + const sourceFile = chunk.fileName; + + // We don't want to pull the function definitions out of their actual sourcefiles, just the places where they've + // been injected + if (sourceFile.includes('buildPolyfills')) { + return null; + } + + const parserOptions = { + sourceFileName: sourceFile, + // We supply a custom parser which wraps the provided `acorn` parser in order to override the `ecmaVersion` value. + // See https://github.com/benjamn/recast/issues/578. + parser: { + parse(source, options) { + return acornParser.parse(source, { + ...options, + // By this point in the build, everything should already have been down-compiled to whatever JS version + // we're targeting. Setting this parser to `latest` just means that whatever that version is (or changes + // to in the future), this parser will be able to handle the generated code. + ecmaVersion: 'latest', + }); + }, + }, + }; + + const ast = recast.parse(code, parserOptions); + + // Find function definitions and function expressions whose identifiers match a known polyfill name + const polyfillNodes = findPolyfillNodes(ast); + + if (polyfillNodes.length === 0) { + return null; + } + + console.log(`${sourceFile} - polyfills: ${polyfillNodes.map(node => node.name)}`); + + // Depending on the output format, generate `import { x, y, z } from '...'` or `var { x, y, z } = require('...')` + const importOrRequireNode = createImportOrRequireNode(polyfillNodes, sourceFile, moduleFormat); + + // Insert our new `import` or `require` node at the top of the file, and then delete the function definitions it's + // meant to replace (polyfill nodes get marked for deletion in `findPolyfillNodes`) + ast.program.body = [importOrRequireNode, ...ast.program.body.filter(node => !node.shouldDelete)]; + + // In spite of the name, this doesn't actually print anything - it just stringifies the code, and keeps track of + // where original nodes end up in order to generate a sourcemap. + const result = recast.print(ast, { + sourceMapName: `${sourceFile}.map`, + quote: 'single', + }); + + return { code: result.code, map: result.map }; + }, + }; +} + +/** + * Extract the function name, regardless of the format in which the function is declared + */ +function getNodeName(node) { + // Function expressions and functions pulled from objects + if (node.type === 'VariableDeclaration') { + // In practice sucrase and rollup only ever declare one polyfill at a time, so it's safe to just grab the first + // entry here + const declarationId = node.declarations[0].id; + + // Note: Sucrase and rollup seem to only use the first type of variable declaration for their polyfills, but good to + // cover our bases + + // Declarations of the form + // `const dogs = function() { return "are great"; };` + // or + // `const dogs = () => "are great"; + if (declarationId.type === 'Identifier') { + return declarationId.name; + } + // Declarations of the form + // `const { dogs } = { dogs: function() { return "are great"; } }` + // or + // `const { dogs } = { dogs: () => "are great" }` + else if (declarationId.type === 'ObjectPattern') { + return declarationId.properties[0].key.name; + } + // Any other format + else { + return 'unknown variable'; + } + } + + // Regular old functions, of the form + // `function dogs() { return "are great"; }` + else if (node.type === 'FunctionDeclaration') { + return node.id.name; + } + + // If we get here, this isn't a node we're interested in, so just return a string we know will never match any of the + // polyfill names + else { + return 'nope'; + } +} + +/** + * Find all nodes whose identifiers match a known polyfill name. + * + * Note: In theory, this could yield false positives, if any of the magic names were assigned to something other than a + * polyfill function, but the chances of that are slim. Also, it only searches the module global scope, but that's + * always where the polyfills appear, so no reason to traverse the whole tree. + */ +function findPolyfillNodes(ast) { + const isPolyfillNode = node => { + const nodeName = getNodeName(node); + if (POLYFILL_NAMES.has(nodeName)) { + // Mark this node for later deletion, since we're going to replace it with an import statement + node.shouldDelete = true; + // Store the name in a consistent spot, regardless of node type + node.name = nodeName; + + return true; + } + + return false; + }; + + return ast.program.body.filter(isPolyfillNode); +} + +/** + * Create a node representing an `import` or `require` statement of the form + * + * import { < polyfills > } from '...' + * or + * var { < polyfills > } = require('...') + * + * @param polyfillNodes The nodes from the current version of the code, defining the polyfill functions + * @param currentSourceFile The path, relative to `src/`, of the file currently being transpiled + * @param moduleFormat Either 'cjs' or 'esm' + * @returns A single node which can be subbed in for the polyfill definition nodes + */ +function createImportOrRequireNode(polyfillNodes, currentSourceFile, moduleFormat) { + const { + callExpression, + identifier, + importDeclaration, + importSpecifier, + literal, + objectPattern, + property, + variableDeclaration, + variableDeclarator, + } = recast.types.builders; + + // Since our polyfills live in `@sentry/utils`, if we're importing or requiring them there the path will have to be + // relative + const isUtilsPackage = process.cwd().endsWith('packages/utils'); + const importSource = literal( + isUtilsPackage + ? `./${path.relative(path.dirname(currentSourceFile), 'buildPolyfills')}` + : `@sentry/utils/${moduleFormat}/buildPolyfills`, + ); + + // This is the `x, y, z` of inside of `import { x, y, z }` or `var { x, y, z }` + const importees = polyfillNodes.map(({ name: fnName }) => + moduleFormat === 'esm' + ? importSpecifier(identifier(fnName)) + : property.from({ kind: 'init', key: identifier(fnName), value: identifier(fnName), shorthand: true }), + ); + + const requireFn = identifier('require'); + + return moduleFormat === 'esm' + ? importDeclaration(importees, importSource) + : variableDeclaration('var', [ + variableDeclarator(objectPattern(importees), callExpression(requireFn, [importSource])), + ]); +} diff --git a/rollup/plugins/index.js b/rollup/plugins/index.js new file mode 100644 index 000000000000..014b3b383b4d --- /dev/null +++ b/rollup/plugins/index.js @@ -0,0 +1,2 @@ +export * from './bundlePlugins'; +export * from './npmPlugins'; diff --git a/rollup/plugins/npmPlugins.js b/rollup/plugins/npmPlugins.js new file mode 100644 index 000000000000..ccdb99bb2ba1 --- /dev/null +++ b/rollup/plugins/npmPlugins.js @@ -0,0 +1,130 @@ +/** + * Regex Replace plugin docs: https://github.com/jetiny/rollup-plugin-re + * Replace plugin docs: https://github.com/rollup/plugins/tree/master/packages/replace + * Sucrase plugin docs: https://github.com/rollup/plugins/tree/master/packages/sucrase + */ + +// We need both replacement plugins because one handles regex and the other runs both before and after rollup does its +// bundling work. +import regexReplace from 'rollup-plugin-re'; +import replace from '@rollup/plugin-replace'; +import sucrase from '@rollup/plugin-sucrase'; + +/** + * Create a plugin to transpile TS syntax using `sucrase`. + * + * @returns An instance of the `@rollup/plugin-sucrase` plugin + */ +export function makeSucrasePlugin() { + return sucrase({ + transforms: ['typescript', 'jsx'], + }); +} + +/** + * Create a plugin to switch all instances of `const` to `var`, both to prevent problems when we shadow `global` and + * because it's fewer characters. + * + * Note that the generated plugin runs the replacement both before and after rollup does its code manipulation, to + * increase the chances that nothing is missed. + * + * TODO This is pretty brute-force-y. Perhaps we could switch to using a parser, the way we (will) do for both our jest + * transformer and the polyfill build script. + * + * @returns An instance of the `@rollup/plugin-replace` plugin + */ +export function makeConstToVarPlugin() { + return replace({ + // TODO `preventAssignment` will default to true in version 5.x of the replace plugin, at which point we can get rid + // of this. (It actually makes no difference in this case whether it's true or false, since we never assign to + // `const`, but if we don't give it a value, it will spam with warnings.) + preventAssignment: true, + values: { + // Include a space at the end to guarantee we're not accidentally catching the beginning of the words "constant," + // "constantly," etc. + 'const ': 'var ', + }, + }); +} + +/** + * Create a plugin which can be used to pause the build process at the given hook. + * + * Hooks can be found here: https://rollupjs.org/guide/en/#build-hooks. + * + * @param hookName The name of the hook at which to pause. + * @returns A plugin which inserts a debugger statement in the phase represented by the given hook + * + * For convenience, here are pre-built debuggers for every hook: + * + * makeDebuggerPlugin('buildStart'), + * makeDebuggerPlugin('options'), + * makeDebuggerPlugin('resolveId'), + * makeDebuggerPlugin('resolveDynamicImport'), + * makeDebuggerPlugin('load'), + * makeDebuggerPlugin('transform'), + * makeDebuggerPlugin('shouldTransformCachedModule'), + * makeDebuggerPlugin('moduleParsed'), + * makeDebuggerPlugin('buildEnd'), + * makeDebuggerPlugin('watchChange'), + * makeDebuggerPlugin('closeWatcher'), + * makeDebuggerPlugin('outputOptions'), + * makeDebuggerPlugin('renderStart'), + * makeDebuggerPlugin('banner'), + * makeDebuggerPlugin('footer'), + * makeDebuggerPlugin('intro'), + * makeDebuggerPlugin('outro'), + * makeDebuggerPlugin('augmentChunkHash'), + * makeDebuggerPlugin('renderDynamicImport'), + * makeDebuggerPlugin('resolveFileUrl'), + * makeDebuggerPlugin('resolveImportMeta'), + * makeDebuggerPlugin('renderChunk'), + * makeDebuggerPlugin('renderError'), + * makeDebuggerPlugin('generateBundle'), + * makeDebuggerPlugin('writeBundle'), + * makeDebuggerPlugin('closeBundle'), + */ +export function makeDebuggerPlugin(hookName) { + return { + name: 'debugger-plugin', + [hookName]: (..._args) => { + // eslint-disable-next-line no-debugger + debugger; + return null; + }, + }; +} + +/** + * Create a plugin to strip eslint-style comments from the output. + * + * @returns A `rollup-plugin-re` instance. + */ +export function makeRemoveESLintCommentsPlugin() { + return regexReplace({ + patterns: [ + { + test: /\/[/*] eslint-.*\n/g, + replace: '', + }, + ], + }); +} + +/** + * Create a plugin to strip multiple consecutive blank lines, with or without whitespace in them. from the output. + * + * @returns A `rollup-plugin-re` instance. + */ +export function makeRemoveBlankLinesPlugin() { + return regexReplace({ + patterns: [ + { + test: /\n(\n\s*)+\n/g, + replace: '\n\n', + }, + ], + }); +} + +export { makeExtractPolyfillsPlugin } from './extractPolyfillsPlugin.js'; diff --git a/rollup/utils.js b/rollup/utils.js new file mode 100644 index 000000000000..6a7462788a47 --- /dev/null +++ b/rollup/utils.js @@ -0,0 +1,30 @@ +/** + * Helper function to compensate for the fact that JS can't handle negative array indices very well + */ +export const insertAt = (arr, index, ...insertees) => { + const newArr = [...arr]; + // Add 1 to the array length so that the inserted element ends up in the right spot with respect to the length of the + // new array (which will be one element longer), rather than that of the current array + const destinationIndex = index >= 0 ? index : arr.length + 1 + index; + newArr.splice(destinationIndex, 0, ...insertees); + return newArr; +}; + +/** + * Merge two arrays of plugins, making sure they're sorted in the correct order. + */ +export function mergePlugins(pluginsA, pluginsB) { + const plugins = [...pluginsA, ...pluginsB]; + plugins.sort((a, b) => { + // Hacky way to make sure the ones we care about end up where they belong in the order. (Really the TS and sucrase + // plugins are tied - both should come first - but they're mutually exclusive, so they can come in arbitrary order + // here.) + const order = ['typescript', 'sucrase', '...', 'terser', 'license']; + const sortKeyA = order.includes(a.name) ? a.name : '...'; + const sortKeyB = order.includes(b.name) ? b.name : '...'; + + return order.indexOf(sortKeyA) - order.indexOf(sortKeyB); + }); + + return plugins; +} diff --git a/scripts/aws-deploy-local-layer.sh b/scripts/aws-deploy-local-layer.sh new file mode 100755 index 000000000000..b86dcd344d78 --- /dev/null +++ b/scripts/aws-deploy-local-layer.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# +# Builds and deploys the Sentry AWS Lambda layer (including the Sentry SDK and the Sentry Lambda Extension) +# +# The currently checked out version of the SDK in your local directory is used. +# The latest version of the Lambda Extension is fetched from the Sentry Release Registry. +# +# Note: While we normally try to write all of our scripts in TS, this is in bash because it's meant to exactly mirror +# what the lambda-zipping GHA is doing (see https://github.com/getsentry/action-build-aws-lambda-extension) + +set -euo pipefail + +# Cleanup +echo "Preparing local directories for new build..." +rm -rf dist-serverless/ +rm -rf ./packages/serverless/build +rm -rf ./packages/serverless/dist +rm -rf ./packages/serverless/node_modules +rm -f sentry-node-serverless-*.zip + +# Creating Lambda layer +echo "Creating Lambda layer in ./packages/serverless/build/aws/dist-serverless..." +cd packages/serverless +yarn build +cd ../../ +echo "Done creating Lambda layer in ./packages/serverless/build/aws/dist-serverless." + +# Move dist-serverless/ to the root folder for the action to pick it up. +# This is only needed in this script, because in GitHub workflow +# this is done with the upload-artifact/download-artifact actions +echo "Copying Lambda layer in ./packages/serverless/build/aws/dist-serverless to working directory..." +mv ./packages/serverless/build/aws/dist-serverless . +echo "Done copying Lambda layer in ./packages/serverless/build/aws/dist-serverless to working directory." + +# IMPORTANT: +# Please make sure that this does the same as the GitHub action that +# is building the Lambda layer in production! +# see: https://github.com/getsentry/action-build-aws-lambda-extension/blob/main/action.yml#L23-L40 + +# Adding Sentry Lambda extension to Lambda layer +echo "Adding Sentry Lambda extension to Lambda layer in ./dist-serverless..." +mkdir -p dist-serverless/extensions +curl -0 --silent --output dist-serverless/extensions/sentry-lambda-extension $(curl -s https://release-registry.services.sentry.io/apps/sentry-lambda-extension/latest | jq -r .files.\"sentry-lambda-extension\".url) +chmod +x dist-serverless/extensions/sentry-lambda-extension +echo "Done adding Sentry Lambda extension to Lambda layer in ./dist-serverless." + +# Zip Lambda layer and included Lambda extension +echo "Zipping Lambda layer and included Lambda extension..." +cd dist-serverless/ +zip -r -y ../sentry-node-serverless-x.x.x-dev.zip . +cd .. +echo "Done Zipping Lambda layer and included Lambda extension to ./sentry-node-serverless-x.x.x-dev.zip." + +# Deploying zipped Lambda layer to AWS +echo "Deploying zipped Lambda layer to AWS..." + +aws lambda publish-layer-version \ + --layer-name "SentryNodeServerlessSDK-local-dev" \ + --region "eu-central-1" \ + --zip-file "fileb://sentry-node-serverless-x.x.x-dev.zip" \ + --description "Local test build of SentryNodeServerlessSDK (can be deleted)" \ + --no-cli-pager + +echo "Done deploying zipped Lambda layer to AWS as 'SentryNodeServerlessSDK-local-dev'." + +echo "All done. Have a nice day!" diff --git a/scripts/build-types-watch.ts b/scripts/build-types-watch.ts index d8a4cedb32aa..8b29635479d0 100644 --- a/scripts/build-types-watch.ts +++ b/scripts/build-types-watch.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ /** * If `yarn build:types:watch` is run without types files previously having been created, the build will get stuck in an * errored state. This happens because lerna runs all of the packages' `yarn build:types:watch` statements in parallel, @@ -28,7 +29,9 @@ for (const pkg of packages) { continue; } - const packageJSON = JSON.parse(fs.readFileSync(path.resolve(packagePath, 'package.json'), 'utf-8')); + const packageJSON = JSON.parse(fs.readFileSync(path.resolve(packagePath, 'package.json'), 'utf-8')) as { + scripts: Record; + }; if ('build:types' in packageJSON.scripts && !fs.existsSync(path.resolve(packagePath, 'build/types'))) { console.warn( diff --git a/scripts/ensure-bundle-deps.ts b/scripts/ensure-bundle-deps.ts new file mode 100644 index 000000000000..06bdaf863582 --- /dev/null +++ b/scripts/ensure-bundle-deps.ts @@ -0,0 +1,158 @@ +/* eslint-disable no-console */ +import * as childProcess from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as util from 'util'; + +/** + * Ensure that `build:bundle` has all of the dependencies it needs to run. Works at both the repo and package level. + */ +export async function ensureBundleBuildPrereqs(options: { + dependencies: string[]; + maxRetries?: number; +}): Promise { + const { maxRetries = 12, dependencies } = options; + + const { + // The directory in which the yarn command was originally invoked (which won't necessarily be the same as + // `process.cwd()`) + INIT_CWD: yarnInitialDir, + // JSON containing the args passed to `yarn` + npm_config_argv: yarnArgJSON, + } = process.env; + + if (!yarnInitialDir || !yarnArgJSON) { + const received = { INIT_CWD: yarnInitialDir, npm_config_argv: yarnArgJSON }; + throw new Error( + `Missing environment variables needed for ensuring bundle dependencies. Received:\n${util.inspect(received)}\n`, + ); + } + + // Did this build get invoked by a repo-level script, or a package-level script, and which script was it? + const isTopLevelBuild = path.basename(yarnInitialDir) === 'sentry-javascript'; + const yarnScript = (JSON.parse(yarnArgJSON) as { original: string[] }).original[0]; + + // convert '@sentry/xyz` to `xyz` + const dependencyDirs = dependencies.map(npmPackageName => npmPackageName.split('/')[1]); + + // The second half of the conditional tests if this script is being run by the original top-level command or a + // package-level command spawned by it. + const packagesDir = isTopLevelBuild && yarnInitialDir === process.cwd() ? 'packages' : '..'; + + if (checkForBundleDeps(packagesDir, dependencyDirs)) { + // We're good, nothing to do, the files we need are there + return; + } + + // If we get here, the at least some of the dependencies are missing, but how we handle that depends on how we got + // here. There are six possibilities: + // - We ran `build` or `build:bundle` at the repo level + // - We ran `build` or `build:bundle` at the package level + // - We ran `build` or `build:bundle` at the repo level and lerna then ran `build:bundle` at the package level. (We + // shouldn't ever land here under this scenario - the top-level build should already have handled any missing + // dependencies - but it's helpful to consider all the possibilities.) + // + // In the first version of the first scenario (repo-level `build` -> repo-level `build:bundle`), all we have to do is + // wait, because other parts of `build` are creating them as this check is being done. (Waiting 5 or 10 or even 15 + // seconds to start running `build:bundle` in parallel is better than pushing it to the second half of `build`, + // because `build:bundle` is the slowest part of the build and therefore the one we most want to parallelize with + // other slow parts, like `build:types`.) + // + // In all other scenarios, if the dependencies are missing, we have to build them ourselves - with `build:bundle` at + // either level, we're the only thing happening (so no one's going to do it for us), and with package-level `build`, + // types and npm assets are being built simultaneously, but only for the package being bundled, not for its + // dependencies. Either way, it's on us to fix the problem. + // + // TODO: This actually *doesn't* work for package-level `build`, not because of a flaw in this logic, but because + // `build:rollup` has similar dependency needs (it needs types rather than npm builds). We should do something like + // this for that at some point. + + if (isTopLevelBuild && yarnScript === 'build') { + let retries = 0; + + console.log('\nSearching for bundle dependencies...'); + + while (retries < maxRetries && !checkForBundleDeps(packagesDir, dependencyDirs)) { + console.log('Bundle dependencies not found. Trying again in 5 seconds.'); + retries += 1; + await sleep(5000); + } + + if (retries === maxRetries) { + throw new Error( + `\nERROR: \`yarn build:bundle\` (triggered by \`yarn build\`) cannot find its depdendencies, despite waiting ${ + 5 * maxRetries + } seconds for the rest of \`yarn build\` to create them. Something is wrong - it shouldn't take that long. Exiting.`, + ); + } + + console.log(`\nFound all bundle dependencies after ${retries} retries. Beginning bundle build...`); + } + + // top-level `build:bundle`, package-level `build` and `build:bundle` + else { + console.warn('\nWARNING: Missing dependencies for bundle build. They will be built before continuing.'); + + for (const dependencyDir of dependencyDirs) { + console.log(`\nBuilding \`${dependencyDir}\` package...`); + run('yarn build:rollup', { cwd: `${packagesDir}/${dependencyDir}` }); + } + + console.log('\nAll dependencies built successfully. Beginning bundle build...'); + } +} + +/** + * See if all of the necessary dependencies exist + */ +function checkForBundleDeps(packagesDir: string, dependencyDirs: string[]): boolean { + for (const dependencyDir of dependencyDirs) { + const depBuildDir = `${packagesDir}/${dependencyDir}/build`; + + // Checking that the directories exist isn't 100% the same as checking that the files themselves exist, of course, + // but it's a decent proxy, and much simpler to do than checking for individual files. + if ( + !( + (fs.existsSync(`${depBuildDir}/cjs`) && fs.existsSync(`${depBuildDir}/esm`)) || + (fs.existsSync(`${depBuildDir}/npm/cjs`) && fs.existsSync(`${depBuildDir}/npm/esm`)) + ) + ) { + // Fail fast + return false; + } + } + + return true; +} + +/** + * Wait the given number of milliseconds before continuing. + */ +async function sleep(ms: number): Promise { + await new Promise(resolve => + setTimeout(() => { + resolve(); + }, ms), + ); +} + +/** + * Run the given shell command, piping the shell process's `stdin`, `stdout`, and `stderr` to that of the current + * process. Returns contents of `stdout`. + */ +function run(cmd: string, options?: childProcess.ExecSyncOptions): string { + return String(childProcess.execSync(cmd, { stdio: 'inherit', ...options })); +} + +// TODO: Not ideal that we're hard-coding this, and it's easy to get when we're in a package directory, but would take +// more work to get from the repo level. Fortunately this list is unlikely to change very often, and we're the only ones +// we'll break if it gets out of date. +const dependencies = ['@sentry/utils', '@sentry/hub', '@sentry/core']; + +if (['sentry-javascript', 'tracing', 'wasm'].includes(path.basename(process.cwd()))) { + dependencies.push('@sentry/browser'); +} + +void ensureBundleBuildPrereqs({ + dependencies, +}); diff --git a/scripts/prepack.ts b/scripts/prepack.ts index bac7a544a2f5..ace6846db457 100644 --- a/scripts/prepack.ts +++ b/scripts/prepack.ts @@ -7,7 +7,6 @@ */ import * as fs from 'fs'; -import * as fse from 'fs-extra'; import * as path from 'path'; const NPM_BUILD_DIR = 'build/npm'; @@ -20,74 +19,47 @@ const ENTRY_POINTS = ['main', 'module', 'types', 'browser']; const packageWithBundles = process.argv.includes('--bundles'); const buildDir = packageWithBundles ? NPM_BUILD_DIR : BUILD_DIR; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const pkgJson: { [key: string]: unknown } = require(path.resolve('package.json')); + // check if build dir exists -try { - if (!fs.existsSync(path.resolve(buildDir))) { - console.error(`Directory ${buildDir} DOES NOT exist`); - console.error("This script should only be executed after you've run `yarn build`."); - process.exit(1); - } -} catch (error) { - console.error(`Error while looking up directory ${buildDir}`); +if (!fs.existsSync(path.resolve(buildDir))) { + console.error(`\nERROR: Directory '${buildDir}' does not exist in ${pkgJson.name}.`); + console.error("This script should only be executed after you've run `yarn build`."); process.exit(1); } // copy non-code assets to build dir ASSETS.forEach(asset => { const assetPath = path.resolve(asset); - try { - if (!fs.existsSync(assetPath)) { - console.error(`Asset ${asset} does not exist.`); - process.exit(1); - } - const destinationPath = path.resolve(buildDir, path.basename(asset)); - console.log(`Copying ${path.basename(asset)} to ${path.relative('../..', destinationPath)}.`); - fs.copyFileSync(assetPath, destinationPath); - } catch (error) { - console.error(`Error while copying ${asset} to ${buildDir}`); + if (!fs.existsSync(assetPath)) { + console.error(`\nERROR: Asset '${asset}' does not exist.`); process.exit(1); } + const destinationPath = path.resolve(buildDir, path.basename(asset)); + console.log(`Copying ${path.basename(asset)} to ${path.relative('../..', destinationPath)}.`); + fs.copyFileSync(assetPath, destinationPath); }); -// TODO remove in v7! Until then: -// copy CDN bundles into npm dir to temporarily keep bundles in npm tarball -// inside the tarball, they are located in `build/` -// for now, copy it by default, unless explicitly forbidden via an command line arg -const tmpCopyBundles = packageWithBundles && !process.argv.includes('--skipBundleCopy'); -if (tmpCopyBundles) { - const npmTmpBundlesPath = path.resolve(buildDir, 'build'); - const cdnBundlesPath = path.resolve('build', 'bundles'); - try { - if (!fs.existsSync(npmTmpBundlesPath)) { - fs.mkdirSync(npmTmpBundlesPath); - } - void fse.copy(cdnBundlesPath, npmTmpBundlesPath); - } catch (error) { - console.error(`Error while tmp copying CDN bundles to ${buildDir}`); - process.exit(1); - } -} -// end remove - // package.json modifications -const packageJsonPath = path.resolve(buildDir, 'package.json'); +const newPackageJsonPath = path.resolve(buildDir, 'package.json'); // eslint-disable-next-line @typescript-eslint/no-var-requires -const pkgJson: { [key: string]: unknown } = require(packageJsonPath); +const newPkgJson: { [key: string]: unknown } = require(newPackageJsonPath); // modify entry points to point to correct paths (i.e. strip out the build directory) -ENTRY_POINTS.filter(entryPoint => pkgJson[entryPoint]).forEach(entryPoint => { - pkgJson[entryPoint] = (pkgJson[entryPoint] as string).replace(`${buildDir}/`, ''); +ENTRY_POINTS.filter(entryPoint => newPkgJson[entryPoint]).forEach(entryPoint => { + newPkgJson[entryPoint] = (newPkgJson[entryPoint] as string).replace(`${buildDir}/`, ''); }); -delete pkgJson.scripts; -delete pkgJson.volta; -delete pkgJson.jest; +delete newPkgJson.scripts; +delete newPkgJson.volta; +delete newPkgJson.jest; // write modified package.json to file (pretty-printed with 2 spaces) try { - fs.writeFileSync(packageJsonPath, JSON.stringify(pkgJson, null, 2)); + fs.writeFileSync(newPackageJsonPath, JSON.stringify(newPkgJson, null, 2)); } catch (error) { - console.error('Error while writing package.json to disk'); + console.error(`\nERROR: Error while writing modified 'package.json' to disk in ${pkgJson.name}:\n`, error); process.exit(1); } @@ -99,26 +71,28 @@ async function runPackagePrepack(packagePrepackPath: string): Promise { process.exit(1); } } else { - console.error(`Could not find a prepack function in ${packagePrepackPath}.`); + console.error(`\nERROR: Could not find a \`prepack\` function in './scripts/prepack.ts' in ${pkgJson.name}.`); console.error( - 'Make sure, your package-specific prepack script exports `function prepack(buildDir: string): boolean`.', + 'Make sure your package-specific prepack script exports `function prepack(buildDir: string): boolean`.', ); process.exit(1); } } // execute package specific settings -// 1. check if a package called `/scripts/prepack.ts` exitsts +// 1. check if a script called `/scripts/prepack.ts` exists // if yes, 2.) execute that script for things that are package-specific -void (async () => { +async function runPackageSpecificScripts(): Promise { const packagePrepackPath = path.resolve('scripts', 'prepack.ts'); try { if (fs.existsSync(packagePrepackPath)) { await runPackagePrepack(packagePrepackPath); } } catch (error) { - console.error(`Error while trying to access ${packagePrepackPath.toString()}`); + console.error(`\nERROR: Error while trying to load and run ./scripts/prepack.ts in ${pkgJson.name}:\n`, error); process.exit(1); } console.log(`\nSuccessfully finished prepack commands for ${pkgJson.name}\n`); -})(); +} + +void runPackageSpecificScripts(); diff --git a/scripts/test.ts b/scripts/test.ts index 6c2efd8a1b68..e7058f2b66a8 100644 --- a/scripts/test.ts +++ b/scripts/test.ts @@ -1,52 +1,173 @@ -import { spawnSync } from 'child_process'; -import { join } from 'path'; +import * as childProcess from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; -function run(cmd: string, cwd: string = '') { - const result = spawnSync(cmd, { shell: true, stdio: 'inherit', cwd: join(__dirname, '..', cwd || '') }); +const CURRENT_NODE_VERSION = process.version.replace('v', '').split('.')[0]; - if (result.status !== 0) { - process.exit(result.status || undefined); +// We run ember tests in their own job. +const DEFAULT_SKIP_TESTS_PACKAGES = ['@sentry/ember']; +// These packages don't support Node 8 for syntax or dependency reasons. +const NODE_8_SKIP_TESTS_PACKAGES = [ + ...DEFAULT_SKIP_TESTS_PACKAGES, + '@sentry-internal/eslint-plugin-sdk', + '@sentry/react', + '@sentry/wasm', + '@sentry/gatsby', + '@sentry/serverless', + '@sentry/nextjs', + '@sentry/angular', +]; + +// We have to downgrade some of our dependencies in order to run tests in Node 8 and 10. +const NODE_8_LEGACY_DEPENDENCIES = [ + 'jsdom@15.x', + 'jest@25.x', + 'jest-environment-jsdom@25.x', + 'jest-environment-node@25.x', + 'ts-jest@25.x', +]; +const NODE_10_LEGACY_DEPENDENCIES = ['jsdom@16.x']; + +type JSONValue = string | number | boolean | null | JSONArray | JSONObject; + +type JSONObject = { + [key: string]: JSONValue; +}; +type JSONArray = Array; + +interface TSConfigJSON extends JSONObject { + compilerOptions: { lib: string[]; target: string }; +} + +/** + * Run the given shell command, piping the shell process's `stdin`, `stdout`, and `stderr` to that of the current + * process. Returns contents of `stdout`. + */ +function run(cmd: string, options?: childProcess.ExecSyncOptions) { + return childProcess.execSync(cmd, { stdio: 'inherit', ...options }); +} + +/** + * Install the given legacy dependencies, for compatibility with tests run in older versions of Node. + */ +function installLegacyDeps(legacyDeps: string[] = []): void { + // Ignoring engines and scripts lets us get away with having incompatible things installed for SDK packages we're not + // testing in the current node version, and ignoring the root check lets us install things at the repo root. + run(`yarn add --dev --ignore-engines --ignore-scripts --ignore-workspace-root-check ${legacyDeps.join(' ')}`); +} + +/** + * Add a tranformer to our jest config, to do the same `const`-to-`var` replacement as our rollup plugin does. + * + * This is needed because Node 8 doesn't like the way we shadow `global` (`const global = getGlobalObject()`). Changing + * it to a `var` solves this by making it redeclarable. + * + */ +function addJestTransformer(): void { + // Though newer `ts-jest` versions support transformers written in TS, the legacy version does not. + run('yarn tsc --skipLibCheck jest/transformers/constReplacer.ts'); + + // Loading the existing Jest config will error out unless the config file has an accompanying types file, so we have + // to create that before we can load it. + run('yarn tsc --allowJs --skipLibCheck --declaration --emitDeclarationOnly jest/jest.config.js'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const jestConfig = require('../jest/jest.config.js'); + + // Inject the transformer + jestConfig.globals['ts-jest'].astTransformers = ['/../../jest/transformers/constReplacer.js']; + + // When we required the jest config file above, all expressions it contained were evaluated. Specifically, the + // `rootDir: process.cwd()` + // entry was replaced with + // `rootDir: ""`, + // Though it's a little brute-force-y, the easiest way to fix this is to just stringify the code and perform the + // substitution in reverse. + const stringifiedConfig = JSON.stringify(jestConfig, null, 2).replace( + `"rootDir": "${process.cwd()}"`, + 'rootDir: process.cwd()', + ); + + // Now we just have to convert it back to a module and write it to disk + const code = `module.exports = ${stringifiedConfig}`; + fs.writeFileSync(path.resolve('jest/jest.config.js'), code); +} + +/** + * Modify a json file on disk. + * + * @param filepath The path to the file to be modified + * @param transformer A function which takes the JSON data as input and returns a mutated version. It may mutate the + * JSON data in place, but it isn't required to do so. + */ +export function modifyJSONFile(filepath: string, transformer: (json: JSONObject) => JSONObject): void { + const fileContents = fs + .readFileSync(filepath) + .toString() + // get rid of comments, which the `jsonc` format allows, but which will crash `JSON.parse` + .replace(/\/\/.*\n/g, ''); + const json = JSON.parse(fileContents); + const newJSON = transformer(json); + fs.writeFileSync(filepath, JSON.stringify(newJSON, null, 2)); +} + +const es6ifyTestTSConfig = (pkg: string): void => { + const filepath = `packages/${pkg}/tsconfig.test.json`; + const transformer = (json: JSONObject): JSONObject => { + const tsconfig = json as TSConfigJSON; + tsconfig.compilerOptions.target = 'es6'; + return json; + }; + modifyJSONFile(filepath, transformer); +}; + +/** + * Skip tests which don't apply to Node and therefore don't need to run in older Node versions. + * + * TODO We're foreced to skip these tests for compatibility reasons (right now this function only gets called in Node + * 8), but we could be skipping a lot more tests in Node 8-14 - anything where compatibility with different Node + * versions is irrelevant - and only running them in Node 16. + */ +function skipNonNodeTests(): void { + run('rm -rf packages/tracing/test/browser'); +} + +/** + * Run tests, ignoring the given packages + */ +function runWithIgnores(skipPackages: string[] = []): void { + const ignoreFlags = skipPackages.map(dep => `--ignore="${dep}"`).join(' '); + run(`yarn test ${ignoreFlags}`); +} + +/** + * Run the tests, accounting for compatibility problems in older versions of Node. + */ +function runTests(): void { + if (CURRENT_NODE_VERSION === '8') { + installLegacyDeps(NODE_8_LEGACY_DEPENDENCIES); + // Inject a `const`-to-`var` transformer, in order to stop Node 8 from complaining when we shadow `global` + addJestTransformer(); + // TODO Right now, this just skips incompatible tests, but it could be skipping more (hence the aspirational name), + // and not just in Node 8. See `skipNonNodeTests`'s docstring. + skipNonNodeTests(); + es6ifyTestTSConfig('utils'); + runWithIgnores(NODE_8_SKIP_TESTS_PACKAGES); + } + // + else if (CURRENT_NODE_VERSION === '10') { + installLegacyDeps(NODE_10_LEGACY_DEPENDENCIES); + es6ifyTestTSConfig('utils'); + runWithIgnores(DEFAULT_SKIP_TESTS_PACKAGES); + } + // + else if (CURRENT_NODE_VERSION === '12') { + es6ifyTestTSConfig('utils'); + runWithIgnores(DEFAULT_SKIP_TESTS_PACKAGES); + } + // + else { + runWithIgnores(DEFAULT_SKIP_TESTS_PACKAGES); } } -const nodeMajorVersion = parseInt(process.version.split('.')[0].replace('v', ''), 10); - -// control which packages we test on each version of node -if (nodeMajorVersion <= 6) { - // install legacy versions of packages whose current versions don't support node 6 - // ignoring engines and scripts lets us get away with having incompatible things installed for packages we're not testing - run('yarn add --dev --ignore-engines --ignore-scripts nock@10.x', 'packages/node'); - run('yarn add --dev --ignore-engines --ignore-scripts jsdom@11.x', 'packages/tracing'); - run('yarn add --dev --ignore-engines --ignore-scripts jsdom@11.x', 'packages/utils'); - - // only test against @sentry/node and its dependencies - node 6 is too old for anything else to work - const scope = ['@sentry/core', '@sentry/hub', '@sentry/minimal', '@sentry/node', '@sentry/utils', '@sentry/tracing'] - .map(dep => `--scope="${dep}"`) - .join(' '); - - run(`yarn test ${scope}`); -} else if (nodeMajorVersion <= 8) { - // install legacy versions of packages whose current versions don't support node 8 - // ignoring engines and scripts lets us get away with having incompatible things installed for packages we're not testing - run('yarn add --dev --ignore-engines --ignore-scripts jsdom@15.x', 'packages/tracing'); - run('yarn add --dev --ignore-engines --ignore-scripts jsdom@15.x', 'packages/utils'); - - // ember tests happen separately, and the rest fail on node 8 for various syntax or dependency reasons - const ignore = [ - '@sentry/ember', - '@sentry-internal/eslint-plugin-sdk', - '@sentry/react', - '@sentry/wasm', - '@sentry/gatsby', - '@sentry/serverless', - '@sentry/nextjs', - ] - .map(dep => `--ignore="${dep}"`) - .join(' '); - - run(`yarn test ${ignore}`); -} else { - run('yarn test --ignore="@sentry/ember"'); -} - -process.exit(0); +runTests(); diff --git a/scripts/verify-packages-versions.js b/scripts/verify-packages-versions.js index fb6a6e95ec7f..e0efc39311bc 100644 --- a/scripts/verify-packages-versions.js +++ b/scripts/verify-packages-versions.js @@ -1,6 +1,6 @@ const pkg = require('../package.json'); -const TYPESCRIPT_VERSION = '3.7.5'; +const TYPESCRIPT_VERSION = '3.8.3'; if (pkg.devDependencies.typescript !== TYPESCRIPT_VERSION) { console.error(` diff --git a/tsconfig-templates/README.md b/tsconfig-templates/README.md index 4cb4d29d788f..faab3d4983f8 100644 --- a/tsconfig-templates/README.md +++ b/tsconfig-templates/README.md @@ -1,5 +1,5 @@ # `tsconfig` Templates -Every package should get its own copy of these five files. Package-specific options should go in `tsconfig.json` and -test-specific options in `tsconfig.test.json`. The `cjs`, `esm`, and `types` files shouldn't need to be modified, and -only exist because tsconfigs don't support multiple inheritence. +Every package should get its own copy of these three files. Package-specific options should go in `tsconfig.json` and +test-specific options in `tsconfig.test.json`. The `types` file shouldn't need to be modified, and only exists because +tsconfigs don't support multiple inheritence. diff --git a/tsconfig-templates/tsconfig.cjs.json b/tsconfig-templates/tsconfig.cjs.json deleted file mode 100644 index abd80f77e1ff..000000000000 --- a/tsconfig-templates/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "dist" - } -} diff --git a/tsconfig-templates/tsconfig.esm.json b/tsconfig-templates/tsconfig.esm.json deleted file mode 100644 index b6ee3fa615c0..000000000000 --- a/tsconfig-templates/tsconfig.esm.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "es6", - "outDir": "esm" - } -} diff --git a/tsconfig.dev.json b/tsconfig.dev.json new file mode 100644 index 000000000000..8448d6c7a046 --- /dev/null +++ b/tsconfig.dev.json @@ -0,0 +1,10 @@ +// TODO This should eventually end up as the tsconfig for a dev-utils package +{ + "extends": "./tsconfig.json", + + "include": ["**/scripts/**/*.ts", "jest/**/*.ts"], + + "compilerOptions": { + "types": ["node", "jest"], + } +} diff --git a/tsconfig.json b/tsconfig.json index f2ffa0c4e07c..9f46e21db53c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,12 +2,10 @@ "extends": "./packages/typescript/tsconfig.json", "compilerOptions": { - // TODO: turn these on once we switch to only generating types once, using `tsconfig.types.json` - // "declaration": false, - // "declarationMap": false, - "allowSyntheticDefaultImports": true, + "declaration": false, + "declarationMap": false, + "skipLibCheck": true, "types": ["node"], - "noErrorTruncation": true // move me up to @sentry/typescript } } diff --git a/typedoc.js b/typedoc.js index f6e0a7e2a805..4b1843c33aad 100644 --- a/typedoc.js +++ b/typedoc.js @@ -6,7 +6,7 @@ module.exports = { exclude: [ '**/test/**/*', '**/*.js', - '**/dist/**/*', + '**/cjs/**/*', '**/esm/**/*', '**/build/**/*', '**/packages/typescript/**/*', diff --git a/yarn.lock b/yarn.lock index 81cd3f64044a..e5950f2a5cca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,24 +2,202 @@ # yarn lockfile v1 -"@angular/common@^10.0.3": +"@ampproject/remapping@^2.1.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.1.2.tgz#4edca94973ded9630d20101cd8559cedb8d8bd34" + integrity sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg== + dependencies: + "@jridgewell/trace-mapping" "^0.3.0" + +"@angular-devkit/architect@0.1002.4": + version "0.1002.4" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1002.4.tgz#2e1fa9c7a4718a4d0d101516ab0cc9cb653c5c57" + integrity sha512-Vrb2XSnvqj4RByqSWPeG/o9BSNX2DL3pxwQgLMrxofG8/+1VHQ2MsN/KTxBnEZtqeW4/l2QWTsQyzY5frJI69A== + dependencies: + "@angular-devkit/core" "10.2.4" + rxjs "6.6.2" + +"@angular-devkit/build-angular@~0.1002.4": + version "0.1002.4" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.1002.4.tgz#6525c8ac8ec88d79aa34fdb4f224a914f9ea520f" + integrity sha512-0jo8fCbOyo1HGRDKBVzIzmGd3/Z+x5YP/9t1QHQrPTq9gRoVI+1vFgrKh7XApmGPa/S4bN6hows1wnGzTq5xJg== + dependencies: + "@angular-devkit/architect" "0.1002.4" + "@angular-devkit/build-optimizer" "0.1002.4" + "@angular-devkit/build-webpack" "0.1002.4" + "@angular-devkit/core" "10.2.4" + "@babel/core" "7.11.1" + "@babel/generator" "7.11.0" + "@babel/plugin-transform-runtime" "7.11.0" + "@babel/preset-env" "7.11.0" + "@babel/runtime" "7.11.2" + "@babel/template" "7.10.4" + "@jsdevtools/coverage-istanbul-loader" "3.0.5" + "@ngtools/webpack" "10.2.4" + autoprefixer "9.8.6" + babel-loader "8.1.0" + browserslist "^4.9.1" + cacache "15.0.5" + caniuse-lite "^1.0.30001032" + circular-dependency-plugin "5.2.0" + copy-webpack-plugin "6.0.3" + core-js "3.6.4" + css-loader "4.2.2" + cssnano "4.1.10" + file-loader "6.0.0" + find-cache-dir "3.3.1" + glob "7.1.6" + jest-worker "26.3.0" + karma-source-map-support "1.4.0" + less-loader "6.2.0" + license-webpack-plugin "2.3.0" + loader-utils "2.0.0" + mini-css-extract-plugin "0.10.0" + minimatch "3.0.4" + open "7.2.0" + parse5 "6.0.1" + parse5-htmlparser2-tree-adapter "6.0.1" + pnp-webpack-plugin "1.6.4" + postcss "7.0.32" + postcss-import "12.0.1" + postcss-loader "3.0.0" + raw-loader "4.0.1" + regenerator-runtime "0.13.7" + resolve-url-loader "3.1.2" + rimraf "3.0.2" + rollup "2.26.5" + rxjs "6.6.2" + sass "1.26.10" + sass-loader "10.0.1" + semver "7.3.2" + source-map "0.7.3" + source-map-loader "1.0.2" + source-map-support "0.5.19" + speed-measure-webpack-plugin "1.3.3" + style-loader "1.2.1" + stylus "0.54.8" + stylus-loader "3.0.2" + terser "5.3.0" + terser-webpack-plugin "4.1.0" + tree-kill "1.2.2" + webpack "4.44.1" + webpack-dev-middleware "3.7.2" + webpack-dev-server "3.11.0" + webpack-merge "4.2.2" + webpack-sources "1.4.3" + webpack-subresource-integrity "1.4.1" + worker-plugin "5.0.0" + +"@angular-devkit/build-optimizer@0.1002.4": + version "0.1002.4" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.1002.4.tgz#ddaa74e7e79cdc915631ec212780d6b7e2650c4a" + integrity sha512-O705v4N+VCaeTnePYVHf+XZaPxU8eTWCx2mYvCmG0urHh1GCehb+vX1v332tTaC2uzMoH+RSg2Nh2apFX+pE0Q== + dependencies: + loader-utils "2.0.0" + source-map "0.7.3" + tslib "2.0.1" + typescript "4.0.2" + webpack-sources "1.4.3" + +"@angular-devkit/build-webpack@0.1002.4": + version "0.1002.4" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1002.4.tgz#ed3a88a5c0af8a96ac0a14fa59ce66d4b7b850ac" + integrity sha512-5K+hPWmWV1q0HKcvJrTjJ5ABKEQintJlMMaewfmDUDOfslpabtXtY3LF+18a2RBdktAtLpIxoVTX1j/dvotu+w== + dependencies: + "@angular-devkit/architect" "0.1002.4" + "@angular-devkit/core" "10.2.4" + rxjs "6.6.2" + +"@angular-devkit/core@10.2.4": version "10.2.4" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-10.2.4.tgz#fb1772ea5780c96e00411900c54457f0cbcf401b" - integrity sha512-bBfsLJNDQaC2OI1mReDJuSZ/uBb7Pf3HVpRmlQKNIPllIxqX1hLH8I3Plodrns9m32JMJ6FMsQthcP0KMdRCJA== + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-10.2.4.tgz#b1f6b580406e4a497eeba54cf34013b88e36cb47" + integrity sha512-gnm/+Iyaa6Jt3E803bpTjkwDAIb0AhP9badaGwbx44+bhbNSE2WzOBmdsQrsxJXHAMEG9CGeBzeRd8XZtLACWg== dependencies: - tslib "^2.0.0" + ajv "6.12.4" + fast-json-stable-stringify "2.1.0" + magic-string "0.25.7" + rxjs "6.6.2" + source-map "0.7.3" -"@angular/core@^10.0.3": +"@angular-devkit/schematics@10.2.4": version "10.2.4" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-10.2.4.tgz#1124061f8232d79fcff7508c9243ec5ec3fc00f3" - integrity sha512-5xpAvmZwD9nZ8eWx10urjibqEeePGEiFXVMEn3IaJWgfdOcMmeSoioW9JUllT3w85+DlNVWbRbhz0YfE9a4jyw== + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-10.2.4.tgz#6f8bc7c0a5c4ac101460a0f709df33782782e6ad" + integrity sha512-poBGWRwMgnnnmoZfwyOBcQMJm7U5y5XxnxvMsBJEyAQRxfQa+KLvcCfGWXqskNTyBdQFpy4kxmtCzRClkoEiKQ== dependencies: - tslib "^2.0.0" + "@angular-devkit/core" "10.2.4" + ora "5.0.0" + rxjs "6.6.2" -"@angular/router@^10.0.3": +"@angular/cli@^10.2.4": version "10.2.4" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-10.2.4.tgz#0c6a680d8cbf8f5ce8b904636c8ee0e75765124d" - integrity sha512-y3xMwZHWS84fbm3FoU8vTAeXaTuPd4ZfmZ3dhkG9c1tkVq/jCmc6pkqNxjv3L1iPenKrvt2bFhh+wCs+bcUPhw== + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-10.2.4.tgz#f8899eee8f774cd805b1831a8f2f865024e9f4e1" + integrity sha512-S8xAJemX3zE/I/xi81DT6NuzfDwEAEtEeITHxrAH0AHE4kaUBy2O9bAopvYqMNzxs/XGqyxMv8vwYYpGax7EEQ== + dependencies: + "@angular-devkit/architect" "0.1002.4" + "@angular-devkit/core" "10.2.4" + "@angular-devkit/schematics" "10.2.4" + "@schematics/angular" "10.2.4" + "@schematics/update" "0.1002.4" + "@yarnpkg/lockfile" "1.1.0" + ansi-colors "4.1.1" + debug "4.1.1" + ini "1.3.6" + inquirer "7.3.3" + npm-package-arg "8.0.1" + npm-pick-manifest "6.1.0" + open "7.2.0" + pacote "9.5.12" + read-package-tree "5.3.1" + rimraf "3.0.2" + semver "7.3.2" + symbol-observable "1.2.0" + universal-analytics "0.4.23" + uuid "8.3.0" + +"@angular/common@~10.2.5": + version "10.2.5" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-10.2.5.tgz#5313f530446998e2f7af2dc43611addcfa6fd1c1" + integrity sha512-553yf6ZUHNqT4XpOqbW7EKKMfX56u/8DkwYXuSv8MAKdl4/AW6gliFOEJGYo04JcKF7Knq3VPvGSCO9kupf0hg== + dependencies: + tslib "^2.0.0" + +"@angular/compiler-cli@~10.2.5": + version "10.2.5" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-10.2.5.tgz#adb65bb9ecea14762a501226fde7760b73c3ab1e" + integrity sha512-xddSpKudoPidEebIW3x1CvQdx69WEmnFg4DneeQi/tit7mtAKYTJemzYZmP6abdSYhtxovL0bPX5LxYlrtuxIw== + dependencies: + canonical-path "1.0.0" + chokidar "^3.0.0" + convert-source-map "^1.5.1" + dependency-graph "^0.7.2" + fs-extra "4.0.2" + magic-string "^0.25.0" + minimist "^1.2.0" + reflect-metadata "^0.1.2" + semver "^6.3.0" + source-map "^0.6.1" + sourcemap-codec "^1.4.8" + tslib "^2.0.0" + yargs "^16.1.1" + +"@angular/compiler@^10.2.5": + version "10.2.5" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-10.2.5.tgz#1ff8514fdd2c07ff3c265b960dc49af6376071c9" + integrity sha512-ddJiTPCoVBIGjFDYoYWDpmq3Zs8UKoWpzaeW4u+p17gWW54HwyT5XTxrgtbeUmaxIuRdL4/KT1lGHs9/9bwbCA== + dependencies: + tslib "^2.0.0" + +"@angular/core@~10.2.5": + version "10.2.5" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-10.2.5.tgz#2050b0dbb180aa98c2ec46bba6d4827565ba2a2d" + integrity sha512-krhOKNTj5XE92Rk9ASX5KmgTF72j7qT2PLVxrGEVjuUKjBY2XaK3TV0Kotq9zI3qa9WgeCrP/Njn6jlKQCCAEQ== + dependencies: + tslib "^2.0.0" + +"@angular/router@~10.2.5": + version "10.2.5" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-10.2.5.tgz#acc75a29ab0b54c8ebad7d2a896986a59d7d99ec" + integrity sha512-AtSMB/d4V+pw/FL4G/mWWoiJJtZ/075TqsGW7uEFKgxS6Gh2kalv6BTMlXVG5GO+2oU0lsuDvguq5E7Atbak3Q== dependencies: tslib "^2.0.0" @@ -51,6 +229,11 @@ dependencies: "@babel/highlight" "^7.16.7" +"@babel/compat-data@^7.11.0", "@babel/compat-data@^7.17.0", "@babel/compat-data@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.7.tgz#078d8b833fbbcc95286613be8c716cef2b519fa2" + integrity sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ== + "@babel/compat-data@^7.13.0", "@babel/compat-data@^7.13.12", "@babel/compat-data@^7.13.8", "@babel/compat-data@^7.15.0": version "7.15.0" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.15.0.tgz#2dbaf8b85334796cafbb0f5793a90a2fc010b176" @@ -61,6 +244,28 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.0.tgz#ea269d7f78deb3a7826c39a4048eecda541ebdaa" integrity sha512-DGjt2QZse5SGd9nfOSqO4WLJ8NN/oHkijbXbPrxuoJO3oIPJL3TciZs9FX+cOHNiY9E9l0opL8g7BmLe3T+9ew== +"@babel/core@7.11.1": + version "7.11.1" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.1.tgz#2c55b604e73a40dc21b0e52650b11c65cf276643" + integrity sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.0" + "@babel/helper-module-transforms" "^7.11.0" + "@babel/helpers" "^7.10.4" + "@babel/parser" "^7.11.1" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.11.0" + "@babel/types" "^7.11.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + "@babel/core@^7.1.0", "@babel/core@^7.1.6", "@babel/core@^7.12.0", "@babel/core@^7.3.4": version "7.13.14" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.14.tgz#8e46ebbaca460a63497c797e574038ab04ae6d06" @@ -82,6 +287,27 @@ semver "^6.3.0" source-map "^0.5.0" +"@babel/core@^7.12.3", "@babel/core@^7.7.2", "@babel/core@^7.7.5", "@babel/core@^7.8.0": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.9.tgz#6bae81a06d95f4d0dec5bb9d74bbc1f58babdcfe" + integrity sha512-5ug+SfZCpDAkVp9SFIZAzlW18rlzsOcJGaetCjkySnrXXDUw9AR8cDUm1iByTmdWM6yxX6/zycaV76w3YTF2gw== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.9" + "@babel/helper-compilation-targets" "^7.17.7" + "@babel/helper-module-transforms" "^7.17.7" + "@babel/helpers" "^7.17.9" + "@babel/parser" "^7.17.9" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.9" + "@babel/types" "^7.17.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + "@babel/core@^7.12.9": version "7.15.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.15.5.tgz#f8ed9ace730722544609f90c9bb49162dc3bf5b9" @@ -124,6 +350,24 @@ semver "^6.3.0" source-map "^0.5.0" +"@babel/generator@7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.0.tgz#4b90c78d8c12825024568cbe83ee6c9af193585c" + integrity sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ== + dependencies: + "@babel/types" "^7.11.0" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/generator@^7.11.0", "@babel/generator@^7.17.9", "@babel/generator@^7.7.2": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.9.tgz#f4af9fd38fa8de143c29fce3f71852406fc1e2fc" + integrity sha512-rAdDousTwxbIxbz5I7GEQ3lUip+xVCXooZNbsydCWs3xA7ZsYOv+CFRdzGxRX78BmQHu9B1Eso59AOZQOJDEdQ== + dependencies: + "@babel/types" "^7.17.0" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/generator@^7.13.9", "@babel/generator@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.0.tgz#d40f3d1d5075e62d3500bccb67f3daa8a95265b2" @@ -180,6 +424,24 @@ "@babel/helper-explode-assignable-expression" "^7.12.13" "@babel/types" "^7.12.13" +"@babel/helper-builder-binary-assignment-operator-visitor@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz#38d138561ea207f0f69eb1626a418e4f7e6a580b" + integrity sha512-C6FdbRaxYjwVu/geKW4ZeQ0Q31AftgRcdSnZ5/jsH6BzCJbtvXvhpfkbkThYSuutZA7nCXpPR6AD9zd1dprMkA== + dependencies: + "@babel/helper-explode-assignable-expression" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/helper-compilation-targets@^7.10.4", "@babel/helper-compilation-targets@^7.16.7", "@babel/helper-compilation-targets@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz#a3c2924f5e5f0379b356d4cfb313d1414dc30e46" + integrity sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w== + dependencies: + "@babel/compat-data" "^7.17.7" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.17.5" + semver "^6.3.0" + "@babel/helper-compilation-targets@^7.12.0", "@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.13.10", "@babel/helper-compilation-targets@^7.13.8", "@babel/helper-compilation-targets@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz#cf6d94f30fbefc139123e27dd6b02f65aeedb7b9" @@ -223,6 +485,19 @@ "@babel/helper-replace-supers" "^7.16.0" "@babel/helper-split-export-declaration" "^7.16.0" +"@babel/helper-create-class-features-plugin@^7.16.10": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.9.tgz#71835d7fb9f38bd9f1378e40a4c0902fdc2ea49d" + integrity sha512-kUjip3gruz6AJKOq5i3nC6CoCEEF/oHH3cp6tOZhB+IyyyPyW0g1Gfsxn3mkk6S08pIA2y8GQh609v9G/5sHVQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-member-expression-to-functions" "^7.17.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-create-class-features-plugin@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.7.tgz#9c5b34b53a01f2097daf10678d65135c1b9f84ba" @@ -244,6 +519,14 @@ "@babel/helper-annotate-as-pure" "^7.12.13" regexpu-core "^4.7.1" +"@babel/helper-create-regexp-features-plugin@^7.16.7": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz#1dcc7d40ba0c6b6b25618997c5dbfd310f186fe1" + integrity sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + regexpu-core "^5.0.1" + "@babel/helper-define-polyfill-provider@^0.1.5": version "0.1.5" resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.1.5.tgz#3c2f91b7971b9fc11fe779c945c014065dea340e" @@ -272,6 +555,13 @@ dependencies: "@babel/types" "^7.13.0" +"@babel/helper-explode-assignable-expression@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz#12a6d8522fdd834f194e868af6354e8650242b7a" + integrity sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-function-name@^7.12.13", "@babel/helper-function-name@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz#845744dafc4381a4a5fb6afa6c3d36f98a787ebc" @@ -299,6 +589,14 @@ "@babel/template" "^7.16.7" "@babel/types" "^7.16.7" +"@babel/helper-function-name@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz#136fcd54bc1da82fcb47565cf16fd8e444b1ff12" + integrity sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg== + dependencies: + "@babel/template" "^7.16.7" + "@babel/types" "^7.17.0" + "@babel/helper-get-function-arity@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz#098818934a137fce78b536a3e015864be1e2879b" @@ -362,6 +660,20 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-member-expression-to-functions@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz#a34013b57d8542a8c4ff8ba3f747c02452a4d8c4" + integrity sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw== + dependencies: + "@babel/types" "^7.17.0" + +"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" + integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.15.4", "@babel/helper-module-imports@^7.8.3": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz#e18007d230632dea19b47853b984476e7b4e103f" @@ -376,6 +688,20 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-module-transforms@^7.11.0", "@babel/helper-module-transforms@^7.16.7", "@babel/helper-module-transforms@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz#3943c7f777139e7954a5355c815263741a9c1cbd" + integrity sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw== + dependencies: + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.3" + "@babel/types" "^7.17.0" + "@babel/helper-module-transforms@^7.13.0", "@babel/helper-module-transforms@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.15.4.tgz#962cc629a7f7f9a082dd62d0307fa75fe8788d7c" @@ -444,6 +770,15 @@ "@babel/helper-wrap-function" "^7.13.0" "@babel/types" "^7.13.0" +"@babel/helper-remap-async-to-generator@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3" + integrity sha512-fm0gH7Flb8H51LqJHy3HJ3wnE1+qtYR2A99K06ahwrawLdOFsCEWjZOrYricXJHoPSudNKxrMBUPEIPxiIIvBw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-wrap-function" "^7.16.8" + "@babel/types" "^7.16.8" + "@babel/helper-replace-supers@^7.12.13", "@babel/helper-replace-supers@^7.13.0", "@babel/helper-replace-supers@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz#52a8ab26ba918c7f6dee28628b07071ac7b7347a" @@ -489,6 +824,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-simple-access@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz#aaa473de92b7987c6dfa7ce9a7d9674724823367" + integrity sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA== + dependencies: + "@babel/types" "^7.17.0" + "@babel/helper-skip-transparent-expression-wrappers@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf" @@ -559,6 +901,25 @@ "@babel/traverse" "^7.13.0" "@babel/types" "^7.13.0" +"@babel/helper-wrap-function@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz#58afda087c4cd235de92f7ceedebca2c41274200" + integrity sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw== + dependencies: + "@babel/helper-function-name" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.8" + "@babel/types" "^7.16.8" + +"@babel/helpers@^7.10.4", "@babel/helpers@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.9.tgz#b2af120821bfbe44f9907b1826e168e819375a1a" + integrity sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.9" + "@babel/types" "^7.17.0" + "@babel/helpers@^7.13.10", "@babel/helpers@^7.16.0": version "7.16.3" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.3.tgz#27fc64f40b996e7074dc73128c3e5c3e7f55c43c" @@ -609,6 +970,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.2.tgz#0c1680aa44ad4605b16cbdcc5c341a61bde9c746" integrity sha512-IoVDIHpsgE/fu7eXBeRWt8zLbDrSvD7H1gpomOkPpBoEN8KCruCqSDdqo8dddwQQrui30KSvQBaMUOJiuFu6QQ== +"@babel/parser@^7.10.4", "@babel/parser@^7.11.1", "@babel/parser@^7.14.7", "@babel/parser@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.9.tgz#9c94189a6062f0291418ca021077983058e171ef" + integrity sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg== + "@babel/parser@^7.15.4", "@babel/parser@^7.15.5": version "7.15.6" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.6.tgz#043b9aa3c303c0722e5377fef9197f4cf1796549" @@ -633,6 +999,15 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" "@babel/plugin-proposal-optional-chaining" "^7.13.12" +"@babel/plugin-proposal-async-generator-functions@^7.10.4": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz#3bdd1ebbe620804ea9416706cd67d60787504bc8" + integrity sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-remap-async-to-generator" "^7.16.8" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-proposal-async-generator-functions@^7.13.8": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.8.tgz#87aacb574b3bc4b5603f6fe41458d72a5a2ec4b1" @@ -650,6 +1025,14 @@ "@babel/helper-create-class-features-plugin" "^7.13.0" "@babel/helper-plugin-utils" "^7.13.0" +"@babel/plugin-proposal-class-properties@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz#925cad7b3b1a2fcea7e59ecc8eb5954f961f91b0" + integrity sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-proposal-class-properties@^7.14.5": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.0.tgz#c029618267ddebc7280fa286e0f8ca2a278a2d1a" @@ -667,6 +1050,14 @@ "@babel/helper-plugin-utils" "^7.13.0" "@babel/plugin-syntax-decorators" "^7.12.13" +"@babel/plugin-proposal-dynamic-import@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz#c19c897eaa46b27634a00fee9fb7d829158704b2" + integrity sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-proposal-dynamic-import@^7.13.8": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.13.8.tgz#876a1f6966e1dec332e8c9451afda3bebcdf2e1d" @@ -683,6 +1074,14 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-dynamic-import" "^7.8.3" +"@babel/plugin-proposal-export-namespace-from@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz#09de09df18445a5786a305681423ae63507a6163" + integrity sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-proposal-export-namespace-from@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.13.tgz#393be47a4acd03fa2af6e3cde9b06e33de1b446d" @@ -699,6 +1098,14 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" +"@babel/plugin-proposal-json-strings@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.7.tgz#9732cb1d17d9a2626a08c5be25186c195b6fa6e8" + integrity sha512-lNZ3EEggsGY78JavgbHsK9u5P3pQaW7k4axlgFLYkMd7UBsiNahCITShLjNQschPyjtO6dADrL24757IdhBrsQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-proposal-json-strings@^7.13.8": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.13.8.tgz#bf1fb362547075afda3634ed31571c5901afef7b" @@ -707,6 +1114,14 @@ "@babel/helper-plugin-utils" "^7.13.0" "@babel/plugin-syntax-json-strings" "^7.8.3" +"@babel/plugin-proposal-logical-assignment-operators@^7.11.0": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz#be23c0ba74deec1922e639832904be0bea73cdea" + integrity sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-proposal-logical-assignment-operators@^7.13.8": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.13.8.tgz#93fa78d63857c40ce3c8c3315220fd00bfbb4e1a" @@ -723,6 +1138,14 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" +"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz#141fc20b6857e59459d430c850a0011e36561d99" + integrity sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-proposal-nullish-coalescing-operator@^7.13.8": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.13.8.tgz#3730a31dafd3c10d8ccd10648ed80a2ac5472ef3" @@ -739,6 +1162,14 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" +"@babel/plugin-proposal-numeric-separator@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.7.tgz#d6b69f4af63fb38b6ca2558442a7fb191236eba9" + integrity sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-proposal-numeric-separator@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.13.tgz#bd9da3188e787b5120b4f9d465a8261ce67ed1db" @@ -755,6 +1186,17 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-numeric-separator" "^7.10.4" +"@babel/plugin-proposal-object-rest-spread@^7.11.0": + version "7.17.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.17.3.tgz#d9eb649a54628a51701aef7e0ea3d17e2b9dd390" + integrity sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw== + dependencies: + "@babel/compat-data" "^7.17.0" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.16.7" + "@babel/plugin-proposal-object-rest-spread@^7.13.8": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.13.8.tgz#5d210a4d727d6ce3b18f9de82cc99a3964eed60a" @@ -766,6 +1208,14 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-transform-parameters" "^7.13.0" +"@babel/plugin-proposal-optional-catch-binding@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.7.tgz#c623a430674ffc4ab732fd0a0ae7722b67cb74cf" + integrity sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-proposal-optional-catch-binding@^7.13.8": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.13.8.tgz#3ad6bd5901506ea996fc31bdcf3ccfa2bed71107" @@ -774,6 +1224,15 @@ "@babel/helper-plugin-utils" "^7.13.0" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" +"@babel/plugin-proposal-optional-chaining@^7.11.0": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz#7cd629564724816c0e8a969535551f943c64c39a" + integrity sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-proposal-optional-chaining@^7.13.12": version "7.13.12" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.12.tgz#ba9feb601d422e0adea6760c2bd6bbb7bfec4866" @@ -792,6 +1251,14 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" "@babel/plugin-syntax-optional-chaining" "^7.8.3" +"@babel/plugin-proposal-private-methods@^7.10.4": + version "7.16.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.11.tgz#e8df108288555ff259f4527dbe84813aac3a1c50" + integrity sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.16.10" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-proposal-private-methods@^7.13.0": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.13.0.tgz#04bd4c6d40f6e6bbfa2f57e2d8094bad900ef787" @@ -818,6 +1285,14 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" +"@babel/plugin-proposal-unicode-property-regex@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.7.tgz#635d18eb10c6214210ffc5ff4932552de08188a2" + integrity sha512-QRK0YI/40VLhNVGIjRNAAQkEHws0cswSdFFjpFyt943YmJIU1da9uW63Iu6NFV6CxTZW5eTDCrwZUstBWgp/Rg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-proposal-unicode-property-regex@^7.12.13", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.13.tgz#bebde51339be829c17aaaaced18641deb62b39ba" @@ -826,14 +1301,21 @@ "@babel/helper-create-regexp-features-plugin" "^7.12.13" "@babel/helper-plugin-utils" "^7.12.13" -"@babel/plugin-syntax-async-generators@^7.8.4": +"@babel/plugin-syntax-async-generators@^7.8.0", "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.12.13": +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.10.4", "@babel/plugin-syntax-class-properties@^7.12.13", "@babel/plugin-syntax-class-properties@^7.8.3": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== @@ -847,7 +1329,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.12.13" -"@babel/plugin-syntax-dynamic-import@^7.8.3": +"@babel/plugin-syntax-dynamic-import@^7.8.0", "@babel/plugin-syntax-dynamic-import@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== @@ -861,49 +1343,56 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-json-strings@^7.8.3": +"@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.0", "@babel/plugin-syntax-json-strings@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-numeric-separator@^7.10.4": +"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": +"@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": +"@babel/plugin-syntax-optional-catch-binding@^7.8.0", "@babel/plugin-syntax-optional-catch-binding@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-optional-chaining@^7.8.3": +"@babel/plugin-syntax-optional-chaining@^7.8.0", "@babel/plugin-syntax-optional-chaining@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== @@ -917,6 +1406,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" +"@babel/plugin-syntax-top-level-await@^7.10.4", "@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-top-level-await@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.13.tgz#c5f0fa6e249f5b739727f923540cf7a806130178" @@ -938,13 +1434,20 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-typescript@^7.16.7": +"@babel/plugin-syntax-typescript@^7.16.7", "@babel/plugin-syntax-typescript@^7.7.2": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz#39c9b55ee153151990fb038651d58d3fd03f98f8" integrity sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A== dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-arrow-functions@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz#44125e653d94b98db76369de9c396dc14bef4154" + integrity sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-arrow-functions@^7.13.0": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.13.0.tgz#10a59bebad52d637a027afa692e8d5ceff5e3dae" @@ -952,6 +1455,15 @@ dependencies: "@babel/helper-plugin-utils" "^7.13.0" +"@babel/plugin-transform-async-to-generator@^7.10.4": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz#b83dff4b970cf41f1b819f8b49cc0cfbaa53a808" + integrity sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-remap-async-to-generator" "^7.16.8" + "@babel/plugin-transform-async-to-generator@^7.13.0": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.13.0.tgz#8e112bf6771b82bf1e974e5e26806c5c99aa516f" @@ -961,6 +1473,13 @@ "@babel/helper-plugin-utils" "^7.13.0" "@babel/helper-remap-async-to-generator" "^7.13.0" +"@babel/plugin-transform-block-scoped-functions@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.7.tgz#4d0d57d9632ef6062cdf354bb717102ee042a620" + integrity sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-block-scoped-functions@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.13.tgz#a9bf1836f2a39b4eb6cf09967739de29ea4bf4c4" @@ -968,6 +1487,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.12.13" +"@babel/plugin-transform-block-scoping@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.7.tgz#f50664ab99ddeaee5bc681b8f3a6ea9d72ab4f87" + integrity sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-block-scoping@^7.12.13", "@babel/plugin-transform-block-scoping@^7.8.3": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.13.tgz#f36e55076d06f41dfd78557ea039c1b581642e61" @@ -975,6 +1501,20 @@ dependencies: "@babel/helper-plugin-utils" "^7.12.13" +"@babel/plugin-transform-classes@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz#8f4b9562850cd973de3b498f1218796eb181ce00" + integrity sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + globals "^11.1.0" + "@babel/plugin-transform-classes@^7.13.0": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.13.0.tgz#0265155075c42918bf4d3a4053134176ad9b533b" @@ -988,6 +1528,13 @@ "@babel/helper-split-export-declaration" "^7.12.13" globals "^11.1.0" +"@babel/plugin-transform-computed-properties@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.7.tgz#66dee12e46f61d2aae7a73710f591eb3df616470" + integrity sha512-gN72G9bcmenVILj//sv1zLNaPyYcOzUho2lIJBMh/iakJ9ygCo/hEF9cpGb61SCMEDxbbyBoVQxrt+bWKu5KGw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-computed-properties@^7.13.0": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.13.0.tgz#845c6e8b9bb55376b1fa0b92ef0bdc8ea06644ed" @@ -995,6 +1542,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.13.0" +"@babel/plugin-transform-destructuring@^7.10.4": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.7.tgz#49dc2675a7afa9a5e4c6bdee636061136c3408d1" + integrity sha512-XVh0r5yq9sLR4vZ6eVZe8FKfIcSgaTBxVBRSYokRj2qksf6QerYnTxz9/GTuKTH/n/HwLP7t6gtlybHetJ/6hQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-destructuring@^7.13.0": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.13.0.tgz#c5dce270014d4e1ebb1d806116694c12b7028963" @@ -1002,6 +1556,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.13.0" +"@babel/plugin-transform-dotall-regex@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.7.tgz#6b2d67686fab15fb6a7fd4bd895d5982cfc81241" + integrity sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-dotall-regex@^7.12.13", "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.13.tgz#3f1601cc29905bfcb67f53910f197aeafebb25ad" @@ -1010,6 +1572,13 @@ "@babel/helper-create-regexp-features-plugin" "^7.12.13" "@babel/helper-plugin-utils" "^7.12.13" +"@babel/plugin-transform-duplicate-keys@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.7.tgz#2207e9ca8f82a0d36a5a67b6536e7ef8b08823c9" + integrity sha512-03DvpbRfvWIXyK0/6QiR1KMTWeT6OcQ7tbhjrXyFS02kjuX/mu5Bvnh5SDSWHxyawit2g5aWhKwI86EE7GUnTw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-duplicate-keys@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.13.tgz#6f06b87a8b803fd928e54b81c258f0a0033904de" @@ -1017,6 +1586,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.12.13" +"@babel/plugin-transform-exponentiation-operator@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.7.tgz#efa9862ef97e9e9e5f653f6ddc7b665e8536fe9b" + integrity sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-exponentiation-operator@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.13.tgz#4d52390b9a273e651e4aba6aee49ef40e80cd0a1" @@ -1025,6 +1602,13 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.12.13" "@babel/helper-plugin-utils" "^7.12.13" +"@babel/plugin-transform-for-of@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.7.tgz#649d639d4617dff502a9a158c479b3b556728d8c" + integrity sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-for-of@^7.13.0": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.13.0.tgz#c799f881a8091ac26b54867a845c3e97d2696062" @@ -1032,6 +1616,15 @@ dependencies: "@babel/helper-plugin-utils" "^7.13.0" +"@babel/plugin-transform-function-name@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.7.tgz#5ab34375c64d61d083d7d2f05c38d90b97ec65cf" + integrity sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA== + dependencies: + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-function-name@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.13.tgz#bb024452f9aaed861d374c8e7a24252ce3a50051" @@ -1040,6 +1633,13 @@ "@babel/helper-function-name" "^7.12.13" "@babel/helper-plugin-utils" "^7.12.13" +"@babel/plugin-transform-literals@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.7.tgz#254c9618c5ff749e87cb0c0cef1a0a050c0bdab1" + integrity sha512-6tH8RTpTWI0s2sV6uq3e/C9wPo4PTqqZps4uF0kzQ9/xPLFQtipynvmT1g/dOfEJ+0EQsHhkQ/zyRId8J2b8zQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-literals@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.13.tgz#2ca45bafe4a820197cf315794a4d26560fe4bdb9" @@ -1047,6 +1647,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.12.13" +"@babel/plugin-transform-member-expression-literals@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.7.tgz#6e5dcf906ef8a098e630149d14c867dd28f92384" + integrity sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-member-expression-literals@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.13.tgz#5ffa66cd59b9e191314c9f1f803b938e8c081e40" @@ -1054,6 +1661,15 @@ dependencies: "@babel/helper-plugin-utils" "^7.12.13" +"@babel/plugin-transform-modules-amd@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.7.tgz#b28d323016a7daaae8609781d1f8c9da42b13186" + integrity sha512-KaaEtgBL7FKYwjJ/teH63oAmE3lP34N3kshz8mm4VMAw7U3PxjVwwUmxEFksbgsNUaO3wId9R2AVQYSEGRa2+g== + dependencies: + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + babel-plugin-dynamic-import-node "^2.3.3" + "@babel/plugin-transform-modules-amd@^7.12.1", "@babel/plugin-transform-modules-amd@^7.13.0": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.13.0.tgz#19f511d60e3d8753cc5a6d4e775d3a5184866cc3" @@ -1063,7 +1679,17 @@ "@babel/helper-plugin-utils" "^7.13.0" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.13.8": +"@babel/plugin-transform-modules-commonjs@^7.10.4": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.9.tgz#274be1a2087beec0254d4abd4d86e52442e1e5b6" + integrity sha512-2TBFd/r2I6VlYn0YRTz2JdazS+FoUuQ2rIFHoAxtyP/0G3D82SBLaRq9rnUkpqlLg03Byfl/+M32mpxjO6KaPw== + dependencies: + "@babel/helper-module-transforms" "^7.17.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-commonjs@^7.13.8": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.13.8.tgz#7b01ad7c2dcf2275b06fa1781e00d13d420b3e1b" integrity sha512-9QiOx4MEGglfYZ4XOnU79OHr6vIWUakIj9b4mioN8eQIoEh+pf5p/zEB36JpDFWA12nNMiRf7bfoRvl9Rn79Bw== @@ -1083,6 +1709,17 @@ "@babel/helper-simple-access" "^7.16.0" babel-plugin-dynamic-import-node "^2.3.3" +"@babel/plugin-transform-modules-systemjs@^7.10.4": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.17.8.tgz#81fd834024fae14ea78fbe34168b042f38703859" + integrity sha512-39reIkMTUVagzgA5x88zDYXPCMT6lcaRKs1+S9K6NKBPErbgO/w/kP8GlNQTC87b412ZTlmNgr3k2JrWgHH+Bw== + dependencies: + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-module-transforms" "^7.17.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + babel-plugin-dynamic-import-node "^2.3.3" + "@babel/plugin-transform-modules-systemjs@^7.13.8": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.13.8.tgz#6d066ee2bff3c7b3d60bf28dec169ad993831ae3" @@ -1094,6 +1731,14 @@ "@babel/helper-validator-identifier" "^7.12.11" babel-plugin-dynamic-import-node "^2.3.3" +"@babel/plugin-transform-modules-umd@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.7.tgz#23dad479fa585283dbd22215bff12719171e7618" + integrity sha512-EMh7uolsC8O4xhudF2F6wedbSHm1HHZ0C6aJ7K67zcDNidMzVcxWdGr+htW9n21klm+bOn+Rx4CBsAntZd3rEQ== + dependencies: + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-modules-umd@^7.13.0": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.13.0.tgz#8a3d96a97d199705b9fd021580082af81c06e70b" @@ -1102,6 +1747,13 @@ "@babel/helper-module-transforms" "^7.13.0" "@babel/helper-plugin-utils" "^7.13.0" +"@babel/plugin-transform-named-capturing-groups-regex@^7.10.4": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.8.tgz#7f860e0e40d844a02c9dcf9d84965e7dfd666252" + integrity sha512-j3Jw+n5PvpmhRR+mrgIh04puSANCk/T/UA3m3P1MjJkhlK906+ApHhDIqBQDdOgL/r1UYpz4GNclTXxyZrYGSw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.16.7" + "@babel/plugin-transform-named-capturing-groups-regex@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.13.tgz#2213725a5f5bbbe364b50c3ba5998c9599c5c9d9" @@ -1109,6 +1761,13 @@ dependencies: "@babel/helper-create-regexp-features-plugin" "^7.12.13" +"@babel/plugin-transform-new-target@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.7.tgz#9967d89a5c243818e0800fdad89db22c5f514244" + integrity sha512-xiLDzWNMfKoGOpc6t3U+etCE2yRnn3SM09BXqWPIZOBpL2gvVrBWUKnsJx0K/ADi5F5YC5f8APFfWrz25TdlGg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-new-target@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.13.tgz#e22d8c3af24b150dd528cbd6e685e799bf1c351c" @@ -1123,6 +1782,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" +"@babel/plugin-transform-object-super@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz#ac359cf8d32cf4354d27a46867999490b6c32a94" + integrity sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/plugin-transform-object-super@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.13.tgz#b4416a2d63b8f7be314f3d349bd55a9c1b5171f7" @@ -1131,6 +1798,13 @@ "@babel/helper-plugin-utils" "^7.12.13" "@babel/helper-replace-supers" "^7.12.13" +"@babel/plugin-transform-parameters@^7.10.4", "@babel/plugin-transform-parameters@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.7.tgz#a1721f55b99b736511cb7e0152f61f17688f331f" + integrity sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-parameters@^7.13.0": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.13.0.tgz#8fa7603e3097f9c0b7ca1a4821bc2fb52e9e5007" @@ -1138,6 +1812,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.13.0" +"@babel/plugin-transform-property-literals@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.7.tgz#2dadac85155436f22c696c4827730e0fe1057a55" + integrity sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-property-literals@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.13.tgz#4e6a9e37864d8f1b3bc0e2dce7bf8857db8b1a81" @@ -1145,6 +1826,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.12.13" +"@babel/plugin-transform-regenerator@^7.10.4": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.17.9.tgz#0a33c3a61cf47f45ed3232903683a0afd2d3460c" + integrity sha512-Lc2TfbxR1HOyn/c6b4Y/b6NHoTb67n/IoWLxTu4kC7h4KQnWlhCq2S8Tx0t2SVvv5Uu87Hs+6JEJ5kt2tYGylQ== + dependencies: + regenerator-transform "^0.15.0" + "@babel/plugin-transform-regenerator@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.13.tgz#b628bcc9c85260ac1aeb05b45bde25210194a2f5" @@ -1152,6 +1840,13 @@ dependencies: regenerator-transform "^0.14.2" +"@babel/plugin-transform-reserved-words@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.7.tgz#1d798e078f7c5958eec952059c460b220a63f586" + integrity sha512-KQzzDnZ9hWQBjwi5lpY5v9shmm6IVG0U9pB18zvMu2i4H90xpT4gmqwPYsn8rObiadYe2M0gmgsiOIF5A/2rtg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-reserved-words@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.13.tgz#7d9988d4f06e0fe697ea1d9803188aa18b472695" @@ -1159,6 +1854,16 @@ dependencies: "@babel/helper-plugin-utils" "^7.12.13" +"@babel/plugin-transform-runtime@7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.11.0.tgz#e27f78eb36f19448636e05c33c90fd9ad9b8bccf" + integrity sha512-LFEsP+t3wkYBlis8w6/kmnd6Kb1dxTd+wGJ8MlxTGzQo//ehtqlVL4S9DNUa53+dtPSQobN2CXx4d81FqC58cw== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + resolve "^1.8.1" + semver "^5.5.1" + "@babel/plugin-transform-runtime@^7.13.9": version "7.13.10" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.10.tgz#a1e40d22e2bf570c591c9c7e5ab42d6bf1e419e1" @@ -1171,6 +1876,13 @@ babel-plugin-polyfill-regenerator "^0.1.2" semver "^6.3.0" +"@babel/plugin-transform-shorthand-properties@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz#e8549ae4afcf8382f711794c0c7b6b934c5fbd2a" + integrity sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-shorthand-properties@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.13.tgz#db755732b70c539d504c6390d9ce90fe64aff7ad" @@ -1178,6 +1890,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.12.13" +"@babel/plugin-transform-spread@^7.11.0": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.7.tgz#a303e2122f9f12e0105daeedd0f30fb197d8ff44" + integrity sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" + "@babel/plugin-transform-spread@^7.13.0": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.13.0.tgz#84887710e273c1815ace7ae459f6f42a5d31d5fd" @@ -1186,6 +1906,13 @@ "@babel/helper-plugin-utils" "^7.13.0" "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" +"@babel/plugin-transform-sticky-regex@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.7.tgz#c84741d4f4a38072b9a1e2e3fd56d359552e8660" + integrity sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-sticky-regex@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.13.tgz#760ffd936face73f860ae646fb86ee82f3d06d1f" @@ -1193,6 +1920,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.12.13" +"@babel/plugin-transform-template-literals@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.7.tgz#f3d1c45d28967c8e80f53666fc9c3e50618217ab" + integrity sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-template-literals@^7.13.0": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.13.0.tgz#a36049127977ad94438dee7443598d1cefdf409d" @@ -1200,6 +1934,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.13.0" +"@babel/plugin-transform-typeof-symbol@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.7.tgz#9cdbe622582c21368bd482b660ba87d5545d4f7e" + integrity sha512-p2rOixCKRJzpg9JB4gjnG4gjWkWa89ZoYUnl9snJ1cWIcTH/hvxZqfO+WjG6T8DRBpctEol5jw1O5rA8gkCokQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-typeof-symbol@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.13.tgz#785dd67a1f2ea579d9c2be722de8c84cb85f5a7f" @@ -1251,6 +1992,13 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-typescript" "^7.2.0" +"@babel/plugin-transform-unicode-escapes@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz#da8717de7b3287a2c6d659750c964f302b31ece3" + integrity sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-unicode-escapes@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.13.tgz#840ced3b816d3b5127dd1d12dcedc5dead1a5e74" @@ -1258,6 +2006,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.12.13" +"@babel/plugin-transform-unicode-regex@^7.10.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.7.tgz#0f7aa4a501198976e25e82702574c34cfebe9ef2" + integrity sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-transform-unicode-regex@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.13.tgz#b52521685804e155b1202e83fc188d34bb70f5ac" @@ -1274,6 +2030,80 @@ core-js "^2.6.5" regenerator-runtime "^0.13.4" +"@babel/preset-env@7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.0.tgz#860ee38f2ce17ad60480c2021ba9689393efb796" + integrity sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg== + dependencies: + "@babel/compat-data" "^7.11.0" + "@babel/helper-compilation-targets" "^7.10.4" + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-proposal-async-generator-functions" "^7.10.4" + "@babel/plugin-proposal-class-properties" "^7.10.4" + "@babel/plugin-proposal-dynamic-import" "^7.10.4" + "@babel/plugin-proposal-export-namespace-from" "^7.10.4" + "@babel/plugin-proposal-json-strings" "^7.10.4" + "@babel/plugin-proposal-logical-assignment-operators" "^7.11.0" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4" + "@babel/plugin-proposal-numeric-separator" "^7.10.4" + "@babel/plugin-proposal-object-rest-spread" "^7.11.0" + "@babel/plugin-proposal-optional-catch-binding" "^7.10.4" + "@babel/plugin-proposal-optional-chaining" "^7.11.0" + "@babel/plugin-proposal-private-methods" "^7.10.4" + "@babel/plugin-proposal-unicode-property-regex" "^7.10.4" + "@babel/plugin-syntax-async-generators" "^7.8.0" + "@babel/plugin-syntax-class-properties" "^7.10.4" + "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.0" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.0" + "@babel/plugin-syntax-top-level-await" "^7.10.4" + "@babel/plugin-transform-arrow-functions" "^7.10.4" + "@babel/plugin-transform-async-to-generator" "^7.10.4" + "@babel/plugin-transform-block-scoped-functions" "^7.10.4" + "@babel/plugin-transform-block-scoping" "^7.10.4" + "@babel/plugin-transform-classes" "^7.10.4" + "@babel/plugin-transform-computed-properties" "^7.10.4" + "@babel/plugin-transform-destructuring" "^7.10.4" + "@babel/plugin-transform-dotall-regex" "^7.10.4" + "@babel/plugin-transform-duplicate-keys" "^7.10.4" + "@babel/plugin-transform-exponentiation-operator" "^7.10.4" + "@babel/plugin-transform-for-of" "^7.10.4" + "@babel/plugin-transform-function-name" "^7.10.4" + "@babel/plugin-transform-literals" "^7.10.4" + "@babel/plugin-transform-member-expression-literals" "^7.10.4" + "@babel/plugin-transform-modules-amd" "^7.10.4" + "@babel/plugin-transform-modules-commonjs" "^7.10.4" + "@babel/plugin-transform-modules-systemjs" "^7.10.4" + "@babel/plugin-transform-modules-umd" "^7.10.4" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.4" + "@babel/plugin-transform-new-target" "^7.10.4" + "@babel/plugin-transform-object-super" "^7.10.4" + "@babel/plugin-transform-parameters" "^7.10.4" + "@babel/plugin-transform-property-literals" "^7.10.4" + "@babel/plugin-transform-regenerator" "^7.10.4" + "@babel/plugin-transform-reserved-words" "^7.10.4" + "@babel/plugin-transform-shorthand-properties" "^7.10.4" + "@babel/plugin-transform-spread" "^7.11.0" + "@babel/plugin-transform-sticky-regex" "^7.10.4" + "@babel/plugin-transform-template-literals" "^7.10.4" + "@babel/plugin-transform-typeof-symbol" "^7.10.4" + "@babel/plugin-transform-unicode-escapes" "^7.10.4" + "@babel/plugin-transform-unicode-regex" "^7.10.4" + "@babel/preset-modules" "^0.1.3" + "@babel/types" "^7.11.0" + browserslist "^4.12.0" + core-js-compat "^3.6.2" + invariant "^2.2.2" + levenary "^1.1.1" + semver "^5.5.0" + "@babel/preset-env@^7.10.2", "@babel/preset-env@^7.12.0": version "7.13.12" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.12.tgz#6dff470478290582ac282fb77780eadf32480237" @@ -1349,6 +2179,17 @@ core-js-compat "^3.9.0" semver "^6.3.0" +"@babel/preset-modules@^0.1.3": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" + integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + "@babel/preset-modules@^0.1.4": version "0.1.4" resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.4.tgz#362f2b68c662842970fdb5e254ffc8fc1c2e415e" @@ -1378,6 +2219,13 @@ "@babel/helper-validator-option" "^7.16.7" "@babel/plugin-transform-typescript" "^7.16.7" +"@babel/runtime@7.11.2": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" + integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@7.12.18": version "7.12.18" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.18.tgz#af137bd7e7d9705a412b3caaf991fe6aaa97831b" @@ -1399,6 +2247,31 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.7.6": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72" + integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/template@7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" + integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/template@^7.10.4", "@babel/template@^7.16.7", "@babel/template@^7.3.3": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" + integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/types" "^7.16.7" + "@babel/template@^7.12.13", "@babel/template@^7.15.4", "@babel/template@^7.4.0": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.15.4.tgz#51898d35dcf3faa670c4ee6afcfd517ee139f194" @@ -1417,16 +2290,7 @@ "@babel/parser" "^7.16.0" "@babel/types" "^7.16.0" -"@babel/template@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" - integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== - dependencies: - "@babel/code-frame" "^7.16.7" - "@babel/parser" "^7.16.7" - "@babel/types" "^7.16.7" - -"@babel/traverse@^7.1.0", "@babel/traverse@^7.1.6", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.13", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0": +"@babel/traverse@^7.1.6", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.13", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0": version "7.13.13" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.13.tgz#39aa9c21aab69f74d948a486dd28a2dbdbf5114d" integrity sha512-CblEcwmXKR6eP43oQGG++0QMTtCjAsa3frUuzHoiIJWpaIIi8dwMyEFUJoXRLxagGqCK+jALRwIO+o3R9p/uUg== @@ -1440,6 +2304,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.11.0", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9", "@babel/traverse@^7.7.2": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.9.tgz#1f9b207435d9ae4a8ed6998b2b82300d83c37a0d" + integrity sha512-PQO8sDIJ8SIwipTPiR71kJQCKQYB5NGImbOviK8K+kg5xkNSYXLBupuX9QhatFowrsvo9Hj8WgArg3W7ijNAQw== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.9" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.17.9" + "@babel/types" "^7.17.0" + debug "^4.1.0" + globals "^11.1.0" + "@babel/traverse@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.15.4.tgz#ff8510367a144bfbff552d9e18e28f3e2889c22d" @@ -1504,6 +2384,14 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.10.4", "@babel/types@^7.11.0", "@babel/types@^7.17.0", "@babel/types@^7.3.3": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b" + integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + "@babel/types@^7.15.4": version "7.15.6" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.6.tgz#99abdc48218b2881c058dd0a7ab05b99c9be758f" @@ -1528,6 +2416,11 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + "@cnakazawa/watch@^1.0.3": version "1.0.4" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" @@ -1536,6 +2429,18 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@cspotcode/source-map-consumer@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" + integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== + +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== + dependencies: + "@cspotcode/source-map-consumer" "0.8.0" + "@ember-data/rfc395-data@^0.0.4": version "0.0.4" resolved "https://registry.yarnpkg.com/@ember-data/rfc395-data/-/rfc395-data-0.0.4.tgz#ecb86efdf5d7733a76ff14ea651a1b0ed1f8a843" @@ -1633,24 +2538,10 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@fimbul/bifrost@^0.21.0": - version "0.21.0" - resolved "https://registry.yarnpkg.com/@fimbul/bifrost/-/bifrost-0.21.0.tgz#d0fafa25938fda475657a6a1e407a21bbe02c74e" - integrity sha512-ou8VU+nTmOW1jeg+FT+sn+an/M0Xb9G16RucrfhjXGWv1Q97kCoM5CG9Qj7GYOSdu7km72k7nY83Eyr53Bkakg== - dependencies: - "@fimbul/ymir" "^0.21.0" - get-caller-file "^2.0.0" - tslib "^1.8.1" - tsutils "^3.5.0" - -"@fimbul/ymir@^0.21.0": - version "0.21.0" - resolved "https://registry.yarnpkg.com/@fimbul/ymir/-/ymir-0.21.0.tgz#8525726787aceeafd4e199472c0d795160b5d4a1" - integrity sha512-T/y7WqPsm4n3zhT08EpB5sfdm2Kvw3gurAxr2Lr5dQeLi8ZsMlNT/Jby+ZmuuAAd1PnXYzKp+2SXgIkQIIMCUg== - dependencies: - inversify "^5.0.0" - reflect-metadata "^0.1.12" - tslib "^1.8.1" +"@gar/promisify@^1.0.1": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" + integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== "@glimmer/component@~1.0.0": version "1.0.4" @@ -1953,7 +2844,23 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== -"@jest/console@^24.7.1", "@jest/console@^24.9.0": +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0" integrity sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ== @@ -1962,39 +2869,51 @@ chalk "^2.0.1" slash "^2.0.0" -"@jest/core@^24.9.0": - version "24.9.0" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.9.0.tgz#2ceccd0b93181f9c4850e74f2a9ad43d351369c4" - integrity sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A== +"@jest/console@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.5.1.tgz#260fe7239602fe5130a94f1aa386eff54b014bba" + integrity sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg== dependencies: - "@jest/console" "^24.7.1" - "@jest/reporters" "^24.9.0" - "@jest/test-result" "^24.9.0" - "@jest/transform" "^24.9.0" - "@jest/types" "^24.9.0" - ansi-escapes "^3.0.0" - chalk "^2.0.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^27.5.1" + jest-util "^27.5.1" + slash "^3.0.0" + +"@jest/core@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.5.1.tgz#267ac5f704e09dc52de2922cbf3af9edcd64b626" + integrity sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ== + dependencies: + "@jest/console" "^27.5.1" + "@jest/reporters" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.8.1" exit "^0.1.2" - graceful-fs "^4.1.15" - jest-changed-files "^24.9.0" - jest-config "^24.9.0" - jest-haste-map "^24.9.0" - jest-message-util "^24.9.0" - jest-regex-util "^24.3.0" - jest-resolve "^24.9.0" - jest-resolve-dependencies "^24.9.0" - jest-runner "^24.9.0" - jest-runtime "^24.9.0" - jest-snapshot "^24.9.0" - jest-util "^24.9.0" - jest-validate "^24.9.0" - jest-watcher "^24.9.0" - micromatch "^3.1.10" - p-each-series "^1.0.0" - realpath-native "^1.1.0" - rimraf "^2.5.4" - slash "^2.0.0" - strip-ansi "^5.0.0" + graceful-fs "^4.2.9" + jest-changed-files "^27.5.1" + jest-config "^27.5.1" + jest-haste-map "^27.5.1" + jest-message-util "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-resolve-dependencies "^27.5.1" + jest-runner "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + jest-watcher "^27.5.1" + micromatch "^4.0.4" + rimraf "^3.0.0" + slash "^3.0.0" + strip-ansi "^6.0.0" "@jest/environment@^24.9.0": version "24.9.0" @@ -2006,6 +2925,16 @@ "@jest/types" "^24.9.0" jest-mock "^24.9.0" +"@jest/environment@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.5.1.tgz#d7425820511fe7158abbecc010140c3fd3be9c74" + integrity sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA== + dependencies: + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + jest-mock "^27.5.1" + "@jest/fake-timers@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.9.0.tgz#ba3e6bf0eecd09a636049896434d306636540c93" @@ -2015,34 +2944,59 @@ jest-message-util "^24.9.0" jest-mock "^24.9.0" -"@jest/reporters@^24.9.0": - version "24.9.0" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.9.0.tgz#86660eff8e2b9661d042a8e98a028b8d631a5b43" - integrity sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw== +"@jest/fake-timers@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74" + integrity sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ== dependencies: - "@jest/environment" "^24.9.0" - "@jest/test-result" "^24.9.0" - "@jest/transform" "^24.9.0" - "@jest/types" "^24.9.0" - chalk "^2.0.1" + "@jest/types" "^27.5.1" + "@sinonjs/fake-timers" "^8.0.1" + "@types/node" "*" + jest-message-util "^27.5.1" + jest-mock "^27.5.1" + jest-util "^27.5.1" + +"@jest/globals@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.5.1.tgz#7ac06ce57ab966566c7963431cef458434601b2b" + integrity sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/types" "^27.5.1" + expect "^27.5.1" + +"@jest/reporters@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.5.1.tgz#ceda7be96170b03c923c37987b64015812ffec04" + integrity sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" exit "^0.1.2" glob "^7.1.2" - istanbul-lib-coverage "^2.0.2" - istanbul-lib-instrument "^3.0.1" - istanbul-lib-report "^2.0.4" - istanbul-lib-source-maps "^3.0.1" - istanbul-reports "^2.2.6" - jest-haste-map "^24.9.0" - jest-resolve "^24.9.0" - jest-runtime "^24.9.0" - jest-util "^24.9.0" - jest-worker "^24.6.0" - node-notifier "^5.4.2" - slash "^2.0.0" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^5.1.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-haste-map "^27.5.1" + jest-resolve "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" + slash "^3.0.0" source-map "^0.6.0" - string-length "^2.0.0" + string-length "^4.0.1" + terminal-link "^2.0.0" + v8-to-istanbul "^8.1.0" -"@jest/source-map@^24.3.0", "@jest/source-map@^24.9.0": +"@jest/source-map@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714" integrity sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg== @@ -2051,6 +3005,15 @@ graceful-fs "^4.1.15" source-map "^0.6.0" +"@jest/source-map@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.5.1.tgz#6608391e465add4205eae073b55e7f279e04e8cf" + integrity sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.2.9" + source-map "^0.6.0" + "@jest/test-result@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.9.0.tgz#11796e8aa9dbf88ea025757b3152595ad06ba0ca" @@ -2060,15 +3023,25 @@ "@jest/types" "^24.9.0" "@types/istanbul-lib-coverage" "^2.0.0" -"@jest/test-sequencer@^24.9.0": - version "24.9.0" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz#f8f334f35b625a4f2f355f2fe7e6036dad2e6b31" - integrity sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A== +"@jest/test-result@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.5.1.tgz#56a6585fa80f7cdab72b8c5fc2e871d03832f5bb" + integrity sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag== dependencies: - "@jest/test-result" "^24.9.0" - jest-haste-map "^24.9.0" - jest-runner "^24.9.0" - jest-runtime "^24.9.0" + "@jest/console" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz#4057e0e9cea4439e544c6353c6affe58d095745b" + integrity sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ== + dependencies: + "@jest/test-result" "^27.5.1" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-runtime "^27.5.1" "@jest/transform@^24.9.0": version "24.9.0" @@ -2092,6 +3065,27 @@ source-map "^0.6.1" write-file-atomic "2.4.1" +"@jest/transform@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409" + integrity sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^27.5.1" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-regex-util "^27.5.1" + jest-util "^27.5.1" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + source-map "^0.6.1" + write-file-atomic "^3.0.0" + "@jest/types@>=24 <=26": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" @@ -2123,6 +3117,46 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" +"@jest/types@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" + integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^16.0.0" + chalk "^4.0.0" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c" + integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.11" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" + integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== + +"@jridgewell/trace-mapping@^0.3.0": + version "0.3.4" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3" + integrity sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jsdevtools/coverage-istanbul-loader@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz#2a4bc65d0271df8d4435982db4af35d81754ee26" + integrity sha512-EUCPEkaRPvmHjWAAZkWMT7JDzpw7FKB00WTISaiXsbNOd5hCHg77XLA8sLYLFDo1zepYLo2w7GstN8YBqRXZfA== + dependencies: + convert-source-map "^1.7.0" + istanbul-lib-instrument "^4.0.3" + loader-utils "^2.0.0" + merge-source-map "^1.1.0" + schema-utils "^2.7.0" + "@lerna/add@3.13.3": version "3.13.3" resolved "https://registry.yarnpkg.com/@lerna/add/-/add-3.13.3.tgz#f4c1674839780e458f0426d4f7b6d0a77b9a2ae9" @@ -2784,6 +3818,15 @@ resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-10.1.3.tgz#65b3e1b9846c02452787fde1d54ad9c54b506dbd" integrity sha512-P4GJZuLKfD/o42JvGZ/xP4Hxg68vd3NeZxOLqIuQKFjjaYgC2IrO+lE5PTwGmRkytjfprJC+9j7Jss/xQAS6QA== +"@ngtools/webpack@10.2.4": + version "10.2.4" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-10.2.4.tgz#6060970d1de053b5c33f5dd649ffb9f34e6b74a7" + integrity sha512-7rnGrd0TlnAHwOSwvKjKuD+/vwPEP2aVwD9ZnvWYafQFpLYQj+9TYOBj+nbg2l4PCRx5ByYy7xPKnu88GX5/lw== + dependencies: + "@angular-devkit/core" "10.2.4" + enhanced-resolve "4.3.0" + webpack-sources "1.4.3" + "@nodelib/fs.scandir@2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" @@ -2810,6 +3853,22 @@ "@nodelib/fs.scandir" "2.1.4" fastq "^1.6.0" +"@npmcli/fs@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" + integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" + +"@npmcli/move-file@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + "@octokit/auth-token@^2.4.0": version "2.4.5" resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3" @@ -3027,6 +4086,18 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.12.tgz#431ec342a7195622f86688bbda82e3166ce8cb28" integrity sha512-6RglhutqrGFMO1MNUXp95RBuYIuc8wTnMAV5MUhLmjTOy78ncwOw7RgeQ/HeymkKXRhZd0s2DNrM1rL7unk3MQ== +"@prisma/client@^3.12.0": + version "3.12.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.12.0.tgz#a0eb49ffea5c128dd11dffb896d7139a60073d12" + integrity sha512-4NEQjUcWja/NVBvfuDFscWSk1/rXg3+wj+TSkqXCb1tKlx/bsUE00rxsvOvGg7VZ6lw1JFpGkwjwmsOIc4zvQw== + dependencies: + "@prisma/engines-version" "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" + +"@prisma/engines-version@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980": + version "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz#829ca3d9d0d92555f44644606d4edfd45b2f5886" + integrity sha512-o+jo8d7ZEiVpcpNWUDh3fj2uPQpBxl79XE9ih9nkogJbhw6P33274SHnqheedZ7PyvPIK/mvU8MLNYgetgXPYw== + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -3080,6 +4151,19 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= +"@rollup/plugin-commonjs@^15.0.0": + version "15.1.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-15.1.0.tgz#1e7d076c4f1b2abf7e65248570e555defc37c238" + integrity sha512-xCQqz4z/o0h2syQ7d9LskIMvBSH4PX5PjYdpSSvgS+pQik3WahkQVNWg3D8XJeYjZoVWnIUQYDghuEMRGrmQYQ== + dependencies: + "@rollup/pluginutils" "^3.1.0" + commondir "^1.0.1" + estree-walker "^2.0.1" + glob "^7.1.6" + is-reference "^1.2.1" + magic-string "^0.25.7" + resolve "^1.17.0" + "@rollup/plugin-commonjs@^21.0.1": version "21.0.1" resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-21.0.1.tgz#1e57c81ae1518e4df0954d681c642e7d94588fee" @@ -3093,6 +4177,13 @@ magic-string "^0.25.7" resolve "^1.17.0" +"@rollup/plugin-json@^4.0.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3" + integrity sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw== + dependencies: + "@rollup/pluginutils" "^3.0.8" + "@rollup/plugin-node-resolve@^13.1.3": version "13.1.3" resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.1.3.tgz#2ed277fb3ad98745424c1d2ba152484508a92d79" @@ -3105,6 +4196,18 @@ is-module "^1.0.0" resolve "^1.19.0" +"@rollup/plugin-node-resolve@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-9.0.0.tgz#39bd0034ce9126b39c1699695f440b4b7d2b62e6" + integrity sha512-gPz+utFHLRrd41WMP13Jq5mqqzHL3OXrfj3/MkSyB6UBIcuNt9j60GCbarzMzdf1VHFpOxfQh/ez7wyadLMqkg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + "@types/resolve" "1.17.1" + builtin-modules "^3.1.0" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.17.0" + "@rollup/plugin-replace@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-3.0.1.tgz#f774550f482091719e52e9f14f67ffc0046a883d" @@ -3113,7 +4216,15 @@ "@rollup/pluginutils" "^3.1.0" magic-string "^0.25.7" -"@rollup/pluginutils@^3.1.0": +"@rollup/plugin-sucrase@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@rollup/plugin-sucrase/-/plugin-sucrase-4.0.3.tgz#b972ba61db0faaba397e09daaffcdbd38c167e2c" + integrity sha512-gZrjT985isK+EmHt3Dyr9z4JfO9IGuYkxck96yIyIUU9EKnZtDXUZ6ap5kvIdEnY8kLeiypiUEfK+/QtMIlA2A== + dependencies: + "@rollup/pluginutils" "^4.1.1" + sucrase "^3.20.0" + +"@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.0.9", "@rollup/pluginutils@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== @@ -3122,6 +4233,14 @@ estree-walker "^1.0.1" picomatch "^2.2.2" +"@rollup/pluginutils@^4.1.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" + integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== + dependencies: + estree-walker "^2.0.1" + picomatch "^2.2.2" + "@rollup/pluginutils@^4.1.2": version "4.1.2" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.2.tgz#ed5821c15e5e05e32816f5fb9ec607cdf5a75751" @@ -3130,10 +4249,33 @@ estree-walker "^2.0.1" picomatch "^2.2.2" -"@sentry/cli@^1.73.0": - version "1.73.0" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.73.0.tgz#0d0bce913e0060ae192741c6693c57e50078c886" - integrity sha512-n4YINqmoncGUkLEpd4WuW+oD+aoUyQPhRbSBSYkbCFxPPmopn1VExCB2Vvzwj7vjXYRRGkix6keBMS0LLs3A3Q== +"@schematics/angular@10.2.4": + version "10.2.4" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-10.2.4.tgz#3b99b9da572b57381d221e2008804e6bb9c98b82" + integrity sha512-irU3cnamfd5Hgy1B6oY7oweApJHhVaD2oYPq0NfI+F14JalERO+DGO0Tq3MWmEGn32tLQPv9fwM5O8EElEp9pA== + dependencies: + "@angular-devkit/core" "10.2.4" + "@angular-devkit/schematics" "10.2.4" + jsonc-parser "2.3.0" + +"@schematics/update@0.1002.4": + version "0.1002.4" + resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.1002.4.tgz#e8f5ff82d308f72fd521abd88316f7b0ccd296c1" + integrity sha512-qnDn3SSMmolfzWpj8CTAoC/TSPe43azKPYLR5r76GkRvuUbwr/dQEj92wu59twjGcsmjF54qcG4fGaxMndUn3Q== + dependencies: + "@angular-devkit/core" "10.2.4" + "@angular-devkit/schematics" "10.2.4" + "@yarnpkg/lockfile" "1.1.0" + ini "1.3.6" + npm-package-arg "^8.0.0" + pacote "9.5.12" + semver "7.3.2" + semver-intersect "1.4.0" + +"@sentry/cli@^1.74.4": + version "1.74.4" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.74.4.tgz#7df82f68045a155e1885bfcbb5d303e5259eb18e" + integrity sha512-BMfzYiedbModsNBJlKeBOLVYUtwSi99LJ8gxxE4Bp5N8hyjNIN0WVrozAVZ27mqzAuy6151Za3dpmOLO86YlGw== dependencies: https-proxy-agent "^5.0.0" mkdirp "^0.5.5" @@ -3141,13 +4283,14 @@ npmlog "^4.1.2" progress "^2.0.3" proxy-from-env "^1.1.0" + which "^2.0.2" -"@sentry/webpack-plugin@1.18.8": - version "1.18.8" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.18.8.tgz#247a73a0aa9e28099a736bbe89ca0d35cbac7636" - integrity sha512-PtKr0NL62b5L3kPFGjwSNbIUwwcW5E5G6bQxAYZGpkgL1MFPnS4ND0SAsySuX0byQJRFFium5A19LpzyvQZSlQ== +"@sentry/webpack-plugin@1.18.9": + version "1.18.9" + resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.18.9.tgz#acb48c0f96fdb9e73f1e1db374ea31ded6d883a8" + integrity sha512-+TrenJrgFM0QTOwBnw0ZXWMvc0PiOebp6GN5EbGEx3JPCQqXOfXFzCaEjBtASKRgcNCL7zGly41S25YR6Hm+jw== dependencies: - "@sentry/cli" "^1.73.0" + "@sentry/cli" "^1.74.4" "@simple-dom/interface@^1.4.0": version "1.4.0" @@ -3173,6 +4316,13 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@sinonjs/fake-timers@^8.0.1": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz#3fdc2b6cb58935b21bfb8d1625eb1300484316e7" + integrity sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg== + dependencies: + "@sinonjs/commons" "^1.7.0" + "@sinonjs/formatio@^3.2.1": version "3.2.2" resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.2.tgz#771c60dfa75ea7f2d68e3b94c7e888a78781372c" @@ -3287,6 +4437,11 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + "@ts-type/package-dts@^1.0.58": version "1.0.58" resolved "https://registry.yarnpkg.com/@ts-type/package-dts/-/package-dts-1.0.58.tgz#75f6fdf5f1e8f262a5081b90346439b4c4bc8d01" @@ -3295,20 +4450,45 @@ "@types/semver" "^7.3.9" ts-type "^2.1.4" +"@tsconfig/node10@^1.0.7": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" + integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== + +"@tsconfig/node12@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" + integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== + +"@tsconfig/node14@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" + integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== + +"@tsconfig/node16@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" + integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== + "@types/aria-query@^4.2.0": version "4.2.1" resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.1.tgz#78b5433344e2f92e8b306c06a5622c50c245bf6b" integrity sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg== +"@types/array.prototype.flat@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@types/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz#5433a141730f8e1d7a8e7486458ceb8144ee5edc" + integrity sha512-JOvNJUU/zjfJWcA1aHDnCKHwQjZ7VQ3UNfbcMKXrkQKKyMkJHrQ9vpSVMhgsztrtsbIRJKazMDvg2QggFVwJqw== + "@types/aws-lambda@^8.10.62": version "8.10.73" resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.73.tgz#77773c9accb2cec26fcb7c6b510a555805604a53" integrity sha512-P+a6TRQbRnVQOIjWkmw6F23wiJcF+4Uniasbzx7NAXjLQCVGx/Z4VoMfit81/pxlmcXNxAMGuYPugn6CrJLilQ== -"@types/babel__core@^7.1.0": - version "7.1.16" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.16.tgz#bc12c74b7d65e82d29876b5d0baf5c625ac58702" - integrity sha512-EAEHtisTMM+KaKwfWdC3oyllIqswlznXCIVCt7/oRNrh+DhgT4UEBNC/jlADNjvw7UnfbcdkGQcPVZ1xYiLcrQ== +"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": + version "7.1.19" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" + integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" @@ -3338,6 +4518,13 @@ dependencies: "@babel/types" "^7.3.0" +"@types/babel__traverse@^7.0.4": + version "7.14.2" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.14.2.tgz#ffcd470bbb3f8bf30481678fb5502278ca833a43" + integrity sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA== + dependencies: + "@babel/types" "^7.3.0" + "@types/body-parser@*": version "1.19.0" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" @@ -3663,6 +4850,13 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/graceful-fs@^4.1.2": + version "4.1.5" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" + integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== + dependencies: + "@types/node" "*" + "@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8", "@types/history@*": name "@types/history-4" version "4.7.8" @@ -3697,6 +4891,11 @@ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw== +"@types/istanbul-lib-coverage@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + "@types/istanbul-lib-report@*": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" @@ -3728,12 +4927,13 @@ "@types/puppeteer" "*" jest-environment-node ">=24 <=26" -"@types/jest@^24.0.11": - version "24.9.1" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.9.1.tgz#02baf9573c78f1b9974a5f36778b366aa77bd534" - integrity sha512-Fb38HkXSVA4L8fGKEZ6le5bB8r6MRWlOCZbVuWZcmOMSCd2wCYOwN1ibj8daIoV9naq7aaOZjrLCoCMptKU/4Q== +"@types/jest@^27.4.1": + version "27.4.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d" + integrity sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw== dependencies: - jest-diff "^24.3.0" + jest-matcher-utils "^27.0.0" + pretty-format "^27.0.0" "@types/jquery@*": version "3.5.5" @@ -3857,6 +5057,11 @@ pg-protocol "*" pg-types "^2.2.0" +"@types/prettier@^2.1.5": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.4.tgz#5d9b63132df54d8909fce1c3f8ca260fdd693e17" + integrity sha512-ReVR2rLTV1kvtlWFyuot+d1pkpG2Fw/XKE3PDAdj57rbM97ttSp9JZ2UsP+2EHTylra9cUf6JA7tGwW1INzUrA== + "@types/prop-types@*": version "15.7.3" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" @@ -4039,6 +5244,15 @@ "@types/source-list-map" "*" source-map "^0.7.3" +"@types/webpack-sources@^0.1.5": + version "0.1.9" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.9.tgz#da69b06eb34f6432e6658acb5a6893c55d983920" + integrity sha512-bvzMnzqoK16PQIC8AYHNdW45eREJQMd6WG/msQWX5V2+vZmODCOPb4TJcbgRljTZZTwTM4wUMcsI8FftNA7new== + dependencies: + "@types/node" "*" + "@types/source-list-map" "*" + source-map "^0.6.1" + "@types/webpack@^4.41.31": version "4.41.31" resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.31.tgz#c35f252a3559ddf9c85c0d8b0b42019025e581aa" @@ -4456,6 +5670,11 @@ tslib "^2.3.1" upath2 "^3.1.12" +"@yarnpkg/lockfile@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + JSONStream@^1.0.4, JSONStream@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -4464,7 +5683,7 @@ JSONStream@^1.0.4, JSONStream@^1.3.4: jsonparse "^1.2.0" through ">=2.2.7 <3" -abab@^2.0.0, abab@^2.0.3, abab@^2.0.5: +abab@^2.0.3, abab@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== @@ -4502,14 +5721,6 @@ accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" -acorn-globals@^4.1.0, acorn-globals@^4.3.2: - version "4.3.4" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" - integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A== - dependencies: - acorn "^6.0.1" - acorn-walk "^6.0.1" - acorn-globals@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" @@ -4528,7 +5739,7 @@ acorn-jsx@^5.3.1: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== -acorn-walk@^6.0.1, acorn-walk@^6.1.1: +acorn-walk@^6.1.1: version "6.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== @@ -4543,31 +5754,49 @@ acorn-walk@^8.0.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.0.2.tgz#d4632bfc63fd93d0f15fd05ea0e984ffd3f5a8c3" integrity sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A== -acorn@^5.5.3: - version "5.7.4" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" - integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^6.0.1, acorn@^6.0.5, acorn@^6.4.1: +acorn@^6.0.5, acorn@^6.4.1: version "6.4.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== -acorn@^7.1.0, acorn@^7.1.1, acorn@^7.4.0: +acorn@^7.1.1, acorn@^7.4.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.0.4, acorn@^8.1.0: +acorn@^8.0.4: version "8.1.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.0.tgz#52311fd7037ae119cbb134309e901aa46295b3fe" integrity sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA== +acorn@^8.2.4, acorn@^8.5.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== + acorn@^8.4.1: version "8.6.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895" integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw== +acorn@^8.7.0: + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== + +adjust-sourcemap-loader@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-3.0.0.tgz#5ae12fb5b7b1c585e80bbb5a63ec163a1a45e61e" + integrity sha512-YBrGyT2/uVQ/c6Rr+t6ZJXniY03YtHGMJQYal368burRGYKqhx9qGTWqcBU5s1CwYY9E/ri63RYyG1IacMZtqw== + dependencies: + loader-utils "^2.0.0" + regex-parser "^2.2.11" + after@0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" @@ -4585,6 +5814,14 @@ agentkeepalive@^3.4.1: dependencies: humanize-ms "^1.2.1" +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + ajv-errors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" @@ -4595,6 +5832,16 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== +ajv@6.12.4: + version "6.12.4" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" + integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -4650,6 +5897,11 @@ ansi-colors@3.2.3: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== +ansi-colors@4.1.1, ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + ansi-colors@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9" @@ -4657,12 +5909,12 @@ ansi-colors@^1.0.1: dependencies: ansi-wrap "^0.1.0" -ansi-colors@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== +ansi-colors@^3.0.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== -ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: +ansi-escapes@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== @@ -4674,7 +5926,7 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.21.3" -ansi-html@^0.0.7: +ansi-html@0.0.7, ansi-html@^0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= @@ -4689,7 +5941,7 @@ ansi-regex@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-regex@^4.0.0, ansi-regex@^4.1.0: +ansi-regex@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== @@ -4752,6 +6004,11 @@ ansicolors@~0.2.1: resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef" integrity sha1-vgiVmQl7dKXJxKhKDNvNtivYeu8= +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= + anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -4760,7 +6017,7 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -anymatch@^3.0.0, anymatch@~3.1.1: +anymatch@^3.0.0, anymatch@^3.0.3, anymatch@~3.1.1, anymatch@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== @@ -4818,6 +6075,11 @@ aria-query@^5.0.0: resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== +arity-n@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/arity-n/-/arity-n-1.0.4.tgz#d9e76b11733e08569c0847ae7b39b2860b30b745" + integrity sha1-2edrEXM+CFacCEeuezmyhgswt0U= + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -4858,6 +6120,11 @@ array-flatten@1.1.1: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= +array-flatten@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + array-from@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195" @@ -4922,6 +6189,16 @@ array.prototype.flat@^1.2.3: define-properties "^1.1.3" es-abstract "^1.18.0-next.1" +array.prototype.flat@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz#0b0c1567bf57b38b56b4c97b8aa72ab45e4adc7b" + integrity sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.2" + es-shim-unscopables "^1.0.0" + array.prototype.flatmap@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz#94cfd47cc1556ec0747d97f7c7738c58122004c9" @@ -5022,10 +6299,12 @@ ast-types@0.13.3: resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.3.tgz#50da3f28d17bdbc7969a3a2d83a0e4a72ae755a7" integrity sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA== -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +ast-types@0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.14.2.tgz#600b882df8583e3cd4f2df5fa20fa83759d4bdfd" + integrity sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA== + dependencies: + tslib "^2.0.1" astral-regex@^2.0.0: version "2.0.0" @@ -5132,6 +6411,32 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +autoprefixer@9.8.6: + version "9.8.6" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" + integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== + dependencies: + browserslist "^4.12.0" + caniuse-lite "^1.0.30001109" + colorette "^1.2.1" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^7.0.32" + postcss-value-parser "^4.1.0" + +autoprefixer@^9.6.5: + version "9.8.8" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.8.tgz#fd4bd4595385fa6f06599de749a4d5f7a474957a" + integrity sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA== + dependencies: + browserslist "^4.12.0" + caniuse-lite "^1.0.30001109" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + picocolors "^0.2.1" + postcss "^7.0.32" + postcss-value-parser "^4.1.0" + available-typed-arrays@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" @@ -5342,18 +6647,30 @@ babel-import-util@^0.2.0: resolved "https://registry.yarnpkg.com/babel-import-util/-/babel-import-util-0.2.0.tgz#b468bb679919601a3570f9e317536c54f2862e23" integrity sha512-CtWYYHU/MgK88rxMrLfkD356dApswtR/kWZ/c6JifG1m10e7tBBrs/366dFzWMAoqYmG5/JSh+94tUSpIwh+ag== -babel-jest@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54" - integrity sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw== +babel-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" + integrity sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg== + dependencies: + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^27.5.1" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-loader@8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.1.0.tgz#c611d5112bd5209abe8b9fa84c3e4da25275f1c3" + integrity sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw== dependencies: - "@jest/transform" "^24.9.0" - "@jest/types" "^24.9.0" - "@types/babel__core" "^7.1.0" - babel-plugin-istanbul "^5.1.0" - babel-preset-jest "^24.9.0" - chalk "^2.4.2" - slash "^2.0.0" + find-cache-dir "^2.1.0" + loader-utils "^1.4.0" + mkdirp "^0.5.3" + pify "^4.0.1" + schema-utils "^2.6.5" babel-loader@^8.0.6: version "8.2.2" @@ -5486,11 +6803,25 @@ babel-plugin-istanbul@^5.1.0: istanbul-lib-instrument "^3.3.0" test-exclude "^5.2.3" -babel-plugin-jest-hoist@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz#4f837091eb407e01447c8843cbec546d0002d756" - integrity sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw== +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e" + integrity sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ== dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" babel-plugin-module-resolver@^3.2.0: @@ -5796,6 +7127,24 @@ babel-polyfill@^6.26.0: core-js "^2.5.0" regenerator-runtime "^0.10.5" +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + babel-preset-env@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.7.0.tgz#dea79fa4ebeb883cd35dab07e260c1c9c04df77a" @@ -5832,13 +7181,13 @@ babel-preset-env@^1.7.0: invariant "^2.2.2" semver "^5.3.0" -babel-preset-jest@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz#192b521e2217fb1d1f67cf73f70c336650ad3cdc" - integrity sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg== +babel-preset-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81" + integrity sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag== dependencies: - "@babel/plugin-syntax-object-rest-spread" "^7.0.0" - babel-plugin-jest-hoist "^24.9.0" + babel-plugin-jest-hoist "^27.5.1" + babel-preset-current-node-syntax "^1.0.0" babel-register@^6.26.0: version "6.26.0" @@ -5959,6 +7308,11 @@ basic-auth@~2.0.1: dependencies: safe-buffer "5.1.2" +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= + bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -6097,6 +7451,18 @@ body@^5.1.0: raw-body "~1.1.0" safe-json-parse "~1.0.1" +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + boolbase@^1.0.0, boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -6807,6 +8173,17 @@ browserslist@^4.0.0, browserslist@^4.16.3, browserslist@^4.16.6: escalade "^3.1.1" node-releases "^1.1.75" +browserslist@^4.12.0, browserslist@^4.20.2, browserslist@^4.7.0, browserslist@^4.9.1: + version "4.20.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.2.tgz#567b41508757ecd904dab4d1c646c612cd3d4f88" + integrity sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA== + dependencies: + caniuse-lite "^1.0.30001317" + electron-to-chromium "^1.4.84" + escalade "^3.1.1" + node-releases "^2.0.2" + picocolors "^1.0.0" + browserslist@^4.14.5: version "4.18.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.18.1.tgz#60d3920f25b6860eb917c6c7b185576f4d8b017f" @@ -6892,11 +8269,16 @@ buffer-equal-constant-time@1.0.1: resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= -buffer-from@1.x, buffer-from@^1.0.0: +buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" + integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== + buffer-writer@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" @@ -6977,6 +8359,29 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +cacache@15.0.5: + version "15.0.5" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0" + integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A== + dependencies: + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.0" + tar "^6.0.2" + unique-filename "^1.1.1" + cacache@^11.3.3: version "11.3.3" resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.3.tgz#8bd29df8c6a718a6ebd2d010da4d7972ae3bbadc" @@ -7018,6 +8423,30 @@ cacache@^12.0.0, cacache@^12.0.2: unique-filename "^1.1.1" y18n "^4.0.0" +cacache@^15.0.4, cacache@^15.0.5: + version "15.3.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" + integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== + dependencies: + "@npmcli/fs" "^1.0.0" + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.0.2" + unique-filename "^1.1.1" + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -7124,6 +8553,11 @@ camelcase-keys@^6.2.2: map-obj "^4.0.0" quick-lru "^4.0.1" +camelcase@5.3.1, camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + camelcase@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" @@ -7134,12 +8568,7 @@ camelcase@^4.1.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= -camelcase@^5.0.0, camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -camelcase@^6.1.0: +camelcase@^6.0.0, camelcase@^6.1.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -7166,20 +8595,15 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001173, caniuse-lite@^1.0.30001179, caniuse-lite@^1.0.30001254: - version "1.0.30001257" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz#150aaf649a48bee531104cfeda57f92ce587f6e5" - integrity sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001173, caniuse-lite@^1.0.30001179, caniuse-lite@^1.0.30001254, caniuse-lite@^1.0.30001274, caniuse-lite@^1.0.30001280, caniuse-lite@^1.0.30001317: + version "1.0.30001339" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001339.tgz" + integrity sha512-Es8PiVqCe+uXdms0Gu5xP5PF2bxLR7OBp3wUzUnuO7OHzhOfCyg3hdiGWVPVxhiuniOzng+hTc1u3fEQ0TlkSQ== -caniuse-lite@^1.0.30001274: - version "1.0.30001279" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001279.tgz#eb06818da481ef5096a3b3760f43e5382ed6b0ce" - integrity sha512-VfEHpzHEXj6/CxggTwSFoZBBYGQfQv9Cf42KPlO79sWXCD1QNKWKsKzFeWL7QpZHJQYAvocqV6Rty1yJMkqWLQ== - -caniuse-lite@^1.0.30001280: - version "1.0.30001286" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001286.tgz#3e9debad420419618cfdf52dc9b6572b28a8fff6" - integrity sha512-zaEMRH6xg8ESMi2eQ3R4eZ5qw/hJiVsO/HlLwniIwErij0JDr9P+8V4dtx1l+kLq6j3yy8l8W4fst1lBnat5wQ== +canonical-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/canonical-path/-/canonical-path-1.0.0.tgz#fcb470c23958def85081856be7a86e904f180d1d" + integrity sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg== capture-exit@^2.0.0: version "2.0.0" @@ -7262,6 +8686,11 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -7294,6 +8723,21 @@ chokidar@3.5.1, chokidar@^3.0.2, chokidar@^3.3.1, chokidar@^3.4.1, chokidar@^3.5 optionalDependencies: fsevents "~2.3.1" +"chokidar@>=2.0.0 <4.0.0", "chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.2.1: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -7318,6 +8762,11 @@ chownr@^1.1.1, chownr@^1.1.2, chownr@^1.1.4: resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + chrome-trace-event@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" @@ -7335,6 +8784,11 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +ci-info@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2" + integrity sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw== + ci-job-number@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/ci-job-number/-/ci-job-number-1.2.2.tgz#f4e5918fcaeeda95b604f214be7d7d4a961fe0c0" @@ -7348,6 +8802,16 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" +circular-dependency-plugin@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/circular-dependency-plugin/-/circular-dependency-plugin-5.2.0.tgz#e09dbc2dd3e2928442403e2d45b41cea06bc0a93" + integrity sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw== + +cjs-module-lexer@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" + integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -7392,7 +8856,7 @@ clean-css@^5.2.2: dependencies: source-map "~0.6.0" -clean-stack@^2.2.0: +clean-stack@^2.0.0, clean-stack@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== @@ -7421,6 +8885,11 @@ cli-spinners@^2.0.0, cli-spinners@^2.5.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939" integrity sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q== +cli-spinners@^2.4.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" + integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== + cli-table3@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" @@ -7550,6 +9019,11 @@ codecov@^3.6.5: teeny-request "6.0.1" urlgrey "0.4.4" +collect-v8-coverage@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" + integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== + collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -7631,7 +9105,7 @@ combine-source-map@^0.8.0: lodash.memoize "~3.0.3" source-map "~0.5.3" -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -7660,7 +9134,7 @@ commander@^3.0.2: resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== -commander@^4.1.1: +commander@^4.0.0, commander@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== @@ -7670,7 +9144,7 @@ commander@^5.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -commander@^6.2.0, commander@^6.2.1: +commander@^6.0.0, commander@^6.2.0, commander@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== @@ -7728,6 +9202,13 @@ component-inherit@0.0.3: resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= +compose-function@3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/compose-function/-/compose-function-3.0.3.tgz#9ed675f13cc54501d30950a486ff6a7ba3ab185f" + integrity sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8= + dependencies: + arity-n "^1.0.4" + compressible@^2.0.12, compressible@~2.0.16: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -7793,6 +9274,11 @@ configstore@^5.0.0, configstore@^5.0.1: write-file-atomic "^3.0.0" xdg-basedir "^4.0.0" +connect-history-api-fallback@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" + integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== + connect@^3.6.6, connect@^3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" @@ -7955,6 +9441,11 @@ convert-source-map@1.7.0, convert-source-map@^1.4.0, convert-source-map@^1.5.1, dependencies: safe-buffer "~5.1.1" +convert-source-map@^0.3.3: + version "0.3.5" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" + integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA= + convert-source-map@~1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" @@ -7980,6 +9471,13 @@ cookie@^0.4.1, cookie@~0.4.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== +copy-anything@^2.0.1: + version "2.0.6" + resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480" + integrity sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw== + dependencies: + is-what "^3.14.1" + copy-concurrently@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" @@ -8002,6 +9500,31 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= +copy-webpack-plugin@6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-6.0.3.tgz#2b3d2bfc6861b96432a65f0149720adbd902040b" + integrity sha512-q5m6Vz4elsuyVEIUXr7wJdIdePWTubsqVbEMvf1WQnHGv0Q+9yPRu7MtYFPt+GBOXRav9lvIINifTQ1vSCs+eA== + dependencies: + cacache "^15.0.4" + fast-glob "^3.2.4" + find-cache-dir "^3.3.1" + glob-parent "^5.1.1" + globby "^11.0.1" + loader-utils "^2.0.0" + normalize-path "^3.0.0" + p-limit "^3.0.1" + schema-utils "^2.7.0" + serialize-javascript "^4.0.0" + webpack-sources "^1.4.3" + +core-js-compat@^3.6.2: + version "3.22.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.22.1.tgz#47b9c5e79efbf13935f637449fa1cdec8cd9515f" + integrity sha512-CWbNqTluLMvZg1cjsQUbGiCM91dobSHKfDIyCoxuqxthdjGuUlaMbCsSehP3CBiVvG0C7P6UIrC1v0hgFE75jw== + dependencies: + browserslist "^4.20.2" + semver "7.0.0" + core-js-compat@^3.8.1, core-js-compat@^3.9.0: version "3.9.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.9.1.tgz#4e572acfe90aff69d76d8c37759d21a5c59bb455" @@ -8010,6 +9533,11 @@ core-js-compat@^3.8.1, core-js-compat@^3.9.0: browserslist "^4.16.3" semver "7.0.0" +core-js@3.6.4: + version "3.6.4" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.4.tgz#440a83536b458114b9cb2ac1580ba377dc470647" + integrity sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw== + core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" @@ -8104,6 +9632,11 @@ create-react-context@^0.2.2: fbjs "^0.8.0" gud "^1.0.0" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-env@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" @@ -8122,7 +9655,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -8166,6 +9699,24 @@ css-declaration-sorter@^4.0.1: postcss "^7.0.1" timsort "^0.3.0" +css-loader@4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.2.2.tgz#b668b3488d566dc22ebcf9425c5f254a05808c89" + integrity sha512-omVGsTkZPVwVRpckeUnLshPp12KsmMSLqYxs12+RzM9jRR5Y+Idn/tBffjXRvOE+qW7if24cuceFJqYR5FmGBg== + dependencies: + camelcase "^6.0.0" + cssesc "^3.0.0" + icss-utils "^4.1.1" + loader-utils "^2.0.0" + postcss "^7.0.32" + postcss-modules-extract-imports "^2.0.0" + postcss-modules-local-by-default "^3.0.3" + postcss-modules-scope "^2.2.0" + postcss-modules-values "^3.0.0" + postcss-value-parser "^4.1.0" + schema-utils "^2.7.0" + semver "^7.3.2" + css-loader@^5.1.1: version "5.2.0" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.0.tgz#a9ecda190500863673ce4434033710404efbff00" @@ -8200,6 +9751,13 @@ css-loader@^5.2.0: schema-utils "^3.0.0" semver "^7.3.5" +css-parse@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-2.0.0.tgz#a468ee667c16d81ccf05c58c38d2a97c780dbfd4" + integrity sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q= + dependencies: + css "^2.0.0" + css-select-base-adapter@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" @@ -8257,6 +9815,16 @@ css.escape@1.5.1: resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= +css@^2.0.0: + version "2.2.4" + resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" + integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== + dependencies: + inherits "^2.0.3" + source-map "^0.6.1" + source-map-resolve "^0.5.2" + urix "^0.1.0" + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -8336,7 +9904,7 @@ cssnano-util-same-parent@^4.0.0: resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3" integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q== -cssnano@^4.1.10: +cssnano@4.1.10, cssnano@^4.1.10: version "4.1.10" resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2" integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ== @@ -8353,24 +9921,22 @@ csso@^4.0.2: dependencies: css-tree "^1.1.2" -cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0", cssom@~0.3.6: - version "0.3.8" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" - integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== - -cssom@^0.4.1, cssom@^0.4.4: +cssom@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== -cssstyle@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1" - integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA== - dependencies: - cssom "0.3.x" +cssom@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" + integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== + +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== -cssstyle@^2.0.0, cssstyle@^2.3.0: +cssstyle@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== @@ -8382,6 +9948,11 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.7.tgz#2a5fb75e1015e84dd15692f71e89a1450290950b" integrity sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g== +cuint@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" + integrity sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs= + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -8407,6 +9978,14 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + dag-map@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/dag-map/-/dag-map-2.0.2.tgz#9714b472de82a1843de2fba9b6876938cab44c68" @@ -8431,15 +10010,6 @@ data-uri-to-buffer@3.0.1: resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== -data-urls@^1.0.0, data-urls@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" - integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== - dependencies: - abab "^2.0.0" - whatwg-mimetype "^2.2.0" - whatwg-url "^7.0.0" - data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -8449,6 +10019,15 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +data-urls@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.1.tgz#597fc2ae30f8bc4dbcf731fcd1b1954353afc6f8" + integrity sha512-Ds554NeT5Gennfoo9KN50Vh6tpgtvYEwraYjejXnyTpu1C7oXKxdFk75REooENHE8ndTVOJuv+BEs4/J/xcozw== + dependencies: + abab "^2.0.3" + whatwg-mimetype "^3.0.0" + whatwg-url "^10.0.0" + date-and-time@^0.14.2: version "0.14.2" resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-0.14.2.tgz#a4266c3dead460f6c231fe9674e585908dac354e" @@ -8510,7 +10089,14 @@ debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: dependencies: ms "2.1.2" -debug@^3.0.1, debug@^3.1.0, debug@^3.1.1, debug@^3.2.6: +debug@4.1.1, debug@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +debug@^3.0.1, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -8531,13 +10117,6 @@ debug@^4.3.3, debug@~4.3.1, debug@~4.3.2: dependencies: ms "2.1.2" -debug@~4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== - dependencies: - ms "^2.1.1" - debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -8561,6 +10140,11 @@ decimal.js@^10.2.1: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3" integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw== +decimal.js@^10.3.1: + version "10.3.1" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" + integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== + decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" @@ -8592,6 +10176,18 @@ deep-eql@^3.0.1: dependencies: type-detect "^4.0.0" +deep-equal@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" + integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== + dependencies: + is-arguments "^1.0.4" + is-date-object "^1.0.1" + is-regex "^1.0.4" + object-is "^1.0.1" + object-keys "^1.1.1" + regexp.prototype.flags "^1.2.0" + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -8607,6 +10203,14 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +default-gateway@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" + integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== + dependencies: + execa "^1.0.0" + ip-regex "^2.1.0" + defaults@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" @@ -8626,6 +10230,14 @@ define-properties@^1.1.2, define-properties@^1.1.3: dependencies: object-keys "^1.0.12" +define-properties@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + define-property@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" @@ -8648,6 +10260,19 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" +del@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" + integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== + dependencies: + "@types/glob" "^7.1.1" + globby "^6.1.0" + is-path-cwd "^2.0.0" + is-path-in-cwd "^2.0.0" + p-map "^2.0.0" + pify "^4.0.1" + rimraf "^2.6.3" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -8673,6 +10298,11 @@ depd@~2.0.0: resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== +dependency-graph@^0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.7.2.tgz#91db9de6eb72699209d88aea4c1fd5221cac1c49" + integrity sha512-KqtH4/EZdtdfWX0p6MGP9jljvxSY6msy/pRUD4jgNwVpv3v1QmNLlsB3LDSSUg79BRVSn7jI1QPRtArGABovAQ== + dependency-tree@^8.0.0: version "8.1.0" resolved "https://registry.yarnpkg.com/dependency-tree/-/dependency-tree-8.1.0.tgz#1b896a0418bd7ba3e6d55c39bb664452a001579f" @@ -8724,15 +10354,15 @@ detect-indent@^6.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd" integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA== -detect-newline@3.1.0: +detect-newline@3.1.0, detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== -detect-newline@^2.1.0: +detect-node@^2.0.4: version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" - integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== detective-amd@^3.0.1: version "3.1.0" @@ -8839,16 +10469,16 @@ di@^0.0.1: resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw= -diff-sequences@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" - integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== - diff-sequences@^27.4.0: version "27.4.0" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.4.0.tgz#d783920ad8d06ec718a060d00196dfef25b132a5" integrity sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww== +diff-sequences@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" + integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== + diff@3.5.0, diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -8883,6 +10513,26 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= + +dns-packet@^1.3.1: + version "1.3.4" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.4.tgz#e3455065824a2507ba886c55a89963bb107dec6f" + integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA== + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= + dependencies: + buffer-indexof "^1.0.0" + doctrine@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" @@ -8969,13 +10619,6 @@ domelementtype@^2.2.0: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== -domexception@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" - integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== - dependencies: - webidl-conversions "^4.0.2" - domexception@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" @@ -8983,6 +10626,13 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" +domexception@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" + integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== + dependencies: + webidl-conversions "^7.0.0" + domhandler@^4.0.0, domhandler@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626" @@ -9112,6 +10762,11 @@ electron-to-chromium@^1.3.896: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.14.tgz#b0aa41fbfbf2eff8c2c6f7a871c03075250f8956" integrity sha512-RsGkAN9JEAYMObS72kzUsPPcPGMqX1rBqGuXi9aa4TBKLzICoLf+DAAtd0fVFzrniJqYzpby47gthCUoObfs0Q== +electron-to-chromium@^1.4.84: + version "1.4.114" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.114.tgz#d85ec0808dd50b0cf6e6b262480ffd385f71c873" + integrity sha512-gRwLpVYWHGbERPU6o8pKfR168V6enWEXzZc6zQNNXbgJ7UJna+9qzAIHY94+9KOv71D/CH+QebLA9pChD2q8zA== + elliptic@^6.5.3: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" @@ -9823,6 +11478,11 @@ emit-function@0.0.2: resolved "https://registry.yarnpkg.com/emit-function/-/emit-function-0.0.2.tgz#e3a50b3d61be1bf8ca88b924bf713157a5bec124" integrity sha1-46ULPWG+G/jKiLkkv3ExV6W+wSQ= +emittery@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" + integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== + emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -9925,7 +11585,16 @@ engine.io@~6.1.0: engine.io-parser "~5.0.3" ws "~8.2.3" -enhanced-resolve@^4.5.0: +enhanced-resolve@4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126" + integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.5.0" + tapable "^1.0.0" + +enhanced-resolve@^4.3.0, enhanced-resolve@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec" integrity sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg== @@ -9992,7 +11661,7 @@ errlop@^2.0.0: resolved "https://registry.yarnpkg.com/errlop/-/errlop-2.2.0.tgz#1ff383f8f917ae328bebb802d6ca69666a42d21b" integrity sha512-e64Qj9+4aZzjzzFpZC7p5kmm/ccCrbLhAJplhsDXQFs87XTsXwOpH4s1Io2s90Tau/8r2j9f4l/thhDevRjzxw== -errno@^0.1.3, errno@~0.1.7: +errno@^0.1.1, errno@^0.1.3, errno@~0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== @@ -10035,6 +11704,35 @@ es-abstract@^1.17.2, es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2: string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.0" +es-abstract@^1.19.0, es-abstract@^1.19.2, es-abstract@^1.19.5: + version "1.20.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.0.tgz#b2d526489cceca004588296334726329e0a6bfb6" + integrity sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-weakref "^1.0.2" + object-inspect "^1.12.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + regexp.prototype.flags "^1.4.1" + string.prototype.trimend "^1.0.5" + string.prototype.trimstart "^1.0.5" + unbox-primitive "^1.0.2" + es-abstract@^1.19.1: version "1.19.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" @@ -10066,6 +11764,13 @@ es-module-lexer@^0.9.0: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-shim-unscopables@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -10075,11 +11780,37 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.35, es5-ext@^0.10.50: + version "0.10.60" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.60.tgz#e8060a86472842b93019c31c34865012449883f4" + integrity sha512-jpKNXIt60htYG59/9FGf2PYT3pwMpnEbNKysU+k/4FGwyGtMotOvcZOuW+EmXXYASRqYSXQfGL5cVIthOTgbkg== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + next-tick "^1.1.0" + +es6-iterator@2.0.3, es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + es6-object-assign@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c" integrity sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw= +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -10117,18 +11848,6 @@ escodegen@1.8.x: optionalDependencies: source-map "~0.2.0" -escodegen@^1.11.1, escodegen@^1.9.1: - version "1.14.3" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" - integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== - dependencies: - esprima "^4.0.1" - estraverse "^4.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - escodegen@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" @@ -10390,7 +12109,7 @@ estraverse@^1.9.1: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" integrity sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q= -estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.1.1: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -10400,6 +12119,11 @@ estraverse@^5.1.0, estraverse@^5.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== +estree-walker@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" + integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== + estree-walker@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" @@ -10458,6 +12182,13 @@ events@^3.0.0, events@^3.2.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +eventsource@^1.0.7: + version "1.1.0" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.0.tgz#00e8ca7c92109e94b0ddf32dac677d841028cfaf" + integrity sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg== + dependencies: + original "^1.0.0" + evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" @@ -10514,6 +12245,21 @@ execa@^4.0.0, execa@^4.1.0: signal-exit "^3.0.2" strip-final-newline "^2.0.0" +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + exists-sync@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/exists-sync/-/exists-sync-0.0.4.tgz#9744c2c428cc03b01060db454d4b12f0ef3c8879" @@ -10573,17 +12319,15 @@ expect@=27.2.5: jest-message-util "^27.2.5" jest-regex-util "^27.0.6" -expect@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" - integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q== +expect@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-27.5.1.tgz#83ce59f1e5bdf5f9d2b94b61d2050db48f3fef74" + integrity sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw== dependencies: - "@jest/types" "^24.9.0" - ansi-styles "^3.2.0" - jest-get-type "^24.9.0" - jest-matcher-utils "^24.9.0" - jest-message-util "^24.9.0" - jest-regex-util "^24.9.0" + "@jest/types" "^27.5.1" + jest-get-type "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" express@^4.10.7, express@^4.16.4, express@^4.17.1: version "4.17.1" @@ -10657,6 +12401,13 @@ express@^4.17.3: utils-merge "1.0.1" vary "~1.1.2" +ext@^1.1.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.6.0.tgz#3871d50641e874cc172e2b53f919842d19db4c52" + integrity sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg== + dependencies: + type "^2.5.0" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -10755,7 +12506,18 @@ fast-glob@^3.0.3, fast-glob@^3.1.1: micromatch "^4.0.2" picomatch "^2.2.1" -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: +fast-glob@^3.2.4: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -10798,6 +12560,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +faye-websocket@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ= + dependencies: + websocket-driver ">=0.5.1" + faye-websocket@^0.11.3: version "0.11.3" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" @@ -10805,6 +12574,13 @@ faye-websocket@^0.11.3: dependencies: websocket-driver ">=0.5.1" +faye-websocket@~0.11.1: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + fb-watchman@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" @@ -10858,6 +12634,14 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-loader@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.0.0.tgz#97bbfaab7a2460c07bcbd72d3a6922407f67649f" + integrity sha512-/aMOAYEFXDdjG0wytpTL5YQLfZnnTmLNjn+AIrJ/6HVnTfDqLsVKUUwkDf4I4kgex36BvjuXEn/TX9B/1ESyqQ== + dependencies: + loader-utils "^2.0.0" + schema-utils "^2.6.5" + file-loader@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" @@ -10978,6 +12762,11 @@ find-index@^1.1.0: resolved "https://registry.yarnpkg.com/find-index/-/find-index-1.1.1.tgz#4b221f8d46b7f8bea33d8faed953f3ca7a081cbc" integrity sha512-XYKutXMrIK99YMUPf91KX5QVJoG31/OsgftD6YoTPAObfQIxM4ziA9f0J1AsqKhJmo+IeaIPP0CFopTD4bdUBw== +find-parent-dir@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.1.tgz#c5c385b96858c3351f95d446cab866cbf9f11125" + integrity sha512-o4UcykWV/XN9wm+jMEtWLPlV8RXCZnMhQI6F6OdHeSez7iiJWePw8ijOlskJZMsaQoGR/b7dH6lO02HhaTN7+A== + find-pkg@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/find-pkg/-/find-pkg-0.1.2.tgz#1bdc22c06e36365532e2a248046854b9788da557" @@ -11190,6 +12979,24 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -11251,6 +13058,15 @@ fs-exists-sync@^0.1.0: resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0= +fs-extra@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.2.tgz#f91704c53d1b461f893452b0c307d9997647ab6b" + integrity sha1-+RcExT0bRh+JNFKwwwfZmXZHq2s= + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-extra@^0.24.0: version "0.24.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.24.0.tgz#d4e4342a96675cb7846633a6099249332b539952" @@ -11315,7 +13131,7 @@ fs-extra@^8.0.0, fs-extra@^8.0.1, fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.1, fs-extra@^9.1.0: +fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== @@ -11343,6 +13159,13 @@ fs-minipass@^1.2.7: dependencies: minipass "^2.6.0" +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs-tree-diff@^0.5.2, fs-tree-diff@^0.5.3, fs-tree-diff@^0.5.4, fs-tree-diff@^0.5.6: version "0.5.9" resolved "https://registry.yarnpkg.com/fs-tree-diff/-/fs-tree-diff-0.5.9.tgz#a4ec6182c2f5bd80b9b83c8e23e4522e6f5fd946" @@ -11398,21 +13221,41 @@ fsevents@^1.2.7: bindings "^1.5.0" nan "^2.12.1" -fsevents@~2.3.1, fsevents@~2.3.2: +fsevents@^2.3.2, fsevents@~2.3.1, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -11464,7 +13307,7 @@ genfun@^5.0.0: resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA== -gensync@^1.0.0-beta.2: +gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== @@ -11482,7 +13325,7 @@ get-caller-file@^1.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== -get-caller-file@^2.0.0, get-caller-file@^2.0.1, get-caller-file@^2.0.5: +get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -11513,6 +13356,11 @@ get-own-enumerable-property-symbols@^3.0.0: resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + get-pkg-repo@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz#c73b489c06d80cc5536c2c853f9e05232056972d" @@ -11705,7 +13553,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.0: +glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -11734,6 +13582,18 @@ glob@7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@7.2.0, glob@^7.0.0, glob@^7.0.3, glob@^7.0.4, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" @@ -11843,6 +13703,17 @@ globby@^11.0.1, globby@^11.0.2: merge2 "^1.3.0" slash "^3.0.0" +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + globby@^8.0.1: version "8.0.2" resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.2.tgz#5697619ccd95c5275dbb2d6faa42087c1a941d8d" @@ -11967,6 +13838,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== +graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + "graceful-readlink@>= 1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" @@ -12010,6 +13886,11 @@ gzip-size@^6.0.0: dependencies: duplexer "^0.1.2" +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + handlebars@^4.0.1, handlebars@^4.0.4, handlebars@^4.3.1, handlebars@^4.7.3, handlebars@^4.7.6: version "4.7.7" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" @@ -12059,6 +13940,11 @@ has-bigints@^1.0.1: resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== +has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + has-binary2@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" @@ -12086,6 +13972,13 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + has-symbol-support-x@^1.4.1: version "1.4.2" resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455" @@ -12096,6 +13989,11 @@ has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + has-to-string-tag-x@^1.2.0: version "1.4.1" resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d" @@ -12283,6 +14181,13 @@ history@^4.6.0, history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" +history@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b" + integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ== + dependencies: + "@babel/runtime" "^7.7.6" + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -12324,6 +14229,13 @@ hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== +hosted-git-info@^3.0.2: + version "3.0.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.8.tgz#6e35d4cc87af2c5f816e4cb9ce350ba87a3f370d" + integrity sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw== + dependencies: + lru-cache "^6.0.0" + hosted-git-info@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961" @@ -12331,6 +14243,16 @@ hosted-git-info@^4.0.1: dependencies: lru-cache "^6.0.0" +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + hsl-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" @@ -12346,13 +14268,6 @@ html-comment-regex@^1.1.0: resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== -html-encoding-sniffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" - integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== - dependencies: - whatwg-encoding "^1.0.1" - html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -12360,6 +14275,18 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" +html-encoding-sniffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" + integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== + dependencies: + whatwg-encoding "^2.0.0" + +html-entities@^1.3.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc" + integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA== + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -12404,6 +14331,11 @@ http-cache-semantics@3.8.1, http-cache-semantics@^3.8.1: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= + http-errors@1.7.2: version "1.7.2" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" @@ -12460,7 +14392,7 @@ http-proxy-agent@^2.1.0: agent-base "4" debug "3.1.0" -http-proxy-agent@^4.0.0: +http-proxy-agent@^4.0.0, http-proxy-agent@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== @@ -12469,7 +14401,26 @@ http-proxy-agent@^4.0.0: agent-base "6" debug "4" -http-proxy@^1.13.1, http-proxy@^1.18.1: +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + +http-proxy-middleware@0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" + integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== + dependencies: + http-proxy "^1.17.0" + is-glob "^4.0.0" + lodash "^4.17.11" + micromatch "^3.1.10" + +http-proxy@^1.13.1, http-proxy@^1.17.0, http-proxy@^1.18.1: version "1.18.1" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== @@ -12526,6 +14477,11 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -12540,13 +14496,27 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +iconv-lite@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ== dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +icss-utils@^4.0.0, icss-utils@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" + integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== + dependencies: + postcss "^7.0.14" + icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" @@ -12589,11 +14559,28 @@ ignore@^5.1.1, ignore@^5.1.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== +image-size@~0.5.0: + version "0.5.5" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" + integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= +immutable@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23" + integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw== + +import-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" + integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk= + dependencies: + import-from "^2.1.0" + import-fresh@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" @@ -12610,6 +14597,13 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" +import-from@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" + integrity sha1-M1238qev/VOqpHHUuAId7ja387E= + dependencies: + resolve-from "^3.0.0" + import-local@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc" @@ -12626,6 +14620,14 @@ import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -12691,6 +14693,11 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +ini@1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.6.tgz#f1c46a2a93a253e7b3905115e74d527cd23061a1" + integrity sha512-IZUoxEjNjubzrmvzZU4lKP7OnYmX72XRl3sqkfJhBKweKi5rnGi5+IUdlj/H1M+Ip5JQ1WzaDMOBRY90Ajc5jg== + ini@^1.3.2, ini@^1.3.4, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" @@ -12710,6 +14717,13 @@ init-package-json@^1.10.3: validate-npm-package-license "^3.0.1" validate-npm-package-name "^3.0.0" +injection-js@^2.2.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/injection-js/-/injection-js-2.4.0.tgz#ebe8871b1a349f23294eaa751bbd8209a636e754" + integrity sha512-6jiJt0tCAo9zjHbcwLiPL+IuNe9SQ6a9g0PEzafThW3fOQi0mrmiJGBJvDD6tmhPh8cQHIQtCOrJuBfQME4kPA== + dependencies: + tslib "^2.0.0" + inline-source-map-comment@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/inline-source-map-comment/-/inline-source-map-comment-1.0.5.tgz#50a8a44c2a790dfac441b5c94eccd5462635faf6" @@ -12728,6 +14742,25 @@ inline-source-map@~0.6.0: dependencies: source-map "~0.5.3" +inquirer@7.3.3, inquirer@^7.0.1: + version "7.3.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.19" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.6.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + inquirer@^6, inquirer@^6.2.0: version "6.5.2" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" @@ -12747,24 +14780,13 @@ inquirer@^6, inquirer@^6.2.0: strip-ansi "^5.1.0" through "^2.3.6" -inquirer@^7.0.1: - version "7.3.3" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" - integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== +internal-ip@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" + integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== dependencies: - ansi-escapes "^4.2.1" - chalk "^4.1.0" - cli-cursor "^3.1.0" - cli-width "^3.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.19" - mute-stream "0.0.8" - run-async "^2.4.0" - rxjs "^6.6.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - through "^2.3.6" + default-gateway "^4.2.0" + ipaddr.js "^1.9.0" internal-slot@^1.0.3: version "1.0.3" @@ -12795,11 +14817,6 @@ invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -inversify@^5.0.0: - version "5.0.5" - resolved "https://registry.yarnpkg.com/inversify/-/inversify-5.0.5.tgz#bd1f8e6d8e0f739331acd8ba9bc954635aae0bbf" - integrity sha512-60QsfPz8NAU/GZqXu8hJ+BhNf/C/c+Hp0eDc6XMIJTxBiP36AQyyQKpBkOVTLWBFDQWYVHpbbEuIsHu9dLuJDA== - invert-kv@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" @@ -12810,12 +14827,12 @@ ip-regex@^2.1.0: resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= -ip@1.1.5, ip@^1.1.5: +ip@1.1.5, ip@^1.1.0, ip@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= -ipaddr.js@1.9.1: +ipaddr.js@1.9.1, ipaddr.js@^1.9.0: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== @@ -12825,6 +14842,11 @@ is-absolute-url@^2.0.0: resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= +is-absolute-url@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" + integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== + is-accessor-descriptor@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" @@ -12942,6 +14964,13 @@ is-core-module@^2.8.0: dependencies: has "^1.0.3" +is-core-module@^2.8.1: + version "2.9.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" + integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== + dependencies: + has "^1.0.3" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -13090,6 +15119,11 @@ is-negative-zero@^2.0.1: resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + is-number-object@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" @@ -13122,6 +15156,25 @@ is-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf" integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA== +is-path-cwd@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" + integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + +is-path-in-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" + integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== + dependencies: + is-path-inside "^2.1.0" + +is-path-inside@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" + integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== + dependencies: + path-is-inside "^1.0.2" + is-plain-obj@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" @@ -13144,10 +15197,10 @@ is-plain-object@^5.0.0: resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== -is-potential-custom-element-name@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" - integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== is-redirect@^1.0.0: version "1.0.0" @@ -13161,6 +15214,14 @@ is-reference@^1.2.1: dependencies: "@types/estree" "*" +is-regex@^1.0.4, is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-regex@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" @@ -13169,14 +15230,6 @@ is-regex@^1.1.2: call-bind "^1.0.2" has-symbols "^1.0.1" -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" @@ -13207,6 +15260,13 @@ is-shared-array-buffer@^1.0.1: resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + is-ssh@^1.3.0: version "1.3.2" resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.3.2.tgz#a4b82ab63d73976fd8263cceee27f99a88bdae2b" @@ -13312,6 +15372,18 @@ is-weakref@^1.0.1: dependencies: call-bind "^1.0.0" +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-what@^3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" + integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA== + is-windows@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" @@ -13327,7 +15399,7 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= -is-wsl@^2.1.0, is-wsl@^2.2.0: +is-wsl@^2.1.0, is-wsl@^2.1.1, is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== @@ -13389,12 +15461,17 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.5: +istanbul-lib-coverage@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== -istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0: +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + +istanbul-lib-instrument@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630" integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== @@ -13407,32 +15484,52 @@ istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0: istanbul-lib-coverage "^2.0.5" semver "^6.0.0" -istanbul-lib-report@^2.0.4: - version "2.0.8" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33" - integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== +istanbul-lib-instrument@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" + integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== dependencies: - istanbul-lib-coverage "^2.0.5" - make-dir "^2.1.0" - supports-color "^6.1.0" + "@babel/core" "^7.7.5" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.0.0" + semver "^6.3.0" -istanbul-lib-source-maps@^3.0.1: - version "3.0.6" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" - integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz#7b49198b657b27a730b8e9cb601f1e1bff24c59a" + integrity sha512-czwUz525rkOFDJxfKK6mYfIs9zBKILyrZQxjz3ABhjQXhbhFsSbo1HW/BFcsDnfJYJWA6thRR5/TUY2qs5W99Q== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== dependencies: debug "^4.1.1" - istanbul-lib-coverage "^2.0.5" - make-dir "^2.1.0" - rimraf "^2.6.3" + istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@^2.2.6: - version "2.2.7" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.7.tgz#5d939f6237d7b48393cc0959eab40cd4fd056931" - integrity sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg== +istanbul-reports@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.4.tgz#1b6f068ecbc6c331040aab5741991273e609e40c" + integrity sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw== dependencies: html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" istanbul@0.4.5, istanbul@^0.4.0: version "0.4.5" @@ -13480,56 +15577,87 @@ isurl@^1.0.0-alpha5: has-to-string-tag-x "^1.2.0" is-object "^1.0.1" -jest-changed-files@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039" - integrity sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg== +jest-changed-files@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5" + integrity sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw== dependencies: - "@jest/types" "^24.9.0" - execa "^1.0.0" - throat "^4.0.0" + "@jest/types" "^27.5.1" + execa "^5.0.0" + throat "^6.0.1" -jest-cli@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.9.0.tgz#ad2de62d07472d419c6abc301fc432b98b10d2af" - integrity sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg== +jest-circus@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.5.1.tgz#37a5a4459b7bf4406e53d637b49d22c65d125ecc" + integrity sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw== dependencies: - "@jest/core" "^24.9.0" - "@jest/test-result" "^24.9.0" - "@jest/types" "^24.9.0" - chalk "^2.0.1" + "@jest/environment" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^0.7.0" + expect "^27.5.1" + is-generator-fn "^2.0.0" + jest-each "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" + slash "^3.0.0" + stack-utils "^2.0.3" + throat "^6.0.1" + +jest-cli@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.5.1.tgz#278794a6e6458ea8029547e6c6cbf673bd30b145" + integrity sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw== + dependencies: + "@jest/core" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + chalk "^4.0.0" exit "^0.1.2" - import-local "^2.0.0" - is-ci "^2.0.0" - jest-config "^24.9.0" - jest-util "^24.9.0" - jest-validate "^24.9.0" + graceful-fs "^4.2.9" + import-local "^3.0.2" + jest-config "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" prompts "^2.0.1" - realpath-native "^1.1.0" - yargs "^13.3.0" + yargs "^16.2.0" -jest-config@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.9.0.tgz#fb1bbc60c73a46af03590719efa4825e6e4dd1b5" - integrity sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ== +jest-config@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.5.1.tgz#5c387de33dca3f99ad6357ddeccd91bf3a0e4a41" + integrity sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA== dependencies: - "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^24.9.0" - "@jest/types" "^24.9.0" - babel-jest "^24.9.0" - chalk "^2.0.1" + "@babel/core" "^7.8.0" + "@jest/test-sequencer" "^27.5.1" + "@jest/types" "^27.5.1" + babel-jest "^27.5.1" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" glob "^7.1.1" - jest-environment-jsdom "^24.9.0" - jest-environment-node "^24.9.0" - jest-get-type "^24.9.0" - jest-jasmine2 "^24.9.0" - jest-regex-util "^24.3.0" - jest-resolve "^24.9.0" - jest-util "^24.9.0" - jest-validate "^24.9.0" - micromatch "^3.1.10" - pretty-format "^24.9.0" - realpath-native "^1.1.0" + graceful-fs "^4.2.9" + jest-circus "^27.5.1" + jest-environment-jsdom "^27.5.1" + jest-environment-node "^27.5.1" + jest-get-type "^27.5.1" + jest-jasmine2 "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-runner "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^27.5.1" + slash "^3.0.0" + strip-json-comments "^3.1.1" jest-dev-server@^4.4.0: version "4.4.0" @@ -13544,16 +15672,6 @@ jest-dev-server@^4.4.0: tree-kill "^1.2.2" wait-on "^3.3.0" -jest-diff@^24.3.0, jest-diff@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" - integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ== - dependencies: - chalk "^2.0.1" - diff-sequences "^24.9.0" - jest-get-type "^24.9.0" - pretty-format "^24.9.0" - jest-diff@^27.2.5, jest-diff@^27.4.2: version "27.4.2" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.4.2.tgz#786b2a5211d854f848e2dcc1e324448e9481f36f" @@ -13564,37 +15682,48 @@ jest-diff@^27.2.5, jest-diff@^27.4.2: jest-get-type "^27.4.0" pretty-format "^27.4.2" -jest-docblock@^24.3.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2" - integrity sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA== +jest-diff@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" + integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== dependencies: - detect-newline "^2.1.0" + chalk "^4.0.0" + diff-sequences "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" -jest-each@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.9.0.tgz#eb2da602e2a610898dbc5f1f6df3ba86b55f8b05" - integrity sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog== +jest-docblock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.5.1.tgz#14092f364a42c6108d42c33c8cf30e058e25f6c0" + integrity sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ== dependencies: - "@jest/types" "^24.9.0" - chalk "^2.0.1" - jest-get-type "^24.9.0" - jest-util "^24.9.0" - pretty-format "^24.9.0" + detect-newline "^3.0.0" -jest-environment-jsdom@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz#4b0806c7fc94f95edb369a69cc2778eec2b7375b" - integrity sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA== +jest-each@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.5.1.tgz#5bc87016f45ed9507fed6e4702a5b468a5b2c44e" + integrity sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ== dependencies: - "@jest/environment" "^24.9.0" - "@jest/fake-timers" "^24.9.0" - "@jest/types" "^24.9.0" - jest-mock "^24.9.0" - jest-util "^24.9.0" - jsdom "^11.5.1" + "@jest/types" "^27.5.1" + chalk "^4.0.0" + jest-get-type "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" + +jest-environment-jsdom@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz#ea9ccd1fc610209655a77898f86b2b559516a546" + integrity sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + jest-mock "^27.5.1" + jest-util "^27.5.1" + jsdom "^16.6.0" -jest-environment-node@24, "jest-environment-node@>=24 <=26", jest-environment-node@^24.9.0: +"jest-environment-node@>=24 <=26": version "24.9.0" resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.9.0.tgz#333d2d2796f9687f2aeebf0742b519f33c1cbfd3" integrity sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA== @@ -13605,6 +15734,18 @@ jest-environment-node@24, "jest-environment-node@>=24 <=26", jest-environment-no jest-mock "^24.9.0" jest-util "^24.9.0" +jest-environment-node@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.5.1.tgz#dedc2cfe52fab6b8f5714b4808aefa85357a365e" + integrity sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + jest-mock "^27.5.1" + jest-util "^27.5.1" + jest-environment-puppeteer@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/jest-environment-puppeteer/-/jest-environment-puppeteer-4.4.0.tgz#d82a37e0e0c51b63cc6b15dea101d53967508860" @@ -13615,16 +15756,16 @@ jest-environment-puppeteer@^4.4.0: jest-dev-server "^4.4.0" merge-deep "^3.0.2" -jest-get-type@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" - integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== - jest-get-type@^27.0.6, jest-get-type@^27.4.0: version "27.4.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.4.0.tgz#7503d2663fffa431638337b3998d39c5e928e9b5" integrity sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ== +jest-get-type@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" + integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== + jest-haste-map@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d" @@ -13644,35 +15785,56 @@ jest-haste-map@^24.9.0: optionalDependencies: fsevents "^1.2.7" -jest-jasmine2@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz#1f7b1bd3242c1774e62acabb3646d96afc3be6a0" - integrity sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw== +jest-haste-map@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" + integrity sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng== dependencies: - "@babel/traverse" "^7.1.0" - "@jest/environment" "^24.9.0" - "@jest/test-result" "^24.9.0" - "@jest/types" "^24.9.0" - chalk "^2.0.1" + "@jest/types" "^27.5.1" + "@types/graceful-fs" "^4.1.2" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^27.5.1" + jest-serializer "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" + micromatch "^4.0.4" + walker "^1.0.7" + optionalDependencies: + fsevents "^2.3.2" + +jest-jasmine2@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz#a037b0034ef49a9f3d71c4375a796f3b230d1ac4" + integrity sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/source-map" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" co "^4.6.0" - expect "^24.9.0" + expect "^27.5.1" is-generator-fn "^2.0.0" - jest-each "^24.9.0" - jest-matcher-utils "^24.9.0" - jest-message-util "^24.9.0" - jest-runtime "^24.9.0" - jest-snapshot "^24.9.0" - jest-util "^24.9.0" - pretty-format "^24.9.0" - throat "^4.0.0" - -jest-leak-detector@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz#b665dea7c77100c5c4f7dfcb153b65cf07dcf96a" - integrity sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA== + jest-each "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" + throat "^6.0.1" + +jest-leak-detector@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz#6ec9d54c3579dd6e3e66d70e3498adf80fde3fb8" + integrity sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ== dependencies: - jest-get-type "^24.9.0" - pretty-format "^24.9.0" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" jest-matcher-utils@=27.2.5: version "27.2.5" @@ -13684,15 +15846,15 @@ jest-matcher-utils@=27.2.5: jest-get-type "^27.0.6" pretty-format "^27.2.5" -jest-matcher-utils@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073" - integrity sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA== +jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" + integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== dependencies: - chalk "^2.0.1" - jest-diff "^24.9.0" - jest-get-type "^24.9.0" - pretty-format "^24.9.0" + chalk "^4.0.0" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" jest-matcher-utils@^27.2.5: version "27.4.2" @@ -13733,6 +15895,21 @@ jest-message-util@^27.2.5: slash "^3.0.0" stack-utils "^2.0.3" +jest-message-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf" + integrity sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^27.5.1" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^27.5.1" + slash "^3.0.0" + stack-utils "^2.0.3" + jest-mock@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.9.0.tgz#c22835541ee379b908673ad51087a2185c13f1c6" @@ -13740,7 +15917,15 @@ jest-mock@^24.9.0: dependencies: "@jest/types" "^24.9.0" -jest-pnp-resolver@^1.2.1: +jest-mock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" + integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og== + dependencies: + "@jest/types" "^27.5.1" + "@types/node" "*" + +jest-pnp-resolver@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== @@ -13753,7 +15938,7 @@ jest-puppeteer@^4.4.0: expect-puppeteer "^4.4.0" jest-environment-puppeteer "^4.4.0" -jest-regex-util@^24.3.0, jest-regex-util@^24.9.0: +jest-regex-util@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.9.0.tgz#c13fb3380bde22bf6575432c493ea8fe37965636" integrity sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA== @@ -13763,103 +15948,131 @@ jest-regex-util@^27.0.6: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.4.0.tgz#e4c45b52653128843d07ad94aec34393ea14fbca" integrity sha512-WeCpMpNnqJYMQoOjm1nTtsgbR4XHAk1u00qDoNBQoykM280+/TmgA5Qh5giC1ecy6a5d4hbSsHzpBtu5yvlbEg== -jest-resolve-dependencies@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz#ad055198959c4cfba8a4f066c673a3f0786507ab" - integrity sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g== - dependencies: - "@jest/types" "^24.9.0" - jest-regex-util "^24.3.0" - jest-snapshot "^24.9.0" +jest-regex-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" + integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== -jest-resolve@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321" - integrity sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ== +jest-resolve-dependencies@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz#d811ecc8305e731cc86dd79741ee98fed06f1da8" + integrity sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg== dependencies: - "@jest/types" "^24.9.0" - browser-resolve "^1.11.3" - chalk "^2.0.1" - jest-pnp-resolver "^1.2.1" - realpath-native "^1.1.0" + "@jest/types" "^27.5.1" + jest-regex-util "^27.5.1" + jest-snapshot "^27.5.1" -jest-runner@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.9.0.tgz#574fafdbd54455c2b34b4bdf4365a23857fcdf42" - integrity sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg== +jest-resolve@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.5.1.tgz#a2f1c5a0796ec18fe9eb1536ac3814c23617b384" + integrity sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw== dependencies: - "@jest/console" "^24.7.1" - "@jest/environment" "^24.9.0" - "@jest/test-result" "^24.9.0" - "@jest/types" "^24.9.0" - chalk "^2.4.2" - exit "^0.1.2" - graceful-fs "^4.1.15" - jest-config "^24.9.0" - jest-docblock "^24.3.0" - jest-haste-map "^24.9.0" - jest-jasmine2 "^24.9.0" - jest-leak-detector "^24.9.0" - jest-message-util "^24.9.0" - jest-resolve "^24.9.0" - jest-runtime "^24.9.0" - jest-util "^24.9.0" - jest-worker "^24.6.0" + "@jest/types" "^27.5.1" + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-pnp-resolver "^1.2.2" + jest-util "^27.5.1" + jest-validate "^27.5.1" + resolve "^1.20.0" + resolve.exports "^1.1.0" + slash "^3.0.0" + +jest-runner@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.5.1.tgz#071b27c1fa30d90540805c5645a0ec167c7b62e5" + integrity sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ== + dependencies: + "@jest/console" "^27.5.1" + "@jest/environment" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.8.1" + graceful-fs "^4.2.9" + jest-docblock "^27.5.1" + jest-environment-jsdom "^27.5.1" + jest-environment-node "^27.5.1" + jest-haste-map "^27.5.1" + jest-leak-detector "^27.5.1" + jest-message-util "^27.5.1" + jest-resolve "^27.5.1" + jest-runtime "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" source-map-support "^0.5.6" - throat "^4.0.0" + throat "^6.0.1" -jest-runtime@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.9.0.tgz#9f14583af6a4f7314a6a9d9f0226e1a781c8e4ac" - integrity sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw== - dependencies: - "@jest/console" "^24.7.1" - "@jest/environment" "^24.9.0" - "@jest/source-map" "^24.3.0" - "@jest/transform" "^24.9.0" - "@jest/types" "^24.9.0" - "@types/yargs" "^13.0.0" - chalk "^2.0.1" - exit "^0.1.2" +jest-runtime@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.5.1.tgz#4896003d7a334f7e8e4a53ba93fb9bcd3db0a1af" + integrity sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/globals" "^27.5.1" + "@jest/source-map" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + execa "^5.0.0" glob "^7.1.3" - graceful-fs "^4.1.15" - jest-config "^24.9.0" - jest-haste-map "^24.9.0" - jest-message-util "^24.9.0" - jest-mock "^24.9.0" - jest-regex-util "^24.3.0" - jest-resolve "^24.9.0" - jest-snapshot "^24.9.0" - jest-util "^24.9.0" - jest-validate "^24.9.0" - realpath-native "^1.1.0" - slash "^2.0.0" - strip-bom "^3.0.0" - yargs "^13.3.0" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-message-util "^27.5.1" + jest-mock "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + slash "^3.0.0" + strip-bom "^4.0.0" jest-serializer@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.9.0.tgz#e6d7d7ef96d31e8b9079a714754c5d5c58288e73" integrity sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ== -jest-snapshot@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.9.0.tgz#ec8e9ca4f2ec0c5c87ae8f925cf97497b0e951ba" - integrity sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew== +jest-serializer@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" + integrity sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w== dependencies: + "@types/node" "*" + graceful-fs "^4.2.9" + +jest-snapshot@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.5.1.tgz#b668d50d23d38054a51b42c4039cab59ae6eb6a1" + integrity sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA== + dependencies: + "@babel/core" "^7.7.2" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/traverse" "^7.7.2" "@babel/types" "^7.0.0" - "@jest/types" "^24.9.0" - chalk "^2.0.1" - expect "^24.9.0" - jest-diff "^24.9.0" - jest-get-type "^24.9.0" - jest-matcher-utils "^24.9.0" - jest-message-util "^24.9.0" - jest-resolve "^24.9.0" - mkdirp "^0.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/babel__traverse" "^7.0.4" + "@types/prettier" "^2.1.5" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^27.5.1" + graceful-fs "^4.2.9" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + jest-haste-map "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-util "^27.5.1" natural-compare "^1.4.0" - pretty-format "^24.9.0" - semver "^6.2.0" + pretty-format "^27.5.1" + semver "^7.3.2" jest-util@^24.9.0: version "24.9.0" @@ -13879,30 +16092,51 @@ jest-util@^24.9.0: slash "^2.0.0" source-map "^0.6.0" -jest-validate@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.9.0.tgz#0775c55360d173cd854e40180756d4ff52def8ab" - integrity sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ== +jest-util@^27.0.0, jest-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" + integrity sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw== dependencies: - "@jest/types" "^24.9.0" - camelcase "^5.3.1" - chalk "^2.0.1" - jest-get-type "^24.9.0" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.5.1.tgz#9197d54dc0bdb52260b8db40b46ae668e04df067" + integrity sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ== + dependencies: + "@jest/types" "^27.5.1" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^27.5.1" leven "^3.1.0" - pretty-format "^24.9.0" + pretty-format "^27.5.1" + +jest-watcher@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.5.1.tgz#71bd85fb9bde3a2c2ec4dc353437971c43c642a2" + integrity sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw== + dependencies: + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + jest-util "^27.5.1" + string-length "^4.0.1" -jest-watcher@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.9.0.tgz#4b56e5d1ceff005f5b88e528dc9afc8dd4ed2b3b" - integrity sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw== +jest-worker@26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f" + integrity sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw== dependencies: - "@jest/test-result" "^24.9.0" - "@jest/types" "^24.9.0" - "@types/yargs" "^13.0.0" - ansi-escapes "^3.0.0" - chalk "^2.0.1" - jest-util "^24.9.0" - string-length "^2.0.0" + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" jest-worker@27.0.0-next.5: version "27.0.0-next.5" @@ -13913,7 +16147,7 @@ jest-worker@27.0.0-next.5: merge-stream "^2.0.0" supports-color "^8.0.0" -jest-worker@^24.6.0, jest-worker@^24.9.0: +jest-worker@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw== @@ -13921,7 +16155,7 @@ jest-worker@^24.6.0, jest-worker@^24.9.0: merge-stream "^2.0.0" supports-color "^6.1.0" -jest-worker@^26.2.1: +jest-worker@^26.2.1, jest-worker@^26.3.0: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== @@ -13939,13 +16173,23 @@ jest-worker@^27.0.6: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-24.9.0.tgz#987d290c05a08b52c56188c1002e368edb007171" - integrity sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw== +jest-worker@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== dependencies: - import-local "^2.0.0" - jest-cli "^24.9.0" + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest/-/jest-27.5.1.tgz#dadf33ba70a779be7a6fc33015843b51494f63fc" + integrity sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ== + dependencies: + "@jest/core" "^27.5.1" + import-local "^3.0.2" + jest-cli "^27.5.1" jmespath@0.15.0: version "0.15.0" @@ -14016,77 +16260,13 @@ jsdoctypeparser@^9.0.0: resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-9.0.0.tgz#8c97e2fb69315eb274b0f01377eaa5c940bd7b26" integrity sha512-jrTA2jJIL6/DAEILBEh2/w9QxCuwmvNXIry39Ay/HVfhE3o2yVV0U44blYkqdHA/OKloJEqvJy0xU+GSdE2SIw== -jsdom@^11.5.1: - version "11.12.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" - integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw== - dependencies: - abab "^2.0.0" - acorn "^5.5.3" - acorn-globals "^4.1.0" - array-equal "^1.0.0" - cssom ">= 0.3.2 < 0.4.0" - cssstyle "^1.0.0" - data-urls "^1.0.0" - domexception "^1.0.1" - escodegen "^1.9.1" - html-encoding-sniffer "^1.0.2" - left-pad "^1.3.0" - nwsapi "^2.0.7" - parse5 "4.0.0" - pn "^1.1.0" - request "^2.87.0" - request-promise-native "^1.0.5" - sax "^1.2.4" - symbol-tree "^3.2.2" - tough-cookie "^2.3.4" - w3c-hr-time "^1.0.1" - webidl-conversions "^4.0.2" - whatwg-encoding "^1.0.3" - whatwg-mimetype "^2.1.0" - whatwg-url "^6.4.1" - ws "^5.2.0" - xml-name-validator "^3.0.0" - -jsdom@^15.0.0: - version "15.2.1" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-15.2.1.tgz#d2feb1aef7183f86be521b8c6833ff5296d07ec5" - integrity sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g== - dependencies: - abab "^2.0.0" - acorn "^7.1.0" - acorn-globals "^4.3.2" - array-equal "^1.0.0" - cssom "^0.4.1" - cssstyle "^2.0.0" - data-urls "^1.1.0" - domexception "^1.0.1" - escodegen "^1.11.1" - html-encoding-sniffer "^1.0.2" - nwsapi "^2.2.0" - parse5 "5.1.0" - pn "^1.1.0" - request "^2.88.0" - request-promise-native "^1.0.7" - saxes "^3.1.9" - symbol-tree "^3.2.2" - tough-cookie "^3.0.1" - w3c-hr-time "^1.0.1" - w3c-xmlserializer "^1.1.2" - webidl-conversions "^4.0.2" - whatwg-encoding "^1.0.5" - whatwg-mimetype "^2.3.0" - whatwg-url "^7.0.0" - ws "^7.0.0" - xml-name-validator "^3.0.0" - -jsdom@^16.2.2: - version "16.5.2" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.5.2.tgz#583fac89a0aea31dbf6237e7e4bedccd9beab472" - integrity sha512-JxNtPt9C1ut85boCbJmffaQ06NBnzkQY/MWO3YxPW8IWS38A26z+B1oBvA9LwKrytewdfymnhi4UNH3/RAgZrg== +jsdom@^16.6.0: + version "16.7.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" + integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== dependencies: abab "^2.0.5" - acorn "^8.1.0" + acorn "^8.2.4" acorn-globals "^6.0.0" cssom "^0.4.4" cssstyle "^2.3.0" @@ -14094,12 +16274,13 @@ jsdom@^16.2.2: decimal.js "^10.2.1" domexception "^2.0.1" escodegen "^2.0.0" + form-data "^3.0.0" html-encoding-sniffer "^2.0.1" - is-potential-custom-element-name "^1.0.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-potential-custom-element-name "^1.0.1" nwsapi "^2.2.0" parse5 "6.0.1" - request "^2.88.2" - request-promise-native "^1.0.9" saxes "^5.0.1" symbol-tree "^3.2.4" tough-cookie "^4.0.0" @@ -14109,9 +16290,42 @@ jsdom@^16.2.2: whatwg-encoding "^1.0.5" whatwg-mimetype "^2.3.0" whatwg-url "^8.5.0" - ws "^7.4.4" + ws "^7.4.6" xml-name-validator "^3.0.0" +jsdom@^19.0.0: + version "19.0.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-19.0.0.tgz#93e67c149fe26816d38a849ea30ac93677e16b6a" + integrity sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A== + dependencies: + abab "^2.0.5" + acorn "^8.5.0" + acorn-globals "^6.0.0" + cssom "^0.5.0" + cssstyle "^2.3.0" + data-urls "^3.0.1" + decimal.js "^10.3.1" + domexception "^4.0.0" + escodegen "^2.0.0" + form-data "^4.0.0" + html-encoding-sniffer "^3.0.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.0" + parse5 "6.0.1" + saxes "^5.0.1" + symbol-tree "^3.2.4" + tough-cookie "^4.0.0" + w3c-hr-time "^1.0.2" + w3c-xmlserializer "^3.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^2.0.0" + whatwg-mimetype "^3.0.0" + whatwg-url "^10.0.0" + ws "^8.2.3" + xml-name-validator "^4.0.0" + jsesc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" @@ -14186,6 +16400,11 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +json3@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" + integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== + json5@2.x, json5@^2.1.2: version "2.2.0" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" @@ -14205,6 +16424,16 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" +json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + +jsonc-parser@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.0.tgz#7c7fc988ee1486d35734faaaa866fadb00fa91ee" + integrity sha512-b0EBt8SWFNnixVdvoR2ZtEGa9ZqLhbJnOjezn+WP+8kspFm+PFYDN8Z4Bc7pRlDjvuVcADSUkroIuTWWn/YiIA== + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" @@ -14355,6 +16584,13 @@ karma-sinon@^1.0.5: resolved "https://registry.yarnpkg.com/karma-sinon/-/karma-sinon-1.0.5.tgz#4e3443f2830fdecff624d3747163f1217daa2a9a" integrity sha1-TjRD8oMP3s/2JNN0cWPxIX2qKpo= +karma-source-map-support@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz#58526ceccf7e8730e56effd97a4de8d712ac0d6b" + integrity sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A== + dependencies: + source-map-support "^0.5.5" + karma-typescript-es6-transform@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/karma-typescript-es6-transform/-/karma-typescript-es6-transform-4.1.1.tgz#b24e8ea8cb8431c5342f7bbb9f1fd6060335ca39" @@ -14458,6 +16694,11 @@ keyv@3.0.0: dependencies: json-buffer "3.0.0" +killable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" + integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== + kind-of@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" @@ -14494,6 +16735,11 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +klona@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc" + integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== + last-call-webpack-plugin@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555" @@ -14528,11 +16774,6 @@ leek@0.0.24: lodash.assign "^3.2.0" rsvp "^3.0.21" -left-pad@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" - integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== - lerna@3.13.4: version "3.13.4" resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.13.4.tgz#03026c11c5643f341fda42e4fb1882e2df35e6cb" @@ -14556,11 +16797,44 @@ lerna@3.13.4: import-local "^1.0.0" npmlog "^4.1.2" +less-loader@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-6.2.0.tgz#8b26f621c155b342eefc24f5bd6e9dc40c42a719" + integrity sha512-Cl5h95/Pz/PWub/tCBgT1oNMFeH1WTD33piG80jn5jr12T4XbxZcjThwNXDQ7AG649WEynuIzO4b0+2Tn9Qolg== + dependencies: + clone "^2.1.2" + less "^3.11.3" + loader-utils "^2.0.0" + schema-utils "^2.7.0" + +less@^3.10.3, less@^3.11.3: + version "3.13.1" + resolved "https://registry.yarnpkg.com/less/-/less-3.13.1.tgz#0ebc91d2a0e9c0c6735b83d496b0ab0583077909" + integrity sha512-SwA1aQXGUvp+P5XdZslUOhhLnClSLIjWvJhmd+Vgib5BFIr9lMNlQwmwUNOjXThF/A0x+MCYYPeWEfeWiLRnTw== + dependencies: + copy-anything "^2.0.1" + tslib "^1.10.0" + optionalDependencies: + errno "^0.1.1" + graceful-fs "^4.1.2" + image-size "~0.5.0" + make-dir "^2.1.0" + mime "^1.4.1" + native-request "^1.0.5" + source-map "~0.6.0" + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== +levenary@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77" + integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ== + dependencies: + leven "^3.1.0" + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -14602,6 +16876,14 @@ libnpmpublish@^1.1.1: semver "^5.5.1" ssri "^6.0.1" +license-webpack-plugin@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.0.tgz#c00f70d5725ba0408de208acb9e66612cc2eceda" + integrity sha512-JK/DXrtN6UeYQSgkg5q1+pgJ8aiKPL9tnz9Wzw+Ikkf+8mJxG56x6t8O+OH/tAeF/5NREnelTEMyFtbJNkjH4w== + dependencies: + "@types/webpack-sources" "^0.1.5" + webpack-sources "^1.2.0" + lie@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" @@ -14703,16 +16985,7 @@ loader-utils@1.2.3: emojis-list "^2.0.0" json5 "^1.0.1" -loader-utils@^1.2.3, loader-utils@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - -loader-utils@^2.0.0: +loader-utils@2.0.0, loader-utils@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== @@ -14721,6 +16994,15 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" +loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" + integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" + loader.js@~4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/loader.js/-/loader.js-4.7.0.tgz#a1a52902001c83631efde9688b8ab3799325ef1f" @@ -14993,7 +17275,7 @@ log-symbols@2.2.0, log-symbols@^2.1.0, log-symbols@^2.2.0: dependencies: chalk "^2.0.1" -log-symbols@^4.1.0: +log-symbols@^4.0.0, log-symbols@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== @@ -15023,6 +17305,11 @@ log4js@^6.4.1: rfdc "^1.3.0" streamroller "^3.0.2" +loglevel@^1.6.8: + version "1.8.0" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114" + integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA== + lolex@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7" @@ -15086,6 +17373,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^7.4.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.8.0.tgz#649aaeb294a56297b5cbc5d70f198dcc5ebe5747" + integrity sha512-AmXqneQZL3KZMIgBpaPTeI6pfwh+xQ2vutMsyqOu1TBdEXFZgpG/80wuJ531w2ZN7TI0/oc8CPxzh/DKQudZqg== + lru_map@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" @@ -15141,6 +17433,20 @@ magic-string@0.25.7, magic-string@^0.25.1, magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.4" +magic-string@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.16.0.tgz#970ebb0da7193301285fb1aa650f39bdd81eb45a" + integrity sha1-lw67DacZMwEoX7GqZQ85vdgetFo= + dependencies: + vlq "^0.2.1" + +magic-string@^0.25.0: + version "0.25.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== + dependencies: + sourcemap-codec "^1.4.8" + make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" @@ -15439,6 +17745,13 @@ merge-descriptors@1.0.1: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= +merge-source-map@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" + integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== + dependencies: + source-map "^0.6.1" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -15527,6 +17840,11 @@ mime-db@1.51.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.26, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.30" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.30.tgz#6e7be8b4c479825f85ed6326695db73f9305d62d" @@ -15541,12 +17859,19 @@ mime-types@^2.1.27, mime-types@~2.1.34: dependencies: mime-db "1.51.0" -mime@1.6.0: +mime-types@~2.1.17: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0, mime@^1.4.1: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.2.0, mime@^2.3.1, mime@^2.4.6, mime@^2.5.2: +mime@^2.2.0, mime@^2.3.1, mime@^2.4.4, mime@^2.4.6, mime@^2.5.2: version "2.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== @@ -15571,6 +17896,16 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== +mini-css-extract-plugin@0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.10.0.tgz#a0e6bfcad22a9c73f6c882a3c7557a98e2d3d27d" + integrity sha512-QgKgJBjaJhxVPwrLNqqwNS0AGkuQQ31Hp4xGXEK/P7wehEg6qmNtReHKai3zRXqY60wGVWLYcOMJK2b98aGc3A== + dependencies: + loader-utils "^1.1.0" + normalize-url "1.9.1" + schema-utils "^1.0.0" + webpack-sources "^1.1.0" + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -15620,6 +17955,27 @@ minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.2: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + minipass@^2.2.0, minipass@^2.3.5, minipass@^2.6.0, minipass@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" @@ -15628,6 +17984,13 @@ minipass@^2.2.0, minipass@^2.3.5, minipass@^2.6.0, minipass@^2.9.0: safe-buffer "^5.1.2" yallist "^3.0.0" +minipass@^3.0.0, minipass@^3.1.1: + version "3.1.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" + integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ== + dependencies: + yallist "^4.0.0" + minizlib@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" @@ -15635,6 +17998,14 @@ minizlib@^1.3.3: dependencies: minipass "^2.9.0" +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + mississippi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" @@ -15679,14 +18050,14 @@ mkdirp@0.5.4: dependencies: minimist "^1.2.5" -mkdirp@0.5.x, mkdirp@0.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.0, mkdirp@~0.5.1: +mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== dependencies: minimist "^1.2.5" -mkdirp@1.0.4, mkdirp@^1.0.4: +mkdirp@1.0.4, mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -15778,10 +18149,10 @@ mongodb-memory-server-core@7.6.3: uuid "^8.3.1" yauzl "^2.10.0" -mongodb-memory-server@^7.6.3: +mongodb-memory-server-global@^7.6.3: version "7.6.3" - resolved "https://registry.yarnpkg.com/mongodb-memory-server/-/mongodb-memory-server-7.6.3.tgz#8b2827363ca16aaf250cba07f7a2b49e502735d4" - integrity sha512-yHDE9FGxOpSRUzitF9Qx3JjEgayCSJI3JOW2wgeBH/5PAsUdisy2nRxRiNwwLDooQ7tohllWCRTXlWqyarUEMQ== + resolved "https://registry.yarnpkg.com/mongodb-memory-server-global/-/mongodb-memory-server-global-7.6.3.tgz#ad662a640db254eea7927668834c26b665c13547" + integrity sha512-WLlMqkEasuanHjoxyMxlyvQ/HtJgq0eGyrfCXX6lTnY/26Zfs96W2daeWLOQ48VLInSOh2umBvE74Ykqj7gVyA== dependencies: mongodb-memory-server-core "7.6.3" tslib "^2.3.0" @@ -15847,6 +18218,19 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= + +multicast-dns@^6.0.1: + version "6.2.3" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" + integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== + dependencies: + dns-packet "^1.3.1" + thunky "^1.0.2" + multimatch@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b" @@ -15882,6 +18266,15 @@ mysql@^2.18.1: safe-buffer "5.1.2" sqlstring "2.3.1" +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + nan@^2.12.1: version "2.14.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" @@ -15909,6 +18302,11 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +native-request@^1.0.5: + version "1.1.0" + resolved "https://registry.yarnpkg.com/native-request/-/native-request-1.1.0.tgz#acdb30fe2eefa3e1bc8c54b3a6852e9c5c0d3cb0" + integrity sha512-uZ5rQaeRn15XmpgE0xoPL8YWqcX90VtCFglYwAgkvKM5e8fog+vePLAhHxuuv/gRkrQxIeh5U3q9sMNUrENqWw== + native-url@0.3.4: version "0.3.4" resolved "https://registry.yarnpkg.com/native-url/-/native-url-0.3.4.tgz#29c943172aed86c63cee62c8c04db7f5756661f8" @@ -15944,6 +18342,11 @@ new-find-package-json@^1.1.0: debug "^4.3.2" tslib "^2.3.0" +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + next@10.1.3: version "10.1.3" resolved "https://registry.yarnpkg.com/next/-/next-10.1.3.tgz#e26e8371343a42bc2ba9be5cb253a7d324d03673" @@ -16000,6 +18403,37 @@ next@10.1.3: vm-browserify "1.1.2" watchpack "2.1.1" +ng-packagr@^10.1.0: + version "10.1.2" + resolved "https://registry.yarnpkg.com/ng-packagr/-/ng-packagr-10.1.2.tgz#7c869ec5bea92ff3ab65392913ad4e8af749799a" + integrity sha512-pm61gu6jPkohL8tFWk+2DwUtb3rs5GpND1ZjKUYv5WUJPUQmBfG5WvEO/CDVQpSDWhNWWLTt17NIQ+RS3hNUHg== + dependencies: + "@rollup/plugin-commonjs" "^15.0.0" + "@rollup/plugin-json" "^4.0.0" + "@rollup/plugin-node-resolve" "^9.0.0" + ajv "^6.12.3" + ansi-colors "^4.1.1" + autoprefixer "^9.6.5" + browserslist "^4.7.0" + chokidar "^3.2.1" + commander "^6.0.0" + cssnano-preset-default "^4.0.7" + fs-extra "^9.0.0" + glob "^7.1.2" + injection-js "^2.2.1" + less "^3.10.3" + node-sass-tilde-importer "^1.0.0" + postcss "^7.0.29" + postcss-url "^8.0.0" + read-pkg-up "^5.0.0" + rimraf "^3.0.0" + rollup "^2.8.0" + rollup-plugin-sourcemaps "^0.6.0" + rxjs "^6.5.0" + sass "^1.23.0" + stylus "^0.54.7" + terser "^5.0.0" + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -16165,17 +18599,6 @@ node-modules-regexp@^1.0.0: resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= -node-notifier@^5.4.2: - version "5.4.5" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.5.tgz#0cbc1a2b0f658493b4025775a13ad938e96091ef" - integrity sha512-tVbHs7DyTLtzOiN78izLA85zRqB9NvEXkAf014Vx3jtSvn/xBl6bR8ZYifj+dFcFrKI21huSQgJZ6ZtL3B4HfQ== - dependencies: - growly "^1.3.0" - is-wsl "^1.1.0" - semver "^5.5.0" - shellwords "^0.1.1" - which "^1.3.0" - node-notifier@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-9.0.1.tgz#cea837f4c5e733936c7b9005e6545cea825d1af4" @@ -16198,6 +18621,18 @@ node-releases@^2.0.1: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== +node-releases@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.3.tgz#225ee7488e4a5e636da8da52854844f9d716ca96" + integrity sha512-maHFz6OLqYxz+VQyCAtA3PTX4UP/53pa05fyDNc9CwjvJ0yEh6+xBwKsgCxMNhS8taUKBFYxfuiaD9U/55iFaw== + +node-sass-tilde-importer@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/node-sass-tilde-importer/-/node-sass-tilde-importer-1.0.2.tgz#1a15105c153f648323b4347693fdb0f331bad1ce" + integrity sha512-Swcmr38Y7uB78itQeBm3mThjxBy9/Ah/ykPIaURY/L6Nec9AyRoL/jJ7ECfMR+oZeCTVQNxVMu/aHU+TLRVbdg== + dependencies: + find-parent-dir "^0.3.0" + node-source-walk@^4.0.0, node-source-walk@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/node-source-walk/-/node-source-walk-4.2.0.tgz#c2efe731ea8ba9c03c562aa0a9d984e54f27bc2c" @@ -16249,6 +18684,21 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= + +normalize-url@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" + integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw= + dependencies: + object-assign "^4.0.1" + prepend-http "^1.0.0" + query-string "^4.1.0" + sort-keys "^1.0.0" + normalize-url@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" @@ -16270,6 +18720,13 @@ npm-bundled@^1.0.1, npm-bundled@^1.1.1: dependencies: npm-normalize-package-bin "^1.0.1" +npm-install-checks@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-4.0.0.tgz#a37facc763a2fde0497ef2c6d0ac7c3fbe00d7b4" + integrity sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w== + dependencies: + semver "^7.1.1" + npm-lifecycle@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/npm-lifecycle/-/npm-lifecycle-2.1.1.tgz#0027c09646f0fd346c5c93377bdaba59c6748fdf" @@ -16289,6 +18746,15 @@ npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== +npm-package-arg@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.0.1.tgz#9d76f8d7667b2373ffda60bb801a27ef71e3e270" + integrity sha512-/h5Fm6a/exByzFSTm7jAyHbgOqErl9qSNJDQF32Si/ZzgwT2TERVxRxn3Jurw1wflgyVVAxnFR4fRHPM7y1ClQ== + dependencies: + hosted-git-info "^3.0.2" + semver "^7.0.0" + validate-npm-package-name "^3.0.0" + "npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0: version "6.1.1" resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.1.1.tgz#02168cb0a49a2b75bf988a28698de7b529df5cb7" @@ -16299,6 +18765,15 @@ npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: semver "^5.6.0" validate-npm-package-name "^3.0.0" +npm-package-arg@^8.0.0: + version "8.1.5" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.5.tgz#3369b2d5fe8fdc674baa7f1786514ddc15466e44" + integrity sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q== + dependencies: + hosted-git-info "^4.0.1" + semver "^7.3.4" + validate-npm-package-name "^3.0.0" + npm-package-arg@^8.1.0: version "8.1.2" resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.2.tgz#b868016ae7de5619e729993fbd8d11dc3c52ab62" @@ -16327,6 +18802,15 @@ npm-packlist@^2.1.4: npm-bundled "^1.1.1" npm-normalize-package-bin "^1.0.1" +npm-pick-manifest@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz#2befed87b0fce956790f62d32afb56d7539c022a" + integrity sha512-ygs4k6f54ZxJXrzT0x34NybRlLeZ4+6nECAIbr2i0foTnijtS1TJiyzpqtuUAJOps/hO0tNDr8fRV5g+BtRlTw== + dependencies: + npm-install-checks "^4.0.0" + npm-package-arg "^8.0.0" + semver "^7.0.0" + npm-pick-manifest@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz#f4d9e5fd4be2153e5f4e5f9b7be8dc419a99abb7" @@ -16390,7 +18874,7 @@ npm-run-path@^3.0.0: dependencies: path-key "^3.0.0" -npm-run-path@^4.0.0: +npm-run-path@^4.0.0, npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== @@ -16426,12 +18910,17 @@ null-check@^1.0.0: resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd" integrity sha1-l33/1xdgErnsMNKjnbXPcqBDnt0= +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= + number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= -nwsapi@^2.0.7, nwsapi@^2.2.0: +nwsapi@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== @@ -16465,6 +18954,11 @@ object-inspect@^1.11.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.1.tgz#d4bd7d7de54b9a75599f59a00bd698c1f1c6549b" integrity sha512-If7BjFlpkzzBeV1cqgT3OSWT3azyoxDGajR+iGnFBfVV2EWyDyWaZZW2ERDjUaY2QM8i5jI3Sj7mhsM4DDAqWA== +object-inspect@^1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" + integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== + object-inspect@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" @@ -16556,6 +19050,11 @@ object.values@^1.1.0, object.values@^1.1.1, object.values@^1.1.3: es-abstract "^1.18.0-next.2" has "^1.0.3" +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + octokit-pagination-methods@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" @@ -16587,13 +19086,21 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" -onetime@^5.1.0: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" +open@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/open/-/open-7.2.0.tgz#212959bd7b0ce2e8e3676adc76e3cf2f0a2498b4" + integrity sha512-4HeyhxCvBTI5uBePsAdi55C5fmqnWZ2e2MlmvWi5KW5tdH5rxoiv/aMtbeVxKZc3eWkT1GymMnLG8XC4Rq4TDQ== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + open@^8.3.0: version "8.4.0" resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" @@ -16608,6 +19115,13 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== +opn@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" + integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== + dependencies: + is-wsl "^1.1.0" + optimize-css-assets-webpack-plugin@^5.0.4: version "5.0.4" resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.4.tgz#85883c6528aaa02e30bbad9908c92926bb52dc90" @@ -16647,6 +19161,20 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +ora@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.0.0.tgz#4f0b34f2994877b49b452a707245ab1e9f6afccb" + integrity sha512-s26qdWqke2kjN/wC4dy+IQPBIMWBJlSU/0JZhk30ZDBLelW25rv66yutUWARMigpGPzcXHb+Nac5pNhN/WsARw== + dependencies: + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.4.0" + is-interactive "^1.0.0" + log-symbols "^4.0.0" + mute-stream "0.0.8" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + ora@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318" @@ -16674,6 +19202,13 @@ ora@^5.1.0, ora@^5.3.0: strip-ansi "^6.0.0" wcwidth "^1.0.1" +original@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" + integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== + dependencies: + url-parse "^1.4.3" + os-browserify@0.3.0, os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" @@ -16729,13 +19264,6 @@ p-defer@^3.0.0: resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-3.0.0.tgz#d1dceb4ee9b2b604b1d94ffec83760175d4e6f83" integrity sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw== -p-each-series@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71" - integrity sha1-kw89Et0fUOdDRFeiLNbwSsatf3E= - dependencies: - p-reduce "^1.0.0" - p-event@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5" @@ -16824,6 +19352,18 @@ p-map@^1.2.0: resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + p-pipe@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/p-pipe/-/p-pipe-1.2.0.tgz#4b1a11399a11520a67790ee5a0c1d5881d6befe9" @@ -16834,6 +19374,13 @@ p-reduce@^1.0.0: resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= +p-retry@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" + integrity sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w== + dependencies: + retry "^0.12.0" + p-timeout@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" @@ -16885,7 +19432,7 @@ packet-reader@1.0.0: resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== -pacote@^9.5.0: +pacote@9.5.12, pacote@^9.5.0: version "9.5.12" resolved "https://registry.yarnpkg.com/pacote/-/pacote-9.5.12.tgz#1e11dd7a8d736bcc36b375a9804d41bb0377bf66" integrity sha512-BUIj/4kKbwWg4RtnBncXPJd15piFSVNpTzY0rysSr3VnMowTYgkGKcaHrbReepAkjTr8lH2CVWRi58Spg2CicQ== @@ -16988,7 +19535,7 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" -parse-json@^5.0.0: +parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -17033,15 +19580,12 @@ parse-url@^5.0.0: parse-path "^4.0.0" protocols "^1.4.0" -parse5@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" - integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== - -parse5@5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" - integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== +parse5-htmlparser2-tree-adapter@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" + integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== + dependencies: + parse5 "^6.0.1" parse5@6.0.1, parse5@^6.0.1: version "6.0.1" @@ -17058,7 +19602,7 @@ parseuri@0.0.6: resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a" integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow== -parseurl@~1.3.3: +parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -17113,6 +19657,11 @@ path-is-absolute@1.0.1, path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= +path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + path-is-network-drive@^1.0.13: version "1.0.13" resolved "https://registry.yarnpkg.com/path-is-network-drive/-/path-is-network-drive-1.0.13.tgz#c9aa0183eb72c328aa83f43def93ddcb9d7ec4d4" @@ -17283,6 +19832,11 @@ pgpass@1.x: dependencies: split2 "^4.1.0" +picocolors@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" + integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -17342,6 +19896,11 @@ pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" +pirates@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + pixelmatch@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.2.1.tgz#9e4e4f4aa59648208a31310306a5bed5522b0d65" @@ -17440,11 +19999,6 @@ pluralize@^8.0.0: resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== -pn@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" - integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== - pngjs@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-4.0.1.tgz#f803869bb2fc1bfe1bf99aa4ec21c108117cfdbe" @@ -17462,7 +20016,7 @@ pnp-webpack-plugin@1.6.4, pnp-webpack-plugin@^1.6.4: dependencies: ts-pnp "^1.1.6" -portfinder@^1.0.28: +portfinder@^1.0.26, portfinder@^1.0.28: version "1.0.28" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== @@ -17532,6 +20086,34 @@ postcss-discard-overridden@^4.0.1: dependencies: postcss "^7.0.0" +postcss-import@12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.1.tgz#cf8c7ab0b5ccab5649024536e565f841928b7153" + integrity sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw== + dependencies: + postcss "^7.0.1" + postcss-value-parser "^3.2.3" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-load-config@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.2.tgz#c5ea504f2c4aef33c7359a34de3573772ad7502a" + integrity sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw== + dependencies: + cosmiconfig "^5.0.0" + import-cwd "^2.0.0" + +postcss-loader@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d" + integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA== + dependencies: + loader-utils "^1.1.0" + postcss "^7.0.0" + postcss-load-config "^2.0.0" + schema-utils "^1.0.0" + postcss-merge-longhand@^4.0.11: version "4.0.11" resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24" @@ -17594,11 +20176,28 @@ postcss-minify-selectors@^4.0.2: postcss "^7.0.0" postcss-selector-parser "^3.0.0" +postcss-modules-extract-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" + integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== + dependencies: + postcss "^7.0.5" + postcss-modules-extract-imports@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== +postcss-modules-local-by-default@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" + integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== + dependencies: + icss-utils "^4.1.1" + postcss "^7.0.32" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + postcss-modules-local-by-default@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" @@ -17608,6 +20207,14 @@ postcss-modules-local-by-default@^4.0.0: postcss-selector-parser "^6.0.2" postcss-value-parser "^4.1.0" +postcss-modules-scope@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" + integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^6.0.0" + postcss-modules-scope@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" @@ -17615,6 +20222,14 @@ postcss-modules-scope@^3.0.0: dependencies: postcss-selector-parser "^6.0.4" +postcss-modules-values@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" + integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== + dependencies: + icss-utils "^4.0.0" + postcss "^7.0.6" + postcss-modules-values@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" @@ -17741,6 +20356,14 @@ postcss-selector-parser@^3.0.0: indexes-of "^1.0.1" uniq "^1.0.1" +postcss-selector-parser@^6.0.0: + version "6.0.10" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" + integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: version "6.0.4" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" @@ -17770,7 +20393,18 @@ postcss-unique-selectors@^4.0.1: postcss "^7.0.0" uniqs "^2.0.0" -postcss-value-parser@^3.0.0: +postcss-url@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/postcss-url/-/postcss-url-8.0.0.tgz#7b10059bd12929cdbb1971c60f61a0e5af86b4ca" + integrity sha512-E2cbOQ5aii2zNHh8F6fk1cxls7QVFZjLPSrqvmiza8OuXLzIpErij8BDS5Y3STPfJgpIMNCPEr8JlKQWEoozUw== + dependencies: + mime "^2.3.1" + minimatch "^3.0.4" + mkdirp "^0.5.0" + postcss "^7.0.2" + xxhashjs "^0.2.1" + +postcss-value-parser@^3.0.0, postcss-value-parser@^3.2.3: version "3.3.1" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== @@ -17789,6 +20423,24 @@ postcss-values-parser@^2.0.1: indexes-of "^1.0.1" uniq "^1.0.1" +postcss@7.0.21: + version "7.0.21" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.21.tgz#06bb07824c19c2021c5d056d5b10c35b989f7e17" + integrity sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + +postcss@7.0.32: + version "7.0.32" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" + integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + postcss@8.1.7: version "8.1.7" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.1.7.tgz#ff6a82691bd861f3354fd9b17b2332f88171233f" @@ -17808,6 +20460,14 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.27, postcss@^7.0.32: source-map "^0.6.1" supports-color "^6.1.0" +postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.29, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.39" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" + integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== + dependencies: + picocolors "^0.2.1" + source-map "^0.6.1" + postcss@^8.1.7, postcss@^8.2.8: version "8.2.15" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.15.tgz#9e66ccf07292817d226fc315cbbf9bc148fbca65" @@ -17877,7 +20537,7 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prepend-http@^1.0.1: +prepend-http@^1.0.0, prepend-http@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= @@ -17900,17 +20560,7 @@ pretty-error@^4.0.0: lodash "^4.17.20" renderkid "^3.0.0" -pretty-format@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" - integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== - dependencies: - "@jest/types" "^24.9.0" - ansi-regex "^4.0.0" - ansi-styles "^3.2.0" - react-is "^16.8.4" - -pretty-format@^27.0.2: +pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== @@ -18228,7 +20878,7 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== -query-string@^4.2.2: +query-string@^4.1.0, query-string@^4.2.2: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" integrity sha1-u7aTucqRXCMlFbIosaArYJBD2+s= @@ -18270,6 +20920,11 @@ querystring@^0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -18370,6 +21025,14 @@ raw-body@~1.1.0: bytes "1" string_decoder "0.10" +raw-loader@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.1.tgz#14e1f726a359b68437e183d5a5b7d33a3eba6933" + integrity sha512-baolhQBSi3iNh1cglJjA0mYzga+wePk7vdEX//1dTFd+v4TsQlQE0jitJSNF1OIP82rdYulH7otaVmdlDaJ64A== + dependencies: + loader-utils "^2.0.0" + schema-utils "^2.6.5" + rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -18395,7 +21058,7 @@ react-error-boundary@^3.1.0: dependencies: "@babel/runtime" "^7.12.5" -react-is@16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4: +react-is@16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -18452,6 +21115,13 @@ react-refresh@0.8.3: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +"react-router-6@npm:react-router@6.3.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" + integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== + dependencies: + history "^5.2.0" + react@^18.0.0: version "18.0.0" resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" @@ -18459,6 +21129,13 @@ react@^18.0.0: dependencies: loose-envify "^1.1.0" +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha1-5mTvMRYRZsl1HNvo28+GtftY93Q= + dependencies: + pify "^2.3.0" + read-cmd-shim@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16" @@ -18476,7 +21153,7 @@ read-cmd-shim@^1.0.1: normalize-package-data "^2.0.0" npm-normalize-package-bin "^1.0.0" -read-package-tree@^5.1.6: +read-package-tree@5.3.1, read-package-tree@^5.1.6: version "5.3.1" resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.3.1.tgz#a32cb64c7f31eb8a6f31ef06f9cedf74068fe636" integrity sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw== @@ -18517,6 +21194,14 @@ read-pkg-up@^4.0.0: find-up "^3.0.0" read-pkg "^3.0.0" +read-pkg-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-5.0.0.tgz#b6a6741cb144ed3610554f40162aa07a6db621b8" + integrity sha512-XBQjqOBtTzyol2CpsQOw8LHV0XbDZVG7xMMjmXAJomlVY03WOBRmYgDJETlvcg0H63AJvPRwT7GFi5rvOzUOKg== + dependencies: + find-up "^3.0.0" + read-pkg "^5.0.0" + read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" @@ -18553,7 +21238,7 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -read-pkg@^5.2.0: +read-pkg@^5.0.0, read-pkg@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== @@ -18583,7 +21268,7 @@ read@1, read@~1.0.1: string_decoder "~1.1.1" util-deprecate "~1.0.1" -"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: +"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -18628,6 +21313,13 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + realpath-native@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" @@ -18645,6 +21337,16 @@ recast@^0.18.1: private "^0.1.8" source-map "~0.6.1" +recast@^0.20.5: + version "0.20.5" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.20.5.tgz#8e2c6c96827a1b339c634dd232957d230553ceae" + integrity sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ== + dependencies: + ast-types "0.14.2" + esprima "~4.0.0" + source-map "~0.6.1" + tslib "^2.0.1" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -18691,11 +21393,18 @@ redux@^4.0.5: loose-envify "^1.4.0" symbol-observable "^1.2.0" -reflect-metadata@^0.1.12: +reflect-metadata@^0.1.2: version "0.1.13" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== +regenerate-unicode-properties@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56" + integrity sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" @@ -18703,11 +21412,16 @@ regenerate-unicode-properties@^8.2.0: dependencies: regenerate "^1.4.0" -regenerate@^1.2.1, regenerate@^1.4.0: +regenerate@^1.2.1, regenerate@^1.4.0, regenerate@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== +regenerator-runtime@0.13.7, regenerator-runtime@^0.13.4: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== + regenerator-runtime@^0.10.5: version "0.10.5" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" @@ -18718,11 +21432,6 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-runtime@^0.13.4: - version "0.13.7" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" - integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== - regenerator-runtime@^0.9.5: version "0.9.6" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.9.6.tgz#d33eb95d0d2001a4be39659707c51b0cb71ce029" @@ -18744,6 +21453,13 @@ regenerator-transform@^0.14.2: dependencies: "@babel/runtime" "^7.8.4" +regenerator-transform@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537" + integrity sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg== + dependencies: + "@babel/runtime" "^7.8.4" + regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" @@ -18752,6 +21468,20 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" +regex-parser@^2.2.11: + version "2.2.11" + resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58" + integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== + +regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.1: + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + functions-have-names "^1.2.2" + regexp.prototype.flags@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" @@ -18786,6 +21516,18 @@ regexpu-core@^4.7.1: unicode-match-property-ecmascript "^1.0.4" unicode-match-property-value-ecmascript "^1.2.0" +regexpu-core@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.0.1.tgz#c531122a7840de743dcf9c83e923b5560323ced3" + integrity sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.0.1" + regjsgen "^0.6.0" + regjsparser "^0.8.2" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.0.0" + regextras@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/regextras/-/regextras-0.7.1.tgz#be95719d5f43f9ef0b9fa07ad89b7c606995a3b2" @@ -18816,6 +21558,11 @@ regjsgen@^0.5.1: resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== +regjsgen@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.6.0.tgz#83414c5354afd7d6627b16af5f10f41c4e71808d" + integrity sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA== + regjsparser@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" @@ -18830,6 +21577,13 @@ regjsparser@^0.6.4: dependencies: jsesc "~0.5.0" +regjsparser@^0.8.2: + version "0.8.4" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.8.4.tgz#8a14285ffcc5de78c5b95d62bbf413b6bc132d5f" + integrity sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA== + dependencies: + jsesc "~0.5.0" + relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" @@ -18896,22 +21650,6 @@ replace-in-file@^4.0.0: glob "^7.1.6" yargs "^15.0.2" -request-promise-core@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" - integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw== - dependencies: - lodash "^4.17.19" - -request-promise-native@^1.0.5, request-promise-native@^1.0.7, request-promise-native@^1.0.9: - version "1.0.9" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28" - integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g== - dependencies: - request-promise-core "1.1.4" - stealthy-require "^1.1.1" - tough-cookie "^2.3.3" - request@^2.87.0, request@^2.88.0, request@^2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" @@ -19003,6 +21741,13 @@ resolve-cwd@^2.0.0: dependencies: resolve-from "^3.0.0" +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + resolve-dependency-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-dependency-path/-/resolve-dependency-path-2.0.0.tgz#11700e340717b865d216c66cabeb4a2a3c696736" @@ -19034,6 +21779,11 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + resolve-package-path@^1.0.11, resolve-package-path@^1.2.2, resolve-package-path@^1.2.6: version "1.2.7" resolved "https://registry.yarnpkg.com/resolve-package-path/-/resolve-package-path-1.2.7.tgz#2a7bc37ad96865e239330e3102c31322847e652e" @@ -19083,24 +21833,37 @@ resolve-pathname@^3.0.0: resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== +resolve-url-loader@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-3.1.2.tgz#235e2c28e22e3e432ba7a5d4e305c59a58edfc08" + integrity sha512-QEb4A76c8Mi7I3xNKXlRKQSlLBwjUV/ULFMP+G7n3/7tJZ8MG5wsZ3ucxP1Jz8Vevn6fnJsxDx9cIls+utGzPQ== + dependencies: + adjust-sourcemap-loader "3.0.0" + camelcase "5.3.1" + compose-function "3.0.3" + convert-source-map "1.7.0" + es6-iterator "2.0.3" + loader-utils "1.2.3" + postcss "7.0.21" + rework "1.0.1" + rework-visit "1.0.0" + source-map "0.6.1" + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= +resolve.exports@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" + integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== + resolve@1.1.7, resolve@1.1.x: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@1.x, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.5.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== - dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" - resolve@^1.1.6: version "1.21.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.21.0.tgz#b51adc97f3472e6a5cf4444d34bc9d6b9037591f" @@ -19110,6 +21873,23 @@ resolve@^1.1.6: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.1.7, resolve@^1.3.2, resolve@^1.8.1: + version "1.22.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== + dependencies: + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.5.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + resolve@^2.0.0-next.3: version "2.0.0-next.3" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46" @@ -19168,6 +21948,19 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rework-visit@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rework-visit/-/rework-visit-1.0.0.tgz#9945b2803f219e2f7aca00adb8bc9f640f842c9a" + integrity sha1-mUWygD8hni96ygCtuLyfZA+ELJo= + +rework@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rework/-/rework-1.0.1.tgz#30806a841342b54510aa4110850cd48534144aa7" + integrity sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc= + dependencies: + convert-source-map "^0.3.3" + css "^2.0.0" + rfdc@^1.1.4, rfdc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" @@ -19190,7 +21983,7 @@ rimraf@2, rimraf@^2.2.8, rimraf@^2.3.4, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@^2. dependencies: glob "^7.1.3" -rimraf@^3.0.0, rimraf@^3.0.1, rimraf@^3.0.2: +rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.1, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -19234,6 +22027,22 @@ rollup-plugin-license@^2.6.1: spdx-expression-validate "2.0.0" spdx-satisfies "5.0.1" +rollup-plugin-re@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/rollup-plugin-re/-/rollup-plugin-re-1.0.7.tgz#fe174704ed59cda84caf02bd013b582e6fdaa4f6" + integrity sha1-/hdHBO1ZzahMrwK9ATtYLm/apPY= + dependencies: + magic-string "^0.16.0" + rollup-pluginutils "^2.0.1" + +rollup-plugin-sourcemaps@^0.6.0: + version "0.6.3" + resolved "https://registry.yarnpkg.com/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.6.3.tgz#bf93913ffe056e414419607f1d02780d7ece84ed" + integrity sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw== + dependencies: + "@rollup/pluginutils" "^3.0.9" + source-map-resolve "^0.6.0" + rollup-plugin-terser@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" @@ -19256,6 +22065,20 @@ rollup-plugin-typescript2@^0.31.2: resolve "^1.20.0" tslib "^2.3.1" +rollup-pluginutils@^2.0.1: + version "2.8.2" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" + integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== + dependencies: + estree-walker "^0.6.1" + +rollup@2.26.5: + version "2.26.5" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.26.5.tgz#5562ec36fcba3eed65cfd630bd78e037ad0e0307" + integrity sha512-rCyFG3ZtQdnn9YwfuAVH0l/Om34BdO5lwCA0W6Hq+bNB21dVEBbCRxhaHOmu1G7OBFDWytbzAC104u7rxHwGjA== + optionalDependencies: + fsevents "~2.1.2" + rollup@^2.67.1: version "2.67.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.67.1.tgz#4402665706fa00f321d446ce45f880e02cf54f01" @@ -19263,6 +22086,13 @@ rollup@^2.67.1: optionalDependencies: fsevents "~2.3.2" +rollup@^2.8.0: + version "2.70.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.70.2.tgz#808d206a8851628a065097b7ba2053bd83ba0c0d" + integrity sha512-EitogNZnfku65I1DD5Mxe8JYRUCy0hkK5X84IlDtUs+O6JRMpRciXTzyCUuX11b5L5pvjH+OmFXiQ3XjabcXgg== + optionalDependencies: + fsevents "~2.3.2" + rsvp@^3.0.14, rsvp@^3.0.17, rsvp@^3.0.18, rsvp@^3.0.21, rsvp@^3.0.6, rsvp@^3.1.0: version "3.6.2" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" @@ -19302,7 +22132,14 @@ rx@^4.1.0: resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" integrity sha1-pfE/957zt0D+MKqAP7CfmIBdR4I= -rxjs@^6.4.0, rxjs@^6.6.0: +rxjs@6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2" + integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg== + dependencies: + tslib "^1.9.0" + +rxjs@^6.4.0, rxjs@^6.5.0, rxjs@^6.6.0: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== @@ -19331,7 +22168,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@^2.1.2, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -19358,6 +22195,17 @@ saslprep@^1.0.0: dependencies: sparse-bitfield "^3.0.3" +sass-loader@10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.0.1.tgz#10c0364d8034f22fee25ddcc9eded20f99bbe3b4" + integrity sha512-b2PSldKVTS3JcFPHSrEXh3BeAfR7XknGiGCAO5aHruR3Pf3kqLP3Gb2ypXLglRrAzgZkloNxLZ7GXEGDX0hBUQ== + dependencies: + klona "^2.0.3" + loader-utils "^2.0.0" + neo-async "^2.6.2" + schema-utils "^2.7.0" + semver "^7.3.2" + sass-lookup@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/sass-lookup/-/sass-lookup-3.0.0.tgz#3b395fa40569738ce857bc258e04df2617c48cac" @@ -19365,23 +22213,32 @@ sass-lookup@^3.0.0: dependencies: commander "^2.16.0" +sass@1.26.10: + version "1.26.10" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.26.10.tgz#851d126021cdc93decbf201d1eca2a20ee434760" + integrity sha512-bzN0uvmzfsTvjz0qwccN1sPm2HxxpNI/Xa+7PlUEMS+nQvbyuEK7Y0qFqxlPHhiNHb1Ze8WQJtU31olMObkAMw== + dependencies: + chokidar ">=2.0.0 <4.0.0" + +sass@^1.23.0: + version "1.50.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.50.1.tgz#e9b078a1748863013c4712d2466ce8ca4e4ed292" + integrity sha512-noTnY41KnlW2A9P8sdwESpDmo+KBNkukI1i8+hOK3footBUcohNHtdOJbckp46XO95nuvcHDDZ+4tmOnpK3hjw== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + sax@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= -sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: +sax@>=0.6.0, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -saxes@^3.1.9: - version "3.1.11" - resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b" - integrity sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g== - dependencies: - xmlchars "^2.1.1" - saxes@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" @@ -19405,7 +22262,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.6.5: +schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0: version "2.7.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== @@ -19432,7 +22289,26 @@ schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" -"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0: +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= + +selfsigned@^1.10.7: + version "1.10.14" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.14.tgz#ee51d84d9dcecc61e07e4aba34f229ab525c1574" + integrity sha512-lkjaiAye+wBZDCBsu5BGi0XiLRxeUlsGod5ZP924CRSEoGuZAw/f7y9RKu28rwTfiHVhdavhB0qH0INV6P1lEA== + dependencies: + node-forge "^0.10.0" + +semver-intersect@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/semver-intersect/-/semver-intersect-1.4.0.tgz#bdd9c06bedcdd2fedb8cd352c3c43ee8c61321f3" + integrity sha512-d8fvGg5ycKAq0+I6nfWeCx6ffaWJCsBYU0H2Rq56+/zFePYfT8mXkB3tWBSjR5BerkHNZ5eTPIk1/LBYas35xQ== + dependencies: + semver "^5.0.0" + +"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.0.0, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -19442,6 +22318,11 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== +semver@7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + semver@7.3.4: version "7.3.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" @@ -19449,11 +22330,25 @@ semver@7.3.4: dependencies: lru-cache "^6.0.0" +semver@7.x: + version "7.3.6" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.6.tgz#5d73886fb9c0c6602e79440b97165c29581cbb2b" + integrity sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w== + dependencies: + lru-cache "^7.4.0" + semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.0.0, semver@^7.1.1: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + dependencies: + lru-cache "^6.0.0" + semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: version "7.3.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" @@ -19518,6 +22413,19 @@ serialize-javascript@^6.0.0: dependencies: randombytes "^2.1.0" +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + serve-static@1.14.1: version "1.14.1" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" @@ -19648,6 +22556,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + silent-error@^1.0.0, silent-error@^1.0.1, silent-error@^1.1.0, silent-error@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/silent-error/-/silent-error-1.1.1.tgz#f72af5b0d73682a2ba1778b7e32cd8aa7c2d8662" @@ -19875,6 +22788,27 @@ socket.io@^4.2.0: socket.io-adapter "~2.3.3" socket.io-parser "~4.0.4" +sockjs-client@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5" + integrity sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g== + dependencies: + debug "^3.2.5" + eventsource "^1.0.7" + faye-websocket "~0.11.1" + inherits "^2.0.3" + json3 "^3.3.2" + url-parse "^1.4.3" + +sockjs@0.3.20: + version "0.3.20" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.20.tgz#b26a283ec562ef8b2687b44033a4eeceac75d855" + integrity sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA== + dependencies: + faye-websocket "^0.10.0" + uuid "^3.4.0" + websocket-driver "0.6.5" + socks-proxy-agent@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386" @@ -19908,6 +22842,13 @@ socks@~2.3.2: ip "1.1.5" smart-buffer "^4.1.0" +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0= + dependencies: + is-plain-obj "^1.0.0" + sort-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" @@ -19937,12 +22878,28 @@ source-list-map@^2.0.0: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== +"source-map-js@>=0.6.2 <2.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + source-map-js@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf" integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA== -source-map-resolve@^0.5.0: +source-map-loader@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-1.0.2.tgz#b0a6582b2eaa387ede1ecf8061ae0b93c23f9eb0" + integrity sha512-oX8d6ndRjN+tVyjj6PlXSyFPhDdVAPsZA30nD3/II8g4uOv8fCz0DMn5sy8KtVbDfKQxOpGwGJnK3xIW3tauDw== + dependencies: + data-urls "^2.0.0" + iconv-lite "^0.6.2" + loader-utils "^2.0.0" + schema-utils "^2.7.0" + source-map "^0.6.1" + +source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== @@ -19953,6 +22910,22 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" +source-map-resolve@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" + integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + +source-map-support@0.5.19, source-map-support@^0.5.6, source-map-support@~0.5.12: + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-support@^0.4.15, source-map-support@^0.4.18: version "0.4.18" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" @@ -19960,7 +22933,7 @@ source-map-support@^0.4.15, source-map-support@^0.4.18: dependencies: source-map "^0.5.6" -source-map-support@^0.5.17, source-map-support@~0.5.20: +source-map-support@^0.5.5, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -19968,14 +22941,6 @@ source-map-support@^0.5.17, source-map-support@~0.5.20: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-support@^0.5.6, source-map-support@~0.5.12: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - source-map-url@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.3.0.tgz#7ecaf13b57bcd09da8a40c5d269db33799d4aaf9" @@ -20029,7 +22994,7 @@ source-map@~0.2.0: dependencies: amdefine ">=0.0.4" -sourcemap-codec@^1.4.4: +sourcemap-codec@^1.4.4, sourcemap-codec@^1.4.8: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== @@ -20122,6 +23087,36 @@ spdx-satisfies@5.0.1: spdx-expression-parse "^3.0.0" spdx-ranges "^2.0.0" +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +speed-measure-webpack-plugin@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.3.3.tgz#6ff894fc83e8a6310dde3af863a0329cd79da4f5" + integrity sha512-2ljD4Ch/rz2zG3HsLsnPfp23osuPBS0qPuz9sGpkNXTN1Ic4M+W9xB8l8rS8ob2cO4b1L+WTJw/0AJwWYVgcxQ== + dependencies: + chalk "^2.0.1" + split-on-first@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" @@ -20209,6 +23204,13 @@ ssri@^6.0.0, ssri@^6.0.1: dependencies: figgy-pudding "^3.5.1" +ssri@^8.0.0, ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + stable@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" @@ -20255,11 +23257,6 @@ static-extend@^0.1.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -stealthy-require@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" - integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= - stream-browserify@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" @@ -20366,13 +23363,13 @@ string-hash@1.1.3: resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs= -string-length@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" - integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0= +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== dependencies: - astral-regex "^1.0.0" - strip-ansi "^4.0.0" + char-regex "^1.0.2" + strip-ansi "^6.0.0" string-template@~0.2.1: version "0.2.1" @@ -20458,6 +23455,15 @@ string.prototype.trimend@^1.0.4: call-bind "^1.0.2" define-properties "^1.1.3" +string.prototype.trimend@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0" + integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + string.prototype.trimstart@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" @@ -20466,6 +23472,15 @@ string.prototype.trimstart@^1.0.4: call-bind "^1.0.2" define-properties "^1.1.3" +string.prototype.trimstart@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef" + integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + string_decoder@0.10, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -20599,6 +23614,14 @@ stubs@^3.0.0: resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" integrity sha1-6NK6H6nJBXAwPAMLaQD31fiavls= +style-loader@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.2.1.tgz#c5cbbfbf1170d076cfdd86e0109c5bba114baa1a" + integrity sha512-ByHSTQvHLkWE9Ir5+lGbVOXhxX10fbprhLvdg96wedFZb4NDekDPxVKv5Fwmio+QcMlkkNfuK+5W1peQ5CUhZg== + dependencies: + loader-utils "^2.0.0" + schema-utils "^2.6.6" + style-loader@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-2.0.0.tgz#9669602fd4690740eaaec137799a03addbbc393c" @@ -20645,6 +23668,15 @@ stylis@3.5.4: resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe" integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q== +stylus-loader@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/stylus-loader/-/stylus-loader-3.0.2.tgz#27a706420b05a38e038e7cacb153578d450513c6" + integrity sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA== + dependencies: + loader-utils "^1.0.2" + lodash.clonedeep "^4.5.0" + when "~3.6.x" + stylus-lookup@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/stylus-lookup/-/stylus-lookup-3.0.2.tgz#c9eca3ff799691020f30b382260a67355fefdddd" @@ -20653,6 +23685,32 @@ stylus-lookup@^3.0.1: commander "^2.8.1" debug "^4.1.0" +stylus@0.54.8, stylus@^0.54.7: + version "0.54.8" + resolved "https://registry.yarnpkg.com/stylus/-/stylus-0.54.8.tgz#3da3e65966bc567a7b044bfe0eece653e099d147" + integrity sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg== + dependencies: + css-parse "~2.0.0" + debug "~3.1.0" + glob "^7.1.6" + mkdirp "~1.0.4" + safer-buffer "^2.1.2" + sax "~1.2.4" + semver "^6.3.0" + source-map "^0.7.3" + +sucrase@^3.20.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.21.0.tgz#6a5affdbe716b22e4dc99c57d366ad0d216444b9" + integrity sha512-FjAhMJjDcifARI7bZej0Bi1yekjWQHoEvWIXhLPwDhC6O4iZ5PtGb86WV56riW87hzpgB13wwBKO9vKAiWu5VQ== + dependencies: + commander "^4.0.0" + glob "7.1.6" + lines-and-columns "^1.1.6" + mz "^2.7.0" + pirates "^4.0.1" + ts-interface-checker "^0.1.9" + sum-up@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/sum-up/-/sum-up-1.0.3.tgz#1c661f667057f63bcb7875aa1438bc162525156e" @@ -20707,6 +23765,14 @@ supports-color@^8.0.0: dependencies: has-flag "^4.0.0" +supports-hyperlinks@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" + integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -20731,12 +23797,12 @@ svgo@^1.0.0: unquote "~1.1.1" util.promisify "~1.0.0" -symbol-observable@^1.2.0: +symbol-observable@1.2.0, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== -symbol-tree@^3.2.2, symbol-tree@^3.2.4: +symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== @@ -20838,6 +23904,18 @@ tar@^4.4.10, tar@^4.4.8: safe-buffer "^5.2.1" yallist "^3.1.1" +tar@^6.0.2: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + teeny-request@6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-6.0.1.tgz#9b1f512cef152945827ba7e34f62523a4ce2c5b0" @@ -20897,6 +23975,29 @@ temp@~0.4.0: resolved "https://registry.yarnpkg.com/temp/-/temp-0.4.0.tgz#671ad63d57be0fe9d7294664b3fc400636678a60" integrity sha1-ZxrWPVe+D+nXKUZks/xABjZnimA= +terminal-link@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" + integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== + dependencies: + ansi-escapes "^4.2.1" + supports-hyperlinks "^2.0.0" + +terser-webpack-plugin@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-4.1.0.tgz#6e9d6ae4e1a900d88ddce8da6a47507ea61f44bc" + integrity sha512-0ZWDPIP8BtEDZdChbufcXUigOYk6dOX/P/X0hWxqDDcVAQLb8Yy/0FAaemSfax3PAA67+DJR778oz8qVbmy4hA== + dependencies: + cacache "^15.0.5" + find-cache-dir "^3.3.1" + jest-worker "^26.3.0" + p-limit "^3.0.2" + schema-utils "^2.6.6" + serialize-javascript "^4.0.0" + source-map "^0.6.1" + terser "^5.0.0" + webpack-sources "^1.4.3" + terser-webpack-plugin@^1.4.3: version "1.4.5" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b" @@ -20923,6 +24024,15 @@ terser-webpack-plugin@^5.1.3: source-map "^0.6.1" terser "^5.7.2" +terser@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.3.0.tgz#c481f4afecdcc182d5e2bdd2ff2dc61555161e81" + integrity sha512-XTT3D3AwxC54KywJijmY2mxZ8nJiEjBHVYzq8l9OaYuRFWeQNBwvipuzzYEP4e+/AVcd1hqG/CqgsdIRyT45Fg== + dependencies: + commander "^2.20.0" + source-map "~0.6.1" + source-map-support "~0.5.12" + terser@^4.1.2, terser@^4.3.9: version "4.8.0" resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" @@ -20951,6 +24061,15 @@ test-exclude@^5.2.3: read-pkg-up "^4.0.0" require-main-filename "^2.0.0" +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + testem@^3.2.0: version "3.4.0" resolved "https://registry.yarnpkg.com/testem/-/testem-3.4.0.tgz#48ab6b98e96085eeddac1fb46337872b13e9e06c" @@ -21001,10 +24120,24 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.6.0.tgz#d7e4ab13fe54e32e08873be40d51b74229b00fc4" integrity sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ== -throat@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" - integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY= + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + +throat@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" + integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== through2@3.0.0: version "3.0.0" @@ -21047,6 +24180,11 @@ through@~2.2.0, through@~2.2.7: resolved "https://registry.yarnpkg.com/through/-/through-2.2.7.tgz#6e8e21200191d4eb6a99f6f010df46aa1c6eb2bd" integrity sha1-bo4hIAGR1OtqmfbwEN9Gqhxusr0= +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + timed-out@^4.0.0, timed-out@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" @@ -21199,23 +24337,6 @@ totalist@^1.0.0: resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== -tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - -tough-cookie@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" - integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg== - dependencies: - ip-regex "^2.1.0" - psl "^1.1.28" - punycode "^2.1.1" - tough-cookie@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" @@ -21225,6 +24346,14 @@ tough-cookie@^4.0.0: punycode "^2.1.1" universalify "^0.1.2" +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + tr46@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" @@ -21239,12 +24368,19 @@ tr46@^2.0.2: dependencies: punycode "^2.1.1" +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= -tree-kill@^1.2.2: +tree-kill@1.2.2, tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== @@ -21296,31 +24432,42 @@ trim-right@^1.0.1: resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= -ts-jest@^24.3.0: - version "24.3.0" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.3.0.tgz#b97814e3eab359ea840a1ac112deae68aa440869" - integrity sha512-Hb94C/+QRIgjVZlJyiWwouYUF+siNJHJHknyspaOcZ+OQAIdFG/UrdQVXw/0B8Z3No34xkUXZJpOTy9alOWdVQ== +ts-interface-checker@^0.1.9: + version "0.1.13" + resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" + integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== + +ts-jest@^27.1.4: + version "27.1.4" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.1.4.tgz#84d42cf0f4e7157a52e7c64b1492c46330943e00" + integrity sha512-qjkZlVPWVctAezwsOD1OPzbZ+k7zA5z3oxII4dGdZo5ggX/PL7kvwTM0pXTr10fAtbiVpJaL3bWd502zAhpgSQ== dependencies: bs-logger "0.x" - buffer-from "1.x" fast-json-stable-stringify "2.x" + jest-util "^27.0.0" json5 "2.x" lodash.memoize "4.x" make-error "1.x" - mkdirp "0.x" - resolve "1.x" - semver "^5.5" - yargs-parser "10.x" - -ts-node@^8.10.2: - version "8.10.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" - integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== - dependencies: + semver "7.x" + yargs-parser "20.x" + +ts-node@^10.7.0: + version "10.7.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5" + integrity sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== + dependencies: + "@cspotcode/source-map-support" "0.7.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" arg "^4.1.0" + create-require "^1.1.0" diff "^4.0.1" make-error "^1.1.1" - source-map-support "^0.5.17" + v8-compile-cache-lib "^3.0.0" yn "3.1.1" ts-pnp@^1.1.6: @@ -21346,7 +24493,12 @@ tsconfig-paths@^3.9.0: minimist "^1.2.0" strip-bom "^3.0.0" -tslib@^1.10.0, tslib@^1.7.1, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: +tslib@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" + integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== + +tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -21356,33 +24508,17 @@ tslib@^2.0.0, tslib@^2.0.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== +tslib@^2.0.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tslib@^2.3.0, tslib@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== -tslint-config-prettier@^1.18.0: - version "1.18.0" - resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz#75f140bde947d35d8f0d238e0ebf809d64592c37" - integrity sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg== - -tslint-consistent-codestyle@^1.15.1: - version "1.16.0" - resolved "https://registry.yarnpkg.com/tslint-consistent-codestyle/-/tslint-consistent-codestyle-1.16.0.tgz#52348ea899a7e025b37cc6545751c6a566a19077" - integrity sha512-ebR/xHyMEuU36hGNOgCfjGBNYxBPixf0yU1Yoo6s3BrpBRFccjPOmIVaVvQsWAUAMdmfzHOCihVkcaMfimqvHw== - dependencies: - "@fimbul/bifrost" "^0.21.0" - tslib "^1.7.1" - tsutils "^2.29.0" - -tsutils@^2.29.0: - version "2.29.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" - integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== - dependencies: - tslib "^1.8.1" - -tsutils@^3.0.0, tsutils@^3.17.1, tsutils@^3.5.0: +tsutils@^3.0.0, tsutils@^3.17.1: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== @@ -21473,6 +24609,16 @@ type-is@~1.6.17, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +type@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.5.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/type/-/type-2.6.0.tgz#3ca6099af5981d36ca86b78442973694278a219f" + integrity sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ== + typedarray-dts@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/typedarray-dts/-/typedarray-dts-1.0.0.tgz#9dec9811386dbfba964c295c2606cf9a6b982d06" @@ -21523,10 +24669,15 @@ typescript-memoize@^1.0.1: resolved "https://registry.yarnpkg.com/typescript-memoize/-/typescript-memoize-1.0.1.tgz#0a8199aa28f6fe18517f6e9308ef7bfbe9a98d59" integrity sha512-oJNge1qUrOK37d5Y6Ly2txKeuelYVsFtNF6U9kXIN7juudcQaHJQg2MxLOy0CqtkW65rVDYuTCOjnSIVPd8z3w== -typescript@3.7.5: - version "3.7.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae" - integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw== +typescript@3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" + integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== + +typescript@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" + integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== typescript@^3.9.5, typescript@^3.9.7: version "3.9.9" @@ -21538,6 +24689,11 @@ typescript@^4.5.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== +typescript@~4.0.2: + version "4.0.8" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.8.tgz#5739105541db80a971fdbd0d56511d1a6f17d37f" + integrity sha512-oz1765PN+imfz1MlZzSZPtC/tqcwsCyIYA8L47EkRnRW97ztRk83SzMiWLrnChC0vqoYxSU1fcFUDA5gV/ZiPg== + ua-parser-js@^0.7.18, ua-parser-js@^0.7.30: version "0.7.31" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" @@ -21573,6 +24729,16 @@ unbox-primitive@^1.0.0, unbox-primitive@^1.0.1: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + unbzip2-stream@^1.3.3: version "1.4.3" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" @@ -21599,6 +24765,11 @@ unicode-canonical-property-names-ecmascript@^1.0.4: resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + unicode-match-property-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" @@ -21607,16 +24778,34 @@ unicode-match-property-ecmascript@^1.0.4: unicode-canonical-property-names-ecmascript "^1.0.4" unicode-property-aliases-ecmascript "^1.0.4" +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531" integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ== +unicode-match-property-value-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714" + integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== + unicode-property-aliases-ecmascript@^1.0.4: version "1.1.0" resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== +unicode-property-aliases-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" + integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -21658,6 +24847,15 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" +universal-analytics@0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/universal-analytics/-/universal-analytics-0.4.23.tgz#d915e676850c25c4156762471bdd7cf2eaaca8ac" + integrity sha512-lgMIH7XBI6OgYn1woDEmxhGdj8yDefMKg7GkWdeATAlQZFrMrNyxSkpDzY57iY0/6fdlzTbBV03OawvvzG+q7A== + dependencies: + debug "^4.1.1" + request "^2.88.2" + uuid "^3.0.0" + universal-user-agent@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557" @@ -21750,6 +24948,14 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" +url-parse@^1.4.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + url-to-options@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" @@ -21867,7 +25073,12 @@ uuid@3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== -uuid@^3.0.1, uuid@^3.3.2: +uuid@8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" + integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== + +uuid@^3.0.0, uuid@^3.0.1, uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -21877,11 +25088,25 @@ uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.1, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +v8-to-istanbul@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed" + integrity sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + source-map "^0.7.3" + validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -21931,6 +25156,11 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vlq@^0.2.1: + version "0.2.3" + resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" + integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow== + vm-browserify@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019" @@ -21946,22 +25176,13 @@ void-elements@^2.0.0: resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= -w3c-hr-time@^1.0.1, w3c-hr-time@^1.0.2: +w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== dependencies: browser-process-hrtime "^1.0.0" -w3c-xmlserializer@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz#30485ca7d70a6fd052420a3d12fd90e6339ce794" - integrity sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg== - dependencies: - domexception "^1.0.1" - webidl-conversions "^4.0.2" - xml-name-validator "^3.0.0" - w3c-xmlserializer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" @@ -21969,6 +25190,13 @@ w3c-xmlserializer@^2.0.0: dependencies: xml-name-validator "^3.0.0" +w3c-xmlserializer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz#06cdc3eefb7e4d0b20a560a5a3aeb0d2d9a65923" + integrity sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg== + dependencies: + xml-name-validator "^4.0.0" + wait-on@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-3.3.0.tgz#9940981d047a72a9544a97b8b5fca45b2170a082" @@ -22087,6 +25315,13 @@ watchpack@^2.3.1: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + wcwidth@^1.0.0, wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" @@ -22114,6 +25349,11 @@ webidl-conversions@^6.1.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + webpack-bundle-analyzer@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.0.tgz#74013106e7e2b07cbd64f3a5ae847f7e814802c7" @@ -22129,7 +25369,83 @@ webpack-bundle-analyzer@^4.4.0: sirv "^1.0.7" ws "^7.3.1" -webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1: +webpack-dev-middleware@3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3" + integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw== + dependencies: + memory-fs "^0.4.1" + mime "^2.4.4" + mkdirp "^0.5.1" + range-parser "^1.2.1" + webpack-log "^2.0.0" + +webpack-dev-middleware@^3.7.2: + version "3.7.3" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5" + integrity sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ== + dependencies: + memory-fs "^0.4.1" + mime "^2.4.4" + mkdirp "^0.5.1" + range-parser "^1.2.1" + webpack-log "^2.0.0" + +webpack-dev-server@3.11.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz#8f154a3bce1bcfd1cc618ef4e703278855e7ff8c" + integrity sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg== + dependencies: + ansi-html "0.0.7" + bonjour "^3.5.0" + chokidar "^2.1.8" + compression "^1.7.4" + connect-history-api-fallback "^1.6.0" + debug "^4.1.1" + del "^4.1.1" + express "^4.17.1" + html-entities "^1.3.1" + http-proxy-middleware "0.19.1" + import-local "^2.0.0" + internal-ip "^4.3.0" + ip "^1.1.5" + is-absolute-url "^3.0.3" + killable "^1.0.1" + loglevel "^1.6.8" + opn "^5.5.0" + p-retry "^3.0.1" + portfinder "^1.0.26" + schema-utils "^1.0.0" + selfsigned "^1.10.7" + semver "^6.3.0" + serve-index "^1.9.1" + sockjs "0.3.20" + sockjs-client "1.4.0" + spdy "^4.0.2" + strip-ansi "^3.0.1" + supports-color "^6.1.0" + url "^0.11.0" + webpack-dev-middleware "^3.7.2" + webpack-log "^2.0.0" + ws "^6.2.1" + yargs "^13.3.2" + +webpack-log@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" + integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== + dependencies: + ansi-colors "^3.0.0" + uuid "^3.3.2" + +webpack-merge@4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" + integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== + dependencies: + lodash "^4.17.15" + +webpack-sources@1.4.3, webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== @@ -22142,6 +25458,42 @@ webpack-sources@^3.2.2: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.2.tgz#d88e3741833efec57c4c789b6010db9977545260" integrity sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw== +webpack-subresource-integrity@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/webpack-subresource-integrity/-/webpack-subresource-integrity-1.4.1.tgz#e8bf918b444277df46a66cd84542cbcdc5a6272d" + integrity sha512-XMLFInbGbB1HV7K4vHWANzc1CN0t/c4bBvnlvGxGwV45yE/S/feAXIm8dJsCkzqWtSKnmaEgTp/meyeThxG4Iw== + dependencies: + webpack-sources "^1.3.0" + +webpack@4.44.1: + version "4.44.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.1.tgz#17e69fff9f321b8f117d1fda714edfc0b939cc21" + integrity sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/wasm-edit" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + acorn "^6.4.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^4.3.0" + eslint-scope "^4.0.3" + json-parse-better-errors "^1.0.2" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.3" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" + schema-utils "^1.0.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.3" + watchpack "^1.7.4" + webpack-sources "^1.4.1" + webpack@^4.30.0, webpack@^4.44.1: version "4.46.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.46.0.tgz#bf9b4404ea20a073605e0a011d188d77cb6ad542" @@ -22201,6 +25553,13 @@ webpack@^5.52.0, webpack@^5.65.0: watchpack "^2.3.1" webpack-sources "^3.2.2" +websocket-driver@0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" + integrity sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY= + dependencies: + websocket-extensions ">=0.1.1" + websocket-driver@>=0.5.1: version "0.7.4" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" @@ -22215,23 +25574,43 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== -whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5: +whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== dependencies: iconv-lite "0.4.24" +whatwg-encoding@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" + integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== + dependencies: + iconv-lite "0.6.3" + whatwg-fetch@>=0.10.0: version "3.6.2" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== -whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: +whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + +whatwg-url@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-10.0.0.tgz#37264f720b575b4a311bd4094ed8c760caaa05da" + integrity sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" @@ -22240,15 +25619,6 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" -whatwg-url@^6.4.1: - version "6.5.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" - integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - whatwg-url@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" @@ -22267,6 +25637,11 @@ whatwg-url@^8.0.0, whatwg-url@^8.5.0: tr46 "^2.0.2" webidl-conversions "^6.1.0" +when@~3.6.x: + version "3.6.4" + resolved "https://registry.yarnpkg.com/when/-/when-3.6.4.tgz#473b517ec159e2b85005497a13983f095412e34e" + integrity sha1-RztRfsFZ4rhQBUl6E5g/CVQS404= + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -22296,7 +25671,7 @@ which-typed-array@^1.1.2: has-symbols "^1.0.1" is-typed-array "^1.1.3" -which@1, which@1.3.1, which@^1.1.1, which@^1.2.1, which@^1.2.12, which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: +which@1, which@1.3.1, which@^1.1.1, which@^1.2.1, which@^1.2.12, which@^1.2.14, which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -22346,6 +25721,13 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" +worker-plugin@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/worker-plugin/-/worker-plugin-5.0.0.tgz#113b5fe1f4a5d6a957cecd29915bedafd70bb537" + integrity sha512-AXMUstURCxDD6yGam2r4E34aJg6kW85IiaeX72hi+I1cxyaMUtrvVY6sbfpGKAj5e7f68Acl62BjQF5aOOx2IQ== + dependencies: + loader-utils "^1.1.0" + workerpool@^2.3.0: version "2.3.3" resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-2.3.3.tgz#49a70089bd55e890d68cc836a19419451d7c81d7" @@ -22460,14 +25842,14 @@ write-pkg@^3.1.0: sort-keys "^2.0.0" write-json-file "^2.2.0" -ws@^5.2.0: - version "5.2.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.3.tgz#05541053414921bc29c63bee14b8b0dd50b07b3d" - integrity sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA== +ws@^6.2.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" + integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== dependencies: async-limiter "~1.0.0" -ws@^7.0.0, ws@^7.2.3, ws@^7.3.1, ws@^7.4.4, ws@~7.4.2: +ws@^7.2.3, ws@^7.3.1, ws@~7.4.2: version "7.4.4" resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== @@ -22477,6 +25859,11 @@ ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== +ws@^8.2.3: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== + ws@~8.2.3: version "8.2.3" resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" @@ -22492,6 +25879,11 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml-name-validator@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" + integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== + xml2js@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" @@ -22505,7 +25897,7 @@ xmlbuilder@~9.0.1: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= -xmlchars@^2.1.1, xmlchars@^2.2.0: +xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== @@ -22525,6 +25917,13 @@ xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== +xxhashjs@^0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/xxhashjs/-/xxhashjs-0.2.2.tgz#8a6251567621a1c46a5ae204da0249c7f8caa9d8" + integrity sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw== + dependencies: + cuint "^0.2.2" + "y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" @@ -22553,13 +25952,6 @@ yam@^1.0.0: fs-extra "^4.0.2" lodash.merge "^4.6.0" -yargs-parser@10.x: - version "10.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" - integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== - dependencies: - camelcase "^4.1.0" - yargs-parser@13.1.2, yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" @@ -22568,6 +25960,11 @@ yargs-parser@13.1.2, yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@20.x, yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + yargs-parser@^11.1.1: version "11.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" @@ -22584,11 +25981,6 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.2: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - yargs-parser@^20.2.3: version "20.2.7" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" @@ -22603,7 +25995,7 @@ yargs-unparser@1.6.0: lodash "^4.17.15" yargs "^13.3.0" -yargs@13.3.2, yargs@^13.3.0: +yargs@13.3.2, yargs@^13.3.0, yargs@^13.3.2: version "13.3.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== @@ -22654,7 +26046,7 @@ yargs@^15.0.2, yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.1.1: +yargs@^16.1.1, yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==