diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b8138afb33c..a5523a861eee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -661,7 +661,7 @@ jobs: needs: [job_get_metadata, job_build] if: needs.job_get_metadata.outputs.changed_node == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-20.04 - timeout-minutes: 10 + timeout-minutes: 15 strategy: fail-fast: false matrix: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index 43dbbd0088bd..000000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: 'close stale issues/PRs' -on: - schedule: - - cron: '0 0 * * *' - workflow_dispatch: -jobs: - stale: - runs-on: ubuntu-20.04 - steps: - - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 - with: - repo-token: ${{ github.token }} - days-before-stale: 21 - days-before-close: 7 - only-labels: '' - operations-per-run: 100 - remove-stale-when-updated: true - debug-only: false - ascending: false - - exempt-issue-labels: 'Status: Backlog,Status: In Progress' - stale-issue-label: 'Status: Stale' - stale-issue-message: |- - This issue has gone three weeks without activity. In another week, I will close it. - - But! If you comment or otherwise update it, I will reset the clock, and if you label it `Status: Backlog` or `Status: In Progress`, I will leave it alone ... forever! - - ---- - - "A weed is but an unloved flower." ― _Ella Wheeler Wilcox_ 🥀 - close-issue-label: '' - close-issue-message: '' - - exempt-pr-labels: 'Status: Backlog,Status: In Progress' - stale-pr-label: 'Status: Stale' - stale-pr-message: |- - This pull request has gone three weeks without activity. In another week, I will close it. - - But! If you comment or otherwise update it, I will reset the clock, and if you label it `Status: Backlog` or `Status: In Progress`, I will leave it alone ... forever! - - ---- - - "A weed is but an unloved flower." ― _Ella Wheeler Wilcox_ 🥀 - close-pr-label: - close-pr-message: '' diff --git a/.gitignore b/.gitignore index 8574a81de0a4..777b23658572 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ build/ dist/ coverage/ scratch/ -*.d.ts *.js.map *.pyc *.tsbuildinfo diff --git a/CHANGELOG.md b/CHANGELOG.md index f2b1e7168f88..5b7a9fe0e146 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,68 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.58.1 + +- fix(node): Set propagation context even when tracingOptions are not defined (#8517) + +## 7.58.0 + +### Important Changes + +- **Performance Monitoring not required for Distributed Tracing** + +This release adds support for [distributed tracing](https://docs.sentry.io/platforms/javascript/usage/distributed-tracing/) without requiring performance monitoring to be active on the JavaScript SDKs (browser and node). This means even if there is no sampled transaction/span, the SDK will still propagate traces to downstream services. Distributed Tracing can be configured with the `tracePropagationTargets` option, which controls what requests to attach the `sentry-trace` and `baggage` HTTP headers to (which is what propagates tracing information). + +```js +Sentry.init({ + tracePropagationTargets: ["third-party-site.com", /^https:\/\/yourserver\.io\/api/], +}); +``` + +- feat(tracing): Add tracing without performance to browser and client Sveltekit (#8458) +- feat(node): Add tracing without performance to Node http integration (#8450) +- feat(node): Add tracing without performance to Node Undici (#8449) +- feat(node): Populate propagation context using env variables (#8422) + +- **feat(core): Support `AggregateErrors` in `LinkedErrors` integration (#8463)** + +This release adds support for [`AggregateErrors`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError). AggregateErrors are considered as Exception Groups by Sentry, and will be visualized and grouped differently. See the [Exception Groups Changelog Post](https://changelog.getsentry.com/announcements/exception-groups-now-supported-for-python-and-net) for more details. + +Exception Group support requires Self-Hosted Sentry [version 23.5.1](https://github.com/getsentry/self-hosted/releases/tag/23.5.1) or newer. + +- **feat(replay): Add a new option `networkDetailDenyUrls` (#8439)** + +This release adds a new option `networkDetailDenyUrls` to the `Replay` integration. This option allows you to specify a list of URLs that should not be captured by the `Replay` integration, which can be used alongside the existing `networkDetailAllowUrls` for finely grained control of which URLs should have network details captured. + +```js +Sentry.init({ + integrations: [ + new Sentry.Integrations.Replay({ + networkDetailDenyUrls: [/^http:\/\/example.com\/test$/], + }), + ], +}); +``` + +### Other Changes + +- feat(core): Add helpers to get module metadata from injected code (#8438) +- feat(core): Add sampling decision to trace envelope header (#8483) +- feat(node): Add trace context to checkin (#8503) +- feat(node): Export `getModule` for Electron SDK (#8488) +- feat(types): Allow `user.id` to be a number (#8330) +- fix(browser): Set anonymous `crossorigin` attribute on report dialog (#8424) +- fix(nextjs): Ignore `tunnelRoute` when doing static exports (#8471) +- fix(nextjs): Use `basePath` option for `tunnelRoute` (#8454) +- fix(node): Apply source context to linked errors even when it is uncached (#8453) +- fix(node): report errorMiddleware errors as unhandled (#8048) +- fix(react): Add support for `basename` option of `createBrowserRouter` (#8457) +- fix(remix): Add explicit `@sentry/node` exports. (#8509) +- fix(remix): Don't inject trace/baggage to `redirect` and `catch` responses (#8467) +- fix(replay): Adjust slow/multi click handling (#8380) + +Work in this release contributed by @mrdulin, @donaldxdonald & @ziyad-elabid-nw. Thank you for your contributions! + ## 7.57.0 ### Important Changes diff --git a/lerna.json b/lerna.json index 71aa98569aca..b7536d8b23a7 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "7.57.0", + "version": "7.58.1", "npmClient": "yarn" } diff --git a/packages/angular-ivy/package.json b/packages/angular-ivy/package.json index 8113d62c2f88..3c39d494ebf9 100644 --- a/packages/angular-ivy/package.json +++ b/packages/angular-ivy/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular-ivy", - "version": "7.57.0", + "version": "7.58.1", "description": "Official Sentry SDK for Angular with full Ivy Support", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular-ivy", @@ -21,9 +21,9 @@ "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry/browser": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/angular/package.json b/packages/angular/package.json index cd09ea50f476..7e8cb3123fe3 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular", - "version": "7.57.0", + "version": "7.58.1", "description": "Official Sentry SDK for Angular", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular", @@ -21,9 +21,9 @@ "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry/browser": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/browser-integration-tests/package.json b/packages/browser-integration-tests/package.json index 6f15ea585efd..9c7a07f2aa13 100644 --- a/packages/browser-integration-tests/package.json +++ b/packages/browser-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-integration-tests", - "version": "7.57.0", + "version": "7.58.1", "main": "index.js", "license": "MIT", "engines": { diff --git a/packages/browser-integration-tests/suites/replay/dsc/test.ts b/packages/browser-integration-tests/suites/replay/dsc/test.ts index 54de1ee7db81..ffd2cf1877da 100644 --- a/packages/browser-integration-tests/suites/replay/dsc/test.ts +++ b/packages/browser-integration-tests/suites/replay/dsc/test.ts @@ -49,6 +49,7 @@ sentryTest( trace_id: expect.any(String), public_key: 'public', replay_id: replay.session?.id, + sampled: 'true', }); }, ); @@ -93,6 +94,7 @@ sentryTest( sample_rate: '1', trace_id: expect.any(String), public_key: 'public', + sampled: 'true', }); }, ); @@ -152,6 +154,7 @@ sentryTest( trace_id: expect.any(String), public_key: 'public', replay_id: replay.session?.id, + sampled: 'true', }); }, ); @@ -199,6 +202,7 @@ sentryTest( sample_rate: '1', trace_id: expect.any(String), public_key: 'public', + sampled: 'true', }); }, ); diff --git a/packages/browser-integration-tests/suites/replay/slowClick/multiClick/test.ts b/packages/browser-integration-tests/suites/replay/slowClick/multiClick/test.ts index 82bc436f714e..86582bf98153 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/multiClick/test.ts +++ b/packages/browser-integration-tests/suites/replay/slowClick/multiClick/test.ts @@ -64,8 +64,9 @@ sentryTest('captures multi click when not detecting slow click', async ({ getLoc ]); }); -sentryTest('captures multiple multi clicks', async ({ getLocalTestUrl, page, forceFlushReplay }) => { - if (shouldSkipReplayTest()) { +sentryTest('captures multiple multi clicks', async ({ getLocalTestUrl, page, forceFlushReplay, browserName }) => { + // This test seems to only be flakey on firefox and webkit + if (shouldSkipReplayTest() || ['firefox', 'webkit'].includes(browserName)) { sentryTest.skip(); } diff --git a/packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/test.ts b/packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/test.ts index bc94930e0be5..b244768f7f82 100644 --- a/packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/test.ts +++ b/packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/test.ts @@ -27,6 +27,7 @@ sentryTest( transaction: expect.stringContaining('/index.html'), trace_id: expect.any(String), public_key: 'public', + sampled: 'true', }); }, ); diff --git a/packages/browser-integration-tests/suites/tracing/envelope-header/test.ts b/packages/browser-integration-tests/suites/tracing/envelope-header/test.ts index b8df56e72f7c..71adc007385c 100644 --- a/packages/browser-integration-tests/suites/tracing/envelope-header/test.ts +++ b/packages/browser-integration-tests/suites/tracing/envelope-header/test.ts @@ -30,6 +30,7 @@ sentryTest( sample_rate: '1', trace_id: expect.any(String), public_key: 'public', + sampled: 'true', }); }, ); diff --git a/packages/browser/package.json b/packages/browser/package.json index 163d0c441f82..f4d458cd56c6 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/browser", - "version": "7.57.0", + "version": "7.58.1", "description": "Official Sentry SDK for browsers", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser", @@ -23,15 +23,15 @@ "access": "public" }, "dependencies": { - "@sentry-internal/tracing": "7.57.0", - "@sentry/core": "7.57.0", - "@sentry/replay": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry-internal/tracing": "7.58.1", + "@sentry/core": "7.58.1", + "@sentry/replay": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "tslib": "^2.4.1 || ^1.9.3" }, "devDependencies": { - "@sentry-internal/integration-shims": "7.57.0", + "@sentry-internal/integration-shims": "7.58.1", "@types/md5": "2.1.33", "btoa": "^1.2.1", "chai": "^4.1.2", diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 0a69fc44d858..9dbe7f977d7e 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -34,6 +34,7 @@ export { spanStatusfromHttpCode, trace, makeMultiplexedTransport, + ModuleMetadata, } from '@sentry/core'; export type { SpanStatusType } from '@sentry/core'; export type { Span } from '@sentry/types'; diff --git a/packages/core/package.json b/packages/core/package.json index e55deeccda68..8a154bcc79e0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/core", - "version": "7.57.0", + "version": "7.58.1", "description": "Base implementation for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/core", @@ -23,8 +23,8 @@ "access": "public" }, "dependencies": { - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "tslib": "^2.4.1 || ^1.9.3" }, "scripts": { diff --git a/packages/core/src/checkin.ts b/packages/core/src/checkin.ts index 2417736bcdae..e7dd6c906f4c 100644 --- a/packages/core/src/checkin.ts +++ b/packages/core/src/checkin.ts @@ -1,26 +1,42 @@ -import type { CheckInEvelope, CheckInItem, DsnComponents, SdkMetadata, SerializedCheckIn } from '@sentry/types'; -import { createEnvelope, dsnToString } from '@sentry/utils'; +import type { + CheckInEvelope, + CheckInItem, + DsnComponents, + DynamicSamplingContext, + SdkMetadata, + SerializedCheckIn, +} from '@sentry/types'; +import { createEnvelope, dropUndefinedKeys, dsnToString } from '@sentry/utils'; /** * Create envelope from check in item. */ export function createCheckInEnvelope( checkIn: SerializedCheckIn, + dynamicSamplingContext?: Partial, metadata?: SdkMetadata, tunnel?: string, dsn?: DsnComponents, ): CheckInEvelope { const headers: CheckInEvelope[0] = { sent_at: new Date().toISOString(), - ...(metadata && - metadata.sdk && { - sdk: { - name: metadata.sdk.name, - version: metadata.sdk.version, - }, - }), - ...(!!tunnel && !!dsn && { dsn: dsnToString(dsn) }), }; + + if (metadata && metadata.sdk) { + headers.sdk = { + name: metadata.sdk.name, + version: metadata.sdk.version, + }; + } + + if (!!tunnel && !!dsn) { + headers.dsn = dsnToString(dsn); + } + + if (dynamicSamplingContext) { + headers.trace = dropUndefinedKeys(dynamicSamplingContext) as DynamicSamplingContext; + } + const item = createCheckInEnvelopeItem(checkIn); return createEnvelope(headers, [item]); } diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index ba283914fbbd..ad3d33013253 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -196,13 +196,15 @@ export function startTransaction( * to create a monitor automatically when sending a check in. */ export function captureCheckIn(checkIn: CheckIn, upsertMonitorConfig?: MonitorConfig): string { - const client = getCurrentHub().getClient(); + const hub = getCurrentHub(); + const scope = hub.getScope(); + const client = hub.getClient(); if (!client) { __DEBUG_BUILD__ && logger.warn('Cannot capture check-in. No client defined.'); } else if (!client.captureCheckIn) { __DEBUG_BUILD__ && logger.warn('Cannot capture check-in. Client does not support sending check-ins.'); } else { - return client.captureCheckIn(checkIn, upsertMonitorConfig); + return client.captureCheckIn(checkIn, upsertMonitorConfig, scope); } return uuid4(); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index db9d21c2f2e8..9ff61f05cdb3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -46,7 +46,7 @@ export { prepareEvent } from './utils/prepareEvent'; export { createCheckInEnvelope } from './checkin'; export { hasTracingEnabled } from './utils/hasTracingEnabled'; export { DEFAULT_ENVIRONMENT } from './constants'; - +export { ModuleMetadata } from './integrations/metadata'; import * as Integrations from './integrations'; export { Integrations }; diff --git a/packages/core/src/integrations/metadata.ts b/packages/core/src/integrations/metadata.ts new file mode 100644 index 000000000000..05af1d88ebe9 --- /dev/null +++ b/packages/core/src/integrations/metadata.ts @@ -0,0 +1,57 @@ +import type { EventItem, EventProcessor, Hub, Integration } from '@sentry/types'; +import { forEachEnvelopeItem } from '@sentry/utils'; + +import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metadata'; + +/** + * Adds module metadata to stack frames. + * + * Metadata can be injected by the Sentry bundler plugins using the `_experiments.moduleMetadata` config option. + * + * When this integration is added, the metadata passed to the bundler plugin is added to the stack frames of all events + * under the `module_metadata` property. This can be used to help in tagging or routing of events from different teams + * our sources + */ +export class ModuleMetadata implements Integration { + /* + * @inheritDoc + */ + public static id: string = 'ModuleMetadata'; + + /** + * @inheritDoc + */ + public name: string = ModuleMetadata.id; + + /** + * @inheritDoc + */ + public setupOnce(addGlobalEventProcessor: (processor: EventProcessor) => void, getCurrentHub: () => Hub): void { + const client = getCurrentHub().getClient(); + + if (!client || typeof client.on !== 'function') { + return; + } + + // We need to strip metadata from stack frames before sending them to Sentry since these are client side only. + client.on('beforeEnvelope', envelope => { + forEachEnvelopeItem(envelope, (item, type) => { + if (type === 'event') { + const event = Array.isArray(item) ? (item as EventItem)[1] : undefined; + + if (event) { + stripMetadataFromStackFrames(event); + item[1] = event; + } + } + }); + }); + + const stackParser = client.getOptions().stackParser; + + addGlobalEventProcessor(event => { + addMetadataToStackFrames(stackParser, event); + return event; + }); + } +} diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index d0e1474970ab..71e6279eab80 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -264,11 +264,13 @@ export class Transaction extends SpanClass implements TransactionInterface { dsc.transaction = this.name; } + if (this.sampled !== undefined) { + dsc.sampled = String(this.sampled); + } + // Uncomment if we want to make DSC immutable // this._frozenDynamicSamplingContext = dsc; - client.emit && client.emit('createDsc', dsc); - return dsc; } diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index a82cb89dc5e0..9c03ae9a6853 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -1 +1 @@ -export const SDK_VERSION = '7.57.0'; +export const SDK_VERSION = '7.58.1'; diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index 162b53e4bb51..eed6b4cb3a2f 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -1673,7 +1673,7 @@ describe('BaseClient', () => { }), ); - // @ts-ignore + // @ts-expect-error Accessing private transport API const mockSend = jest.spyOn(client._transport, 'send'); const errorEvent: Event = { message: 'error' }; @@ -1701,7 +1701,7 @@ describe('BaseClient', () => { }), ); - // @ts-ignore + // @ts-expect-error Accessing private transport API const mockSend = jest.spyOn(client._transport, 'send'); const transactionEvent: Event = { type: 'transaction', event_id: 'tr1' }; @@ -1731,7 +1731,7 @@ describe('BaseClient', () => { }), ); - // @ts-ignore + // @ts-expect-error Accessing private transport API const mockSend = jest.spyOn(client._transport, 'send').mockImplementation(() => { return Promise.reject('send error'); }); @@ -1763,7 +1763,7 @@ describe('BaseClient', () => { }), ); - // @ts-ignore + // @ts-expect-error Accessing private transport API const mockSend = jest.spyOn(client._transport, 'send').mockImplementation(() => { return Promise.resolve({ statusCode: 200 }); }); diff --git a/packages/core/test/lib/checkin.test.ts b/packages/core/test/lib/checkin.test.ts index 38a8fce56e95..5ae6bac6b5f3 100644 --- a/packages/core/test/lib/checkin.test.ts +++ b/packages/core/test/lib/checkin.test.ts @@ -10,6 +10,10 @@ describe('createCheckInEnvelope', () => { monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', status: 'in_progress', }, + { + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + public_key: 'testPublicKey', + }, { sdk: { name: 'testSdkName', @@ -30,6 +34,10 @@ describe('createCheckInEnvelope', () => { name: 'testSdkName', version: 'testSdkVersion', }, + trace: { + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + public_key: 'testPublicKey', + }, sent_at: expect.any(String), }); }); diff --git a/packages/core/test/lib/integrations/metadata.test.ts b/packages/core/test/lib/integrations/metadata.test.ts new file mode 100644 index 000000000000..464da2d97fbb --- /dev/null +++ b/packages/core/test/lib/integrations/metadata.test.ts @@ -0,0 +1,66 @@ +import type { Event } from '@sentry/types'; +import { createStackParser, GLOBAL_OBJ, nodeStackLineParser, parseEnvelope } from '@sentry/utils'; +import { TextDecoder, TextEncoder } from 'util'; + +import { createTransport, getCurrentHub, ModuleMetadata } from '../../../src'; +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; + +const stackParser = createStackParser(nodeStackLineParser()); + +const stack = new Error().stack || ''; + +describe('ModuleMetadata integration', () => { + beforeEach(() => { + TestClient.sendEventCalled = undefined; + TestClient.instance = undefined; + + GLOBAL_OBJ._sentryModuleMetadata = GLOBAL_OBJ._sentryModuleMetadata || {}; + GLOBAL_OBJ._sentryModuleMetadata[stack] = { team: 'frontend' }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Adds and removes metadata from stack frames', done => { + const options = getDefaultTestClientOptions({ + dsn: 'https://username@domain/123', + enableSend: true, + stackParser, + integrations: [new ModuleMetadata()], + beforeSend: (event, _hint) => { + // copy the frames since reverse in in-place + const lastFrame = [...(event.exception?.values?.[0].stacktrace?.frames || [])].reverse()[0]; + // Ensure module_metadata is populated in beforeSend callback + expect(lastFrame?.module_metadata).toEqual({ team: 'frontend' }); + return event; + }, + transport: () => + createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, async req => { + const [, items] = parseEnvelope(req.body, new TextEncoder(), new TextDecoder()); + + expect(items[0][1]).toBeDefined(); + const event = items[0][1] as Event; + const error = event.exception?.values?.[0]; + + // Ensure we're looking at the same error we threw + expect(error?.value).toEqual('Some error'); + + const lastFrame = [...(error?.stacktrace?.frames || [])].reverse()[0]; + // Ensure the last frame is in fact for this file + expect(lastFrame?.filename).toEqual(__filename); + + // Ensure module_metadata has been stripped from the event + expect(lastFrame?.module_metadata).toBeUndefined(); + + done(); + return {}; + }), + }); + + const client = new TestClient(options); + const hub = getCurrentHub(); + hub.bindClient(client); + hub.captureException(new Error('Some error')); + }); +}); diff --git a/packages/core/test/mocks/client.ts b/packages/core/test/mocks/client.ts index 7ca980189e19..f7d8830a1402 100644 --- a/packages/core/test/mocks/client.ts +++ b/packages/core/test/mocks/client.ts @@ -54,7 +54,7 @@ export class TestClient extends BaseClient { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types public eventFromException(exception: any): PromiseLike { - return resolvedSyncPromise({ + const event: Event = { exception: { values: [ { @@ -65,7 +65,14 @@ export class TestClient extends BaseClient { }, ], }, - }); + }; + + const frames = this._options.stackParser(exception.stack || '', 1); + if (frames.length && event?.exception?.values?.[0]) { + event.exception.values[0] = { ...event.exception.values[0], stacktrace: { frames } }; + } + + return resolvedSyncPromise(event); } public eventFromMessage( diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index c8b4473d24e4..dc9d486e4f9d 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/e2e-tests", - "version": "7.57.0", + "version": "7.58.1", "license": "MIT", "engines": { "node": ">=10" diff --git a/packages/ember/package.json b/packages/ember/package.json index 7e1310e6119a..5124656e64ea 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/ember", - "version": "7.57.0", + "version": "7.58.1", "description": "Official Sentry SDK for Ember.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember", @@ -29,9 +29,9 @@ }, "dependencies": { "@embroider/macros": "^1.9.0", - "@sentry/browser": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry/browser": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "ember-auto-import": "^1.12.1 || ^2.4.3", "ember-cli-babel": "^7.26.11", "ember-cli-htmlbars": "^6.1.1", diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index bc6964aeb198..8a5c8b770d95 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-config-sdk", - "version": "7.57.0", + "version": "7.58.1", "description": "Official Sentry SDK eslint config", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-config-sdk", @@ -19,8 +19,8 @@ "access": "public" }, "dependencies": { - "@sentry-internal/eslint-plugin-sdk": "7.57.0", - "@sentry-internal/typescript": "7.57.0", + "@sentry-internal/eslint-plugin-sdk": "7.58.1", + "@sentry-internal/typescript": "7.58.1", "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", "eslint-config-prettier": "^6.11.0", diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index b082d120cb07..a4670f569d8b 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-plugin-sdk", - "version": "7.57.0", + "version": "7.58.1", "description": "Official Sentry SDK eslint plugin", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-plugin-sdk", diff --git a/packages/gatsby/.gitignore b/packages/gatsby/.gitignore new file mode 100644 index 000000000000..c318d0bec1a6 --- /dev/null +++ b/packages/gatsby/.gitignore @@ -0,0 +1,2 @@ +gatsby-browser.d.ts +gatsby-node.d.ts diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index d1bc8c916c69..892e0d655273 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/gatsby", - "version": "7.57.0", + "version": "7.58.1", "description": "Official Sentry SDK for Gatsby.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby", @@ -27,10 +27,10 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.57.0", - "@sentry/react": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry/core": "7.58.1", + "@sentry/react": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "@sentry/webpack-plugin": "1.19.0" }, "peerDependencies": { diff --git a/packages/hub/package.json b/packages/hub/package.json index 94b5062e1334..18cc83894874 100644 --- a/packages/hub/package.json +++ b/packages/hub/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/hub", - "version": "7.57.0", + "version": "7.58.1", "description": "Sentry hub which handles global state managment.", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/hub", @@ -23,9 +23,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry/core": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "tslib": "^2.4.1 || ^1.9.3" }, "scripts": { diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json index 88ea627ef2b6..d1c107a217d1 100644 --- a/packages/integration-shims/package.json +++ b/packages/integration-shims/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/integration-shims", - "version": "7.57.0", + "version": "7.58.1", "description": "Shims for integrations in Sentry SDK.", "main": "build/cjs/index.js", "module": "build/esm/index.js", @@ -43,7 +43,7 @@ "url": "https://github.com/getsentry/sentry-javascript/issues" }, "dependencies": { - "@sentry/types": "7.57.0" + "@sentry/types": "7.58.1" }, "engines": { "node": ">=12" diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 2ecdd0e294b1..a0b0eeedc7c9 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/integrations", - "version": "7.57.0", + "version": "7.58.1", "description": "Pluggable integrations that can be used to enhance JS SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/integrations", @@ -23,13 +23,13 @@ } }, "dependencies": { - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "localforage": "^1.8.1", "tslib": "^2.4.1 || ^1.9.3" }, "devDependencies": { - "@sentry/browser": "7.57.0", + "@sentry/browser": "7.58.1", "chai": "^4.1.2" }, "scripts": { diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 28ff7415a0d0..dc6e5f000097 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nextjs", - "version": "7.57.0", + "version": "7.58.1", "description": "Official Sentry SDK for Next.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs", @@ -25,12 +25,12 @@ }, "dependencies": { "@rollup/plugin-commonjs": "24.0.0", - "@sentry/core": "7.57.0", - "@sentry/integrations": "7.57.0", - "@sentry/node": "7.57.0", - "@sentry/react": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry/core": "7.58.1", + "@sentry/integrations": "7.58.1", + "@sentry/node": "7.58.1", + "@sentry/react": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "@sentry/webpack-plugin": "1.20.0", "chalk": "3.0.0", "rollup": "2.78.0", diff --git a/packages/nextjs/src/edge/edgeclient.ts b/packages/nextjs/src/edge/edgeclient.ts index ce13d6666448..39b795c81a53 100644 --- a/packages/nextjs/src/edge/edgeclient.ts +++ b/packages/nextjs/src/edge/edgeclient.ts @@ -1,14 +1,22 @@ import type { Scope } from '@sentry/core'; -import { addTracingExtensions, BaseClient, createCheckInEnvelope, SDK_VERSION } from '@sentry/core'; +import { + addTracingExtensions, + BaseClient, + createCheckInEnvelope, + getDynamicSamplingContextFromClient, + SDK_VERSION, +} from '@sentry/core'; import type { CheckIn, ClientOptions, + DynamicSamplingContext, Event, EventHint, MonitorConfig, SerializedCheckIn, Severity, SeverityLevel, + TraceContext, } from '@sentry/types'; import { logger, uuid4 } from '@sentry/utils'; @@ -72,7 +80,7 @@ export class EdgeClient extends BaseClient { * @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want * to create a monitor automatically when sending a check in. */ - public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig): string { + public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string { const id = checkIn.status !== 'in_progress' && checkIn.checkInId ? checkIn.checkInId : uuid4(); if (!this._isEnabled()) { __DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture checkin.'); @@ -103,7 +111,20 @@ export class EdgeClient extends BaseClient { }; } - const envelope = createCheckInEnvelope(serializedCheckIn, this.getSdkMetadata(), tunnel, this.getDsn()); + const [dynamicSamplingContext, traceContext] = this._getTraceInfoFromScope(scope); + if (traceContext) { + serializedCheckIn.contexts = { + trace: traceContext, + }; + } + + const envelope = createCheckInEnvelope( + serializedCheckIn, + dynamicSamplingContext, + this.getSdkMetadata(), + tunnel, + this.getDsn(), + ); __DEBUG_BUILD__ && logger.info('Sending checkin:', checkIn.monitorSlug, checkIn.status); void this._sendEnvelope(envelope); @@ -124,4 +145,30 @@ export class EdgeClient extends BaseClient { event.server_name = event.server_name || process.env.SENTRY_NAME; return super._prepareEvent(event, hint, scope); } + + /** Extract trace information from scope */ + private _getTraceInfoFromScope( + scope: Scope | undefined, + ): [dynamicSamplingContext: Partial | undefined, traceContext: TraceContext | undefined] { + if (!scope) { + return [undefined, undefined]; + } + + const span = scope.getSpan(); + if (span) { + return [span?.transaction?.getDynamicSamplingContext(), span?.getTraceContext()]; + } + + const { traceId, spanId, parentSpanId, dsc } = scope.getPropagationContext(); + const traceContext: TraceContext = { + trace_id: traceId, + span_id: spanId, + parent_span_id: parentSpanId, + }; + if (dsc) { + return [dsc, traceContext]; + } + + return [getDynamicSamplingContextFromClient(traceId, this, scope), traceContext]; + } } diff --git a/packages/nextjs/test/integration/sentry.client.config.js b/packages/nextjs/test/integration/sentry.client.config.js index e8da7a90914b..dbb6e760ea8d 100644 --- a/packages/nextjs/test/integration/sentry.client.config.js +++ b/packages/nextjs/test/integration/sentry.client.config.js @@ -3,7 +3,7 @@ import { Integrations } from '@sentry/tracing'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - tracesSampleRate: 1, + tracesSampler: () => true, debug: process.env.SDK_DEBUG, integrations: [ diff --git a/packages/nextjs/test/integration/sentry.edge.config.js b/packages/nextjs/test/integration/sentry.edge.config.js index 8d83922b637d..36600e702048 100644 --- a/packages/nextjs/test/integration/sentry.edge.config.js +++ b/packages/nextjs/test/integration/sentry.edge.config.js @@ -2,6 +2,7 @@ import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - tracesSampleRate: 1, + tracesSampleRate: 1.0, + tracePropagationTargets: ['http://example.com'], debug: process.env.SDK_DEBUG, }); diff --git a/packages/nextjs/test/integration/sentry.server.config.js b/packages/nextjs/test/integration/sentry.server.config.js index da0759f4475e..54c5db73a1a2 100644 --- a/packages/nextjs/test/integration/sentry.server.config.js +++ b/packages/nextjs/test/integration/sentry.server.config.js @@ -2,7 +2,8 @@ import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - tracesSampleRate: 1, + tracesSampleRate: 1.0, + tracePropagationTargets: ['http://example.com'], debug: process.env.SDK_DEBUG, integrations: defaults => [ diff --git a/packages/node-integration-tests/package.json b/packages/node-integration-tests/package.json index 0745880eac07..03087b94a2ce 100644 --- a/packages/node-integration-tests/package.json +++ b/packages/node-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/node-integration-tests", - "version": "7.57.0", + "version": "7.58.1", "license": "MIT", "engines": { "node": ">=10" diff --git a/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts b/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts index f582288f8314..750f49b40210 100644 --- a/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts +++ b/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts @@ -12,6 +12,7 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', environment: 'prod', + tracePropagationTargets: [/^(?!.*express).*$/], // eslint-disable-next-line deprecation/deprecation integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })], tracesSampleRate: 1.0, diff --git a/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts b/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts index bcf53c1f4a30..4a3303af9863 100644 --- a/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts +++ b/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts @@ -14,7 +14,8 @@ test('should attach a `baggage` header to an outgoing request.', async () => { host: 'somewhere.not.sentry', baggage: 'sentry-environment=prod,sentry-release=1.0,sentry-user_segment=SegmentA,sentry-public_key=public' + - ',sentry-trace_id=86f39e84263a4de99c326acab3bfe3bd,sentry-sample_rate=1,sentry-transaction=GET%20%2Ftest%2Fexpress', + ',sentry-trace_id=86f39e84263a4de99c326acab3bfe3bd,sentry-sample_rate=1,sentry-transaction=GET%20%2Ftest%2Fexpress' + + ',sentry-sampled=true', }, }); }); diff --git a/packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/server.ts b/packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/server.ts index 6674e2a1bb77..93738d4c0a12 100644 --- a/packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/server.ts +++ b/packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/server.ts @@ -12,6 +12,8 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', environment: 'prod', + // disable requests to /express + tracePropagationTargets: [/^(?!.*express).*$/], // eslint-disable-next-line deprecation/deprecation integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })], tracesSampleRate: 1.0, diff --git a/packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/server.ts b/packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/server.ts index 86cfa033b79e..867bb0e6131e 100644 --- a/packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/server.ts +++ b/packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/server.ts @@ -12,6 +12,8 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', environment: 'prod', + // disable requests to /express + tracePropagationTargets: [/^(?!.*express).*$/], // eslint-disable-next-line deprecation/deprecation integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })], tracesSampleRate: 1.0, diff --git a/packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts b/packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts index 78e24ffa9f10..21738f3b3fb8 100644 --- a/packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts +++ b/packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts @@ -12,6 +12,8 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', environment: 'prod', + // disable requests to /express + tracePropagationTargets: [/^(?!.*express).*$/], // eslint-disable-next-line deprecation/deprecation integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })], tracesSampleRate: 1.0, diff --git a/packages/node-integration-tests/suites/express/sentry-trace/server.ts b/packages/node-integration-tests/suites/express/sentry-trace/server.ts index 76bb399e3bc0..1cfc02648f02 100644 --- a/packages/node-integration-tests/suites/express/sentry-trace/server.ts +++ b/packages/node-integration-tests/suites/express/sentry-trace/server.ts @@ -12,6 +12,7 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', environment: 'prod', + tracePropagationTargets: [/^(?!.*express).*$/], // eslint-disable-next-line deprecation/deprecation integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })], tracesSampleRate: 1.0, diff --git a/packages/node-integration-tests/suites/express/tracing/server.ts b/packages/node-integration-tests/suites/express/tracing/server.ts index e857621ad22e..1c56a81fef98 100644 --- a/packages/node-integration-tests/suites/express/tracing/server.ts +++ b/packages/node-integration-tests/suites/express/tracing/server.ts @@ -7,6 +7,8 @@ const app = express(); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + // disable attaching headers to /test/* endpoints + tracePropagationTargets: [/^(?!.*test).*$/], integrations: [new Sentry.Integrations.Http({ tracing: true }), new Sentry.Integrations.Express({ app })], tracesSampleRate: 1.0, }); diff --git a/packages/node-integration-tests/utils/index.ts b/packages/node-integration-tests/utils/index.ts index cde2cb745cd9..88120853ee69 100644 --- a/packages/node-integration-tests/utils/index.ts +++ b/packages/node-integration-tests/utils/index.ts @@ -202,11 +202,11 @@ export class TestEnv { */ public async getAPIResponse( url?: string, - headers?: Record, + headers: Record = {}, endServer: boolean = true, ): Promise { try { - const { data } = await axios.get(url || this.url, { headers: headers || {} }); + const { data } = await axios.get(url || this.url, { headers }); return data; } finally { await Sentry.flush(); diff --git a/packages/node/package.json b/packages/node/package.json index 74c86c613e5c..94eda3d06dee 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node", - "version": "7.57.0", + "version": "7.58.1", "description": "Official Sentry SDK for Node.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node", @@ -23,10 +23,10 @@ "access": "public" }, "dependencies": { - "@sentry-internal/tracing": "7.57.0", - "@sentry/core": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry-internal/tracing": "7.58.1", + "@sentry/core": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "cookie": "^0.4.1", "https-proxy-agent": "^5.0.0", "lru_map": "^0.3.3", diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts index ccdbeda279e1..50af36448046 100644 --- a/packages/node/src/client.ts +++ b/packages/node/src/client.ts @@ -1,13 +1,22 @@ import type { Scope } from '@sentry/core'; -import { addTracingExtensions, BaseClient, createCheckInEnvelope, SDK_VERSION, SessionFlusher } from '@sentry/core'; +import { + addTracingExtensions, + BaseClient, + createCheckInEnvelope, + getDynamicSamplingContextFromClient, + SDK_VERSION, + SessionFlusher, +} from '@sentry/core'; import type { CheckIn, + DynamicSamplingContext, Event, EventHint, MonitorConfig, SerializedCheckIn, Severity, SeverityLevel, + TraceContext, } from '@sentry/types'; import { logger, resolvedSyncPromise, uuid4 } from '@sentry/utils'; import * as os from 'os'; @@ -154,7 +163,7 @@ export class NodeClient extends BaseClient { * to create a monitor automatically when sending a check in. * @returns A string representing the id of the check in. */ - public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig): string { + public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string { const id = checkIn.status !== 'in_progress' && checkIn.checkInId ? checkIn.checkInId : uuid4(); if (!this._isEnabled()) { __DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture checkin.'); @@ -185,7 +194,20 @@ export class NodeClient extends BaseClient { }; } - const envelope = createCheckInEnvelope(serializedCheckIn, this.getSdkMetadata(), tunnel, this.getDsn()); + const [dynamicSamplingContext, traceContext] = this._getTraceInfoFromScope(scope); + if (traceContext) { + serializedCheckIn.contexts = { + trace: traceContext, + }; + } + + const envelope = createCheckInEnvelope( + serializedCheckIn, + dynamicSamplingContext, + this.getSdkMetadata(), + tunnel, + this.getDsn(), + ); __DEBUG_BUILD__ && logger.info('Sending checkin:', checkIn.monitorSlug, checkIn.status); void this._sendEnvelope(envelope); @@ -220,4 +242,30 @@ export class NodeClient extends BaseClient { this._sessionFlusher.incrementSessionStatusCount(); } } + + /** Extract trace information from scope */ + private _getTraceInfoFromScope( + scope: Scope | undefined, + ): [dynamicSamplingContext: Partial | undefined, traceContext: TraceContext | undefined] { + if (!scope) { + return [undefined, undefined]; + } + + const span = scope.getSpan(); + if (span) { + return [span?.transaction?.getDynamicSamplingContext(), span?.getTraceContext()]; + } + + const { traceId, spanId, parentSpanId, dsc } = scope.getPropagationContext(); + const traceContext: TraceContext = { + trace_id: traceId, + span_id: spanId, + parent_span_id: parentSpanId, + }; + if (dsc) { + return [dsc, traceContext]; + } + + return [getDynamicSamplingContextFromClient(traceId, this, scope), traceContext]; + } } diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 6d2545c3bf9d..1d1ef3bed507 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -53,15 +53,6 @@ export function tracingHandler(): ( return next(); } - if (!hasTracingEnabled(options)) { - __DEBUG_BUILD__ && - logger.warn( - 'Sentry `tracingHandler` is being used, but tracing is disabled. Please enable tracing by setting ' + - 'either `tracesSampleRate` or `tracesSampler` in your `Sentry.init()` options.', - ); - return next(); - } - const sentryTrace = req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined; const baggage = req.headers?.baggage; const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( @@ -70,6 +61,10 @@ export function tracingHandler(): ( ); hub.getScope().setPropagationContext(propagationContext); + if (!hasTracingEnabled(options)) { + return next(); + } + const [name, source] = extractPathForTransaction(req, { path: true, method: true }); const transaction = startTransaction( { diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 54d761861348..b210500e90e0 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -1,14 +1,26 @@ import type { Hub } from '@sentry/core'; -import { getCurrentHub } from '@sentry/core'; -import type { EventProcessor, Integration, SanitizedRequestData, Span, TracePropagationTargets } from '@sentry/types'; -import { dynamicSamplingContextToSentryBaggageHeader, fill, logger, stringMatchesSomePattern } from '@sentry/utils'; +import { getCurrentHub, getDynamicSamplingContextFromClient } from '@sentry/core'; +import type { + DynamicSamplingContext, + EventProcessor, + Integration, + SanitizedRequestData, + TracePropagationTargets, +} from '@sentry/types'; +import { + dynamicSamplingContextToSentryBaggageHeader, + fill, + generateSentryTraceHeader, + logger, + stringMatchesSomePattern, +} from '@sentry/utils'; import type * as http from 'http'; import type * as https from 'https'; import { LRUMap } from 'lru_map'; import type { NodeClient } from '../client'; import { NODE_VERSION } from '../nodeVersion'; -import type { RequestMethod, RequestMethodArgs } from './utils/http'; +import type { RequestMethod, RequestMethodArgs, RequestOptions } from './utils/http'; import { cleanSpanDescription, extractRawUrl, extractUrl, isSentryRequest, normalizeRequestArgs } from './utils/http'; interface TracingOptions { @@ -96,13 +108,20 @@ export class Http implements Integration { return; } - // TODO (v8): `tracePropagationTargets` and `shouldCreateSpanForRequest` will be removed from clientOptions - // and we will no longer have to do this optional merge, we can just pass `this._tracing` directly. - const tracingOptions = this._tracing ? { ...clientOptions, ...this._tracing } : undefined; + const shouldCreateSpanForRequest = + // eslint-disable-next-line deprecation/deprecation + this._tracing?.shouldCreateSpanForRequest || clientOptions?.shouldCreateSpanForRequest; + // eslint-disable-next-line deprecation/deprecation + const tracePropagationTargets = clientOptions?.tracePropagationTargets || this._tracing?.tracePropagationTargets; // eslint-disable-next-line @typescript-eslint/no-var-requires const httpModule = require('http'); - const wrappedHttpHandlerMaker = _createWrappedRequestMethodFactory(this._breadcrumbs, tracingOptions, httpModule); + const wrappedHttpHandlerMaker = _createWrappedRequestMethodFactory( + httpModule, + this._breadcrumbs, + shouldCreateSpanForRequest, + tracePropagationTargets, + ); fill(httpModule, 'get', wrappedHttpHandlerMaker); fill(httpModule, 'request', wrappedHttpHandlerMaker); @@ -113,9 +132,10 @@ export class Http implements Integration { // eslint-disable-next-line @typescript-eslint/no-var-requires const httpsModule = require('https'); const wrappedHttpsHandlerMaker = _createWrappedRequestMethodFactory( - this._breadcrumbs, - tracingOptions, httpsModule, + this._breadcrumbs, + shouldCreateSpanForRequest, + tracePropagationTargets, ); fill(httpsModule, 'get', wrappedHttpsHandlerMaker); fill(httpsModule, 'request', wrappedHttpsHandlerMaker); @@ -138,16 +158,17 @@ type WrappedRequestMethodFactory = (original: OriginalRequestMethod) => WrappedR * @returns A function which accepts the exiting handler and returns a wrapped handler */ function _createWrappedRequestMethodFactory( - breadcrumbsEnabled: boolean, - tracingOptions: TracingOptions | undefined, httpModule: typeof http | typeof https, + breadcrumbsEnabled: boolean, + shouldCreateSpanForRequest: ((url: string) => boolean) | undefined, + tracePropagationTargets: TracePropagationTargets | undefined, ): WrappedRequestMethodFactory { // We're caching results so we don't have to recompute regexp every time we create a request. const createSpanUrlMap = new LRUMap(100); const headersUrlMap = new LRUMap(100); const shouldCreateSpan = (url: string): boolean => { - if (tracingOptions?.shouldCreateSpanForRequest === undefined) { + if (shouldCreateSpanForRequest === undefined) { return true; } @@ -156,14 +177,13 @@ function _createWrappedRequestMethodFactory( return cachedDecision; } - const decision = tracingOptions.shouldCreateSpanForRequest(url); + const decision = shouldCreateSpanForRequest(url); createSpanUrlMap.set(url, decision); return decision; }; const shouldAttachTraceData = (url: string): boolean => { - // eslint-disable-next-line deprecation/deprecation - if (tracingOptions?.tracePropagationTargets === undefined) { + if (tracePropagationTargets === undefined) { return true; } @@ -172,12 +192,41 @@ function _createWrappedRequestMethodFactory( return cachedDecision; } - // eslint-disable-next-line deprecation/deprecation - const decision = stringMatchesSomePattern(url, tracingOptions.tracePropagationTargets); + const decision = stringMatchesSomePattern(url, tracePropagationTargets); headersUrlMap.set(url, decision); return decision; }; + /** + * Captures Breadcrumb based on provided request/response pair + */ + function addRequestBreadcrumb( + event: string, + requestSpanData: SanitizedRequestData, + req: http.ClientRequest, + res?: http.IncomingMessage, + ): void { + if (!getCurrentHub().getIntegration(Http)) { + return; + } + + getCurrentHub().addBreadcrumb( + { + category: 'http', + data: { + status_code: res && res.statusCode, + ...requestSpanData, + }, + type: 'http', + }, + { + event, + request: req, + response: res, + }, + ); + } + return function wrappedRequestMethodFactory(originalRequestMethod: OriginalRequestMethod): WrappedRequestMethod { return function wrappedMethod(this: unknown, ...args: RequestMethodArgs): http.ClientRequest { const requestArgs = normalizeRequestArgs(httpModule, args); @@ -191,74 +240,38 @@ function _createWrappedRequestMethodFactory( return originalRequestMethod.apply(httpModule, requestArgs); } - let requestSpan: Span | undefined; - const parentSpan = getCurrentHub().getScope().getSpan(); - - const method = requestOptions.method || 'GET'; - const requestSpanData: SanitizedRequestData = { - url: requestUrl, - 'http.method': method, - }; - if (requestOptions.hash) { - // strip leading "#" - requestSpanData['http.fragment'] = requestOptions.hash.substring(1); - } - if (requestOptions.search) { - // strip leading "?" - requestSpanData['http.query'] = requestOptions.search.substring(1); - } + const hub = getCurrentHub(); + const scope = hub.getScope(); + const parentSpan = scope.getSpan(); - if (tracingOptions && shouldCreateSpan(rawRequestUrl)) { - if (parentSpan) { - requestSpan = parentSpan.startChild({ - description: `${method} ${requestSpanData.url}`, + const data = getRequestSpanData(requestUrl, requestOptions); + + const requestSpan = shouldCreateSpan(rawRequestUrl) + ? parentSpan?.startChild({ op: 'http.client', - data: requestSpanData, - }); - - if (shouldAttachTraceData(rawRequestUrl)) { - const sentryTraceHeader = requestSpan.toTraceparent(); - __DEBUG_BUILD__ && - logger.log( - `[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to "${requestUrl}": `, - ); - - requestOptions.headers = { - ...requestOptions.headers, - 'sentry-trace': sentryTraceHeader, - }; - - if (parentSpan.transaction) { - const dynamicSamplingContext = parentSpan.transaction.getDynamicSamplingContext(); - const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); - - let newBaggageHeaderField; - if (!requestOptions.headers || !requestOptions.headers.baggage) { - newBaggageHeaderField = sentryBaggageHeader; - } else if (!sentryBaggageHeader) { - newBaggageHeaderField = requestOptions.headers.baggage; - } else if (Array.isArray(requestOptions.headers.baggage)) { - newBaggageHeaderField = [...requestOptions.headers.baggage, sentryBaggageHeader]; - } else { - // Type-cast explanation: - // Technically this the following could be of type `(number | string)[]` but for the sake of simplicity - // we say this is undefined behaviour, since it would not be baggage spec conform if the user did this. - newBaggageHeaderField = [requestOptions.headers.baggage, sentryBaggageHeader] as string[]; - } - - requestOptions.headers = { - ...requestOptions.headers, - // Setting a hader to `undefined` will crash in node so we only set the baggage header when it's defined - ...(newBaggageHeaderField && { baggage: newBaggageHeaderField }), - }; - } - } else { - __DEBUG_BUILD__ && - logger.log( - `[Tracing] Not adding sentry-trace header to outgoing request (${requestUrl}) due to mismatching tracePropagationTargets option.`, - ); - } + description: `${data['http.method']} ${data.url}`, + data, + }) + : undefined; + + if (shouldAttachTraceData(rawRequestUrl)) { + if (requestSpan) { + const sentryTraceHeader = requestSpan.toTraceparent(); + const dynamicSamplingContext = requestSpan?.transaction?.getDynamicSamplingContext(); + addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, dynamicSamplingContext); + } else { + const client = hub.getClient(); + const { traceId, sampled, dsc } = scope.getPropagationContext(); + const sentryTraceHeader = generateSentryTraceHeader(traceId, undefined, sampled); + const dynamicSamplingContext = + dsc || (client ? getDynamicSamplingContextFromClient(traceId, client, scope) : undefined); + addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, dynamicSamplingContext); } + } else { + __DEBUG_BUILD__ && + logger.log( + `[Tracing] Not adding sentry-trace header to outgoing request (${requestUrl}) due to mismatching tracePropagationTargets option.`, + ); } // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access @@ -268,7 +281,7 @@ function _createWrappedRequestMethodFactory( // eslint-disable-next-line @typescript-eslint/no-this-alias const req = this; if (breadcrumbsEnabled) { - addRequestBreadcrumb('response', requestSpanData, req, res); + addRequestBreadcrumb('response', data, req, res); } if (requestSpan) { if (res.statusCode) { @@ -283,7 +296,7 @@ function _createWrappedRequestMethodFactory( const req = this; if (breadcrumbsEnabled) { - addRequestBreadcrumb('error', requestSpanData, req); + addRequestBreadcrumb('error', data, req); } if (requestSpan) { requestSpan.setHttpStatus(500); @@ -295,32 +308,55 @@ function _createWrappedRequestMethodFactory( }; } -/** - * Captures Breadcrumb based on provided request/response pair - */ -function addRequestBreadcrumb( - event: string, - requestSpanData: SanitizedRequestData, - req: http.ClientRequest, - res?: http.IncomingMessage, +function addHeadersToRequestOptions( + requestOptions: RequestOptions, + requestUrl: string, + sentryTraceHeader: string, + dynamicSamplingContext: Partial | undefined, ): void { - if (!getCurrentHub().getIntegration(Http)) { - return; + __DEBUG_BUILD__ && + logger.log(`[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to "${requestUrl}": `); + const sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); + const sentryBaggageHeader = normalizeBaggageHeader(requestOptions, sentryBaggage); + requestOptions.headers = { + ...requestOptions.headers, + 'sentry-trace': sentryTraceHeader, + // Setting a header to `undefined` will crash in node so we only set the baggage header when it's defined + ...(sentryBaggageHeader && { baggage: sentryBaggageHeader }), + }; +} + +function getRequestSpanData(requestUrl: string, requestOptions: RequestOptions): SanitizedRequestData { + const method = requestOptions.method || 'GET'; + const data: SanitizedRequestData = { + url: requestUrl, + 'http.method': method, + }; + if (requestOptions.hash) { + // strip leading "#" + data['http.fragment'] = requestOptions.hash.substring(1); + } + if (requestOptions.search) { + // strip leading "?" + data['http.query'] = requestOptions.search.substring(1); } + return data; +} - getCurrentHub().addBreadcrumb( - { - category: 'http', - data: { - status_code: res && res.statusCode, - ...requestSpanData, - }, - type: 'http', - }, - { - event, - request: req, - response: res, - }, - ); +function normalizeBaggageHeader( + requestOptions: RequestOptions, + sentryBaggageHeader: string | undefined, +): string | string[] | undefined { + if (!requestOptions.headers || !requestOptions.headers.baggage) { + return sentryBaggageHeader; + } else if (!sentryBaggageHeader) { + return requestOptions.headers.baggage as string | string[]; + } else if (Array.isArray(requestOptions.headers.baggage)) { + return [...requestOptions.headers.baggage, sentryBaggageHeader]; + } + + // Type-cast explanation: + // Technically this the following could be of type `(number | string)[]` but for the sake of simplicity + // we say this is undefined behaviour, since it would not be baggage spec conform if the user did this. + return [requestOptions.headers.baggage, sentryBaggageHeader] as string[]; } diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index 38f920283b7e..0c69dec37d3f 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -1,8 +1,9 @@ -import type { Hub } from '@sentry/core'; -import type { EventProcessor, Integration } from '@sentry/types'; +import { getCurrentHub, getDynamicSamplingContextFromClient } from '@sentry/core'; +import type { EventProcessor, Integration, Span } from '@sentry/types'; import { dynamicRequire, dynamicSamplingContextToSentryBaggageHeader, + generateSentryTraceHeader, getSanitizedUrlString, parseUrl, stringMatchesSomePattern, @@ -12,7 +13,13 @@ import { LRUMap } from 'lru_map'; import type { NodeClient } from '../../client'; import { NODE_VERSION } from '../../nodeVersion'; import { isSentryRequest } from '../utils/http'; -import type { DiagnosticsChannel, RequestCreateMessage, RequestEndMessage, RequestErrorMessage } from './types'; +import type { + DiagnosticsChannel, + RequestCreateMessage, + RequestEndMessage, + RequestErrorMessage, + RequestWithSentry, +} from './types'; export enum ChannelName { // https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md#undicirequestcreate @@ -81,7 +88,7 @@ export class Undici implements Integration { /** * @inheritDoc */ - public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { + public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void): void { // Requires Node 16+ to use the diagnostics_channel API. if (NODE_VERSION.major && NODE_VERSION.major < 16) { return; @@ -99,169 +106,205 @@ export class Undici implements Integration { return; } - const shouldCreateSpan = (url: string): boolean => { - if (this._options.shouldCreateSpanForRequest === undefined) { + // https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md + ds.subscribe(ChannelName.RequestCreate, this._onRequestCreate); + ds.subscribe(ChannelName.RequestEnd, this._onRequestEnd); + ds.subscribe(ChannelName.RequestError, this._onRequestError); + } + + /** Helper that wraps shouldCreateSpanForRequest option */ + private _shouldCreateSpan(url: string): boolean { + if (this._options.shouldCreateSpanForRequest === undefined) { + return true; + } + + const cachedDecision = this._createSpanUrlMap.get(url); + if (cachedDecision !== undefined) { + return cachedDecision; + } + + const decision = this._options.shouldCreateSpanForRequest(url); + this._createSpanUrlMap.set(url, decision); + return decision; + } + + private _onRequestCreate = (message: unknown): void => { + const hub = getCurrentHub(); + if (!hub.getIntegration(Undici)) { + return; + } + + const { request } = message as RequestCreateMessage; + + const stringUrl = request.origin ? request.origin.toString() + request.path : request.path; + + if (isSentryRequest(stringUrl) || request.__sentry_span__ !== undefined) { + return; + } + + const client = hub.getClient(); + if (!client) { + return; + } + + const clientOptions = client.getOptions(); + const scope = hub.getScope(); + + const parentSpan = scope.getSpan(); + + const span = this._shouldCreateSpan(stringUrl) ? createRequestSpan(parentSpan, request, stringUrl) : undefined; + if (span) { + request.__sentry_span__ = span; + } + + const shouldAttachTraceData = (url: string): boolean => { + if (clientOptions.tracePropagationTargets === undefined) { return true; } - const cachedDecision = this._createSpanUrlMap.get(url); + const cachedDecision = this._headersUrlMap.get(url); if (cachedDecision !== undefined) { return cachedDecision; } - const decision = this._options.shouldCreateSpanForRequest(url); - this._createSpanUrlMap.set(url, decision); + const decision = stringMatchesSomePattern(url, clientOptions.tracePropagationTargets); + this._headersUrlMap.set(url, decision); return decision; }; - // https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md - ds.subscribe(ChannelName.RequestCreate, message => { - const hub = getCurrentHub(); - if (!hub.getIntegration(Undici)) { - return; + if (shouldAttachTraceData(stringUrl)) { + if (span) { + const dynamicSamplingContext = span?.transaction?.getDynamicSamplingContext(); + const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); + + setHeadersOnRequest(request, span.toTraceparent(), sentryBaggageHeader); + } else { + const { traceId, sampled, dsc } = scope.getPropagationContext(); + const sentryTrace = generateSentryTraceHeader(traceId, undefined, sampled); + const dynamicSamplingContext = dsc || getDynamicSamplingContextFromClient(traceId, client, scope); + const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); + setHeadersOnRequest(request, sentryTrace, sentryBaggageHeader); } + } + }; - const { request } = message as RequestCreateMessage; + private _onRequestEnd = (message: unknown): void => { + const hub = getCurrentHub(); + if (!hub.getIntegration(Undici)) { + return; + } - const stringUrl = request.origin ? request.origin.toString() + request.path : request.path; - const url = parseUrl(stringUrl); + const { request, response } = message as RequestEndMessage; - if (isSentryRequest(stringUrl) || request.__sentry__ !== undefined) { - return; - } + const stringUrl = request.origin ? request.origin.toString() + request.path : request.path; - const client = hub.getClient(); - const scope = hub.getScope(); - - const activeSpan = scope.getSpan(); - - if (activeSpan && client) { - const clientOptions = client.getOptions(); - - if (shouldCreateSpan(stringUrl)) { - const method = request.method || 'GET'; - const data: Record = { - 'http.method': method, - }; - if (url.search) { - data['http.query'] = url.search; - } - if (url.hash) { - data['http.fragment'] = url.hash; - } - const span = activeSpan.startChild({ - op: 'http.client', - description: `${method} ${getSanitizedUrlString(url)}`, - data, - }); - request.__sentry__ = span; - - const shouldAttachTraceData = (url: string): boolean => { - if (clientOptions.tracePropagationTargets === undefined) { - return true; - } - - const cachedDecision = this._headersUrlMap.get(url); - if (cachedDecision !== undefined) { - return cachedDecision; - } - - const decision = stringMatchesSomePattern(url, clientOptions.tracePropagationTargets); - this._headersUrlMap.set(url, decision); - return decision; - }; - - if (shouldAttachTraceData(stringUrl)) { - request.addHeader('sentry-trace', span.toTraceparent()); - if (span.transaction) { - const dynamicSamplingContext = span.transaction.getDynamicSamplingContext(); - const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); - if (sentryBaggageHeader) { - request.addHeader('baggage', sentryBaggageHeader); - } - } - } - } - } - }); + if (isSentryRequest(stringUrl)) { + return; + } - ds.subscribe(ChannelName.RequestEnd, message => { - const hub = getCurrentHub(); - if (!hub.getIntegration(Undici)) { - return; - } + const span = request.__sentry_span__; + if (span) { + span.setHttpStatus(response.statusCode); + span.finish(); + } - const { request, response } = message as RequestEndMessage; + if (this._options.breadcrumbs) { + hub.addBreadcrumb( + { + category: 'http', + data: { + method: request.method, + status_code: response.statusCode, + url: stringUrl, + }, + type: 'http', + }, + { + event: 'response', + request, + response, + }, + ); + } + }; - const stringUrl = request.origin ? request.origin.toString() + request.path : request.path; + private _onRequestError = (message: unknown): void => { + const hub = getCurrentHub(); + if (!hub.getIntegration(Undici)) { + return; + } - if (isSentryRequest(stringUrl)) { - return; - } + const { request } = message as RequestErrorMessage; - const span = request.__sentry__; - if (span) { - span.setHttpStatus(response.statusCode); - span.finish(); - } + const stringUrl = request.origin ? request.origin.toString() + request.path : request.path; - if (this._options.breadcrumbs) { - hub.addBreadcrumb( - { - category: 'http', - data: { - method: request.method, - status_code: response.statusCode, - url: stringUrl, - }, - type: 'http', - }, - { - event: 'response', - request, - response, - }, - ); - } - }); + if (isSentryRequest(stringUrl)) { + return; + } - ds.subscribe(ChannelName.RequestError, message => { - const hub = getCurrentHub(); - if (!hub.getIntegration(Undici)) { - return; - } + const span = request.__sentry_span__; + if (span) { + span.setStatus('internal_error'); + span.finish(); + } - const { request } = message as RequestErrorMessage; + if (this._options.breadcrumbs) { + hub.addBreadcrumb( + { + category: 'http', + data: { + method: request.method, + url: stringUrl, + }, + level: 'error', + type: 'http', + }, + { + event: 'error', + request, + }, + ); + } + }; +} - const stringUrl = request.origin ? request.origin.toString() + request.path : request.path; +function setHeadersOnRequest( + request: RequestWithSentry, + sentryTrace: string, + sentryBaggageHeader: string | undefined, +): void { + if (request.__sentry_has_headers__) { + return; + } - if (isSentryRequest(stringUrl)) { - return; - } + request.addHeader('sentry-trace', sentryTrace); + if (sentryBaggageHeader) { + request.addHeader('baggage', sentryBaggageHeader); + } - const span = request.__sentry__; - if (span) { - span.setStatus('internal_error'); - span.finish(); - } + request.__sentry_has_headers__ = true; +} - if (this._options.breadcrumbs) { - hub.addBreadcrumb( - { - category: 'http', - data: { - method: request.method, - url: stringUrl, - }, - level: 'error', - type: 'http', - }, - { - event: 'error', - request, - }, - ); - } - }); +function createRequestSpan( + activeSpan: Span | undefined, + request: RequestWithSentry, + stringUrl: string, +): Span | undefined { + const url = parseUrl(stringUrl); + + const method = request.method || 'GET'; + const data: Record = { + 'http.method': method, + }; + if (url.search) { + data['http.query'] = url.search; + } + if (url.hash) { + data['http.fragment'] = url.hash; } + return activeSpan?.startChild({ + op: 'http.client', + description: `${method} ${getSanitizedUrlString(url)}`, + data, + }); } diff --git a/packages/node/src/integrations/undici/types.ts b/packages/node/src/integrations/undici/types.ts index c2d2db125195..f56e708f456c 100644 --- a/packages/node/src/integrations/undici/types.ts +++ b/packages/node/src/integrations/undici/types.ts @@ -234,7 +234,8 @@ export interface UndiciResponse { } export interface RequestWithSentry extends UndiciRequest { - __sentry__?: Span; + __sentry_span__?: Span; + __sentry_has_headers__?: boolean; } export interface RequestCreateMessage { diff --git a/packages/node/test/context-lines.test.ts b/packages/node/test/context-lines.test.ts index b469796214b1..cfdd44e8b840 100644 --- a/packages/node/test/context-lines.test.ts +++ b/packages/node/test/context-lines.test.ts @@ -107,7 +107,8 @@ describe('ContextLines', () => { expect(readFileSpy).toHaveBeenCalledTimes(0); }); }); - test.only('does not attempt to readfile multiple times if it fails', async () => { + + test('does not attempt to readfile multiple times if it fails', async () => { expect.assertions(1); contextLines = new ContextLines({}); diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index b4d4734e88bd..5cbea24a9b01 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -54,6 +54,21 @@ describe('tracing', () => { return transaction; } + function getHub(customOptions: Partial = {}) { + const options = getDefaultNodeClientOptions({ + dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', + tracesSampleRate: 1.0, + integrations: [new HttpIntegration({ tracing: true })], + release: '1.0.0', + environment: 'production', + ...customOptions, + }); + const hub = new Hub(new NodeClient(options)); + jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + + return hub; + } + it("creates a span for each outgoing non-sentry request when there's a transaction on the scope", () => { nock('http://dogs.are.great').get('/').reply(200); @@ -116,7 +131,8 @@ describe('tracing', () => { expect(baggageHeader).toEqual( 'sentry-environment=production,sentry-release=1.0.0,' + 'sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,' + - 'sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1,sentry-transaction=dogpark', + 'sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1,' + + 'sentry-transaction=dogpark,sentry-sampled=true', ); }); @@ -128,10 +144,10 @@ describe('tracing', () => { const request = http.get({ host: 'http://dogs.are.great/', headers: { baggage: 'dog=great' } }); const baggageHeader = request.getHeader('baggage') as string; - expect(baggageHeader).toEqual([ - 'dog=great', - 'sentry-environment=production,sentry-release=1.0.0,sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1,sentry-transaction=dogpark', - ]); + expect(baggageHeader[0]).toEqual('dog=great'); + expect(baggageHeader[1]).toEqual( + 'sentry-environment=production,sentry-release=1.0.0,sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1,sentry-transaction=dogpark,sentry-sampled=true', + ); }); it('adds the transaction name to the the baggage header if a valid transaction source is set', async () => { @@ -144,7 +160,7 @@ describe('tracing', () => { expect(baggageHeader).toEqual([ 'dog=great', - 'sentry-environment=production,sentry-release=1.0.0,sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1,sentry-transaction=dogpark', + 'sentry-environment=production,sentry-release=1.0.0,sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1,sentry-transaction=dogpark,sentry-sampled=true', ]); }); @@ -158,10 +174,55 @@ describe('tracing', () => { expect(baggageHeader).toEqual([ 'dog=great', - 'sentry-environment=production,sentry-release=1.0.0,sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1', + 'sentry-environment=production,sentry-release=1.0.0,sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1,sentry-sampled=true', ]); }); + it('generates and uses propagation context to attach baggage and sentry-trace header', async () => { + nock('http://dogs.are.great').get('/').reply(200); + + const request = http.get('http://dogs.are.great/'); + const sentryTraceHeader = request.getHeader('sentry-trace') as string; + const baggageHeader = request.getHeader('baggage') as string; + + const parts = sentryTraceHeader.split('-'); + expect(parts.length).toEqual(3); + expect(parts[0]).toEqual('12312012123120121231201212312012'); + expect(parts[1]).toEqual(expect.any(String)); + expect(parts[2]).toEqual('1'); + + expect(baggageHeader).toEqual( + 'sentry-environment=production,sentry-release=1.0.0,sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=12312012123120121231201212312012,sentry-sample_rate=1,sentry-sampled=true', + ); + }); + + it('uses incoming propagation context to attach baggage and sentry-trace', async () => { + nock('http://dogs.are.great').get('/').reply(200); + + const hub = getHub(); + hub.getScope().setPropagationContext({ + traceId: '86f39e84263a4de99c326acab3bfe3bd', + spanId: '86f39e84263a4de9', + sampled: true, + dsc: { + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + public_key: 'test-public-key', + }, + }); + + const request = http.get('http://dogs.are.great/'); + const sentryTraceHeader = request.getHeader('sentry-trace') as string; + const baggageHeader = request.getHeader('baggage') as string; + + const parts = sentryTraceHeader.split('-'); + expect(parts.length).toEqual(3); + expect(parts[0]).toEqual('86f39e84263a4de99c326acab3bfe3bd'); + expect(parts[1]).toEqual(expect.any(String)); + expect(parts[2]).toEqual('1'); + + expect(baggageHeader).toEqual('sentry-trace_id=86f39e84263a4de99c326acab3bfe3bd,sentry-public_key=test-public-key'); + }); + it("doesn't attach the sentry-trace header to outgoing sentry requests", () => { nock('http://squirrelchasers.ingest.sentry.io').get('/api/12312012/store/').reply(200); @@ -270,9 +331,8 @@ describe('tracing', () => { return transaction; } - // TODO (v8): These can be removed once we remove these properties from client options describe('as client options', () => { - it("doesn't create span if shouldCreateSpanForRequest returns false", () => { + it('creates span with propagation context if shouldCreateSpanForRequest returns false', () => { const url = 'http://dogs.are.great/api/v1/index/'; nock(url).get(/.*/).reply(200); @@ -295,8 +355,15 @@ describe('tracing', () => { expect(httpSpans.length).toBe(0); // And headers are not attached without span creation - expect(request.getHeader('sentry-trace')).toBeUndefined(); - expect(request.getHeader('baggage')).toBeUndefined(); + expect(request.getHeader('sentry-trace')).toBeDefined(); + expect(request.getHeader('baggage')).toBeDefined(); + + const propagationContext = hub.getScope().getPropagationContext(); + + expect((request.getHeader('sentry-trace') as string).includes(propagationContext.traceId)).toBe(true); + expect(request.getHeader('baggage')).toEqual( + `sentry-environment=production,sentry-release=1.0.0,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=${propagationContext.traceId}`, + ); }); it.each([ @@ -366,7 +433,7 @@ describe('tracing', () => { }); describe('as Http integration constructor options', () => { - it("doesn't create span if shouldCreateSpanForRequest returns false", () => { + it('creates span with propagation context if shouldCreateSpanForRequest returns false', () => { const url = 'http://dogs.are.great/api/v1/index/'; nock(url).get(/.*/).reply(200); @@ -393,8 +460,15 @@ describe('tracing', () => { expect(httpSpans.length).toBe(0); // And headers are not attached without span creation - expect(request.getHeader('sentry-trace')).toBeUndefined(); - expect(request.getHeader('baggage')).toBeUndefined(); + expect(request.getHeader('sentry-trace')).toBeDefined(); + expect(request.getHeader('baggage')).toBeDefined(); + + const propagationContext = hub.getScope().getPropagationContext(); + + expect((request.getHeader('sentry-trace') as string).includes(propagationContext.traceId)).toBe(true); + expect(request.getHeader('baggage')).toEqual( + `sentry-environment=production,sentry-release=1.0.0,sentry-public_key=dogsarebadatkeepingsecrets,sentry-trace_id=${propagationContext.traceId}`, + ); }); it.each([ diff --git a/packages/node/test/integrations/undici.test.ts b/packages/node/test/integrations/undici.test.ts index b719d579dcbe..b30ac92c0695 100644 --- a/packages/node/test/integrations/undici.test.ts +++ b/packages/node/test/integrations/undici.test.ts @@ -1,5 +1,5 @@ import type { Transaction } from '@sentry/core'; -import { Hub, makeMain } from '@sentry/core'; +import { Hub, makeMain, runWithAsyncContext } from '@sentry/core'; import * as http from 'http'; import type { fetch as FetchType } from 'undici'; @@ -15,8 +15,8 @@ let hub: Hub; let fetch: typeof FetchType; beforeAll(async () => { - await setupTestServer(); try { + await setupTestServer(); // need to conditionally require `undici` because it's not available in Node 10 // eslint-disable-next-line @typescript-eslint/no-var-requires fetch = require('undici').fetch; @@ -28,7 +28,7 @@ beforeAll(async () => { const DEFAULT_OPTIONS = getDefaultNodeClientOptions({ dsn: SENTRY_DSN, - tracesSampleRate: 1, + tracesSampler: () => true, integrations: [new Undici()], }); @@ -51,10 +51,10 @@ conditionalTest({ min: 16 })('Undici integration', () => { it.each([ [ 'simple url', - 'http://localhost:18099', + 'http://localhost:18100', undefined, { - description: 'GET http://localhost:18099/', + description: 'GET http://localhost:18100/', op: 'http.client', data: expect.objectContaining({ 'http.method': 'GET', @@ -63,10 +63,10 @@ conditionalTest({ min: 16 })('Undici integration', () => { ], [ 'url with query', - 'http://localhost:18099?foo=bar', + 'http://localhost:18100?foo=bar', undefined, { - description: 'GET http://localhost:18099/', + description: 'GET http://localhost:18100/', op: 'http.client', data: expect.objectContaining({ 'http.method': 'GET', @@ -76,10 +76,10 @@ conditionalTest({ min: 16 })('Undici integration', () => { ], [ 'url with POST method', - 'http://localhost:18099', + 'http://localhost:18100', { method: 'POST' }, { - description: 'POST http://localhost:18099/', + description: 'POST http://localhost:18100/', data: expect.objectContaining({ 'http.method': 'POST', }), @@ -87,10 +87,10 @@ conditionalTest({ min: 16 })('Undici integration', () => { ], [ 'url with POST method', - 'http://localhost:18099', + 'http://localhost:18100', { method: 'POST' }, { - description: 'POST http://localhost:18099/', + description: 'POST http://localhost:18100/', data: expect.objectContaining({ 'http.method': 'POST', }), @@ -98,10 +98,10 @@ conditionalTest({ min: 16 })('Undici integration', () => { ], [ 'url with GET as default', - 'http://localhost:18099', + 'http://localhost:18100', { method: undefined }, { - description: 'GET http://localhost:18099/', + description: 'GET http://localhost:18100/', }, ], ])('creates a span with a %s', async (_: string, request, requestInit, expected) => { @@ -180,54 +180,86 @@ conditionalTest({ min: 16 })('Undici integration', () => { const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; hub.getScope().setSpan(transaction); - const undoPatch = patchUndici(hub, { shouldCreateSpanForRequest: url => url.includes('yes') }); + const undoPatch = patchUndici({ shouldCreateSpanForRequest: url => url.includes('yes') }); - await fetch('http://localhost:18099/no', { method: 'POST' }); + await fetch('http://localhost:18100/no', { method: 'POST' }); expect(transaction.spanRecorder?.spans.length).toBe(1); - await fetch('http://localhost:18099/yes', { method: 'POST' }); + await fetch('http://localhost:18100/yes', { method: 'POST' }); expect(transaction.spanRecorder?.spans.length).toBe(2); undoPatch(); }); - it('attaches the sentry trace and baggage headers', async () => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; - hub.getScope().setSpan(transaction); + // This flakes on CI for some reason: https://github.com/getsentry/sentry-javascript/pull/8449 + it.skip('attaches the sentry trace and baggage headers if there is an active span', async () => { + expect.assertions(3); - await fetch('http://localhost:18099', { method: 'POST' }); + await runWithAsyncContext(async () => { + const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; + hub.getScope().setSpan(transaction); - expect(transaction.spanRecorder?.spans.length).toBe(2); - const span = transaction.spanRecorder?.spans[1]; + await fetch('http://localhost:18100', { method: 'POST' }); + + expect(transaction.spanRecorder?.spans.length).toBe(2); + const span = transaction.spanRecorder?.spans[1]; + + expect(requestHeaders['sentry-trace']).toEqual(span?.toTraceparent()); + expect(requestHeaders['baggage']).toEqual( + `sentry-environment=production,sentry-public_key=0,sentry-trace_id=${transaction.traceId},sentry-sample_rate=1,sentry-transaction=test-transaction`, + ); + }); + }); + + // This flakes on CI for some reason: https://github.com/getsentry/sentry-javascript/pull/8449 + it.skip('attaches the sentry trace and baggage headers if there is no active span', async () => { + const scope = hub.getScope(); - expect(requestHeaders['sentry-trace']).toEqual(span?.toTraceparent()); + await fetch('http://localhost:18100', { method: 'POST' }); + + const propagationContext = scope.getPropagationContext(); + + expect(requestHeaders['sentry-trace'].includes(propagationContext.traceId)).toBe(true); expect(requestHeaders['baggage']).toEqual( - `sentry-environment=production,sentry-public_key=0,sentry-trace_id=${transaction.traceId},sentry-sample_rate=1,sentry-transaction=test-transaction`, + `sentry-environment=production,sentry-public_key=0,sentry-trace_id=${propagationContext.traceId},sentry-sample_rate=1,sentry-transaction=test-transaction,sentry-sampled=true`, ); }); - it('does not attach headers if `shouldCreateSpanForRequest` does not create a span', async () => { + // This flakes on CI for some reason: https://github.com/getsentry/sentry-javascript/pull/8449 + it.skip('attaches headers if `shouldCreateSpanForRequest` does not create a span using propagation context', async () => { const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; - hub.getScope().setSpan(transaction); + const scope = hub.getScope(); + const propagationContext = scope.getPropagationContext(); - const undoPatch = patchUndici(hub, { shouldCreateSpanForRequest: url => url.includes('yes') }); + scope.setSpan(transaction); - await fetch('http://localhost:18099/no', { method: 'POST' }); + const undoPatch = patchUndici({ shouldCreateSpanForRequest: url => url.includes('yes') }); - expect(requestHeaders['sentry-trace']).toBeUndefined(); - expect(requestHeaders['baggage']).toBeUndefined(); + await fetch('http://localhost:18100/no', { method: 'POST' }); + + expect(requestHeaders['sentry-trace']).toBeDefined(); + expect(requestHeaders['baggage']).toBeDefined(); + + expect(requestHeaders['sentry-trace'].includes(propagationContext.traceId)).toBe(true); + const firstSpanId = requestHeaders['sentry-trace'].split('-')[1]; - await fetch('http://localhost:18099/yes', { method: 'POST' }); + await fetch('http://localhost:18100/yes', { method: 'POST' }); expect(requestHeaders['sentry-trace']).toBeDefined(); expect(requestHeaders['baggage']).toBeDefined(); + expect(requestHeaders['sentry-trace'].includes(propagationContext.traceId)).toBe(false); + + const secondSpanId = requestHeaders['sentry-trace'].split('-')[1]; + expect(firstSpanId).not.toBe(secondSpanId); + undoPatch(); }); - it('uses tracePropagationTargets', async () => { + // This flakes on CI for some reason: https://github.com/getsentry/sentry-javascript/pull/8449 + it.skip('uses tracePropagationTargets', async () => { const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; hub.getScope().setSpan(transaction); @@ -236,14 +268,14 @@ conditionalTest({ min: 16 })('Undici integration', () => { expect(transaction.spanRecorder?.spans.length).toBe(1); - await fetch('http://localhost:18099/no', { method: 'POST' }); + await fetch('http://localhost:18100/no', { method: 'POST' }); expect(transaction.spanRecorder?.spans.length).toBe(2); expect(requestHeaders['sentry-trace']).toBeUndefined(); expect(requestHeaders['baggage']).toBeUndefined(); - await fetch('http://localhost:18099/yes', { method: 'POST' }); + await fetch('http://localhost:18100/yes', { method: 'POST' }); expect(transaction.spanRecorder?.spans.length).toBe(3); @@ -262,7 +294,7 @@ conditionalTest({ min: 16 })('Undici integration', () => { data: { method: 'POST', status_code: 200, - url: 'http://localhost:18099/', + url: 'http://localhost:18100/', }, type: 'http', timestamp: expect.any(Number), @@ -272,7 +304,7 @@ conditionalTest({ min: 16 })('Undici integration', () => { }); hub.bindClient(client); - await fetch('http://localhost:18099', { method: 'POST' }); + await fetch('http://localhost:18100', { method: 'POST' }); }); it('adds a breadcrumb on errored request', async () => { @@ -306,9 +338,9 @@ conditionalTest({ min: 16 })('Undici integration', () => { it('does not add a breadcrumb if disabled', async () => { expect.assertions(0); - const undoPatch = patchUndici(hub, { breadcrumbs: false }); + const undoPatch = patchUndici({ breadcrumbs: false }); - await fetch('http://localhost:18099', { method: 'POST' }); + await fetch('http://localhost:18100', { method: 'POST' }); undoPatch(); }); @@ -351,37 +383,34 @@ function setupTestServer() { res.end(); // also terminate socket because keepalive hangs connection a bit - res.connection.end(); + res.connection?.end(); }); - testServer.listen(18099, 'localhost'); + testServer?.listen(18100); return new Promise(resolve => { testServer?.on('listening', resolve); }); } -function patchUndici(hub: Hub, userOptions: Partial): () => void { - let options: any = {}; - const client = hub.getClient(); - if (client) { - const undici = client.getIntegration(Undici); - if (undici) { - // @ts-ignore need to access private property - options = { ...undici._options }; - // @ts-ignore need to access private property - undici._options = Object.assign(undici._options, userOptions); - } +function patchUndici(userOptions: Partial): () => void { + try { + const undici = hub.getClient()!.getIntegration(Undici); + // @ts-ignore need to access private property + options = { ...undici._options }; + // @ts-ignore need to access private property + undici._options = Object.assign(undici._options, userOptions); + } catch (_) { + throw new Error('Could not undo patching of undici'); } return () => { - const client = hub.getClient(); - if (client) { - const undici = client.getIntegration(Undici); - if (undici) { - // @ts-ignore need to access private property - undici._options = { ...options }; - } + try { + const undici = hub.getClient()!.getIntegration(Undici); + // @ts-expect-error Need to override readonly property + undici!['_options'] = { ...options }; + } catch (_) { + throw new Error('Could not undo patching of undici'); } }; } diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index fe4ec8f3060b..f32c1ccb0c48 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/opentelemetry-node", - "version": "7.57.0", + "version": "7.58.1", "description": "Official Sentry SDK for OpenTelemetry Node.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/opentelemetry-node", @@ -23,9 +23,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0" + "@sentry/core": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1" }, "peerDependencies": { "@opentelemetry/api": "1.x", @@ -39,7 +39,7 @@ "@opentelemetry/sdk-trace-base": "^1.7.0", "@opentelemetry/sdk-trace-node": "^1.7.0", "@opentelemetry/semantic-conventions": "^1.7.0", - "@sentry/node": "7.57.0" + "@sentry/node": "7.58.1" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/opentelemetry-node/test/propagator.test.ts b/packages/opentelemetry-node/test/propagator.test.ts index bb747c23ee1f..a70ecb051663 100644 --- a/packages/opentelemetry-node/test/propagator.test.ts +++ b/packages/opentelemetry-node/test/propagator.test.ts @@ -85,7 +85,7 @@ describe('SentryPropagator', () => { spanId: '6e0c63257de34c92', sampled: true, }, - 'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=sampled-transaction', + 'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=sampled-transaction,sentry-sampled=true', 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1', ], [ @@ -101,7 +101,7 @@ describe('SentryPropagator', () => { spanId: '6e0c63257de34c92', sampled: false, }, - 'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=not-sampled-transaction', + 'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=not-sampled-transaction,sentry-sampled=false', 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-0', ], [ @@ -161,7 +161,7 @@ describe('SentryPropagator', () => { const baggage = propagation.createBaggage({ foo: { value: 'bar' } }); propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter); expect(carrier[SENTRY_BAGGAGE_HEADER]).toBe( - 'foo=bar,sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=sampled-transaction', + 'foo=bar,sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=sampled-transaction,sentry-sampled=true', ); }); diff --git a/packages/overhead-metrics/package.json b/packages/overhead-metrics/package.json index 9dc0191c2f17..9de5770a1421 100644 --- a/packages/overhead-metrics/package.json +++ b/packages/overhead-metrics/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "7.57.0", + "version": "7.58.1", "name": "@sentry-internal/overhead-metrics", "main": "index.js", "author": "Sentry", diff --git a/packages/react/package.json b/packages/react/package.json index fca1c03686be..57b824c0aab9 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react", - "version": "7.57.0", + "version": "7.58.1", "description": "Official Sentry SDK for React.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react", @@ -23,9 +23,9 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry/browser": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "hoist-non-react-statics": "^3.3.2", "tslib": "^2.4.1 || ^1.9.3" }, diff --git a/packages/remix/package.json b/packages/remix/package.json index a7ae64ed07ff..1708ec1f7094 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/remix", - "version": "7.57.0", + "version": "7.58.1", "description": "Official Sentry SDK for Remix", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/remix", @@ -28,11 +28,11 @@ }, "dependencies": { "@sentry/cli": "2.2.0", - "@sentry/core": "7.57.0", - "@sentry/node": "7.57.0", - "@sentry/react": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry/core": "7.58.1", + "@sentry/node": "7.58.1", + "@sentry/react": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "tslib": "^2.4.1 || ^1.9.3", "yargs": "^17.6.0" }, diff --git a/packages/remix/src/client/errors.tsx b/packages/remix/src/client/errors.tsx new file mode 100644 index 000000000000..9c9fd5c4b449 --- /dev/null +++ b/packages/remix/src/client/errors.tsx @@ -0,0 +1,65 @@ +import { captureException, withScope } from '@sentry/core'; +import { addExceptionMechanism, isNodeEnv, isString } from '@sentry/utils'; + +import type { ErrorResponse } from '../utils/vendor/types'; + +/** + * Checks whether the given error is an ErrorResponse. + * ErrorResponse is when users throw a response from their loader or action functions. + * This is in fact a server-side error that we capture on the client. + * + * @param error The error to check. + * @returns boolean + */ +function isErrorResponse(error: unknown): error is ErrorResponse { + return typeof error === 'object' && error !== null && 'status' in error && 'statusText' in error; +} + +/** + * Captures an error that is thrown inside a Remix ErrorBoundary. + * + * @param error The error to capture. + * @returns void + */ +export function captureRemixErrorBoundaryError(error: unknown): void { + const isClientSideRuntimeError = !isNodeEnv() && error instanceof Error; + const isRemixErrorResponse = isErrorResponse(error); + // Server-side errors apart from `ErrorResponse`s also appear here without their stacktraces. + // So, we only capture: + // 1. `ErrorResponse`s + // 2. Client-side runtime errors here, + // And other server - side errors in `handleError` function where stacktraces are available. + if (isRemixErrorResponse || isClientSideRuntimeError) { + const eventData = isRemixErrorResponse + ? { + function: 'ErrorResponse', + ...error.data, + } + : { + function: 'ReactError', + }; + + withScope(scope => { + scope.addEventProcessor(event => { + addExceptionMechanism(event, { + type: 'instrument', + handled: true, + data: eventData, + }); + return event; + }); + + if (isRemixErrorResponse) { + if (isString(error.data)) { + captureException(error.data); + } else if (error.statusText) { + captureException(error.statusText); + } else { + captureException(error); + } + } else { + captureException(error); + } + }); + } +} diff --git a/packages/remix/src/performance/client.tsx b/packages/remix/src/client/performance.tsx similarity index 95% rename from packages/remix/src/performance/client.tsx rename to packages/remix/src/client/performance.tsx index 879c93e51f42..a3f7815b7bdc 100644 --- a/packages/remix/src/performance/client.tsx +++ b/packages/remix/src/client/performance.tsx @@ -4,6 +4,8 @@ import type { Transaction, TransactionContext } from '@sentry/types'; import { isNodeEnv, logger } from '@sentry/utils'; import * as React from 'react'; +import { getFutureFlagsBrowser } from '../utils/futureFlags'; + const DEFAULT_TAGS = { 'routing.instrumentation': 'remix-router', } as const; @@ -93,7 +95,8 @@ export function withSentry

, R extends React.FC wrapWithErrorBoundary?: boolean; errorBoundaryOptions?: ErrorBoundaryProps; } = { - wrapWithErrorBoundary: true, + // We don't want to wrap application with Sentry's ErrorBoundary by default for Remix v2 + wrapWithErrorBoundary: getFutureFlagsBrowser()?.v2_errorBoundary ? false : true, errorBoundaryOptions: {}, }, ): R { diff --git a/packages/remix/src/index.client.tsx b/packages/remix/src/index.client.tsx index 5c76ee4907bf..64951a3f10cd 100644 --- a/packages/remix/src/index.client.tsx +++ b/packages/remix/src/index.client.tsx @@ -3,7 +3,8 @@ import { configureScope, init as reactInit } from '@sentry/react'; import { buildMetadata } from './utils/metadata'; import type { RemixOptions } from './utils/remixOptions'; -export { remixRouterInstrumentation, withSentry } from './performance/client'; +export { remixRouterInstrumentation, withSentry } from './client/performance'; +export { captureRemixErrorBoundaryError } from './client/errors'; export * from '@sentry/react'; export function init(options: RemixOptions): void { diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index 21cfc20b17ab..4c37351001c2 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -6,9 +6,58 @@ import { instrumentServer } from './utils/instrumentServer'; import { buildMetadata } from './utils/metadata'; import type { RemixOptions } from './utils/remixOptions'; -export { ErrorBoundary, withErrorBoundary } from '@sentry/react'; -export { remixRouterInstrumentation, withSentry } from './performance/client'; +// We need to explicitly export @sentry/node as they end up under `default` in ESM builds +// See: https://github.com/getsentry/sentry-javascript/issues/8474 +export { + addGlobalEventProcessor, + addBreadcrumb, + captureCheckIn, + captureException, + captureEvent, + captureMessage, + configureScope, + createTransport, + extractTraceparentData, + getActiveTransaction, + getHubFromCarrier, + getCurrentHub, + Hub, + makeMain, + Scope, + startTransaction, + SDK_VERSION, + setContext, + setExtra, + setExtras, + setTag, + setTags, + setUser, + spanStatusfromHttpCode, + trace, + withScope, + autoDiscoverNodePerformanceMonitoringIntegrations, + makeNodeTransport, + defaultIntegrations, + defaultStackParser, + lastEventId, + flush, + close, + getSentryRelease, + addRequestDataToEvent, + DEFAULT_USER_INCLUDES, + extractRequestData, + deepReadDirSync, + Integrations, + Handlers, +} from '@sentry/node'; + +// Keeping the `*` exports for backwards compatibility and types export * from '@sentry/node'; + +export { captureRemixServerException } from './utils/instrumentServer'; +export { ErrorBoundary, withErrorBoundary } from '@sentry/react'; +export { remixRouterInstrumentation, withSentry } from './client/performance'; +export { captureRemixErrorBoundaryError } from './client/errors'; export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express'; function sdkAlreadyInitialized(): boolean { diff --git a/packages/remix/src/utils/futureFlags.ts b/packages/remix/src/utils/futureFlags.ts new file mode 100644 index 000000000000..7d797c19e8a2 --- /dev/null +++ b/packages/remix/src/utils/futureFlags.ts @@ -0,0 +1,35 @@ +import { GLOBAL_OBJ } from '@sentry/utils'; + +import type { FutureConfig, ServerBuild } from './vendor/types'; + +export type EnhancedGlobal = typeof GLOBAL_OBJ & { + __remixContext?: { + future?: FutureConfig; + }; +}; + +/** + * Get the future flags from the Remix browser context + * + * @returns The future flags + */ +export function getFutureFlagsBrowser(): FutureConfig | undefined { + const window = GLOBAL_OBJ as EnhancedGlobal; + + if (!window.__remixContext) { + return; + } + + return window.__remixContext.future; +} + +/** + * Get the future flags from the Remix server build + * + * @param build The Remix server build + * + * @returns The future flags + */ +export function getFutureFlagsServer(build: ServerBuild): FutureConfig | undefined { + return build.future; +} diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index 64dea4cfb92b..b0ba45f69c26 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -13,12 +13,15 @@ import { tracingContextFromHeaders, } from '@sentry/utils'; +import { getFutureFlagsServer } from './futureFlags'; +import { extractData, getRequestMatch, isDeferredData, isResponse, json, matchServerRoutes } from './vendor/response'; import type { AppData, CreateRequestHandlerFunction, DataFunction, DataFunctionArgs, EntryContext, + FutureConfig, HandleDocumentRequestFunction, ReactRouterDomPkg, RemixRequest, @@ -26,10 +29,11 @@ import type { ServerBuild, ServerRoute, ServerRouteManifest, -} from './types'; -import { extractData, getRequestMatch, isDeferredData, isResponse, json, matchServerRoutes } from './vendor/response'; +} from './vendor/types'; import { normalizeRemixRequest } from './web-fetch'; +let FUTURE_FLAGS: FutureConfig | undefined; + // Flag to track if the core request handler is instrumented. export let isRequestHandlerWrapped = false; @@ -56,7 +60,16 @@ async function extractResponseError(response: Response): Promise { return responseData; } -async function captureRemixServerException(err: unknown, name: string, request: Request): Promise { +/** + * Captures an exception happened in the Remix server. + * + * @param err The error to capture. + * @param name The name of the origin function. + * @param request The request object. + * + * @returns A promise that resolves when the exception is captured. + */ +export async function captureRemixServerException(err: unknown, name: string, request: Request): Promise { // Skip capturing if the thrown error is not a 5xx response // https://remix.run/docs/en/v1/api/conventions#throwing-responses-in-loaders if (isResponse(err) && err.status < 500) { @@ -145,7 +158,10 @@ function makeWrappedDocumentRequestFunction( span?.finish(); } catch (err) { - await captureRemixServerException(err, 'documentRequest', request); + if (!FUTURE_FLAGS?.v2_errorBoundary) { + await captureRemixServerException(err, 'documentRequest', request); + } + throw err; } @@ -182,7 +198,10 @@ function makeWrappedDataFunction(origFn: DataFunction, id: string, name: 'action currentScope.setSpan(activeTransaction); span?.finish(); } catch (err) { - await captureRemixServerException(err, name, args.request); + if (!FUTURE_FLAGS?.v2_errorBoundary) { + await captureRemixServerException(err, name, args.request); + } + throw err; } @@ -439,6 +458,7 @@ function makeWrappedCreateRequestHandler( isRequestHandlerWrapped = true; return function (this: unknown, build: ServerBuild, ...args: unknown[]): RequestHandler { + FUTURE_FLAGS = getFutureFlagsServer(build); const newBuild = instrumentBuild(build); const requestHandler = origCreateRequestHandler.call(this, newBuild, ...args); diff --git a/packages/remix/src/utils/serverAdapters/express.ts b/packages/remix/src/utils/serverAdapters/express.ts index 000ad3a00b15..742c938f2d06 100644 --- a/packages/remix/src/utils/serverAdapters/express.ts +++ b/packages/remix/src/utils/serverAdapters/express.ts @@ -20,7 +20,7 @@ import type { ExpressResponse, ReactRouterDomPkg, ServerBuild, -} from '../types'; +} from '../vendor/types'; let pkg: ReactRouterDomPkg; diff --git a/packages/remix/src/utils/vendor/response.ts b/packages/remix/src/utils/vendor/response.ts index ae85fff74734..fed25dd0f534 100644 --- a/packages/remix/src/utils/vendor/response.ts +++ b/packages/remix/src/utils/vendor/response.ts @@ -6,7 +6,7 @@ // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -import type { DeferredData, ReactRouterDomPkg, RouteMatch, ServerRoute } from '../types'; +import type { DeferredData, ReactRouterDomPkg, RouteMatch, ServerRoute } from './types'; /** * Based on Remix Implementation diff --git a/packages/remix/src/utils/types.ts b/packages/remix/src/utils/vendor/types.ts similarity index 89% rename from packages/remix/src/utils/types.ts rename to packages/remix/src/utils/vendor/types.ts index 74dcf10215cc..faaa7e5f6f60 100644 --- a/packages/remix/src/utils/types.ts +++ b/packages/remix/src/utils/vendor/types.ts @@ -14,6 +14,42 @@ import type * as Express from 'express'; import type { Agent } from 'https'; import type { ComponentType } from 'react'; +type Dev = { + command?: string; + scheme?: string; + host?: string; + port?: number; + restart?: boolean; + tlsKey?: string; + tlsCert?: string; +}; + +export interface FutureConfig { + unstable_dev: boolean | Dev; + /** @deprecated Use the `postcss` config option instead */ + unstable_postcss: boolean; + /** @deprecated Use the `tailwind` config option instead */ + unstable_tailwind: boolean; + v2_errorBoundary: boolean; + v2_headers: boolean; + v2_meta: boolean; + v2_normalizeFormMethod: boolean; + v2_routeConvention: boolean; +} + +export interface RemixConfig { + [key: string]: any; + future: FutureConfig; +} + +export interface ErrorResponse { + status: number; + statusText: string; + data: any; + error?: Error; + internal: boolean; +} + export type RemixRequestState = { method: string; redirect: RequestRedirect; @@ -133,6 +169,7 @@ export interface ServerBuild { assets: AssetsManifest; publicPath?: string; assetsBuildDirectory?: string; + future?: FutureConfig; } export interface HandleDocumentRequestFunction { diff --git a/packages/remix/src/utils/web-fetch.ts b/packages/remix/src/utils/web-fetch.ts index 1e69a77b5dba..1961329c2f4b 100644 --- a/packages/remix/src/utils/web-fetch.ts +++ b/packages/remix/src/utils/web-fetch.ts @@ -24,8 +24,8 @@ import { logger } from '@sentry/utils'; -import type { RemixRequest } from './types'; import { getClientIPAddress } from './vendor/getIpAddress'; +import type { RemixRequest } from './vendor/types'; /* * Symbol extractor utility to be able to access internal fields of Remix requests. diff --git a/packages/remix/test/integration/app_v1/entry.server.tsx b/packages/remix/test/integration/app_v1/entry.server.tsx index ae879492e236..d48f2644fac4 100644 --- a/packages/remix/test/integration/app_v1/entry.server.tsx +++ b/packages/remix/test/integration/app_v1/entry.server.tsx @@ -6,6 +6,7 @@ import * as Sentry from '@sentry/remix'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1, + tracePropagationTargets: ['example.org'], // Disabling to test series of envelopes deterministically. autoSessionTracking: false, }); diff --git a/packages/remix/test/integration/app_v2/entry.server.tsx b/packages/remix/test/integration/app_v2/entry.server.tsx index ae879492e236..784cb2a39cd4 100644 --- a/packages/remix/test/integration/app_v2/entry.server.tsx +++ b/packages/remix/test/integration/app_v2/entry.server.tsx @@ -1,4 +1,4 @@ -import type { EntryContext } from '@remix-run/node'; +import type { EntryContext, DataFunctionArgs } from '@remix-run/node'; import { RemixServer } from '@remix-run/react'; import { renderToString } from 'react-dom/server'; import * as Sentry from '@sentry/remix'; @@ -6,10 +6,19 @@ import * as Sentry from '@sentry/remix'; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1, + tracePropagationTargets: ['example.org'], // Disabling to test series of envelopes deterministically. autoSessionTracking: false, }); +export function handleError(error: unknown, { request }: DataFunctionArgs): void { + if (error instanceof Error) { + Sentry.captureRemixServerException(error, 'remix.server', request); + } else { + Sentry.captureException(error); + } +} + export default function handleRequest( request: Request, responseStatusCode: number, diff --git a/packages/remix/test/integration/app_v2/root.tsx b/packages/remix/test/integration/app_v2/root.tsx index 5af1f8cc7a1a..2320451cee74 100644 --- a/packages/remix/test/integration/app_v2/root.tsx +++ b/packages/remix/test/integration/app_v2/root.tsx @@ -1,6 +1,15 @@ import { V2_MetaFunction, LoaderFunction, json, defer, redirect } from '@remix-run/node'; -import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react'; -import { withSentry } from '@sentry/remix'; +import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useRouteError } from '@remix-run/react'; +import { V2_ErrorBoundaryComponent } from '@remix-run/react/dist/routeModules'; +import { captureRemixErrorBoundaryError, withSentry } from '@sentry/remix'; + +export const ErrorBoundary: V2_ErrorBoundaryComponent = () => { + const error = useRouteError(); + + captureRemixErrorBoundaryError(error); + + return

error
; +}; export const meta: V2_MetaFunction = ({ data }) => [ { charset: 'utf-8' }, diff --git a/packages/remix/test/integration/common/routes/action-json-response.$id.tsx b/packages/remix/test/integration/common/routes/action-json-response.$id.tsx index 2c7a19059596..ff0f6940fe44 100644 --- a/packages/remix/test/integration/common/routes/action-json-response.$id.tsx +++ b/packages/remix/test/integration/common/routes/action-json-response.$id.tsx @@ -3,8 +3,10 @@ import { useActionData } from '@remix-run/react'; export const loader: LoaderFunction = async ({ params: { id } }) => { if (id === '-1') { - throw new Error('Unexpected Server Error from Loader'); + throw new Error('Unexpected Server Error'); } + + return null; }; export const action: ActionFunction = async ({ params: { id } }) => { diff --git a/packages/remix/test/integration/common/routes/loader-json-response.$id.tsx b/packages/remix/test/integration/common/routes/loader-json-response.$id.tsx index 55b53e2d70dc..a4ad3dc48339 100644 --- a/packages/remix/test/integration/common/routes/loader-json-response.$id.tsx +++ b/packages/remix/test/integration/common/routes/loader-json-response.$id.tsx @@ -5,7 +5,7 @@ type LoaderData = { id: string }; export const loader: LoaderFunction = async ({ params: { id } }) => { if (id === '-2') { - throw new Error('Unexpected Server Error from Loader'); + throw new Error('Unexpected Server Error'); } if (id === '-1') { diff --git a/packages/remix/test/integration/test/client/errorboundary.test.ts b/packages/remix/test/integration/test/client/errorboundary.test.ts index b90b3e8d3eaa..9c496e3e3040 100644 --- a/packages/remix/test/integration/test/client/errorboundary.test.ts +++ b/packages/remix/test/integration/test/client/errorboundary.test.ts @@ -21,16 +21,20 @@ test('should capture React component errors.', async ({ page }) => { expect(errorEnvelope.level).toBe('error'); expect(errorEnvelope.sdk?.name).toBe('sentry.javascript.remix'); expect(errorEnvelope.exception?.values).toMatchObject([ - { - type: 'React ErrorBoundary Error', - value: 'Sentry React Component Error', - stacktrace: { frames: expect.any(Array) }, - }, + ...(!useV2 + ? [ + { + type: 'React ErrorBoundary Error', + value: 'Sentry React Component Error', + stacktrace: { frames: expect.any(Array) }, + }, + ] + : []), { type: 'Error', value: 'Sentry React Component Error', stacktrace: { frames: expect.any(Array) }, - mechanism: { type: 'generic', handled: true }, + mechanism: { type: useV2 ? 'instrument' : 'generic', handled: true }, }, ]); }); diff --git a/packages/remix/test/integration/test/client/meta-tags.test.ts b/packages/remix/test/integration/test/client/meta-tags.test.ts index f956787b35c6..6d0a0cd06307 100644 --- a/packages/remix/test/integration/test/client/meta-tags.test.ts +++ b/packages/remix/test/integration/test/client/meta-tags.test.ts @@ -2,7 +2,13 @@ import { test, expect } from '@playwright/test'; import { getFirstSentryEnvelopeRequest } from './utils/helpers'; import { Event } from '@sentry/types'; -test('should inject `sentry-trace` and `baggage` meta tags inside the root page.', async ({ page }) => { +test('should inject `sentry-trace` and `baggage` meta tags inside the root page.', async ({ page, browserName }) => { + // This test is flaky on firefox + // https://github.com/getsentry/sentry-javascript/issues/8398 + if (browserName === 'firefox') { + test.skip(); + } + await page.goto('/'); const sentryTraceTag = await page.$('meta[name="sentry-trace"]'); @@ -16,7 +22,16 @@ test('should inject `sentry-trace` and `baggage` meta tags inside the root page. expect(sentryBaggageContent).toEqual(expect.any(String)); }); -test('should inject `sentry-trace` and `baggage` meta tags inside a parameterized route.', async ({ page }) => { +test('should inject `sentry-trace` and `baggage` meta tags inside a parameterized route.', async ({ + page, + browserName, +}) => { + // This test is flaky on firefox + // https://github.com/getsentry/sentry-javascript/issues/8398 + if (browserName === 'firefox') { + test.skip(); + } + await page.goto('/loader-json-response/0'); const sentryTraceTag = await page.$('meta[name="sentry-trace"]'); @@ -30,7 +45,16 @@ test('should inject `sentry-trace` and `baggage` meta tags inside a parameterize expect(sentryBaggageContent).toEqual(expect.any(String)); }); -test('should send transactions with corresponding `sentry-trace` and `baggage` inside root page', async ({ page }) => { +test('should send transactions with corresponding `sentry-trace` and `baggage` inside root page', async ({ + page, + browserName, +}) => { + // This test is flaky on firefox + // https://github.com/getsentry/sentry-javascript/issues/8398 + if (browserName === 'firefox') { + test.skip(); + } + const envelope = await getFirstSentryEnvelopeRequest(page, '/'); const sentryTraceTag = await page.$('meta[name="sentry-trace"]'); @@ -47,7 +71,14 @@ test('should send transactions with corresponding `sentry-trace` and `baggage` i test('should send transactions with corresponding `sentry-trace` and `baggage` inside a parameterized route', async ({ page, + browserName, }) => { + // This test is flaky on firefox + // https://github.com/getsentry/sentry-javascript/issues/8398 + if (browserName === 'firefox') { + test.skip(); + } + const envelope = await getFirstSentryEnvelopeRequest(page, '/loader-json-response/0'); const sentryTraceTag = await page.$('meta[name="sentry-trace"]'); diff --git a/packages/remix/test/integration/test/server/action.test.ts b/packages/remix/test/integration/test/server/action.test.ts index cc25a87611d4..a2a4632ba962 100644 --- a/packages/remix/test/integration/test/server/action.test.ts +++ b/packages/remix/test/integration/test/server/action.test.ts @@ -81,7 +81,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada stacktrace: expect.any(Object), mechanism: { data: { - function: 'action', + function: useV2 ? 'remix.server' : 'action', }, handled: true, type: 'instrument', @@ -197,11 +197,11 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada values: [ { type: 'Error', - value: 'Unexpected Server Error from Loader', + value: 'Unexpected Server Error', stacktrace: expect.any(Object), mechanism: { data: { - function: 'loader', + function: useV2 ? 'remix.server' : 'loader', }, handled: true, type: 'instrument', @@ -254,7 +254,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada stacktrace: expect.any(Object), mechanism: { data: { - function: 'action', + function: useV2 ? 'ErrorResponse' : 'action', }, handled: true, type: 'instrument', @@ -303,11 +303,13 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada values: [ { type: 'Error', - value: 'Non-Error exception captured with keys: data', + value: useV2 + ? 'Non-Error exception captured with keys: data, internal, status, statusText' + : 'Non-Error exception captured with keys: data', stacktrace: expect.any(Object), mechanism: { data: { - function: 'action', + function: useV2 ? 'ErrorResponse' : 'action', }, handled: true, type: 'instrument', @@ -360,7 +362,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada stacktrace: expect.any(Object), mechanism: { data: { - function: 'action', + function: useV2 ? 'ErrorResponse' : 'action', }, handled: true, type: 'instrument', @@ -409,11 +411,13 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada values: [ { type: 'Error', - value: 'Non-Error exception captured with keys: [object has no keys]', + value: useV2 + ? 'Non-Error exception captured with keys: data, internal, status, statusText' + : 'Non-Error exception captured with keys: [object has no keys]', stacktrace: expect.any(Object), mechanism: { data: { - function: 'action', + function: useV2 ? 'ErrorResponse' : 'action', }, handled: true, type: 'instrument', diff --git a/packages/remix/test/integration/test/server/loader.test.ts b/packages/remix/test/integration/test/server/loader.test.ts index 8a99c699cc37..ccaa93b05e36 100644 --- a/packages/remix/test/integration/test/server/loader.test.ts +++ b/packages/remix/test/integration/test/server/loader.test.ts @@ -35,11 +35,11 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada values: [ { type: 'Error', - value: 'Unexpected Server Error from Loader', + value: 'Unexpected Server Error', stacktrace: expect.any(Object), mechanism: { data: { - function: 'loader', + function: useV2 ? 'remix.server' : 'loader', }, handled: true, type: 'instrument', @@ -134,11 +134,11 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada values: [ { type: 'Error', - value: 'Unexpected Server Error from Loader', + value: 'Unexpected Server Error', stacktrace: expect.any(Object), mechanism: { data: { - function: 'loader', + function: useV2 ? 'remix.server' : 'loader', }, handled: true, type: 'instrument', diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json index 52d5bd4d5f8a..137a5e58c96a 100644 --- a/packages/replay-worker/package.json +++ b/packages/replay-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-worker", - "version": "7.57.0", + "version": "7.58.1", "description": "Worker for @sentry/replay", "main": "build/npm/esm/index.js", "module": "build/npm/esm/index.js", diff --git a/packages/replay/package.json b/packages/replay/package.json index 486f63181e07..8026824a5ee5 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/replay", - "version": "7.57.0", + "version": "7.58.1", "description": "User replays for Sentry", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -53,16 +53,16 @@ "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { "@babel/core": "^7.17.5", - "@sentry-internal/replay-worker": "7.57.0", + "@sentry-internal/replay-worker": "7.58.1", "@sentry-internal/rrweb": "1.108.0", "@sentry-internal/rrweb-snapshot": "1.108.0", "jsdom-worker": "^0.2.1", "tslib": "^2.4.1 || ^1.9.3" }, "dependencies": { - "@sentry/core": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0" + "@sentry/core": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1" }, "engines": { "node": ">=12" diff --git a/packages/serverless/package.json b/packages/serverless/package.json index 0d21cbd09867..a2872def4e79 100644 --- a/packages/serverless/package.json +++ b/packages/serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/serverless", - "version": "7.57.0", + "version": "7.58.1", "description": "Official Sentry SDK for various serverless solutions", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/serverless", @@ -23,10 +23,10 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.57.0", - "@sentry/node": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry/core": "7.58.1", + "@sentry/node": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "@types/aws-lambda": "^8.10.62", "@types/express": "^4.17.14", "tslib": "^2.4.1 || ^1.9.3" diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 9ede012356a6..601a4b26daf4 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/svelte", - "version": "7.57.0", + "version": "7.58.1", "description": "Official Sentry SDK for Svelte", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/svelte", @@ -23,9 +23,9 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry/browser": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "magic-string": "^0.30.0", "tslib": "^2.4.1 || ^1.9.3" }, diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 36b6611e8af6..fac4fa887f7f 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/sveltekit", - "version": "7.57.0", + "version": "7.58.1", "description": "Official Sentry SDK for SvelteKit", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/sveltekit", @@ -20,13 +20,13 @@ "@sveltejs/kit": "1.x" }, "dependencies": { - "@sentry-internal/tracing": "7.57.0", - "@sentry/core": "7.57.0", - "@sentry/integrations": "7.57.0", - "@sentry/node": "7.57.0", - "@sentry/svelte": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry-internal/tracing": "7.58.1", + "@sentry/core": "7.58.1", + "@sentry/integrations": "7.58.1", + "@sentry/node": "7.58.1", + "@sentry/svelte": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "@sentry/vite-plugin": "^0.6.0", "magicast": "0.2.8", "sorcery": "0.11.0" diff --git a/packages/tracing-internal/package.json b/packages/tracing-internal/package.json index 65af197d8714..dfa9dcc2a26a 100644 --- a/packages/tracing-internal/package.json +++ b/packages/tracing-internal/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/tracing", - "version": "7.57.0", + "version": "7.58.1", "description": "Sentry Internal Tracing Package", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tracing-internal", @@ -23,9 +23,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry/core": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "tslib": "^2.4.1 || ^1.9.3" }, "devDependencies": { diff --git a/packages/tracing-internal/src/node/integrations/prisma.ts b/packages/tracing-internal/src/node/integrations/prisma.ts index 458e0f304ebe..359ff887b024 100644 --- a/packages/tracing-internal/src/node/integrations/prisma.ts +++ b/packages/tracing-internal/src/node/integrations/prisma.ts @@ -65,6 +65,7 @@ export class Prisma implements Integration { // https://github.com/getsentry/sentry-javascript/issues/7216#issuecomment-1602375012 // In the future we might explore providing a dedicated PrismaClient middleware instead of this hack. if (isValidPrismaClient(options.client) && !options.client._sentryInstrumented) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any addNonEnumerableProperty(options.client as any, '_sentryInstrumented', true); options.client.$use((params, next: (params: PrismaMiddlewareParams) => Promise) => { diff --git a/packages/tracing-internal/test/browser/browsertracing.test.ts b/packages/tracing-internal/test/browser/browsertracing.test.ts index fa5e575e8ada..0754afd65fc8 100644 --- a/packages/tracing-internal/test/browser/browsertracing.test.ts +++ b/packages/tracing-internal/test/browser/browsertracing.test.ts @@ -636,6 +636,7 @@ conditionalTest({ min: 10 })('BrowserTracing', () => { release: '1.0.0', environment: 'production', public_key: 'pubKey', + sampled: 'false', trace_id: expect.not.stringMatching('12312012123120121231201212312012'), }); }); diff --git a/packages/tracing/package.json b/packages/tracing/package.json index ead974488346..dbe00537564f 100644 --- a/packages/tracing/package.json +++ b/packages/tracing/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/tracing", - "version": "7.57.0", + "version": "7.58.1", "description": "Sentry Performance Monitoring Package", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tracing", @@ -23,14 +23,14 @@ "access": "public" }, "dependencies": { - "@sentry-internal/tracing": "7.57.0" + "@sentry-internal/tracing": "7.58.1" }, "devDependencies": { - "@sentry-internal/integration-shims": "7.57.0", - "@sentry/browser": "7.57.0", - "@sentry/core": "7.57.0", - "@sentry/types": "7.57.0", - "@sentry/utils": "7.57.0", + "@sentry-internal/integration-shims": "7.58.1", + "@sentry/browser": "7.58.1", + "@sentry/core": "7.58.1", + "@sentry/types": "7.58.1", + "@sentry/utils": "7.58.1", "@types/express": "^4.17.14" }, "scripts": { diff --git a/packages/types/package.json b/packages/types/package.json index 5beded66150e..e7ead80a3637 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/types", - "version": "7.57.0", + "version": "7.58.1", "description": "Types for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/types", diff --git a/packages/types/src/checkin.ts b/packages/types/src/checkin.ts index a316c0c7a375..b19ff7b78770 100644 --- a/packages/types/src/checkin.ts +++ b/packages/types/src/checkin.ts @@ -1,3 +1,5 @@ +import type { TraceContext } from './context'; + interface CrontabSchedule { type: 'crontab'; // The crontab schedule string, e.g. 0 * * * *. @@ -36,6 +38,9 @@ export interface SerializedCheckIn { // See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones timezone?: string; }; + contexts?: { + trace?: TraceContext; + }; } interface InProgressCheckIn { diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 831b1c10e9f2..ac779fc3058e 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -74,9 +74,10 @@ export interface Client { * @param checkIn An object that describes a check in. * @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want * to create a monitor automatically when sending a check in. + * @param scope An optional scope containing event metadata. * @returns A string representing the id of the check in. */ - captureCheckIn?(checkIn: CheckIn, monitorConfig?: MonitorConfig): string; + captureCheckIn?(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string; /** Returns the current Dsn. */ getDsn(): DsnComponents | undefined; diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index 3f3ebf999ef9..288453fc4fdb 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -20,6 +20,7 @@ export type DynamicSamplingContext = { transaction?: string; user_segment?: string; replay_id?: string; + sampled?: string; }; export type EnvelopeItemType = @@ -86,7 +87,7 @@ type ReplayRecordingItem = BaseEnvelopeItem