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;