diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c5047e7acd0a..29dcba922106 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1046,6 +1046,8 @@ jobs:
exclude:
- is_dependabot: true
test-application: 'cloudflare-astro'
+ - is_dependabot: true
+ test-application: 'cloudflare-workers'
steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
@@ -1125,6 +1127,7 @@ jobs:
test-application:
[
'cloudflare-astro',
+ 'cloudflare-workers',
'react-send-to-sentry',
'node-express-send-to-sentry',
'debug-id-sourcemaps',
diff --git a/.size-limit.js b/.size-limit.js
index dc85fffe40af..72050f7225f3 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -149,7 +149,7 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing, Replay)',
path: createCDNPath('bundle.tracing.replay.min.js'),
gzip: true,
- limit: '72 KB',
+ limit: '73 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Feedback)',
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 208ec7eb2a68..3446e195a6b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,36 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+# 8.22.0
+
+### Important Changes
+
+- **feat(cloudflare): Add plugin for cloudflare pages (#13123)**
+
+This release adds support for Cloudflare Pages to `@sentry/cloudflare`, our SDK for the
+[Cloudflare Workers JavaScript Runtime](https://developers.cloudflare.com/workers/)! For details on how to use it,
+please see the [README](./packages/cloudflare/README.md). Any feedback/bug reports are greatly appreciated, please
+[reach out on GitHub](https://github.com/getsentry/sentry-javascript/issues/12620).
+
+```javascript
+// functions/_middleware.js
+import * as Sentry from '@sentry/cloudflare';
+
+export const onRequest = Sentry.sentryPagesPlugin({
+ dsn: __PUBLIC_DSN__,
+ // Set tracesSampleRate to 1.0 to capture 100% of spans for tracing.
+ tracesSampleRate: 1.0,
+});
+```
+
+### Other Changes
+
+- feat(meta-sdks): Remove runtime tags (#13105)
+- feat(nestjs): Automatic instrumentation of nestjs guards (#13129)
+- feat(nestjs): Filter all HttpExceptions (#13120)
+- feat(replay): Capture exception when `internal_sdk_error` client report happens (#13072)
+- fix: Use `globalThis` for code injection (#13132)
+
## 8.21.0
### Important Changes
diff --git a/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts b/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts
index 257c47fbfa9b..f4defc27182c 100644
--- a/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts
+++ b/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts
@@ -127,7 +127,7 @@ export const expectedLCPPerformanceSpan = {
endTimestamp: expect.any(Number),
data: {
value: expect.any(Number),
- nodeId: expect.any(Number),
+ nodeIds: expect.any(Array),
rating: expect.any(String),
size: expect.any(Number),
},
@@ -140,6 +140,7 @@ export const expectedCLSPerformanceSpan = {
endTimestamp: expect.any(Number),
data: {
value: expect.any(Number),
+ nodeIds: expect.any(Array),
rating: expect.any(String),
size: expect.any(Number),
},
@@ -154,7 +155,7 @@ export const expectedFIDPerformanceSpan = {
value: expect.any(Number),
rating: expect.any(String),
size: expect.any(Number),
- nodeId: expect.any(Number),
+ nodeIds: expect.any(Array),
},
};
@@ -167,7 +168,7 @@ export const expectedINPPerformanceSpan = {
value: expect.any(Number),
rating: expect.any(String),
size: expect.any(Number),
- nodeId: expect.any(Number),
+ nodeIds: expect.any(Array),
},
};
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/.npmrc b/dev-packages/e2e-tests/test-applications/cloudflare-workers/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/.prettierrc b/dev-packages/e2e-tests/test-applications/cloudflare-workers/.prettierrc
new file mode 100644
index 000000000000..5c7b5d3c7a75
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "printWidth": 140,
+ "singleQuote": true,
+ "semi": true,
+ "useTabs": true
+}
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json
new file mode 100644
index 000000000000..bb01c0b8a8ad
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "cloudflare-workers",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "deploy": "wrangler deploy",
+ "dev": "wrangler dev --var E2E_TEST_DSN=$E2E_TEST_DSN",
+ "build": "wrangler deploy --dry-run --var E2E_TEST_DSN=$E2E_TEST_DSN",
+ "test": "vitest",
+ "typecheck": "tsc --noEmit",
+ "cf-typegen": "wrangler types",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm typecheck"
+ },
+ "dependencies": {
+ "@sentry/cloudflare": "latest || *"
+ },
+ "devDependencies": {
+ "@cloudflare/vitest-pool-workers": "^0.4.5",
+ "@cloudflare/workers-types": "^4.20240725.0",
+ "typescript": "^5.5.2",
+ "vitest": "1.5.0",
+ "wrangler": "^3.60.3"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts
new file mode 100644
index 000000000000..a3366168fa08
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts
@@ -0,0 +1,26 @@
+/**
+ * Welcome to Cloudflare Workers! This is your first worker.
+ *
+ * - Run `npm run dev` in your terminal to start a development server
+ * - Open a browser tab at http://localhost:8787/ to see your worker in action
+ * - Run `npm run deploy` to publish your worker
+ *
+ * Bind resources to your worker in `wrangler.toml`. After adding bindings, a type definition for the
+ * `Env` object can be regenerated with `npm run cf-typegen`.
+ *
+ * Learn more at https://developers.cloudflare.com/workers/
+ */
+import * as Sentry from '@sentry/cloudflare';
+
+export default Sentry.withSentry(
+ (env: Env) => ({
+ dsn: env.E2E_TEST_DSN,
+ // Set tracesSampleRate to 1.0 to capture 100% of spans for tracing.
+ tracesSampleRate: 1.0,
+ }),
+ {
+ async fetch(request, env, ctx) {
+ return new Response('Hello World!');
+ },
+ } satisfies ExportedHandler,
+);
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/test/index.spec.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/test/index.spec.ts
new file mode 100644
index 000000000000..21c9d1b7999a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/test/index.spec.ts
@@ -0,0 +1,25 @@
+import { describe, expect, it } from 'vitest';
+import worker from '../src/index';
+// test/index.spec.ts
+import { SELF, createExecutionContext, env, waitOnExecutionContext } from 'cloudflare:test';
+
+// For now, you'll need to do something like this to get a correctly-typed
+// `Request` to pass to `worker.fetch()`.
+const IncomingRequest = Request;
+
+describe('Hello World worker', () => {
+ it('responds with Hello World! (unit style)', async () => {
+ const request = new IncomingRequest('http://example.com');
+ // Create an empty context to pass to `worker.fetch()`.
+ const ctx = createExecutionContext();
+ const response = await worker.fetch(request, env, ctx);
+ // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
+ await waitOnExecutionContext(ctx);
+ expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
+ });
+
+ it('responds with Hello World! (integration style)', async () => {
+ const response = await SELF.fetch('https://example.com');
+ expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/test/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers/test/tsconfig.json
new file mode 100644
index 000000000000..bc019a7e2bfb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/test/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "types": ["@cloudflare/workers-types/experimental", "@cloudflare/vitest-pool-workers"]
+ },
+ "include": ["./**/*.ts", "../src/env.d.ts"],
+ "exclude": []
+}
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tsconfig.json
new file mode 100644
index 000000000000..79207ab7ae9a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tsconfig.json
@@ -0,0 +1,105 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig.json to read more about this file */
+
+ /* Projects */
+ // "incremental": true, /* Enable incremental compilation */
+ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
+ // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
+ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
+ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
+ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
+
+ /* Language and Environment */
+ "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
+ "lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
+ "jsx": "react-jsx" /* Specify what JSX code is generated. */,
+ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
+ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
+ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
+ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
+ // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
+ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
+ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
+
+ /* Modules */
+ "module": "es2022" /* Specify what module code is generated. */,
+ // "rootDir": "./", /* Specify the root folder within your source files. */
+ "moduleResolution": "Bundler" /* Specify how TypeScript looks up a file from a given module specifier. */,
+ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
+ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
+ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
+ // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
+ "types": [
+ "@cloudflare/workers-types/2023-07-01"
+ ] /* Specify type package names to be included without being referenced in a source file. */,
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
+ "resolveJsonModule": true /* Enable importing .json files */,
+ // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */
+
+ /* JavaScript Support */
+ "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */,
+ "checkJs": false /* Enable error reporting in type-checked JavaScript files. */,
+ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
+
+ /* Emit */
+ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
+ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
+ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
+ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
+ // "outDir": "./", /* Specify an output folder for all emitted files. */
+ // "removeComments": true, /* Disable emitting comments. */
+ "noEmit": true /* Disable emitting files from a compilation. */,
+ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
+ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
+ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
+ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+ // "newLine": "crlf", /* Set the newline character for emitting files. */
+ // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
+ // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
+ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
+ // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
+ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
+ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
+
+ /* Interop Constraints */
+ "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
+ "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
+ // "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
+ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
+
+ /* Type Checking */
+ "strict": true /* Enable all strict type-checking options. */,
+ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
+ // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
+ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+ // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
+ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
+ // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
+ // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
+ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
+ // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
+ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
+ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
+ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
+ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
+ // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
+ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
+ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
+ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
+ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
+
+ /* Completeness */
+ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
+ "skipLibCheck": true /* Skip type checking all .d.ts files. */
+ },
+ "exclude": ["test"],
+ "include": ["worker-configuration.d.ts", "src/**/*.ts"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/vitest.config.mts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/vitest.config.mts
new file mode 100644
index 000000000000..931e5113e0c2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/vitest.config.mts
@@ -0,0 +1,11 @@
+import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
+
+export default defineWorkersConfig({
+ test: {
+ poolOptions: {
+ workers: {
+ wrangler: { configPath: './wrangler.toml' },
+ },
+ },
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/worker-configuration.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/worker-configuration.d.ts
new file mode 100644
index 000000000000..0c9e04919e42
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/worker-configuration.d.ts
@@ -0,0 +1,6 @@
+// Generated by Wrangler on Mon Jul 29 2024 21:44:31 GMT-0400 (Eastern Daylight Time)
+// by running `wrangler types`
+
+interface Env {
+ E2E_TEST_DSN: '';
+}
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/wrangler.toml b/dev-packages/e2e-tests/test-applications/cloudflare-workers/wrangler.toml
new file mode 100644
index 000000000000..2fc762f4025c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/wrangler.toml
@@ -0,0 +1,111 @@
+#:schema node_modules/wrangler/config-schema.json
+name = "cloudflare-workers"
+main = "src/index.ts"
+compatibility_date = "2024-07-25"
+compatibility_flags = ["nodejs_compat"]
+
+# [vars]
+# E2E_TEST_DSN = ""
+
+# Automatically place your workloads in an optimal location to minimize latency.
+# If you are running back-end logic in a Worker, running it closer to your back-end infrastructure
+# rather than the end user may result in better performance.
+# Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
+# [placement]
+# mode = "smart"
+
+# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
+# Docs:
+# - https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
+# Note: Use secrets to store sensitive data.
+# - https://developers.cloudflare.com/workers/configuration/secrets/
+# [vars]
+# MY_VARIABLE = "production_value"
+
+# Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflare’s global network
+# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#workers-ai
+# [ai]
+# binding = "AI"
+
+# Bind an Analytics Engine dataset. Use Analytics Engine to write analytics within your Pages Function.
+# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#analytics-engine-datasets
+# [[analytics_engine_datasets]]
+# binding = "MY_DATASET"
+
+# Bind a headless browser instance running on Cloudflare's global network.
+# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#browser-rendering
+# [browser]
+# binding = "MY_BROWSER"
+
+# Bind a D1 database. D1 is Cloudflare’s native serverless SQL database.
+# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#d1-databases
+# [[d1_databases]]
+# binding = "MY_DB"
+# database_name = "my-database"
+# database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+
+# Bind a dispatch namespace. Use Workers for Platforms to deploy serverless functions programmatically on behalf of your customers.
+# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#dispatch-namespace-bindings-workers-for-platforms
+# [[dispatch_namespaces]]
+# binding = "MY_DISPATCHER"
+# namespace = "my-namespace"
+
+# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model.
+# Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps.
+# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects
+# [[durable_objects.bindings]]
+# name = "MY_DURABLE_OBJECT"
+# class_name = "MyDurableObject"
+
+# Durable Object migrations.
+# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#migrations
+# [[migrations]]
+# tag = "v1"
+# new_classes = ["MyDurableObject"]
+
+# Bind a Hyperdrive configuration. Use to accelerate access to your existing databases from Cloudflare Workers.
+# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#hyperdrive
+# [[hyperdrive]]
+# binding = "MY_HYPERDRIVE"
+# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+
+# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs.
+# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#kv-namespaces
+# [[kv_namespaces]]
+# binding = "MY_KV_NAMESPACE"
+# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+
+# Bind an mTLS certificate. Use to present a client certificate when communicating with another service.
+# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#mtls-certificates
+# [[mtls_certificates]]
+# binding = "MY_CERTIFICATE"
+# certificate_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+
+# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer.
+# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#queues
+# [[queues.producers]]
+# binding = "MY_QUEUE"
+# queue = "my-queue"
+
+# Bind a Queue consumer. Queue Consumers can retrieve tasks scheduled by Producers to act on them.
+# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#queues
+# [[queues.consumers]]
+# queue = "my-queue"
+
+# Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files.
+# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#r2-buckets
+# [[r2_buckets]]
+# binding = "MY_BUCKET"
+# bucket_name = "my-bucket"
+
+# Bind another Worker service. Use this binding to call another Worker without network overhead.
+# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
+# [[services]]
+# binding = "MY_SERVICE"
+# service = "my-service"
+
+# Bind a Vectorize index. Use to store and query vector embeddings for semantic search, classification and other vector search use-cases.
+# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#vectorize-indexes
+# [[vectorize]]
+# binding = "MY_INDEX"
+# index_name = "my-index"
diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-next-app/tests/client-transactions.test.ts
index dd4633620e8b..d46191a77c03 100644
--- a/dev-packages/e2e-tests/test-applications/create-next-app/tests/client-transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/create-next-app/tests/client-transactions.test.ts
@@ -13,7 +13,6 @@ test('Sends a pageload transaction to Sentry', async ({ page }) => {
expect(transactionEvent).toEqual(
expect.objectContaining({
transaction: '/',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -59,7 +58,6 @@ test('captures a navigation transcation to Sentry', async ({ page }) => {
expect(clientTxnEvent).toEqual(
expect.objectContaining({
transaction: '/user/[id]',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts
index 56be8b65d60b..db5aec11ced0 100644
--- a/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts
@@ -32,10 +32,6 @@ test('Sends server-side transactions to Sentry', async ({ baseURL }) => {
}),
status: 'ok',
},
- runtime: {
- name: 'node',
- version: expect.any(String),
- },
}),
spans: [
{
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts
index 2a4f14cae541..eb0ead5e32d0 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts
@@ -1,5 +1,6 @@
-import { Controller, Get, Param } from '@nestjs/common';
+import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
+import { ExampleGuard } from './example.guard';
@Controller()
export class AppController {
@@ -15,14 +16,25 @@ export class AppController {
return this.appService.testMiddleware();
}
+ @Get('test-guard-instrumentation')
+ @UseGuards(ExampleGuard)
+ testGuardInstrumentation() {
+ return {};
+ }
+
@Get('test-exception/:id')
async testException(@Param('id') id: string) {
return this.appService.testException(id);
}
- @Get('test-expected-exception/:id')
- async testExpectedException(@Param('id') id: string) {
- return this.appService.testExpectedException(id);
+ @Get('test-expected-400-exception/:id')
+ async testExpected400Exception(@Param('id') id: string) {
+ return this.appService.testExpected400Exception(id);
+ }
+
+ @Get('test-expected-500-exception/:id')
+ async testExpected500Exception(@Param('id') id: string) {
+ return this.appService.testExpected500Exception(id);
}
@Get('test-span-decorator-async')
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts
index 9a47f0e08e7a..1ae4c50d8901 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts
@@ -30,8 +30,12 @@ export class AppService {
throw new Error(`This is an exception with id ${id}`);
}
- testExpectedException(id: string) {
- throw new HttpException(`This is an expected exception with id ${id}`, HttpStatus.FORBIDDEN);
+ testExpected400Exception(id: string) {
+ throw new HttpException(`This is an expected 400 exception with id ${id}`, HttpStatus.BAD_REQUEST);
+ }
+
+ testExpected500Exception(id: string) {
+ throw new HttpException(`This is an expected 500 exception with id ${id}`, HttpStatus.INTERNAL_SERVER_ERROR);
}
@SentryTraced('wait and return a string')
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/example.guard.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/example.guard.ts
new file mode 100644
index 000000000000..e12bbdc4e994
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/example.guard.ts
@@ -0,0 +1,10 @@
+import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
+import * as Sentry from '@sentry/nestjs';
+
+@Injectable()
+export class ExampleGuard implements CanActivate {
+ canActivate(context: ExecutionContext): boolean {
+ Sentry.startSpan({ name: 'test-guard-span' }, () => {});
+ return true;
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts
index 349b25b0eee9..dad5d391bdde 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts
@@ -29,25 +29,41 @@ test('Sends exception to Sentry', async ({ baseURL }) => {
});
});
-test('Does not send expected exception to Sentry', async ({ baseURL }) => {
+test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => {
let errorEventOccurred = false;
waitForError('nestjs', event => {
- if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected exception with id 123') {
+ if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 400 exception with id 123') {
errorEventOccurred = true;
}
- return event?.transaction === 'GET /test-expected-exception/:id';
+ return event?.transaction === 'GET /test-expected-400-exception/:id';
});
- const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
- return transactionEvent?.transaction === 'GET /test-expected-exception/:id';
+ waitForError('nestjs', event => {
+ if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 500 exception with id 123') {
+ errorEventOccurred = true;
+ }
+
+ return event?.transaction === 'GET /test-expected-500-exception/:id';
});
- const response = await fetch(`${baseURL}/test-expected-exception/123`);
- expect(response.status).toBe(403);
+ const transactionEventPromise400 = waitForTransaction('nestjs', transactionEvent => {
+ return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id';
+ });
+
+ const transactionEventPromise500 = waitForTransaction('nestjs', transactionEvent => {
+ return transactionEvent?.transaction === 'GET /test-expected-500-exception/:id';
+ });
+
+ const response400 = await fetch(`${baseURL}/test-expected-400-exception/123`);
+ expect(response400.status).toBe(400);
+
+ const response500 = await fetch(`${baseURL}/test-expected-500-exception/123`);
+ expect(response500.status).toBe(500);
- await transactionEventPromise;
+ await transactionEventPromise400;
+ await transactionEventPromise500;
await new Promise(resolve => setTimeout(resolve, 10000));
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts
index b7017b72dbf5..ebd8503e1d42 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts
@@ -132,7 +132,8 @@ test('API route transaction includes nest middleware span. Spans created in and
);
});
- await fetch(`${baseURL}/test-middleware-instrumentation`);
+ const response = await fetch(`${baseURL}/test-middleware-instrumentation`);
+ expect(response.status).toBe(200);
const transactionEvent = await pageloadTransactionEventPromise;
@@ -200,3 +201,68 @@ test('API route transaction includes nest middleware span. Spans created in and
// 'ExampleMiddleware' is NOT the parent of 'test-controller-span'
expect(testControllerSpan.parent_span_id).not.toBe(exampleMiddlewareSpanId);
});
+
+test('API route transaction includes nest guard span and span started in guard is nested correctly', async ({
+ baseURL,
+}) => {
+ const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-guard-instrumentation'
+ );
+ });
+
+ const response = await fetch(`${baseURL}/test-guard-instrumentation`);
+ expect(response.status).toBe(200);
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: {
+ 'sentry.op': 'middleware.nestjs',
+ 'sentry.origin': 'auto.middleware.nestjs',
+ },
+ description: 'ExampleGuard',
+ parent_span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'middleware.nestjs',
+ origin: 'auto.middleware.nestjs',
+ },
+ ]),
+ }),
+ );
+
+ const exampleGuardSpan = transactionEvent.spans.find(span => span.description === 'ExampleGuard');
+ const exampleGuardSpanId = exampleGuardSpan?.span_id;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: expect.any(Object),
+ description: 'test-guard-span',
+ parent_span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ origin: 'manual',
+ },
+ ]),
+ }),
+ );
+
+ // verify correct span parent-child relationships
+ const testGuardSpan = transactionEvent.spans.find(span => span.description === 'test-guard-span');
+
+ // 'ExampleGuard' is the parent of 'test-guard-span'
+ expect(testGuardSpan.parent_span_id).toBe(exampleGuardSpanId);
+});
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 42fee84295b8..8d2489bab34d 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
@@ -19,7 +19,6 @@ test('Sends a pageload transaction', async ({ page }) => {
expect(transactionEvent).toEqual(
expect.objectContaining({
transaction: '/',
- tags: { runtime: 'browser' },
transaction_info: { source: 'url' },
type: 'transaction',
contexts: {
diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts
index 2a4f14cae541..eb0ead5e32d0 100644
--- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts
+++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts
@@ -1,5 +1,6 @@
-import { Controller, Get, Param } from '@nestjs/common';
+import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
+import { ExampleGuard } from './example.guard';
@Controller()
export class AppController {
@@ -15,14 +16,25 @@ export class AppController {
return this.appService.testMiddleware();
}
+ @Get('test-guard-instrumentation')
+ @UseGuards(ExampleGuard)
+ testGuardInstrumentation() {
+ return {};
+ }
+
@Get('test-exception/:id')
async testException(@Param('id') id: string) {
return this.appService.testException(id);
}
- @Get('test-expected-exception/:id')
- async testExpectedException(@Param('id') id: string) {
- return this.appService.testExpectedException(id);
+ @Get('test-expected-400-exception/:id')
+ async testExpected400Exception(@Param('id') id: string) {
+ return this.appService.testExpected400Exception(id);
+ }
+
+ @Get('test-expected-500-exception/:id')
+ async testExpected500Exception(@Param('id') id: string) {
+ return this.appService.testExpected500Exception(id);
}
@Get('test-span-decorator-async')
diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts
index 9a47f0e08e7a..1ae4c50d8901 100644
--- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts
+++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts
@@ -30,8 +30,12 @@ export class AppService {
throw new Error(`This is an exception with id ${id}`);
}
- testExpectedException(id: string) {
- throw new HttpException(`This is an expected exception with id ${id}`, HttpStatus.FORBIDDEN);
+ testExpected400Exception(id: string) {
+ throw new HttpException(`This is an expected 400 exception with id ${id}`, HttpStatus.BAD_REQUEST);
+ }
+
+ testExpected500Exception(id: string) {
+ throw new HttpException(`This is an expected 500 exception with id ${id}`, HttpStatus.INTERNAL_SERVER_ERROR);
}
@SentryTraced('wait and return a string')
diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.guard.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.guard.ts
new file mode 100644
index 000000000000..e12bbdc4e994
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.guard.ts
@@ -0,0 +1,10 @@
+import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
+import * as Sentry from '@sentry/nestjs';
+
+@Injectable()
+export class ExampleGuard implements CanActivate {
+ canActivate(context: ExecutionContext): boolean {
+ Sentry.startSpan({ name: 'test-guard-span' }, () => {});
+ return true;
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/errors.test.ts
index 349b25b0eee9..dad5d391bdde 100644
--- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/errors.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/errors.test.ts
@@ -29,25 +29,41 @@ test('Sends exception to Sentry', async ({ baseURL }) => {
});
});
-test('Does not send expected exception to Sentry', async ({ baseURL }) => {
+test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => {
let errorEventOccurred = false;
waitForError('nestjs', event => {
- if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected exception with id 123') {
+ if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 400 exception with id 123') {
errorEventOccurred = true;
}
- return event?.transaction === 'GET /test-expected-exception/:id';
+ return event?.transaction === 'GET /test-expected-400-exception/:id';
});
- const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
- return transactionEvent?.transaction === 'GET /test-expected-exception/:id';
+ waitForError('nestjs', event => {
+ if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 500 exception with id 123') {
+ errorEventOccurred = true;
+ }
+
+ return event?.transaction === 'GET /test-expected-500-exception/:id';
});
- const response = await fetch(`${baseURL}/test-expected-exception/123`);
- expect(response.status).toBe(403);
+ const transactionEventPromise400 = waitForTransaction('nestjs', transactionEvent => {
+ return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id';
+ });
+
+ const transactionEventPromise500 = waitForTransaction('nestjs', transactionEvent => {
+ return transactionEvent?.transaction === 'GET /test-expected-500-exception/:id';
+ });
+
+ const response400 = await fetch(`${baseURL}/test-expected-400-exception/123`);
+ expect(response400.status).toBe(400);
+
+ const response500 = await fetch(`${baseURL}/test-expected-500-exception/123`);
+ expect(response500.status).toBe(500);
- await transactionEventPromise;
+ await transactionEventPromise400;
+ await transactionEventPromise500;
await new Promise(resolve => setTimeout(resolve, 10000));
diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts
index b7017b72dbf5..c646ac9aea74 100644
--- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts
@@ -125,16 +125,17 @@ test('Sends an API route transaction', async ({ baseURL }) => {
test('API route transaction includes nest middleware span. Spans created in and after middleware are nested correctly', async ({
baseURL,
}) => {
- const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
+ const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' &&
transactionEvent?.transaction === 'GET /test-middleware-instrumentation'
);
});
- await fetch(`${baseURL}/test-middleware-instrumentation`);
+ const response = await fetch(`${baseURL}/test-middleware-instrumentation`);
+ expect(response.status).toBe(200);
- const transactionEvent = await pageloadTransactionEventPromise;
+ const transactionEvent = await transactionEventPromise;
expect(transactionEvent).toEqual(
expect.objectContaining({
@@ -200,3 +201,68 @@ test('API route transaction includes nest middleware span. Spans created in and
// 'ExampleMiddleware' is NOT the parent of 'test-controller-span'
expect(testControllerSpan.parent_span_id).not.toBe(exampleMiddlewareSpanId);
});
+
+test('API route transaction includes nest guard span and span started in guard is nested correctly', async ({
+ baseURL,
+}) => {
+ const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-guard-instrumentation'
+ );
+ });
+
+ const response = await fetch(`${baseURL}/test-guard-instrumentation`);
+ expect(response.status).toBe(200);
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: {
+ 'sentry.op': 'middleware.nestjs',
+ 'sentry.origin': 'auto.middleware.nestjs',
+ },
+ description: 'ExampleGuard',
+ parent_span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'middleware.nestjs',
+ origin: 'auto.middleware.nestjs',
+ },
+ ]),
+ }),
+ );
+
+ const exampleGuardSpan = transactionEvent.spans.find(span => span.description === 'ExampleGuard');
+ const exampleGuardSpanId = exampleGuardSpan?.span_id;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: expect.any(Object),
+ description: 'test-guard-span',
+ parent_span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ origin: 'manual',
+ },
+ ]),
+ }),
+ );
+
+ // verify correct span parent-child relationships
+ const testGuardSpan = transactionEvent.spans.find(span => span.description === 'test-guard-span');
+
+ // 'ExampleGuard' is the parent of 'test-guard-span'
+ expect(testGuardSpan.parent_span_id).toBe(exampleGuardSpanId);
+});
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 156c2775f5ff..1b054c099b3d 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
@@ -220,7 +220,7 @@ export const ReplayRecordingData = [
value: expect.any(Number),
size: expect.any(Number),
rating: expect.any(String),
- nodeId: 16,
+ nodeIds: [16],
},
},
},
@@ -239,6 +239,7 @@ export const ReplayRecordingData = [
value: expect.any(Number),
size: expect.any(Number),
rating: expect.any(String),
+ nodeIds: expect.any(Array),
},
},
},
@@ -257,7 +258,7 @@ export const ReplayRecordingData = [
value: expect.any(Number),
size: expect.any(Number),
rating: expect.any(String),
- nodeId: 10,
+ nodeIds: [10],
},
},
},
diff --git a/dev-packages/e2e-tests/test-applications/solidstart/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart/tests/errors.client.test.ts
index c9ab1db244b5..0f5ef61b365a 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart/tests/errors.client.test.ts
+++ b/dev-packages/e2e-tests/test-applications/solidstart/tests/errors.client.test.ts
@@ -26,7 +26,6 @@ test.describe('client-side errors', () => {
},
transaction: '/client-error',
});
- expect(error.tags).toMatchObject({ runtime: 'browser' });
expect(error.transaction).toEqual('/client-error');
});
});
diff --git a/dev-packages/e2e-tests/test-applications/solidstart/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart/tests/errors.server.test.ts
index 0ccea7d3767e..ccd0a802fbb2 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart/tests/errors.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/solidstart/tests/errors.server.test.ts
@@ -11,7 +11,6 @@ test.describe('server-side errors', () => {
const error = await errorEventPromise;
- expect(error.tags).toMatchObject({ runtime: 'node' });
expect(error).toMatchObject({
exception: {
values: [
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/errors.client.test.ts
index 1984a0db9603..fce77451551b 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/errors.client.test.ts
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/errors.client.test.ts
@@ -26,8 +26,6 @@ test.describe('client-side errors', () => {
}),
);
- expect(errorEvent.tags).toMatchObject({ runtime: 'browser' });
-
expect(errorEvent.transaction).toEqual('/client-error');
});
@@ -53,7 +51,6 @@ test.describe('client-side errors', () => {
}),
);
- expect(errorEvent.tags).toMatchObject({ runtime: 'browser' });
expect(errorEvent.transaction).toEqual('/universal-load-error');
});
});
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/errors.server.test.ts
index 0764d26e05a5..c019a5b7260e 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/errors.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/errors.server.test.ts
@@ -19,8 +19,6 @@ test.describe('server-side errors', () => {
}),
);
- expect(errorEvent.tags).toMatchObject({ runtime: 'node' });
-
expect(errorEvent.request).toEqual({
cookies: {},
headers: expect.objectContaining({
@@ -49,8 +47,6 @@ test.describe('server-side errors', () => {
}),
);
- expect(errorEvent.tags).toMatchObject({ runtime: 'node' });
-
expect(errorEvent.request).toEqual({
cookies: {},
headers: expect.objectContaining({
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.server.test.ts
index 8c23996c9a37..f065f5148411 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.server.test.ts
@@ -13,7 +13,6 @@ test('server pageload request span has nested request span for sub request', asy
expect(serverTxnEvent).toMatchObject({
transaction: 'GET /server-load-fetch',
- tags: { runtime: 'node' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.test.ts
index 622562b9ab6a..95855d8f8e81 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.test.ts
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.test.ts
@@ -21,7 +21,6 @@ test.describe('performance events', () => {
expect(clientTxnEvent).toMatchObject({
transaction: '/users/[id]',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -34,7 +33,6 @@ test.describe('performance events', () => {
expect(serverTxnEvent).toMatchObject({
transaction: 'GET /users/[id]',
- tags: { runtime: 'node' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -77,7 +75,6 @@ test.describe('performance events', () => {
expect(clientTxnEvent).toMatchObject({
transaction: '/users',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -90,7 +87,6 @@ test.describe('performance events', () => {
expect(serverTxnEvent).toMatchObject({
transaction: 'GET /users',
- tags: { runtime: 'node' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -130,7 +126,6 @@ test.describe('performance events', () => {
expect(clientTxnEvent).toMatchObject({
transaction: '/universal-load-fetch',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -143,7 +138,6 @@ test.describe('performance events', () => {
expect(serverTxnEvent).toMatchObject({
transaction: 'GET /api/users',
- tags: { runtime: 'node' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -186,11 +180,11 @@ test.describe('performance events', () => {
test('captures a navigation transaction directly after pageload', async ({ page }) => {
const clientPageloadTxnPromise = waitForTransaction('sveltekit-2-svelte-5', txnEvent => {
- return txnEvent?.contexts?.trace?.op === 'pageload' && txnEvent?.tags?.runtime === 'browser';
+ return txnEvent?.contexts?.trace?.op === 'pageload';
});
const clientNavigationTxnPromise = waitForTransaction('sveltekit-2-svelte-5', txnEvent => {
- return txnEvent?.contexts?.trace?.op === 'navigation' && txnEvent?.tags?.runtime === 'browser';
+ return txnEvent?.contexts?.trace?.op === 'navigation';
});
await waitForInitialPageload(page, { route: '/' });
@@ -205,7 +199,6 @@ test.describe('performance events', () => {
expect(pageloadTxnEvent).toMatchObject({
transaction: '/',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -218,7 +211,6 @@ test.describe('performance events', () => {
expect(navigationTxnEvent).toMatchObject({
transaction: '/users/[id]',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -253,27 +245,15 @@ test.describe('performance events', () => {
test('captures one navigation transaction per redirect', async ({ page }) => {
const clientNavigationRedirect1TxnPromise = waitForTransaction('sveltekit-2-svelte-5', txnEvent => {
- return (
- txnEvent?.contexts?.trace?.op === 'navigation' &&
- txnEvent?.tags?.runtime === 'browser' &&
- txnEvent?.transaction === '/redirect1'
- );
+ return txnEvent?.contexts?.trace?.op === 'navigation' && txnEvent?.transaction === '/redirect1';
});
const clientNavigationRedirect2TxnPromise = waitForTransaction('sveltekit-2-svelte-5', txnEvent => {
- return (
- txnEvent?.contexts?.trace?.op === 'navigation' &&
- txnEvent?.tags?.runtime === 'browser' &&
- txnEvent?.transaction === '/redirect2'
- );
+ return txnEvent?.contexts?.trace?.op === 'navigation' && txnEvent?.transaction === '/redirect2';
});
const clientNavigationRedirect3TxnPromise = waitForTransaction('sveltekit-2-svelte-5', txnEvent => {
- return (
- txnEvent?.contexts?.trace?.op === 'navigation' &&
- txnEvent?.tags?.runtime === 'browser' &&
- txnEvent?.transaction === '/users/[id]'
- );
+ return txnEvent?.contexts?.trace?.op === 'navigation' && txnEvent?.transaction === '/users/[id]';
});
await waitForInitialPageload(page, { route: '/' });
@@ -289,7 +269,6 @@ test.describe('performance events', () => {
expect(redirect1TxnEvent).toMatchObject({
transaction: '/redirect1',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -327,7 +306,6 @@ test.describe('performance events', () => {
expect(redirect2TxnEvent).toMatchObject({
transaction: '/redirect2',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -365,7 +343,6 @@ test.describe('performance events', () => {
expect(redirect3TxnEvent).toMatchObject({
transaction: '/users/[id]',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/errors.client.test.ts
index eecd5e00fae0..2764d5a742f9 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/errors.client.test.ts
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/errors.client.test.ts
@@ -26,8 +26,6 @@ test.describe('client-side errors', () => {
}),
);
- expect(errorEvent.tags).toMatchObject({ runtime: 'browser' });
-
expect(errorEvent.transaction).toEqual('/client-error');
});
@@ -53,7 +51,6 @@ test.describe('client-side errors', () => {
}),
);
- expect(errorEvent.tags).toMatchObject({ runtime: 'browser' });
expect(errorEvent.transaction).toEqual('/universal-load-error');
});
});
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/errors.server.test.ts
index 64a0b2e3c855..c9dc56b9c96b 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/errors.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/errors.server.test.ts
@@ -18,8 +18,6 @@ test.describe('server-side errors', () => {
in_app: true,
}),
);
-
- expect(errorEvent.tags).toMatchObject({ runtime: 'node' });
});
test('captures server load error', async ({ page }) => {
@@ -38,8 +36,6 @@ test.describe('server-side errors', () => {
in_app: true,
}),
);
-
- expect(errorEvent.tags).toMatchObject({ runtime: 'node' });
});
test('captures server route (GET) error', async ({ page }) => {
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/performance.server.test.ts
index 6f11fd17cd5b..4cc3fb5cef9e 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/performance.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/performance.server.test.ts
@@ -13,7 +13,6 @@ test('server pageload request span has nested request span for sub request', asy
expect(serverTxnEvent).toMatchObject({
transaction: 'GET /server-load-fetch',
- tags: { runtime: 'node' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/performance.test.ts
index ddaac44096f5..24f2cd256a63 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/performance.test.ts
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/performance.test.ts
@@ -21,7 +21,6 @@ test.describe('performance events', () => {
expect(clientTxnEvent).toMatchObject({
transaction: '/users/[id]',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -34,7 +33,6 @@ test.describe('performance events', () => {
expect(serverTxnEvent).toMatchObject({
transaction: 'GET /users/[id]',
- tags: { runtime: 'node' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -77,7 +75,6 @@ test.describe('performance events', () => {
expect(clientTxnEvent).toMatchObject({
transaction: '/users',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -90,7 +87,6 @@ test.describe('performance events', () => {
expect(serverTxnEvent).toMatchObject({
transaction: 'GET /users',
- tags: { runtime: 'node' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -130,7 +126,6 @@ test.describe('performance events', () => {
expect(clientTxnEvent).toMatchObject({
transaction: '/universal-load-fetch',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -143,7 +138,6 @@ test.describe('performance events', () => {
expect(serverTxnEvent).toMatchObject({
transaction: 'GET /api/users',
- tags: { runtime: 'node' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -186,11 +180,11 @@ test.describe('performance events', () => {
test('captures a navigation transaction directly after pageload', async ({ page }) => {
const clientPageloadTxnPromise = waitForTransaction('sveltekit-2', txnEvent => {
- return txnEvent?.contexts?.trace?.op === 'pageload' && txnEvent?.tags?.runtime === 'browser';
+ return txnEvent?.contexts?.trace?.op === 'pageload';
});
const clientNavigationTxnPromise = waitForTransaction('sveltekit-2', txnEvent => {
- return txnEvent?.contexts?.trace?.op === 'navigation' && txnEvent?.tags?.runtime === 'browser';
+ return txnEvent?.contexts?.trace?.op === 'navigation';
});
await waitForInitialPageload(page, { route: '/' });
@@ -205,7 +199,6 @@ test.describe('performance events', () => {
expect(pageloadTxnEvent).toMatchObject({
transaction: '/',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -218,7 +211,6 @@ test.describe('performance events', () => {
expect(navigationTxnEvent).toMatchObject({
transaction: '/users/[id]',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -253,27 +245,15 @@ test.describe('performance events', () => {
test('captures one navigation transaction per redirect', async ({ page }) => {
const clientNavigationRedirect1TxnPromise = waitForTransaction('sveltekit-2', txnEvent => {
- return (
- txnEvent?.contexts?.trace?.op === 'navigation' &&
- txnEvent?.tags?.runtime === 'browser' &&
- txnEvent?.transaction === '/redirect1'
- );
+ return txnEvent?.contexts?.trace?.op === 'navigation' && txnEvent?.transaction === '/redirect1';
});
const clientNavigationRedirect2TxnPromise = waitForTransaction('sveltekit-2', txnEvent => {
- return (
- txnEvent?.contexts?.trace?.op === 'navigation' &&
- txnEvent?.tags?.runtime === 'browser' &&
- txnEvent?.transaction === '/redirect2'
- );
+ return txnEvent?.contexts?.trace?.op === 'navigation' && txnEvent?.transaction === '/redirect2';
});
const clientNavigationRedirect3TxnPromise = waitForTransaction('sveltekit-2', txnEvent => {
- return (
- txnEvent?.contexts?.trace?.op === 'navigation' &&
- txnEvent?.tags?.runtime === 'browser' &&
- txnEvent?.transaction === '/users/[id]'
- );
+ return txnEvent?.contexts?.trace?.op === 'navigation' && txnEvent?.transaction === '/users/[id]';
});
await waitForInitialPageload(page, { route: '/' });
@@ -289,7 +269,6 @@ test.describe('performance events', () => {
expect(redirect1TxnEvent).toMatchObject({
transaction: '/redirect1',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -327,7 +306,6 @@ test.describe('performance events', () => {
expect(redirect2TxnEvent).toMatchObject({
transaction: '/redirect2',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -365,7 +343,6 @@ test.describe('performance events', () => {
expect(redirect3TxnEvent).toMatchObject({
transaction: '/users/[id]',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/tests/errors.client.test.ts
index b149496514c4..2676a690a517 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit/tests/errors.client.test.ts
+++ b/dev-packages/e2e-tests/test-applications/sveltekit/tests/errors.client.test.ts
@@ -25,8 +25,6 @@ test.describe('client-side errors', () => {
in_app: true,
}),
);
-
- expect(errorEvent.tags).toMatchObject({ runtime: 'browser' });
});
test('captures universal load error', async ({ page }) => {
@@ -49,7 +47,5 @@ test.describe('client-side errors', () => {
in_app: true,
}),
);
-
- expect(errorEvent.tags).toMatchObject({ runtime: 'browser' });
});
});
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/tests/errors.server.test.ts
index 22b6bb9d340c..fbf8cf6e673a 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit/tests/errors.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/sveltekit/tests/errors.server.test.ts
@@ -20,8 +20,6 @@ test.describe('server-side errors', () => {
}),
);
- expect(errorEvent.tags).toMatchObject({ runtime: 'node' });
-
expect(errorEvent.request).toEqual({
cookies: {},
headers: expect.objectContaining({
@@ -51,8 +49,6 @@ test.describe('server-side errors', () => {
}),
);
- expect(errorEvent.tags).toMatchObject({ runtime: 'node' });
-
expect(errorEvent.request).toEqual({
cookies: {},
headers: expect.objectContaining({
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.server.test.ts
index 42ad638676f7..5c3fd61e5467 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.server.test.ts
@@ -13,7 +13,6 @@ test('server pageload request span has nested request span for sub request', asy
expect(serverTxnEvent).toMatchObject({
transaction: 'GET /server-load-fetch',
- tags: { runtime: 'node' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.test.ts
index 42d4ef82a589..c452e1d48cb3 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.test.ts
+++ b/dev-packages/e2e-tests/test-applications/sveltekit/tests/performance.test.ts
@@ -40,7 +40,6 @@ test('captures a distributed pageload trace', async ({ page }) => {
expect(clientTxnEvent).toMatchObject({
transaction: '/users/[id]',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -53,7 +52,6 @@ test('captures a distributed pageload trace', async ({ page }) => {
expect(serverTxnEvent).toMatchObject({
transaction: 'GET /users/[id]',
- tags: { runtime: 'node' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -92,7 +90,6 @@ test('captures a distributed navigation trace', async ({ page }) => {
expect(clientTxnEvent).toMatchObject({
transaction: '/users/[id]',
- tags: { runtime: 'browser' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
@@ -105,7 +102,6 @@ test('captures a distributed navigation trace', async ({ page }) => {
expect(serverTxnEvent).toMatchObject({
transaction: 'GET /users/[id]',
- tags: { runtime: 'node' },
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
diff --git a/packages/astro/src/client/sdk.ts b/packages/astro/src/client/sdk.ts
index a43c4211f047..e38a552feb39 100644
--- a/packages/astro/src/client/sdk.ts
+++ b/packages/astro/src/client/sdk.ts
@@ -3,7 +3,6 @@ import {
browserTracingIntegration,
getDefaultIntegrations as getBrowserDefaultIntegrations,
init as initBrowserSdk,
- setTag,
} from '@sentry/browser';
import { applySdkMetadata, hasTracingEnabled } from '@sentry/core';
import type { Client, Integration } from '@sentry/types';
@@ -24,11 +23,7 @@ export function init(options: BrowserOptions): Client | undefined {
applySdkMetadata(opts, 'astro', ['astro', 'browser']);
- const client = initBrowserSdk(opts);
-
- setTag('runtime', 'browser');
-
- return client;
+ return initBrowserSdk(opts);
}
function getDefaultIntegrations(options: BrowserOptions): Integration[] | undefined {
diff --git a/packages/astro/src/server/sdk.ts b/packages/astro/src/server/sdk.ts
index cb2cec03f982..884747dcf72a 100644
--- a/packages/astro/src/server/sdk.ts
+++ b/packages/astro/src/server/sdk.ts
@@ -1,6 +1,6 @@
import { applySdkMetadata } from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
-import { init as initNodeSdk, setTag } from '@sentry/node';
+import { init as initNodeSdk } from '@sentry/node';
/**
*
@@ -13,9 +13,5 @@ export function init(options: NodeOptions): NodeClient | undefined {
applySdkMetadata(opts, 'astro', ['astro', 'node']);
- const client = initNodeSdk(opts);
-
- setTag('runtime', 'node');
-
- return client;
+ return initNodeSdk(opts);
}
diff --git a/packages/astro/test/client/sdk.test.ts b/packages/astro/test/client/sdk.test.ts
index 55381f52be17..1ef31131cb77 100644
--- a/packages/astro/test/client/sdk.test.ts
+++ b/packages/astro/test/client/sdk.test.ts
@@ -48,14 +48,6 @@ describe('Sentry client SDK', () => {
);
});
- it('sets the runtime tag on the isolation scope', () => {
- expect(getIsolationScope().getScopeData().tags).toEqual({});
-
- init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' });
-
- expect(getIsolationScope().getScopeData().tags).toEqual({ runtime: 'browser' });
- });
-
describe('automatically adds integrations', () => {
it.each([
['tracesSampleRate', { tracesSampleRate: 0 }],
diff --git a/packages/astro/test/server/sdk.test.ts b/packages/astro/test/server/sdk.test.ts
index b1f9c3854b77..3e571628d29f 100644
--- a/packages/astro/test/server/sdk.test.ts
+++ b/packages/astro/test/server/sdk.test.ts
@@ -39,14 +39,6 @@ describe('Sentry server SDK', () => {
);
});
- it('sets the runtime tag on the isolation scope', () => {
- expect(SentryNode.getIsolationScope().getScopeData().tags).toEqual({});
-
- init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' });
-
- expect(SentryNode.getIsolationScope().getScopeData().tags).toEqual({ runtime: 'node' });
- });
-
it('returns client from init', () => {
expect(init({})).not.toBeUndefined();
});
diff --git a/packages/cloudflare/README.md b/packages/cloudflare/README.md
index 37f0cd94f412..dc0d6de01274 100644
--- a/packages/cloudflare/README.md
+++ b/packages/cloudflare/README.md
@@ -4,7 +4,7 @@
-# Official Sentry SDK for Cloudflare [UNRELEASED]
+# Official Sentry SDK for Cloudflare
[![npm version](https://img.shields.io/npm/v/@sentry/cloudflare.svg)](https://www.npmjs.com/package/@sentry/cloudflare)
[![npm dm](https://img.shields.io/npm/dm/@sentry/cloudflare.svg)](https://www.npmjs.com/package/@sentry/cloudflare)
@@ -18,9 +18,7 @@
**Note: This SDK is unreleased. Please follow the
[tracking GH issue](https://github.com/getsentry/sentry-javascript/issues/12620) for updates.**
-Below details the setup for the Cloudflare Workers. Cloudflare Pages support is in active development.
-
-## Setup (Cloudflare Workers)
+## Install
To get started, first install the `@sentry/cloudflare` package:
@@ -36,6 +34,46 @@ compatibility_flags = ["nodejs_compat"]
# compatibility_flags = ["nodejs_als"]
```
+Then you can either setup up the SDK for [Cloudflare Pages](#setup-cloudflare-pages) or
+[Cloudflare Workers](#setup-cloudflare-workers).
+
+## Setup (Cloudflare Pages)
+
+To use this SDK, add the `sentryPagesPlugin` as
+[middleware to your Cloudflare Pages application](https://developers.cloudflare.com/pages/functions/middleware/).
+
+We recommend adding a `functions/_middleware.js` for the middleware setup so that Sentry is initialized for your entire
+app.
+
+```javascript
+// functions/_middleware.js
+import * as Sentry from '@sentry/cloudflare';
+
+export const onRequest = Sentry.sentryPagesPlugin({
+ dsn: process.env.SENTRY_DSN,
+ // Set tracesSampleRate to 1.0 to capture 100% of spans for tracing.
+ tracesSampleRate: 1.0,
+});
+```
+
+If you need to to chain multiple middlewares, you can do so by exporting an array of middlewares. Make sure the Sentry
+middleware is the first one in the array.
+
+```javascript
+import * as Sentry from '@sentry/cloudflare';
+
+export const onRequest = [
+ // Make sure Sentry is the first middleware
+ Sentry.sentryPagesPlugin({
+ dsn: process.env.SENTRY_DSN,
+ tracesSampleRate: 1.0,
+ }),
+ // Add more middlewares here
+];
+```
+
+## Setup (Cloudflare Workers)
+
To use this SDK, wrap your handler with the `withSentry` function. This will initialize the SDK and hook into the
environment. Note that you can turn off almost all side effects using the respective options.
@@ -58,7 +96,7 @@ export default withSentry(
);
```
-### Sourcemaps (Cloudflare Workers)
+### Sourcemaps
Configure uploading sourcemaps via the Sentry Wizard:
@@ -68,10 +106,11 @@ npx @sentry/wizard@latest -i sourcemaps
See more details in our [docs](https://docs.sentry.io/platforms/javascript/sourcemaps/).
-## Usage (Cloudflare Workers)
+## Usage
To set context information or send manual events, use the exported functions of `@sentry/cloudflare`. Note that these
-functions will require your exported handler to be wrapped in `withSentry`.
+functions will require the usage of the Sentry helpers, either `withSentry` function for Cloudflare Workers or the
+`sentryPagesPlugin` middleware for Cloudflare Pages.
```javascript
import * as Sentry from '@sentry/cloudflare';
diff --git a/packages/cloudflare/src/handler.ts b/packages/cloudflare/src/handler.ts
index 45eca78f9946..65f3cf8bcbf1 100644
--- a/packages/cloudflare/src/handler.ts
+++ b/packages/cloudflare/src/handler.ts
@@ -1,23 +1,7 @@
-import type {
- ExportedHandler,
- ExportedHandlerFetchHandler,
- IncomingRequestCfProperties,
-} from '@cloudflare/workers-types';
-import {
- SEMANTIC_ATTRIBUTE_SENTRY_OP,
- SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
- SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
- captureException,
- continueTrace,
- flush,
- setHttpStatus,
- startSpan,
- withIsolationScope,
-} from '@sentry/core';
-import type { Options, Scope, SpanAttributes } from '@sentry/types';
-import { stripUrlQueryAndFragment, winterCGRequestToRequestData } from '@sentry/utils';
+import type { ExportedHandler, ExportedHandlerFetchHandler } from '@cloudflare/workers-types';
+import type { Options } from '@sentry/types';
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
-import { init } from './sdk';
+import { wrapRequestHandler } from './request';
/**
* Extract environment generic from exported handler.
@@ -47,70 +31,8 @@ export function withSentry>(
handler.fetch = new Proxy(handler.fetch, {
apply(target, thisArg, args: Parameters>>) {
const [request, env, context] = args;
- return withIsolationScope(isolationScope => {
- const options = optionsCallback(env);
- const client = init(options);
- isolationScope.setClient(client);
-
- const attributes: SpanAttributes = {
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.cloudflare-worker',
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
- ['http.request.method']: request.method,
- ['url.full']: request.url,
- };
-
- const contentLength = request.headers.get('content-length');
- if (contentLength) {
- attributes['http.request.body.size'] = parseInt(contentLength, 10);
- }
-
- let pathname = '';
- try {
- const url = new URL(request.url);
- pathname = url.pathname;
- attributes['server.address'] = url.hostname;
- attributes['url.scheme'] = url.protocol.replace(':', '');
- } catch {
- // skip
- }
-
- addRequest(isolationScope, request);
- addCloudResourceContext(isolationScope);
- if (request.cf) {
- addCultureContext(isolationScope, request.cf);
- attributes['network.protocol.name'] = request.cf.httpProtocol;
- }
-
- const routeName = `${request.method} ${pathname ? stripUrlQueryAndFragment(pathname) : '/'}`;
-
- return continueTrace(
- { sentryTrace: request.headers.get('sentry-trace') || '', baggage: request.headers.get('baggage') },
- () => {
- // Note: This span will not have a duration unless I/O happens in the handler. This is
- // because of how the cloudflare workers runtime works.
- // See: https://developers.cloudflare.com/workers/runtime-apis/performance/
- return startSpan(
- {
- name: routeName,
- attributes,
- },
- async span => {
- try {
- const res = await (target.apply(thisArg, args) as ReturnType);
- setHttpStatus(span, res.status);
- return res;
- } catch (e) {
- captureException(e, { mechanism: { handled: false, type: 'cloudflare' } });
- throw e;
- } finally {
- context.waitUntil(flush(2000));
- }
- },
- );
- },
- );
- });
+ const options = optionsCallback(env);
+ return wrapRequestHandler({ options, request, context }, () => target.apply(thisArg, args));
},
});
@@ -120,19 +42,3 @@ export function withSentry>(
return handler;
}
-
-function addCloudResourceContext(isolationScope: Scope): void {
- isolationScope.setContext('cloud_resource', {
- 'cloud.provider': 'cloudflare',
- });
-}
-
-function addCultureContext(isolationScope: Scope, cf: IncomingRequestCfProperties): void {
- isolationScope.setContext('culture', {
- timezone: cf.timezone,
- });
-}
-
-function addRequest(isolationScope: Scope, request: Request): void {
- isolationScope.setSDKProcessingMetadata({ request: winterCGRequestToRequestData(request) });
-}
diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts
index 6ef2b536aef4..3708d3ae9382 100644
--- a/packages/cloudflare/src/index.ts
+++ b/packages/cloudflare/src/index.ts
@@ -85,6 +85,7 @@ export {
} from '@sentry/core';
export { withSentry } from './handler';
+export { sentryPagesPlugin } from './pages-plugin';
export { CloudflareClient } from './client';
export { getDefaultIntegrations } from './sdk';
diff --git a/packages/cloudflare/src/pages-plugin.ts b/packages/cloudflare/src/pages-plugin.ts
new file mode 100644
index 000000000000..7f7070ddfbf7
--- /dev/null
+++ b/packages/cloudflare/src/pages-plugin.ts
@@ -0,0 +1,32 @@
+import { setAsyncLocalStorageAsyncContextStrategy } from './async';
+import type { CloudflareOptions } from './client';
+import { wrapRequestHandler } from './request';
+
+/**
+ * Plugin middleware for Cloudflare Pages.
+ *
+ * Initializes the SDK and wraps cloudflare pages requests with SDK instrumentation.
+ *
+ * @example
+ * ```javascript
+ * // functions/_middleware.js
+ * import * as Sentry from '@sentry/cloudflare';
+ *
+ * export const onRequest = Sentry.sentryPagesPlugin({
+ * dsn: process.env.SENTRY_DSN,
+ * tracesSampleRate: 1.0,
+ * });
+ * ```
+ *
+ * @param _options
+ * @returns
+ */
+export function sentryPagesPlugin<
+ Env = unknown,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ Params extends string = any,
+ Data extends Record = Record,
+>(options: CloudflareOptions): PagesPluginFunction {
+ setAsyncLocalStorageAsyncContextStrategy();
+ return context => wrapRequestHandler({ options, request: context.request, context }, () => context.next());
+}
diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts
new file mode 100644
index 000000000000..b10037ec8bc0
--- /dev/null
+++ b/packages/cloudflare/src/request.ts
@@ -0,0 +1,123 @@
+import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
+
+import {
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+ captureException,
+ continueTrace,
+ flush,
+ setHttpStatus,
+ startSpan,
+ withIsolationScope,
+} from '@sentry/core';
+import type { Scope, SpanAttributes } from '@sentry/types';
+import { stripUrlQueryAndFragment, winterCGRequestToRequestData } from '@sentry/utils';
+import type { CloudflareOptions } from './client';
+import { init } from './sdk';
+
+interface RequestHandlerWrapperOptions {
+ options: CloudflareOptions;
+ request: Request>;
+ context: ExecutionContext;
+}
+
+/**
+ * Wraps a cloudflare request handler in Sentry instrumentation
+ */
+export function wrapRequestHandler(
+ wrapperOptions: RequestHandlerWrapperOptions,
+ handler: (...args: unknown[]) => Response | Promise,
+): Promise {
+ return withIsolationScope(async isolationScope => {
+ const { options, request, context } = wrapperOptions;
+ const client = init(options);
+ isolationScope.setClient(client);
+
+ const attributes: SpanAttributes = {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.cloudflare',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
+ ['http.request.method']: request.method,
+ ['url.full']: request.url,
+ };
+
+ const contentLength = request.headers.get('content-length');
+ if (contentLength) {
+ attributes['http.request.body.size'] = parseInt(contentLength, 10);
+ }
+
+ let pathname = '';
+ try {
+ const url = new URL(request.url);
+ pathname = url.pathname;
+ attributes['server.address'] = url.hostname;
+ attributes['url.scheme'] = url.protocol.replace(':', '');
+ } catch {
+ // skip
+ }
+
+ addCloudResourceContext(isolationScope);
+ if (request) {
+ addRequest(isolationScope, request);
+ if (request.cf) {
+ addCultureContext(isolationScope, request.cf);
+ attributes['network.protocol.name'] = request.cf.httpProtocol;
+ }
+ }
+
+ const routeName = `${request.method} ${pathname ? stripUrlQueryAndFragment(pathname) : '/'}`;
+
+ return continueTrace(
+ { sentryTrace: request.headers.get('sentry-trace') || '', baggage: request.headers.get('baggage') },
+ () => {
+ // Note: This span will not have a duration unless I/O happens in the handler. This is
+ // because of how the cloudflare workers runtime works.
+ // See: https://developers.cloudflare.com/workers/runtime-apis/performance/
+ return startSpan(
+ {
+ name: routeName,
+ attributes,
+ },
+ async span => {
+ try {
+ const res = await handler();
+ setHttpStatus(span, res.status);
+ return res;
+ } catch (e) {
+ captureException(e, { mechanism: { handled: false, type: 'cloudflare' } });
+ throw e;
+ } finally {
+ context.waitUntil(flush(2000));
+ }
+ },
+ );
+ },
+ );
+ });
+}
+
+/**
+ * Set cloud resource context on scope.
+ */
+function addCloudResourceContext(scope: Scope): void {
+ scope.setContext('cloud_resource', {
+ 'cloud.provider': 'cloudflare',
+ });
+}
+
+/**
+ * Set culture context on scope
+ */
+function addCultureContext(scope: Scope, cf: IncomingRequestCfProperties): void {
+ scope.setContext('culture', {
+ timezone: cf.timezone,
+ });
+}
+
+/**
+ * Set request data on scope
+ */
+function addRequest(scope: Scope, request: Request): void {
+ scope.setSDKProcessingMetadata({ request: winterCGRequestToRequestData(request) });
+}
diff --git a/packages/cloudflare/src/sdk.ts b/packages/cloudflare/src/sdk.ts
index edc242656195..ca2035388c12 100644
--- a/packages/cloudflare/src/sdk.ts
+++ b/packages/cloudflare/src/sdk.ts
@@ -17,14 +17,15 @@ import { makeCloudflareTransport } from './transport';
import { defaultStackParser } from './vendor/stacktrace';
/** Get the default integrations for the Cloudflare SDK. */
-export function getDefaultIntegrations(_options: Options): Integration[] {
+export function getDefaultIntegrations(options: Options): Integration[] {
+ const sendDefaultPii = options.sendDefaultPii ?? false;
return [
dedupeIntegration(),
inboundFiltersIntegration(),
functionToStringIntegration(),
linkedErrorsIntegration(),
fetchIntegration(),
- requestDataIntegration(),
+ requestDataIntegration(sendDefaultPii ? undefined : { include: { cookies: false } }),
];
}
diff --git a/packages/cloudflare/test/handler.test.ts b/packages/cloudflare/test/handler.test.ts
index e8358dd63f50..238fbd987c90 100644
--- a/packages/cloudflare/test/handler.test.ts
+++ b/packages/cloudflare/test/handler.test.ts
@@ -3,16 +3,13 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
-import * as SentryCore from '@sentry/core';
-import type { Event } from '@sentry/types';
-import { CloudflareClient } from '../src/client';
import { withSentry } from '../src/handler';
const MOCK_ENV = {
SENTRY_DSN: 'https://public@dsn.ingest.sentry.io/1337',
};
-describe('withSentry', () => {
+describe('sentryPagesPlugin', () => {
beforeEach(() => {
vi.clearAllMocks();
});
@@ -50,249 +47,6 @@ describe('withSentry', () => {
expect(result).toBe(response);
});
-
- test('flushes the event after the handler is done using the cloudflare context.waitUntil', async () => {
- const handler = {
- async fetch(_request, _env, _context) {
- return new Response('test');
- },
- } satisfies ExportedHandler;
-
- const context = createMockExecutionContext();
- const wrappedHandler = withSentry(() => ({}), handler);
- await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, context);
-
- // eslint-disable-next-line @typescript-eslint/unbound-method
- expect(context.waitUntil).toHaveBeenCalledTimes(1);
- // eslint-disable-next-line @typescript-eslint/unbound-method
- expect(context.waitUntil).toHaveBeenLastCalledWith(expect.any(Promise));
- });
-
- test('creates a cloudflare client and sets it on the handler', async () => {
- const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind');
- const handler = {
- async fetch(_request, _env, _context) {
- return new Response('test');
- },
- } satisfies ExportedHandler;
-
- const context = createMockExecutionContext();
- const wrappedHandler = withSentry(() => ({}), handler);
- await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, context);
-
- expect(initAndBindSpy).toHaveBeenCalledTimes(1);
- expect(initAndBindSpy).toHaveBeenLastCalledWith(CloudflareClient, expect.any(Object));
- });
-
- describe('scope instrumentation', () => {
- test('adds cloud resource context', async () => {
- const handler = {
- async fetch(_request, _env, _context) {
- SentryCore.captureMessage('test');
- return new Response('test');
- },
- } satisfies ExportedHandler;
-
- let sentryEvent: Event = {};
- const wrappedHandler = withSentry(
- (env: any) => ({
- dsn: env.MOCK_DSN,
- beforeSend(event) {
- sentryEvent = event;
- return null;
- },
- }),
- handler,
- );
- await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, createMockExecutionContext());
- expect(sentryEvent.contexts?.cloud_resource).toEqual({ 'cloud.provider': 'cloudflare' });
- });
-
- test('adds request information', async () => {
- const handler = {
- async fetch(_request, _env, _context) {
- SentryCore.captureMessage('test');
- return new Response('test');
- },
- } satisfies ExportedHandler;
-
- let sentryEvent: Event = {};
- const wrappedHandler = withSentry(
- (env: any) => ({
- dsn: env.MOCK_DSN,
- beforeSend(event) {
- sentryEvent = event;
- return null;
- },
- }),
- handler,
- );
- await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, createMockExecutionContext());
- expect(sentryEvent.sdkProcessingMetadata?.request).toEqual({
- headers: {},
- url: 'https://example.com/',
- method: 'GET',
- });
- });
-
- test('adds culture context', async () => {
- const handler = {
- async fetch(_request, _env, _context) {
- SentryCore.captureMessage('test');
- return new Response('test');
- },
- } satisfies ExportedHandler;
-
- let sentryEvent: Event = {};
- const wrappedHandler = withSentry(
- (env: any) => ({
- dsn: env.MOCK_DSN,
- beforeSend(event) {
- sentryEvent = event;
- return null;
- },
- }),
- handler,
- );
- const mockRequest = new Request('https://example.com') as any;
- mockRequest.cf = {
- timezone: 'UTC',
- };
- await wrappedHandler.fetch(mockRequest, { ...MOCK_ENV }, createMockExecutionContext());
- expect(sentryEvent.contexts?.culture).toEqual({ timezone: 'UTC' });
- });
- });
-
- describe('error instrumentation', () => {
- test('captures errors thrown by the handler', async () => {
- const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException');
- const error = new Error('test');
- const handler = {
- async fetch(_request, _env, _context) {
- throw error;
- },
- } satisfies ExportedHandler;
-
- const wrappedHandler = withSentry(() => ({}), handler);
- expect(captureExceptionSpy).not.toHaveBeenCalled();
- try {
- await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, createMockExecutionContext());
- } catch {
- // ignore
- }
- expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
- expect(captureExceptionSpy).toHaveBeenLastCalledWith(error, {
- mechanism: { handled: false, type: 'cloudflare' },
- });
- });
-
- test('re-throws the error after capturing', async () => {
- const error = new Error('test');
- const handler = {
- async fetch(_request, _env, _context) {
- throw error;
- },
- } satisfies ExportedHandler;
-
- const wrappedHandler = withSentry(() => ({}), handler);
- let thrownError: Error | undefined;
- try {
- await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, createMockExecutionContext());
- } catch (e: any) {
- thrownError = e;
- }
-
- expect(thrownError).toBe(error);
- });
- });
-
- describe('tracing instrumentation', () => {
- test('continues trace with sentry trace and baggage', async () => {
- const handler = {
- async fetch(_request, _env, _context) {
- SentryCore.captureMessage('test');
- return new Response('test');
- },
- } satisfies ExportedHandler;
-
- let sentryEvent: Event = {};
- const wrappedHandler = withSentry(
- (env: any) => ({
- dsn: env.MOCK_DSN,
- tracesSampleRate: 0,
- beforeSend(event) {
- sentryEvent = event;
- return null;
- },
- }),
- handler,
- );
-
- const request = new Request('https://example.com') as any;
- request.headers.set('sentry-trace', '12312012123120121231201212312012-1121201211212012-1');
- request.headers.set(
- 'baggage',
- 'sentry-release=2.1.12,sentry-public_key=public,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=0.3232',
- );
- await wrappedHandler.fetch(request, MOCK_ENV, createMockExecutionContext());
- expect(sentryEvent.contexts?.trace).toEqual({
- parent_span_id: '1121201211212012',
- span_id: expect.any(String),
- trace_id: '12312012123120121231201212312012',
- });
- });
-
- test('creates a span that wraps fetch handler', async () => {
- const handler = {
- async fetch(_request, _env, _context) {
- return new Response('test');
- },
- } satisfies ExportedHandler;
-
- let sentryEvent: Event = {};
- const wrappedHandler = withSentry(
- (env: any) => ({
- dsn: env.MOCK_DSN,
- tracesSampleRate: 1,
- beforeSendTransaction(event) {
- sentryEvent = event;
- return null;
- },
- }),
- handler,
- );
-
- const request = new Request('https://example.com') as any;
- request.cf = {
- httpProtocol: 'HTTP/1.1',
- };
- request.headers.set('content-length', '10');
-
- await wrappedHandler.fetch(request, MOCK_ENV, createMockExecutionContext());
- expect(sentryEvent.transaction).toEqual('GET /');
- expect(sentryEvent.spans).toHaveLength(0);
- expect(sentryEvent.contexts?.trace).toEqual({
- data: {
- 'sentry.origin': 'auto.http.cloudflare-worker',
- 'sentry.op': 'http.server',
- 'sentry.source': 'url',
- 'http.request.method': 'GET',
- 'url.full': 'https://example.com/',
- 'server.address': 'example.com',
- 'network.protocol.name': 'HTTP/1.1',
- 'url.scheme': 'https',
- 'sentry.sample_rate': 1,
- 'http.response.status_code': 200,
- 'http.request.body.size': 10,
- },
- op: 'http.server',
- origin: 'auto.http.cloudflare-worker',
- span_id: expect.any(String),
- status: 'ok',
- trace_id: expect.any(String),
- });
- });
- });
});
function createMockExecutionContext(): ExecutionContext {
diff --git a/packages/cloudflare/test/pages-plugin.test.ts b/packages/cloudflare/test/pages-plugin.test.ts
new file mode 100644
index 000000000000..6e8b87351f8e
--- /dev/null
+++ b/packages/cloudflare/test/pages-plugin.test.ts
@@ -0,0 +1,36 @@
+// Note: These tests run the handler in Node.js, which has some differences to the cloudflare workers runtime.
+// Although this is not ideal, this is the best we can do until we have a better way to test cloudflare workers.
+
+import { beforeEach, describe, expect, test, vi } from 'vitest';
+import type { CloudflareOptions } from '../src/client';
+
+import { sentryPagesPlugin } from '../src/pages-plugin';
+
+const MOCK_OPTIONS: CloudflareOptions = {
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+};
+
+describe('sentryPagesPlugin', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ test('passes through the response from the handler', async () => {
+ const response = new Response('test');
+ const mockOnRequest = sentryPagesPlugin(MOCK_OPTIONS);
+
+ const result = await mockOnRequest({
+ request: new Request('https://example.com'),
+ functionPath: 'test',
+ waitUntil: vi.fn(),
+ passThroughOnException: vi.fn(),
+ next: () => Promise.resolve(response),
+ env: { ASSETS: { fetch: vi.fn() } },
+ params: {},
+ data: {},
+ pluginArgs: MOCK_OPTIONS,
+ });
+
+ expect(result).toBe(response);
+ });
+});
diff --git a/packages/cloudflare/test/request.test.ts b/packages/cloudflare/test/request.test.ts
new file mode 100644
index 000000000000..93764a292ab4
--- /dev/null
+++ b/packages/cloudflare/test/request.test.ts
@@ -0,0 +1,274 @@
+// Note: These tests run the handler in Node.js, which has some differences to the cloudflare workers runtime.
+// Although this is not ideal, this is the best we can do until we have a better way to test cloudflare workers.
+
+import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
+
+import * as SentryCore from '@sentry/core';
+import type { Event } from '@sentry/types';
+import { setAsyncLocalStorageAsyncContextStrategy } from '../src/async';
+import type { CloudflareOptions } from '../src/client';
+import { CloudflareClient } from '../src/client';
+import { wrapRequestHandler } from '../src/request';
+
+const MOCK_OPTIONS: CloudflareOptions = {
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+};
+
+describe('withSentry', () => {
+ beforeAll(() => {
+ setAsyncLocalStorageAsyncContextStrategy();
+ });
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ test('passes through the response from the handler', async () => {
+ const response = new Response('test');
+ const result = await wrapRequestHandler(
+ { options: MOCK_OPTIONS, request: new Request('https://example.com'), context: createMockExecutionContext() },
+ () => response,
+ );
+ expect(result).toBe(response);
+ });
+
+ test('flushes the event after the handler is done using the cloudflare context.waitUntil', async () => {
+ const context = createMockExecutionContext();
+ await wrapRequestHandler(
+ { options: MOCK_OPTIONS, request: new Request('https://example.com'), context },
+ () => new Response('test'),
+ );
+
+ // eslint-disable-next-line @typescript-eslint/unbound-method
+ expect(context.waitUntil).toHaveBeenCalledTimes(1);
+ // eslint-disable-next-line @typescript-eslint/unbound-method
+ expect(context.waitUntil).toHaveBeenLastCalledWith(expect.any(Promise));
+ });
+
+ test('creates a cloudflare client and sets it on the handler', async () => {
+ const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind');
+ await wrapRequestHandler(
+ { options: MOCK_OPTIONS, request: new Request('https://example.com'), context: createMockExecutionContext() },
+ () => new Response('test'),
+ );
+
+ expect(initAndBindSpy).toHaveBeenCalledTimes(1);
+ expect(initAndBindSpy).toHaveBeenLastCalledWith(CloudflareClient, expect.any(Object));
+ });
+
+ describe('scope instrumentation', () => {
+ test('adds cloud resource context', async () => {
+ let sentryEvent: Event = {};
+ await wrapRequestHandler(
+ {
+ options: {
+ ...MOCK_OPTIONS,
+ beforeSend(event) {
+ sentryEvent = event;
+ return null;
+ },
+ },
+ request: new Request('https://example.com'),
+ context: createMockExecutionContext(),
+ },
+ () => {
+ SentryCore.captureMessage('cloud resource');
+ return new Response('test');
+ },
+ );
+
+ expect(sentryEvent.contexts?.cloud_resource).toEqual({ 'cloud.provider': 'cloudflare' });
+ });
+
+ test('adds request information', async () => {
+ let sentryEvent: Event = {};
+ await wrapRequestHandler(
+ {
+ options: {
+ ...MOCK_OPTIONS,
+ beforeSend(event) {
+ sentryEvent = event;
+ return null;
+ },
+ },
+ request: new Request('https://example.com'),
+ context: createMockExecutionContext(),
+ },
+ () => {
+ SentryCore.captureMessage('request');
+ return new Response('test');
+ },
+ );
+
+ expect(sentryEvent.sdkProcessingMetadata?.request).toEqual({
+ headers: {},
+ url: 'https://example.com/',
+ method: 'GET',
+ });
+ });
+
+ test('adds culture context', async () => {
+ const mockRequest = new Request('https://example.com') as any;
+ mockRequest.cf = {
+ timezone: 'UTC',
+ };
+
+ let sentryEvent: Event = {};
+ await wrapRequestHandler(
+ {
+ options: {
+ ...MOCK_OPTIONS,
+ beforeSend(event) {
+ sentryEvent = event;
+ return null;
+ },
+ },
+ request: mockRequest,
+ context: createMockExecutionContext(),
+ },
+ () => {
+ SentryCore.captureMessage('culture');
+ return new Response('test');
+ },
+ );
+
+ expect(sentryEvent.contexts?.culture).toEqual({ timezone: 'UTC' });
+ });
+ });
+
+ describe('error instrumentation', () => {
+ test('captures errors thrown by the handler', async () => {
+ const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException');
+ const error = new Error('test');
+
+ expect(captureExceptionSpy).not.toHaveBeenCalled();
+
+ try {
+ await wrapRequestHandler(
+ { options: MOCK_OPTIONS, request: new Request('https://example.com'), context: createMockExecutionContext() },
+ () => {
+ throw error;
+ },
+ );
+ } catch {
+ // ignore
+ }
+
+ expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
+ expect(captureExceptionSpy).toHaveBeenLastCalledWith(error, {
+ mechanism: { handled: false, type: 'cloudflare' },
+ });
+ });
+
+ test('re-throws the error after capturing', async () => {
+ const error = new Error('test');
+ let thrownError: Error | undefined;
+ try {
+ await wrapRequestHandler(
+ { options: MOCK_OPTIONS, request: new Request('https://example.com'), context: createMockExecutionContext() },
+ () => {
+ throw error;
+ },
+ );
+ } catch (e: any) {
+ thrownError = e;
+ }
+
+ expect(thrownError).toBe(error);
+ });
+ });
+
+ describe('tracing instrumentation', () => {
+ test('continues trace with sentry trace and baggage', async () => {
+ const mockRequest = new Request('https://example.com') as any;
+ mockRequest.headers.set('sentry-trace', '12312012123120121231201212312012-1121201211212012-1');
+ mockRequest.headers.set(
+ 'baggage',
+ 'sentry-release=2.1.12,sentry-public_key=public,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=0.3232',
+ );
+
+ let sentryEvent: Event = {};
+ await wrapRequestHandler(
+ {
+ options: {
+ ...MOCK_OPTIONS,
+ tracesSampleRate: 0,
+ beforeSend(event) {
+ sentryEvent = event;
+ return null;
+ },
+ },
+ request: mockRequest,
+ context: createMockExecutionContext(),
+ },
+ () => {
+ SentryCore.captureMessage('sentry-trace');
+ return new Response('test');
+ },
+ );
+ expect(sentryEvent.contexts?.trace).toEqual({
+ parent_span_id: '1121201211212012',
+ span_id: expect.any(String),
+ trace_id: '12312012123120121231201212312012',
+ });
+ });
+
+ test('creates a span that wraps request handler', async () => {
+ const mockRequest = new Request('https://example.com') as any;
+ mockRequest.cf = {
+ httpProtocol: 'HTTP/1.1',
+ };
+ mockRequest.headers.set('content-length', '10');
+
+ let sentryEvent: Event = {};
+ await wrapRequestHandler(
+ {
+ options: {
+ ...MOCK_OPTIONS,
+ tracesSampleRate: 1,
+ beforeSendTransaction(event) {
+ sentryEvent = event;
+ return null;
+ },
+ },
+ request: mockRequest,
+ context: createMockExecutionContext(),
+ },
+ () => {
+ SentryCore.captureMessage('sentry-trace');
+ return new Response('test');
+ },
+ );
+
+ expect(sentryEvent.transaction).toEqual('GET /');
+ expect(sentryEvent.spans).toHaveLength(0);
+ expect(sentryEvent.contexts?.trace).toEqual({
+ data: {
+ 'sentry.origin': 'auto.http.cloudflare',
+ 'sentry.op': 'http.server',
+ 'sentry.source': 'url',
+ 'http.request.method': 'GET',
+ 'url.full': 'https://example.com/',
+ 'server.address': 'example.com',
+ 'network.protocol.name': 'HTTP/1.1',
+ 'url.scheme': 'https',
+ 'sentry.sample_rate': 1,
+ 'http.response.status_code': 200,
+ 'http.request.body.size': 10,
+ },
+ op: 'http.server',
+ origin: 'auto.http.cloudflare',
+ span_id: expect.any(String),
+ status: 'ok',
+ trace_id: expect.any(String),
+ });
+ });
+ });
+});
+
+function createMockExecutionContext(): ExecutionContext {
+ return {
+ waitUntil: vi.fn(),
+ passThroughOnException: vi.fn(),
+ };
+}
diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts
index b274b85ec43b..7402d3f374f0 100644
--- a/packages/nestjs/src/setup.ts
+++ b/packages/nestjs/src/setup.ts
@@ -6,6 +6,7 @@ import type {
NestInterceptor,
OnModuleInit,
} from '@nestjs/common';
+import { HttpException } from '@nestjs/common';
import { Catch } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { Global, Module } from '@nestjs/common';
@@ -63,10 +64,8 @@ class SentryGlobalFilter extends BaseExceptionFilter {
* Catches exceptions and reports them to Sentry unless they are expected errors.
*/
public catch(exception: unknown, host: ArgumentsHost): void {
- const status_code = (exception as { status?: number }).status;
-
// don't report expected errors
- if (status_code !== undefined && status_code >= 400 && status_code < 500) {
+ if (exception instanceof HttpException) {
return super.catch(exception, host);
}
diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts
index 49a8fefb22d9..597cc3d4cd91 100644
--- a/packages/nextjs/src/client/index.ts
+++ b/packages/nextjs/src/client/index.ts
@@ -1,4 +1,4 @@
-import { addEventProcessor, applySdkMetadata, hasTracingEnabled, setTag } from '@sentry/core';
+import { addEventProcessor, applySdkMetadata, hasTracingEnabled } from '@sentry/core';
import type { BrowserOptions } from '@sentry/react';
import { getDefaultIntegrations as getReactDefaultIntegrations, init as reactInit } from '@sentry/react';
import type { Client, EventProcessor, Integration } from '@sentry/types';
@@ -34,7 +34,6 @@ export function init(options: BrowserOptions): Client | undefined {
const client = reactInit(opts);
- setTag('runtime', 'browser');
const filterTransactions: EventProcessor = event =>
event.type === 'transaction' && event.transaction === '/404' ? null : event;
filterTransactions.id = 'NextClient404Filter';
diff --git a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts
index 5bc84baf95ce..bf89ce90ac2c 100644
--- a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts
+++ b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts
@@ -18,13 +18,9 @@ export default function valueInjectionLoader(this: LoaderThis, us
// We do not want to cache injected values across builds
this.cacheable(false);
- // Define some global proxy that works on server and on the browser.
- let injectedCode =
- 'var _sentryCollisionFreeGlobalObject = typeof window != "undefined" ? window : typeof global != "undefined" ? global : typeof self != "undefined" ? self : {};\n';
-
- Object.entries(values).forEach(([key, value]) => {
- injectedCode += `_sentryCollisionFreeGlobalObject["${key}"] = ${JSON.stringify(value)};\n`;
- });
+ const injectedCode = Object.entries(values)
+ .map(([key, value]) => `globalThis["${key}"] = ${JSON.stringify(value)};`)
+ .join('\n');
return `${injectedCode}\n${userCode}`;
}
diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts
index 1749a3b824d4..169c7cde5bfc 100644
--- a/packages/nextjs/test/clientSdk.test.ts
+++ b/packages/nextjs/test/clientSdk.test.ts
@@ -73,14 +73,6 @@ describe('Client init()', () => {
);
});
- it('sets runtime on scope', () => {
- expect(SentryReact.getIsolationScope().getScopeData().tags).toEqual({});
-
- init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' });
-
- expect(SentryReact.getIsolationScope().getScopeData().tags).toEqual({ runtime: 'browser' });
- });
-
it('adds 404 transaction filter', () => {
init({
dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012',
diff --git a/packages/node/src/integrations/tracing/nest.ts b/packages/node/src/integrations/tracing/nest.ts
index 2ec5fa840387..dbf3c40ab171 100644
--- a/packages/node/src/integrations/tracing/nest.ts
+++ b/packages/node/src/integrations/tracing/nest.ts
@@ -17,11 +17,13 @@ import {
getDefaultIsolationScope,
getIsolationScope,
spanToJSON,
+ startSpan,
startSpanManual,
withActiveSpan,
} from '@sentry/core';
import type { IntegrationFn, Span } from '@sentry/types';
import { addNonEnumerableProperty, logger } from '@sentry/utils';
+import type { Observable } from 'rxjs';
import { generateInstrumentOnce } from '../../otel/instrument';
interface MinimalNestJsExecutionContext {
@@ -66,7 +68,10 @@ export interface InjectableTarget {
name: string;
sentryPatched?: boolean;
prototype: {
- use?: (req: unknown, res: unknown, next: () => void) => void;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ use?: (req: unknown, res: unknown, next: () => void, ...args: any[]) => void;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ canActivate?: (...args: any[]) => boolean | Promise | Observable;
};
}
@@ -152,7 +157,7 @@ export class SentryNestInstrumentation extends InstrumentationBase {
const [req, res, next, ...args] = argsUse;
const prevSpan = getActiveSpan();
- startSpanManual(
+ return startSpanManual(
{
name: target.name,
attributes: {
@@ -167,15 +172,40 @@ export class SentryNestInstrumentation extends InstrumentationBase {
if (prevSpan) {
withActiveSpan(prevSpan, () => {
- Reflect.apply(originalNext, thisArgNext, argsNext);
+ return Reflect.apply(originalNext, thisArgNext, argsNext);
});
} else {
- Reflect.apply(originalNext, thisArgNext, argsNext);
+ return Reflect.apply(originalNext, thisArgNext, argsNext);
}
},
});
- originalUse.apply(thisArgUse, [req, res, nextProxy, args]);
+ return originalUse.apply(thisArgUse, [req, res, nextProxy, args]);
+ },
+ );
+ },
+ });
+ }
+
+ // patch guards
+ if (typeof target.prototype.canActivate === 'function') {
+ // patch only once
+ if (isPatched(target)) {
+ return original(options)(target);
+ }
+
+ target.prototype.canActivate = new Proxy(target.prototype.canActivate, {
+ apply: (originalCanActivate, thisArgCanActivate, argsCanActivate) => {
+ return startSpan(
+ {
+ name: target.name,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs',
+ },
+ },
+ () => {
+ return originalCanActivate.apply(thisArgCanActivate, argsCanActivate);
},
);
},
@@ -262,7 +292,7 @@ export function setupNestErrorHandler(app: MinimalNestJsApp, baseFilter: NestJsE
const status_code = (exception as { status?: number }).status;
// don't report expected errors
- if (status_code !== undefined && status_code >= 400 && status_code < 500) {
+ if (status_code !== undefined) {
return originalCatch.apply(target, [exception, host]);
}
diff --git a/packages/remix/src/index.client.tsx b/packages/remix/src/index.client.tsx
index 711fd3c2d2fc..615287bed17b 100644
--- a/packages/remix/src/index.client.tsx
+++ b/packages/remix/src/index.client.tsx
@@ -1,4 +1,4 @@
-import { applySdkMetadata, setTag } from '@sentry/core';
+import { applySdkMetadata } from '@sentry/core';
import { init as reactInit } from '@sentry/react';
import type { Client } from '@sentry/types';
import { logger } from '@sentry/utils';
@@ -37,9 +37,5 @@ export function init(options: RemixOptions): Client | undefined {
applySdkMetadata(opts, 'remix', ['remix', 'react']);
- const client = reactInit(opts);
-
- setTag('runtime', 'browser');
-
- return client;
+ return reactInit(opts);
}
diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts
index 978a4a5c15d9..7ab6efb15827 100644
--- a/packages/remix/src/index.server.ts
+++ b/packages/remix/src/index.server.ts
@@ -1,11 +1,6 @@
import { applySdkMetadata } from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
-import {
- getDefaultIntegrations as getDefaultNodeIntegrations,
- init as nodeInit,
- isInitialized,
- setTag,
-} from '@sentry/node';
+import { getDefaultIntegrations as getDefaultNodeIntegrations, init as nodeInit, isInitialized } from '@sentry/node';
import type { Integration } from '@sentry/types';
import { logger } from '@sentry/utils';
@@ -194,7 +189,5 @@ export function init(options: RemixOptions): NodeClient | undefined {
instrumentServer(options);
- setTag('runtime', 'node');
-
return client;
}
diff --git a/packages/remix/test/index.client.test.ts b/packages/remix/test/index.client.test.ts
index 6b04a7ccd800..365794e0f213 100644
--- a/packages/remix/test/index.client.test.ts
+++ b/packages/remix/test/index.client.test.ts
@@ -43,12 +43,4 @@ describe('Client init()', () => {
it('returns client from init', () => {
expect(init({})).not.toBeUndefined();
});
-
- it('sets runtime on scope', () => {
- expect(SentryReact.getIsolationScope().getScopeData().tags).toEqual({});
-
- init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' });
-
- expect(SentryReact.getIsolationScope().getScopeData().tags).toEqual({ runtime: 'browser' });
- });
});
diff --git a/packages/remix/test/index.server.test.ts b/packages/remix/test/index.server.test.ts
index 6ee76317a366..842684a4640a 100644
--- a/packages/remix/test/index.server.test.ts
+++ b/packages/remix/test/index.server.test.ts
@@ -48,14 +48,6 @@ describe('Server init()', () => {
expect(nodeInit).toHaveBeenCalledTimes(1);
});
- it('sets runtime on scope', () => {
- expect(SentryNode.getIsolationScope().getScopeData().tags).toEqual({});
-
- init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' });
-
- expect(SentryNode.getIsolationScope().getScopeData().tags).toEqual({ runtime: 'node' });
- });
-
it('returns client from init', () => {
expect(init({})).not.toBeUndefined();
});
diff --git a/packages/remix/test/integration/test/server/instrumentation-legacy/ssr.test.ts b/packages/remix/test/integration/test/server/instrumentation-legacy/ssr.test.ts
index e67258b9e14d..9fafe0a70056 100644
--- a/packages/remix/test/integration/test/server/instrumentation-legacy/ssr.test.ts
+++ b/packages/remix/test/integration/test/server/instrumentation-legacy/ssr.test.ts
@@ -19,12 +19,12 @@ describe('Server Side Rendering', () => {
},
},
},
- tags: useV2
- ? {
- // Testing that the wrapped `handleError` correctly adds tags
- 'remix-test-tag': 'remix-test-value',
- }
- : {},
+ ...(useV2 && {
+ tags: {
+ // Testing that the wrapped `handleError` correctly adds tags
+ 'remix-test-tag': 'remix-test-value',
+ },
+ }),
});
assertSentryEvent(event![2]!, {
diff --git a/packages/remix/test/integration/test/server/instrumentation-otel/ssr.test.ts b/packages/remix/test/integration/test/server/instrumentation-otel/ssr.test.ts
index 587e57abb1c3..f3a5d7e4124f 100644
--- a/packages/remix/test/integration/test/server/instrumentation-otel/ssr.test.ts
+++ b/packages/remix/test/integration/test/server/instrumentation-otel/ssr.test.ts
@@ -20,12 +20,12 @@ describe('Server Side Rendering', () => {
},
},
},
- tags: useV2
- ? {
- // Testing that the wrapped `handleError` correctly adds tags
- 'remix-test-tag': 'remix-test-value',
- }
- : {},
+ ...(useV2 && {
+ tags: {
+ // Testing that the wrapped `handleError` correctly adds tags
+ 'remix-test-tag': 'remix-test-value',
+ },
+ }),
});
assertSentryEvent(event[2], {
diff --git a/packages/replay-internal/src/replay.ts b/packages/replay-internal/src/replay.ts
index a0ef13276e1a..f42d6ef6964a 100644
--- a/packages/replay-internal/src/replay.ts
+++ b/packages/replay-internal/src/replay.ts
@@ -241,6 +241,15 @@ export class ReplayContainer implements ReplayContainerInterface {
return this._options;
}
+ /** A wrapper to conditionally capture exceptions. */
+ public handleException(error: unknown): void {
+ DEBUG_BUILD && logger.error('[Replay]', error);
+
+ if (DEBUG_BUILD && this._options._experiments && this._options._experiments.captureExceptions) {
+ captureException(error);
+ }
+ }
+
/**
* Initializes the plugin based on sampling configuration. Should not be
* called outside of constructor.
@@ -264,7 +273,7 @@ export class ReplayContainer implements ReplayContainerInterface {
if (!this.session) {
// This should not happen, something wrong has occurred
- this._handleException(new Error('Unable to initialize and create session'));
+ this.handleException(new Error('Unable to initialize and create session'));
return;
}
@@ -389,7 +398,7 @@ export class ReplayContainer implements ReplayContainerInterface {
: {}),
});
} catch (err) {
- this._handleException(err);
+ this.handleException(err);
}
}
@@ -408,7 +417,7 @@ export class ReplayContainer implements ReplayContainerInterface {
return true;
} catch (err) {
- this._handleException(err);
+ this.handleException(err);
return false;
}
}
@@ -450,7 +459,7 @@ export class ReplayContainer implements ReplayContainerInterface {
// is started after, it will not have `previousSessionId`
clearSession(this);
} catch (err) {
- this._handleException(err);
+ this.handleException(err);
}
}
@@ -777,15 +786,6 @@ export class ReplayContainer implements ReplayContainerInterface {
this.startRecording();
}
- /** A wrapper to conditionally capture exceptions. */
- private _handleException(error: unknown): void {
- DEBUG_BUILD && logger.error('[Replay]', error);
-
- if (DEBUG_BUILD && this._options._experiments && this._options._experiments.captureExceptions) {
- captureException(error);
- }
- }
-
/**
* Loads (or refreshes) the current session.
*/
@@ -873,7 +873,7 @@ export class ReplayContainer implements ReplayContainerInterface {
this._hasInitializedCoreListeners = true;
}
} catch (err) {
- this._handleException(err);
+ this.handleException(err);
}
this._performanceCleanupCallback = setupPerformanceObserver(this);
@@ -898,7 +898,7 @@ export class ReplayContainer implements ReplayContainerInterface {
this._performanceCleanupCallback();
}
} catch (err) {
- this._handleException(err);
+ this.handleException(err);
}
}
@@ -1161,7 +1161,7 @@ export class ReplayContainer implements ReplayContainerInterface {
timestamp,
});
} catch (err) {
- this._handleException(err);
+ this.handleException(err);
// This means we retried 3 times and all of them failed,
// or we ran into a problem we don't want to retry, like rate limiting.
diff --git a/packages/replay-internal/src/types/performance.ts b/packages/replay-internal/src/types/performance.ts
index 5241c12d847a..6b264a44ee9c 100644
--- a/packages/replay-internal/src/types/performance.ts
+++ b/packages/replay-internal/src/types/performance.ts
@@ -108,9 +108,9 @@ export interface WebVitalData {
*/
rating: 'good' | 'needs-improvement' | 'poor';
/**
- * The recording id of the LCP node. -1 if not found
+ * The recording id of the web vital nodes. -1 if not found
*/
- nodeId?: number;
+ nodeIds?: number[];
}
/**
diff --git a/packages/replay-internal/src/types/replay.ts b/packages/replay-internal/src/types/replay.ts
index 7ebacad9e100..1e510e2bc519 100644
--- a/packages/replay-internal/src/types/replay.ts
+++ b/packages/replay-internal/src/types/replay.ts
@@ -485,6 +485,7 @@ export interface ReplayContainer {
checkAndHandleExpiredSession(): boolean | void;
setInitialState(): void;
getCurrentRoute(): string | undefined;
+ handleException(err: unknown): void;
}
type RequestBody = null | Blob | BufferSource | FormData | URLSearchParams | string;
diff --git a/packages/replay-internal/src/util/addEvent.ts b/packages/replay-internal/src/util/addEvent.ts
index b2a011687428..f397ea0564f6 100644
--- a/packages/replay-internal/src/util/addEvent.ts
+++ b/packages/replay-internal/src/util/addEvent.ts
@@ -79,8 +79,8 @@ async function _addEvent(
return await replay.eventBuffer.addEvent(eventAfterPossibleCallback);
} catch (error) {
const reason = error && error instanceof EventBufferSizeExceededError ? 'addEventSizeExceeded' : 'addEvent';
+ replay.handleException(error);
- DEBUG_BUILD && logger.error(error);
await replay.stop({ reason });
const client = getClient();
diff --git a/packages/replay-internal/src/util/createPerformanceEntries.ts b/packages/replay-internal/src/util/createPerformanceEntries.ts
index 28ccf60280e8..d55c2269d0f4 100644
--- a/packages/replay-internal/src/util/createPerformanceEntries.ts
+++ b/packages/replay-internal/src/util/createPerformanceEntries.ts
@@ -183,7 +183,7 @@ function createResourceEntry(
*/
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;
+ const node = lastEntry && lastEntry.element ? [lastEntry.element] : undefined;
return getWebVital(metric, 'largest-contentful-paint', node);
}
@@ -191,14 +191,18 @@ export function getLargestContentfulPaint(metric: Metric): ReplayPerformanceEntr
* 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]
- ? firstEntry.sources[0].node
- : undefined
- : undefined;
- return getWebVital(metric, 'cumulative-layout-shift', node);
+ const lastEntry = metric.entries[metric.entries.length - 1] as
+ | (PerformanceEntry & { sources?: LayoutShiftAttribution[] })
+ | undefined;
+ const nodes: Node[] = [];
+ if (lastEntry && lastEntry.sources) {
+ for (const source of lastEntry.sources) {
+ if (source.node) {
+ nodes.push(source.node);
+ }
+ }
+ }
+ return getWebVital(metric, 'cumulative-layout-shift', nodes);
}
/**
@@ -206,7 +210,7 @@ export function getCumulativeLayoutShift(metric: Metric): ReplayPerformanceEntry
*/
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;
+ const node = lastEntry && lastEntry.target ? [lastEntry.target] : undefined;
return getWebVital(metric, 'first-input-delay', node);
}
@@ -215,18 +219,14 @@ 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;
+ const node = lastEntry && lastEntry.target ? [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 {
+function getWebVital(metric: Metric, name: string, nodes: Node[] | undefined): ReplayPerformanceEntry {
const value = metric.value;
const rating = metric.rating;
@@ -241,7 +241,7 @@ export function getWebVital(
value,
size: value,
rating,
- nodeId: node ? record.mirror.getId(node) : undefined,
+ nodeIds: nodes ? nodes.map(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 f1f9f71bc85c..d85698d1be1d 100644
--- a/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts
+++ b/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts
@@ -83,13 +83,13 @@ describe('Unit | util | createPerformanceEntries', () => {
name: 'largest-contentful-paint',
start: 1672531205.108299,
end: 1672531205.108299,
- data: { value: 5108.299, rating: 'good', size: 5108.299, nodeId: undefined },
+ data: { value: 5108.299, rating: 'good', size: 5108.299, nodeIds: undefined },
});
});
});
describe('getCumulativeLayoutShift', () => {
- it('works with an CLS metric', async () => {
+ it('works with a CLS metric', async () => {
const metric = {
value: 5108.299,
rating: 'good' as const,
@@ -103,7 +103,7 @@ describe('Unit | util | createPerformanceEntries', () => {
name: 'cumulative-layout-shift',
start: 1672531205.108299,
end: 1672531205.108299,
- data: { value: 5108.299, size: 5108.299, rating: 'good', nodeId: undefined },
+ data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: [] },
});
});
});
@@ -123,7 +123,7 @@ describe('Unit | util | createPerformanceEntries', () => {
name: 'first-input-delay',
start: 1672531205.108299,
end: 1672531205.108299,
- data: { value: 5108.299, size: 5108.299, rating: 'good', nodeId: undefined },
+ data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: undefined },
});
});
});
@@ -143,7 +143,7 @@ describe('Unit | util | createPerformanceEntries', () => {
name: 'interaction-to-next-paint',
start: 1672531205.108299,
end: 1672531205.108299,
- data: { value: 5108.299, size: 5108.299, rating: 'good', nodeId: undefined },
+ data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: undefined },
});
});
});
diff --git a/packages/solidstart/src/client/sdk.ts b/packages/solidstart/src/client/sdk.ts
index 1b5cf2359306..f44a2134ce50 100644
--- a/packages/solidstart/src/client/sdk.ts
+++ b/packages/solidstart/src/client/sdk.ts
@@ -1,4 +1,4 @@
-import { applySdkMetadata, setTag } from '@sentry/core';
+import { applySdkMetadata } from '@sentry/core';
import type { BrowserOptions } from '@sentry/solid';
import { init as initSolidSDK } from '@sentry/solid';
import type { Client } from '@sentry/types';
@@ -13,9 +13,5 @@ export function init(options: BrowserOptions): Client | undefined {
applySdkMetadata(opts, 'solidstart', ['solidstart', 'solid']);
- const client = initSolidSDK(opts);
-
- setTag('runtime', 'browser');
-
- return client;
+ return initSolidSDK(opts);
}
diff --git a/packages/solidstart/src/server/sdk.ts b/packages/solidstart/src/server/sdk.ts
index 86287f79ea75..7329100d9de9 100644
--- a/packages/solidstart/src/server/sdk.ts
+++ b/packages/solidstart/src/server/sdk.ts
@@ -1,4 +1,4 @@
-import { applySdkMetadata, setTag } from '@sentry/core';
+import { applySdkMetadata } from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { init as initNodeSdk } from '@sentry/node';
@@ -12,9 +12,5 @@ export function init(options: NodeOptions): NodeClient | undefined {
applySdkMetadata(opts, 'solidstart', ['solidstart', 'node']);
- const client = initNodeSdk(opts);
-
- setTag('runtime', 'node');
-
- return client;
+ return initNodeSdk(opts);
}
diff --git a/packages/solidstart/test/client/sdk.test.ts b/packages/solidstart/test/client/sdk.test.ts
index 76fa71ade8ec..886bb29b515d 100644
--- a/packages/solidstart/test/client/sdk.test.ts
+++ b/packages/solidstart/test/client/sdk.test.ts
@@ -33,10 +33,4 @@ describe('Initialize Solid Start SDK', () => {
expect(browserInit).toHaveBeenCalledTimes(1);
expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata));
});
-
- it('sets the runtime tag on the isolation scope', () => {
- solidStartInit({ dsn: 'https://public@dsn.ingest.sentry.io/1337' });
-
- expect(SentrySolid.getIsolationScope().getScopeData().tags).toEqual({ runtime: 'browser' });
- });
});
diff --git a/packages/solidstart/test/server/sdk.test.ts b/packages/solidstart/test/server/sdk.test.ts
index ac610ad6dcd4..e658876c0a12 100644
--- a/packages/solidstart/test/server/sdk.test.ts
+++ b/packages/solidstart/test/server/sdk.test.ts
@@ -33,10 +33,4 @@ describe('Initialize Solid Start SDK', () => {
expect(browserInit).toHaveBeenCalledTimes(1);
expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata));
});
-
- it('sets the runtime tag on the isolation scope', () => {
- solidStartInit({ dsn: 'https://public@dsn.ingest.sentry.io/1337' });
-
- expect(SentryNode.getIsolationScope().getScopeData().tags).toEqual({ runtime: 'node' });
- });
});
diff --git a/packages/sveltekit/src/client/sdk.ts b/packages/sveltekit/src/client/sdk.ts
index 65c7ffb8deab..98fd328c7abe 100644
--- a/packages/sveltekit/src/client/sdk.ts
+++ b/packages/sveltekit/src/client/sdk.ts
@@ -1,4 +1,4 @@
-import { applySdkMetadata, hasTracingEnabled, setTag } from '@sentry/core';
+import { applySdkMetadata, hasTracingEnabled } from '@sentry/core';
import type { BrowserOptions } from '@sentry/svelte';
import { getDefaultIntegrations as getDefaultSvelteIntegrations } from '@sentry/svelte';
import { WINDOW, init as initSvelteSdk } from '@sentry/svelte';
@@ -37,8 +37,6 @@ export function init(options: BrowserOptions): Client | undefined {
restoreFetch(actualFetch);
}
- setTag('runtime', 'browser');
-
return client;
}
diff --git a/packages/sveltekit/src/server/sdk.ts b/packages/sveltekit/src/server/sdk.ts
index 889a60c14e57..7f3acbf57fbd 100644
--- a/packages/sveltekit/src/server/sdk.ts
+++ b/packages/sveltekit/src/server/sdk.ts
@@ -1,4 +1,4 @@
-import { applySdkMetadata, setTag } from '@sentry/core';
+import { applySdkMetadata } from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { getDefaultIntegrations as getDefaultNodeIntegrations } from '@sentry/node';
import { init as initNodeSdk } from '@sentry/node';
@@ -17,9 +17,5 @@ export function init(options: NodeOptions): NodeClient | undefined {
applySdkMetadata(opts, 'sveltekit', ['sveltekit', 'node']);
- const client = initNodeSdk(opts);
-
- setTag('runtime', 'node');
-
- return client;
+ return initNodeSdk(opts);
}
diff --git a/packages/sveltekit/src/vite/injectGlobalValues.ts b/packages/sveltekit/src/vite/injectGlobalValues.ts
index d0f6424a338d..8a14a004dd84 100644
--- a/packages/sveltekit/src/vite/injectGlobalValues.ts
+++ b/packages/sveltekit/src/vite/injectGlobalValues.ts
@@ -21,21 +21,9 @@ export function getGlobalValueInjectionCode(globalSentryValues: GlobalSentryValu
return '';
}
- const sentryGlobal = '_global';
-
- const globalCode = `var ${sentryGlobal} =
- typeof window !== 'undefined' ?
- window :
- typeof globalThis !== 'undefined' ?
- globalThis :
- typeof global !== 'undefined' ?
- global :
- typeof self !== 'undefined' ?
- self :
- {};`;
const injectedValuesCode = Object.entries(globalSentryValues)
- .map(([key, value]) => `${sentryGlobal}["${key}"] = ${JSON.stringify(value)};`)
+ .map(([key, value]) => `globalThis["${key}"] = ${JSON.stringify(value)};`)
.join('\n');
- return `${globalCode}\n${injectedValuesCode}\n`;
+ return `${injectedValuesCode}\n`;
}
diff --git a/packages/sveltekit/test/client/sdk.test.ts b/packages/sveltekit/test/client/sdk.test.ts
index 46cab7400d12..cdecffbea3a5 100644
--- a/packages/sveltekit/test/client/sdk.test.ts
+++ b/packages/sveltekit/test/client/sdk.test.ts
@@ -41,14 +41,6 @@ describe('Sentry client SDK', () => {
);
});
- it('sets the runtime tag on the isolation scope', () => {
- expect(getIsolationScope().getScopeData().tags).toEqual({});
-
- init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' });
-
- expect(getIsolationScope().getScopeData().tags).toEqual({ runtime: 'browser' });
- });
-
describe('automatically added integrations', () => {
it.each([
['tracesSampleRate', { tracesSampleRate: 0 }],
diff --git a/packages/sveltekit/test/server/sdk.test.ts b/packages/sveltekit/test/server/sdk.test.ts
index aa7dbc560e1a..4c6c9917c572 100644
--- a/packages/sveltekit/test/server/sdk.test.ts
+++ b/packages/sveltekit/test/server/sdk.test.ts
@@ -41,14 +41,6 @@ describe('Sentry server SDK', () => {
);
});
- it('sets the runtime tag on the isolation scope', () => {
- expect(SentryNode.getIsolationScope().getScopeData().tags).toEqual({});
-
- init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' });
-
- expect(SentryNode.getIsolationScope().getScopeData().tags).toEqual({ runtime: 'node' });
- });
-
it('adds rewriteFramesIntegration by default', () => {
init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
diff --git a/packages/sveltekit/test/vite/injectGlobalValues.test.ts b/packages/sveltekit/test/vite/injectGlobalValues.test.ts
index 999ee497a2cc..67003f73aedc 100644
--- a/packages/sveltekit/test/vite/injectGlobalValues.test.ts
+++ b/packages/sveltekit/test/vite/injectGlobalValues.test.ts
@@ -9,18 +9,8 @@ describe('getGlobalValueInjectionCode', () => {
something: 'else',
__sentry_sveltekit_output_dir: '.svelte-kit/output',
});
- expect(injectionCode).toEqual(`var _global =
- typeof window !== 'undefined' ?
- window :
- typeof globalThis !== 'undefined' ?
- globalThis :
- typeof global !== 'undefined' ?
- global :
- typeof self !== 'undefined' ?
- self :
- {};
-_global["something"] = "else";
-_global["__sentry_sveltekit_output_dir"] = ".svelte-kit/output";
+ expect(injectionCode).toEqual(`globalThis["something"] = "else";
+globalThis["__sentry_sveltekit_output_dir"] = ".svelte-kit/output";
`);
// Check that the code above is in fact valid and works as expected
diff --git a/packages/types/src/options.ts b/packages/types/src/options.ts
index 82123c01a380..5179c1fdb70e 100644
--- a/packages/types/src/options.ts
+++ b/packages/types/src/options.ts
@@ -286,11 +286,15 @@ export interface ClientOptions PromiseLike | ErrorEvent | null;
/**
- * An event-processing callback for spans. This allows a span to be modified before it's sent.
- *
+ * This function can be defined to modify or entirely drop a child span before it's sent.
* Returning `null` will cause this span to be dropped.
+ *
+ * Note that this function is only called for child spans and not for the root span (formerly known as transaction).
+ * If you want to modify or drop the root span, use {@link Options.beforeSendTransaction} instead.
+ *
* @param span The span generated by the SDK.
- * @returns A new span that will be sent | null.
+ *
+ * @returns A new span that will be sent or null if the span should not be sent.
*/
beforeSendSpan?: (span: SpanJSON) => SpanJSON | null;