From 5d0f6f4a2a8f56a3941496ed53982de4349b94cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 18:22:16 +0000 Subject: [PATCH 01/14] feat(deps): bump @opentelemetry/instrumentation-connect from 0.36.0 to 0.36.1 (#12240) --- packages/node/package.json | 2 +- yarn.lock | 81 +++++++++++++++++++++++++++++--------- 2 files changed, 63 insertions(+), 20 deletions(-) diff --git a/packages/node/package.json b/packages/node/package.json index 2dc9e35894b9..4c77f755d5df 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -74,7 +74,7 @@ "@opentelemetry/context-async-hooks": "^1.23.0", "@opentelemetry/core": "^1.24.1", "@opentelemetry/instrumentation": "^0.51.1", - "@opentelemetry/instrumentation-connect": "0.36.0", + "@opentelemetry/instrumentation-connect": "0.36.1", "@opentelemetry/instrumentation-express": "0.39.0", "@opentelemetry/instrumentation-fastify": "0.36.1", "@opentelemetry/instrumentation-graphql": "0.40.0", diff --git a/yarn.lock b/yarn.lock index 52fbb7373cb6..8819fb29597c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6305,14 +6305,14 @@ "@opentelemetry/propagation-utils" "^0.30.9" "@opentelemetry/semantic-conventions" "^1.22.0" -"@opentelemetry/instrumentation-connect@0.36.0": - version "0.36.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.36.0.tgz#6a83722f0cb22a7f9b3bd8185f940308bbed0e50" - integrity sha512-k9++bmJZ9zDEs3u3DnKTn2l7QTiNFg3gPx7G9rW0TPnP+xZoBSBTrEcGYBaqflQlrFG23Q58+X1sM2ayWPv5Fg== +"@opentelemetry/instrumentation-connect@0.36.1": + version "0.36.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.36.1.tgz#4ef93ade4c7224213d9e4411190c22d753166a18" + integrity sha512-xI5Q/CMmzBmHshPnzzjD19ptFaYO/rQWzokpNio4QixZYWhJsa35QgRvN9FhPkwgtuJIbt/CWWAufJ3egJNHEA== dependencies: "@opentelemetry/core" "^1.8.0" "@opentelemetry/instrumentation" "^0.51.0" - "@opentelemetry/semantic-conventions" "^1.0.0" + "@opentelemetry/semantic-conventions" "^1.22.0" "@types/connect" "3.4.36" "@opentelemetry/instrumentation-express@0.39.0": @@ -8061,14 +8061,7 @@ "@types/express-serve-static-core" "*" "@types/node" "*" -"@types/connect@*": - version "3.4.34" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.34.tgz#170a40223a6d666006d93ca128af2beb1d9b1901" - integrity sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ== - dependencies: - "@types/node" "*" - -"@types/connect@3.4.36": +"@types/connect@*", "@types/connect@3.4.36": version "3.4.36" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.36.tgz#e511558c15a39cb29bd5357eebb57bd1459cd1ab" integrity sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w== @@ -8458,7 +8451,17 @@ dependencies: "@types/unist" "*" -"@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8", "@types/history@*": +"@types/history-4@npm:@types/history@4.7.8": + version "4.7.8" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" + integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== + +"@types/history-5@npm:@types/history@4.7.8": + version "4.7.8" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" + integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== + +"@types/history@*": version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== @@ -8824,7 +8827,15 @@ "@types/history" "^3" "@types/react" "*" -"@types/react-router-4@npm:@types/react-router@5.1.14", "@types/react-router-5@npm:@types/react-router@5.1.14": +"@types/react-router-4@npm:@types/react-router@5.1.14": + version "5.1.14" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" + integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== + dependencies: + "@types/history" "*" + "@types/react" "*" + +"@types/react-router-5@npm:@types/react-router@5.1.14": version "5.1.14" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== @@ -26123,7 +26134,7 @@ react-is@^18.0.0: dependencies: "@remix-run/router" "1.0.2" -"react-router-6@npm:react-router@6.3.0", react-router@6.3.0: +"react-router-6@npm:react-router@6.3.0": version "6.3.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== @@ -26138,6 +26149,13 @@ react-router-dom@^6.2.2: history "^5.2.0" react-router "6.3.0" +react-router@6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" + integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== + dependencies: + history "^5.2.0" + react@^18.0.0: version "18.0.0" resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" @@ -28456,7 +28474,7 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -28482,6 +28500,15 @@ string-width@^2.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -28577,7 +28604,14 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -31204,7 +31238,7 @@ workerpool@^6.4.0: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.4.0.tgz#f8d5cfb45fde32fa3b7af72ad617c3369567a462" integrity sha512-i3KR1mQMNwY2wx20ozq2EjISGtQWDIfV56We+yGJ5yDs8jTwQiLLaqHlkBHITlCuJnYlVRmXegxFxZg7gqI++A== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -31222,6 +31256,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 4644dd77a41f1527b9c46c06679ef162116ab785 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 May 2024 14:42:11 -0400 Subject: [PATCH 02/14] chore: Remove express integration comment from e2e tests (#12241) We are safe to change this now. --- .../multiple-routers/common-infix-parameterized/server.ts | 3 --- .../suites/express/multiple-routers/common-infix/server.ts | 3 --- .../common-prefix-parameterized-reverse/server.ts | 3 --- .../multiple-routers/common-prefix-parameterized/server.ts | 3 --- .../common-prefix-same-length-parameterized copy/server.ts | 3 --- .../common-prefix-same-length-parameterized/server.ts | 3 --- .../suites/express/multiple-routers/common-prefix/server.ts | 3 --- .../suites/express/multiple-routers/complex-router/server.ts | 3 --- .../multiple-routers/middle-layer-parameterized/server.ts | 3 --- .../suites/express/sentry-trace/baggage-header-out/server.ts | 3 --- .../baggage-other-vendors-with-sentry-entries/server.ts | 3 --- .../express/sentry-trace/baggage-other-vendors/server.ts | 3 --- .../express/sentry-trace/baggage-transaction-name/server.ts | 3 --- .../suites/express/sentry-trace/server.ts | 3 --- .../suites/express/sentry-trace/trace-header-assign/server.ts | 3 --- 15 files changed, 45 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix-parameterized/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix-parameterized/server.ts index c41ce7e3ae1a..673c146e9d8c 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix-parameterized/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix-parameterized/server.ts @@ -4,9 +4,6 @@ import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [ - // TODO: This used to have the Express integration - ], tracesSampleRate: 1.0, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix/server.ts index 0e97e7fe4718..eff1564d3f0a 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix/server.ts @@ -4,9 +4,6 @@ import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [ - // TODO: This used to have the Express integration - ], tracesSampleRate: 1.0, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized-reverse/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized-reverse/server.ts index 31f41de294d5..755a32bf4389 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized-reverse/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized-reverse/server.ts @@ -4,9 +4,6 @@ import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [ - // TODO: This used to have the Express integration - ], tracesSampleRate: 1.0, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized/server.ts index f24e8754cb89..7db74e8e3dea 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized/server.ts @@ -4,9 +4,6 @@ import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [ - // TODO: This used to use the Express integration - ], tracesSampleRate: 1.0, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized copy/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized copy/server.ts index a006358edc25..654afa3b8c8d 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized copy/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized copy/server.ts @@ -4,9 +4,6 @@ import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [ - // TODO: This used to have the Express integration - ], tracesSampleRate: 1.0, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized/server.ts index a85ef02682d6..017c810ed842 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized/server.ts @@ -4,9 +4,6 @@ import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [ - // TODO: This used to have the Express integration - ], tracesSampleRate: 1.0, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix/server.ts index 4c03905d5d2a..497cbf2efffb 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix/server.ts @@ -4,9 +4,6 @@ import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [ - // TODO: This used to have the Express integration - ], tracesSampleRate: 1.0, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/complex-router/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/complex-router/server.ts index bdc8c03d176e..b7ffeeba937a 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/complex-router/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/complex-router/server.ts @@ -4,9 +4,6 @@ import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [ - // TODO: This used to use the Express integration - ], tracesSampleRate: 1.0, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/server.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/server.ts index 0d005f1c55d7..12a00ce4e1db 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/server.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/server.ts @@ -4,9 +4,6 @@ import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', - integrations: [ - // TODO: This used to use the Express integration - ], tracesSampleRate: 1.0, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts index a4a9bf108a95..07c21c8d21ea 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts @@ -8,9 +8,6 @@ Sentry.init({ release: '1.0', environment: 'prod', tracePropagationTargets: [/^(?!.*express).*$/], - integrations: [ - // TODO: This used to use the Express integration - ], tracesSampleRate: 1.0, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/server.ts index 92abb2444294..260fb34af5c2 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/server.ts @@ -9,9 +9,6 @@ Sentry.init({ environment: 'prod', // disable requests to /express tracePropagationTargets: [/^(?!.*express).*$/], - integrations: [ - // TODO: This used to use the Express integration - ], tracesSampleRate: 1.0, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/server.ts index effc44c2b248..1c00fbd72bde 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/server.ts @@ -9,9 +9,6 @@ Sentry.init({ environment: 'prod', // disable requests to /express tracePropagationTargets: [/^(?!.*express).*$/], - integrations: [ - // TODO: This used to use the Express integration - ], tracesSampleRate: 1.0, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts index ed8f7487a9c3..80bb7b38a39a 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts @@ -9,9 +9,6 @@ Sentry.init({ environment: 'prod', // disable requests to /express tracePropagationTargets: [/^(?!.*express).*$/], - integrations: [ - // TODO: This used to use the Express integration - ], tracesSampleRate: 1.0, // TODO: We're rethinking the mechanism for including Pii data in DSC, hence commenting out sendDefaultPii for now // sendDefaultPii: true, diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/server.ts index b9218b905e9e..6ebc2d4cac95 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/server.ts @@ -8,9 +8,6 @@ Sentry.init({ release: '1.0', environment: 'prod', tracePropagationTargets: [/^(?!.*express).*$/], - integrations: [ - // TODO: This used to use the Express integration - ], tracesSampleRate: 1.0, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/server.ts index f1393c3cfc5b..1cc4a0dcc639 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/server.ts @@ -7,9 +7,6 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', environment: 'prod', - integrations: [ - // TODO: This used to use the Express integration - ], tracesSampleRate: 1.0, transport: loggingTransport, }); From 7185a5388337da3df6c9ca8a7bbddb4491e8b2ec Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 May 2024 14:43:42 -0400 Subject: [PATCH 03/14] feat(react): Add `Sentry.reactErrorHandler` (#12147) This PR introduces `Sentry.reactErrorHandler`, which looks like so: ```js import * as Sentry from '@sentry/react'; import { hydrateRoot } from "react-dom/client"; ReactDOM.hydrateRoot( document.getElementById("root"), , { onUncaughtError: Sentry.reactErrorHandler(), onCaughtError: Sentry.reactErrorHandler((error, errorInfo) => { // optional callback if users want custom config. }), } ); ``` To validate this change, we add a react 19 e2e test. --- .github/workflows/build.yml | 1 + .../test-applications/react-19/.gitignore | 29 +++++ .../test-applications/react-19/.npmrc | 2 + .../test-applications/react-19/package.json | 57 ++++++++++ .../react-19/playwright.config.ts | 82 ++++++++++++++ .../react-19/public/index.html | 24 ++++ .../react-19/src/globals.d.ts | 5 + .../test-applications/react-19/src/index.tsx | 29 +++++ .../react-19/src/pages/Index.jsx | 51 +++++++++ .../react-19/src/react-app-env.d.ts | 1 + .../react-19/start-event-proxy.mjs | 6 + .../react-19/tests/errors.test.ts | 42 +++++++ .../test-applications/react-19/tsconfig.json | 20 ++++ packages/react/README.md | 35 +++++- packages/react/package.json | 4 +- packages/react/src/error.ts | 106 ++++++++++++++++++ packages/react/src/errorboundary.tsx | 73 +++--------- packages/react/src/index.ts | 1 + packages/react/test/error.test.ts | 14 +++ packages/react/test/errorboundary.test.tsx | 17 +-- yarn.lock | 90 ++++----------- 21 files changed, 540 insertions(+), 149 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/react-19/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/react-19/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/react-19/package.json create mode 100644 dev-packages/e2e-tests/test-applications/react-19/playwright.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-19/public/index.html create mode 100644 dev-packages/e2e-tests/test-applications/react-19/src/globals.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-19/src/index.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx create mode 100644 dev-packages/e2e-tests/test-applications/react-19/src/react-app-env.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-19/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-19/tsconfig.json create mode 100644 packages/react/src/error.ts create mode 100644 packages/react/test/error.test.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd827c293a61..195e316581c3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1009,6 +1009,7 @@ jobs: 'nextjs-app-dir', 'nextjs-14', 'nextjs-15', + 'react-19', 'react-create-hash-router', 'react-router-6-use-routes', 'react-router-5', diff --git a/dev-packages/e2e-tests/test-applications/react-19/.gitignore b/dev-packages/e2e-tests/test-applications/react-19/.gitignore new file mode 100644 index 000000000000..84634c973eeb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +/test-results/ +/playwright-report/ +/playwright/.cache/ + +!*.d.ts diff --git a/dev-packages/e2e-tests/test-applications/react-19/.npmrc b/dev-packages/e2e-tests/test-applications/react-19/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/react-19/package.json b/dev-packages/e2e-tests/test-applications/react-19/package.json new file mode 100644 index 000000000000..4c2f7d0df36e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/package.json @@ -0,0 +1,57 @@ +{ + "name": "react-19-test-app", + "version": "0.1.0", + "private": true, + "dependencies": { + "@sentry/react": "latest || *", + "@testing-library/jest-dom": "5.14.1", + "@testing-library/react": "13.0.0", + "@testing-library/user-event": "13.2.1", + "history": "4.9.0", + "@types/history": "4.7.11", + "@types/jest": "27.0.1", + "@types/node": "16.7.13", + "@types/react": "npm:types-react@rc", + "@types/react-dom": "npm:types-react-dom@rc", + "react": "19.0.0-rc-935180c7e0-20240524", + "react-dom": "19.0.0-rc-935180c7e0-20240524", + "react-scripts": "5.0.1", + "typescript": "4.9.5", + "web-vitals": "2.1.0" + }, + "scripts": { + "build": "react-scripts build", + "dev": "react-scripts start", + "start": "serve -s build", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:assert": "pnpm test" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@playwright/test": "^1.43.1", + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", + "serve": "14.0.1" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/react-19/playwright.config.ts b/dev-packages/e2e-tests/test-applications/react-19/playwright.config.ts new file mode 100644 index 000000000000..3d7268ce5dc1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/playwright.config.ts @@ -0,0 +1,82 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +const reactPort = 3030; +const eventProxyPort = 3031; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 150_000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: 0, + /* Opt out of parallel tests on CI. */ + workers: 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + baseURL: `http://localhost:${reactPort}`, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + // For now we only test Chrome! + // { + // name: 'firefox', + // use: { + // ...devices['Desktop Firefox'], + // }, + // }, + // { + // name: 'webkit', + // use: { + // ...devices['Desktop Safari'], + // }, + // }, + ], + + /* Run your local dev server before starting the tests */ + + webServer: [ + { + command: 'node start-event-proxy.mjs', + port: eventProxyPort, + }, + { + command: 'pnpm start', + port: reactPort, + env: { + PORT: `${reactPort}`, + }, + }, + ], +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/react-19/public/index.html b/dev-packages/e2e-tests/test-applications/react-19/public/index.html new file mode 100644 index 000000000000..39da76522bea --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/public/index.html @@ -0,0 +1,24 @@ + + + + + + + + React App + + + +
+ + + diff --git a/dev-packages/e2e-tests/test-applications/react-19/src/globals.d.ts b/dev-packages/e2e-tests/test-applications/react-19/src/globals.d.ts new file mode 100644 index 000000000000..ffa61ca49acc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/src/globals.d.ts @@ -0,0 +1,5 @@ +interface Window { + recordedTransactions?: string[]; + capturedExceptionId?: string; + sentryReplayId?: string; +} diff --git a/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx new file mode 100644 index 000000000000..6f6bb0640e73 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx @@ -0,0 +1,29 @@ +import * as Sentry from '@sentry/react'; +// biome-ignore lint/nursery/noUnusedImports: +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import Index from './pages/Index'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: + process.env.REACT_APP_E2E_TEST_DSN || + 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576', + release: 'e2e-test', + tunnel: 'http://localhost:3031/', // proxy server +}); + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement, { + onUncaughtError: Sentry.reactErrorHandler((error, errorInfo) => { + console.warn(error, errorInfo); + }), + onCaughtError: Sentry.reactErrorHandler((error, errorInfo) => { + console.warn(error, errorInfo); + }), +}); + +root.render( +
+ +
, +); diff --git a/dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx b/dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx new file mode 100644 index 000000000000..14fc3e9f97d1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx @@ -0,0 +1,51 @@ +import * as React from 'react'; + +const Index = () => { + const [caughtError, setCaughtError] = React.useState(false); + const [uncaughtError, setUncaughtError] = React.useState(false); + + return ( + <> +
+ +

React 19

+ {caughtError && } + +
+
+
+ {uncaughtError && } + +
+ + ); +}; + +function Throw({ error }) { + throw new Error(`${error} error`); +} + +class SampleErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { error: null }; + } + + componentDidCatch(error, errorInfo) { + this.setState({ error }); + // no-op + } + + render() { + if (this.state.error) { + return
Caught an error: {JSON.stringify(this.state.error)}
; + } + return this.props.children; + } +} + +export default Index; diff --git a/dev-packages/e2e-tests/test-applications/react-19/src/react-app-env.d.ts b/dev-packages/e2e-tests/test-applications/react-19/src/react-app-env.d.ts new file mode 100644 index 000000000000..6431bc5fc6b2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/dev-packages/e2e-tests/test-applications/react-19/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-19/start-event-proxy.mjs new file mode 100644 index 000000000000..e0102436fdd0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'react-19', +}); diff --git a/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts new file mode 100644 index 000000000000..9040d217d1bb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts @@ -0,0 +1,42 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/event-proxy-server'; + +test('Catches errors caught by error boundary', async ({ page }) => { + page.on('console', message => { + expect(message.text()).toContain('caught error'); + }); + + const errorEventPromise = waitForError('react-19', event => { + return !event.type && event.exception?.values?.[0]?.value === 'caught error'; + }); + + await page.goto('/'); + + const exceptionButton = page.locator('id=caughtError-button'); + await exceptionButton.click(); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(2); + expect(errorEvent.exception?.values?.[0]?.value).toBe('caught error'); +}); + +test('Catches errors uncaught by error boundary', async ({ page }) => { + page.on('console', message => { + expect(message.text()).toContain('uncaught error'); + }); + + const errorEventPromise = waitForError('react-19', event => { + return !event.type && event.exception?.values?.[0]?.value === 'uncaught error'; + }); + + await page.goto('/'); + + const exceptionButton = page.locator('id=uncaughtError-button'); + await exceptionButton.click(); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(2); + expect(errorEvent.exception?.values?.[0]?.value).toBe('uncaught error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-19/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-19/tsconfig.json new file mode 100644 index 000000000000..4cc95dc2689a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2018", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": ["src", "tests"] +} diff --git a/packages/react/README.md b/packages/react/README.md index 49c09247c9ea..5645b03d9fb0 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -20,7 +20,7 @@ To use this SDK, call `Sentry.init(options)` before you mount your React compone ```javascript import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; import * as Sentry from '@sentry/react'; Sentry.init({ @@ -30,12 +30,39 @@ Sentry.init({ // ... -ReactDOM.render(, rootNode); +const container = document.getElementById(“app”); +const root = createRoot(container); +root.render(); -// Can also use with React Concurrent Mode -// ReactDOM.createRoot(rootNode).render(); +// also works with hydrateRoot +// const domNode = document.getElementById('root'); +// const root = hydrateRoot(domNode, reactNode); +// root.render(); ``` +### React 19 + +Starting with React 19, the `createRoot` and `hydrateRoot` methods expose error hooks that can be used to capture errors +automatically. Use the `Sentry.reactErrorHandler` function to capture errors in the error hooks you are interested in. + +```js +const container = document.getElementById(“app”); +const root = createRoot(container, { + // Callback called when an error is thrown and not caught by an Error Boundary. + onUncaughtError: Sentry.reactErrorHandler((error, errorInfo) => { + console.warn('Uncaught error', error, errorInfo.componentStack); + }), + // Callback called when React catches an error in an Error Boundary. + onCaughtError: Sentry.reactErrorHandler(), + // Callback called when React automatically recovers from errors. + onRecoverableError: Sentry.reactErrorHandler(), +}); +root.render(); +``` + +If you want more finely grained control over error handling, we recommend only adding the `onUncaughtError` and +`onRecoverableError` hooks and using an `ErrorBoundary` component instead of the `onCaughtError` hook. + ### ErrorBoundary `@sentry/react` exports an ErrorBoundary component that will automatically send Javascript errors from inside a diff --git a/packages/react/package.json b/packages/react/package.json index 9ab1ac01ec01..fe934846d07b 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -56,9 +56,9 @@ "@testing-library/react-hooks": "^7.0.2", "@types/history-4": "npm:@types/history@4.7.8", "@types/history-5": "npm:@types/history@4.7.8", - "@types/hoist-non-react-statics": "^3.3.1", + "@types/hoist-non-react-statics": "^3.3.5", "@types/node-fetch": "^2.6.0", - "@types/react": "^17.0.3", + "@types/react": "17.0.3", "@types/react-router-3": "npm:@types/react-router@3.0.24", "@types/react-router-4": "npm:@types/react-router@5.1.14", "@types/react-router-5": "npm:@types/react-router@5.1.14", diff --git a/packages/react/src/error.ts b/packages/react/src/error.ts new file mode 100644 index 000000000000..ce83e39e33ee --- /dev/null +++ b/packages/react/src/error.ts @@ -0,0 +1,106 @@ +import { captureException } from '@sentry/browser'; +import type { EventHint } from '@sentry/types'; +import { isError } from '@sentry/utils'; +import { version } from 'react'; +import type { ErrorInfo } from 'react'; + +/** + * See if React major version is 17+ by parsing version string. + */ +export function isAtLeastReact17(reactVersion: string): boolean { + const reactMajor = reactVersion.match(/^([^.]+)/); + return reactMajor !== null && parseInt(reactMajor[0]) >= 17; +} + +/** + * Recurse through `error.cause` chain to set cause on an error. + */ +export function setCause(error: Error & { cause?: Error }, cause: Error): void { + const seenErrors = new WeakSet(); + + function recurse(error: Error & { cause?: Error }, cause: Error): void { + // If we've already seen the error, there is a recursive loop somewhere in the error's + // cause chain. Let's just bail out then to prevent a stack overflow. + if (seenErrors.has(error)) { + return; + } + if (error.cause) { + seenErrors.add(error); + return recurse(error.cause, cause); + } + error.cause = cause; + } + + recurse(error, cause); +} + +/** + * Captures an error that was thrown by a React ErrorBoundary or React root. + * + * @param error The error to capture. + * @param errorInfo The errorInfo provided by React. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured Sentry event. + */ +export function captureReactException( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error: any, + { componentStack }: ErrorInfo, + hint?: EventHint, +): string { + // If on React version >= 17, create stack trace from componentStack param and links + // to to the original error using `error.cause` otherwise relies on error param for stacktrace. + // Linking errors requires the `LinkedErrors` integration be enabled. + // See: https://reactjs.org/blog/2020/08/10/react-v17-rc.html#native-component-stacks + // + // Although `componentDidCatch` is typed to accept an `Error` object, it can also be invoked + // with non-error objects. This is why we need to check if the error is an error-like object. + // See: https://github.com/getsentry/sentry-javascript/issues/6167 + if (isAtLeastReact17(version) && isError(error) && componentStack) { + const errorBoundaryError = new Error(error.message); + errorBoundaryError.name = `React ErrorBoundary ${error.name}`; + errorBoundaryError.stack = componentStack; + + // Using the `LinkedErrors` integration to link the errors together. + setCause(error, errorBoundaryError); + } + + return captureException(error, { + ...hint, + captureContext: { + contexts: { react: { componentStack } }, + }, + }); +} + +/** + * Creates an error handler that can be used with the `onCaughtError`, `onUncaughtError`, + * and `onRecoverableError` options in `createRoot` and `hydrateRoot` React DOM methods. + * + * @param callback An optional callback that will be called after the error is captured. + * Use this to add custom handling for errors. + * + * @example + * + * ```JavaScript + * const root = createRoot(container, { + * onCaughtError: Sentry.reactErrorHandler(), + * onUncaughtError: Sentry.reactErrorHandler((error, errorInfo) => { + * console.warn('Caught error', error, errorInfo.componentStack); + * }); + * }); + * ``` + */ +export function reactErrorHandler( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback?: (error: any, errorInfo: ErrorInfo, eventId: string) => void, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): (error: any, errorInfo: ErrorInfo) => void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (error: any, errorInfo: ErrorInfo) => { + const eventId = captureReactException(error, errorInfo); + if (callback) { + callback(error, errorInfo, eventId); + } + }; +} diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index 3ce1d0442b81..e12ca9f44d79 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -1,16 +1,12 @@ import type { ReportDialogOptions } from '@sentry/browser'; -import { captureException, getClient, showReportDialog, withScope } from '@sentry/browser'; +import { getClient, showReportDialog, withScope } from '@sentry/browser'; import type { Scope } from '@sentry/types'; -import { isError, logger } from '@sentry/utils'; +import { logger } from '@sentry/utils'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; import { DEBUG_BUILD } from './debug-build'; - -export function isAtLeastReact17(version: string): boolean { - const major = version.match(/^([^.]+)/); - return major !== null && parseInt(major[0]) >= 17; -} +import { captureReactException } from './error'; export const UNKNOWN_COMPONENT = 'unknown'; @@ -40,13 +36,13 @@ export type ErrorBoundaryProps = { */ fallback?: React.ReactElement | FallbackRender | undefined; /** Called when the error boundary encounters an error */ - onError?: ((error: unknown, componentStack: string, eventId: string) => void) | undefined; + onError?: ((error: unknown, componentStack: string | undefined, eventId: string) => void) | undefined; /** Called on componentDidMount() */ onMount?: (() => void) | undefined; /** Called if resetError() is called from the fallback render props function */ - onReset?: ((error: unknown, componentStack: string | null, eventId: string | null) => void) | undefined; + onReset?: ((error: unknown, componentStack: string | null | undefined, eventId: string | null) => void) | undefined; /** Called on componentWillUnmount() */ - onUnmount?: ((error: unknown, componentStack: string | null, eventId: string | null) => void) | undefined; + onUnmount?: ((error: unknown, componentStack: string | null | undefined, eventId: string | null) => void) | undefined; /** Called before the error is captured by Sentry, allows for you to add tags or context using the scope */ beforeCapture?: ((scope: Scope, error: unknown, componentStack: string | undefined) => void) | undefined; }; @@ -69,25 +65,6 @@ const INITIAL_STATE = { eventId: null, }; -function setCause(error: Error & { cause?: Error }, cause: Error): void { - const seenErrors = new WeakMap(); - - function recurse(error: Error & { cause?: Error }, cause: Error): void { - // If we've already seen the error, there is a recursive loop somewhere in the error's - // cause chain. Let's just bail out then to prevent a stack overflow. - if (seenErrors.has(error)) { - return; - } - if (error.cause) { - seenErrors.set(error, true); - return recurse(error.cause, cause); - } - error.cause = cause; - } - - recurse(error, cause); -} - /** * A ErrorBoundary component that logs errors to Sentry. * NOTE: If you are a Sentry user, and you are seeing this stack frame, it means the @@ -118,41 +95,21 @@ class ErrorBoundary extends React.Component { - // If on React version >= 17, create stack trace from componentStack param and links - // to to the original error using `error.cause` otherwise relies on error param for stacktrace. - // Linking errors requires the `LinkedErrors` integration be enabled. - // See: https://reactjs.org/blog/2020/08/10/react-v17-rc.html#native-component-stacks - // - // Although `componentDidCatch` is typed to accept an `Error` object, it can also be invoked - // with non-error objects. This is why we need to check if the error is an error-like object. - // See: https://github.com/getsentry/sentry-javascript/issues/6167 - if (isAtLeastReact17(React.version) && isError(error)) { - const errorBoundaryError = new Error(error.message); - errorBoundaryError.name = `React ErrorBoundary ${error.name}`; - errorBoundaryError.stack = componentStack; - - // Using the `LinkedErrors` integration to link the errors together. - setCause(error, errorBoundaryError); - } - if (beforeCapture) { - beforeCapture(scope, error, componentStack); + beforeCapture(scope, error, passedInComponentStack); } - const eventId = captureException(error, { - captureContext: { - contexts: { react: { componentStack } }, - }, - // If users provide a fallback component we can assume they are handling the error. - // Therefore, we set the mechanism depending on the presence of the fallback prop. - mechanism: { handled: !!this.props.fallback }, - }); + const eventId = captureReactException(error, errorInfo, { mechanism: { handled: !!this.props.fallback } }); if (onError) { - onError(error, componentStack, eventId); + onError(error, passedInComponentStack, eventId); } if (showDialog) { this._lastEventId = eventId; @@ -232,7 +189,6 @@ function withErrorBoundary

>( WrappedComponent: React.ComponentType

, errorBoundaryOptions: ErrorBoundaryProps, ): React.FC

{ - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const componentDisplayName = WrappedComponent.displayName || WrappedComponent.name || UNKNOWN_COMPONENT; const Wrapped: React.FC

= (props: P) => ( @@ -241,7 +197,6 @@ function withErrorBoundary

>( ); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access Wrapped.displayName = `errorBoundary(${componentDisplayName})`; // Copy over static methods from Wrapped component to Profiler HOC diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index ef6627cf0c5e..b0ee93d48677 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,6 +1,7 @@ export * from '@sentry/browser'; export { init } from './sdk'; +export { reactErrorHandler } from './error'; export { Profiler, withProfiler, useProfiler } from './profiler'; export type { ErrorBoundaryProps, FallbackRender } from './errorboundary'; export { ErrorBoundary, withErrorBoundary } from './errorboundary'; diff --git a/packages/react/test/error.test.ts b/packages/react/test/error.test.ts new file mode 100644 index 000000000000..780c6f9657fb --- /dev/null +++ b/packages/react/test/error.test.ts @@ -0,0 +1,14 @@ +import { isAtLeastReact17 } from '../src/error'; + +describe('isAtLeastReact17', () => { + test.each([ + ['React 16', '16.0.4', false], + ['React 17', '17.0.0', true], + ['React 17 with no patch', '17.4', true], + ['React 17 with no patch and no minor', '17', true], + ['React 18', '18.1.0', true], + ['React 19', '19.0.0', true], + ])('%s', (_: string, input: string, output: ReturnType) => { + expect(isAtLeastReact17(input)).toBe(output); + }); +}); diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx index 10c5130f88d7..d185dc8a9647 100644 --- a/packages/react/test/errorboundary.test.tsx +++ b/packages/react/test/errorboundary.test.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { useState } from 'react'; import type { ErrorBoundaryProps } from '../src/errorboundary'; -import { ErrorBoundary, UNKNOWN_COMPONENT, isAtLeastReact17, withErrorBoundary } from '../src/errorboundary'; +import { ErrorBoundary, UNKNOWN_COMPONENT, withErrorBoundary } from '../src/errorboundary'; const mockCaptureException = jest.fn(); const mockShowReportDialog = jest.fn(); @@ -53,7 +53,7 @@ interface TestAppProps extends ErrorBoundaryProps { errorComp?: JSX.Element; } -const TestApp: React.FC = ({ children, errorComp, ...props }) => { +const TestApp: React.FC = ({ children, errorComp, ...props }): any => { const customErrorComp = errorComp || ; const [isError, setError] = React.useState(false); return ( @@ -581,16 +581,3 @@ describe('ErrorBoundary', () => { }); }); }); - -describe('isAtLeastReact17', () => { - test.each([ - ['React 16', '16.0.4', false], - ['React 17', '17.0.0', true], - ['React 17 with no patch', '17.4', true], - ['React 17 with no patch and no minor', '17', true], - ['React 18', '18.1.0', true], - ['React 19', '19.0.0', true], - ])('%s', (_: string, input: string, output: ReturnType) => { - expect(isAtLeastReact17(input)).toBe(output); - }); -}); diff --git a/yarn.lock b/yarn.lock index 8819fb29597c..522f1ef1a8be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8451,17 +8451,8 @@ dependencies: "@types/unist" "*" -"@types/history-4@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history-5@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history@*": +"@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8", "@types/history@*": + name "@types/history-4" version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== @@ -8471,10 +8462,10 @@ resolved "https://registry.yarnpkg.com/@types/history/-/history-3.2.4.tgz#0b6c62240d1fac020853aa5608758991d9f6ef3d" integrity sha512-q7x8QeCRk2T6DR2UznwYW//mpN5uNlyajkewH2xd1s1ozCS4oHFRg2WMusxwLFlE57EkUYsd/gCapLBYzV3ffg== -"@types/hoist-non-react-statics@^3.3.1": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" - integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== +"@types/hoist-non-react-statics@^3.3.5": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" + integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== dependencies: "@types/react" "*" hoist-non-react-statics "^3.3.0" @@ -8827,15 +8818,7 @@ "@types/history" "^3" "@types/react" "*" -"@types/react-router-4@npm:@types/react-router@5.1.14": - version "5.1.14" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" - integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== - dependencies: - "@types/history" "*" - "@types/react" "*" - -"@types/react-router-5@npm:@types/react-router@5.1.14": +"@types/react-router-4@npm:@types/react-router@5.1.14", "@types/react-router-5@npm:@types/react-router@5.1.14": version "5.1.14" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== @@ -8850,7 +8833,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16.9.0", "@types/react@^17.0.3": +"@types/react@*", "@types/react@17.0.3", "@types/react@>=16.9.0": version "17.0.3" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79" integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg== @@ -26056,12 +26039,12 @@ rc@^1.2.7, rc@^1.2.8: strip-json-comments "~2.0.1" react-dom@^18.0.0: - version "18.0.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.0.0.tgz#26b88534f8f1dbb80853e1eabe752f24100d8023" - integrity sha512-XqX7uzmFo0pUceWFCt7Gff6IyIMzFUn7QMZrbrQfGxtaxXZIcGQzoNpRLE3fQLnS4XzLLPMZX2T9TRcSrasicw== + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== dependencies: loose-envify "^1.1.0" - scheduler "^0.21.0" + scheduler "^0.23.2" react-error-boundary@^3.1.0: version "3.1.1" @@ -26134,7 +26117,8 @@ react-is@^18.0.0: dependencies: "@remix-run/router" "1.0.2" -"react-router-6@npm:react-router@6.3.0": +"react-router-6@npm:react-router@6.3.0", react-router@6.3.0: + name react-router-6 version "6.3.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== @@ -26149,13 +26133,6 @@ react-router-dom@^6.2.2: history "^5.2.0" react-router "6.3.0" -react-router@6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" - integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== - dependencies: - history "^5.2.0" - react@^18.0.0: version "18.0.0" resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" @@ -27365,10 +27342,10 @@ saxes@^6.0.0: dependencies: xmlchars "^2.2.0" -scheduler@^0.21.0: - version "0.21.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.21.0.tgz#6fd2532ff5a6d877b6edb12f00d8ab7e8f308820" - integrity sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ== +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== dependencies: loose-envify "^1.1.0" @@ -28474,7 +28451,7 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -28500,15 +28477,6 @@ string-width@^2.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -28604,14 +28572,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -31238,7 +31199,7 @@ workerpool@^6.4.0: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.4.0.tgz#f8d5cfb45fde32fa3b7af72ad617c3369567a462" integrity sha512-i3KR1mQMNwY2wx20ozq2EjISGtQWDIfV56We+yGJ5yDs8jTwQiLLaqHlkBHITlCuJnYlVRmXegxFxZg7gqI++A== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -31256,15 +31217,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 8007c133399fc3d4148c9d7f4b899e7e0d9f34de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 19:15:12 +0000 Subject: [PATCH 04/14] feat(deps): bump @sentry/cli from 2.31.0 to 2.31.2 (#12239) --- packages/remix/package.json | 2 +- yarn.lock | 156 +++++++++++++++++++++++------------- 2 files changed, 103 insertions(+), 55 deletions(-) diff --git a/packages/remix/package.json b/packages/remix/package.json index b95be12742d4..bb5377e1a79c 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -58,7 +58,7 @@ }, "dependencies": { "@remix-run/router": "1.x", - "@sentry/cli": "^2.31.0", + "@sentry/cli": "^2.31.2", "@sentry/core": "8.5.0", "@sentry/node": "8.5.0", "@sentry/opentelemetry": "8.5.0", diff --git a/yarn.lock b/yarn.lock index 522f1ef1a8be..5187b6779fc1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7117,45 +7117,45 @@ magic-string "0.27.0" unplugin "1.0.1" -"@sentry/cli-darwin@2.31.0": - version "2.31.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.31.0.tgz#59e0805db8926a55676c74690e5083a0a78ae11f" - integrity sha512-VM5liyxMnm4K2g0WsrRPXRCMLhaT09C7gK5Fz/CxKYh9sbMZB7KA4hV/3klkyuyw1+ECF1J66cefhNkFZepUig== - -"@sentry/cli-linux-arm64@2.31.0": - version "2.31.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.31.0.tgz#38604d2d1e7c2e50d48610d38523e371d2104cd7" - integrity sha512-eENJTmXoFX3uNr8xRW7Bua2Sw3V1tylQfdtS85pNjZPdbm3U8wYQSWu2VoZkK2ASOoC+17YC8jTQxq62KWnSeQ== - -"@sentry/cli-linux-arm@2.31.0": - version "2.31.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.31.0.tgz#6e802a279011703d39e4b31de7b950c522a73261" - integrity sha512-AZoCN3waXEfXGCd3YSrikcX/y63oQe0Tiyapkeoifq/0QhI+2MOOrAQb60gthsXwb0UDK/XeFi3PaxyUCphzxA== - -"@sentry/cli-linux-i686@2.31.0": - version "2.31.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.31.0.tgz#d4586a18145f43b37324231e0f19f8f23793fc58" - integrity sha512-cQUFb3brhLaNSIoNzjU/YASnTM1I3TDJP9XXzH0eLK9sSopCcDcc6OrYEYvdjJXZKzFv5sbc9UNMsIDbh4+rYg== - -"@sentry/cli-linux-x64@2.31.0": - version "2.31.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.31.0.tgz#f89fd87b47a5eb10c292846f3a1a754cf97105fe" - integrity sha512-z1zTNg91nZJRdcGHC/bCU1KwIaifV0MLJteip9KrFDprzhJk1HtMxFOS0+OZ5/UH21CjAFmg9Pj6IAGqm3BYjA== - -"@sentry/cli-win32-i686@2.31.0": - version "2.31.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.31.0.tgz#cb3dbb539c8f8bcac4b1f95ab45a87b5143997ee" - integrity sha512-+K7fdk57aUd4CmYrQfDGYPzVyxsTnVro6IPb5QSSLpP03dL7ko5208epu4m2SyN/MkFvscy9Di3n3DTvIfDU2w== - -"@sentry/cli-win32-x64@2.31.0": - version "2.31.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.31.0.tgz#8ac3fa4ae0634911af4f4a497d58d2adce0f303a" - integrity sha512-w5cvpZ6VVlhlyleY8TYHmrP7g48vKHnoVt5xFccfxT+HqQI/AxodvzgVvBTM2kB/sh/kHwexp6bJGWCdkGftww== - -"@sentry/cli@^2.22.3", "@sentry/cli@^2.31.0": - version "2.31.0" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.31.0.tgz#a659216576fef56733de659057d6b9039d0b64e9" - integrity sha512-nCESoXAG3kRUO5n3QbDYAqX6RU3z1ORjnd7a3sqijYsCGHfOpcjGdS7JYLVg5if+tXMEF5529BPXFe5Kg/J9tw== +"@sentry/cli-darwin@2.31.2": + version "2.31.2" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.31.2.tgz#faeb87d09d8b21b8b8dd2e2aa848b538f01ddd26" + integrity sha512-BHA/JJXj1dlnoZQdK4efRCtHRnbBfzbIZUKAze7oRR1RfNqERI84BVUQeKateD3jWSJXQfEuclIShc61KOpbKw== + +"@sentry/cli-linux-arm64@2.31.2": + version "2.31.2" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.31.2.tgz#669c9c3f7f9130d26f5db732f793378863d58869" + integrity sha512-FLVKkJ/rWvPy/ka7OrUdRW63a/z8HYI1Gt8Pr6rWs50hb7YJja8lM8IO10tYmcFE/tODICsnHO9HTeUg2g2d1w== + +"@sentry/cli-linux-arm@2.31.2": + version "2.31.2" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.31.2.tgz#3e36ed7db09e922f00221281252e58dfd8755ea5" + integrity sha512-W8k5mGYYZz/I/OxZH65YAK7dCkQAl+wbuoASGOQjUy5VDgqH0QJ8kGJufXvFPM+f3ZQGcKAnVsZ6tFqZXETBAw== + +"@sentry/cli-linux-i686@2.31.2": + version "2.31.2" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.31.2.tgz#02b7da274369b78a5676c20bb26cc37caed5244b" + integrity sha512-A64QtzaPi3MYFpZ+Fwmi0mrSyXgeLJ0cWr4jdeTGrzNpeowSteKgd6tRKU+LVq0k5shKE7wdnHk+jXnoajulMA== + +"@sentry/cli-linux-x64@2.31.2": + version "2.31.2" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.31.2.tgz#54f74a9e5925db9ddafebc0efd4056c5377be5fd" + integrity sha512-YL/r+15R4mOEiU3mzn7iFQOeFEUB6KxeKGTTrtpeOGynVUGIdq4nV5rHow5JDbIzOuBS3SpOmcIMluvo1NCh0g== + +"@sentry/cli-win32-i686@2.31.2": + version "2.31.2" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.31.2.tgz#5dab845a824be0927566171aa05f015e887fe82d" + integrity sha512-Az/2bmW+TFI059RE0mSBIxTBcoShIclz7BDebmIoCkZ+retrwAzpmBnBCDAHow+Yi43utOow+3/4idGa2OxcLw== + +"@sentry/cli-win32-x64@2.31.2": + version "2.31.2" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.31.2.tgz#e12fec0a54f6d9cced5235fbc68ba8f94165634b" + integrity sha512-XIzyRnJu539NhpFa+JYkotzVwv3NrZ/4GfHB/JWA2zReRvsk39jJG8D5HOmm0B9JA63QQT7Dt39RW8g3lkmb6w== + +"@sentry/cli@^2.22.3", "@sentry/cli@^2.31.2": + version "2.31.2" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.31.2.tgz#39df8e52966aa8db4f9c51f4bc77abd62b6a630e" + integrity sha512-2aKyUx6La2P+pplL8+2vO67qJ+c1C79KYWAyQBE0JIT5kvKK9JpwtdNoK1F0/2mRpwhhYPADCz3sVIRqmL8cQQ== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -7163,13 +7163,13 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.31.0" - "@sentry/cli-linux-arm" "2.31.0" - "@sentry/cli-linux-arm64" "2.31.0" - "@sentry/cli-linux-i686" "2.31.0" - "@sentry/cli-linux-x64" "2.31.0" - "@sentry/cli-win32-i686" "2.31.0" - "@sentry/cli-win32-x64" "2.31.0" + "@sentry/cli-darwin" "2.31.2" + "@sentry/cli-linux-arm" "2.31.2" + "@sentry/cli-linux-arm64" "2.31.2" + "@sentry/cli-linux-i686" "2.31.2" + "@sentry/cli-linux-x64" "2.31.2" + "@sentry/cli-win32-i686" "2.31.2" + "@sentry/cli-win32-x64" "2.31.2" "@sentry/vite-plugin@2.14.2", "@sentry/vite-plugin@^2.14.2": version "2.14.2" @@ -8451,8 +8451,17 @@ dependencies: "@types/unist" "*" -"@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8", "@types/history@*": - name "@types/history-4" +"@types/history-4@npm:@types/history@4.7.8": + version "4.7.8" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" + integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== + +"@types/history-5@npm:@types/history@4.7.8": + version "4.7.8" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" + integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== + +"@types/history@*": version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== @@ -8818,7 +8827,15 @@ "@types/history" "^3" "@types/react" "*" -"@types/react-router-4@npm:@types/react-router@5.1.14", "@types/react-router-5@npm:@types/react-router@5.1.14": +"@types/react-router-4@npm:@types/react-router@5.1.14": + version "5.1.14" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" + integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== + dependencies: + "@types/history" "*" + "@types/react" "*" + +"@types/react-router-5@npm:@types/react-router@5.1.14": version "5.1.14" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== @@ -26117,8 +26134,7 @@ react-is@^18.0.0: dependencies: "@remix-run/router" "1.0.2" -"react-router-6@npm:react-router@6.3.0", react-router@6.3.0: - name react-router-6 +"react-router-6@npm:react-router@6.3.0": version "6.3.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== @@ -26133,6 +26149,13 @@ react-router-dom@^6.2.2: history "^5.2.0" react-router "6.3.0" +react-router@6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" + integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== + dependencies: + history "^5.2.0" + react@^18.0.0: version "18.0.0" resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" @@ -28451,7 +28474,7 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -28477,6 +28500,15 @@ string-width@^2.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -28572,7 +28604,14 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -31199,7 +31238,7 @@ workerpool@^6.4.0: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.4.0.tgz#f8d5cfb45fde32fa3b7af72ad617c3369567a462" integrity sha512-i3KR1mQMNwY2wx20ozq2EjISGtQWDIfV56We+yGJ5yDs8jTwQiLLaqHlkBHITlCuJnYlVRmXegxFxZg7gqI++A== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -31217,6 +31256,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 38bd57b0785c97c413f36f89ff931d927e469078 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 28 May 2024 10:49:48 +0200 Subject: [PATCH 05/14] fix(tracing): Ensure sent spans are limited to 1000 (#12252) To avoid too large payloads, we should only send up to 1000 spans. We actually had two different problems here in browser and node: 1. In browser, we _did_ limit to 1000, but we did so in an unfortunate way, which is to reset the set after 1000 spans. So if a span had 1010 children, only the last 10 would be send. 2. In node, we just sent all spans. I rewrote this to now consistently send the first 1000 spans of a transaction. I decided to keep all spans (no matter the limit) on the internal parent-child map. Due to this this may grow a lot, but we can avoid inconsistent state (e.g. what happens if a span has a parent in the map, but the parent does not have the span as children, ...) All of these should be garbage collected together ideally, so this should be fine hopefully. --- .../suites/tracing/maxSpans/init.js | 15 ++++++++++++++ .../suites/tracing/maxSpans/test.ts | 20 +++++++++++++++++++ .../suites/tracing/maxSpans/scenario.ts | 15 ++++++++++++++ .../suites/tracing/maxSpans/test.ts | 19 ++++++++++++++++++ packages/core/src/tracing/sentrySpan.ts | 9 ++++++++- packages/core/src/utils/spanUtils.ts | 2 +- packages/opentelemetry/src/spanExporter.ts | 9 ++++++++- 7 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/maxSpans/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/maxSpans/test.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/maxSpans/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/maxSpans/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/maxSpans/init.js b/dev-packages/browser-integration-tests/suites/tracing/maxSpans/init.js new file mode 100644 index 000000000000..162bc4a04cd9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/maxSpans/init.js @@ -0,0 +1,15 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [], + tracesSampleRate: 1, +}); + +Sentry.startSpan({ name: 'parent' }, () => { + for (let i = 0; i < 5000; i++) { + Sentry.startInactiveSpan({ name: `child ${i}` }).end(); + } +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/maxSpans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/maxSpans/test.ts new file mode 100644 index 000000000000..328c5c364c87 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/maxSpans/test.ts @@ -0,0 +1,20 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequestOnUrl } from '../../../utils/helpers'; + +sentryTest('it limits spans to 1000', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const req = await waitForTransactionRequestOnUrl(page, url); + const transaction = envelopeRequestParser(req); + + expect(transaction.spans).toHaveLength(1000); + expect(transaction.spans).toContainEqual(expect.objectContaining({ description: 'child 0' })); + expect(transaction.spans).toContainEqual(expect.objectContaining({ description: 'child 999' })); + expect(transaction.spans).not.toContainEqual(expect.objectContaining({ description: 'child 1000' })); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/maxSpans/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/maxSpans/scenario.ts new file mode 100644 index 000000000000..f589d221e37c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/maxSpans/scenario.ts @@ -0,0 +1,15 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +Sentry.startSpan({ name: 'parent' }, () => { + for (let i = 0; i < 5000; i++) { + Sentry.startInactiveSpan({ name: `child ${i}` }).end(); + } +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/maxSpans/test.ts b/dev-packages/node-integration-tests/suites/tracing/maxSpans/test.ts new file mode 100644 index 000000000000..9bc623b12200 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/maxSpans/test.ts @@ -0,0 +1,19 @@ +import type { SpanJSON } from '@sentry/types'; +import { createRunner } from '../../../utils/runner'; + +test('it limits spans to 1000', done => { + const expectedSpans: SpanJSON[] = []; + for (let i = 0; i < 1000; i++) { + expectedSpans.push(expect.objectContaining({ description: `child ${i}` })); + } + + createRunner(__dirname, 'scenario.ts') + .ignore('session', 'sessions') + .expect({ + transaction: { + transaction: 'parent', + spans: expectedSpans, + }, + }) + .start(done); +}); diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index b2dd49f32b2a..9d9706d4ebce 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -41,6 +41,8 @@ import { logSpanEnd } from './logSpans'; import { timedEventsToMeasurements } from './measurement'; import { getCapturedScopesOnSpan } from './utils'; +const MAX_SPAN_COUNT = 1000; + /** * Span contains all data about a span */ @@ -310,7 +312,12 @@ export class SentrySpan implements Span { contexts: { trace: spanToTransactionTraceContext(this), }, - spans, + spans: + // spans.sort() mutates the array, but `spans` is already a copy so we can safely do this here + // we do not use spans anymore after this point + spans.length > MAX_SPAN_COUNT + ? spans.sort((a, b) => a.start_timestamp - b.start_timestamp).slice(0, MAX_SPAN_COUNT) + : spans, start_timestamp: this._startTime, timestamp: this._endTime, transaction: this._name, diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 55b3df65aa2b..0878f0b383b3 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -208,7 +208,7 @@ export function addChildSpanToSpan(span: SpanWithPotentialChildren, childSpan: S // We store a list of child spans on the parent span // We need this for `getSpanDescendants()` to work - if (span[CHILD_SPANS_FIELD] && span[CHILD_SPANS_FIELD].size < 1000) { + if (span[CHILD_SPANS_FIELD]) { span[CHILD_SPANS_FIELD].add(childSpan); } else { addNonEnumerableProperty(span, CHILD_SPANS_FIELD, new Set([childSpan])); diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index f570b9f51859..aa5f37f550e9 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -31,6 +31,8 @@ import { parseSpanDescription } from './utils/parseSpanDescription'; type SpanNodeCompleted = SpanNode & { span: ReadableSpan }; +const MAX_SPAN_COUNT = 1000; + /** * A Sentry-specific exporter that converts OpenTelemetry Spans to Sentry Spans & Transactions. */ @@ -140,7 +142,12 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { createAndFinishSpanForOtelSpan(child, spans, remaining); }); - transactionEvent.spans = spans; + // spans.sort() mutates the array, but we do not use this anymore after this point + // so we can safely mutate it here + transactionEvent.spans = + spans.length > MAX_SPAN_COUNT + ? spans.sort((a, b) => a.start_timestamp - b.start_timestamp).slice(0, MAX_SPAN_COUNT) + : spans; const measurements = timedEventsToMeasurements(span.events); if (measurements) { From d5f1caf523f4d9656df7d903cd449adfff8574ca Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 28 May 2024 15:24:32 +0200 Subject: [PATCH 06/14] test: Improve E2E node tests (#12258) This does two main things: 1. Updates node E2E tests to use `fetch` instead of axios 2. Update node E2E tests to avoid sending to Sentry. Instead, we check everywhere via the proxy that the data sent is OK. Part of https://github.com/getsentry/sentry-javascript/issues/11910 --- .../tests/behaviour-server.test.ts | 6 +- .../node-connect/tests/errors.test.ts | 44 +-------- .../node-connect/tests/transactions.test.ts | 33 +------ .../node-express/tests/errors.test.ts | 97 ++----------------- .../node-express/tests/transactions.test.ts | 38 +------- .../test-applications/node-fastify/src/app.ts | 13 +++ .../node-fastify/tests/errors.test.ts | 44 +-------- .../node-fastify/tests/propagation.test.ts | 23 +++-- .../node-fastify/tests/transactions.test.ts | 38 +------- .../node-hapi/tests/errors.test.ts | 87 ++++------------- .../node-hapi/tests/transactions.test.ts | 11 +-- .../node-koa/tests/errors.test.ts | 44 +-------- .../node-koa/tests/propagation.test.ts | 23 +++-- .../node-koa/tests/transactions.test.ts | 39 +------- .../node-nestjs/tests/errors.test.ts | 50 +--------- .../node-nestjs/tests/propagation.test.ts | 23 +++-- .../node-nestjs/tests/transactions.test.ts | 39 +------- 17 files changed, 102 insertions(+), 550 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-next-app/tests/behaviour-server.test.ts index becf9bf9bce7..6b126c3e8130 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/tests/behaviour-server.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-next-app/tests/behaviour-server.test.ts @@ -7,7 +7,8 @@ const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; const EVENT_POLLING_TIMEOUT = 90_000; test('Sends a server-side exception to Sentry', async ({ baseURL }) => { - const { data } = await axios.get(`${baseURL}/api/error`); + const response = await fetch(`${baseURL}/api/error`); + const data = await response.json(); const { exceptionId } = data; const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionId}/`; @@ -39,7 +40,8 @@ test('Sends a server-side exception to Sentry', async ({ baseURL }) => { }); test('Sends server-side transactions to Sentry', async ({ baseURL }) => { - const { data } = await axios.get(`${baseURL}/api/success`); + const response = await fetch(`${baseURL}/api/success`); + const data = await response.json(); const { transactionIds } = data; console.log(`Polling for transaction eventIds: ${JSON.stringify(transactionIds)}`); diff --git a/dev-packages/e2e-tests/test-applications/node-connect/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-connect/tests/errors.test.ts index c83fa1b38889..0ae0b8c017e2 100644 --- a/dev-packages/e2e-tests/test-applications/node-connect/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-connect/tests/errors.test.ts @@ -1,54 +1,12 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/event-proxy-server'; -import axios, { AxiosError } from 'axios'; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; -const EVENT_POLLING_TIMEOUT = 90_000; - -test('Sends exception to Sentry', async ({ baseURL }) => { - const { data } = await axios.get(`${baseURL}/test-error`); - const { exceptionId } = data; - - const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionId}/`; - - console.log(`Polling for error eventId: ${exceptionId}`); - - await expect - .poll( - async () => { - try { - const response = await axios.get(url, { headers: { Authorization: `Bearer ${authToken}` } }); - - return response.status; - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status !== 404) { - throw e; - } else { - return e.response.status; - } - } else { - throw e; - } - } - }, - { timeout: EVENT_POLLING_TIMEOUT }, - ) - .toBe(200); -}); test('Sends correct error event', async ({ baseURL }) => { const errorEventPromise = waitForError('node-connect', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception'; }); - try { - await axios.get(`${baseURL}/test-exception`); - } catch { - // this results in an error, but we don't care - we want to check the error event - } + await fetch(`${baseURL}/test-exception`); const errorEvent = await errorEventPromise; diff --git a/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts index aef603305b8e..d18e4c1b6c20 100644 --- a/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts @@ -1,6 +1,5 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/event-proxy-server'; -import axios, { AxiosError } from 'axios'; const authToken = process.env.E2E_TEST_AUTH_TOKEN; const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; @@ -15,7 +14,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { ); }); - await axios.get(`${baseURL}/test-transaction`); + await fetch(`${baseURL}/test-transaction`); const transactionEvent = await pageloadTransactionEventPromise; const transactionEventId = transactionEvent.event_id; @@ -35,7 +34,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'http.method': 'GET', 'http.scheme': 'http', 'http.target': '/test-transaction', - 'http.user_agent': 'axios/1.6.7', + 'http.user_agent': 'node', 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -97,32 +96,4 @@ test('Sends an API route transaction', async ({ baseURL }) => { }, }), ); - - await expect - .poll( - async () => { - try { - const response = await axios.get( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - return response.status; - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status !== 404) { - throw e; - } else { - return e.response.status; - } - } else { - throw e; - } - } - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); }); diff --git a/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts index 4e725f8eb8ad..4b7c3c71edc6 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts @@ -1,54 +1,12 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/event-proxy-server'; -import axios, { AxiosError, AxiosResponse } from 'axios'; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; -const EVENT_POLLING_TIMEOUT = 90_000; - -test('Sends exception to Sentry', async ({ baseURL }) => { - const { data } = await axios.get(`${baseURL}/test-error`); - const { exceptionId } = data; - - const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionId}/`; - - console.log(`Polling for error eventId: ${exceptionId}`); - - await expect - .poll( - async () => { - try { - const response = await axios.get(url, { headers: { Authorization: `Bearer ${authToken}` } }); - - return response.status; - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status !== 404) { - throw e; - } else { - return e.response.status; - } - } else { - throw e; - } - } - }, - { timeout: EVENT_POLLING_TIMEOUT }, - ) - .toBe(200); -}); test('Sends correct error event', async ({ baseURL }) => { const errorEventPromise = waitForError('node-express', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); - try { - await axios.get(`${baseURL}/test-exception/123`); - } catch { - // this results in an error, but we don't care - we want to check the error event - } + await fetch(`${baseURL}/test-exception/123`); const errorEvent = await errorEventPromise; @@ -71,55 +29,14 @@ test('Sends correct error event', async ({ baseURL }) => { }); test('Should record caught exceptions with local variable', async ({ baseURL }) => { - const { data } = await axios.get(`${baseURL}/test-local-variables-caught`); - const { exceptionId } = data; - - const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionId}/json/`; - - console.log(`Polling for error eventId: ${exceptionId}`); - - let response: AxiosResponse; - - await expect - .poll( - async () => { - try { - response = await axios.get(url, { headers: { Authorization: `Bearer ${authToken}` } }); - - return response.status; - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status !== 404) { - throw e; - } else { - return e.response.status; - } - } else { - throw e; - } - } - }, - { timeout: EVENT_POLLING_TIMEOUT }, - ) - .toBe(200); - - const frames = response!.data.exception.values[0].stacktrace.frames; - - expect(frames[frames.length - 1].vars?.randomVariableToRecord).toBeDefined(); -}); - -test('Should record uncaught exceptions with local variable', async ({ baseURL }) => { - const errorEventPromise = waitForError('node-express', errorEvent => { - return !!errorEvent?.exception?.values?.[0]?.value?.includes('Uncaught Local Variable Error'); - }); - - await axios.get(`${baseURL}/test-local-variables-uncaught`).catch(() => { - // noop + const errorEventPromise = waitForError('node-express', event => { + return event.transaction === 'GET /test-local-variables-caught'; }); - const routehandlerError = await errorEventPromise; + await fetch(`${baseURL}/test-local-variables-caught`); - const frames = routehandlerError!.exception!.values![0]!.stacktrace!.frames!; + const errorEvent = await errorEventPromise; - expect(frames[frames.length - 1].vars?.randomVariableToRecord).toBeDefined(); + const frames = errorEvent.exception?.values?.[0].stacktrace?.frames; + expect(frames?.[frames.length - 1].vars?.randomVariableToRecord).toBeDefined(); }); diff --git a/dev-packages/e2e-tests/test-applications/node-express/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-express/tests/transactions.test.ts index fcb3913e1fce..ec28035f0d38 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/tests/transactions.test.ts @@ -1,11 +1,5 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/event-proxy-server'; -import axios, { AxiosError } from 'axios'; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; -const EVENT_POLLING_TIMEOUT = 90_000; test('Sends an API route transaction', async ({ baseURL }) => { const pageloadTransactionEventPromise = waitForTransaction('node-express', transactionEvent => { @@ -15,7 +9,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { ); }); - await axios.get(`${baseURL}/test-transaction`); + await fetch(`${baseURL}/test-transaction`); const transactionEvent = await pageloadTransactionEventPromise; const transactionEventId = transactionEvent.event_id; @@ -35,7 +29,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'http.method': 'GET', 'http.scheme': 'http', 'http.target': '/test-transaction', - 'http.user_agent': 'axios/1.6.7', + 'http.user_agent': 'node', 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -124,32 +118,4 @@ test('Sends an API route transaction', async ({ baseURL }) => { timestamp: expect.any(Number), trace_id: expect.any(String), }); - - await expect - .poll( - async () => { - try { - const response = await axios.get( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - return response.status; - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status !== 404) { - throw e; - } else { - return e.response.status; - } - } else { - throw e; - } - } - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); }); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify/src/app.ts index 187d259b1f5b..275dfa786ca3 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify/src/app.ts @@ -1,6 +1,19 @@ import type * as S from '@sentry/node'; const Sentry = require('@sentry/node') as typeof S; +// We wrap console.warn to find out if a warning is incorrectly logged +console.warn = new Proxy(console.warn, { + apply: function (target, thisArg, argumentsList) { + const msg = argumentsList[0]; + if (typeof msg === 'string' && msg.startsWith('[Sentry]')) { + console.error(`Sentry warning was triggered: ${msg}`); + process.exit(1); + } + + return target.apply(thisArg, argumentsList); + }, +}); + Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify/tests/errors.test.ts index 8014803360cd..3bd2584c6b22 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify/tests/errors.test.ts @@ -1,54 +1,12 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/event-proxy-server'; -import axios, { AxiosError } from 'axios'; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; -const EVENT_POLLING_TIMEOUT = 90_000; - -test('Sends exception to Sentry', async ({ baseURL }) => { - const { data } = await axios.get(`${baseURL}/test-error`); - const { exceptionId } = data; - - const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionId}/`; - - console.log(`Polling for error eventId: ${exceptionId}`); - - await expect - .poll( - async () => { - try { - const response = await axios.get(url, { headers: { Authorization: `Bearer ${authToken}` } }); - - return response.status; - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status !== 404) { - throw e; - } else { - return e.response.status; - } - } else { - throw e; - } - } - }, - { timeout: EVENT_POLLING_TIMEOUT }, - ) - .toBe(200); -}); test('Sends correct error event', async ({ baseURL }) => { const errorEventPromise = waitForError('node-fastify', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); - try { - await axios.get(`${baseURL}/test-exception/123`); - } catch { - // this results in an error, but we don't care - we want to check the error event - } + await fetch(`${baseURL}/test-exception/123`); const errorEvent = await errorEventPromise; diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify/tests/propagation.test.ts index 471d9daa16b5..6900e4d09cd2 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify/tests/propagation.test.ts @@ -2,7 +2,6 @@ import crypto from 'crypto'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/event-proxy-server'; import { SpanJSON } from '@sentry/types'; -import axios from 'axios'; test('Propagates trace for outgoing http requests', async ({ baseURL }) => { const id = crypto.randomUUID(); @@ -21,7 +20,8 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-http/${id}`); + const response = await fetch(`${baseURL}/test-outgoing-http/${id}`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; const outboundTransaction = await outboundTransactionPromise; @@ -66,7 +66,7 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'http.method': 'GET', 'http.scheme': 'http', 'http.target': `/test-outgoing-http/${id}`, - 'http.user_agent': 'axios/1.6.7', + 'http.user_agent': 'node', 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -135,7 +135,8 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-fetch/${id}`); + const response = await fetch(`${baseURL}/test-outgoing-fetch/${id}`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; const outboundTransaction = await outboundTransactionPromise; @@ -180,7 +181,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'http.method': 'GET', 'http.scheme': 'http', 'http.target': `/test-outgoing-fetch/${id}`, - 'http.user_agent': 'axios/1.6.7', + 'http.user_agent': 'node', 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -240,7 +241,8 @@ test('Propagates trace for outgoing external http requests', async ({ baseURL }) ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-http-external-allowed`); + const response = await fetch(`${baseURL}/test-outgoing-http-external-allowed`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; @@ -276,7 +278,8 @@ test('Does not propagate outgoing http requests not covered by tracePropagationT ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-http-external-disallowed`); + const response = await fetch(`${baseURL}/test-outgoing-http-external-disallowed`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; @@ -299,7 +302,8 @@ test('Propagates trace for outgoing external fetch requests', async ({ baseURL } ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-fetch-external-allowed`); + const response = await fetch(`${baseURL}/test-outgoing-fetch-external-allowed`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; @@ -335,7 +339,8 @@ test('Does not propagate outgoing fetch requests not covered by tracePropagation ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-fetch-external-disallowed`); + const response = await fetch(`${baseURL}/test-outgoing-fetch-external-disallowed`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify/tests/transactions.test.ts index cfc320f3d4dd..ee1e6effbc71 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify/tests/transactions.test.ts @@ -1,11 +1,5 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/event-proxy-server'; -import axios, { AxiosError } from 'axios'; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; -const EVENT_POLLING_TIMEOUT = 90_000; test('Sends an API route transaction', async ({ baseURL }) => { const pageloadTransactionEventPromise = waitForTransaction('node-fastify', transactionEvent => { @@ -15,7 +9,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { ); }); - await axios.get(`${baseURL}/test-transaction`); + await fetch(`${baseURL}/test-transaction`); const transactionEvent = await pageloadTransactionEventPromise; const transactionEventId = transactionEvent.event_id; @@ -35,7 +29,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'http.method': 'GET', 'http.scheme': 'http', 'http.target': '/test-transaction', - 'http.user_agent': 'axios/1.6.7', + 'http.user_agent': 'node', 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -134,32 +128,4 @@ test('Sends an API route transaction', async ({ baseURL }) => { trace_id: expect.any(String), origin: 'manual', }); - - await expect - .poll( - async () => { - try { - const response = await axios.get( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - return response.status; - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status !== 404) { - throw e; - } else { - return e.response.status; - } - } else { - throw e; - } - } - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); }); diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-hapi/tests/errors.test.ts index 8782e8bb42a9..7abe75892d19 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-hapi/tests/errors.test.ts @@ -1,83 +1,32 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/event-proxy-server'; -import axios, { AxiosError } from 'axios'; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; -const EVENT_POLLING_TIMEOUT = 90_000; - -test('Sends captured exception to Sentry', async ({ baseURL }) => { - const { data } = await axios.get(`${baseURL}/test-error`); - const { exceptionId } = data; - - const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionId}/`; - - console.log(`Polling for error eventId: ${exceptionId}`); - - await expect - .poll( - async () => { - try { - const response = await axios.get(url, { headers: { Authorization: `Bearer ${authToken}` } }); - - return response.status; - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status !== 404) { - throw e; - } else { - return e.response.status; - } - } else { - throw e; - } - } - }, - { timeout: EVENT_POLLING_TIMEOUT }, - ) - .toBe(200); -}); test('Sends thrown error to Sentry', async ({ baseURL }) => { const errorEventPromise = waitForError('node-hapi', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'This is an error'; }); - try { - await axios.get(`${baseURL}/test-failure`); - } catch (e) {} + await fetch(`${baseURL}/test-failure`); const errorEvent = await errorEventPromise; const errorEventId = errorEvent.event_id; - await expect - .poll( - async () => { - try { - const response = await axios.get( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${errorEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an error'); + + expect(errorEvent.request).toEqual({ + method: 'GET', + cookies: {}, + headers: expect.any(Object), + url: 'http://localhost:3030/test-failure', + }); - return response.status; - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status !== 404) { - throw e; - } else { - return e.response.status; - } - } else { - throw e; - } - } - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); + expect(errorEvent.transaction).toEqual('GET /test-failure'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.any(String), + span_id: expect.any(String), + }); }); test('sends error with parameterized transaction name', async ({ baseURL }) => { @@ -85,9 +34,7 @@ test('sends error with parameterized transaction name', async ({ baseURL }) => { return errorEvent?.exception?.values?.[0]?.value === 'This is an error with id 123'; }); - try { - await axios.get(`${baseURL}/test-error/123`); - } catch {} + await fetch(`${baseURL}/test-error/123`); const errorEvent = await errorEventPromise; diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts index 0f38cff86e79..936861ce8f18 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts @@ -1,6 +1,5 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/event-proxy-server'; -import axios from 'axios'; test('Sends successful transaction', async ({ baseURL }) => { const pageloadTransactionEventPromise = waitForTransaction('node-hapi', transactionEvent => { @@ -9,10 +8,9 @@ test('Sends successful transaction', async ({ baseURL }) => { ); }); - await axios.get(`${baseURL}/test-success`); + await fetch(`${baseURL}/test-success`); const transactionEvent = await pageloadTransactionEventPromise; - const transactionEventId = transactionEvent.event_id; expect(transactionEvent.contexts?.trace).toEqual({ data: { @@ -29,7 +27,7 @@ test('Sends successful transaction', async ({ baseURL }) => { 'http.method': 'GET', 'http.scheme': 'http', 'http.target': '/test-success', - 'http.user_agent': 'axios/1.6.7', + 'http.user_agent': 'node', 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -90,10 +88,9 @@ test('Sends parameterized transactions to Sentry', async ({ baseURL }) => { ); }); - await axios.get(`${baseURL}/test-param/123`); + await fetch(`${baseURL}/test-param/123`); const transactionEvent = await pageloadTransactionEventPromise; - const transactionEventId = transactionEvent.event_id; expect(transactionEvent?.contexts?.trace?.op).toBe('http.server'); expect(transactionEvent?.contexts?.trace?.data?.['http.route']).toBe('/test-param/{param}'); @@ -114,7 +111,7 @@ test('Isolates requests', async ({ baseURL }) => { ); }); - await Promise.all([axios.get(`${baseURL}/test-param/888`), axios.get(`${baseURL}/test-param/999`)]); + await Promise.all([fetch(`${baseURL}/test-param/888`), fetch(`${baseURL}/test-param/999`)]); const transaction1 = await transaction1Promise; const transaction2 = await transaction2Promise; diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/errors.test.ts index ce9ade128884..1838d66580e6 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/errors.test.ts @@ -1,54 +1,12 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/event-proxy-server'; -import axios, { AxiosError } from 'axios'; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; -const EVENT_POLLING_TIMEOUT = 90_000; - -test('Sends exception to Sentry', async ({ baseURL }) => { - const { data } = await axios.get(`${baseURL}/test-error`); - const { exceptionId } = data; - - const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionId}/`; - - console.log(`Polling for error eventId: ${exceptionId}`); - - await expect - .poll( - async () => { - try { - const response = await axios.get(url, { headers: { Authorization: `Bearer ${authToken}` } }); - - return response.status; - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status !== 404) { - throw e; - } else { - return e.response.status; - } - } else { - throw e; - } - } - }, - { timeout: EVENT_POLLING_TIMEOUT }, - ) - .toBe(200); -}); test('Sends correct error event', async ({ baseURL }) => { const errorEventPromise = waitForError('node-koa', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); - try { - await axios.get(`${baseURL}/test-exception/123`); - } catch { - // this results in an error, but we don't care - we want to check the error event - } + await fetch(`${baseURL}/test-exception/123`); const errorEvent = await errorEventPromise; diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts index 801539ebbafe..6e3abf414ba3 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts @@ -2,7 +2,6 @@ import crypto from 'crypto'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/event-proxy-server'; import { SpanJSON } from '@sentry/types'; -import axios from 'axios'; test('Propagates trace for outgoing http requests', async ({ baseURL }) => { const id = crypto.randomUUID(); @@ -21,7 +20,8 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-http/${id}`); + const response = await fetch(`${baseURL}/test-outgoing-http/${id}`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; const outboundTransaction = await outboundTransactionPromise; @@ -66,7 +66,7 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'http.method': 'GET', 'http.scheme': 'http', 'http.target': `/test-outgoing-http/${id}`, - 'http.user_agent': 'axios/1.6.7', + 'http.user_agent': 'node', 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -135,7 +135,8 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-fetch/${id}`); + const response = await fetch(`${baseURL}/test-outgoing-fetch/${id}`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; const outboundTransaction = await outboundTransactionPromise; @@ -180,7 +181,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'http.method': 'GET', 'http.scheme': 'http', 'http.target': `/test-outgoing-fetch/${id}`, - 'http.user_agent': 'axios/1.6.7', + 'http.user_agent': 'node', 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -240,7 +241,8 @@ test('Propagates trace for outgoing external http requests', async ({ baseURL }) ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-http-external-allowed`); + const response = await fetch(`${baseURL}/test-outgoing-http-external-allowed`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; @@ -276,7 +278,8 @@ test('Does not propagate outgoing http requests not covered by tracePropagationT ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-http-external-disallowed`); + const response = await fetch(`${baseURL}/test-outgoing-http-external-disallowed`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; @@ -299,7 +302,8 @@ test('Propagates trace for outgoing external fetch requests', async ({ baseURL } ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-fetch-external-allowed`); + const response = await fetch(`${baseURL}/test-outgoing-fetch-external-allowed`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; @@ -335,7 +339,8 @@ test('Does not propagate outgoing fetch requests not covered by tracePropagation ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-fetch-external-disallowed`); + const response = await fetch(`${baseURL}/test-outgoing-fetch-external-disallowed`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts index af70c480fc24..d2db2aa54ae6 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts @@ -1,11 +1,5 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/event-proxy-server'; -import axios, { AxiosError } from 'axios'; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; -const EVENT_POLLING_TIMEOUT = 90_000; test('Sends an API route transaction', async ({ baseURL }) => { const pageloadTransactionEventPromise = waitForTransaction('node-koa', transactionEvent => { @@ -15,10 +9,9 @@ test('Sends an API route transaction', async ({ baseURL }) => { ); }); - await axios.get(`${baseURL}/test-transaction`); + await fetch(`${baseURL}/test-transaction`); const transactionEvent = await pageloadTransactionEventPromise; - const transactionEventId = transactionEvent.event_id; expect(transactionEvent.contexts?.trace).toEqual({ data: { @@ -35,7 +28,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'http.method': 'GET', 'http.scheme': 'http', 'http.target': '/test-transaction', - 'http.user_agent': 'axios/1.6.7', + 'http.user_agent': 'node', 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -129,32 +122,4 @@ test('Sends an API route transaction', async ({ baseURL }) => { }, }), ); - - await expect - .poll( - async () => { - try { - const response = await axios.get( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - return response.status; - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status !== 404) { - throw e; - } else { - return e.response.status; - } - } else { - throw e; - } - } - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); }); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs/tests/errors.test.ts index dd0e4cc7e1bf..f57d656068df 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs/tests/errors.test.ts @@ -1,59 +1,13 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/event-proxy-server'; -import axios, { AxiosError } from 'axios'; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; -const EVENT_POLLING_TIMEOUT = 90_000; - -test('Sends captured error to Sentry', async ({ baseURL }) => { - const { data } = await axios.get(`${baseURL}/test-error`); - const { exceptionId } = data; - - const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionId}/`; - - console.log(`Polling for error eventId: ${exceptionId}`); - - await expect - .poll( - async () => { - try { - const response = await axios.get(url, { - headers: { Authorization: `Bearer ${authToken}` }, - }); - - return response.status; - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status !== 404) { - throw e; - } else { - return e.response.status; - } - } else { - throw e; - } - } - }, - { timeout: EVENT_POLLING_TIMEOUT }, - ) - .toBe(200); -}); test('Sends exception to Sentry', async ({ baseURL }) => { const errorEventPromise = waitForError('node-nestjs', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); - try { - await axios.get(`${baseURL}/test-exception/123`); - // Should never be reached! - expect(false).toBe(true); - } catch (error) { - expect(error).toBeInstanceOf(AxiosError); - expect(error.response?.status).toBe(500); - } + const response = await fetch(`${baseURL}/test-exception/123`); + expect(response.status).toBe(500); const errorEvent = await errorEventPromise; diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs/tests/propagation.test.ts index e0a75a0b3af0..b1a80710a4df 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs/tests/propagation.test.ts @@ -2,7 +2,6 @@ import crypto from 'crypto'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/event-proxy-server'; import { SpanJSON } from '@sentry/types'; -import axios from 'axios'; test('Propagates trace for outgoing http requests', async ({ baseURL }) => { const id = crypto.randomUUID(); @@ -21,7 +20,8 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-http/${id}`); + const response = await fetch(`${baseURL}/test-outgoing-http/${id}`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; const outboundTransaction = await outboundTransactionPromise; @@ -66,7 +66,7 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'http.method': 'GET', 'http.scheme': 'http', 'http.target': `/test-outgoing-http/${id}`, - 'http.user_agent': 'axios/1.6.7', + 'http.user_agent': 'node', 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -135,7 +135,8 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-fetch/${id}`); + const response = await fetch(`${baseURL}/test-outgoing-fetch/${id}`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; const outboundTransaction = await outboundTransactionPromise; @@ -180,7 +181,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'http.method': 'GET', 'http.scheme': 'http', 'http.target': `/test-outgoing-fetch/${id}`, - 'http.user_agent': 'axios/1.6.7', + 'http.user_agent': 'node', 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -240,7 +241,8 @@ test('Propagates trace for outgoing external http requests', async ({ baseURL }) ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-http-external-allowed`); + const response = await fetch(`${baseURL}/test-outgoing-http-external-allowed`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; @@ -276,7 +278,8 @@ test('Does not propagate outgoing http requests not covered by tracePropagationT ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-http-external-disallowed`); + const response = await fetch(`${baseURL}/test-outgoing-http-external-disallowed`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; @@ -299,7 +302,8 @@ test('Propagates trace for outgoing external fetch requests', async ({ baseURL } ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-fetch-external-allowed`); + const response = await fetch(`${baseURL}/test-outgoing-fetch-external-allowed`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; @@ -335,7 +339,8 @@ test('Does not propagate outgoing fetch requests not covered by tracePropagation ); }); - const { data } = await axios.get(`${baseURL}/test-outgoing-fetch-external-disallowed`); + const response = await fetch(`${baseURL}/test-outgoing-fetch-external-disallowed`); + const data = await response.json(); const inboundTransaction = await inboundTransactionPromise; diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs/tests/transactions.test.ts index 9268e777b502..5b48b1854afa 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs/tests/transactions.test.ts @@ -1,11 +1,5 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/event-proxy-server'; -import axios, { AxiosError } from 'axios'; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; -const EVENT_POLLING_TIMEOUT = 90_000; test('Sends an API route transaction', async ({ baseURL }) => { const pageloadTransactionEventPromise = waitForTransaction('node-nestjs', transactionEvent => { @@ -15,10 +9,9 @@ test('Sends an API route transaction', async ({ baseURL }) => { ); }); - await axios.get(`${baseURL}/test-transaction`); + await fetch(`${baseURL}/test-transaction`); const transactionEvent = await pageloadTransactionEventPromise; - const transactionEventId = transactionEvent.event_id; expect(transactionEvent.contexts?.trace).toEqual({ data: { @@ -35,7 +28,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'http.method': 'GET', 'http.scheme': 'http', 'http.target': '/test-transaction', - 'http.user_agent': 'axios/1.6.7', + 'http.user_agent': 'node', 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -131,32 +124,4 @@ test('Sends an API route transaction', async ({ baseURL }) => { }, }), ); - - await expect - .poll( - async () => { - try { - const response = await axios.get( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - return response.status; - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status !== 404) { - throw e; - } else { - return e.response.status; - } - } else { - throw e; - } - } - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); }); From cd97287adc3fb7243ef3b0e85019222b8c073c41 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 28 May 2024 09:33:13 -0400 Subject: [PATCH 07/14] fix(core): Pass in cron monitor config correctly (#12248) Fixes https://github.com/getsentry/sentry-javascript/issues/12217 Make sure `failure_issue_threshold` and `recovery_threshold` get passed in correctly. --- packages/core/src/server-runtime-client.ts | 2 ++ packages/core/test/lib/serverruntimeclient.test.ts | 4 ++++ packages/types/src/checkin.ts | 8 ++++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 7aa101b6d527..6d1a3683b05b 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -179,6 +179,8 @@ export class ServerRuntimeClient< checkin_margin: monitorConfig.checkinMargin, max_runtime: monitorConfig.maxRuntime, timezone: monitorConfig.timezone, + failure_issue_threshold: monitorConfig.failureIssueThreshold, + recovery_threshold: monitorConfig.recoveryThreshold, }; } diff --git a/packages/core/test/lib/serverruntimeclient.test.ts b/packages/core/test/lib/serverruntimeclient.test.ts index ee5d96863174..8b149f7f2aaa 100644 --- a/packages/core/test/lib/serverruntimeclient.test.ts +++ b/packages/core/test/lib/serverruntimeclient.test.ts @@ -89,6 +89,8 @@ describe('ServerRuntimeClient', () => { checkinMargin: 2, maxRuntime: 12333, timezone: 'Canada/Eastern', + failureIssueThreshold: 2, + recoveryThreshold: 3, }, ); @@ -112,6 +114,8 @@ describe('ServerRuntimeClient', () => { checkin_margin: 2, max_runtime: 12333, timezone: 'Canada/Eastern', + failure_issue_threshold: 2, + recovery_threshold: 3, }, }, ], diff --git a/packages/types/src/checkin.ts b/packages/types/src/checkin.ts index 9a1f9ee935e7..02811295870e 100644 --- a/packages/types/src/checkin.ts +++ b/packages/types/src/checkin.ts @@ -37,6 +37,10 @@ export interface SerializedCheckIn { // A tz database string representing the timezone which the monitor's execution schedule is in. // See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones timezone?: string; + // How many consecutive failed check-ins it takes to create an issue. + failure_issue_threshold?: number; + // How many consecutive OK check-ins it takes to resolve an issue. + recovery_threshold?: number; }; contexts?: { trace?: TraceContext; @@ -84,7 +88,7 @@ export interface MonitorConfig { // See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones timezone?: SerializedMonitorConfig['timezone']; // How many consecutive failed check-ins it takes to create an issue. - failure_issue_threshold?: number; + failureIssueThreshold?: SerializedMonitorConfig['failure_issue_threshold']; // How many consecutive OK check-ins it takes to resolve an issue. - recovery_threshold?: number; + recoveryThreshold?: SerializedMonitorConfig['recovery_threshold']; } From 09af745725bb603ce80dfa83e00f40804097b86b Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 28 May 2024 15:44:00 +0200 Subject: [PATCH 08/14] feat(sveltekit): Add request data to server-side events (#12254) This PR adds request data to server-side Sentry events by extracting the request object in our `sentryHandle` request handler. This is in line with our other SDKs and was simply missing from the SvelteKit SDK so far. --- .../test/errors.server.test.ts | 29 +++++++++++++++++++ .../test/performance.server.test.ts | 10 +++++++ .../sveltekit/test/errors.server.test.ts | 29 +++++++++++++++++++ .../sveltekit/test/performance.server.test.ts | 10 +++++++ packages/sveltekit/src/server/handle.ts | 12 ++++++-- packages/sveltekit/test/server/handle.test.ts | 2 ++ 6 files changed, 90 insertions(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/test/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/test/errors.server.test.ts index f169b0bf6b68..bbd87a5366a6 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/test/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/test/errors.server.test.ts @@ -20,6 +20,16 @@ test.describe('server-side errors', () => { ); expect(errorEvent.tags).toMatchObject({ runtime: 'node' }); + + expect(errorEvent.request).toEqual({ + cookies: {}, + headers: expect.objectContaining({ + accept: expect.any(String), + 'user-agent': expect.any(String), + }), + method: 'GET', + url: 'http://localhost:3030/universal-load-error', + }); }); test('captures server load error', async ({ page }) => { @@ -40,6 +50,16 @@ test.describe('server-side errors', () => { ); expect(errorEvent.tags).toMatchObject({ runtime: 'node' }); + + expect(errorEvent.request).toEqual({ + cookies: {}, + headers: expect.objectContaining({ + accept: expect.any(String), + 'user-agent': expect.any(String), + }), + method: 'GET', + url: 'http://localhost:3030/server-load-error', + }); }); test('captures server route (GET) error', async ({ page }) => { @@ -61,5 +81,14 @@ test.describe('server-side errors', () => { ); expect(errorEvent.transaction).toEqual('GET /server-route-error'); + + expect(errorEvent.request).toEqual({ + cookies: {}, + headers: expect.objectContaining({ + accept: expect.any(String), + }), + method: 'GET', + url: 'http://localhost:3030/server-route-error', + }); }); }); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/test/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/test/performance.server.test.ts index d43941f2a0c2..10d0690e70b8 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/test/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/test/performance.server.test.ts @@ -32,4 +32,14 @@ test('server pageload request span has nested request span for sub request', asy expect.objectContaining({ op: 'http.server', description: 'GET /api/users' }), ]), ); + + expect(serverTxnEvent.request).toEqual({ + cookies: {}, + headers: expect.objectContaining({ + accept: expect.any(String), + 'user-agent': expect.any(String), + }), + method: 'GET', + url: 'http://localhost:3030/server-load-fetch', + }); }); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts index d2215cf8e763..5a3940a213b5 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts @@ -21,6 +21,16 @@ test.describe('server-side errors', () => { ); expect(errorEvent.tags).toMatchObject({ runtime: 'node' }); + + expect(errorEvent.request).toEqual({ + cookies: {}, + headers: expect.objectContaining({ + accept: expect.any(String), + 'user-agent': expect.any(String), + }), + method: 'GET', + url: 'http://localhost:3030/universal-load-error', + }); }); test('captures server load error', async ({ page }) => { @@ -42,6 +52,16 @@ test.describe('server-side errors', () => { ); expect(errorEvent.tags).toMatchObject({ runtime: 'node' }); + + expect(errorEvent.request).toEqual({ + cookies: {}, + headers: expect.objectContaining({ + accept: expect.any(String), + 'user-agent': expect.any(String), + }), + method: 'GET', + url: 'http://localhost:3030/server-load-error', + }); }); test('captures server route (GET) error', async ({ page }) => { @@ -64,5 +84,14 @@ test.describe('server-side errors', () => { ); expect(errorEvent.transaction).toEqual('GET /server-route-error'); + + expect(errorEvent.request).toEqual({ + cookies: {}, + headers: expect.objectContaining({ + accept: expect.any(String), + }), + method: 'GET', + url: 'http://localhost:3030/server-route-error', + }); }); }); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.server.test.ts index a363d28f291b..e462d08ddeeb 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.server.test.ts @@ -32,4 +32,14 @@ test('server pageload request span has nested request span for sub request', asy expect.objectContaining({ op: 'http.server', description: 'GET /api/users' }), ]), ); + + expect(serverTxnEvent.request).toEqual({ + cookies: {}, + headers: expect.objectContaining({ + accept: expect.any(String), + 'user-agent': expect.any(String), + }), + method: 'GET', + url: 'http://localhost:3030/server-load-fetch', + }); }); diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index ee55ed810e8e..56ddc23e1885 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -2,6 +2,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveSpan, + getCurrentScope, getDefaultIsolationScope, getIsolationScope, getRootSpan, @@ -12,7 +13,12 @@ import { import { startSpan } from '@sentry/core'; import { captureException, continueTrace } from '@sentry/node'; import type { Span } from '@sentry/types'; -import { dynamicSamplingContextToSentryBaggageHeader, logger, objectify } from '@sentry/utils'; +import { + dynamicSamplingContextToSentryBaggageHeader, + logger, + objectify, + winterCGRequestToRequestData, +} from '@sentry/utils'; import type { Handle, ResolveOptions } from '@sveltejs/kit'; import { getDynamicSamplingContextFromSpan } from '@sentry/opentelemetry'; @@ -168,9 +174,10 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { return instrumentHandle(input, options); } - return withIsolationScope(() => { + return withIsolationScope(isolationScope => { // We only call continueTrace in the initial top level request to avoid // creating a new root span for the sub request. + isolationScope.setSDKProcessingMetadata({ request: winterCGRequestToRequestData(input.event.request.clone()) }); return continueTrace(getTracePropagationData(input.event), () => instrumentHandle(input, options)); }); }; @@ -206,6 +213,7 @@ async function instrumentHandle( name: routeName, }, async (span?: Span) => { + getCurrentScope().setSDKProcessingMetadata({ request: winterCGRequestToRequestData(event.request.clone()) }); const res = await resolve(event, { transformPageChunk: addSentryCodeToPage(options), }); diff --git a/packages/sveltekit/test/server/handle.test.ts b/packages/sveltekit/test/server/handle.test.ts index e029a118b411..e34f507af047 100644 --- a/packages/sveltekit/test/server/handle.test.ts +++ b/packages/sveltekit/test/server/handle.test.ts @@ -44,6 +44,8 @@ function mockEvent(override: Record = {}): Parameters[0 ...override, }; + event.request.clone = () => event.request; + return event; } From 253c61086a8e001236a2f69a529b33e4bf4a2c30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 May 2024 13:46:31 +0000 Subject: [PATCH 09/14] feat(deps): bump @opentelemetry/context-async-hooks from 1.23.0 to 1.24.1 (#12238) --- packages/node/package.json | 2 +- packages/opentelemetry/package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/node/package.json b/packages/node/package.json index 4c77f755d5df..faa1826a4575 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -71,7 +71,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.8.0", - "@opentelemetry/context-async-hooks": "^1.23.0", + "@opentelemetry/context-async-hooks": "^1.24.1", "@opentelemetry/core": "^1.24.1", "@opentelemetry/instrumentation": "^0.51.1", "@opentelemetry/instrumentation-connect": "0.36.1", diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index 2de7a61d2557..850f467c650a 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -55,7 +55,7 @@ }, "devDependencies": { "@opentelemetry/api": "^1.8.0", - "@opentelemetry/context-async-hooks": "^1.23.0", + "@opentelemetry/context-async-hooks": "^1.24.1", "@opentelemetry/core": "^1.24.1", "@opentelemetry/sdk-trace-base": "^1.23.0", "@opentelemetry/semantic-conventions": "^1.23.0" diff --git a/yarn.lock b/yarn.lock index 5187b6779fc1..f08e90c12f97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6251,10 +6251,10 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.8.0.tgz#5aa7abb48f23f693068ed2999ae627d2f7d902ec" integrity sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w== -"@opentelemetry/context-async-hooks@^1.23.0": - version "1.23.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.23.0.tgz#4c4627fe2857324459b0a78b5a83cbc64a415d14" - integrity sha512-wazGJZDRevibOJ+VgyrT+9+8sybZAxpZx2G7vy30OAtk92OpZCg7HgNxT11NUx0VBDWcRx1dOatMYGOVplQ7QA== +"@opentelemetry/context-async-hooks@^1.24.1": + version "1.24.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.24.1.tgz#1db7116d78f60e993e0d337bd497885a53deba1a" + integrity sha512-R5r6DO4kgEOVBxFXhXjwospLQkv+sYxwCfjvoZBe7Zm6KKXAV9kDSJhi/D1BweowdZmO+sdbENLs374gER8hpQ== "@opentelemetry/context-base@^0.12.0": version "0.12.0" From d63d7c673f4a837628ead6bbe2f70e5b1613ab65 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 28 May 2024 16:32:59 +0200 Subject: [PATCH 10/14] feat(metrics): Add `timings` method to metrics (#12226) This introduces a new method, `metrics.timing()`, which can be used in two ways: 1. With a numeric value, to simplify creating a distribution metric. This will default to `second` as unit: ```js Sentry.metrics.timing('myMetric', 100); ``` 2. With a callback, which will wrap the duration of the callback. This can accept a sync or async callback. It will create an inactive span around the callback and at the end emit a metric with the duration of the span in seconds: ```js const returnValue = Sentry.metrics.timing('myMetric', measureThisFunction); ``` Closes https://github.com/getsentry/sentry-javascript/issues/12215 --- .../suites/metrics/metricsEvent/init.js | 17 ++ .../suites/metrics/metricsEvent/test.ts | 17 +- .../suites/metrics/timing/init.js | 39 ++++ .../suites/metrics/timing/test.ts | 175 ++++++++++++++++++ .../rollup-utils/plugins/bundlePlugins.mjs | 3 + packages/browser/src/metrics.ts | 23 ++- packages/core/src/metrics/exports-default.ts | 30 ++- packages/core/src/metrics/exports.ts | 60 +++++- packages/core/src/metrics/metric-summary.ts | 21 +-- packages/core/test/lib/metrics/timing.test.ts | 124 +++++++++++++ packages/integration-shims/src/metrics.ts | 6 + packages/types/src/metrics.ts | 14 +- 12 files changed, 505 insertions(+), 24 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/metrics/timing/init.js create mode 100644 dev-packages/browser-integration-tests/suites/metrics/timing/test.ts create mode 100644 packages/core/test/lib/metrics/timing.test.ts diff --git a/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/init.js b/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/init.js index 97182a9af6e7..878444f52a0a 100644 --- a/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/init.js +++ b/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/init.js @@ -15,3 +15,20 @@ Sentry.metrics.gauge('gauge', 5); Sentry.metrics.gauge('gauge', '15'); Sentry.metrics.set('set', 'nope'); Sentry.metrics.set('set', 'another'); + +Sentry.metrics.timing('timing', 99, 'hour'); +Sentry.metrics.timing('timingSync', () => { + sleepSync(200); +}); +Sentry.metrics.timing('timingAsync', async () => { + await new Promise(resolve => setTimeout(resolve, 200)); +}); + +function sleepSync(milliseconds) { + var start = new Date().getTime(); + for (var i = 0; i < 1e7; i++) { + if (new Date().getTime() - start > milliseconds) { + break; + } + } +} diff --git a/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/test.ts b/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/test.ts index 05a6d238be93..38b0139edad3 100644 --- a/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/test.ts +++ b/dev-packages/browser-integration-tests/suites/metrics/metricsEvent/test.ts @@ -17,9 +17,18 @@ sentryTest('collects metrics', async ({ getLocalTestUrl, page }) => { const statsdBuffer = await getFirstSentryEnvelopeRequest(page, url, properEnvelopeRequestParser); const statsdString = new TextDecoder().decode(statsdBuffer); // Replace all the Txxxxxx to remove the timestamps - const normalisedStatsdString = statsdString.replace(/T\d+\n?/g, 'T000000'); + const normalisedStatsdString = statsdString.replace(/T\d+\n?/g, 'T000000').trim(); - expect(normalisedStatsdString).toEqual( - 'increment@none:6|c|T000000distribution@none:42:45|d|T000000gauge@none:15:5:15:20:2|g|T000000set@none:3387254:3443787523|s|T000000', - ); + const parts = normalisedStatsdString.split('T000000'); + + expect(parts).toEqual([ + 'increment@none:6|c|', + 'distribution@none:42:45|d|', + 'gauge@none:15:5:15:20:2|g|', + 'set@none:3387254:3443787523|s|', + 'timing@hour:99|d|', + expect.stringMatching(/timingSync@second:0.(\d+)\|d\|/), + expect.stringMatching(/timingAsync@second:0.(\d+)\|d\|/), + '', // trailing element + ]); }); diff --git a/dev-packages/browser-integration-tests/suites/metrics/timing/init.js b/dev-packages/browser-integration-tests/suites/metrics/timing/init.js new file mode 100644 index 000000000000..87f087b04ecf --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/metrics/timing/init.js @@ -0,0 +1,39 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, + release: '1.0.0', + autoSessionTracking: false, +}); + +window.timingSync = () => { + // Ensure we always have a wrapping span + return Sentry.startSpan({ name: 'manual span' }, () => { + return Sentry.metrics.timing('timingSync', () => { + sleepSync(200); + return 'sync done'; + }); + }); +}; + +window.timingAsync = () => { + // Ensure we always have a wrapping span + return Sentry.startSpan({ name: 'manual span' }, () => { + return Sentry.metrics.timing('timingAsync', async () => { + await new Promise(resolve => setTimeout(resolve, 200)); + return 'async done'; + }); + }); +}; + +function sleepSync(milliseconds) { + var start = new Date().getTime(); + for (var i = 0; i < 1e7; i++) { + if (new Date().getTime() - start > milliseconds) { + break; + } + } +} diff --git a/dev-packages/browser-integration-tests/suites/metrics/timing/test.ts b/dev-packages/browser-integration-tests/suites/metrics/timing/test.ts new file mode 100644 index 000000000000..c6b369025c7a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/metrics/timing/test.ts @@ -0,0 +1,175 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../utils/fixtures'; +import { + envelopeRequestParser, + properEnvelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequest, +} from '../../../utils/helpers'; + +sentryTest('allows to wrap sync methods with a timing metric', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const beforeTime = Math.floor(Date.now() / 1000); + + const metricsPromiseReq = page.waitForRequest(req => { + const postData = req.postData(); + if (!postData) { + return false; + } + + try { + // this implies this is a metrics envelope + return typeof envelopeRequestParser(req) === 'string'; + } catch { + return false; + } + }); + + const transactionPromise = waitForTransactionRequest(page); + + await page.goto(url); + await page.waitForFunction('typeof window.timingSync === "function"'); + const response = await page.evaluate('window.timingSync()'); + + expect(response).toBe('sync done'); + + const statsdString = envelopeRequestParser(await metricsPromiseReq); + const transactionEvent = properEnvelopeRequestParser(await transactionPromise); + + expect(typeof statsdString).toEqual('string'); + + const parsedStatsd = /timingSync@second:(0\.\d+)\|d\|#(.+)\|T(\d+)/.exec(statsdString); + + expect(parsedStatsd).toBeTruthy(); + + const duration = parseFloat(parsedStatsd![1]); + const tags = parsedStatsd![2]; + const timestamp = parseInt(parsedStatsd![3], 10); + + expect(timestamp).toBeGreaterThanOrEqual(beforeTime); + expect(tags).toEqual('release:1.0.0,transaction:manual span'); + expect(duration).toBeGreaterThan(0.2); + expect(duration).toBeLessThan(1); + + expect(transactionEvent).toBeDefined(); + expect(transactionEvent.transaction).toEqual('manual span'); + + const spans = transactionEvent.spans || []; + + expect(spans.length).toBe(1); + const span = spans[0]; + expect(span.op).toEqual('metrics.timing'); + expect(span.description).toEqual('timingSync'); + expect(span.timestamp! - span.start_timestamp).toEqual(duration); + expect(span._metrics_summary).toEqual({ + 'd:timingSync@second': [ + { + count: 1, + max: duration, + min: duration, + sum: duration, + tags: { + release: '1.0.0', + transaction: 'manual span', + }, + }, + ], + }); +}); + +sentryTest('allows to wrap async methods with a timing metric', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const beforeTime = Math.floor(Date.now() / 1000); + + const metricsPromiseReq = page.waitForRequest(req => { + const postData = req.postData(); + if (!postData) { + return false; + } + + try { + // this implies this is a metrics envelope + return typeof envelopeRequestParser(req) === 'string'; + } catch { + return false; + } + }); + + const transactionPromise = waitForTransactionRequest(page); + + await page.goto(url); + await page.waitForFunction('typeof window.timingAsync === "function"'); + const response = await page.evaluate('window.timingAsync()'); + + expect(response).toBe('async done'); + + const statsdString = envelopeRequestParser(await metricsPromiseReq); + const transactionEvent = properEnvelopeRequestParser(await transactionPromise); + + expect(typeof statsdString).toEqual('string'); + + const parsedStatsd = /timingAsync@second:(0\.\d+)\|d\|#(.+)\|T(\d+)/.exec(statsdString); + + expect(parsedStatsd).toBeTruthy(); + + const duration = parseFloat(parsedStatsd![1]); + const tags = parsedStatsd![2]; + const timestamp = parseInt(parsedStatsd![3], 10); + + expect(timestamp).toBeGreaterThanOrEqual(beforeTime); + expect(tags).toEqual('release:1.0.0,transaction:manual span'); + expect(duration).toBeGreaterThan(0.2); + expect(duration).toBeLessThan(1); + + expect(transactionEvent).toBeDefined(); + expect(transactionEvent.transaction).toEqual('manual span'); + + const spans = transactionEvent.spans || []; + + expect(spans.length).toBe(1); + const span = spans[0]; + expect(span.op).toEqual('metrics.timing'); + expect(span.description).toEqual('timingAsync'); + expect(span.timestamp! - span.start_timestamp).toEqual(duration); + expect(span._metrics_summary).toEqual({ + 'd:timingAsync@second': [ + { + count: 1, + max: duration, + min: duration, + sum: duration, + tags: { + release: '1.0.0', + transaction: 'manual span', + }, + }, + ], + }); +}); diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs index 169062694d24..760fdc05daa6 100644 --- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs +++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs @@ -121,7 +121,10 @@ export function makeTerserPlugin() { // These are used by instrument.ts in utils for identifying HTML elements & events '_sentryCaptured', '_sentryId', + // Keeps the frozen DSC on a Sentry Span '_frozenDsc', + // This keeps metrics summary on spans + '_metrics_summary', // These are used to keep span & scope relationships '_sentryRootSpan', '_sentryChildSpans', diff --git a/packages/browser/src/metrics.ts b/packages/browser/src/metrics.ts index 267fe90d03c9..39529af3e4a0 100644 --- a/packages/browser/src/metrics.ts +++ b/packages/browser/src/metrics.ts @@ -1,5 +1,5 @@ import { BrowserMetricsAggregator, metrics as metricsCore } from '@sentry/core'; -import type { MetricData, Metrics } from '@sentry/types'; +import type { DurationUnit, MetricData, Metrics } from '@sentry/types'; /** * Adds a value to a counter metric @@ -37,9 +37,30 @@ function gauge(name: string, value: number, data?: MetricData): void { metricsCore.gauge(BrowserMetricsAggregator, name, value, data); } +/** + * Adds a timing metric. + * The metric is added as a distribution metric. + * + * You can either directly capture a numeric `value`, or wrap a callback function in `timing`. + * In the latter case, the duration of the callback execution will be captured as a span & a metric. + * + * @experimental This API is experimental and might have breaking changes in the future. + */ +function timing(name: string, value: number, unit?: DurationUnit, data?: Omit): void; +function timing(name: string, callback: () => T, unit?: DurationUnit, data?: Omit): T; +function timing( + name: string, + value: number | (() => T), + unit: DurationUnit = 'second', + data?: Omit, +): T | void { + return metricsCore.timing(BrowserMetricsAggregator, name, value, unit, data); +} + export const metrics: Metrics = { increment, distribution, set, gauge, + timing, }; diff --git a/packages/core/src/metrics/exports-default.ts b/packages/core/src/metrics/exports-default.ts index 86d294d059d8..1e4a6487b9a2 100644 --- a/packages/core/src/metrics/exports-default.ts +++ b/packages/core/src/metrics/exports-default.ts @@ -1,4 +1,10 @@ -import type { Client, MetricData, Metrics, MetricsAggregator as MetricsAggregatorInterface } from '@sentry/types'; +import type { + Client, + DurationUnit, + MetricData, + Metrics, + MetricsAggregator as MetricsAggregatorInterface, +} from '@sentry/types'; import { MetricsAggregator } from './aggregator'; import { metrics as metricsCore } from './exports'; @@ -38,6 +44,26 @@ function gauge(name: string, value: number, data?: MetricData): void { metricsCore.gauge(MetricsAggregator, name, value, data); } +/** + * Adds a timing metric. + * The metric is added as a distribution metric. + * + * You can either directly capture a numeric `value`, or wrap a callback function in `timing`. + * In the latter case, the duration of the callback execution will be captured as a span & a metric. + * + * @experimental This API is experimental and might have breaking changes in the future. + */ +function timing(name: string, value: number, unit?: DurationUnit, data?: Omit): void; +function timing(name: string, callback: () => T, unit?: DurationUnit, data?: Omit): T; +function timing( + name: string, + value: number | (() => T), + unit: DurationUnit = 'second', + data?: Omit, +): T | void { + return metricsCore.timing(MetricsAggregator, name, value, unit, data); +} + /** * Returns the metrics aggregator for a given client. */ @@ -52,7 +78,7 @@ export const metricsDefault: Metrics & { distribution, set, gauge, - + timing, /** * @ignore This is for internal use only. */ diff --git a/packages/core/src/metrics/exports.ts b/packages/core/src/metrics/exports.ts index 665ac9c12816..752471856153 100644 --- a/packages/core/src/metrics/exports.ts +++ b/packages/core/src/metrics/exports.ts @@ -1,7 +1,9 @@ -import type { Client, MetricData, MetricsAggregator as MetricsAggregatorInterface } from '@sentry/types'; -import { getGlobalSingleton, logger } from '@sentry/utils'; +import type { Client, DurationUnit, MetricData, MetricsAggregator as MetricsAggregatorInterface } from '@sentry/types'; +import { getGlobalSingleton, logger, timestampInSeconds } from '@sentry/utils'; import { getClient } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; +import { startSpanManual } from '../tracing'; +import { handleCallbackErrors } from '../utils/handleCallbackErrors'; import { getActiveSpan, getRootSpan, spanToJSON } from '../utils/spanUtils'; import { COUNTER_METRIC_TYPE, DISTRIBUTION_METRIC_TYPE, GAUGE_METRIC_TYPE, SET_METRIC_TYPE } from './constants'; import type { MetricType } from './types'; @@ -52,6 +54,7 @@ function addToMetricsAggregator( const span = getActiveSpan(); const rootSpan = span ? getRootSpan(span) : undefined; + const transactionName = rootSpan && spanToJSON(rootSpan).description; const { unit, tags, timestamp } = data; const { release, environment } = client.getOptions(); @@ -62,8 +65,8 @@ function addToMetricsAggregator( if (environment) { metricTags.environment = environment; } - if (rootSpan) { - metricTags.transaction = spanToJSON(rootSpan).description || ''; + if (transactionName) { + metricTags.transaction = transactionName; } DEBUG_BUILD && logger.log(`Adding value of ${value} to ${metricType} metric ${name}`); @@ -90,6 +93,54 @@ function distribution(aggregator: MetricsAggregatorConstructor, name: string, va addToMetricsAggregator(aggregator, DISTRIBUTION_METRIC_TYPE, name, ensureNumber(value), data); } +/** + * Adds a timing metric. + * The metric is added as a distribution metric. + * + * You can either directly capture a numeric `value`, or wrap a callback function in `timing`. + * In the latter case, the duration of the callback execution will be captured as a span & a metric. + * + * @experimental This API is experimental and might have breaking changes in the future. + */ +function timing( + aggregator: MetricsAggregatorConstructor, + name: string, + value: number | (() => T), + unit: DurationUnit = 'second', + data?: Omit, +): T | void { + // callback form + if (typeof value === 'function') { + const startTime = timestampInSeconds(); + + return startSpanManual( + { + op: 'metrics.timing', + name, + startTime, + onlyIfParent: true, + }, + span => { + return handleCallbackErrors( + () => value(), + () => { + // no special error handling necessary + }, + () => { + const endTime = timestampInSeconds(); + const timeDiff = endTime - startTime; + distribution(aggregator, name, timeDiff, { ...data, unit: 'second' }); + span.end(endTime); + }, + ); + }, + ); + } + + // value form + distribution(aggregator, name, value, { ...data, unit }); +} + /** * Adds a value to a set metric. Value must be a string or integer. * @@ -113,6 +164,7 @@ export const metrics = { distribution, set, gauge, + timing, /** * @ignore This is for internal use only. */ diff --git a/packages/core/src/metrics/metric-summary.ts b/packages/core/src/metrics/metric-summary.ts index f1324def357d..c8672bed8bc6 100644 --- a/packages/core/src/metrics/metric-summary.ts +++ b/packages/core/src/metrics/metric-summary.ts @@ -10,17 +10,17 @@ import type { MetricType } from './types'; */ type MetricSummaryStorage = Map; -let SPAN_METRIC_SUMMARY: WeakMap | undefined; +const METRICS_SPAN_FIELD = '_sentryMetrics'; -function getMetricStorageForSpan(span: Span): MetricSummaryStorage | undefined { - return SPAN_METRIC_SUMMARY ? SPAN_METRIC_SUMMARY.get(span) : undefined; -} +type SpanWithPotentialMetrics = Span & { + [METRICS_SPAN_FIELD]?: MetricSummaryStorage; +}; /** * Fetches the metric summary if it exists for the passed span */ export function getMetricSummaryJsonForSpan(span: Span): Record> | undefined { - const storage = getMetricStorageForSpan(span); + const storage = (span as SpanWithPotentialMetrics)[METRICS_SPAN_FIELD]; if (!storage) { return undefined; @@ -50,7 +50,10 @@ export function updateMetricSummaryOnSpan( tags: Record, bucketKey: string, ): void { - const storage = getMetricStorageForSpan(span) || new Map(); + const existingStorage = (span as SpanWithPotentialMetrics)[METRICS_SPAN_FIELD]; + const storage = + existingStorage || + ((span as SpanWithPotentialMetrics)[METRICS_SPAN_FIELD] = new Map()); const exportKey = `${metricType}:${sanitizedName}@${unit}`; const bucketItem = storage.get(bucketKey); @@ -79,10 +82,4 @@ export function updateMetricSummaryOnSpan( }, ]); } - - if (!SPAN_METRIC_SUMMARY) { - SPAN_METRIC_SUMMARY = new WeakMap(); - } - - SPAN_METRIC_SUMMARY.set(span, storage); } diff --git a/packages/core/test/lib/metrics/timing.test.ts b/packages/core/test/lib/metrics/timing.test.ts new file mode 100644 index 000000000000..fa7c9f88da77 --- /dev/null +++ b/packages/core/test/lib/metrics/timing.test.ts @@ -0,0 +1,124 @@ +import { getCurrentScope, getIsolationScope, setCurrentClient } from '../../../src'; +import { MetricsAggregator } from '../../../src/metrics/aggregator'; +import { metrics as metricsCore } from '../../../src/metrics/exports'; +import { metricsDefault } from '../../../src/metrics/exports-default'; +import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; + +const PUBLIC_DSN = 'https://username@domain/123'; + +describe('metrics.timing', () => { + let testClient: TestClient; + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + tracesSampleRate: 0.0, + }); + + beforeEach(() => { + testClient = new TestClient(options); + setCurrentClient(testClient); + }); + + afterEach(() => { + getCurrentScope().setClient(undefined); + getCurrentScope().clear(); + getIsolationScope().clear(); + }); + + it('works with minimal data', async () => { + const res = metricsDefault.timing('t1', 10); + expect(res).toStrictEqual(undefined); + + const sendSpy = jest.spyOn(testClient.getTransport()!, 'send'); + + metricsCore.getMetricsAggregatorForClient(testClient, MetricsAggregator)!.flush(); + + expect(sendSpy).toHaveBeenCalledTimes(1); + expect(sendSpy).toHaveBeenCalledWith([ + { sent_at: expect.any(String) }, + [[{ length: expect.any(Number), type: 'statsd' }, expect.stringMatching(/t1@second:10\|d\|T(\d+)/)]], + ]); + }); + + it('allows to define a unit', async () => { + const res = metricsDefault.timing('t1', 10, 'hour'); + expect(res).toStrictEqual(undefined); + + const sendSpy = jest.spyOn(testClient.getTransport()!, 'send'); + + metricsCore.getMetricsAggregatorForClient(testClient, MetricsAggregator)!.flush(); + + expect(sendSpy).toHaveBeenCalledTimes(1); + expect(sendSpy).toHaveBeenCalledWith([ + { sent_at: expect.any(String) }, + [[{ length: expect.any(Number), type: 'statsd' }, expect.stringMatching(/t1@hour:10\|d\|T(\d+)/)]], + ]); + }); + + it('allows to define data', async () => { + const res = metricsDefault.timing('t1', 10, 'hour', { + tags: { tag1: 'value1', tag2: 'value2' }, + }); + expect(res).toStrictEqual(undefined); + + const sendSpy = jest.spyOn(testClient.getTransport()!, 'send'); + + metricsCore.getMetricsAggregatorForClient(testClient, MetricsAggregator)!.flush(); + + expect(sendSpy).toHaveBeenCalledTimes(1); + expect(sendSpy).toHaveBeenCalledWith([ + { sent_at: expect.any(String) }, + [ + [ + { length: expect.any(Number), type: 'statsd' }, + expect.stringMatching(/t1@hour:10\|d|#tag1:value1,tag2:value2\|T(\d+)/), + ], + ], + ]); + }); + + it('works with a sync callback', async () => { + const res = metricsDefault.timing('t1', () => { + sleepSync(200); + return 'oho'; + }); + expect(res).toStrictEqual('oho'); + + const sendSpy = jest.spyOn(testClient.getTransport()!, 'send'); + + metricsCore.getMetricsAggregatorForClient(testClient, MetricsAggregator)!.flush(); + + expect(sendSpy).toHaveBeenCalledTimes(1); + expect(sendSpy).toHaveBeenCalledWith([ + { sent_at: expect.any(String) }, + [[{ length: expect.any(Number), type: 'statsd' }, expect.stringMatching(/t1@second:(0.\d+)\|d\|T(\d+)/)]], + ]); + }); + + it('works with an async callback', async () => { + const res = metricsDefault.timing('t1', async () => { + await new Promise(resolve => setTimeout(resolve, 200)); + return 'oho'; + }); + expect(res).toBeInstanceOf(Promise); + expect(await res).toStrictEqual('oho'); + + const sendSpy = jest.spyOn(testClient.getTransport()!, 'send'); + + metricsCore.getMetricsAggregatorForClient(testClient, MetricsAggregator)!.flush(); + + expect(sendSpy).toHaveBeenCalledTimes(1); + expect(sendSpy).toHaveBeenCalledWith([ + { sent_at: expect.any(String) }, + [[{ length: expect.any(Number), type: 'statsd' }, expect.stringMatching(/t1@second:(0.\d+)\|d\|T(\d+)/)]], + ]); + }); +}); + +function sleepSync(milliseconds: number): void { + const start = Date.now(); + for (let i = 0; i < 1e7; i++) { + if (new Date().getTime() - start > milliseconds) { + break; + } + } +} diff --git a/packages/integration-shims/src/metrics.ts b/packages/integration-shims/src/metrics.ts index 9af425e0f629..3f2414742501 100644 --- a/packages/integration-shims/src/metrics.ts +++ b/packages/integration-shims/src/metrics.ts @@ -13,4 +13,10 @@ export const metricsShim: Metrics = { distribution: warn, set: warn, gauge: warn, + timing: (_name: unknown, value: number | (() => unknown)) => { + warn(); + if (typeof value === 'function') { + return value(); + } + }, }; diff --git a/packages/types/src/metrics.ts b/packages/types/src/metrics.ts index 843068db0aef..474f5b94c207 100644 --- a/packages/types/src/metrics.ts +++ b/packages/types/src/metrics.ts @@ -1,5 +1,5 @@ import type { Client } from './client'; -import type { MeasurementUnit } from './measurement'; +import type { DurationUnit, MeasurementUnit } from './measurement'; import type { Primitive } from './misc'; export interface MetricData { @@ -99,4 +99,16 @@ export interface Metrics { * @experimental This API is experimental and might have breaking changes in the future. */ gauge(name: string, value: number, data?: MetricData): void; + + /** + * Adds a timing metric. + * The metric is added as a distribution metric. + * + * You can either directly capture a numeric `value`, or wrap a callback function in `timing`. + * In the latter case, the duration of the callback execution will be captured as a span & a metric. + * + * @experimental This API is experimental and might have breaking changes in the future. + */ + timing(name: string, value: number, unit?: DurationUnit, data?: Omit): void; + timing(name: string, callback: () => T, unit?: DurationUnit, data?: Omit): T; } From fb1ed256f56a7a7bd66921839ad21c6e63510bc5 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 28 May 2024 16:46:03 +0200 Subject: [PATCH 11/14] fix(nextjs): Don't capture suspense errors in server components (#12261) --- .../nextjs-15/app/suspense-error/page.tsx | 15 ++++++++++++ .../nextjs-15/tests/suspense-error.test.ts | 24 +++++++++++++++++++ packages/nextjs/src/server/index.ts | 14 +++++++++-- 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/app/suspense-error/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/tests/suspense-error.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/suspense-error/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/suspense-error/page.tsx new file mode 100644 index 000000000000..007e2e2feac2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/suspense-error/page.tsx @@ -0,0 +1,15 @@ +import * as Sentry from '@sentry/nextjs'; +import { use } from 'react'; +export const dynamic = 'force-dynamic'; + +export default async function Page() { + try { + use(fetch('http://example.com/')); + } catch (e) { + Sentry.captureException(e); // This error should not be reported + await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for any async event processors to run + await Sentry.flush(); + } + + return

test

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/suspense-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/suspense-error.test.ts new file mode 100644 index 000000000000..ddbc4a9edee3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/suspense-error.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; + +test('should not capture serverside suspense errors', async ({ page }) => { + const pageServerComponentTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { + return transactionEvent?.transaction === 'Page Server Component (/suspense-error)'; + }); + + let errorEvent; + waitForError('nextjs-15', async transactionEvent => { + return transactionEvent?.transaction === 'Page Server Component (/suspense-error)'; + }).then(event => { + errorEvent = event; + }); + + await page.goto(`/suspense-error`); + + await page.waitForTimeout(5000); + + const pageServerComponentTransaction = await pageServerComponentTransactionPromise; + expect(pageServerComponentTransaction).toBeDefined(); + + expect(errorEvent).toBeUndefined(); +}); diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 2b5d7251186a..9ffdcfdc6225 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -190,13 +190,23 @@ export function init(options: NodeOptions): void { const originalException = hint.originalException; - const isReactControlFlowError = + const isPostponeError = typeof originalException === 'object' && originalException !== null && '$$typeof' in originalException && originalException.$$typeof === Symbol.for('react.postpone'); - if (isReactControlFlowError) { + if (isPostponeError) { + // Postpone errors are used for partial-pre-rendering (PPR) + return null; + } + + // We don't want to capture suspense errors as they are simply used by React/Next.js for control flow + const exceptionMessage = event.exception?.values?.[0]?.value; + if ( + exceptionMessage?.includes('Suspense Exception: This is not a real error!') || + exceptionMessage?.includes('Suspense Exception: This is not a real error, and should not leak') + ) { return null; } From 005f40df3e9e2715b9f634d4490d1e42760be61d Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 28 May 2024 16:52:51 +0200 Subject: [PATCH 12/14] ref(core): Use versioned carrier on global object (#12206) This PR implements a versioned Sentry carrier as described in #12188. The idea is that SDKs can from now on access their global Sentry instance and thereby no longer overwrite or interfere with potentially other SDKs (e.g. 3rd party libraries, scripts, etc). Internally, SDKs can access their carrier via the `window.__SENTRY__[SDK_VERSION]`. Externally (spotlight, loader script) via `window.__SENTRY__[window.__SENTRY__.version]`. --------- Co-authored-by: Francesco Novy --- .../fixtures/loader.js | 140 +++++++++++++++++- .../loader/onLoad/customInit/init.js | 6 +- .../acs/getCurrentScope/init.js | 19 +++ .../acs/getCurrentScope/subject.js | 10 ++ .../acs/getCurrentScope/template.html | 9 ++ .../acs/getCurrentScope/test.ts | 13 ++ .../old-sdk-interop/hub/isOlderThan/init.js | 19 +++ .../hub/isOlderThan/subject.js | 10 ++ .../hub/isOlderThan/template.html | 9 ++ .../old-sdk-interop/hub/isOlderThan/test.ts | 13 ++ packages/core/package.json | 1 - .../core/src/asyncContext/stackStrategy.ts | 20 +-- packages/core/src/carrier.ts | 47 +++--- packages/core/src/index.ts | 3 +- packages/core/src/integration.ts | 2 +- packages/core/src/sdk.ts | 17 --- packages/core/src/utils/prepareEvent.ts | 2 +- packages/core/src/utils/sdkMetadata.ts | 2 +- packages/core/test/lib/carrier.test.ts | 91 ++++++++++++ packages/core/test/lib/clear-global-scope.ts | 5 +- packages/utils/package.json | 1 + packages/utils/src/index.ts | 1 + packages/{core => utils}/src/version.ts | 0 packages/utils/src/worldwide.ts | 61 +++++--- 24 files changed, 411 insertions(+), 90 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/init.js create mode 100644 dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/template.html create mode 100644 dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/init.js create mode 100644 dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/template.html create mode 100644 dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/test.ts create mode 100644 packages/core/test/lib/carrier.test.ts rename packages/{core => utils}/src/version.ts (100%) diff --git a/dev-packages/browser-integration-tests/fixtures/loader.js b/dev-packages/browser-integration-tests/fixtures/loader.js index 2f4705810b2f..17a9a1d652a8 100644 --- a/dev-packages/browser-integration-tests/fixtures/loader.js +++ b/dev-packages/browser-integration-tests/fixtures/loader.js @@ -1,5 +1,139 @@ -!function(n,e,r,t,i,o,a,c,s){for(var u=s,f=0;f-1){u&&"no"===document.scripts[f].getAttribute("data-lazy")&&(u=!1);break}var p=[];function l(n){return"e"in n}function d(n){return"p"in n}function _(n){return"f"in n}var v=[];function y(n){u&&(l(n)||d(n)||_(n)&&n.f.indexOf("capture")>-1||_(n)&&n.f.indexOf("showReportDialog")>-1)&&m(),v.push(n)}function g(){y({e:[].slice.call(arguments)})}function h(n){y({p:n})}function E(){try{n.SENTRY_SDK_SOURCE="loader";var e=n[i],o=e.init;e.init=function(i){n.removeEventListener(r,g),n.removeEventListener(t,h);var a=c;for(var s in i)Object.prototype.hasOwnProperty.call(i,s)&&(a[s]=i[s]);!function(n,e){var r=n.integrations||[];if(!Array.isArray(r))return;var t=r.map((function(n){return n.name}));n.tracesSampleRate&&-1===t.indexOf("BrowserTracing")&&(e.BrowserTracing?r.push(new e.BrowserTracing):e.browserTracingIntegration&&r.push(e.browserTracingIntegration()));(n.replaysSessionSampleRate||n.replaysOnErrorSampleRate)&&-1===t.indexOf("Replay")&&(e.Replay?r.push(new e.Replay):e.replayIntegration&&r.push(e.replayIntegration()));n.integrations=r}(a,e),o(a)},setTimeout((function(){return function(e){try{"function"==typeof n.sentryOnLoad&&(n.sentryOnLoad(),n.sentryOnLoad=void 0);for(var r=0;r -1) { + u && 'no' === document.scripts[f].getAttribute('data-lazy') && (u = !1); + break; + } + var p = []; + function l(n) { + return 'e' in n; + } + function d(n) { + return 'p' in n; + } + function _(n) { + return 'f' in n; + } + var v = []; + function y(n) { + u && + (l(n) || d(n) || (_(n) && n.f.indexOf('capture') > -1) || (_(n) && n.f.indexOf('showReportDialog') > -1)) && + m(), + v.push(n); + } + function g() { + y({ e: [].slice.call(arguments) }); + } + function h(n) { + y({ p: n }); + } + function E() { + try { + n.SENTRY_SDK_SOURCE = 'loader'; + var e = n[i], + o = e.init; + (e.init = function (i) { + n.removeEventListener(r, g), n.removeEventListener(t, h); + var a = c; + for (var s in i) Object.prototype.hasOwnProperty.call(i, s) && (a[s] = i[s]); + !(function (n, e) { + var r = n.integrations || []; + if (!Array.isArray(r)) return; + var t = r.map(function (n) { + return n.name; + }); + n.tracesSampleRate && + -1 === t.indexOf('BrowserTracing') && + (e.BrowserTracing + ? r.push(new e.BrowserTracing()) + : e.browserTracingIntegration && r.push(e.browserTracingIntegration())); + (n.replaysSessionSampleRate || n.replaysOnErrorSampleRate) && + -1 === t.indexOf('Replay') && + (e.Replay ? r.push(new e.Replay()) : e.replayIntegration && r.push(e.replayIntegration())); + n.integrations = r; + })(a, e), + o(a); + }), + setTimeout(function () { + return (function (e) { + try { + 'function' == typeof n.sentryOnLoad && (n.sentryOnLoad(), (n.sentryOnLoad = void 0)); + for (var r = 0; r < p.length; r++) 'function' == typeof p[r] && p[r](); + p.splice(0); + for (r = 0; r < v.length; r++) { + _((o = v[r])) && 'init' === o.f && e.init.apply(e, o.a); + } + L() || e.init(); + var t = n.onerror, + i = n.onunhandledrejection; + for (r = 0; r < v.length; r++) { + var o; + if (_((o = v[r]))) { + if ('init' === o.f) continue; + e[o.f].apply(e, o.a); + } else l(o) && t ? t.apply(n, o.e) : d(o) && i && i.apply(n, [o.p]); + } + } catch (n) { + console.error(n); + } + })(e); + }); + } catch (n) { + console.error(n); + } + } + var O = !1; + function m() { + if (!O) { + O = !0; + var n = e.scripts[0], + r = e.createElement('script'); + (r.src = a), + (r.crossOrigin = 'anonymous'), + r.addEventListener('load', E, { once: !0, passive: !0 }), + n.parentNode.insertBefore(r, n); + } + } + function L() { + var e = n.__SENTRY__; + + // TODO: This is a temporary hack to make the loader script compatible with the versioned + // carrier. This needs still needs to be added to the actual loader script before we + // release the loader for v8! + var v = e && e.version && e[e.version]; + + return !(void 0 === e || !e.hub || !e.hub.getClient()) || !!v; + } + (n[i] = n[i] || {}), + (n[i].onLoad = function (n) { + L() ? n() : p.push(n); + }), + (n[i].forceLoad = function () { + setTimeout(function () { + m(); + }); + }), + [ + 'init', + 'addBreadcrumb', + 'captureMessage', + 'captureException', + 'captureEvent', + 'configureScope', + 'withScope', + 'showReportDialog', + ].forEach(function (e) { + n[i][e] = function () { + y({ f: e, a: arguments }); + }; + }), + n.addEventListener(r, g), + n.addEventListener(t, h), + u || + setTimeout(function () { + m(); + }); +})( window, document, 'error', @@ -8,5 +142,5 @@ 'loader.js', __LOADER_BUNDLE__, __LOADER_OPTIONS__, - __LOADER_LAZY__ + __LOADER_LAZY__, ); diff --git a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customInit/init.js b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customInit/init.js index 4a9e000fd1c2..0ed922379b56 100644 --- a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customInit/init.js +++ b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customInit/init.js @@ -16,5 +16,9 @@ window.sentryIsLoaded = () => { const __sentry = window.__SENTRY__; // If there is a global __SENTRY__ that means that in any of the callbacks init() was already invoked - return !!(!(typeof __sentry === 'undefined') && __sentry.hub && __sentry.hub.getClient()); + return !!( + !(typeof __sentry === 'undefined') && + __sentry.version && + !!__sentry[__sentry.version] + ); }; diff --git a/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/init.js b/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/init.js new file mode 100644 index 000000000000..c21a085e735e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/init.js @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/browser'; + +/** + * This simulates an relatively new v7 SDK setting acs on the __SENTRY__ carrier. + * see: https://github.com/getsentry/sentry-javascript/issues/12054 + */ +window.__SENTRY__ = { + acs: { + getCurrentScope: () => { + return 'scope'; + }, + }, +}; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', +}); diff --git a/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/subject.js b/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/subject.js new file mode 100644 index 000000000000..6b195f6d2b20 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/subject.js @@ -0,0 +1,10 @@ +const sentryCarrier = window && window.__SENTRY__; + +/** + * Simulate an old pre v8 SDK obtaining the hub from the global sentry carrier + * and checking for the hub version. + */ +const res = sentryCarrier.acs && sentryCarrier.acs.getCurrentScope(); + +// Write back result into the document +document.getElementById('currentScope').innerText = res && 'scope'; diff --git a/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/template.html b/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/template.html new file mode 100644 index 000000000000..b0a083eab503 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/template.html @@ -0,0 +1,9 @@ + + + + + + +

+ + diff --git a/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/test.ts b/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/test.ts new file mode 100644 index 000000000000..18e26c659a3b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/test.ts @@ -0,0 +1,13 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; + +sentryTest( + "doesn't crash if older SDKs access `acs.getCurrentScope` on the global object", + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + await expect(page.locator('#currentScope')).toHaveText('scope'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/init.js b/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/init.js new file mode 100644 index 000000000000..0bb1961ce1d9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/init.js @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/browser'; + +/** + * This simulates an old, pre-v8 SDK setting itself up on the global __SENTRY__ carrier. + * see: https://github.com/getsentry/sentry-javascript/issues/12155 + */ +window.__SENTRY__ = { + hub: { + isOlderThan: version => { + return version < 7; + }, + }, +}; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', +}); diff --git a/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/subject.js b/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/subject.js new file mode 100644 index 000000000000..3de7e795e416 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/subject.js @@ -0,0 +1,10 @@ +const sentryCarrier = window && window.__SENTRY__; + +/** + * Simulate an old pre v8 SDK obtaining the hub from the global sentry carrier + * and checking for the hub version. + */ +const res = sentryCarrier.hub && sentryCarrier.hub.isOlderThan(7); + +// Write back result into the document +document.getElementById('olderThan').innerText = res; diff --git a/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/template.html b/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/template.html new file mode 100644 index 000000000000..a363fad46013 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/template.html @@ -0,0 +1,9 @@ + + + + + + +

+ + diff --git a/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/test.ts b/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/test.ts new file mode 100644 index 000000000000..5cdc3e833f8a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/test.ts @@ -0,0 +1,13 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; + +sentryTest( + "doesn't crash if older SDKs access `hub.isOlderThan` on the global object", + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + await expect(page.locator('#olderThan')).toHaveText('false'); + }, +); diff --git a/packages/core/package.json b/packages/core/package.json index 4f6088735125..b4958d4db216 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -63,7 +63,6 @@ "lint": "eslint . --format stylish", "test": "jest", "test:watch": "jest --watch", - "version": "node ../../scripts/versionbump.js src/version.ts", "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { diff --git a/packages/core/src/asyncContext/stackStrategy.ts b/packages/core/src/asyncContext/stackStrategy.ts index aa075b644e85..7da6960e7801 100644 --- a/packages/core/src/asyncContext/stackStrategy.ts +++ b/packages/core/src/asyncContext/stackStrategy.ts @@ -132,19 +132,9 @@ export class AsyncContextStack { */ function getAsyncContextStack(): AsyncContextStack { const registry = getMainCarrier(); + const sentry = getSentryCarrier(registry); - // For now we continue to keep this as `hub` on the ACS, - // as e.g. the Loader Script relies on this. - // Eventually we may change this if/when we update the loader to not require this field anymore - // Related, we also write to `hub` in {@link ./../sdk.ts registerClientOnGlobalHub} - const sentry = getSentryCarrier(registry) as { hub?: AsyncContextStack }; - - if (sentry.hub) { - return sentry.hub; - } - - sentry.hub = new AsyncContextStack(getDefaultCurrentScope(), getDefaultIsolationScope()); - return sentry.hub; + return (sentry.stack = sentry.stack || new AsyncContextStack(getDefaultCurrentScope(), getDefaultIsolationScope())); } function withScope(callback: (scope: ScopeInterface) => T): T { @@ -152,9 +142,9 @@ function withScope(callback: (scope: ScopeInterface) => T): T { } function withSetScope(scope: ScopeInterface, callback: (scope: ScopeInterface) => T): T { - const hub = getAsyncContextStack() as AsyncContextStack; - return hub.withScope(() => { - hub.getStackTop().scope = scope; + const stack = getAsyncContextStack() as AsyncContextStack; + return stack.withScope(() => { + stack.getStackTop().scope = scope; return callback(scope); }); } diff --git a/packages/core/src/carrier.ts b/packages/core/src/carrier.ts index 215ed9673148..7442244223ca 100644 --- a/packages/core/src/carrier.ts +++ b/packages/core/src/carrier.ts @@ -1,35 +1,32 @@ -import type { Integration } from '@sentry/types'; -import { GLOBAL_OBJ } from '@sentry/utils'; +import type { Client, Integration, MetricsAggregator, Scope } from '@sentry/types'; +import { GLOBAL_OBJ, SDK_VERSION } from '@sentry/utils'; +import type { AsyncContextStack } from './asyncContext/stackStrategy'; import type { AsyncContextStrategy } from './asyncContext/types'; /** - * An object that contains a hub and maintains a scope stack. + * An object that contains globally accessible properties and maintains a scope stack. * @hidden */ export interface Carrier { - __SENTRY__?: SentryCarrier; + __SENTRY__?: VersionedCarrier; } +type VersionedCarrier = { + version?: string; +} & Record, SentryCarrier>; + interface SentryCarrier { acs?: AsyncContextStrategy; -} + stack?: AsyncContextStack; -/** - * An object that contains a hub and maintains a scope stack. - * @hidden - */ -export interface Carrier { - __SENTRY__?: SentryCarrier; -} + globalScope?: Scope; + defaultIsolationScope?: Scope; + defaultCurrentScope?: Scope; + globalMetricsAggregators?: WeakMap | undefined; -interface SentryCarrier { - acs?: AsyncContextStrategy; - /** - * Extra Hub properties injected by various SDKs - */ + // TODO(v9): Remove these properties - they are no longer used and were left over in v8 integrations?: Integration[]; extensions?: { - /** Extension methods for the hub, which are bound to the current Hub instance */ // eslint-disable-next-line @typescript-eslint/ban-types [key: string]: Function; }; @@ -50,10 +47,12 @@ export function getMainCarrier(): Carrier { /** Will either get the existing sentry carrier, or create a new one. */ export function getSentryCarrier(carrier: Carrier): SentryCarrier { - if (!carrier.__SENTRY__) { - carrier.__SENTRY__ = { - extensions: {}, - }; - } - return carrier.__SENTRY__; + const __SENTRY__ = (carrier.__SENTRY__ = carrier.__SENTRY__ || {}); + + // For now: First SDK that sets the .version property wins + __SENTRY__.version = __SENTRY__.version || SDK_VERSION; + + // Intentionally populating and returning the version of "this" SDK instance + // rather than what's set in .version so that "this" SDK always gets its carrier + return (__SENTRY__[SDK_VERSION] = __SENTRY__[SDK_VERSION] || {}); } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 03dfa8e63aa3..7ab3c4008dd1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -56,7 +56,6 @@ export { initAndBind, setCurrentClient } from './sdk'; export { createTransport } from './transports/base'; export { makeOfflineTransport } from './transports/offline'; export { makeMultiplexedTransport } from './transports/multiplexed'; -export { SDK_VERSION } from './version'; export { getIntegrationsToSetup, addIntegration, @@ -107,3 +106,5 @@ export { captureFeedback } from './feedback'; // eslint-disable-next-line deprecation/deprecation export { getCurrentHubShim, getCurrentHub } from './getCurrentHubShim'; + +export { SDK_VERSION } from '@sentry/utils'; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index c5f9499f342e..f39e09dae3ff 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -146,7 +146,7 @@ export function setupIntegration(client: Client, integration: Integration, integ DEBUG_BUILD && logger.log(`Integration installed: ${integration.name}`); } -/** Add an integration to the current hub's client. */ +/** Add an integration to the current scope's client. */ export function addIntegration(integration: Integration): void { const client = getClient(); diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index 70bf19779c9b..d4c974242e1b 100644 --- a/packages/core/src/sdk.ts +++ b/packages/core/src/sdk.ts @@ -2,8 +2,6 @@ import type { Client, ClientOptions } from '@sentry/types'; import { consoleSandbox, logger } from '@sentry/utils'; import { getCurrentScope } from './currentScopes'; -import type { AsyncContextStack } from './asyncContext/stackStrategy'; -import { getMainCarrier, getSentryCarrier } from './carrier'; import { DEBUG_BUILD } from './debug-build'; /** A class object that can instantiate Client objects. */ @@ -44,19 +42,4 @@ export function initAndBind( */ export function setCurrentClient(client: Client): void { getCurrentScope().setClient(client); - registerClientOnGlobalHub(client); -} - -/** - * Unfortunately, we still have to manually bind the client to the "hub" property set on the global - * Sentry carrier object. This is because certain scripts (e.g. our loader script) obtain - * the client via `window.__SENTRY__.hub.getClient()`. - * - * @see {@link ./asyncContext/stackStrategy.ts getAsyncContextStack} - */ -function registerClientOnGlobalHub(client: Client): void { - const sentryGlobal = getSentryCarrier(getMainCarrier()) as { hub?: AsyncContextStack }; - if (sentryGlobal.hub && typeof sentryGlobal.hub.getStackTop === 'function') { - sentryGlobal.hub.getStackTop().client = client; - } } diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 6bc21cadbffa..c60c6d21331b 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -76,7 +76,7 @@ export function prepareEvent( const clientEventProcessors = client ? client.getEventProcessors() : []; // This should be the last thing called, since we want that - // {@link Hub.addEventProcessor} gets the finished prepared event. + // {@link Scope.addEventProcessor} gets the finished prepared event. // Merge scope data together const data = getGlobalScope().getScopeData(); diff --git a/packages/core/src/utils/sdkMetadata.ts b/packages/core/src/utils/sdkMetadata.ts index e865861f4f55..9ad46bc5b375 100644 --- a/packages/core/src/utils/sdkMetadata.ts +++ b/packages/core/src/utils/sdkMetadata.ts @@ -1,5 +1,5 @@ import type { Options } from '@sentry/types'; -import { SDK_VERSION } from '../version'; +import { SDK_VERSION } from '@sentry/utils'; /** * A builder for the SDK metadata in the options for the SDK initialization. diff --git a/packages/core/test/lib/carrier.test.ts b/packages/core/test/lib/carrier.test.ts new file mode 100644 index 000000000000..3c94c96b98c1 --- /dev/null +++ b/packages/core/test/lib/carrier.test.ts @@ -0,0 +1,91 @@ +import { SDK_VERSION } from '@sentry/utils'; +import { getSentryCarrier } from '../../src/carrier'; + +describe('getSentryCarrier', () => { + describe('base case (one SDK)', () => { + it('populates the default sentry carrier object if it does not exist', () => { + const globalObject = {}; + const sentryCarrier = getSentryCarrier(globalObject); + + expect(sentryCarrier).toEqual({}); + + expect(globalObject).toEqual({ + __SENTRY__: { + version: SDK_VERSION, + [SDK_VERSION]: {}, + }, + }); + }); + + it('returns the existing sentry carrier object if it already exists', () => { + const originalGlobalObject = { + __SENTRY__: { + version: SDK_VERSION, + [SDK_VERSION]: { + acs: {}, + }, + }, + }; + + const globalObject = { ...originalGlobalObject }; + // @ts-expect-error - this is just a test object, not passing a full ACS + const sentryCarrier = getSentryCarrier(globalObject); + + expect(sentryCarrier).toEqual({ + acs: {}, + }); + + expect(globalObject).toStrictEqual(originalGlobalObject); + }); + }); + + describe('multiple (older) SDKs', () => { + it("returns the version of the sentry carrier object of the SDK's version rather than the one set in .version", () => { + const sentryCarrier = getSentryCarrier({ + __SENTRY__: { + version: '8.0.0' as const, // another SDK set this + '8.0.0': { + // @ts-expect-error - this is just a test object, not passing a full stack + stack: {}, + }, + [SDK_VERSION]: { + // @ts-expect-error - this is just a test object, not passing a full ACS + acs: {}, + }, + hub: {}, + }, + }); + + expect(sentryCarrier).toEqual({ + acs: {}, + }); + }); + + it("doesn't overwrite the .version property if it's already set and creates a new global sentry carrier for the SDK version if not set yet", () => { + const globalObject = { + __SENTRY__: { + version: '8.0.0' as const, + '8.0.0': { + // and this object + acs: {}, + }, + }, + }; + + // @ts-expect-error - this is just a test object, no need to pass a hub + const sentryCarrier = getSentryCarrier(globalObject); + + expect(sentryCarrier).toEqual({}); + + expect(globalObject).toEqual({ + __SENTRY__: { + version: '8.0.0', + '8.0.0': { + acs: {}, + }, + [SDK_VERSION]: {}, + }, + }); + }); + }); +}); diff --git a/packages/core/test/lib/clear-global-scope.ts b/packages/core/test/lib/clear-global-scope.ts index 5290a610e961..0ebe6884429a 100644 --- a/packages/core/test/lib/clear-global-scope.ts +++ b/packages/core/test/lib/clear-global-scope.ts @@ -1,6 +1,7 @@ import { GLOBAL_OBJ } from '@sentry/utils'; +import { getSentryCarrier } from '../../src/carrier'; export function clearGlobalScope() { - const __SENTRY__ = (GLOBAL_OBJ.__SENTRY__ = GLOBAL_OBJ.__SENTRY__ || {}); - __SENTRY__.globalScope = undefined; + const carrier = getSentryCarrier(GLOBAL_OBJ); + carrier.globalScope = undefined; } diff --git a/packages/utils/package.json b/packages/utils/package.json index 3c4186b1ccc9..31b93ea62719 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -67,6 +67,7 @@ "test": "jest", "test:watch": "jest --watch", "test:package": "node test/types/index.js", + "version": "node ../../scripts/versionbump.js src/version.ts", "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 2fb6f420ab58..433e29a68d79 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -36,3 +36,4 @@ export * from './anr'; export * from './lru'; export * from './buildPolyfills'; export * from './propagationContext'; +export * from './version'; diff --git a/packages/core/src/version.ts b/packages/utils/src/version.ts similarity index 100% rename from packages/core/src/version.ts rename to packages/utils/src/version.ts diff --git a/packages/utils/src/worldwide.ts b/packages/utils/src/worldwide.ts index cc5241f00e0f..0263fc5ec719 100644 --- a/packages/utils/src/worldwide.ts +++ b/packages/utils/src/worldwide.ts @@ -15,9 +15,38 @@ import type { Client, MetricsAggregator, Scope } from '@sentry/types'; import type { SdkSource } from './env'; +import { SDK_VERSION } from './version'; + +interface SentryCarrier { + acs?: any; + stack?: any; + + globalScope?: Scope; + defaultIsolationScope?: Scope; + defaultCurrentScope?: Scope; + globalMetricsAggregators: WeakMap | undefined; + + /** Overwrites TextEncoder used in `@sentry/utils`, need for `react-native@0.73` and older */ + encodePolyfill?: (input: string) => Uint8Array; + /** Overwrites TextDecoder used in `@sentry/utils`, need for `react-native@0.73` and older */ + decodePolyfill?: (input: Uint8Array) => string; +} + +// TODO(v9): Clean up or remove this type +type BackwardsCompatibleSentryCarrier = SentryCarrier & { + // pre-v7 hub (replaced by .stack) + hub: any; + integrations?: any[]; + logger: any; + extensions?: { + /** Extension methods for the hub, which are bound to the current Hub instance */ + // eslint-disable-next-line @typescript-eslint/ban-types + [key: string]: Function; + }; +}; /** Internal global with common properties and Sentry extensions */ -export interface InternalGlobal { +export type InternalGlobal = { navigator?: { userAgent?: string }; console: Console; Sentry?: any; @@ -43,23 +72,9 @@ export interface InternalGlobal { * file. */ _sentryDebugIds?: Record; - __SENTRY__: { - hub: any; - logger: any; - extensions?: { - /** Extension methods for the hub, which are bound to the current Hub instance */ - // eslint-disable-next-line @typescript-eslint/ban-types - [key: string]: Function; - }; - globalScope: Scope | undefined; - defaultCurrentScope: Scope | undefined; - defaultIsolationScope: Scope | undefined; - globalMetricsAggregators: WeakMap | undefined; - /** Overwrites TextEncoder used in `@sentry/utils`, need for `react-native@0.73` and older */ - encodePolyfill?: (input: string) => Uint8Array; - /** Overwrites TextDecoder used in `@sentry/utils`, need for `react-native@0.73` and older */ - decodePolyfill?: (input: Uint8Array) => string; - }; + __SENTRY__: Record, SentryCarrier> & { + version?: string; + } & BackwardsCompatibleSentryCarrier; /** * Raw module metadata that is injected by bundler plugins. * @@ -67,13 +82,13 @@ export interface InternalGlobal { */ _sentryModuleMetadata?: Record; _sentryEsmLoaderHookRegistered?: boolean; -} +}; /** Get's the global object for the current JavaScript runtime */ export const GLOBAL_OBJ = globalThis as unknown as InternalGlobal; /** - * Returns a global singleton contained in the global `__SENTRY__` object. + * Returns a global singleton contained in the global `__SENTRY__[]` object. * * If the singleton doesn't already exist in `__SENTRY__`, it will be created using the given factory * function and added to the `__SENTRY__` object. @@ -83,9 +98,9 @@ export const GLOBAL_OBJ = globalThis as unknown as InternalGlobal; * @param obj (Optional) The global object on which to look for `__SENTRY__`, if not `GLOBAL_OBJ`'s return value * @returns the singleton */ -export function getGlobalSingleton(name: keyof InternalGlobal['__SENTRY__'], creator: () => T, obj?: unknown): T { +export function getGlobalSingleton(name: keyof SentryCarrier, creator: () => T, obj?: unknown): T { const gbl = (obj || GLOBAL_OBJ) as InternalGlobal; const __SENTRY__ = (gbl.__SENTRY__ = gbl.__SENTRY__ || {}); - const singleton = __SENTRY__[name] || (__SENTRY__[name] = creator()); - return singleton; + const versionedCarrier = (__SENTRY__[SDK_VERSION] = __SENTRY__[SDK_VERSION] || {}); + return versionedCarrier[name] || (versionedCarrier[name] = creator()); } From 82f44327e435430201a43dda1d7ae8434e3261f9 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 29 May 2024 09:42:06 +0200 Subject: [PATCH 13/14] test(replay): Remove outdated test constraint (#12270) Noticed this while looking into https://github.com/getsentry/sentry-javascript/issues/12244, console should be instrumented just fine in the CDN bundle. --- .../suites/replay/captureConsoleLog/test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/replay/captureConsoleLog/test.ts b/dev-packages/browser-integration-tests/suites/replay/captureConsoleLog/test.ts index d773cd553c05..8ea7d10bd158 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureConsoleLog/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureConsoleLog/test.ts @@ -4,9 +4,7 @@ import { sentryTest } from '../../../utils/fixtures'; import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers'; sentryTest('should capture console messages in replay', async ({ getLocalTestPath, page, forceFlushReplay }) => { - // console integration is not used in bundles/loader - const bundle = process.env.PW_BUNDLE || ''; - if (shouldSkipReplayTest() || bundle.startsWith('bundle_') || bundle.startsWith('loader_')) { + if (shouldSkipReplayTest()) { sentryTest.skip(); } @@ -57,9 +55,7 @@ sentryTest('should capture console messages in replay', async ({ getLocalTestPat }); sentryTest('should capture very large console logs', async ({ getLocalTestPath, page, forceFlushReplay }) => { - // console integration is not used in bundles/loader - const bundle = process.env.PW_BUNDLE || ''; - if (shouldSkipReplayTest() || bundle.startsWith('bundle_') || bundle.startsWith('loader_')) { + if (shouldSkipReplayTest()) { sentryTest.skip(); } From 787a29b9634007fc666a6e9aa766ee047d9c094c Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 29 May 2024 10:28:08 +0200 Subject: [PATCH 14/14] meta: Add Changelog entry for 8.6.0 Update CHANGELOG.md Co-authored-by: Francesco Novy --- CHANGELOG.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b9edfc5bd99..364a9359178f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,60 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.6.0 + +### Important Changes + +- **feat(metrics): Add `timings` method to metrics (#12226)** + + This introduces a new method, `metrics.timing()`, which can be used in two ways: + + 1. With a numeric value, to simplify creating a distribution metric. This will default to `second` as unit: + + ```js + Sentry.metrics.timing('myMetric', 100); + ``` + + 2. With a callback, which will wrap the duration of the callback. This can accept a sync or async callback. It will + create an inactive span around the callback and at the end emit a metric with the duration of the span in seconds: + + ```js + const returnValue = Sentry.metrics.timing('myMetric', measureThisFunction); + ``` + +- **feat(react): Add `Sentry.reactErrorHandler` (#12147)** + + This PR introduces `Sentry.reactErrorHandler`, which you can use in React 19 as follows: + + ```js + import * as Sentry from '@sentry/react'; + import { hydrateRoot } from 'react-dom/client'; + + ReactDOM.hydrateRoot( + document.getElementById('root'), + + + , + { + onUncaughtError: Sentry.reactErrorHandler(), + onCaughtError: Sentry.reactErrorHandler((error, errorInfo) => { + // optional callback if users want custom config. + }), + }, + ); + ``` + + For more details, take a look at [the PR](https://github.com/getsentry/sentry-javascript/pull/12147). Our + documentation will be updated soon! + +### Other Changes + +- feat(sveltekit): Add request data to server-side events (#12254) +- fix(core): Pass in cron monitor config correctly (#12248) +- fix(nextjs): Don't capture suspense errors in server components (#12261) +- fix(tracing): Ensure sent spans are limited to 1000 (#12252) +- ref(core): Use versioned carrier on global object (#12206) + ## 8.5.0 ### Important Changes