From aecf26f22dbf65ce2c0caadc4ce71b46266c9f45 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 10 Jan 2024 11:31:04 +0000 Subject: [PATCH 001/184] chore: Enforce formatting of MD files in repository root --- CHANGELOG.md | 878 ++++++++++++++++++++++++++++++------------------ CONTRIBUTING.md | 107 ++++-- MIGRATION.md | 434 +++++++++++++----------- README.md | 16 +- package.json | 4 +- 5 files changed, 870 insertions(+), 569 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bf94ab74c14..93b86edc5a9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,14 +18,16 @@ - ref: Deprecate `deepReadDirSync` (#10016) - ref: Deprecate `lastEventId()` (#10043) -Please take a look at the [Migration docs](./MIGRATION.md) for more details. These methods will be removed in the upcoming [v8 major release](https://github.com/getsentry/sentry-javascript/discussions/9802). +Please take a look at the [Migration docs](./MIGRATION.md) for more details. These methods will be removed in the +upcoming [v8 major release](https://github.com/getsentry/sentry-javascript/discussions/9802). #### Cron Monitoring Support for `cron` and `node-cron` libraries - feat(node): Instrumentation for `cron` library (#9999) - feat(node): Instrumentation for `node-cron` library (#9904) -This release adds instrumentation for the `cron` and `node-cron` libraries. This allows you to monitor your cron jobs with [Sentry cron monitors](https://docs.sentry.io/product/crons/). +This release adds instrumentation for the `cron` and `node-cron` libraries. This allows you to monitor your cron jobs +with [Sentry cron monitors](https://docs.sentry.io/product/crons/). For [`cron`](https://www.npmjs.com/package/cron): @@ -78,7 +80,7 @@ cronWithCheckIn.schedule( - fix(astro): Handle non-utf8 encoded streams in middleware (#9989) - fix(astro): prevent sentry from externalized (#9994) - fix(core): Ensure `withScope` sets current scope correctly with async callbacks (#9974) -- fix(node): ANR fixes and additions (#9998) +- fix(node): ANR fixes and additions (#9998) - fix(node): Anr should not block exit (#10035) - fix(node): Correctly resolve module name (#10001) - fix(node): Handle inspector already open (#10025) @@ -97,7 +99,9 @@ Work in this release contributed by @joshkel. Thank you for your contribution! - **feat: Add server runtime metrics aggregator (#9894)** -The release adds alpha support for [Sentry developer metrics](https://github.com/getsentry/sentry/discussions/58584) in the server runtime SDKs (`@sentry/node`, `@sentry/deno`, `@sentry/nextjs` server-side, etc.). Via the newly introduced APIs, you can now flush metrics directly to Sentry. +The release adds alpha support for [Sentry developer metrics](https://github.com/getsentry/sentry/discussions/58584) in +the server runtime SDKs (`@sentry/node`, `@sentry/deno`, `@sentry/nextjs` server-side, etc.). Via the newly introduced +APIs, you can now flush metrics directly to Sentry. To enable capturing metrics, you first need to add the `metricsAggregator` experiment to your `Sentry.init` call. @@ -128,7 +132,10 @@ Sentry.metrics.set('valuable.ids', 2); - **feat(node): Rework ANR to use worker script via an integration (#9945)** -The [ANR tracking integration for Node](https://docs.sentry.io/platforms/node/configuration/application-not-responding/) has been reworked to use an integration. ANR tracking now requires a minimum Node version of 16 or higher. Previously you had to call `Sentry.enableANRDetection` before running your application, now you can simply add the `Anr` integration to your `Sentry.init` call. +The [ANR tracking integration for Node](https://docs.sentry.io/platforms/node/configuration/application-not-responding/) +has been reworked to use an integration. ANR tracking now requires a minimum Node version of 16 or higher. Previously +you had to call `Sentry.enableANRDetection` before running your application, now you can simply add the `Anr` +integration to your `Sentry.init` call. ```js import * as Sentry from '@sentry/node'; @@ -168,7 +175,8 @@ Sentry.init({ - **feat(core): Deprecate `configureScope` (#9887)** - **feat(core): Deprecate `pushScope` & `popScope` (#9890)** -This release deprecates `configureScope`, `pushScope`, and `popScope`, which will be removed in the upcoming v8 major release. +This release deprecates `configureScope`, `pushScope`, and `popScope`, which will be removed in the upcoming v8 major +release. #### Hapi Integration @@ -181,23 +189,21 @@ const Sentry = require('@sentry/node'); const Hapi = require('@hapi/hapi'); const init = async () => { - const server = Hapi.server({ - // your server configuration ... - }); + const server = Hapi.server({ + // your server configuration ... + }); - Sentry.init({ - dsn: '__DSN__', - tracesSampleRate: 1.0, - integrations: [ - new Sentry.Integrations.Hapi({ server }), - ], - }); + Sentry.init({ + dsn: '__DSN__', + tracesSampleRate: 1.0, + integrations: [new Sentry.Integrations.Hapi({ server })], + }); - server.route({ - // your route configuration ... - }); + server.route({ + // your route configuration ... + }); - await server.start(); + await server.start(); }; ``` @@ -205,7 +211,9 @@ const init = async () => { - **chore(sveltekit): Add SvelteKit 2.0 to peer dependencies (#9861)** -This release adds support for SvelteKit 2.0 in the `@sentry/sveltekit` package. If you're upgrading from SvelteKit 1.x to 2.x and already use the Sentry SvelteKit SDK, no changes apart from upgrading to this (or a newer) version are necessary. +This release adds support for SvelteKit 2.0 in the `@sentry/sveltekit` package. If you're upgrading from SvelteKit 1.x +to 2.x and already use the Sentry SvelteKit SDK, no changes apart from upgrading to this (or a newer) version are +necessary. ### Other Changes @@ -233,16 +241,16 @@ Work in this release contributed by @adam187, and @jghinestrosa. Thank you for y - **feat(browser): Add browser metrics sdk (#9794)** -The release adds alpha support for [Sentry developer metrics](https://github.com/getsentry/sentry/discussions/58584) in the Browser SDKs (`@sentry/browser` and related framework SDKs). Via the newly introduced APIs, you can now flush metrics directly to Sentry. +The release adds alpha support for [Sentry developer metrics](https://github.com/getsentry/sentry/discussions/58584) in +the Browser SDKs (`@sentry/browser` and related framework SDKs). Via the newly introduced APIs, you can now flush +metrics directly to Sentry. To enable capturing metrics, you first need to add the `MetricsAggregator` integration. ```js Sentry.init({ dsn: '__DSN__', - integrations: [ - new Sentry.metrics.MetricsAggregator(), - ], + integrations: [new Sentry.metrics.MetricsAggregator()], }); ``` @@ -266,17 +274,17 @@ In a future release we'll add support for server runtimes (Node, Deno, Bun, Verc - **feat(deno): Optionally instrument `Deno.cron` (#9808)** -This releases add support for instrumenting [Deno cron's](https://deno.com/blog/cron) with [Sentry cron monitors](https://docs.sentry.io/product/crons/). This requires v1.38 of Deno run with the `--unstable` flag and the usage of the `DenoCron` Sentry integration. +This releases add support for instrumenting [Deno cron's](https://deno.com/blog/cron) with +[Sentry cron monitors](https://docs.sentry.io/product/crons/). This requires v1.38 of Deno run with the `--unstable` +flag and the usage of the `DenoCron` Sentry integration. ```ts // Import from the Deno registry -import * as Sentry from "https://deno.land/x/sentry/index.mjs"; +import * as Sentry from 'https://deno.land/x/sentry/index.mjs'; Sentry.init({ dsn: '__DSN__', - integrations: [ - new Sentry.DenoCron(), - ], + integrations: [new Sentry.DenoCron()], }); ``` @@ -325,10 +333,12 @@ Sentry.init({ - **ref(nextjs): Set `automaticVercelMonitors` to be `false` by default (#9697)** From this version onwards the default for the `automaticVercelMonitors` option in the Next.js SDK is set to false. -Previously, if you made use of Vercel Crons the SDK automatically instrumented the relevant routes to create Sentry monitors. -Because this feature will soon be generally available, we are now flipping the default to avoid situations where quota is used unexpectedly. +Previously, if you made use of Vercel Crons the SDK automatically instrumented the relevant routes to create Sentry +monitors. Because this feature will soon be generally available, we are now flipping the default to avoid situations +where quota is used unexpectedly. -If you want to continue using this feature, make sure to set the `automaticVercelMonitors` flag to `true` in your `next.config.js` Sentry settings. +If you want to continue using this feature, make sure to set the `automaticVercelMonitors` flag to `true` in your +`next.config.js` Sentry settings. ### Other Changes @@ -381,7 +391,8 @@ Work in this release contributed by @arya-s. Thank you for your contribution! - fix(node): Improve error handling and shutdown handling for ANR (#9548) - fix(tracing-internal): Fix case when originalURL contain query params (#9531) -Work in this release contributed by @powerfulyang, @LubomirIgonda1, @joshkel, and @alexgleason. Thank you for your contributions! +Work in this release contributed by @powerfulyang, @LubomirIgonda1, @joshkel, and @alexgleason. Thank you for your +contributions! ## 7.81.0 @@ -389,19 +400,21 @@ Work in this release contributed by @powerfulyang, @LubomirIgonda1, @joshkel, an **- feat(nextjs): Add instrumentation utility for server actions (#9553)** -This release adds a utility function `withServerActionInstrumentation` to the `@sentry/nextjs` SDK for instrumenting your Next.js server actions with error and performance monitoring. +This release adds a utility function `withServerActionInstrumentation` to the `@sentry/nextjs` SDK for instrumenting +your Next.js server actions with error and performance monitoring. -You can optionally pass form data and headers to record them, and configure the wrapper to record the Server Action responses: +You can optionally pass form data and headers to record them, and configure the wrapper to record the Server Action +responses: ```tsx -import * as Sentry from "@sentry/nextjs"; -import { headers } from "next/headers"; +import * as Sentry from '@sentry/nextjs'; +import { headers } from 'next/headers'; export default function ServerComponent() { async function myServerAction(formData: FormData) { - "use server"; + 'use server'; return await Sentry.withServerActionInstrumentation( - "myServerAction", // The name you want to associate this Server Action with in Sentry + 'myServerAction', // The name you want to associate this Server Action with in Sentry { formData, // Optionally pass in the form data headers: headers(), // Optionally pass in headers @@ -410,8 +423,8 @@ export default function ServerComponent() { async () => { // ... Your Server Action code - return { name: "John Doe" }; - } + return { name: 'John Doe' }; + }, ); } @@ -465,13 +478,14 @@ Work in this release contributed by @snoozbuster. Thank you for your contributio - **Replay Bundle Size improvements** -We've dramatically decreased the bundle size of our Replay package, reducing the minified & gzipped bundle size by ~20 KB! -This was possible by extensive use of tree shaking and a host of small changes to reduce our footprint: +We've dramatically decreased the bundle size of our Replay package, reducing the minified & gzipped bundle size by ~20 +KB! This was possible by extensive use of tree shaking and a host of small changes to reduce our footprint: - feat(replay): Update rrweb to 2.2.0 (#9414) - ref(replay): Use fflate instead of pako for compression (#9436) -By using [tree shaking](https://docs.sentry.io/platforms/javascript/configuration/tree-shaking/) it is possible to shave up to 10 additional KB off the bundle. +By using [tree shaking](https://docs.sentry.io/platforms/javascript/configuration/tree-shaking/) it is possible to shave +up to 10 additional KB off the bundle. ### Other Changes @@ -513,15 +527,16 @@ By using [tree shaking](https://docs.sentry.io/platforms/javascript/configuratio - **feat(core): Add cron monitor wrapper helper (#9395)** -This release adds `Sentry.withMonitor()`, a wrapping function that wraps a callback with a cron monitor that will automatically report completions and failures: +This release adds `Sentry.withMonitor()`, a wrapping function that wraps a callback with a cron monitor that will +automatically report completions and failures: ```ts -import * as Sentry from "@sentry/node"; +import * as Sentry from '@sentry/node'; // withMonitor() will send checkin when callback is started/finished // works with async and sync callbacks. const result = Sentry.withMonitor( - "dailyEmail", + 'dailyEmail', () => { // withCheckIn return value is same return value here return sendEmail(); @@ -529,12 +544,12 @@ const result = Sentry.withMonitor( // Optional upsert options { schedule: { - type: "crontab", - value: "0 * * * *", + type: 'crontab', + value: '0 * * * *', }, // 🇨🇦🫡 - timezone: "Canada/Eastern", - } + timezone: 'Canada/Eastern', + }, ); ``` @@ -565,9 +580,12 @@ Work in this release contributed by @LubomirIgonda1. Thank you for your contribu - **feat(opentelemetry): Add new `@sentry/opentelemetry` package (#9238)** -This release publishes a new package, `@sentry/opentelemetry`. This is a runtime agnostic replacement for `@sentry/opentelemetry-node` and exports a couple of useful utilities which can be used to use Sentry together with OpenTelemetry. +This release publishes a new package, `@sentry/opentelemetry`. This is a runtime agnostic replacement for +`@sentry/opentelemetry-node` and exports a couple of useful utilities which can be used to use Sentry together with +OpenTelemetry. -You can read more about [@sentry/opentelemetry in the Readme](https://github.com/getsentry/sentry-javascript/tree/develop/packages/opentelemetry). +You can read more about +[@sentry/opentelemetry in the Readme](https://github.com/getsentry/sentry-javascript/tree/develop/packages/opentelemetry). - **feat(replay): Allow to treeshake rrweb features (#9274)** @@ -577,7 +595,8 @@ Starting with this release, you can configure the following build-time flags in - `__RRWEB_EXCLUDE_IFRAME__` - `__RRWEB_EXCLUDE_SHADOW_DOM__` -You can read more about [tree shaking in our docs](https://docs.sentry.io/platforms/javascript/configuration/tree-shaking/). +You can read more about +[tree shaking in our docs](https://docs.sentry.io/platforms/javascript/configuration/tree-shaking/). ### Other Changes @@ -620,22 +639,22 @@ Work in this release contributed by @LubomirIgonda1. Thank you for your contribu - **feat(astro): Add `sentryAstro` integration (#9218)** -This Release introduces the first alpha version of our new SDK for Astro. -At this time, the SDK is considered experimental and things might break and change in future versions. +This Release introduces the first alpha version of our new SDK for Astro. At this time, the SDK is considered +experimental and things might break and change in future versions. The core of the SDK is an Astro integration which you easily add to your Astro config: ```js // astro.config.js -import { defineConfig } from "astro/config"; -import sentry from "@sentry/astro"; +import { defineConfig } from 'astro/config'; +import sentry from '@sentry/astro'; export default defineConfig({ integrations: [ sentry({ - dsn: "__DSN__", + dsn: '__DSN__', sourceMapsUploadOptions: { - project: "astro", + project: 'astro', authToken: process.env.SENTRY_AUTH_TOKEN, }, }), @@ -681,9 +700,14 @@ Work in this release contributed by @aldenquimby. Thank you for your contributio - **feat(replay): Upgrade to rrweb2** -This is fully backwards compatible with prior versions of the Replay SDK. The only breaking change that we will making is to not be masking `aria-label` by default. The reason for this change is to align with our core SDK which also does not mask `aria-label`. This change also enables better support of searching by clicks. +This is fully backwards compatible with prior versions of the Replay SDK. The only breaking change that we will making +is to not be masking `aria-label` by default. The reason for this change is to align with our core SDK which also does +not mask `aria-label`. This change also enables better support of searching by clicks. -Another change that needs to be highlighted is the 13% bundle size increase. This bundle size increase is necessary to bring improved recording performance and improved replay fidelity, especially in regards to web components and iframes. We will be investigating the reduction of the bundle size in [this PR](https://github.com/getsentry/sentry-javascript/issues/8815). +Another change that needs to be highlighted is the 13% bundle size increase. This bundle size increase is necessary to +bring improved recording performance and improved replay fidelity, especially in regards to web components and iframes. +We will be investigating the reduction of the bundle size in +[this PR](https://github.com/getsentry/sentry-javascript/issues/8815). Here are benchmarks comparing the version 1 of rrweb to version 2 @@ -719,20 +743,21 @@ Work in this release contributed by @vlad-zhukov. Thank you for your contributio - **feat(node): App Not Responding with stack traces (#9079)** -This release introduces support for Application Not Responding (ANR) errors for Node.js applications. -These errors are triggered when the Node.js main thread event loop of an application is blocked for more than five seconds. -The Node SDK reports ANR errors as Sentry events and can optionally attach a stacktrace of the blocking code to the ANR event. +This release introduces support for Application Not Responding (ANR) errors for Node.js applications. These errors are +triggered when the Node.js main thread event loop of an application is blocked for more than five seconds. The Node SDK +reports ANR errors as Sentry events and can optionally attach a stacktrace of the blocking code to the ANR event. -To enable ANR detection, import and use the `enableANRDetection` function from the `@sentry/node` package before you run the rest of your application code. -Any event loop blocking before calling `enableANRDetection` will not be detected by the SDK. +To enable ANR detection, import and use the `enableANRDetection` function from the `@sentry/node` package before you run +the rest of your application code. Any event loop blocking before calling `enableANRDetection` will not be detected by +the SDK. Example (ESM): ```ts -import * as Sentry from "@sentry/node"; +import * as Sentry from '@sentry/node'; Sentry.init({ - dsn: "___PUBLIC_DSN___", + dsn: '___PUBLIC_DSN___', tracesSampleRate: 1.0, }); @@ -744,10 +769,10 @@ runApp(); Example (CJS): ```ts -const Sentry = require("@sentry/node"); +const Sentry = require('@sentry/node'); Sentry.init({ - dsn: "___PUBLIC_DSN___", + dsn: '___PUBLIC_DSN___', tracesSampleRate: 1.0, }); @@ -780,13 +805,17 @@ Work in this release contributed by @jorrit. Thank you for your contribution! - **feat: Add Bun SDK (#9029)** -This release contains the beta version of `@sentry/bun`, our SDK for the [Bun JavaScript runtime](https://bun.sh/)! For details on how to use it, please see the [README](./packages/bun/README.md). Any feedback/bug reports are greatly appreciated, please [reach out on GitHub](https://github.com/getsentry/sentry-javascript/discussions/7979). +This release contains the beta version of `@sentry/bun`, our SDK for the [Bun JavaScript runtime](https://bun.sh/)! For +details on how to use it, please see the [README](./packages/bun/README.md). Any feedback/bug reports are greatly +appreciated, please [reach out on GitHub](https://github.com/getsentry/sentry-javascript/discussions/7979). -Note that as of now the Bun runtime does not support global error handlers. This is being actively worked on, see [the tracking issue in Bun's GitHub repo](https://github.com/oven-sh/bun/issues/5091). +Note that as of now the Bun runtime does not support global error handlers. This is being actively worked on, see +[the tracking issue in Bun's GitHub repo](https://github.com/oven-sh/bun/issues/5091). - **feat(remix): Add Remix 2.x release support. (#8940)** -The Sentry Remix SDK now officially supports Remix v2! See [our Remix docs for more details](https://docs.sentry.io/platforms/javascript/guides/remix/). +The Sentry Remix SDK now officially supports Remix v2! See +[our Remix docs for more details](https://docs.sentry.io/platforms/javascript/guides/remix/). ### Other Changes @@ -802,7 +831,8 @@ The Sentry Remix SDK now officially supports Remix v2! See [our Remix docs for m Work in this release contributed by @Dima-Dim, @krist7599555 and @lifeiscontent. Thank you for your contributions! -Special thanks for @isaacharrisholt for helping us implement a Vercel Edge Runtime SDK which we use under the hood for our Next.js SDK. +Special thanks for @isaacharrisholt for helping us implement a Vercel Edge Runtime SDK which we use under the hood for +our Next.js SDK. ## 7.69.0 @@ -812,32 +842,37 @@ Special thanks for @isaacharrisholt for helping us implement a Vercel Edge Runti - feat: Update span performance API names (#8971) - feat(core): Introduce startSpanManual (#8913) -This release introduces a new set of top level APIs for the Performance Monitoring SDKs. These aim to simplify creating spans and reduce the boilerplate needed for performance instrumentation. The three new methods introduced are `Sentry.startSpan`, `Sentry.startInactiveSpan`, and `Sentry.startSpanManual`. These methods are available in the browser and node SDKs. +This release introduces a new set of top level APIs for the Performance Monitoring SDKs. These aim to simplify creating +spans and reduce the boilerplate needed for performance instrumentation. The three new methods introduced are +`Sentry.startSpan`, `Sentry.startInactiveSpan`, and `Sentry.startSpanManual`. These methods are available in the browser +and node SDKs. -`Sentry.startSpan` wraps a callback in a span. The span is automatically finished when the callback returns. This is the recommended way to create spans. +`Sentry.startSpan` wraps a callback in a span. The span is automatically finished when the callback returns. This is the +recommended way to create spans. ```js // Start a span that tracks the duration of expensiveFunction -const result = Sentry.startSpan({ name: "important function" }, () => { +const result = Sentry.startSpan({ name: 'important function' }, () => { return expensiveFunction(); }); // You can also mutate the span wrapping the callback to set data or status -Sentry.startSpan({ name: "important function" }, (span) => { +Sentry.startSpan({ name: 'important function' }, span => { // span is undefined if performance monitoring is turned off or if // the span was not sampled. This is done to reduce overhead. - span?.setData("version", "1.0.0"); + span?.setData('version', '1.0.0'); return expensiveFunction(); }); ``` -If you don't want the span to finish when the callback returns, use `Sentry.startSpanManual` to control when the span is finished. This is useful for event emitters or similar. +If you don't want the span to finish when the callback returns, use `Sentry.startSpanManual` to control when the span is +finished. This is useful for event emitters or similar. ```js // Start a span that tracks the duration of middleware function middleware(_req, res, next) { - return Sentry.startSpanManual({ name: "middleware" }, (span, finish) => { - res.once("finish", () => { + return Sentry.startSpanManual({ name: 'middleware' }, (span, finish) => { + res.once('finish', () => { span?.setHttpStatus(res.status); finish(); }); @@ -846,18 +881,21 @@ function middleware(_req, res, next) { } ``` -`Sentry.startSpan` and `Sentry.startSpanManual` create a span and make it active for the duration of the callback. Any spans created while this active span is running will be added as a child span to it. If you want to create a span without making it active, use `Sentry.startInactiveSpan`. This is useful for creating parallel spans that are not related to each other. +`Sentry.startSpan` and `Sentry.startSpanManual` create a span and make it active for the duration of the callback. Any +spans created while this active span is running will be added as a child span to it. If you want to create a span +without making it active, use `Sentry.startInactiveSpan`. This is useful for creating parallel spans that are not +related to each other. ```js -const span1 = Sentry.startInactiveSpan({ name: "span1" }); +const span1 = Sentry.startInactiveSpan({ name: 'span1' }); someWork(); -const span2 = Sentry.startInactiveSpan({ name: "span2" }); +const span2 = Sentry.startInactiveSpan({ name: 'span2' }); moreWork(); -const span3 = Sentry.startInactiveSpan({ name: "span3" }); +const span3 = Sentry.startInactiveSpan({ name: 'span3' }); evenMoreWork(); @@ -914,13 +952,13 @@ Work in this release contributed by @Duncanxyz and @malay44. Thank you for your - feat(serverless): Mark errors caught in Serverless handlers as unhandled (#8907) - feat(vue): Mark errors caught by Vue wrappers as unhandled (#8905) -This release fixes inconsistent behaviour of when our SDKs classify captured errors as unhandled. -Previously, some of our instrumentations correctly set unhandled, while others set handled. -Going forward, all errors caught automatically from our SDKs will be marked as unhandled. -If you manually capture errors (e.g. by calling `Sentry.captureException`), your errors will continue to be reported as handled. +This release fixes inconsistent behaviour of when our SDKs classify captured errors as unhandled. Previously, some of +our instrumentations correctly set unhandled, while others set handled. Going forward, all errors caught automatically +from our SDKs will be marked as unhandled. If you manually capture errors (e.g. by calling `Sentry.captureException`), +your errors will continue to be reported as handled. -This change might lead to a decrease in reported crash-free sessions and consequently in your release health score. -If you have concerns about this, feel free to open an issue. +This change might lead to a decrease in reported crash-free sessions and consequently in your release health score. If +you have concerns about this, feel free to open an issue. ### Other Changes @@ -978,7 +1016,8 @@ Work in this release contributed by @SorsOps. Thank you for your contribution! - feat(tracing): Add db connection attributes for mysql spans (#8775) - feat(tracing): Add db connection attributes for postgres spans (#8778) - feat(tracing): Improve data collection for mongodb spans (#8774) -- fix(nextjs): Execute sentry config independently of `autoInstrumentServerFunctions` and `autoInstrumentAppDirectory` (#8781) +- fix(nextjs): Execute sentry config independently of `autoInstrumentServerFunctions` and `autoInstrumentAppDirectory` + (#8781) - fix(replay): Ensure we do not flush if flush took too long (#8784) - fix(replay): Ensure we do not try to flush when we force stop replay (#8783) - fix(replay): Fix `hasCheckout` handling (#8782) @@ -991,15 +1030,18 @@ Work in this release contributed by @SorsOps. Thank you for your contribution! - **feat(integrations): Add `ContextLines` integration for html-embedded JS stack frames (#8699)** -This release adds the `ContextLines` integration as an optional integration for the Browser SDKs to `@sentry/integrations`. +This release adds the `ContextLines` integration as an optional integration for the Browser SDKs to +`@sentry/integrations`. -This integration adds source code from inline JavaScript of the current page's HTML (e.g. JS in ` + diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/test.ts b/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/test.ts new file mode 100644 index 000000000000..583ff672e590 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/test.ts @@ -0,0 +1,79 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getReplayRecordingContent, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers'; + +sentryTest('can manually snapshot canvas', async ({ getLocalTestUrl, page, browserName }) => { + if (shouldSkipReplayTest() || browserName === 'webkit' || (process.env.PW_BUNDLE || '').startsWith('bundle')) { + sentryTest.skip(); + } + + const reqPromise0 = waitForReplayRequest(page, 0); + const reqPromise1 = waitForReplayRequest(page, 1); + const reqPromise2 = waitForReplayRequest(page, 2); + const reqPromise3 = waitForReplayRequest(page, 3); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + await reqPromise0; + await Promise.all([page.click('#draw'), reqPromise1]); + + const { incrementalSnapshots } = getReplayRecordingContent(await reqPromise2); + expect(incrementalSnapshots).toEqual([]); + + await page.evaluate(() => { + (window as any).Sentry.getClient().getIntegrationById('ReplayCanvas').snapshot(); + }); + + const { incrementalSnapshots: incrementalSnapshotsManual } = getReplayRecordingContent(await reqPromise3); + expect(incrementalSnapshotsManual).toEqual( + expect.arrayContaining([ + { + data: { + commands: [ + { + args: [0, 0, 150, 150], + property: 'clearRect', + }, + { + args: [ + { + args: [ + { + data: [ + { + base64: expect.any(String), + rr_type: 'ArrayBuffer', + }, + ], + rr_type: 'Blob', + type: 'image/webp', + }, + ], + rr_type: 'ImageBitmap', + }, + 0, + 0, + ], + property: 'drawImage', + }, + ], + id: 9, + source: 9, + type: 0, + }, + timestamp: 0, + type: 3, + }, + ]), + ); +}); diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index 81a70f18c20d..0a495945f2d1 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -53,7 +53,7 @@ "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { "@babel/core": "^7.17.5", - "@sentry-internal/rrweb": "2.8.0" + "@sentry-internal/rrweb": "2.9.0" }, "dependencies": { "@sentry/core": "7.93.0", diff --git a/packages/replay-canvas/src/canvas.ts b/packages/replay-canvas/src/canvas.ts index 90b65e5ccd35..ba3ec85ebcfb 100644 --- a/packages/replay-canvas/src/canvas.ts +++ b/packages/replay-canvas/src/canvas.ts @@ -4,11 +4,13 @@ import type { CanvasManagerInterface, CanvasManagerOptions } from '@sentry/repla import type { Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; interface ReplayCanvasOptions { + enableManualSnapshot?: boolean; quality: 'low' | 'medium' | 'high'; } type GetCanvasManager = (options: CanvasManagerOptions) => CanvasManagerInterface; export interface ReplayCanvasIntegrationOptions { + enableManualSnapshot?: boolean; recordCanvas: true; getCanvasManager: GetCanvasManager; sampling: { @@ -58,21 +60,34 @@ const INTEGRATION_NAME = 'ReplayCanvas'; const replayCanvasIntegration = ((options: Partial = {}) => { const _canvasOptions = { quality: options.quality || 'medium', + enableManualSnapshot: options.enableManualSnapshot, }; + let canvasManagerResolve: (value: CanvasManager) => void; + const _canvasManager: Promise = new Promise(resolve => (canvasManagerResolve = resolve)); + return { name: INTEGRATION_NAME, // eslint-disable-next-line @typescript-eslint/no-empty-function setupOnce() {}, getOptions(): ReplayCanvasIntegrationOptions { - const { quality } = _canvasOptions; + const { quality, enableManualSnapshot } = _canvasOptions; return { + enableManualSnapshot, recordCanvas: true, - getCanvasManager: (options: CanvasManagerOptions) => new CanvasManager(options), + getCanvasManager: (options: CanvasManagerOptions) => { + const manager = new CanvasManager({ ...options, enableManualSnapshot }); + canvasManagerResolve(manager); + return manager; + }, ...(CANVAS_QUALITY[quality || 'medium'] || CANVAS_QUALITY.medium), }; }, + async snapshot(canvasElement?: HTMLCanvasElement) { + const canvasManager = await _canvasManager; + canvasManager.snapshot(canvasElement); + }, }; }) satisfies IntegrationFn; diff --git a/packages/replay-canvas/test/canvas.test.ts b/packages/replay-canvas/test/canvas.test.ts index e5464ba544ec..346df7b4a89d 100644 --- a/packages/replay-canvas/test/canvas.test.ts +++ b/packages/replay-canvas/test/canvas.test.ts @@ -16,10 +16,11 @@ it('initializes with default options', () => { }); }); -it('initializes with quality option', () => { - const rc = new ReplayCanvas({ quality: 'low' }); +it('initializes with quality option and manual snapshot', () => { + const rc = new ReplayCanvas({ enableManualSnapshot: true, quality: 'low' }); expect(rc.getOptions()).toEqual({ + enableManualSnapshot: true, recordCanvas: true, getCanvasManager: expect.any(Function), sampling: { diff --git a/packages/replay/package.json b/packages/replay/package.json index 936b861a7f17..0b44a611a270 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -54,8 +54,8 @@ "devDependencies": { "@babel/core": "^7.17.5", "@sentry-internal/replay-worker": "7.93.0", - "@sentry-internal/rrweb": "2.8.0", - "@sentry-internal/rrweb-snapshot": "2.8.0", + "@sentry-internal/rrweb": "2.9.0", + "@sentry-internal/rrweb-snapshot": "2.9.0", "fflate": "^0.8.1", "jsdom-worker": "^0.2.1" }, diff --git a/packages/replay/src/types/replay.ts b/packages/replay/src/types/replay.ts index 93fd60a868f3..61325e4a9959 100644 --- a/packages/replay/src/types/replay.ts +++ b/packages/replay/src/types/replay.ts @@ -538,6 +538,7 @@ export interface SlowClickConfig { } export interface ReplayCanvasIntegrationOptions { + enableManualSnapshot?: boolean; recordCanvas: true; getCanvasManager: (options: CanvasManagerOptions) => CanvasManagerInterface; sampling: { diff --git a/packages/replay/src/types/rrweb.ts b/packages/replay/src/types/rrweb.ts index 7cbea7af07eb..a490a6e46c1b 100644 --- a/packages/replay/src/types/rrweb.ts +++ b/packages/replay/src/types/rrweb.ts @@ -56,6 +56,7 @@ export interface CanvasManagerInterface { export interface CanvasManagerOptions { recordCanvas: boolean; + enableManualSnapshot?: boolean; blockClass: string | RegExp; blockSelector: string | null; unblockSelector: string | null; From ba9999f0a0e87105a8c3504238145e9e6d4b1c64 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 17 Jan 2024 14:37:43 -0500 Subject: [PATCH 061/184] ref(vue): Convert Vue integration to use functional approach (#10218) Wanted to test out integration changes, so decided try it with Vue integration. --- packages/vue/src/index.ts | 2 +- packages/vue/src/integration.ts | 82 +++++++++---------- .../test/integration/VueIntegration.test.ts | 5 +- packages/vue/test/integration/init.test.ts | 9 -- 4 files changed, 42 insertions(+), 56 deletions(-) diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 6afcc0f60ae8..d89359530043 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -4,4 +4,4 @@ export { init } from './sdk'; export { vueRouterInstrumentation } from './router'; export { attachErrorHandler } from './errorhandler'; export { createTracingMixins } from './tracing'; -export { VueIntegration } from './integration'; +export { vueIntegration, VueIntegration } from './integration'; diff --git a/packages/vue/src/integration.ts b/packages/vue/src/integration.ts index f6477dfb8999..5065e1486400 100644 --- a/packages/vue/src/integration.ts +++ b/packages/vue/src/integration.ts @@ -1,5 +1,5 @@ -import { hasTracingEnabled } from '@sentry/core'; -import type { Client, Hub, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass, defineIntegration, hasTracingEnabled } from '@sentry/core'; +import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import { GLOBAL_OBJ, arrayify, consoleSandbox } from '@sentry/utils'; import { DEFAULT_HOOKS } from './constants'; @@ -18,55 +18,49 @@ const DEFAULT_CONFIG: VueOptions = { trackComponents: false, }; -/** - * Initialize Vue error & performance tracking. - */ -export class VueIntegration implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Vue'; - - /** - * @inheritDoc - */ - public name: string; - - private readonly _options: Partial; +const INTEGRATION_NAME = 'Vue'; - public constructor(options: Partial = {}) { - this.name = VueIntegration.id; - this._options = options; - } +const _vueIntegration = ((integrationOptions: Partial = {}) => { + return { + name: INTEGRATION_NAME, + // TODO v8: Remove this + setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function + setup(client) { + _setupIntegration(client, integrationOptions); + }, + }; +}) satisfies IntegrationFn; - /** @inheritDoc */ - public setupOnce(_addGlobalEventProcessor: unknown, getCurrentHub: () => Hub): void { - // eslint-disable-next-line deprecation/deprecation - this._setupIntegration(getCurrentHub().getClient()); - } +export const vueIntegration = defineIntegration(_vueIntegration); - /** Just here for easier testing */ - protected _setupIntegration(client: Client | undefined): void { - const options: Options = { ...DEFAULT_CONFIG, ...(client && client.getOptions()), ...this._options }; +/** + * Initialize Vue error & performance tracking. + */ +// eslint-disable-next-line deprecation/deprecation +export const VueIntegration = convertIntegrationFnToClass( + INTEGRATION_NAME, + vueIntegration, +) as IntegrationClass; - if (!options.Vue && !options.app) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - `[@sentry/vue]: Misconfigured SDK. Vue specific errors will not be captured. +function _setupIntegration(client: Client, integrationOptions: Partial): void { + const options: Options = { ...DEFAULT_CONFIG, ...client.getOptions(), ...integrationOptions }; + if (!options.Vue && !options.app) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + `[@sentry/vue]: Misconfigured SDK. Vue specific errors will not be captured. Update your \`Sentry.init\` call with an appropriate config option: \`app\` (Application Instance - Vue 3) or \`Vue\` (Vue Constructor - Vue 2).`, - ); - }); - return; - } + ); + }); + return; + } - if (options.app) { - const apps = arrayify(options.app); - apps.forEach(app => vueInit(app, options)); - } else if (options.Vue) { - vueInit(options.Vue, options); - } + if (options.app) { + const apps = arrayify(options.app); + apps.forEach(app => vueInit(app, options)); + } else if (options.Vue) { + vueInit(options.Vue, options); } } diff --git a/packages/vue/test/integration/VueIntegration.test.ts b/packages/vue/test/integration/VueIntegration.test.ts index 335133296e06..08af038676d0 100644 --- a/packages/vue/test/integration/VueIntegration.test.ts +++ b/packages/vue/test/integration/VueIntegration.test.ts @@ -1,3 +1,4 @@ +import type { Client } from '@sentry/types'; import { logger } from '@sentry/utils'; import { createApp } from 'vue'; @@ -36,7 +37,7 @@ describe('Sentry.VueIntegration', () => { // This would normally happen through client.addIntegration() const integration = new Sentry.VueIntegration({ app }); - integration['_setupIntegration'](Sentry.getClient()); + integration['setup']?.(Sentry.getClient() as Client); app.mount(el); @@ -58,7 +59,7 @@ describe('Sentry.VueIntegration', () => { // This would normally happen through client.addIntegration() const integration = new Sentry.VueIntegration({ app }); - integration['_setupIntegration'](Sentry.getClient()); + integration['setup']?.(Sentry.getClient() as Client); expect(warnings).toEqual([ '[@sentry/vue]: Misconfigured SDK. Vue app is already mounted. Make sure to call `app.mount()` after `Sentry.init()`.', diff --git a/packages/vue/test/integration/init.test.ts b/packages/vue/test/integration/init.test.ts index 557f3fc694e2..6a117427e2c8 100644 --- a/packages/vue/test/integration/init.test.ts +++ b/packages/vue/test/integration/init.test.ts @@ -104,8 +104,6 @@ Update your \`Sentry.init\` call with an appropriate config option: }); function runInit(options: Partial): void { - const hasRunBefore = Sentry.getClient()?.getIntegrationByName?.(VueIntegration.id); - const integration = new VueIntegration(); Sentry.init({ @@ -114,11 +112,4 @@ function runInit(options: Partial): void { integrations: [integration], ...options, }); - - // Because our integrations API is terrible to test, we need to make sure to check - // If we've already had this integration registered before - // if that's the case, `setup()` will not be run, so we need to manually run it :( - if (hasRunBefore) { - integration['_setupIntegration'](Sentry.getClient()); - } } From b13a82eb903b5cccaa04670936598bc5bbe13648 Mon Sep 17 00:00:00 2001 From: Catherine Lee <55311782+c298lee@users.noreply.github.com> Date: Wed, 17 Jan 2024 15:31:36 -0500 Subject: [PATCH 062/184] ref(replay): Add yarn file to upgrade rrweb to 2.9.0 (#10228) --- yarn.lock | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/yarn.lock b/yarn.lock index 67dd512c89c4..c02f5da0ff3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5231,33 +5231,33 @@ semver "7.3.2" semver-intersect "1.4.0" -"@sentry-internal/rrdom@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.8.0.tgz#092c6201b71474f650fdda0320a9a82319c0f460" - integrity sha512-2jPoYMwRDwMjpYPnQ0/BTjpVvemABrQpH2zWuBYsACCLcAeCenNNQ7Qrqciv+hBnpUwBpTD1sYDqvSx2bP0C+w== +"@sentry-internal/rrdom@2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.9.0.tgz#dbb30c00a859156e9bfdfe701af85477fa082cbf" + integrity sha512-8jULvAmXunPfNChUCOhKSr4rRg7govoH7L/8XuRsK4++wJryjOJDO/zMnway5c3u03PKbFcZFcqCyKjaQQKcHg== dependencies: - "@sentry-internal/rrweb-snapshot" "2.8.0" + "@sentry-internal/rrweb-snapshot" "2.9.0" -"@sentry-internal/rrweb-snapshot@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.8.0.tgz#4db7c10125d53fb20b4ff2a2a61a63b3f3ccaac1" - integrity sha512-JTpwdT6sVS4X3zr+qBs6/SsCR2JMLGHMDz4pN48HHDuBmW0QqiXDrM4SD9Tl7vUahHBCO0hG+E/UGFUbCV2b3w== +"@sentry-internal/rrweb-snapshot@2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.9.0.tgz#f7b682992e70174547c495a4a6deae39136cecf2" + integrity sha512-oK8L3g41PFli1MpItYIFYCisCB+XjpqbEup0lVyTa/6wvKe0SOxZK9aUb/y03/2onSMmQ+FRkKLL6Kd0gHYJOA== -"@sentry-internal/rrweb-types@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.8.0.tgz#ca1da1847630c4457a7b838d79f58b2b89b2d740" - integrity sha512-K66baWV0srzWl+ioUPf1FRGEp7gNOl8PqVaabmQfXc7RmA5t2215+63f1f3dMnEwMSJF0WmojA7orFcSy8sx0g== +"@sentry-internal/rrweb-types@2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.9.0.tgz#a70450ab7ca9884fd8d70bdb45dc214ed554956e" + integrity sha512-s3YhCvXzMM7byAfjHyCWmSOUBDbzUpWHWZj7FR6G8xa3nIrIePceziMc9wxEdqi7nCcmDHPc+kZ2GzDaZIrebA== dependencies: - "@sentry-internal/rrweb-snapshot" "2.8.0" + "@sentry-internal/rrweb-snapshot" "2.9.0" -"@sentry-internal/rrweb@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.8.0.tgz#d1492bdabff2a87001dd7d4514038d40c142267f" - integrity sha512-vKqGk7epbi8a4wn5yWMJCe+m2tbYDHT7v258o/moj0YdlBXMxHjIBJF/m1AlqreQlCP7AESyFnYFX2OTdKyygQ== +"@sentry-internal/rrweb@2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.9.0.tgz#a41af914baaf69c7a1e76d22d1780d50c3dfed0e" + integrity sha512-fDPYXWHOwt/PZzOklS17xPsjMsZ6D0K7CX3tvaDE4IkHCHM1PmGJhrXo05NL86WHhRKNKeRT3WQaokrrZzU5zA== dependencies: - "@sentry-internal/rrdom" "2.8.0" - "@sentry-internal/rrweb-snapshot" "2.8.0" - "@sentry-internal/rrweb-types" "2.8.0" + "@sentry-internal/rrdom" "2.9.0" + "@sentry-internal/rrweb-snapshot" "2.9.0" + "@sentry-internal/rrweb-types" "2.9.0" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" From df84b36b5ccb9051f4e5632ec01984ecd32a1026 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Wed, 17 Jan 2024 16:12:41 -0500 Subject: [PATCH 063/184] add tests --- .../test/browser/metrics/index.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/tracing-internal/test/browser/metrics/index.test.ts index f24b6ce4b45a..213d3188de24 100644 --- a/packages/tracing-internal/test/browser/metrics/index.test.ts +++ b/packages/tracing-internal/test/browser/metrics/index.test.ts @@ -1,6 +1,23 @@ +import { GLOBAL_OBJ } from '@sentry/utils'; import { Transaction } from '../../../src'; import type { ResourceEntry } from '../../../src/browser/metrics'; import { _addMeasureSpans, _addResourceSpans } from '../../../src/browser/metrics'; +import { WINDOW } from '../../../src/browser/types'; + +const mockWindowLocation = { + ancestorOrigins: {}, + href: "https://github.com/getsentry/sentry-javascript/pull/10205/files", + origin: "https://github.com", + protocol: "https:", + host: "github.com", + hostname: "github.com", + port: "", + pathname: "/getsentry/sentry-javascript/pull/10205/files", + search: "", + hash: "" +} as Window['location']; + +WINDOW.location = mockWindowLocation; describe('_addMeasureSpans', () => { // eslint-disable-next-line deprecation/deprecation @@ -100,6 +117,9 @@ describe('_addResourceSpans', () => { ['http.response_content_length']: entry.encodedBodySize, ['http.response_transfer_size']: entry.transferSize, ['resource.render_blocking_status']: entry.renderBlockingStatus, + ['url.scheme']: WINDOW.location.protocol, + ['server.address']: WINDOW.location.host, + ['url.same_origin']: false, }, description: '/assets/to/css', endTimestamp: timeOrigin + startTime + duration, From f7c4d1355f58b950d42d98c8e1e7f1617fb18363 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Wed, 17 Jan 2024 16:18:44 -0500 Subject: [PATCH 064/184] fix tests --- .../test/browser/metrics/index.test.ts | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/tracing-internal/test/browser/metrics/index.test.ts index 213d3188de24..78dc4f52ad43 100644 --- a/packages/tracing-internal/test/browser/metrics/index.test.ts +++ b/packages/tracing-internal/test/browser/metrics/index.test.ts @@ -1,4 +1,4 @@ -import { GLOBAL_OBJ } from '@sentry/utils'; +import { GLOBAL_OBJ, parseUrl } from '@sentry/utils'; import { Transaction } from '../../../src'; import type { ResourceEntry } from '../../../src/browser/metrics'; import { _addMeasureSpans, _addResourceSpans } from '../../../src/browser/metrics'; @@ -6,19 +6,21 @@ import { WINDOW } from '../../../src/browser/types'; const mockWindowLocation = { ancestorOrigins: {}, - href: "https://github.com/getsentry/sentry-javascript/pull/10205/files", - origin: "https://github.com", + href: "https://example.com/path/to/something", + origin: "https://example.com", protocol: "https:", - host: "github.com", - hostname: "github.com", + host: "example.com", + hostname: "example.com", port: "", - pathname: "/getsentry/sentry-javascript/pull/10205/files", + pathname: "/path/to/something", search: "", hash: "" } as Window['location']; WINDOW.location = mockWindowLocation; +const resourceEntryName = 'https://example.com/assets/to/css'; + describe('_addMeasureSpans', () => { // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ op: 'pageload', name: '/' }); @@ -73,7 +75,7 @@ describe('_addResourceSpans', () => { decodedBodySize: 256, renderBlockingStatus: 'non-blocking', }; - _addResourceSpans(transaction, entry, '/assets/to/me', 123, 456, 100); + _addResourceSpans(transaction, entry, resourceEntryName, 123, 456, 100); // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(0); @@ -87,7 +89,7 @@ describe('_addResourceSpans', () => { decodedBodySize: 256, renderBlockingStatus: 'non-blocking', }; - _addResourceSpans(transaction, entry, '/assets/to/me', 123, 456, 100); + _addResourceSpans(transaction, entry, 'https://example.com/assets/to/me', 123, 456, 100); // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(0); @@ -106,7 +108,7 @@ describe('_addResourceSpans', () => { const startTime = 23; const duration = 356; - _addResourceSpans(transaction, entry, '/assets/to/css', startTime, duration, timeOrigin); + _addResourceSpans(transaction, entry, resourceEntryName, startTime, duration, timeOrigin); // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); @@ -117,9 +119,9 @@ describe('_addResourceSpans', () => { ['http.response_content_length']: entry.encodedBodySize, ['http.response_transfer_size']: entry.transferSize, ['resource.render_blocking_status']: entry.renderBlockingStatus, - ['url.scheme']: WINDOW.location.protocol, - ['server.address']: WINDOW.location.host, - ['url.same_origin']: false, + ['url.scheme']: 'https:', + ['server.address']: 'example.com', + ['url.same_origin']: true, }, description: '/assets/to/css', endTimestamp: timeOrigin + startTime + duration, @@ -157,7 +159,7 @@ describe('_addResourceSpans', () => { const entry: ResourceEntry = { initiatorType, }; - _addResourceSpans(transaction, entry, '/assets/to/me', 123, 234, 465); + _addResourceSpans(transaction, entry, 'https://example.com/assets/to/me', 123, 234, 465); // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( @@ -177,7 +179,7 @@ describe('_addResourceSpans', () => { renderBlockingStatus: 'non-blocking', }; - _addResourceSpans(transaction, entry, '/assets/to/css', 100, 23, 345); + _addResourceSpans(transaction, entry, resourceEntryName, 100, 23, 345); // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); @@ -189,6 +191,7 @@ describe('_addResourceSpans', () => { ['http.response_content_length']: entry.encodedBodySize, ['http.response_transfer_size']: entry.transferSize, ['resource.render_blocking_status']: entry.renderBlockingStatus, + ['resource.render_blocking_status']: entry.renderBlockingStatus, }, }), ); @@ -202,7 +205,7 @@ describe('_addResourceSpans', () => { decodedBodySize: 2147483647, }; - _addResourceSpans(transaction, entry, '/assets/to/css', 100, 23, 345); + _addResourceSpans(transaction, entry, resourceEntryName, 100, 23, 345); // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); @@ -224,7 +227,7 @@ describe('_addResourceSpans', () => { decodedBodySize: null, } as unknown as ResourceEntry; - _addResourceSpans(transaction, entry, '/assets/to/css', 100, 23, 345); + _addResourceSpans(transaction, entry, resourceEntryName, 100, 23, 345); // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); From 797828d68a9f9d209c2fa16a4ccf061449bcfd8c Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Wed, 17 Jan 2024 16:30:03 -0500 Subject: [PATCH 065/184] fix failure --- packages/tracing-internal/test/browser/metrics/index.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/tracing-internal/test/browser/metrics/index.test.ts index 78dc4f52ad43..50efac903642 100644 --- a/packages/tracing-internal/test/browser/metrics/index.test.ts +++ b/packages/tracing-internal/test/browser/metrics/index.test.ts @@ -191,7 +191,9 @@ describe('_addResourceSpans', () => { ['http.response_content_length']: entry.encodedBodySize, ['http.response_transfer_size']: entry.transferSize, ['resource.render_blocking_status']: entry.renderBlockingStatus, - ['resource.render_blocking_status']: entry.renderBlockingStatus, + ['url.scheme']: 'https:', + ['server.address']: 'example.com', + ['url.same_origin']: true, }, }), ); From e92abb92249ba08d78a878fd15b614799b92b370 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Wed, 17 Jan 2024 16:49:47 -0500 Subject: [PATCH 066/184] fix test --- .../tracing-internal/test/browser/metrics/index.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/tracing-internal/test/browser/metrics/index.test.ts index 50efac903642..b9ca22756829 100644 --- a/packages/tracing-internal/test/browser/metrics/index.test.ts +++ b/packages/tracing-internal/test/browser/metrics/index.test.ts @@ -236,7 +236,12 @@ describe('_addResourceSpans', () => { // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ - data: {}, + data: { "server.address": "example.com", "url.same_origin": true, "url.scheme": "https" }, + "description": "/assets/to/css", + "endTimestamp": 468, + "op": "resource.css", + "origin": "auto.resource.browser.metrics", + "startTimestamp": 445, }), ); }); From 55e6244d8bbeffbb1d524dbb4f2d39e999857c02 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Wed, 17 Jan 2024 16:53:40 -0500 Subject: [PATCH 067/184] fix linting --- packages/tracing-internal/test/browser/metrics/index.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/tracing-internal/test/browser/metrics/index.test.ts index b9ca22756829..79ed92f1b2b0 100644 --- a/packages/tracing-internal/test/browser/metrics/index.test.ts +++ b/packages/tracing-internal/test/browser/metrics/index.test.ts @@ -1,4 +1,3 @@ -import { GLOBAL_OBJ, parseUrl } from '@sentry/utils'; import { Transaction } from '../../../src'; import type { ResourceEntry } from '../../../src/browser/metrics'; import { _addMeasureSpans, _addResourceSpans } from '../../../src/browser/metrics'; From 5ea753f05ca989a1cd64fc67ec17e1a816cd8fd2 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Wed, 17 Jan 2024 16:54:15 -0500 Subject: [PATCH 068/184] update scheme --- packages/tracing-internal/test/browser/metrics/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/tracing-internal/test/browser/metrics/index.test.ts index 79ed92f1b2b0..da6dcd56f0d7 100644 --- a/packages/tracing-internal/test/browser/metrics/index.test.ts +++ b/packages/tracing-internal/test/browser/metrics/index.test.ts @@ -235,7 +235,7 @@ describe('_addResourceSpans', () => { // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ - data: { "server.address": "example.com", "url.same_origin": true, "url.scheme": "https" }, + data: { "server.address": "example.com", "url.same_origin": true, "url.scheme": "https:" }, "description": "/assets/to/css", "endTimestamp": 468, "op": "resource.css", From 0204e6ea46f3a83c005787af17287c7dc4853be1 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Wed, 17 Jan 2024 17:01:35 -0500 Subject: [PATCH 069/184] fix --- packages/tracing-internal/src/browser/metrics/index.ts | 2 +- .../tracing-internal/test/browser/metrics/index.test.ts | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index b4ffebf8a9f7..2a1f7eb0eba1 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -423,7 +423,7 @@ export function _addResourceSpans( data['resource.render_blocking_status'] = entry.renderBlockingStatus; } if (parsedUrl.protocol) { - data['url.scheme'] = parsedUrl.protocol; + data['url.scheme'] = parsedUrl.protocol.split(':').pop(); // the protocol returned by parseUrl includes a :, but OTEL spec does not, so we remove it. } if (parsedUrl.host) { diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/tracing-internal/test/browser/metrics/index.test.ts index da6dcd56f0d7..8c9a81761b9c 100644 --- a/packages/tracing-internal/test/browser/metrics/index.test.ts +++ b/packages/tracing-internal/test/browser/metrics/index.test.ts @@ -7,7 +7,7 @@ const mockWindowLocation = { ancestorOrigins: {}, href: "https://example.com/path/to/something", origin: "https://example.com", - protocol: "https:", + protocol: "https", host: "example.com", hostname: "example.com", port: "", @@ -235,12 +235,7 @@ describe('_addResourceSpans', () => { // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ - data: { "server.address": "example.com", "url.same_origin": true, "url.scheme": "https:" }, - "description": "/assets/to/css", - "endTimestamp": 468, - "op": "resource.css", - "origin": "auto.resource.browser.metrics", - "startTimestamp": 445, + data: { "server.address": "example.com", "url.same_origin": true, "url.scheme": "https" }, }), ); }); From e0cbd903555fee2c40ddf8d722a7003a09fea7bc Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Wed, 17 Jan 2024 17:08:39 -0500 Subject: [PATCH 070/184] update test 2 --- .../test/browser/metrics/index.test.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/tracing-internal/test/browser/metrics/index.test.ts index 8c9a81761b9c..d661b45247d7 100644 --- a/packages/tracing-internal/test/browser/metrics/index.test.ts +++ b/packages/tracing-internal/test/browser/metrics/index.test.ts @@ -213,8 +213,14 @@ describe('_addResourceSpans', () => { // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ - data: {}, - }), + data: { "server.address": "example.com", "url.same_origin": true, "url.scheme": "https" }, + description: "/assets/to/css", + endTimestamp: 468, + op: "resource.css", + origin: "auto.resource.browser.metrics", + startTimestamp: 445 + } + ), ); }); @@ -236,6 +242,11 @@ describe('_addResourceSpans', () => { expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ data: { "server.address": "example.com", "url.same_origin": true, "url.scheme": "https" }, + description: "/assets/to/css", + endTimestamp: 468, + op: "resource.css", + origin: "auto.resource.browser.metrics", + startTimestamp: 445 }), ); }); From 3bdd4fc6eb8c3c2b5793cb7807d5ddfe6cfcbf99 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Wed, 17 Jan 2024 17:09:22 -0500 Subject: [PATCH 071/184] remove colon --- packages/tracing-internal/test/browser/metrics/index.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/tracing-internal/test/browser/metrics/index.test.ts index d661b45247d7..3fb811725b7e 100644 --- a/packages/tracing-internal/test/browser/metrics/index.test.ts +++ b/packages/tracing-internal/test/browser/metrics/index.test.ts @@ -118,7 +118,7 @@ describe('_addResourceSpans', () => { ['http.response_content_length']: entry.encodedBodySize, ['http.response_transfer_size']: entry.transferSize, ['resource.render_blocking_status']: entry.renderBlockingStatus, - ['url.scheme']: 'https:', + ['url.scheme']: 'https', ['server.address']: 'example.com', ['url.same_origin']: true, }, @@ -190,7 +190,7 @@ describe('_addResourceSpans', () => { ['http.response_content_length']: entry.encodedBodySize, ['http.response_transfer_size']: entry.transferSize, ['resource.render_blocking_status']: entry.renderBlockingStatus, - ['url.scheme']: 'https:', + ['url.scheme']: 'https', ['server.address']: 'example.com', ['url.same_origin']: true, }, From 68f26ccb190433fd3ba252c4a54232a416993c53 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 17 Jan 2024 19:23:11 -0500 Subject: [PATCH 072/184] ref(react): Use new span API in React Profiler (#10104) --- packages/core/src/index.ts | 1 + packages/react/src/profiler.tsx | 123 +++++++++++--------------- packages/react/test/profiler.test.tsx | 103 ++++++++++----------- 3 files changed, 104 insertions(+), 123 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c72d05675236..871332a20ad6 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -35,6 +35,7 @@ export { startSession, endSession, captureSession, + withActiveSpan, } from './exports'; export { // eslint-disable-next-line deprecation/deprecation diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index 804e7821d7d2..d4f942e33d64 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -1,9 +1,6 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Hub } from '@sentry/browser'; -import { getCurrentHub } from '@sentry/browser'; -import { spanToJSON } from '@sentry/core'; -import type { Span, Transaction } from '@sentry/types'; +import { startInactiveSpan } from '@sentry/browser'; +import { spanToJSON, withActiveSpan } from '@sentry/core'; +import type { Span } from '@sentry/types'; import { timestampInSeconds } from '@sentry/utils'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; @@ -58,16 +55,12 @@ class Profiler extends React.Component { return; } - const activeTransaction = getActiveTransaction(); - if (activeTransaction) { - // eslint-disable-next-line deprecation/deprecation - this._mountSpan = activeTransaction.startChild({ - description: `<${name}>`, - op: REACT_MOUNT_OP, - origin: 'auto.ui.react.profiler', - data: { 'ui.component_name': name }, - }); - } + this._mountSpan = startInactiveSpan({ + name: `<${name}>`, + op: REACT_MOUNT_OP, + origin: 'auto.ui.react.profiler', + attributes: { 'ui.component_name': name }, + }); } // If a component mounted, we can finish the mount activity. @@ -87,16 +80,17 @@ class Profiler extends React.Component { const changedProps = Object.keys(updateProps).filter(k => updateProps[k] !== this.props.updateProps[k]); if (changedProps.length > 0) { const now = timestampInSeconds(); - // eslint-disable-next-line deprecation/deprecation - this._updateSpan = this._mountSpan.startChild({ - data: { - changedProps, - 'ui.component_name': this.props.name, - }, - description: `<${this.props.name}>`, - op: REACT_UPDATE_OP, - origin: 'auto.ui.react.profiler', - startTimestamp: now, + this._updateSpan = withActiveSpan(this._mountSpan, () => { + return startInactiveSpan({ + name: `<${this.props.name}>`, + op: REACT_UPDATE_OP, + origin: 'auto.ui.react.profiler', + startTimestamp: now, + attributes: { + 'ui.component_name': this.props.name, + 'ui.react.changed_props': changedProps, + }, + }); }); } } @@ -114,19 +108,24 @@ class Profiler extends React.Component { // If a component is unmounted, we can say it is no longer on the screen. // This means we can finish the span representing the component render. public componentWillUnmount(): void { + const endTimestamp = timestampInSeconds(); const { name, includeRender = true } = this.props; if (this._mountSpan && includeRender) { - // If we were able to obtain the spanId of the mount activity, we should set the - // next activity as a child to the component mount activity. - // eslint-disable-next-line deprecation/deprecation - this._mountSpan.startChild({ - description: `<${name}>`, - endTimestamp: timestampInSeconds(), - op: REACT_RENDER_OP, - origin: 'auto.ui.react.profiler', - startTimestamp: spanToJSON(this._mountSpan).timestamp, - data: { 'ui.component_name': name }, + const startTimestamp = spanToJSON(this._mountSpan).timestamp; + withActiveSpan(this._mountSpan, () => { + const renderSpan = startInactiveSpan({ + name: `<${name}>`, + op: REACT_RENDER_OP, + origin: 'auto.ui.react.profiler', + startTimestamp, + attributes: { 'ui.component_name': name }, + }); + if (renderSpan) { + // Have to cast to Span because the type of _mountSpan is Span | undefined + // and not getting narrowed properly + renderSpan.end(endTimestamp); + } }); } } @@ -144,6 +143,7 @@ class Profiler extends React.Component { * @param WrappedComponent component that is wrapped by Profiler * @param options the {@link ProfilerProps} you can pass into the Profiler */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any function withProfiler

>( WrappedComponent: React.ComponentType

, // We do not want to have `updateProps` given in options, it is instead filled through the HOC. @@ -185,18 +185,12 @@ function useProfiler( return undefined; } - const activeTransaction = getActiveTransaction(); - if (activeTransaction) { - // eslint-disable-next-line deprecation/deprecation - return activeTransaction.startChild({ - description: `<${name}>`, - op: REACT_MOUNT_OP, - origin: 'auto.ui.react.profiler', - data: { 'ui.component_name': name }, - }); - } - - return undefined; + return startInactiveSpan({ + name: `<${name}>`, + op: REACT_MOUNT_OP, + origin: 'auto.ui.react.profiler', + attributes: { 'ui.component_name': name }, + }); }); React.useEffect(() => { @@ -206,15 +200,21 @@ function useProfiler( return (): void => { if (mountSpan && options.hasRenderSpan) { - // eslint-disable-next-line deprecation/deprecation - mountSpan.startChild({ - description: `<${name}>`, - endTimestamp: timestampInSeconds(), + const startTimestamp = spanToJSON(mountSpan).timestamp; + const endTimestamp = timestampInSeconds(); + + const renderSpan = startInactiveSpan({ + name: `<${name}>`, op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', - startTimestamp: spanToJSON(mountSpan).timestamp, - data: { 'ui.component_name': name }, + startTimestamp, + attributes: { 'ui.component_name': name }, }); + if (renderSpan) { + // Have to cast to Span because the type of _mountSpan is Span | undefined + // and not getting narrowed properly + renderSpan.end(endTimestamp); + } } }; // We only want this to run once. @@ -223,18 +223,3 @@ function useProfiler( } export { withProfiler, Profiler, useProfiler }; - -/** Grabs active transaction off scope */ -export function getActiveTransaction( - // eslint-disable-next-line deprecation/deprecation - hub: Hub = getCurrentHub(), -): T | undefined { - if (hub) { - // eslint-disable-next-line deprecation/deprecation - const scope = hub.getScope(); - // eslint-disable-next-line deprecation/deprecation - return scope.getTransaction() as T | undefined; - } - - return undefined; -} diff --git a/packages/react/test/profiler.test.tsx b/packages/react/test/profiler.test.tsx index 03a8c7ed20dd..2cdbae0e9320 100644 --- a/packages/react/test/profiler.test.tsx +++ b/packages/react/test/profiler.test.tsx @@ -7,38 +7,33 @@ import * as React from 'react'; import { REACT_MOUNT_OP, REACT_RENDER_OP, REACT_UPDATE_OP } from '../src/constants'; import { UNKNOWN_COMPONENT, useProfiler, withProfiler } from '../src/profiler'; -const mockStartChild = jest.fn((spanArgs: SpanContext) => ({ ...spanArgs })); +const mockStartInactiveSpan = jest.fn((spanArgs: SpanContext) => ({ ...spanArgs })); const mockFinish = jest.fn(); // @sent class MockSpan { public constructor(public readonly ctx: SpanContext) {} - public startChild(ctx: SpanContext): MockSpan { - mockStartChild(ctx); - return new MockSpan(ctx); - } - public end(): void { mockFinish(); } } -let activeTransaction: Record; +let activeSpan: Record; jest.mock('@sentry/browser', () => ({ - getCurrentHub: () => ({ - getIntegration: () => undefined, - getScope: () => ({ - getTransaction: () => activeTransaction, - }), - }), + ...jest.requireActual('@sentry/browser'), + getActiveSpan: () => activeSpan, + startInactiveSpan: (ctx: SpanContext) => { + mockStartInactiveSpan(ctx); + return new MockSpan(ctx); + }, })); beforeEach(() => { - mockStartChild.mockClear(); + mockStartInactiveSpan.mockClear(); mockFinish.mockClear(); - activeTransaction = new MockSpan({ op: 'pageload' }); + activeSpan = new MockSpan({ op: 'pageload' }); }); describe('withProfiler', () => { @@ -64,24 +59,24 @@ describe('withProfiler', () => { describe('mount span', () => { it('does not get created if Profiler is disabled', () => { const ProfiledComponent = withProfiler(() =>

Testing

, { disabled: true }); - expect(mockStartChild).toHaveBeenCalledTimes(0); + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(0); render(); - expect(mockStartChild).toHaveBeenCalledTimes(0); + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(0); }); it('is created when a component is mounted', () => { const ProfiledComponent = withProfiler(() =>

Testing

); - expect(mockStartChild).toHaveBeenCalledTimes(0); + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(0); render(); - expect(mockStartChild).toHaveBeenCalledTimes(1); - expect(mockStartChild).toHaveBeenLastCalledWith({ - description: `<${UNKNOWN_COMPONENT}>`, + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(1); + expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({ + name: `<${UNKNOWN_COMPONENT}>`, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', - data: { 'ui.component_name': 'unknown' }, + attributes: { 'ui.component_name': 'unknown' }, }); }); }); @@ -89,47 +84,47 @@ describe('withProfiler', () => { describe('render span', () => { it('is created on unmount', () => { const ProfiledComponent = withProfiler(() =>

Testing

); - expect(mockStartChild).toHaveBeenCalledTimes(0); + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(0); const component = render(); component.unmount(); - expect(mockStartChild).toHaveBeenCalledTimes(2); - expect(mockStartChild).toHaveBeenLastCalledWith({ - description: `<${UNKNOWN_COMPONENT}>`, - endTimestamp: expect.any(Number), + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(2); + expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({ + name: `<${UNKNOWN_COMPONENT}>`, op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', startTimestamp: undefined, - data: { 'ui.component_name': 'unknown' }, + attributes: { 'ui.component_name': 'unknown' }, }); + expect(mockFinish).toHaveBeenCalledTimes(2); }); it('is not created if hasRenderSpan is false', () => { const ProfiledComponent = withProfiler(() =>

Testing

, { includeRender: false, }); - expect(mockStartChild).toHaveBeenCalledTimes(0); + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(0); const component = render(); component.unmount(); - expect(mockStartChild).toHaveBeenCalledTimes(1); + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(1); }); }); describe('update span', () => { it('is created when component is updated', () => { const ProfiledComponent = withProfiler((props: { num: number }) =>
{props.num}
); const { rerender } = render(); - expect(mockStartChild).toHaveBeenCalledTimes(1); + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(1); expect(mockFinish).toHaveBeenCalledTimes(1); // Dispatch new props rerender(); - expect(mockStartChild).toHaveBeenCalledTimes(2); - expect(mockStartChild).toHaveBeenLastCalledWith({ - data: { changedProps: ['num'], 'ui.component_name': 'unknown' }, - description: `<${UNKNOWN_COMPONENT}>`, + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(2); + expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({ + attributes: { 'ui.react.changed_props': ['num'], 'ui.component_name': 'unknown' }, + name: `<${UNKNOWN_COMPONENT}>`, op: REACT_UPDATE_OP, origin: 'auto.ui.react.profiler', startTimestamp: expect.any(Number), @@ -137,10 +132,10 @@ describe('withProfiler', () => { expect(mockFinish).toHaveBeenCalledTimes(2); // New props yet again rerender(); - expect(mockStartChild).toHaveBeenCalledTimes(3); - expect(mockStartChild).toHaveBeenLastCalledWith({ - data: { changedProps: ['num'], 'ui.component_name': 'unknown' }, - description: `<${UNKNOWN_COMPONENT}>`, + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(3); + expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({ + attributes: { 'ui.react.changed_props': ['num'], 'ui.component_name': 'unknown' }, + name: `<${UNKNOWN_COMPONENT}>`, op: REACT_UPDATE_OP, origin: 'auto.ui.react.profiler', startTimestamp: expect.any(Number), @@ -149,7 +144,7 @@ describe('withProfiler', () => { // Should not create spans if props haven't changed rerender(); - expect(mockStartChild).toHaveBeenCalledTimes(3); + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(3); expect(mockFinish).toHaveBeenCalledTimes(3); }); @@ -158,11 +153,11 @@ describe('withProfiler', () => { includeUpdates: false, }); const { rerender } = render(); - expect(mockStartChild).toHaveBeenCalledTimes(1); + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(1); // Dispatch new props rerender(); - expect(mockStartChild).toHaveBeenCalledTimes(1); + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(1); }); }); }); @@ -171,18 +166,18 @@ describe('useProfiler()', () => { describe('mount span', () => { it('does not get created if Profiler is disabled', () => { renderHook(() => useProfiler('Example', { disabled: true })); - expect(mockStartChild).toHaveBeenCalledTimes(0); + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(0); }); it('is created when a component is mounted', () => { renderHook(() => useProfiler('Example')); - expect(mockStartChild).toHaveBeenCalledTimes(1); - expect(mockStartChild).toHaveBeenLastCalledWith({ - description: '', + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(1); + expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({ + name: '', op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', - data: { 'ui.component_name': 'Example' }, + attributes: { 'ui.component_name': 'Example' }, }); }); }); @@ -190,23 +185,23 @@ describe('useProfiler()', () => { describe('render span', () => { it('does not get created when hasRenderSpan is false', () => { const component = renderHook(() => useProfiler('Example', { hasRenderSpan: false })); - expect(mockStartChild).toHaveBeenCalledTimes(1); + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(1); component.unmount(); - expect(mockStartChild).toHaveBeenCalledTimes(1); + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(1); }); it('is created by default', () => { const component = renderHook(() => useProfiler('Example')); - expect(mockStartChild).toHaveBeenCalledTimes(1); + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(1); component.unmount(); - expect(mockStartChild).toHaveBeenCalledTimes(2); - expect(mockStartChild).toHaveBeenLastCalledWith( + expect(mockStartInactiveSpan).toHaveBeenCalledTimes(2); + expect(mockStartInactiveSpan).toHaveBeenLastCalledWith( expect.objectContaining({ - description: '', + name: '', op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', - data: { 'ui.component_name': 'Example' }, + attributes: { 'ui.component_name': 'Example' }, }), ); }); From 7992d2528603c087c97065e8d1d10b14f3af760f Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 18 Jan 2024 12:20:05 +0100 Subject: [PATCH 073/184] fix(tracing-internal): Delay pageload transaction finish until document is interactive (#10215) --- .../pageloadWithHeartbeatTimeout/init.js | 15 ++++++ .../pageloadWithHeartbeatTimeout/test.ts | 27 +++++++++++ packages/core/src/tracing/hubextensions.ts | 11 ++++- packages/core/src/tracing/idletransaction.ts | 46 +++++++++++++++---- .../src/browser/browsertracing.ts | 13 ++++++ .../test/browser/browsertracing.test.ts | 4 ++ 6 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browsertracing/pageloadWithHeartbeatTimeout/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browsertracing/pageloadWithHeartbeatTimeout/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/browsertracing/pageloadWithHeartbeatTimeout/init.js b/dev-packages/browser-integration-tests/suites/tracing/browsertracing/pageloadWithHeartbeatTimeout/init.js new file mode 100644 index 000000000000..ce0d16f0f0db --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browsertracing/pageloadWithHeartbeatTimeout/init.js @@ -0,0 +1,15 @@ +import * as Sentry from '@sentry/browser'; +import { startSpanManual } from '@sentry/browser'; +import { Integrations } from '@sentry/tracing'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new Integrations.BrowserTracing()], + tracesSampleRate: 1, +}); + +setTimeout(() => { + startSpanManual({ name: 'pageload-child-span' }, () => {}); +}, 200); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browsertracing/pageloadWithHeartbeatTimeout/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browsertracing/pageloadWithHeartbeatTimeout/test.ts new file mode 100644 index 000000000000..dbb284aecb3b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browsertracing/pageloadWithHeartbeatTimeout/test.ts @@ -0,0 +1,27 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +// This tests asserts that the pageload transaction will finish itself after about 15 seconds (3x5s of heartbeats) if it +// has a child span without adding any additional ones or finishing any of them finishing. All of the child spans that +// are still running should have the status "cancelled". +sentryTest( + 'should send a pageload transaction terminated via heartbeat timeout', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.contexts?.trace?.op).toBe('pageload'); + expect( + // eslint-disable-next-line deprecation/deprecation + eventData.spans?.find(span => span.description === 'pageload-child-span' && span.status === 'cancelled'), + ).toBeDefined(); + }, +); diff --git a/packages/core/src/tracing/hubextensions.ts b/packages/core/src/tracing/hubextensions.ts index d5451e5e246b..e35543f16631 100644 --- a/packages/core/src/tracing/hubextensions.ts +++ b/packages/core/src/tracing/hubextensions.ts @@ -89,13 +89,22 @@ export function startIdleTransaction( onScope?: boolean, customSamplingContext?: CustomSamplingContext, heartbeatInterval?: number, + delayAutoFinishUntilSignal: boolean = false, ): IdleTransaction { // eslint-disable-next-line deprecation/deprecation const client = hub.getClient(); const options: Partial = (client && client.getOptions()) || {}; // eslint-disable-next-line deprecation/deprecation - let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, finalTimeout, heartbeatInterval, onScope); + let transaction = new IdleTransaction( + transactionContext, + hub, + idleTimeout, + finalTimeout, + heartbeatInterval, + onScope, + delayAutoFinishUntilSignal, + ); transaction = sampleTransaction(transaction, options, { parentSampled: transactionContext.parentSampled, transactionContext, diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts index c764dc4b098d..fed962898d71 100644 --- a/packages/core/src/tracing/idletransaction.ts +++ b/packages/core/src/tracing/idletransaction.ts @@ -95,6 +95,8 @@ export class IdleTransaction extends Transaction { private _finishReason: (typeof IDLE_TRANSACTION_FINISH_REASONS)[number]; + private _autoFinishAllowed: boolean; + /** * @deprecated Transactions will be removed in v8. Use spans instead. */ @@ -113,6 +115,15 @@ export class IdleTransaction extends Transaction { private readonly _heartbeatInterval: number = TRACING_DEFAULTS.heartbeatInterval, // Whether or not the transaction should put itself on the scope when it starts and pop itself off when it ends private readonly _onScope: boolean = false, + /** + * When set to `true`, will disable the idle timeout (`_idleTimeout` option) and heartbeat mechanisms (`_heartbeatInterval` + * option) until the `sendAutoFinishSignal()` method is called. The final timeout mechanism (`_finalTimeout` option) + * will not be affected by this option, meaning the transaction will definitely be finished when the final timeout is + * reached, no matter what this option is configured to. + * + * Defaults to `false`. + */ + delayAutoFinishUntilSignal: boolean = false, ) { super(transactionContext, _idleHub); @@ -122,6 +133,7 @@ export class IdleTransaction extends Transaction { this._idleTimeoutCanceledPermanently = false; this._beforeFinishCallbacks = []; this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[4]; + this._autoFinishAllowed = !delayAutoFinishUntilSignal; if (_onScope) { // We set the transaction here on the scope so error events pick up the trace @@ -131,7 +143,10 @@ export class IdleTransaction extends Transaction { _idleHub.getScope().setSpan(this); } - this._restartIdleTimeout(); + if (!delayAutoFinishUntilSignal) { + this._restartIdleTimeout(); + } + setTimeout(() => { if (!this._finished) { this.setStatus('deadline_exceeded'); @@ -217,7 +232,7 @@ export class IdleTransaction extends Transaction { } /** - * Register a callback function that gets excecuted before the transaction finishes. + * Register a callback function that gets executed before the transaction finishes. * Useful for cleanup or if you want to add any additional spans based on current context. * * This is exposed because users have no other way of running something before an idle transaction @@ -298,6 +313,17 @@ export class IdleTransaction extends Transaction { this._finishReason = reason; } + /** + * Permits the IdleTransaction to automatically end itself via the idle timeout and heartbeat mechanisms when the `delayAutoFinishUntilSignal` option was set to `true`. + */ + public sendAutoFinishSignal(): void { + if (!this._autoFinishAllowed) { + DEBUG_BUILD && logger.log('[Tracing] Received finish signal for idle transaction.'); + this._restartIdleTimeout(); + this._autoFinishAllowed = true; + } + } + /** * Restarts idle timeout, if there is no running idle timeout it will start one. */ @@ -337,8 +363,10 @@ export class IdleTransaction extends Transaction { if (Object.keys(this.activities).length === 0) { const endTimestamp = timestampInSeconds(); if (this._idleTimeoutCanceledPermanently) { - this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[5]; - this.end(endTimestamp); + if (this._autoFinishAllowed) { + this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[5]; + this.end(endTimestamp); + } } else { // We need to add the timeout here to have the real endtimestamp of the transaction // Remember timestampInSeconds is in seconds, timeout is in ms @@ -368,10 +396,12 @@ export class IdleTransaction extends Transaction { this._prevHeartbeatString = heartbeatString; if (this._heartbeatCounter >= 3) { - DEBUG_BUILD && logger.log('[Tracing] Transaction finished because of no change for 3 heart beats'); - this.setStatus('deadline_exceeded'); - this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[0]; - this.end(); + if (this._autoFinishAllowed) { + DEBUG_BUILD && logger.log('[Tracing] Transaction finished because of no change for 3 heart beats'); + this.setStatus('deadline_exceeded'); + this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[0]; + this.end(); + } } else { this._pingHeartbeat(); } diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts index d6ee68302020..b09fb87a2dab 100644 --- a/packages/tracing-internal/src/browser/browsertracing.ts +++ b/packages/tracing-internal/src/browser/browsertracing.ts @@ -368,8 +368,21 @@ export class BrowserTracing implements Integration { true, { location }, // for use in the tracesSampler heartbeatInterval, + isPageloadTransaction, // should wait for finish signal if it's a pageload transaction ); + if (isPageloadTransaction) { + WINDOW.document.addEventListener('readystatechange', () => { + if (['interactive', 'complete'].includes(WINDOW.document.readyState)) { + idleTransaction.sendAutoFinishSignal(); + } + }); + + if (['interactive', 'complete'].includes(WINDOW.document.readyState)) { + idleTransaction.sendAutoFinishSignal(); + } + } + // eslint-disable-next-line deprecation/deprecation const scope = hub.getScope(); diff --git a/packages/tracing-internal/test/browser/browsertracing.test.ts b/packages/tracing-internal/test/browser/browsertracing.test.ts index 54242d83eeac..b9830b8d754c 100644 --- a/packages/tracing-internal/test/browser/browsertracing.test.ts +++ b/packages/tracing-internal/test/browser/browsertracing.test.ts @@ -411,6 +411,7 @@ conditionalTest({ min: 10 })('BrowserTracing', () => { expect.any(Boolean), expect.any(Object), expect.any(Number), + true, ); }); @@ -419,6 +420,7 @@ conditionalTest({ min: 10 })('BrowserTracing', () => { createBrowserTracing(true, { routingInstrumentation: customInstrumentRouting }); const mockFinish = jest.fn(); const transaction = getActiveTransaction(hub) as IdleTransaction; + transaction.sendAutoFinishSignal(); transaction.end = mockFinish; const span = transaction.startChild(); // activities = 1 @@ -433,6 +435,7 @@ conditionalTest({ min: 10 })('BrowserTracing', () => { createBrowserTracing(true, { idleTimeout: 2000, routingInstrumentation: customInstrumentRouting }); const mockFinish = jest.fn(); const transaction = getActiveTransaction(hub) as IdleTransaction; + transaction.sendAutoFinishSignal(); transaction.end = mockFinish; const span = transaction.startChild(); // activities = 1 @@ -461,6 +464,7 @@ conditionalTest({ min: 10 })('BrowserTracing', () => { createBrowserTracing(true, { heartbeatInterval: interval, routingInstrumentation: customInstrumentRouting }); const mockFinish = jest.fn(); const transaction = getActiveTransaction(hub) as IdleTransaction; + transaction.sendAutoFinishSignal(); transaction.end = mockFinish; const span = transaction.startChild(); // activities = 1 From b3f05893f18b3b00c685b7b3c8dbe0570b99351e Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 18 Jan 2024 14:05:54 +0100 Subject: [PATCH 074/184] feat(types): Deprecate `op` on `Span` interface (#10217) As discovered during #10208, the `op` property of the `Span` interface was still missing a deprecation because `Span` inherited it from `SpanContext` which I missed. --- .../startTransaction/basic_usage/test.ts | 4 ++ .../browsertracing/http-timings/test.ts | 1 + .../long-tasks-disabled/test.ts | 1 + .../browsertracing/long-tasks-enabled/test.ts | 1 + .../metrics/pageload-browser-spans/test.ts | 1 + .../metrics/pageload-resource-spans/test.ts | 1 + .../tracing/metrics/web-vitals-fid/test.ts | 1 + .../tracing/metrics/web-vitals-fp-fcp/test.ts | 2 + .../suites/tracing/request/fetch/test.ts | 1 + .../suites/tracing/request/xhr/test.ts | 1 + packages/core/test/lib/tracing/trace.test.ts | 58 ++++++++++++++++++- packages/ember/tests/helpers/utils.ts | 3 + .../opentelemetry-node/src/spanprocessor.ts | 2 +- .../test/spanprocessor.test.ts | 37 ++++++++++-- .../test/browser/metrics/utils.test.ts | 2 + packages/types/src/span.ts | 10 +++- 16 files changed, 116 insertions(+), 10 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts index 1012ae61e1ff..7176b951225f 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts @@ -29,16 +29,20 @@ sentryTest('should report finished spans as children of the root transaction', a expect(transaction.spans).toHaveLength(3); const span_1 = transaction.spans?.[0]; + + // eslint-disable-next-line deprecation/deprecation expect(span_1?.op).toBe('span_1'); expect(span_1?.parentSpanId).toEqual(rootSpanId); // eslint-disable-next-line deprecation/deprecation expect(span_1?.data).toMatchObject({ foo: 'bar', baz: [1, 2, 3] }); const span_3 = transaction.spans?.[1]; + // eslint-disable-next-line deprecation/deprecation expect(span_3?.op).toBe('span_3'); expect(span_3?.parentSpanId).toEqual(rootSpanId); const span_5 = transaction.spans?.[2]; + // eslint-disable-next-line deprecation/deprecation expect(span_5?.op).toBe('span_5'); // eslint-disable-next-line deprecation/deprecation expect(span_5?.parentSpanId).toEqual(span_3?.spanId); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browsertracing/http-timings/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browsertracing/http-timings/test.ts index a8e7f9eec335..dffa69e73bb2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browsertracing/http-timings/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browsertracing/http-timings/test.ts @@ -26,6 +26,7 @@ sentryTest('should create fetch spans with http timing @firefox', async ({ brows const envelopes = await getMultipleSentryEnvelopeRequests(page, 2, { url, timeout: 10000 }); const tracingEvent = envelopes[envelopes.length - 1]; // last envelope contains tracing data on all browsers + // eslint-disable-next-line deprecation/deprecation const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client'); expect(requestSpans).toHaveLength(3); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browsertracing/long-tasks-disabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browsertracing/long-tasks-disabled/test.ts index 6dab208d1c4e..1f7bb54bb36a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browsertracing/long-tasks-disabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browsertracing/long-tasks-disabled/test.ts @@ -16,6 +16,7 @@ sentryTest('should not capture long task when flag is disabled.', async ({ brows const url = await getLocalTestPath({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); + // eslint-disable-next-line deprecation/deprecation const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui')); expect(uiSpans?.length).toBe(0); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browsertracing/long-tasks-enabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browsertracing/long-tasks-enabled/test.ts index 54da1074c1c5..32819fd784e0 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browsertracing/long-tasks-enabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browsertracing/long-tasks-enabled/test.ts @@ -16,6 +16,7 @@ sentryTest('should capture long task.', async ({ browserName, getLocalTestPath, const url = await getLocalTestPath({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); + // eslint-disable-next-line deprecation/deprecation const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui')); expect(uiSpans?.length).toBeGreaterThan(0); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts index b60cdce9703b..504ac975621e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts @@ -12,6 +12,7 @@ sentryTest('should add browser-related spans to pageload transaction', async ({ const url = await getLocalTestPath({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); + // eslint-disable-next-line deprecation/deprecation const browserSpans = eventData.spans?.filter(({ op }) => op === 'browser'); // Spans `connect`, `cache` and `DNS` are not always inside `pageload` transaction. diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts index e98cb5b3d9b2..9ce848384f7b 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts @@ -18,6 +18,7 @@ sentryTest('should add resource spans to pageload transaction', async ({ getLoca const url = await getLocalTestPath({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); + // eslint-disable-next-line deprecation/deprecation const resourceSpans = eventData.spans?.filter(({ op }) => op?.startsWith('resource')); // Webkit 16.0 (which is linked to Playwright 1.27.1) consistently creates 2 consectutive spans for `css`, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts index aaab7059320c..e61588f91e73 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts @@ -25,6 +25,7 @@ sentryTest('should capture a FID vital.', async ({ browserName, getLocalTestPath const fidSpan = eventData.spans?.filter(({ description }) => description === 'first input delay')[0]; expect(fidSpan).toBeDefined(); + // eslint-disable-next-line deprecation/deprecation expect(fidSpan?.op).toBe('ui.action'); expect(fidSpan?.parentSpanId).toBe(eventData.contexts?.trace_span_id); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts index 4914c0b45779..2d8b77b61c7a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts @@ -20,6 +20,7 @@ sentryTest('should capture FP vital.', async ({ browserName, getLocalTestPath, p const fpSpan = eventData.spans?.filter(({ description }) => description === 'first-paint')[0]; expect(fpSpan).toBeDefined(); + // eslint-disable-next-line deprecation/deprecation expect(fpSpan?.op).toBe('paint'); expect(fpSpan?.parentSpanId).toBe(eventData.contexts?.trace_span_id); }); @@ -39,6 +40,7 @@ sentryTest('should capture FCP vital.', async ({ getLocalTestPath, page }) => { const fcpSpan = eventData.spans?.filter(({ description }) => description === 'first-contentful-paint')[0]; expect(fcpSpan).toBeDefined(); + // eslint-disable-next-line deprecation/deprecation expect(fcpSpan?.op).toBe('paint'); expect(fcpSpan?.parentSpanId).toBe(eventData.contexts?.trace_span_id); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts index 7b374422a2f3..15e99f80f8d2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts @@ -25,6 +25,7 @@ sentryTest('should create spans for multiple fetch requests', async ({ getLocalT const envelopes = await getMultipleSentryEnvelopeRequests(page, 2, { url, timeout: 10000 }); const tracingEvent = envelopes[envelopes.length - 1]; // last envelope contains tracing data on all browsers + // eslint-disable-next-line deprecation/deprecation const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client'); expect(requestSpans).toHaveLength(3); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts index c1553e495999..163b85110891 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts @@ -13,6 +13,7 @@ sentryTest('should create spans for multiple XHR requests', async ({ getLocalTes const url = await getLocalTestPath({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); + // eslint-disable-next-line deprecation/deprecation const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client'); expect(requestSpans).toHaveLength(3); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 86c5e6d72a2e..344bea5b7818 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1,4 +1,11 @@ -import { Hub, addTracingExtensions, getCurrentScope, makeMain, spanToJSON } from '../../../src'; +import { + Hub, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + addTracingExtensions, + getCurrentScope, + makeMain, + spanToJSON, +} from '../../../src'; import { Scope } from '../../../src/scope'; import { Span, @@ -116,7 +123,8 @@ describe('startSpan', () => { expect(ref.parentSpanId).toEqual('1234567890123456'); }); - it('allows for transaction to be mutated', async () => { + // TODO (v8): Remove this test in favour of the one below + it('(deprecated op) allows for transaction to be mutated', async () => { let ref: any = undefined; client.on('finishTransaction', transaction => { ref = transaction; @@ -124,6 +132,7 @@ describe('startSpan', () => { try { await startSpan({ name: 'GET users/[id]' }, span => { if (span) { + // eslint-disable-next-line deprecation/deprecation span.op = 'http.server'; } return callback(); @@ -132,6 +141,25 @@ describe('startSpan', () => { // } + expect(spanToJSON(ref).op).toEqual('http.server'); + }); + + it('allows for transaction to be mutated', async () => { + let ref: any = undefined; + client.on('finishTransaction', transaction => { + ref = transaction; + }); + try { + await startSpan({ name: 'GET users/[id]' }, span => { + if (span) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server'); + } + return callback(); + }); + } catch (e) { + // + } + expect(ref.op).toEqual('http.server'); }); @@ -156,7 +184,8 @@ describe('startSpan', () => { expect(ref.spanRecorder.spans[1].status).toEqual(isError ? 'internal_error' : undefined); }); - it('allows for span to be mutated', async () => { + // TODO (v8): Remove this test in favour of the one below + it('(deprecated op) allows for span to be mutated', async () => { let ref: any = undefined; client.on('finishTransaction', transaction => { ref = transaction; @@ -165,6 +194,7 @@ describe('startSpan', () => { await startSpan({ name: 'GET users/[id]', parentSampled: true }, () => { return startSpan({ name: 'SELECT * from users' }, childSpan => { if (childSpan) { + // eslint-disable-next-line deprecation/deprecation childSpan.op = 'db.query'; } return callback(); @@ -177,6 +207,28 @@ describe('startSpan', () => { expect(ref.spanRecorder.spans).toHaveLength(2); expect(ref.spanRecorder.spans[1].op).toEqual('db.query'); }); + + it('allows for span to be mutated', async () => { + let ref: any = undefined; + client.on('finishTransaction', transaction => { + ref = transaction; + }); + try { + await startSpan({ name: 'GET users/[id]', parentSampled: true }, () => { + return startSpan({ name: 'SELECT * from users' }, childSpan => { + if (childSpan) { + childSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'db.query'); + } + return callback(); + }); + }); + } catch (e) { + // + } + + expect(ref.spanRecorder.spans).toHaveLength(2); + expect(spanToJSON(ref.spanRecorder.spans[1]).op).toEqual('db.query'); + }); }); it('creates & finishes span', async () => { diff --git a/packages/ember/tests/helpers/utils.ts b/packages/ember/tests/helpers/utils.ts index 99109074219e..a14088a2329e 100644 --- a/packages/ember/tests/helpers/utils.ts +++ b/packages/ember/tests/helpers/utils.ts @@ -66,12 +66,15 @@ export function assertSentryTransactions( // instead of checking the specific order of runloop spans (which is brittle), // we check (below) that _any_ runloop spans are added const filteredSpans = spans + // eslint-disable-next-line deprecation/deprecation .filter(span => !span.op?.startsWith('ui.ember.runloop.')) .map(s => { + // eslint-disable-next-line deprecation/deprecation return `${s.op} | ${spanToJSON(s).description}`; }); assert.true( + // eslint-disable-next-line deprecation/deprecation spans.some(span => span.op?.startsWith('ui.ember.runloop.')), 'it captures runloop spans', ); diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index b415a3eac730..1ca4e3584b1f 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -204,7 +204,7 @@ function updateSpanWithOtelData(sentrySpan: SentrySpan, otelSpan: OtelSpan): voi }; sentrySpan.setAttributes(allData); - sentrySpan.op = op; + sentrySpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, op); sentrySpan.updateName(description); } diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index c70134fcf9ad..85e56a92f814 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -469,12 +469,16 @@ describe('SentrySpanProcessor', () => { child.updateName('new name'); + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.op).toBe(undefined); + expect(sentrySpan && spanToJSON(sentrySpan).op).toBe(undefined); expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('SELECT * FROM users;'); child.end(); + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.op).toBe(undefined); + expect(sentrySpan && spanToJSON(sentrySpan).op).toBe(undefined); expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('new name'); parentOtelSpan.end(); @@ -493,7 +497,9 @@ describe('SentrySpanProcessor', () => { child.end(); + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.op).toBe('http.client'); + expect(spanToJSON(sentrySpan!).op).toBe('http.client'); parentOtelSpan.end(); }); @@ -511,7 +517,9 @@ describe('SentrySpanProcessor', () => { child.end(); + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.op).toBe('http.server'); + expect(spanToJSON(sentrySpan!).op).toBe('http.server'); parentOtelSpan.end(); }); @@ -692,8 +700,12 @@ describe('SentrySpanProcessor', () => { child.end(); + const { description, op } = spanToJSON(sentrySpan!); + + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.op).toBe('db'); - expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('SELECT * FROM users'); + expect(op).toBe('db'); + expect(description).toBe('SELECT * FROM users'); parentOtelSpan.end(); }); @@ -711,8 +723,12 @@ describe('SentrySpanProcessor', () => { child.end(); + const { description, op } = spanToJSON(sentrySpan!); + + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.op).toBe('db'); - expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('fetch users from DB'); + expect(op).toBe('db'); + expect(description).toBe('fetch users from DB'); parentOtelSpan.end(); }); @@ -730,8 +746,11 @@ describe('SentrySpanProcessor', () => { child.end(); + const { op, description } = spanToJSON(sentrySpan!); + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.op).toBe('rpc'); - expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('test operation'); + expect(op).toBe('rpc'); + expect(description).toBe('test operation'); parentOtelSpan.end(); }); @@ -749,8 +768,12 @@ describe('SentrySpanProcessor', () => { child.end(); + const { op, description } = spanToJSON(sentrySpan!); + + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.op).toBe('message'); - expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('test operation'); + expect(op).toBe('message'); + expect(description).toBe('test operation'); parentOtelSpan.end(); }); @@ -768,8 +791,12 @@ describe('SentrySpanProcessor', () => { child.end(); + const { op, description } = spanToJSON(sentrySpan!); + + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.op).toBe('test faas trigger'); - expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('test operation'); + expect(op).toBe('test faas trigger'); + expect(description).toBe('test operation'); parentOtelSpan.end(); }); diff --git a/packages/tracing-internal/test/browser/metrics/utils.test.ts b/packages/tracing-internal/test/browser/metrics/utils.test.ts index fbafd4f8d880..428d91edc058 100644 --- a/packages/tracing-internal/test/browser/metrics/utils.test.ts +++ b/packages/tracing-internal/test/browser/metrics/utils.test.ts @@ -13,7 +13,9 @@ describe('_startChild()', () => { expect(span).toBeInstanceOf(Span); expect(spanToJSON(span).description).toBe('evaluation'); + // eslint-disable-next-line deprecation/deprecation expect(span.op).toBe('script'); + expect(spanToJSON(span).op).toBe('script'); }); it('adjusts the start timestamp if child span starts before transaction', () => { diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 993dcd66b2a1..bed08b56b6ee 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -166,13 +166,21 @@ export interface SpanContext { } /** Span holding trace_id, span_id */ -export interface Span extends SpanContext { +export interface Span extends Omit { /** * Human-readable identifier for the span. Identical to span.description. * @deprecated Use `spanToJSON(span).description` instead. */ name: string; + /** + * Operation of the Span. + * + * @deprecated Use `startSpan()` functions to set, `span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'op') + * to update and `spanToJSON().op` to read the op instead + */ + op?: string; + /** * The ID of the span. * @deprecated Use `spanContext().spanId` instead. From 76378afa609145f2e803463acd52f78095d06bec Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 18 Jan 2024 14:10:12 +0100 Subject: [PATCH 075/184] feat(types): Add `SerializedEvent` interface (pre v8) (#10240) Add a `SerializedEvent` interface which more accurately represents what an event envelope item contains (i.e. the JSON structure of a serialized `Event` object). Specifically, the `spans` field now correctly only contains the attributes the POJO span will continue to hold, i.e. without the deprecations and methods declared in the `Span` interface. In v8, we should change the `EventItem` type for the event envelope but doing so now is a breaking change I believe (And arguably, not too important to do now anyway). --- .../tracing/browsertracing/http-timings/test.ts | 4 ++-- packages/types/src/envelope.ts | 1 + packages/types/src/event.ts | 13 ++++++++++++- packages/types/src/index.ts | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/browsertracing/http-timings/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browsertracing/http-timings/test.ts index dffa69e73bb2..b6da7522d82c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browsertracing/http-timings/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browsertracing/http-timings/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/types'; +import type { SerializedEvent } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -23,7 +23,7 @@ sentryTest('should create fetch spans with http timing @firefox', async ({ brows const url = await getLocalTestPath({ testDir: __dirname }); - const envelopes = await getMultipleSentryEnvelopeRequests(page, 2, { url, timeout: 10000 }); + const envelopes = await getMultipleSentryEnvelopeRequests(page, 2, { url, timeout: 10000 }); const tracingEvent = envelopes[envelopes.length - 1]; // last envelope contains tracing data on all browsers // eslint-disable-next-line deprecation/deprecation diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index a33f823ab8f5..98bb9145a547 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -83,6 +83,7 @@ type CheckInItemHeaders = { type: 'check_in' }; type StatsdItemHeaders = { type: 'statsd'; length: number }; type ProfileItemHeaders = { type: 'profile' }; +// TODO (v8): Replace `Event` with `SerializedEvent` export type EventItem = BaseEnvelopeItem; export type AttachmentItem = BaseEnvelopeItem; export type UserFeedbackItem = BaseEnvelopeItem; diff --git a/packages/types/src/event.ts b/packages/types/src/event.ts index b9e908371f1e..50322f18fbc6 100644 --- a/packages/types/src/event.ts +++ b/packages/types/src/event.ts @@ -11,7 +11,7 @@ import type { Request } from './request'; import type { CaptureContext } from './scope'; import type { SdkInfo } from './sdkinfo'; import type { Severity, SeverityLevel } from './severity'; -import type { Span } from './span'; +import type { Span, SpanJSON } from './span'; import type { Thread } from './thread'; import type { TransactionSource } from './transaction'; import type { User } from './user'; @@ -86,3 +86,14 @@ export interface EventHint { data?: any; integrations?: string[]; } + +/** + * Represents the event that's sent in an event envelope, omitting interfaces that are no longer representative after + * event serialization. + */ +export interface SerializedEvent extends Omit { + /** + * POJO objects of spans belonging to this event. + */ + spans?: SpanJSON[]; +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 34f668275c94..7f9d66c904fa 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -47,7 +47,7 @@ export type { ProfileItem, } from './envelope'; export type { ExtendedError } from './error'; -export type { Event, EventHint, EventType, ErrorEvent, TransactionEvent } from './event'; +export type { Event, EventHint, EventType, ErrorEvent, TransactionEvent, SerializedEvent } from './event'; export type { EventProcessor } from './eventprocessor'; export type { Exception } from './exception'; export type { Extra, Extras } from './extra'; From 2f3f54451f974a6a172134ddbd72933cc5c21d9e Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 18 Jan 2024 09:02:10 -0500 Subject: [PATCH 076/184] ref(serverless): Use new span APIs for serverless (#10235) Most of the changes here are with tests, otherwise we swap to use `startInactiveSpan` everywhere. IMO it might be more correct to refactor to use `startSpanManual`, but that can come later, I want to keep same behavior for now. --- packages/serverless/src/awsservices.ts | 18 +++++---------- packages/serverless/src/google-cloud-grpc.ts | 21 ++++++----------- packages/serverless/src/google-cloud-http.ts | 23 +++++++------------ .../serverless/test/__mocks__/@sentry/node.ts | 11 +-------- packages/serverless/test/awslambda.test.ts | 7 ------ packages/serverless/test/awsservices.test.ts | 22 +++++------------- packages/serverless/test/gcpfunction.test.ts | 6 ----- .../serverless/test/google-cloud-grpc.test.ts | 12 ++-------- .../serverless/test/google-cloud-http.test.ts | 17 ++++---------- 9 files changed, 34 insertions(+), 103 deletions(-) diff --git a/packages/serverless/src/awsservices.ts b/packages/serverless/src/awsservices.ts index 41b73f58464e..8992b7a7adb0 100644 --- a/packages/serverless/src/awsservices.ts +++ b/packages/serverless/src/awsservices.ts @@ -1,4 +1,4 @@ -import { getCurrentScope } from '@sentry/node'; +import { startInactiveSpan } from '@sentry/node'; import type { Integration, Span } from '@sentry/types'; import { fill } from '@sentry/utils'; // 'aws-sdk/global' import is expected to be type-only so it's erased in the final .js file. @@ -57,19 +57,13 @@ function wrapMakeRequest( ): MakeRequestFunction { return function (this: TService, operation: string, params?: GenericParams, callback?: MakeRequestCallback) { let span: Span | undefined; - const scope = getCurrentScope(); - // eslint-disable-next-line deprecation/deprecation - const transaction = scope.getTransaction(); const req = orig.call(this, operation, params); req.on('afterBuild', () => { - if (transaction) { - // eslint-disable-next-line deprecation/deprecation - span = transaction.startChild({ - description: describe(this, operation, params), - op: 'http.client', - origin: 'auto.http.serverless', - }); - } + span = startInactiveSpan({ + name: describe(this, operation, params), + op: 'http.client', + origin: 'auto.http.serverless', + }); }); req.on('complete', () => { if (span) { diff --git a/packages/serverless/src/google-cloud-grpc.ts b/packages/serverless/src/google-cloud-grpc.ts index 74a88f622333..458af78872f7 100644 --- a/packages/serverless/src/google-cloud-grpc.ts +++ b/packages/serverless/src/google-cloud-grpc.ts @@ -1,6 +1,6 @@ import type { EventEmitter } from 'events'; -import { getCurrentScope } from '@sentry/node'; -import type { Integration, Span } from '@sentry/types'; +import { startInactiveSpan } from '@sentry/node'; +import type { Integration } from '@sentry/types'; import { fill } from '@sentry/utils'; interface GrpcFunction extends CallableFunction { @@ -107,18 +107,11 @@ function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodName: str if (typeof ret?.on !== 'function') { return ret; } - let span: Span | undefined; - const scope = getCurrentScope(); - // eslint-disable-next-line deprecation/deprecation - const transaction = scope.getTransaction(); - if (transaction) { - // eslint-disable-next-line deprecation/deprecation - span = transaction.startChild({ - description: `${callType} ${methodName}`, - op: `grpc.${serviceIdentifier}`, - origin: 'auto.grpc.serverless', - }); - } + const span = startInactiveSpan({ + name: `${callType} ${methodName}`, + op: `grpc.${serviceIdentifier}`, + origin: 'auto.grpc.serverless', + }); ret.on('status', () => { if (span) { span.end(); diff --git a/packages/serverless/src/google-cloud-http.ts b/packages/serverless/src/google-cloud-http.ts index 87687a52f82e..369fa6ad230d 100644 --- a/packages/serverless/src/google-cloud-http.ts +++ b/packages/serverless/src/google-cloud-http.ts @@ -1,8 +1,8 @@ // '@google-cloud/common' import is expected to be type-only so it's erased in the final .js file. // When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. import type * as common from '@google-cloud/common'; -import { getCurrentScope } from '@sentry/node'; -import type { Integration, Span } from '@sentry/types'; +import { startInactiveSpan } from '@sentry/node'; +import type { Integration } from '@sentry/types'; import { fill } from '@sentry/utils'; type RequestOptions = common.DecorateRequestOptions; @@ -51,19 +51,12 @@ export class GoogleCloudHttp implements Integration { /** Returns a wrapped function that makes a request with tracing enabled */ function wrapRequestFunction(orig: RequestFunction): RequestFunction { return function (this: common.Service, reqOpts: RequestOptions, callback: ResponseCallback): void { - let span: Span | undefined; - const scope = getCurrentScope(); - // eslint-disable-next-line deprecation/deprecation - const transaction = scope.getTransaction(); - if (transaction) { - const httpMethod = reqOpts.method || 'GET'; - // eslint-disable-next-line deprecation/deprecation - span = transaction.startChild({ - description: `${httpMethod} ${reqOpts.uri}`, - op: `http.client.${identifyService(this.apiEndpoint)}`, - origin: 'auto.http.serverless', - }); - } + const httpMethod = reqOpts.method || 'GET'; + const span = startInactiveSpan({ + name: `${httpMethod} ${reqOpts.uri}`, + op: `http.client.${identifyService(this.apiEndpoint)}`, + origin: 'auto.http.serverless', + }); orig.call(this, reqOpts, (...args: Parameters) => { if (span) { span.end(); diff --git a/packages/serverless/test/__mocks__/@sentry/node.ts b/packages/serverless/test/__mocks__/@sentry/node.ts index fb929737f8d4..b3ff9edeaa9f 100644 --- a/packages/serverless/test/__mocks__/@sentry/node.ts +++ b/packages/serverless/test/__mocks__/@sentry/node.ts @@ -14,7 +14,6 @@ export const fakeScope = { setTag: jest.fn(), setContext: jest.fn(), setSpan: jest.fn(), - getTransaction: jest.fn(() => fakeTransaction), setSDKProcessingMetadata: jest.fn(), setPropagationContext: jest.fn(), }; @@ -22,11 +21,6 @@ export const fakeSpan = { end: jest.fn(), setHttpStatus: jest.fn(), }; -export const fakeTransaction = { - end: jest.fn(), - setHttpStatus: jest.fn(), - startChild: jest.fn(() => fakeSpan), -}; export const init = jest.fn(); export const addGlobalEventProcessor = jest.fn(); export const getCurrentScope = jest.fn(() => fakeScope); @@ -36,11 +30,9 @@ export const withScope = jest.fn(cb => cb(fakeScope)); export const flush = jest.fn(() => Promise.resolve()); export const getClient = jest.fn(() => ({})); export const startSpanManual = jest.fn((ctx, callback: (span: any) => any) => callback(fakeSpan)); +export const startInactiveSpan = jest.fn(() => fakeSpan); export const resetMocks = (): void => { - fakeTransaction.setHttpStatus.mockClear(); - fakeTransaction.end.mockClear(); - fakeTransaction.startChild.mockClear(); fakeSpan.end.mockClear(); fakeSpan.setHttpStatus.mockClear(); @@ -48,7 +40,6 @@ export const resetMocks = (): void => { fakeScope.setTag.mockClear(); fakeScope.setContext.mockClear(); fakeScope.setSpan.mockClear(); - fakeScope.getTransaction.mockClear(); init.mockClear(); addGlobalEventProcessor.mockClear(); diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index 0da204b31fa4..0d923074067f 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -8,13 +8,6 @@ import * as Sentry from '../src'; const { wrapHandler } = Sentry.AWSLambda; -/** - * Why @ts-expect-error some Sentry.X calls - * - * A hack-ish way to contain everything related to mocks in the same __mocks__ file. - * Thanks to this, we don't have to do more magic than necessary. Just add and export desired method and assert on it. - */ - // Default `timeoutWarningLimit` is 500ms so leaving some space for it to trigger when necessary const DEFAULT_EXECUTION_TIME = 100; let fakeEvent: { [key: string]: unknown }; diff --git a/packages/serverless/test/awsservices.test.ts b/packages/serverless/test/awsservices.test.ts index e4abda7c9364..16464e315fa6 100644 --- a/packages/serverless/test/awsservices.test.ts +++ b/packages/serverless/test/awsservices.test.ts @@ -4,13 +4,6 @@ import * as nock from 'nock'; import { AWSServices } from '../src/awsservices'; -/** - * Why @ts-expect-error some Sentry.X calls - * - * A hack-ish way to contain everything related to mocks in the same __mocks__ file. - * Thanks to this, we don't have to do more magic than necessary. Just add and export desired method and assert on it. - */ - describe('AWSServices', () => { beforeAll(() => { new AWSServices().setupOnce(); @@ -30,11 +23,10 @@ describe('AWSServices', () => { nock('https://foo.s3.amazonaws.com').get('/bar').reply(200, 'contents'); const data = await s3.getObject({ Bucket: 'foo', Key: 'bar' }).promise(); expect(data.Body?.toString('utf-8')).toEqual('contents'); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.startChild).toBeCalledWith({ + expect(SentryNode.startInactiveSpan).toBeCalledWith({ op: 'http.client', origin: 'auto.http.serverless', - description: 'aws.s3.getObject foo', + name: 'aws.s3.getObject foo', }); // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeSpan.end).toBeCalled(); @@ -48,11 +40,10 @@ describe('AWSServices', () => { expect(data.Body?.toString('utf-8')).toEqual('contents'); done(); }); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.startChild).toBeCalledWith({ + expect(SentryNode.startInactiveSpan).toBeCalledWith({ op: 'http.client', origin: 'auto.http.serverless', - description: 'aws.s3.getObject foo', + name: 'aws.s3.getObject foo', }); }); }); @@ -64,11 +55,10 @@ describe('AWSServices', () => { nock('https://lambda.eu-north-1.amazonaws.com').post('/2015-03-31/functions/foo/invocations').reply(201, 'reply'); const data = await lambda.invoke({ FunctionName: 'foo' }).promise(); expect(data.Payload?.toString('utf-8')).toEqual('reply'); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.startChild).toBeCalledWith({ + expect(SentryNode.startInactiveSpan).toBeCalledWith({ op: 'http.client', origin: 'auto.http.serverless', - description: 'aws.lambda.invoke foo', + name: 'aws.lambda.invoke foo', }); }); }); diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index f4486415988b..29cfe0541a0c 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -14,12 +14,6 @@ import type { Request, Response, } from '../src/gcpfunction/general'; -/** - * Why @ts-expect-error some Sentry.X calls - * - * A hack-ish way to contain everything related to mocks in the same __mocks__ file. - * Thanks to this, we don't have to do more magic than necessary. Just add and export desired method and assert on it. - */ describe('GCPFunction', () => { afterEach(() => { diff --git a/packages/serverless/test/google-cloud-grpc.test.ts b/packages/serverless/test/google-cloud-grpc.test.ts index a9fd8f726e08..39ebb4a54ecd 100644 --- a/packages/serverless/test/google-cloud-grpc.test.ts +++ b/packages/serverless/test/google-cloud-grpc.test.ts @@ -11,13 +11,6 @@ import * as nock from 'nock'; import { GoogleCloudGrpc } from '../src/google-cloud-grpc'; -/** - * Why @ts-expect-error some Sentry.X calls - * - * A hack-ish way to contain everything related to mocks in the same __mocks__ file. - * Thanks to this, we don't have to do more magic than necessary. Just add and export desired method and assert on it. - */ - const spyConnect = jest.spyOn(http2, 'connect'); /** Fake HTTP2 stream */ @@ -126,11 +119,10 @@ describe('GoogleCloudGrpc tracing', () => { mockHttp2Session().mockUnaryRequest(Buffer.from('00000000120a1031363337303834313536363233383630', 'hex')); const resp = await pubsub.topic('nicetopic').publish(Buffer.from('data')); expect(resp).toEqual('1637084156623860'); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.startChild).toBeCalledWith({ + expect(SentryNode.startInactiveSpan).toBeCalledWith({ op: 'grpc.pubsub', origin: 'auto.grpc.serverless', - description: 'unary call publish', + name: 'unary call publish', }); await pubsub.close(); }); diff --git a/packages/serverless/test/google-cloud-http.test.ts b/packages/serverless/test/google-cloud-http.test.ts index b285a9a862c8..0ef1466647a5 100644 --- a/packages/serverless/test/google-cloud-http.test.ts +++ b/packages/serverless/test/google-cloud-http.test.ts @@ -6,13 +6,6 @@ import * as nock from 'nock'; import { GoogleCloudHttp } from '../src/google-cloud-http'; -/** - * Why @ts-expect-error some Sentry.X calls - * - * A hack-ish way to contain everything related to mocks in the same __mocks__ file. - * Thanks to this, we don't have to do more magic than necessary. Just add and export desired method and assert on it. - */ - describe('GoogleCloudHttp tracing', () => { beforeAll(() => { new GoogleCloudHttp().setupOnce(); @@ -57,17 +50,15 @@ describe('GoogleCloudHttp tracing', () => { ); const resp = await bigquery.query('SELECT true AS foo'); expect(resp).toEqual([[{ foo: true }]]); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.startChild).toBeCalledWith({ + expect(SentryNode.startInactiveSpan).toBeCalledWith({ op: 'http.client.bigquery', origin: 'auto.http.serverless', - description: 'POST /jobs', + name: 'POST /jobs', }); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.startChild).toBeCalledWith({ + expect(SentryNode.startInactiveSpan).toBeCalledWith({ op: 'http.client.bigquery', origin: 'auto.http.serverless', - description: expect.stringMatching(/^GET \/queries\/.+/), + name: expect.stringMatching(/^GET \/queries\/.+/), }); }); }); From 300a145cdd5f771d0ab5221dab6d921643fd10df Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 18 Jan 2024 09:02:33 -0500 Subject: [PATCH 077/184] feat(types): Add support for new monitor config thresholds (#10225) Update `MonitorConfig` to add support for `failure_issue_threshold` and `recovery_threshold`. --- packages/types/src/checkin.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/types/src/checkin.ts b/packages/types/src/checkin.ts index 992c1781164e..9a1f9ee935e7 100644 --- a/packages/types/src/checkin.ts +++ b/packages/types/src/checkin.ts @@ -83,4 +83,8 @@ export interface MonitorConfig { // A tz database string representing the timezone which the monitor's execution schedule is in. // See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones timezone?: SerializedMonitorConfig['timezone']; + // How many consecutive failed check-ins it takes to create an issue. + failure_issue_threshold?: number; + // How many consecutive OK check-ins it takes to resolve an issue. + recovery_threshold?: number; } From 484040821028ed66c22d8abf3ab3bba1345122a5 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 18 Jan 2024 09:02:50 -0500 Subject: [PATCH 078/184] ref(vue): use functional vue integration as default (#10226) --- packages/vue/src/sdk.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vue/src/sdk.ts b/packages/vue/src/sdk.ts index cc0db40c5c96..d448ad117a25 100644 --- a/packages/vue/src/sdk.ts +++ b/packages/vue/src/sdk.ts @@ -1,6 +1,6 @@ import { SDK_VERSION, defaultIntegrations, init as browserInit } from '@sentry/browser'; -import { VueIntegration } from './integration'; +import { vueIntegration } from './integration'; import type { Options, TracingOptions } from './types'; /** @@ -22,7 +22,7 @@ export function init( version: SDK_VERSION, }, }, - defaultIntegrations: [...defaultIntegrations, new VueIntegration()], + defaultIntegrations: [...defaultIntegrations, vueIntegration()], ...config, }; From 431f3b2513dfe58b6f4f06d09b59cfa46578eb74 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 18 Jan 2024 16:07:33 +0100 Subject: [PATCH 079/184] test(core): Add integration test for `captureUserFeedback` on `captureException` and `captureMessage` (#10239) --- .../withCaptureException/init.js | 16 +++++++++++++ .../withCaptureException/subject.js | 1 + .../withCaptureException/test.ts | 23 +++++++++++++++++++ .../withCaptureMessage/init.js | 16 +++++++++++++ .../withCaptureMessage/subject.js | 1 + .../withCaptureMessage/test.ts | 23 +++++++++++++++++++ 6 files changed, 80 insertions(+) create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/init.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/init.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/test.ts diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/init.js new file mode 100644 index 000000000000..866263273351 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/init.js @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + beforeSend(event) { + Sentry.captureUserFeedback({ + event_id: event.event_id, + name: 'John Doe', + email: 'john@doe.com', + comments: 'This feedback should be attached associated with the captured error', + }); + return event; + }, +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/subject.js new file mode 100644 index 000000000000..7e500a15cf8c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/subject.js @@ -0,0 +1 @@ +Sentry.captureException(new Error('Error with Feedback')); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/test.ts new file mode 100644 index 000000000000..d81b2c7a3db1 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureException/test.ts @@ -0,0 +1,23 @@ +import { expect } from '@playwright/test'; +import type { Event, UserFeedback } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; + +sentryTest('capture user feedback when captureException is called', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const data = (await getMultipleSentryEnvelopeRequests(page, 2, { url })) as (Event | UserFeedback)[]; + + expect(data).toHaveLength(2); + + const errorEvent = ('exception' in data[0] ? data[0] : data[1]) as Event; + const feedback = ('exception' in data[0] ? data[1] : data[0]) as UserFeedback; + + expect(feedback).toEqual({ + comments: 'This feedback should be attached associated with the captured error', + email: 'john@doe.com', + event_id: errorEvent.event_id, + name: 'John Doe', + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/init.js new file mode 100644 index 000000000000..805d6adc2e1e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/init.js @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + beforeSend(event) { + Sentry.captureUserFeedback({ + event_id: event.event_id, + name: 'John Doe', + email: 'john@doe.com', + comments: 'This feedback should be attached associated with the captured message', + }); + return event; + }, +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/subject.js new file mode 100644 index 000000000000..ff25389d6b18 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/subject.js @@ -0,0 +1 @@ +Sentry.captureMessage('Message with Feedback'); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/test.ts new file mode 100644 index 000000000000..808279e2035f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureUserFeedback/withCaptureMessage/test.ts @@ -0,0 +1,23 @@ +import { expect } from '@playwright/test'; +import type { Event, UserFeedback } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; + +sentryTest('capture user feedback when captureMessage is called', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const data = (await getMultipleSentryEnvelopeRequests(page, 2, { url })) as (Event | UserFeedback)[]; + + expect(data).toHaveLength(2); + + const errorEvent = ('exception' in data[0] ? data[0] : data[1]) as Event; + const feedback = ('exception' in data[0] ? data[1] : data[0]) as UserFeedback; + + expect(feedback).toEqual({ + comments: 'This feedback should be attached associated with the captured message', + email: 'john@doe.com', + event_id: errorEvent.event_id, + name: 'John Doe', + }); +}); From 06ea035e5312205ed3682c0359c31eb4358693a3 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Thu, 18 Jan 2024 11:05:35 -0500 Subject: [PATCH 080/184] add beforeAll and afterAll --- .../test/browser/metrics/index.test.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/tracing-internal/test/browser/metrics/index.test.ts index 3fb811725b7e..91a7344db4b8 100644 --- a/packages/tracing-internal/test/browser/metrics/index.test.ts +++ b/packages/tracing-internal/test/browser/metrics/index.test.ts @@ -16,13 +16,13 @@ const mockWindowLocation = { hash: "" } as Window['location']; -WINDOW.location = mockWindowLocation; - +const originalWindowLocation = WINDOW.location; const resourceEntryName = 'https://example.com/assets/to/css'; describe('_addMeasureSpans', () => { // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ op: 'pageload', name: '/' }); + beforeEach(() => { // eslint-disable-next-line deprecation/deprecation transaction.startChild = jest.fn(); @@ -60,6 +60,15 @@ describe('_addMeasureSpans', () => { describe('_addResourceSpans', () => { // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ op: 'pageload', name: '/' }); + + beforeAll(() => { + WINDOW.location = mockWindowLocation; + }) + + afterAll(() => { + WINDOW.location = originalWindowLocation; + }) + beforeEach(() => { // eslint-disable-next-line deprecation/deprecation transaction.startChild = jest.fn(); From d110c4e026ac1d2a06e6dac9cc33338a4450f7e3 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Thu, 18 Jan 2024 11:14:21 -0500 Subject: [PATCH 081/184] run yarn fix --- .../test/browser/metrics/index.test.ts | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/tracing-internal/test/browser/metrics/index.test.ts index 91a7344db4b8..2c448f744f23 100644 --- a/packages/tracing-internal/test/browser/metrics/index.test.ts +++ b/packages/tracing-internal/test/browser/metrics/index.test.ts @@ -5,15 +5,15 @@ import { WINDOW } from '../../../src/browser/types'; const mockWindowLocation = { ancestorOrigins: {}, - href: "https://example.com/path/to/something", - origin: "https://example.com", - protocol: "https", - host: "example.com", - hostname: "example.com", - port: "", - pathname: "/path/to/something", - search: "", - hash: "" + href: 'https://example.com/path/to/something', + origin: 'https://example.com', + protocol: 'https', + host: 'example.com', + hostname: 'example.com', + port: '', + pathname: '/path/to/something', + search: '', + hash: '', } as Window['location']; const originalWindowLocation = WINDOW.location; @@ -63,11 +63,11 @@ describe('_addResourceSpans', () => { beforeAll(() => { WINDOW.location = mockWindowLocation; - }) + }); afterAll(() => { WINDOW.location = originalWindowLocation; - }) + }); beforeEach(() => { // eslint-disable-next-line deprecation/deprecation @@ -222,14 +222,13 @@ describe('_addResourceSpans', () => { // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ - data: { "server.address": "example.com", "url.same_origin": true, "url.scheme": "https" }, - description: "/assets/to/css", + data: { 'server.address': 'example.com', 'url.same_origin': true, 'url.scheme': 'https' }, + description: '/assets/to/css', endTimestamp: 468, - op: "resource.css", - origin: "auto.resource.browser.metrics", - startTimestamp: 445 - } - ), + op: 'resource.css', + origin: 'auto.resource.browser.metrics', + startTimestamp: 445, + }), ); }); @@ -250,12 +249,12 @@ describe('_addResourceSpans', () => { // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ - data: { "server.address": "example.com", "url.same_origin": true, "url.scheme": "https" }, - description: "/assets/to/css", + data: { 'server.address': 'example.com', 'url.same_origin': true, 'url.scheme': 'https' }, + description: '/assets/to/css', endTimestamp: 468, - op: "resource.css", - origin: "auto.resource.browser.metrics", - startTimestamp: 445 + op: 'resource.css', + origin: 'auto.resource.browser.metrics', + startTimestamp: 445, }), ); }); From 8536a297ec130e3858320e2f3852efb31f527010 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Thu, 18 Jan 2024 12:46:31 -0330 Subject: [PATCH 082/184] meta(feedback): Remove README and point users to the official documentation (#10233) --- packages/feedback/README.md | 264 +----------------------------------- 1 file changed, 2 insertions(+), 262 deletions(-) diff --git a/packages/feedback/README.md b/packages/feedback/README.md index 37b0cd017e63..fb5b20400a71 100644 --- a/packages/feedback/README.md +++ b/packages/feedback/README.md @@ -11,270 +11,10 @@ This SDK is **considered experimental and in a beta state**. It may experience b To view Feedback in Sentry, your [Sentry organization must be an early adopter](https://docs.sentry.io/product/accounts/early-adopter-features/). -## Pre-requisites - -`@sentry-internal/feedback` currently can only be used by browsers with [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) support. - ## Installation -During the alpha phase, the feedback integration will need to be imported from `@sentry-internal/feedback`. This will be -changed for the general release. - -```shell -npm add @sentry-internal/feedback -``` - -## Setup - -To set up the integration, add the following to your Sentry initialization. This will inject a feedback button to the bottom right corner of your application. Users can then click it to open up a feedback form where they can submit feedback. - -Several options are supported and passable via the integration constructor. See the [configuration section](#configuration) below for more details. - -```javascript -import * as Sentry from '@sentry/browser'; -// or from a framework specific SDK, e.g. -// import * as Sentry from '@sentry/react'; -import { Feedback } from '@sentry-internal/feedback'; - -Sentry.init({ - dsn: '__DSN__', - integrations: [ - new Feedback({ - // Additional SDK configuration goes in here, for example: - // See below for all available options - }) - ], - // ... -}); -``` +Please read the [offical integration documentation](https://docs.sentry.io/platforms/javascript/user-feedback/) for installation instructions. ## Configuration -### General Integration Configuration - -The following options can be configured as options to the integration, in `new Feedback({})`: - -| key | type | default | description | -| --------- | ------- | ------- | ----------- | -| `autoInject` | `boolean` | `true` | Injects the Feedback widget into the application when the integration is added. This is useful to turn off if you bring your own button, or only want to show the widget on certain views. | -| `showBranding` | `boolean` | `true` | Displays the Sentry logo inside of the dialog | -| `colorScheme` | `"system" \| "light" \| "dark"` | `"system"` | The color theme to use. `"system"` will follow your OS colorscheme. | - -### User/form Related Configuration -| key | type | default | description | -| --------- | ------- | ------- | ----------- | -| `showName` | `boolean` | `true` | Displays the name field on the feedback form, however will still capture the name (if available) from Sentry SDK context. | -| `showEmail` | `boolean` | `true` | Displays the email field on the feedback form, however will still capture the email (if available) from Sentry SDK context. | -| `isNameRequired` | `boolean` | `false` | Requires the name field on the feedback form to be filled in. | -| `isEmailRequired` | `boolean` | `false` | Requires the email field on the feedback form to be filled in. | -| `useSentryUser` | `Record` | `{ email: 'email', name: 'username'}` | Map of the `email` and `name` fields to the corresponding Sentry SDK user fields that were called with `Sentry.setUser`. | - -By default the Feedback integration will attempt to fill in the name/email fields if you have set a user context via [`Sentry.setUser`](https://docs.sentry.io/platforms/javascript/enriching-events/identify-user/). By default it expects the email and name fields to be `email` and `username`. Below is an example configuration with non-default user fields. - -```javascript -Sentry.setUser({ - userEmail: 'foo@example.com', - fullName: 'Jane Doe', -}); - -new Feedback({ - useSentryUser: { - email: 'userEmail', - name: 'fullName', - }, -}) -``` - -### Text Customization -Most text that you see in the default Feedback widget can be customized. - -| key | default | description | -| --------- | ------- | ----------- | -| `buttonLabel` | `Report a Bug` | The label of the widget button. | -| `submitButtonLabel` | `Send Bug Report` | The label of the submit button used in the feedback form dialog. | -| `cancelButtonLabel` | `Cancel` | The label of the cancel button used in the feedback form dialog. | -| `formTitle` | `Report a Bug` | The title at the top of the feedback form dialog. | -| `nameLabel` | `Name` | The label of the name input field. | -| `namePlaceholder` | `Your Name` | The placeholder for the name input field. | -| `emailLabel` | `Email` | The label of the email input field. | -| `emailPlaceholder` | `your.email@example.org` | The placeholder for the email input field. | -| `messageLabel` | `Description` | The label for the feedback description input field. | -| `messagePlaceholder` | `What's the bug? What did you expect?` | The placeholder for the feedback description input field. | -| `successMessageText` | `Thank you for your report!` | The message to be displayed after a succesful feedback submission. | - - -Example of customization - -```javascript -new Feedback({ - buttonLabel: 'Feedback', - submitButtonLabel: 'Send Feedback', - formTitle: 'Send Feedback', -}); -``` - -### Theme Customization -Colors can be customized via the Feedback constructor or by defining CSS variables on the widget button. If you use the default widget button, it will have an `id="sentry-feedback`, meaning you can use the `#sentry-feedback` selector to define CSS variables to override. - -| key | css variable | light | dark | description | -| --- | --- | --- | --- | --- | -| `background` | `--background` | `#ffffff` | `#29232f` | Background color of the widget actor and dialog | -| `backgroundHover` | `--background-hover` | `#f6f6f7` | `#352f3b` | Background color of widget actor when in a hover state | -| `foreground` | `--foreground` | `#2b2233` | `#ebe6ef` | Foreground color, e.g. text color | -| `error` | `--error` | `#df3338` | `#f55459` | Color used for error related components (e.g. text color when there was an error submitting feedback) | -| `success` | `--success` | `#268d75` | `#2da98c` | Color used for success-related components (e.g. text color when feedback is submitted successfully) | -| `border` | `--border` | `1.5px solid rgba(41, 35, 47, 0.13)` | `1.5px solid rgba(235, 230, 239, 0.15)` | The border style used for the widget actor and dialog | -| `boxShadow` | `--box-shadow` | `0px 4px 24px 0px rgba(43, 34, 51, 0.12)` | `0px 4px 24px 0px rgba(43, 34, 51, 0.12)` | The box shadow style used for the widget actor and dialog | -| `submitBackground` | `--submit-background` | `rgba(88, 74, 192, 1)` | `rgba(88, 74, 192, 1)` | Background color for the submit button | -| `submitBackgroundHover` | `--submit-background-hover` | `rgba(108, 95, 199, 1)` | `rgba(108, 95, 199, 1)` | Background color when hovering over the submit button | -| `submitBorder` | `--submit-border` | `rgba(108, 95, 199, 1)` | `rgba(108, 95, 199, 1)` | Border style for the submit button | -| `submitOutlineFocus` | `--submit-outline-focus` | `rgba(108, 95, 199, 1)` | `rgba(108, 95, 199, 1)` | Outline color for the submit button, in the focused state | -| `submitForeground` | `--submit-foreground` | `#ffffff` | `#ffffff` | Foreground color for the submit button | -| `submitForegroundHover` | `--submit-foreground-hover` | `#ffffff` | `#ffffff` | Foreground color for the submit button when hovering | -| `cancelBackground` | `--cancel-background` | `transparent` | `transparent` | Background color for the cancel button | -| `cancelBackgroundHover` | `--cancel-background-hover` | `var(--background-hover)` | `var(--background-hover)` | Background color when hovering over the cancel button | -| `cancelBorder` | `--cancel-border` | `var(--border)` | `var(--border)` | Border style for the cancel button | -| `cancelOutlineFocus` | `--cancel-outline-focus` | `var(--input-outline-focus)` | `var(--input-outline-focus)` | Outline color for the cancel button, in the focused state | -| `cancelForeground` | `--cancel-foreground` | `var(--foreground)` | `var(--foreground)` | Foreground color for the cancel button | -| `cancelForegroundHover` | `--cancel-foreground-hover` | `var(--foreground)` | `var(--foreground)` | Foreground color for the cancel button when hovering | -| `inputBackground` | `--input-background` | `inherit` | `inherit` | Background color for form inputs | -| `inputForeground` | `--input-foreground` | `inherit` | `inherit` | Foreground color for form inputs | -| `inputBorder` | `--input-border` | `var(--border)` | `var(--border)` | Border styles for form inputs | -| `inputOutlineFocus` | `--input-outline-focus` | `rgba(108, 95, 199, 1)` | `rgba(108, 95, 199, 1)` | Outline color for form inputs when focused | - -Here is an example of customizing only the background color for the light theme using the Feedback constructor configuration. -```javascript -new Feedback({ - themeLight: { - background: "#cccccc", - }, -}) -``` - -Or the same example above but using the CSS variables method: - -```css -#sentry-feedback { - --background: #cccccc; -} -``` - -### Additional UI Customization -Similar to theme customization above, these are additional CSS variables that can be overridden. Note these are not supported in the constructor. - -| Variable | Default | Description | -| --- | --- | --- | -| `--bottom` | `1rem` | By default the widget has a position of fixed, and is in the bottom right corner. | -| `--right` | `1rem` | By default the widget has a position of fixed, and is in the bottom right corner. | -| `--top` | `auto` | By default the widget has a position of fixed, and is in the bottom right corner. | -| `--left` | `auto` | By default the widget has a position of fixed, and is in the bottom right corner. | -| `--z-index` | `100000` | The z-index of the widget | -| `--font-family` | `"'Helvetica Neue', Arial, sans-serif"` | Default font-family to use| -| `--font-size` | `14px` | Font size | - -### Event Callbacks -Sometimes it’s important to know when someone has started to interact with the feedback form, so you can add custom logging, or start/stop background timers on the page until the user is done. - -Pass these callbacks when you initialize the Feedback integration: - -```javascript -new Feedback({ - onFormOpen: () => {}, - onFormClose: () => {}, - onSubmitSuccess: () => {}, - onSubmitError: () => {}, -}); -``` - -## Further Customization -There are two more methods in the integration that can help customization. - -### Bring Your Own Button - -You can skip the default widget button and use your own button. Call `feedback.attachTo()` to have the SDK attach a click listener to your own button. You can additionally supply the same customization options that the constructor accepts (e.g. for text labels and colors). - -```javascript -const feedback = new Feedback({ - // Disable injecting the default widget - autoInject: false, -}); - -feedback.attachTo(document.querySelector('#your-button'), { - formTitle: "Report a Bug!" -}); -``` - -Alternatively you can call `feedback.openDialog()`: - -```typescript -import {BrowserClient, getCurrentHub} from '@sentry/react'; -import {Feedback} from '@sentry-internal/feedback'; - -function MyFeedbackButton() { - const client = getCurrentHub().getClient(); - const feedback = client?.getIntegrationByName('Feedback'); - - // Don't render custom feedback button if Feedback integration not installed - if (!feedback) { - return null; - } - - return ( - - ) -} -``` - -### Bring Your Own Widget - -You can also bring your own widget and UI and simply pass a feedback object to the `sendFeedback()` function. The `sendFeedback` function accepts two parameters: -* a feedback object with a required `message` property, and additionally, optional `name` and `email` properties -* an options object - -```javascript -sendFeedback({ - name: 'Jane Doe', // optional - email: 'email@example.org', // optional - message: 'This is an example feedback', // required -}, { - includeReplay: true, // optional -}) -``` - -Here is a simple example - -```html -
- - -