From 0ee13e774304aa9ef622f753dc092c0b2aa1a417 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 8 Apr 2022 17:12:20 +0200 Subject: [PATCH 01/10] ref(build): Rename `dist` directories to `cjs` (#4900) rename the dist directories in all build dirs to cjs. Hence, also the tarballs' structure changes which is why this PR also introduces a breaking change. Additional change: cleanup `yarn clean` commands by removing no longer existing directories --- packages/angular/tsconfig.cjs.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 packages/angular/tsconfig.cjs.json diff --git a/packages/angular/tsconfig.cjs.json b/packages/angular/tsconfig.cjs.json new file mode 100644 index 000000000000..4ec31d2ff68b --- /dev/null +++ b/packages/angular/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + + "compilerOptions": { + "module": "commonjs", + "outDir": "cjs" + } +} From 882feba3c36b8b93593de37c9d235a89a4f79618 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 8 Apr 2022 14:04:04 -0400 Subject: [PATCH 02/10] meta: 7.0.0-alpha.0 changelog (#4892) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f1b98f430a9..a4bd21c14ede 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,7 @@ - **breaking** feat(types): Delete RequestSessionStatus enum (#4889) - **breaking** feat(types): Delete Status enum (#4891) - **breaking** feat(types): Delete SessionStatus enum (#4890) -- + ## 6.19.7 - fix(react): Add children prop type to ErrorBoundary component (#4966) From 7cfe065a707d269b5d2b0b06c076a07b4f00a96b Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 11 Apr 2022 13:54:26 +0000 Subject: [PATCH 03/10] release: 7.0.0-alpha.0 --- packages/angular/package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/angular/package.json b/packages/angular/package.json index 2b1c8f69fd55..3baab7b7bf9b 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,10 @@ { "name": "@sentry/angular", +<<<<<<< HEAD "version": "7.0.0-alpha.1", +======= + "version": "7.0.0-alpha.0", +>>>>>>> 66f63dec3 (release: 7.0.0-alpha.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", From cbe84e1d1f613e01e6ccaf8e980e095c0ee10bb9 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Mon, 11 Apr 2022 16:47:44 -0700 Subject: [PATCH 04/10] chore(dev): Update `jest` and friends (#4897) This updates `jest`, `ts-jest`, and `jest-environment-node` to the latest versions, in order to facilitate code transformations during `ts-jest`'s on-the-fly compilation that will become necessary once we move to ES6. (More detail on this to come in the PR which actually introduces said transformation, but TL;DR the way we use and extend `global` is fine if it's a `var` (which it is in ES5 Land) but less fine if it's a `const` (which it becomes under ES6), and we need to fix that for tests to run.) It also updates `jsdom`. Together these updates meant that a larger number of packages needed to be downgraded in order for tests to run in node 8 and 10. This therefore also reworks the test script a bit to account for those changes. Finally, this removes the test environment from our main jest config, as its value has become the default in latest version of jest. --- yarn.lock | 94 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2d280ea21e26..96c3a8d5c35d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -395,6 +395,15 @@ jsesc "^2.5.1" source-map "^0.5.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/helper-annotate-as-pure@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz#0f58e86dfc4bb3b1fcd7db806570e177d439b6ab" @@ -462,6 +471,16 @@ browserslist "^4.17.5" semver "^6.3.0" +"@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-create-class-features-plugin@^7.13.0", "@babel/helper-create-class-features-plugin@^7.5.5": version "7.13.11" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.11.tgz#30d30a005bca2c953f5653fc25091a492177f4f6" @@ -730,6 +749,20 @@ "@babel/traverse" "^7.16.0" "@babel/types" "^7.16.0" +"@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-optimise-call-expression@^7.12.13", "@babel/helper-optimise-call-expression@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz#f310a5121a3b9cc52d9ab19122bd729822dee171" @@ -938,6 +971,15 @@ "@babel/traverse" "^7.15.4" "@babel/types" "^7.15.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/highlight@^7.10.4", "@babel/highlight@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" @@ -1420,6 +1462,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.12.13" +"@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-typescript@^7.12.13", "@babel/plugin-syntax-typescript@^7.2.0": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.13.tgz#9dff111ca64154cef0f4dc52cf843d9f12ce4474" @@ -2359,6 +2408,22 @@ debug "^4.1.0" globals "^11.1.0" +"@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/types@7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c" @@ -6274,11 +6339,6 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - async-mutex@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.3.2.tgz#1485eda5bda1b0ec7c8df1ac2e815757ad1831df" @@ -14461,13 +14521,13 @@ import-local@^1.0.0: pkg-dir "^2.0.0" resolve-cwd "^2.0.0" -import-local@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" - integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== +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 "^3.0.0" - resolve-cwd "^2.0.0" + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" import-local@^3.0.2: version "3.1.0" @@ -15696,6 +15756,16 @@ jest-matcher-utils@^27.2.5: jest-get-type "^27.4.0" pretty-format "^27.4.2" +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 "^4.0.0" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + jest-message-util@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.9.0.tgz#527f54a1e380f5e202a8d1149b0ec872f43119e3" @@ -17277,7 +17347,7 @@ make-dir@^1.0.0: dependencies: pify "^3.0.0" -make-dir@^2.0.0, make-dir@^2.1.0: +make-dir@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== From 6d1fb76a6f73148a02501471c51523414c9657a1 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 14 Apr 2022 09:09:52 -0400 Subject: [PATCH 05/10] meta: 7.0.0-alpha.1 changelog (#4939) --- CHANGELOG.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4bd21c14ede..dcd8824f2d79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,25 +20,25 @@ ## 7.0.0-alpha.0 -- **breaking** feat: Drop support for Node 6 (#4851) -- **breaking** feat: Remove references to @sentry/apm (#4845) -- **breaking** feat: Delete deprecated startSpan and child methods (#4849) -- **breaking** feat(bundles): Stop publishing CDN bundles on npm (#4901) -- **breaking** ref(build): Rename dist directories to cjs (#4900) -- **breaking** ref(build): Update to TypeScript 3.8.3 (#4895) -- **breaking** feat(browser): Remove top level eventbuilder exports (#4887) -- **breaking** feat(core): Delete API class (#4848) -- **breaking** feat(core): Remove whitelistUrls/blacklistUrls (#4850) -- **breaking** feat(gatsby): Remove Sentry from window (#4857) -- **breaking** feat(hub): Remove getActiveDomain (#4858) -- **breaking** feat(hub): Remove setTransaction scope method (#4865) -- **breaking** feat(integrations): Remove old angular, ember, and vue integrations (#4893) -- **breaking** feat(node): Remove deprecated frameContextLines (#4884) -- **breaking** feat(tracing): Rename registerRequestInstrumentation -> instrumentOutgoingRequests (#4859) -- **breaking** feat(types): Remove deprecated user dsn field (#4864) -- **breaking** feat(types): Delete RequestSessionStatus enum (#4889) -- **breaking** feat(types): Delete Status enum (#4891) -- **breaking** feat(types): Delete SessionStatus enum (#4890) +- **(breaking)** feat: Drop support for Node 6 (#4851) +- **(breaking)** feat: Remove references to @sentry/apm (#4845) +- **(breaking)** feat: Delete deprecated startSpan and child methods (#4849) +- **(breaking)** feat(bundles): Stop publishing CDN bundles on npm (#4901) +- **(breaking)** ref(build): Rename dist directories to cjs (#4900) +- **(breaking)** ref(build): Update to TypeScript 3.8.3 (#4895) +- **(breaking)** feat(browser): Remove top level eventbuilder exports (#4887) +- **(breaking)** feat(core): Delete API class (#4848) +- **(breaking)** feat(core): Remove whitelistUrls/blacklistUrls (#4850) +- **(breaking)** feat(gatsby): Remove Sentry from window (#4857) +- **(breaking)** feat(hub): Remove getActiveDomain (#4858) +- **(breaking)** feat(hub): Remove setTransaction scope method (#4865) +- **(breaking)** feat(integrations): Remove old angular, ember, and vue integrations (#4893) +- **(breaking)** feat(node): Remove deprecated frameContextLines (#4884) +- **(breaking)** feat(tracing): Rename registerRequestInstrumentation -> instrumentOutgoingRequests (#4859) +- **(breaking)** feat(types): Remove deprecated user dsn field (#4864) +- **(breaking)** feat(types): Delete RequestSessionStatus enum (#4889) +- **(breaking)** feat(types): Delete Status enum (#4891) +- **(breaking)** feat(types): Delete SessionStatus enum (#4890) ## 6.19.7 From d9eaaff629b815d56636d2de414eef5b6cfd5f98 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Tue, 19 Apr 2022 06:09:05 -0700 Subject: [PATCH 06/10] ref(build): Split up rollup config code (#4950) As part of the new build process, a fair amount of new rollup-related code is going to be added. To keep things from getting too unwieldy, this splits the existing code up into modules. It also makes two other small changes, one for consistency and one to differentiate the current rollup code (which is for building bundles) from the future rollup code (which will be for building npm packages): - All plugins are now generated through factory functions (`makeXXXPlugin`). - Both the `makeConfigVariants` function and the individual `rollup.config.js` files have been renamed to make it clear they're for creating bundles. For now all of the resulting modules live in a `rollup` folder at the top level of the repo. In the long run, these would be good candidates to go into an `@sentry-internal/dev-utils` package. --- packages/wasm/rollup.bundle.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wasm/rollup.bundle.config.js b/packages/wasm/rollup.bundle.config.js index e928d466049d..265b557c76a7 100644 --- a/packages/wasm/rollup.bundle.config.js +++ b/packages/wasm/rollup.bundle.config.js @@ -3,7 +3,7 @@ import { makeBaseBundleConfig, makeBundleConfigVariants } from '../../rollup/ind const baseBundleConfig = makeBaseBundleConfig({ input: 'src/index.ts', isAddOn: true, - jsVersion: 'es6', + jsVersion: 'es5', licenseTitle: '@sentry/wasm', outputFileBase: 'bundles/wasm', }); From 29d3739a8d254af358552304463c68160a35d517 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Sun, 24 Apr 2022 23:20:28 -0400 Subject: [PATCH 07/10] feat: Delete old transports Delete old transports as we've switched entirely to new v7 transports. --- packages/browser/src/transports/base.ts | 212 ----- packages/browser/src/transports/fetch.ts | 107 +-- packages/browser/src/transports/index.ts | 8 +- packages/browser/src/transports/new-fetch.ts | 39 - packages/browser/src/transports/new-xhr.ts | 55 -- packages/browser/src/transports/xhr.ts | 112 +-- .../browser/test/unit/transports/base.test.ts | 133 --- .../test/unit/transports/fetch.test.ts | 585 ++---------- .../test/unit/transports/new-fetch.test.ts | 98 -- .../test/unit/transports/new-xhr.test.ts | 109 --- .../browser/test/unit/transports/xhr.test.ts | 506 ++-------- packages/core/src/baseclient.ts | 15 +- packages/core/src/index.ts | 1 - packages/core/src/transports/base.ts | 6 +- packages/core/src/transports/noop.ts | 22 - .../core/test/lib/transports/base.test.ts | 4 +- .../new-transport/scenario.ts | 17 - .../startTransaction/new-transport/test.ts | 12 - packages/node/src/transports/base/index.ts | 270 ------ .../src/transports/{base => }/http-module.ts | 0 packages/node/src/transports/http.ts | 161 +++- packages/node/src/transports/https.ts | 32 - packages/node/src/transports/index.ts | 7 +- packages/node/src/transports/new.ts | 141 --- .../node/test/transports/custom/index.test.ts | 24 - .../node/test/transports/custom/transports.ts | 11 - packages/node/test/transports/http.test.ts | 895 ++++++------------ packages/node/test/transports/https.test.ts | 588 +++++++----- .../node/test/transports/new/http.test.ts | 347 ------- .../node/test/transports/new/https.test.ts | 397 -------- .../transports/{new => }/test-server-certs.ts | 0 packages/types/src/client.ts | 4 +- packages/types/src/index.ts | 2 - packages/types/src/options.ts | 6 +- packages/types/src/transport.ts | 68 +- 35 files changed, 1000 insertions(+), 3994 deletions(-) delete mode 100644 packages/browser/src/transports/base.ts delete mode 100644 packages/browser/src/transports/new-fetch.ts delete mode 100644 packages/browser/src/transports/new-xhr.ts delete mode 100644 packages/browser/test/unit/transports/base.test.ts delete mode 100644 packages/browser/test/unit/transports/new-fetch.test.ts delete mode 100644 packages/browser/test/unit/transports/new-xhr.test.ts delete mode 100644 packages/core/src/transports/noop.ts delete mode 100644 packages/node-integration-tests/suites/public-api/startTransaction/new-transport/scenario.ts delete mode 100644 packages/node-integration-tests/suites/public-api/startTransaction/new-transport/test.ts delete mode 100644 packages/node/src/transports/base/index.ts rename packages/node/src/transports/{base => }/http-module.ts (100%) delete mode 100644 packages/node/src/transports/https.ts delete mode 100644 packages/node/src/transports/new.ts delete mode 100644 packages/node/test/transports/custom/index.test.ts delete mode 100644 packages/node/test/transports/custom/transports.ts delete mode 100644 packages/node/test/transports/new/http.test.ts delete mode 100644 packages/node/test/transports/new/https.test.ts rename packages/node/test/transports/{new => }/test-server-certs.ts (100%) 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..9d60536e565f 100644 --- a/packages/browser/src/transports/fetch.ts +++ b/packages/browser/src/transports/fetch.ts @@ -1,86 +1,39 @@ -import { Event, Response, SentryRequest, Session, TransportOptions } from '@sentry/types'; -import { SentryError, supportsReferrerPolicy, SyncPromise } from '@sentry/utils'; +import { createTransport } from '@sentry/core'; +import { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; -import { BaseTransport } from './base'; 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, - }); - } +export interface FetchTransportOptions extends BaseTransportOptions { + requestOptions?: RequestInit; +} - const options: RequestInit = { - body: sentryRequest.body, +/** + * Creates a Transport that uses the Fetch API to send events to Sentry. + */ +export function makeNewFetchTransport( + options: FetchTransportOptions, + 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', + ...options.requestOptions, }; - 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 => { + 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/index.ts b/packages/browser/src/transports/index.ts index 287e14e0ac50..b9e82ec5cc39 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 { makeNewFetchTransport } from './fetch'; +export { makeNewXHRTransport } 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 9a9d7b14ae19..000000000000 --- a/packages/browser/src/transports/new-fetch.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { createTransport } from '@sentry/core'; -import { BaseTransportOptions, NewTransport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; - -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 d45a0019914c..000000000000 --- a/packages/browser/src/transports/new-xhr.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { createTransport } from '@sentry/core'; -import { BaseTransportOptions, NewTransport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; -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/xhr.ts b/packages/browser/src/transports/xhr.ts index 5da7de258bf6..cd54c72b1588 100644 --- a/packages/browser/src/transports/xhr.ts +++ b/packages/browser/src/transports/xhr.ts @@ -1,63 +1,55 @@ -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 { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; +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): Transport { + 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); } - 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({ bufferSize: options.bufferSize }, makeRequest); } 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 75894049c1ca..000000000000 --- a/packages/browser/test/unit/transports/base.test.ts +++ /dev/null @@ -1,133 +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'; - -// @ts-ignore We're purposely not implementing the methods of the abstract `BaseTransport` class in order to be able to -// assert on what the class provides and what it leaves to the concrete class to implement -class SimpleTransport extends BaseTransport {} - -// TODO(v7): Re-enable these tests with client reports -describe.skip('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', async () => { - expect.assertions(1); - const transport = new SimpleTransport({ dsn: testDsn }); - - try { - await 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..035afae7c501 100644 --- a/packages/browser/test/unit/transports/fetch.test.ts +++ b/packages/browser/test/unit/transports/fetch.test.ts @@ -1,515 +1,98 @@ -import { SentryError } from '@sentry/utils'; +import { EventEnvelope, EventItem } from '@sentry/types'; +import { createEnvelope, serializeEnvelope } from '@sentry/utils'; -import { Event, Response, Transports } from '../../../src'; +import { FetchTransportOptions, makeNewFetchTransport } from '../../../src/transports/fetch'; +import { FetchImpl } from '../../../src/transports/utils'; -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 DEFAULT_FETCH_TRANSPORT_OPTIONS: FetchTransportOptions = { + url: 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7', }; -const transactionPayload: Event = { - event_id: '42', - type: 'transaction', -}; - -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 = 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', + }); }); - 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 = 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'); }); - 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 = makeNewFetchTransport( + { ...DEFAULT_FETCH_TRANSPORT_OPTIONS, requestOptions: 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), + 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 603b0f6037dc..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 null; - 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!({} as Event)]); - - 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!({} as Event), - ]); - - 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!({} as Event)]); - - 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!({} 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/test/unit/transports/xhr.test.ts b/packages/browser/test/unit/transports/xhr.test.ts index 2a0f43d89815..9b07703c7ab3 100644 --- a/packages/browser/test/unit/transports/xhr.test.ts +++ b/packages/browser/test/unit/transports/xhr.test.ts @@ -1,441 +1,109 @@ -import { SentryError } from '@sentry/utils'; -import { fakeServer, SinonFakeServer } from 'sinon'; +import { EventEnvelope, EventItem } from '@sentry/types'; +import { createEnvelope, serializeEnvelope } from '@sentry/utils'; -import { Event, Response, Transports } from '../../../src'; +import { makeNewXHRTransport, XHRTransportOptions } 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: XHRTransportOptions = { + url: 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7', }; -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, {}, '']); + // casting `window` as `any` because XMLHttpRequest is missing in Window (TS-only) + jest.spyOn(window as any, 'XMLHttpRequest').mockImplementation(() => xhrMock as XMLHttpRequest); - const eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toBe('success'); - expect(server.requests.length).toBe(2); - }); + return xhrMock; +} - 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; +describe('NewXHRTransport', () => { + const xhrMock: Partial = createXHRMock(); - server.respondWith('POST', storeUrl, [429, { 'X-Sentry-Rate-Limits': `${retryAfterSeconds}:error: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) - // 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); - }); + afterEach(() => { + jest.clearAllMocks(); + }); - 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; + afterAll(() => { + jest.restoreAllMocks(); + }); - server.respondWith('POST', storeUrl, [200, { 'X-Sentry-Rate-Limits': `${retryAfterSeconds}:error:scope` }, '']); + 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); - 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); + await Promise.all([transport.send(ERROR_ENVELOPE), (xhrMock as XMLHttpRequest).onreadystatechange!({} as Event)]); - let eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toBe('success'); - expect(server.requests.length).toBe(1); + 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)); + }); - 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('returns the correct response', async () => { + const transport = makeNewXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS); - server.respondWith('POST', storeUrl, [200, {}, '']); + const [res] = 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(res).toBeDefined(); + expect(res.status).toEqual('success'); + }); - 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 = makeNewXHRTransport(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: XHRTransportOptions = { + ...DEFAULT_XHR_TRANSPORT_OPTIONS, + headers, + }; + + const transport = makeNewXHRTransport(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/core/src/baseclient.ts b/packages/core/src/baseclient.ts index e2502289742a..833b40aaf9c7 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -9,7 +9,6 @@ import { EventHint, Integration, IntegrationClass, - NewTransport, SessionAggregates, Severity, SeverityLevel, @@ -77,7 +76,7 @@ export abstract class BaseClient implements Client { /** The client Dsn, if specified in options. Without this Dsn, the SDK will be disabled. */ protected readonly _dsn?: DsnComponents; - protected readonly _transport?: NewTransport; + protected readonly _transport?: Transport; /** Array of set up integrations. */ protected _integrations: IntegrationIndex = {}; @@ -92,8 +91,6 @@ export abstract class BaseClient implements Client { * Initializes this client instance. * * @param options Options for the client. - * @param transport The (old) Transport instance for the client to use (TODO(v7): remove) - * @param newTransport The NewTransport instance for the client to use */ protected constructor(options: O) { this._options = options; @@ -213,7 +210,7 @@ export abstract class BaseClient implements Client { /** * @inheritDoc */ - public getTransport(): NewTransport | undefined { + public getTransport(): Transport | undefined { return this._transport; } @@ -567,12 +564,12 @@ export abstract class BaseClient implements Client { // eslint-disable-next-line @typescript-eslint/unbound-method const { beforeSend, sampleRate } = this.getOptions(); - type RecordLostEvent = NonNullable; - type RecordLostEventParams = Parameters; + // type RecordLostEvent = NonNullable; + type RecordLostEventParams = unknown[]; // Parameters; + // no-op as new transports don't have client outcomes + // TODO(v7): Re-add this functionality function recordLostEvent(_outcome: RecordLostEventParams[0], _category: RecordLostEventParams[1]): void { - // no-op as new transports don't have client outcomes - // TODO(v7): Re-add this functionality // if (transport.recordLostEvent) { // transport.recordLostEvent(outcome, category); // } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f5932bdbeaa4..0b0585a3da68 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -27,7 +27,6 @@ export { export { BaseClient } from './baseclient'; export { eventToSentryRequest, sessionToSentryRequest } from './request'; export { initAndBind } from './sdk'; -export { NoopTransport } from './transports/noop'; export { createTransport } from './transports/base'; export { SDK_VERSION } from './version'; export { getIntegrationsToSetup } from './integration'; diff --git a/packages/core/src/transports/base.ts b/packages/core/src/transports/base.ts index 8c6cfe373bfe..97389ea9a45a 100644 --- a/packages/core/src/transports/base.ts +++ b/packages/core/src/transports/base.ts @@ -1,7 +1,7 @@ import { Envelope, InternalBaseTransportOptions, - NewTransport, + Transport, TransportCategory, TransportRequest, TransportRequestExecutor, @@ -24,7 +24,7 @@ import { export const DEFAULT_TRANSPORT_BUFFER_SIZE = 30; /** - * Creates a `NewTransport` + * Creates an instance of a Sentry `Transport` * * @param options * @param makeRequest @@ -33,7 +33,7 @@ export function createTransport( options: InternalBaseTransportOptions, makeRequest: TransportRequestExecutor, buffer: PromiseBuffer = makePromiseBuffer(options.bufferSize || DEFAULT_TRANSPORT_BUFFER_SIZE), -): NewTransport { +): Transport { let rateLimits: RateLimits = {}; const flush = (timeout?: number): PromiseLike => buffer.drain(timeout); 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/test/lib/transports/base.test.ts b/packages/core/test/lib/transports/base.test.ts index 78b40d0a4f7c..8b4dc8965c02 100644 --- a/packages/core/test/lib/transports/base.test.ts +++ b/packages/core/test/lib/transports/base.test.ts @@ -1,4 +1,4 @@ -import { EventEnvelope, EventItem, NewTransport, TransportMakeRequestResponse, TransportResponse } from '@sentry/types'; +import { EventEnvelope, EventItem, Transport, TransportMakeRequestResponse, TransportResponse } from '@sentry/types'; import { createEnvelope, PromiseBuffer, resolvedSyncPromise, serializeEnvelope } from '@sentry/utils'; import { createTransport } from '../../../src/transports/base'; @@ -88,7 +88,7 @@ describe('createTransport', () => { function createTestTransport( initialTransportResponse: TransportMakeRequestResponse, - ): [NewTransport, (res: TransportMakeRequestResponse) => void] { + ): [Transport, (res: TransportMakeRequestResponse) => void] { let transportResponse: TransportMakeRequestResponse = initialTransportResponse; function setTransportResponse(res: TransportMakeRequestResponse) { 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/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 100% rename from packages/node/src/transports/base/http-module.ts rename to packages/node/src/transports/http-module.ts diff --git a/packages/node/src/transports/http.ts b/packages/node/src/transports/http.ts index a9234d830876..74a9a13f094a 100644 --- a/packages/node/src/transports/http.ts +++ b/packages/node/src/transports/http.ts @@ -1,32 +1,141 @@ -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 { eventStatusFromHttpCode } from '@sentry/utils'; 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); - } +// 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): 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({ bufferSize: options.bufferSize }, 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 + }); + + 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/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 958562933321..ba59ba8878a4 100644 --- a/packages/node/src/transports/index.ts +++ b/packages/node/src/transports/index.ts @@ -1,6 +1,3 @@ -export type { NodeTransportOptions } from './new'; +export type { NodeTransportOptions } from './http'; -export { BaseTransport } from './base'; -export { HTTPTransport } from './http'; -export { HTTPSTransport } from './https'; -export { makeNodeTransport } from './new'; +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 e31a58b03b7b..000000000000 --- a/packages/node/src/transports/new.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { createTransport } from '@sentry/core'; -import { - BaseTransportOptions, - NewTransport, - TransportMakeRequestResponse, - TransportRequest, - TransportRequestExecutor, -} from '@sentry/types'; -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/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 8c7abfa2ade2..d8e1f5889226 100644 --- a/packages/node/test/transports/http.test.ts +++ b/packages/node/test/transports/http.test.ts @@ -1,711 +1,346 @@ -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 { HTTPTransport } from '../../src/transports/http'; - -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(), - ignoreDuration: false, -}; -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(), - })), + +import { makeNodeTransport } from '../../src/transports'; + +jest.mock('@sentry/core', () => { + const actualCore = jest.requireActual('@sentry/core'); + return { + ...actualCore, + createTransport: jest.fn().mockImplementation(actualCore.createTransport), }; - return transport; -} +}); + +// 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; -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'); +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); - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions, true); - expect(mockSetEncoding).toHaveBeenCalled(); - }); - - 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', - }); - } 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`)); - } - }); + describe('.send()', () => { + it('should correctly return successful server response', async () => { + await setupTestServer({ statusCode: SUCCESS }); - test('sends a request to tunnel if configured', async () => { - const transport = createTransport({ dsn, tunnel }); + const transport = makeNodeTransport({ url: TEST_SERVER_URL }); + const transportResponse = await transport.send(EVENT_ENVELOPE); - await transport.sendEvent({ - message: 'test', + expect(transportResponse).toEqual(expect.objectContaining({ status: 'success' })); }); - 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 envelope to server', async () => { + await setupTestServer({ statusCode: SUCCESS }, (req, body) => { + expect(req.method).toBe('POST'); + expect(body).toBe(SERIALIZED_EVENT_ENVELOPE); + }); - mock.mockRestore(); - }); + const transport = makeNodeTransport({ url: TEST_SERVER_URL }); + await transport.send(EVENT_ENVELOPE); + }); - 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})`)); - } + 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(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 transport = makeNodeTransport({ + url: TEST_SERVER_URL, + headers: { + 'X-Some-Custom-Header-1': 'value1', + 'X-Some-Custom-Header-2': 'value2', + }, + }); - 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'); - } + await transport.send(EVENT_ENVELOPE); + }); - mockHeaders = {}; - mockReturnCode = 200; + 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 eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toEqual('success'); + const transport = makeNodeTransport({ url: TEST_SERVER_URL }); + await expect(transport.send(EVENT_ENVELOPE)).rejects.toEqual(expect.objectContaining({ status: expectedStatus })); + }); - 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({ url: TEST_SERVER_URL }); + const transportResponse = 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})`)); - } + expect(transportResponse).toEqual(expect.objectContaining({ status: 'success' })); + }); - 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'); - } + 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({ url: TEST_SERVER_URL }); + const transportResponse = await transport.send(EVENT_ENVELOPE); - mock.mockRestore(); + expect(transportResponse).toEqual(expect.objectContaining({ status: 'success' })); + }); }); - 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})`)); - } - - 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'); - } + describe('proxy', () => { + it('can be configured through option', () => { + makeNodeTransport({ + url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', + proxy: 'http://example.com', + }); - 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'); + }); - 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('can be configured through env variables option', () => { + process.env.http_proxy = 'http://example.com'; + makeNodeTransport({ + url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', + }); - mockHeaders = {}; - mockReturnCode = 200; + expect(httpProxyAgent).toHaveBeenCalledTimes(1); + expect(httpProxyAgent).toHaveBeenCalledWith('http://example.com'); + delete process.env.http_proxy; + }); - const eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toEqual('success'); + 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', + }); - const sessionsRes = await transport.sendSession(sessionPayload); - expect(sessionsRes.status).toEqual('success'); + expect(httpProxyAgent).toHaveBeenCalledTimes(1); + expect(httpProxyAgent).toHaveBeenCalledWith('http://bar.com'); + delete process.env.http_proxy; + }); - const transactionRes = await transport.sendEvent(transactionPayload); - expect(transactionRes.status).toEqual('success'); + 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', + }); - mock.mockRestore(); - }); + expect(httpProxyAgent).not.toHaveBeenCalled(); - 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})`)); - } + delete process.env.no_proxy; + }); - 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('no_proxy works with a port', () => { + process.env.http_proxy = 'http://example.com:8080'; + process.env.no_proxy = 'sentry.io:8989'; - 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'); - } + makeNodeTransport({ + url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', + }); - mockHeaders = {}; - mockReturnCode = 200; + expect(httpProxyAgent).not.toHaveBeenCalled(); - const eventRes = await transport.sendEvent(eventPayload); - expect(eventRes.status).toEqual('success'); + delete process.env.no_proxy; + delete process.env.http_proxy; + }); - const transactionRes = await transport.sendEvent(transactionPayload); - expect(transactionRes.status).toEqual('success'); + 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'; - mock.mockRestore(); - }); + makeNodeTransport({ + url: 'http://9e9fd4523d784609a5fc0ebb1080592f@sentry.io:8989/mysubpath/50622', + }); - 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})`)); - } + expect(httpProxyAgent).not.toHaveBeenCalled(); - 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'); - } + delete process.env.no_proxy; + delete process.env.http_proxy; + }); + }); - mockHeaders = {}; - mockReturnCode = 200; + 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', + }, + }); - const transactionRes = await transport.sendEvent(transactionPayload); - expect(transactionRes.status).toEqual('success'); + makeNodeTransport({ url: TEST_SERVER_URL }); + 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), + category: 'error', + }); - mock.mockRestore(); + await expect(executorResult).resolves.toEqual( + expect.objectContaining({ + headers: { + 'retry-after': '2700', + 'x-sentry-rate-limits': '60::organization, 2700::organization', + }, + 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({ url: TEST_SERVER_URL }); + 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), + category: 'error', + }); - mock.mockRestore(); + await expect(executorResult).resolves.toEqual( + expect.objectContaining({ + headers: { + 'retry-after': null, + 'x-sentry-rate-limits': null, + }, + statusCode: SUCCESS, + }), + ); }); - 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({ url: TEST_SERVER_URL }); + 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), + 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({ + headers: { + 'retry-after': '2700', + 'x-sentry-rate-limits': '60::organization, 2700::organization', + }, + statusCode: SUCCESS, + }), + ); + }); - 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({ url: TEST_SERVER_URL }); + 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), + 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({ + headers: { + 'retry-after': '2700', + 'x-sentry-rate-limits': '60::organization, 2700::organization', + }, + statusCode: RATE_LIMIT, + }), + ); }); }); diff --git a/packages/node/test/transports/https.test.ts b/packages/node/test/transports/https.test.ts index 8d7f4dd9aa65..28c37b7c966b 100644 --- a/packages/node/test/transports/https.test.ts +++ b/packages/node/test/transports/https.test.ts @@ -1,332 +1,396 @@ -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 { 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 }); +const TEST_SERVER_URL = 'https://localhost:8099'; - 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 session', async () => { - const transport = createTransport({ dsn }); - await transport.sendSession(sessionsPayload); +const SERIALIZED_EVENT_ENVELOPE = serializeEnvelope(EVENT_ENVELOPE); - const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; - assertBasicOptions(requestOptions, true); - expect(mockSetEncoding).toHaveBeenCalled(); - }); +const unsafeHttpsModule: HTTPModule = { + request: jest + .fn() + .mockImplementation((options: https.RequestOptions, callback?: (res: HTTPModuleRequestIncomingMessage) => void) => { + return https.request({ ...options, rejectUnauthorized: false }, callback); + }), +}; - test('send 400 request mode session', async () => { - mockReturnCode = 400; - const transport = createTransport({ dsn }); +describe('makeNewHttpsTransport()', () => { + 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 }); + describe('.send()', () => { + it('should correctly return successful server response', async () => { + await setupTestServer({ statusCode: SUCCESS }); - 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`)); - } - }); + const transport = makeNodeTransport({ httpModule: unsafeHttpsModule, url: TEST_SERVER_URL }); + const transportResponse = await transport.send(EVENT_ENVELOPE); + + expect(transportResponse).toEqual(expect.objectContaining({ status: 'success' })); + }); - test('sends a request to tunnel if configured', async () => { - const transport = createTransport({ dsn, tunnel }); + 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({ httpModule: unsafeHttpsModule, url: TEST_SERVER_URL }); + 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'); - }); + 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', + }), + ); + }); - 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})`)); - } + const transport = makeNodeTransport({ + httpModule: unsafeHttpsModule, + url: TEST_SERVER_URL, + 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'); - } + await transport.send(EVENT_ENVELOPE); + }); - try { - await transport.sendEvent({ message: 'test' }); - } catch (e) { - expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`)); - } + 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 }); - mock.mockRestore(); - }); + const transport = makeNodeTransport({ httpModule: unsafeHttpsModule, url: TEST_SERVER_URL }); + await expect(transport.send(EVENT_ENVELOPE)).rejects.toEqual(expect.objectContaining({ status: expectedStatus })); + }); - test('transport options', async () => { - mockReturnCode = 200; - const transport = createTransport({ - dsn, - headers: { - a: 'b', - }, + 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' })); }); - 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({ httpModule: unsafeHttpsModule, url: TEST_SERVER_URL }); + const transportResponse = await transport.send(EVENT_ENVELOPE); + + expect(transportResponse).toEqual(expect.objectContaining({ status: 'success' })); }); - 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({ + 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({ + 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({ + 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({ + 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({ + 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({ + 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({ + 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({ + 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: { + '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/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/types/src/client.ts b/packages/types/src/client.ts index b1cadf971197..9b91c6712e06 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -5,7 +5,7 @@ import { ClientOptions } from './options'; import { Scope } from './scope'; import { Session, SessionAggregates } from './session'; import { Severity, SeverityLevel } from './severity'; -import { NewTransport } from './transport'; +import { Transport } from './transport'; /** * User-Facing Sentry SDK Client. @@ -72,7 +72,7 @@ export interface Client { * * @returns The transport. */ - getTransport(): NewTransport | undefined; + getTransport(): Transport | undefined; /** * Flush the event queue and set the client to `enabled = false`. See {@link Client.flush}. diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 3c066e3cc0bf..ab2592cfe02e 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -65,14 +65,12 @@ export type { Thread } from './thread'; export type { Outcome, Transport, - TransportOptions, TransportCategory, TransportRequest, TransportMakeRequestResponse, TransportResponse, InternalBaseTransportOptions, BaseTransportOptions, - NewTransport, TransportRequestExecutor, } from './transport'; export type { User, UserFeedback } from './user'; diff --git a/packages/types/src/options.ts b/packages/types/src/options.ts index 61188d6c099f..b310b8e1799e 100644 --- a/packages/types/src/options.ts +++ b/packages/types/src/options.ts @@ -5,7 +5,7 @@ import { CaptureContext } from './scope'; import { SdkMetadata } from './sdkmetadata'; import { StackLineParser, StackParser } from './stacktrace'; import { SamplingContext } from './transaction'; -import { BaseTransportOptions, NewTransport } from './transport'; +import { BaseTransportOptions, Transport } from './transport'; export interface ClientOptions { /** @@ -61,7 +61,7 @@ export interface ClientOptions NewTransport; + transport: (transportOptions: TO) => Transport; /** * A stack parser implementation @@ -221,7 +221,7 @@ export interface Options /** * Transport object that should be used to send events to Sentry */ - transport?: (transportOptions: TO) => NewTransport; + transport?: (transportOptions: TO) => Transport; /** * A stack parser implementation or an array of stack line parsers diff --git a/packages/types/src/transport.ts b/packages/types/src/transport.ts index 1250d4cb01d5..1c2449a386b5 100644 --- a/packages/types/src/transport.ts +++ b/packages/types/src/transport.ts @@ -1,11 +1,5 @@ -import { DsnLike } from './dsn'; import { Envelope } from './envelope'; -import { Event } from './event'; import { EventStatus } from './eventstatus'; -import { SentryRequestType } from './request'; -import { Response } from './response'; -import { SdkMetadata } from './sdkmetadata'; -import { Session, SessionAggregates } from './session'; export type Outcome = | 'before_send' @@ -48,69 +42,9 @@ export interface BaseTransportOptions extends InternalBaseTransportOptions { url: string; } -export interface NewTransport { +export interface Transport { send(request: Envelope): PromiseLike; flush(timeout?: number): PromiseLike; } export type TransportRequestExecutor = (request: TransportRequest) => PromiseLike; - -/** 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; - - /** - * 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; - - /** - * Increment the counter for the specific client outcome - */ - recordLostEvent?(type: Outcome, category: SentryRequestType): void; -} - -/** JSDoc */ -export type TransportClass = new (options: TransportOptions) => T; - -/** 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; -} From dac51b783123d8db9c24dff232d9c4a311ca0b3c Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 26 Apr 2022 09:13:00 -0400 Subject: [PATCH 08/10] Address PR review --- packages/browser/src/sdk.ts | 4 ++-- packages/browser/src/transports/fetch.ts | 2 +- packages/browser/src/transports/index.ts | 4 ++-- packages/browser/src/transports/xhr.ts | 2 +- packages/browser/test/unit/transports/fetch.test.ts | 8 ++++---- packages/browser/test/unit/transports/xhr.test.ts | 10 +++++----- packages/core/test/mocks/client.ts | 4 ++++ packages/node/src/transports/http.ts | 7 ------- 8 files changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 9b138864515a..184893f15cf0 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -14,7 +14,7 @@ import { IS_DEBUG_BUILD } from './flags'; import { ReportDialogOptions, wrap as internalWrap } from './helpers'; import { Breadcrumbs, Dedupe, GlobalHandlers, LinkedErrors, TryCatch, UserAgent } from './integrations'; import { defaultStackParsers } from './stack-parsers'; -import { makeNewFetchTransport, makeNewXHRTransport } from './transports'; +import { makeFetchTransport, makeXHRTransport } from './transports'; export const defaultIntegrations = [ new CoreIntegrations.InboundFilters(), @@ -106,7 +106,7 @@ export function init(options: BrowserOptions = {}): void { ...options, stackParser: stackParserFromOptions(options.stackParser || defaultStackParsers), integrations: getIntegrationsToSetup(options), - transport: options.transport || (supportsFetch() ? makeNewFetchTransport : makeNewXHRTransport), + transport: options.transport || (supportsFetch() ? makeFetchTransport : makeXHRTransport), }; initAndBind(BrowserClient, clientOptions); diff --git a/packages/browser/src/transports/fetch.ts b/packages/browser/src/transports/fetch.ts index 9d60536e565f..f1aa6709da80 100644 --- a/packages/browser/src/transports/fetch.ts +++ b/packages/browser/src/transports/fetch.ts @@ -10,7 +10,7 @@ export interface FetchTransportOptions extends BaseTransportOptions { /** * Creates a Transport that uses the Fetch API to send events to Sentry. */ -export function makeNewFetchTransport( +export function makeFetchTransport( options: FetchTransportOptions, nativeFetch: FetchImpl = getNativeFetchImplementation(), ): Transport { diff --git a/packages/browser/src/transports/index.ts b/packages/browser/src/transports/index.ts index b9e82ec5cc39..c30287e3e616 100644 --- a/packages/browser/src/transports/index.ts +++ b/packages/browser/src/transports/index.ts @@ -1,2 +1,2 @@ -export { makeNewFetchTransport } from './fetch'; -export { makeNewXHRTransport } from './xhr'; +export { makeFetchTransport } from './fetch'; +export { makeXHRTransport } from './xhr'; diff --git a/packages/browser/src/transports/xhr.ts b/packages/browser/src/transports/xhr.ts index cd54c72b1588..4b36e348de73 100644 --- a/packages/browser/src/transports/xhr.ts +++ b/packages/browser/src/transports/xhr.ts @@ -19,7 +19,7 @@ export interface XHRTransportOptions extends BaseTransportOptions { /** * Creates a Transport that uses the XMLHttpRequest API to send events to Sentry. */ -export function makeNewXHRTransport(options: XHRTransportOptions): Transport { +export function makeXHRTransport(options: XHRTransportOptions): Transport { function makeRequest(request: TransportRequest): PromiseLike { return new SyncPromise((resolve, _reject) => { const xhr = new XMLHttpRequest(); diff --git a/packages/browser/test/unit/transports/fetch.test.ts b/packages/browser/test/unit/transports/fetch.test.ts index 035afae7c501..64ea580c1845 100644 --- a/packages/browser/test/unit/transports/fetch.test.ts +++ b/packages/browser/test/unit/transports/fetch.test.ts @@ -1,7 +1,7 @@ import { EventEnvelope, EventItem } from '@sentry/types'; import { createEnvelope, serializeEnvelope } from '@sentry/utils'; -import { FetchTransportOptions, makeNewFetchTransport } from '../../../src/transports/fetch'; +import { FetchTransportOptions, makeFetchTransport } from '../../../src/transports/fetch'; import { FetchImpl } from '../../../src/transports/utils'; const DEFAULT_FETCH_TRANSPORT_OPTIONS: FetchTransportOptions = { @@ -31,7 +31,7 @@ describe('NewFetchTransport', () => { text: () => Promise.resolve({}), }), ) as unknown as FetchImpl; - const transport = makeNewFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch); + const transport = makeFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch); expect(mockFetch).toHaveBeenCalledTimes(0); const res = await transport.send(ERROR_ENVELOPE); @@ -58,7 +58,7 @@ describe('NewFetchTransport', () => { text: () => Promise.resolve({}), }), ) as unknown as FetchImpl; - const transport = makeNewFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch); + const transport = makeFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch); expect(headers.get).toHaveBeenCalledTimes(0); await transport.send(ERROR_ENVELOPE); @@ -83,7 +83,7 @@ describe('NewFetchTransport', () => { referrer: 'http://example.org', }; - const transport = makeNewFetchTransport( + const transport = makeFetchTransport( { ...DEFAULT_FETCH_TRANSPORT_OPTIONS, requestOptions: REQUEST_OPTIONS }, mockFetch, ); diff --git a/packages/browser/test/unit/transports/xhr.test.ts b/packages/browser/test/unit/transports/xhr.test.ts index 9b07703c7ab3..3ec634360177 100644 --- a/packages/browser/test/unit/transports/xhr.test.ts +++ b/packages/browser/test/unit/transports/xhr.test.ts @@ -1,7 +1,7 @@ import { EventEnvelope, EventItem } from '@sentry/types'; import { createEnvelope, serializeEnvelope } from '@sentry/utils'; -import { makeNewXHRTransport, XHRTransportOptions } from '../../../src/transports/xhr'; +import { makeXHRTransport, XHRTransportOptions } from '../../../src/transports/xhr'; const DEFAULT_XHR_TRANSPORT_OPTIONS: XHRTransportOptions = { url: 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7', @@ -52,7 +52,7 @@ describe('NewXHRTransport', () => { }); it('makes an XHR request to the given URL', async () => { - const transport = makeNewXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS); + const transport = makeXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS); expect(xhrMock.open).toHaveBeenCalledTimes(0); expect(xhrMock.setRequestHeader).toHaveBeenCalledTimes(0); expect(xhrMock.send).toHaveBeenCalledTimes(0); @@ -66,7 +66,7 @@ describe('NewXHRTransport', () => { }); it('returns the correct response', async () => { - const transport = makeNewXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS); + const transport = makeXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS); const [res] = await Promise.all([ transport.send(ERROR_ENVELOPE), @@ -78,7 +78,7 @@ describe('NewXHRTransport', () => { }); it('sets rate limit response headers', async () => { - const transport = makeNewXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS); + const transport = makeXHRTransport(DEFAULT_XHR_TRANSPORT_OPTIONS); await Promise.all([transport.send(ERROR_ENVELOPE), (xhrMock as XMLHttpRequest).onreadystatechange!({} as Event)]); @@ -98,7 +98,7 @@ describe('NewXHRTransport', () => { headers, }; - const transport = makeNewXHRTransport(options); + const transport = makeXHRTransport(options); await Promise.all([transport.send(ERROR_ENVELOPE), (xhrMock as XMLHttpRequest).onreadystatechange!({} as Event)]); expect(xhrMock.setRequestHeader).toHaveBeenCalledTimes(3); diff --git a/packages/core/test/mocks/client.ts b/packages/core/test/mocks/client.ts index 9f02e5b6540d..076b1f7b34c2 100644 --- a/packages/core/test/mocks/client.ts +++ b/packages/core/test/mocks/client.ts @@ -6,6 +6,10 @@ import { BaseClient } from '../../src/baseclient'; import { initAndBind } from '../../src/sdk'; import { createTransport } from '../../src/transports/base'; +// TODO(v7): Add client reports tests to this file +// See old tests in packages/browser/test/unit/transports/base.test.ts +// from https://github.com/getsentry/sentry-javascript/pull/4967 + export function getDefaultTestClientOptions(options: Partial = {}): TestClientOptions { return { integrations: [], diff --git a/packages/node/src/transports/http.ts b/packages/node/src/transports/http.ts index 74a9a13f094a..0352a8232e04 100644 --- a/packages/node/src/transports/http.ts +++ b/packages/node/src/transports/http.ts @@ -13,13 +13,6 @@ import { URL } from 'url'; import { HTTPModule } from './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; From 3bf55271c56c075193b47da1ca16f79c000e6f07 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 26 Apr 2022 12:06:09 -0400 Subject: [PATCH 09/10] fix bad rebase --- CHANGELOG.md | 40 +++++++++++++-------------- packages/angular/package.json | 4 --- packages/angular/tsconfig.cjs.json | 8 ------ packages/wasm/rollup.bundle.config.js | 2 +- 4 files changed, 21 insertions(+), 33 deletions(-) delete mode 100644 packages/angular/tsconfig.cjs.json diff --git a/CHANGELOG.md b/CHANGELOG.md index dcd8824f2d79..6f1b98f430a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,26 +20,26 @@ ## 7.0.0-alpha.0 -- **(breaking)** feat: Drop support for Node 6 (#4851) -- **(breaking)** feat: Remove references to @sentry/apm (#4845) -- **(breaking)** feat: Delete deprecated startSpan and child methods (#4849) -- **(breaking)** feat(bundles): Stop publishing CDN bundles on npm (#4901) -- **(breaking)** ref(build): Rename dist directories to cjs (#4900) -- **(breaking)** ref(build): Update to TypeScript 3.8.3 (#4895) -- **(breaking)** feat(browser): Remove top level eventbuilder exports (#4887) -- **(breaking)** feat(core): Delete API class (#4848) -- **(breaking)** feat(core): Remove whitelistUrls/blacklistUrls (#4850) -- **(breaking)** feat(gatsby): Remove Sentry from window (#4857) -- **(breaking)** feat(hub): Remove getActiveDomain (#4858) -- **(breaking)** feat(hub): Remove setTransaction scope method (#4865) -- **(breaking)** feat(integrations): Remove old angular, ember, and vue integrations (#4893) -- **(breaking)** feat(node): Remove deprecated frameContextLines (#4884) -- **(breaking)** feat(tracing): Rename registerRequestInstrumentation -> instrumentOutgoingRequests (#4859) -- **(breaking)** feat(types): Remove deprecated user dsn field (#4864) -- **(breaking)** feat(types): Delete RequestSessionStatus enum (#4889) -- **(breaking)** feat(types): Delete Status enum (#4891) -- **(breaking)** feat(types): Delete SessionStatus enum (#4890) - +- **breaking** feat: Drop support for Node 6 (#4851) +- **breaking** feat: Remove references to @sentry/apm (#4845) +- **breaking** feat: Delete deprecated startSpan and child methods (#4849) +- **breaking** feat(bundles): Stop publishing CDN bundles on npm (#4901) +- **breaking** ref(build): Rename dist directories to cjs (#4900) +- **breaking** ref(build): Update to TypeScript 3.8.3 (#4895) +- **breaking** feat(browser): Remove top level eventbuilder exports (#4887) +- **breaking** feat(core): Delete API class (#4848) +- **breaking** feat(core): Remove whitelistUrls/blacklistUrls (#4850) +- **breaking** feat(gatsby): Remove Sentry from window (#4857) +- **breaking** feat(hub): Remove getActiveDomain (#4858) +- **breaking** feat(hub): Remove setTransaction scope method (#4865) +- **breaking** feat(integrations): Remove old angular, ember, and vue integrations (#4893) +- **breaking** feat(node): Remove deprecated frameContextLines (#4884) +- **breaking** feat(tracing): Rename registerRequestInstrumentation -> instrumentOutgoingRequests (#4859) +- **breaking** feat(types): Remove deprecated user dsn field (#4864) +- **breaking** feat(types): Delete RequestSessionStatus enum (#4889) +- **breaking** feat(types): Delete Status enum (#4891) +- **breaking** feat(types): Delete SessionStatus enum (#4890) +- ## 6.19.7 - fix(react): Add children prop type to ErrorBoundary component (#4966) diff --git a/packages/angular/package.json b/packages/angular/package.json index 3baab7b7bf9b..2b1c8f69fd55 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,10 +1,6 @@ { "name": "@sentry/angular", -<<<<<<< HEAD "version": "7.0.0-alpha.1", -======= - "version": "7.0.0-alpha.0", ->>>>>>> 66f63dec3 (release: 7.0.0-alpha.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", diff --git a/packages/angular/tsconfig.cjs.json b/packages/angular/tsconfig.cjs.json deleted file mode 100644 index 4ec31d2ff68b..000000000000 --- a/packages/angular/tsconfig.cjs.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - - "compilerOptions": { - "module": "commonjs", - "outDir": "cjs" - } -} diff --git a/packages/wasm/rollup.bundle.config.js b/packages/wasm/rollup.bundle.config.js index 265b557c76a7..e928d466049d 100644 --- a/packages/wasm/rollup.bundle.config.js +++ b/packages/wasm/rollup.bundle.config.js @@ -3,7 +3,7 @@ import { makeBaseBundleConfig, makeBundleConfigVariants } from '../../rollup/ind const baseBundleConfig = makeBaseBundleConfig({ input: 'src/index.ts', isAddOn: true, - jsVersion: 'es5', + jsVersion: 'es6', licenseTitle: '@sentry/wasm', outputFileBase: 'bundles/wasm', }); From fd7a3fb05004d16535a93f88a1ba8745cfbcf49c Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 26 Apr 2022 12:06:45 -0400 Subject: [PATCH 10/10] yarn.lock changes --- yarn.lock | 94 +++++++------------------------------------------------ 1 file changed, 12 insertions(+), 82 deletions(-) diff --git a/yarn.lock b/yarn.lock index 96c3a8d5c35d..2d280ea21e26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -395,15 +395,6 @@ jsesc "^2.5.1" source-map "^0.5.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/helper-annotate-as-pure@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz#0f58e86dfc4bb3b1fcd7db806570e177d439b6ab" @@ -471,16 +462,6 @@ browserslist "^4.17.5" semver "^6.3.0" -"@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-create-class-features-plugin@^7.13.0", "@babel/helper-create-class-features-plugin@^7.5.5": version "7.13.11" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.11.tgz#30d30a005bca2c953f5653fc25091a492177f4f6" @@ -749,20 +730,6 @@ "@babel/traverse" "^7.16.0" "@babel/types" "^7.16.0" -"@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-optimise-call-expression@^7.12.13", "@babel/helper-optimise-call-expression@^7.15.4": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz#f310a5121a3b9cc52d9ab19122bd729822dee171" @@ -971,15 +938,6 @@ "@babel/traverse" "^7.15.4" "@babel/types" "^7.15.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/highlight@^7.10.4", "@babel/highlight@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" @@ -1462,13 +1420,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.12.13" -"@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-typescript@^7.12.13", "@babel/plugin-syntax-typescript@^7.2.0": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.13.tgz#9dff111ca64154cef0f4dc52cf843d9f12ce4474" @@ -2408,22 +2359,6 @@ debug "^4.1.0" globals "^11.1.0" -"@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/types@7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c" @@ -6339,6 +6274,11 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + async-mutex@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.3.2.tgz#1485eda5bda1b0ec7c8df1ac2e815757ad1831df" @@ -14521,13 +14461,13 @@ import-local@^1.0.0: pkg-dir "^2.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== +import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== dependencies: - pkg-dir "^4.2.0" - resolve-cwd "^3.0.0" + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" import-local@^3.0.2: version "3.1.0" @@ -15756,16 +15696,6 @@ jest-matcher-utils@^27.2.5: jest-get-type "^27.4.0" pretty-format "^27.4.2" -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 "^4.0.0" - jest-diff "^27.5.1" - jest-get-type "^27.5.1" - pretty-format "^27.5.1" - jest-message-util@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.9.0.tgz#527f54a1e380f5e202a8d1149b0ec872f43119e3" @@ -17347,7 +17277,7 @@ make-dir@^1.0.0: dependencies: pify "^3.0.0" -make-dir@^2.0.0: +make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==