From fa8c1801131610e5fd2768b6b25894b047d14586 Mon Sep 17 00:00:00 2001 From: Maxim Lepekha Date: Mon, 3 Feb 2025 14:32:54 +0100 Subject: [PATCH 01/24] feat(nuxt): add `silent`, `errorHandler`, `release` to `SourceMapsOptions` (#15246) This pull request includes changes to enhance the configuration options for managing Sentry source maps in a Nuxt.js project. The most important changes include adding new options to suppress logs, handle errors during release creation, and manage Sentry releases. Enhancements to Sentry source maps configuration: * [`packages/nuxt/src/common/types.ts`](diffhunk://#diff-199725da81bbdbb2e85f3dfe2b1a2bf453b4c9755059e5050815d7fcde2f59a5R11-R41): Added `silent`, `errorHandler`, and `release` options to the `SourceMapsOptions` type to provide more control over logging, error handling, and release management. * [`packages/nuxt/src/vite/sourceMaps.ts`](diffhunk://#diff-d511a0577f152ed6476e519722483c904cd2878586fdcd27f71c5587036bee37R96-R98): Updated the `getPluginOptions` function to include the new `silent`, `errorHandler`, and `release` options, allowing them to be used during the source maps upload process. Before submitting a pull request, please take a look at our [Contributing](https://github.com/getsentry/sentry-javascript/blob/master/CONTRIBUTING.md) guidelines and verify: - [x] If you've added code that should be tested, please add tests. - [x] Ensure your code lints and the test suite passes (`yarn lint`) & (`yarn test`). --------- Co-authored-by: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> --- packages/nuxt/src/common/types.ts | 44 ++++++++++++++++++++++++++++ packages/nuxt/src/vite/sourceMaps.ts | 6 ++++ 2 files changed, 50 insertions(+) diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 8a9a453ff7db..b646ca9a25e2 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -8,6 +8,50 @@ export type SentryNuxtClientOptions = Omit[0] & objec export type SentryNuxtServerOptions = Omit[0] & object, 'app'>; type SourceMapsOptions = { + /** + * Suppresses all logs. + * + * @default false + */ + silent?: boolean; + + /** + * When an error occurs during release creation or sourcemaps upload, the plugin will call this function. + * + * By default, the plugin will simply throw an error, thereby stopping the bundling process. + * If an `errorHandler` callback is provided, compilation will continue, unless an error is + * thrown in the provided callback. + * + * To allow compilation to continue but still emit a warning, set this option to the following: + * + * ```js + * (err) => { + * console.warn(err); + * } + * ``` + */ + errorHandler?: (err: Error) => void; + + /** + * Options related to managing the Sentry releases for a build. + * + * More info: https://docs.sentry.io/product/releases/ + */ + release?: { + /** + * Unique identifier for the release you want to create. + * + * This value can also be specified via the `SENTRY_RELEASE` environment variable. + * + * Defaults to automatically detecting a value for your environment. + * This includes values for Cordova, Heroku, AWS CodeBuild, CircleCI, Xcode, and Gradle, and otherwise uses the git `HEAD`'s commit SHA. + * (the latter requires access to git CLI and for the root directory to be a valid repository) + * + * If you didn't provide a value and the plugin can't automatically detect one, no release will be created. + */ + name?: string; + }; + /** * If this flag is `true`, and an auth token is detected, the Sentry SDK will * automatically generate and upload source maps to Sentry during a production build. diff --git a/packages/nuxt/src/vite/sourceMaps.ts b/packages/nuxt/src/vite/sourceMaps.ts index 0b264e822bcc..4f1e1184e637 100644 --- a/packages/nuxt/src/vite/sourceMaps.ts +++ b/packages/nuxt/src/vite/sourceMaps.ts @@ -93,6 +93,12 @@ export function getPluginOptions( telemetry: sourceMapsUploadOptions.telemetry ?? true, url: sourceMapsUploadOptions.url ?? process.env.SENTRY_URL, debug: moduleOptions.debug ?? false, + silent: sourceMapsUploadOptions.silent ?? false, + errorHandler: sourceMapsUploadOptions.errorHandler, + release: { + name: sourceMapsUploadOptions.release?.name, + ...moduleOptions?.unstable_sentryBundlerPluginOptions?.release, + }, _metaOptions: { telemetry: { metaFramework: 'nuxt', From acd8b000984390110f8bdc36a29ea75791e6f75a Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Mon, 3 Feb 2025 16:43:18 +0100 Subject: [PATCH 02/24] chore: Add external contributor to CHANGELOG.md (#15266) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #15246 --------- Co-authored-by: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Co-authored-by: Francesco Gringl-Novy --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ee78937f08..f633e40348a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott -Work in this release was contributed by @tjhiggins, @chris-basebone, @GrizliK1988, @davidturissini, @nwalters512, @aloisklink, @arturovt, @benjick, @maximepvrt, @mstrokin, @kunal-511, @jahands, @jrandolf, @tannerlinsley, @Zen-cronic and @nathankleyn. Thank you for your contributions! +Work in this release was contributed by @tjhiggins, @chris-basebone, @GrizliK1988, @davidturissini, @nwalters512, @aloisklink, @arturovt, @benjick, @maximepvrt, @mstrokin, @kunal-511, @jahands, @jrandolf, @tannerlinsley, @Zen-cronic, @maxmaxme and @nathankleyn. Thank you for your contributions! ## 9.0.0-alpha.1 From e8c02609819121a495cfa639141d9e150cbd8685 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Mon, 3 Feb 2025 17:32:16 +0100 Subject: [PATCH 03/24] test: Only run flaky test detection for chromium (#15264) This streamlines this a bit and should be good enough here. --- .github/workflows/flaky-test-detector.yml | 2 ++ .../browser-integration-tests/scripts/detectFlakyTests.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml index a232e1e735b1..c24f306c98ea 100644 --- a/.github/workflows/flaky-test-detector.yml +++ b/.github/workflows/flaky-test-detector.yml @@ -51,6 +51,8 @@ jobs: - name: Install Playwright uses: ./.github/actions/install-playwright + with: + browsers: 'chromium' - name: Determine changed tests uses: dorny/paths-filter@v3.0.1 diff --git a/dev-packages/browser-integration-tests/scripts/detectFlakyTests.ts b/dev-packages/browser-integration-tests/scripts/detectFlakyTests.ts index 6fa8e8ddd416..1105346562c9 100644 --- a/dev-packages/browser-integration-tests/scripts/detectFlakyTests.ts +++ b/dev-packages/browser-integration-tests/scripts/detectFlakyTests.ts @@ -46,7 +46,9 @@ ${changedPaths.join('\n')} try { await new Promise((resolve, reject) => { const cp = childProcess.spawn( - `npx playwright test ${testPaths.length ? testPaths.join(' ') : './suites'} --repeat-each ${repeatEachCount}`, + `npx playwright test ${ + testPaths.length ? testPaths.join(' ') : './suites' + } --repeat-each ${repeatEachCount} --project=chromium`, { shell: true, cwd, stdio: 'inherit' }, ); From 3c4df06006adc0a4cd21d393f8b3b0b6d4030768 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Mon, 3 Feb 2025 19:22:42 +0100 Subject: [PATCH 04/24] chore(browser): Export ipAddress helpers for use in other SDKs (#15079) - Related to https://github.com/getsentry/sentry-javascript/pull/15008 Export helpers to avoid duplications in sentry/react-native. - RN PR https://github.com/getsentry/sentry-react-native/pull/4466 --- packages/browser/src/client.ts | 33 +++++++------------------- packages/core/src/index.ts | 1 + packages/core/src/utils/ipAddress.ts | 35 ++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 24 deletions(-) create mode 100644 packages/core/src/utils/ipAddress.ts diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 20b43ca6ddac..0e5b3fb6214c 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -9,7 +9,13 @@ import type { Scope, SeverityLevel, } from '@sentry/core'; -import { Client, applySdkMetadata, getSDKSource } from '@sentry/core'; +import { + Client, + addAutoIpAddressToSession, + addAutoIpAddressToUser, + applySdkMetadata, + getSDKSource, +} from '@sentry/core'; import { eventFromException, eventFromMessage } from './eventbuilder'; import { WINDOW } from './helpers'; import type { BrowserTransportOptions } from './transports/types'; @@ -84,29 +90,8 @@ export class BrowserClient extends Client { } if (this._options.sendDefaultPii) { - this.on('postprocessEvent', event => { - if (event.user?.ip_address === undefined) { - event.user = { - ...event.user, - ip_address: '{{auto}}', - }; - } - }); - - this.on('beforeSendSession', session => { - if ('aggregates' in session) { - if (session.attrs?.['ip_address'] === undefined) { - session.attrs = { - ...session.attrs, - ip_address: '{{auto}}', - }; - } - } else { - if (session.ipAddress === undefined) { - session.ipAddress = '{{auto}}'; - } - } - }); + this.on('postprocessEvent', addAutoIpAddressToUser); + this.on('beforeSendSession', addAutoIpAddressToSession); } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2c89d0e8a60b..fcc1d55128cf 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -72,6 +72,7 @@ export { hasTracingEnabled } from './utils/hasTracingEnabled'; export { isSentryRequestUrl } from './utils/isSentryRequestUrl'; export { handleCallbackErrors } from './utils/handleCallbackErrors'; export { parameterize } from './utils/parameterize'; +export { addAutoIpAddressToSession, addAutoIpAddressToUser } from './utils/ipAddress'; export { spanToTraceHeader, spanToJSON, diff --git a/packages/core/src/utils/ipAddress.ts b/packages/core/src/utils/ipAddress.ts new file mode 100644 index 000000000000..8ca389dc68a1 --- /dev/null +++ b/packages/core/src/utils/ipAddress.ts @@ -0,0 +1,35 @@ +import type { Session, SessionAggregates, User } from '../types-hoist'; + +// By default, we want to infer the IP address, unless this is explicitly set to `null` +// We do this after all other processing is done +// If `ip_address` is explicitly set to `null` or a value, we leave it as is + +/** + * @internal + */ +export function addAutoIpAddressToUser(objWithMaybeUser: { user?: User | null }): void { + if (objWithMaybeUser.user?.ip_address === undefined) { + objWithMaybeUser.user = { + ...objWithMaybeUser.user, + ip_address: '{{auto}}', + }; + } +} + +/** + * @internal + */ +export function addAutoIpAddressToSession(session: Session | SessionAggregates): void { + if ('aggregates' in session) { + if (session.attrs?.['ip_address'] === undefined) { + session.attrs = { + ...session.attrs, + ip_address: '{{auto}}', + }; + } + } else { + if (session.ipAddress === undefined) { + session.ipAddress = '{{auto}}'; + } + } +} From bb7237a7dcc0a86f388730eddcbc43befb771544 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 4 Feb 2025 12:00:31 +0100 Subject: [PATCH 05/24] feat(svelte)!: Disable component update tracking by default (#15265) Due to a change in the lifecycle of Svelte components in Svelte 5 (using Rune mode), our SDK can no longer leverage the `(before|after)Update` hooks to track component update spans. For v9, this patch therefore disables update tracking by default. --- .../tests/performance.client.test.ts | 24 ----------- .../src/routes/components/+page.svelte | 2 +- .../src/routes/components/Component1.svelte | 2 +- .../src/routes/components/Component2.svelte | 2 +- .../src/routes/components/Component3.svelte | 2 +- packages/svelte/src/config.ts | 33 ++++++--------- packages/svelte/src/constants.ts | 5 --- packages/svelte/src/performance.ts | 19 +++++---- packages/svelte/src/preprocessors.ts | 2 +- packages/svelte/src/types.ts | 7 +++- packages/svelte/test/config.test.ts | 2 +- packages/svelte/test/performance.test.ts | 40 +++++++------------ packages/svelte/test/preprocessors.test.ts | 12 +++--- 13 files changed, 57 insertions(+), 95 deletions(-) delete mode 100644 packages/svelte/src/constants.ts diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.client.test.ts index c31e51bf9e99..a2131287ec2e 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.client.test.ts @@ -109,30 +109,6 @@ test.describe('client-specific performance events', () => { op: 'ui.svelte.init', origin: 'auto.ui.svelte', }), - expect.objectContaining({ - data: { 'sentry.op': 'ui.svelte.update', 'sentry.origin': 'auto.ui.svelte' }, - description: '', - op: 'ui.svelte.update', - origin: 'auto.ui.svelte', - }), - expect.objectContaining({ - data: { 'sentry.op': 'ui.svelte.update', 'sentry.origin': 'auto.ui.svelte' }, - description: '', - op: 'ui.svelte.update', - origin: 'auto.ui.svelte', - }), - expect.objectContaining({ - data: { 'sentry.op': 'ui.svelte.update', 'sentry.origin': 'auto.ui.svelte' }, - description: '', - op: 'ui.svelte.update', - origin: 'auto.ui.svelte', - }), - expect.objectContaining({ - data: { 'sentry.op': 'ui.svelte.update', 'sentry.origin': 'auto.ui.svelte' }, - description: '', - op: 'ui.svelte.update', - origin: 'auto.ui.svelte', - }), ]), ); }); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/+page.svelte index eff3fa3f2e8d..3c1052bbbe9c 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/+page.svelte +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/+page.svelte @@ -5,7 +5,7 @@ import Component2 from "./Component2.svelte"; import Component3 from "./Component3.svelte"; - Sentry.trackComponent({componentName: 'components/+page'}) + Sentry.trackComponent({componentName: 'components/+page', trackUpdates: true})

Demonstrating Component Tracking

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component1.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component1.svelte index a675711e4b68..dfcf01de0b07 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component1.svelte +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component1.svelte @@ -2,7 +2,7 @@ import Component2 from "./Component2.svelte"; import {trackComponent} from '@sentry/sveltekit'; - trackComponent({componentName: 'Component1'}); + trackComponent({componentName: 'Component1', trackUpdates: true});

Howdy, I'm component 1

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component2.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component2.svelte index 2b2f38308077..1b3ad103b3b7 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component2.svelte +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component2.svelte @@ -2,7 +2,7 @@ import Component3 from "./Component3.svelte"; import {trackComponent} from '@sentry/sveltekit'; - trackComponent({componentName: 'Component2'}); + trackComponent({componentName: 'Component2', trackUpdates: true});

Howdy, I'm component 2

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component3.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component3.svelte index 9b4e028f78e7..9b813ff2c744 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component3.svelte +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component3.svelte @@ -1,6 +1,6 @@

Howdy, I'm component 3

diff --git a/packages/svelte/src/config.ts b/packages/svelte/src/config.ts index b4a0ae7d4f35..4bd11e0f659e 100644 --- a/packages/svelte/src/config.ts +++ b/packages/svelte/src/config.ts @@ -3,7 +3,7 @@ import type { PreprocessorGroup } from 'svelte/types/compiler/preprocess'; import { componentTrackingPreprocessor, defaultComponentTrackingOptions } from './preprocessors'; import type { SentryPreprocessorGroup, SentrySvelteConfigOptions, SvelteConfig } from './types'; -const DEFAULT_SENTRY_OPTIONS: SentrySvelteConfigOptions = { +const defaultSentryOptions: SentrySvelteConfigOptions = { componentTracking: defaultComponentTrackingOptions, }; @@ -20,32 +20,25 @@ export function withSentryConfig( sentryOptions?: SentrySvelteConfigOptions, ): SvelteConfig { const mergedOptions = { - ...DEFAULT_SENTRY_OPTIONS, + ...defaultSentryOptions, ...sentryOptions, + componentTracking: { + ...defaultSentryOptions.componentTracking, + ...sentryOptions?.componentTracking, + }, }; const originalPreprocessors = getOriginalPreprocessorArray(originalConfig); - // Map is insertion-order-preserving. It's important to add preprocessors - // to this map in the right order we want to see them being executed. - // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map - const sentryPreprocessors = new Map(); - - const shouldTrackComponents = mergedOptions.componentTracking?.trackComponents; - if (shouldTrackComponents) { - const firstPassPreproc: SentryPreprocessorGroup = componentTrackingPreprocessor(mergedOptions.componentTracking); - sentryPreprocessors.set(firstPassPreproc.sentryId || '', firstPassPreproc); + // Bail if users already added the preprocessor + if (originalPreprocessors.find((p: PreprocessorGroup) => !!(p as SentryPreprocessorGroup).sentryId)) { + return originalConfig; } - // We prioritize user-added preprocessors, so we don't insert sentry processors if they - // have already been added by users. - originalPreprocessors.forEach((p: SentryPreprocessorGroup) => { - if (p.sentryId) { - sentryPreprocessors.delete(p.sentryId); - } - }); - - const mergedPreprocessors = [...sentryPreprocessors.values(), ...originalPreprocessors]; + const mergedPreprocessors = [...originalPreprocessors]; + if (mergedOptions.componentTracking.trackComponents) { + mergedPreprocessors.unshift(componentTrackingPreprocessor(mergedOptions.componentTracking)); + } return { ...originalConfig, diff --git a/packages/svelte/src/constants.ts b/packages/svelte/src/constants.ts deleted file mode 100644 index cb8255040c03..000000000000 --- a/packages/svelte/src/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const UI_SVELTE_INIT = 'ui.svelte.init'; - -export const UI_SVELTE_UPDATE = 'ui.svelte.update'; - -export const DEFAULT_COMPONENT_NAME = 'Svelte Component'; diff --git a/packages/svelte/src/performance.ts b/packages/svelte/src/performance.ts index b50be258bc58..05f33fe1cfdf 100644 --- a/packages/svelte/src/performance.ts +++ b/packages/svelte/src/performance.ts @@ -2,8 +2,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; import type { Span } from '@sentry/core'; import { afterUpdate, beforeUpdate, onMount } from 'svelte'; -import { startInactiveSpan } from '@sentry/core'; -import { DEFAULT_COMPONENT_NAME, UI_SVELTE_INIT, UI_SVELTE_UPDATE } from './constants'; +import { logger, startInactiveSpan } from '@sentry/core'; import type { TrackComponentOptions } from './types'; const defaultTrackComponentOptions: { @@ -12,7 +11,7 @@ const defaultTrackComponentOptions: { componentName?: string; } = { trackInit: true, - trackUpdates: true, + trackUpdates: false, }; /** @@ -29,21 +28,27 @@ export function trackComponent(options?: TrackComponentOptions): void { const customComponentName = mergedOptions.componentName; - const componentName = `<${customComponentName || DEFAULT_COMPONENT_NAME}>`; + const componentName = `<${customComponentName || 'Svelte Component'}>`; if (mergedOptions.trackInit) { recordInitSpan(componentName); } if (mergedOptions.trackUpdates) { - recordUpdateSpans(componentName); + try { + recordUpdateSpans(componentName); + } catch { + logger.warn( + "Cannot track component updates. This is likely because you're using Svelte 5 in Runes mode. Set `trackUpdates: false` in `withSentryConfig` or `trackComponent` to disable this warning.", + ); + } } } function recordInitSpan(componentName: string): void { const initSpan = startInactiveSpan({ onlyIfParent: true, - op: UI_SVELTE_INIT, + op: 'ui.svelte.init', name: componentName, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.svelte' }, }); @@ -58,7 +63,7 @@ function recordUpdateSpans(componentName: string): void { beforeUpdate(() => { updateSpan = startInactiveSpan({ onlyIfParent: true, - op: UI_SVELTE_UPDATE, + op: 'ui.svelte.update', name: componentName, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.svelte' }, }); diff --git a/packages/svelte/src/preprocessors.ts b/packages/svelte/src/preprocessors.ts index c966c6e00eef..67936be39858 100644 --- a/packages/svelte/src/preprocessors.ts +++ b/packages/svelte/src/preprocessors.ts @@ -6,7 +6,7 @@ import type { ComponentTrackingInitOptions, SentryPreprocessorGroup, TrackCompon export const defaultComponentTrackingOptions: Required = { trackComponents: true, trackInit: true, - trackUpdates: true, + trackUpdates: false, }; export const FIRST_PASS_COMPONENT_TRACKING_PREPROC_ID = 'FIRST_PASS_COMPONENT_TRACKING_PREPROCESSOR'; diff --git a/packages/svelte/src/types.ts b/packages/svelte/src/types.ts index 8079019d8568..ff79920ab9a4 100644 --- a/packages/svelte/src/types.ts +++ b/packages/svelte/src/types.ts @@ -29,7 +29,7 @@ export type SpanOptions = { * onMount lifecycle hook. This span tells how long it takes a component * to be created and inserted into the DOM. * - * Defaults to true if component tracking is enabled + * @default `true` if component tracking is enabled */ trackInit?: boolean; @@ -37,7 +37,10 @@ export type SpanOptions = { * If true, a span is recorded between a component's beforeUpdate and afterUpdate * lifecycle hooks. * - * Defaults to true if component tracking is enabled + * Caution: Component updates can only be tracked in Svelte versions prior to version 5 + * or in Svelte 5 in legacy mode (i.e. without Runes). + * + * @default `false` if component tracking is enabled */ trackUpdates?: boolean; }; diff --git a/packages/svelte/test/config.test.ts b/packages/svelte/test/config.test.ts index a8c84297082a..21f51dc66518 100644 --- a/packages/svelte/test/config.test.ts +++ b/packages/svelte/test/config.test.ts @@ -60,7 +60,7 @@ describe('withSentryConfig', () => { const wrappedConfig = withSentryConfig(originalConfig); - expect(wrappedConfig).toEqual({ ...originalConfig, preprocess: [sentryPreproc] }); + expect(wrappedConfig).toEqual({ ...originalConfig }); }); it('handles multiple wraps correctly by only adding our preprocessors once', () => { diff --git a/packages/svelte/test/performance.test.ts b/packages/svelte/test/performance.test.ts index fdd2ed089a87..64e38599cdda 100644 --- a/packages/svelte/test/performance.test.ts +++ b/packages/svelte/test/performance.test.ts @@ -9,7 +9,6 @@ import { getClient, getCurrentScope, getIsolationScope, init, startSpan } from ' import type { TransactionEvent } from '@sentry/core'; -// @ts-expect-error svelte import import DummyComponent from './components/Dummy.svelte'; const PUBLIC_DSN = 'https://username@domain/123'; @@ -37,7 +36,7 @@ describe('Sentry.trackComponent()', () => { }); }); - it('creates init and update spans on component initialization', async () => { + it('creates init spans on component initialization by default', async () => { startSpan({ name: 'outer' }, span => { expect(span).toBeDefined(); render(DummyComponent, { props: { options: {} } }); @@ -47,7 +46,7 @@ describe('Sentry.trackComponent()', () => { expect(transactions).toHaveLength(1); const transaction = transactions[0]!; - expect(transaction.spans).toHaveLength(2); + expect(transaction.spans).toHaveLength(1); const rootSpanId = transaction.contexts?.trace?.span_id; expect(rootSpanId).toBeDefined(); @@ -68,29 +67,14 @@ describe('Sentry.trackComponent()', () => { timestamp: expect.any(Number), trace_id: expect.stringMatching(/[a-f0-9]{32}/), }); - - expect(transaction.spans![1]).toEqual({ - data: { - 'sentry.op': 'ui.svelte.update', - 'sentry.origin': 'auto.ui.svelte', - }, - description: '', - op: 'ui.svelte.update', - origin: 'auto.ui.svelte', - parent_span_id: rootSpanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }); }); - it('creates an update span, when the component is updated', async () => { + it('creates an update span, if `trackUpdates` is `true`', async () => { startSpan({ name: 'outer' }, async span => { expect(span).toBeDefined(); // first we create the component - const { component } = render(DummyComponent, { props: { options: {} } }); + const { component } = render(DummyComponent, { props: { options: { trackUpdates: true } } }); // then trigger an update // (just changing the trackUpdates prop so that we trigger an update. # @@ -175,7 +159,7 @@ describe('Sentry.trackComponent()', () => { startSpan({ name: 'outer' }, span => { expect(span).toBeDefined(); - render(DummyComponent, { props: { options: { trackInit: false } } }); + render(DummyComponent, { props: { options: { trackInit: false, trackUpdates: true } } }); }); await getClient()?.flush(); @@ -206,7 +190,13 @@ describe('Sentry.trackComponent()', () => { expect(span).toBeDefined(); render(DummyComponent, { - props: { options: { componentName: 'CustomComponentName' } }, + props: { + options: { + componentName: 'CustomComponentName', + // enabling updates to check for both span names in one test + trackUpdates: true, + }, + }, }); }); @@ -220,7 +210,7 @@ describe('Sentry.trackComponent()', () => { expect(transaction.spans![1]?.description).toEqual(''); }); - it("doesn't do anything, if there's no ongoing transaction", async () => { + it("doesn't do anything, if there's no ongoing parent span", async () => { render(DummyComponent, { props: { options: { componentName: 'CustomComponentName' } }, }); @@ -230,11 +220,11 @@ describe('Sentry.trackComponent()', () => { expect(transactions).toHaveLength(0); }); - it("doesn't record update spans, if there's no ongoing root span at that time", async () => { + it("doesn't record update spans, if there's no ongoing parent span at that time", async () => { const component = startSpan({ name: 'outer' }, span => { expect(span).toBeDefined(); - const { component } = render(DummyComponent, { props: { options: {} } }); + const { component } = render(DummyComponent, { props: { options: { trackUpdates: true } } }); return component; }); diff --git a/packages/svelte/test/preprocessors.test.ts b/packages/svelte/test/preprocessors.test.ts index f816c67e706c..b4d607e35a40 100644 --- a/packages/svelte/test/preprocessors.test.ts +++ b/packages/svelte/test/preprocessors.test.ts @@ -24,7 +24,7 @@ function expectComponentCodeToBeModified( preprocessedComponents.forEach(cmp => { const expectedFunctionCallOptions = { trackInit: options?.trackInit ?? true, - trackUpdates: options?.trackUpdates ?? true, + trackUpdates: options?.trackUpdates ?? false, componentName: cmp.name, }; const expectedFunctionCall = `trackComponent(${JSON.stringify(expectedFunctionCallOptions)});\n`; @@ -115,7 +115,7 @@ describe('componentTrackingPreprocessor', () => { expect(cmp2?.newCode).toEqual(cmp2?.originalCode); - expectComponentCodeToBeModified([cmp1!, cmp3!], { trackInit: true, trackUpdates: true }); + expectComponentCodeToBeModified([cmp1!, cmp3!], { trackInit: true, trackUpdates: false }); }); it('doesnt inject the function call to the same component more than once', () => { @@ -149,7 +149,7 @@ describe('componentTrackingPreprocessor', () => { return { ...cmp, newCode: res.code, map: res.map }; }); - expectComponentCodeToBeModified([cmp11!, cmp2!], { trackInit: true, trackUpdates: true }); + expectComponentCodeToBeModified([cmp11!, cmp2!], { trackInit: true }); expect(cmp12!.newCode).toEqual(cmp12!.originalCode); }); @@ -228,7 +228,7 @@ describe('componentTrackingPreprocessor', () => { expect(processedCode.code).toEqual( '\n' + "

I'm just a plain component

\n" + '', @@ -248,7 +248,7 @@ describe('componentTrackingPreprocessor', () => { expect(processedCode.code).toEqual( '\n" + "

I'm a component with a script

\n" + '', @@ -267,7 +267,7 @@ describe('componentTrackingPreprocessor', () => { expect(processedCode.code).toEqual( '", ); }); From 76854e8d2fec1296971b5eb4b813357f78168061 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 4 Feb 2025 12:01:16 +0100 Subject: [PATCH 06/24] ref(sveltekit): Clean up sub-request check (#15251) Removes a no longer necessary fallback check that we only needed in SvelteKit 1.26.0 or older. For Kit 2.x, we can rely on the `event.isSubRequest` flag to identify sub vs. actual requests in our request handler. fixes #15244 --- packages/sveltekit/src/server/handle.ts | 9 +--- packages/sveltekit/test/server/handle.test.ts | 49 +------------------ 2 files changed, 2 insertions(+), 56 deletions(-) diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index 9bb9de9ce394..3a26ee64fd2a 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -3,7 +3,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, continueTrace, - getActiveSpan, getCurrentScope, getDefaultIsolationScope, getIsolationScope, @@ -100,19 +99,13 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { }; const sentryRequestHandler: Handle = input => { - // event.isSubRequest was added in SvelteKit 1.21.0 and we can use it to check - // if we should create a new execution context or not. // In case of a same-origin `fetch` call within a server`load` function, // SvelteKit will actually just re-enter the `handle` function and set `isSubRequest` // to `true` so that no additional network call is made. // We want the `http.server` span of that nested call to be a child span of the // currently active span instead of a new root span to correctly reflect this // behavior. - // As a fallback for Kit < 1.21.0, we check if there is an active span only if there's none, - // we create a new execution context. - const isSubRequest = typeof input.event.isSubRequest === 'boolean' ? input.event.isSubRequest : !!getActiveSpan(); - - if (isSubRequest) { + if (input.event.isSubRequest) { return instrumentHandle(input, options); } diff --git a/packages/sveltekit/test/server/handle.test.ts b/packages/sveltekit/test/server/handle.test.ts index 150f59ae9bc8..cde6a78f1378 100644 --- a/packages/sveltekit/test/server/handle.test.ts +++ b/packages/sveltekit/test/server/handle.test.ts @@ -149,53 +149,6 @@ describe('sentryHandle', () => { expect(spans).toHaveLength(1); }); - it('[kit>=1.21.0] creates a child span for nested server calls (i.e. if there is an active span)', async () => { - let _span: Span | undefined = undefined; - let txnCount = 0; - client.on('spanEnd', span => { - if (span === getRootSpan(span)) { - _span = span; - ++txnCount; - } - }); - - try { - await sentryHandle()({ - event: mockEvent(), - resolve: async _ => { - // simulating a nested load call: - await sentryHandle()({ - event: mockEvent({ route: { id: 'api/users/details/[id]', isSubRequest: true } }), - resolve: resolve(type, isError), - }); - return mockResponse; - }, - }); - } catch (e) { - // - } - - expect(txnCount).toEqual(1); - expect(_span!).toBeDefined(); - - expect(spanToJSON(_span!).description).toEqual('GET /users/[id]'); - expect(spanToJSON(_span!).op).toEqual('http.server'); - expect(spanToJSON(_span!).status).toEqual(isError ? 'internal_error' : 'ok'); - expect(spanToJSON(_span!).data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toEqual('route'); - - expect(spanToJSON(_span!).timestamp).toBeDefined(); - - const spans = getSpanDescendants(_span!).map(spanToJSON); - - expect(spans).toHaveLength(2); - expect(spans).toEqual( - expect.arrayContaining([ - expect.objectContaining({ op: 'http.server', description: 'GET /users/[id]' }), - expect.objectContaining({ op: 'http.server', description: 'GET api/users/details/[id]' }), - ]), - ); - }); - it('creates a child span for nested server calls (i.e. if there is an active span)', async () => { let _span: Span | undefined = undefined; let txnCount = 0; @@ -212,7 +165,7 @@ describe('sentryHandle', () => { resolve: async _ => { // simulating a nested load call: await sentryHandle()({ - event: mockEvent({ route: { id: 'api/users/details/[id]' } }), + event: mockEvent({ route: { id: 'api/users/details/[id]', isSubRequest: true } }), resolve: resolve(type, isError), }); return mockResponse; From a3e08aded2bda13a4db3b6b79492008041b40765 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 4 Feb 2025 15:29:01 +0100 Subject: [PATCH 07/24] feat(core): Add `inheritOrSampleWith` helper to `traceSampler` (#15277) --- .../tracesSampler/server.js | 8 ++---- .../tracesSampler/test.ts | 26 ++++++++++++++++++- packages/core/src/tracing/sampling.ts | 19 +++++++++++++- packages/core/src/types-hoist/options.ts | 4 +-- .../core/src/types-hoist/samplingcontext.ts | 14 +++++++--- packages/core/test/lib/tracing/trace.test.ts | 1 + packages/opentelemetry/test/trace.test.ts | 4 +++ 7 files changed, 63 insertions(+), 13 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js index 5dc1d17588e5..5f616438fe90 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js @@ -4,12 +4,8 @@ const Sentry = require('@sentry/node'); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, - tracesSampler: ({ parentSampleRate }) => { - if (parentSampleRate) { - return parentSampleRate; - } - - return 0.69; + tracesSampler: ({ inheritOrSampleWith }) => { + return inheritOrSampleWith(0.69); }, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/test.ts b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/test.ts index 304725268f03..f97773711941 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/test.ts @@ -11,7 +11,7 @@ describe('parentSampleRate propagation with tracesSampler', () => { expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.69/); }); - test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming sample rate', async () => { + test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming sample rate (1 -> because there is a positive sampling decision and inheritOrSampleWith was used)', async () => { const runner = createRunner(__dirname, 'server.js').start(); const response = await runner.makeRequest('get', '/check', { headers: { @@ -20,6 +20,30 @@ describe('parentSampleRate propagation with tracesSampler', () => { }, }); + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=1/); + }); + + test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming sample rate (0 -> because there is a negative sampling decision and inheritOrSampleWith was used)', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-0', + baggage: '', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0/); + }); + + test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming sample rate (the fallback value -> because there is no sampling decision and inheritOrSampleWith was used)', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac', + baggage: '', + }, + }); + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.69/); }); diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts index 70c62cd20992..0820b7be2cf0 100644 --- a/packages/core/src/tracing/sampling.ts +++ b/packages/core/src/tracing/sampling.ts @@ -27,7 +27,24 @@ export function sampleSpan( // work; prefer the hook if so let sampleRate; if (typeof options.tracesSampler === 'function') { - sampleRate = options.tracesSampler(samplingContext); + sampleRate = options.tracesSampler({ + ...samplingContext, + inheritOrSampleWith: fallbackSampleRate => { + // If we have an incoming parent sample rate, we'll just use that one. + // The sampling decision will be inherited because of the sample_rand that was generated when the trace reached the incoming boundaries of the SDK. + if (typeof samplingContext.parentSampleRate === 'number') { + return samplingContext.parentSampleRate; + } + + // Fallback if parent sample rate is not on the incoming trace (e.g. if there is no baggage) + // This is to provide backwards compatibility if there are incoming traces from older SDKs that don't send a parent sample rate or a sample rand. In these cases we just want to force either a sampling decision on the downstream traces via the sample rate. + if (typeof samplingContext.parentSampled === 'boolean') { + return Number(samplingContext.parentSampled); + } + + return fallbackSampleRate; + }, + }); localSampleRateWasApplied = true; } else if (samplingContext.parentSampled !== undefined) { sampleRate = samplingContext.parentSampled; diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index 58634930c993..8e52b32eacf7 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -2,7 +2,7 @@ import type { CaptureContext } from '../scope'; import type { Breadcrumb, BreadcrumbHint } from './breadcrumb'; import type { ErrorEvent, EventHint, TransactionEvent } from './event'; import type { Integration } from './integration'; -import type { SamplingContext } from './samplingcontext'; +import type { TracesSamplerSamplingContext } from './samplingcontext'; import type { SdkMetadata } from './sdkmetadata'; import type { SpanJSON } from './span'; import type { StackLineParser, StackParser } from './stacktrace'; @@ -243,7 +243,7 @@ export interface ClientOptions number | boolean; + tracesSampler?: (samplingContext: TracesSamplerSamplingContext) => number | boolean; /** * An event-processing callback for error and message events, guaranteed to be invoked after all other event diff --git a/packages/core/src/types-hoist/samplingcontext.ts b/packages/core/src/types-hoist/samplingcontext.ts index 6f0d2a0800cf..b0a52862870c 100644 --- a/packages/core/src/types-hoist/samplingcontext.ts +++ b/packages/core/src/types-hoist/samplingcontext.ts @@ -10,9 +10,7 @@ export interface CustomSamplingContext { } /** - * Data passed to the `tracesSampler` function, which forms the basis for whatever decisions it might make. - * - * Adds default data to data provided by the user. + * Auxiliary data for various sampling mechanisms in the Sentry SDK. */ export interface SamplingContext extends CustomSamplingContext { /** @@ -42,3 +40,13 @@ export interface SamplingContext extends CustomSamplingContext { /** Initial attributes that have been passed to the span being sampled. */ attributes?: SpanAttributes; } + +/** + * Auxiliary data passed to the `tracesSampler` function. + */ +export interface TracesSamplerSamplingContext extends SamplingContext { + /** + * Returns a sample rate value that matches the sampling decision from the incoming trace, or falls back to the provided `fallbackSampleRate`. + */ + inheritOrSampleWith: (fallbackSampleRate: number) => number; +} diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 0eee7338a93d..c33b50c01a85 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -608,6 +608,7 @@ describe('startSpan', () => { test2: 'aa', test3: 'bb', }, + inheritOrSampleWith: expect.any(Function), }); }); diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index a841bec6ffb6..184b93b1e71b 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -1350,6 +1350,7 @@ describe('trace (sampling)', () => { parentSampled: undefined, name: 'outer', attributes: {}, + inheritOrSampleWith: expect.any(Function), }); // Now return `false`, it should not sample @@ -1416,6 +1417,7 @@ describe('trace (sampling)', () => { attr2: 1, 'sentry.op': 'test.op', }, + inheritOrSampleWith: expect.any(Function), }); // Now return `0`, it should not sample @@ -1457,6 +1459,7 @@ describe('trace (sampling)', () => { parentSampled: undefined, name: 'outer3', attributes: {}, + inheritOrSampleWith: expect.any(Function), }); }); @@ -1490,6 +1493,7 @@ describe('trace (sampling)', () => { parentSampled: true, name: 'outer', attributes: {}, + inheritOrSampleWith: expect.any(Function), }); }); From 1e4362e96919ea3b2017e152d51519003b9abe66 Mon Sep 17 00:00:00 2001 From: Catherine Lee <55311782+c298lee@users.noreply.github.com> Date: Tue, 4 Feb 2025 12:51:04 -0500 Subject: [PATCH 08/24] feat(user feedback): Adds toolbar for cropping and annotating (#15282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - adds a toolbar for cropping and annotations - changes from inline styles to multiple class names in BEM format With annotation option: ![Screenshot 2025-02-03 at 3 51 04 PM](https://github.com/user-attachments/assets/97e4ac38-4926-49e5-a6f3-d474174e3c38) Without annotation option (to confirm that it looks the same as before): ![Screenshot 2025-02-03 at 5 09 01 PM](https://github.com/user-attachments/assets/8b614c38-3c1b-4d7e-986e-ead86a3f4349) Closes https://github.com/getsentry/sentry-javascript/issues/15252 --- .../src/screenshot/components/CropIcon.tsx | 23 +++ .../src/screenshot/components/PenIcon.tsx | 2 +- .../components/ScreenshotEditor.tsx | 180 ++++++++++-------- .../components/ScreenshotInput.css.ts | 48 ++++- 4 files changed, 166 insertions(+), 87 deletions(-) create mode 100644 packages/feedback/src/screenshot/components/CropIcon.tsx diff --git a/packages/feedback/src/screenshot/components/CropIcon.tsx b/packages/feedback/src/screenshot/components/CropIcon.tsx new file mode 100644 index 000000000000..091179d86004 --- /dev/null +++ b/packages/feedback/src/screenshot/components/CropIcon.tsx @@ -0,0 +1,23 @@ +import type { VNode, h as hType } from 'preact'; + +interface FactoryParams { + h: typeof hType; +} + +export default function CropIconFactory({ + h, // eslint-disable-line @typescript-eslint/no-unused-vars +}: FactoryParams) { + return function CropIcon(): VNode { + return ( + + + + ); + }; +} diff --git a/packages/feedback/src/screenshot/components/PenIcon.tsx b/packages/feedback/src/screenshot/components/PenIcon.tsx index ec50862c1dd4..75a0faedf480 100644 --- a/packages/feedback/src/screenshot/components/PenIcon.tsx +++ b/packages/feedback/src/screenshot/components/PenIcon.tsx @@ -9,7 +9,7 @@ export default function PenIconFactory({ }: FactoryParams) { return function PenIcon(): VNode { return ( - + ({ __html: createScreenshotInputStyles(options.styleNonce).innerText }), []); @@ -86,6 +88,7 @@ export function ScreenshotEditorFactory({ const [croppingRect, setCroppingRect] = hooks.useState({ startX: 0, startY: 0, endX: 0, endY: 0 }); const [confirmCrop, setConfirmCrop] = hooks.useState(false); const [isResizing, setIsResizing] = hooks.useState(false); + const [isCropping, setIsCropping] = hooks.useState(true); const [isAnnotating, setIsAnnotating] = hooks.useState(false); hooks.useEffect(() => { @@ -142,6 +145,10 @@ export function ScreenshotEditorFactory({ const croppingBox = constructRect(croppingRect); ctx.clearRect(0, 0, imageDimensions.width, imageDimensions.height); + if (!isCropping) { + return; + } + // draw gray overlay around the selection ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; ctx.fillRect(0, 0, imageDimensions.width, imageDimensions.height); @@ -154,7 +161,7 @@ export function ScreenshotEditorFactory({ ctx.strokeStyle = '#000000'; ctx.lineWidth = 1; ctx.strokeRect(croppingBox.x + 3, croppingBox.y + 3, croppingBox.width - 6, croppingBox.height - 6); - }, [croppingRect]); + }, [croppingRect, isCropping]); function onGrabButton(e: Event, corner: string): void { setIsAnnotating(false); @@ -398,102 +405,115 @@ export function ScreenshotEditorFactory({ return (