From 5e1adf8764d1ff584cdaaf016385ac9b7c1f9801 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 6 Jun 2024 17:23:47 +0000
Subject: [PATCH 01/31] feat(deps): Bump @opentelemetry/sdk-metrics from 1.23.0
to 1.25.0 (#12391)
---
yarn.lock | 92 ++++++++++++++++++++++++++++++++++++-------------------
1 file changed, 61 insertions(+), 31 deletions(-)
diff --git a/yarn.lock b/yarn.lock
index 0463b8364fde..d61a25fe3cb5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6213,13 +6213,6 @@
resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.12.0.tgz#4906ae27359d3311e3dea1b63770a16f60848550"
integrity sha512-UXwSsXo3F3yZ1dIBOG9ID8v2r9e+bqLWoizCtTb8rXtwF+N5TM7hzzvQz72o3nBU+zrI/D5e+OqAYK8ZgDd3DA==
-"@opentelemetry/core@1.23.0":
- version "1.23.0"
- resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.23.0.tgz#f2e7ada7f35750f3c1674aef1e52c879005c0731"
- integrity sha512-hdQ/a9TMzMQF/BO8Cz1juA43/L5YGtCSiKoOHmrTEf7VMDAZgy8ucpWx3eQTnQ3gBloRcWtzvcrMZABC3PTSKQ==
- dependencies:
- "@opentelemetry/semantic-conventions" "1.23.0"
-
"@opentelemetry/core@1.24.1", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.8.0":
version "1.24.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.24.1.tgz#35ab9d2ac9ca938e0ffbdfa40c49c169ac8ba80d"
@@ -6445,14 +6438,6 @@
resolved "https://registry.yarnpkg.com/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz#906ac8e4d804d4109f3ebd5c224ac988276fdc47"
integrity sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==
-"@opentelemetry/resources@1.23.0":
- version "1.23.0"
- resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.23.0.tgz#4c71430f3e20c4d88b67ef5629759fae108485e5"
- integrity sha512-iPRLfVfcEQynYGo7e4Di+ti+YQTAY0h5mQEUJcHlU9JOqpb4x965O6PZ+wMcwYVY63G96KtdS86YCM1BF1vQZg==
- dependencies:
- "@opentelemetry/core" "1.23.0"
- "@opentelemetry/semantic-conventions" "1.23.0"
-
"@opentelemetry/resources@1.24.1", "@opentelemetry/resources@^1.8.0":
version "1.24.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.24.1.tgz#5e2cb84814824f3b1e1017e6caeeee8402e0ad6e"
@@ -6478,12 +6463,12 @@
"@opentelemetry/core" "^0.12.0"
"@opentelemetry/sdk-metrics@^1.9.1":
- version "1.23.0"
- resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.23.0.tgz#b4cf3cc86b6dedf5c438c67c829df7399bf64be1"
- integrity sha512-4OkvW6+wST4h6LFG23rXSTf6nmTf201h9dzq7bE0z5R9ESEVLERZz6WXwE7PSgg1gdjlaznm1jLJf8GttypFDg==
+ version "1.25.0"
+ resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.0.tgz#0c954d580c17821ae4385d29447718df09e80b79"
+ integrity sha512-IF+Sv4VHgBr/BPMKabl+GouJIhEqAOexCHgXVTISdz3q9P9H/uA8ScCF+22gitQ69aFtESbdYOV+Fen5+avQng==
dependencies:
- "@opentelemetry/core" "1.23.0"
- "@opentelemetry/resources" "1.23.0"
+ "@opentelemetry/core" "1.25.0"
+ "@opentelemetry/resources" "1.25.0"
lodash.merge "^4.6.2"
"@opentelemetry/sdk-trace-base@^1.22":
@@ -6504,11 +6489,6 @@
"@opentelemetry/resources" "1.25.0"
"@opentelemetry/semantic-conventions" "1.25.0"
-"@opentelemetry/semantic-conventions@1.23.0":
- version "1.23.0"
- resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.23.0.tgz#627f2721b960fe586b7f72a07912cb7699f06eef"
- integrity sha512-MiqFvfOzfR31t8cc74CTP1OZfz7MbqpAnLCra8NqQoaHJX6ncIRTdYOQYBDQ2uFISDq0WY8Y9dDTWvsgzzBYRg==
-
"@opentelemetry/semantic-conventions@1.24.1", "@opentelemetry/semantic-conventions@^1.17.0", "@opentelemetry/semantic-conventions@^1.22.0", "@opentelemetry/semantic-conventions@^1.23.0":
version "1.24.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.24.1.tgz#d4bcebda1cb5146d47a2a53daaa7922f8e084dfb"
@@ -8449,7 +8429,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==
@@ -8815,7 +8805,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==
@@ -26099,7 +26097,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==
@@ -26114,6 +26112,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"
@@ -28432,7 +28437,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==
@@ -28458,6 +28463,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"
@@ -28553,7 +28567,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==
@@ -31180,7 +31201,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==
@@ -31198,6 +31219,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 cecb0d7ad9ae05b7f0856a48375e53a5fa1b5d53 Mon Sep 17 00:00:00 2001
From: Jonas
Date: Thu, 6 Jun 2024 14:06:47 -0400
Subject: [PATCH 02/31] feat(node): add continuous profiling mode (#12124)
This PR introduce a new continuous profiling mode. This mode is
exclusive from the current mode which considers starting and stopping
profiles on a per span basis.
I've picked the interval duration of 5s as somewhat arbitrarily. The
idea is that we dont want profiles to grow too large, because that might
become a performance issue in the event that we have a lot of deep stack
samples to process.
Since profiling mode is exclusive, we will require users to add a
profilerMode (subject to change) as the SDK option (this is subject to
change as we align the APIs cross sdks). In terms of convenience, we are
likely also going to add a Sentry.profiler.start/stop methods so that
users can have access as to when they can start and stop the profiler
(not implemented as we havent standardized on the approach yet) -
currently this relies on
getIntegrationByName("ProfilingIntegration").profiler.stop
Since the UI does not support this mode yet, I will hide the
profilerMode hidden and only allow the current automated instrumentation
---
.../profiling-node/bindings/cpu_profiler.cc | 215 +++++---
packages/profiling-node/src/cpu_profiler.ts | 37 +-
packages/profiling-node/src/integration.ts | 462 +++++++++++++-----
.../profiling-node/src/spanProfileUtils.ts | 8 +-
packages/profiling-node/src/types.ts | 95 ++--
packages/profiling-node/src/utils.ts | 178 ++++++-
.../profiling-node/test/cpu_profiler.test.ts | 74 ++-
.../profiling-node/test/integration.test.ts | 5 +-
.../test/spanProfileUtils.test.ts | 352 ++++++++++++-
packages/types/src/envelope.ts | 10 +-
packages/types/src/index.ts | 3 +
packages/types/src/profiling.ts | 44 +-
packages/utils/src/envelope.ts | 1 +
13 files changed, 1179 insertions(+), 305 deletions(-)
diff --git a/packages/profiling-node/bindings/cpu_profiler.cc b/packages/profiling-node/bindings/cpu_profiler.cc
index 6f9651bcd6a4..cbff754623bc 100644
--- a/packages/profiling-node/bindings/cpu_profiler.cc
+++ b/packages/profiling-node/bindings/cpu_profiler.cc
@@ -10,6 +10,7 @@
#include
#include
+#include
#include
#include
#include
@@ -23,6 +24,10 @@ static const v8::CpuProfilingNamingMode
static const v8::CpuProfilingLoggingMode
kDefaultLoggingMode(v8::CpuProfilingLoggingMode::kEagerLogging);
+enum ProfileFormat {
+ kFormatThread = 0,
+ kFormatChunk = 1,
+};
// Allow users to override the default logging mode via env variable. This is
// useful because sometimes the flow of the profiled program can be to execute
// many sequential transaction - in that case, it may be preferable to set eager
@@ -50,6 +55,12 @@ v8::CpuProfilingLoggingMode GetLoggingMode() {
return kDefaultLoggingMode;
}
+uint64_t timestamp_milliseconds() {
+ return std::chrono::duration_cast(
+ std::chrono::system_clock::now().time_since_epoch())
+ .count();
+}
+
class SentryProfile;
class Profiler;
@@ -241,6 +252,7 @@ class Profiler {
class SentryProfile {
private:
uint64_t started_at;
+ uint64_t timestamp;
uint16_t heap_write_index = 0;
uint16_t cpu_write_index = 0;
@@ -258,7 +270,7 @@ class SentryProfile {
public:
explicit SentryProfile(const char *id)
- : started_at(uv_hrtime()),
+ : started_at(uv_hrtime()), timestamp(timestamp_milliseconds()),
memory_sampler_cb([this](uint64_t ts, v8::HeapStatistics &stats) {
if ((heap_write_index >= heap_stats_ts.capacity()) ||
heap_write_index >= heap_stats_usage.capacity()) {
@@ -302,6 +314,7 @@ class SentryProfile {
const std::vector &cpu_usage_timestamps() const;
const std::vector &cpu_usage_values() const;
const uint16_t &cpu_usage_write_index() const;
+ const uint64_t &profile_start_timestamp() const;
void Start(Profiler *profiler);
v8::CpuProfile *Stop(Profiler *profiler);
@@ -314,6 +327,7 @@ void SentryProfile::Start(Profiler *profiler) {
.ToLocalChecked();
started_at = uv_hrtime();
+ timestamp = timestamp_milliseconds();
// Initialize the CPU Profiler
profiler->cpu_profiler->StartProfiling(
@@ -368,6 +382,9 @@ const std::vector &SentryProfile::cpu_usage_values() const {
const uint16_t &SentryProfile::cpu_usage_write_index() const {
return cpu_write_index;
};
+const uint64_t &SentryProfile::profile_start_timestamp() const {
+ return timestamp;
+}
static void CleanupSentryProfile(Profiler *profiler,
SentryProfile *sentry_profile,
@@ -528,8 +545,9 @@ CreateFrameNode(const napi_env &env, const v8::CpuProfileNode &node,
return js_node;
};
-napi_value CreateSample(const napi_env &env, const uint32_t stack_id,
- const int64_t sample_timestamp_us,
+napi_value CreateSample(const napi_env &env, const enum ProfileFormat format,
+ const uint32_t stack_id, const int64_t sample_timestamp,
+ const double chunk_timestamp,
const uint32_t thread_id) {
napi_value js_node;
napi_create_object(env, &js_node);
@@ -543,11 +561,20 @@ napi_value CreateSample(const napi_env &env, const uint32_t stack_id,
NAPI_AUTO_LENGTH, &thread_id_prop);
napi_set_named_property(env, js_node, "thread_id", thread_id_prop);
- napi_value elapsed_since_start_ns_prop;
- napi_create_int64(env, sample_timestamp_us * 1000,
- &elapsed_since_start_ns_prop);
- napi_set_named_property(env, js_node, "elapsed_since_start_ns",
- elapsed_since_start_ns_prop);
+ switch (format) {
+ case ProfileFormat::kFormatThread: {
+ napi_value timestamp;
+ napi_create_int64(env, sample_timestamp, ×tamp);
+ napi_set_named_property(env, js_node, "elapsed_since_start_ns", timestamp);
+ } break;
+ case ProfileFormat::kFormatChunk: {
+ napi_value timestamp;
+ napi_create_double(env, chunk_timestamp, ×tamp);
+ napi_set_named_property(env, js_node, "timestamp", timestamp);
+ } break;
+ default:
+ break;
+ }
return js_node;
};
@@ -566,11 +593,13 @@ std::string hashCpuProfilerNodeByPath(const v8::CpuProfileNode *node,
}
static void GetSamples(const napi_env &env, const v8::CpuProfile *profile,
+ ProfileFormat format,
+ const uint64_t profile_start_timestamp_ms,
const uint32_t thread_id, napi_value &samples,
napi_value &stacks, napi_value &frames,
napi_value &resources) {
const int64_t profile_start_time_us = profile->GetStartTime();
- const int sampleCount = profile->GetSamplesCount();
+ const int64_t sampleCount = profile->GetSamplesCount();
uint32_t unique_stack_id = 0;
uint32_t unique_frame_id = 0;
@@ -590,7 +619,7 @@ static void GetSamples(const napi_env &env, const v8::CpuProfile *profile,
uint32_t stack_index = unique_stack_id;
const v8::CpuProfileNode *node = profile->GetSample(i);
- const int64_t sample_timestamp = profile->GetSampleTimestamp(i);
+ const int64_t sample_timestamp_us = profile->GetSampleTimestamp(i);
// If a node was only on top of the stack once, then it will only ever
// be inserted once and there is no need for hashing.
@@ -609,8 +638,16 @@ static void GetSamples(const napi_env &env, const v8::CpuProfile *profile,
}
}
- napi_value sample = CreateSample(
- env, stack_index, sample_timestamp - profile_start_time_us, thread_id);
+ uint64_t sample_delta_us = sample_timestamp_us - profile_start_time_us;
+ uint64_t sample_timestamp_ns = sample_delta_us * 1e3;
+ uint64_t sample_offset_from_profile_start_ms =
+ (sample_timestamp_us - profile_start_time_us) * 1e-3;
+ double seconds_since_start =
+ profile_start_timestamp_ms + sample_offset_from_profile_start_ms;
+
+ napi_value sample = nullptr;
+ sample = CreateSample(env, format, stack_index, sample_timestamp_ns,
+ seconds_since_start, thread_id);
if (stack_index != unique_stack_id) {
napi_value index;
@@ -671,19 +708,19 @@ static void GetSamples(const napi_env &env, const v8::CpuProfile *profile,
}
}
-static napi_value
-TranslateMeasurementsDouble(const napi_env &env, const char *unit,
- const uint16_t size,
- const std::vector &values,
- const std::vector ×tamps) {
- if (size > values.size() || size > timestamps.size()) {
+static napi_value TranslateMeasurementsDouble(
+ const napi_env &env, const enum ProfileFormat format, const char *unit,
+ const uint64_t profile_start_timestamp_ms, const uint16_t size,
+ const std::vector &values,
+ const std::vector ×tamps_ns) {
+ if (size > values.size() || size > timestamps_ns.size()) {
napi_throw_range_error(env, "NAPI_ERROR",
"CPU measurement size is larger than the number of "
"values or timestamps");
return nullptr;
}
- if (values.size() != timestamps.size()) {
+ if (values.size() != timestamps_ns.size()) {
napi_throw_range_error(env, "NAPI_ERROR",
"CPU measurement entries are corrupt, expected "
"values and timestamps to be of equal length");
@@ -713,11 +750,19 @@ TranslateMeasurementsDouble(const napi_env &env, const char *unit,
}
}
- napi_value ts;
- napi_create_int64(env, timestamps[i], &ts);
-
napi_set_named_property(env, entry, "value", value);
- napi_set_named_property(env, entry, "elapsed_since_start_ns", ts);
+
+ if (format == ProfileFormat::kFormatThread) {
+ napi_value ts;
+ napi_create_int64(env, timestamps_ns[i], &ts);
+ napi_set_named_property(env, entry, "elapsed_since_start_ns", ts);
+ } else if (format == ProfileFormat::kFormatChunk) {
+ napi_value ts;
+ napi_create_double(
+ env, profile_start_timestamp_ms + (timestamps_ns[i] * 1e-6), &ts);
+ napi_set_named_property(env, entry, "timestamp", ts);
+ }
+
napi_set_element(env, values_array, i, entry);
}
@@ -727,17 +772,19 @@ TranslateMeasurementsDouble(const napi_env &env, const char *unit,
}
static napi_value
-TranslateMeasurements(const napi_env &env, const char *unit,
+TranslateMeasurements(const napi_env &env, const enum ProfileFormat format,
+ const char *unit,
+ const uint64_t profile_start_timestamp_ms,
const uint16_t size, const std::vector &values,
- const std::vector ×tamps) {
- if (size > values.size() || size > timestamps.size()) {
+ const std::vector ×tamps_ns) {
+ if (size > values.size() || size > timestamps_ns.size()) {
napi_throw_range_error(env, "NAPI_ERROR",
"Memory measurement size is larger than the number "
"of values or timestamps");
return nullptr;
}
- if (values.size() != timestamps.size()) {
+ if (values.size() != timestamps_ns.size()) {
napi_throw_range_error(env, "NAPI_ERROR",
"Memory measurement entries are corrupt, expected "
"values and timestamps to be of equal length");
@@ -761,11 +808,22 @@ TranslateMeasurements(const napi_env &env, const char *unit,
napi_value value;
napi_create_int64(env, values[i], &value);
- napi_value ts;
- napi_create_int64(env, timestamps[i], &ts);
-
napi_set_named_property(env, entry, "value", value);
- napi_set_named_property(env, entry, "elapsed_since_start_ns", ts);
+ switch (format) {
+ case ProfileFormat::kFormatThread: {
+ napi_value ts;
+ napi_create_int64(env, timestamps_ns[i], &ts);
+ napi_set_named_property(env, entry, "elapsed_since_start_ns", ts);
+ } break;
+ case ProfileFormat::kFormatChunk: {
+ napi_value ts;
+ napi_create_double(
+ env, profile_start_timestamp_ms + (timestamps_ns[i] * 1e-6), &ts);
+ napi_set_named_property(env, entry, "timestamp", ts);
+ } break;
+ default:
+ break;
+ }
napi_set_element(env, values_array, i, entry);
}
@@ -776,6 +834,8 @@ TranslateMeasurements(const napi_env &env, const char *unit,
static napi_value TranslateProfile(const napi_env &env,
const v8::CpuProfile *profile,
+ const enum ProfileFormat format,
+ const uint64_t profile_start_timestamp_ms,
const uint32_t thread_id,
bool collect_resources) {
napi_value js_profile;
@@ -805,7 +865,8 @@ static napi_value TranslateProfile(const napi_env &env,
napi_set_named_property(env, js_profile, "profiler_logging_mode",
logging_mode);
- GetSamples(env, profile, thread_id, samples, stacks, frames, resources);
+ GetSamples(env, profile, format, profile_start_timestamp_ms, thread_id,
+ samples, stacks, frames, resources);
if (collect_resources) {
napi_set_named_property(env, js_profile, "resources", resources);
@@ -892,14 +953,14 @@ static napi_value StartProfiling(napi_env env, napi_callback_info info) {
// StopProfiling(string title)
// https://v8docs.nodesource.com/node-18.2/d2/d34/classv8_1_1_cpu_profiler.html#a40ca4c8a8aa4c9233aa2a2706457cc80
static napi_value StopProfiling(napi_env env, napi_callback_info info) {
- size_t argc = 3;
- napi_value argv[3];
+ size_t argc = 4;
+ napi_value argv[4];
assert(napi_get_cb_info(env, info, &argc, argv, NULL, NULL) == napi_ok);
- if (argc < 2) {
+ if (argc < 3) {
napi_throw_error(env, "NAPI_ERROR",
- "StopProfiling expects at least two arguments.");
+ "StopProfiling expects at least three arguments.");
napi_value napi_null;
assert(napi_get_null(env, &napi_null) == napi_ok);
@@ -921,14 +982,17 @@ static napi_value StopProfiling(napi_env env, napi_callback_info info) {
return napi_null;
}
- // Verify the second argument is a number
- napi_valuetype callbacktype1;
- assert(napi_typeof(env, argv[1], &callbacktype1) == napi_ok);
+ size_t len;
+ assert(napi_get_value_string_utf8(env, argv[0], NULL, 0, &len) == napi_ok);
- if (callbacktype1 != napi_number) {
+ char *title = (char *)malloc(len + 1);
+ assert(napi_get_value_string_utf8(env, argv[0], title, len + 1, &len) ==
+ napi_ok);
+
+ if (len < 1) {
napi_throw_error(
env, "NAPI_ERROR",
- "StopProfiling expects a thread_id integer as second argument.");
+ "StopProfiling expects a non empty string as first argument.");
napi_value napi_null;
assert(napi_get_null(env, &napi_null) == napi_ok);
@@ -936,16 +1000,13 @@ static napi_value StopProfiling(napi_env env, napi_callback_info info) {
return napi_null;
}
- size_t len;
- assert(napi_get_value_string_utf8(env, argv[0], NULL, 0, &len) == napi_ok);
-
- char *title = (char *)malloc(len + 1);
- assert(napi_get_value_string_utf8(env, argv[0], title, len + 1, &len) ==
- napi_ok);
+ // Verify the second argument is a number
+ napi_valuetype callbacktype1;
+ assert(napi_typeof(env, argv[1], &callbacktype1) == napi_ok);
- if (len < 1) {
+ if (callbacktype1 != napi_number) {
napi_throw_error(env, "NAPI_ERROR",
- "StopProfiling expects a string as first argument.");
+ "StopProfiling expects a format type as second argument.");
napi_value napi_null;
assert(napi_get_null(env, &napi_null) == napi_ok);
@@ -953,9 +1014,27 @@ static napi_value StopProfiling(napi_env env, napi_callback_info info) {
return napi_null;
}
+ // Verify the second argument is a number
+ napi_valuetype callbacktype2;
+ assert(napi_typeof(env, argv[2], &callbacktype2) == napi_ok);
+
+ if (callbacktype2 != napi_number) {
+ napi_throw_error(
+ env, "NAPI_ERROR",
+ "StopProfiling expects a thread_id integer as third argument.");
+
+ napi_value napi_null;
+ assert(napi_get_null(env, &napi_null) == napi_ok);
+ return napi_null;
+ }
+
+ // Get the value of the second argument and convert it to uint8
+ int32_t format;
+ assert(napi_get_value_int32(env, argv[1], &format) == napi_ok);
+
// Get the value of the second argument and convert it to uint64
int64_t thread_id;
- assert(napi_get_value_int64(env, argv[1], &thread_id) == napi_ok);
+ assert(napi_get_value_int64(env, argv[2], &thread_id) == napi_ok);
// Get profiler from instance data
Profiler *profiler;
@@ -967,7 +1046,6 @@ static napi_value StopProfiling(napi_env env, napi_callback_info info) {
napi_value napi_null;
assert(napi_get_null(env, &napi_null) == napi_ok);
-
return napi_null;
}
@@ -994,23 +1072,39 @@ static napi_value StopProfiling(napi_env env, napi_callback_info info) {
};
napi_valuetype callbacktype3;
- assert(napi_typeof(env, argv[2], &callbacktype3) == napi_ok);
+ assert(napi_typeof(env, argv[3], &callbacktype3) == napi_ok);
bool collect_resources;
- napi_get_value_bool(env, argv[2], &collect_resources);
+ napi_get_value_bool(env, argv[3], &collect_resources);
+
+ const ProfileFormat format_type = static_cast(format);
+
+ if (format_type != ProfileFormat::kFormatThread &&
+ format_type != ProfileFormat::kFormatChunk) {
+ napi_throw_error(
+ env, "NAPI_ERROR",
+ "StopProfiling expects a valid format type as second argument.");
+
+ napi_value napi_null;
+ assert(napi_get_null(env, &napi_null) == napi_ok);
+ return napi_null;
+ }
- napi_value js_profile =
- TranslateProfile(env, cpu_profile, thread_id, collect_resources);
+ napi_value js_profile = TranslateProfile(
+ env, cpu_profile, format_type, profile->second->profile_start_timestamp(),
+ thread_id, collect_resources);
napi_value measurements;
napi_create_object(env, &measurements);
if (profile->second->heap_usage_write_index() > 0) {
static const char *memory_unit = "byte";
- napi_value heap_usage_measurements = TranslateMeasurements(
- env, memory_unit, profile->second->heap_usage_write_index(),
- profile->second->heap_usage_values(),
- profile->second->heap_usage_timestamps());
+ napi_value heap_usage_measurements =
+ TranslateMeasurements(env, format_type, memory_unit,
+ profile->second->profile_start_timestamp(),
+ profile->second->heap_usage_write_index(),
+ profile->second->heap_usage_values(),
+ profile->second->heap_usage_timestamps());
if (heap_usage_measurements != nullptr) {
napi_set_named_property(env, measurements, "memory_footprint",
@@ -1021,7 +1115,8 @@ static napi_value StopProfiling(napi_env env, napi_callback_info info) {
if (profile->second->cpu_usage_write_index() > 0) {
static const char *cpu_unit = "percent";
napi_value cpu_usage_measurements = TranslateMeasurementsDouble(
- env, cpu_unit, profile->second->cpu_usage_write_index(),
+ env, format_type, cpu_unit, profile->second->profile_start_timestamp(),
+ profile->second->cpu_usage_write_index(),
profile->second->cpu_usage_values(),
profile->second->cpu_usage_timestamps());
diff --git a/packages/profiling-node/src/cpu_profiler.ts b/packages/profiling-node/src/cpu_profiler.ts
index 433ade1d4b46..9ab470e2ca70 100644
--- a/packages/profiling-node/src/cpu_profiler.ts
+++ b/packages/profiling-node/src/cpu_profiler.ts
@@ -7,7 +7,13 @@ import { getAbi } from 'node-abi';
import { GLOBAL_OBJ, logger } from '@sentry/utils';
import { DEBUG_BUILD } from './debug-build';
-import type { PrivateV8CpuProfilerBindings, V8CpuProfilerBindings } from './types';
+import type {
+ PrivateV8CpuProfilerBindings,
+ RawChunkCpuProfile,
+ RawThreadCpuProfile,
+ V8CpuProfilerBindings,
+} from './types';
+import type { ProfileFormat } from './types';
const stdlib = familySync();
const platform = process.env['BUILD_PLATFORM'] || _platform();
@@ -151,24 +157,39 @@ export function importCppBindingsModule(): PrivateV8CpuProfilerBindings {
}
const PrivateCpuProfilerBindings: PrivateV8CpuProfilerBindings = importCppBindingsModule();
-const CpuProfilerBindings: V8CpuProfilerBindings = {
- startProfiling(name: string) {
+
+class Bindings implements V8CpuProfilerBindings {
+ public startProfiling(name: string): void {
if (!PrivateCpuProfilerBindings) {
DEBUG_BUILD && logger.log('[Profiling] Bindings not loaded, ignoring call to startProfiling.');
return;
}
return PrivateCpuProfilerBindings.startProfiling(name);
- },
- stopProfiling(name: string) {
+ }
+
+ public stopProfiling(name: string, format: ProfileFormat.THREAD): RawThreadCpuProfile | null;
+ public stopProfiling(name: string, format: ProfileFormat.CHUNK): RawChunkCpuProfile | null;
+ public stopProfiling(
+ name: string,
+ format: ProfileFormat.CHUNK | ProfileFormat.THREAD,
+ ): RawThreadCpuProfile | RawChunkCpuProfile | null {
if (!PrivateCpuProfilerBindings) {
DEBUG_BUILD &&
logger.log('[Profiling] Bindings not loaded or profile was never started, ignoring call to stopProfiling.');
return null;
}
- return PrivateCpuProfilerBindings.stopProfiling(name, threadId, !!GLOBAL_OBJ._sentryDebugIds);
- },
-};
+
+ return PrivateCpuProfilerBindings.stopProfiling(
+ name,
+ format as unknown as any,
+ threadId,
+ !!GLOBAL_OBJ._sentryDebugIds,
+ );
+ }
+}
+
+const CpuProfilerBindings = new Bindings();
export { PrivateCpuProfilerBindings };
export { CpuProfilerBindings };
diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts
index f8a9ae4e5e4d..6cba86fe4f8d 100644
--- a/packages/profiling-node/src/integration.ts
+++ b/packages/profiling-node/src/integration.ts
@@ -1,31 +1,324 @@
-import { defineIntegration, getCurrentScope, getRootSpan, spanToJSON } from '@sentry/core';
+import { defineIntegration, getCurrentScope, getIsolationScope, getRootSpan, spanToJSON } from '@sentry/core';
import type { NodeClient } from '@sentry/node';
-import type { IntegrationFn, Span } from '@sentry/types';
+import type { Integration, IntegrationFn, Profile, ProfileChunk, Span } from '@sentry/types';
-import { logger } from '@sentry/utils';
+import { LRUMap, logger, timestampInSeconds, uuid4 } from '@sentry/utils';
+import { CpuProfilerBindings } from './cpu_profiler';
import { DEBUG_BUILD } from './debug-build';
import { NODE_MAJOR, NODE_VERSION } from './nodeVersion';
import { MAX_PROFILE_DURATION_MS, maybeProfileSpan, stopSpanProfile } from './spanProfileUtils';
-import type { Profile, RawThreadCpuProfile } from './types';
+import type { RawThreadCpuProfile } from './types';
+import { ProfileFormat } from './types';
+
+import {
+ addProfilesToEnvelope,
+ createProfilingChunkEvent,
+ createProfilingEvent,
+ findProfiledTransactionsFromEnvelope,
+ makeProfileChunkEnvelope,
+} from './utils';
+
+const CHUNK_INTERVAL_MS = 5000;
+const PROFILE_MAP = new LRUMap(50);
+const PROFILE_TIMEOUTS: Record = {};
-import { addProfilesToEnvelope, createProfilingEvent, findProfiledTransactionsFromEnvelope } from './utils';
+function addToProfileQueue(profile_id: string, profile: RawThreadCpuProfile): void {
+ PROFILE_MAP.set(profile_id, profile);
+}
-const MAX_PROFILE_QUEUE_LENGTH = 50;
-const PROFILE_QUEUE: RawThreadCpuProfile[] = [];
-const PROFILE_TIMEOUTS: Record = {};
+function takeFromProfileQueue(profile_id: string): RawThreadCpuProfile | undefined {
+ const profile = PROFILE_MAP.get(profile_id);
+ PROFILE_MAP.remove(profile_id);
+ return profile;
+}
+
+/**
+ * Instruments the client to automatically invoke the profiler on span start and stop events.
+ * @param client
+ */
+function setupAutomatedSpanProfiling(client: NodeClient): void {
+ const spanToProfileIdMap = new WeakMap();
+
+ client.on('spanStart', span => {
+ if (span !== getRootSpan(span)) {
+ return;
+ }
+
+ const profile_id = maybeProfileSpan(client, span);
+
+ if (profile_id) {
+ const options = client.getOptions();
+ // Not intended for external use, hence missing types, but we want to profile a couple of things at Sentry that
+ // currently exceed the default timeout set by the SDKs.
+ const maxProfileDurationMs =
+ (options._experiments && options._experiments['maxProfileDurationMs']) || MAX_PROFILE_DURATION_MS;
+
+ if (PROFILE_TIMEOUTS[profile_id]) {
+ global.clearTimeout(PROFILE_TIMEOUTS[profile_id]);
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+ delete PROFILE_TIMEOUTS[profile_id];
+ }
+
+ // Enqueue a timeout to prevent profiles from running over max duration.
+ const timeout = global.setTimeout(() => {
+ DEBUG_BUILD &&
+ logger.log('[Profiling] max profile duration elapsed, stopping profiling for:', spanToJSON(span).description);
+
+ const profile = stopSpanProfile(span, profile_id);
+ if (profile) {
+ addToProfileQueue(profile_id, profile);
+ }
+ }, maxProfileDurationMs);
+
+ // Unref timeout so it doesn't keep the process alive.
+ timeout.unref();
+
+ getCurrentScope().setContext('profile', { profile_id });
+ spanToProfileIdMap.set(span, profile_id);
+ }
+ });
+
+ client.on('spanEnd', span => {
+ const profile_id = spanToProfileIdMap.get(span);
+
+ if (profile_id) {
+ if (PROFILE_TIMEOUTS[profile_id]) {
+ global.clearTimeout(PROFILE_TIMEOUTS[profile_id]);
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+ delete PROFILE_TIMEOUTS[profile_id];
+ }
+ const profile = stopSpanProfile(span, profile_id);
+
+ if (profile) {
+ addToProfileQueue(profile_id, profile);
+ }
+ }
+ });
+
+ client.on('beforeEnvelope', (envelope): void => {
+ // if not profiles are in queue, there is nothing to add to the envelope.
+ if (!PROFILE_MAP.size) {
+ return;
+ }
+
+ const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
+ if (!profiledTransactionEvents.length) {
+ return;
+ }
+
+ const profilesToAddToEnvelope: Profile[] = [];
+
+ for (const profiledTransaction of profiledTransactionEvents) {
+ const profileContext = profiledTransaction.contexts?.['profile'];
+ const profile_id = profileContext?.['profile_id'];
+
+ if (!profile_id) {
+ throw new TypeError('[Profiling] cannot find profile for a transaction without a profile context');
+ }
+
+ // Remove the profile from the transaction context before sending, relay will take care of the rest.
+ if (profileContext) {
+ delete profiledTransaction.contexts?.['profile'];
+ }
+
+ const cpuProfile = takeFromProfileQueue(profile_id);
+ if (!cpuProfile) {
+ DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
+ continue;
+ }
+
+ const profile = createProfilingEvent(client, cpuProfile, profiledTransaction);
+ if (!profile) return;
+
+ profilesToAddToEnvelope.push(profile);
+
+ // @ts-expect-error profile does not inherit from Event
+ client.emit('preprocessEvent', profile, {
+ event_id: profiledTransaction.event_id,
+ });
+ }
+
+ addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
+ });
+}
+
+interface ChunkData {
+ id: string;
+ timer: NodeJS.Timeout | undefined;
+ startTimestampMS: number;
+ startTraceID: string;
+}
+class ContinuousProfiler {
+ private _profilerId = uuid4();
+ private _client: NodeClient | undefined = undefined;
+ private _chunkData: ChunkData | undefined = undefined;
+
+ /**
+ * Called when the profiler is attached to the client (continuous mode is enabled). If of the profiler
+ * methods called before the profiler is initialized will result in a noop action with debug logs.
+ * @param client
+ */
+ public initialize(client: NodeClient): void {
+ this._client = client;
+ }
+
+ /**
+ * Recursively schedules chunk profiling to start and stop at a set interval.
+ * Once the user calls stop(), the current chunk will be stopped and flushed to Sentry and no new chunks will
+ * will be started. To restart continuous mode after calling stop(), the user must call start() again.
+ * @returns void
+ */
+ public start(): void {
+ if (!this._client) {
+ // The client is not attached to the profiler if the user has not enabled continuous profiling.
+ // In this case, calling start() and stop() is a noop action.The reason this exists is because
+ // it makes the types easier to work with and avoids users having to do null checks.
+ DEBUG_BUILD && logger.log('[Profiling] Profiler was never attached to the client.');
+ return;
+ }
+ if (this._chunkData) {
+ DEBUG_BUILD &&
+ logger.log(
+ `[Profiling] Chunk with chunk_id ${this._chunkData.id} is still running, current chunk will be stopped a new chunk will be started.`,
+ );
+ this.stop();
+ }
+
+ const traceId =
+ getCurrentScope().getPropagationContext().traceId || getIsolationScope().getPropagationContext().traceId;
+ this._initializeChunk(traceId);
+ this._startChunkProfiling(this._chunkData!);
+ }
+
+ /**
+ * Stops the current chunk and flushes the profile to Sentry.
+ * @returns void
+ */
+ public stop(): void {
+ if (this._chunkData?.timer) {
+ global.clearTimeout(this._chunkData.timer);
+ this._chunkData.timer = undefined;
+ DEBUG_BUILD && logger.log(`[Profiling] Stopping profiling chunk: ${this._chunkData.id}`);
+ }
+ if (!this._client) {
+ DEBUG_BUILD &&
+ logger.log('[Profiling] Failed to collect profile, sentry client was never attached to the profiler.');
+ return;
+ }
+ if (!this._chunkData?.id) {
+ DEBUG_BUILD &&
+ logger.log(`[Profiling] Failed to collect profile for: ${this._chunkData?.id}, the chunk_id is missing.`);
+ return;
+ }
+ const profile = CpuProfilerBindings.stopProfiling(this._chunkData.id, ProfileFormat.CHUNK);
+ if (!profile || !this._chunkData.startTimestampMS) {
+ DEBUG_BUILD && logger.log(`[Profiling] _chunkiledStartTraceID to collect profile for: ${this._chunkData.id}`);
+ return;
+ }
+ if (profile) {
+ DEBUG_BUILD && logger.log(`[Profiling] Sending profile chunk ${this._chunkData.id}.`);
+ }
+
+ DEBUG_BUILD && logger.log(`[Profiling] Profile chunk ${this._chunkData.id} sent to Sentry.`);
+ const chunk = createProfilingChunkEvent(
+ this._chunkData.startTimestampMS,
+ this._client,
+ this._client.getOptions(),
+ profile,
+ {
+ chunk_id: this._chunkData.id,
+ trace_id: this._chunkData.startTraceID,
+ profiler_id: this._profilerId,
+ },
+ );
+
+ if (!chunk) {
+ DEBUG_BUILD && logger.log(`[Profiling] Failed to create profile chunk for: ${this._chunkData.id}`);
+ this._reset();
+ return;
+ }
+
+ this._flush(chunk);
+ // Depending on the profile and stack sizes, stopping the profile and converting
+ // the format may negatively impact the performance of the application. To avoid
+ // blocking for too long, enqueue the next chunk start inside the next macrotask.
+ // clear current chunk
+ this._reset();
+ }
+
+ /**
+ * Flushes the profile chunk to Sentry.
+ * @param chunk
+ */
+ private _flush(chunk: ProfileChunk): void {
+ if (!this._client) {
+ DEBUG_BUILD &&
+ logger.log('[Profiling] Failed to collect profile, sentry client was never attached to the profiler.');
+ return;
+ }
+
+ const transport = this._client.getTransport();
+ if (!transport) {
+ DEBUG_BUILD && logger.log('[Profiling] No transport available to send profile chunk.');
+ return;
+ }
+
+ const dsn = this._client.getDsn();
+ const metadata = this._client.getSdkMetadata();
+ const tunnel = this._client.getOptions().tunnel;
+
+ const envelope = makeProfileChunkEnvelope(chunk, metadata?.sdk, tunnel, dsn);
+ transport.send(envelope).then(null, reason => {
+ DEBUG_BUILD && logger.error('Error while sending profile chunk envelope:', reason);
+ });
+ }
+
+ /**
+ * Starts the profiler and registers the flush timer for a given chunk.
+ * @param chunk
+ */
+ private _startChunkProfiling(chunk: ChunkData): void {
+ CpuProfilerBindings.startProfiling(chunk.id!);
+ DEBUG_BUILD && logger.log(`[Profiling] starting profiling chunk: ${chunk.id}`);
+
+ chunk.timer = global.setTimeout(() => {
+ DEBUG_BUILD && logger.log(`[Profiling] Stopping profiling chunk: ${chunk.id}`);
+ this.stop();
+ DEBUG_BUILD && logger.log('[Profiling] Starting new profiling chunk.');
+ setImmediate(this.start.bind(this));
+ }, CHUNK_INTERVAL_MS);
+
+ // Unref timeout so it doesn't keep the process alive.
+ chunk.timer.unref();
+ }
-function addToProfileQueue(profile: RawThreadCpuProfile): void {
- PROFILE_QUEUE.push(profile);
+ /**
+ * Initializes new profile chunk metadata
+ */
+ private _initializeChunk(traceId: string): void {
+ this._chunkData = {
+ id: uuid4(),
+ startTraceID: traceId,
+ startTimestampMS: timestampInSeconds(),
+ timer: undefined,
+ };
+ }
- // We only want to keep the last n profiles in the queue.
- if (PROFILE_QUEUE.length > MAX_PROFILE_QUEUE_LENGTH) {
- PROFILE_QUEUE.shift();
+ /**
+ * Resets the current chunk state.
+ */
+ private _reset(): void {
+ this._chunkData = undefined;
}
}
+export interface ProfilingIntegration extends Integration {
+ _profiler: ContinuousProfiler;
+}
+
/** Exported only for tests. */
-export const _nodeProfilingIntegration = (() => {
+export const _nodeProfilingIntegration = ((): ProfilingIntegration => {
if (DEBUG_BUILD && ![16, 18, 20, 22].includes(NODE_MAJOR)) {
logger.warn(
`[Profiling] You are using a Node.js version that does not have prebuilt binaries (${NODE_VERSION}).`,
@@ -37,129 +330,32 @@ export const _nodeProfilingIntegration = (() => {
return {
name: 'ProfilingIntegration',
+ _profiler: new ContinuousProfiler(),
setup(client: NodeClient) {
- const spanToProfileIdMap = new WeakMap();
-
- client.on('spanStart', span => {
- if (span !== getRootSpan(span)) {
- return;
+ DEBUG_BUILD && logger.log('[Profiling] Profiling integration setup.');
+ const options = client.getOptions();
+
+ const mode =
+ (options.profilesSampleRate === undefined || options.profilesSampleRate === 0) && !options.profilesSampler
+ ? 'continuous'
+ : 'span';
+ switch (mode) {
+ case 'continuous': {
+ DEBUG_BUILD && logger.log('[Profiling] Continuous profiler mode enabled.');
+ this._profiler.initialize(client);
+ break;
}
-
- const profile_id = maybeProfileSpan(client, span, undefined);
-
- if (profile_id) {
- const options = client.getOptions();
- // Not intended for external use, hence missing types, but we want to profile a couple of things at Sentry that
- // currently exceed the default timeout set by the SDKs.
- const maxProfileDurationMs =
- (options._experiments && options._experiments['maxProfileDurationMs']) || MAX_PROFILE_DURATION_MS;
-
- if (PROFILE_TIMEOUTS[profile_id]) {
- global.clearTimeout(PROFILE_TIMEOUTS[profile_id]);
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
- delete PROFILE_TIMEOUTS[profile_id];
- }
-
- // Enqueue a timeout to prevent profiles from running over max duration.
- PROFILE_TIMEOUTS[profile_id] = global.setTimeout(() => {
- DEBUG_BUILD &&
- logger.log(
- '[Profiling] max profile duration elapsed, stopping profiling for:',
- spanToJSON(span).description,
- );
-
- const profile = stopSpanProfile(span, profile_id);
- if (profile) {
- addToProfileQueue(profile);
- }
- }, maxProfileDurationMs);
-
- getCurrentScope().setContext('profile', { profile_id });
-
- spanToProfileIdMap.set(span, profile_id);
+ // Default to span profiling when no mode profiler mode is set
+ case 'span':
+ case undefined: {
+ DEBUG_BUILD && logger.log('[Profiling] Span profiler mode enabled.');
+ setupAutomatedSpanProfiling(client);
+ break;
}
- });
-
- client.on('spanEnd', span => {
- const profile_id = spanToProfileIdMap.get(span);
-
- if (profile_id) {
- if (PROFILE_TIMEOUTS[profile_id]) {
- global.clearTimeout(PROFILE_TIMEOUTS[profile_id]);
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
- delete PROFILE_TIMEOUTS[profile_id];
- }
- const profile = stopSpanProfile(span, profile_id);
-
- if (profile) {
- addToProfileQueue(profile);
- }
- }
- });
-
- client.on('beforeEnvelope', (envelope): void => {
- // if not profiles are in queue, there is nothing to add to the envelope.
- if (!PROFILE_QUEUE.length) {
- return;
- }
-
- const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
- if (!profiledTransactionEvents.length) {
- return;
- }
-
- const profilesToAddToEnvelope: Profile[] = [];
-
- for (const profiledTransaction of profiledTransactionEvents) {
- const profileContext = profiledTransaction.contexts?.['profile'];
- const profile_id = profileContext?.['profile_id'];
-
- if (!profile_id) {
- throw new TypeError('[Profiling] cannot find profile for a transaction without a profile context');
- }
-
- // Remove the profile from the transaction context before sending, relay will take care of the rest.
- if (profileContext) {
- delete profiledTransaction.contexts?.['profile'];
- }
-
- // We need to find both a profile and a transaction event for the same profile_id.
- const profileIndex = PROFILE_QUEUE.findIndex(p => p.profile_id === profile_id);
- if (profileIndex === -1) {
- DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
- continue;
- }
-
- const cpuProfile = PROFILE_QUEUE[profileIndex];
- if (!cpuProfile) {
- DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
- continue;
- }
-
- // Remove the profile from the queue.
- PROFILE_QUEUE.splice(profileIndex, 1);
- const profile = createProfilingEvent(client, cpuProfile, profiledTransaction);
-
- if (client.emit && profile) {
- const integrations =
- client['_integrations'] && client['_integrations'] !== null && !Array.isArray(client['_integrations'])
- ? Object.keys(client['_integrations'])
- : undefined;
-
- // @ts-expect-error bad overload due to unknown event
- client.emit('preprocessEvent', profile, {
- event_id: profiledTransaction.event_id,
- integrations,
- });
- }
-
- if (profile) {
- profilesToAddToEnvelope.push(profile);
- }
+ default: {
+ DEBUG_BUILD && logger.warn(`[Profiling] Unknown profiler mode: ${mode}, profiler was not initialized`);
}
-
- addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
- });
+ }
},
};
}) satisfies IntegrationFn;
diff --git a/packages/profiling-node/src/spanProfileUtils.ts b/packages/profiling-node/src/spanProfileUtils.ts
index 957ee0e16303..1b347a61b741 100644
--- a/packages/profiling-node/src/spanProfileUtils.ts
+++ b/packages/profiling-node/src/spanProfileUtils.ts
@@ -5,6 +5,7 @@ import { logger, uuid4 } from '@sentry/utils';
import { CpuProfilerBindings } from './cpu_profiler';
import { DEBUG_BUILD } from './debug-build';
+import type { RawThreadCpuProfile } from './types';
import { isValidSampleRate } from './utils';
export const MAX_PROFILE_DURATION_MS = 30 * 1000;
@@ -107,16 +108,13 @@ export function maybeProfileSpan(
* @param profile_id
* @returns
*/
-export function stopSpanProfile(
- span: Span,
- profile_id: string | undefined,
-): ReturnType<(typeof CpuProfilerBindings)['stopProfiling']> | null {
+export function stopSpanProfile(span: Span, profile_id: string | undefined): RawThreadCpuProfile | null {
// Should not happen, but satisfy the type checker and be safe regardless.
if (!profile_id) {
return null;
}
- const profile = CpuProfilerBindings.stopProfiling(profile_id);
+ const profile = CpuProfilerBindings.stopProfiling(profile_id, 0);
DEBUG_BUILD && logger.log(`[Profiling] stopped profiling of transaction: ${spanToJSON(span).description}`);
diff --git a/packages/profiling-node/src/types.ts b/packages/profiling-node/src/types.ts
index 3042335269eb..1c2c444887cd 100644
--- a/packages/profiling-node/src/types.ts
+++ b/packages/profiling-node/src/types.ts
@@ -6,7 +6,11 @@ interface Sample {
elapsed_since_start_ns: string;
}
-type Stack = number[];
+interface ChunkSample {
+ stack_id: number;
+ thread_id: string;
+ timestamp: number;
+}
type Frame = {
function: string;
@@ -23,15 +27,6 @@ interface Measurement {
}[];
}
-export interface DebugImage {
- code_file: string;
- type: string;
- debug_id: string;
- image_addr?: string;
- image_size?: number;
- image_vmaddr?: string;
-}
-
// Profile is marked as optional because it is deleted from the metadata
// by the integration before the event is processed by other integrations.
export interface ProfiledEvent extends Event {
@@ -40,66 +35,50 @@ export interface ProfiledEvent extends Event {
};
}
-export interface RawThreadCpuProfile {
+interface BaseProfile {
profile_id?: string;
- stacks: ReadonlyArray;
- samples: ReadonlyArray;
- frames: ReadonlyArray;
- resources: ReadonlyArray;
+ stacks: number[][];
+ frames: Frame[];
+ resources: string[];
profiler_logging_mode: 'eager' | 'lazy';
measurements: Record;
}
-export interface ThreadCpuProfile {
- stacks: ReadonlyArray;
- samples: ReadonlyArray;
- frames: ReadonlyArray;
- thread_metadata: Record;
- queue_metadata?: Record;
+export interface RawThreadCpuProfile extends BaseProfile {
+ samples: Sample[];
+}
+
+export interface RawChunkCpuProfile extends BaseProfile {
+ samples: ChunkSample[];
}
export interface PrivateV8CpuProfilerBindings {
startProfiling(name: string): void;
- stopProfiling(name: string, threadId: number, collectResources: boolean): RawThreadCpuProfile | null;
+
+ stopProfiling(
+ name: string,
+ format: ProfileFormat.THREAD,
+ threadId: number,
+ collectResources: boolean,
+ ): RawThreadCpuProfile | null;
+ stopProfiling(
+ name: string,
+ format: ProfileFormat.CHUNK,
+ threadId: number,
+ collectResources: boolean,
+ ): RawChunkCpuProfile | null;
+
+ // Helper methods exposed for testing
getFrameModule(abs_path: string): string;
}
+export enum ProfileFormat {
+ THREAD = 0,
+ CHUNK = 1,
+}
+
export interface V8CpuProfilerBindings {
startProfiling(name: string): void;
- stopProfiling(name: string): RawThreadCpuProfile | null;
-}
-export interface Profile {
- event_id: string;
- version: string;
- os: {
- name: string;
- version: string;
- build_number: string;
- };
- runtime: {
- name: string;
- version: string;
- };
- device: {
- architecture: string;
- is_emulator: boolean;
- locale: string;
- manufacturer: string;
- model: string;
- };
- timestamp: string;
- release: string;
- environment: string;
- platform: string;
- profile: ThreadCpuProfile;
- debug_meta?: {
- images: DebugImage[];
- };
- transaction: {
- name: string;
- id: string;
- trace_id: string;
- active_thread_id: string;
- };
- measurements: Record;
+ stopProfiling(name: string, format: ProfileFormat.THREAD): RawThreadCpuProfile | null;
+ stopProfiling(name: string, format: ProfileFormat.CHUNK): RawChunkCpuProfile | null;
}
diff --git a/packages/profiling-node/src/utils.ts b/packages/profiling-node/src/utils.ts
index 884e71c3d10e..5661129791bb 100644
--- a/packages/profiling-node/src/utils.ts
+++ b/packages/profiling-node/src/utils.ts
@@ -1,19 +1,37 @@
-import * as os from 'node:os';
-import { env, versions } from 'node:process';
-import { isMainThread, threadId } from 'node:worker_threads';
-import type { Client, Context, Envelope, Event, StackFrame, StackParser } from '@sentry/types';
-
-import { GLOBAL_OBJ, forEachEnvelopeItem, logger } from '@sentry/utils';
-
+/* eslint-disable max-lines */
+import * as os from 'os';
+import type {
+ Client,
+ Context,
+ DebugImage,
+ DsnComponents,
+ Envelope,
+ Event,
+ EventEnvelopeHeaders,
+ Profile,
+ ProfileChunk,
+ ProfileChunkEnvelope,
+ SdkInfo,
+ StackFrame,
+ StackParser,
+ ThreadCpuProfile,
+} from '@sentry/types';
+import { GLOBAL_OBJ, createEnvelope, dsnToString, forEachEnvelopeItem, logger, uuid4 } from '@sentry/utils';
+
+import { env, versions } from 'process';
+import { isMainThread, threadId } from 'worker_threads';
+
+import type { ProfileChunkItem } from '@sentry/types/build/types/envelope';
+import type { ContinuousThreadCpuProfile } from '../../types/src/profiling';
import { DEBUG_BUILD } from './debug-build';
-import type { Profile, RawThreadCpuProfile, ThreadCpuProfile } from './types';
-import type { DebugImage } from './types';
+import type { RawChunkCpuProfile, RawThreadCpuProfile } from './types';
// We require the file because if we import it, it will be included in the bundle.
// I guess tsc does not check file contents when it's imported.
const THREAD_ID_STRING = String(threadId);
const THREAD_NAME = isMainThread ? 'main' : 'worker';
const FORMAT_VERSION = '1';
+const CONTINUOUS_FORMAT_VERSION = '2';
// Os machine was backported to 16.18, but this was not reflected in the types
// @ts-expect-error ignore missing
@@ -32,7 +50,9 @@ const ARCH = os.arch();
* @param {ThreadCpuProfile | RawThreadCpuProfile} profile
* @returns {boolean}
*/
-function isRawThreadCpuProfile(profile: ThreadCpuProfile | RawThreadCpuProfile): profile is RawThreadCpuProfile {
+function isRawThreadCpuProfile(
+ profile: ThreadCpuProfile | RawThreadCpuProfile | ContinuousThreadCpuProfile | RawChunkCpuProfile,
+): profile is RawThreadCpuProfile | RawChunkCpuProfile {
return !('thread_metadata' in profile);
}
@@ -43,7 +63,9 @@ function isRawThreadCpuProfile(profile: ThreadCpuProfile | RawThreadCpuProfile):
* @param {ThreadCpuProfile | RawThreadCpuProfile} profile
* @returns {ThreadCpuProfile}
*/
-export function enrichWithThreadInformation(profile: ThreadCpuProfile | RawThreadCpuProfile): ThreadCpuProfile {
+export function enrichWithThreadInformation(
+ profile: ThreadCpuProfile | RawThreadCpuProfile | ContinuousThreadCpuProfile | RawChunkCpuProfile,
+): ThreadCpuProfile | ContinuousThreadCpuProfile {
if (!isRawThreadCpuProfile(profile)) {
return profile;
}
@@ -57,7 +79,7 @@ export function enrichWithThreadInformation(profile: ThreadCpuProfile | RawThrea
name: THREAD_NAME,
},
},
- };
+ } as ThreadCpuProfile | ContinuousThreadCpuProfile;
}
/**
@@ -88,7 +110,6 @@ export function createProfilingEvent(client: Client, profile: RawThreadCpuProfil
* @param {options}
* @returns {Profile}
*/
-
function createProfilePayload(
client: Client,
cpuProfile: RawThreadCpuProfile,
@@ -146,7 +167,7 @@ function createProfilePayload(
debug_meta: {
images: applyDebugMetadata(client, cpuProfile.resources),
},
- profile: enrichedThreadProfile,
+ profile: enrichedThreadProfile as ThreadCpuProfile,
transaction: {
name: transaction,
id: event_id,
@@ -158,6 +179,82 @@ function createProfilePayload(
return profile;
}
+/**
+ * Create a profile chunk from raw thread profile
+ * @param {RawThreadCpuProfile} cpuProfile
+ * @param {options}
+ * @returns {Profile}
+ */
+function createProfileChunkPayload(
+ client: Client,
+ cpuProfile: RawChunkCpuProfile,
+ {
+ release,
+ environment,
+ start_timestamp,
+ trace_id,
+ profiler_id,
+ chunk_id,
+ }: {
+ release: string;
+ environment: string;
+ start_timestamp: number;
+ trace_id: string | undefined;
+ chunk_id: string;
+ profiler_id: string;
+ },
+): ProfileChunk {
+ // Log a warning if the profile has an invalid traceId (should be uuidv4).
+ // All profiles and transactions are rejected if this is the case and we want to
+ // warn users that this is happening if they enable debug flag
+ if (trace_id && trace_id.length !== 32) {
+ DEBUG_BUILD && logger.log(`[Profiling] Invalid traceId: ${trace_id} on profiled event`);
+ }
+
+ const enrichedThreadProfile = enrichWithThreadInformation(cpuProfile);
+
+ const profile: ProfileChunk = {
+ chunk_id: chunk_id,
+ profiler_id: profiler_id,
+ timestamp: new Date(start_timestamp).toISOString(),
+ platform: 'node',
+ version: CONTINUOUS_FORMAT_VERSION,
+ release: release,
+ environment: environment,
+ measurements: cpuProfile.measurements,
+ debug_meta: {
+ images: applyDebugMetadata(client, cpuProfile.resources),
+ },
+ profile: enrichedThreadProfile as ContinuousThreadCpuProfile,
+ };
+
+ return profile;
+}
+
+/**
+ * Creates a profiling chunk envelope item, if the profile does not pass validation, returns null.
+ */
+export function createProfilingChunkEvent(
+ start_timestamp: number,
+ client: Client,
+ options: { release?: string; environment?: string },
+ profile: RawChunkCpuProfile,
+ identifiers: { trace_id: string | undefined; chunk_id: string; profiler_id: string },
+): ProfileChunk | null {
+ if (!isValidProfileChunk(profile)) {
+ return null;
+ }
+
+ return createProfileChunkPayload(client, profile, {
+ release: options.release ?? '',
+ environment: options.environment ?? '',
+ start_timestamp: start_timestamp,
+ trace_id: identifiers.trace_id ?? '',
+ chunk_id: identifiers.chunk_id,
+ profiler_id: identifiers.profiler_id,
+ });
+}
+
/**
* Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1).
* @param {unknown} rate
@@ -210,6 +307,24 @@ export function isValidProfile(profile: RawThreadCpuProfile): profile is RawThre
return true;
}
+/**
+ * Checks if the profile chunk is valid and can be sent to Sentry.
+ * @param profile
+ * @returns
+ */
+export function isValidProfileChunk(profile: RawChunkCpuProfile): profile is RawChunkCpuProfile {
+ if (profile.samples.length <= 1) {
+ DEBUG_BUILD &&
+ // Log a warning if the profile has less than 2 samples so users can know why
+ // they are not seeing any profiling data and we cant avoid the back and forth
+ // of asking them to provide us with a dump of the profile data.
+ logger.log('[Profiling] Discarding profile chunk because it contains less than 2 samples');
+ return false;
+ }
+
+ return true;
+}
+
/**
* Adds items to envelope if they are not already present - mutates the envelope.
* @param {Envelope} envelope
@@ -262,6 +377,41 @@ export function findProfiledTransactionsFromEnvelope(envelope: Envelope): Event[
return events;
}
+/**
+ * Creates event envelope headers for a profile chunk. This is separate from createEventEnvelopeHeaders util
+ * as the profile chunk does not conform to the sentry event type
+ */
+export function createEventEnvelopeHeaders(
+ sdkInfo: SdkInfo | undefined,
+ tunnel: string | undefined,
+ dsn?: DsnComponents,
+): EventEnvelopeHeaders {
+ return {
+ event_id: uuid4(),
+ sent_at: new Date().toISOString(),
+ ...(sdkInfo && { sdk: sdkInfo }),
+ ...(!!tunnel && dsn && { dsn: dsnToString(dsn) }),
+ };
+}
+
+/**
+ * Creates a standalone profile_chunk envelope.
+ */
+export function makeProfileChunkEnvelope(
+ chunk: ProfileChunk,
+ sdkInfo: SdkInfo | undefined,
+ tunnel: string | undefined,
+ dsn?: DsnComponents,
+): ProfileChunkEnvelope {
+ const profileChunkHeader: ProfileChunkItem[0] = {
+ type: 'profile_chunk',
+ };
+
+ return createEnvelope(createEventEnvelopeHeaders(sdkInfo, tunnel, dsn), [
+ [profileChunkHeader, chunk],
+ ]);
+}
+
const debugIdStackParserCache = new WeakMap>();
/**
diff --git a/packages/profiling-node/test/cpu_profiler.test.ts b/packages/profiling-node/test/cpu_profiler.test.ts
index 8f66a91cb5ef..be12e740510a 100644
--- a/packages/profiling-node/test/cpu_profiler.test.ts
+++ b/packages/profiling-node/test/cpu_profiler.test.ts
@@ -1,5 +1,6 @@
+import type { ContinuousThreadCpuProfile, ThreadCpuProfile } from '@sentry/types';
import { CpuProfilerBindings, PrivateCpuProfilerBindings } from '../src/cpu_profiler';
-import type { RawThreadCpuProfile, ThreadCpuProfile } from '../src/types';
+import type { RawThreadCpuProfile } from '../src/types';
// Required because we test a hypothetical long profile
// and we cannot use advance timers as the c++ relies on
@@ -18,13 +19,16 @@ const fibonacci = (n: number): number => {
};
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
-const profiled = async (name: string, fn: () => void) => {
+const profiled = async (name: string, fn: () => void, format: 0 | 1 = 0) => {
CpuProfilerBindings.startProfiling(name);
await fn();
- return CpuProfilerBindings.stopProfiling(name);
+ return CpuProfilerBindings.stopProfiling(name, format);
};
-const assertValidSamplesAndStacks = (stacks: ThreadCpuProfile['stacks'], samples: ThreadCpuProfile['samples']) => {
+const assertValidSamplesAndStacks = (
+ stacks: ThreadCpuProfile['stacks'],
+ samples: ThreadCpuProfile['samples'] | ContinuousThreadCpuProfile['samples'],
+) => {
expect(stacks.length).toBeGreaterThan(0);
expect(samples.length).toBeGreaterThan(0);
expect(stacks.length <= samples.length).toBe(true);
@@ -68,16 +72,25 @@ describe('Private bindings', () => {
PrivateCpuProfilerBindings.startProfiling('profiled-program');
await wait(100);
expect(() => {
- const profile = PrivateCpuProfilerBindings.stopProfiling('profiled-program', 0, false);
+ const profile = PrivateCpuProfilerBindings.stopProfiling('profiled-program', 0, 0, false);
if (!profile) throw new Error('No profile');
}).not.toThrow();
});
+ it('throws if invalid format is supplied', async () => {
+ PrivateCpuProfilerBindings.startProfiling('profiled-program');
+ await wait(100);
+ expect(() => {
+ const profile = PrivateCpuProfilerBindings.stopProfiling('profiled-program', Number.MAX_SAFE_INTEGER, 0, false);
+ if (!profile) throw new Error('No profile');
+ }).toThrow('StopProfiling expects a valid format type as second argument.');
+ });
+
it('collects resources', async () => {
PrivateCpuProfilerBindings.startProfiling('profiled-program');
await wait(100);
- const profile = PrivateCpuProfilerBindings.stopProfiling('profiled-program', 0, true);
+ const profile = PrivateCpuProfilerBindings.stopProfiling('profiled-program', 0, 0, true);
if (!profile) throw new Error('No profile');
expect(profile.resources.length).toBeGreaterThan(0);
@@ -94,7 +107,7 @@ describe('Private bindings', () => {
PrivateCpuProfilerBindings.startProfiling('profiled-program');
await wait(100);
- const profile = PrivateCpuProfilerBindings.stopProfiling('profiled-program', 0, false);
+ const profile = PrivateCpuProfilerBindings.stopProfiling('profiled-program', 0, 0, false);
if (!profile) throw new Error('No profile');
expect(profile.resources.length).toBe(0);
@@ -159,27 +172,27 @@ describe('Profiler bindings', () => {
CpuProfilerBindings.startProfiling('same-title');
CpuProfilerBindings.startProfiling('same-title');
- const first = CpuProfilerBindings.stopProfiling('same-title');
- const second = CpuProfilerBindings.stopProfiling('same-title');
+ const first = CpuProfilerBindings.stopProfiling('same-title', 0);
+ const second = CpuProfilerBindings.stopProfiling('same-title', 0);
expect(first).not.toBe(null);
expect(second).toBe(null);
});
- it('weird cases', () => {
+ it('multiple calls with same title', () => {
CpuProfilerBindings.startProfiling('same-title');
expect(() => {
- CpuProfilerBindings.stopProfiling('same-title');
- CpuProfilerBindings.stopProfiling('same-title');
+ CpuProfilerBindings.stopProfiling('same-title', 0);
+ CpuProfilerBindings.stopProfiling('same-title', 0);
}).not.toThrow();
});
it('does not crash if stopTransaction is called before startTransaction', () => {
- expect(CpuProfilerBindings.stopProfiling('does not exist')).toBe(null);
+ expect(CpuProfilerBindings.stopProfiling('does not exist', 0)).toBe(null);
});
it('does crash if name is invalid', () => {
- expect(() => CpuProfilerBindings.stopProfiling('')).toThrow();
+ expect(() => CpuProfilerBindings.stopProfiling('', 0)).toThrow();
// @ts-expect-error test invalid input
expect(() => CpuProfilerBindings.stopProfiling(undefined)).toThrow();
// @ts-expect-error test invalid input
@@ -189,8 +202,8 @@ describe('Profiler bindings', () => {
});
it('does not throw if stopTransaction is called before startTransaction', () => {
- expect(CpuProfilerBindings.stopProfiling('does not exist')).toBe(null);
- expect(() => CpuProfilerBindings.stopProfiling('does not exist')).not.toThrow();
+ expect(CpuProfilerBindings.stopProfiling('does not exist', 0)).toBe(null);
+ expect(() => CpuProfilerBindings.stopProfiling('does not exist', 0)).not.toThrow();
});
it('compiles with eager logging by default', async () => {
@@ -202,6 +215,27 @@ describe('Profiler bindings', () => {
expect(profile.profiler_logging_mode).toBe('eager');
});
+ it('chunk format type', async () => {
+ const profile = await profiled(
+ 'non nullable stack',
+ async () => {
+ await wait(1000);
+ fibonacci(36);
+ await wait(1000);
+ },
+ 1,
+ );
+
+ if (!profile) fail('Profile is null');
+
+ for (const sample of profile.samples) {
+ if (!('timestamp' in sample)) {
+ throw new Error(`Sample ${JSON.stringify(sample)} has no timestamp`);
+ }
+ expect(sample.timestamp).toBeDefined();
+ }
+ });
+
it('stacks are not null', async () => {
const profile = await profiled('non nullable stack', async () => {
await wait(1000);
@@ -216,7 +250,7 @@ describe('Profiler bindings', () => {
it('samples at ~99hz', async () => {
CpuProfilerBindings.startProfiling('profile');
await wait(100);
- const profile = CpuProfilerBindings.stopProfiling('profile');
+ const profile = CpuProfilerBindings.stopProfiling('profile', 0);
if (!profile) fail('Profile is null');
@@ -240,7 +274,7 @@ describe('Profiler bindings', () => {
it('collects memory footprint', async () => {
CpuProfilerBindings.startProfiling('profile');
await wait(1000);
- const profile = CpuProfilerBindings.stopProfiling('profile');
+ const profile = CpuProfilerBindings.stopProfiling('profile', 0);
const heap_usage = profile?.measurements['memory_footprint'];
if (!heap_usage) {
@@ -256,7 +290,7 @@ describe('Profiler bindings', () => {
it('collects cpu usage', async () => {
CpuProfilerBindings.startProfiling('profile');
await wait(1000);
- const profile = CpuProfilerBindings.stopProfiling('profile');
+ const profile = CpuProfilerBindings.stopProfiling('profile', 0);
const cpu_usage = profile?.measurements['cpu_usage'];
if (!cpu_usage) {
@@ -272,7 +306,7 @@ describe('Profiler bindings', () => {
it('does not overflow measurement buffer if profile runs longer than 30s', async () => {
CpuProfilerBindings.startProfiling('profile');
await wait(35000);
- const profile = CpuProfilerBindings.stopProfiling('profile');
+ const profile = CpuProfilerBindings.stopProfiling('profile', 0);
expect(profile).not.toBe(null);
expect(profile?.measurements?.['cpu_usage']?.values.length).toBeLessThanOrEqual(300);
expect(profile?.measurements?.['memory_footprint']?.values.length).toBeLessThanOrEqual(300);
diff --git a/packages/profiling-node/test/integration.test.ts b/packages/profiling-node/test/integration.test.ts
index 040ed5297205..92d1018e18d4 100644
--- a/packages/profiling-node/test/integration.test.ts
+++ b/packages/profiling-node/test/integration.test.ts
@@ -35,7 +35,7 @@ describe('ProfilingIntegration', () => {
getTransport: () => transport,
} as unknown as NodeClient;
- integration.setup(client);
+ integration?.setup?.(client);
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(transport.send).not.toHaveBeenCalled();
@@ -54,6 +54,7 @@ describe('ProfilingIntegration', () => {
getOptions: () => {
return {
_metadata: {},
+ profilesSampleRate: 1,
};
},
getDsn: () => {
@@ -64,7 +65,7 @@ describe('ProfilingIntegration', () => {
const spy = jest.spyOn(client, 'on');
- integration.setup(client);
+ integration?.setup?.(client);
expect(spy).toHaveBeenCalledTimes(3);
expect(spy).toHaveBeenCalledWith('spanStart', expect.any(Function));
diff --git a/packages/profiling-node/test/spanProfileUtils.test.ts b/packages/profiling-node/test/spanProfileUtils.test.ts
index 687b6ca60768..9cc2ae58a972 100644
--- a/packages/profiling-node/test/spanProfileUtils.test.ts
+++ b/packages/profiling-node/test/spanProfileUtils.test.ts
@@ -1,10 +1,13 @@
import * as Sentry from '@sentry/node';
import { getMainCarrier } from '@sentry/core';
+import type { NodeClientOptions } from '@sentry/node/build/types/types';
import type { Transport } from '@sentry/types';
import { GLOBAL_OBJ, createEnvelope, logger } from '@sentry/utils';
import { CpuProfilerBindings } from '../src/cpu_profiler';
-import { _nodeProfilingIntegration } from '../src/integration';
+import { type ProfilingIntegration, _nodeProfilingIntegration } from '../src/integration';
+
+jest.setTimeout(10000);
function makeClientWithHooks(): [Sentry.NodeClient, Transport] {
const integration = _nodeProfilingIntegration();
@@ -28,9 +31,49 @@ function makeClientWithHooks(): [Sentry.NodeClient, Transport] {
return [client, client.getTransport() as Transport];
}
+function makeContinuousProfilingClient(): [Sentry.NodeClient, Transport] {
+ const integration = _nodeProfilingIntegration();
+ const client = new Sentry.NodeClient({
+ stackParser: Sentry.defaultStackParser,
+ tracesSampleRate: 1,
+ profilesSampleRate: undefined,
+ debug: true,
+ environment: 'test-environment',
+ dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302',
+ integrations: [integration],
+ transport: _opts =>
+ Sentry.makeNodeTransport({
+ url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302',
+ recordDroppedEvent: () => {
+ return undefined;
+ },
+ }),
+ });
+
+ return [client, client.getTransport() as Transport];
+}
+
+function makeClientOptions(
+ options: Omit,
+): NodeClientOptions {
+ return {
+ stackParser: Sentry.defaultStackParser,
+ integrations: [_nodeProfilingIntegration()],
+ debug: true,
+ transport: _opts =>
+ Sentry.makeNodeTransport({
+ url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302',
+ recordDroppedEvent: () => {
+ return undefined;
+ },
+ }),
+ ...options,
+ };
+}
+
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
-describe('spanProfileUtils', () => {
+describe('automated span instrumentation', () => {
beforeEach(() => {
jest.useRealTimers();
// We will mock the carrier as if it has been initialized by the SDK, else everything is short circuited
@@ -65,6 +108,7 @@ describe('spanProfileUtils', () => {
Sentry.setCurrentClient(client);
client.init();
+ // @ts-expect-error we just mock the return type and ignore the signature
jest.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => {
return {
samples: [
@@ -103,6 +147,7 @@ describe('spanProfileUtils', () => {
Sentry.setCurrentClient(client);
client.init();
+ // @ts-expect-error we just mock the return type and ignore the signature
jest.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => {
return {
samples: [
@@ -248,6 +293,21 @@ describe('spanProfileUtils', () => {
},
});
});
+
+ it('automated span instrumentation does not support continuous profiling', () => {
+ const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling');
+
+ const [client] = makeClientWithHooks();
+ Sentry.setCurrentClient(client);
+ client.init();
+
+ const integration = client.getIntegrationByName('ProfilingIntegration');
+ if (!integration) {
+ throw new Error('Profiling integration not found');
+ }
+ integration._profiler.start();
+ expect(startProfilingSpy).not.toHaveBeenCalled();
+ });
});
it('does not crash if stop is called multiple times', async () => {
@@ -270,6 +330,7 @@ describe('spanProfileUtils', () => {
'Error\n at filename3.js (filename3.js:36:15)': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb',
};
+ // @ts-expect-error we just mock the return type and ignore the signature
jest.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => {
return {
samples: [
@@ -322,3 +383,290 @@ describe('spanProfileUtils', () => {
});
});
});
+
+describe('continuous profiling', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ // We will mock the carrier as if it has been initialized by the SDK, else everything is short circuited
+ getMainCarrier().__SENTRY__ = {};
+ GLOBAL_OBJ._sentryDebugIds = undefined as any;
+ });
+ afterEach(() => {
+ const client = Sentry.getClient();
+ const integration = client?.getIntegrationByName('ProfilingIntegration');
+
+ if (integration) {
+ integration._profiler.stop();
+ }
+
+ jest.clearAllMocks();
+ jest.restoreAllMocks();
+ jest.runAllTimers();
+ delete getMainCarrier().__SENTRY__;
+ });
+
+ it('initializes the continuous profiler and binds the sentry client', () => {
+ const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling');
+
+ const [client] = makeContinuousProfilingClient();
+ Sentry.setCurrentClient(client);
+ client.init();
+
+ expect(startProfilingSpy).not.toHaveBeenCalledTimes(1);
+
+ const integration = client.getIntegrationByName('ProfilingIntegration');
+ if (!integration) {
+ throw new Error('Profiling integration not found');
+ }
+ integration._profiler.start();
+
+ expect(integration._profiler).toBeDefined();
+ expect(integration._profiler['_client']).toBe(client);
+ });
+
+ it('starts a continuous profile', () => {
+ const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling');
+
+ const [client] = makeContinuousProfilingClient();
+ Sentry.setCurrentClient(client);
+ client.init();
+
+ expect(startProfilingSpy).not.toHaveBeenCalledTimes(1);
+ const integration = client.getIntegrationByName('ProfilingIntegration');
+ if (!integration) {
+ throw new Error('Profiling integration not found');
+ }
+ integration._profiler.start();
+ expect(startProfilingSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('multiple calls to start abort previous profile', () => {
+ const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling');
+ const stopProfilingSpy = jest.spyOn(CpuProfilerBindings, 'stopProfiling');
+
+ const [client] = makeContinuousProfilingClient();
+ Sentry.setCurrentClient(client);
+ client.init();
+
+ expect(startProfilingSpy).not.toHaveBeenCalledTimes(1);
+ const integration = client.getIntegrationByName('ProfilingIntegration');
+ if (!integration) {
+ throw new Error('Profiling integration not found');
+ }
+ integration._profiler.start();
+ integration._profiler.start();
+ expect(startProfilingSpy).toHaveBeenCalledTimes(2);
+ expect(stopProfilingSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('restarts a new chunk after previous', async () => {
+ const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling');
+ const stopProfilingSpy = jest.spyOn(CpuProfilerBindings, 'stopProfiling');
+
+ const [client] = makeContinuousProfilingClient();
+ Sentry.setCurrentClient(client);
+ client.init();
+
+ expect(startProfilingSpy).not.toHaveBeenCalledTimes(1);
+ const integration = client.getIntegrationByName('ProfilingIntegration');
+ if (!integration) {
+ throw new Error('Profiling integration not found');
+ }
+ integration._profiler.start();
+
+ jest.advanceTimersByTime(5001);
+ expect(stopProfilingSpy).toHaveBeenCalledTimes(1);
+ expect(startProfilingSpy).toHaveBeenCalledTimes(2);
+ });
+
+ it('stops a continuous profile after interval', async () => {
+ const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling');
+ const stopProfilingSpy = jest.spyOn(CpuProfilerBindings, 'stopProfiling');
+
+ const [client] = makeContinuousProfilingClient();
+ Sentry.setCurrentClient(client);
+ client.init();
+
+ expect(startProfilingSpy).not.toHaveBeenCalledTimes(1);
+ const integration = client.getIntegrationByName('ProfilingIntegration');
+ if (!integration) {
+ throw new Error('Profiling integration not found');
+ }
+ integration._profiler.start();
+
+ jest.advanceTimersByTime(5001);
+ expect(stopProfilingSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('manullly stopping a chunk doesnt restart the profiler', async () => {
+ const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling');
+ const stopProfilingSpy = jest.spyOn(CpuProfilerBindings, 'stopProfiling');
+
+ const [client] = makeContinuousProfilingClient();
+ Sentry.setCurrentClient(client);
+ client.init();
+
+ expect(startProfilingSpy).not.toHaveBeenCalledTimes(1);
+ const integration = client.getIntegrationByName('ProfilingIntegration');
+ if (!integration) {
+ throw new Error('Profiling integration not found');
+ }
+ integration._profiler.start();
+
+ jest.advanceTimersByTime(1000);
+
+ integration._profiler.stop();
+ expect(stopProfilingSpy).toHaveBeenCalledTimes(1);
+
+ jest.advanceTimersByTime(1000);
+ expect(startProfilingSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('continuous mode does not instrument spans', () => {
+ const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling');
+
+ const [client] = makeContinuousProfilingClient();
+ Sentry.setCurrentClient(client);
+ client.init();
+
+ Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' });
+ expect(startProfilingSpy).not.toHaveBeenCalled();
+ });
+
+ it('sends as profile_chunk envelope type', async () => {
+ // @ts-expect-error we just mock the return type and ignore the signature
+ jest.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => {
+ return {
+ samples: [
+ {
+ stack_id: 0,
+ thread_id: '0',
+ elapsed_since_start_ns: '10',
+ },
+ {
+ stack_id: 0,
+ thread_id: '0',
+ elapsed_since_start_ns: '10',
+ },
+ ],
+ measurements: {},
+ stacks: [[0]],
+ frames: [],
+ resources: [],
+ profiler_logging_mode: 'lazy',
+ };
+ });
+
+ const [client, transport] = makeContinuousProfilingClient();
+ Sentry.setCurrentClient(client);
+ client.init();
+
+ const transportSpy = jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve({}));
+
+ const integration = client.getIntegrationByName('ProfilingIntegration');
+ if (!integration) {
+ throw new Error('Profiling integration not found');
+ }
+ integration._profiler.start();
+ jest.advanceTimersByTime(1000);
+ integration._profiler.stop();
+ jest.advanceTimersByTime(1000);
+
+ expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0].type).toBe('profile_chunk');
+ });
+});
+
+describe('span profiling mode', () => {
+ it.each([
+ ['profilesSampleRate=1', makeClientOptions({ profilesSampleRate: 1 })],
+ ['profilesSampler is defined', makeClientOptions({ profilesSampler: () => 1 })],
+ ])('%s', async (_label, options) => {
+ const logSpy = jest.spyOn(logger, 'log');
+ const client = new Sentry.NodeClient({
+ ...options,
+ dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302',
+ tracesSampleRate: 1,
+ transport: _opts =>
+ Sentry.makeNodeTransport({
+ url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302',
+ recordDroppedEvent: () => {
+ return undefined;
+ },
+ }),
+ integrations: [_nodeProfilingIntegration()],
+ });
+
+ Sentry.setCurrentClient(client);
+ client.init();
+
+ const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling');
+ const transport = client.getTransport();
+
+ if (!transport) {
+ throw new Error('Transport not found');
+ }
+
+ jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve({}));
+ Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' });
+
+ expect(startProfilingSpy).toHaveBeenCalled();
+
+ const integration = client.getIntegrationByName('ProfilingIntegration');
+
+ if (!integration) {
+ throw new Error('Profiling integration not found');
+ }
+
+ integration._profiler.start();
+ expect(logSpy).toHaveBeenLastCalledWith('[Profiling] Profiler was never attached to the client.');
+ });
+});
+describe('continuous profiling mode', () => {
+ it.each([
+ ['profilesSampleRate=0', makeClientOptions({ profilesSampleRate: 0 })],
+ ['profilesSampleRate=undefined', makeClientOptions({ profilesSampleRate: undefined })],
+ // @ts-expect-error test invalid value
+ ['profilesSampleRate=null', makeClientOptions({ profilesSampleRate: null })],
+ [
+ 'profilesSampler is not defined and profilesSampleRate is not set',
+ makeClientOptions({ profilesSampler: undefined, profilesSampleRate: 0 }),
+ ],
+ ])('%s', async (_label, options) => {
+ const client = new Sentry.NodeClient({
+ ...options,
+ dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302',
+ tracesSampleRate: 1,
+ transport: _opts =>
+ Sentry.makeNodeTransport({
+ url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302',
+ recordDroppedEvent: () => {
+ return undefined;
+ },
+ }),
+ integrations: [_nodeProfilingIntegration()],
+ });
+
+ Sentry.setCurrentClient(client);
+ client.init();
+
+ const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling');
+ const transport = client.getTransport();
+
+ if (!transport) {
+ throw new Error('Transport not found');
+ }
+
+ jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve({}));
+
+ const integration = client.getIntegrationByName('ProfilingIntegration');
+ if (!integration) {
+ throw new Error('Profiling integration not found');
+ }
+ integration._profiler.start();
+ const callCount = startProfilingSpy.mock.calls.length;
+ expect(startProfilingSpy).toHaveBeenCalled();
+
+ Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' });
+ expect(startProfilingSpy).toHaveBeenCalledTimes(callCount);
+ });
+});
diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts
index d7089fbb0225..29e8fc123b16 100644
--- a/packages/types/src/envelope.ts
+++ b/packages/types/src/envelope.ts
@@ -4,7 +4,7 @@ import type { ClientReport } from './clientreport';
import type { DsnComponents } from './dsn';
import type { Event } from './event';
import type { FeedbackEvent, UserFeedback } from './feedback';
-import type { Profile } from './profiling';
+import type { Profile, ProfileChunk } from './profiling';
import type { ReplayEvent, ReplayRecordingData } from './replay';
import type { SdkInfo } from './sdkinfo';
import type { SerializedSession, SessionAggregates } from './session';
@@ -36,6 +36,7 @@ export type EnvelopeItemType =
| 'attachment'
| 'event'
| 'profile'
+ | 'profile_chunk'
| 'replay_event'
| 'replay_recording'
| 'check_in'
@@ -79,8 +80,9 @@ type ClientReportItemHeaders = { type: 'client_report' };
type ReplayEventItemHeaders = { type: 'replay_event' };
type ReplayRecordingItemHeaders = { type: 'replay_recording'; length: number };
type CheckInItemHeaders = { type: 'check_in' };
-type StatsdItemHeaders = { type: 'statsd'; length: number };
type ProfileItemHeaders = { type: 'profile' };
+type ProfileChunkItemHeaders = { type: 'profile_chunk' };
+type StatsdItemHeaders = { type: 'statsd'; length: number };
type SpanItemHeaders = { type: 'span' };
export type EventItem = BaseEnvelopeItem;
@@ -96,6 +98,7 @@ type ReplayRecordingItem = BaseEnvelopeItem;
export type FeedbackItem = BaseEnvelopeItem;
export type ProfileItem = BaseEnvelopeItem;
+export type ProfileChunkItem = BaseEnvelopeItem;
export type SpanItem = BaseEnvelopeItem>;
export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: DynamicSamplingContext };
@@ -116,13 +119,16 @@ export type ReplayEnvelope = [ReplayEnvelopeHeaders, [ReplayEventItem, ReplayRec
export type CheckInEnvelope = BaseEnvelope;
export type StatsdEnvelope = BaseEnvelope;
export type SpanEnvelope = BaseEnvelope;
+export type ProfileChunkEnvelope = BaseEnvelope;
export type Envelope =
| EventEnvelope
| SessionEnvelope
| ClientReportEnvelope
+ | ProfileChunkEnvelope
| ReplayEnvelope
| CheckInEnvelope
| StatsdEnvelope
| SpanEnvelope;
+
export type EnvelopeItem = Envelope[1][number];
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index c90b7841f9ff..8fbfd37e95ab 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -45,6 +45,7 @@ export type {
StatsdItem,
StatsdEnvelope,
ProfileItem,
+ ProfileChunkEnvelope,
SpanEnvelope,
SpanItem,
} from './envelope';
@@ -69,7 +70,9 @@ export type {
ThreadCpuStack,
ThreadCpuFrame,
ThreadCpuProfile,
+ ContinuousThreadCpuProfile,
Profile,
+ ProfileChunk,
} from './profiling';
export type { ReplayEvent, ReplayRecordingData, ReplayRecordingMode } from './replay';
export type {
diff --git a/packages/types/src/profiling.ts b/packages/types/src/profiling.ts
index 3650500fcd7b..5161b6b64b2e 100644
--- a/packages/types/src/profiling.ts
+++ b/packages/types/src/profiling.ts
@@ -12,6 +12,13 @@ export interface ThreadCpuSample {
elapsed_since_start_ns: string;
}
+export interface ContinuousThreadCpuSample {
+ stack_id: StackId;
+ thread_id: ThreadId;
+ queue_address?: string;
+ timestamp: number;
+}
+
export type ThreadCpuStack = FrameId[];
export type ThreadCpuFrame = {
@@ -34,7 +41,37 @@ export interface ThreadCpuProfile {
queue_metadata?: Record;
}
-export interface Profile {
+export interface ContinuousThreadCpuProfile {
+ samples: ContinuousThreadCpuSample[];
+ stacks: ThreadCpuStack[];
+ frames: ThreadCpuFrame[];
+ thread_metadata: Record;
+ queue_metadata?: Record;
+}
+
+interface BaseProfile {
+ timestamp: string;
+ version: string;
+ release: string;
+ environment: string;
+ platform: string;
+ profile: T;
+ debug_meta?: {
+ images: DebugImage[];
+ };
+ measurements?: Record<
+ string,
+ {
+ unit: MeasurementUnit;
+ values: {
+ elapsed_since_start_ns: number;
+ value: number;
+ }[];
+ }
+ >;
+}
+
+export interface Profile extends BaseProfile {
event_id: string;
version: string;
os: {
@@ -86,3 +123,8 @@ export interface Profile {
}
>;
}
+
+export interface ProfileChunk extends BaseProfile {
+ chunk_id: string;
+ profiler_id: string;
+}
diff --git a/packages/utils/src/envelope.ts b/packages/utils/src/envelope.ts
index 17c40bed92ad..8bf29788edf0 100644
--- a/packages/utils/src/envelope.ts
+++ b/packages/utils/src/envelope.ts
@@ -217,6 +217,7 @@ const ITEM_TYPE_TO_DATA_CATEGORY_MAP: Record = {
client_report: 'internal',
user_report: 'default',
profile: 'profile',
+ profile_chunk: 'profile',
replay_event: 'replay',
replay_recording: 'replay',
check_in: 'monitor',
From d6ec881083cf66efd498ddb37abe274b89962361 Mon Sep 17 00:00:00 2001
From: Francesco Novy
Date: Fri, 7 Jun 2024 09:39:21 +0200
Subject: [PATCH 03/31] fix(browser): Fix INP span creation & transaction
tagging (#12372)
Instead of https://github.com/getsentry/sentry-javascript/pull/12358,
this is a simpler change which ensures we pick the transaction from the
scope instead.
I also added tests for the various different scenarios, to ensure we see
how they behave:
1. INP is emitted _during_ pageload (span is active)
2. INP is emitted _after_ pageload
a. Pageload is parametrized (route)
b. Pageload is unparametrized (URL)
When the pageload is unparametrized (default browser SDK), the
transaction is not added to the DSC envelope header (which is correct
and also what we do in other places). it is _always_ added to the span
attributes now, though. If no span is active, it will use
transactionName from the last active pageload/navigation span.
There may be edge cases where this is not 100% correct (e.g. if the INP
span is only emitted once the pageload is done but another navigation
already started) but IMHO these are more edge cases and this change is
probably fine for now..?
(While at it, I also added an origin to the INP spans)
---
.size-limit.js | 20 +--
.../metrics/web-vitals-inp-late/init.js | 27 ++++
.../metrics/web-vitals-inp-late/subject.js | 18 +++
.../metrics/web-vitals-inp-late/template.html | 12 ++
.../metrics/web-vitals-inp-late/test.ts | 98 +++++++++++
.../web-vitals-inp-parametrized-late/init.js | 27 ++++
.../subject.js | 18 +++
.../template.html | 12 ++
.../web-vitals-inp-parametrized-late/test.ts | 102 ++++++++++++
.../web-vitals-inp-parametrized/init.js | 27 ++++
.../web-vitals-inp-parametrized/subject.js | 18 +++
.../web-vitals-inp-parametrized/template.html | 12 ++
.../web-vitals-inp-parametrized/test.ts | 99 ++++++++++++
.../tracing/metrics/web-vitals-inp/init.js | 14 +-
.../tracing/metrics/web-vitals-inp/test.ts | 153 +++++++++---------
packages/browser-utils/src/index.ts | 1 +
.../src/metrics/browserMetrics.ts | 2 +-
packages/browser-utils/src/metrics/inp.ts | 63 +++++++-
.../browser-utils/src/metrics/instrument.ts | 15 +-
.../src/tracing/browserTracingIntegration.ts | 14 +-
20 files changed, 660 insertions(+), 92 deletions(-)
create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/init.js
create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/subject.js
create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/template.html
create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts
create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/init.js
create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/subject.js
create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/template.html
create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts
create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/init.js
create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/subject.js
create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/template.html
create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts
diff --git a/.size-limit.js b/.size-limit.js
index f9b62e7198e9..ac2b8591254a 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -15,14 +15,14 @@ module.exports = [
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration'),
gzip: true,
- limit: '34 KB',
+ limit: '35 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay)',
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration'),
gzip: true,
- limit: '71 KB',
+ limit: '72 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay) - with treeshaking flags',
@@ -48,14 +48,14 @@ module.exports = [
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'),
gzip: true,
- limit: '75 KB',
+ limit: '76 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay, Feedback)',
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration'),
gzip: true,
- limit: '87 KB',
+ limit: '89 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay, Feedback, metrics)',
@@ -69,21 +69,21 @@ module.exports = [
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'metrics'),
gzip: true,
- limit: '40 KB',
+ limit: '30 KB',
},
{
name: '@sentry/browser (incl. Feedback)',
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'feedbackIntegration'),
gzip: true,
- limit: '40 KB',
+ limit: '41 KB',
},
{
name: '@sentry/browser (incl. sendFeedback)',
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'sendFeedback'),
gzip: true,
- limit: '28 KB',
+ limit: '29 KB',
},
{
name: '@sentry/browser (incl. FeedbackAsync)',
@@ -107,7 +107,7 @@ module.exports = [
import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'),
ignore: ['react/jsx-runtime'],
gzip: true,
- limit: '37 KB',
+ limit: '38 KB',
},
// Vue SDK (ESM)
{
@@ -143,7 +143,7 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing)',
path: createCDNPath('bundle.tracing.min.js'),
gzip: true,
- limit: '36 KB',
+ limit: '37 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay)',
@@ -193,7 +193,7 @@ module.exports = [
import: createImport('init'),
ignore: ['next/router', 'next/constants'],
gzip: true,
- limit: '37 KB',
+ limit: '38 KB',
},
// SvelteKit SDK (ESM)
{
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/init.js
new file mode 100644
index 000000000000..1044a4b68bda
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/init.js
@@ -0,0 +1,27 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ Sentry.browserTracingIntegration({
+ idleTimeout: 1000,
+ enableLongTask: false,
+ enableInp: true,
+ instrumentPageLoad: false,
+ instrumentNavigation: false,
+ }),
+ ],
+ tracesSampleRate: 1,
+});
+
+const client = Sentry.getClient();
+
+// Force page load transaction name to a testable value
+Sentry.startBrowserTracingPageLoadSpan(client, {
+ name: 'test-url',
+ attributes: {
+ [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
+ },
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/subject.js
new file mode 100644
index 000000000000..ed6db5b5afe2
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/subject.js
@@ -0,0 +1,18 @@
+const blockUI = (delay = 70) => e => {
+ const startTime = Date.now();
+
+ function getElasped() {
+ const time = Date.now();
+ return time - startTime;
+ }
+
+ while (getElasped() < delay) {
+ //
+ }
+
+ e.target.classList.add('clicked');
+};
+
+document.querySelector('[data-test-id=not-so-slow-button]').addEventListener('click', blockUI(300));
+document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(450));
+document.querySelector('[data-test-id=normal-button]').addEventListener('click', blockUI());
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/template.html
new file mode 100644
index 000000000000..25c6920f07e2
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/template.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ Rendered Before Long Task
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts
new file mode 100644
index 000000000000..1ec7ec50998a
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts
@@ -0,0 +1,98 @@
+import { expect } from '@playwright/test';
+import type { Event as SentryEvent, SpanEnvelope } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import {
+ getFirstSentryEnvelopeRequest,
+ getMultipleSentryEnvelopeRequests,
+ properFullEnvelopeRequestParser,
+ shouldSkipTracingTest,
+} from '../../../../utils/helpers';
+
+sentryTest('should capture an INP click event span after pageload', async ({ browserName, getLocalTestPath, page }) => {
+ const supportedBrowsers = ['chromium'];
+
+ if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
+ 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 getLocalTestPath({ testDir: __dirname });
+
+ await page.goto(url);
+ await getFirstSentryEnvelopeRequest(page); // wait for page load
+
+ const spanEnvelopePromise = getMultipleSentryEnvelopeRequests(
+ page,
+ 1,
+ { envelopeType: 'span' },
+ properFullEnvelopeRequestParser,
+ );
+
+ await page.locator('[data-test-id=normal-button]').click();
+ await page.locator('.clicked[data-test-id=normal-button]').isVisible();
+
+ await page.waitForTimeout(500);
+
+ // Page hide to trigger INP
+ await page.evaluate(() => {
+ window.dispatchEvent(new Event('pagehide'));
+ });
+
+ // Get the INP span envelope
+ const spanEnvelope = (await spanEnvelopePromise)[0];
+
+ const spanEnvelopeHeaders = spanEnvelope[0];
+ const spanEnvelopeItem = spanEnvelope[1][0][1];
+
+ const traceId = spanEnvelopeHeaders.trace!.trace_id;
+ expect(traceId).toMatch(/[a-f0-9]{32}/);
+
+ expect(spanEnvelopeHeaders).toEqual({
+ sent_at: expect.any(String),
+ trace: {
+ environment: 'production',
+ public_key: 'public',
+ sample_rate: '1',
+ sampled: 'true',
+ trace_id: traceId,
+ },
+ });
+
+ const inpValue = spanEnvelopeItem.measurements?.inp.value;
+ expect(inpValue).toBeGreaterThan(0);
+
+ expect(spanEnvelopeItem).toEqual({
+ data: {
+ 'sentry.exclusive_time': inpValue,
+ 'sentry.op': 'ui.interaction.click',
+ 'sentry.origin': 'auto.http.browser.inp',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'custom',
+ transaction: 'test-url',
+ },
+ measurements: {
+ inp: {
+ unit: 'millisecond',
+ value: inpValue,
+ },
+ },
+ description: 'body > NormalButton',
+ exclusive_time: inpValue,
+ op: 'ui.interaction.click',
+ origin: 'auto.http.browser.inp',
+ is_segment: true,
+ segment_id: spanEnvelopeItem.span_id,
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: traceId,
+ });
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/init.js
new file mode 100644
index 000000000000..895e6f60ff42
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/init.js
@@ -0,0 +1,27 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ Sentry.browserTracingIntegration({
+ idleTimeout: 1000,
+ enableLongTask: false,
+ enableInp: true,
+ instrumentPageLoad: false,
+ instrumentNavigation: false,
+ }),
+ ],
+ tracesSampleRate: 1,
+});
+
+const client = Sentry.getClient();
+
+// Force page load transaction name to a testable value
+Sentry.startBrowserTracingPageLoadSpan(client, {
+ name: 'test-route',
+ attributes: {
+ [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ },
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/subject.js
new file mode 100644
index 000000000000..ed6db5b5afe2
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/subject.js
@@ -0,0 +1,18 @@
+const blockUI = (delay = 70) => e => {
+ const startTime = Date.now();
+
+ function getElasped() {
+ const time = Date.now();
+ return time - startTime;
+ }
+
+ while (getElasped() < delay) {
+ //
+ }
+
+ e.target.classList.add('clicked');
+};
+
+document.querySelector('[data-test-id=not-so-slow-button]').addEventListener('click', blockUI(300));
+document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(450));
+document.querySelector('[data-test-id=normal-button]').addEventListener('click', blockUI());
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/template.html
new file mode 100644
index 000000000000..25c6920f07e2
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/template.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ Rendered Before Long Task
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts
new file mode 100644
index 000000000000..1354c373253e
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts
@@ -0,0 +1,102 @@
+import { expect } from '@playwright/test';
+import type { Event as SentryEvent, SpanEnvelope } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import {
+ getFirstSentryEnvelopeRequest,
+ getMultipleSentryEnvelopeRequests,
+ properFullEnvelopeRequestParser,
+ shouldSkipTracingTest,
+} from '../../../../utils/helpers';
+
+sentryTest(
+ 'should capture an INP click event span after pageload for a parametrized transaction',
+ async ({ browserName, getLocalTestPath, page }) => {
+ const supportedBrowsers = ['chromium'];
+
+ if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
+ 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 getLocalTestPath({ testDir: __dirname });
+
+ await page.goto(url);
+ await getFirstSentryEnvelopeRequest(page); // wait for page load
+
+ const spanEnvelopePromise = getMultipleSentryEnvelopeRequests(
+ page,
+ 1,
+ { envelopeType: 'span' },
+ properFullEnvelopeRequestParser,
+ );
+
+ await page.locator('[data-test-id=normal-button]').click();
+ await page.locator('.clicked[data-test-id=normal-button]').isVisible();
+
+ await page.waitForTimeout(500);
+
+ // Page hide to trigger INP
+ await page.evaluate(() => {
+ window.dispatchEvent(new Event('pagehide'));
+ });
+
+ // Get the INP span envelope
+ const spanEnvelope = (await spanEnvelopePromise)[0];
+
+ const spanEnvelopeHeaders = spanEnvelope[0];
+ const spanEnvelopeItem = spanEnvelope[1][0][1];
+
+ const traceId = spanEnvelopeHeaders.trace!.trace_id;
+ expect(traceId).toMatch(/[a-f0-9]{32}/);
+
+ expect(spanEnvelopeHeaders).toEqual({
+ sent_at: expect.any(String),
+ trace: {
+ environment: 'production',
+ public_key: 'public',
+ sample_rate: '1',
+ sampled: 'true',
+ trace_id: traceId,
+ transaction: 'test-route',
+ },
+ });
+
+ const inpValue = spanEnvelopeItem.measurements?.inp.value;
+ expect(inpValue).toBeGreaterThan(0);
+
+ expect(spanEnvelopeItem).toEqual({
+ data: {
+ 'sentry.exclusive_time': inpValue,
+ 'sentry.op': 'ui.interaction.click',
+ 'sentry.origin': 'auto.http.browser.inp',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'custom',
+ transaction: 'test-route',
+ },
+ measurements: {
+ inp: {
+ unit: 'millisecond',
+ value: inpValue,
+ },
+ },
+ description: 'body > NormalButton',
+ exclusive_time: inpValue,
+ op: 'ui.interaction.click',
+ origin: 'auto.http.browser.inp',
+ is_segment: true,
+ segment_id: spanEnvelopeItem.span_id,
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: traceId,
+ });
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/init.js
new file mode 100644
index 000000000000..fa9619209dfe
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/init.js
@@ -0,0 +1,27 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ Sentry.browserTracingIntegration({
+ idleTimeout: 4000,
+ enableLongTask: false,
+ enableInp: true,
+ instrumentPageLoad: false,
+ instrumentNavigation: false,
+ }),
+ ],
+ tracesSampleRate: 1,
+});
+
+const client = Sentry.getClient();
+
+// Force page load transaction name to a testable value
+Sentry.startBrowserTracingPageLoadSpan(client, {
+ name: 'test-route',
+ attributes: {
+ [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ },
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/subject.js
new file mode 100644
index 000000000000..ed6db5b5afe2
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/subject.js
@@ -0,0 +1,18 @@
+const blockUI = (delay = 70) => e => {
+ const startTime = Date.now();
+
+ function getElasped() {
+ const time = Date.now();
+ return time - startTime;
+ }
+
+ while (getElasped() < delay) {
+ //
+ }
+
+ e.target.classList.add('clicked');
+};
+
+document.querySelector('[data-test-id=not-so-slow-button]').addEventListener('click', blockUI(300));
+document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(450));
+document.querySelector('[data-test-id=normal-button]').addEventListener('click', blockUI());
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/template.html
new file mode 100644
index 000000000000..25c6920f07e2
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/template.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ Rendered Before Long Task
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts
new file mode 100644
index 000000000000..248cb7d1e510
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts
@@ -0,0 +1,99 @@
+import { expect } from '@playwright/test';
+import type { SpanEnvelope } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import {
+ getMultipleSentryEnvelopeRequests,
+ properFullEnvelopeRequestParser,
+ shouldSkipTracingTest,
+} from '../../../../utils/helpers';
+
+sentryTest(
+ 'should capture an INP click event span during pageload for a parametrized transaction',
+ async ({ browserName, getLocalTestPath, page }) => {
+ const supportedBrowsers = ['chromium'];
+
+ if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
+ 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 getLocalTestPath({ testDir: __dirname });
+
+ await page.goto(url);
+
+ const spanEnvelopePromise = getMultipleSentryEnvelopeRequests(
+ page,
+ 1,
+ { envelopeType: 'span' },
+ properFullEnvelopeRequestParser,
+ );
+
+ await page.locator('[data-test-id=normal-button]').click();
+ await page.locator('.clicked[data-test-id=normal-button]').isVisible();
+
+ await page.waitForTimeout(500);
+
+ // Page hide to trigger INP
+ await page.evaluate(() => {
+ window.dispatchEvent(new Event('pagehide'));
+ });
+
+ // Get the INP span envelope
+ const spanEnvelope = (await spanEnvelopePromise)[0];
+
+ const spanEnvelopeHeaders = spanEnvelope[0];
+ const spanEnvelopeItem = spanEnvelope[1][0][1];
+
+ const traceId = spanEnvelopeHeaders.trace!.trace_id;
+ expect(traceId).toMatch(/[a-f0-9]{32}/);
+
+ expect(spanEnvelopeHeaders).toEqual({
+ sent_at: expect.any(String),
+ trace: {
+ environment: 'production',
+ public_key: 'public',
+ sample_rate: '1',
+ sampled: 'true',
+ trace_id: traceId,
+ transaction: 'test-route',
+ },
+ });
+
+ const inpValue = spanEnvelopeItem.measurements?.inp.value;
+ expect(inpValue).toBeGreaterThan(0);
+
+ expect(spanEnvelopeItem).toEqual({
+ data: {
+ 'sentry.exclusive_time': inpValue,
+ 'sentry.op': 'ui.interaction.click',
+ 'sentry.origin': 'auto.http.browser.inp',
+ transaction: 'test-route',
+ },
+ measurements: {
+ inp: {
+ unit: 'millisecond',
+ value: inpValue,
+ },
+ },
+ description: 'body > NormalButton',
+ exclusive_time: inpValue,
+ op: 'ui.interaction.click',
+ origin: 'auto.http.browser.inp',
+ segment_id: expect.not.stringMatching(spanEnvelopeItem.span_id!),
+ // parent is the pageload span
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: traceId,
+ });
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/init.js
index b558562e4cd4..a941877ff88e 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/init.js
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/init.js
@@ -6,10 +6,22 @@ Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [
Sentry.browserTracingIntegration({
- idleTimeout: 1000,
+ idleTimeout: 4000,
enableLongTask: false,
enableInp: true,
+ instrumentPageLoad: false,
+ instrumentNavigation: false,
}),
],
tracesSampleRate: 1,
});
+
+const client = Sentry.getClient();
+
+// Force page load transaction name to a testable value
+Sentry.startBrowserTracingPageLoadSpan(client, {
+ name: 'test-url',
+ attributes: {
+ [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
+ },
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts
index 582508f7a584..3f9684cf7f2a 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts
@@ -9,92 +9,95 @@ import {
shouldSkipTracingTest,
} from '../../../../utils/helpers';
-sentryTest('should capture an INP click event span.', async ({ browserName, getLocalTestPath, page }) => {
- const supportedBrowsers = ['chromium'];
-
- if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
- 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' }),
- });
- });
+sentryTest(
+ 'should capture an INP click event span during pageload',
+ async ({ browserName, getLocalTestPath, page }) => {
+ const supportedBrowsers = ['chromium'];
- const url = await getLocalTestPath({ testDir: __dirname });
+ if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
+ sentryTest.skip();
+ }
- await page.goto(url);
- await getFirstSentryEnvelopeRequest(page); // wait for page load
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
- const spanEnvelopePromise = getMultipleSentryEnvelopeRequests(
- page,
- 1,
- { envelopeType: 'span' },
- properFullEnvelopeRequestParser,
- );
+ const url = await getLocalTestPath({ testDir: __dirname });
- await page.locator('[data-test-id=normal-button]').click();
- await page.locator('.clicked[data-test-id=normal-button]').isVisible();
+ await page.goto(url);
- await page.waitForTimeout(500);
+ const spanEnvelopePromise = getMultipleSentryEnvelopeRequests(
+ page,
+ 1,
+ { envelopeType: 'span' },
+ properFullEnvelopeRequestParser,
+ );
- // Page hide to trigger INP
- await page.evaluate(() => {
- window.dispatchEvent(new Event('pagehide'));
- });
+ await page.locator('[data-test-id=normal-button]').click();
+ await page.locator('.clicked[data-test-id=normal-button]').isVisible();
+
+ await page.waitForTimeout(500);
- // Get the INP span envelope
- const spanEnvelope = (await spanEnvelopePromise)[0];
+ // Page hide to trigger INP
+ await page.evaluate(() => {
+ window.dispatchEvent(new Event('pagehide'));
+ });
- const spanEnvelopeHeaders = spanEnvelope[0];
- const spanEnvelopeItem = spanEnvelope[1][0][1];
+ // Get the INP span envelope
+ const spanEnvelope = (await spanEnvelopePromise)[0];
+
+ const spanEnvelopeHeaders = spanEnvelope[0];
+ const spanEnvelopeItem = spanEnvelope[1][0][1];
+
+ const traceId = spanEnvelopeHeaders.trace!.trace_id;
+ expect(traceId).toMatch(/[a-f0-9]{32}/);
+
+ expect(spanEnvelopeHeaders).toEqual({
+ sent_at: expect.any(String),
+ trace: {
+ environment: 'production',
+ public_key: 'public',
+ sample_rate: '1',
+ sampled: 'true',
+ trace_id: traceId,
+ // no transaction, because span source is URL
+ },
+ });
- const traceId = spanEnvelopeHeaders.trace!.trace_id;
- expect(traceId).toMatch(/[a-f0-9]{32}/);
+ const inpValue = spanEnvelopeItem.measurements?.inp.value;
+ expect(inpValue).toBeGreaterThan(0);
- expect(spanEnvelopeHeaders).toEqual({
- sent_at: expect.any(String),
- trace: {
- environment: 'production',
- public_key: 'public',
- sample_rate: '1',
- sampled: 'true',
- trace_id: traceId,
- },
- });
-
- const inpValue = spanEnvelopeItem.measurements?.inp.value;
- expect(inpValue).toBeGreaterThan(0);
-
- expect(spanEnvelopeItem).toEqual({
- data: {
- 'sentry.exclusive_time': inpValue,
- 'sentry.op': 'ui.interaction.click',
- 'sentry.origin': 'manual',
- 'sentry.sample_rate': 1,
- 'sentry.source': 'custom',
- },
- measurements: {
- inp: {
- unit: 'millisecond',
- value: inpValue,
+ expect(spanEnvelopeItem).toEqual({
+ data: {
+ 'sentry.exclusive_time': inpValue,
+ 'sentry.op': 'ui.interaction.click',
+ 'sentry.origin': 'auto.http.browser.inp',
+ transaction: 'test-url',
+ },
+ measurements: {
+ inp: {
+ unit: 'millisecond',
+ value: inpValue,
+ },
},
- },
- description: 'body > NormalButton',
- exclusive_time: inpValue,
- op: 'ui.interaction.click',
- origin: 'manual',
- is_segment: true,
- segment_id: spanEnvelopeItem.span_id,
- span_id: expect.stringMatching(/[a-f0-9]{16}/),
- start_timestamp: expect.any(Number),
- timestamp: expect.any(Number),
- trace_id: traceId,
- });
-});
+ description: 'body > NormalButton',
+ exclusive_time: inpValue,
+ op: 'ui.interaction.click',
+ origin: 'auto.http.browser.inp',
+ segment_id: expect.not.stringMatching(spanEnvelopeItem.span_id!),
+ // Parent is the pageload span
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: traceId,
+ });
+ },
+);
sentryTest(
'should choose the slowest interaction click event when INP is triggered.',
diff --git a/packages/browser-utils/src/index.ts b/packages/browser-utils/src/index.ts
index 7e7c4d0a387a..8e48e0988db9 100644
--- a/packages/browser-utils/src/index.ts
+++ b/packages/browser-utils/src/index.ts
@@ -12,6 +12,7 @@ export {
startTrackingLongTasks,
startTrackingWebVitals,
startTrackingINP,
+ registerInpInteractionListener,
} from './metrics/browserMetrics';
export { addClickKeypressInstrumentationHandler } from './instrument/dom';
diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts
index 877b0612fb06..bb9969f1fde6 100644
--- a/packages/browser-utils/src/metrics/browserMetrics.ts
+++ b/packages/browser-utils/src/metrics/browserMetrics.ts
@@ -157,7 +157,7 @@ export function startTrackingInteractions(): void {
});
}
-export { startTrackingINP } from './inp';
+export { startTrackingINP, registerInpInteractionListener } from './inp';
/** Starts tracking the Cumulative Layout Shift on the current page. */
function _trackCLS(): () => void {
diff --git a/packages/browser-utils/src/metrics/inp.ts b/packages/browser-utils/src/metrics/inp.ts
index c6c0113d6be3..00e524c048b6 100644
--- a/packages/browser-utils/src/metrics/inp.ts
+++ b/packages/browser-utils/src/metrics/inp.ts
@@ -2,6 +2,7 @@ import {
SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT,
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
getActiveSpan,
getClient,
getCurrentScope,
@@ -11,9 +12,21 @@ import {
} from '@sentry/core';
import type { Integration, SpanAttributes } from '@sentry/types';
import { browserPerformanceTimeOrigin, dropUndefinedKeys, htmlTreeAsString } from '@sentry/utils';
-import { addInpInstrumentationHandler } from './instrument';
+import {
+ addInpInstrumentationHandler,
+ addPerformanceInstrumentationHandler,
+ isPerformanceEventTiming,
+} from './instrument';
import { getBrowserPerformanceAPI, msToSec } from './utils';
+// We only care about name here
+interface PartialRouteInfo {
+ name: string | undefined;
+}
+
+const LAST_INTERACTIONS: number[] = [];
+const INTERACTIONS_ROUTE_MAP = new Map();
+
/**
* Start tracking INP webvital events.
*/
@@ -73,6 +86,7 @@ function _trackINP(): () => void {
return;
}
+ const { interactionId } = entry;
const interactionType = INP_ENTRY_MAP[entry.name];
const options = client.getOptions();
@@ -83,7 +97,15 @@ function _trackINP(): () => void {
const activeSpan = getActiveSpan();
const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;
- const routeName = rootSpan ? spanToJSON(rootSpan).description : undefined;
+ // We first try to lookup the route name from our INTERACTIONS_ROUTE_MAP,
+ // where we cache the route per interactionId
+ const cachedRouteName = interactionId != null ? INTERACTIONS_ROUTE_MAP.get(interactionId) : undefined;
+
+ // Else, we try to use the active span.
+ // Finally, we fall back to look at the transactionName on the scope
+ const routeName =
+ cachedRouteName || (rootSpan ? spanToJSON(rootSpan).description : scope.getScopeData().transactionName);
+
const user = scope.getUser();
// We need to get the replay, user, and activeTransaction from the current scope
@@ -107,6 +129,7 @@ function _trackINP(): () => void {
environment: options.environment,
transaction: routeName,
[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: metric.value,
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser.inp',
user: userDisplay || undefined,
profile_id: profileId || undefined,
replay_id: replayId || undefined,
@@ -130,3 +153,39 @@ function _trackINP(): () => void {
span.end(startTime + duration);
});
}
+
+/** Register a listener to cache route information for INP interactions. */
+export function registerInpInteractionListener(latestRoute: PartialRouteInfo): void {
+ const handleEntries = ({ entries }: { entries: PerformanceEntry[] }): void => {
+ entries.forEach(entry => {
+ if (!isPerformanceEventTiming(entry) || !latestRoute.name) {
+ return;
+ }
+
+ const interactionId = entry.interactionId;
+ if (interactionId == null) {
+ return;
+ }
+
+ // If the interaction was already recorded before, nothing more to do
+ if (INTERACTIONS_ROUTE_MAP.has(interactionId)) {
+ return;
+ }
+
+ // We keep max. 10 interactions in the list, then remove the oldest one & clean up
+ if (LAST_INTERACTIONS.length > 10) {
+ const last = LAST_INTERACTIONS.shift() as number;
+ INTERACTIONS_ROUTE_MAP.delete(last);
+ }
+
+ // We add the interaction to the list of recorded interactions
+ // and store the route information for this interaction
+ // (we clone the object because it is mutated when it changes)
+ LAST_INTERACTIONS.push(interactionId);
+ INTERACTIONS_ROUTE_MAP.set(interactionId, latestRoute.name);
+ });
+ };
+
+ addPerformanceInstrumentationHandler('event', handleEntries);
+ addPerformanceInstrumentationHandler('first-input', handleEntries);
+}
diff --git a/packages/browser-utils/src/metrics/instrument.ts b/packages/browser-utils/src/metrics/instrument.ts
index 06a27c4225ff..e22a345e3116 100644
--- a/packages/browser-utils/src/metrics/instrument.ts
+++ b/packages/browser-utils/src/metrics/instrument.ts
@@ -8,7 +8,13 @@ import { onLCP } from './web-vitals/getLCP';
import { observe } from './web-vitals/lib/observe';
import { onTTFB } from './web-vitals/onTTFB';
-type InstrumentHandlerTypePerformanceObserver = 'longtask' | 'event' | 'navigation' | 'paint' | 'resource';
+type InstrumentHandlerTypePerformanceObserver =
+ | 'longtask'
+ | 'event'
+ | 'navigation'
+ | 'paint'
+ | 'resource'
+ | 'first-input';
type InstrumentHandlerTypeMetric = 'cls' | 'lcp' | 'fid' | 'ttfb' | 'inp';
@@ -324,3 +330,10 @@ function getCleanupCallback(
}
};
}
+
+/**
+ * Check if a PerformanceEntry is a PerformanceEventTiming by checking for the `duration` property.
+ */
+export function isPerformanceEventTiming(entry: PerformanceEntry): entry is PerformanceEventTiming {
+ return 'duration' in entry;
+}
diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts
index f6528e4d155d..c058b1930928 100644
--- a/packages/browser/src/tracing/browserTracingIntegration.ts
+++ b/packages/browser/src/tracing/browserTracingIntegration.ts
@@ -2,6 +2,7 @@
import {
addHistoryInstrumentationHandler,
addPerformanceEntries,
+ registerInpInteractionListener,
startTrackingINP,
startTrackingInteractions,
startTrackingLongTasks,
@@ -40,6 +41,11 @@ import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from
export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing';
+interface RouteInfo {
+ name: string | undefined;
+ source: TransactionSource | undefined;
+}
+
/** Options for Browser Tracing integration */
export interface BrowserTracingOptions {
/**
@@ -204,7 +210,7 @@ export const browserTracingIntegration = ((_options: Partial {
From 1f82e4748b6e14ba0166813f1085376661f6ad28 Mon Sep 17 00:00:00 2001
From: Lukas Stracke
Date: Fri, 7 Jun 2024 10:34:01 +0200
Subject: [PATCH 04/31] fix(aws-serverless): Only auto-patch handler in CJS
when loading `awslambda-auto` (#12392)
Guards the `tryPatcHandler` call by checking for the availability of
`require` (which is undefined in ESM). In ESM mode, this call fails because
`require` is not available. So let's not call it in this case.
---
packages/aws-serverless/src/awslambda-auto.ts | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/packages/aws-serverless/src/awslambda-auto.ts b/packages/aws-serverless/src/awslambda-auto.ts
index 9cf3ba68ae6e..2f23fe652005 100644
--- a/packages/aws-serverless/src/awslambda-auto.ts
+++ b/packages/aws-serverless/src/awslambda-auto.ts
@@ -1,3 +1,5 @@
+// Important: This file cannot import anything other than the index file below.
+// This is the entry point to the lambda layer, which only contains the entire SDK bundled into the index file
import * as Sentry from './index';
const lambdaTaskRoot = process.env.LAMBDA_TASK_ROOT;
@@ -21,7 +23,9 @@ if (lambdaTaskRoot) {
),
});
- Sentry.tryPatchHandler(lambdaTaskRoot, handlerString);
+ if (typeof require !== 'undefined') {
+ Sentry.tryPatchHandler(lambdaTaskRoot, handlerString);
+ }
} else {
throw Error('LAMBDA_TASK_ROOT environment variable is not set');
}
From 273e5e4a1579ccd0f5286c6035d182a1c5459d32 Mon Sep 17 00:00:00 2001
From: Lukas Stracke
Date: Fri, 7 Jun 2024 10:47:25 +0200
Subject: [PATCH 05/31] test(e2e): Add ESM http instrumentation test (#12379)
add a test to our ESM Express e2e test app to test that http instrumentation
is working correctly by ensuring a `http.client` span is captured.
---------
Co-authored-by: Francesco Novy
---
.../node-express-esm-preload/src/app.mjs | 15 +++++++
.../tests/server.test.ts | 43 +++++++++++++++++++
2 files changed, 58 insertions(+)
diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/src/app.mjs b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/src/app.mjs
index abb70111543d..cc680866ab1a 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/src/app.mjs
+++ b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/src/app.mjs
@@ -1,3 +1,4 @@
+import * as http from 'http';
import * as Sentry from '@sentry/node';
import express from 'express';
@@ -25,6 +26,20 @@ app.get('/test-error', function (req, res) {
}, 100);
});
+app.get('/http-req', function (req, res) {
+ http
+ .request('http://example.com', httpRes => {
+ let data = '';
+ httpRes.on('data', d => {
+ data += d;
+ });
+ httpRes.on('end', () => {
+ res.status(200).send(data).end();
+ });
+ })
+ .end();
+});
+
Sentry.setupExpressErrorHandler(app);
app.use(function onError(err, req, res, next) {
diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/tests/server.test.ts b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/tests/server.test.ts
index f1b06f9d0304..533a44cefaf4 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-esm-preload/tests/server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-express-esm-preload/tests/server.test.ts
@@ -121,3 +121,46 @@ test('Should record a transaction for route with parameters', async ({ request }
trace_id: expect.any(String),
});
});
+
+test('Should record spans from http instrumentation', async ({ request }) => {
+ const transactionEventPromise = waitForTransaction('node-express-esm-preload', transactionEvent => {
+ return transactionEvent.contexts?.trace?.data?.['http.target'] === '/http-req';
+ });
+
+ await request.get('/http-req');
+
+ const transactionEvent = await transactionEventPromise;
+
+ const httpClientSpan = transactionEvent.spans?.find(span => span.op === 'http.client');
+
+ expect(httpClientSpan).toEqual({
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: {
+ 'http.flavor': '1.1',
+ 'http.host': 'example.com:80',
+ 'http.method': 'GET',
+ 'http.response.status_code': 200,
+ 'http.response_content_length_uncompressed': expect.any(Number),
+ 'http.status_code': 200,
+ 'http.status_text': 'OK',
+ 'http.target': '/',
+ 'http.url': 'http://example.com/',
+ 'net.peer.ip': expect.any(String),
+ 'net.peer.name': 'example.com',
+ 'net.peer.port': 80,
+ 'net.transport': 'ip_tcp',
+ 'otel.kind': 'CLIENT',
+ 'sentry.op': 'http.client',
+ 'sentry.origin': 'auto.http.otel.http',
+ url: 'http://example.com/',
+ },
+ description: 'GET http://example.com/',
+ parent_span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'http.client',
+ origin: 'auto.http.otel.http',
+ });
+});
From 28da80250dd1ee6cdfa4f2b0dca35aeac9846015 Mon Sep 17 00:00:00 2001
From: James Pulec
Date: Fri, 7 Jun 2024 02:21:50 -0700
Subject: [PATCH 06/31] fix(nextjs): correct types conditional export ordering
(#12355)
## Description
As stated
[here](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#packagejson-exports-imports-and-self-referencing)
in the typescript docs, `types` exports should always come first, so
that they are resolved correctly. This also applies to the nested
`types` within and `exports` condition.
This corrects an issue where `eslint-import-resolver-typescript` was not
correctly resolving the full types for Sentry.
---
packages/nextjs/package.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json
index 934cbb62612d..479fb9ee69d8 100644
--- a/packages/nextjs/package.json
+++ b/packages/nextjs/package.json
@@ -15,6 +15,7 @@
"exports": {
"./package.json": "./package.json",
".": {
+ "types": "./build/types/index.types.d.ts",
"edge": {
"import": "./build/esm/edge/index.js",
"require": "./build/cjs/edge/index.js",
@@ -40,8 +41,7 @@
"require": "./build/cjs/index.client.js"
},
"node": "./build/cjs/index.server.js",
- "import": "./build/esm/index.server.js",
- "types": "./build/types/index.types.d.ts"
+ "import": "./build/esm/index.server.js"
},
"./import": {
"import": {
From 77b47e1d5139dffc8044cf3d0ef25ae9835fdf4f Mon Sep 17 00:00:00 2001
From: Francesco Novy
Date: Fri, 7 Jun 2024 11:42:26 +0200
Subject: [PATCH 07/31] fix: Fix types export order (#12404)
Ensure the `types` export is always first, as documented here:
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#packagejson-exports-imports-and-self-referencing
Follow up to https://github.com/getsentry/sentry-javascript/pull/12355
---
packages/astro/package.json | 8 ++++----
packages/remix/package.json | 4 ++--
packages/sveltekit/package.json | 4 ++--
3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 93071d542f3a..2fd4c8a2cdb4 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -31,17 +31,17 @@
"types": "build/types/index.types.d.ts",
"exports": {
".": {
+ "types": "./build/types/index.types.d.ts",
"node": "./build/esm/index.server.js",
"browser": "./build/esm/index.client.js",
"import": "./build/esm/index.client.js",
- "require": "./build/cjs/index.server.js",
- "types": "./build/types/index.types.d.ts"
+ "require": "./build/cjs/index.server.js"
},
"./middleware": {
+ "types": "./build/types/integration/middleware/index.types.d.ts",
"node": "./build/esm/integration/middleware/index.js",
"import": "./build/esm/integration/middleware/index.js",
- "require": "./build/cjs/integration/middleware/index.js",
- "types": "./build/types/integration/middleware/index.types.d.ts"
+ "require": "./build/cjs/integration/middleware/index.js"
},
"./import": {
"import": {
diff --git a/packages/remix/package.json b/packages/remix/package.json
index 511b0d6d975d..acf21ec24f8d 100644
--- a/packages/remix/package.json
+++ b/packages/remix/package.json
@@ -28,12 +28,12 @@
"exports": {
"./package.json": "./package.json",
".": {
+ "types": "./build/types/index.types.d.ts",
"browser": {
"import": "./build/esm/index.client.js",
"require": "./build/cjs/index.client.js"
},
- "node": "./build/cjs/index.server.js",
- "types": "./build/types/index.types.d.ts"
+ "node": "./build/cjs/index.server.js"
},
"./import": {
"import": {
diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json
index 05614778ecce..0ef4e41c238c 100644
--- a/packages/sveltekit/package.json
+++ b/packages/sveltekit/package.json
@@ -22,12 +22,12 @@
"exports": {
"./package.json": "./package.json",
".": {
+ "types": "./build/types/index.types.d.ts",
"browser": {
"import": "./build/esm/index.client.js",
"require": "./build/cjs/index.client.js"
},
- "node": "./build/cjs/index.server.js",
- "types": "./build/types/index.types.d.ts"
+ "node": "./build/cjs/index.server.js"
}
},
"publishConfig": {
From c8e9f84d9dbe8b81b04a3eb849a43112eee48af6 Mon Sep 17 00:00:00 2001
From: Francesco Novy
Date: Fri, 7 Jun 2024 14:22:33 +0200
Subject: [PATCH 08/31] build: Cleanup dependencies (#12405)
Remove some unneeded deps, and align playwright version everywhere.
---
dev-packages/overhead-metrics/package.json | 4 +-
packages/browser/package.json | 4 -
yarn.lock | 96 +++-------------------
3 files changed, 14 insertions(+), 90 deletions(-)
diff --git a/dev-packages/overhead-metrics/package.json b/dev-packages/overhead-metrics/package.json
index a2c1dde0fc97..0f5421239d73 100644
--- a/dev-packages/overhead-metrics/package.json
+++ b/dev-packages/overhead-metrics/package.json
@@ -24,8 +24,8 @@
"filesize": "^10.0.6",
"fs-extra": "^11.1.0",
"p-timeout": "^6.0.0",
- "playwright": "^1.31.1",
- "playwright-core": "^1.29.1",
+ "playwright": "^1.44.1",
+ "playwright-core": "^1.44.1",
"simple-git": "^3.16.0",
"simple-statistics": "^7.8.0",
"typescript": "4.9.5"
diff --git a/packages/browser/package.json b/packages/browser/package.json
index 9d1a9138bed8..fa386baf2b8f 100644
--- a/packages/browser/package.json
+++ b/packages/browser/package.json
@@ -52,11 +52,7 @@
},
"devDependencies": {
"@sentry-internal/integration-shims": "8.8.0",
- "@types/md5": "2.1.33",
- "btoa": "^1.2.1",
"fake-indexeddb": "^4.0.1",
- "node-fetch": "^2.6.0",
- "playwright": "^1.31.1",
"webpack": "^4.47.0"
},
"scripts": {
diff --git a/yarn.lock b/yarn.lock
index d61a25fe3cb5..479522489271 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8429,17 +8429,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==
@@ -8585,13 +8576,6 @@
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.3.8.tgz#84dbf2d020a9209a272058725e168f21d331a67e"
integrity sha512-jYvz8UMLDgy3a5SkGJne8H7VA7zPV2Lwohjx0V8V31+SqAjNmurWMkk9cQhfvlcnXWudBpK9xPM1n4rljOcHYQ==
-"@types/md5@2.1.33":
- version "2.1.33"
- resolved "https://registry.yarnpkg.com/@types/md5/-/md5-2.1.33.tgz#8c8dba30df4ad0e92296424f08c4898dd808e8df"
- integrity sha512-8+X960EtKLoSblhauxLKy3zzotagjoj3Jt1Tx9oaxUdZEPIBl+mkrUz6PNKpzJgkrKSN9YgkWTA29c0KnLshmA==
- dependencies:
- "@types/node" "*"
-
"@types/mdast@^3.0.0":
version "3.0.13"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.13.tgz#b7ba6e52d0faeb9c493e32c205f3831022be4e1b"
@@ -8805,15 +8789,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==
@@ -12079,11 +12055,6 @@ bson@^1.1.4:
resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.6.tgz#fb819be9a60cd677e0853aee4ca712a785d6618a"
integrity sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==
-btoa@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73"
- integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==
-
buffer-crc32@^0.2.5, buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
@@ -24934,17 +24905,12 @@ pkg-up@^3.1.0:
dependencies:
find-up "^3.0.0"
-playwright-core@1.40.1, playwright-core@^1.29.1:
- version "1.40.1"
- resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.40.1.tgz#442d15e86866a87d90d07af528e0afabe4c75c05"
- integrity sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==
-
-playwright-core@1.44.1:
+playwright-core@1.44.1, playwright-core@^1.44.1:
version "1.44.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.44.1.tgz#53ec975503b763af6fc1a7aa995f34bc09ff447c"
integrity sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==
-playwright@1.44.1:
+playwright@1.44.1, playwright@^1.44.1:
version "1.44.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.44.1.tgz#5634369d777111c1eea9180430b7a184028e7892"
integrity sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==
@@ -24953,15 +24919,6 @@ playwright@1.44.1:
optionalDependencies:
fsevents "2.3.2"
-playwright@^1.31.1:
- version "1.40.1"
- resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.40.1.tgz#a11bf8dca15be5a194851dbbf3df235b9f53d7ae"
- integrity sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==
- dependencies:
- playwright-core "1.40.1"
- optionalDependencies:
- fsevents "2.3.2"
-
pluralize@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
@@ -26097,7 +26054,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==
@@ -26112,13 +26070,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"
@@ -28437,7 +28388,8 @@ 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:
+ name string-width-cjs
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==
@@ -28463,15 +28415,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"
@@ -28567,14 +28510,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==
@@ -31201,7 +31137,8 @@ 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:
+ name wrap-ansi-cjs
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -31219,15 +31156,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 1df9c69fd6c0d1d332261837e86bde70206bb544 Mon Sep 17 00:00:00 2001
From: Soichiro Kogo <20456656+soch4n@users.noreply.github.com>
Date: Sat, 8 Jun 2024 02:13:27 +0900
Subject: [PATCH 09/31] fix(vue): Handle span name assignment for nested routes
in VueRouter (#12398)
Fix an issue where the correct transactionName could not be obtained
when using nested routes in VueRouter.
---
.../vue-3/src/router/index.ts | 9 ++++++
.../vue-3/src/views/CategoryIdView.vue | 3 ++
.../vue-3/tests/performance.test.ts | 29 +++++++++++++++++++
packages/vue/src/router.ts | 5 ++--
packages/vue/test/router.test.ts | 10 +++++++
5 files changed, 54 insertions(+), 2 deletions(-)
create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/src/views/CategoryIdView.vue
diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts b/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts
index 8c3ac217716f..aac6fb815f43 100644
--- a/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts
+++ b/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts
@@ -21,6 +21,15 @@ const router = createRouter({
path: '/users-error/:id',
component: () => import('../views/UserIdErrorView.vue'),
},
+ {
+ path: '/categories',
+ children: [
+ {
+ path: ':id',
+ component: () => import('../views/CategoryIdView.vue'),
+ },
+ ],
+ },
],
});
diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/views/CategoryIdView.vue b/dev-packages/e2e-tests/test-applications/vue-3/src/views/CategoryIdView.vue
new file mode 100644
index 000000000000..c3b59c9fb7f5
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-3/src/views/CategoryIdView.vue
@@ -0,0 +1,3 @@
+
+ Category ID: {{ $route.params.id }}
+
diff --git a/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts
index bdf7b5b8e1fe..d9a594b5abe7 100644
--- a/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts
+++ b/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts
@@ -66,6 +66,35 @@ test('sends a navigation transaction with a parameterized URL', async ({ page })
});
});
+test('sends a pageload transaction with a nested route URL', async ({ page }) => {
+ const transactionPromise = waitForTransaction('vue-3', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto(`/categories/123`);
+
+ const rootSpan = await transactionPromise;
+
+ expect(rootSpan).toMatchObject({
+ contexts: {
+ trace: {
+ data: {
+ 'sentry.source': 'route',
+ 'sentry.origin': 'auto.pageload.vue',
+ 'sentry.op': 'pageload',
+ 'params.id': '123',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.vue',
+ },
+ },
+ transaction: '/categories/:id',
+ transaction_info: {
+ source: 'route',
+ },
+ });
+});
+
test('sends a pageload transaction with a route name as transaction name if available', async ({ page }) => {
const transactionPromise = waitForTransaction('vue-3', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts
index ba42c8ba9fb5..b1d7163e48d1 100644
--- a/packages/vue/src/router.ts
+++ b/packages/vue/src/router.ts
@@ -83,8 +83,9 @@ export function instrumentVueRouter(
if (to.name && options.routeLabel !== 'path') {
spanName = to.name.toString();
transactionSource = 'custom';
- } else if (to.matched[0] && to.matched[0].path) {
- spanName = to.matched[0].path;
+ } else if (to.matched.length > 0) {
+ const lastIndex = to.matched.length - 1;
+ spanName = to.matched[lastIndex].path;
transactionSource = 'route';
}
diff --git a/packages/vue/test/router.test.ts b/packages/vue/test/router.test.ts
index 8ff42d49e2b9..e835855ebd7a 100644
--- a/packages/vue/test/router.test.ts
+++ b/packages/vue/test/router.test.ts
@@ -43,6 +43,14 @@ const testRoutes: Record = {
path: '/accounts/4',
query: {},
},
+ nestedRoute: {
+ matched: [{ path: '/' }, { path: '/categories' }, { path: '/categories/:categoryId' }],
+ params: {
+ categoryId: '1',
+ },
+ path: '/categories/1',
+ query: {},
+ },
namedRoute: {
matched: [{ path: '/login' }],
name: 'login-screen',
@@ -85,6 +93,7 @@ describe('instrumentVueRouter()', () => {
it.each([
['normalRoute1', 'normalRoute2', '/accounts/:accountId', 'route'],
+ ['normalRoute1', 'nestedRoute', '/categories/:categoryId', 'route'],
['normalRoute2', 'namedRoute', 'login-screen', 'custom'],
['normalRoute2', 'unmatchedRoute', '/e8733846-20ac-488c-9871-a5cbcb647294', 'url'],
])(
@@ -122,6 +131,7 @@ describe('instrumentVueRouter()', () => {
it.each([
['initialPageloadRoute', 'normalRoute1', '/books/:bookId/chapter/:chapterId', 'route'],
+ ['initialPageloadRoute', 'nestedRoute', '/categories/:categoryId', 'route'],
['initialPageloadRoute', 'namedRoute', 'login-screen', 'custom'],
['initialPageloadRoute', 'unmatchedRoute', '/e8733846-20ac-488c-9871-a5cbcb647294', 'url'],
])(
From 691e5de9f0fc68521a6e6dba9e18b7e94e845ef7 Mon Sep 17 00:00:00 2001
From: Catherine Lee <55311782+c298lee@users.noreply.github.com>
Date: Fri, 7 Jun 2024 17:55:21 -0400
Subject: [PATCH 10/31] feat(replay): Replay Web Vital Breadcrumbs (#12296)
Adds CLS, FID, and INP breadcrumbs. Updates all web vital breadcrumbs to
include rating
Closes https://github.com/getsentry/sentry-javascript/issues/11639
---------
Signed-off-by: dependabot[bot]
Co-authored-by: Luca Forstner
Co-authored-by: Andrei <168741329+andreiborza@users.noreply.github.com>
Co-authored-by: Francesco Novy
Co-authored-by: Lukas Stracke
Co-authored-by: Yamagishi Kazutoshi
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.../suites/replay/customEvents/test.ts | 4 +
.../suites/replay/multiple-pages/test.ts | 16 ++-
.../utils/replayEventTemplates.ts | 41 +++++++-
.../tests/fixtures/ReplayRecordingData.ts | 2 +-
.../tests/fixtures/ReplayRecordingData.ts | 40 +++++++-
packages/browser-utils/src/index.ts | 1 +
.../src/coreHandlers/performanceObserver.ts | 24 +++--
.../replay-internal/src/types/performance.ts | 9 +-
.../replay-internal/src/types/replayFrame.ts | 10 +-
.../src/util/createPerformanceEntries.ts | 99 ++++++++++++++++---
.../unit/util/createPerformanceEntry.test.ts | 73 +++++++++++++-
11 files changed, 284 insertions(+), 35 deletions(-)
diff --git a/dev-packages/browser-integration-tests/suites/replay/customEvents/test.ts b/dev-packages/browser-integration-tests/suites/replay/customEvents/test.ts
index 41c90d94ffdf..053c31c3881e 100644
--- a/dev-packages/browser-integration-tests/suites/replay/customEvents/test.ts
+++ b/dev-packages/browser-integration-tests/suites/replay/customEvents/test.ts
@@ -2,8 +2,10 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../utils/fixtures';
import {
+ expectedCLSPerformanceSpan,
expectedClickBreadcrumb,
expectedFCPPerformanceSpan,
+ expectedFIDPerformanceSpan,
expectedFPPerformanceSpan,
expectedLCPPerformanceSpan,
expectedMemoryPerformanceSpan,
@@ -62,6 +64,8 @@ sentryTest(
expect.arrayContaining([
expectedNavigationPerformanceSpan,
expectedLCPPerformanceSpan,
+ expectedCLSPerformanceSpan,
+ expectedFIDPerformanceSpan,
expectedFPPerformanceSpan,
expectedFCPPerformanceSpan,
expectedMemoryPerformanceSpan, // two memory spans - once per flush
diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts
index 1148847f09c7..7bacf5a8ae17 100644
--- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts
+++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts
@@ -2,8 +2,10 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../utils/fixtures';
import {
+ expectedCLSPerformanceSpan,
expectedClickBreadcrumb,
expectedFCPPerformanceSpan,
+ expectedFIDPerformanceSpan,
expectedFPPerformanceSpan,
expectedLCPPerformanceSpan,
expectedMemoryPerformanceSpan,
@@ -78,11 +80,13 @@ sentryTest(
const collectedPerformanceSpans = [...recording0.performanceSpans, ...recording1.performanceSpans];
const collectedBreadcrumbs = [...recording0.breadcrumbs, ...recording1.breadcrumbs];
- expect(collectedPerformanceSpans.length).toEqual(6);
+ expect(collectedPerformanceSpans.length).toEqual(8);
expect(collectedPerformanceSpans).toEqual(
expect.arrayContaining([
expectedNavigationPerformanceSpan,
expectedLCPPerformanceSpan,
+ expectedCLSPerformanceSpan,
+ expectedFIDPerformanceSpan,
expectedFPPerformanceSpan,
expectedFCPPerformanceSpan,
expectedMemoryPerformanceSpan, // two memory spans - once per flush
@@ -116,11 +120,13 @@ sentryTest(
const collectedPerformanceSpansAfterReload = [...recording2.performanceSpans, ...recording3.performanceSpans];
const collectedBreadcrumbsAdterReload = [...recording2.breadcrumbs, ...recording3.breadcrumbs];
- expect(collectedPerformanceSpansAfterReload.length).toEqual(6);
+ expect(collectedPerformanceSpansAfterReload.length).toEqual(8);
expect(collectedPerformanceSpansAfterReload).toEqual(
expect.arrayContaining([
expectedReloadPerformanceSpan,
expectedLCPPerformanceSpan,
+ expectedCLSPerformanceSpan,
+ expectedFIDPerformanceSpan,
expectedFPPerformanceSpan,
expectedFCPPerformanceSpan,
expectedMemoryPerformanceSpan,
@@ -188,6 +194,8 @@ sentryTest(
expect.arrayContaining([
expectedNavigationPerformanceSpan,
expectedLCPPerformanceSpan,
+ expectedCLSPerformanceSpan,
+ expectedFIDPerformanceSpan,
expectedFPPerformanceSpan,
expectedFCPPerformanceSpan,
expectedMemoryPerformanceSpan,
@@ -304,11 +312,13 @@ sentryTest(
];
const collectedBreadcrumbsAfterIndexNavigation = [...recording8.breadcrumbs, ...recording9.breadcrumbs];
- expect(collectedPerformanceSpansAfterIndexNavigation.length).toEqual(6);
+ expect(collectedPerformanceSpansAfterIndexNavigation.length).toEqual(8);
expect(collectedPerformanceSpansAfterIndexNavigation).toEqual(
expect.arrayContaining([
expectedNavigationPerformanceSpan,
expectedLCPPerformanceSpan,
+ expectedCLSPerformanceSpan,
+ expectedFIDPerformanceSpan,
expectedFPPerformanceSpan,
expectedFCPPerformanceSpan,
expectedMemoryPerformanceSpan,
diff --git a/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts b/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts
index 03354c6b3185..257c47fbfa9b 100644
--- a/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts
+++ b/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts
@@ -121,17 +121,56 @@ export const expectedMemoryPerformanceSpan = {
};
export const expectedLCPPerformanceSpan = {
- op: 'largest-contentful-paint',
+ op: 'web-vital',
description: 'largest-contentful-paint',
startTimestamp: expect.any(Number),
endTimestamp: expect.any(Number),
data: {
value: expect.any(Number),
nodeId: expect.any(Number),
+ rating: expect.any(String),
size: expect.any(Number),
},
};
+export const expectedCLSPerformanceSpan = {
+ op: 'web-vital',
+ description: 'cumulative-layout-shift',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ data: {
+ value: expect.any(Number),
+ rating: expect.any(String),
+ size: expect.any(Number),
+ },
+};
+
+export const expectedFIDPerformanceSpan = {
+ op: 'web-vital',
+ description: 'first-input-delay',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ data: {
+ value: expect.any(Number),
+ rating: expect.any(String),
+ size: expect.any(Number),
+ nodeId: expect.any(Number),
+ },
+};
+
+export const expectedINPPerformanceSpan = {
+ op: 'web-vital',
+ description: 'interaction-to-next-paint',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ data: {
+ value: expect.any(Number),
+ rating: expect.any(String),
+ size: expect.any(Number),
+ nodeId: expect.any(Number),
+ },
+};
+
export const expectedFCPPerformanceSpan = {
op: 'paint',
description: 'first-contentful-paint',
diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts
index 554fac59f88e..e7fd943c0f08 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts
+++ b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts
@@ -212,7 +212,7 @@ export const ReplayRecordingData = [
data: {
tag: 'performanceSpan',
payload: {
- op: 'largest-contentful-paint',
+ op: 'web-vital',
description: 'largest-contentful-paint',
startTimestamp: expect.any(Number),
endTimestamp: expect.any(Number),
diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/fixtures/ReplayRecordingData.ts b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/fixtures/ReplayRecordingData.ts
index 554fac59f88e..156c2775f5ff 100644
--- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/fixtures/ReplayRecordingData.ts
+++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/fixtures/ReplayRecordingData.ts
@@ -212,18 +212,56 @@ export const ReplayRecordingData = [
data: {
tag: 'performanceSpan',
payload: {
- op: 'largest-contentful-paint',
+ op: 'web-vital',
description: 'largest-contentful-paint',
startTimestamp: expect.any(Number),
endTimestamp: expect.any(Number),
data: {
value: expect.any(Number),
size: expect.any(Number),
+ rating: expect.any(String),
nodeId: 16,
},
},
},
},
+ {
+ type: 5,
+ timestamp: expect.any(Number),
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ op: 'web-vital',
+ description: 'cumulative-layout-shift',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ data: {
+ value: expect.any(Number),
+ size: expect.any(Number),
+ rating: expect.any(String),
+ },
+ },
+ },
+ },
+ {
+ type: 5,
+ timestamp: expect.any(Number),
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ op: 'web-vital',
+ description: 'first-input-delay',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ data: {
+ value: expect.any(Number),
+ size: expect.any(Number),
+ rating: expect.any(String),
+ nodeId: 10,
+ },
+ },
+ },
+ },
{
type: 5,
timestamp: expect.any(Number),
diff --git a/packages/browser-utils/src/index.ts b/packages/browser-utils/src/index.ts
index 8e48e0988db9..f59ccbf8da8f 100644
--- a/packages/browser-utils/src/index.ts
+++ b/packages/browser-utils/src/index.ts
@@ -4,6 +4,7 @@ export {
addFidInstrumentationHandler,
addTtfbInstrumentationHandler,
addLcpInstrumentationHandler,
+ addInpInstrumentationHandler,
} from './metrics/instrument';
export {
diff --git a/packages/replay-internal/src/coreHandlers/performanceObserver.ts b/packages/replay-internal/src/coreHandlers/performanceObserver.ts
index 45b843760e52..638ef53b05fb 100644
--- a/packages/replay-internal/src/coreHandlers/performanceObserver.ts
+++ b/packages/replay-internal/src/coreHandlers/performanceObserver.ts
@@ -1,7 +1,18 @@
-import { addLcpInstrumentationHandler, addPerformanceInstrumentationHandler } from '@sentry-internal/browser-utils';
-
+import {
+ addClsInstrumentationHandler,
+ addFidInstrumentationHandler,
+ addInpInstrumentationHandler,
+ addLcpInstrumentationHandler,
+ addPerformanceInstrumentationHandler,
+} from '@sentry-internal/browser-utils';
import type { ReplayContainer } from '../types';
-import { getLargestContentfulPaint } from '../util/createPerformanceEntries';
+import {
+ getCumulativeLayoutShift,
+ getFirstInputDelay,
+ getInteractionToNextPaint,
+ getLargestContentfulPaint,
+ webVitalHandler,
+} from '../util/createPerformanceEntries';
/**
* Sets up a PerformanceObserver to listen to all performance entry types.
@@ -26,9 +37,10 @@ export function setupPerformanceObserver(replay: ReplayContainer): () => void {
});
clearCallbacks.push(
- addLcpInstrumentationHandler(({ metric }) => {
- replay.replayPerformanceEntries.push(getLargestContentfulPaint(metric));
- }),
+ addLcpInstrumentationHandler(webVitalHandler(getLargestContentfulPaint, replay)),
+ addClsInstrumentationHandler(webVitalHandler(getCumulativeLayoutShift, replay)),
+ addFidInstrumentationHandler(webVitalHandler(getFirstInputDelay, replay)),
+ addInpInstrumentationHandler(webVitalHandler(getInteractionToNextPaint, replay)),
);
// A callback to cleanup all handlers
diff --git a/packages/replay-internal/src/types/performance.ts b/packages/replay-internal/src/types/performance.ts
index 2fe87d24a9c8..5241c12d847a 100644
--- a/packages/replay-internal/src/types/performance.ts
+++ b/packages/replay-internal/src/types/performance.ts
@@ -96,12 +96,17 @@ export type ResourceData = Pick function to normalize data for event
@@ -25,6 +26,42 @@ const ENTRY_TYPES: Record<
navigation: createNavigationEntry,
};
+export interface Metric {
+ /**
+ * The current value of the metric.
+ */
+ value: number;
+
+ /**
+ * The rating as to whether the metric value is within the "good",
+ * "needs improvement", or "poor" thresholds of the metric.
+ */
+ rating: 'good' | 'needs-improvement' | 'poor';
+
+ /**
+ * Any performance entries relevant to the metric value calculation.
+ * The array may also be empty if the metric value was not based on any
+ * entries (e.g. a CLS value of 0 given no layout shifts).
+ */
+ entries: PerformanceEntry[] | PerformanceEventTiming[];
+}
+
+interface LayoutShiftAttribution {
+ node?: Node;
+ previousRect: DOMRectReadOnly;
+ currentRect: DOMRectReadOnly;
+}
+
+/**
+ * Handler creater for web vitals
+ */
+export function webVitalHandler(
+ getter: (metric: Metric) => ReplayPerformanceEntry,
+ replay: ReplayContainer,
+): (data: { metric: Metric }) => void {
+ return ({ metric }) => void replay.replayPerformanceEntries.push(getter(metric));
+}
+
/**
* Create replay performance entries from the browser performance entries.
*/
@@ -141,29 +178,65 @@ function createResourceEntry(
}
/**
- * Add a LCP event to the replay based on an LCP metric.
+ * Add a LCP event to the replay based on a LCP metric.
*/
-export function getLargestContentfulPaint(metric: {
- value: number;
- entries: PerformanceEntry[];
-}): ReplayPerformanceEntry {
- const entries = metric.entries;
- const lastEntry = entries[entries.length - 1] as (PerformanceEntry & { element?: Element }) | undefined;
- const element = lastEntry ? lastEntry.element : undefined;
+export function getLargestContentfulPaint(metric: Metric): ReplayPerformanceEntry {
+ const lastEntry = metric.entries[metric.entries.length - 1] as (PerformanceEntry & { element?: Node }) | undefined;
+ const node = lastEntry ? lastEntry.element : undefined;
+ return getWebVital(metric, 'largest-contentful-paint', node);
+}
+/**
+ * Add a CLS event to the replay based on a CLS metric.
+ */
+export function getCumulativeLayoutShift(metric: Metric): ReplayPerformanceEntry {
+ // get first node that shifts
+ const firstEntry = metric.entries[0] as (PerformanceEntry & { sources?: LayoutShiftAttribution[] }) | undefined;
+ const node = firstEntry ? (firstEntry.sources ? firstEntry.sources[0].node : undefined) : undefined;
+ return getWebVital(metric, 'cumulative-layout-shift', node);
+}
+
+/**
+ * Add a FID event to the replay based on a FID metric.
+ */
+export function getFirstInputDelay(metric: Metric): ReplayPerformanceEntry {
+ const lastEntry = metric.entries[metric.entries.length - 1] as (PerformanceEntry & { target?: Node }) | undefined;
+ const node = lastEntry ? lastEntry.target : undefined;
+ return getWebVital(metric, 'first-input-delay', node);
+}
+
+/**
+ * Add an INP event to the replay based on an INP metric.
+ */
+export function getInteractionToNextPaint(metric: Metric): ReplayPerformanceEntry {
+ const lastEntry = metric.entries[metric.entries.length - 1] as (PerformanceEntry & { target?: Node }) | undefined;
+ const node = lastEntry ? lastEntry.target : undefined;
+ return getWebVital(metric, 'interaction-to-next-paint', node);
+}
+
+/**
+ * Add an web vital event to the replay based on the web vital metric.
+ */
+export function getWebVital(
+ metric: Metric,
+ name: string,
+ node: Node | undefined,
+): ReplayPerformanceEntry {
const value = metric.value;
+ const rating = metric.rating;
const end = getAbsoluteTime(value);
- const data: ReplayPerformanceEntry = {
- type: 'largest-contentful-paint',
- name: 'largest-contentful-paint',
+ const data: ReplayPerformanceEntry = {
+ type: 'web-vital',
+ name,
start: end,
end,
data: {
value,
size: value,
- nodeId: element ? record.mirror.getId(element) : undefined,
+ rating,
+ nodeId: node ? record.mirror.getId(node) : undefined,
},
};
diff --git a/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts b/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts
index 176de1c2d32e..f13d72feecf4 100644
--- a/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts
+++ b/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts
@@ -11,7 +11,13 @@ vi.mock('@sentry/utils', async () => ({
}));
import { WINDOW } from '../../../src/constants';
-import { createPerformanceEntries, getLargestContentfulPaint } from '../../../src/util/createPerformanceEntries';
+import {
+ createPerformanceEntries,
+ getCumulativeLayoutShift,
+ getFirstInputDelay,
+ getInteractionToNextPaint,
+ getLargestContentfulPaint,
+} from '../../../src/util/createPerformanceEntries';
import { PerformanceEntryNavigation } from '../../fixtures/performanceEntry/navigation';
describe('Unit | util | createPerformanceEntries', () => {
@@ -66,17 +72,78 @@ describe('Unit | util | createPerformanceEntries', () => {
it('works with an LCP metric', async () => {
const metric = {
value: 5108.299,
+ rating: 'good' as const,
entries: [],
};
const event = getLargestContentfulPaint(metric);
expect(event).toEqual({
- type: 'largest-contentful-paint',
+ type: 'web-vital',
name: 'largest-contentful-paint',
start: 1672531205.108299,
end: 1672531205.108299,
- data: { value: 5108.299, size: 5108.299, nodeId: undefined },
+ data: { value: 5108.299, rating: 'good', size: 5108.299, nodeId: undefined },
+ });
+ });
+ });
+
+ describe('getCumulativeLayoutShift', () => {
+ it('works with an CLS metric', async () => {
+ const metric = {
+ value: 5108.299,
+ rating: 'good' as const,
+ entries: [],
+ };
+
+ const event = getCumulativeLayoutShift(metric);
+
+ expect(event).toEqual({
+ type: 'web-vital',
+ name: 'cumulative-layout-shift',
+ start: 1672531205.108299,
+ end: 1672531205.108299,
+ data: { value: 5108.299, size: 5108.299, rating: 'good', nodeId: undefined },
+ });
+ });
+ });
+
+ describe('getFirstInputDelay', () => {
+ it('works with an FID metric', async () => {
+ const metric = {
+ value: 5108.299,
+ rating: 'good' as const,
+ entries: [],
+ };
+
+ const event = getFirstInputDelay(metric);
+
+ expect(event).toEqual({
+ type: 'web-vital',
+ name: 'first-input-delay',
+ start: 1672531205.108299,
+ end: 1672531205.108299,
+ data: { value: 5108.299, size: 5108.299, rating: 'good', nodeId: undefined },
+ });
+ });
+ });
+
+ describe('getInteractionToNextPaint', () => {
+ it('works with an INP metric', async () => {
+ const metric = {
+ value: 5108.299,
+ rating: 'good' as const,
+ entries: [],
+ };
+
+ const event = getInteractionToNextPaint(metric);
+
+ expect(event).toEqual({
+ type: 'web-vital',
+ name: 'interaction-to-next-paint',
+ start: 1672531205.108299,
+ end: 1672531205.108299,
+ data: { value: 5108.299, size: 5108.299, rating: 'good', nodeId: undefined },
});
});
});
From 34455c92c4f7abaa0e09548f13e8953c9c2c2365 Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Mon, 10 Jun 2024 03:04:15 -0400
Subject: [PATCH 11/31] docs: Add external contributors to 8.8.0 changelog note
(#12413)
Forgot to shoutout @dohooo, @mohd-akram and @ykzts for their help fixing
bugs with the SDK this release - will also update the main release
notes!
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f7f203ec826..aa40eb7279cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -48,6 +48,8 @@ If you are still encountering issues with OpenTelemetry instrumentation and ESM,
- ref(browser): Ensure idle span ending is consistent (#12310)
- ref(profiling): unref timer (#12340)
+Work in this release contributed by @dohooo, @mohd-akram, and @ykzts. Thank you for your contributions!
+
## 8.7.0
### Important Changes
From 8b0799815950a6c434690c2250eedfb6affef864 Mon Sep 17 00:00:00 2001
From: Francesco Novy
Date: Mon, 10 Jun 2024 09:13:29 +0200
Subject: [PATCH 12/31] test(e2e): Update nextjs-app-dir E2E tests to avoid
sending to Sentry (#12403)
Part of https://github.com/getsentry/sentry-javascript/issues/11910
This removes the tests in nextjs-app-dir where we still polled Sentry.
---
.../nextjs-app-dir/start-event-proxy.mjs | 2 +-
.../tests/async-context-edge.test.ts | 4 +-
...client-app-routing-instrumentation.test.ts | 8 +--
.../tests/client-errors.test.ts | 29 ++++++++
.../connected-servercomponent-trace.test.ts | 10 +--
.../tests/devErrorSymbolification.test.ts | 2 +-
.../nextjs-app-dir/tests/edge-route.test.ts | 6 +-
.../nextjs-app-dir/tests/edge.test.ts | 4 +-
.../nextjs-app-dir/tests/exceptions.test.ts | 37 ----------
.../nextjs-app-dir/tests/middleware.test.ts | 8 +--
.../tests/pages-ssr-errors.test.ts | 8 +--
.../tests/request-instrumentation.test.ts | 2 +-
.../tests/route-handlers.test.ts | 14 ++--
.../tests/server-components.test.ts | 67 +++++++++--------
.../nextjs-app-dir/tests/transactions.test.ts | 71 +++++++++++--------
.../node-fastify/tests/transactions.test.ts | 1 -
16 files changed, 142 insertions(+), 131 deletions(-)
create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-errors.test.ts
delete mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/exceptions.test.ts
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.mjs
index 4d029c3b87be..7e8016bf98a5 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.mjs
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.mjs
@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';
startEventProxyServer({
port: 3031,
- proxyServerName: 'nextjs-13-app-dir',
+ proxyServerName: 'nextjs-app-dir',
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts
index e5fea269e322..ecce719f0656 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts
@@ -2,9 +2,7 @@ import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
test('Should allow for async context isolation in the edge SDK', async ({ request }) => {
- // test.skip(process.env.TEST_ENV === 'development', "Doesn't work in dev mode.");
-
- const edgerouteTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return transactionEvent?.transaction === 'GET /api/async-context-edge-endpoint';
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts
index 5e8e89eec3d8..41e63f910f79 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts
@@ -4,7 +4,7 @@ import { waitForTransaction } from '@sentry-internal/test-utils';
test('Creates a pageload transaction for app router routes', async ({ page }) => {
const randomRoute = String(Math.random());
- const clientPageloadTransactionPromise = waitForTransaction('nextjs-13-app-dir', transactionEvent => {
+ const clientPageloadTransactionPromise = waitForTransaction('nextjs-app-dir', transactionEvent => {
return (
transactionEvent?.transaction === `/server-component/parameter/${randomRoute}` &&
transactionEvent.contexts?.trace?.op === 'pageload'
@@ -19,7 +19,7 @@ test('Creates a pageload transaction for app router routes', async ({ page }) =>
test('Creates a navigation transaction for app router routes', async ({ page }) => {
const randomRoute = String(Math.random());
- const clientPageloadTransactionPromise = waitForTransaction('nextjs-13-app-dir', transactionEvent => {
+ const clientPageloadTransactionPromise = waitForTransaction('nextjs-app-dir', transactionEvent => {
return (
transactionEvent?.transaction === `/server-component/parameter/${randomRoute}` &&
transactionEvent.contexts?.trace?.op === 'pageload'
@@ -30,14 +30,14 @@ test('Creates a navigation transaction for app router routes', async ({ page })
await clientPageloadTransactionPromise;
await page.getByText('Page (/server-component/parameter/[parameter])').isVisible();
- const clientNavigationTransactionPromise = waitForTransaction('nextjs-13-app-dir', transactionEvent => {
+ const clientNavigationTransactionPromise = waitForTransaction('nextjs-app-dir', transactionEvent => {
return (
transactionEvent?.transaction === '/server-component/parameter/foo/bar/baz' &&
transactionEvent.contexts?.trace?.op === 'navigation'
);
});
- const serverComponentTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
transactionEvent?.transaction === 'Page Server Component (/server-component/parameter/[...parameters])' &&
(await clientNavigationTransactionPromise).contexts?.trace?.trace_id ===
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-errors.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-errors.test.ts
new file mode 100644
index 000000000000..b45f61e274a2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-errors.test.ts
@@ -0,0 +1,29 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test('Sends a client-side exception to Sentry', async ({ page }) => {
+ await page.goto('/');
+
+ const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Click Error';
+ });
+
+ await page.getByText('Throw error').click();
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('Click Error');
+
+ expect(errorEvent.request).toEqual({
+ headers: expect.any(Object),
+ url: 'http://localhost:3030/',
+ });
+
+ expect(errorEvent.transaction).toEqual('/');
+
+ expect(errorEvent.contexts?.trace).toEqual({
+ trace_id: expect.any(String),
+ span_id: expect.any(String),
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts
index 09a18791db1f..287c1e8f8633 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts
@@ -4,7 +4,7 @@ import { waitForTransaction } from '@sentry-internal/test-utils';
test('Will capture a connected trace for all server components and generation functions when visiting a page', async ({
page,
}) => {
- const someConnectedEvent = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const someConnectedEvent = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
transactionEvent?.transaction === 'Layout Server Component (/(nested-layout)/nested-layout)' ||
transactionEvent?.transaction === 'Layout Server Component (/(nested-layout))' ||
@@ -13,28 +13,28 @@ test('Will capture a connected trace for all server components and generation fu
);
});
- const layout1Transaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const layout1Transaction = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
transactionEvent?.transaction === 'Layout Server Component (/(nested-layout)/nested-layout)' &&
(await someConnectedEvent).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
);
});
- const layout2Transaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const layout2Transaction = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
transactionEvent?.transaction === 'Layout Server Component (/(nested-layout))' &&
(await someConnectedEvent).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
);
});
- const pageTransaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const pageTransaction = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
transactionEvent?.transaction === 'Page Server Component (/(nested-layout)/nested-layout)' &&
(await someConnectedEvent).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
);
});
- const generateMetadataTransaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const generateMetadataTransaction = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
transactionEvent?.transaction === 'Page.generateMetadata (/(nested-layout)/nested-layout)' &&
(await someConnectedEvent).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/devErrorSymbolification.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/devErrorSymbolification.test.ts
index e4b122521c2d..d1ca11ad9a9e 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/devErrorSymbolification.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/devErrorSymbolification.test.ts
@@ -10,7 +10,7 @@ test.describe('dev mode error symbolification', () => {
test('should have symbolicated dev errors', async ({ page }) => {
await page.goto('/');
- const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
+ const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Click Error';
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts
index df0dda64c4ba..df7ce7afd19a 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
test('Should create a transaction for edge routes', async ({ request }) => {
- const edgerouteTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
transactionEvent?.transaction === 'GET /api/edge-endpoint' && transactionEvent?.contexts?.trace?.status === 'ok'
);
@@ -24,7 +24,7 @@ test('Should create a transaction for edge routes', async ({ request }) => {
});
test('Should create a transaction with error status for faulty edge routes', async ({ request }) => {
- const edgerouteTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
transactionEvent?.transaction === 'GET /api/error-edge-endpoint' &&
transactionEvent?.contexts?.trace?.status === 'internal_error'
@@ -47,7 +47,7 @@ test('Should create a transaction with error status for faulty edge routes', asy
});
test('Should record exceptions for faulty edge routes', async ({ request }) => {
- const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
+ const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Edge Route Error';
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts
index f6c0e7f5bad4..f5277dee6f66 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
test('Should record exceptions for faulty edge server components', async ({ page }) => {
- const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
+ const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Edge Server Component Error';
});
@@ -20,7 +20,7 @@ test('Should record exceptions for faulty edge server components', async ({ page
});
test('Should record transaction for edge server components', async ({ page }) => {
- const serverComponentTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return transactionEvent?.transaction === 'Page Server Component (/edge-server-components)';
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/exceptions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/exceptions.test.ts
deleted file mode 100644
index 4f0f4a6abfce..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/exceptions.test.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { waitForError } from '@sentry-internal/test-utils';
-
-const authToken = process.env.E2E_TEST_AUTH_TOKEN;
-const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
-const sentryTestProject = process.env.E2E_TEST_SENTRY_PROJECT;
-const EVENT_POLLING_TIMEOUT = 90_000;
-
-test('Sends a client-side exception to Sentry', async ({ page }) => {
- await page.goto('/');
-
- const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
- return errorEvent?.exception?.values?.[0]?.value === 'Click Error';
- });
-
- await page.getByText('Throw error').click();
-
- const errorEvent = await errorEventPromise;
- const exceptionEventId = errorEvent.event_id;
-
- expect(errorEvent.transaction).toBe('/');
-
- await expect
- .poll(
- async () => {
- const response = await fetch(
- `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`,
- { headers: { Authorization: `Bearer ${authToken}` } },
- );
- return response.status;
- },
- {
- timeout: EVENT_POLLING_TIMEOUT,
- },
- )
- .toBe(200);
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts
index 2a7b2deab27c..11a5f48799bd 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
test('Should create a transaction for middleware', async ({ request }) => {
- const middlewareTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return transactionEvent?.transaction === 'middleware' && transactionEvent?.contexts?.trace?.status === 'ok';
});
@@ -21,7 +21,7 @@ test('Should create a transaction for middleware', async ({ request }) => {
});
test('Should create a transaction with error status for faulty middleware', async ({ request }) => {
- const middlewareTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
transactionEvent?.transaction === 'middleware' && transactionEvent?.contexts?.trace?.status === 'internal_error'
);
@@ -39,7 +39,7 @@ test('Should create a transaction with error status for faulty middleware', asyn
});
test('Records exceptions happening in middleware', async ({ request }) => {
- const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
+ const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Middleware Error';
});
@@ -56,7 +56,7 @@ test('Records exceptions happening in middleware', async ({ request }) => {
});
test('Should trace outgoing fetch requests inside middleware and create breadcrumbs for it', async ({ request }) => {
- const middlewareTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
transactionEvent?.transaction === 'middleware' &&
!!transactionEvent.spans?.find(span => span.op === 'http.client')
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts
index 9cd49e58990f..a67e4328ba1c 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts
@@ -2,11 +2,11 @@ import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
test('Will capture error for SSR rendering error with a connected trace (Class Component)', async ({ page }) => {
- const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
+ const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error Class';
});
- const serverComponentTransaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const serverComponentTransaction = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
transactionEvent?.transaction === '/pages-router/ssr-error-class' &&
(await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
@@ -20,11 +20,11 @@ test('Will capture error for SSR rendering error with a connected trace (Class C
});
test('Will capture error for SSR rendering error with a connected trace (Functional Component)', async ({ page }) => {
- const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
+ const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error FC';
});
- const ssrTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const ssrTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
transactionEvent?.transaction === '/pages-router/ssr-error-fc' &&
(await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/request-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/request-instrumentation.test.ts
index 8d07d0192f44..d032c6985c94 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/request-instrumentation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/request-instrumentation.test.ts
@@ -5,7 +5,7 @@ import { waitForTransaction } from '@sentry-internal/test-utils';
// Sometimes the request span was included in the handler span, more often it wasn't. I have no idea why. Maybe one day we will
// figure it out. Today is not that day.
test.skip('Should send a transaction with a http span', async ({ request }) => {
- const transactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const transactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return transactionEvent?.transaction === 'GET /api/request-instrumentation';
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts
index d10f402c0c17..afa02e60884a 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
test('Should create a transaction for route handlers', async ({ request }) => {
- const routehandlerTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const routehandlerTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return transactionEvent?.transaction === 'GET /route-handlers/[param]';
});
@@ -19,7 +19,7 @@ test('Should create a transaction for route handlers', async ({ request }) => {
test('Should create a transaction for route handlers and correctly set span status depending on http status', async ({
request,
}) => {
- const routehandlerTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const routehandlerTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return transactionEvent?.transaction === 'POST /route-handlers/[param]';
});
@@ -33,11 +33,11 @@ test('Should create a transaction for route handlers and correctly set span stat
});
test('Should record exceptions and transactions for faulty route handlers', async ({ request }) => {
- const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
+ const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'route-handler-error';
});
- const routehandlerTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const routehandlerTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return transactionEvent?.transaction === 'PUT /route-handlers/[param]/error';
});
@@ -65,7 +65,7 @@ test('Should record exceptions and transactions for faulty route handlers', asyn
test.describe('Edge runtime', () => {
test('should create a transaction for route handlers', async ({ request }) => {
- const routehandlerTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const routehandlerTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return transactionEvent?.transaction === 'PATCH /route-handlers/[param]/edge';
});
@@ -79,11 +79,11 @@ test.describe('Edge runtime', () => {
});
test('should record exceptions and transactions for faulty route handlers', async ({ request }) => {
- const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
+ const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'route-handler-edge-error';
});
- const routehandlerTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const routehandlerTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return transactionEvent?.transaction === 'DELETE /route-handlers/[param]/edge';
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts
index 9c28f78510da..6f0413d0cc61 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts
@@ -1,16 +1,11 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
-const authToken = process.env.E2E_TEST_AUTH_TOKEN;
-const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
-const sentryTestProject = process.env.E2E_TEST_SENTRY_PROJECT;
-const EVENT_POLLING_TIMEOUT = 90_000;
-
test('Sends a transaction for a server component', async ({ page }) => {
// TODO: Fix that this is flakey on dev server - might be an SDK bug
test.skip(process.env.TEST_ENV === 'production', 'Flakey on dev-server');
- const serverComponentTransactionPromise = waitForTransaction('nextjs-13-app-dir', transactionEvent => {
+ const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'function.nextjs' &&
transactionEvent?.transaction === 'Page Server Component (/server-component/parameter/[...parameters])'
@@ -20,37 +15,51 @@ test('Sends a transaction for a server component', async ({ page }) => {
await page.goto('/server-component/parameter/1337/42');
const transactionEvent = await serverComponentTransactionPromise;
- const transactionEventId = transactionEvent.event_id;
-
- expect(transactionEvent.request?.headers).toBeDefined();
-
- await expect
- .poll(
- async () => {
- const response = await fetch(
- `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`,
- { headers: { Authorization: `Bearer ${authToken}` } },
- );
- return response.status;
+
+ expect(transactionEvent.contexts?.trace).toEqual({
+ data: expect.objectContaining({
+ 'sentry.op': 'function.nextjs',
+ 'sentry.origin': 'auto.function.nextjs',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'component',
+ }),
+ op: 'function.nextjs',
+ origin: 'auto.function.nextjs',
+ span_id: expect.any(String),
+ status: 'ok',
+ trace_id: expect.any(String),
+ });
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ request: {
+ cookies: {},
+ headers: expect.any(Object),
+ url: expect.any(String),
},
- {
- timeout: EVENT_POLLING_TIMEOUT,
+ transaction: 'Page Server Component (/server-component/parameter/[...parameters])',
+ type: 'transaction',
+ transaction_info: {
+ source: 'component',
},
- )
- .toBe(200);
+ spans: [],
+ }),
+ );
});
test('Should not set an error status on a server component transaction when it redirects', async ({ page }) => {
// TODO: Fix that this is flakey on dev server - might be an SDK bug
test.skip(process.env.TEST_ENV === 'production', 'Flakey on dev-server');
- const serverComponentTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return transactionEvent?.transaction === 'Page Server Component (/server-component/redirect)';
});
await page.goto('/server-component/redirect');
- expect((await serverComponentTransactionPromise).contexts?.trace?.status).not.toBe('internal_error');
+ const transactionEvent = await serverComponentTransactionPromise;
+
+ expect(transactionEvent.contexts?.trace?.status).not.toBe('internal_error');
});
test('Should set a "not_found" status on a server component transaction when notFound() is called', async ({
@@ -59,21 +68,23 @@ test('Should set a "not_found" status on a server component transaction when not
// TODO: Fix that this is flakey on dev server - might be an SDK bug
test.skip(process.env.TEST_ENV === 'production', 'Flakey on dev-server');
- const serverComponentTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return transactionEvent?.transaction === 'Page Server Component (/server-component/not-found)';
});
await page.goto('/server-component/not-found');
- expect((await serverComponentTransactionPromise).contexts?.trace?.status).toBe('not_found');
+ const transactionEvent = await serverComponentTransactionPromise;
+
+ expect(transactionEvent.contexts?.trace?.status).toBe('not_found');
});
test('Should capture an error and transaction with correct status for a faulty server component', async ({ page }) => {
- const transactionEventPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const transactionEventPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return transactionEvent?.transaction === 'Page Server Component (/server-component/faulty)';
});
- const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
+ const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'I am a faulty server component';
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts
index 4d0f71ac9d97..4a5c7771705b 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts
@@ -3,35 +3,43 @@ import { waitForTransaction } from '@sentry-internal/test-utils';
const packageJson = require('../package.json');
-const authToken = process.env.E2E_TEST_AUTH_TOKEN;
-const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
-const sentryTestProject = process.env.E2E_TEST_SENTRY_PROJECT;
-const EVENT_POLLING_TIMEOUT = 90_000;
-
test('Sends a pageload transaction', async ({ page }) => {
- const pageloadTransactionEventPromise = waitForTransaction('nextjs-13-app-dir', transactionEvent => {
+ const pageloadTransactionEventPromise = waitForTransaction('nextjs-app-dir', transactionEvent => {
return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/';
});
await page.goto('/');
const transactionEvent = await pageloadTransactionEventPromise;
- const transactionEventId = transactionEvent.event_id;
-
- await expect
- .poll(
- async () => {
- const response = await fetch(
- `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`,
- { headers: { Authorization: `Bearer ${authToken}` } },
- );
- return response.status;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ transaction: '/',
+ tags: { runtime: 'browser' },
+ transaction_info: { source: 'url' },
+ type: 'transaction',
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ op: 'pageload',
+ origin: 'auto.pageload.nextjs.app_router_instrumentation',
+ data: expect.objectContaining({
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'url',
+ }),
+ },
},
- {
- timeout: EVENT_POLLING_TIMEOUT,
+ request: {
+ headers: {
+ 'User-Agent': expect.any(String),
+ },
+ url: 'http://localhost:3030/',
},
- )
- .toBe(200);
+ }),
+ );
});
test('Should send a transaction for instrumented server actions', async ({ page }) => {
@@ -39,22 +47,24 @@ test('Should send a transaction for instrumented server actions', async ({ page
const nextjsMajor = Number(nextjsVersion.split('.')[0]);
test.skip(!isNaN(nextjsMajor) && nextjsMajor < 14, 'only applies to nextjs apps >= version 14');
- const serverComponentTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return transactionEvent?.transaction === 'serverAction/myServerAction';
});
await page.goto('/server-action');
await page.getByText('Run Action').click();
- expect(await serverComponentTransactionPromise).toBeDefined();
- expect((await serverComponentTransactionPromise).extra).toMatchObject({
+ const transactionEvent = await serverComponentTransactionPromise;
+
+ expect(transactionEvent).toBeDefined();
+ expect(transactionEvent.extra).toMatchObject({
'server_action_form_data.some-text-value': 'some-default-value',
server_action_result: {
city: 'Vienna',
},
});
- expect(Object.keys((await serverComponentTransactionPromise).request?.headers || {}).length).toBeGreaterThan(0);
+ expect(Object.keys(transactionEvent.request?.headers || {}).length).toBeGreaterThan(0);
});
test('Should set not_found status for server actions calling notFound()', async ({ page }) => {
@@ -62,21 +72,23 @@ test('Should set not_found status for server actions calling notFound()', async
const nextjsMajor = Number(nextjsVersion.split('.')[0]);
test.skip(!isNaN(nextjsMajor) && nextjsMajor < 14, 'only applies to nextjs apps >= version 14');
- const serverComponentTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return transactionEvent?.transaction === 'serverAction/notFoundServerAction';
});
await page.goto('/server-action');
await page.getByText('Run NotFound Action').click();
- expect(await serverComponentTransactionPromise).toBeDefined();
- expect(await (await serverComponentTransactionPromise).contexts?.trace?.status).toBe('not_found');
+ const transactionEvent = await serverComponentTransactionPromise;
+
+ expect(transactionEvent).toBeDefined();
+ expect(transactionEvent.contexts?.trace?.status).toBe('not_found');
});
test('Will not include spans in pageload transaction with faulty timestamps for slow loading pages', async ({
page,
}) => {
- const pageloadTransactionEventPromise = waitForTransaction('nextjs-13-app-dir', transactionEvent => {
+ const pageloadTransactionEventPromise = waitForTransaction('nextjs-app-dir', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/very-slow-component'
);
@@ -86,6 +98,5 @@ test('Will not include spans in pageload transaction with faulty timestamps for
const pageLoadTransaction = await pageloadTransactionEventPromise;
- // @ts-expect-error We are looking at the serialized span format here
- expect(pageLoadTransaction.spans?.filter(span => span.timestamp < span.start_timestamp)).toHaveLength(0);
+ expect(pageLoadTransaction.spans?.filter(span => span.timestamp! < span.start_timestamp)).toHaveLength(0);
});
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 aa4704a1d950..fa0ace0e1e5d 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
@@ -12,7 +12,6 @@ test('Sends an API route transaction', async ({ baseURL }) => {
await fetch(`${baseURL}/test-transaction`);
const transactionEvent = await pageloadTransactionEventPromise;
- const transactionEventId = transactionEvent.event_id;
expect(transactionEvent.contexts?.trace).toEqual({
data: {
From 51bc570b1ddaf8fa1a9827b7acd57ec5012e109f Mon Sep 17 00:00:00 2001
From: Francesco Novy
Date: Mon, 10 Jun 2024 09:24:57 +0200
Subject: [PATCH 13/31] test: Remove unneeded E2E test app dependencies
(#12406)
We don't actually use these, so no need to install them.
---
.../create-react-app/package.json | 7 +------
.../create-react-app/src/index.tsx | 6 ------
.../create-react-app/src/reportWebVitals.ts | 15 ---------------
.../test-applications/react-19/package.json | 7 +------
.../react-create-hash-router/package.json | 4 ----
.../test-applications/react-router-5/package.json | 7 +------
.../react-send-to-sentry/package.json | 7 +------
7 files changed, 4 insertions(+), 49 deletions(-)
delete mode 100644 dev-packages/e2e-tests/test-applications/create-react-app/src/reportWebVitals.ts
diff --git a/dev-packages/e2e-tests/test-applications/create-react-app/package.json b/dev-packages/e2e-tests/test-applications/create-react-app/package.json
index 4b1c62920154..ce3471d2a7d1 100644
--- a/dev-packages/e2e-tests/test-applications/create-react-app/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-react-app/package.json
@@ -4,18 +4,13 @@
"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",
- "@types/jest": "27.0.1",
"@types/node": "16.7.13",
"@types/react": "18.0.0",
"@types/react-dom": "18.0.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-scripts": "5.0.1",
- "typescript": "4.9.5",
- "web-vitals": "2.1.0"
+ "typescript": "4.9.5"
},
"scripts": {
"start": "react-scripts start",
diff --git a/dev-packages/e2e-tests/test-applications/create-react-app/src/index.tsx b/dev-packages/e2e-tests/test-applications/create-react-app/src/index.tsx
index 1c10b3e92da6..3dcb4c4fd07f 100644
--- a/dev-packages/e2e-tests/test-applications/create-react-app/src/index.tsx
+++ b/dev-packages/e2e-tests/test-applications/create-react-app/src/index.tsx
@@ -3,7 +3,6 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
-import reportWebVitals from './reportWebVitals';
Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
@@ -21,8 +20,3 @@ root.render(
,
);
-
-// If you want to start measuring performance in your app, pass a function
-// to log results (for example: reportWebVitals(console.log))
-// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
-reportWebVitals();
diff --git a/dev-packages/e2e-tests/test-applications/create-react-app/src/reportWebVitals.ts b/dev-packages/e2e-tests/test-applications/create-react-app/src/reportWebVitals.ts
deleted file mode 100644
index 49a2a16e0fbc..000000000000
--- a/dev-packages/e2e-tests/test-applications/create-react-app/src/reportWebVitals.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { ReportHandler } from 'web-vitals';
-
-const reportWebVitals = (onPerfEntry?: ReportHandler) => {
- if (onPerfEntry && onPerfEntry instanceof Function) {
- import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
- getCLS(onPerfEntry);
- getFID(onPerfEntry);
- getFCP(onPerfEntry);
- getLCP(onPerfEntry);
- getTTFB(onPerfEntry);
- });
- }
-};
-
-export default reportWebVitals;
diff --git a/dev-packages/e2e-tests/test-applications/react-19/package.json b/dev-packages/e2e-tests/test-applications/react-19/package.json
index d83bd81d6c3e..058fd0bb847a 100644
--- a/dev-packages/e2e-tests/test-applications/react-19/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-19/package.json
@@ -4,20 +4,15 @@
"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"
+ "typescript": "4.9.5"
},
"scripts": {
"build": "react-scripts build",
diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json
index c289c76af506..d8d6a58fe16a 100644
--- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/package.json
@@ -4,10 +4,6 @@
"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",
- "@types/jest": "27.0.1",
"@types/node": "16.7.13",
"@types/react": "18.0.0",
"@types/react-dom": "18.0.0",
diff --git a/dev-packages/e2e-tests/test-applications/react-router-5/package.json b/dev-packages/e2e-tests/test-applications/react-router-5/package.json
index e60c3e7f346b..55ccf5492d9f 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-5/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-router-5/package.json
@@ -4,12 +4,8 @@
"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": "18.0.0",
"@types/react-dom": "18.0.0",
@@ -19,8 +15,7 @@
"react-dom": "18.2.0",
"react-router-dom": "5.3.4",
"react-scripts": "5.0.1",
- "typescript": "4.9.5",
- "web-vitals": "2.1.0"
+ "typescript": "4.9.5"
},
"scripts": {
"build": "react-scripts build",
diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json
index cfe6db496351..4684e3401e63 100644
--- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json
+++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/package.json
@@ -4,10 +4,6 @@
"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",
- "@types/jest": "27.0.1",
"@types/node": "16.7.13",
"@types/react": "18.0.0",
"@types/react-dom": "18.0.0",
@@ -15,8 +11,7 @@
"react-dom": "18.2.0",
"react-router-dom": "^6.4.1",
"react-scripts": "5.0.1",
- "typescript": "4.9.5",
- "web-vitals": "2.1.0"
+ "typescript": "4.9.5"
},
"scripts": {
"build": "react-scripts build",
From d2363ccd9a7e4054523a6dc6c4c437f039bf6348 Mon Sep 17 00:00:00 2001
From: Lukas Stracke
Date: Mon, 10 Jun 2024 10:13:55 +0200
Subject: [PATCH 14/31] chore(changelog): Add external contributor credit
message to unreleased (#12427)
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa40eb7279cd..652a444f5718 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+Work in this release was contributed by @soch4n. Thank you for your contribution!
+
## 8.8.0
- **feat: Upgrade OTEL dependencies (#12388)**
From 71f2062048071d8fea655de88089e96cd4137c5c Mon Sep 17 00:00:00 2001
From: Luca Forstner
Date: Mon, 10 Jun 2024 11:33:25 +0200
Subject: [PATCH 15/31] feat(nextjs): Allow for suppressing warning about
missing global error handler file (#12369)
---
packages/nextjs/src/config/webpack.ts | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts
index 44537ede59f8..e529a2b47f57 100644
--- a/packages/nextjs/src/config/webpack.ts
+++ b/packages/nextjs/src/config/webpack.ts
@@ -291,14 +291,18 @@ export function constructWebpackConfigFunction(
globalErrorFile => fs.existsSync(path.join(appDirPath!, globalErrorFile)),
);
- if (!hasGlobalErrorFile && !showedMissingGlobalErrorWarningMsg) {
+ if (
+ !hasGlobalErrorFile &&
+ !showedMissingGlobalErrorWarningMsg &&
+ !process.env.SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING
+ ) {
// eslint-disable-next-line no-console
console.log(
`${chalk.yellow(
'warn',
)} - It seems like you don't have a global error handler set up. It is recommended that you add a ${chalk.cyan(
'global-error.js',
- )} file with Sentry instrumentation so that React rendering errors are reported to Sentry. Read more: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#react-render-errors-in-app-router`,
+ )} file with Sentry instrumentation so that React rendering errors are reported to Sentry. Read more: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#react-render-errors-in-app-router (you can suppress this warning by setting SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING=1 as environment variable)`,
);
showedMissingGlobalErrorWarningMsg = true;
}
From 30f0c5513f8f433f6a8c91926d4970458e2469dd Mon Sep 17 00:00:00 2001
From: Andrei <168741329+andreiborza@users.noreply.github.com>
Date: Mon, 10 Jun 2024 11:34:04 +0200
Subject: [PATCH 16/31] feat(solidjs): Add sourcemap instructions to README
(#12425)
---
packages/solidjs/README.md | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/packages/solidjs/README.md b/packages/solidjs/README.md
index 00d451763624..68929dff1c21 100644
--- a/packages/solidjs/README.md
+++ b/packages/solidjs/README.md
@@ -6,6 +6,10 @@
# Official Sentry SDK for SolidJS
+[](https://www.npmjs.com/package/@sentry/solidjs)
+[](https://www.npmjs.com/package/@sentry/solidjs)
+[](https://www.npmjs.com/package/@sentry/solidjs)
+
This SDK is work in progress, and should not be used before officially released.
# Solid Router
@@ -48,3 +52,9 @@ render(
document.getElementById('root'),
);
```
+
+# Sourcemaps and Releases
+
+To generate and upload source maps of your Solid JS app bundle, check our guide
+[how to configure your bundler](https://docs.sentry.io/platforms/javascript/guides/solid/sourcemaps/#uploading-source-maps)
+to emit source maps.
From 2149597c53823205783486666f47d01810312425 Mon Sep 17 00:00:00 2001
From: Francesco Novy
Date: Mon, 10 Jun 2024 13:12:49 +0200
Subject: [PATCH 17/31] docs: Update contributing docs to mention volta
(#12431)
This updates contributing docs to mention that we use volta, + also to
set up PNPM support.
---
CONTRIBUTING.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a02b096ed125..161cd60ef540 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -14,9 +14,10 @@ Developer Documentation.
## Setting up an Environment
-To run the test suite and our code linter, node.js and yarn are required.
+We use [Volta](https://volta.sh/) to ensure we use consistent versions of node, yarn and pnpm.
-[`node` download](https://nodejs.org/download) [`yarn` download](https://yarnpkg.com/en/docs/install)
+Make sure to also enable [pnpm support in Volta](https://docs.volta.sh/advanced/pnpm) if you want to run the E2E tests
+locally.
`sentry-javascript` is a monorepo containing several packages, and we use `lerna` to manage them. To get started,
install all dependencies, and then perform an initial build, so TypeScript can read all of the linked type definitions.
From a238fffad2737e541532c5fd78d57d89f860e3a5 Mon Sep 17 00:00:00 2001
From: Lukas Stracke
Date: Mon, 10 Jun 2024 13:58:50 +0200
Subject: [PATCH 18/31] fix(aws-serverless): Only start root span in Sentry
wrapper if Otel didn't wrap handler (#12407)
Fix span collision by checking if the AWS lambda handler was already
wrapped by Otel instrumentation and only if not starting our own root
span. The rest of our handler is still being used (i.e. the flushing
logic, error capturing, etc) regardless of otel wrapping.
Also Adjusted E2E tests to:
- more closely resemble the AWS environment
- enable auto patching of the handler with the Sentry SDK handler
- better check for Otel and manual spans
Ref #12409
---
.github/workflows/build.yml | 2 +-
.../.npmrc | 0
.../package.json | 0
.../playwright.config.ts | 0
.../src/lambda-function.js | 21 ++++++
.../aws-lambda-layer-cjs/src/run-lambda.js | 7 ++
.../src/run.js | 5 +-
.../start-event-proxy.mjs | 2 +-
.../aws-lambda-layer-cjs/tests/basic.test.ts | 69 +++++++++++++++++++
.../aws-lambda-layer/src/lambda-function.js | 19 -----
.../aws-lambda-layer/src/run-lambda.js | 2 -
.../aws-lambda-layer/tests/basic.test.ts | 36 ----------
packages/aws-serverless/src/sdk.ts | 25 ++++++-
13 files changed, 125 insertions(+), 63 deletions(-)
rename dev-packages/e2e-tests/test-applications/{aws-lambda-layer => aws-lambda-layer-cjs}/.npmrc (100%)
rename dev-packages/e2e-tests/test-applications/{aws-lambda-layer => aws-lambda-layer-cjs}/package.json (100%)
rename dev-packages/e2e-tests/test-applications/{aws-lambda-layer => aws-lambda-layer-cjs}/playwright.config.ts (100%)
create mode 100644 dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/lambda-function.js
create mode 100644 dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/run-lambda.js
rename dev-packages/e2e-tests/test-applications/{aws-lambda-layer => aws-lambda-layer-cjs}/src/run.js (64%)
rename dev-packages/e2e-tests/test-applications/{aws-lambda-layer => aws-lambda-layer-cjs}/start-event-proxy.mjs (71%)
create mode 100644 dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts
delete mode 100644 dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/lambda-function.js
delete mode 100644 dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/run-lambda.js
delete mode 100644 dev-packages/e2e-tests/test-applications/aws-lambda-layer/tests/basic.test.ts
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 35fb1d8d1436..c3bda957409b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -993,7 +993,7 @@ jobs:
[
'angular-17',
'angular-18',
- 'aws-lambda-layer',
+ 'aws-lambda-layer-cjs',
'cloudflare-astro',
'node-express',
'create-react-app',
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/.npmrc b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/.npmrc
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/aws-lambda-layer/.npmrc
rename to dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/.npmrc
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/package.json b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/aws-lambda-layer/package.json
rename to dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/playwright.config.ts b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/playwright.config.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/aws-lambda-layer/playwright.config.ts
rename to dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/playwright.config.ts
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/lambda-function.js b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/lambda-function.js
new file mode 100644
index 000000000000..c688ed35a0c4
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/lambda-function.js
@@ -0,0 +1,21 @@
+const Sentry = require('@sentry/aws-serverless');
+
+const http = require('http');
+
+async function handle() {
+ await Sentry.startSpan({ name: 'manual-span', op: 'test' }, async () => {
+ await new Promise(resolve => {
+ http.get('http://example.com', res => {
+ res.on('data', d => {
+ process.stdout.write(d);
+ });
+
+ res.on('end', () => {
+ resolve();
+ });
+ });
+ });
+ });
+}
+
+module.exports = { handle };
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/run-lambda.js b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/run-lambda.js
new file mode 100644
index 000000000000..1d6e059e78f3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/run-lambda.js
@@ -0,0 +1,7 @@
+const { handle } = require('./lambda-function');
+const event = {};
+const context = {
+ invokedFunctionArn: 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda',
+ functionName: 'my-lambda',
+};
+handle(event, context);
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/run.js b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/run.js
similarity index 64%
rename from dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/run.js
rename to dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/run.js
index 2a99cff2d48e..2605f624ca9a 100644
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/run.js
+++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/src/run.js
@@ -4,8 +4,9 @@ child_process.execSync('node ./src/run-lambda.js', {
stdio: 'inherit',
env: {
...process.env,
- LAMBDA_TASK_ROOT: '.',
- _HANDLER: 'handle',
+ // On AWS, LAMBDA_TASK_ROOT is usually /var/task but for testing, we set it to the CWD to correctly apply our handler
+ LAMBDA_TASK_ROOT: process.cwd(),
+ _HANDLER: 'src/lambda-function.handle',
NODE_OPTIONS: '--require @sentry/aws-serverless/dist/awslambda-auto',
SENTRY_DSN: 'http://public@localhost:3031/1337',
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/start-event-proxy.mjs
similarity index 71%
rename from dev-packages/e2e-tests/test-applications/aws-lambda-layer/start-event-proxy.mjs
rename to dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/start-event-proxy.mjs
index e64e99cda75b..abc7ea7b0ab2 100644
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/start-event-proxy.mjs
+++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/start-event-proxy.mjs
@@ -2,6 +2,6 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';
startEventProxyServer({
port: 3031,
- proxyServerName: 'aws-serverless-lambda-layer',
+ proxyServerName: 'aws-serverless-lambda-layer-cjs',
forwardToSentry: false,
});
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts
new file mode 100644
index 000000000000..a1211c0f11bc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts
@@ -0,0 +1,69 @@
+import * as child_process from 'child_process';
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Lambda layer SDK bundle sends events', async ({ request }) => {
+ const transactionEventPromise = waitForTransaction('aws-serverless-lambda-layer-cjs', transactionEvent => {
+ return transactionEvent?.transaction === 'my-lambda';
+ });
+
+ // Waiting for 1s here because attaching the listener for events in `waitForTransaction` is not synchronous
+ // Since in this test, we don't start a browser via playwright, we don't have the usual delays (page.goto, etc)
+ // which are usually enough for us to never have noticed this race condition before.
+ // This is a workaround but probably sufficient as long as we only experience it in this test.
+ await new Promise(resolve =>
+ setTimeout(() => {
+ resolve();
+ }, 1000),
+ );
+
+ child_process.execSync('pnpm start', {
+ stdio: 'ignore',
+ });
+
+ const transactionEvent = await transactionEventPromise;
+
+ // shows the SDK sent a transaction
+ expect(transactionEvent.transaction).toEqual('my-lambda'); // name should be the function name
+ expect(transactionEvent.contexts?.trace).toEqual({
+ data: {
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'custom',
+ 'sentry.origin': 'auto.otel.aws-lambda',
+ 'cloud.account.id': '123453789012',
+ 'faas.id': 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda',
+ 'otel.kind': 'SERVER',
+ },
+ origin: 'auto.otel.aws-lambda',
+ span_id: expect.any(String),
+ status: 'ok',
+ trace_id: expect.any(String),
+ });
+
+ expect(transactionEvent.spans).toHaveLength(2);
+
+ // shows that the Otel Http instrumentation is working
+ expect(transactionEvent.spans).toContainEqual(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'sentry.op': 'http.client',
+ 'sentry.origin': 'auto.http.otel.http',
+ url: 'http://example.com/',
+ }),
+ description: 'GET http://example.com/',
+ op: 'http.client',
+ }),
+ );
+
+ // shows that the manual span creation is working
+ expect(transactionEvent.spans).toContainEqual(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'sentry.op': 'test',
+ 'sentry.origin': 'manual',
+ }),
+ description: 'manual-span',
+ op: 'test',
+ }),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/lambda-function.js b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/lambda-function.js
deleted file mode 100644
index aa8f236b742d..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/lambda-function.js
+++ /dev/null
@@ -1,19 +0,0 @@
-const Sentry = require('@sentry/aws-serverless');
-
-const http = require('http');
-
-function handle() {
- Sentry.startSpanManual({ name: 'aws-lambda-layer-test-txn', op: 'test' }, span => {
- http.get('http://example.com', res => {
- res.on('data', d => {
- process.stdout.write(d);
- });
-
- res.on('end', () => {
- span.end();
- });
- });
- });
-}
-
-module.exports = { handle };
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/run-lambda.js b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/run-lambda.js
deleted file mode 100644
index 5e573c484637..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/run-lambda.js
+++ /dev/null
@@ -1,2 +0,0 @@
-const { handle } = require('./lambda-function');
-handle();
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/tests/basic.test.ts b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/tests/basic.test.ts
deleted file mode 100644
index 7f7f5ec1854c..000000000000
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/tests/basic.test.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import * as child_process from 'child_process';
-import { expect, test } from '@playwright/test';
-import { waitForTransaction } from '@sentry-internal/test-utils';
-
-test('Lambda layer SDK bundle sends events', async ({ request }) => {
- const transactionEventPromise = waitForTransaction('aws-serverless-lambda-layer', transactionEvent => {
- return transactionEvent?.transaction === 'aws-lambda-layer-test-txn';
- });
-
- await new Promise(resolve =>
- setTimeout(() => {
- resolve();
- }, 1000),
- );
-
- child_process.execSync('pnpm start', {
- stdio: 'ignore',
- });
-
- const transactionEvent = await transactionEventPromise;
-
- // shows the SDK sent a transaction
- expect(transactionEvent.transaction).toEqual('aws-lambda-layer-test-txn');
-
- // shows that the Otel Http instrumentation is working
- expect(transactionEvent.spans).toHaveLength(1);
- expect(transactionEvent.spans![0]).toMatchObject({
- data: expect.objectContaining({
- 'sentry.op': 'http.client',
- 'sentry.origin': 'auto.http.otel.http',
- url: 'http://example.com/',
- }),
- description: 'GET http://example.com/',
- op: 'http.client',
- });
-});
diff --git a/packages/aws-serverless/src/sdk.ts b/packages/aws-serverless/src/sdk.ts
index 7c491fefc008..905672241ffb 100644
--- a/packages/aws-serverless/src/sdk.ts
+++ b/packages/aws-serverless/src/sdk.ts
@@ -320,7 +320,9 @@ export function wrapHandler(
throw e;
} finally {
clearTimeout(timeoutWarningTimer);
- span?.end();
+ if (span && span.isRecording()) {
+ span.end();
+ }
await flush(options.flushTimeout).catch(e => {
DEBUG_BUILD && logger.error(e);
});
@@ -328,7 +330,10 @@ export function wrapHandler(
return rv;
}
- if (options.startTrace) {
+ // Only start a trace and root span if the handler is not already wrapped by Otel instrumentation
+ // Otherwise, we create two root spans (one from otel, one from our wrapper).
+ // If Otel instrumentation didn't work or was filtered by users, we still want to trace the handler.
+ if (options.startTrace && !isWrappedByOtel(handler)) {
const eventWithHeaders = event as { headers?: { [key: string]: string } };
const sentryTrace =
@@ -361,3 +366,19 @@ export function wrapHandler(
});
};
}
+
+/**
+ * Checks if Otel's AWSLambda instrumentation successfully wrapped the handler.
+ * Check taken from @opentelemetry/core
+ */
+function isWrappedByOtel(
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ handler: Function & { __original?: unknown; __unwrap?: unknown; __wrapped?: boolean },
+): boolean {
+ return (
+ typeof handler === 'function' &&
+ typeof handler.__original === 'function' &&
+ typeof handler.__unwrap === 'function' &&
+ handler.__wrapped === true
+ );
+}
From fee7f686cf88023c1053b80c1471a94ce8b47eb2 Mon Sep 17 00:00:00 2001
From: Lukas Stracke
Date: Mon, 10 Jun 2024 14:13:34 +0200
Subject: [PATCH 19/31] fix(aws-serverless): Add `op` to Otel-generated lambda
function root span (#12430)
Add the `sentry.op` attribute/`op` property to the created root
span/transaction of a lambda function.
---
.../test-applications/aws-lambda-layer-cjs/tests/basic.test.ts | 2 ++
packages/aws-serverless/src/integration/awslambda.ts | 3 ++-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts
index a1211c0f11bc..53a44c424cf4 100644
--- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts
+++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts
@@ -30,10 +30,12 @@ test('Lambda layer SDK bundle sends events', async ({ request }) => {
'sentry.sample_rate': 1,
'sentry.source': 'custom',
'sentry.origin': 'auto.otel.aws-lambda',
+ 'sentry.op': 'function.aws.lambda',
'cloud.account.id': '123453789012',
'faas.id': 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda',
'otel.kind': 'SERVER',
},
+ op: 'function.aws.lambda',
origin: 'auto.otel.aws-lambda',
span_id: expect.any(String),
status: 'ok',
diff --git a/packages/aws-serverless/src/integration/awslambda.ts b/packages/aws-serverless/src/integration/awslambda.ts
index bc89baad5c38..c6516603b6b8 100644
--- a/packages/aws-serverless/src/integration/awslambda.ts
+++ b/packages/aws-serverless/src/integration/awslambda.ts
@@ -1,5 +1,5 @@
import { AwsLambdaInstrumentation } from '@opentelemetry/instrumentation-aws-lambda';
-import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration } from '@sentry/core';
import { addOpenTelemetryInstrumentation } from '@sentry/node';
import type { IntegrationFn } from '@sentry/types';
@@ -11,6 +11,7 @@ const _awsLambdaIntegration = (() => {
new AwsLambdaInstrumentation({
requestHook(span) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.otel.aws-lambda');
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.aws.lambda');
},
}),
);
From 54c26cd70007a74d24c3b921b5a3b2505b1cc867 Mon Sep 17 00:00:00 2001
From: Andrei <168741329+andreiborza@users.noreply.github.com>
Date: Mon, 10 Jun 2024 14:58:13 +0200
Subject: [PATCH 20/31] feat(solidjs): Add `withSentryErrorBoundary` HOC
(#12421)
To automatically capture exceptions from inside a component tree and
render a fallback component, wrap the native Solid
JS `ErrorBoundary` component with `Sentry.withSentryErrorBoundary`.
```js
import * as Sentry from '@sentry/solidjs';
import { ErrorBoundary } from 'solid-js';
Sentry.init({
dsn: '__PUBLIC_DSN__',
tracesSampleRate: 1.0, // Capture 100% of the transactions
});
const SentryErrorBoundary = Sentry.withSentryErrorBoundary(ErrorBoundary);
render(
() => (
Error: {err.message}
}>
),
document.getElementById('root'),
);
```
**Note**: When using an `ErrorBoundary` in conjunction with Solid
Router, the fallback component renders twice, see
[here](https://github.com/solidjs/solid-router/issues/440).
---
.../solidjs/src/pageroot.tsx | 5 +
.../src/pages/errorboundaryexample.tsx | 24 ++++
.../test-applications/solidjs/src/routes.ts | 5 +
.../solidjs/tests/errorboundary.test.ts | 75 ++++++++++++
packages/solidjs/README.md | 27 ++++-
packages/solidjs/package.json | 2 +
packages/solidjs/src/errorboundary.ts | 31 +++++
packages/solidjs/src/index.ts | 1 +
packages/solidjs/test/errorboundary.test.tsx | 108 ++++++++++++++++++
packages/solidjs/tsconfig.test.json | 4 +-
yarn.lock | 46 ++++++++
11 files changed, 326 insertions(+), 2 deletions(-)
create mode 100644 dev-packages/e2e-tests/test-applications/solidjs/src/pages/errorboundaryexample.tsx
create mode 100644 dev-packages/e2e-tests/test-applications/solidjs/tests/errorboundary.test.ts
create mode 100644 packages/solidjs/src/errorboundary.ts
create mode 100644 packages/solidjs/test/errorboundary.test.tsx
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/src/pageroot.tsx b/dev-packages/e2e-tests/test-applications/solidjs/src/pageroot.tsx
index d9770c8a3868..0919c0e362db 100644
--- a/dev-packages/e2e-tests/test-applications/solidjs/src/pageroot.tsx
+++ b/dev-packages/e2e-tests/test-applications/solidjs/src/pageroot.tsx
@@ -10,6 +10,11 @@ export default function PageRoot(props) {
Home
+
+
+ Error Boundary Example
+
+
Error
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/src/pages/errorboundaryexample.tsx b/dev-packages/e2e-tests/test-applications/solidjs/src/pages/errorboundaryexample.tsx
new file mode 100644
index 000000000000..b1dd324a785f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidjs/src/pages/errorboundaryexample.tsx
@@ -0,0 +1,24 @@
+import * as Sentry from '@sentry/solidjs';
+import { ErrorBoundary } from 'solid-js';
+
+const SentryErrorBoundary = Sentry.withSentryErrorBoundary(ErrorBoundary);
+
+export default function ErrorBoundaryExample() {
+ return (
+ (
+
+ Error Boundary Fallback
+
+ {error.message}
+
+
+
+ )}
+ >
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/src/routes.ts b/dev-packages/e2e-tests/test-applications/solidjs/src/routes.ts
index 7b115f68c00c..96b78e113ef5 100644
--- a/dev-packages/e2e-tests/test-applications/solidjs/src/routes.ts
+++ b/dev-packages/e2e-tests/test-applications/solidjs/src/routes.ts
@@ -1,5 +1,6 @@
import { lazy } from 'solid-js';
+import ErrorBoundaryExample from './pages/errorboundaryexample';
import Home from './pages/home';
export const routes = [
@@ -11,6 +12,10 @@ export const routes = [
path: '/user/:id',
component: lazy(() => import('./pages/user')),
},
+ {
+ path: '/error-boundary-example',
+ component: ErrorBoundaryExample,
+ },
{
path: '**',
component: lazy(() => import('./errors/404')),
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/tests/errorboundary.test.ts b/dev-packages/e2e-tests/test-applications/solidjs/tests/errorboundary.test.ts
new file mode 100644
index 000000000000..212c0ede2a2f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/solidjs/tests/errorboundary.test.ts
@@ -0,0 +1,75 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test('captures an exception', async ({ page }) => {
+ const errorEventPromise = waitForError('solidjs', errorEvent => {
+ return !errorEvent.type;
+ });
+
+ const [, errorEvent] = await Promise.all([page.goto('/error-boundary-example'), errorEventPromise]);
+
+ expect(errorEvent).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'ReferenceError',
+ value: 'NonExistentComponent is not defined',
+ mechanism: {
+ type: 'generic',
+ handled: true,
+ },
+ },
+ ],
+ },
+ transaction: '/error-boundary-example',
+ });
+});
+
+test('captures a second exception after resetting the boundary', async ({ page }) => {
+ const firstErrorEventPromise = waitForError('solidjs', errorEvent => {
+ return !errorEvent.type;
+ });
+
+ const [, firstErrorEvent] = await Promise.all([page.goto('/error-boundary-example'), firstErrorEventPromise]);
+
+ expect(firstErrorEvent).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'ReferenceError',
+ value: 'NonExistentComponent is not defined',
+ mechanism: {
+ type: 'generic',
+ handled: true,
+ },
+ },
+ ],
+ },
+ transaction: '/error-boundary-example',
+ });
+
+ const secondErrorEventPromise = waitForError('solidjs', errorEvent => {
+ return !errorEvent.type;
+ });
+
+ const [, secondErrorEvent] = await Promise.all([
+ page.locator('#errorBoundaryResetBtn').click(),
+ await secondErrorEventPromise,
+ ]);
+
+ expect(secondErrorEvent).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'ReferenceError',
+ value: 'NonExistentComponent is not defined',
+ mechanism: {
+ type: 'generic',
+ handled: true,
+ },
+ },
+ ],
+ },
+ transaction: '/error-boundary-example',
+ });
+});
diff --git a/packages/solidjs/README.md b/packages/solidjs/README.md
index 68929dff1c21..c861adbfdba9 100644
--- a/packages/solidjs/README.md
+++ b/packages/solidjs/README.md
@@ -37,7 +37,6 @@ Sentry.init({
dsn: '__PUBLIC_DSN__',
integrations: [Sentry.solidRouterBrowserTracingIntegration({ useBeforeLeave, useLocation })],
tracesSampleRate: 1.0, // Capture 100% of the transactions
- debug: true,
});
const SentryRouter = Sentry.withSentryRouterRouting(Router);
@@ -53,6 +52,32 @@ render(
);
```
+# ErrorBoundary
+
+To automatically capture exceptions from inside a component tree and render a fallback component, wrap the native Solid
+JS `ErrorBoundary` component with `Sentry.withSentryErrorBoundary`.
+
+```js
+import * as Sentry from '@sentry/solidjs';
+import { ErrorBoundary } from 'solid-js';
+
+Sentry.init({
+ dsn: '__PUBLIC_DSN__',
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+});
+
+const SentryErrorBoundary = Sentry.withSentryErrorBoundary(ErrorBoundary);
+
+render(
+ () => (
+ Error: {err.message}
}>
+
+
+ ),
+ document.getElementById('root'),
+);
+```
+
# Sourcemaps and Releases
To generate and upload source maps of your Solid JS app bundle, check our guide
diff --git a/packages/solidjs/package.json b/packages/solidjs/package.json
index aa6930b6c4aa..d1ba81386d8a 100644
--- a/packages/solidjs/package.json
+++ b/packages/solidjs/package.json
@@ -54,6 +54,8 @@
"@solidjs/router": "^0.13.5",
"@solidjs/testing-library": "0.8.5",
"solid-js": "^1.8.11",
+ "@testing-library/jest-dom": "^6.4.5",
+ "@testing-library/user-event": "^14.5.2",
"vite-plugin-solid": "^2.8.2"
},
"scripts": {
diff --git a/packages/solidjs/src/errorboundary.ts b/packages/solidjs/src/errorboundary.ts
new file mode 100644
index 000000000000..8a1ad0efa902
--- /dev/null
+++ b/packages/solidjs/src/errorboundary.ts
@@ -0,0 +1,31 @@
+import { captureException } from '@sentry/browser';
+import type { Component, JSX } from 'solid-js';
+import { mergeProps, splitProps } from 'solid-js';
+import { createComponent } from 'solid-js/web';
+
+type ErrorBoundaryProps = {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ fallback: JSX.Element | ((err: any, reset: () => void) => JSX.Element);
+ children: JSX.Element;
+};
+
+/**
+ * A higher-order component to wrap Solid's ErrorBoundary to capture exceptions.
+ */
+export function withSentryErrorBoundary(ErrorBoundary: Component): Component {
+ const SentryErrorBoundary = (props: ErrorBoundaryProps): JSX.Element => {
+ const [local, others] = splitProps(props, ['fallback']);
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const fallback = (error: any, reset: () => void): JSX.Element => {
+ captureException(error);
+
+ const f = local.fallback;
+ return typeof f === 'function' ? f(error, reset) : f;
+ };
+
+ return createComponent(ErrorBoundary, mergeProps({ fallback }, others));
+ };
+
+ return SentryErrorBoundary;
+}
diff --git a/packages/solidjs/src/index.ts b/packages/solidjs/src/index.ts
index 77f17110f5e1..48aee7358776 100644
--- a/packages/solidjs/src/index.ts
+++ b/packages/solidjs/src/index.ts
@@ -3,3 +3,4 @@ export * from '@sentry/browser';
export { init } from './sdk';
export * from './solidrouter';
+export * from './errorboundary';
diff --git a/packages/solidjs/test/errorboundary.test.tsx b/packages/solidjs/test/errorboundary.test.tsx
new file mode 100644
index 000000000000..f1e3f2c0e56a
--- /dev/null
+++ b/packages/solidjs/test/errorboundary.test.tsx
@@ -0,0 +1,108 @@
+/* eslint-disable @typescript-eslint/unbound-method */
+import type * as SentryBrowser from '@sentry/browser';
+import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core';
+import { render } from '@solidjs/testing-library';
+import userEvent from '@testing-library/user-event';
+import { vi } from 'vitest';
+
+import { ErrorBoundary } from 'solid-js';
+import { BrowserClient, withSentryErrorBoundary } from '../src';
+
+const mockCaptureException = vi.fn();
+vi.mock('@sentry/browser', async () => {
+ const actual = await vi.importActual('@sentry/browser');
+ return {
+ ...actual,
+ captureException: (...args) => mockCaptureException(...args),
+ } as typeof SentryBrowser;
+});
+
+const user = userEvent.setup();
+const SentryErrorBoundary = withSentryErrorBoundary(ErrorBoundary);
+
+describe('withSentryErrorBoundary', () => {
+ function createMockBrowserClient(): BrowserClient {
+ return new BrowserClient({
+ integrations: [],
+ tracesSampleRate: 1,
+ transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})),
+ stackParser: () => [],
+ });
+ }
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+
+ const client = createMockBrowserClient();
+ setCurrentClient(client);
+ });
+
+ afterEach(() => {
+ getCurrentScope().setClient(undefined);
+ });
+
+ it('calls `captureException` when an error occurs`', () => {
+ render(() => (
+ Ooops, an error occurred.}>
+
+
+ ));
+
+ expect(mockCaptureException).toHaveBeenCalledTimes(1);
+ expect(mockCaptureException).toHaveBeenLastCalledWith(new ReferenceError('NonExistentComponent is not defined'));
+ });
+
+ it('renders the fallback component', async () => {
+ const { findByText } = render(() => (
+ Ooops, an error occurred.}>
+
+
+ ));
+
+ expect(await findByText('Ooops, an error occurred.')).toBeInTheDocument();
+ });
+
+ it('passes the `error` and `reset` function to the fallback component', () => {
+ const mockFallback = vi.fn();
+
+ render(() => {
+
+
+ ;
+ });
+
+ expect(mockFallback).toHaveBeenCalledTimes(1);
+ expect(mockFallback).toHaveBeenCalledWith(
+ new ReferenceError('NonExistentComponent is not defined'),
+ expect.any(Function),
+ );
+ });
+
+ it('calls `captureException` again after resetting', async () => {
+ const { findByRole } = render(() => (
+ }>
+
+
+ ));
+
+ expect(mockCaptureException).toHaveBeenCalledTimes(1);
+ expect(mockCaptureException).toHaveBeenNthCalledWith(1, new ReferenceError('NonExistentComponent is not defined'));
+
+ const button = await findByRole('button');
+ await user.click(button);
+
+ expect(mockCaptureException).toHaveBeenCalledTimes(2);
+ expect(mockCaptureException).toHaveBeenNthCalledWith(2, new ReferenceError('NonExistentComponent is not defined'));
+ });
+
+ it('renders children when there is no error', async () => {
+ const { queryByText } = render(() => (
+ Oops, an error occurred.}>
+ Adopt a cat
+
+ ));
+
+ expect(await queryByText('Adopt a cat')).toBeInTheDocument();
+ expect(await queryByText('Ooops, an error occurred')).not.toBeInTheDocument();
+ });
+});
diff --git a/packages/solidjs/tsconfig.test.json b/packages/solidjs/tsconfig.test.json
index fc9e549d35ce..adecd5079938 100644
--- a/packages/solidjs/tsconfig.test.json
+++ b/packages/solidjs/tsconfig.test.json
@@ -5,8 +5,10 @@
"compilerOptions": {
// should include all types from `./tsconfig.json` plus types for all test frameworks used
- "types": ["vitest/globals"]
+ "types": ["vitest/globals", "vite/client", "@testing-library/jest-dom"],
// other package-specific, test-specific options
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js"
}
}
diff --git a/yarn.lock b/yarn.lock
index 479522489271..b88db586cd18 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -63,6 +63,11 @@
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.3.tgz#90749bde8b89cd41764224f5aac29cd4138f75ff"
integrity sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==
+"@adobe/css-tools@^4.3.2":
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63"
+ integrity sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==
+
"@ampproject/remapping@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
@@ -3420,6 +3425,13 @@
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.9.2":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12"
+ integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
+ dependencies:
+ regenerator-runtime "^0.14.0"
+
"@babel/template@7.18.10", "@babel/template@^7.18.10", "@babel/template@^7.3.3":
version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
@@ -7831,6 +7843,20 @@
lz-string "^1.5.0"
pretty-format "^27.0.2"
+"@testing-library/jest-dom@^6.4.5":
+ version "6.4.5"
+ resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.4.5.tgz#badb40296477149136dabef32b572ddd3b56adf1"
+ integrity sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==
+ dependencies:
+ "@adobe/css-tools" "^4.3.2"
+ "@babel/runtime" "^7.9.2"
+ aria-query "^5.0.0"
+ chalk "^3.0.0"
+ css.escape "^1.5.1"
+ dom-accessibility-api "^0.6.3"
+ lodash "^4.17.21"
+ redent "^3.0.0"
+
"@testing-library/react-hooks@^7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz#3388d07f562d91e7f2431a4a21b5186062ecfee0"
@@ -7858,6 +7884,11 @@
dependencies:
"@testing-library/dom" "^8.1.0"
+"@testing-library/user-event@^14.5.2":
+ version "14.5.2"
+ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.2.tgz#db7257d727c891905947bd1c1a99da20e03c2ebd"
+ integrity sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==
+
"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@@ -13601,6 +13632,11 @@ css-what@^6.0.1:
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
+css.escape@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
+ integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==
+
cssdb@^7.0.0, cssdb@^7.1.0:
version "7.11.2"
resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.11.2.tgz#127a2f5b946ee653361a5af5333ea85a39df5ae5"
@@ -14178,6 +14214,11 @@ dom-accessibility-api@^0.5.9:
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.13.tgz#102ee5f25eacce09bdf1cfa5a298f86da473be4b"
integrity sha512-R305kwb5CcMDIpSHUnLyIAp7SrSPBx6F0VfQFB3M75xVMHhXJJIdePYgbPPh1o57vCHNu5QztokWUPsLjWzFqw==
+dom-accessibility-api@^0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8"
+ integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==
+
dom-converter@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
@@ -26361,6 +26402,11 @@ regenerator-runtime@^0.13.4:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee"
integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==
+regenerator-runtime@^0.14.0:
+ version "0.14.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
+ integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
+
regenerator-transform@^0.15.0:
version "0.15.0"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537"
From b3a7086cebf10bcb589ba4ecd4381e81605bea22 Mon Sep 17 00:00:00 2001
From: Andrei <168741329+andreiborza@users.noreply.github.com>
Date: Mon, 10 Jun 2024 17:16:29 +0200
Subject: [PATCH 21/31] feat(solid): Rename solidjs package to solid (#12436)
---
.github/workflows/build.yml | 2 +-
.../{solidjs => solid}/.gitignore | 0
.../{solidjs => solid}/.npmrc | 0
.../{solidjs => solid}/README.md | 0
.../{solidjs => solid}/index.html | 0
.../{solidjs => solid}/package.json | 4 +-
.../{solidjs => solid}/playwright.config.mjs | 0
.../{solidjs => solid}/postcss.config.js | 0
.../{solidjs => solid}/src/errors/404.tsx | 0
.../{solidjs => solid}/src/index.css | 0
.../{solidjs => solid}/src/index.tsx | 2 +-
.../{solidjs => solid}/src/pageroot.tsx | 0
.../src/pages/errorboundaryexample.tsx | 2 +-
.../{solidjs => solid}/src/pages/home.tsx | 2 +-
.../{solidjs => solid}/src/pages/user.tsx | 0
.../{solidjs => solid}/src/routes.ts | 0
.../{solidjs => solid}/start-event-proxy.mjs | 2 +-
.../{solidjs => solid}/tailwind.config.ts | 0
.../tests/errorboundary.test.ts | 6 +-
.../{solidjs => solid}/tests/errors.test.ts | 4 +-
.../tests/performance.test.ts | 14 +-
.../{solidjs => solid}/tsconfig.json | 0
.../{solidjs => solid}/vite.config.ts | 0
.../e2e-tests/verdaccio-config/config.yaml | 2 +-
package.json | 2 +-
packages/{solidjs => solid}/.eslintrc.js | 0
packages/{solidjs => solid}/LICENSE | 0
packages/{solidjs => solid}/README.md | 12 +-
packages/{solidjs => solid}/package.json | 8 +-
.../{solidjs => solid}/rollup.npm.config.mjs | 0
.../{solidjs => solid}/src/debug-build.ts | 0
.../{solidjs => solid}/src/errorboundary.ts | 0
packages/{solidjs => solid}/src/index.ts | 0
packages/{solidjs => solid}/src/sdk.ts | 4 +-
.../{solidjs => solid}/src/solidrouter.ts | 2 +-
.../test/errorboundary.test.tsx | 0
packages/{solidjs => solid}/test/sdk.test.ts | 6 +-
.../test/solidrouter.test.tsx | 4 +-
packages/{solidjs => solid}/tsconfig.json | 0
.../{solidjs => solid}/tsconfig.test.json | 0
.../{solidjs => solid}/tsconfig.types.json | 0
packages/{solidjs => solid}/vite.config.ts | 0
scripts/node-unit-tests.ts | 2 +-
yarn.lock | 240 ++++++++++++------
44 files changed, 204 insertions(+), 116 deletions(-)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/.gitignore (100%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/.npmrc (100%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/README.md (100%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/index.html (100%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/package.json (93%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/playwright.config.mjs (100%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/postcss.config.js (100%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/src/errors/404.tsx (100%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/src/index.css (100%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/src/index.tsx (94%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/src/pageroot.tsx (100%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/src/pages/errorboundaryexample.tsx (94%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/src/pages/home.tsx (93%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/src/pages/user.tsx (100%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/src/routes.ts (100%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/start-event-proxy.mjs (78%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/tailwind.config.ts (100%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/tests/errorboundary.test.ts (89%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/tests/errors.test.ts (81%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/tests/performance.test.ts (82%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/tsconfig.json (100%)
rename dev-packages/e2e-tests/test-applications/{solidjs => solid}/vite.config.ts (100%)
rename packages/{solidjs => solid}/.eslintrc.js (100%)
rename packages/{solidjs => solid}/LICENSE (100%)
rename packages/{solidjs => solid}/README.md (85%)
rename packages/{solidjs => solid}/package.json (93%)
rename packages/{solidjs => solid}/rollup.npm.config.mjs (100%)
rename packages/{solidjs => solid}/src/debug-build.ts (100%)
rename packages/{solidjs => solid}/src/errorboundary.ts (100%)
rename packages/{solidjs => solid}/src/index.ts (100%)
rename packages/{solidjs => solid}/src/sdk.ts (80%)
rename packages/{solidjs => solid}/src/solidrouter.ts (99%)
rename packages/{solidjs => solid}/test/errorboundary.test.tsx (100%)
rename packages/{solidjs => solid}/test/sdk.test.ts (80%)
rename packages/{solidjs => solid}/test/solidrouter.test.tsx (99%)
rename packages/{solidjs => solid}/tsconfig.json (100%)
rename packages/{solidjs => solid}/tsconfig.test.json (100%)
rename packages/{solidjs => solid}/tsconfig.types.json (100%)
rename packages/{solidjs => solid}/vite.config.ts (100%)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c3bda957409b..bd039420fc3d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1014,7 +1014,7 @@ jobs:
'react-router-6-use-routes',
'react-router-5',
'react-router-6',
- 'solidjs',
+ 'solid',
'svelte-5',
'sveltekit',
'sveltekit-2',
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/.gitignore b/dev-packages/e2e-tests/test-applications/solid/.gitignore
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/solidjs/.gitignore
rename to dev-packages/e2e-tests/test-applications/solid/.gitignore
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/.npmrc b/dev-packages/e2e-tests/test-applications/solid/.npmrc
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/solidjs/.npmrc
rename to dev-packages/e2e-tests/test-applications/solid/.npmrc
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/README.md b/dev-packages/e2e-tests/test-applications/solid/README.md
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/solidjs/README.md
rename to dev-packages/e2e-tests/test-applications/solid/README.md
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/index.html b/dev-packages/e2e-tests/test-applications/solid/index.html
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/solidjs/index.html
rename to dev-packages/e2e-tests/test-applications/solid/index.html
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/package.json b/dev-packages/e2e-tests/test-applications/solid/package.json
similarity index 93%
rename from dev-packages/e2e-tests/test-applications/solidjs/package.json
rename to dev-packages/e2e-tests/test-applications/solid/package.json
index 1922a6e26d17..c63d3080748f 100644
--- a/dev-packages/e2e-tests/test-applications/solidjs/package.json
+++ b/dev-packages/e2e-tests/test-applications/solid/package.json
@@ -1,5 +1,5 @@
{
- "name": "solidjs",
+ "name": "solid",
"version": "0.0.0",
"description": "",
"scripts": {
@@ -28,6 +28,6 @@
"dependencies": {
"@solidjs/router": "^0.13.5",
"solid-js": "^1.8.11",
- "@sentry/solidjs": "latest || *"
+ "@sentry/solid": "latest || *"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/solid/playwright.config.mjs
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/solidjs/playwright.config.mjs
rename to dev-packages/e2e-tests/test-applications/solid/playwright.config.mjs
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/postcss.config.js b/dev-packages/e2e-tests/test-applications/solid/postcss.config.js
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/solidjs/postcss.config.js
rename to dev-packages/e2e-tests/test-applications/solid/postcss.config.js
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/src/errors/404.tsx b/dev-packages/e2e-tests/test-applications/solid/src/errors/404.tsx
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/solidjs/src/errors/404.tsx
rename to dev-packages/e2e-tests/test-applications/solid/src/errors/404.tsx
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/src/index.css b/dev-packages/e2e-tests/test-applications/solid/src/index.css
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/solidjs/src/index.css
rename to dev-packages/e2e-tests/test-applications/solid/src/index.css
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/src/index.tsx b/dev-packages/e2e-tests/test-applications/solid/src/index.tsx
similarity index 94%
rename from dev-packages/e2e-tests/test-applications/solidjs/src/index.tsx
rename to dev-packages/e2e-tests/test-applications/solid/src/index.tsx
index 47e7c0e52904..b975502ef590 100644
--- a/dev-packages/e2e-tests/test-applications/solidjs/src/index.tsx
+++ b/dev-packages/e2e-tests/test-applications/solid/src/index.tsx
@@ -1,5 +1,5 @@
/* @refresh reload */
-import * as Sentry from '@sentry/solidjs';
+import * as Sentry from '@sentry/solid';
import { Router, useBeforeLeave, useLocation } from '@solidjs/router';
import { render } from 'solid-js/web';
import './index.css';
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/src/pageroot.tsx b/dev-packages/e2e-tests/test-applications/solid/src/pageroot.tsx
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/solidjs/src/pageroot.tsx
rename to dev-packages/e2e-tests/test-applications/solid/src/pageroot.tsx
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/src/pages/errorboundaryexample.tsx b/dev-packages/e2e-tests/test-applications/solid/src/pages/errorboundaryexample.tsx
similarity index 94%
rename from dev-packages/e2e-tests/test-applications/solidjs/src/pages/errorboundaryexample.tsx
rename to dev-packages/e2e-tests/test-applications/solid/src/pages/errorboundaryexample.tsx
index b1dd324a785f..b4cb4e93a02f 100644
--- a/dev-packages/e2e-tests/test-applications/solidjs/src/pages/errorboundaryexample.tsx
+++ b/dev-packages/e2e-tests/test-applications/solid/src/pages/errorboundaryexample.tsx
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/solidjs';
+import * as Sentry from '@sentry/solid';
import { ErrorBoundary } from 'solid-js';
const SentryErrorBoundary = Sentry.withSentryErrorBoundary(ErrorBoundary);
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/src/pages/home.tsx b/dev-packages/e2e-tests/test-applications/solid/src/pages/home.tsx
similarity index 93%
rename from dev-packages/e2e-tests/test-applications/solidjs/src/pages/home.tsx
rename to dev-packages/e2e-tests/test-applications/solid/src/pages/home.tsx
index 7500846f0555..08e92728762c 100644
--- a/dev-packages/e2e-tests/test-applications/solidjs/src/pages/home.tsx
+++ b/dev-packages/e2e-tests/test-applications/solid/src/pages/home.tsx
@@ -25,7 +25,7 @@ export default function Home() {
class="border rounded-lg px-2 mb-2 border-red-500 text-red-500 cursor-pointer"
id="errorBtn"
onClick={() => {
- throw new Error('Error thrown from SolidJS E2E test app');
+ throw new Error('Error thrown from Solid E2E test app');
}}
>
Throw error
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/src/pages/user.tsx b/dev-packages/e2e-tests/test-applications/solid/src/pages/user.tsx
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/solidjs/src/pages/user.tsx
rename to dev-packages/e2e-tests/test-applications/solid/src/pages/user.tsx
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/src/routes.ts b/dev-packages/e2e-tests/test-applications/solid/src/routes.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/solidjs/src/routes.ts
rename to dev-packages/e2e-tests/test-applications/solid/src/routes.ts
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/solid/start-event-proxy.mjs
similarity index 78%
rename from dev-packages/e2e-tests/test-applications/solidjs/start-event-proxy.mjs
rename to dev-packages/e2e-tests/test-applications/solid/start-event-proxy.mjs
index 207afe3f56e1..075d4dcb5cf5 100644
--- a/dev-packages/e2e-tests/test-applications/solidjs/start-event-proxy.mjs
+++ b/dev-packages/e2e-tests/test-applications/solid/start-event-proxy.mjs
@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';
startEventProxyServer({
port: 3031,
- proxyServerName: 'solidjs',
+ proxyServerName: 'solid',
});
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/tailwind.config.ts b/dev-packages/e2e-tests/test-applications/solid/tailwind.config.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/solidjs/tailwind.config.ts
rename to dev-packages/e2e-tests/test-applications/solid/tailwind.config.ts
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/tests/errorboundary.test.ts b/dev-packages/e2e-tests/test-applications/solid/tests/errorboundary.test.ts
similarity index 89%
rename from dev-packages/e2e-tests/test-applications/solidjs/tests/errorboundary.test.ts
rename to dev-packages/e2e-tests/test-applications/solid/tests/errorboundary.test.ts
index 212c0ede2a2f..279eec70c059 100644
--- a/dev-packages/e2e-tests/test-applications/solidjs/tests/errorboundary.test.ts
+++ b/dev-packages/e2e-tests/test-applications/solid/tests/errorboundary.test.ts
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';
test('captures an exception', async ({ page }) => {
- const errorEventPromise = waitForError('solidjs', errorEvent => {
+ const errorEventPromise = waitForError('solid', errorEvent => {
return !errorEvent.type;
});
@@ -26,7 +26,7 @@ test('captures an exception', async ({ page }) => {
});
test('captures a second exception after resetting the boundary', async ({ page }) => {
- const firstErrorEventPromise = waitForError('solidjs', errorEvent => {
+ const firstErrorEventPromise = waitForError('solid', errorEvent => {
return !errorEvent.type;
});
@@ -48,7 +48,7 @@ test('captures a second exception after resetting the boundary', async ({ page }
transaction: '/error-boundary-example',
});
- const secondErrorEventPromise = waitForError('solidjs', errorEvent => {
+ const secondErrorEventPromise = waitForError('solid', errorEvent => {
return !errorEvent.type;
});
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/solid/tests/errors.test.ts
similarity index 81%
rename from dev-packages/e2e-tests/test-applications/solidjs/tests/errors.test.ts
rename to dev-packages/e2e-tests/test-applications/solid/tests/errors.test.ts
index 92618f628407..b55a9192ab1c 100644
--- a/dev-packages/e2e-tests/test-applications/solidjs/tests/errors.test.ts
+++ b/dev-packages/e2e-tests/test-applications/solid/tests/errors.test.ts
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';
test('sends an error', async ({ page }) => {
- const errorPromise = waitForError('solidjs', async errorEvent => {
+ const errorPromise = waitForError('solid', async errorEvent => {
return !errorEvent.type;
});
@@ -15,7 +15,7 @@ test('sends an error', async ({ page }) => {
values: [
{
type: 'Error',
- value: 'Error thrown from SolidJS E2E test app',
+ value: 'Error thrown from Solid E2E test app',
mechanism: {
type: 'onerror',
handled: false,
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/solid/tests/performance.test.ts
similarity index 82%
rename from dev-packages/e2e-tests/test-applications/solidjs/tests/performance.test.ts
rename to dev-packages/e2e-tests/test-applications/solid/tests/performance.test.ts
index 166dfe01d32b..f73ff4940527 100644
--- a/dev-packages/e2e-tests/test-applications/solidjs/tests/performance.test.ts
+++ b/dev-packages/e2e-tests/test-applications/solid/tests/performance.test.ts
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
test('sends a pageload transaction', async ({ page }) => {
- const transactionPromise = waitForTransaction('solidjs', async transactionEvent => {
+ const transactionPromise = waitForTransaction('solid', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
});
@@ -23,7 +23,7 @@ test('sends a pageload transaction', async ({ page }) => {
});
test('sends a navigation transaction', async ({ page }) => {
- const transactionPromise = waitForTransaction('solidjs', async transactionEvent => {
+ const transactionPromise = waitForTransaction('solid', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});
@@ -35,7 +35,7 @@ test('sends a navigation transaction', async ({ page }) => {
contexts: {
trace: {
op: 'navigation',
- origin: 'auto.navigation.solidjs.solidrouter',
+ origin: 'auto.navigation.solid.solidrouter',
},
},
transaction: '/user/5',
@@ -49,7 +49,7 @@ test('updates the transaction when using the back button', async ({ page }) => {
// Solid Router sends a `-1` navigation when using the back button.
// The sentry solidRouterBrowserTracingIntegration tries to update such
// transactions with the proper name once the `useLocation` hook triggers.
- const navigationTxnPromise = waitForTransaction('solidjs', async transactionEvent => {
+ const navigationTxnPromise = waitForTransaction('solid', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});
@@ -61,7 +61,7 @@ test('updates the transaction when using the back button', async ({ page }) => {
contexts: {
trace: {
op: 'navigation',
- origin: 'auto.navigation.solidjs.solidrouter',
+ origin: 'auto.navigation.solid.solidrouter',
},
},
transaction: '/user/5',
@@ -70,7 +70,7 @@ test('updates the transaction when using the back button', async ({ page }) => {
},
});
- const backNavigationTxnPromise = waitForTransaction('solidjs', async transactionEvent => {
+ const backNavigationTxnPromise = waitForTransaction('solid', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});
@@ -80,7 +80,7 @@ test('updates the transaction when using the back button', async ({ page }) => {
contexts: {
trace: {
op: 'navigation',
- origin: 'auto.navigation.solidjs.solidrouter',
+ origin: 'auto.navigation.solid.solidrouter',
},
},
transaction: '/',
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/tsconfig.json b/dev-packages/e2e-tests/test-applications/solid/tsconfig.json
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/solidjs/tsconfig.json
rename to dev-packages/e2e-tests/test-applications/solid/tsconfig.json
diff --git a/dev-packages/e2e-tests/test-applications/solidjs/vite.config.ts b/dev-packages/e2e-tests/test-applications/solid/vite.config.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/solidjs/vite.config.ts
rename to dev-packages/e2e-tests/test-applications/solid/vite.config.ts
diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml
index d9b11385dfa2..f582d7e02ef0 100644
--- a/dev-packages/e2e-tests/verdaccio-config/config.yaml
+++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml
@@ -128,7 +128,7 @@ packages:
unpublish: $all
# proxy: npmjs # Don't proxy for E2E tests!
- '@sentry/solidjs':
+ '@sentry/solid':
access: $all
publish: $all
unpublish: $all
diff --git a/package.json b/package.json
index 42a1b6cd0b89..bd4246a45862 100644
--- a/package.json
+++ b/package.json
@@ -69,7 +69,7 @@
"packages/replay-internal",
"packages/replay-canvas",
"packages/replay-worker",
- "packages/solidjs",
+ "packages/solid",
"packages/svelte",
"packages/sveltekit",
"packages/types",
diff --git a/packages/solidjs/.eslintrc.js b/packages/solid/.eslintrc.js
similarity index 100%
rename from packages/solidjs/.eslintrc.js
rename to packages/solid/.eslintrc.js
diff --git a/packages/solidjs/LICENSE b/packages/solid/LICENSE
similarity index 100%
rename from packages/solidjs/LICENSE
rename to packages/solid/LICENSE
diff --git a/packages/solidjs/README.md b/packages/solid/README.md
similarity index 85%
rename from packages/solidjs/README.md
rename to packages/solid/README.md
index c861adbfdba9..67f24b6bd841 100644
--- a/packages/solidjs/README.md
+++ b/packages/solid/README.md
@@ -4,11 +4,11 @@
-# Official Sentry SDK for SolidJS
+# Official Sentry SDK for Solid
-[](https://www.npmjs.com/package/@sentry/solidjs)
-[](https://www.npmjs.com/package/@sentry/solidjs)
-[](https://www.npmjs.com/package/@sentry/solidjs)
+[](https://www.npmjs.com/package/@sentry/solid)
+[](https://www.npmjs.com/package/@sentry/solid)
+[](https://www.npmjs.com/package/@sentry/solid)
This SDK is work in progress, and should not be used before officially released.
@@ -30,7 +30,7 @@ Wrap `Router`, `MemoryRouter` or `HashRouter` from `@solidjs/router` using `Sent
creates a higher order component, which will enable Sentry to reach your router context.
```js
-import * as Sentry from '@sentry/solidjs';
+import * as Sentry from '@sentry/solid';
import { Route, Router, useBeforeLeave, useLocation } from '@solidjs/router';
Sentry.init({
@@ -58,7 +58,7 @@ To automatically capture exceptions from inside a component tree and render a fa
JS `ErrorBoundary` component with `Sentry.withSentryErrorBoundary`.
```js
-import * as Sentry from '@sentry/solidjs';
+import * as Sentry from '@sentry/solid';
import { ErrorBoundary } from 'solid-js';
Sentry.init({
diff --git a/packages/solidjs/package.json b/packages/solid/package.json
similarity index 93%
rename from packages/solidjs/package.json
rename to packages/solid/package.json
index d1ba81386d8a..05b00d189840 100644
--- a/packages/solidjs/package.json
+++ b/packages/solid/package.json
@@ -1,9 +1,9 @@
{
- "name": "@sentry/solidjs",
+ "name": "@sentry/solid",
"version": "8.8.0",
- "description": "Official Sentry SDK for SolidJS",
+ "description": "Official Sentry SDK for Solid",
"repository": "git://github.com/getsentry/sentry-javascript.git",
- "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solidjs",
+ "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solid",
"author": "Sentry",
"license": "MIT",
"engines": {
@@ -71,7 +71,7 @@
"build:types:watch": "tsc -p tsconfig.types.json --watch",
"build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build",
"circularDepCheck": "madge --circular src/index.ts",
- "clean": "rimraf build coverage sentry-solidjs-*.tgz",
+ "clean": "rimraf build coverage sentry-solid-*.tgz",
"fix": "eslint . --format stylish --fix",
"lint": "eslint . --format stylish",
"test": "vitest run",
diff --git a/packages/solidjs/rollup.npm.config.mjs b/packages/solid/rollup.npm.config.mjs
similarity index 100%
rename from packages/solidjs/rollup.npm.config.mjs
rename to packages/solid/rollup.npm.config.mjs
diff --git a/packages/solidjs/src/debug-build.ts b/packages/solid/src/debug-build.ts
similarity index 100%
rename from packages/solidjs/src/debug-build.ts
rename to packages/solid/src/debug-build.ts
diff --git a/packages/solidjs/src/errorboundary.ts b/packages/solid/src/errorboundary.ts
similarity index 100%
rename from packages/solidjs/src/errorboundary.ts
rename to packages/solid/src/errorboundary.ts
diff --git a/packages/solidjs/src/index.ts b/packages/solid/src/index.ts
similarity index 100%
rename from packages/solidjs/src/index.ts
rename to packages/solid/src/index.ts
diff --git a/packages/solidjs/src/sdk.ts b/packages/solid/src/sdk.ts
similarity index 80%
rename from packages/solidjs/src/sdk.ts
rename to packages/solid/src/sdk.ts
index 7e33431a63b1..83a14a30bc2a 100644
--- a/packages/solidjs/src/sdk.ts
+++ b/packages/solid/src/sdk.ts
@@ -3,14 +3,14 @@ import { init as browserInit } from '@sentry/browser';
import { applySdkMetadata } from '@sentry/core';
/**
- * Initializes the SolidJS SDK
+ * Initializes the Solid SDK
*/
export function init(options: BrowserOptions): void {
const opts = {
...options,
};
- applySdkMetadata(opts, 'solidjs');
+ applySdkMetadata(opts, 'solid');
browserInit(opts);
}
diff --git a/packages/solidjs/src/solidrouter.ts b/packages/solid/src/solidrouter.ts
similarity index 99%
rename from packages/solidjs/src/solidrouter.ts
rename to packages/solid/src/solidrouter.ts
index d38b21313570..d22e7afb889a 100644
--- a/packages/solidjs/src/solidrouter.ts
+++ b/packages/solid/src/solidrouter.ts
@@ -71,7 +71,7 @@ function handleNavigation(location: string): void {
name: location,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solidjs.solidrouter',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solid.solidrouter',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
},
});
diff --git a/packages/solidjs/test/errorboundary.test.tsx b/packages/solid/test/errorboundary.test.tsx
similarity index 100%
rename from packages/solidjs/test/errorboundary.test.tsx
rename to packages/solid/test/errorboundary.test.tsx
diff --git a/packages/solidjs/test/sdk.test.ts b/packages/solid/test/sdk.test.ts
similarity index 80%
rename from packages/solidjs/test/sdk.test.ts
rename to packages/solid/test/sdk.test.ts
index 6b075b0099c4..1fa704f5cc2c 100644
--- a/packages/solidjs/test/sdk.test.ts
+++ b/packages/solid/test/sdk.test.ts
@@ -6,7 +6,7 @@ import { init as solidInit } from '../src/sdk';
const browserInit = vi.spyOn(SentryBrowser, 'init');
-describe('Initialize SolidJS SDk', () => {
+describe('Initialize Solid SDk', () => {
beforeEach(() => {
vi.clearAllMocks();
});
@@ -19,8 +19,8 @@ describe('Initialize SolidJS SDk', () => {
const expectedMetadata = {
_metadata: {
sdk: {
- name: 'sentry.javascript.solidjs',
- packages: [{ name: 'npm:@sentry/solidjs', version: SDK_VERSION }],
+ name: 'sentry.javascript.solid',
+ packages: [{ name: 'npm:@sentry/solid', version: SDK_VERSION }],
version: SDK_VERSION,
},
},
diff --git a/packages/solidjs/test/solidrouter.test.tsx b/packages/solid/test/solidrouter.test.tsx
similarity index 99%
rename from packages/solidjs/test/solidrouter.test.tsx
rename to packages/solid/test/solidrouter.test.tsx
index 4530eaa75871..a268a8ee50ed 100644
--- a/packages/solidjs/test/solidrouter.test.tsx
+++ b/packages/solid/test/solidrouter.test.tsx
@@ -139,7 +139,7 @@ describe('solidRouterBrowserTracingIntegration', () => {
data: expect.objectContaining({
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solidjs.solidrouter',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solid.solidrouter',
}),
}),
);
@@ -173,7 +173,7 @@ describe('solidRouterBrowserTracingIntegration', () => {
data: expect.objectContaining({
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solidjs.solidrouter',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solid.solidrouter',
}),
}),
);
diff --git a/packages/solidjs/tsconfig.json b/packages/solid/tsconfig.json
similarity index 100%
rename from packages/solidjs/tsconfig.json
rename to packages/solid/tsconfig.json
diff --git a/packages/solidjs/tsconfig.test.json b/packages/solid/tsconfig.test.json
similarity index 100%
rename from packages/solidjs/tsconfig.test.json
rename to packages/solid/tsconfig.test.json
diff --git a/packages/solidjs/tsconfig.types.json b/packages/solid/tsconfig.types.json
similarity index 100%
rename from packages/solidjs/tsconfig.types.json
rename to packages/solid/tsconfig.types.json
diff --git a/packages/solidjs/vite.config.ts b/packages/solid/vite.config.ts
similarity index 100%
rename from packages/solidjs/vite.config.ts
rename to packages/solid/vite.config.ts
diff --git a/scripts/node-unit-tests.ts b/scripts/node-unit-tests.ts
index 119f9764fc60..f7c6d0fded5a 100644
--- a/scripts/node-unit-tests.ts
+++ b/scripts/node-unit-tests.ts
@@ -15,7 +15,7 @@ const DEFAULT_SKIP_TESTS_PACKAGES = [
'@sentry/vue',
'@sentry/react',
'@sentry/angular',
- '@sentry/solidjs',
+ '@sentry/solid',
'@sentry/svelte',
'@sentry/profiling-node',
'@sentry-internal/browser-utils',
diff --git a/yarn.lock b/yarn.lock
index b88db586cd18..9ab00a9b9c84 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1093,6 +1093,14 @@
"@babel/highlight" "^7.24.2"
picocolors "^1.0.0"
+"@babel/code-frame@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465"
+ integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==
+ dependencies:
+ "@babel/highlight" "^7.24.7"
+ picocolors "^1.0.0"
+
"@babel/compat-data@^7.13.0", "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.19.4", "@babel/compat-data@^7.20.0":
version "7.20.1"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.1.tgz#f2e6ef7790d8c8dbf03d379502dcc246dcce0b30"
@@ -1123,6 +1131,11 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98"
integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==
+"@babel/compat-data@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.7.tgz#d23bbea508c3883ba8251fb4164982c36ea577ed"
+ integrity sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==
+
"@babel/core@7.18.10":
version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.10.tgz#39ad504991d77f1f3da91be0b8b949a5bc466fb8"
@@ -1229,20 +1242,20 @@
semver "^6.3.1"
"@babel/core@^7.23.3":
- version "7.24.5"
- resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.5.tgz#15ab5b98e101972d171aeef92ac70d8d6718f06a"
- integrity sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.7.tgz#b676450141e0b52a3d43bc91da86aa608f950ac4"
+ integrity sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==
dependencies:
"@ampproject/remapping" "^2.2.0"
- "@babel/code-frame" "^7.24.2"
- "@babel/generator" "^7.24.5"
- "@babel/helper-compilation-targets" "^7.23.6"
- "@babel/helper-module-transforms" "^7.24.5"
- "@babel/helpers" "^7.24.5"
- "@babel/parser" "^7.24.5"
- "@babel/template" "^7.24.0"
- "@babel/traverse" "^7.24.5"
- "@babel/types" "^7.24.5"
+ "@babel/code-frame" "^7.24.7"
+ "@babel/generator" "^7.24.7"
+ "@babel/helper-compilation-targets" "^7.24.7"
+ "@babel/helper-module-transforms" "^7.24.7"
+ "@babel/helpers" "^7.24.7"
+ "@babel/parser" "^7.24.7"
+ "@babel/template" "^7.24.7"
+ "@babel/traverse" "^7.24.7"
+ "@babel/types" "^7.24.7"
convert-source-map "^2.0.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
@@ -1328,12 +1341,12 @@
"@jridgewell/trace-mapping" "^0.3.25"
jsesc "^2.5.1"
-"@babel/generator@^7.24.5":
- version "7.24.5"
- resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.5.tgz#e5afc068f932f05616b66713e28d0f04e99daeb3"
- integrity sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==
+"@babel/generator@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.7.tgz#1654d01de20ad66b4b4d99c135471bc654c55e6d"
+ integrity sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==
dependencies:
- "@babel/types" "^7.24.5"
+ "@babel/types" "^7.24.7"
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.25"
jsesc "^2.5.1"
@@ -1410,6 +1423,17 @@
lru-cache "^5.1.1"
semver "^6.3.1"
+"@babel/helper-compilation-targets@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz#4eb6c4a80d6ffeac25ab8cd9a21b5dfa48d503a9"
+ integrity sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==
+ dependencies:
+ "@babel/compat-data" "^7.24.7"
+ "@babel/helper-validator-option" "^7.24.7"
+ browserslist "^4.22.2"
+ lru-cache "^5.1.1"
+ semver "^6.3.1"
+
"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.19.0", "@babel/helper-create-class-features-plugin@^7.5.5":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.19.0.tgz#bfd6904620df4e46470bae4850d66be1054c404b"
@@ -1502,6 +1526,13 @@
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167"
integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==
+"@babel/helper-environment-visitor@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9"
+ integrity sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==
+ dependencies:
+ "@babel/types" "^7.24.7"
+
"@babel/helper-explode-assignable-expression@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096"
@@ -1525,6 +1556,14 @@
"@babel/template" "^7.22.15"
"@babel/types" "^7.23.0"
+"@babel/helper-function-name@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz#75f1e1725742f39ac6584ee0b16d94513da38dd2"
+ integrity sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==
+ dependencies:
+ "@babel/template" "^7.24.7"
+ "@babel/types" "^7.24.7"
+
"@babel/helper-hoist-variables@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678"
@@ -1539,6 +1578,13 @@
dependencies:
"@babel/types" "^7.22.5"
+"@babel/helper-hoist-variables@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz#b4ede1cde2fd89436397f30dc9376ee06b0f25ee"
+ integrity sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==
+ dependencies:
+ "@babel/types" "^7.24.7"
+
"@babel/helper-member-expression-to-functions@^7.18.9":
version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815"
@@ -1567,13 +1613,21 @@
dependencies:
"@babel/types" "^7.22.15"
-"@babel/helper-module-imports@^7.24.1", "@babel/helper-module-imports@^7.24.3":
+"@babel/helper-module-imports@^7.24.1":
version "7.24.3"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz#6ac476e6d168c7c23ff3ba3cf4f7841d46ac8128"
integrity sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==
dependencies:
"@babel/types" "^7.24.0"
+"@babel/helper-module-imports@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b"
+ integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==
+ dependencies:
+ "@babel/traverse" "^7.24.7"
+ "@babel/types" "^7.24.7"
+
"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.19.6", "@babel/helper-module-transforms@^7.20.2":
version "7.20.2"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz#ac53da669501edd37e658602a21ba14c08748712"
@@ -1610,16 +1664,16 @@
"@babel/helper-split-export-declaration" "^7.22.6"
"@babel/helper-validator-identifier" "^7.22.20"
-"@babel/helper-module-transforms@^7.24.5":
- version "7.24.5"
- resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz#ea6c5e33f7b262a0ae762fd5986355c45f54a545"
- integrity sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==
+"@babel/helper-module-transforms@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz#31b6c9a2930679498db65b685b1698bfd6c7daf8"
+ integrity sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==
dependencies:
- "@babel/helper-environment-visitor" "^7.22.20"
- "@babel/helper-module-imports" "^7.24.3"
- "@babel/helper-simple-access" "^7.24.5"
- "@babel/helper-split-export-declaration" "^7.24.5"
- "@babel/helper-validator-identifier" "^7.24.5"
+ "@babel/helper-environment-visitor" "^7.24.7"
+ "@babel/helper-module-imports" "^7.24.7"
+ "@babel/helper-simple-access" "^7.24.7"
+ "@babel/helper-split-export-declaration" "^7.24.7"
+ "@babel/helper-validator-identifier" "^7.24.7"
"@babel/helper-optimise-call-expression@^7.18.6":
version "7.18.6"
@@ -1655,6 +1709,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz#945681931a52f15ce879fd5b86ce2dae6d3d7f2a"
integrity sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==
+"@babel/helper-plugin-utils@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz#98c84fe6fe3d0d3ae7bfc3a5e166a46844feb2a0"
+ integrity sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==
+
"@babel/helper-remap-async-to-generator@^7.18.6", "@babel/helper-remap-async-to-generator@^7.18.9":
version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519"
@@ -1708,12 +1767,13 @@
dependencies:
"@babel/types" "^7.22.5"
-"@babel/helper-simple-access@^7.24.5":
- version "7.24.5"
- resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz#50da5b72f58c16b07fbd992810be6049478e85ba"
- integrity sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==
+"@babel/helper-simple-access@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3"
+ integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==
dependencies:
- "@babel/types" "^7.24.5"
+ "@babel/traverse" "^7.24.7"
+ "@babel/types" "^7.24.7"
"@babel/helper-skip-transparent-expression-wrappers@^7.18.9":
version "7.18.9"
@@ -1743,12 +1803,12 @@
dependencies:
"@babel/types" "^7.22.5"
-"@babel/helper-split-export-declaration@^7.24.5":
- version "7.24.5"
- resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz#b9a67f06a46b0b339323617c8c6213b9055a78b6"
- integrity sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==
+"@babel/helper-split-export-declaration@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856"
+ integrity sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==
dependencies:
- "@babel/types" "^7.24.5"
+ "@babel/types" "^7.24.7"
"@babel/helper-string-parser@^7.19.4":
version "7.19.4"
@@ -1770,10 +1830,10 @@
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83"
integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==
-"@babel/helper-string-parser@^7.24.1":
- version "7.24.1"
- resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e"
- integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==
+"@babel/helper-string-parser@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz#4d2d0f14820ede3b9807ea5fc36dfc8cd7da07f2"
+ integrity sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==
"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1":
version "7.19.1"
@@ -1785,10 +1845,10 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
-"@babel/helper-validator-identifier@^7.24.5":
- version "7.24.5"
- resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz#918b1a7fa23056603506370089bd990d8720db62"
- integrity sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==
+"@babel/helper-validator-identifier@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db"
+ integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==
"@babel/helper-validator-option@^7.16.7", "@babel/helper-validator-option@^7.18.6":
version "7.18.6"
@@ -1805,6 +1865,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307"
integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==
+"@babel/helper-validator-option@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz#24c3bb77c7a425d1742eec8fb433b5a1b38e62f6"
+ integrity sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==
+
"@babel/helper-wrap-function@^7.18.9":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.19.0.tgz#89f18335cff1152373222f76a4b37799636ae8b1"
@@ -1869,14 +1934,13 @@
"@babel/traverse" "^7.24.1"
"@babel/types" "^7.24.0"
-"@babel/helpers@^7.24.5":
- version "7.24.5"
- resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.5.tgz#fedeb87eeafa62b621160402181ad8585a22a40a"
- integrity sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==
+"@babel/helpers@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.7.tgz#aa2ccda29f62185acb5d42fb4a3a1b1082107416"
+ integrity sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==
dependencies:
- "@babel/template" "^7.24.0"
- "@babel/traverse" "^7.24.5"
- "@babel/types" "^7.24.5"
+ "@babel/template" "^7.24.7"
+ "@babel/types" "^7.24.7"
"@babel/highlight@^7.10.4", "@babel/highlight@^7.18.6":
version "7.18.6"
@@ -1925,6 +1989,16 @@
js-tokens "^4.0.0"
picocolors "^1.0.0"
+"@babel/highlight@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d"
+ integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.24.7"
+ chalk "^2.4.2"
+ js-tokens "^4.0.0"
+ picocolors "^1.0.0"
+
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.4", "@babel/parser@^7.18.10", "@babel/parser@^7.20.2", "@babel/parser@^7.4.5", "@babel/parser@^7.7.0":
version "7.20.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.3.tgz#5358cf62e380cf69efcb87a7bb922ff88bfac6e2"
@@ -1935,7 +2009,7 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.15.tgz#eec9f36d8eaf0948bb88c87a46784b5ee9fd0c89"
integrity sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg==
-"@babel/parser@^7.21.8", "@babel/parser@^7.24.5":
+"@babel/parser@^7.21.8":
version "7.24.5"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.5.tgz#4a4d5ab4315579e5398a82dcf636ca80c3392790"
integrity sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==
@@ -1965,6 +2039,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.4.tgz#234487a110d89ad5a3ed4a8a566c36b9453e8c88"
integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==
+"@babel/parser@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85"
+ integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==
+
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.4":
version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.4.tgz#6125f0158543fb4edf1c22f322f3db67f21cb3e1"
@@ -2300,11 +2379,11 @@
"@babel/helper-plugin-utils" "^7.8.0"
"@babel/plugin-syntax-jsx@^7.18.6":
- version "7.24.1"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz#3f6ca04b8c841811dbc3c5c5f837934e0d626c10"
- integrity sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz#39a1fa4a7e3d3d7f34e2acc6be585b718d30e02d"
+ integrity sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==
dependencies:
- "@babel/helper-plugin-utils" "^7.24.0"
+ "@babel/helper-plugin-utils" "^7.24.7"
"@babel/plugin-syntax-jsx@^7.22.5":
version "7.22.5"
@@ -3459,6 +3538,15 @@
"@babel/parser" "^7.24.0"
"@babel/types" "^7.24.0"
+"@babel/template@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315"
+ integrity sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==
+ dependencies:
+ "@babel/code-frame" "^7.24.7"
+ "@babel/parser" "^7.24.7"
+ "@babel/types" "^7.24.7"
+
"@babel/traverse@^7.13.0", "@babel/traverse@^7.19.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.22.10", "@babel/traverse@^7.23.0", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.2":
version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8"
@@ -3507,19 +3595,19 @@
debug "^4.3.1"
globals "^11.1.0"
-"@babel/traverse@^7.24.5":
- version "7.24.5"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.5.tgz#972aa0bc45f16983bf64aa1f877b2dd0eea7e6f8"
- integrity sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==
- dependencies:
- "@babel/code-frame" "^7.24.2"
- "@babel/generator" "^7.24.5"
- "@babel/helper-environment-visitor" "^7.22.20"
- "@babel/helper-function-name" "^7.23.0"
- "@babel/helper-hoist-variables" "^7.22.5"
- "@babel/helper-split-export-declaration" "^7.24.5"
- "@babel/parser" "^7.24.5"
- "@babel/types" "^7.24.5"
+"@babel/traverse@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.7.tgz#de2b900163fa741721ba382163fe46a936c40cf5"
+ integrity sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==
+ dependencies:
+ "@babel/code-frame" "^7.24.7"
+ "@babel/generator" "^7.24.7"
+ "@babel/helper-environment-visitor" "^7.24.7"
+ "@babel/helper-function-name" "^7.24.7"
+ "@babel/helper-hoist-variables" "^7.24.7"
+ "@babel/helper-split-export-declaration" "^7.24.7"
+ "@babel/parser" "^7.24.7"
+ "@babel/types" "^7.24.7"
debug "^4.3.1"
globals "^11.1.0"
@@ -3577,13 +3665,13 @@
"@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0"
-"@babel/types@^7.24.5":
- version "7.24.5"
- resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.5.tgz#7661930afc638a5383eb0c4aee59b74f38db84d7"
- integrity sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==
+"@babel/types@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.7.tgz#6027fe12bc1aa724cd32ab113fb7f1988f1f66f2"
+ integrity sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==
dependencies:
- "@babel/helper-string-parser" "^7.24.1"
- "@babel/helper-validator-identifier" "^7.24.5"
+ "@babel/helper-string-parser" "^7.24.7"
+ "@babel/helper-validator-identifier" "^7.24.7"
to-fast-properties "^2.0.0"
"@bcoe/v8-coverage@^0.2.3":
From 1d8a8fe9bac0b0283ef11dab85a74e8fbb047192 Mon Sep 17 00:00:00 2001
From: Andrei <168741329+andreiborza@users.noreply.github.com>
Date: Mon, 10 Jun 2024 17:41:47 +0200
Subject: [PATCH 22/31] feat(solidjs): Add entry for Solid SDK to .craft.yml
(#12426)
Waiting with merging this until the package is ready.
---------
Co-authored-by: Lukas Stracke
---
.craft.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.craft.yml b/.craft.yml
index 5c1a754a52e9..77bfdfcd14b7 100644
--- a/.craft.yml
+++ b/.craft.yml
@@ -58,6 +58,9 @@ targets:
- name: npm
id: '@sentry/react'
includeNames: /^sentry-react-\d.*\.tgz$/
+ - name: npm
+ id: '@sentry/solid'
+ includeNames: /^sentry-solid-\d.*\.tgz$/
- name: npm
id: '@sentry/svelte'
includeNames: /^sentry-svelte-\d.*\.tgz$/
From bbe4532e1345e313ef88c349af4925efc3542db9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 Jun 2024 15:55:01 +0000
Subject: [PATCH 23/31] feat(deps): bump @opentelemetry/propagator-aws-xray
from 1.24.1 to 1.25.0 (#12437)
---
yarn.lock | 74 ++++++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 60 insertions(+), 14 deletions(-)
diff --git a/yarn.lock b/yarn.lock
index 9ab00a9b9c84..541965e3a543 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6527,11 +6527,11 @@
integrity sha512-hhTW8pFp9PSyosYzzuUL9rdm7HF97w3OCyElufFHyUnYnKkCBbu8ne2LyF/KSdI/xZ81ubxWZs78hX4S7pLq5g==
"@opentelemetry/propagator-aws-xray@^1.3.1":
- version "1.24.1"
- resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-aws-xray/-/propagator-aws-xray-1.24.1.tgz#fd041d43f0eee7d482d272bc23688d42abe216d5"
- integrity sha512-RzwoLe6QzsYGcpmxxDbbbgSpe3ncxSM4dtFHXh/rCYGjyq0nZGXKvk26mJtWZ4kQ3nuiIoqSZueIuGmt/mvOTA==
+ version "1.25.0"
+ resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-aws-xray/-/propagator-aws-xray-1.25.0.tgz#cbbe70cc45235e195d1f07c6e845df766b674b88"
+ integrity sha512-+honT9J/Xa6Mxk7AO/VlSUGaVSSQzqHr0wZDWrSunnc3eVbS5YTuv7UrcoHTED+AYziawTlx7ICeAX2VPc1M+w==
dependencies:
- "@opentelemetry/core" "1.24.1"
+ "@opentelemetry/core" "1.25.0"
"@opentelemetry/redis-common@^0.36.2":
version "0.36.2"
@@ -8548,8 +8548,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==
@@ -8908,7 +8917,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==
@@ -26183,8 +26200,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==
@@ -26199,6 +26215,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"
@@ -28522,8 +28545,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:
- name string-width-cjs
+"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==
@@ -28549,6 +28571,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"
@@ -28644,7 +28675,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==
@@ -31271,8 +31309,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:
- name wrap-ansi-cjs
+"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==
@@ -31290,6 +31327,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 31e4cda1ee70a2e90261a45030e53e69801dfe71 Mon Sep 17 00:00:00 2001
From: Francesco Novy
Date: Mon, 10 Jun 2024 18:05:56 +0200
Subject: [PATCH 24/31] fix(replay): Fix guard for exception event (#12441)
Fixes https://github.com/getsentry/sentry-javascript/issues/11759,
hopefully.
One more example for why we should enable noUncheckedIndexedAccess...
See https://github.com/getsentry/sentry-javascript/issues/12440
---
.../replay-internal/src/coreHandlers/handleBeforeSendEvent.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/replay-internal/src/coreHandlers/handleBeforeSendEvent.ts b/packages/replay-internal/src/coreHandlers/handleBeforeSendEvent.ts
index 297d0bc2bbe3..f7434f595693 100644
--- a/packages/replay-internal/src/coreHandlers/handleBeforeSendEvent.ts
+++ b/packages/replay-internal/src/coreHandlers/handleBeforeSendEvent.ts
@@ -21,7 +21,8 @@ export function handleBeforeSendEvent(replay: ReplayContainer): BeforeSendEventC
}
function handleHydrationError(replay: ReplayContainer, event: ErrorEvent): void {
- const exceptionValue = event.exception && event.exception.values && event.exception.values[0].value;
+ const exceptionValue =
+ event.exception && event.exception.values && event.exception.values[0] && event.exception.values[0].value;
if (typeof exceptionValue !== 'string') {
return;
}
From 5b676837b93403966bfe0c3b4a387558f7d6ba9a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 Jun 2024 17:08:13 +0000
Subject: [PATCH 25/31] feat(deps): bump @opentelemetry/instrumentation-mongodb
from 0.44.0 to 0.45.0 (#12439)
---
.../suites/tracing/mongodb/test.ts | 132 +++++++++++++++---
packages/node/package.json | 2 +-
yarn.lock | 12 +-
3 files changed, 121 insertions(+), 25 deletions(-)
diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts
index b8c16862c34c..2f79385521d3 100644
--- a/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts
@@ -21,63 +21,159 @@ describe('MongoDB experimental Test', () => {
const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
- spans: expect.arrayContaining([
+ spans: [
expect.objectContaining({
- data: expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.db.otel.mongo',
+ 'sentry.op': 'db',
+ 'db.system': 'mongodb',
+ 'db.name': 'admin',
+ 'db.mongodb.collection': '$cmd',
+ 'db.operation': 'isMaster',
+ 'db.connection_string': expect.any(String),
+ 'net.peer.name': expect.any(String),
+ 'net.peer.port': expect.any(Number),
+ 'db.statement':
+ '{"ismaster":"?","client":{"driver":{"name":"?","version":"?"},"os":{"type":"?","name":"?","architecture":"?","version":"?"},"platform":"?"},"compression":[],"helloOk":"?"}',
+ 'otel.kind': 'CLIENT',
+ },
+ description:
+ '{"ismaster":"?","client":{"driver":{"name":"?","version":"?"},"os":{"type":"?","name":"?","architecture":"?","version":"?"},"platform":"?"},"compression":[],"helloOk":"?"}',
+ op: 'db',
+ origin: 'auto.db.otel.mongo',
+ }),
+ expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.db.otel.mongo',
+ 'sentry.op': 'db',
+ 'db.system': 'mongodb',
+ 'db.name': 'admin',
+ 'db.mongodb.collection': '$cmd',
+ 'db.operation': 'isMaster',
+ 'db.connection_string': expect.any(String),
+ 'net.peer.name': expect.any(String),
+ 'net.peer.port': expect.any(Number),
+ 'db.statement':
+ '{"ismaster":"?","client":{"driver":{"name":"?","version":"?"},"os":{"type":"?","name":"?","architecture":"?","version":"?"},"platform":"?"},"compression":[],"helloOk":"?"}',
+ 'otel.kind': 'CLIENT',
+ },
+ description:
+ '{"ismaster":"?","client":{"driver":{"name":"?","version":"?"},"os":{"type":"?","name":"?","architecture":"?","version":"?"},"platform":"?"},"compression":[],"helloOk":"?"}',
+ op: 'db',
+ origin: 'auto.db.otel.mongo',
+ }),
+ expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.db.otel.mongo',
+ 'sentry.op': 'db',
'db.system': 'mongodb',
'db.name': 'admin',
- 'db.operation': 'insert',
'db.mongodb.collection': 'movies',
- }),
- description: '{"title":"?","_id":"?"}',
+ 'db.operation': 'insert',
+ 'db.connection_string': expect.any(String),
+ 'net.peer.name': expect.any(String),
+ 'net.peer.port': expect.any(Number),
+ 'db.statement':
+ '{"title":"?","_id":{"_bsontype":"?","id":{"0":"?","1":"?","2":"?","3":"?","4":"?","5":"?","6":"?","7":"?","8":"?","9":"?","10":"?","11":"?"}}}',
+ 'otel.kind': 'CLIENT',
+ },
+ description:
+ '{"title":"?","_id":{"_bsontype":"?","id":{"0":"?","1":"?","2":"?","3":"?","4":"?","5":"?","6":"?","7":"?","8":"?","9":"?","10":"?","11":"?"}}}',
op: 'db',
origin: 'auto.db.otel.mongo',
}),
expect.objectContaining({
- data: expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.db.otel.mongo',
+ 'sentry.op': 'db',
'db.system': 'mongodb',
'db.name': 'admin',
- 'db.operation': 'find',
'db.mongodb.collection': 'movies',
- }),
+ 'db.operation': 'find',
+ 'db.connection_string': expect.any(String),
+ 'net.peer.name': expect.any(String),
+ 'net.peer.port': expect.any(Number),
+ 'db.statement': '{"title":"?"}',
+ 'otel.kind': 'CLIENT',
+ },
description: '{"title":"?"}',
op: 'db',
origin: 'auto.db.otel.mongo',
}),
expect.objectContaining({
- data: expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.db.otel.mongo',
+ 'sentry.op': 'db',
'db.system': 'mongodb',
'db.name': 'admin',
- 'db.operation': 'update',
'db.mongodb.collection': 'movies',
- }),
+ 'db.operation': 'update',
+ 'db.connection_string': expect.any(String),
+ 'net.peer.name': expect.any(String),
+ 'net.peer.port': expect.any(Number),
+ 'db.statement': '{"title":"?"}',
+ 'otel.kind': 'CLIENT',
+ },
description: '{"title":"?"}',
op: 'db',
origin: 'auto.db.otel.mongo',
}),
expect.objectContaining({
- data: expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.db.otel.mongo',
+ 'sentry.op': 'db',
'db.system': 'mongodb',
'db.name': 'admin',
- 'db.operation': 'find',
'db.mongodb.collection': 'movies',
- }),
+ 'db.operation': 'find',
+ 'db.connection_string': expect.any(String),
+ 'net.peer.name': expect.any(String),
+ 'net.peer.port': expect.any(Number),
+ 'db.statement': '{"title":"?"}',
+ 'otel.kind': 'CLIENT',
+ },
description: '{"title":"?"}',
op: 'db',
origin: 'auto.db.otel.mongo',
}),
expect.objectContaining({
- data: expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.db.otel.mongo',
+ 'sentry.op': 'db',
'db.system': 'mongodb',
'db.name': 'admin',
- 'db.operation': 'find',
'db.mongodb.collection': 'movies',
- }),
+ 'db.operation': 'find',
+ 'db.connection_string': expect.any(String),
+ 'net.peer.name': expect.any(String),
+ 'net.peer.port': expect.any(Number),
+ 'db.statement': '{"title":"?"}',
+ 'otel.kind': 'CLIENT',
+ },
description: '{"title":"?"}',
op: 'db',
origin: 'auto.db.otel.mongo',
}),
- ]),
+ expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.db.otel.mongo',
+ 'sentry.op': 'db',
+ 'db.system': 'mongodb',
+ 'db.name': 'admin',
+ 'db.mongodb.collection': '$cmd',
+ 'db.connection_string': expect.any(String),
+ 'net.peer.name': expect.any(String),
+ 'net.peer.port': expect.any(Number),
+ 'db.statement':
+ '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":{"0":"?","1":"?","2":"?","3":"?","4":"?","5":"?","6":"?","7":"?","8":"?","9":"?","10":"?","11":"?","12":"?","13":"?","14":"?","15":"?"}}}]}',
+ 'otel.kind': 'CLIENT',
+ },
+ description:
+ '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":{"0":"?","1":"?","2":"?","3":"?","4":"?","5":"?","6":"?","7":"?","8":"?","9":"?","10":"?","11":"?","12":"?","13":"?","14":"?","15":"?"}}}]}',
+ op: 'db',
+ origin: 'auto.db.otel.mongo',
+ }),
+ ],
};
test('CJS - should auto-instrument `mongodb` package.', done => {
diff --git a/packages/node/package.json b/packages/node/package.json
index dcd3ef83f6a1..87c777da4d7d 100644
--- a/packages/node/package.json
+++ b/packages/node/package.json
@@ -82,7 +82,7 @@
"@opentelemetry/instrumentation-http": "0.52.0",
"@opentelemetry/instrumentation-ioredis": "0.41.0",
"@opentelemetry/instrumentation-koa": "0.41.0",
- "@opentelemetry/instrumentation-mongodb": "0.44.0",
+ "@opentelemetry/instrumentation-mongodb": "0.45.0",
"@opentelemetry/instrumentation-mongoose": "0.39.0",
"@opentelemetry/instrumentation-mysql": "0.39.0",
"@opentelemetry/instrumentation-mysql2": "0.39.0",
diff --git a/yarn.lock b/yarn.lock
index 541965e3a543..ddd585d8d26b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6431,10 +6431,10 @@
"@types/koa" "2.14.0"
"@types/koa__router" "12.0.3"
-"@opentelemetry/instrumentation-mongodb@0.44.0":
- version "0.44.0"
- resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.44.0.tgz#359ecc72a903f8f586f34d7a57b7e122037621a9"
- integrity sha512-VPnmN5LZN8gWQ1znRz7mdZBly4h4G8Fsp8NJYqgM1CEoglX+O/Dj36zesZVSi1InPyDX2hGDTt6Qp3DFYjl7WA==
+"@opentelemetry/instrumentation-mongodb@0.45.0":
+ version "0.45.0"
+ resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.45.0.tgz#d6373e30f3e83eba87f7e6e2ea72c1351467d6b5"
+ integrity sha512-xnZP9+ayeB1JJyNE9cIiwhOJTzNEsRhXVdLgfzmrs48Chhhk026mQdM5CITfyXSCfN73FGAIB8d91+pflJEfWQ==
dependencies:
"@opentelemetry/instrumentation" "^0.52.0"
"@opentelemetry/sdk-metrics" "^1.9.1"
@@ -6589,12 +6589,12 @@
"@opentelemetry/resources" "1.25.0"
"@opentelemetry/semantic-conventions" "1.25.0"
-"@opentelemetry/semantic-conventions@1.24.1", "@opentelemetry/semantic-conventions@^1.17.0", "@opentelemetry/semantic-conventions@^1.22.0", "@opentelemetry/semantic-conventions@^1.23.0":
+"@opentelemetry/semantic-conventions@1.24.1":
version "1.24.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.24.1.tgz#d4bcebda1cb5146d47a2a53daaa7922f8e084dfb"
integrity sha512-VkliWlS4/+GHLLW7J/rVBA00uXus1SWvwFvcUDxDwmFxYfg/2VI6ekwdXS28cjI8Qz2ky2BzG8OUHo+WeYIWqw==
-"@opentelemetry/semantic-conventions@1.25.0", "@opentelemetry/semantic-conventions@^1.25.0":
+"@opentelemetry/semantic-conventions@1.25.0", "@opentelemetry/semantic-conventions@^1.17.0", "@opentelemetry/semantic-conventions@^1.22.0", "@opentelemetry/semantic-conventions@^1.23.0", "@opentelemetry/semantic-conventions@^1.25.0":
version "1.25.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.0.tgz#390eb4d42a29c66bdc30066af9035645e9bb7270"
integrity sha512-M+kkXKRAIAiAP6qYyesfrC5TOmDpDVtsxuGfPcqd9B/iBrac+E14jYwrgm0yZBUIbIP2OnqC3j+UgkXLm1vxUQ==
From ceaeeba30bb594a04a6a1d6915800a8fbafb77e4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 10 Jun 2024 18:18:19 +0000
Subject: [PATCH 26/31] feat(deps): bump @opentelemetry/instrumentation-express
from 0.40.0 to 0.40.1 (#12438)
---
packages/node/package.json | 2 +-
yarn.lock | 12 ++++++------
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/packages/node/package.json b/packages/node/package.json
index 87c777da4d7d..880873b0436f 100644
--- a/packages/node/package.json
+++ b/packages/node/package.json
@@ -75,7 +75,7 @@
"@opentelemetry/core": "^1.25.0",
"@opentelemetry/instrumentation": "^0.52.0",
"@opentelemetry/instrumentation-connect": "0.37.0",
- "@opentelemetry/instrumentation-express": "0.40.0",
+ "@opentelemetry/instrumentation-express": "0.40.1",
"@opentelemetry/instrumentation-fastify": "0.37.0",
"@opentelemetry/instrumentation-graphql": "0.41.0",
"@opentelemetry/instrumentation-hapi": "0.39.0",
diff --git a/yarn.lock b/yarn.lock
index ddd585d8d26b..2843c1b4182d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6313,14 +6313,14 @@
resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.12.0.tgz#4906ae27359d3311e3dea1b63770a16f60848550"
integrity sha512-UXwSsXo3F3yZ1dIBOG9ID8v2r9e+bqLWoizCtTb8rXtwF+N5TM7hzzvQz72o3nBU+zrI/D5e+OqAYK8ZgDd3DA==
-"@opentelemetry/core@1.24.1", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.8.0":
+"@opentelemetry/core@1.24.1":
version "1.24.1"
resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.24.1.tgz#35ab9d2ac9ca938e0ffbdfa40c49c169ac8ba80d"
integrity sha512-wMSGfsdmibI88K9wB498zXY04yThPexo8jvwNNlm542HZB7XrrMRBbAyKJqG8qDRJwIBdBrPMi4V9ZPW/sqrcg==
dependencies:
"@opentelemetry/semantic-conventions" "1.24.1"
-"@opentelemetry/core@1.25.0", "@opentelemetry/core@^1.25.0":
+"@opentelemetry/core@1.25.0", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.25.0", "@opentelemetry/core@^1.8.0":
version "1.25.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.25.0.tgz#ad034f5c2669f589bd703bfbbaa38b51f8504053"
integrity sha512-n0B3s8rrqGrasTgNkXLKXzN0fXo+6IYP7M5b7AMsrZM33f/y6DS6kJ0Btd7SespASWq8bgL3taLo0oe0vB52IQ==
@@ -6367,10 +6367,10 @@
"@opentelemetry/semantic-conventions" "^1.22.0"
"@types/connect" "3.4.36"
-"@opentelemetry/instrumentation-express@0.40.0":
- version "0.40.0"
- resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.40.0.tgz#e2172b39e7abc218b29564aa59e92c1ab8ca8389"
- integrity sha512-ahITgz2cFaMvqGDvxOdgxjgQyGmFccGMIoiwYpZQ+MJQt5qxvRZhau794/McdvtUp4LrK5OfvK1hQp4YsW2VGA==
+"@opentelemetry/instrumentation-express@0.40.1":
+ version "0.40.1"
+ resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.40.1.tgz#b4c31a352691b060b330e4c028a8ef5472b89e27"
+ integrity sha512-+RKMvVe2zw3kIXRup9c1jFu3T4d0fs5aKy015TpiMyoCKX1UMu3Z0lfgYtuyiSTANvg5hZnDbWmQmqSPj9VTvg==
dependencies:
"@opentelemetry/core" "^1.8.0"
"@opentelemetry/instrumentation" "^0.52.0"
From ce6fbcd5d7298afed588afaf8abafe8c1028f6fb Mon Sep 17 00:00:00 2001
From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com>
Date: Tue, 11 Jun 2024 08:57:22 +0200
Subject: [PATCH 27/31] feat(redis): Add cache logic for redis-4 (#12429)
Follow-up as cache logic for `ioredis` was already added.
---
.../node-integration-tests/package.json | 1 +
.../tracing/redis-cache/scenario-redis-4.js | 46 +++++++
.../suites/tracing/redis-cache/test.ts | 93 +++++++++++++-
packages/node/package.json | 1 +
.../node/src/integrations/tracing/index.ts | 3 +-
.../node/src/integrations/tracing/redis.ts | 117 +++++++++++-------
packages/node/src/utils/redisCache.ts | 14 ++-
.../test/integrations/tracing/redis.test.ts | 8 +-
yarn.lock | 72 ++++++++++-
9 files changed, 293 insertions(+), 62 deletions(-)
create mode 100644 dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-4.js
diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json
index fc821bff7875..705e417d4f50 100644
--- a/dev-packages/node-integration-tests/package.json
+++ b/dev-packages/node-integration-tests/package.json
@@ -55,6 +55,7 @@
"node-schedule": "^2.1.1",
"pg": "^8.7.3",
"proxy": "^2.1.1",
+ "redis-4": "npm:redis@^4.6.14",
"reflect-metadata": "0.2.1",
"rxjs": "^7.8.1",
"yargs": "^16.2.0"
diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-4.js b/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-4.js
new file mode 100644
index 000000000000..31156674a654
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-4.js
@@ -0,0 +1,46 @@
+const { loggingTransport } = require('@sentry-internal/node-integration-tests');
+const Sentry = require('@sentry/node');
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+ integrations: [Sentry.redisIntegration({ cachePrefixes: ['redis-cache:'] })],
+});
+
+// Stop the process from exiting before the transaction is sent
+setInterval(() => {}, 1000);
+
+const { createClient } = require('redis-4');
+
+async function run() {
+ const redisClient = await createClient().connect();
+
+ await Sentry.startSpan(
+ {
+ name: 'Test Span Redis 4',
+ op: 'test-span-redis-4',
+ },
+ async () => {
+ try {
+ await redisClient.set('redis-test-key', 'test-value');
+ await redisClient.set('redis-cache:test-key', 'test-value');
+
+ await redisClient.set('redis-cache:test-key-set-EX', 'test-value', { EX: 10 });
+ await redisClient.setEx('redis-cache:test-key-setex', 10, 'test-value');
+
+ await redisClient.get('redis-test-key');
+ await redisClient.get('redis-cache:test-key');
+ await redisClient.get('redis-cache:unavailable-data');
+
+ await redisClient.mGet(['redis-test-key', 'redis-cache:test-key', 'redis-cache:unavailable-data']);
+ } finally {
+ await redisClient.disconnect();
+ }
+ },
+ );
+}
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+run();
diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts
index adbd88921a66..0c0807c8f480 100644
--- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts
@@ -42,7 +42,7 @@ describe('redis cache auto instrumentation', () => {
.start(done);
});
- test('should create cache spans for prefixed keys', done => {
+ test('should create cache spans for prefixed keys (ioredis)', done => {
const EXPECTED_TRANSACTION = {
transaction: 'Test Span',
spans: expect.arrayContaining([
@@ -139,4 +139,95 @@ describe('redis cache auto instrumentation', () => {
.expect({ transaction: EXPECTED_TRANSACTION })
.start(done);
});
+
+ test('should create cache spans for prefixed keys (redis-4)', done => {
+ const EXPECTED_REDIS_CONNECT = {
+ transaction: 'redis-connect',
+ };
+
+ const EXPECTED_TRANSACTION = {
+ transaction: 'Test Span Redis 4',
+ spans: expect.arrayContaining([
+ // SET
+ expect.objectContaining({
+ description: 'redis-cache:test-key',
+ op: 'cache.put',
+ origin: 'auto.db.otel.redis',
+ data: expect.objectContaining({
+ 'sentry.origin': 'auto.db.otel.redis',
+ 'db.statement': 'SET redis-cache:test-key [1 other arguments]',
+ 'cache.key': ['redis-cache:test-key'],
+ 'cache.item_size': 2,
+ }),
+ }),
+ // SET (with EX)
+ expect.objectContaining({
+ description: 'redis-cache:test-key-set-EX',
+ op: 'cache.put',
+ origin: 'auto.db.otel.redis',
+ data: expect.objectContaining({
+ 'sentry.origin': 'auto.db.otel.redis',
+ 'db.statement': 'SET redis-cache:test-key-set-EX [3 other arguments]',
+ 'cache.key': ['redis-cache:test-key-set-EX'],
+ 'cache.item_size': 2,
+ }),
+ }),
+ // SETEX
+ expect.objectContaining({
+ description: 'redis-cache:test-key-setex',
+ op: 'cache.put',
+ origin: 'auto.db.otel.redis',
+ data: expect.objectContaining({
+ 'sentry.origin': 'auto.db.otel.redis',
+ 'db.statement': 'SETEX redis-cache:test-key-setex [2 other arguments]',
+ 'cache.key': ['redis-cache:test-key-setex'],
+ 'cache.item_size': 2,
+ }),
+ }),
+ // GET
+ expect.objectContaining({
+ description: 'redis-cache:test-key',
+ op: 'cache.get',
+ origin: 'auto.db.otel.redis',
+ data: expect.objectContaining({
+ 'sentry.origin': 'auto.db.otel.redis',
+ 'db.statement': 'GET redis-cache:test-key',
+ 'cache.hit': true,
+ 'cache.key': ['redis-cache:test-key'],
+ 'cache.item_size': 10,
+ }),
+ }),
+ // GET (unavailable - no cache hit)
+ expect.objectContaining({
+ description: 'redis-cache:unavailable-data',
+ op: 'cache.get',
+ origin: 'auto.db.otel.redis',
+ data: expect.objectContaining({
+ 'sentry.origin': 'auto.db.otel.redis',
+ 'db.statement': 'GET redis-cache:unavailable-data',
+ 'cache.hit': false,
+ 'cache.key': ['redis-cache:unavailable-data'],
+ }),
+ }),
+ // MGET
+ expect.objectContaining({
+ description: 'redis-test-key, redis-cache:test-key, redis-cache:unavailable-data',
+ op: 'cache.get',
+ origin: 'auto.db.otel.redis',
+ data: expect.objectContaining({
+ 'sentry.origin': 'auto.db.otel.redis',
+ 'db.statement': 'MGET [3 other arguments]',
+ 'cache.hit': true,
+ 'cache.key': ['redis-test-key', 'redis-cache:test-key', 'redis-cache:unavailable-data'],
+ }),
+ }),
+ ]),
+ };
+
+ createRunner(__dirname, 'scenario-redis-4.js')
+ .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port=6379'] })
+ .expect({ transaction: EXPECTED_REDIS_CONNECT })
+ .expect({ transaction: EXPECTED_TRANSACTION })
+ .start(done);
+ });
});
diff --git a/packages/node/package.json b/packages/node/package.json
index 880873b0436f..8deb575e6a9e 100644
--- a/packages/node/package.json
+++ b/packages/node/package.json
@@ -88,6 +88,7 @@
"@opentelemetry/instrumentation-mysql2": "0.39.0",
"@opentelemetry/instrumentation-nestjs-core": "0.38.0",
"@opentelemetry/instrumentation-pg": "0.42.0",
+ "@opentelemetry/instrumentation-redis-4": "0.40.0",
"@opentelemetry/resources": "^1.25.0",
"@opentelemetry/sdk-trace-base": "^1.25.0",
"@opentelemetry/semantic-conventions": "^1.25.0",
diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts
index 55a01ba13651..bee4f06db8f5 100644
--- a/packages/node/src/integrations/tracing/index.ts
+++ b/packages/node/src/integrations/tracing/index.ts
@@ -13,7 +13,7 @@ import { instrumentMysql, mysqlIntegration } from './mysql';
import { instrumentMysql2, mysql2Integration } from './mysql2';
import { instrumentNest, nestIntegration } from './nest';
import { instrumentPostgres, postgresIntegration } from './postgres';
-import { redisIntegration } from './redis';
+import { instrumentRedis, redisIntegration } from './redis';
/**
* With OTEL, all performance integrations will be added, as OTEL only initializes them when the patched package is actually required.
@@ -60,5 +60,6 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) =>
instrumentPostgres,
instrumentHapi,
instrumentGraphql,
+ instrumentRedis,
];
}
diff --git a/packages/node/src/integrations/tracing/redis.ts b/packages/node/src/integrations/tracing/redis.ts
index 87bcf6b9cb25..4204e4a2abe5 100644
--- a/packages/node/src/integrations/tracing/redis.ts
+++ b/packages/node/src/integrations/tracing/redis.ts
@@ -1,4 +1,7 @@
+import type { Span } from '@opentelemetry/api';
+import type { RedisResponseCustomAttributeFunction } from '@opentelemetry/instrumentation-ioredis';
import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';
+import { RedisInstrumentation } from '@opentelemetry/instrumentation-redis-4';
import {
SEMANTIC_ATTRIBUTE_CACHE_HIT,
SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE,
@@ -9,12 +12,14 @@ import {
spanToJSON,
} from '@sentry/core';
import type { IntegrationFn } from '@sentry/types';
+import { truncate } from '@sentry/utils';
import { generateInstrumentOnce } from '../../otel/instrument';
import {
GET_COMMANDS,
calculateCacheItemSize,
getCacheKeySafely,
getCacheOperation,
+ isInCommands,
shouldConsiderForCache,
} from '../../utils/redisCache';
@@ -26,64 +31,80 @@ const INTEGRATION_NAME = 'Redis';
let _redisOptions: RedisOptions = {};
-export const instrumentRedis = generateInstrumentOnce(INTEGRATION_NAME, () => {
+const cacheResponseHook: RedisResponseCustomAttributeFunction = (span: Span, redisCommand, cmdArgs, response) => {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.redis');
+
+ const safeKey = getCacheKeySafely(redisCommand, cmdArgs);
+ const cacheOperation = getCacheOperation(redisCommand);
+
+ if (
+ !safeKey ||
+ !cacheOperation ||
+ !_redisOptions?.cachePrefixes ||
+ !shouldConsiderForCache(redisCommand, safeKey, _redisOptions.cachePrefixes)
+ ) {
+ // not relevant for cache
+ return;
+ }
+
+ // otel/ioredis seems to be using the old standard, as there was a change to those params: https://github.com/open-telemetry/opentelemetry-specification/issues/3199
+ // We are using params based on the docs: https://opentelemetry.io/docs/specs/semconv/attributes-registry/network/
+ const networkPeerAddress = spanToJSON(span).data?.['net.peer.name'];
+ const networkPeerPort = spanToJSON(span).data?.['net.peer.port'];
+ if (networkPeerPort && networkPeerAddress) {
+ span.setAttributes({ 'network.peer.address': networkPeerAddress, 'network.peer.port': networkPeerPort });
+ }
+
+ const cacheItemSize = calculateCacheItemSize(response);
+
+ if (cacheItemSize) {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE, cacheItemSize);
+ }
+
+ if (isInCommands(GET_COMMANDS, redisCommand) && cacheItemSize !== undefined) {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, cacheItemSize > 0);
+ }
+
+ span.setAttributes({
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: cacheOperation,
+ [SEMANTIC_ATTRIBUTE_CACHE_KEY]: safeKey,
+ });
+
+ const spanDescription = safeKey.join(', ');
+
+ span.updateName(truncate(spanDescription, 1024));
+};
+
+const instrumentIORedis = generateInstrumentOnce('IORedis', () => {
return new IORedisInstrumentation({
- responseHook: (span, redisCommand, cmdArgs, response) => {
- span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.redis');
-
- const safeKey = getCacheKeySafely(redisCommand, cmdArgs);
- const cacheOperation = getCacheOperation(redisCommand);
-
- if (
- !safeKey ||
- !cacheOperation ||
- !_redisOptions?.cachePrefixes ||
- !shouldConsiderForCache(redisCommand, safeKey, _redisOptions.cachePrefixes)
- ) {
- // not relevant for cache
- return;
- }
-
- // otel/ioredis seems to be using the old standard, as there was a change to those params: https://github.com/open-telemetry/opentelemetry-specification/issues/3199
- // We are using params based on the docs: https://opentelemetry.io/docs/specs/semconv/attributes-registry/network/
- const networkPeerAddress = spanToJSON(span).data?.['net.peer.name'];
- const networkPeerPort = spanToJSON(span).data?.['net.peer.port'];
- if (networkPeerPort && networkPeerAddress) {
- span.setAttributes({ 'network.peer.address': networkPeerAddress, 'network.peer.port': networkPeerPort });
- }
-
- const cacheItemSize = calculateCacheItemSize(response);
-
- if (cacheItemSize) {
- span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE, cacheItemSize);
- }
-
- if (GET_COMMANDS.includes(redisCommand) && cacheItemSize !== undefined) {
- span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, cacheItemSize > 0);
- }
-
- span.setAttributes({
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: cacheOperation,
- [SEMANTIC_ATTRIBUTE_CACHE_KEY]: safeKey,
- });
-
- const spanDescription = safeKey.join(', ');
-
- span.updateName(spanDescription.length > 1024 ? `${spanDescription.substring(0, 1024)}...` : spanDescription);
- },
+ responseHook: cacheResponseHook,
});
});
+const instrumentRedis4 = generateInstrumentOnce('Redis-4', () => {
+ return new RedisInstrumentation({
+ responseHook: cacheResponseHook,
+ });
+});
+
+/** To be able to preload all Redis OTel instrumentations with just one ID ("Redis"), all the instrumentations are generated in this one function */
+export const instrumentRedis = Object.assign(
+ (): void => {
+ instrumentIORedis();
+ instrumentRedis4();
+
+ // todo: implement them gradually
+ // new LegacyRedisInstrumentation({}),
+ },
+ { id: INTEGRATION_NAME },
+);
+
const _redisIntegration = ((options: RedisOptions = {}) => {
return {
name: INTEGRATION_NAME,
setupOnce() {
_redisOptions = options;
instrumentRedis();
-
- // todo: implement them gradually
- // new LegacyRedisInstrumentation({}),
- // new RedisInstrumentation({}),
},
};
}) satisfies IntegrationFn;
diff --git a/packages/node/src/utils/redisCache.ts b/packages/node/src/utils/redisCache.ts
index 9b394c6996da..9f0e7e6f1ac8 100644
--- a/packages/node/src/utils/redisCache.ts
+++ b/packages/node/src/utils/redisCache.ts
@@ -7,15 +7,19 @@ export const GET_COMMANDS = ['get', 'mget'];
export const SET_COMMANDS = ['set', 'setex'];
// todo: del, expire
+/** Checks if a given command is in the list of redis commands.
+ * Useful because commands can come in lowercase or uppercase (depending on the library). */
+export function isInCommands(redisCommands: string[], command: string): boolean {
+ return redisCommands.includes(command.toLowerCase());
+}
+
/** Determine cache operation based on redis statement */
export function getCacheOperation(
command: string,
): 'cache.get' | 'cache.put' | 'cache.remove' | 'cache.flush' | undefined {
- const lowercaseStatement = command.toLowerCase();
-
- if (GET_COMMANDS.includes(lowercaseStatement)) {
+ if (isInCommands(GET_COMMANDS, command)) {
return 'cache.get';
- } else if (SET_COMMANDS.includes(lowercaseStatement)) {
+ } else if (isInCommands(SET_COMMANDS, command)) {
return 'cache.put';
} else {
return undefined;
@@ -44,7 +48,7 @@ export function getCacheKeySafely(redisCommand: string, cmdArgs: IORedisCommandA
}
};
- if (SINGLE_ARG_COMMANDS.includes(redisCommand) && cmdArgs.length > 0) {
+ if (isInCommands(SINGLE_ARG_COMMANDS, redisCommand) && cmdArgs.length > 0) {
return processArg(cmdArgs[0]);
}
diff --git a/packages/node/test/integrations/tracing/redis.test.ts b/packages/node/test/integrations/tracing/redis.test.ts
index 307991f24a73..57eb727964be 100644
--- a/packages/node/test/integrations/tracing/redis.test.ts
+++ b/packages/node/test/integrations/tracing/redis.test.ts
@@ -13,12 +13,18 @@ describe('Redis', () => {
expect(result).toBe(undefined);
});
- it('should return a string representation of a single argument', () => {
+ it('should return a string array representation of a single argument', () => {
const cmdArgs = ['key1'];
const result = getCacheKeySafely('get', cmdArgs);
expect(result).toStrictEqual(['key1']);
});
+ it('should return a string array representation of a single argument (uppercase)', () => {
+ const cmdArgs = ['key1'];
+ const result = getCacheKeySafely('GET', cmdArgs);
+ expect(result).toStrictEqual(['key1']);
+ });
+
it('should return only the key for multiple arguments', () => {
const cmdArgs = ['key1', 'the-value'];
const result = getCacheKeySafely('get', cmdArgs);
diff --git a/yarn.lock b/yarn.lock
index 2843c1b4182d..e83c8fe9f310 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6486,6 +6486,15 @@
"@types/pg" "8.6.1"
"@types/pg-pool" "2.0.4"
+"@opentelemetry/instrumentation-redis-4@0.40.0":
+ version "0.40.0"
+ resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.40.0.tgz#4a1bc9bebfb869de8d982b1a1a5b550bdb68d15b"
+ integrity sha512-0ieQYJb6yl35kXA75LQUPhHtGjtQU9L85KlWa7d4ohBbk/iQKZ3X3CFl5jC5vNMq/GGPB3+w3IxNvALlHtrp7A==
+ dependencies:
+ "@opentelemetry/instrumentation" "^0.52.0"
+ "@opentelemetry/redis-common" "^0.36.2"
+ "@opentelemetry/semantic-conventions" "^1.22.0"
+
"@opentelemetry/instrumentation@0.52.0", "@opentelemetry/instrumentation@^0.52.0":
version "0.52.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.0.tgz#f8b790bfb1c61c27e0ba846bc6d0e377da195d1e"
@@ -6714,6 +6723,40 @@
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
+"@redis/bloom@1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71"
+ integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==
+
+"@redis/client@1.5.16":
+ version "1.5.16"
+ resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.16.tgz#1d5919077a06a4b935b0e4bef9e036eef1a10371"
+ integrity sha512-X1a3xQ5kEMvTib5fBrHKh6Y+pXbeKXqziYuxOUo1ojQNECg4M5Etd1qqyhMap+lFUOAh8S7UYevgJHOm4A+NOg==
+ dependencies:
+ cluster-key-slot "1.1.2"
+ generic-pool "3.9.0"
+ yallist "4.0.0"
+
+"@redis/graph@1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.1.tgz#8c10df2df7f7d02741866751764031a957a170ea"
+ integrity sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==
+
+"@redis/json@1.0.6":
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.6.tgz#b7a7725bbb907765d84c99d55eac3fcf772e180e"
+ integrity sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==
+
+"@redis/search@1.1.6":
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.6.tgz#33bcdd791d9ed88ab6910243a355d85a7fedf756"
+ integrity sha512-mZXCxbTYKBQ3M2lZnEddwEAks0Kc7nauire8q20oA0oA/LoA+E/b5Y5KZn232ztPb1FkIGqo12vh3Lf+Vw5iTw==
+
+"@redis/time-series@1.0.5":
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.5.tgz#a6d70ef7a0e71e083ea09b967df0a0ed742bc6ad"
+ integrity sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==
+
"@remix-run/node@^1.4.3":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@remix-run/node/-/node-1.5.1.tgz#1c367d4035baaef8f0ea66962a826456d62f0030"
@@ -12926,7 +12969,7 @@ clsx@^2.0.0:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==
-cluster-key-slot@^1.1.0:
+cluster-key-slot@1.1.2, cluster-key-slot@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
@@ -17457,6 +17500,11 @@ generate-function@^2.3.1:
dependencies:
is-property "^1.0.2"
+generic-pool@3.9.0:
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4"
+ integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==
+
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
@@ -26456,6 +26504,18 @@ redeyed@~1.0.0:
dependencies:
esprima "~3.0.0"
+"redis-4@npm:redis@^4.6.14":
+ version "4.6.14"
+ resolved "https://registry.yarnpkg.com/redis/-/redis-4.6.14.tgz#599e49b65816c56a6683f6b19dc374c8e786d091"
+ integrity sha512-GrNg/e33HtsQwNXL7kJT+iNFPSwE1IPmd7wzV3j4f2z0EYxZfZE7FVTmUysgAtqQQtg5NXF5SNLR9OdO/UHOfw==
+ dependencies:
+ "@redis/bloom" "1.2.0"
+ "@redis/client" "1.5.16"
+ "@redis/graph" "1.1.1"
+ "@redis/json" "1.0.6"
+ "@redis/search" "1.1.6"
+ "@redis/time-series" "1.0.5"
+
redis-errors@^1.0.0, redis-errors@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
@@ -31487,16 +31547,16 @@ yalc@^1.0.0-pre.53:
npm-packlist "^2.1.5"
yargs "^16.1.1"
+yallist@4.0.0, yallist@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+ integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
yallist@^3.0.0, yallist@^3.0.2:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
-yallist@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
- integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
-
yam@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/yam/-/yam-1.0.0.tgz#7f6c91dc0f5de75a031e6da6b3907c3d25ab0de5"
From e8e86d042668475885499214202a7e2f953d3bd7 Mon Sep 17 00:00:00 2001
From: Andrei <168741329+andreiborza@users.noreply.github.com>
Date: Tue, 11 Jun 2024 09:39:30 +0200
Subject: [PATCH 28/31] feat(solid): Update README to indicate alpha release
(#12448)
---
packages/solid/README.md | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/packages/solid/README.md b/packages/solid/README.md
index 67f24b6bd841..06ef8f810e93 100644
--- a/packages/solid/README.md
+++ b/packages/solid/README.md
@@ -10,7 +10,10 @@
[](https://www.npmjs.com/package/@sentry/solid)
[](https://www.npmjs.com/package/@sentry/solid)
-This SDK is work in progress, and should not be used before officially released.
+This SDK is considered **experimental and in an alpha state**. It may experience breaking changes. Please reach out on
+[GitHub](https://github.com/getsentry/sentry-javascript/issues/new/choose) if you have any feedback or concerns. This
+SDK currently only supports [Solid](https://www.solidjs.com/) and is not yet officially compatible with
+[Solid Start](https://start.solidjs.com/).
# Solid Router
From 1014da2ef403d4f1a41ea1981fa56fd68b70777b Mon Sep 17 00:00:00 2001
From: Luca Forstner
Date: Tue, 11 Jun 2024 11:05:47 +0200
Subject: [PATCH 29/31] feat(utils): Backfill stack trace on fetch errors if it
is missing (#12389)
---
.../playwright.config.ts | 2 +-
.../globalHandlers/fetchStackTrace/subject.js | 1 +
.../globalHandlers/fetchStackTrace/test.ts | 13 +++++++++++
.../integrations/globalHandlers/init.js | 7 ++++++
.../suites/tracing/request/fetch/test.ts | 8 ++-----
packages/utils/src/instrument/fetch.ts | 22 ++++++++++++++++++-
6 files changed, 45 insertions(+), 8 deletions(-)
create mode 100644 dev-packages/browser-integration-tests/suites/integrations/globalHandlers/fetchStackTrace/subject.js
create mode 100644 dev-packages/browser-integration-tests/suites/integrations/globalHandlers/fetchStackTrace/test.ts
create mode 100644 dev-packages/browser-integration-tests/suites/integrations/globalHandlers/init.js
diff --git a/dev-packages/browser-integration-tests/playwright.config.ts b/dev-packages/browser-integration-tests/playwright.config.ts
index 77ed6014d230..b03f758e11dc 100644
--- a/dev-packages/browser-integration-tests/playwright.config.ts
+++ b/dev-packages/browser-integration-tests/playwright.config.ts
@@ -11,7 +11,7 @@ const config: PlaywrightTestConfig = {
testMatch: /test.ts/,
use: {
- trace: process.env.CI ? 'retain-on-failure' : 'off',
+ trace: 'retain-on-failure',
},
projects: [
diff --git a/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/fetchStackTrace/subject.js b/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/fetchStackTrace/subject.js
new file mode 100644
index 000000000000..2f2e65131b96
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/fetchStackTrace/subject.js
@@ -0,0 +1 @@
+fetch('http://localhost:123/fake/endpoint/that/will/fail');
diff --git a/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/fetchStackTrace/test.ts b/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/fetchStackTrace/test.ts
new file mode 100644
index 000000000000..8d70241ec592
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/fetchStackTrace/test.ts
@@ -0,0 +1,13 @@
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers';
+
+sentryTest('should create errors with stack traces for failing fetch calls', async ({ getLocalTestPath, page }) => {
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const envelopes = await getMultipleSentryEnvelopeRequests(page, 3, { url, timeout: 10000 });
+ const errorEvent = envelopes.find(event => !event.type)!;
+ expect(errorEvent?.exception?.values?.[0].stacktrace?.frames?.length).toBeGreaterThan(0);
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/init.js b/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/init.js
new file mode 100644
index 000000000000..d8c94f36fdd0
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/init.js
@@ -0,0 +1,7 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts
index de6b1521d686..6a98022dcdd2 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts
@@ -17,12 +17,8 @@ sentryTest('should create spans for fetch requests', async ({ getLocalTestPath,
// We will wait 500ms for all envelopes to be sent. Generally, in all browsers, the last sent
// envelope contains tracing data.
-
- // If we are on FF or webkit:
- // 1st envelope contains CORS error
- // 2nd envelope contains the tracing data we want to check here
- const envelopes = await getMultipleSentryEnvelopeRequests(page, 2, { url, timeout: 10000 });
- const tracingEvent = envelopes[envelopes.length - 1]; // last envelope contains tracing data on all browsers
+ const envelopes = await getMultipleSentryEnvelopeRequests(page, 4, { url, timeout: 10000 });
+ const tracingEvent = envelopes.find(event => event.type === 'transaction')!; // last envelope contains tracing data on all browsers
const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client');
diff --git a/packages/utils/src/instrument/fetch.ts b/packages/utils/src/instrument/fetch.ts
index 4c502334024a..24c435fdf07c 100644
--- a/packages/utils/src/instrument/fetch.ts
+++ b/packages/utils/src/instrument/fetch.ts
@@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { HandlerDataFetch } from '@sentry/types';
-import { fill } from '../object';
+import { isError } from '../is';
+import { addNonEnumerableProperty, fill } from '../object';
import { supportsNativeFetch } from '../supports';
import { timestampInSeconds } from '../time';
import { GLOBAL_OBJ } from '../worldwide';
@@ -45,6 +46,15 @@ function instrumentFetch(): void {
...handlerData,
});
+ // We capture the stack right here and not in the Promise error callback because Safari (and probably other
+ // browsers too) will wipe the stack trace up to this point, only leaving us with this file which is useless.
+
+ // NOTE: If you are a Sentry user, and you are seeing this stack frame,
+ // it means the error, that was caused by your fetch call did not
+ // have a stack trace, so the SDK backfilled the stack trace so
+ // you can see which fetch call failed.
+ const virtualStackTrace = new Error().stack;
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return originalFetch.apply(GLOBAL_OBJ, args).then(
(response: Response) => {
@@ -65,6 +75,16 @@ function instrumentFetch(): void {
};
triggerHandlers('fetch', erroredHandlerData);
+
+ if (isError(error) && error.stack === undefined) {
+ // NOTE: If you are a Sentry user, and you are seeing this stack frame,
+ // it means the error, that was caused by your fetch call did not
+ // have a stack trace, so the SDK backfilled the stack trace so
+ // you can see which fetch call failed.
+ error.stack = virtualStackTrace;
+ addNonEnumerableProperty(error, 'framesToPop', 1);
+ }
+
// NOTE: If you are a Sentry user, and you are seeing this stack frame,
// it means the sentry.javascript SDK caught an error invoking your application code.
// This is expected behavior and NOT indicative of a bug with sentry.javascript.
From cc7db73efe9773fd334041a74a8cb6b90f75db9f Mon Sep 17 00:00:00 2001
From: Lukas Stracke
Date: Tue, 11 Jun 2024 11:23:04 +0200
Subject: [PATCH 30/31] fix(astro): Ensure server-side exports work correctly
(#12453)
Fix a server-side re-export problem of `@sentry/node` exports
in the Astro SDK. It seems that the `export * from '@sentry/node'` "overruled" the
explicit exports. So this patch changes our export statements to:
- only export explicit, named exports in the JS server side entry point
- continue exporting all types via the `*` export in the types entry
point
fixes #12410
---
packages/astro/src/index.server.ts | 30 +++++++++++++++++++++++++++---
packages/astro/src/index.types.ts | 7 +++++--
packages/astro/src/server/sdk.ts | 1 +
3 files changed, 33 insertions(+), 5 deletions(-)
diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts
index 6657b3030cb1..d1635cdada54 100644
--- a/packages/astro/src/index.server.ts
+++ b/packages/astro/src/index.server.ts
@@ -92,11 +92,35 @@ export {
setupHapiErrorHandler,
spotlightIntegration,
addOpenTelemetryInstrumentation,
+ metrics,
+ NodeClient,
+ addIntegration,
+ anrIntegration,
+ captureConsoleIntegration,
+ captureSession,
+ connectIntegration,
+ createGetModuleFromFilename,
+ debugIntegration,
+ dedupeIntegration,
+ endSession,
+ extraErrorDataIntegration,
+ getAutoPerformanceIntegrations,
+ httpIntegration,
+ initOpenTelemetry,
+ koaIntegration,
+ nativeNodeFetchIntegration,
+ rewriteFramesIntegration,
+ sessionTimingIntegration,
+ setupConnectErrorHandler,
+ setupKoaErrorHandler,
+ spanToBaggageHeader,
+ spanToJSON,
+ spanToTraceHeader,
+ startSession,
+ trpcMiddleware,
+ zodErrorsIntegration,
} from '@sentry/node';
-// We can still leave this for the carrier init and type exports
-export * from '@sentry/node';
-
export { init } from './server/sdk';
export default sentryAstro;
diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts
index e5fe8fd965b4..2227679dff21 100644
--- a/packages/astro/src/index.types.ts
+++ b/packages/astro/src/index.types.ts
@@ -3,6 +3,9 @@
// exports in this file - which we do below.
export * from './index.client';
export * from './index.server';
+export * from '@sentry/node';
+
+import type { NodeOptions } from '@sentry/node';
import type { Integration, Options, StackParser } from '@sentry/types';
@@ -11,7 +14,7 @@ import type * as serverSdk from './index.server';
import sentryAstro from './index.server';
/** Initializes Sentry Astro SDK */
-export declare function init(options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions): void;
+export declare function init(options: Options | clientSdk.BrowserOptions | NodeOptions): void;
export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration;
export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration;
@@ -29,5 +32,5 @@ export declare const continueTrace: typeof clientSdk.continueTrace;
export declare const Span: clientSdk.Span;
-export declare const metrics: typeof clientSdk.metrics & typeof serverSdk.metrics;
+export declare const metrics: typeof clientSdk.metrics & typeof serverSdk;
export default sentryAstro;
diff --git a/packages/astro/src/server/sdk.ts b/packages/astro/src/server/sdk.ts
index c2398c7d019f..503557d6d7cd 100644
--- a/packages/astro/src/server/sdk.ts
+++ b/packages/astro/src/server/sdk.ts
@@ -10,6 +10,7 @@ export function init(options: NodeOptions): void {
const opts = {
...options,
};
+
applySdkMetadata(opts, 'astro', ['astro', 'node']);
initNodeSdk(opts);
From 332a7ab6e47eb9cedc04fbaf77d0910b87aaa8b3 Mon Sep 17 00:00:00 2001
From: Andrei Borza
Date: Tue, 11 Jun 2024 10:04:47 +0200
Subject: [PATCH 31/31] meta(changelog): Update changelog for 8.9.0
---
CHANGELOG.md | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 652a444f5718..81a28a8de323 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,35 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 8.9.0
+
+### Important changes
+
+- **feat(solid): Add Solid SDK**
+
+ This release adds a dedicated SDK for [Solid JS](https://www.solidjs.com/) in alpha state with instrumentation for
+ [Solid Router](https://docs.solidjs.com/solid-router) and a custom `ErrorBoundary`. See the
+ [package README](https://github.com/getsentry/sentry-javascript/blob/develop/packages/solid/README.md) for how to use
+ the SDK.
+
+### Other changes
+
+- feat(deps): bump @opentelemetry/instrumentation-express from 0.40.0 to 0.40.1 (#12438)
+- feat(deps): bump @opentelemetry/instrumentation-mongodb from 0.44.0 to 0.45.0 (#12439)
+- feat(deps): bump @opentelemetry/propagator-aws-xray from 1.24.1 to 1.25.0 (#12437)
+- feat(nextjs): Allow for suppressing warning about missing global error handler file (#12369)
+- feat(redis): Add cache logic for redis-4 (#12429)
+- feat(replay): Replay Web Vital Breadcrumbs (#12296)
+- fix: Fix types export order (#12404)
+- fix(astro): Ensure server-side exports work correctly (#12453)
+- fix(aws-serverless): Add `op` to Otel-generated lambda function root span (#12430)
+- fix(aws-serverless): Only auto-patch handler in CJS when loading `awslambda-auto` (#12392)
+- fix(aws-serverless): Only start root span in Sentry wrapper if Otel didn't wrap handler (#12407)
+- fix(browser): Fix INP span creation & transaction tagging (#12372)
+- fix(nextjs): correct types conditional export ordering (#12355)
+- fix(replay): Fix guard for exception event (#12441)
+- fix(vue): Handle span name assignment for nested routes in VueRouter (#12398)
+
Work in this release was contributed by @soch4n. Thank you for your contribution!
## 8.8.0