diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bf94ab74c14..0901896acc7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,71 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.93.0 + +### Important Changes + +#### Deprecations + +As we're moving closer to the next major version of the SDK, more public APIs were deprecated. + +To get a head start on migrating to the replacement APIs, please take a look at our +[migration guide](https://github.com/getsentry/sentry-javascript/blob/develop/MIGRATION.md). + +- feat(core): Deprecate `getActiveTransaction()` & `scope.getTransaction()` (#10098) +- feat(core): Deprecate `Hub.shouldSendDefaultPii` (#10062) +- feat(core): Deprecate `new Transaction()` (#10125) +- feat(core): Deprecate `scope.getSpan()` & `scope.setSpan()` (#10114) +- feat(core): Deprecate `scope.setTransactionName()` (#10113) +- feat(core): Deprecate `span.startChild()` (#10091) +- feat(core): Deprecate `startTransaction()` (#10073) +- feat(core): Deprecate `Transaction.getDynamicSamplingContext` in favor of `getDynamicSamplingContextFromSpan` (#10094) +- feat(core): Deprecate arguments for `startSpan()` (#10101) +- feat(core): Deprecate hub capture APIs and add them to `Scope` (#10039) +- feat(core): Deprecate session APIs on hub and add global replacements (#10054) +- feat(core): Deprecate span `name` and `description` (#10056) +- feat(core): Deprecate span `tags`, `data`, `context` & setters (#10053) +- feat(core): Deprecate transaction metadata in favor of attributes (#10097) +- feat(core): Deprecate `span.sampled` in favor of `span.isRecording()` (#10034) +- ref(node-experimental): Deprecate `lastEventId` on scope (#10093) + +#### Cron Monitoring Support for `node-schedule` library + +This release adds auto instrumented check-ins for the `node-schedule` library. + +```ts +import * as Sentry from '@sentry/node'; +import * as schedule from 'node-schedule'; + +const scheduleWithCheckIn = Sentry.cron.instrumentNodeSchedule(schedule); + +const job = scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * * *', () => { + console.log('You will see this message every minute'); +}); +``` + +- feat(node): Instrumentation for `node-schedule` library (#10086) + +### Other Changes + +- feat(core): Add `span.spanContext()` (#10037) +- feat(core): Add `spanToJSON()` method to get span properties (#10074) +- feat(core): Allow to pass `scope` to `startSpan` APIs (#10076) +- feat(core): Allow to pass start/end timestamp for spans flexibly (#10060) +- feat(node): Make `getModuleFromFilename` compatible with ESM (#10061) +- feat(replay): Update rrweb to 2.7.3 (#10072) +- feat(utils): Add `parameterize` function (#9145) +- fix(astro): Use correct package name for CF (#10099) +- fix(core): Do not run `setup` for integration on client multiple times (#10116) +- fix(core): Ensure we copy passed in span data/tags/attributes (#10105) +- fix(cron): Make name required for instrumentNodeCron option (#10070) +- fix(nextjs): Don't capture not-found and redirect errors in generation functions (#10057) +- fix(node): `LocalVariables` integration should have correct name (#10084) +- fix(node): Anr events should have an `event_id` (#10068) +- fix(node): Revert to only use sync debugger for `LocalVariables` (#10077) +- fix(node): Update ANR min node version to v16.17.0 (#10107) + + ## 7.92.0 ### Important Changes diff --git a/MIGRATION.md b/MIGRATION.md index 99e9e3023732..95aa940265f3 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -8,9 +8,89 @@ npx @sentry/migr8@latest This will let you select which updates to run, and automatically update your code. Make sure to still review all code changes! +## Deprecate `Hub` + +The `Hub` has been a very important part of the Sentry SDK API up until now. +Hubs were the SDK's "unit of concurrency" to keep track of data across threads and to scope data to certain parts of your code. +Because it is overly complicated and confusing to power users, it is going to be replaced by a set of new APIs: the "new Scope API". + +`Scope`s have existed before in the SDK but we are now expanding on them because we have found them powerful enough to fully cover the `Hub` API. + +If you are using the `Hub` right now, see the following table on how to migrate to the new API: + +| Old `Hub` API | New `Scope` API | +| --- | --- | +| `new Hub()` | `withScope()`, `withIsolationScope()` or `new Scope()` | +| hub.isOlderThan() | REMOVED - Was used to compare `Hub` instances, which are gonna be removed | +| hub.bindClient() | A combination of `scope.setClient()` and `client.setupIntegrations()` | +| hub.pushScope() | `Sentry.withScope()` | +| hub.popScope() | `Sentry.withScope()` | +| hub.withScope() | `Sentry.withScope()` | +| getClient() | `Sentry.getClient()` | +| getScope() | `Sentry.getCurrentScope()` to get the currently active scope | +| getIsolationScope() | `Sentry.getIsolationScope()` | +| getStack() | REMOVED - The stack used to hold scopes. Scopes are used directly now | +| getStackTop() | REMOVED - The stack used to hold scopes. Scopes are used directly now | +| captureException() | `Sentry.captureException()` | +| captureMessage() | `Sentry.captureMessage()` | +| captureEvent() | `Sentry.captureEvent()` | +| lastEventId() | REMOVED - Use event processors or beforeSend instead | +| addBreadcrumb() | `Sentry.addBreadcrumb()` | +| setUser() | `Sentry.setUser()` | +| setTags() | `Sentry.setTags()` | +| setExtras() | `Sentry.setExtras()` | +| setTag() | `Sentry.setTag()` | +| setExtra() | `Sentry.setExtra()` | +| setContext() | `Sentry.setContext()` | +| configureScope() | REMOVED - Scopes are now the unit of concurrency | +| run() | `Sentry.withScope()` or `Sentry.withIsolationScope()` | +| getIntegration() | `client.getIntegration()` | +| startTransaction() | `Sentry.startSpan()`, `Sentry.startInactiveSpan()` or `Sentry.startSpanManual()` | +| traceHeaders() | REMOVED - The closest equivalent is now `spanToTraceHeader(getActiveSpan())` | +| captureSession() | `Sentry.captureSession()` | +| startSession() | `Sentry.startSession()` | +| endSession() | `Sentry.endSession()` | +| shouldSendDefaultPii() | REMOVED - The closest equivalent is `Sentry.getClient().getOptions().sendDefaultPii` | + +## Deprecate `scope.getSpan()` and `scope.setSpan()` + +Instead, you can get the currently active span via `Sentry.getActiveSpan()`. +Setting a span on the scope happens automatically when you use the new performance APIs `startSpan()` and `startSpanManual()`. + +## Deprecate `scope.setTransactionName()` + +Instead, either set this as attributes or tags, or use an event processor to set `event.transaction`. + +## Deprecate `scope.getTransaction()` and `getActiveTransaction()` + +Instead, you should not rely on the active transaction, but just use `startSpan()` APIs, which handle this for you. + +## Deprecate arguments for `startSpan()` APIs + +In v8, the API to start a new span will be reduced from the currently available options. +Going forward, only these argument will be passable to `startSpan()`, `startSpanManual()` and `startInactiveSpan()`: + +* `name` +* `attributes` +* `origin` +* `op` +* `startTime` +* `scope` + +## Deprecate `startTransaction()` & `span.startChild()` + +In v8, the old performance API `startTransaction()` (and `hub.startTransaction()`), as well as `span.startChild()`, will be removed. +Instead, use the new performance APIs: + +* `startSpan()` +* `startSpanManual()` +* `startInactiveSpan()` + +You can [read more about the new performance APIs here](./docs/v8-new-performance-apis.md). + ## Deprecate `Sentry.lastEventId()` and `hub.lastEventId()` -`Sentry.lastEventId()` sometimes causes race conditons, so we are deprecating it in favour of the `beforeSend` callback. +`Sentry.lastEventId()` sometimes causes race conditions, so we are deprecating it in favour of the `beforeSend` callback. ```js // Before @@ -46,6 +126,19 @@ In v8, the Span class is heavily reworked. The following properties & methods ar * `span.setName(newName)`: Use `span.updateName(newName)` instead. * `span.toTraceparent()`: use `spanToTraceHeader(span)` util instead. * `span.getTraceContext()`: Use `spanToTraceContext(span)` utility function instead. +* `span.sampled`: Use `span.isRecording()` instead. +* `span.spanId`: Use `span.spanContext().spanId` instead. +* `span.traceId`: Use `span.spanContext().traceId` instead. +* `span.name`: Use `spanToJSON(span).description` instead. +* `span.description`: Use `spanToJSON(span).description` instead. +* `span.getDynamicSamplingContext`: Use `getDynamicSamplingContextFromSpan` utility function instead. +* `transaction.setMetadata()`: Use attributes instead, or set data on the scope. +* `transaction.metadata`: Use attributes instead, or set data on the scope. +* `span.tags`: Set tags on the surrounding scope instead, or use attributes. +* `span.data`: Use `spanToJSON(span).data` instead. +* `span.setTag()`: Use `span.setAttribute()` instead or set tags on the surrounding scope. +* `span.setData()`: Use `span.setAttribute()` instead. +* `transaction.setContext()`: Set context on the surrounding scope instead. ## Deprecate `pushScope` & `popScope` in favor of `withScope` diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 5c880aa59b8c..7641db7f8499 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -45,7 +45,7 @@ "dependencies": { "@babel/preset-typescript": "^7.16.7", "@playwright/test": "^1.31.1", - "@sentry-internal/rrweb": "2.6.0", + "@sentry-internal/rrweb": "2.7.3", "@sentry/browser": "7.92.0", "@sentry/tracing": "7.92.0", "axios": "1.6.0", diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts index 93ceb1e70001..37ac97c633ca 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts @@ -4,57 +4,62 @@ import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers'; -sentryTest('captures Breadcrumb for clicks & debounces them for a second', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - await page.route('**/foo', route => { - return route.fulfill({ - status: 200, - body: JSON.stringify({ - userNames: ['John', 'Jane'], - }), - headers: { - 'Content-Type': 'application/json', - }, +sentryTest( + 'captures Breadcrumb for clicks & debounces them for a second', + async ({ getLocalTestUrl, page, browserName }) => { + sentryTest.skip(browserName === 'chromium', 'This consistently flakes on chrome.'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + userNames: ['John', 'Jane'], + }), + headers: { + 'Content-Type': 'application/json', + }, + }); }); - }); - - const promise = getFirstSentryEnvelopeRequest(page); - - await page.goto(url); - - await page.click('#button1'); - // not debounced because other target - await page.click('#button2'); - // This should be debounced - await page.click('#button2'); - - // Wait a second for the debounce to finish - await page.waitForTimeout(1000); - await page.click('#button2'); - - const [eventData] = await Promise.all([promise, page.evaluate('Sentry.captureException("test exception")')]); - - expect(eventData.exception?.values).toHaveLength(1); - - expect(eventData.breadcrumbs).toEqual([ - { - timestamp: expect.any(Number), - category: 'ui.click', - message: 'body > button#button1[type="button"]', - }, - { - timestamp: expect.any(Number), - category: 'ui.click', - message: 'body > button#button2[type="button"]', - }, - { - timestamp: expect.any(Number), - category: 'ui.click', - message: 'body > button#button2[type="button"]', - }, - ]); -}); + + const promise = getFirstSentryEnvelopeRequest(page); + + await page.goto(url); + + await page.click('#button1'); + // not debounced because other target + await page.click('#button2'); + // This should be debounced + await page.click('#button2'); + + // Wait a second for the debounce to finish + await page.waitForTimeout(1000); + await page.click('#button2'); + + const [eventData] = await Promise.all([promise, page.evaluate('Sentry.captureException("test exception")')]); + + expect(eventData.exception?.values).toHaveLength(1); + + expect(eventData.breadcrumbs).toEqual([ + { + timestamp: expect.any(Number), + category: 'ui.click', + message: 'body > button#button1[type="button"]', + }, + { + timestamp: expect.any(Number), + category: 'ui.click', + message: 'body > button#button2[type="button"]', + }, + { + timestamp: expect.any(Number), + category: 'ui.click', + message: 'body > button#button2[type="button"]', + }, + ]); + }, +); sentryTest( 'uses the annotated component name in the breadcrumb messages and adds it to the data object', diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/eventBreadcrumbs/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureException/eventBreadcrumbs/subject.js new file mode 100644 index 000000000000..2926745ae8d5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/eventBreadcrumbs/subject.js @@ -0,0 +1,3 @@ +Sentry.captureMessage('a'); + +Sentry.captureException(new Error('test_simple_breadcrumb_error')); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/eventBreadcrumbs/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureException/eventBreadcrumbs/test.ts new file mode 100644 index 000000000000..831c255cb8f8 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/eventBreadcrumbs/test.ts @@ -0,0 +1,23 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; + +sentryTest( + 'should capture recorded transactions as breadcrumbs for the following event sent', + async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const events = await getMultipleSentryEnvelopeRequests(page, 2, { url }); + + const errorEvent = events.find(event => event.exception?.values?.[0].value === 'test_simple_breadcrumb_error')!; + + expect(errorEvent.breadcrumbs).toHaveLength(1); + expect(errorEvent.breadcrumbs?.[0]).toMatchObject({ + category: 'sentry.event', + event_id: expect.any(String), + level: expect.any(String), + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/transactionBreadcrumbs/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureException/transactionBreadcrumbs/subject.js new file mode 100644 index 000000000000..86bf5ab29abd --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/transactionBreadcrumbs/subject.js @@ -0,0 +1,8 @@ +Sentry.captureEvent({ + event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', + message: 'someMessage', + transaction: 'wat', + type: 'transaction', +}); + +Sentry.captureException(new Error('test_simple_breadcrumb_error')); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/transactionBreadcrumbs/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureException/transactionBreadcrumbs/test.ts new file mode 100644 index 000000000000..c69437183591 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/transactionBreadcrumbs/test.ts @@ -0,0 +1,22 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; + +sentryTest( + 'should capture recorded transactions as breadcrumbs for the following event sent', + async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const events = await getMultipleSentryEnvelopeRequests(page, 2, { url }); + + const errorEvent = events.find(event => event.exception?.values?.[0].value === 'test_simple_breadcrumb_error')!; + + expect(errorEvent.breadcrumbs).toHaveLength(1); + expect(errorEvent.breadcrumbs?.[0]).toMatchObject({ + category: 'sentry.transaction', + message: expect.any(String), + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/parameterized_message/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/parameterized_message/subject.js new file mode 100644 index 000000000000..a86616cd52dc --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/parameterized_message/subject.js @@ -0,0 +1,6 @@ +import { parameterize } from '@sentry/utils'; + +const x = 'first'; +const y = 'second'; + +Sentry.captureMessage(parameterize`This is a log statement with ${x} and ${y} params`); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts new file mode 100644 index 000000000000..4c948d439bff --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts @@ -0,0 +1,22 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest('should capture a parameterized representation of the message', async ({ getLocalTestPath, page }) => { + const bundle = process.env.PW_BUNDLE; + + if (bundle && bundle.startsWith('bundle_')) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.logentry).toStrictEqual({ + message: 'This is a log statement with %s and %s params', + params: ['first', 'second'], + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts index a4b5d8b9bd02..95d09927e463 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts @@ -29,6 +29,7 @@ sentryTest('should report finished spans as children of the root transaction', a expect(transaction.spans).toHaveLength(1); const span_1 = transaction.spans?.[0]; + // eslint-disable-next-line deprecation/deprecation expect(span_1?.description).toBe('child_span'); expect(span_1?.parentSpanId).toEqual(rootSpanId); }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts index 2fba18b0804b..1012ae61e1ff 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts @@ -31,6 +31,7 @@ sentryTest('should report finished spans as children of the root transaction', a const span_1 = transaction.spans?.[0]; expect(span_1?.op).toBe('span_1'); expect(span_1?.parentSpanId).toEqual(rootSpanId); + // eslint-disable-next-line deprecation/deprecation expect(span_1?.data).toMatchObject({ foo: 'bar', baz: [1, 2, 3] }); const span_3 = transaction.spans?.[1]; @@ -39,5 +40,6 @@ sentryTest('should report finished spans as children of the root transaction', a const span_5 = transaction.spans?.[2]; expect(span_5?.op).toBe('span_5'); + // eslint-disable-next-line deprecation/deprecation expect(span_5?.parentSpanId).toEqual(span_3?.spanId); }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/subject.js index d2ae465addf7..9d4da2e3027a 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/subject.js @@ -2,10 +2,8 @@ const chicken = {}; const egg = { contains: chicken }; chicken.lays = egg; -const circularObject = chicken; - -const transaction = Sentry.startTransaction({ name: 'circular_object_test_transaction', data: circularObject }); -const span = transaction.startChild({ op: 'circular_object_test_span', data: circularObject }); +const transaction = Sentry.startTransaction({ name: 'circular_object_test_transaction', data: { chicken } }); +const span = transaction.startChild({ op: 'circular_object_test_span', data: { chicken } }); span.end(); transaction.end(); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/test.ts index 1870f679b3da..b6e88e821cbb 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/test.ts @@ -17,12 +17,12 @@ sentryTest('should be able to handle circular data', async ({ getLocalTestPath, expect(eventData.contexts).toMatchObject({ trace: { - data: { lays: { contains: '[Circular ~]' } }, + data: { chicken: { lays: { contains: '[Circular ~]' } } }, }, }); expect(eventData?.spans?.[0]).toMatchObject({ - data: { lays: { contains: '[Circular ~]' } }, + data: { chicken: { lays: { contains: '[Circular ~]' } } }, op: 'circular_object_test_span', }); diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js new file mode 100644 index 000000000000..e530d741a8bc --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js @@ -0,0 +1,24 @@ +import { getCanvasManager } from '@sentry-internal/rrweb'; +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.Replay = new Sentry.Replay({ + flushMinDelay: 50, + flushMaxDelay: 50, + minReplayDuration: 0, + _experiments: { + canvas: { + manager: getCanvasManager, + }, + }, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 0, + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + debug: true, + + integrations: [window.Replay], +}); diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/records/template.html b/dev-packages/browser-integration-tests/suites/replay/canvas/records/template.html new file mode 100644 index 000000000000..5f23d569fcc2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/records/template.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts b/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts new file mode 100644 index 000000000000..372ca8978356 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts @@ -0,0 +1,71 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getReplayRecordingContent, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers'; + +sentryTest('can record canvas', async ({ getLocalTestUrl, page, browserName }) => { + if (shouldSkipReplayTest() || browserName === 'webkit') { + sentryTest.skip(); + } + + const reqPromise0 = waitForReplayRequest(page, 0); + const reqPromise1 = waitForReplayRequest(page, 1); + const reqPromise2 = waitForReplayRequest(page, 2); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + await reqPromise0; + await Promise.all([page.click('#draw'), reqPromise1]); + + const { incrementalSnapshots } = getReplayRecordingContent(await reqPromise2); + expect(incrementalSnapshots).toEqual( + expect.arrayContaining([ + { + data: { + commands: [ + { + args: [0, 0, 150, 150], + property: 'clearRect', + }, + { + args: [ + { + args: [ + { + data: [ + { + base64: expect.any(String), + rr_type: 'ArrayBuffer', + }, + ], + rr_type: 'Blob', + type: 'image/webp', + }, + ], + rr_type: 'ImageBitmap', + }, + 0, + 0, + ], + property: 'drawImage', + }, + ], + id: 9, + source: 9, + type: 0, + }, + timestamp: 0, + type: 3, + }, + ]), + ); +}); diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/template.html b/dev-packages/browser-integration-tests/suites/replay/canvas/template.html new file mode 100644 index 000000000000..2b3e2f0b27b4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/template.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts b/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts index 4468a254bde4..63c103e51259 100644 --- a/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts @@ -33,7 +33,10 @@ sentryTest( await page.evaluate(() => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); + scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; + }); }); const req0 = await transactionReq; @@ -78,7 +81,10 @@ sentryTest( await page.evaluate(() => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); + scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; + }); }); const req0 = await transactionReq; @@ -135,7 +141,10 @@ sentryTest( await page.evaluate(() => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); + scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; + }); }); const req0 = await transactionReq; @@ -183,7 +192,10 @@ sentryTest( await page.evaluate(async () => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); + scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; + }); }); const req0 = await transactionReq; diff --git a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/init.js b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/init.js new file mode 100644 index 000000000000..4958e35f2198 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '0.1', + // intentionally disabling this, we want to leverage the deprecated hub API + autoSessionTracking: false, +}); + +// simulate old startSessionTracking behavior +Sentry.getCurrentHub().startSession({ ignoreDuration: true }); +Sentry.getCurrentHub().captureSession(); diff --git a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/template.html b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/template.html new file mode 100644 index 000000000000..77906444cbce --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/template.html @@ -0,0 +1,9 @@ + + + + + + + Navigate + + diff --git a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts new file mode 100644 index 000000000000..8a48f161c93b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts @@ -0,0 +1,39 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { SessionContext } from '@sentry/types'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; + +sentryTest('should start a new session on pageload.', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + const session = await getFirstSentryEnvelopeRequest(page, url); + + expect(session).toBeDefined(); + expect(session.init).toBe(true); + expect(session.errors).toBe(0); + expect(session.status).toBe('ok'); +}); + +sentryTest('should start a new session with navigation.', async ({ getLocalTestPath, page, browserName }) => { + // Navigations get CORS error on Firefox and WebKit as we're using `file://` protocol. + if (browserName !== 'chromium') { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + await page.route('**/foo', (route: Route) => route.fulfill({ path: `${__dirname}/dist/index.html` })); + + const initSession = await getFirstSentryEnvelopeRequest(page, url); + + await page.click('#navigate'); + + const newSession = await getFirstSentryEnvelopeRequest(page, url); + + expect(newSession).toBeDefined(); + expect(newSession.init).toBe(true); + expect(newSession.errors).toBe(0); + expect(newSession.status).toBe('ok'); + expect(newSession.sid).toBeDefined(); + expect(initSession.sid).not.toBe(newSession.sid); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js b/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js index 7d000c0ac2cd..c2fcbb33a24c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js @@ -13,5 +13,8 @@ Sentry.init({ const scope = Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); -scope.setTransactionName('testTransactionDSC'); +scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; +}); scope.getTransaction().setMetadata({ source: 'custom' }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/envelope-header/init.js b/dev-packages/browser-integration-tests/suites/tracing/envelope-header/init.js index f382a49c153d..1528306fcbde 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/envelope-header/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/envelope-header/init.js @@ -13,4 +13,7 @@ Sentry.init({ const scope = Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); -scope.setTransactionName('testTransactionDSC'); +scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts index 55e0b4d0e833..aaab7059320c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts @@ -21,6 +21,7 @@ sentryTest('should capture a FID vital.', async ({ browserName, getLocalTestPath expect(eventData.measurements).toBeDefined(); expect(eventData.measurements?.fid?.value).toBeDefined(); + // eslint-disable-next-line deprecation/deprecation const fidSpan = eventData.spans?.filter(({ description }) => description === 'first input delay')[0]; expect(fidSpan).toBeDefined(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts index 3a97c62d7f68..4914c0b45779 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts @@ -16,6 +16,7 @@ sentryTest('should capture FP vital.', async ({ browserName, getLocalTestPath, p expect(eventData.measurements).toBeDefined(); expect(eventData.measurements?.fp?.value).toBeDefined(); + // eslint-disable-next-line deprecation/deprecation const fpSpan = eventData.spans?.filter(({ description }) => description === 'first-paint')[0]; expect(fpSpan).toBeDefined(); @@ -34,6 +35,7 @@ sentryTest('should capture FCP vital.', async ({ getLocalTestPath, page }) => { expect(eventData.measurements).toBeDefined(); expect(eventData.measurements?.fcp?.value).toBeDefined(); + // eslint-disable-next-line deprecation/deprecation const fcpSpan = eventData.spans?.filter(({ description }) => description === 'first-contentful-paint')[0]; expect(fcpSpan).toBeDefined(); diff --git a/dev-packages/browser-integration-tests/utils/replayHelpers.ts b/dev-packages/browser-integration-tests/utils/replayHelpers.ts index 613bd5b447f1..2a951e4a215e 100644 --- a/dev-packages/browser-integration-tests/utils/replayHelpers.ts +++ b/dev-packages/browser-integration-tests/utils/replayHelpers.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import type { fullSnapshotEvent, incrementalSnapshotEvent } from '@sentry-internal/rrweb'; import { EventType } from '@sentry-internal/rrweb'; import type { ReplayEventWithTime } from '@sentry/browser'; @@ -5,6 +6,7 @@ import type { InternalEventContext, RecordingEvent, ReplayContainer, + ReplayPluginOptions, Session, } from '@sentry/replay/build/npm/types/types'; import type { Breadcrumb, Event, ReplayEvent, ReplayRecordingMode } from '@sentry/types'; @@ -171,6 +173,8 @@ export function getReplaySnapshot(page: Page): Promise<{ _isPaused: boolean; _isEnabled: boolean; _context: InternalEventContext; + _options: ReplayPluginOptions; + _hasCanvas: boolean; session: Session | undefined; recordingMode: ReplayRecordingMode; }> { @@ -182,6 +186,9 @@ export function getReplaySnapshot(page: Page): Promise<{ _isPaused: replay.isPaused(), _isEnabled: replay.isEnabled(), _context: replay.getContext(), + _options: replay.getOptions(), + // We cannot pass the function through as this is serialized + _hasCanvas: typeof replay.getOptions()._experiments.canvas?.manager === 'function', session: replay.session, recordingMode: replay.recordingMode, }; diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts b/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts index 8a5a53f2e4dc..56eeb9189882 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts +++ b/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts @@ -3,9 +3,12 @@ import * as Sentry from '@sentry/nextjs'; import type { NextApiRequest, NextApiResponse } from 'next'; export default function handler(req: NextApiRequest, res: NextApiResponse) { + // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test-transaction', op: 'e2e-test' }); + // eslint-disable-next-line deprecation/deprecation Sentry.getCurrentHub().getScope().setSpan(transaction); + // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild(); span.end(); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-notfound/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-notfound/page.tsx new file mode 100644 index 000000000000..46d4ddd7f962 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-notfound/page.tsx @@ -0,0 +1,11 @@ +import { notFound } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export default function PageWithRedirect() { + return

Hello World!

; +} + +export async function generateMetadata() { + notFound(); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-redirect/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-redirect/page.tsx new file mode 100644 index 000000000000..f1f37d7a32c6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-redirect/page.tsx @@ -0,0 +1,11 @@ +import { redirect } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export default function PageWithRedirect() { + return

Hello World!

; +} + +export async function generateMetadata() { + redirect('/'); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-14/app/page.tsx new file mode 100644 index 000000000000..6f4e63ef5748 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

Home

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts index 3828312607ea..b5fe7ee67393 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts @@ -77,3 +77,37 @@ test('Should send a transaction and an error event for a faulty generateViewport expect(await transactionPromise).toBeDefined(); expect(await errorEventPromise).toBeDefined(); }); + +test('Should send a transaction event with correct status for a generateMetadata() function invokation with redirect()', async ({ + page, +}) => { + const testTitle = 'redirect-foobar'; + + const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { + return ( + transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions/with-redirect)' && + transactionEvent.contexts?.trace?.data?.['searchParams']?.['metadataTitle'] === testTitle + ); + }); + + await page.goto(`/generation-functions/with-redirect?metadataTitle=${testTitle}`); + + expect((await transactionPromise).contexts?.trace?.status).toBe('ok'); +}); + +test('Should send a transaction event with correct status for a generateMetadata() function invokation with notfound()', async ({ + page, +}) => { + const testTitle = 'notfound-foobar'; + + const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { + return ( + transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions/with-notfound)' && + transactionEvent.contexts?.trace?.data?.['searchParams']?.['metadataTitle'] === testTitle + ); + }); + + await page.goto(`/generation-functions/with-notfound?metadataTitle=${testTitle}`); + + expect((await transactionPromise).contexts?.trace?.status).toBe('not_found'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts index ca4f4cb0d1ff..f975ec49e606 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts @@ -130,9 +130,9 @@ test('Should send a transaction for instrumented server actions', async ({ page await page.getByText('Run Action').click(); expect(await serverComponentTransactionPromise).toBeDefined(); - expect((await serverComponentTransactionPromise).contexts?.trace?.data?.['server_action_form_data']).toEqual( - expect.objectContaining({ 'some-text-value': 'some-default-value' }), - ); + expect( + (await serverComponentTransactionPromise).contexts?.trace?.data?.['server_action_form_data.some-text-value'], + ).toEqual('some-default-value'); expect((await serverComponentTransactionPromise).contexts?.trace?.data?.['server_action_result']).toEqual({ city: 'Vienna', }); diff --git a/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts index 269f8df45bbe..43c952b23594 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts @@ -34,9 +34,11 @@ app.get('/test-param/:param', function (req, res) { }); app.get('/test-transaction', async function (req, res) { + // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test-transaction', op: 'e2e-test' }); Sentry.getCurrentScope().setSpan(transaction); + // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild(); span.end(); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts index 31e5580fe56a..11ccc4d732e0 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts @@ -26,8 +26,10 @@ app.use(Sentry.Handlers.tracingHandler()); app.use(cors()); app.get('/test/express', (_req, res) => { + // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.getCurrentHub().getScope().getTransaction(); if (transaction) { + // eslint-disable-next-line deprecation/deprecation transaction.traceId = '86f39e84263a4de99c326acab3bfe3bd'; } const headers = http.get('http://somewhere.not.sentry/').getHeaders(); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts index 061a7815ca75..28a84f87cf52 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts @@ -1,4 +1,5 @@ import http from 'http'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import * as Sentry from '@sentry/node'; import * as Tracing from '@sentry/tracing'; import cors from 'cors'; @@ -29,10 +30,12 @@ app.use(Sentry.Handlers.tracingHandler()); app.use(cors()); app.get('/test/express', (_req, res) => { + // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.getCurrentHub().getScope().getTransaction(); if (transaction) { + // eslint-disable-next-line deprecation/deprecation transaction.traceId = '86f39e84263a4de99c326acab3bfe3bd'; - transaction.setMetadata({ source: 'route' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } const headers = http.get('http://somewhere.not.sentry/').getHeaders(); diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-memory-test.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-memory-test.js new file mode 100644 index 000000000000..7b227d4d08de --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-memory-test.js @@ -0,0 +1,44 @@ +/* eslint-disable no-unused-vars */ +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + includeLocalVariables: true, + beforeSend: _ => { + return null; + }, + // Stop the rate limiting from kicking in + integrations: [new Sentry.Integrations.LocalVariables({ maxExceptionsPerSecond: 10000000 })], +}); + +class Some { + two(name) { + throw new Error('Enough!'); + } +} + +function one(name) { + const arr = [1, '2', null]; + const obj = { + name, + num: 5, + }; + + const ty = new Some(); + + ty.two(name); +} + +// Every millisecond cause a caught exception +setInterval(() => { + try { + one('some name'); + } catch (e) { + // + } +}, 1); + +// Every second send a memory usage update to parent process +setInterval(() => { + process.send({ memUsage: process.memoryUsage() }); +}, 1000); diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts index 0b542c19c629..1c58fd802122 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts @@ -109,4 +109,29 @@ conditionalTest({ min: 18 })('LocalVariables integration', () => { done(); }); }); + + test('Should not leak memory', done => { + const testScriptPath = path.resolve(__dirname, 'local-variables-memory-test.js'); + + const child = childProcess.spawn('node', [testScriptPath], { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + }); + + let reportedCount = 0; + + child.on('message', msg => { + reportedCount++; + const rssMb = msg.memUsage.rss / 1024 / 1024; + // We shouldn't use more than 100MB of memory + expect(rssMb).toBeLessThan(100); + }); + + // Wait for 20 seconds + setTimeout(() => { + // Ensure we've had memory usage reported at least 15 times + expect(reportedCount).toBeGreaterThan(15); + child.kill(); + done(); + }, 20000); + }); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts new file mode 100644 index 000000000000..db81bb18d331 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/node'; +import { parameterize } from '@sentry/utils'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', +}); + +const x = 'first'; +const y = 'second'; + +Sentry.captureMessage(parameterize`This is a log statement with ${x} and ${y} params`); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts new file mode 100644 index 000000000000..d9015987187f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts @@ -0,0 +1,13 @@ +import { TestEnv, assertSentryEvent } from '../../../../utils'; + +test('should capture a parameterized representation of the message', async () => { + const env = await TestEnv.init(__dirname); + const event = await env.getEnvelopeRequest(); + + assertSentryEvent(event[2], { + logentry: { + message: 'This is a log statement with %s and %s params', + params: ['first', 'second'], + }, + }); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startTransaction/basic-usage/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startTransaction/basic-usage/scenario.ts index 1e4931a2bae7..70596da19716 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startTransaction/basic-usage/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startTransaction/basic-usage/scenario.ts @@ -9,6 +9,7 @@ Sentry.init({ tracesSampleRate: 1.0, }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction_1' }); transaction.end(); diff --git a/dev-packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts index a340de7b21fe..f82fe81d969a 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts @@ -8,7 +8,9 @@ Sentry.init({ tracesSampleRate: 1.0, }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction_1' }); +// eslint-disable-next-line deprecation/deprecation const span_1 = transaction.startChild({ op: 'span_1', data: { @@ -22,16 +24,20 @@ for (let i = 0; i < 2000; i++); span_1.end(); // span_2 doesn't finish +// eslint-disable-next-line deprecation/deprecation transaction.startChild({ op: 'span_2' }); for (let i = 0; i < 4000; i++); +// eslint-disable-next-line deprecation/deprecation const span_3 = transaction.startChild({ op: 'span_3' }); for (let i = 0; i < 4000; i++); // span_4 is the child of span_3 but doesn't finish. +// eslint-disable-next-line deprecation/deprecation span_3.startChild({ op: 'span_4', data: { qux: 'quux' } }); // span_5 is another child of span_3 but finishes. +// eslint-disable-next-line deprecation/deprecation span_3.startChild({ op: 'span_5' }).end(); // span_3 also finishes diff --git a/dev-packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts index b37bd6df6fc9..1584274bce7d 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts @@ -27,8 +27,10 @@ const server = new ApolloServer({ resolvers, }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction', op: 'transaction' }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts index 36f8c3503832..67d8e13750de 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts @@ -16,11 +16,13 @@ const client = new MongoClient(process.env.MONGO_URL || '', { }); async function run(): Promise { + // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'Test Transaction', op: 'transaction', }); + // eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); try { diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts index 9b033e72a669..8273d4dfa96a 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts @@ -19,11 +19,13 @@ connection.connect(function (err: unknown) { } }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ op: 'transaction', name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); connection.query('SELECT 1 + 1 AS solution', function () { diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts index 23d07f346875..2b06970b5243 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts @@ -19,21 +19,23 @@ connection.connect(function (err: unknown) { } }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ op: 'transaction', name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); const query = connection.query('SELECT 1 + 1 AS solution'); const query2 = connection.query('SELECT NOW()', ['1', '2']); query.on('end', () => { - transaction.setTag('result_done', 'yes'); + transaction.setAttribute('result_done', 'yes'); query2.on('end', () => { - transaction.setTag('result_done2', 'yes'); + transaction.setAttribute('result_done2', 'yes'); // Wait a bit to ensure the queries completed setTimeout(() => { diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/test.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/test.ts index 0f6dee99d59b..f83e4297b8ba 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/test.ts @@ -7,11 +7,15 @@ test('should auto-instrument `mysql` package when using query without callback', expect(envelope).toHaveLength(3); assertSentryTransaction(envelope[2], { - transaction: 'Test Transaction', - tags: { - result_done: 'yes', - result_done2: 'yes', + contexts: { + trace: { + data: { + result_done: 'yes', + result_done2: 'yes', + }, + }, }, + transaction: 'Test Transaction', spans: [ { description: 'SELECT 1 + 1 AS solution', diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts index bf9e4bf90e35..d88b2d1c8d24 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts @@ -13,11 +13,13 @@ const connection = mysql.createConnection({ password: 'docker', }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ op: 'transaction', name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); connection.query('SELECT 1 + 1 AS solution', function () { diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts index b41be87c9550..47fb37e054f7 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts @@ -8,11 +8,13 @@ Sentry.init({ integrations: [...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations()], }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ op: 'transaction', name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); const client = new pg.Client(); diff --git a/dev-packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts index 20847871e7a1..6191dbf31d75 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts @@ -13,11 +13,13 @@ Sentry.init({ }); async function run(): Promise { + // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'Test Transaction', op: 'transaction', }); + // eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); try { diff --git a/dev-packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts index 7c86686cbba8..600b5ef71038 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts @@ -10,8 +10,10 @@ Sentry.init({ integrations: [new Sentry.Integrations.Http({ tracing: true })], }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction' }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); http.get('http://match-this-url.com/api/v0'); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts index 7b34ffab0613..6a699fa07af7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts @@ -29,8 +29,10 @@ const server = new ApolloServer({ resolvers, }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction', op: 'transaction' }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts index cff8329d22a3..51359ac726da 100644 --- a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts @@ -17,11 +17,13 @@ const client = new MongoClient(process.env.MONGO_URL || '', { }); async function run(): Promise { + // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'Test Transaction', op: 'transaction', }); + // eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); try { diff --git a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts index 30f9fb368b3a..ce53d776fe54 100644 --- a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts @@ -20,11 +20,13 @@ connection.connect(function (err: unknown) { } }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ op: 'transaction', name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); connection.query('SELECT 1 + 1 AS solution', function () { diff --git a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts index 95248c82f075..f9bfa0de0294 100644 --- a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts @@ -9,11 +9,13 @@ Sentry.init({ tracesSampleRate: 1.0, }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ op: 'transaction', name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); const client = new pg.Client(); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts index 7e8a7c6eca5f..b5003141caec 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts @@ -15,11 +15,13 @@ Sentry.init({ }); async function run(): Promise { + // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'Test Transaction', op: 'transaction', }); + // eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); try { diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts index 9fdeba1fcb95..8ecb7ed3cc61 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts @@ -12,8 +12,10 @@ Sentry.init({ integrations: [new Sentry.Integrations.Http({ tracing: true })], }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction' }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); http.get('http://match-this-url.com/api/v0'); diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs index 66f8e8c78228..9aef97dc828f 100644 --- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs +++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs @@ -135,6 +135,9 @@ export function makeTerserPlugin() { // These are used by instrument.ts in utils for identifying HTML elements & events '_sentryCaptured', '_sentryId', + // For v7 backwards-compatibility we need to access txn._frozenDynamicSamplingContext + // TODO (v8): Remove this reserved word + '_frozenDynamicSamplingContext', ], }, }, diff --git a/docs/v8-new-performance-apis.md b/docs/v8-new-performance-apis.md index e7ec274bbb10..2f531858dd2c 100644 --- a/docs/v8-new-performance-apis.md +++ b/docs/v8-new-performance-apis.md @@ -50,8 +50,8 @@ below to see which things used to exist, and how they can/should be mapped going | `status` | use utility method TODO | | `sampled` | `spanIsSampled(span)` | | `startTimestamp` | `startTime` - note that this has a different format! | -| `tags` | `spanGetAttributes(span)`, or set tags on the scope | -| `data` | `spanGetAttributes(span)` | +| `tags` | use attributes, or set tags on the scope | +| `data` | `spanToJSON(span).data` | | `transaction` | ??? Removed | | `instrumenter` | Removed | | `finish()` | `end()` | @@ -72,13 +72,13 @@ In addition, a transaction has this API: | Old name | Replace with | | --------------------------- | ------------------------------------------------ | -| `name` | `spanGetName(span)` (TODO) | +| `name` | `spanToJSON(span).description` | | `trimEnd` | Removed | | `parentSampled` | `spanIsSampled(span)` & `spanContext().isRemote` | -| `metadata` | `spanGetMetadata(span)` | +| `metadata` | Use attributes instead or set on scope | | `setContext()` | Set context on scope instead | | `setMeasurement()` | ??? TODO | -| `setMetadata()` | `spanSetMetadata(span, metadata)` | +| `setMetadata()` | Use attributes instead or set on scope | | `getDynamicSamplingContext` | ??? TODO | ### Attributes vs. Data vs. Tags vs. Context diff --git a/packages/angular-ivy/README.md b/packages/angular-ivy/README.md index f487ffa22707..6967e7570a82 100644 --- a/packages/angular-ivy/README.md +++ b/packages/angular-ivy/README.md @@ -215,33 +215,22 @@ export class FooterComponent implements OnInit { } ``` -You can also add your own custom spans by attaching them to the current active transaction using `getActiveTransaction` -helper. For example, if you'd like to track the duration of Angular boostraping process, you can do it as follows: +You can also add your own custom spans via `startSpan()`. For example, if you'd like to track the duration of Angular boostraping process, you can do it as follows: ```javascript import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { init, getActiveTransaction } from '@sentry/angular-ivy'; +import { init, startSpan } from '@sentry/angular'; import { AppModule } from './app/app.module'; // ... - -const activeTransaction = getActiveTransaction(); -const boostrapSpan = - activeTransaction && - activeTransaction.startChild({ - description: 'platform-browser-dynamic', - op: 'ui.angular.bootstrap', - }); - -platformBrowserDynamic() - .bootstrapModule(AppModule) - .then(() => console.log(`Bootstrap success`)) - .catch(err => console.error(err)); - .finally(() => { - if (bootstrapSpan) { - boostrapSpan.finish(); - } - }) +startSpan({ + name: 'platform-browser-dynamic', + op: 'ui.angular.bootstrap' + }, + async () => { + await platformBrowserDynamic().bootstrapModule(AppModule); + } +); ``` diff --git a/packages/angular-ivy/ng-package.json b/packages/angular-ivy/ng-package.json index b50faf694df3..38d9d7f5ac68 100644 --- a/packages/angular-ivy/ng-package.json +++ b/packages/angular-ivy/ng-package.json @@ -5,9 +5,10 @@ "entryFile": "src/index.ts", "umdModuleIds": { "@sentry/browser": "Sentry", - "@sentry/utils": "Sentry.util" + "@sentry/utils": "Sentry.util", + "@sentry/core": "Sentry.core" } }, - "allowedNonPeerDependencies": ["@sentry/browser", "@sentry/utils", "@sentry/types", "tslib"], + "allowedNonPeerDependencies": ["@sentry/browser", "@sentry/core", "@sentry/utils", "@sentry/types", "tslib"], "assets": ["README.md", "LICENSE"] } diff --git a/packages/angular-ivy/package.json b/packages/angular-ivy/package.json index 9d9ccd91e4db..14b10ae27954 100644 --- a/packages/angular-ivy/package.json +++ b/packages/angular-ivy/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@sentry/browser": "7.92.0", + "@sentry/core": "7.92.0", "@sentry/types": "7.92.0", "@sentry/utils": "7.92.0", "tslib": "^2.4.1" diff --git a/packages/angular/README.md b/packages/angular/README.md index aa18724839a4..302b060bdb39 100644 --- a/packages/angular/README.md +++ b/packages/angular/README.md @@ -215,33 +215,22 @@ export class FooterComponent implements OnInit { } ``` -You can also add your own custom spans by attaching them to the current active transaction using `getActiveTransaction` -helper. For example, if you'd like to track the duration of Angular boostraping process, you can do it as follows: +You can also add your own custom spans via `startSpan()`. For example, if you'd like to track the duration of Angular boostraping process, you can do it as follows: ```javascript import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { init, getActiveTransaction } from '@sentry/angular'; +import { init, startSpan } from '@sentry/angular'; import { AppModule } from './app/app.module'; // ... - -const activeTransaction = getActiveTransaction(); -const boostrapSpan = - activeTransaction && - activeTransaction.startChild({ - description: 'platform-browser-dynamic', - op: 'ui.angular.bootstrap', - }); - -platformBrowserDynamic() - .bootstrapModule(AppModule) - .then(() => console.log(`Bootstrap success`)) - .catch(err => console.error(err)); - .finally(() => { - if (bootstrapSpan) { - boostrapSpan.finish(); - } - }) +startSpan({ + name: 'platform-browser-dynamic', + op: 'ui.angular.bootstrap' + }, + async () => { + await platformBrowserDynamic().bootstrapModule(AppModule); + } +); ``` diff --git a/packages/angular/ng-package.json b/packages/angular/ng-package.json index 88df70c1c7bd..28794322dd0a 100644 --- a/packages/angular/ng-package.json +++ b/packages/angular/ng-package.json @@ -5,9 +5,10 @@ "entryFile": "src/index.ts", "umdModuleIds": { "@sentry/browser": "Sentry", - "@sentry/utils": "Sentry.util" + "@sentry/utils": "Sentry.util", + "@sentry/core": "Sentry.core" } }, - "whitelistedNonPeerDependencies": ["@sentry/browser", "@sentry/utils", "@sentry/types", "tslib"], + "whitelistedNonPeerDependencies": ["@sentry/browser", "@sentry/core", "@sentry/utils", "@sentry/types", "tslib"], "assets": ["README.md", "LICENSE"] } diff --git a/packages/angular/package.json b/packages/angular/package.json index c1cfc9765071..55998227fc7c 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@sentry/browser": "7.92.0", + "@sentry/core": "7.92.0", "@sentry/types": "7.92.0", "@sentry/utils": "7.92.0", "tslib": "^2.4.1" diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index b6f188a35d14..f7f0536463a2 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -5,6 +5,7 @@ export * from '@sentry/browser'; export { init } from './sdk'; export { createErrorHandler, SentryErrorHandler } from './errorhandler'; export { + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, // TODO `instrumentAngularRouting` is just an alias for `routingInstrumentation`; deprecate the latter at some point instrumentAngularRouting, // new name diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index e65ecd84d8df..efd2c840420b 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -8,6 +8,7 @@ import type { ActivatedRouteSnapshot, Event, RouterState } from '@angular/router import { NavigationCancel, NavigationError, Router } from '@angular/router'; import { NavigationEnd, NavigationStart, ResolveEnd } from '@angular/router'; import { WINDOW, getCurrentScope } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; import type { Span, Transaction, TransactionContext } from '@sentry/types'; import { logger, stripUrlQueryAndFragment, timestampInSeconds } from '@sentry/utils'; import type { Observable } from 'rxjs'; @@ -39,7 +40,9 @@ export function routingInstrumentation( name: WINDOW.location.pathname, op: 'pageload', origin: 'auto.pageload.angular', - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, }); } } @@ -47,9 +50,12 @@ export function routingInstrumentation( export const instrumentAngularRouting = routingInstrumentation; /** - * Grabs active transaction off scope + * Grabs active transaction off scope. + * + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. */ export function getActiveTransaction(): Transaction | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentScope().getTransaction(); } @@ -69,6 +75,7 @@ export class TraceService implements OnDestroy { } const strippedUrl = stripUrlQueryAndFragment(navigationEvent.url); + // eslint-disable-next-line deprecation/deprecation let activeTransaction = getActiveTransaction(); if (!activeTransaction && stashedStartTransactionOnLocationChange) { @@ -76,7 +83,9 @@ export class TraceService implements OnDestroy { name: strippedUrl, op: 'navigation', origin: 'auto.navigation.angular', - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, }); } @@ -84,6 +93,7 @@ export class TraceService implements OnDestroy { if (this._routingSpan) { this._routingSpan.end(); } + // eslint-disable-next-line deprecation/deprecation this._routingSpan = activeTransaction.startChild({ description: `${navigationEvent.url}`, op: ANGULAR_ROUTING_OP, @@ -115,11 +125,13 @@ export class TraceService implements OnDestroy { (event.state as unknown as RouterState & { root: ActivatedRouteSnapshot }).root, ); + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); // TODO (v8 / #5416): revisit the source condition. Do we want to make the parameterized route the default? - if (transaction && transaction.metadata.source === 'url') { + const attributes = (transaction && spanToJSON(transaction).data) || {}; + if (transaction && attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'url') { transaction.updateName(route); - transaction.setMetadata({ source: 'route' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } }), ); @@ -181,8 +193,10 @@ export class TraceDirective implements OnInit, AfterViewInit { this.componentName = UNKNOWN_COMPONENT; } + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); if (activeTransaction) { + // eslint-disable-next-line deprecation/deprecation this._tracingSpan = activeTransaction.startChild({ description: `<${this.componentName}>`, op: ANGULAR_INIT_OP, @@ -223,8 +237,10 @@ export function TraceClassDecorator(): ClassDecorator { const originalOnInit = target.prototype.ngOnInit; // eslint-disable-next-line @typescript-eslint/no-explicit-any target.prototype.ngOnInit = function (...args: any[]): ReturnType { + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); if (activeTransaction) { + // eslint-disable-next-line deprecation/deprecation tracingSpan = activeTransaction.startChild({ description: `<${target.name}>`, op: ANGULAR_INIT_OP, @@ -260,8 +276,10 @@ export function TraceMethodDecorator(): MethodDecorator { // eslint-disable-next-line @typescript-eslint/no-explicit-any descriptor.value = function (...args: any[]): ReturnType { const now = timestampInSeconds(); + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); if (activeTransaction) { + // eslint-disable-next-line deprecation/deprecation activeTransaction.startChild({ description: `<${target.constructor.name}>`, endTimestamp: now, diff --git a/packages/angular/test/tracing.test.ts b/packages/angular/test/tracing.test.ts index 695e3d7af564..c2406f628128 100644 --- a/packages/angular/test/tracing.test.ts +++ b/packages/angular/test/tracing.test.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; import type { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { TraceClassDecorator, TraceDirective, TraceMethodDecorator, instrumentAngularRouting } from '../src'; import { getParameterizedRouteFromSnapshot } from '../src/tracing'; @@ -11,7 +12,14 @@ const defaultStartTransaction = (ctx: any) => { transaction = { ...ctx, updateName: jest.fn(name => (transaction.name = name)), - setMetadata: jest.fn(), + setAttribute: jest.fn(), + toJSON: () => ({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + ...ctx.data, + ...ctx.attributes, + }, + }), }; return transaction; @@ -45,7 +53,7 @@ describe('Angular Tracing', () => { name: '/', op: 'pageload', origin: 'auto.pageload.angular', - metadata: { source: 'url' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, }); }); }); @@ -107,11 +115,15 @@ describe('Angular Tracing', () => { const customStartTransaction = jest.fn((ctx: any) => { transaction = { ...ctx, - metadata: { - ...ctx.metadata, - source: 'custom', - }, + toJSON: () => ({ + data: { + ...ctx.data, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + }, + }), + metadata: ctx.metadata, updateName: jest.fn(name => (transaction.name = name)), + setAttribute: jest.fn(), }; return transaction; @@ -135,12 +147,12 @@ describe('Angular Tracing', () => { name: url, op: 'pageload', origin: 'auto.pageload.angular', - metadata: { source: 'url' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, }); expect(transaction.updateName).toHaveBeenCalledTimes(0); expect(transaction.name).toEqual(url); - expect(transaction.metadata.source).toBe('custom'); + expect(transaction.toJSON().data).toEqual({ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }); env.destroy(); }); @@ -326,10 +338,10 @@ describe('Angular Tracing', () => { name: url, op: 'navigation', origin: 'auto.navigation.angular', - metadata: { source: 'url' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, }); expect(transaction.updateName).toHaveBeenCalledWith(result); - expect(transaction.setMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(transaction.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); env.destroy(); }); diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 6a1011053007..2eec2fc1e0e9 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -22,6 +22,7 @@ export { createTransport, // eslint-disable-next-line deprecation/deprecation extractTraceparentData, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, @@ -32,6 +33,7 @@ export { Hub, makeMain, Scope, + // eslint-disable-next-line deprecation/deprecation startTransaction, SDK_VERSION, setContext, diff --git a/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts index 142a0b0b3019..4e68053fea6d 100644 --- a/packages/astro/src/integration/index.ts +++ b/packages/astro/src/integration/index.ts @@ -84,7 +84,7 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { // Prevent Sentry from being externalized for SSR. // Cloudflare like environments have Node.js APIs are available under `node:` prefix. // Ref: https://developers.cloudflare.com/workers/runtime-apis/nodejs/ - if (config?.adapter?.name.startsWith('@astro/cloudflare')) { + if (config?.adapter?.name.startsWith('@astrojs/cloudflare')) { updateConfig({ vite: { ssr: { diff --git a/packages/astro/src/server/meta.ts b/packages/astro/src/server/meta.ts index bdc0d89d4f80..1a908acc9ceb 100644 --- a/packages/astro/src/server/meta.ts +++ b/packages/astro/src/server/meta.ts @@ -1,4 +1,8 @@ -import { getDynamicSamplingContextFromClient, spanToTraceHeader } from '@sentry/core'; +import { + getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, + spanToTraceHeader, +} from '@sentry/core'; import type { Client, Scope, Span } from '@sentry/types'; import { TRACEPARENT_REGEXP, @@ -33,7 +37,7 @@ export function getTracingMetaTags( const sentryTrace = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled); const dynamicSamplingContext = transaction - ? transaction.getDynamicSamplingContext() + ? getDynamicSamplingContextFromSpan(transaction) : dsc ? dsc : client diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 907714d33874..d5cc61b73e95 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -1,6 +1,8 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { captureException, continueTrace, + getActiveSpan, getClient, getCurrentScope, runWithAsyncContext, @@ -69,7 +71,7 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseH // if there is an active span, we know that this handle call is nested and hence // we don't create a new domain for it. If we created one, nested server calls would // create new transactions instead of adding a child span to the currently active span. - if (getCurrentScope().getSpan()) { + if (getActiveSpan()) { return instrumentRequest(ctx, next, handlerOptions); } return runWithAsyncContext(() => { @@ -111,6 +113,7 @@ async function instrumentRequest( try { const interpolatedRoute = interpolateRouteFromUrlAndParams(ctx.url.pathname, ctx.params); + const source = interpolatedRoute ? 'route' : 'url'; // storing res in a variable instead of directly returning is necessary to // invoke the catch block if next() throws const res = await startSpan( @@ -121,12 +124,13 @@ async function instrumentRequest( origin: 'auto.http.astro', status: 'ok', metadata: { + // eslint-disable-next-line deprecation/deprecation ...traceCtx?.metadata, - source: interpolatedRoute ? 'route' : 'url', }, data: { method, url: stripUrlQueryAndFragment(ctx.url.href), + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, ...(ctx.url.search && { 'http.query': ctx.url.search }), ...(ctx.url.hash && { 'http.fragment': ctx.url.hash }), ...(options.trackHeaders && { headers: allHeaders }), diff --git a/packages/astro/test/server/meta.test.ts b/packages/astro/test/server/meta.test.ts index 69caef326936..f235ad34d7ca 100644 --- a/packages/astro/test/server/meta.test.ts +++ b/packages/astro/test/server/meta.test.ts @@ -3,10 +3,17 @@ import { vi } from 'vitest'; import { getTracingMetaTags, isValidBaggageString } from '../../src/server/meta'; +const TRACE_FLAG_SAMPLED = 0x1; + const mockedSpan = { - sampled: true, - traceId: '12345678901234567890123456789012', - spanId: '1234567890123456', + isRecording: () => true, + spanContext: () => { + return { + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: TRACE_FLAG_SAMPLED, + }; + }, transaction: { getDynamicSamplingContext: () => ({ environment: 'production', @@ -25,6 +32,10 @@ const mockedScope = { describe('getTracingMetaTags', () => { it('returns the tracing tags from the span, if it is provided', () => { { + vi.spyOn(SentryCore, 'getDynamicSamplingContextFromSpan').mockReturnValueOnce({ + environment: 'production', + }); + const tags = getTracingMetaTags(mockedSpan, mockedScope, mockedClient); expect(tags).toEqual({ @@ -70,9 +81,14 @@ describe('getTracingMetaTags', () => { const tags = getTracingMetaTags( // @ts-expect-error - only passing a partial span object { - sampled: true, - traceId: '12345678901234567890123456789012', - spanId: '1234567890123456', + isRecording: () => true, + spanContext: () => { + return { + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: TRACE_FLAG_SAMPLED, + }; + }, transaction: undefined, }, mockedScope, @@ -84,7 +100,7 @@ describe('getTracingMetaTags', () => { }); }); - it('returns only the `sentry-trace` tag if no DSC is available', () => { + it('returns only the `sentry-trace` tag if no DSC is available without a client', () => { vi.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ trace_id: '', public_key: undefined, @@ -93,9 +109,14 @@ describe('getTracingMetaTags', () => { const tags = getTracingMetaTags( // @ts-expect-error - only passing a partial span object { - sampled: true, - traceId: '12345678901234567890123456789012', - spanId: '1234567890123456', + isRecording: () => true, + spanContext: () => { + return { + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: TRACE_FLAG_SAMPLED, + }; + }, transaction: undefined, }, mockedScope, diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index 13508cebf057..9fa5bc430c90 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -1,5 +1,6 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import * as SentryNode from '@sentry/node'; -import type { Client } from '@sentry/types'; +import type { Client, Span } from '@sentry/types'; import { vi } from 'vitest'; import { handleRequest, interpolateRouteFromUrlAndParams } from '../../src/server/middleware'; @@ -14,7 +15,9 @@ vi.mock('../../src/server/meta', () => ({ describe('sentryMiddleware', () => { const startSpanSpy = vi.spyOn(SentryNode, 'startSpan'); - const getSpanMock = vi.fn(() => {}); + const getSpanMock = vi.fn(() => { + return {} as Span | undefined; + }); const setUserMock = vi.fn(); beforeEach(() => { @@ -25,6 +28,7 @@ describe('sentryMiddleware', () => { getSpan: getSpanMock, } as any; }); + vi.spyOn(SentryNode, 'getActiveSpan').mockImplementation(getSpanMock); vi.spyOn(SentryNode, 'getClient').mockImplementation(() => ({}) as Client); }); @@ -57,10 +61,9 @@ describe('sentryMiddleware', () => { data: { method: 'GET', url: 'https://mydomain.io/users/123/details', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', }, - metadata: { - source: 'route', - }, + metadata: {}, name: 'GET /users/[id]/details', op: 'http.server', origin: 'auto.http.astro', @@ -94,10 +97,9 @@ describe('sentryMiddleware', () => { data: { method: 'GET', url: 'http://localhost:1234/a%xx', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, - metadata: { - source: 'url', - }, + metadata: {}, name: 'GET a%xx', op: 'http.server', origin: 'auto.http.astro', @@ -159,8 +161,10 @@ describe('sentryMiddleware', () => { expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ + data: expect.objectContaining({ + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }), metadata: { - source: 'route', dynamicSamplingContext: { release: '1.0.0', }, diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index c8d4e4ce2f08..14d4660b8482 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -7,6 +7,7 @@ import type { Event, EventHint, Options, + ParameterizedString, Severity, SeverityLevel, UserFeedback, @@ -84,7 +85,7 @@ export class BrowserClient extends BaseClient { * @inheritDoc */ public eventFromMessage( - message: string, + message: ParameterizedString, // eslint-disable-next-line deprecation/deprecation level: Severity | SeverityLevel = 'info', hint?: EventHint, diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index 6955fbfa26fe..9e72fb288cb4 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -1,5 +1,14 @@ import { getClient } from '@sentry/core'; -import type { Event, EventHint, Exception, Severity, SeverityLevel, StackFrame, StackParser } from '@sentry/types'; +import type { + Event, + EventHint, + Exception, + ParameterizedString, + Severity, + SeverityLevel, + StackFrame, + StackParser, +} from '@sentry/types'; import { addExceptionMechanism, addExceptionTypeValue, @@ -9,6 +18,7 @@ import { isError, isErrorEvent, isEvent, + isParameterizedString, isPlainObject, normalizeToSize, resolvedSyncPromise, @@ -167,7 +177,7 @@ export function eventFromException( */ export function eventFromMessage( stackParser: StackParser, - message: string, + message: ParameterizedString, // eslint-disable-next-line deprecation/deprecation level: Severity | SeverityLevel = 'info', hint?: EventHint, @@ -264,23 +274,32 @@ export function eventFromUnknownInput( */ export function eventFromString( stackParser: StackParser, - input: string, + message: ParameterizedString, syntheticException?: Error, attachStacktrace?: boolean, ): Event { - const event: Event = { - message: input, - }; + const event: Event = {}; if (attachStacktrace && syntheticException) { const frames = parseStackFrames(stackParser, syntheticException); if (frames.length) { event.exception = { - values: [{ value: input, stacktrace: { frames } }], + values: [{ value: message, stacktrace: { frames } }], }; } } + if (isParameterizedString(message)) { + const { __sentry_template_string__, __sentry_template_values__ } = message; + + event.logentry = { + message: __sentry_template_string__, + params: __sentry_template_values__, + }; + return event; + } + + event.message = message; return event; } diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 0d87e7898283..f63fe20fdead 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -45,6 +45,7 @@ export { lastEventId, makeMain, Scope, + // eslint-disable-next-line deprecation/deprecation startTransaction, getActiveSpan, startSpan, diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index d9afa470b2ba..97abefea8242 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -46,6 +46,7 @@ export { setMeasurement, // eslint-disable-next-line deprecation/deprecation extractTraceparentData, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, spanStatusfromHttpCode, // eslint-disable-next-line deprecation/deprecation diff --git a/packages/browser/src/profiling/hubextensions.ts b/packages/browser/src/profiling/hubextensions.ts index 462929fff04b..9fd156a1b90b 100644 --- a/packages/browser/src/profiling/hubextensions.ts +++ b/packages/browser/src/profiling/hubextensions.ts @@ -1,4 +1,5 @@ /* eslint-disable complexity */ +import { spanToJSON } from '@sentry/core'; import type { Transaction } from '@sentry/types'; import { logger, timestampInSeconds, uuid4 } from '@sentry/utils'; @@ -56,7 +57,7 @@ export function startProfileForTransaction(transaction: Transaction): Transactio } if (DEBUG_BUILD) { - logger.log(`[Profiling] started profiling transaction: ${transaction.name || transaction.description}`); + logger.log(`[Profiling] started profiling transaction: ${spanToJSON(transaction).description}`); } // We create "unique" transaction names to avoid concurrent transactions with same names @@ -87,11 +88,7 @@ export function startProfileForTransaction(transaction: Transaction): Transactio } if (processedProfile) { if (DEBUG_BUILD) { - logger.log( - '[Profiling] profile for:', - transaction.name || transaction.description, - 'already exists, returning early', - ); + logger.log('[Profiling] profile for:', spanToJSON(transaction).description, 'already exists, returning early'); } return null; } @@ -105,14 +102,14 @@ export function startProfileForTransaction(transaction: Transaction): Transactio } if (DEBUG_BUILD) { - logger.log(`[Profiling] stopped profiling of transaction: ${transaction.name || transaction.description}`); + logger.log(`[Profiling] stopped profiling of transaction: ${spanToJSON(transaction).description}`); } // In case of an overlapping transaction, stopProfiling may return null and silently ignore the overlapping profile. if (!profile) { if (DEBUG_BUILD) { logger.log( - `[Profiling] profiler returned null profile for: ${transaction.name || transaction.description}`, + `[Profiling] profiler returned null profile for: ${spanToJSON(transaction).description}`, 'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started', ); } @@ -135,7 +132,7 @@ export function startProfileForTransaction(transaction: Transaction): Transactio if (DEBUG_BUILD) { logger.log( '[Profiling] max profile duration elapsed, stopping profiling for:', - transaction.name || transaction.description, + spanToJSON(transaction).description, ); } // If the timeout exceeds, we want to stop profiling, but not finish the transaction @@ -159,6 +156,8 @@ export function startProfileForTransaction(transaction: Transaction): Transactio // Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared. void onProfileHandler().then( () => { + // TODO: Can we rewrite this to use attributes? + // eslint-disable-next-line deprecation/deprecation transaction.setContext('profile', { profile_id: profileId, start_timestamp: startTimestamp }); originalEnd(); }, diff --git a/packages/browser/src/profiling/integration.ts b/packages/browser/src/profiling/integration.ts index 3f823429c122..b50c60552a7f 100644 --- a/packages/browser/src/profiling/integration.ts +++ b/packages/browser/src/profiling/integration.ts @@ -24,6 +24,7 @@ const browserProfilingIntegration: IntegrationFn = () => { setup(client) { const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation const transaction = scope.getTransaction(); if (transaction && isAutomatedPageLoadTransaction(transaction)) { diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index f2fdc5e4c10d..9114884384b7 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -515,7 +515,7 @@ export function shouldProfileTransaction(transaction: Transaction): boolean { return false; } - if (!transaction.sampled) { + if (!transaction.isRecording()) { if (DEBUG_BUILD) { logger.log('[Profiling] Discarding profile because transaction was not sampled.'); } diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 579bb57e9698..451cce98e3b7 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -1,11 +1,13 @@ import type { Hub } from '@sentry/core'; import { Integrations as CoreIntegrations, + captureSession, getClient, getCurrentHub, getIntegrationsToSetup, getReportDialogEndpoint, initAndBind, + startSession, } from '@sentry/core'; import type { UserFeedback } from '@sentry/types'; import { @@ -250,11 +252,6 @@ export function wrap(fn: (...args: any) => any): any { return internalWrap(fn)(); } -function startSessionOnHub(hub: Hub): void { - hub.startSession({ ignoreDuration: true }); - hub.captureSession(); -} - /** * Enable automatic Session Tracking for the initial page load. */ @@ -264,29 +261,19 @@ function startSessionTracking(): void { return; } - const hub = getCurrentHub(); - - // The only way for this to be false is for there to be a version mismatch between @sentry/browser (>= 6.0.0) and - // @sentry/hub (< 5.27.0). In the simple case, there won't ever be such a mismatch, because the two packages are - // pinned at the same version in package.json, but there are edge cases where it's possible. See - // https://github.com/getsentry/sentry-javascript/issues/3207 and - // https://github.com/getsentry/sentry-javascript/issues/3234 and - // https://github.com/getsentry/sentry-javascript/issues/3278. - if (!hub.captureSession) { - return; - } - // The session duration for browser sessions does not track a meaningful // concept that can be used as a metric. // Automatically captured sessions are akin to page views, and thus we // discard their duration. - startSessionOnHub(hub); + startSession({ ignoreDuration: true }); + captureSession(); // We want to create a session for every navigation as well addHistoryInstrumentationHandler(({ from, to }) => { // Don't create an additional session for the initial route or if the location did not change if (from !== undefined && from !== to) { - startSessionOnHub(getCurrentHub()); + startSession({ ignoreDuration: true }); + captureSession(); } }); } diff --git a/packages/browser/test/integration/suites/api.js b/packages/browser/test/integration/suites/api.js index 9cb59277c6e0..462659e75ac7 100644 --- a/packages/browser/test/integration/suites/api.js +++ b/packages/browser/test/integration/suites/api.js @@ -20,47 +20,6 @@ describe('API', function () { }); }); - it('should capture Sentry internal event as breadcrumbs for the following event sent', function () { - return runInSandbox(sandbox, { manual: true }, function () { - window.allowSentryBreadcrumbs = true; - Sentry.captureMessage('a'); - Sentry.captureMessage('b'); - // For the loader - Sentry.flush && Sentry.flush(2000); - window.finalizeManualTest(); - }).then(function (summary) { - assert.equal(summary.events.length, 2); - assert.equal(summary.breadcrumbs.length, 2); - assert.equal(summary.events[1].breadcrumbs[0].category, 'sentry.event'); - assert.equal(summary.events[1].breadcrumbs[0].event_id, summary.events[0].event_id); - assert.equal(summary.events[1].breadcrumbs[0].level, summary.events[0].level); - }); - }); - - it('should capture Sentry internal transaction as breadcrumbs for the following event sent', function () { - return runInSandbox(sandbox, { manual: true }, function () { - window.allowSentryBreadcrumbs = true; - Sentry.captureEvent({ - event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', - message: 'someMessage', - transaction: 'wat', - type: 'transaction', - }); - Sentry.captureMessage('c'); - // For the loader - Sentry.flush && Sentry.flush(2000); - window.finalizeManualTest(); - }).then(function (summary) { - // We have a length of one here since transactions don't go through beforeSend - // and we add events to summary in beforeSend - assert.equal(summary.events.length, 1); - assert.equal(summary.breadcrumbs.length, 2); - assert.equal(summary.events[0].breadcrumbs[0].category, 'sentry.transaction'); - assert.isNotEmpty(summary.events[0].breadcrumbs[0].event_id); - assert.isUndefined(summary.events[0].breadcrumbs[0].level); - }); - }); - it('should generate a synthetic trace for captureException w/ non-errors', function () { return runInSandbox(sandbox, function () { throwNonError(); diff --git a/packages/browser/test/unit/profiling/hubextensions.test.ts b/packages/browser/test/unit/profiling/hubextensions.test.ts index 26d836b12b02..30b7769e836d 100644 --- a/packages/browser/test/unit/profiling/hubextensions.test.ts +++ b/packages/browser/test/unit/profiling/hubextensions.test.ts @@ -10,6 +10,9 @@ import { JSDOM } from 'jsdom'; import { onProfilingStartRouteTransaction } from '../../../src'; +// eslint-disable-next-line no-bitwise +const TraceFlagSampled = 0x1 << 0; + // @ts-expect-error store a reference so we can reset it later const globalDocument = global.document; // @ts-expect-error store a reference so we can reset it later @@ -67,9 +70,17 @@ describe('BrowserProfilingIntegration', () => { // @ts-expect-error force api to be undefined global.window.Profiler = undefined; // set sampled to true so that profiling does not early return - const mockTransaction = { sampled: true } as Transaction; + const mockTransaction = { + isRecording: () => true, + spanContext: () => ({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: TraceFlagSampled, + }), + } as Transaction; expect(() => onProfilingStartRouteTransaction(mockTransaction)).not.toThrow(); }); + it('does not throw if constructor throws', () => { const spy = jest.fn(); @@ -81,7 +92,14 @@ describe('BrowserProfilingIntegration', () => { } // set sampled to true so that profiling does not early return - const mockTransaction = { sampled: true } as Transaction; + const mockTransaction = { + isRecording: () => true, + spanContext: () => ({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: TraceFlagSampled, + }), + } as Transaction; // @ts-expect-error override with our own constructor global.window.Profiler = Profiler; diff --git a/packages/browser/test/unit/profiling/integration.test.ts b/packages/browser/test/unit/profiling/integration.test.ts index ae95927ac2cd..bfcdde5c33ea 100644 --- a/packages/browser/test/unit/profiling/integration.test.ts +++ b/packages/browser/test/unit/profiling/integration.test.ts @@ -50,6 +50,7 @@ describe('BrowserProfilingIntegration', () => { const client = Sentry.getClient(); + // eslint-disable-next-line deprecation/deprecation const currentTransaction = Sentry.getCurrentHub().getScope().getTransaction(); expect(currentTransaction?.op).toBe('pageload'); currentTransaction?.end(); diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 964e34560bdf..bb033496da02 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -39,6 +39,7 @@ export { // eslint-disable-next-line deprecation/deprecation extractTraceparentData, flush, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, @@ -52,6 +53,7 @@ export { makeMain, runWithAsyncContext, Scope, + // eslint-disable-next-line deprecation/deprecation startTransaction, SDK_VERSION, setContext, diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index 89d245908400..fec3aae439af 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -1,8 +1,10 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, Transaction, captureException, continueTrace, convertIntegrationFnToClass, + getCurrentScope, runWithAsyncContext, startSpan, } from '@sentry/core'; @@ -54,6 +56,7 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] const parsedUrl = parseUrl(request.url); const data: Record = { 'http.request.method': request.method || 'GET', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }; if (parsedUrl.search) { data['http.query'] = parsedUrl.search; @@ -72,8 +75,8 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] ...ctx, data, metadata: { + // eslint-disable-next-line deprecation/deprecation ...ctx.metadata, - source: 'url', request: { url, method: request.method, @@ -88,9 +91,10 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] >); if (response && response.status) { span?.setHttpStatus(response.status); - span?.setData('http.response.status_code', response.status); + span?.setAttribute('http.response.status_code', response.status); if (span instanceof Transaction) { - span.setContext('response', { + const scope = getCurrentScope(); + scope.setContext('response', { headers: response.headers.toJSON(), status_code: response.status, }); diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts index 1113612ace81..6356300562ac 100644 --- a/packages/bun/test/integrations/bunserver.test.ts +++ b/packages/bun/test/integrations/bunserver.test.ts @@ -1,5 +1,5 @@ import { beforeAll, beforeEach, describe, expect, test } from 'bun:test'; -import { Hub, makeMain } from '@sentry/core'; +import { Hub, makeMain, spanToJSON } from '@sentry/core'; import { BunClient } from '../../src/client'; import { instrumentBunServe } from '../../src/integrations/bunserver'; @@ -26,11 +26,12 @@ describe('Bun Serve Integration', () => { test('generates a transaction around a request', async () => { client.on('finishTransaction', transaction => { expect(transaction.status).toBe('ok'); + // eslint-disable-next-line deprecation/deprecation expect(transaction.tags).toEqual({ 'http.status_code': '200', }); expect(transaction.op).toEqual('http.server'); - expect(transaction.name).toEqual('GET /'); + expect(spanToJSON(transaction).description).toEqual('GET /'); }); const server = Bun.serve({ @@ -48,11 +49,12 @@ describe('Bun Serve Integration', () => { test('generates a post transaction', async () => { client.on('finishTransaction', transaction => { expect(transaction.status).toBe('ok'); + // eslint-disable-next-line deprecation/deprecation expect(transaction.tags).toEqual({ 'http.status_code': '200', }); expect(transaction.op).toEqual('http.server'); - expect(transaction.name).toEqual('POST /'); + expect(spanToJSON(transaction).description).toEqual('POST /'); }); const server = Bun.serve({ @@ -78,10 +80,11 @@ describe('Bun Serve Integration', () => { const SENTRY_BAGGAGE_HEADER = 'sentry-version=1.0,sentry-environment=production'; client.on('finishTransaction', transaction => { - expect(transaction.traceId).toBe(TRACE_ID); + expect(transaction.spanContext().traceId).toBe(TRACE_ID); expect(transaction.parentSpanId).toBe(PARENT_SPAN_ID); - expect(transaction.sampled).toBe(true); + expect(transaction.isRecording()).toBe(true); + // eslint-disable-next-line deprecation/deprecation expect(transaction.metadata?.dynamicSamplingContext).toStrictEqual({ version: '1.0', environment: 'production' }); }); diff --git a/packages/core/package.json b/packages/core/package.json index 3ef693023f85..61c2e117e6ea 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -56,5 +56,12 @@ "volta": { "extends": "../../package.json" }, + "madge": { + "detectiveOptions": { + "ts": { + "skipTypeImports": true + } + } + }, "sideEffects": false } diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 75b736bbf803..628df591248d 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -19,6 +19,7 @@ import type { MetricBucketItem, MetricsAggregator, Outcome, + ParameterizedString, PropagationContext, SdkMetadata, Session, @@ -36,6 +37,7 @@ import { addItemToEnvelope, checkOrSetAlreadyCaught, createAttachmentEnvelopeItem, + isParameterizedString, isPlainObject, isPrimitive, isThenable, @@ -182,7 +184,7 @@ export abstract class BaseClient implements Client { * @inheritDoc */ public captureMessage( - message: string, + message: ParameterizedString, // eslint-disable-next-line deprecation/deprecation level?: Severity | SeverityLevel, hint?: EventHint, @@ -190,8 +192,10 @@ export abstract class BaseClient implements Client { ): string | undefined { let eventId: string | undefined = hint && hint.event_id; + const eventMessage = isParameterizedString(message) ? message : String(message); + const promisedEvent = isPrimitive(message) - ? this.eventFromMessage(String(message), level, hint) + ? this.eventFromMessage(eventMessage, level, hint) : this.eventFromException(message, hint); this._process( @@ -816,7 +820,7 @@ export abstract class BaseClient implements Client { * @inheritDoc */ public abstract eventFromMessage( - _message: string, + _message: ParameterizedString, // eslint-disable-next-line deprecation/deprecation _level?: Severity | SeverityLevel, _hint?: EventHint, diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index d479a3093d51..795352735ae8 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -12,66 +12,70 @@ import type { FinishedCheckIn, MonitorConfig, Primitive, + Scope as ScopeInterface, + Session, + SessionContext, Severity, SeverityLevel, TransactionContext, User, } from '@sentry/types'; -import { isThenable, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; +import { GLOBAL_OBJ, isThenable, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; +import { DEFAULT_ENVIRONMENT } from './constants'; import { DEBUG_BUILD } from './debug-build'; import type { Hub } from './hub'; -import { getCurrentHub } from './hub'; +import { getCurrentHub, getIsolationScope } from './hub'; import type { Scope } from './scope'; +import { closeSession, makeSession, updateSession } from './session'; import type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; import { parseEventHintOrCaptureContext } from './utils/prepareEvent'; -// Note: All functions in this file are typed with a return value of `ReturnType`, -// where HUB_FUNCTION is some method on the Hub class. -// -// This is done to make sure the top level SDK methods stay in sync with the hub methods. -// Although every method here has an explicit return type, some of them (that map to void returns) do not -// contain `return` keywords. This is done to save on bundle size, as `return` is not minifiable. - /** * Captures an exception event and sends it to Sentry. - * This accepts an event hint as optional second parameter. - * Alternatively, you can also pass a CaptureContext directly as second parameter. + * + * @param exception The exception to capture. + * @param hint Optinal additional data to attach to the Sentry event. + * @returns the id of the captured Sentry event. */ export function captureException( // eslint-disable-next-line @typescript-eslint/no-explicit-any exception: any, hint?: ExclusiveEventHintOrCaptureContext, -): ReturnType { +): string { + // eslint-disable-next-line deprecation/deprecation return getCurrentHub().captureException(exception, parseEventHintOrCaptureContext(hint)); } /** * Captures a message event and sends it to Sentry. * - * @param message The message to send to Sentry. - * @param Severity Define the level of the message. - * @returns The generated eventId. + * @param exception The exception to capture. + * @param captureContext Define the level of the message or pass in additional data to attach to the message. + * @returns the id of the captured message. */ export function captureMessage( message: string, // eslint-disable-next-line deprecation/deprecation captureContext?: CaptureContext | Severity | SeverityLevel, -): ReturnType { +): string { // This is necessary to provide explicit scopes upgrade, without changing the original // arity of the `captureMessage(message, level)` method. const level = typeof captureContext === 'string' ? captureContext : undefined; const context = typeof captureContext !== 'string' ? { captureContext } : undefined; + // eslint-disable-next-line deprecation/deprecation return getCurrentHub().captureMessage(message, level, context); } /** * Captures a manually created event and sends it to Sentry. * - * @param event The event to send to Sentry. - * @returns The generated eventId. + * @param exception The event to send to Sentry. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured event. */ -export function captureEvent(event: Event, hint?: EventHint): ReturnType { +export function captureEvent(event: Event, hint?: EventHint): string { + // eslint-disable-next-line deprecation/deprecation return getCurrentHub().captureEvent(event, hint); } @@ -164,11 +168,33 @@ export function setUser(user: User | null): ReturnType { * pushScope(); * callback(); * popScope(); - * - * @param callback that will be enclosed into push/popScope. */ -export function withScope(callback: (scope: Scope) => T): T { - return getCurrentHub().withScope(callback); +export function withScope(callback: (scope: Scope) => T): T; +/** + * Set the given scope as the active scope in the callback. + */ +export function withScope(scope: ScopeInterface | undefined, callback: (scope: Scope) => T): T; +/** + * Either creates a new active scope, or sets the given scope as active scope in the given callback. + */ +export function withScope( + ...rest: [callback: (scope: Scope) => T] | [scope: ScopeInterface | undefined, callback: (scope: Scope) => T] +): T { + // If a scope is defined, we want to make this the active scope instead of the default one + if (rest.length === 2) { + const [scope, callback] = rest; + if (!scope) { + return getCurrentHub().withScope(callback); + } + + const hub = getCurrentHub(); + return hub.withScope(() => { + hub.getStackTop().scope = scope as Scope; + return callback(scope as Scope); + }); + } + + return getCurrentHub().withScope(rest[0]); } /** @@ -190,11 +216,14 @@ export function withScope(callback: (scope: Scope) => T): T { * default values). See {@link Options.tracesSampler}. * * @returns The transaction which was just started + * + * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. */ export function startTransaction( context: TransactionContext, customSamplingContext?: CustomSamplingContext, ): ReturnType { + // eslint-disable-next-line deprecation/deprecation return getCurrentHub().startTransaction({ ...context }, customSamplingContext); } @@ -319,3 +348,99 @@ export function getClient(): C | undefined { export function getCurrentScope(): Scope { return getCurrentHub().getScope(); } + +/** + * Start a session on the current isolation scope. + * + * @param context (optional) additional properties to be applied to the returned session object + * + * @returns the new active session + */ +export function startSession(context?: SessionContext): Session { + const client = getClient(); + const isolationScope = getIsolationScope(); + const currentScope = getCurrentScope(); + + const { release, environment = DEFAULT_ENVIRONMENT } = (client && client.getOptions()) || {}; + + // Will fetch userAgent if called from browser sdk + const { userAgent } = GLOBAL_OBJ.navigator || {}; + + const session = makeSession({ + release, + environment, + user: isolationScope.getUser(), + ...(userAgent && { userAgent }), + ...context, + }); + + // End existing session if there's one + const currentSession = isolationScope.getSession(); + if (currentSession && currentSession.status === 'ok') { + updateSession(currentSession, { status: 'exited' }); + } + + endSession(); + + // Afterwards we set the new session on the scope + isolationScope.setSession(session); + + // TODO (v8): Remove this and only use the isolation scope(?). + // For v7 though, we can't "soft-break" people using getCurrentHub().getScope().setSession() + currentScope.setSession(session); + + return session; +} + +/** + * End the session on the current isolation scope. + */ +export function endSession(): void { + const isolationScope = getIsolationScope(); + const currentScope = getCurrentScope(); + + const session = isolationScope.getSession(); + if (session) { + closeSession(session); + } + _sendSessionUpdate(); + + // the session is over; take it off of the scope + isolationScope.setSession(); + + // TODO (v8): Remove this and only use the isolation scope(?). + // For v7 though, we can't "soft-break" people using getCurrentHub().getScope().setSession() + currentScope.setSession(); +} + +/** + * Sends the current Session on the scope + */ +function _sendSessionUpdate(): void { + const isolationScope = getIsolationScope(); + const currentScope = getCurrentScope(); + const client = getClient(); + // TODO (v8): Remove currentScope and only use the isolation scope(?). + // For v7 though, we can't "soft-break" people using getCurrentHub().getScope().setSession() + const session = currentScope.getSession() || isolationScope.getSession(); + if (session && client && client.captureSession) { + client.captureSession(session); + } +} + +/** + * Sends the current session on the scope to Sentry + * + * @param end If set the session will be marked as exited and removed from the scope. + * Defaults to `false`. + */ +export function captureSession(end: boolean = false): void { + // both send the update and pull the session from the scope + if (end) { + endSession(); + return; + } + + // only send the update + _sendSessionUpdate(); +} diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 07f1310f94a2..0787f7c164ed 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -166,6 +166,7 @@ export class Hub implements HubInterface { public bindClient(client?: Client): void { const top = this.getStackTop(); top.client = client; + top.scope.setClient(client); if (client && client.setupIntegrations) { client.setupIntegrations(); } @@ -262,27 +263,26 @@ export class Hub implements HubInterface { /** * @inheritDoc + * + * @deprecated Use `Sentry.captureException()` instead. */ public captureException(exception: unknown, hint?: EventHint): string { const eventId = (this._lastEventId = hint && hint.event_id ? hint.event_id : uuid4()); const syntheticException = new Error('Sentry syntheticException'); - this._withClient((client, scope) => { - client.captureException( - exception, - { - originalException: exception, - syntheticException, - ...hint, - event_id: eventId, - }, - scope, - ); + this.getScope().captureException(exception, { + originalException: exception, + syntheticException, + ...hint, + event_id: eventId, }); + return eventId; } /** * @inheritDoc + * + * @deprecated Use `Sentry.captureMessage()` instead. */ public captureMessage( message: string, @@ -292,24 +292,20 @@ export class Hub implements HubInterface { ): string { const eventId = (this._lastEventId = hint && hint.event_id ? hint.event_id : uuid4()); const syntheticException = new Error(message); - this._withClient((client, scope) => { - client.captureMessage( - message, - level, - { - originalException: message, - syntheticException, - ...hint, - event_id: eventId, - }, - scope, - ); + this.getScope().captureMessage(message, level, { + originalException: message, + syntheticException, + ...hint, + event_id: eventId, }); + return eventId; } /** * @inheritDoc + * + * @deprecated Use `Sentry.captureEvent()` instead. */ public captureEvent(event: Event, hint?: EventHint): string { const eventId = hint && hint.event_id ? hint.event_id : uuid4(); @@ -317,9 +313,7 @@ export class Hub implements HubInterface { this._lastEventId = eventId; } - this._withClient((client, scope) => { - client.captureEvent(event, { ...hint, event_id: eventId }, scope); - }); + this.getScope().captureEvent(event, { ...hint, event_id: eventId }); return eventId; } @@ -440,7 +434,23 @@ export class Hub implements HubInterface { } /** - * @inheritDoc + * Starts a new `Transaction` and returns it. This is the entry point to manual tracing instrumentation. + * + * A tree structure can be built by adding child spans to the transaction, and child spans to other spans. To start a + * new child span within the transaction or any span, call the respective `.startChild()` method. + * + * Every child span must be finished before the transaction is finished, otherwise the unfinished spans are discarded. + * + * The transaction must be finished with a call to its `.end()` method, at which point the transaction with all its + * finished child spans will be sent to Sentry. + * + * @param context Properties of the new `Transaction`. + * @param customSamplingContext Information given to the transaction sampling function (along with context-dependent + * default values). See {@link Options.tracesSampler}. + * + * @returns The transaction which was just started + * + * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. */ public startTransaction(context: TransactionContext, customSamplingContext?: CustomSamplingContext): Transaction { const result = this._callExtensionMethod('startTransaction', context, customSamplingContext); @@ -471,10 +481,13 @@ Sentry.init({...}); /** * @inheritDoc + * + * @deprecated Use top level `captureSession` instead. */ public captureSession(endSession: boolean = false): void { // both send the update and pull the session from the scope if (endSession) { + // eslint-disable-next-line deprecation/deprecation return this.endSession(); } @@ -484,6 +497,7 @@ Sentry.init({...}); /** * @inheritDoc + * @deprecated Use top level `endSession` instead. */ public endSession(): void { const layer = this.getStackTop(); @@ -500,6 +514,7 @@ Sentry.init({...}); /** * @inheritDoc + * @deprecated Use top level `startSession` instead. */ public startSession(context?: SessionContext): Session { const { scope, client } = this.getStackTop(); @@ -521,6 +536,7 @@ Sentry.init({...}); if (currentSession && currentSession.status === 'ok') { updateSession(currentSession, { status: 'exited' }); } + // eslint-disable-next-line deprecation/deprecation this.endSession(); // Afterwards we set the new session on the scope @@ -532,6 +548,9 @@ Sentry.init({...}); /** * Returns if default PII should be sent to Sentry and propagated in ourgoing requests * when Tracing is used. + * + * @deprecated Use top-level `getClient().getOptions().sendDefaultPii` instead. This function + * only unnecessarily increased API surface but only wrapped accessing the option. */ public shouldSendDefaultPii(): boolean { const client = this.getClient(); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 20748ff11589..e277c01f2dbe 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -5,6 +5,7 @@ export type { ServerRuntimeClientOptions } from './server-runtime-client'; export type { RequestDataIntegrationOptions } from './integrations/requestdata'; export * from './tracing'; +export * from './semanticAttributes'; export { createEventEnvelope, createSessionEnvelope } from './envelope'; export { addBreadcrumb, @@ -19,6 +20,7 @@ export { flush, // eslint-disable-next-line deprecation/deprecation lastEventId, + // eslint-disable-next-line deprecation/deprecation startTransaction, setContext, setExtra, @@ -29,6 +31,9 @@ export { withScope, getClient, getCurrentScope, + startSession, + endSession, + captureSession, } from './exports'; export { getCurrentHub, @@ -71,7 +76,11 @@ export { createCheckInEnvelope } from './checkin'; export { hasTracingEnabled } from './utils/hasTracingEnabled'; export { isSentryRequestUrl } from './utils/isSentryRequestUrl'; export { handleCallbackErrors } from './utils/handleCallbackErrors'; -export { spanToTraceHeader } from './utils/spanUtils'; +export { + spanToTraceHeader, + spanToJSON, + spanIsSampled, +} from './utils/spanUtils'; export { DEFAULT_ENVIRONMENT } from './constants'; export { ModuleMetadata } from './integrations/metadata'; export { RequestData } from './integrations/requestdata'; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index f9be8b325782..16b0c145b0b4 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -101,6 +101,10 @@ export function setupIntegrations(client: Client, integrations: Integration[]): /** Setup a single integration. */ export function setupIntegration(client: Client, integration: Integration, integrationIndex: IntegrationIndex): void { + if (integrationIndex[integration.name]) { + DEBUG_BUILD && logger.log(`Integration skipped because it was already installed: ${integration.name}`); + return; + } integrationIndex[integration.name] = integration; // `setupOnce` is only called the first time diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts index fcf70ccced1a..1deab79df241 100644 --- a/packages/core/src/integrations/requestdata.ts +++ b/packages/core/src/integrations/requestdata.ts @@ -2,6 +2,7 @@ import type { Client, IntegrationFn, Transaction } from '@sentry/types'; import type { AddRequestDataToEventOptions, TransactionNamingScheme } from '@sentry/utils'; import { addRequestDataToEvent, extractPathForTransaction } from '@sentry/utils'; import { convertIntegrationFnToClass } from '../integration'; +import { spanToJSON } from '../utils/spanUtils'; export type RequestDataIntegrationOptions = { /** @@ -105,18 +106,20 @@ const requestDataIntegration: IntegrationFn = (options: RequestDataIntegrationOp const reqWithTransaction = req as { _sentryTransaction?: Transaction }; const transaction = reqWithTransaction._sentryTransaction; if (transaction) { + const name = spanToJSON(transaction).description || ''; + // TODO (v8): Remove the nextjs check and just base it on `transactionNamingScheme` for all SDKs. (We have to // keep it the way it is for the moment, because changing the names of transactions in Sentry has the potential // to break things like alert rules.) const shouldIncludeMethodInTransactionName = getSDKName(client) === 'sentry.javascript.nextjs' - ? transaction.name.startsWith('/api') + ? name.startsWith('/api') : transactionNamingScheme !== 'path'; const [transactionValue] = extractPathForTransaction(req, { path: true, method: shouldIncludeMethodInTransactionName, - customRoute: transaction.name, + customRoute: name, }); processedEvent.transaction = transactionValue; diff --git a/packages/core/src/metrics/exports.ts b/packages/core/src/metrics/exports.ts index 66074a7e846c..03e81ed49f0e 100644 --- a/packages/core/src/metrics/exports.ts +++ b/packages/core/src/metrics/exports.ts @@ -3,6 +3,7 @@ import { logger } from '@sentry/utils'; import type { BaseClient } from '../baseclient'; import { DEBUG_BUILD } from '../debug-build'; import { getClient, getCurrentScope } from '../exports'; +import { spanToJSON } from '../utils/spanUtils'; import { COUNTER_METRIC_TYPE, DISTRIBUTION_METRIC_TYPE, GAUGE_METRIC_TYPE, SET_METRIC_TYPE } from './constants'; import { MetricsAggregator } from './integration'; import type { MetricType } from './types'; @@ -29,6 +30,7 @@ function addToMetricsAggregator( } const { unit, tags, timestamp } = data; const { release, environment } = client.getOptions(); + // eslint-disable-next-line deprecation/deprecation const transaction = scope.getTransaction(); const metricTags: Record = {}; if (release) { @@ -38,7 +40,7 @@ function addToMetricsAggregator( metricTags.environment = environment; } if (transaction) { - metricTags.transaction = transaction.name; + metricTags.transaction = spanToJSON(transaction).description || ''; } DEBUG_BUILD && logger.log(`Adding value of ${value} to ${metricType} metric ${name}`); diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index cff498bc85a3..255af68dd48e 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -24,7 +24,7 @@ import type { Transaction, User, } from '@sentry/types'; -import { dateTimestampInSeconds, isPlainObject, uuid4 } from '@sentry/utils'; +import { dateTimestampInSeconds, isPlainObject, logger, uuid4 } from '@sentry/utils'; import { getGlobalEventProcessors, notifyEventProcessors } from './eventProcessors'; import { updateSession } from './session'; @@ -89,7 +89,9 @@ export class Scope implements ScopeInterface { // eslint-disable-next-line deprecation/deprecation protected _level?: Severity | SeverityLevel; - /** Transaction Name */ + /** + * Transaction Name + */ protected _transactionName?: string; /** Span */ @@ -281,7 +283,8 @@ export class Scope implements ScopeInterface { } /** - * @inheritDoc + * Sets the transaction name on the scope for future events. + * @deprecated Use extra or tags instead. */ public setTransactionName(name?: string): this { this._transactionName = name; @@ -305,7 +308,9 @@ export class Scope implements ScopeInterface { } /** - * @inheritDoc + * Sets the Span on the scope. + * @param span Span + * @deprecated Instead of setting a span on a scope, use `startSpan()`/`startSpanManual()` instead. */ public setSpan(span?: Span): this { this._span = span; @@ -314,19 +319,21 @@ export class Scope implements ScopeInterface { } /** - * @inheritDoc + * Returns the `Span` if there is one. + * @deprecated Use `getActiveSpan()` instead. */ public getSpan(): Span | undefined { return this._span; } /** - * @inheritDoc + * Returns the `Transaction` attached to the scope (if there is one). + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. */ public getTransaction(): Transaction | undefined { // Often, this span (if it exists at all) will be a transaction, but it's not guaranteed to be. Regardless, it will // have a pointer to the currently-active transaction. - const span = this.getSpan(); + const span = this._span; return span && span.transaction; } @@ -581,6 +588,90 @@ export class Scope implements ScopeInterface { return this._propagationContext; } + /** + * Capture an exception for this scope. + * + * @param exception The exception to capture. + * @param hint Optinal additional data to attach to the Sentry event. + * @returns the id of the captured Sentry event. + */ + public captureException(exception: unknown, hint?: EventHint): string { + const eventId = hint && hint.event_id ? hint.event_id : uuid4(); + + if (!this._client) { + logger.warn('No client configured on scope - will not capture exception!'); + return eventId; + } + + const syntheticException = new Error('Sentry syntheticException'); + + this._client.captureException( + exception, + { + originalException: exception, + syntheticException, + ...hint, + event_id: eventId, + }, + this, + ); + + return eventId; + } + + /** + * Capture a message for this scope. + * + * @param message The message to capture. + * @param level An optional severity level to report the message with. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured message. + */ + public captureMessage(message: string, level?: SeverityLevel, hint?: EventHint): string { + const eventId = hint && hint.event_id ? hint.event_id : uuid4(); + + if (!this._client) { + logger.warn('No client configured on scope - will not capture message!'); + return eventId; + } + + const syntheticException = new Error(message); + + this._client.captureMessage( + message, + level, + { + originalException: message, + syntheticException, + ...hint, + event_id: eventId, + }, + this, + ); + + return eventId; + } + + /** + * Captures a manually created event for this scope and sends it to Sentry. + * + * @param exception The event to capture. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured event. + */ + public captureEvent(event: Event, hint?: EventHint): string { + const eventId = hint && hint.event_id ? hint.event_id : uuid4(); + + if (!this._client) { + logger.warn('No client configured on scope - will not capture event!'); + return eventId; + } + + this._client.captureEvent(event, { ...hint, event_id: eventId }, this); + + return eventId; + } + /** * This will be called on every set call. */ diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts new file mode 100644 index 000000000000..6239e6f6acf7 --- /dev/null +++ b/packages/core/src/semanticAttributes.ts @@ -0,0 +1,11 @@ +/** + * Use this attribute to represent the source of a span. + * Should be one of: custom, url, route, view, component, task, unknown + * + */ +export const SEMANTIC_ATTRIBUTE_SENTRY_SOURCE = 'sentry.source'; + +/** + * Use this attribute to represent the sample rate used for a span. + */ +export const SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE = 'sentry.sample_rate'; diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 68e1eb065d89..00f23be2dc0a 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -6,6 +6,7 @@ import type { Event, EventHint, MonitorConfig, + ParameterizedString, SerializedCheckIn, Severity, SeverityLevel, @@ -20,7 +21,11 @@ import { getClient } from './exports'; import { MetricsAggregator } from './metrics/aggregator'; import type { Scope } from './scope'; import { SessionFlusher } from './sessionflusher'; -import { addTracingExtensions, getDynamicSamplingContextFromClient } from './tracing'; +import { + addTracingExtensions, + getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, +} from './tracing'; import { spanToTraceContext } from './utils/spanUtils'; export interface ServerRuntimeClientOptions extends ClientOptions { @@ -63,7 +68,7 @@ export class ServerRuntimeClient< * @inheritDoc */ public eventFromMessage( - message: string, + message: ParameterizedString, // eslint-disable-next-line deprecation/deprecation level: Severity | SeverityLevel = 'info', hint?: EventHint, @@ -254,9 +259,10 @@ export class ServerRuntimeClient< return [undefined, undefined]; } + // eslint-disable-next-line deprecation/deprecation const span = scope.getSpan(); if (span) { - const samplingContext = span.transaction ? span.transaction.getDynamicSamplingContext() : undefined; + const samplingContext = span.transaction ? getDynamicSamplingContextFromSpan(span) : undefined; return [samplingContext, spanToTraceContext(span)]; } diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 4c82ef60ffd4..4d3786bab9c5 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -1,6 +1,5 @@ import type { SerializedSession, Session, SessionContext, SessionStatus } from '@sentry/types'; import { dropUndefinedKeys, timestampInSeconds, uuid4 } from '@sentry/utils'; - /** * Creates a new `Session` object by setting certain default parameters. If optional @param context * is passed, the passed properties are applied to the session object. diff --git a/packages/core/src/tracing/dynamicSamplingContext.ts b/packages/core/src/tracing/dynamicSamplingContext.ts index f8e8cd107c87..318c232eb43b 100644 --- a/packages/core/src/tracing/dynamicSamplingContext.ts +++ b/packages/core/src/tracing/dynamicSamplingContext.ts @@ -1,12 +1,14 @@ -import type { Client, DynamicSamplingContext, Scope } from '@sentry/types'; +import type { Client, DynamicSamplingContext, Scope, Span, Transaction } from '@sentry/types'; import { dropUndefinedKeys } from '@sentry/utils'; import { DEFAULT_ENVIRONMENT } from '../constants'; +import { getClient, getCurrentScope } from '../exports'; +import { spanIsSampled, spanToJSON } from '../utils/spanUtils'; /** * Creates a dynamic sampling context from a client. * - * Dispatchs the `createDsc` lifecycle hook as a side effect. + * Dispatches the `createDsc` lifecycle hook as a side effect. */ export function getDynamicSamplingContextFromClient( trace_id: string, @@ -30,3 +32,62 @@ export function getDynamicSamplingContextFromClient( return dsc; } + +/** + * A Span with a frozen dynamic sampling context. + */ +type TransactionWithV7FrozenDsc = Transaction & { _frozenDynamicSamplingContext?: DynamicSamplingContext }; + +/** + * Creates a dynamic sampling context from a span (and client and scope) + * + * @param span the span from which a few values like the root span name and sample rate are extracted. + * + * @returns a dynamic sampling context + */ +export function getDynamicSamplingContextFromSpan(span: Span): Readonly> { + const client = getClient(); + if (!client) { + return {}; + } + + // passing emit=false here to only emit later once the DSC is actually populated + const dsc = getDynamicSamplingContextFromClient(spanToJSON(span).trace_id || '', client, getCurrentScope()); + + // As long as we use `Transaction`s internally, this should be fine. + // TODO: We need to replace this with a `getRootSpan(span)` function though + const txn = span.transaction as TransactionWithV7FrozenDsc | undefined; + if (!txn) { + return dsc; + } + + // TODO (v8): Remove v7FrozenDsc as a Transaction will no longer have _frozenDynamicSamplingContext + // For now we need to avoid breaking users who directly created a txn with a DSC, where this field is still set. + // @see Transaction class constructor + const v7FrozenDsc = txn && txn._frozenDynamicSamplingContext; + if (v7FrozenDsc) { + return v7FrozenDsc; + } + + // TODO (v8): Replace txn.metadata with txn.attributes[] + // We can't do this yet because attributes aren't always set yet. + // eslint-disable-next-line deprecation/deprecation + const { sampleRate: maybeSampleRate, source } = txn.metadata; + if (maybeSampleRate != null) { + dsc.sample_rate = `${maybeSampleRate}`; + } + + // We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII + const jsonSpan = spanToJSON(txn); + + // after JSON conversion, txn.name becomes jsonSpan.description + if (source && source !== 'url') { + dsc.transaction = jsonSpan.description; + } + + dsc.sampled = String(spanIsSampled(txn)); + + client.emit && client.emit('createDsc', dsc); + + return dsc; +} diff --git a/packages/core/src/tracing/errors.ts b/packages/core/src/tracing/errors.ts index 9030f02efadc..5a885dd1f090 100644 --- a/packages/core/src/tracing/errors.ts +++ b/packages/core/src/tracing/errors.ts @@ -27,6 +27,7 @@ export function registerErrorInstrumentation(): void { * If an error or unhandled promise occurs, we mark the active transaction as failed */ function errorCallback(): void { + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); if (activeTransaction) { const status: SpanStatusType = 'internal_error'; diff --git a/packages/core/src/tracing/hubextensions.ts b/packages/core/src/tracing/hubextensions.ts index 7047a54f1d72..c63e5710b6f2 100644 --- a/packages/core/src/tracing/hubextensions.ts +++ b/packages/core/src/tracing/hubextensions.ts @@ -13,6 +13,7 @@ import { Transaction } from './transaction'; /** Returns all trace headers that are currently on the top scope. */ function traceHeaders(this: Hub): { [key: string]: string } { const scope = this.getScope(); + // eslint-disable-next-line deprecation/deprecation const span = scope.getSpan(); return span @@ -55,16 +56,18 @@ function _startTransaction( The transaction will not be sampled. Please use the ${configInstrumenter} instrumentation to start transactions.`, ); + // eslint-disable-next-line deprecation/deprecation transactionContext.sampled = false; } + // eslint-disable-next-line deprecation/deprecation let transaction = new Transaction(transactionContext, this); transaction = sampleTransaction(transaction, options, { parentSampled: transactionContext.parentSampled, transactionContext, ...customSamplingContext, }); - if (transaction.sampled) { + if (transaction.isRecording()) { transaction.initSpanRecorder(options._experiments && (options._experiments.maxSpans as number)); } if (client && client.emit) { @@ -88,13 +91,14 @@ export function startIdleTransaction( const client = hub.getClient(); const options: Partial = (client && client.getOptions()) || {}; + // eslint-disable-next-line deprecation/deprecation let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, finalTimeout, heartbeatInterval, onScope); transaction = sampleTransaction(transaction, options, { parentSampled: transactionContext.parentSampled, transactionContext, ...customSamplingContext, }); - if (transaction.sampled) { + if (transaction.isRecording()) { transaction.initSpanRecorder(options._experiments && (options._experiments.maxSpans as number)); } if (client && client.emit) { diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts index 458bd9281627..dfab5782e914 100644 --- a/packages/core/src/tracing/idletransaction.ts +++ b/packages/core/src/tracing/idletransaction.ts @@ -1,13 +1,13 @@ /* eslint-disable max-lines */ -import type { TransactionContext } from '@sentry/types'; +import type { SpanTimeInput, TransactionContext } from '@sentry/types'; import { logger, timestampInSeconds } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import type { Hub } from '../hub'; +import { spanTimeInputToSeconds } from '../utils/spanUtils'; import type { Span } from './span'; import { SpanRecorder } from './span'; import { Transaction } from './transaction'; -import { ensureTimestampInSeconds } from './utils'; export const TRACING_DEFAULTS = { idleTimeout: 1000, @@ -45,18 +45,18 @@ export class IdleTransactionSpanRecorder extends SpanRecorder { public add(span: Span): void { // We should make sure we do not push and pop activities for // the transaction that this span recorder belongs to. - if (span.spanId !== this.transactionSpanId) { + if (span.spanContext().spanId !== this.transactionSpanId) { // We patch span.end() to pop an activity after setting an endTimestamp. // eslint-disable-next-line @typescript-eslint/unbound-method const originalEnd = span.end; span.end = (...rest: unknown[]) => { - this._popActivity(span.spanId); + this._popActivity(span.spanContext().spanId); return originalEnd.apply(span, rest); }; // We should only push new activities if the span does not have an end timestamp. if (span.endTimestamp === undefined) { - this._pushActivity(span.spanId); + this._pushActivity(span.spanContext().spanId); } } @@ -95,6 +95,9 @@ export class IdleTransaction extends Transaction { private _finishReason: (typeof IDLE_TRANSACTION_FINISH_REASONS)[number]; + /** + * @deprecated Transactions will be removed in v8. Use spans instead. + */ public constructor( transactionContext: TransactionContext, private readonly _idleHub: Hub, @@ -123,7 +126,8 @@ export class IdleTransaction extends Transaction { if (_onScope) { // We set the transaction here on the scope so error events pick up the trace // context and attach it to the error. - DEBUG_BUILD && logger.log(`Setting idle transaction on scope. Span ID: ${this.spanId}`); + DEBUG_BUILD && logger.log(`Setting idle transaction on scope. Span ID: ${this.spanContext().spanId}`); + // eslint-disable-next-line deprecation/deprecation _idleHub.getScope().setSpan(this); } @@ -138,14 +142,14 @@ export class IdleTransaction extends Transaction { } /** {@inheritDoc} */ - public end(endTimestamp: number = timestampInSeconds()): string | undefined { - const endTimestampInS = ensureTimestampInSeconds(endTimestamp); + public end(endTimestamp?: SpanTimeInput): string | undefined { + const endTimestampInS = spanTimeInputToSeconds(endTimestamp); this._finished = true; this.activities = {}; if (this.op === 'ui.action.click') { - this.setTag(FINISH_REASON_TAG, this._finishReason); + this.setAttribute(FINISH_REASON_TAG, this._finishReason); } if (this.spanRecorder) { @@ -153,12 +157,12 @@ export class IdleTransaction extends Transaction { logger.log('[Tracing] finishing IdleTransaction', new Date(endTimestampInS * 1000).toISOString(), this.op); for (const callback of this._beforeFinishCallbacks) { - callback(this, endTimestamp); + callback(this, endTimestampInS); } this.spanRecorder.spans = this.spanRecorder.spans.filter((span: Span) => { // If we are dealing with the transaction itself, we just return it - if (span.spanId === this.spanId) { + if (span.spanContext().spanId === this.spanContext().spanId) { return true; } @@ -196,7 +200,9 @@ export class IdleTransaction extends Transaction { // if `this._onScope` is `true`, the transaction put itself on the scope when it started if (this._onScope) { const scope = this._idleHub.getScope(); + // eslint-disable-next-line deprecation/deprecation if (scope.getTransaction() === this) { + // eslint-disable-next-line deprecation/deprecation scope.setSpan(undefined); } } @@ -233,7 +239,7 @@ export class IdleTransaction extends Transaction { this._popActivity(id); }; - this.spanRecorder = new IdleTransactionSpanRecorder(pushActivity, popActivity, this.spanId, maxlen); + this.spanRecorder = new IdleTransactionSpanRecorder(pushActivity, popActivity, this.spanContext().spanId, maxlen); // Start heartbeat so that transactions do not run forever. DEBUG_BUILD && logger.log('Starting heartbeat'); diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index 759a42cbdfe0..ecdc5f595095 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -19,5 +19,5 @@ export { startSpanManual, continueTrace, } from './trace'; -export { getDynamicSamplingContextFromClient } from './dynamicSamplingContext'; +export { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; export { setMeasurement } from './measurement'; diff --git a/packages/core/src/tracing/measurement.ts b/packages/core/src/tracing/measurement.ts index b13bcb6b5a4a..7fff22688f20 100644 --- a/packages/core/src/tracing/measurement.ts +++ b/packages/core/src/tracing/measurement.ts @@ -6,6 +6,7 @@ import { getActiveTransaction } from './utils'; * Adds a measurement to the current active transaction. */ export function setMeasurement(name: string, value: number, unit: MeasurementUnit): void { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (transaction) { transaction.setMeasurement(name, value, unit); diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts index 6c6ab19bf7c9..f14aeb131db4 100644 --- a/packages/core/src/tracing/sampling.ts +++ b/packages/core/src/tracing/sampling.ts @@ -2,7 +2,9 @@ import type { Options, SamplingContext } from '@sentry/types'; import { isNaN, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '../semanticAttributes'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; +import { spanToJSON } from '../utils/spanUtils'; import type { Transaction } from './transaction'; /** @@ -21,15 +23,16 @@ export function sampleTransaction( ): T { // nothing to do if tracing is not enabled if (!hasTracingEnabled(options)) { + // eslint-disable-next-line deprecation/deprecation transaction.sampled = false; return transaction; } // if the user has forced a sampling decision by passing a `sampled` value in their transaction context, go with that + // eslint-disable-next-line deprecation/deprecation if (transaction.sampled !== undefined) { - transaction.setMetadata({ - sampleRate: Number(transaction.sampled), - }); + // eslint-disable-next-line deprecation/deprecation + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, Number(transaction.sampled)); return transaction; } @@ -38,28 +41,23 @@ export function sampleTransaction( let sampleRate; if (typeof options.tracesSampler === 'function') { sampleRate = options.tracesSampler(samplingContext); - transaction.setMetadata({ - sampleRate: Number(sampleRate), - }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, Number(sampleRate)); } else if (samplingContext.parentSampled !== undefined) { sampleRate = samplingContext.parentSampled; } else if (typeof options.tracesSampleRate !== 'undefined') { sampleRate = options.tracesSampleRate; - transaction.setMetadata({ - sampleRate: Number(sampleRate), - }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, Number(sampleRate)); } else { // When `enableTracing === true`, we use a sample rate of 100% sampleRate = 1; - transaction.setMetadata({ - sampleRate, - }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, sampleRate); } // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The // only valid values are booleans or numbers between 0 and 1.) if (!isValidSampleRate(sampleRate)) { DEBUG_BUILD && logger.warn('[Tracing] Discarding transaction because of invalid sample rate.'); + // eslint-disable-next-line deprecation/deprecation transaction.sampled = false; return transaction; } @@ -74,15 +72,18 @@ export function sampleTransaction( : 'a negative sampling decision was inherited or tracesSampleRate is set to 0' }`, ); + // eslint-disable-next-line deprecation/deprecation transaction.sampled = false; return transaction; } // Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is // a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false. + // eslint-disable-next-line deprecation/deprecation transaction.sampled = Math.random() < (sampleRate as number | boolean); // if we're not going to keep it, we're done + // eslint-disable-next-line deprecation/deprecation if (!transaction.sampled) { DEBUG_BUILD && logger.log( @@ -93,7 +94,8 @@ export function sampleTransaction( return transaction; } - DEBUG_BUILD && logger.log(`[Tracing] starting ${transaction.op} transaction - ${transaction.name}`); + DEBUG_BUILD && + logger.log(`[Tracing] starting ${transaction.op} transaction - ${spanToJSON(transaction).description}`); return transaction; } diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index e30f6e416675..10a4d97efa08 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -6,15 +6,24 @@ import type { SpanAttributeValue, SpanAttributes, SpanContext, + SpanContextData, + SpanJSON, SpanOrigin, + SpanTimeInput, TraceContext, Transaction, } from '@sentry/types'; import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; -import { spanToTraceContext, spanToTraceHeader } from '../utils/spanUtils'; -import { ensureTimestampInSeconds } from './utils'; +import { + TRACE_FLAG_NONE, + TRACE_FLAG_SAMPLED, + spanTimeInputToSeconds, + spanToJSON, + spanToTraceContext, + spanToTraceHeader, +} from '../utils/spanUtils'; /** * Keeps track of finished spans for a given transaction @@ -51,16 +60,6 @@ export class SpanRecorder { * Span contains all data about a span */ export class Span implements SpanInterface { - /** - * @inheritDoc - */ - public traceId: string; - - /** - * @inheritDoc - */ - public spanId: string; - /** * @inheritDoc */ @@ -71,11 +70,6 @@ export class Span implements SpanInterface { */ public status?: SpanStatusType | string; - /** - * @inheritDoc - */ - public sampled?: boolean; - /** * Timestamp in seconds when the span was created. */ @@ -92,26 +86,18 @@ export class Span implements SpanInterface { public op?: string; /** - * @inheritDoc - */ - public description?: string; - - /** - * @inheritDoc + * Tags for the span. + * @deprecated Use `getSpanAttributes(span)` instead. */ public tags: { [key: string]: Primitive }; /** - * @inheritDoc + * Data for the span. + * @deprecated Use `getSpanAttributes(span)` instead. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any public data: { [key: string]: any }; - /** - * @inheritDoc - */ - public attributes: SpanAttributes; - /** * List of spans that were finalized */ @@ -132,6 +118,14 @@ export class Span implements SpanInterface { */ public origin?: SpanOrigin; + protected _traceId: string; + protected _spanId: string; + protected _sampled: boolean | undefined; + protected _name?: string; + protected _attributes: SpanAttributes; + + private _logMessage?: string; + /** * You should never call the constructor manually, always use `Sentry.startTransaction()` * or call `startChild()` on an existing span. @@ -140,31 +134,29 @@ export class Span implements SpanInterface { * @hidden */ public constructor(spanContext: SpanContext = {}) { - this.traceId = spanContext.traceId || uuid4(); - this.spanId = spanContext.spanId || uuid4().substring(16); + this._traceId = spanContext.traceId || uuid4(); + this._spanId = spanContext.spanId || uuid4().substring(16); this.startTimestamp = spanContext.startTimestamp || timestampInSeconds(); - this.tags = spanContext.tags || {}; - this.data = spanContext.data || {}; - this.attributes = spanContext.attributes || {}; + // eslint-disable-next-line deprecation/deprecation + this.tags = spanContext.tags ? { ...spanContext.tags } : {}; + // eslint-disable-next-line deprecation/deprecation + this.data = spanContext.data ? { ...spanContext.data } : {}; + this._attributes = spanContext.attributes ? { ...spanContext.attributes } : {}; this.instrumenter = spanContext.instrumenter || 'sentry'; this.origin = spanContext.origin || 'manual'; + // eslint-disable-next-line deprecation/deprecation + this._name = spanContext.name || spanContext.description; if (spanContext.parentSpanId) { this.parentSpanId = spanContext.parentSpanId; } // We want to include booleans as well here if ('sampled' in spanContext) { - this.sampled = spanContext.sampled; + this._sampled = spanContext.sampled; } if (spanContext.op) { this.op = spanContext.op; } - if (spanContext.description) { - this.description = spanContext.description; - } - if (spanContext.name) { - this.description = spanContext.name; - } if (spanContext.status) { this.status = spanContext.status; } @@ -173,28 +165,131 @@ export class Span implements SpanInterface { } } - /** An alias for `description` of the Span. */ + // This rule conflicts with another eslint rule :( + /* eslint-disable @typescript-eslint/member-ordering */ + + /** + * An alias for `description` of the Span. + * @deprecated Use `spanToJSON(span).description` instead. + */ public get name(): string { - return this.description || ''; + return this._name || ''; } + /** * Update the name of the span. + * @deprecated Use `spanToJSON(span).description` instead. */ public set name(name: string) { this.updateName(name); } /** - * @inheritDoc + * Get the description of the Span. + * @deprecated Use `spanToJSON(span).description` instead. + */ + public get description(): string | undefined { + return this._name; + } + + /** + * Get the description of the Span. + * @deprecated Use `spanToJSON(span).description` instead. + */ + public set description(description: string | undefined) { + this._name = description; + } + + /** + * The ID of the trace. + * @deprecated Use `spanContext().traceId` instead. + */ + public get traceId(): string { + return this._traceId; + } + + /** + * The ID of the trace. + * @deprecated You cannot update the traceId of a span after span creation. + */ + public set traceId(traceId: string) { + this._traceId = traceId; + } + + /** + * The ID of the span. + * @deprecated Use `spanContext().spanId` instead. + */ + public get spanId(): string { + return this._spanId; + } + + /** + * The ID of the span. + * @deprecated You cannot update the spanId of a span after span creation. + */ + public set spanId(spanId: string) { + this._spanId = spanId; + } + + /** + * Was this span chosen to be sent as part of the sample? + * @deprecated Use `isRecording()` instead. + */ + public get sampled(): boolean | undefined { + return this._sampled; + } + + /** + * Was this span chosen to be sent as part of the sample? + * @deprecated You cannot update the sampling decision of a span after span creation. + */ + public set sampled(sampled: boolean | undefined) { + this._sampled = sampled; + } + + /** + * Attributes for the span. + * @deprecated Use `getSpanAttributes(span)` instead. + */ + public get attributes(): SpanAttributes { + return this._attributes; + } + + /** + * Attributes for the span. + * @deprecated Use `setAttributes()` instead. + */ + public set attributes(attributes: SpanAttributes) { + this._attributes = attributes; + } + + /* eslint-enable @typescript-eslint/member-ordering */ + + /** @inheritdoc */ + public spanContext(): SpanContextData { + const { _spanId: spanId, _traceId: traceId, _sampled: sampled } = this; + return { + spanId, + traceId, + traceFlags: sampled ? TRACE_FLAG_SAMPLED : TRACE_FLAG_NONE, + }; + } + + /** + * Creates a new `Span` while setting the current `Span.id` as `parentSpanId`. + * Also the `sampled` decision will be inherited. + * + * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. */ public startChild( spanContext?: Pick>, ): Span { const childSpan = new Span({ ...spanContext, - parentSpanId: this.spanId, - sampled: this.sampled, - traceId: this.traceId, + parentSpanId: this._spanId, + sampled: this._sampled, + traceId: this._traceId, }); childSpan.spanRecorder = this.spanRecorder; @@ -206,30 +301,41 @@ export class Span implements SpanInterface { if (DEBUG_BUILD && childSpan.transaction) { const opStr = (spanContext && spanContext.op) || '< unknown op >'; - const nameStr = childSpan.transaction.name || '< unknown name >'; - const idStr = childSpan.transaction.spanId; + const nameStr = spanToJSON(childSpan).description || '< unknown name >'; + const idStr = childSpan.transaction.spanContext().spanId; const logMessage = `[Tracing] Starting '${opStr}' span on transaction '${nameStr}' (${idStr}).`; - childSpan.transaction.metadata.spanMetadata[childSpan.spanId] = { logMessage }; logger.log(logMessage); + this._logMessage = logMessage; } return childSpan; } /** - * @inheritDoc + * Sets the tag attribute on the current span. + * + * Can also be used to unset a tag, by passing `undefined`. + * + * @param key Tag key + * @param value Tag value + * @deprecated Use `setAttribute()` instead. */ public setTag(key: string, value: Primitive): this { + // eslint-disable-next-line deprecation/deprecation this.tags = { ...this.tags, [key]: value }; return this; } /** - * @inheritDoc + * Sets the data attribute on the current span + * @param key Data key + * @param value Data value + * @deprecated Use `setAttribute()` instead. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any public setData(key: string, value: any): this { + // eslint-disable-next-line deprecation/deprecation this.data = { ...this.data, [key]: value }; return this; } @@ -238,9 +344,9 @@ export class Span implements SpanInterface { public setAttribute(key: string, value: SpanAttributeValue | undefined): void { if (value === undefined) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this.attributes[key]; + delete this._attributes[key]; } else { - this.attributes[key] = value; + this._attributes[key] = value; } } @@ -261,7 +367,9 @@ export class Span implements SpanInterface { * @inheritDoc */ public setHttpStatus(httpStatus: number): this { + // eslint-disable-next-line deprecation/deprecation this.setTag('http.status_code', String(httpStatus)); + // eslint-disable-next-line deprecation/deprecation this.setData('http.response.status_code', httpStatus); const spanStatus = spanStatusfromHttpCode(httpStatus); if (spanStatus !== 'unknown_error') { @@ -279,7 +387,7 @@ export class Span implements SpanInterface { * @inheritDoc */ public updateName(name: string): this { - this.description = name; + this._name = name; return this; } @@ -300,21 +408,20 @@ export class Span implements SpanInterface { } /** @inheritdoc */ - public end(endTimestamp?: number): void { + public end(endTimestamp?: SpanTimeInput): void { if ( DEBUG_BUILD && // Don't call this for transactions this.transaction && - this.transaction.spanId !== this.spanId + this.transaction.spanContext().spanId !== this._spanId ) { - const { logMessage } = this.transaction.metadata.spanMetadata[this.spanId]; + const logMessage = this._logMessage; if (logMessage) { logger.log((logMessage as string).replace('Starting', 'Finishing')); } } - this.endTimestamp = - typeof endTimestamp === 'number' ? ensureTimestampInSeconds(endTimestamp) : timestampInSeconds(); + this.endTimestamp = spanTimeInputToSeconds(endTimestamp); } /** @@ -330,16 +437,17 @@ export class Span implements SpanInterface { public toContext(): SpanContext { return dropUndefinedKeys({ data: this._getData(), - description: this.description, + description: this._name, endTimestamp: this.endTimestamp, op: this.op, parentSpanId: this.parentSpanId, - sampled: this.sampled, - spanId: this.spanId, + sampled: this._sampled, + spanId: this._spanId, startTimestamp: this.startTimestamp, status: this.status, + // eslint-disable-next-line deprecation/deprecation tags: this.tags, - traceId: this.traceId, + traceId: this._traceId, }); } @@ -347,17 +455,20 @@ export class Span implements SpanInterface { * @inheritDoc */ public updateWithContext(spanContext: SpanContext): this { + // eslint-disable-next-line deprecation/deprecation this.data = spanContext.data || {}; - this.description = spanContext.description; + // eslint-disable-next-line deprecation/deprecation + this._name = spanContext.name || spanContext.description; this.endTimestamp = spanContext.endTimestamp; this.op = spanContext.op; this.parentSpanId = spanContext.parentSpanId; - this.sampled = spanContext.sampled; - this.spanId = spanContext.spanId || this.spanId; + this._sampled = spanContext.sampled; + this._spanId = spanContext.spanId || this._spanId; this.startTimestamp = spanContext.startTimestamp || this.startTimestamp; this.status = spanContext.status; + // eslint-disable-next-line deprecation/deprecation this.tags = spanContext.tags || {}; - this.traceId = spanContext.traceId || this.traceId; + this._traceId = spanContext.traceId || this._traceId; return this; } @@ -370,37 +481,38 @@ export class Span implements SpanInterface { } /** - * @inheritDoc + * Get JSON representation of this span. */ - public toJSON(): { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data?: { [key: string]: any }; - description?: string; - op?: string; - parent_span_id?: string; - span_id: string; - start_timestamp: number; - status?: string; - tags?: { [key: string]: Primitive }; - timestamp?: number; - trace_id: string; - origin?: SpanOrigin; - } { + public getSpanJSON(): SpanJSON { return dropUndefinedKeys({ data: this._getData(), - description: this.description, + description: this._name, op: this.op, parent_span_id: this.parentSpanId, - span_id: this.spanId, + span_id: this._spanId, start_timestamp: this.startTimestamp, status: this.status, + // eslint-disable-next-line deprecation/deprecation tags: Object.keys(this.tags).length > 0 ? this.tags : undefined, timestamp: this.endTimestamp, - trace_id: this.traceId, + trace_id: this._traceId, origin: this.origin, }); } + /** @inheritdoc */ + public isRecording(): boolean { + return !this.endTimestamp && !!this._sampled; + } + + /** + * Convert the object to JSON. + * @deprecated Use `spanToJSON(span)` instead. + */ + public toJSON(): SpanJSON { + return this.getSpanJSON(); + } + /** * Get the merged data for this span. * For now, this combines `data` and `attributes` together, @@ -412,7 +524,8 @@ export class Span implements SpanInterface { [key: string]: any; } | undefined { - const { data, attributes } = this; + // eslint-disable-next-line deprecation/deprecation + const { data, _attributes: attributes } = this; const hasData = Object.keys(data).length > 0; const hasAttributes = Object.keys(attributes).length > 0; diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index b5d49f4767a7..015bf4757b8f 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -1,4 +1,15 @@ -import type { Span, TransactionContext } from '@sentry/types'; +import type { + Instrumenter, + Primitive, + Scope, + Span, + SpanTimeInput, + TransactionContext, + TransactionMetadata, +} from '@sentry/types'; +import type { SpanAttributes } from '@sentry/types'; +import type { SpanOrigin } from '@sentry/types'; +import type { TransactionSource } from '@sentry/types'; import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; @@ -7,6 +18,106 @@ import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; import { handleCallbackErrors } from '../utils/handleCallbackErrors'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; +import { spanTimeInputToSeconds } from '../utils/spanUtils'; + +interface StartSpanOptions extends TransactionContext { + /** A manually specified start time for the created `Span` object. */ + startTime?: SpanTimeInput; + + /** If defined, start this span off this scope instead off the current scope. */ + scope?: Scope; + + /** The name of the span. */ + name: string; + + /** An op for the span. This is a categorization for spans. */ + op?: string; + + /** The origin of the span - if it comes from auto instrumenation or manual instrumentation. */ + origin?: SpanOrigin; + + /** Attributes for the span. */ + attributes?: SpanAttributes; + + // All remaining fields are deprecated + + /** + * @deprecated Manually set the end timestamp instead. + */ + trimEnd?: boolean; + + /** + * @deprecated This cannot be set manually anymore. + */ + parentSampled?: boolean; + + /** + * @deprecated Use attributes or set data on scopes instead. + */ + metadata?: Partial; + + /** + * The name thingy. + * @deprecated Use `name` instead. + */ + description?: string; + + /** + * @deprecated Use `span.setStatus()` instead. + */ + status?: string; + + /** + * @deprecated Use `scope` instead. + */ + parentSpanId?: string; + + /** + * @deprecated You cannot manually set the span to sampled anymore. + */ + sampled?: boolean; + + /** + * @deprecated You cannot manually set the spanId anymore. + */ + spanId?: string; + + /** + * @deprecated You cannot manually set the traceId anymore. + */ + traceId?: string; + + /** + * @deprecated Use an attribute instead. + */ + source?: TransactionSource; + + /** + * @deprecated Use attributes or set tags on the scope instead. + */ + tags?: { [key: string]: Primitive }; + + /** + * @deprecated Use attributes instead. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: { [key: string]: any }; + + /** + * @deprecated Use `startTime` instead. + */ + startTimestamp?: number; + + /** + * @deprecated Use `span.end()` instead. + */ + endTimestamp?: number; + + /** + * @deprecated You cannot set the instrumenter manually anymore. + */ + instrumenter?: Instrumenter; +} /** * Wraps a function with a transaction/span and finishes the span after the function is done. @@ -30,14 +141,15 @@ export function trace( // eslint-disable-next-line @typescript-eslint/no-empty-function afterFinish: () => void = () => {}, ): T { - const ctx = normalizeContext(context); - const hub = getCurrentHub(); const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); + const ctx = normalizeContext(context); const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(activeSpan); return handleCallbackErrors( @@ -48,6 +160,7 @@ export function trace( }, () => { activeSpan && activeSpan.end(); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(parentSpan); afterFinish(); }, @@ -65,14 +178,16 @@ export function trace( * or you didn't set `tracesSampleRate`, this function will not generate spans * and the `span` returned from the callback will be undefined. */ -export function startSpan(context: TransactionContext, callback: (span: Span | undefined) => T): T { +export function startSpan(context: StartSpanOptions, callback: (span: Span | undefined) => T): T { const ctx = normalizeContext(context); - return withScope(scope => { + return withScope(context.scope, scope => { const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(activeSpan); return handleCallbackErrors( @@ -105,16 +220,18 @@ export const startActiveSpan = startSpan; * and the `span` returned from the callback will be undefined. */ export function startSpanManual( - context: TransactionContext, + context: StartSpanOptions, callback: (span: Span | undefined, finish: () => void) => T, ): T { const ctx = normalizeContext(context); - return withScope(scope => { + return withScope(context.scope, scope => { const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(activeSpan); function finishAndSetSpan(): void { @@ -143,26 +260,29 @@ export function startSpanManual( * or you didn't set `tracesSampleRate` or `tracesSampler`, this function will not generate spans * and the `span` returned from the callback will be undefined. */ -export function startInactiveSpan(context: TransactionContext): Span | undefined { +export function startInactiveSpan(context: StartSpanOptions): Span | undefined { if (!hasTracingEnabled()) { return undefined; } - const ctx = { ...context }; - // If a name is set and a description is not, set the description to the name. - if (ctx.name !== undefined && ctx.description === undefined) { - ctx.description = ctx.name; - } - + const ctx = normalizeContext(context); const hub = getCurrentHub(); - const parentSpan = getActiveSpan(); - return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx); + const parentSpan = context.scope + ? // eslint-disable-next-line deprecation/deprecation + context.scope.getSpan() + : getActiveSpan(); + return parentSpan + ? // eslint-disable-next-line deprecation/deprecation + parentSpan.startChild(ctx) + : // eslint-disable-next-line deprecation/deprecation + hub.startTransaction(ctx); } /** * Returns the currently active span. */ export function getActiveSpan(): Span | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentScope().getSpan(); } @@ -235,15 +355,27 @@ function createChildSpanOrTransaction( if (!hasTracingEnabled()) { return undefined; } - return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx); + return parentSpan + ? // eslint-disable-next-line deprecation/deprecation + parentSpan.startChild(ctx) + : // eslint-disable-next-line deprecation/deprecation + hub.startTransaction(ctx); } -function normalizeContext(context: TransactionContext): TransactionContext { - const ctx = { ...context }; - // If a name is set and a description is not, set the description to the name. - if (ctx.name !== undefined && ctx.description === undefined) { - ctx.description = ctx.name; +/** + * This converts StartSpanOptions to TransactionContext. + * For the most part (for now) we accept the same options, + * but some of them need to be transformed. + * + * Eventually the StartSpanOptions will be more aligned with OpenTelemetry. + */ +function normalizeContext(context: StartSpanOptions): TransactionContext { + if (context.startTime) { + const ctx: TransactionContext & { startTime?: SpanTimeInput } = { ...context }; + ctx.startTimestamp = spanTimeInputToSeconds(context.startTime); + delete ctx.startTime; + return ctx; } - return ctx; + return context; } diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index 4652ab160143..6e9e3e62f9d9 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -4,31 +4,30 @@ import type { DynamicSamplingContext, MeasurementUnit, Measurements, + SpanTimeInput, Transaction as TransactionInterface, TransactionContext, TransactionEvent, TransactionMetadata, } from '@sentry/types'; -import { dropUndefinedKeys, logger, timestampInSeconds } from '@sentry/utils'; +import { dropUndefinedKeys, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; -import { spanToTraceContext } from '../utils/spanUtils'; -import { getDynamicSamplingContextFromClient } from './dynamicSamplingContext'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; +import { spanTimeInputToSeconds, spanToTraceContext } from '../utils/spanUtils'; +import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { Span as SpanClass, SpanRecorder } from './span'; -import { ensureTimestampInSeconds } from './utils'; /** JSDoc */ export class Transaction extends SpanClass implements TransactionInterface { - public metadata: TransactionMetadata; - /** * The reference to the current hub. */ public _hub: Hub; - private _name: string; + protected _name: string; private _measurements: Measurements; @@ -36,21 +35,22 @@ export class Transaction extends SpanClass implements TransactionInterface { private _trimEnd?: boolean; + // DO NOT yet remove this property, it is used in a hack for v7 backwards compatibility. private _frozenDynamicSamplingContext: Readonly> | undefined; + private _metadata: Partial; + /** * This constructor should never be called manually. Those instrumenting tracing should use * `Sentry.startTransaction()`, and internal methods should use `hub.startTransaction()`. * @internal * @hideconstructor * @hidden + * + * @deprecated Transactions will be removed in v8. Use spans instead. */ public constructor(transactionContext: TransactionContext, hub?: Hub) { super(transactionContext); - // We need to delete description since it's set by the Span class constructor - // but not needed for transactions. - delete this.description; - this._measurements = {}; this._contexts = {}; @@ -58,10 +58,9 @@ export class Transaction extends SpanClass implements TransactionInterface { this._name = transactionContext.name || ''; - this.metadata = { - source: 'custom', + this._metadata = { + // eslint-disable-next-line deprecation/deprecation ...transactionContext.metadata, - spanMetadata: {}, }; this._trimEnd = transactionContext.trimEnd; @@ -71,26 +70,68 @@ export class Transaction extends SpanClass implements TransactionInterface { // If Dynamic Sampling Context is provided during the creation of the transaction, we freeze it as it usually means // there is incoming Dynamic Sampling Context. (Either through an incoming request, a baggage meta-tag, or other means) - const incomingDynamicSamplingContext = this.metadata.dynamicSamplingContext; + const incomingDynamicSamplingContext = this._metadata.dynamicSamplingContext; if (incomingDynamicSamplingContext) { // We shallow copy this in case anything writes to the original reference of the passed in `dynamicSamplingContext` this._frozenDynamicSamplingContext = { ...incomingDynamicSamplingContext }; } } - /** Getter for `name` property */ + // This sadly conflicts with the getter/setter ordering :( + /* eslint-disable @typescript-eslint/member-ordering */ + + /** + * Getter for `name` property. + * @deprecated Use `spanToJSON(span).description` instead. + */ public get name(): string { return this._name; } /** * Setter for `name` property, which also sets `source` as custom. + * @deprecated Use `updateName()` and `setMetadata()` instead. */ public set name(newName: string) { // eslint-disable-next-line deprecation/deprecation this.setName(newName); } + /** + * Get the metadata for this transaction. + * @deprecated Use `spanGetMetadata(transaction)` instead. + */ + public get metadata(): TransactionMetadata { + // We merge attributes in for backwards compatibility + return { + // Defaults + // eslint-disable-next-line deprecation/deprecation + source: 'custom', + spanMetadata: {}, + + // Legacy metadata + ...this._metadata, + + // From attributes + ...(this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] && { + source: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionMetadata['source'], + }), + ...(this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] && { + sampleRate: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] as TransactionMetadata['sampleRate'], + }), + }; + } + + /** + * Update the metadata for this transaction. + * @deprecated Use `spanGetMetadata(transaction)` instead. + */ + public set metadata(metadata: TransactionMetadata) { + this._metadata = metadata; + } + + /* eslint-enable @typescript-eslint/member-ordering */ + /** * Setter for `name` property, which also sets `source` on the metadata. * @@ -98,7 +139,7 @@ export class Transaction extends SpanClass implements TransactionInterface { */ public setName(name: string, source: TransactionMetadata['source'] = 'custom'): void { this._name = name; - this.metadata.source = source; + this.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); } /** @inheritdoc */ @@ -119,7 +160,8 @@ export class Transaction extends SpanClass implements TransactionInterface { } /** - * @inheritDoc + * Set the context of a transaction event. + * @deprecated Use either `.setAttribute()`, or set the context on the scope before creating the transaction. */ public setContext(key: string, context: Context | null): void { if (context === null) { @@ -138,22 +180,23 @@ export class Transaction extends SpanClass implements TransactionInterface { } /** - * @inheritDoc + * Store metadata on this transaction. + * @deprecated Use attributes or store data on the scope instead. */ public setMetadata(newMetadata: Partial): void { - this.metadata = { ...this.metadata, ...newMetadata }; + this._metadata = { ...this._metadata, ...newMetadata }; } /** * @inheritDoc */ - public end(endTimestamp?: number): string | undefined { - const timestampInS = - typeof endTimestamp === 'number' ? ensureTimestampInSeconds(endTimestamp) : timestampInSeconds(); + public end(endTimestamp?: SpanTimeInput): string | undefined { + const timestampInS = spanTimeInputToSeconds(endTimestamp); const transaction = this._finishTransaction(timestampInS); if (!transaction) { return undefined; } + // eslint-disable-next-line deprecation/deprecation return this._hub.captureEvent(transaction); } @@ -166,7 +209,7 @@ export class Transaction extends SpanClass implements TransactionInterface { return dropUndefinedKeys({ ...spanContext, - name: this.name, + name: this._name, trimEnd: this._trimEnd, }); } @@ -178,8 +221,7 @@ export class Transaction extends SpanClass implements TransactionInterface { // eslint-disable-next-line deprecation/deprecation super.updateWithContext(transactionContext); - this.name = transactionContext.name || ''; - + this._name = transactionContext.name || ''; this._trimEnd = transactionContext.trimEnd; return this; @@ -189,39 +231,11 @@ export class Transaction extends SpanClass implements TransactionInterface { * @inheritdoc * * @experimental + * + * @deprecated Use top-level `getDynamicSamplingContextFromSpan` instead. */ public getDynamicSamplingContext(): Readonly> { - if (this._frozenDynamicSamplingContext) { - return this._frozenDynamicSamplingContext; - } - - const hub = this._hub || getCurrentHub(); - const client = hub.getClient(); - - if (!client) return {}; - - const scope = hub.getScope(); - const dsc = getDynamicSamplingContextFromClient(this.traceId, client, scope); - - const maybeSampleRate = this.metadata.sampleRate; - if (maybeSampleRate !== undefined) { - dsc.sample_rate = `${maybeSampleRate}`; - } - - // We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII - const source = this.metadata.source; - if (source && source !== 'url') { - dsc.transaction = this.name; - } - - if (this.sampled !== undefined) { - dsc.sampled = String(this.sampled); - } - - // Uncomment if we want to make DSC immutable - // this._frozenDynamicSamplingContext = dsc; - - return dsc; + return getDynamicSamplingContextFromSpan(this); } /** @@ -243,9 +257,9 @@ export class Transaction extends SpanClass implements TransactionInterface { return undefined; } - if (!this.name) { + if (!this._name) { DEBUG_BUILD && logger.warn('Transaction has no name, falling back to ``.'); - this.name = ''; + this._name = ''; } // just sets the end timestamp @@ -256,7 +270,7 @@ export class Transaction extends SpanClass implements TransactionInterface { client.emit('finishTransaction', this); } - if (this.sampled !== true) { + if (this._sampled !== true) { // At this point if `sampled !== true` we want to discard the transaction. DEBUG_BUILD && logger.log('[Tracing] Discarding transaction because its trace was not chosen to be sampled.'); @@ -278,7 +292,10 @@ export class Transaction extends SpanClass implements TransactionInterface { }).endTimestamp; } - const metadata = this.metadata; + // eslint-disable-next-line deprecation/deprecation + const { metadata } = this; + // eslint-disable-next-line deprecation/deprecation + const { source } = metadata; const transaction: TransactionEvent = { contexts: { @@ -286,19 +303,21 @@ export class Transaction extends SpanClass implements TransactionInterface { // We don't want to override trace context trace: spanToTraceContext(this), }, + // TODO: Pass spans serialized via `spanToJSON()` here instead in v8. spans: finishedSpans, start_timestamp: this.startTimestamp, + // eslint-disable-next-line deprecation/deprecation tags: this.tags, timestamp: this.endTimestamp, - transaction: this.name, + transaction: this._name, type: 'transaction', sdkProcessingMetadata: { ...metadata, - dynamicSamplingContext: this.getDynamicSamplingContext(), + dynamicSamplingContext: getDynamicSamplingContextFromSpan(this), }, - ...(metadata.source && { + ...(source && { transaction_info: { - source: metadata.source, + source, }, }), }; @@ -314,7 +333,7 @@ export class Transaction extends SpanClass implements TransactionInterface { transaction.measurements = this._measurements; } - DEBUG_BUILD && logger.log(`[Tracing] Finishing ${this.op} transaction: ${this.name}.`); + DEBUG_BUILD && logger.log(`[Tracing] Finishing ${this.op} transaction: ${this._name}.`); return transaction; } diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts index 4c1d49780554..bfea3d5d1e28 100644 --- a/packages/core/src/tracing/utils.ts +++ b/packages/core/src/tracing/utils.ts @@ -4,10 +4,15 @@ import { extractTraceparentData as _extractTraceparentData } from '@sentry/utils import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; -/** Grabs active transaction off scope, if any */ +/** + * Grabs active transaction off scope. + * + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. + */ export function getActiveTransaction(maybeHub?: Hub): T | undefined { const hub = maybeHub || getCurrentHub(); const scope = hub.getScope(); + // eslint-disable-next-line deprecation/deprecation return scope.getTransaction() as T | undefined; } @@ -27,11 +32,3 @@ export { stripUrlQueryAndFragment } from '@sentry/utils'; * @deprecated Import this function from `@sentry/utils` instead */ export const extractTraceparentData = _extractTraceparentData; - -/** - * Converts a timestamp to second, if it was in milliseconds, or keeps it as second. - */ -export function ensureTimestampInSeconds(timestamp: number): number { - const isMs = timestamp > 9999999999; - return isMs ? timestamp / 1000 : timestamp; -} diff --git a/packages/core/src/utils/applyScopeDataToEvent.ts b/packages/core/src/utils/applyScopeDataToEvent.ts index 96a85740ef64..f834776ac9ba 100644 --- a/packages/core/src/utils/applyScopeDataToEvent.ts +++ b/packages/core/src/utils/applyScopeDataToEvent.ts @@ -1,6 +1,7 @@ import type { Breadcrumb, Event, PropagationContext, ScopeData, Span } from '@sentry/types'; import { arrayify } from '@sentry/utils'; -import { spanToTraceContext } from './spanUtils'; +import { getDynamicSamplingContextFromSpan } from '../tracing/dynamicSamplingContext'; +import { spanToJSON, spanToTraceContext } from './spanUtils'; /** * Applies data from the scope to the event and runs all event processors on it. @@ -37,6 +38,7 @@ export function mergeScopeData(data: ScopeData, mergeData: ScopeData): void { eventProcessors, attachments, propagationContext, + // eslint-disable-next-line deprecation/deprecation transactionName, span, } = mergeData; @@ -52,6 +54,7 @@ export function mergeScopeData(data: ScopeData, mergeData: ScopeData): void { } if (transactionName) { + // eslint-disable-next-line deprecation/deprecation data.transactionName = transactionName; } @@ -122,7 +125,15 @@ export function mergeArray( } function applyDataToEvent(event: Event, data: ScopeData): void { - const { extra, tags, user, contexts, level, transactionName } = data; + const { + extra, + tags, + user, + contexts, + level, + // eslint-disable-next-line deprecation/deprecation + transactionName, + } = data; if (extra && Object.keys(extra).length) { event.extra = { ...extra, ...event.extra }; @@ -166,10 +177,10 @@ function applySpanToEvent(event: Event, span: Span): void { const transaction = span.transaction; if (transaction) { event.sdkProcessingMetadata = { - dynamicSamplingContext: transaction.getDynamicSamplingContext(), + dynamicSamplingContext: getDynamicSamplingContextFromSpan(span), ...event.sdkProcessingMetadata, }; - const transactionName = transaction.name; + const transactionName = spanToJSON(transaction).description; if (transactionName) { event.tags = { transaction: transactionName, ...event.tags }; } diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 6f448df8496d..17be8c5354de 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -15,6 +15,7 @@ import { DEFAULT_ENVIRONMENT } from '../constants'; import { getGlobalEventProcessors, notifyEventProcessors } from '../eventProcessors'; import { Scope, getGlobalScope } from '../scope'; import { applyScopeDataToEvent, mergeScopeData } from './applyScopeDataToEvent'; +import { spanToJSON } from './spanUtils'; /** * This type makes sure that we get either a CaptureContext, OR an EventHint. @@ -326,10 +327,14 @@ function normalizeEvent(event: Event | null, depth: number, maxBreadth: number): // event.spans[].data may contain circular/dangerous data so we need to normalize it if (event.spans) { normalized.spans = event.spans.map(span => { - // We cannot use the spread operator here because `toJSON` on `span` is non-enumerable - if (span.data) { - span.data = normalize(span.data, depth, maxBreadth); + const data = spanToJSON(span).data; + + if (data) { + // This is a bit weird, as we generally have `Span` instances here, but to be safe we do not assume so + // eslint-disable-next-line deprecation/deprecation + span.data = normalize(data, depth, maxBreadth); } + return span; }); } diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 2dae21a78fca..cbf8ce6d7f5b 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -1,15 +1,20 @@ -import type { Span, TraceContext } from '@sentry/types'; -import { dropUndefinedKeys, generateSentryTraceHeader } from '@sentry/utils'; +import type { Span, SpanJSON, SpanTimeInput, TraceContext } from '@sentry/types'; +import { dropUndefinedKeys, generateSentryTraceHeader, timestampInSeconds } from '@sentry/utils'; +import type { Span as SpanClass } from '../tracing/span'; + +// These are aligned with OpenTelemetry trace flags +export const TRACE_FLAG_NONE = 0x0; +export const TRACE_FLAG_SAMPLED = 0x1; /** * Convert a span to a trace context, which can be sent as the `trace` context in an event. */ export function spanToTraceContext(span: Span): TraceContext { - const { data, description, op, parent_span_id, span_id, status, tags, trace_id, origin } = span.toJSON(); + const { spanId: span_id, traceId: trace_id } = span.spanContext(); + const { data, op, parent_span_id, status, tags, origin } = spanToJSON(span); return dropUndefinedKeys({ data, - description, op, parent_span_id, span_id, @@ -24,5 +29,81 @@ export function spanToTraceContext(span: Span): TraceContext { * Convert a Span to a Sentry trace header. */ export function spanToTraceHeader(span: Span): string { - return generateSentryTraceHeader(span.traceId, span.spanId, span.sampled); + const { traceId, spanId } = span.spanContext(); + const sampled = spanIsSampled(span); + return generateSentryTraceHeader(traceId, spanId, sampled); +} + +/** + * Convert a span time input intp a timestamp in seconds. + */ +export function spanTimeInputToSeconds(input: SpanTimeInput | undefined): number { + if (typeof input === 'number') { + return ensureTimestampInSeconds(input); + } + + if (Array.isArray(input)) { + // See {@link HrTime} for the array-based time format + return input[0] + input[1] / 1e9; + } + + if (input instanceof Date) { + return ensureTimestampInSeconds(input.getTime()); + } + + return timestampInSeconds(); +} + +/** + * Converts a timestamp to second, if it was in milliseconds, or keeps it as second. + */ +function ensureTimestampInSeconds(timestamp: number): number { + const isMs = timestamp > 9999999999; + return isMs ? timestamp / 1000 : timestamp; +} + +/** + * Convert a span to a JSON representation. + * Note that all fields returned here are optional and need to be guarded against. + * + * Note: Because of this, we currently have a circular type dependency (which we opted out of in package.json). + * This is not avoidable as we need `spanToJSON` in `spanUtils.ts`, which in turn is needed by `span.ts` for backwards compatibility. + * And `spanToJSON` needs the Span class from `span.ts` to check here. + * TODO v8: When we remove the deprecated stuff from `span.ts`, we can remove the circular dependency again. + */ +export function spanToJSON(span: Span): Partial { + if (spanIsSpanClass(span)) { + return span.getSpanJSON(); + } + + // Fallback: We also check for `.toJSON()` here... + // eslint-disable-next-line deprecation/deprecation + if (typeof span.toJSON === 'function') { + // eslint-disable-next-line deprecation/deprecation + return span.toJSON(); + } + + return {}; +} + +/** + * Sadly, due to circular dependency checks we cannot actually import the Span class here and check for instanceof. + * :( So instead we approximate this by checking if it has the `getSpanJSON` method. + */ +function spanIsSpanClass(span: Span): span is SpanClass { + return typeof (span as SpanClass).getSpanJSON === 'function'; +} + +/** + * Returns true if a span is sampled. + * In most cases, you should just use `span.isRecording()` instead. + * However, this has a slightly different semantic, as it also returns false if the span is finished. + * So in the case where this distinction is important, use this method. + */ +export function spanIsSampled(span: Span): boolean { + // We align our trace flags with the ones OpenTelemetry use + // So we also check for sampled the same way they do. + const { traceFlags } = span.spanContext(); + // eslint-disable-next-line no-bitwise + return Boolean(traceFlags & TRACE_FLAG_SAMPLED); } diff --git a/packages/core/test/lib/exports.test.ts b/packages/core/test/lib/exports.test.ts index 7a4ec6987dd1..a02673d15a1f 100644 --- a/packages/core/test/lib/exports.test.ts +++ b/packages/core/test/lib/exports.test.ts @@ -1,4 +1,14 @@ -import { Hub, Scope, getCurrentScope, makeMain, withScope } from '../../src'; +import { + Hub, + Scope, + captureSession, + endSession, + getCurrentScope, + getIsolationScope, + makeMain, + startSession, + withScope, +} from '../../src'; import { TestClient, getDefaultTestClientOptions } from '../mocks/client'; function getTestClient(): TestClient { @@ -124,4 +134,119 @@ describe('withScope', () => { expect(getCurrentScope()).toBe(scope1); }); + + it('allows to pass a custom scope', () => { + const scope1 = getCurrentScope(); + scope1.setExtra('x1', 'x1'); + + const customScope = new Scope(); + customScope.setExtra('x2', 'x2'); + + withScope(customScope, scope2 => { + expect(scope2).not.toBe(scope1); + expect(scope2).toBe(customScope); + expect(getCurrentScope()).toBe(scope2); + expect(scope2['_extra']).toEqual({ x2: 'x2' }); + }); + + withScope(customScope, scope3 => { + expect(scope3).not.toBe(scope1); + expect(scope3).toBe(customScope); + expect(getCurrentScope()).toBe(scope3); + expect(scope3['_extra']).toEqual({ x2: 'x2' }); + }); + + expect(getCurrentScope()).toBe(scope1); + }); +}); + +describe('session APIs', () => { + beforeEach(() => { + const client = getTestClient(); + const hub = new Hub(client); + makeMain(hub); + }); + + describe('startSession', () => { + it('starts a session', () => { + const session = startSession(); + + expect(session).toMatchObject({ + status: 'ok', + errors: 0, + init: true, + environment: 'production', + ignoreDuration: false, + sid: expect.any(String), + did: undefined, + timestamp: expect.any(Number), + started: expect.any(Number), + duration: expect.any(Number), + toJSON: expect.any(Function), + }); + }); + + it('ends a previously active session and removes it from the scope', () => { + const session1 = startSession(); + + expect(session1.status).toBe('ok'); + expect(getIsolationScope().getSession()).toBe(session1); + + const session2 = startSession(); + + expect(session2.status).toBe('ok'); + expect(session1.status).toBe('exited'); + expect(getIsolationScope().getSession()).toBe(session2); + }); + }); + + describe('endSession', () => { + it('ends a session and removes it from the scope', () => { + const session = startSession(); + + expect(session.status).toBe('ok'); + expect(getIsolationScope().getSession()).toBe(session); + + endSession(); + + expect(session.status).toBe('exited'); + expect(getIsolationScope().getSession()).toBe(undefined); + }); + }); + + describe('captureSession', () => { + it('captures a session without ending it by default', () => { + const session = startSession({ release: '1.0.0' }); + + expect(session.status).toBe('ok'); + expect(session.init).toBe(true); + expect(getIsolationScope().getSession()).toBe(session); + + captureSession(); + + // this flag indicates the session was sent via BaseClient + expect(session.init).toBe(false); + + // session is still active and on the scope + expect(session.status).toBe('ok'); + expect(getIsolationScope().getSession()).toBe(session); + }); + + it('captures a session and ends it if end is `true`', () => { + const session = startSession({ release: '1.0.0' }); + + expect(session.status).toBe('ok'); + expect(session.init).toBe(true); + expect(getIsolationScope().getSession()).toBe(session); + + captureSession(true); + + // this flag indicates the session was sent via BaseClient + expect(session.init).toBe(false); + + // session is still active and on the scope + expect(session.status).toBe('exited'); + expect(getIsolationScope().getSession()).toBe(undefined); + }); + }); }); diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index 137a7dce4df3..19e22773b59b 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -377,7 +377,7 @@ describe('setupIntegration', () => { setupIntegration(client2, integration3, integrationIndex); setupIntegration(client2, integration4, integrationIndex); - expect(integrationIndex).toEqual({ test: integration4 }); + expect(integrationIndex).toEqual({ test: integration1 }); expect(integration1.setupOnce).toHaveBeenCalledTimes(1); expect(integration2.setupOnce).not.toHaveBeenCalled(); expect(integration3.setupOnce).not.toHaveBeenCalled(); @@ -394,32 +394,32 @@ describe('setupIntegration', () => { const client1 = getTestClient(); const client2 = getTestClient(); - const integrationIndex = {}; + const integrationIndex1 = {}; + const integrationIndex2 = {}; const integration1 = new CustomIntegration(); const integration2 = new CustomIntegration(); const integration3 = new CustomIntegration(); const integration4 = new CustomIntegration(); - setupIntegration(client1, integration1, integrationIndex); - setupIntegration(client1, integration2, integrationIndex); - setupIntegration(client2, integration3, integrationIndex); - setupIntegration(client2, integration4, integrationIndex); + setupIntegration(client1, integration1, integrationIndex1); + setupIntegration(client1, integration2, integrationIndex1); + setupIntegration(client2, integration3, integrationIndex2); + setupIntegration(client2, integration4, integrationIndex2); - expect(integrationIndex).toEqual({ test: integration4 }); + expect(integrationIndex1).toEqual({ test: integration1 }); + expect(integrationIndex2).toEqual({ test: integration3 }); expect(integration1.setupOnce).toHaveBeenCalledTimes(1); expect(integration2.setupOnce).not.toHaveBeenCalled(); expect(integration3.setupOnce).not.toHaveBeenCalled(); expect(integration4.setupOnce).not.toHaveBeenCalled(); expect(integration1.setup).toHaveBeenCalledTimes(1); - expect(integration2.setup).toHaveBeenCalledTimes(1); + expect(integration2.setup).toHaveBeenCalledTimes(0); expect(integration3.setup).toHaveBeenCalledTimes(1); - expect(integration4.setup).toHaveBeenCalledTimes(1); + expect(integration4.setup).toHaveBeenCalledTimes(0); expect(integration1.setup).toHaveBeenCalledWith(client1); - expect(integration2.setup).toHaveBeenCalledWith(client1); expect(integration3.setup).toHaveBeenCalledWith(client2); - expect(integration4.setup).toHaveBeenCalledWith(client2); }); it('binds preprocessEvent for each client', () => { @@ -432,18 +432,20 @@ describe('setupIntegration', () => { const client1 = getTestClient(); const client2 = getTestClient(); - const integrationIndex = {}; + const integrationIndex1 = {}; + const integrationIndex2 = {}; const integration1 = new CustomIntegration(); const integration2 = new CustomIntegration(); const integration3 = new CustomIntegration(); const integration4 = new CustomIntegration(); - setupIntegration(client1, integration1, integrationIndex); - setupIntegration(client1, integration2, integrationIndex); - setupIntegration(client2, integration3, integrationIndex); - setupIntegration(client2, integration4, integrationIndex); + setupIntegration(client1, integration1, integrationIndex1); + setupIntegration(client1, integration2, integrationIndex1); + setupIntegration(client2, integration3, integrationIndex2); + setupIntegration(client2, integration4, integrationIndex2); - expect(integrationIndex).toEqual({ test: integration4 }); + expect(integrationIndex1).toEqual({ test: integration1 }); + expect(integrationIndex2).toEqual({ test: integration3 }); expect(integration1.setupOnce).toHaveBeenCalledTimes(1); expect(integration2.setupOnce).not.toHaveBeenCalled(); expect(integration3.setupOnce).not.toHaveBeenCalled(); @@ -456,14 +458,12 @@ describe('setupIntegration', () => { client2.captureEvent({ event_id: '2c' }); expect(integration1.preprocessEvent).toHaveBeenCalledTimes(2); - expect(integration2.preprocessEvent).toHaveBeenCalledTimes(2); + expect(integration2.preprocessEvent).toHaveBeenCalledTimes(0); expect(integration3.preprocessEvent).toHaveBeenCalledTimes(3); - expect(integration4.preprocessEvent).toHaveBeenCalledTimes(3); + expect(integration4.preprocessEvent).toHaveBeenCalledTimes(0); expect(integration1.preprocessEvent).toHaveBeenLastCalledWith({ event_id: '1b' }, {}, client1); - expect(integration2.preprocessEvent).toHaveBeenLastCalledWith({ event_id: '1b' }, {}, client1); expect(integration3.preprocessEvent).toHaveBeenLastCalledWith({ event_id: '2c' }, {}, client2); - expect(integration4.preprocessEvent).toHaveBeenLastCalledWith({ event_id: '2c' }, {}, client2); }); it('allows to mutate events in preprocessEvent', async () => { @@ -504,18 +504,20 @@ describe('setupIntegration', () => { const client1 = getTestClient(); const client2 = getTestClient(); - const integrationIndex = {}; + const integrationIndex1 = {}; + const integrationIndex2 = {}; const integration1 = new CustomIntegration(); const integration2 = new CustomIntegration(); const integration3 = new CustomIntegration(); const integration4 = new CustomIntegration(); - setupIntegration(client1, integration1, integrationIndex); - setupIntegration(client1, integration2, integrationIndex); - setupIntegration(client2, integration3, integrationIndex); - setupIntegration(client2, integration4, integrationIndex); + setupIntegration(client1, integration1, integrationIndex1); + setupIntegration(client1, integration2, integrationIndex1); + setupIntegration(client2, integration3, integrationIndex2); + setupIntegration(client2, integration4, integrationIndex2); - expect(integrationIndex).toEqual({ test: integration4 }); + expect(integrationIndex1).toEqual({ test: integration1 }); + expect(integrationIndex2).toEqual({ test: integration3 }); expect(integration1.setupOnce).toHaveBeenCalledTimes(1); expect(integration2.setupOnce).not.toHaveBeenCalled(); expect(integration3.setupOnce).not.toHaveBeenCalled(); @@ -528,30 +530,20 @@ describe('setupIntegration', () => { client2.captureEvent({ event_id: '2c' }); expect(integration1.processEvent).toHaveBeenCalledTimes(2); - expect(integration2.processEvent).toHaveBeenCalledTimes(2); + expect(integration2.processEvent).toHaveBeenCalledTimes(0); expect(integration3.processEvent).toHaveBeenCalledTimes(3); - expect(integration4.processEvent).toHaveBeenCalledTimes(3); + expect(integration4.processEvent).toHaveBeenCalledTimes(0); expect(integration1.processEvent).toHaveBeenLastCalledWith( expect.objectContaining({ event_id: '1b' }), {}, client1, ); - expect(integration2.processEvent).toHaveBeenLastCalledWith( - expect.objectContaining({ event_id: '1b' }), - {}, - client1, - ); expect(integration3.processEvent).toHaveBeenLastCalledWith( expect.objectContaining({ event_id: '2c' }), {}, client2, ); - expect(integration4.processEvent).toHaveBeenLastCalledWith( - expect.objectContaining({ event_id: '2c' }), - {}, - client2, - ); }); it('allows to mutate events in processEvent', async () => { diff --git a/packages/core/test/lib/integrations/metadata.test.ts b/packages/core/test/lib/integrations/metadata.test.ts index 15678a66fdb6..7e8bfcea9fa4 100644 --- a/packages/core/test/lib/integrations/metadata.test.ts +++ b/packages/core/test/lib/integrations/metadata.test.ts @@ -61,6 +61,7 @@ describe('ModuleMetadata integration', () => { const client = new TestClient(options); const hub = getCurrentHub(); hub.bindClient(client); + // eslint-disable-next-line deprecation/deprecation hub.captureException(new Error('Some error')); }); }); diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts index 3122f3b3e3e5..87af429d0859 100644 --- a/packages/core/test/lib/scope.test.ts +++ b/packages/core/test/lib/scope.test.ts @@ -1,4 +1,4 @@ -import type { Attachment, Breadcrumb, Client } from '@sentry/types'; +import type { Attachment, Breadcrumb, Client, Event } from '@sentry/types'; import { applyScopeDataToEvent } from '../../src'; import { Scope, getGlobalScope, setGlobalScope } from '../../src/scope'; @@ -212,4 +212,256 @@ describe('Scope', () => { expect(clonedScope.getClient()).toBe(fakeClient); }); }); + + describe('.captureException()', () => { + it('should call captureException() on client with newly generated event ID if not explicitly passed in', () => { + const fakeCaptureException = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureException: fakeCaptureException, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + const exception = new Error(); + + scope.captureException(exception); + + expect(fakeCaptureException).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ event_id: expect.any(String) }), + scope, + ); + }); + + it('should return event ID when no client is on the scope', () => { + const scope = new Scope(); + + const exception = new Error(); + + const eventId = scope.captureException(exception); + + expect(eventId).toEqual(expect.any(String)); + }); + + it('should pass exception to captureException() on client', () => { + const fakeCaptureException = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureException: fakeCaptureException, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + const exception = new Error(); + + scope.captureException(exception); + + expect(fakeCaptureException).toHaveBeenCalledWith(exception, expect.anything(), scope); + }); + + it('should call captureException() on client with a synthetic exception', () => { + const fakeCaptureException = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureException: fakeCaptureException, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureException(new Error()); + + expect(fakeCaptureException).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ syntheticException: expect.any(Error) }), + scope, + ); + }); + + it('should pass the original exception to captureException() on client', () => { + const fakeCaptureException = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureException: fakeCaptureException, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + const exception = new Error(); + scope.captureException(exception); + + expect(fakeCaptureException).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ originalException: exception }), + scope, + ); + }); + + it('should forward hint to captureException() on client', () => { + const fakeCaptureException = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureException: fakeCaptureException, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureException(new Error(), { event_id: 'asdf', data: { foo: 'bar' } }); + + expect(fakeCaptureException).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ event_id: 'asdf', data: { foo: 'bar' } }), + scope, + ); + }); + }); + + describe('.captureMessage()', () => { + it('should call captureMessage() on client with newly generated event ID if not explicitly passed in', () => { + const fakeCaptureMessage = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureMessage: fakeCaptureMessage, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureMessage('foo'); + + expect(fakeCaptureMessage).toHaveBeenCalledWith( + expect.anything(), + undefined, + expect.objectContaining({ event_id: expect.any(String) }), + scope, + ); + }); + + it('should return event ID when no client is on the scope', () => { + const scope = new Scope(); + + const eventId = scope.captureMessage('foo'); + + expect(eventId).toEqual(expect.any(String)); + }); + + it('should pass exception to captureMessage() on client', () => { + const fakeCaptureMessage = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureMessage: fakeCaptureMessage, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureMessage('bar'); + + expect(fakeCaptureMessage).toHaveBeenCalledWith('bar', undefined, expect.anything(), scope); + }); + + it('should call captureMessage() on client with a synthetic exception', () => { + const fakeCaptureMessage = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureMessage: fakeCaptureMessage, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureMessage('foo'); + + expect(fakeCaptureMessage).toHaveBeenCalledWith( + expect.anything(), + undefined, + expect.objectContaining({ syntheticException: expect.any(Error) }), + scope, + ); + }); + + it('should pass the original exception to captureMessage() on client', () => { + const fakeCaptureMessage = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureMessage: fakeCaptureMessage, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureMessage('baz'); + + expect(fakeCaptureMessage).toHaveBeenCalledWith( + expect.anything(), + undefined, + expect.objectContaining({ originalException: 'baz' }), + scope, + ); + }); + + it('should forward level and hint to captureMessage() on client', () => { + const fakeCaptureMessage = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureMessage: fakeCaptureMessage, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureMessage('asdf', 'fatal', { event_id: 'asdf', data: { foo: 'bar' } }); + + expect(fakeCaptureMessage).toHaveBeenCalledWith( + expect.anything(), + 'fatal', + expect.objectContaining({ event_id: 'asdf', data: { foo: 'bar' } }), + scope, + ); + }); + }); + + describe('.captureEvent()', () => { + it('should call captureEvent() on client with newly generated event ID if not explicitly passed in', () => { + const fakeCaptureEvent = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureEvent: fakeCaptureEvent, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureEvent({}); + + expect(fakeCaptureEvent).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ event_id: expect.any(String) }), + scope, + ); + }); + + it('should return event ID when no client is on the scope', () => { + const scope = new Scope(); + + const eventId = scope.captureEvent({}); + + expect(eventId).toEqual(expect.any(String)); + }); + + it('should pass event to captureEvent() on client', () => { + const fakeCaptureEvent = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureEvent: fakeCaptureEvent, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + const event: Event = { event_id: 'asdf' }; + + scope.captureEvent(event); + + expect(fakeCaptureEvent).toHaveBeenCalledWith(event, expect.anything(), scope); + }); + + it('should forward hint to captureEvent() on client', () => { + const fakeCaptureEvent = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureEvent: fakeCaptureEvent, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureEvent({}, { event_id: 'asdf', data: { foo: 'bar' } }); + + expect(fakeCaptureEvent).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ event_id: 'asdf', data: { foo: 'bar' } }), + scope, + ); + }); + }); }); diff --git a/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts b/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts new file mode 100644 index 000000000000..da8bf1595e21 --- /dev/null +++ b/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts @@ -0,0 +1,128 @@ +import type { TransactionSource } from '@sentry/types'; +import { Hub, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, makeMain } from '../../../src'; +import { Transaction, getDynamicSamplingContextFromSpan, startInactiveSpan } from '../../../src/tracing'; +import { addTracingExtensions } from '../../../src/tracing'; +import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; + +describe('getDynamicSamplingContextFromSpan', () => { + let hub: Hub; + beforeEach(() => { + const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0, release: '1.0.1' }); + const client = new TestClient(options); + hub = new Hub(client); + hub.bindClient(client); + makeMain(hub); + addTracingExtensions(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('returns the DSC provided during transaction creation', () => { + // eslint-disable-next-line deprecation/deprecation + const transaction = new Transaction({ + name: 'tx', + metadata: { dynamicSamplingContext: { environment: 'myEnv' } }, + }); + + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction); + + expect(dynamicSamplingContext).toStrictEqual({ environment: 'myEnv' }); + }); + + test('returns a new DSC, if no DSC was provided during transaction creation (via attributes)', () => { + const transaction = startInactiveSpan({ name: 'tx' }); + + // Setting the attribute should overwrite the computed values + transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, 0.56); + transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction!); + + expect(dynamicSamplingContext).toStrictEqual({ + release: '1.0.1', + environment: 'production', + sampled: 'true', + sample_rate: '0.56', + trace_id: expect.any(String), + transaction: 'tx', + }); + }); + + test('returns a new DSC, if no DSC was provided during transaction creation (via deprecated metadata)', () => { + const transaction = startInactiveSpan({ + name: 'tx', + }); + + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction!); + + expect(dynamicSamplingContext).toStrictEqual({ + release: '1.0.1', + environment: 'production', + sampled: 'true', + sample_rate: '1', + trace_id: expect.any(String), + transaction: 'tx', + }); + }); + + test('returns a new DSC, if no DSC was provided during transaction creation (via new Txn and deprecated metadata)', () => { + // eslint-disable-next-line deprecation/deprecation + const transaction = new Transaction({ + name: 'tx', + metadata: { + sampleRate: 0.56, + source: 'route', + }, + sampled: true, + }); + + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction!); + + expect(dynamicSamplingContext).toStrictEqual({ + release: '1.0.1', + environment: 'production', + sampled: 'true', + sample_rate: '0.56', + trace_id: expect.any(String), + transaction: 'tx', + }); + }); + + describe('Including transaction name in DSC', () => { + test('is not included if transaction source is url', () => { + // eslint-disable-next-line deprecation/deprecation + const transaction = new Transaction({ + name: 'tx', + metadata: { + source: 'url', + sampleRate: 0.56, + }, + }); + + const dsc = getDynamicSamplingContextFromSpan(transaction); + expect(dsc.transaction).toBeUndefined(); + }); + + test.each([ + ['is included if transaction source is parameterized route/url', 'route'], + ['is included if transaction source is a custom name', 'custom'], + ])('%s', (_: string, source) => { + // eslint-disable-next-line deprecation/deprecation + const transaction = new Transaction({ + name: 'tx', + metadata: { + ...(source && { source: source as TransactionSource }), + }, + }); + + // Only setting the attribute manually because we're directly calling new Transaction() + transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); + + const dsc = getDynamicSamplingContextFromSpan(transaction); + + expect(dsc.transaction).toEqual('tx'); + }); + }); +}); diff --git a/packages/core/test/lib/tracing/errors.test.ts b/packages/core/test/lib/tracing/errors.test.ts index 60b5db5c0c1d..d2714901d39e 100644 --- a/packages/core/test/lib/tracing/errors.test.ts +++ b/packages/core/test/lib/tracing/errors.test.ts @@ -1,5 +1,5 @@ import { BrowserClient } from '@sentry/browser'; -import { Hub, addTracingExtensions, makeMain } from '@sentry/core'; +import { Hub, addTracingExtensions, makeMain, startInactiveSpan, startSpan } from '@sentry/core'; import type { HandlerDataError, HandlerDataUnhandledRejection } from '@sentry/types'; import { getDefaultBrowserClientOptions } from '../../../../tracing/test/testutils'; @@ -30,19 +30,14 @@ beforeAll(() => { }); describe('registerErrorHandlers()', () => { - let hub: Hub; beforeEach(() => { mockAddGlobalErrorInstrumentationHandler.mockClear(); mockAddGlobalUnhandledRejectionInstrumentationHandler.mockClear(); - const options = getDefaultBrowserClientOptions(); - hub = new Hub(new BrowserClient(options)); + const options = getDefaultBrowserClientOptions({ enableTracing: true }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); }); - afterEach(() => { - hub.getScope().setSpan(undefined); - }); - it('registers error instrumentation', () => { registerErrorInstrumentation(); expect(mockAddGlobalErrorInstrumentationHandler).toHaveBeenCalledTimes(1); @@ -53,7 +48,8 @@ describe('registerErrorHandlers()', () => { it('does not set status if transaction is not on scope', () => { registerErrorInstrumentation(); - const transaction = hub.startTransaction({ name: 'test' }); + + const transaction = startInactiveSpan({ name: 'test' })!; expect(transaction.status).toBe(undefined); mockErrorCallback({} as HandlerDataError); @@ -66,22 +62,19 @@ describe('registerErrorHandlers()', () => { it('sets status for transaction on scope on error', () => { registerErrorInstrumentation(); - const transaction = hub.startTransaction({ name: 'test' }); - hub.getScope().setSpan(transaction); - - mockErrorCallback({} as HandlerDataError); - expect(transaction.status).toBe('internal_error'); - transaction.end(); + startSpan({ name: 'test' }, span => { + mockErrorCallback({} as HandlerDataError); + expect(span?.status).toBe('internal_error'); + }); }); it('sets status for transaction on scope on unhandledrejection', () => { registerErrorInstrumentation(); - const transaction = hub.startTransaction({ name: 'test' }); - hub.getScope().setSpan(transaction); - mockUnhandledRejectionCallback({}); - expect(transaction.status).toBe('internal_error'); - transaction.end(); + startSpan({ name: 'test' }, span => { + mockUnhandledRejectionCallback({}); + expect(span?.status).toBe('internal_error'); + }); }); }); diff --git a/packages/core/test/lib/tracing/span.test.ts b/packages/core/test/lib/tracing/span.test.ts index c0b13df647f6..1adff93123ac 100644 --- a/packages/core/test/lib/tracing/span.test.ts +++ b/packages/core/test/lib/tracing/span.test.ts @@ -1,57 +1,63 @@ +import { timestampInSeconds } from '@sentry/utils'; import { Span } from '../../../src'; +import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '../../../src/utils/spanUtils'; describe('span', () => { - it('works with name', () => { - const span = new Span({ name: 'span name' }); - expect(span.name).toEqual('span name'); - expect(span.description).toEqual('span name'); - }); + describe('name', () => { + /* eslint-disable deprecation/deprecation */ + it('works with name', () => { + const span = new Span({ name: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); + }); - it('works with description', () => { - const span = new Span({ description: 'span name' }); - expect(span.name).toEqual('span name'); - expect(span.description).toEqual('span name'); - }); + it('works with description', () => { + const span = new Span({ description: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); + }); - it('works without name', () => { - const span = new Span({}); - expect(span.name).toEqual(''); - expect(span.description).toEqual(undefined); - }); + it('works without name', () => { + const span = new Span({}); + expect(span.name).toEqual(''); + expect(span.description).toEqual(undefined); + }); - it('allows to update the name via setter', () => { - const span = new Span({ name: 'span name' }); - expect(span.name).toEqual('span name'); - expect(span.description).toEqual('span name'); + it('allows to update the name via setter', () => { + const span = new Span({ name: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); - span.name = 'new name'; + span.name = 'new name'; - expect(span.name).toEqual('new name'); - expect(span.description).toEqual('new name'); - }); + expect(span.name).toEqual('new name'); + expect(span.description).toEqual('new name'); + }); - it('allows to update the name via setName', () => { - const span = new Span({ name: 'span name' }); - expect(span.name).toEqual('span name'); - expect(span.description).toEqual('span name'); + it('allows to update the name via setName', () => { + const span = new Span({ name: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); - // eslint-disable-next-line deprecation/deprecation - span.setName('new name'); + // eslint-disable-next-line deprecation/deprecation + span.setName('new name'); - expect(span.name).toEqual('new name'); - expect(span.description).toEqual('new name'); - }); + expect(span.name).toEqual('new name'); + expect(span.description).toEqual('new name'); + }); - it('allows to update the name via updateName', () => { - const span = new Span({ name: 'span name' }); - expect(span.name).toEqual('span name'); - expect(span.description).toEqual('span name'); + it('allows to update the name via updateName', () => { + const span = new Span({ name: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); - span.updateName('new name'); + span.updateName('new name'); - expect(span.name).toEqual('new name'); - expect(span.description).toEqual('new name'); + expect(span.name).toEqual('new name'); + expect(span.description).toEqual('new name'); + }); }); + /* eslint-enable deprecation/deprecation */ describe('setAttribute', () => { it('allows to set attributes', () => { @@ -68,7 +74,7 @@ describe('span', () => { span.setAttribute('boolArray', [true, false]); span.setAttribute('arrayWithUndefined', [1, undefined, 2]); - expect(span.attributes).toEqual({ + expect(span['_attributes']).toEqual({ str: 'bar', num: 1, zero: 0, @@ -86,11 +92,11 @@ describe('span', () => { span.setAttribute('str', 'bar'); - expect(Object.keys(span.attributes).length).toEqual(1); + expect(Object.keys(span['_attributes']).length).toEqual(1); span.setAttribute('str', undefined); - expect(Object.keys(span.attributes).length).toEqual(0); + expect(Object.keys(span['_attributes']).length).toEqual(0); }); it('disallows invalid attribute types', () => { @@ -111,7 +117,7 @@ describe('span', () => { it('allows to set attributes', () => { const span = new Span(); - const initialAttributes = span.attributes; + const initialAttributes = span['_attributes']; expect(initialAttributes).toEqual({}); @@ -129,7 +135,7 @@ describe('span', () => { }; span.setAttributes(newAttributes); - expect(span.attributes).toEqual({ + expect(span['_attributes']).toEqual({ str: 'bar', num: 1, zero: 0, @@ -141,14 +147,14 @@ describe('span', () => { arrayWithUndefined: [1, undefined, 2], }); - expect(span.attributes).not.toBe(newAttributes); + expect(span['_attributes']).not.toBe(newAttributes); span.setAttributes({ num: 2, numArray: [3, 4], }); - expect(span.attributes).toEqual({ + expect(span['_attributes']).toEqual({ str: 'bar', num: 2, zero: 0, @@ -166,11 +172,91 @@ describe('span', () => { span.setAttribute('str', 'bar'); - expect(Object.keys(span.attributes).length).toEqual(1); + expect(Object.keys(span['_attributes']).length).toEqual(1); span.setAttributes({ str: undefined }); - expect(Object.keys(span.attributes).length).toEqual(0); + expect(Object.keys(span['_attributes']).length).toEqual(0); + }); + }); + + describe('end', () => { + it('works without endTimestamp', () => { + const span = new Span(); + const now = timestampInSeconds(); + span.end(); + + expect(span.endTimestamp).toBeGreaterThanOrEqual(now); + }); + + it('works with endTimestamp in seconds', () => { + const span = new Span(); + const timestamp = timestampInSeconds() - 1; + span.end(timestamp); + + expect(span.endTimestamp).toEqual(timestamp); + }); + + it('works with endTimestamp in milliseconds', () => { + const span = new Span(); + const timestamp = Date.now() - 1000; + span.end(timestamp); + + expect(span.endTimestamp).toEqual(timestamp / 1000); + }); + + it('works with endTimestamp in array form', () => { + const span = new Span(); + const seconds = Math.floor(timestampInSeconds() - 1); + span.end([seconds, 0]); + + expect(span.endTimestamp).toEqual(seconds); + }); + }); + + describe('isRecording', () => { + it('returns true for sampled span', () => { + const span = new Span({ sampled: true }); + expect(span.isRecording()).toEqual(true); + }); + + it('returns false for sampled, finished span', () => { + const span = new Span({ sampled: true, endTimestamp: Date.now() }); + expect(span.isRecording()).toEqual(false); + }); + + it('returns false for unsampled span', () => { + const span = new Span({ sampled: false }); + expect(span.isRecording()).toEqual(false); + }); + }); + + describe('spanContext', () => { + it('works with default span', () => { + const span = new Span(); + expect(span.spanContext()).toEqual({ + spanId: span['_spanId'], + traceId: span['_traceId'], + traceFlags: TRACE_FLAG_NONE, + }); + }); + + it('works sampled span', () => { + const span = new Span({ sampled: true }); + expect(span.spanContext()).toEqual({ + spanId: span['_spanId'], + traceId: span['_traceId'], + traceFlags: TRACE_FLAG_SAMPLED, + }); + }); + + it('works unsampled span', () => { + const span = new Span({ sampled: false }); + expect(span.spanContext()).toEqual({ + spanId: span['_spanId'], + traceId: span['_traceId'], + traceFlags: TRACE_FLAG_NONE, + }); }); }); @@ -184,9 +270,11 @@ describe('span', () => { it('works with data only', () => { const span = new Span(); + // eslint-disable-next-line deprecation/deprecation span.setData('foo', 'bar'); expect(span['_getData']()).toEqual({ foo: 'bar' }); + // eslint-disable-next-line deprecation/deprecation expect(span['_getData']()).toBe(span.data); }); @@ -195,6 +283,7 @@ describe('span', () => { span.setAttribute('foo', 'bar'); expect(span['_getData']()).toEqual({ foo: 'bar' }); + // eslint-disable-next-line deprecation/deprecation expect(span['_getData']()).toBe(span.attributes); }); @@ -202,11 +291,15 @@ describe('span', () => { const span = new Span(); span.setAttribute('foo', 'foo'); span.setAttribute('bar', 'bar'); + // eslint-disable-next-line deprecation/deprecation span.setData('foo', 'foo2'); + // eslint-disable-next-line deprecation/deprecation span.setData('baz', 'baz'); expect(span['_getData']()).toEqual({ foo: 'foo', bar: 'bar', baz: 'baz' }); + // eslint-disable-next-line deprecation/deprecation expect(span['_getData']()).not.toBe(span.attributes); + // eslint-disable-next-line deprecation/deprecation expect(span['_getData']()).not.toBe(span.data); }); }); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 30eac02c881f..d2a9ea8034d9 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1,6 +1,13 @@ -import type { Span } from '@sentry/types'; import { Hub, addTracingExtensions, getCurrentScope, makeMain } from '../../../src'; -import { continueTrace, startInactiveSpan, startSpan, startSpanManual } from '../../../src/tracing'; +import { Scope } from '../../../src/scope'; +import { + Span, + continueTrace, + getActiveSpan, + startInactiveSpan, + startSpan, + startSpanManual, +} from '../../../src/tracing'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; beforeAll(() => { @@ -81,18 +88,6 @@ describe('startSpan', () => { expect(ref.status).toEqual(isError ? 'internal_error' : undefined); }); - it('creates & finishes span', async () => { - let _span: Span | undefined; - startSpan({ name: 'GET users/[id]' }, span => { - expect(span).toBeDefined(); - expect(span?.endTimestamp).toBeUndefined(); - _span = span; - }); - - expect(_span).toBeDefined(); - expect(_span?.endTimestamp).toBeDefined(); - }); - it('allows traceparent information to be overriden', async () => { let ref: any = undefined; client.on('finishTransaction', transaction => { @@ -181,18 +176,58 @@ describe('startSpan', () => { expect(ref.spanRecorder.spans).toHaveLength(2); expect(ref.spanRecorder.spans[1].op).toEqual('db.query'); }); + }); - it('forks the scope', () => { - const initialScope = getCurrentScope(); + it('creates & finishes span', async () => { + let _span: Span | undefined; + startSpan({ name: 'GET users/[id]' }, span => { + expect(span).toBeDefined(); + expect(span?.endTimestamp).toBeUndefined(); + _span = span as Span; + }); - startSpan({ name: 'GET users/[id]' }, span => { - expect(getCurrentScope()).not.toBe(initialScope); - expect(getCurrentScope().getSpan()).toBe(span); - }); + expect(_span).toBeDefined(); + expect(_span?.endTimestamp).toBeDefined(); + }); + + it('allows to pass a `startTime`', () => { + const start = startSpan({ name: 'outer', startTime: [1234, 0] }, span => { + return span?.startTimestamp; + }); + + expect(start).toEqual(1234); + }); - expect(getCurrentScope()).toBe(initialScope); - expect(initialScope.getSpan()).toBe(undefined); + it('forks the scope', () => { + const initialScope = getCurrentScope(); + + startSpan({ name: 'GET users/[id]' }, span => { + expect(getCurrentScope()).not.toBe(initialScope); + expect(getActiveSpan()).toBe(span); }); + + expect(getCurrentScope()).toBe(initialScope); + expect(getActiveSpan()).toBe(undefined); + }); + + it('allows to pass a scope', () => { + const initialScope = getCurrentScope(); + + const manualScope = new Scope(); + const parentSpan = new Span({ spanId: 'parent-span-id' }); + // eslint-disable-next-line deprecation/deprecation + manualScope.setSpan(parentSpan); + + startSpan({ name: 'GET users/[id]', scope: manualScope }, span => { + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope()).toBe(manualScope); + expect(getActiveSpan()).toBe(span); + + expect(span?.parentSpanId).toBe('parent-span-id'); + }); + + expect(getCurrentScope()).toBe(initialScope); + expect(getActiveSpan()).toBe(undefined); }); }); @@ -211,16 +246,49 @@ describe('startSpanManual', () => { startSpanManual({ name: 'GET users/[id]' }, (span, finish) => { expect(getCurrentScope()).not.toBe(initialScope); - expect(getCurrentScope().getSpan()).toBe(span); + expect(getActiveSpan()).toBe(span); finish(); // Is still the active span - expect(getCurrentScope().getSpan()).toBe(span); + expect(getActiveSpan()).toBe(span); }); expect(getCurrentScope()).toBe(initialScope); - expect(initialScope.getSpan()).toBe(undefined); + expect(getActiveSpan()).toBe(undefined); + }); + + it('allows to pass a scope', () => { + const initialScope = getCurrentScope(); + + const manualScope = new Scope(); + const parentSpan = new Span({ spanId: 'parent-span-id' }); + // eslint-disable-next-line deprecation/deprecation + manualScope.setSpan(parentSpan); + + startSpanManual({ name: 'GET users/[id]', scope: manualScope }, (span, finish) => { + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope()).toBe(manualScope); + expect(getActiveSpan()).toBe(span); + expect(span?.parentSpanId).toBe('parent-span-id'); + + finish(); + + // Is still the active span + expect(getActiveSpan()).toBe(span); + }); + + expect(getCurrentScope()).toBe(initialScope); + expect(getActiveSpan()).toBe(undefined); + }); + + it('allows to pass a `startTime`', () => { + const start = startSpanManual({ name: 'outer', startTime: [1234, 0] }, span => { + span?.end(); + return span?.startTimestamp; + }); + + expect(start).toEqual(1234); }); }); @@ -237,16 +305,36 @@ describe('startInactiveSpan', () => { }); it('does not set span on scope', () => { - const initialScope = getCurrentScope(); - const span = startInactiveSpan({ name: 'GET users/[id]' }); expect(span).toBeDefined(); - expect(initialScope.getSpan()).toBeUndefined(); + expect(getActiveSpan()).toBeUndefined(); + + span?.end(); + + expect(getActiveSpan()).toBeUndefined(); + }); + + it('allows to pass a scope', () => { + const manualScope = new Scope(); + const parentSpan = new Span({ spanId: 'parent-span-id' }); + // eslint-disable-next-line deprecation/deprecation + manualScope.setSpan(parentSpan); + + const span = startInactiveSpan({ name: 'GET users/[id]', scope: manualScope }); + + expect(span).toBeDefined(); + expect(span?.parentSpanId).toBe('parent-span-id'); + expect(getActiveSpan()).toBeUndefined(); span?.end(); - expect(initialScope.getSpan()).toBeUndefined(); + expect(getActiveSpan()).toBeUndefined(); + }); + + it('allows to pass a `startTime`', () => { + const span = startInactiveSpan({ name: 'outer', startTime: [1234, 0] }); + expect(span?.startTimestamp).toEqual(1234); }); }); diff --git a/packages/core/test/lib/tracing/transaction.test.ts b/packages/core/test/lib/tracing/transaction.test.ts index 3be3d7dccfcc..415c0448f78e 100644 --- a/packages/core/test/lib/tracing/transaction.test.ts +++ b/packages/core/test/lib/tracing/transaction.test.ts @@ -1,44 +1,107 @@ -import { Transaction } from '../../../src'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, Transaction } from '../../../src'; describe('transaction', () => { - it('works with name', () => { - const transaction = new Transaction({ name: 'span name' }); - expect(transaction.name).toEqual('span name'); - }); + describe('name', () => { + /* eslint-disable deprecation/deprecation */ + it('works with name', () => { + const transaction = new Transaction({ name: 'span name' }); + expect(transaction.name).toEqual('span name'); + }); - it('allows to update the name via setter', () => { - const transaction = new Transaction({ name: 'span name' }); - transaction.setMetadata({ source: 'route' }); - expect(transaction.name).toEqual('span name'); + it('allows to update the name via setter', () => { + const transaction = new Transaction({ name: 'span name' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + expect(transaction.name).toEqual('span name'); - transaction.name = 'new name'; + transaction.name = 'new name'; - expect(transaction.name).toEqual('new name'); - expect(transaction.metadata.source).toEqual('custom'); - }); + expect(transaction.name).toEqual('new name'); + expect(transaction.metadata.source).toEqual('custom'); + }); + + it('allows to update the name via setName', () => { + const transaction = new Transaction({ name: 'span name' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + expect(transaction.name).toEqual('span name'); - it('allows to update the name via setName', () => { - const transaction = new Transaction({ name: 'span name' }); - transaction.setMetadata({ source: 'route' }); - expect(transaction.name).toEqual('span name'); + transaction.setName('new name'); - transaction.setMetadata({ source: 'route' }); + expect(transaction.name).toEqual('new name'); + expect(transaction.metadata.source).toEqual('custom'); + }); - // eslint-disable-next-line deprecation/deprecation - transaction.setName('new name'); + it('allows to update the name via updateName', () => { + const transaction = new Transaction({ name: 'span name' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + expect(transaction.name).toEqual('span name'); - expect(transaction.name).toEqual('new name'); - expect(transaction.metadata.source).toEqual('custom'); + transaction.updateName('new name'); + + expect(transaction.name).toEqual('new name'); + expect(transaction.metadata.source).toEqual('route'); + }); + /* eslint-enable deprecation/deprecation */ }); - it('allows to update the name via updateName', () => { - const transaction = new Transaction({ name: 'span name' }); - transaction.setMetadata({ source: 'route' }); - expect(transaction.name).toEqual('span name'); + describe('metadata', () => { + /* eslint-disable deprecation/deprecation */ + it('works with defaults', () => { + const transaction = new Transaction({ name: 'span name' }); + expect(transaction.metadata).toEqual({ + source: 'custom', + spanMetadata: {}, + }); + }); + + it('allows to set metadata in constructor', () => { + const transaction = new Transaction({ name: 'span name', metadata: { source: 'url', request: {} } }); + expect(transaction.metadata).toEqual({ + source: 'url', + spanMetadata: {}, + request: {}, + }); + }); + + it('allows to set source & sample rate data in constructor', () => { + const transaction = new Transaction({ + name: 'span name', + metadata: { request: {} }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 0.5, + }, + }); + expect(transaction.metadata).toEqual({ + source: 'url', + sampleRate: 0.5, + spanMetadata: {}, + request: {}, + }); + }); + + it('allows to update metadata via setMetadata', () => { + const transaction = new Transaction({ name: 'span name', metadata: { source: 'url', request: {} } }); + + transaction.setMetadata({ source: 'route' }); + + expect(transaction.metadata).toEqual({ + source: 'route', + spanMetadata: {}, + request: {}, + }); + }); + + it('allows to update metadata via setAttribute', () => { + const transaction = new Transaction({ name: 'span name', metadata: { source: 'url', request: {} } }); - transaction.updateName('new name'); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); - expect(transaction.name).toEqual('new name'); - expect(transaction.metadata.source).toEqual('route'); + expect(transaction.metadata).toEqual({ + source: 'route', + spanMetadata: {}, + request: {}, + }); + }); + /* eslint-enable deprecation/deprecation */ }); }); diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index c2ed4dd0d4cd..e521df7c2dc9 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -1,5 +1,6 @@ -import { TRACEPARENT_REGEXP } from '@sentry/utils'; +import { TRACEPARENT_REGEXP, timestampInSeconds } from '@sentry/utils'; import { Span, spanToTraceHeader } from '../../../src'; +import { spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../../../src/utils/spanUtils'; describe('spanToTraceHeader', () => { test('simple', () => { @@ -11,3 +12,118 @@ describe('spanToTraceHeader', () => { expect(spanToTraceHeader(span)).toMatch(TRACEPARENT_REGEXP); }); }); + +describe('spanTimeInputToSeconds', () => { + it('works with undefined', () => { + const now = timestampInSeconds(); + expect(spanTimeInputToSeconds(undefined)).toBeGreaterThanOrEqual(now); + }); + + it('works with a timestamp in seconds', () => { + const timestamp = timestampInSeconds(); + expect(spanTimeInputToSeconds(timestamp)).toEqual(timestamp); + }); + + it('works with a timestamp in milliseconds', () => { + const timestamp = Date.now(); + expect(spanTimeInputToSeconds(timestamp)).toEqual(timestamp / 1000); + }); + + it('works with a Date object', () => { + const timestamp = new Date(); + expect(spanTimeInputToSeconds(timestamp)).toEqual(timestamp.getTime() / 1000); + }); + + it('works with a simple array', () => { + const seconds = Math.floor(timestampInSeconds()); + const timestamp: [number, number] = [seconds, 0]; + expect(spanTimeInputToSeconds(timestamp)).toEqual(seconds); + }); + + it('works with a array with nanoseconds', () => { + const seconds = Math.floor(timestampInSeconds()); + const timestamp: [number, number] = [seconds, 9000]; + expect(spanTimeInputToSeconds(timestamp)).toEqual(seconds + 0.000009); + }); +}); + +describe('spanToJSON', () => { + it('works with a simple span', () => { + const span = new Span(); + expect(spanToJSON(span)).toEqual({ + span_id: span.spanContext().spanId, + trace_id: span.spanContext().traceId, + origin: 'manual', + start_timestamp: span.startTimestamp, + }); + }); + + it('works with a full span', () => { + const span = new Span({ + name: 'test name', + op: 'test op', + parentSpanId: '1234', + spanId: '5678', + status: 'ok', + tags: { + foo: 'bar', + }, + traceId: 'abcd', + origin: 'auto', + startTimestamp: 123, + }); + + expect(spanToJSON(span)).toEqual({ + description: 'test name', + op: 'test op', + parent_span_id: '1234', + span_id: '5678', + status: 'ok', + tags: { + foo: 'bar', + }, + trace_id: 'abcd', + origin: 'auto', + start_timestamp: 123, + }); + }); + + it('works with a custom class without spanToJSON', () => { + const span = { + toJSON: () => { + return { + span_id: 'span_id', + trace_id: 'trace_id', + origin: 'manual', + start_timestamp: 123, + }; + }, + } as unknown as Span; + + expect(spanToJSON(span)).toEqual({ + span_id: 'span_id', + trace_id: 'trace_id', + origin: 'manual', + start_timestamp: 123, + }); + }); + + it('returns empty object if span does not have getter methods', () => { + // eslint-disable-next-line + const span = new Span().toJSON(); + + expect(spanToJSON(span as unknown as Span)).toEqual({}); + }); +}); + +describe('spanIsSampled', () => { + test('sampled', () => { + const span = new Span({ sampled: true }); + expect(spanIsSampled(span)).toBe(true); + }); + + test('not sampled', () => { + const span = new Span({ sampled: false }); + expect(spanIsSampled(span)).toBe(false); + }); +}); diff --git a/packages/core/test/mocks/client.ts b/packages/core/test/mocks/client.ts index 3fe26f9d0bec..7cb1e08cecba 100644 --- a/packages/core/test/mocks/client.ts +++ b/packages/core/test/mocks/client.ts @@ -5,6 +5,7 @@ import type { EventHint, Integration, Outcome, + ParameterizedString, Session, Severity, SeverityLevel, @@ -76,7 +77,7 @@ export class TestClient extends BaseClient { } public eventFromMessage( - message: string, + message: ParameterizedString, // eslint-disable-next-line deprecation/deprecation level: Severity | SeverityLevel = 'info', ): PromiseLike { diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 2530e2f7bdc5..2658d6f31e36 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -38,6 +38,7 @@ export { extractTraceparentData, continueTrace, flush, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, @@ -51,6 +52,7 @@ export { makeMain, runWithAsyncContext, Scope, + // eslint-disable-next-line deprecation/deprecation startTransaction, SDK_VERSION, setContext, diff --git a/packages/deno/test/__snapshots__/mod.test.ts.snap b/packages/deno/test/__snapshots__/mod.test.ts.snap index f278e370312b..607d87b968bc 100644 --- a/packages/deno/test/__snapshots__/mod.test.ts.snap +++ b/packages/deno/test/__snapshots__/mod.test.ts.snap @@ -77,8 +77,8 @@ snapshot[`captureException 1`] = ` lineno: 526, }, { - colno: 24, - context_line: " hub.captureException(something());", + colno: 27, + context_line: " client.captureException(something());", filename: "app:///test/mod.test.ts", function: "", in_app: true, @@ -112,7 +112,7 @@ snapshot[`captureException 1`] = ` post_context: [ " }", "", - " hub.captureException(something());", + " client.captureException(something());", "", " await delay(200);", " await assertSnapshot(t, ev);", @@ -121,7 +121,7 @@ snapshot[`captureException 1`] = ` pre_context: [ "Deno.test('captureException', async t => {", " let ev: sentryTypes.Event | undefined;", - " const [hub] = getTestClient(event => {", + " const [, client] = getTestClient(event => {", " ev = event;", " });", "", diff --git a/packages/deno/test/mod.test.ts b/packages/deno/test/mod.test.ts index a14e04b61ff9..1568584d8281 100644 --- a/packages/deno/test/mod.test.ts +++ b/packages/deno/test/mod.test.ts @@ -35,7 +35,7 @@ function delay(time: number): Promise { Deno.test('captureException', async t => { let ev: sentryTypes.Event | undefined; - const [hub] = getTestClient(event => { + const [, client] = getTestClient(event => { ev = event; }); @@ -43,7 +43,7 @@ Deno.test('captureException', async t => { return new Error('Some unhandled error'); } - hub.captureException(something()); + client.captureException(something()); await delay(200); await assertSnapshot(t, ev); @@ -51,11 +51,11 @@ Deno.test('captureException', async t => { Deno.test('captureMessage', async t => { let ev: sentryTypes.Event | undefined; - const [hub] = getTestClient(event => { + const [, client] = getTestClient(event => { ev = event; }); - hub.captureMessage('Some error message'); + client.captureMessage('Some error message'); await delay(200); await assertSnapshot(t, ev); diff --git a/packages/ember/addon/index.ts b/packages/ember/addon/index.ts index 2a5da643c984..57429c64f789 100644 --- a/packages/ember/addon/index.ts +++ b/packages/ember/addon/index.ts @@ -2,13 +2,14 @@ import { assert, warn } from '@ember/debug'; import type Route from '@ember/routing/route'; import { next } from '@ember/runloop'; import { getOwnConfig, isDevelopingApp, macroCondition } from '@embroider/macros'; +import { startSpan } from '@sentry/browser'; import type { BrowserOptions } from '@sentry/browser'; import * as Sentry from '@sentry/browser'; import { SDK_VERSION } from '@sentry/browser'; -import type { Transaction } from '@sentry/types'; -import { GLOBAL_OBJ, timestampInSeconds } from '@sentry/utils'; +import { GLOBAL_OBJ } from '@sentry/utils'; import Ember from 'ember'; +import type { Transaction } from '@sentry/types'; import type { EmberSentryConfig, GlobalConfig, OwnConfig } from './types'; function _getSentryInitConfig(): EmberSentryConfig['sentry'] { @@ -66,7 +67,13 @@ export function InitSentryForEmber(_runtimeConfig?: BrowserOptions): void { } } +/** + * Grabs active transaction off scope. + * + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. + */ export const getActiveTransaction = (): Transaction | undefined => { + // eslint-disable-next-line deprecation/deprecation return Sentry.getCurrentHub().getScope().getTransaction(); }; @@ -80,22 +87,16 @@ export const instrumentRoutePerformance = (BaseRoute fn: X, args: Parameters, ): Promise> => { - const startTimestamp = timestampInSeconds(); - const result = await fn(...args); - - const currentTransaction = getActiveTransaction(); - if (!currentTransaction) { - return result; - } - currentTransaction - .startChild({ + return startSpan( + { op, - description, + name: description, origin: 'auto.ui.ember', - startTimestamp, - }) - .end(); - return result; + }, + () => { + return fn(...args); + }, + ); }; const routeName = BaseRoute.name; diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index 41d10842fa7c..999474b798bc 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -12,7 +12,7 @@ import type { Span, Transaction } from '@sentry/types'; import { GLOBAL_OBJ, browserPerformanceTimeOrigin, timestampInSeconds } from '@sentry/utils'; import type { BrowserClient } from '..'; -import { getActiveTransaction } from '..'; +import { getActiveSpan, startInactiveSpan } from '..'; import type { EmberRouterMain, EmberSentryConfig, GlobalConfig, OwnConfig, StartTransactionFunction } from '../types'; type SentryTestRouterService = RouterService & { @@ -149,9 +149,9 @@ export function _instrumentEmberRouter( 'routing.instrumentation': '@sentry/ember', }, }); - transitionSpan = activeTransaction?.startChild({ + transitionSpan = startInactiveSpan({ op: 'ui.ember.transition', - description: `route:${fromRoute} -> route:${toRoute}`, + name: `route:${fromRoute} -> route:${toRoute}`, origin: 'auto.ui.ember', }); }); @@ -195,8 +195,8 @@ function _instrumentEmberRunloop(config: EmberSentryConfig): void { if (previousInstance) { return; } - const activeTransaction = getActiveTransaction(); - if (!activeTransaction) { + const activeSpan = getActiveSpan(); + if (!activeSpan) { return; } if (currentQueueSpan) { @@ -211,22 +211,20 @@ function _instrumentEmberRunloop(config: EmberSentryConfig): void { const minQueueDuration = minimumRunloopQueueDuration ?? 5; if ((now - currentQueueStart) * 1000 >= minQueueDuration) { - activeTransaction - ?.startChild({ - op: `ui.ember.runloop.${queue}`, - origin: 'auto.ui.ember', - startTimestamp: currentQueueStart, - endTimestamp: now, - }) - .end(); + startInactiveSpan({ + name: 'runloop', + op: `ui.ember.runloop.${queue}`, + origin: 'auto.ui.ember', + startTimestamp: currentQueueStart, + })?.end(now); } currentQueueStart = undefined; } // Setup for next queue - const stillActiveTransaction = getActiveTransaction(); - if (!stillActiveTransaction) { + const stillActiveSpan = getActiveSpan(); + if (!stillActiveSpan) { return; } currentQueueStart = timestampInSeconds(); @@ -286,15 +284,12 @@ function processComponentRenderAfter( const componentRenderDuration = now - begin.now; if (componentRenderDuration * 1000 >= minComponentDuration) { - const activeTransaction = getActiveTransaction(); - - activeTransaction?.startChild({ + startInactiveSpan({ + name: payload.containerKey || payload.object, op, - description: payload.containerKey || payload.object, origin: 'auto.ui.ember', startTimestamp: begin.now, - endTimestamp: now, - }); + })?.end(now); } } @@ -372,13 +367,12 @@ function _instrumentInitialLoad(config: EmberSentryConfig): void { const startTimestamp = (measure.startTime + browserPerformanceTimeOrigin) / 1000; const endTimestamp = startTimestamp + measure.duration / 1000; - const transaction = getActiveTransaction(); - const span = transaction?.startChild({ + startInactiveSpan({ op: 'ui.ember.init', + name: 'init', origin: 'auto.ui.ember', startTimestamp, - }); - span?.end(endTimestamp); + })?.end(endTimestamp); performance.clearMarks(startName); performance.clearMarks(endName); diff --git a/packages/ember/package.json b/packages/ember/package.json index d99c0a176270..77b4bcd89e2e 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -33,6 +33,7 @@ "dependencies": { "@embroider/macros": "^1.9.0", "@sentry/browser": "7.92.0", + "@sentry/core": "7.92.0", "@sentry/types": "7.92.0", "@sentry/utils": "7.92.0", "ember-auto-import": "^1.12.1 || ^2.4.3", diff --git a/packages/ember/tests/helpers/utils.ts b/packages/ember/tests/helpers/utils.ts index 3ec336cfa59c..99109074219e 100644 --- a/packages/ember/tests/helpers/utils.ts +++ b/packages/ember/tests/helpers/utils.ts @@ -1,3 +1,4 @@ +import { spanToJSON } from '@sentry/core'; import type { Event } from '@sentry/types'; const defaultAssertOptions = { @@ -67,7 +68,7 @@ export function assertSentryTransactions( const filteredSpans = spans .filter(span => !span.op?.startsWith('ui.ember.runloop.')) .map(s => { - return `${s.op} | ${s.description}`; + return `${s.op} | ${spanToJSON(s).description}`; }); assert.true( diff --git a/packages/hub/src/index.ts b/packages/hub/src/index.ts index 057d0e6a9975..579b5b932e7e 100644 --- a/packages/hub/src/index.ts +++ b/packages/hub/src/index.ts @@ -118,6 +118,7 @@ export const configureScope = configureScopeCore; /** * @deprecated This export has moved to @sentry/core. The @sentry/hub package will be removed in v8. */ +// eslint-disable-next-line deprecation/deprecation export const startTransaction = startTransactionCore; /** diff --git a/packages/hub/test/hub.test.ts b/packages/hub/test/hub.test.ts index fbdc3993b989..08ec6a22130a 100644 --- a/packages/hub/test/hub.test.ts +++ b/packages/hub/test/hub.test.ts @@ -3,6 +3,7 @@ import type { Client, Event, EventType } from '@sentry/types'; +import { getCurrentScope, makeMain } from '@sentry/core'; import { Hub, Scope, getCurrentHub } from '../src'; const clientFn: any = jest.fn(); @@ -18,6 +19,7 @@ function makeClient() { getIntegration: jest.fn(), setupIntegrations: jest.fn(), captureMessage: jest.fn(), + captureSession: jest.fn(), } as unknown as Client; } @@ -453,4 +455,102 @@ describe('Hub', () => { expect(hub.shouldSendDefaultPii()).toBe(true); }); }); + + describe('session APIs', () => { + beforeEach(() => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + }); + + describe('startSession', () => { + it('starts a session', () => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + const session = hub.startSession(); + + expect(session).toMatchObject({ + status: 'ok', + errors: 0, + init: true, + environment: 'production', + ignoreDuration: false, + sid: expect.any(String), + did: undefined, + timestamp: expect.any(Number), + started: expect.any(Number), + duration: expect.any(Number), + toJSON: expect.any(Function), + }); + }); + + it('ends a previously active session and removes it from the scope', () => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + + const session1 = hub.startSession(); + + expect(session1.status).toBe('ok'); + expect(getCurrentScope().getSession()).toBe(session1); + + const session2 = hub.startSession(); + + expect(session2.status).toBe('ok'); + expect(session1.status).toBe('exited'); + expect(getCurrentHub().getScope().getSession()).toBe(session2); + }); + }); + + describe('endSession', () => { + it('ends a session and removes it from the scope', () => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + + const session = hub.startSession(); + + expect(session.status).toBe('ok'); + expect(getCurrentScope().getSession()).toBe(session); + + hub.endSession(); + + expect(session.status).toBe('exited'); + expect(getCurrentHub().getScope().getSession()).toBe(undefined); + }); + }); + + describe('captureSession', () => { + it('captures a session without ending it by default', () => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + + const session = hub.startSession(); + + expect(session.status).toBe('ok'); + expect(getCurrentScope().getSession()).toBe(session); + + hub.captureSession(); + + expect(testClient.captureSession).toHaveBeenCalledWith(expect.objectContaining({ status: 'ok' })); + }); + + it('captures a session and ends it if end is `true`', () => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + + const session = hub.startSession(); + + expect(session.status).toBe('ok'); + expect(hub.getScope().getSession()).toBe(session); + + hub.captureSession(true); + + expect(testClient.captureSession).toHaveBeenCalledWith(expect.objectContaining({ status: 'exited' })); + }); + }); + }); }); diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts index 4cb8694b9cca..b8cfaf1914e0 100644 --- a/packages/hub/test/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -361,6 +361,7 @@ describe('Scope', () => { const scope = new Scope(); const span = { fake: 'span', + spanContext: () => ({}), toJSON: () => ({ origin: 'manual' }), } as any; scope.setSpan(span); @@ -374,6 +375,7 @@ describe('Scope', () => { const scope = new Scope(); const span = { fake: 'span', + spanContext: () => ({}), toJSON: () => ({ a: 'b' }), } as any; scope.setSpan(span); @@ -392,8 +394,8 @@ describe('Scope', () => { const scope = new Scope(); const transaction = { fake: 'span', - toJSON: () => ({ a: 'b' }), - name: 'fake transaction', + spanContext: () => ({}), + toJSON: () => ({ a: 'b', description: 'fake transaction' }), getDynamicSamplingContext: () => ({}), } as any; transaction.transaction = transaction; // because this is a transaction, its `transaction` pointer points to itself @@ -407,9 +409,14 @@ describe('Scope', () => { test('adds `transaction` tag when span on scope', async () => { expect.assertions(1); const scope = new Scope(); - const transaction = { name: 'fake transaction', getDynamicSamplingContext: () => ({}) }; + const transaction = { + spanContext: () => ({}), + toJSON: () => ({ description: 'fake transaction' }), + getDynamicSamplingContext: () => ({}), + }; const span = { fake: 'span', + spanContext: () => ({}), toJSON: () => ({ a: 'b' }), transaction, } as any; diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts index 1e31ffaaef0c..5f2064c690e4 100644 --- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts @@ -186,6 +186,7 @@ export function pagesRouterInstrumentation( // We don't want to finish the navigation transaction on `routeChangeComplete`, since users might want to attach // spans to that transaction even after `routeChangeComplete` is fired (eg. HTTP requests in some useEffect // hooks). Instead, we'll simply let the navigation transaction finish itself (it's an `IdleTransaction`). + // eslint-disable-next-line deprecation/deprecation const nextRouteChangeSpan = navigationTransaction.startChild({ op: 'ui.nextjs.route-change', origin: 'auto.ui.nextjs.pages_router_instrumentation', diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts index 9c479a88ceeb..59114ddee709 100644 --- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts +++ b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts @@ -1,4 +1,11 @@ -import { addTracingExtensions, captureException, continueTrace, handleCallbackErrors, startSpan } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + addTracingExtensions, + captureException, + continueTrace, + handleCallbackErrors, + startSpan, +} from '@sentry/core'; import { winterCGRequestToRequestData } from '@sentry/utils'; import type { EdgeRouteHandler } from '../../edge/types'; @@ -34,10 +41,11 @@ export function withEdgeWrapping( name: options.spanDescription, op: options.spanOp, origin: 'auto.function.nextjs.withEdgeWrapping', + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route' }, metadata: { + // eslint-disable-next-line deprecation/deprecation ...transactionContext.metadata, request: req instanceof Request ? winterCGRequestToRequestData(req) : undefined, - source: 'route', }, }, async span => { diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index e25220ce61c2..f7e0917f2c39 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -1,6 +1,8 @@ import type { IncomingMessage, ServerResponse } from 'http'; import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, + getActiveSpan, getActiveTransaction, getCurrentScope, runWithAsyncContext, @@ -85,7 +87,7 @@ export function withTracedServerSideDataFetcher Pr return async function (this: unknown, ...args: Parameters): Promise> { return runWithAsyncContext(async () => { const scope = getCurrentScope(); - const previousSpan: Span | undefined = getTransactionFromRequest(req) ?? scope.getSpan(); + const previousSpan: Span | undefined = getTransactionFromRequest(req) ?? getActiveSpan(); let dataFetcherSpan; const sentryTrace = @@ -100,6 +102,8 @@ export function withTracedServerSideDataFetcher Pr if (platformSupportsStreaming()) { let spanToContinue: Span; if (previousSpan === undefined) { + // TODO: Refactor this to use `startSpan()` + // eslint-disable-next-line deprecation/deprecation const newTransaction = startTransaction( { op: 'http.server', @@ -129,6 +133,7 @@ export function withTracedServerSideDataFetcher Pr spanToContinue = previousSpan; } + // eslint-disable-next-line deprecation/deprecation dataFetcherSpan = spanToContinue.startChild({ op: 'function.nextjs', description: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`, @@ -136,6 +141,8 @@ export function withTracedServerSideDataFetcher Pr status: 'ok', }); } else { + // TODO: Refactor this to use `startSpan()` + // eslint-disable-next-line deprecation/deprecation dataFetcherSpan = startTransaction({ op: 'function.nextjs', name: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`, @@ -150,6 +157,7 @@ export function withTracedServerSideDataFetcher Pr }); } + // eslint-disable-next-line deprecation/deprecation scope.setSpan(dataFetcherSpan); scope.setSDKProcessingMetadata({ request: req }); @@ -163,6 +171,7 @@ export function withTracedServerSideDataFetcher Pr throw e; } finally { dataFetcherSpan.end(); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(previousSpan); if (!platformSupportsStreaming()) { await flushQueue(); @@ -189,6 +198,7 @@ export async function callDataFetcherTraced Promis ): Promise> { const { parameterizedRoute, dataFetchingMethodName } = options; + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (!transaction) { @@ -200,11 +210,12 @@ export async function callDataFetcherTraced Promis // right here so making that check will probabably not even be necessary. // Logic will be: If there is no active transaction, start one with correct name and source. If there is an active // transaction, create a child span with correct name and source. - transaction.name = parameterizedRoute; - transaction.metadata.source = 'route'; + transaction.updateName(parameterizedRoute); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); // Capture the route, since pre-loading, revalidation, etc might mean that this span may happen during another // route's transaction + // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild({ op: 'function.nextjs', origin: 'auto.function.nextjs', diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 85872ac7d703..01e1c75d6f3f 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -104,19 +104,16 @@ async function withServerActionInstrumentationImplementation
= {}; options.formData.forEach((value, key) => { - if (typeof value === 'string') { - formDataObject[key] = value; - } else { - formDataObject[key] = '[non-string value]'; - } + span?.setAttribute( + `server_action_form_data.${key}`, + typeof value === 'string' ? value : '[non-string value]', + ); }); - span?.setData('server_action_form_data', formDataObject); } return result; diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts index fc8f602f524b..16228aa0cda8 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts @@ -1,4 +1,5 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions, captureException, continueTrace, @@ -108,9 +109,12 @@ export function withSentry(apiHandler: NextApiHandler, parameterizedRoute?: stri name: `${reqMethod}${reqPath}`, op: 'http.server', origin: 'auto.http.nextjs', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, metadata: { + // eslint-disable-next-line deprecation/deprecation ...transactionContext.metadata, - source: 'route', request: req, }, }, diff --git a/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts index cd6cc4934493..df18b2ad952d 100644 --- a/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts @@ -1,4 +1,10 @@ -import { addTracingExtensions, getClient, getCurrentScope, spanToTraceHeader } from '@sentry/core'; +import { + addTracingExtensions, + getClient, + getCurrentScope, + getDynamicSamplingContextFromSpan, + spanToTraceHeader, +} from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type App from 'next/app'; @@ -52,6 +58,7 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI }; } = await tracedGetInitialProps.apply(thisArg, args); + // eslint-disable-next-line deprecation/deprecation const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); // Per definition, `pageProps` is not optional, however an increased amount of users doesn't seem to call @@ -65,7 +72,7 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI if (requestTransaction) { appGetInitialProps.pageProps._sentryTraceData = spanToTraceHeader(requestTransaction); - const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestTransaction); appGetInitialProps.pageProps._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); } diff --git a/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts index b26e4a2434c3..44a171d8e6d5 100644 --- a/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts @@ -1,4 +1,10 @@ -import { addTracingExtensions, getClient, getCurrentScope, spanToTraceHeader } from '@sentry/core'; +import { + addTracingExtensions, + getClient, + getCurrentScope, + getDynamicSamplingContextFromSpan, + spanToTraceHeader, +} from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { NextPageContext } from 'next'; import type { ErrorProps } from 'next/error'; @@ -53,11 +59,12 @@ export function wrapErrorGetInitialPropsWithSentry( _sentryBaggage?: string; } = await tracedGetInitialProps.apply(thisArg, args); + // eslint-disable-next-line deprecation/deprecation const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); if (requestTransaction) { errorGetInitialProps._sentryTraceData = spanToTraceHeader(requestTransaction); - const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestTransaction); errorGetInitialProps._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); } diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index d1765aa2c41e..f2e829704dd6 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -1,4 +1,5 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions, captureException, continueTrace, @@ -6,12 +7,13 @@ import { getCurrentScope, handleCallbackErrors, runWithAsyncContext, - startSpan, + startSpanManual, } from '@sentry/core'; import type { WebFetchHeaders } from '@sentry/types'; import { winterCGHeadersToDict } from '@sentry/utils'; import type { GenerationFunctionContext } from '../common/types'; +import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; import { commonObjectToPropagationContext } from './utils/commonObjectTracing'; /** @@ -61,33 +63,49 @@ export function wrapGenerationFunctionWithSentry a transactionContext.parentSpanId = commonSpanId; } - return startSpan( + return startSpanManual( { op: 'function.nextjs', name: `${componentType}.${generationFunctionIdentifier} (${componentRoute})`, origin: 'auto.function.nextjs', ...transactionContext, data, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, metadata: { + // eslint-disable-next-line deprecation/deprecation ...transactionContext.metadata, - source: 'url', request: { headers: headers ? winterCGHeadersToDict(headers) : undefined, }, }, }, - () => { + span => { return handleCallbackErrors( () => originalFunction.apply(thisArg, args), - err => - captureException(err, { - mechanism: { - handled: false, - data: { - function: 'wrapGenerationFunctionWithSentry', + err => { + if (isNotFoundNavigationError(err)) { + // We don't want to report "not-found"s + span?.setStatus('not_found'); + } else if (isRedirectNavigationError(err)) { + // We don't want to report redirects + span?.setStatus('ok'); + } else { + span?.setStatus('internal_error'); + captureException(err, { + mechanism: { + handled: false, + data: { + function: 'wrapGenerationFunctionWithSentry', + }, }, - }, - }), + }); + } + }, + () => { + span?.end(); + }, ); }, ); diff --git a/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts index df4e3febfefc..1a6743765cd6 100644 --- a/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts @@ -1,4 +1,10 @@ -import { addTracingExtensions, getClient, getCurrentScope, spanToTraceHeader } from '@sentry/core'; +import { + addTracingExtensions, + getClient, + getCurrentScope, + getDynamicSamplingContextFromSpan, + spanToTraceHeader, +} from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { NextPage } from 'next'; @@ -49,11 +55,12 @@ export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialPro _sentryBaggage?: string; } = (await tracedGetInitialProps.apply(thisArg, args)) ?? {}; // Next.js allows undefined to be returned from a getInitialPropsFunction. + // eslint-disable-next-line deprecation/deprecation const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); if (requestTransaction) { initialProps._sentryTraceData = spanToTraceHeader(requestTransaction); - const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestTransaction); initialProps._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); } diff --git a/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts b/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts index c74f9db7292b..691570f87683 100644 --- a/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts @@ -1,4 +1,10 @@ -import { addTracingExtensions, getClient, getCurrentScope, spanToTraceHeader } from '@sentry/core'; +import { + addTracingExtensions, + getClient, + getCurrentScope, + getDynamicSamplingContextFromSpan, + spanToTraceHeader, +} from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { GetServerSideProps } from 'next'; @@ -46,11 +52,12 @@ export function wrapGetServerSidePropsWithSentry( >); if (serverSideProps && 'props' in serverSideProps) { + // eslint-disable-next-line deprecation/deprecation const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); if (requestTransaction) { serverSideProps.props._sentryTraceData = spanToTraceHeader(requestTransaction); - const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestTransaction); serverSideProps.props._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); } } diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 427879b3e843..a0a1ae2f77aa 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -1,4 +1,5 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions, captureException, continueTrace, @@ -61,12 +62,15 @@ export function wrapServerComponentWithSentry any> name: `${componentType} Server Component (${componentRoute})`, status: 'ok', origin: 'auto.function.nextjs', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, metadata: { + // eslint-disable-next-line deprecation/deprecation ...transactionContext.metadata, request: { headers: completeHeadersDict, }, - source: 'component', }, }, span => { diff --git a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts index dabba7741e01..71e3072d68b5 100644 --- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts @@ -1,4 +1,4 @@ -import { getCurrentScope } from '@sentry/core'; +import { getActiveSpan } from '@sentry/core'; import { withEdgeWrapping } from '../common/utils/edgeWrapperUtils'; import type { EdgeRouteHandler } from './types'; @@ -14,7 +14,7 @@ export function wrapApiHandlerWithSentry( apply: (wrappingTarget, thisArg, args: Parameters) => { const req = args[0]; - const activeSpan = getCurrentScope().getSpan(); + const activeSpan = getActiveSpan(); const wrappedHandler = withEdgeWrapping(wrappingTarget, { spanDescription: diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts index 1b35f82cbfe8..c77016a1e99c 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -1,6 +1,6 @@ import { BaseClient, getCurrentHub } from '@sentry/core'; import * as SentryReact from '@sentry/react'; -import { BrowserTracing, WINDOW } from '@sentry/react'; +import { BrowserTracing, WINDOW, getCurrentScope } from '@sentry/react'; import type { Integration } from '@sentry/types'; import type { UserIntegrationsFunction } from '@sentry/utils'; import { logger } from '@sentry/utils'; @@ -89,8 +89,13 @@ describe('Client init()', () => { const hub = getCurrentHub(); const transportSend = jest.spyOn(hub.getClient()!.getTransport()!, 'send'); - const transaction = hub.startTransaction({ name: '/404' }); - transaction.end(); + // Ensure we have no current span, so our next span is a transaction + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(undefined); + + SentryReact.startSpan({ name: '/404' }, () => { + // noop + }); expect(transportSend).not.toHaveBeenCalled(); expect(captureEvent.mock.results[0].value).toBeUndefined(); diff --git a/packages/nextjs/test/config/withSentry.test.ts b/packages/nextjs/test/config/withSentry.test.ts index da43ec724944..91b61516a240 100644 --- a/packages/nextjs/test/config/withSentry.test.ts +++ b/packages/nextjs/test/config/withSentry.test.ts @@ -1,5 +1,5 @@ import * as SentryCore from '@sentry/core'; -import { addTracingExtensions } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions } from '@sentry/core'; import type { NextApiRequest, NextApiResponse } from 'next'; import type { AugmentedNextApiResponse, NextApiHandler } from '../../src/common/types'; @@ -45,8 +45,10 @@ describe('withSentry', () => { name: 'GET http://dogs.are.great', op: 'http.server', origin: 'auto.http.nextjs', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, metadata: { - source: 'route', request: expect.objectContaining({ url: 'http://dogs.are.great' }), }, }, diff --git a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts index 2a782250c7c5..97d6e7b103e1 100644 --- a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts +++ b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts @@ -1,4 +1,5 @@ import * as coreSdk from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { withEdgeWrapping } from '../../src/common/utils/edgeWrapperUtils'; @@ -81,7 +82,12 @@ describe('withEdgeWrapping', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ - metadata: { request: { headers: {} }, source: 'route' }, + metadata: { + request: { headers: {} }, + }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, name: 'some label', op: 'some op', origin: 'auto.function.nextjs.withEdgeWrapping', diff --git a/packages/nextjs/test/edge/withSentryAPI.test.ts b/packages/nextjs/test/edge/withSentryAPI.test.ts index b51ce3dfeca6..ea5e7c4319f0 100644 --- a/packages/nextjs/test/edge/withSentryAPI.test.ts +++ b/packages/nextjs/test/edge/withSentryAPI.test.ts @@ -1,4 +1,5 @@ import * as coreSdk from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { wrapApiHandlerWithSentry } from '../../src/edge'; @@ -52,7 +53,12 @@ describe('wrapApiHandlerWithSentry', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ - metadata: { request: { headers: {}, method: 'POST', url: 'https://sentry.io/' }, source: 'route' }, + metadata: { + request: { headers: {}, method: 'POST', url: 'https://sentry.io/' }, + }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, name: 'POST /user/[userId]/post/[postId]', op: 'http.server', origin: 'auto.function.nextjs.withEdgeWrapping', @@ -71,7 +77,10 @@ describe('wrapApiHandlerWithSentry', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ - metadata: { source: 'route' }, + metadata: {}, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, name: 'handler (/user/[userId]/post/[postId])', op: 'http.server', origin: 'auto.function.nextjs.withEdgeWrapping', diff --git a/packages/nextjs/test/serverSdk.test.ts b/packages/nextjs/test/serverSdk.test.ts index 0813d4931874..6201ddf34a53 100644 --- a/packages/nextjs/test/serverSdk.test.ts +++ b/packages/nextjs/test/serverSdk.test.ts @@ -105,8 +105,9 @@ describe('Server init()', () => { const hub = getCurrentHub(); const transportSend = jest.spyOn(hub.getClient()!.getTransport()!, 'send'); - const transaction = hub.startTransaction({ name: '/404' }); - transaction.end(); + SentryNode.startSpan({ name: '/404' }, () => { + // noop + }); // We need to flush because the event processor pipeline is async whereas transaction.end() is sync. await SentryNode.flush(); diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index f9d065de52db..d44fb10da9c1 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -52,7 +52,9 @@ export { extractRequestData, // eslint-disable-next-line deprecation/deprecation deepReadDirSync, + // eslint-disable-next-line deprecation/deprecation getModuleFromFilename, + createGetModuleFromFilename, close, createTransport, // eslint-disable-next-line deprecation/deprecation diff --git a/packages/node-experimental/src/sdk/api.ts b/packages/node-experimental/src/sdk/api.ts index 71a08dd38552..abb1e37944ac 100644 --- a/packages/node-experimental/src/sdk/api.ts +++ b/packages/node-experimental/src/sdk/api.ts @@ -1,7 +1,6 @@ // PUBLIC APIS import { context } from '@opentelemetry/api'; -import { DEFAULT_ENVIRONMENT, closeSession, makeSession, updateSession } from '@sentry/core'; import type { Breadcrumb, BreadcrumbHint, @@ -12,13 +11,12 @@ import type { Extra, Extras, Primitive, - Session, Severity, SeverityLevel, User, } from '@sentry/types'; -import { GLOBAL_OBJ, consoleSandbox, dateTimestampInSeconds } from '@sentry/utils'; -import { getScopesFromContext, setScopesOnContext } from '../utils/contextData'; +import { consoleSandbox, dateTimestampInSeconds } from '@sentry/utils'; +import { getContextFromScope, getScopesFromContext, setScopesOnContext } from '../utils/contextData'; import type { ExclusiveEventHintOrCaptureContext } from '../utils/prepareEvent'; import { parseEventHintOrCaptureContext } from '../utils/prepareEvent'; @@ -29,9 +27,39 @@ export { getCurrentScope, getGlobalScope, getIsolationScope, getClient }; export { setCurrentScope, setIsolationScope } from './scope'; /** - * Fork a scope from the current scope, and make it the current scope in the given callback + * Creates a new scope with and executes the given operation within. + * The scope is automatically removed once the operation + * finishes or throws. + * + * This is essentially a convenience function for: + * + * pushScope(); + * callback(); + * popScope(); */ -export function withScope(callback: (scope: Scope) => T): T { +export function withScope(callback: (scope: Scope) => T): T; +/** + * Set the given scope as the active scope in the callback. + */ +export function withScope(scope: Scope | undefined, callback: (scope: Scope) => T): T; +/** + * Either creates a new active scope, or sets the given scope as active scope in the given callback. + */ +export function withScope( + ...rest: [callback: (scope: Scope) => T] | [scope: Scope | undefined, callback: (scope: Scope) => T] +): T { + // If a scope is defined, we want to make this the active scope instead of the default one + if (rest.length === 2) { + const [scope, callback] = rest; + if (!scope) { + return context.with(context.active(), () => callback(getCurrentScope())); + } + + const ctx = getContextFromScope(scope); + return context.with(ctx || context.active(), () => callback(getCurrentScope())); + } + + const callback = rest[0]; return context.with(context.active(), () => callback(getCurrentScope())); } @@ -61,6 +89,7 @@ export function withIsolationScope(callback: (isolationScope: Scope) => T): T * @deprecated This function will be removed in the next major version of the Sentry SDK. */ export function lastEventId(): string | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentScope().lastEventId(); } @@ -168,60 +197,3 @@ export function setContext( ): void { getIsolationScope().setContext(name, context); } - -/** Start a session on the current isolation scope. */ -export function startSession(context?: Session): Session { - const client = getClient(); - const isolationScope = getIsolationScope(); - - const { release, environment = DEFAULT_ENVIRONMENT } = client.getOptions(); - - // Will fetch userAgent if called from browser sdk - const { userAgent } = GLOBAL_OBJ.navigator || {}; - - const session = makeSession({ - release, - environment, - user: isolationScope.getUser(), - ...(userAgent && { userAgent }), - ...context, - }); - - // End existing session if there's one - const currentSession = isolationScope.getSession && isolationScope.getSession(); - if (currentSession && currentSession.status === 'ok') { - updateSession(currentSession, { status: 'exited' }); - } - endSession(); - - // Afterwards we set the new session on the scope - isolationScope.setSession(session); - - return session; -} - -/** End the session on the current isolation scope. */ -export function endSession(): void { - const isolationScope = getIsolationScope(); - const session = isolationScope.getSession(); - if (session) { - closeSession(session); - } - _sendSessionUpdate(); - - // the session is over; take it off of the scope - isolationScope.setSession(); -} - -/** - * Sends the current Session on the scope - */ -function _sendSessionUpdate(): void { - const scope = getCurrentScope(); - const client = getClient(); - - const session = scope.getSession(); - if (session && client.captureSession) { - client.captureSession(session); - } -} diff --git a/packages/node-experimental/src/sdk/hub.ts b/packages/node-experimental/src/sdk/hub.ts index b58548acb326..5de82086f387 100644 --- a/packages/node-experimental/src/sdk/hub.ts +++ b/packages/node-experimental/src/sdk/hub.ts @@ -10,11 +10,11 @@ import type { TransactionContext, } from '@sentry/types'; +import { endSession, startSession } from '@sentry/core'; import { addBreadcrumb, captureEvent, configureScope, - endSession, getClient, getCurrentScope, lastEventId, @@ -24,7 +24,6 @@ import { setTag, setTags, setUser, - startSession, withScope, } from './api'; import { callExtensionMethod, getGlobalCarrier } from './globals'; @@ -121,6 +120,7 @@ export function getCurrentHub(): Hub { captureSession(endSession?: boolean): void { // both send the update and pull the session from the scope if (endSession) { + // eslint-disable-next-line deprecation/deprecation return this.endSession(); } diff --git a/packages/node-experimental/src/sdk/init.ts b/packages/node-experimental/src/sdk/init.ts index 821757a9a246..0f60bccd343c 100644 --- a/packages/node-experimental/src/sdk/init.ts +++ b/packages/node-experimental/src/sdk/init.ts @@ -1,4 +1,4 @@ -import { getIntegrationsToSetup, hasTracingEnabled } from '@sentry/core'; +import { endSession, getIntegrationsToSetup, hasTracingEnabled, startSession } from '@sentry/core'; import { Integrations, defaultIntegrations as defaultNodeIntegrations, @@ -22,7 +22,7 @@ import { Http } from '../integrations/http'; import { NodeFetch } from '../integrations/node-fetch'; import { setOpenTelemetryContextAsyncContextStrategy } from '../otel/asyncContextStrategy'; import type { NodeExperimentalClientOptions, NodeExperimentalOptions } from '../types'; -import { endSession, getClient, getCurrentScope, getGlobalScope, getIsolationScope, startSession } from './api'; +import { getClient, getCurrentScope, getGlobalScope, getIsolationScope } from './api'; import { NodeExperimentalClient } from './client'; import { getGlobalCarrier } from './globals'; import { setLegacyHubOnCarrier } from './hub'; diff --git a/packages/node-experimental/src/sdk/types.ts b/packages/node-experimental/src/sdk/types.ts index 90c61dceda86..57ad321a5470 100644 --- a/packages/node-experimental/src/sdk/types.ts +++ b/packages/node-experimental/src/sdk/types.ts @@ -2,8 +2,6 @@ import type { Attachment, Breadcrumb, Contexts, - Event, - EventHint, EventProcessor, Extras, Hub, @@ -11,7 +9,6 @@ import type { Primitive, PropagationContext, Scope as BaseScope, - Severity, SeverityLevel, User, } from '@sentry/types'; @@ -35,14 +32,9 @@ export interface Scope extends BaseScope { isolationScope: typeof this | undefined; // @ts-expect-error typeof this is what we want here clone(scope?: Scope): typeof this; - captureException(exception: unknown, hint?: EventHint): string; - captureMessage( - message: string, - // eslint-disable-next-line deprecation/deprecation - level?: Severity | SeverityLevel, - hint?: EventHint, - ): string; - captureEvent(event: Event, hint?: EventHint): string; + /** + * @deprecated This function will be removed in the next major version of the Sentry SDK. + */ lastEventId(): string | undefined; getScopeData(): ScopeData; } diff --git a/packages/node-experimental/src/utils/contextData.ts b/packages/node-experimental/src/utils/contextData.ts index 5c69f186eb6d..cb77d37a9ae0 100644 --- a/packages/node-experimental/src/utils/contextData.ts +++ b/packages/node-experimental/src/utils/contextData.ts @@ -1,10 +1,13 @@ import type { Context } from '@opentelemetry/api'; import { createContextKey } from '@opentelemetry/api'; +import type { Scope } from '@sentry/types'; import type { CurrentScopes } from '../sdk/types'; export const SENTRY_SCOPES_CONTEXT_KEY = createContextKey('sentry_scopes'); +const SCOPE_CONTEXT_MAP = new WeakMap(); + /** * Try to get the current scopes from the given OTEL context. * This requires a Context Manager that was wrapped with getWrappedContextManager. @@ -18,5 +21,17 @@ export function getScopesFromContext(context: Context): CurrentScopes | undefine * This will return a forked context with the Propagation Context set. */ export function setScopesOnContext(context: Context, scopes: CurrentScopes): Context { + // So we can look up the context from the scope later + SCOPE_CONTEXT_MAP.set(scopes.scope, context); + SCOPE_CONTEXT_MAP.set(scopes.isolationScope, context); + return context.setValue(SENTRY_SCOPES_CONTEXT_KEY, scopes); } + +/** + * Get the context related to a scope. + * TODO v8: Use this for the `trace` functions. + * */ +export function getContextFromScope(scope: Scope): Context | undefined { + return SCOPE_CONTEXT_MAP.get(scope); +} diff --git a/packages/node-experimental/test/integration/breadcrumbs.test.ts b/packages/node-experimental/test/integration/breadcrumbs.test.ts index fea78a353011..c576cc85b11a 100644 --- a/packages/node-experimental/test/integration/breadcrumbs.test.ts +++ b/packages/node-experimental/test/integration/breadcrumbs.test.ts @@ -27,6 +27,7 @@ describe('Integration | breadcrumbs', () => { hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); const error = new Error('test'); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); await client.flush(); @@ -118,6 +119,7 @@ describe('Integration | breadcrumbs', () => { hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); @@ -171,6 +173,7 @@ describe('Integration | breadcrumbs', () => { hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); }); @@ -214,6 +217,7 @@ describe('Integration | breadcrumbs', () => { hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); @@ -261,6 +265,7 @@ describe('Integration | breadcrumbs', () => { startSpan({ name: 'inner3' }, () => { hub.addBreadcrumb({ timestamp: 123457, message: 'test4' }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); startSpan({ name: 'inner4' }, () => { @@ -321,6 +326,7 @@ describe('Integration | breadcrumbs', () => { await new Promise(resolve => setTimeout(resolve, 10)); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); }); diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index be48f5f9e6b5..5198c532ef79 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -1,6 +1,7 @@ import { SpanKind, TraceFlags, context, trace } from '@opentelemetry/api'; import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { spanToJSON } from '@sentry/core'; import { SentrySpanProcessor, getCurrentHub, setPropagationContextOnContext } from '@sentry/opentelemetry'; import type { Integration, PropagationContext, TransactionEvent } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -145,7 +146,7 @@ describe('Integration | Transactions', () => { // note: Currently, spans do not have any context/span added to them // This is the same behavior as for the "regular" SDKs - expect(spans.map(span => span.toJSON())).toEqual([ + expect(spans.map(span => spanToJSON(span))).toEqual([ { data: { 'otel.kind': 'INTERNAL' }, description: 'inner span 1', @@ -399,7 +400,7 @@ describe('Integration | Transactions', () => { // note: Currently, spans do not have any context/span added to them // This is the same behavior as for the "regular" SDKs - expect(spans.map(span => span.toJSON())).toEqual([ + expect(spans.map(span => spanToJSON(span))).toEqual([ { data: { 'otel.kind': 'INTERNAL' }, description: 'inner span 1', diff --git a/packages/node/src/cron/cron.ts b/packages/node/src/cron/cron.ts index a8b42ec0fed7..88a3e9e58eb5 100644 --- a/packages/node/src/cron/cron.ts +++ b/packages/node/src/cron/cron.ts @@ -56,6 +56,8 @@ const ERROR_TEXT = 'Automatic instrumentation of CronJob only supports crontab s * ``` */ export function instrumentCron(lib: T & CronJobConstructor, monitorSlug: string): T { + let jobScheduled = false; + return new Proxy(lib, { construct(target, args: ConstructorParameters) { const [cronTime, onTick, onComplete, start, timeZone, ...rest] = args; @@ -64,6 +66,12 @@ export function instrumentCron(lib: T & CronJobConstructor, monitorSlug: stri throw new Error(ERROR_TEXT); } + if (jobScheduled) { + throw new Error(`A job named '${monitorSlug}' has already been scheduled`); + } + + jobScheduled = true; + const cronString = replaceCronNames(cronTime); function monitoredTick(context: unknown, onComplete?: unknown): void | Promise { @@ -90,6 +98,12 @@ export function instrumentCron(lib: T & CronJobConstructor, monitorSlug: stri throw new Error(ERROR_TEXT); } + if (jobScheduled) { + throw new Error(`A job named '${monitorSlug}' has already been scheduled`); + } + + jobScheduled = true; + const cronString = replaceCronNames(cronTime); param.onTick = (context: unknown, onComplete?: unknown) => { diff --git a/packages/node/src/cron/node-cron.ts b/packages/node/src/cron/node-cron.ts index 2f422b9a85f8..4495a0b54909 100644 --- a/packages/node/src/cron/node-cron.ts +++ b/packages/node/src/cron/node-cron.ts @@ -2,12 +2,12 @@ import { withMonitor } from '@sentry/core'; import { replaceCronNames } from './common'; export interface NodeCronOptions { - name?: string; + name: string; timezone?: string; } export interface NodeCron { - schedule: (cronExpression: string, callback: () => void, options?: NodeCronOptions) => unknown; + schedule: (cronExpression: string, callback: () => void, options: NodeCronOptions) => unknown; } /** diff --git a/packages/node/src/cron/node-schedule.ts b/packages/node/src/cron/node-schedule.ts new file mode 100644 index 000000000000..79ae44a06e52 --- /dev/null +++ b/packages/node/src/cron/node-schedule.ts @@ -0,0 +1,60 @@ +import { withMonitor } from '@sentry/core'; +import { replaceCronNames } from './common'; + +export interface NodeSchedule { + scheduleJob( + nameOrExpression: string | Date | object, + expressionOrCallback: string | Date | object | (() => void), + callback?: () => void, + ): unknown; +} + +/** + * Instruments the `node-schedule` library to send a check-in event to Sentry for each job execution. + * + * ```ts + * import * as Sentry from '@sentry/node'; + * import * as schedule from 'node-schedule'; + * + * const scheduleWithCheckIn = Sentry.cron.instrumentNodeSchedule(schedule); + * + * const job = scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * * *', () => { + * console.log('You will see this message every minute'); + * }); + * ``` + */ +export function instrumentNodeSchedule(lib: T & NodeSchedule): T { + return new Proxy(lib, { + get(target, prop: keyof NodeSchedule) { + if (prop === 'scheduleJob') { + // eslint-disable-next-line @typescript-eslint/unbound-method + return new Proxy(target.scheduleJob, { + apply(target, thisArg, argArray: Parameters) { + const [nameOrExpression, expressionOrCallback] = argArray; + + if (typeof nameOrExpression !== 'string' || typeof expressionOrCallback !== 'string') { + throw new Error( + "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string", + ); + } + + const monitorSlug = nameOrExpression; + const expression = expressionOrCallback; + + return withMonitor( + monitorSlug, + () => { + return target.apply(thisArg, argArray); + }, + { + schedule: { type: 'crontab', value: replaceCronNames(expression) }, + }, + ); + }, + }); + } + + return target[prop]; + }, + }); +} diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 9a4bb08bfb4b..892aabd2dd84 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -1,9 +1,11 @@ import type * as http from 'http'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, continueTrace, flush, + getActiveSpan, getClient, getCurrentScope, hasTracingEnabled, @@ -63,20 +65,25 @@ export function tracingHandler(): ( const [name, source] = extractPathForTransaction(req, { path: true, method: true }); const transaction = continueTrace({ sentryTrace, baggage }, ctx => + // TODO: Refactor this to use `startSpan()` + // eslint-disable-next-line deprecation/deprecation startTransaction( { name, op: 'http.server', origin: 'auto.http.node.tracingHandler', ...ctx, + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, + }, metadata: { + // eslint-disable-next-line deprecation/deprecation ...ctx.metadata, // The request should already have been stored in `scope.sdkProcessingMetadata` (which will become // `event.sdkProcessingMetadata` the same way the metadata here will) by `sentryRequestMiddleware`, but on the // off chance someone is using `sentryTracingMiddleware` without `sentryRequestMiddleware`, it doesn't hurt to // be sure request: req, - source, }, }, // extra context passed to the tracesSampler @@ -85,6 +92,7 @@ export function tracingHandler(): ( ); // We put the transaction on the scope so users can attach children to it + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); // We also set __sentry_transaction on the response so people can grab the transaction there to add @@ -269,7 +277,8 @@ export function errorHandler(options?: { // For some reason we need to set the transaction on the scope again // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const transaction = (res as any).__sentry_transaction as Span; - if (transaction && _scope.getSpan() === undefined) { + if (transaction && !getActiveSpan()) { + // eslint-disable-next-line deprecation/deprecation _scope.setSpan(transaction); } @@ -326,11 +335,12 @@ interface TrpcMiddlewareArguments { export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { return function ({ path, type, next, rawInput }: TrpcMiddlewareArguments): T { const clientOptions = getClient()?.getOptions(); + // eslint-disable-next-line deprecation/deprecation const sentryTransaction = getCurrentScope().getTransaction(); if (sentryTransaction) { sentryTransaction.updateName(`trpc/${path}`); - sentryTransaction.setMetadata({ source: 'route' }); + sentryTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); sentryTransaction.op = 'rpc.server'; const trpcContext: Record = { @@ -341,6 +351,8 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { trpcContext.input = normalize(rawInput); } + // TODO: Can we rewrite this to an attribute? Or set this on the scope? + // eslint-disable-next-line deprecation/deprecation sentryTransaction.setContext('trpc', trpcContext); } diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index e712a4fc0d0d..21735a64d6a1 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -38,6 +38,7 @@ export { // eslint-disable-next-line deprecation/deprecation extractTraceparentData, flush, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, @@ -51,6 +52,7 @@ export { makeMain, runWithAsyncContext, Scope, + // eslint-disable-next-line deprecation/deprecation startTransaction, SDK_VERSION, setContext, @@ -84,7 +86,14 @@ export { defaultIntegrations, init, defaultStackParser, getSentryRelease } from export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from '@sentry/utils'; // eslint-disable-next-line deprecation/deprecation export { deepReadDirSync } from './utils'; -export { getModuleFromFilename } from './module'; + +import { createGetModuleFromFilename } from './module'; +/** + * @deprecated use `createGetModuleFromFilename` instead. + */ +export const getModuleFromFilename = createGetModuleFromFilename(); +export { createGetModuleFromFilename }; + // eslint-disable-next-line deprecation/deprecation export { enableAnrDetection } from './integrations/anr/legacy'; @@ -123,9 +132,11 @@ export { hapiErrorPlugin } from './integrations/hapi'; import { instrumentCron } from './cron/cron'; import { instrumentNodeCron } from './cron/node-cron'; +import { instrumentNodeSchedule } from './cron/node-schedule'; /** Methods to instrument cron libraries for Sentry check-ins */ export const cron = { instrumentCron, instrumentNodeCron, + instrumentNodeSchedule, }; diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index 3aa71f0589f3..549e483b51d0 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -56,8 +56,8 @@ const anrIntegration = ((options: Partial = {}) => { return { name: INTEGRATION_NAME, setup(client: NodeClient) { - if (NODE_VERSION.major < 16) { - throw new Error('ANR detection requires Node 16 or later'); + if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) { + throw new Error('ANR detection requires Node 16.17.0 or later'); } // setImmediate is used to ensure that all other integrations have been setup @@ -68,6 +68,8 @@ const anrIntegration = ((options: Partial = {}) => { /** * Starts a thread to detect App Not Responding (ANR) events + * + * ANR detection requires Node 16.17.0 or later */ // eslint-disable-next-line deprecation/deprecation export const Anr = convertIntegrationFnToClass(INTEGRATION_NAME, anrIntegration); diff --git a/packages/node/src/integrations/anr/worker.ts b/packages/node/src/integrations/anr/worker.ts index e2292ce0aff0..4f9278b5b78f 100644 --- a/packages/node/src/integrations/anr/worker.ts +++ b/packages/node/src/integrations/anr/worker.ts @@ -6,9 +6,17 @@ import { updateSession, } from '@sentry/core'; import type { Event, Session, StackFrame, TraceContext } from '@sentry/types'; -import { callFrameToStackFrame, normalizeUrlToBase, stripSentryFramesAndReverse, watchdogTimer } from '@sentry/utils'; +import { + callFrameToStackFrame, + normalizeUrlToBase, + stripSentryFramesAndReverse, + uuid4, + watchdogTimer, +} from '@sentry/utils'; import { Session as InspectorSession } from 'inspector'; import { parentPort, workerData } from 'worker_threads'; + +import { createGetModuleFromFilename } from '../../module'; import { makeNodeTransport } from '../../transports'; import type { WorkerStartData } from './common'; @@ -90,6 +98,7 @@ async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext): log('Sending event'); const event: Event = { + event_id: uuid4(), contexts: { ...options.contexts, trace: traceContext }, release: options.release, environment: options.environment, @@ -151,8 +160,9 @@ if (options.captureStackTrace) { // copy the frames const callFrames = [...event.params.callFrames]; + const getModuleName = options.appRootPath ? createGetModuleFromFilename(options.appRootPath) : () => undefined; const stackFrames = callFrames.map(frame => - callFrameToStackFrame(frame, scripts.get(frame.location.scriptId), () => undefined), + callFrameToStackFrame(frame, scripts.get(frame.location.scriptId), getModuleName), ); // Evaluate a script in the currently paused context diff --git a/packages/node/src/integrations/hapi/index.ts b/packages/node/src/integrations/hapi/index.ts index 732839d3995c..42335f7c4ce5 100644 --- a/packages/node/src/integrations/hapi/index.ts +++ b/packages/node/src/integrations/hapi/index.ts @@ -5,6 +5,7 @@ import { convertIntegrationFnToClass, getActiveTransaction, getCurrentScope, + getDynamicSamplingContextFromSpan, spanToTraceHeader, startTransaction, } from '@sentry/core'; @@ -45,6 +46,7 @@ export const hapiErrorPlugin = { const server = serverArg as unknown as Server; server.events.on('request', (request, event) => { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (request.response && isBoomObject(request.response)) { @@ -75,6 +77,7 @@ export const hapiTracingPlugin = { baggage: request.headers['baggage'] || undefined, }, transactionContext => { + // eslint-disable-next-line deprecation/deprecation return startTransaction({ ...transactionContext, op: 'hapi.request', @@ -84,12 +87,14 @@ export const hapiTracingPlugin = { }, ); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); return h.continue; }); server.ext('onPreResponse', (request, h) => { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (request.response && isResponseObject(request.response) && transaction) { @@ -97,7 +102,7 @@ export const hapiTracingPlugin = { response.header('sentry-trace', spanToTraceHeader(transaction)); const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader( - transaction.getDynamicSamplingContext(), + getDynamicSamplingContextFromSpan(transaction), ); if (dynamicSamplingContext) { @@ -109,6 +114,7 @@ export const hapiTracingPlugin = { }); server.ext('onPostHandler', (request, h) => { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (request.response && isResponseObject(request.response) && transaction) { diff --git a/packages/node/src/integrations/hapi/types.ts b/packages/node/src/integrations/hapi/types.ts index d74c171ef441..a650667fe362 100644 --- a/packages/node/src/integrations/hapi/types.ts +++ b/packages/node/src/integrations/hapi/types.ts @@ -3,6 +3,7 @@ /* eslint-disable @typescript-eslint/unified-signatures */ /* eslint-disable @typescript-eslint/no-empty-interface */ /* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-explicit-any */ // Vendored and simplified from: // - @types/hapi__hapi diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 61046eb8f38d..7c2627b17123 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -1,9 +1,18 @@ import type * as http from 'http'; import type * as https from 'https'; import type { Hub } from '@sentry/core'; -import { spanToTraceHeader } from '@sentry/core'; -import { addBreadcrumb, getClient, getCurrentScope } from '@sentry/core'; -import { getCurrentHub, getDynamicSamplingContextFromClient, isSentryRequestUrl } from '@sentry/core'; +import { + addBreadcrumb, + getActiveSpan, + getClient, + getCurrentHub, + getCurrentScope, + getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, + isSentryRequestUrl, + spanToJSON, + spanToTraceHeader, +} from '@sentry/core'; import type { DynamicSamplingContext, EventProcessor, @@ -246,12 +255,13 @@ function _createWrappedRequestMethodFactory( } const scope = getCurrentScope(); - const parentSpan = scope.getSpan(); + const parentSpan = getActiveSpan(); const data = getRequestSpanData(requestUrl, requestOptions); const requestSpan = shouldCreateSpan(rawRequestUrl) - ? parentSpan?.startChild({ + ? // eslint-disable-next-line deprecation/deprecation + parentSpan?.startChild({ op: 'http.client', origin: 'auto.http.node.http', description: `${data['http.method']} ${data.url}`, @@ -262,7 +272,7 @@ function _createWrappedRequestMethodFactory( if (shouldAttachTraceData(rawRequestUrl)) { if (requestSpan) { const sentryTraceHeader = spanToTraceHeader(requestSpan); - const dynamicSamplingContext = requestSpan?.transaction?.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestSpan); addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, dynamicSamplingContext); } else { const client = getClient(); @@ -292,7 +302,9 @@ function _createWrappedRequestMethodFactory( if (res.statusCode) { requestSpan.setHttpStatus(res.statusCode); } - requestSpan.description = cleanSpanDescription(requestSpan.description, requestOptions, req); + requestSpan.updateName( + cleanSpanDescription(spanToJSON(requestSpan).description || '', requestOptions, req) || '', + ); requestSpan.end(); } }) @@ -305,7 +317,9 @@ function _createWrappedRequestMethodFactory( } if (requestSpan) { requestSpan.setHttpStatus(500); - requestSpan.description = cleanSpanDescription(requestSpan.description, requestOptions, req); + requestSpan.updateName( + cleanSpanDescription(spanToJSON(requestSpan).description || '', requestOptions, req) || '', + ); requestSpan.end(); } }); diff --git a/packages/node/src/integrations/local-variables/index.ts b/packages/node/src/integrations/local-variables/index.ts index 970eaea52719..708b4b41ea24 100644 --- a/packages/node/src/integrations/local-variables/index.ts +++ b/packages/node/src/integrations/local-variables/index.ts @@ -1,21 +1,6 @@ -import { convertIntegrationFnToClass } from '@sentry/core'; -import type { IntegrationFn } from '@sentry/types'; -import { NODE_VERSION } from '../../nodeVersion'; -import type { Options } from './common'; -import { localVariablesAsync } from './local-variables-async'; -import { localVariablesSync } from './local-variables-sync'; - -const INTEGRATION_NAME = 'LocalVariables'; - -/** - * Adds local variables to exception frames - */ -const localVariables: IntegrationFn = (options: Options = {}) => { - return NODE_VERSION.major < 19 ? localVariablesSync(options) : localVariablesAsync(options); -}; +import { LocalVariablesSync } from './local-variables-sync'; /** * Adds local variables to exception frames */ -// eslint-disable-next-line deprecation/deprecation -export const LocalVariables = convertIntegrationFnToClass(INTEGRATION_NAME, localVariables); +export const LocalVariables = LocalVariablesSync; diff --git a/packages/node/src/integrations/local-variables/local-variables-sync.ts b/packages/node/src/integrations/local-variables/local-variables-sync.ts index d2b988cca1e9..32dae4599c02 100644 --- a/packages/node/src/integrations/local-variables/local-variables-sync.ts +++ b/packages/node/src/integrations/local-variables/local-variables-sync.ts @@ -208,7 +208,7 @@ function tryNewAsyncSession(): AsyncSession | undefined { } } -const INTEGRATION_NAME = 'LocalVariablesSync'; +const INTEGRATION_NAME = 'LocalVariables'; /** * Adds local variables to exception frames diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index 117ef602ac38..f4aec53aa30f 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -1,9 +1,11 @@ import { addBreadcrumb, + getActiveSpan, getClient, getCurrentHub, getCurrentScope, getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, isSentryRequestUrl, spanToTraceHeader, } from '@sentry/core'; @@ -156,8 +158,7 @@ export class Undici implements Integration { const clientOptions = client.getOptions(); const scope = getCurrentScope(); - - const parentSpan = scope.getSpan(); + const parentSpan = getActiveSpan(); const span = this._shouldCreateSpan(stringUrl) ? createRequestSpan(parentSpan, request, stringUrl) : undefined; if (span) { @@ -181,7 +182,7 @@ export class Undici implements Integration { if (shouldAttachTraceData(stringUrl)) { if (span) { - const dynamicSamplingContext = span?.transaction?.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(span); const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); setHeadersOnRequest(request, spanToTraceHeader(span), sentryBaggageHeader); @@ -310,6 +311,7 @@ function createRequestSpan( if (url.hash) { data['http.fragment'] = url.hash; } + // eslint-disable-next-line deprecation/deprecation return activeSpan?.startChild({ op: 'http.client', origin: 'auto.http.node.undici', diff --git a/packages/node/src/module.ts b/packages/node/src/module.ts index 73364493ddb2..a2e93fe99115 100644 --- a/packages/node/src/module.ts +++ b/packages/node/src/module.ts @@ -8,62 +8,51 @@ function normalizeWindowsPath(path: string): string { .replace(/\\/g, '/'); // replace all `\` instances with `/` } -// We cache this so we don't have to recompute it -let basePath: string | undefined; - -function getBasePath(): string { - if (!basePath) { - const baseDir = - require && require.main && require.main.filename ? dirname(require.main.filename) : global.process.cwd(); - basePath = `${baseDir}/`; - } - - return basePath; -} - -/** Gets the module from a filename */ -export function getModuleFromFilename( - filename: string | undefined, - basePath: string = getBasePath(), +/** Creates a function that gets the module name from a filename */ +export function createGetModuleFromFilename( + basePath: string = dirname(process.argv[1]), isWindows: boolean = sep === '\\', -): string | undefined { - if (!filename) { - return; - } - +): (filename: string | undefined) => string | undefined { const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath; - const normalizedFilename = isWindows ? normalizeWindowsPath(filename) : filename; - // eslint-disable-next-line prefer-const - let { dir, base: file, ext } = posix.parse(normalizedFilename); + return (filename: string | undefined) => { + if (!filename) { + return; + } - if (ext === '.js' || ext === '.mjs' || ext === '.cjs') { - file = file.slice(0, ext.length * -1); - } + const normalizedFilename = isWindows ? normalizeWindowsPath(filename) : filename; - if (!dir) { - // No dirname whatsoever - dir = '.'; - } + // eslint-disable-next-line prefer-const + let { dir, base: file, ext } = posix.parse(normalizedFilename); - let n = dir.lastIndexOf('/node_modules'); - if (n > -1) { - return `${dir.slice(n + 14).replace(/\//g, '.')}:${file}`; - } + if (ext === '.js' || ext === '.mjs' || ext === '.cjs') { + file = file.slice(0, ext.length * -1); + } - // Let's see if it's a part of the main module - // To be a part of main module, it has to share the same base - n = `${dir}/`.lastIndexOf(normalizedBase, 0); - if (n === 0) { - let moduleName = dir.slice(normalizedBase.length).replace(/\//g, '.'); + if (!dir) { + // No dirname whatsoever + dir = '.'; + } - if (moduleName) { - moduleName += ':'; + let n = dir.lastIndexOf('/node_modules'); + if (n > -1) { + return `${dir.slice(n + 14).replace(/\//g, '.')}:${file}`; } - moduleName += file; - return moduleName; - } + // Let's see if it's a part of the main module + // To be a part of main module, it has to share the same base + n = `${dir}/`.lastIndexOf(normalizedBase, 0); + if (n === 0) { + let moduleName = dir.slice(normalizedBase.length).replace(/\//g, '.'); + + if (moduleName) { + moduleName += ':'; + } + moduleName += file; + + return moduleName; + } - return file; + return file; + }; } diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index c9a1c108dfdd..59a895b0809c 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -1,12 +1,14 @@ /* eslint-disable max-lines */ import { Integrations as CoreIntegrations, + endSession, getClient, - getCurrentHub, getCurrentScope, getIntegrationsToSetup, + getIsolationScope, getMainCarrier, initAndBind, + startSession, } from '@sentry/core'; import type { SessionStatus, StackParser } from '@sentry/types'; import { @@ -32,7 +34,7 @@ import { Spotlight, Undici, } from './integrations'; -import { getModuleFromFilename } from './module'; +import { createGetModuleFromFilename } from './module'; import { makeNodeTransport } from './transports'; import type { NodeClientOptions, NodeOptions } from './types'; @@ -238,26 +240,27 @@ export function getSentryRelease(fallback?: string): string | undefined { } /** Node.js stack parser */ -export const defaultStackParser: StackParser = createStackParser(nodeStackLineParser(getModuleFromFilename)); +export const defaultStackParser: StackParser = createStackParser(nodeStackLineParser(createGetModuleFromFilename())); /** * Enable automatic Session Tracking for the node process. */ function startSessionTracking(): void { - const hub = getCurrentHub(); - hub.startSession(); + startSession(); // Emitted in the case of healthy sessions, error of `mechanism.handled: true` and unhandledrejections because // The 'beforeExit' event is not emitted for conditions causing explicit termination, // such as calling process.exit() or uncaught exceptions. // Ref: https://nodejs.org/api/process.html#process_event_beforeexit process.on('beforeExit', () => { - const session = hub.getScope().getSession(); + const session = getIsolationScope().getSession(); const terminalStates: SessionStatus[] = ['exited', 'crashed']; // Only call endSession, if the Session exists on Scope and SessionStatus is not a // Terminal Status i.e. Exited or Crashed because // "When a session is moved away from ok it must not be updated anymore." // Ref: https://develop.sentry.dev/sdk/sessions/ - if (session && !terminalStates.includes(session.status)) hub.endSession(); + if (session && !terminalStates.includes(session.status)) { + endSession(); + } }); } diff --git a/packages/node/test/cron.test.ts b/packages/node/test/cron.test.ts index d37fcf189926..eee6d4a66711 100644 --- a/packages/node/test/cron.test.ts +++ b/packages/node/test/cron.test.ts @@ -78,6 +78,26 @@ describe('cron check-ins', () => { }, }); }); + + test('throws with multiple jobs same name', () => { + const CronJobWithCheckIn = cron.instrumentCron(CronJobMock, 'my-cron-job'); + + CronJobWithCheckIn.from({ + cronTime: '* * * Jan,Sep Sun', + onTick: () => { + // + }, + }); + + expect(() => { + CronJobWithCheckIn.from({ + cronTime: '* * * Jan,Sep Sun', + onTick: () => { + // + }, + }); + }).toThrowError("A job named 'my-cron-job' has already been scheduled"); + }); }); describe('node-cron', () => { @@ -118,10 +138,76 @@ describe('cron check-ins', () => { const cronWithCheckIn = cron.instrumentNodeCron(nodeCron); expect(() => { + // @ts-expect-error Initially missing name cronWithCheckIn.schedule('* * * * *', () => { // }); }).toThrowError('Missing "name" for scheduled job. A name is required for Sentry check-in monitoring.'); }); }); + + describe('node-schedule', () => { + test('calls withMonitor', done => { + expect.assertions(5); + + class NodeScheduleMock { + scheduleJob( + nameOrExpression: string | Date | object, + expressionOrCallback: string | Date | object | (() => void), + callback: () => void, + ): unknown { + expect(nameOrExpression).toBe('my-cron-job'); + expect(expressionOrCallback).toBe('* * * Jan,Sep Sun'); + expect(callback).toBeInstanceOf(Function); + return callback(); + } + } + + const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock()); + + scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * Jan,Sep Sun', () => { + expect(withMonitorSpy).toHaveBeenCalledTimes(1); + expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), { + schedule: { type: 'crontab', value: '* * * 1,9 0' }, + }); + done(); + }); + }); + + test('throws without crontab string', () => { + class NodeScheduleMock { + scheduleJob(_: string, __: string | Date, ___: () => void): unknown { + return undefined; + } + } + + const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock()); + + expect(() => { + scheduleWithCheckIn.scheduleJob('my-cron-job', new Date(), () => { + // + }); + }).toThrowError( + "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string", + ); + }); + + test('throws without job name', () => { + class NodeScheduleMock { + scheduleJob(_: string, __: () => void): unknown { + return undefined; + } + } + + const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock()); + + expect(() => { + scheduleWithCheckIn.scheduleJob('* * * * *', () => { + // + }); + }).toThrowError( + "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string", + ); + }); + }); }); diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index 14de421db5f7..46683206e6fe 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -340,6 +340,7 @@ describe('tracingHandler', () => { sentryTracingMiddleware(req, res, next); + // eslint-disable-next-line deprecation/deprecation const transaction = sentryCore.getCurrentHub().getScope().getTransaction(); expect(transaction).toBeDefined(); @@ -360,6 +361,7 @@ describe('tracingHandler', () => { }); it('pulls status code from the response', done => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'mockTransaction' }); jest.spyOn(sentryCore, 'startTransaction').mockReturnValue(transaction as Transaction); const finishTransaction = jest.spyOn(transaction, 'end'); @@ -371,8 +373,11 @@ describe('tracingHandler', () => { setImmediate(() => { expect(finishTransaction).toHaveBeenCalled(); expect(transaction.status).toBe('ok'); + // eslint-disable-next-line deprecation/deprecation expect(transaction.tags).toEqual(expect.objectContaining({ 'http.status_code': '200' })); - expect(transaction.data).toEqual(expect.objectContaining({ 'http.response.status_code': 200 })); + expect(sentryCore.spanToJSON(transaction).data).toEqual( + expect.objectContaining({ 'http.response.status_code': 200 }), + ); done(); }); }); @@ -408,6 +413,7 @@ describe('tracingHandler', () => { }); it('closes the transaction when request processing is done', done => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'mockTransaction' }); jest.spyOn(sentryCore, 'startTransaction').mockReturnValue(transaction as Transaction); const finishTransaction = jest.spyOn(transaction, 'end'); @@ -422,8 +428,10 @@ describe('tracingHandler', () => { }); it('waits to finish transaction until all spans are finished, even though `transaction.end()` is registered on `res.finish` event first', done => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'mockTransaction', sampled: true }); transaction.initSpanRecorder(); + // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild({ description: 'reallyCoolHandler', op: 'middleware', @@ -448,7 +456,7 @@ describe('tracingHandler', () => { expect(finishTransaction).toHaveBeenCalled(); expect(span.endTimestamp).toBeLessThanOrEqual(transaction.endTimestamp!); expect(sentEvent.spans?.length).toEqual(1); - expect(sentEvent.spans?.[0].spanId).toEqual(span.spanId); + expect(sentEvent.spans?.[0].spanContext().spanId).toEqual(span.spanContext().spanId); done(); }); }); @@ -462,8 +470,10 @@ describe('tracingHandler', () => { sentryTracingMiddleware(req, res, next); + // eslint-disable-next-line deprecation/deprecation const transaction = sentryCore.getCurrentScope().getTransaction(); + // eslint-disable-next-line deprecation/deprecation expect(transaction?.metadata.request).toEqual(req); }); }); @@ -586,6 +596,7 @@ describe('errorHandler()', () => { // `sentryErrorMiddleware` uses `withScope`, and we need access to the temporary scope it creates, so monkeypatch // `captureException` in order to examine the scope as it exists inside the `withScope` callback + // eslint-disable-next-line deprecation/deprecation hub.captureException = function (this: sentryCore.Hub, _exception: any) { const scope = this.getScope(); expect((scope as any)._sdkProcessingMetadata.request).toEqual(req); diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index 30658128d2b4..c2e003b14d94 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -288,6 +288,7 @@ describe('SentryNode', () => { hub.bindClient(client); expect(getCurrentHub().getClient()).toBe(client); expect(getClient()).toBe(client); + // eslint-disable-next-line deprecation/deprecation hub.captureEvent({ message: 'test domain' }); }); }); diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index 42eb9391ec52..0f1ad687684d 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -1,6 +1,8 @@ import * as http from 'http'; import * as https from 'https'; -import type { Span, Transaction } from '@sentry/core'; +import type { Span } from '@sentry/core'; +import { Transaction } from '@sentry/core'; +import { startInactiveSpan } from '@sentry/core'; import * as sentryCore from '@sentry/core'; import { Hub, addTracingExtensions } from '@sentry/core'; import type { TransactionContext } from '@sentry/types'; @@ -20,6 +22,7 @@ const originalHttpRequest = http.request; describe('tracing', () => { afterEach(() => { + // eslint-disable-next-line deprecation/deprecation sentryCore.getCurrentHub().getScope().setSpan(undefined); }); @@ -36,6 +39,7 @@ describe('tracing', () => { ...customOptions, }); const hub = new Hub(new NodeClient(options)); + sentryCore.makeMain(hub); addTracingExtensions(); hub.getScope().setUser({ @@ -45,14 +49,19 @@ describe('tracing', () => { jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); + // eslint-disable-next-line deprecation/deprecation + jest.spyOn(sentryCore, 'getActiveSpan').mockImplementation(() => hub.getScope().getSpan()); jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); - const transaction = hub.startTransaction({ + const transaction = startInactiveSpan({ name: 'dogpark', traceId: '12312012123120121231201212312012', ...customContext, }); + expect(transaction).toBeInstanceOf(Transaction); + + // eslint-disable-next-line deprecation/deprecation hub.getScope().setSpan(transaction); return transaction; @@ -70,6 +79,8 @@ describe('tracing', () => { const hub = new Hub(new NodeClient(options)); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); + // eslint-disable-next-line deprecation/deprecation + jest.spyOn(sentryCore, 'getActiveSpan').mockImplementation(() => hub.getScope().getSpan()); jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); return hub; } @@ -85,7 +96,7 @@ describe('tracing', () => { expect(spans.length).toEqual(2); // our span is at index 1 because the transaction itself is at index 0 - expect(spans[1].description).toEqual('GET http://dogs.are.great/'); + expect(sentryCore.spanToJSON(spans[1]).description).toEqual('GET http://dogs.are.great/'); expect(spans[1].op).toEqual('http.client'); }); @@ -99,7 +110,7 @@ describe('tracing', () => { // only the transaction itself should be there expect(spans.length).toEqual(1); - expect((spans[0] as Transaction).name).toEqual('dogpark'); + expect(sentryCore.spanToJSON(spans[0]).description).toEqual('dogpark'); }); it('attaches the sentry-trace header to outgoing non-sentry requests', async () => { @@ -287,12 +298,15 @@ describe('tracing', () => { expect(spans.length).toEqual(2); // our span is at index 1 because the transaction itself is at index 0 - expect(spans[1].description).toEqual('GET http://dogs.are.great/spaniel'); + expect(sentryCore.spanToJSON(spans[1]).description).toEqual('GET http://dogs.are.great/spaniel'); expect(spans[1].op).toEqual('http.client'); - expect(spans[1].data['http.method']).toEqual('GET'); - expect(spans[1].data.url).toEqual('http://dogs.are.great/spaniel'); - expect(spans[1].data['http.query']).toEqual('tail=wag&cute=true'); - expect(spans[1].data['http.fragment']).toEqual('learn-more'); + + const spanAttributes = sentryCore.spanToJSON(spans[1]).data || {}; + + expect(spanAttributes['http.method']).toEqual('GET'); + expect(spanAttributes.url).toEqual('http://dogs.are.great/spaniel'); + expect(spanAttributes['http.query']).toEqual('tail=wag&cute=true'); + expect(spanAttributes['http.fragment']).toEqual('learn-more'); }); it('fills in span data from http.RequestOptions object', () => { @@ -305,13 +319,15 @@ describe('tracing', () => { expect(spans.length).toEqual(2); + const spanAttributes = sentryCore.spanToJSON(spans[1]).data || {}; + // our span is at index 1 because the transaction itself is at index 0 - expect(spans[1].description).toEqual('GET http://dogs.are.great/spaniel'); + expect(sentryCore.spanToJSON(spans[1]).description).toEqual('GET http://dogs.are.great/spaniel'); expect(spans[1].op).toEqual('http.client'); - expect(spans[1].data['http.method']).toEqual('GET'); - expect(spans[1].data.url).toEqual('http://dogs.are.great/spaniel'); - expect(spans[1].data['http.query']).toEqual('tail=wag&cute=true'); - expect(spans[1].data['http.fragment']).toEqual('learn-more'); + expect(spanAttributes['http.method']).toEqual('GET'); + expect(spanAttributes.url).toEqual('http://dogs.are.great/spaniel'); + expect(spanAttributes['http.query']).toEqual('tail=wag&cute=true'); + expect(spanAttributes['http.fragment']).toEqual('learn-more'); }); it.each([ @@ -331,7 +347,7 @@ describe('tracing', () => { expect(spans.length).toEqual(2); // our span is at index 1 because the transaction itself is at index 0 - expect(spans[1].description).toEqual(`GET http://${redactedAuth}dogs.are.great/`); + expect(sentryCore.spanToJSON(spans[1]).description).toEqual(`GET http://${redactedAuth}dogs.are.great/`); }); describe('Tracing options', () => { @@ -356,6 +372,8 @@ describe('tracing', () => { jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); + // eslint-disable-next-line deprecation/deprecation + jest.spyOn(sentryCore, 'getActiveSpan').mockImplementation(() => hub.getScope().getSpan()); jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); const client = new NodeClient(options); @@ -367,7 +385,8 @@ describe('tracing', () => { function createTransactionAndPutOnScope(hub: Hub) { addTracingExtensions(); - const transaction = hub.startTransaction({ name: 'dogpark' }); + const transaction = startInactiveSpan({ name: 'dogpark' }); + // eslint-disable-next-line deprecation/deprecation hub.getScope().setSpan(transaction); return transaction; } @@ -383,6 +402,8 @@ describe('tracing', () => { jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); + // eslint-disable-next-line deprecation/deprecation + jest.spyOn(sentryCore, 'getActiveSpan').mockImplementation(() => hub.getScope().getSpan()); jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); httpIntegration.setupOnce( @@ -492,6 +513,8 @@ describe('tracing', () => { jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); + // eslint-disable-next-line deprecation/deprecation + jest.spyOn(sentryCore, 'getActiveSpan').mockImplementation(() => hub.getScope().getSpan()); jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); httpIntegration.setupOnce( diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node/test/integrations/localvariables.test.ts index 94e3ecaea20a..e592c90a3a86 100644 --- a/packages/node/test/integrations/localvariables.test.ts +++ b/packages/node/test/integrations/localvariables.test.ts @@ -169,7 +169,7 @@ describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => { client.setupIntegrations(true); const eventProcessors = client['_eventProcessors']; - const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariablesSync'); + const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables'); expect(eventProcessor).toBeDefined(); @@ -306,7 +306,7 @@ describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => { client.setupIntegrations(true); const eventProcessors = client['_eventProcessors']; - const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariablesSync'); + const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables'); expect(eventProcessor).toBeDefined(); }); @@ -322,7 +322,7 @@ describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => { client.setupIntegrations(true); const eventProcessors = client['_eventProcessors']; - const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariablesSync'); + const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables'); expect(eventProcessor).toBeDefined(); }); diff --git a/packages/node/test/integrations/undici.test.ts b/packages/node/test/integrations/undici.test.ts index 078d90c98721..ce2f475c57d0 100644 --- a/packages/node/test/integrations/undici.test.ts +++ b/packages/node/test/integrations/undici.test.ts @@ -1,5 +1,5 @@ import * as http from 'http'; -import type { Transaction } from '@sentry/core'; +import { Transaction, getActiveSpan, startSpan } from '@sentry/core'; import { spanToTraceHeader } from '@sentry/core'; import { Hub, makeMain, runWithAsyncContext } from '@sentry/core'; import type { fetch as FetchType } from 'undici'; @@ -106,65 +106,73 @@ conditionalTest({ min: 16 })('Undici integration', () => { }, ], ])('creates a span with a %s', async (_: string, request, requestInit, expected) => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; - hub.getScope().setSpan(transaction); + await startSpan({ name: 'outer-span' }, async outerSpan => { + await fetch(request, requestInit); - await fetch(request, requestInit); + expect(outerSpan).toBeInstanceOf(Transaction); + const spans = (outerSpan as Transaction).spanRecorder?.spans || []; - expect(transaction.spanRecorder?.spans.length).toBe(2); + expect(spans.length).toBe(2); - const span = transaction.spanRecorder?.spans[1]; - expect(span).toEqual(expect.objectContaining(expected)); + const span = spans[1]; + expect(span).toEqual(expect.objectContaining(expected)); + }); }); it('creates a span with internal errors', async () => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; - hub.getScope().setSpan(transaction); + await startSpan({ name: 'outer-span' }, async outerSpan => { + try { + await fetch('http://a-url-that-no-exists.com'); + } catch (e) { + // ignore + } - try { - await fetch('http://a-url-that-no-exists.com'); - } catch (e) { - // ignore - } + expect(outerSpan).toBeInstanceOf(Transaction); + const spans = (outerSpan as Transaction).spanRecorder?.spans || []; - expect(transaction.spanRecorder?.spans.length).toBe(2); + expect(spans.length).toBe(2); - const span = transaction.spanRecorder?.spans[1]; - expect(span).toEqual(expect.objectContaining({ status: 'internal_error' })); + const span = spans[1]; + expect(span).toEqual(expect.objectContaining({ status: 'internal_error' })); + }); }); it('creates a span for invalid looking urls', async () => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; - hub.getScope().setSpan(transaction); - - try { - // Intentionally add // to the url - // fetch accepts this URL, but throws an error later on - await fetch('http://a-url-that-no-exists.com//'); - } catch (e) { - // ignore - } - - expect(transaction.spanRecorder?.spans.length).toBe(2); - - const span = transaction.spanRecorder?.spans[1]; - expect(span).toEqual(expect.objectContaining({ description: 'GET http://a-url-that-no-exists.com//' })); - expect(span).toEqual(expect.objectContaining({ status: 'internal_error' })); + await startSpan({ name: 'outer-span' }, async outerSpan => { + try { + // Intentionally add // to the url + // fetch accepts this URL, but throws an error later on + await fetch('http://a-url-that-no-exists.com//'); + } catch (e) { + // ignore + } + + expect(outerSpan).toBeInstanceOf(Transaction); + const spans = (outerSpan as Transaction).spanRecorder?.spans || []; + + expect(spans.length).toBe(2); + + const span = spans[1]; + expect(span).toEqual(expect.objectContaining({ description: 'GET http://a-url-that-no-exists.com//' })); + expect(span).toEqual(expect.objectContaining({ status: 'internal_error' })); + }); }); it('does not create a span for sentry requests', async () => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; - hub.getScope().setSpan(transaction); + await startSpan({ name: 'outer-span' }, async outerSpan => { + try { + await fetch(`${SENTRY_DSN}/sub/route`, { + method: 'POST', + }); + } catch (e) { + // ignore + } - try { - await fetch(`${SENTRY_DSN}/sub/route`, { - method: 'POST', - }); - } catch (e) { - // ignore - } + expect(outerSpan).toBeInstanceOf(Transaction); + const spans = (outerSpan as Transaction).spanRecorder?.spans || []; - expect(transaction.spanRecorder?.spans.length).toBe(1); + expect(spans.length).toBe(1); + }); }); it('does not create a span if there is no active spans', async () => { @@ -174,24 +182,26 @@ conditionalTest({ min: 16 })('Undici integration', () => { // ignore } - expect(hub.getScope().getSpan()).toBeUndefined(); + expect(getActiveSpan()).toBeUndefined(); }); it('does create a span if `shouldCreateSpanForRequest` is defined', async () => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; - hub.getScope().setSpan(transaction); + await startSpan({ name: 'outer-span' }, async outerSpan => { + expect(outerSpan).toBeInstanceOf(Transaction); + const spans = (outerSpan as Transaction).spanRecorder?.spans || []; - const undoPatch = patchUndici({ shouldCreateSpanForRequest: url => url.includes('yes') }); + const undoPatch = patchUndici({ shouldCreateSpanForRequest: url => url.includes('yes') }); - await fetch('http://localhost:18100/no', { method: 'POST' }); + await fetch('http://localhost:18100/no', { method: 'POST' }); - expect(transaction.spanRecorder?.spans.length).toBe(1); + expect(spans.length).toBe(1); - await fetch('http://localhost:18100/yes', { method: 'POST' }); + await fetch('http://localhost:18100/yes', { method: 'POST' }); - expect(transaction.spanRecorder?.spans.length).toBe(2); + expect(spans.length).toBe(2); - undoPatch(); + undoPatch(); + }); }); // This flakes on CI for some reason: https://github.com/getsentry/sentry-javascript/pull/8449 @@ -200,18 +210,22 @@ conditionalTest({ min: 16 })('Undici integration', () => { expect.assertions(3); await runWithAsyncContext(async () => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; - hub.getScope().setSpan(transaction); + await startSpan({ name: 'outer-span' }, async outerSpan => { + expect(outerSpan).toBeInstanceOf(Transaction); + const spans = (outerSpan as Transaction).spanRecorder?.spans || []; - await fetch('http://localhost:18100', { method: 'POST' }); + await fetch('http://localhost:18100', { method: 'POST' }); - expect(transaction.spanRecorder?.spans.length).toBe(2); - const span = transaction.spanRecorder?.spans[1]; + expect(spans.length).toBe(2); + const span = spans[1]; - expect(requestHeaders['sentry-trace']).toEqual(spanToTraceHeader(span!)); - expect(requestHeaders['baggage']).toEqual( - `sentry-environment=production,sentry-public_key=0,sentry-trace_id=${transaction.traceId},sentry-sample_rate=1,sentry-transaction=test-transaction`, - ); + expect(requestHeaders['sentry-trace']).toEqual(spanToTraceHeader(span!)); + expect(requestHeaders['baggage']).toEqual( + `sentry-environment=production,sentry-public_key=0,sentry-trace_id=${ + span.spanContext().traceId + },sentry-sample_rate=1,sentry-transaction=test-transaction`, + ); + }); }); }); @@ -233,59 +247,62 @@ conditionalTest({ min: 16 })('Undici integration', () => { // This flakes on CI for some reason: https://github.com/getsentry/sentry-javascript/pull/8449 // eslint-disable-next-line jest/no-disabled-tests it.skip('attaches headers if `shouldCreateSpanForRequest` does not create a span using propagation context', async () => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; const scope = hub.getScope(); const propagationContext = scope.getPropagationContext(); - scope.setSpan(transaction); + await startSpan({ name: 'outer-span' }, async outerSpan => { + expect(outerSpan).toBeInstanceOf(Transaction); - const undoPatch = patchUndici({ shouldCreateSpanForRequest: url => url.includes('yes') }); + const undoPatch = patchUndici({ shouldCreateSpanForRequest: url => url.includes('yes') }); - await fetch('http://localhost:18100/no', { method: 'POST' }); + await fetch('http://localhost:18100/no', { method: 'POST' }); - expect(requestHeaders['sentry-trace']).toBeDefined(); - expect(requestHeaders['baggage']).toBeDefined(); + 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]; + expect(requestHeaders['sentry-trace'].includes(propagationContext.traceId)).toBe(true); + const firstSpanId = requestHeaders['sentry-trace'].split('-')[1]; - await fetch('http://localhost:18100/yes', { method: 'POST' }); + await fetch('http://localhost:18100/yes', { method: 'POST' }); - expect(requestHeaders['sentry-trace']).toBeDefined(); - expect(requestHeaders['baggage']).toBeDefined(); + expect(requestHeaders['sentry-trace']).toBeDefined(); + expect(requestHeaders['baggage']).toBeDefined(); - expect(requestHeaders['sentry-trace'].includes(propagationContext.traceId)).toBe(false); + expect(requestHeaders['sentry-trace'].includes(propagationContext.traceId)).toBe(false); - const secondSpanId = requestHeaders['sentry-trace'].split('-')[1]; - expect(firstSpanId).not.toBe(secondSpanId); + const secondSpanId = requestHeaders['sentry-trace'].split('-')[1]; + expect(firstSpanId).not.toBe(secondSpanId); - undoPatch(); + undoPatch(); + }); }); // This flakes on CI for some reason: https://github.com/getsentry/sentry-javascript/pull/8449 // eslint-disable-next-line jest/no-disabled-tests it.skip('uses tracePropagationTargets', async () => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; - hub.getScope().setSpan(transaction); - const client = new NodeClient({ ...DEFAULT_OPTIONS, tracePropagationTargets: ['/yes'] }); hub.bindClient(client); - expect(transaction.spanRecorder?.spans.length).toBe(1); + await startSpan({ name: 'outer-span' }, async outerSpan => { + expect(outerSpan).toBeInstanceOf(Transaction); + const spans = (outerSpan as Transaction).spanRecorder?.spans || []; + + expect(spans.length).toBe(1); - await fetch('http://localhost:18100/no', { method: 'POST' }); + await fetch('http://localhost:18100/no', { method: 'POST' }); - expect(transaction.spanRecorder?.spans.length).toBe(2); + expect(spans.length).toBe(2); - expect(requestHeaders['sentry-trace']).toBeUndefined(); - expect(requestHeaders['baggage']).toBeUndefined(); + expect(requestHeaders['sentry-trace']).toBeUndefined(); + expect(requestHeaders['baggage']).toBeUndefined(); - await fetch('http://localhost:18100/yes', { method: 'POST' }); + await fetch('http://localhost:18100/yes', { method: 'POST' }); - expect(transaction.spanRecorder?.spans.length).toBe(3); + expect(spans.length).toBe(3); - expect(requestHeaders['sentry-trace']).toBeDefined(); - expect(requestHeaders['baggage']).toBeDefined(); + expect(requestHeaders['sentry-trace']).toBeDefined(); + expect(requestHeaders['baggage']).toBeDefined(); + }); }); it('adds a breadcrumb on request', async () => { diff --git a/packages/node/test/module.test.ts b/packages/node/test/module.test.ts index 04a6a95a7888..3fdcdccfa6eb 100644 --- a/packages/node/test/module.test.ts +++ b/packages/node/test/module.test.ts @@ -1,40 +1,33 @@ -import { getModuleFromFilename } from '../src/module'; +import { createGetModuleFromFilename } from '../src/module'; -describe('getModuleFromFilename', () => { - test('Windows', () => { - expect( - getModuleFromFilename('C:\\Users\\Tim\\node_modules\\some-dep\\module.js', 'C:\\Users\\Tim\\', true), - ).toEqual('some-dep:module'); +const getModuleFromFilenameWindows = createGetModuleFromFilename('C:\\Users\\Tim\\', true); +const getModuleFromFilenamePosix = createGetModuleFromFilename('/Users/Tim/'); - expect(getModuleFromFilename('C:\\Users\\Tim\\some\\more\\feature.js', 'C:\\Users\\Tim\\', true)).toEqual( - 'some.more:feature', +describe('createGetModuleFromFilename', () => { + test('Windows', () => { + expect(getModuleFromFilenameWindows('C:\\Users\\Tim\\node_modules\\some-dep\\module.js')).toEqual( + 'some-dep:module', ); + expect(getModuleFromFilenameWindows('C:\\Users\\Tim\\some\\more\\feature.js')).toEqual('some.more:feature'); }); test('POSIX', () => { - expect(getModuleFromFilename('/Users/Tim/node_modules/some-dep/module.js', '/Users/Tim/')).toEqual( - 'some-dep:module', - ); - - expect(getModuleFromFilename('/Users/Tim/some/more/feature.js', '/Users/Tim/')).toEqual('some.more:feature'); - expect(getModuleFromFilename('/Users/Tim/main.js', '/Users/Tim/')).toEqual('main'); + expect(getModuleFromFilenamePosix('/Users/Tim/node_modules/some-dep/module.js')).toEqual('some-dep:module'); + expect(getModuleFromFilenamePosix('/Users/Tim/some/more/feature.js')).toEqual('some.more:feature'); + expect(getModuleFromFilenamePosix('/Users/Tim/main.js')).toEqual('main'); }); test('.mjs', () => { - expect(getModuleFromFilename('/Users/Tim/node_modules/some-dep/module.mjs', '/Users/Tim/')).toEqual( - 'some-dep:module', - ); + expect(getModuleFromFilenamePosix('/Users/Tim/node_modules/some-dep/module.mjs')).toEqual('some-dep:module'); }); test('.cjs', () => { - expect(getModuleFromFilename('/Users/Tim/node_modules/some-dep/module.cjs', '/Users/Tim/')).toEqual( - 'some-dep:module', - ); + expect(getModuleFromFilenamePosix('/Users/Tim/node_modules/some-dep/module.cjs')).toEqual('some-dep:module'); }); test('node internal', () => { - expect(getModuleFromFilename('node.js', '/Users/Tim/')).toEqual('node'); - expect(getModuleFromFilename('node:internal/process/task_queues', '/Users/Tim/')).toEqual('task_queues'); - expect(getModuleFromFilename('node:internal/timers', '/Users/Tim/')).toEqual('timers'); + expect(getModuleFromFilenamePosix('node.js')).toEqual('node'); + expect(getModuleFromFilenamePosix('node:internal/process/task_queues')).toEqual('task_queues'); + expect(getModuleFromFilenamePosix('node:internal/timers')).toEqual('timers'); }); }); diff --git a/packages/opentelemetry-node/src/propagator.ts b/packages/opentelemetry-node/src/propagator.ts index ce0f295ce720..9052ede7e966 100644 --- a/packages/opentelemetry-node/src/propagator.ts +++ b/packages/opentelemetry-node/src/propagator.ts @@ -1,7 +1,7 @@ import type { Baggage, Context, TextMapGetter, TextMapSetter } from '@opentelemetry/api'; import { TraceFlags, isSpanContextValid, propagation, trace } from '@opentelemetry/api'; import { W3CBaggagePropagator, isTracingSuppressed } from '@opentelemetry/core'; -import { spanToTraceHeader } from '@sentry/core'; +import { getDynamicSamplingContextFromSpan, spanToTraceHeader } from '@sentry/core'; import { SENTRY_BAGGAGE_KEY_PREFIX, baggageHeaderToDynamicSamplingContext, @@ -36,7 +36,7 @@ export class SentryPropagator extends W3CBaggagePropagator { setter.set(carrier, SENTRY_TRACE_HEADER, spanToTraceHeader(span)); if (span.transaction) { - const dynamicSamplingContext = span.transaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(span); baggage = Object.entries(dynamicSamplingContext).reduce((b, [dscKey, dscValue]) => { if (dscValue) { return b.setEntry(`${SENTRY_BAGGAGE_KEY_PREFIX}${dscKey}`, { value: dscValue }); diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index bb2372a3c2b4..dcec2ebeef43 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -2,7 +2,14 @@ import type { Context } from '@opentelemetry/api'; import { SpanKind, context, trace } from '@opentelemetry/api'; import { suppressTracing } from '@opentelemetry/core'; import type { Span as OtelSpan, SpanProcessor as OtelSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { Transaction, addEventProcessor, addTracingExtensions, getClient, getCurrentHub } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + Transaction, + addEventProcessor, + addTracingExtensions, + getClient, + getCurrentHub, +} from '@sentry/core'; import type { DynamicSamplingContext, Span as SentrySpan, TraceparentData, TransactionContext } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -56,6 +63,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor { const sentryParentSpan = otelParentSpanId && getSentrySpan(otelParentSpanId); if (sentryParentSpan) { + // eslint-disable-next-line deprecation/deprecation const sentryChildSpan = sentryParentSpan.startChild({ description: otelSpan.name, instrumenter: 'otel', @@ -66,6 +74,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor { setSentrySpan(otelSpanId, sentryChildSpan); } else { const traceCtx = getTraceData(otelSpan, parentContext); + // eslint-disable-next-line deprecation/deprecation const transaction = getCurrentHub().startTransaction({ name: otelSpan.name, ...traceCtx, @@ -185,39 +194,35 @@ function updateSpanWithOtelData(sentrySpan: SentrySpan, otelSpan: OtelSpan): voi const { op, description, data } = parseOtelSpanDescription(otelSpan); sentrySpan.setStatus(mapOtelStatus(otelSpan)); - sentrySpan.setData('otel.kind', SpanKind[kind]); - const allData = { ...attributes, ...data }; - - Object.keys(allData).forEach(prop => { - const value = allData[prop]; - sentrySpan.setData(prop, value); - }); + const allData = { + ...attributes, + ...data, + 'otel.kind': SpanKind[kind], + }; + sentrySpan.setAttributes(allData); sentrySpan.op = op; - sentrySpan.description = description; + sentrySpan.updateName(description); } function updateTransactionWithOtelData(transaction: Transaction, otelSpan: OtelSpan): void { const { op, description, source, data } = parseOtelSpanDescription(otelSpan); + // eslint-disable-next-line deprecation/deprecation transaction.setContext('otel', { attributes: otelSpan.attributes, resource: otelSpan.resource.attributes, }); const allData = data || {}; - - Object.keys(allData).forEach(prop => { - const value = allData[prop]; - transaction.setData(prop, value); - }); + transaction.setAttributes(allData); transaction.setStatus(mapOtelStatus(otelSpan)); transaction.op = op; transaction.updateName(description); - transaction.setMetadata({ source }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); } function convertOtelTimeToSeconds([seconds, nano]: [number, number]): number { diff --git a/packages/opentelemetry-node/src/utils/spanData.ts b/packages/opentelemetry-node/src/utils/spanData.ts index 1cdbacf74955..5cec7ee0f93f 100644 --- a/packages/opentelemetry-node/src/utils/spanData.ts +++ b/packages/opentelemetry-node/src/utils/spanData.ts @@ -51,6 +51,7 @@ export function addOtelSpanData( if (sentrySpan instanceof Transaction) { if (metadata) { + // eslint-disable-next-line deprecation/deprecation sentrySpan.setMetadata(metadata); } diff --git a/packages/opentelemetry-node/src/utils/spanMap.ts b/packages/opentelemetry-node/src/utils/spanMap.ts index 8fe43222e93a..eee8e923ccbf 100644 --- a/packages/opentelemetry-node/src/utils/spanMap.ts +++ b/packages/opentelemetry-node/src/utils/spanMap.ts @@ -31,7 +31,7 @@ export function getSentrySpan(spanId: string): SentrySpan | undefined { export function setSentrySpan(spanId: string, sentrySpan: SentrySpan): void { let ref: SpanRefType = SPAN_REF_ROOT; - const rootSpanId = sentrySpan.transaction?.spanId; + const rootSpanId = sentrySpan.transaction?.spanContext().spanId; if (rootSpanId && rootSpanId !== spanId) { const root = SPAN_MAP.get(rootSpanId); diff --git a/packages/opentelemetry-node/test/propagator.test.ts b/packages/opentelemetry-node/test/propagator.test.ts index 345cd9c6eceb..22c686320e76 100644 --- a/packages/opentelemetry-node/test/propagator.test.ts +++ b/packages/opentelemetry-node/test/propagator.test.ts @@ -60,13 +60,15 @@ describe('SentryPropagator', () => { } function createTransactionAndMaybeSpan(type: PerfType, transactionContext: TransactionContext) { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction(transactionContext, hub); - setSentrySpan(transaction.spanId, transaction); + setSentrySpan(transaction.spanContext().spanId, transaction); if (type === PerfType.Span) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { spanId, ...ctx } = transactionContext; - const span = transaction.startChild({ ...ctx, description: transaction.name }); - setSentrySpan(span.spanId, span); + // eslint-disable-next-line deprecation/deprecation + const span = transaction.startChild({ ...ctx, name: transactionContext.name }); + setSentrySpan(span.spanContext().spanId, span); } } diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index 69ef554c132c..f4bc26041ceb 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -5,7 +5,15 @@ import type { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { SemanticAttributes, SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import type { SpanStatusType } from '@sentry/core'; -import { Hub, Span as SentrySpan, Transaction, addTracingExtensions, createTransport, makeMain } from '@sentry/core'; +import { + Hub, + Span as SentrySpan, + Transaction, + addTracingExtensions, + createTransport, + makeMain, + spanToJSON, +} from '@sentry/core'; import { NodeClient } from '@sentry/node'; import { resolvedSyncPromise } from '@sentry/utils'; @@ -81,11 +89,11 @@ describe('SentrySpanProcessor', () => { const sentrySpanTransaction = getSpanForOtelSpan(otelSpan) as Transaction | undefined; expect(sentrySpanTransaction).toBeInstanceOf(Transaction); - expect(sentrySpanTransaction?.name).toBe('GET /users'); + expect(spanToJSON(sentrySpanTransaction!).description).toBe('GET /users'); expect(sentrySpanTransaction?.startTimestamp).toEqual(startTimestampMs / 1000); - expect(sentrySpanTransaction?.traceId).toEqual(otelSpan.spanContext().traceId); + expect(sentrySpanTransaction?.spanContext().traceId).toEqual(otelSpan.spanContext().traceId); expect(sentrySpanTransaction?.parentSpanId).toEqual(otelSpan.parentSpanId); - expect(sentrySpanTransaction?.spanId).toEqual(otelSpan.spanContext().spanId); + expect(sentrySpanTransaction?.spanContext().spanId).toEqual(otelSpan.spanContext().spanId); otelSpan.end(endTime); @@ -109,11 +117,12 @@ describe('SentrySpanProcessor', () => { const sentrySpan = getSpanForOtelSpan(childOtelSpan); expect(sentrySpan).toBeInstanceOf(SentrySpan); - expect(sentrySpan?.description).toBe('SELECT * FROM users;'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('SELECT * FROM users;'); expect(sentrySpan?.startTimestamp).toEqual(startTimestampMs / 1000); - expect(sentrySpan?.spanId).toEqual(childOtelSpan.spanContext().spanId); - expect(sentrySpan?.parentSpanId).toEqual(sentrySpanTransaction?.spanId); + expect(sentrySpan?.spanContext().spanId).toEqual(childOtelSpan.spanContext().spanId); + expect(sentrySpan?.parentSpanId).toEqual(sentrySpanTransaction?.spanContext().spanId); + // eslint-disable-next-line deprecation/deprecation expect(hub.getScope().getSpan()).toBeUndefined(); child.end(endTime); @@ -149,11 +158,12 @@ describe('SentrySpanProcessor', () => { const sentrySpan = getSpanForOtelSpan(childOtelSpan); expect(sentrySpan).toBeInstanceOf(SentrySpan); expect(sentrySpan).toBeInstanceOf(Transaction); - expect(sentrySpan?.name).toBe('SELECT * FROM users;'); + expect(spanToJSON(sentrySpan!).description).toBe('SELECT * FROM users;'); expect(sentrySpan?.startTimestamp).toEqual(startTimestampMs / 1000); - expect(sentrySpan?.spanId).toEqual(childOtelSpan.spanContext().spanId); + expect(sentrySpan?.spanContext().spanId).toEqual(childOtelSpan.spanContext().spanId); expect(sentrySpan?.parentSpanId).toEqual(parentOtelSpan.spanContext().spanId); + // eslint-disable-next-line deprecation/deprecation expect(hub.getScope().getSpan()).toBeUndefined(); child.end(endTime); @@ -172,7 +182,7 @@ describe('SentrySpanProcessor', () => { const sentrySpanTransaction = getSpanForOtelSpan(parentOtelSpan) as Transaction | undefined; expect(sentrySpanTransaction).toBeInstanceOf(SentrySpan); - expect(sentrySpanTransaction?.name).toBe('GET /users'); + expect(spanToJSON(sentrySpanTransaction!).description).toBe('GET /users'); // Create some parallel, independent spans const span1 = tracer.startSpan('SELECT * FROM users;') as OtelSpan; @@ -183,13 +193,13 @@ describe('SentrySpanProcessor', () => { const sentrySpan2 = getSpanForOtelSpan(span2); const sentrySpan3 = getSpanForOtelSpan(span3); - expect(sentrySpan1?.parentSpanId).toEqual(sentrySpanTransaction?.spanId); - expect(sentrySpan2?.parentSpanId).toEqual(sentrySpanTransaction?.spanId); - expect(sentrySpan3?.parentSpanId).toEqual(sentrySpanTransaction?.spanId); + expect(sentrySpan1?.parentSpanId).toEqual(sentrySpanTransaction?.spanContext().spanId); + expect(sentrySpan2?.parentSpanId).toEqual(sentrySpanTransaction?.spanContext().spanId); + expect(sentrySpan3?.parentSpanId).toEqual(sentrySpanTransaction?.spanContext().spanId); - expect(sentrySpan1?.description).toEqual('SELECT * FROM users;'); - expect(sentrySpan2?.description).toEqual('SELECT * FROM companies;'); - expect(sentrySpan3?.description).toEqual('SELECT * FROM locations;'); + expect(spanToJSON(sentrySpan1!).description).toEqual('SELECT * FROM users;'); + expect(spanToJSON(sentrySpan2!).description).toEqual('SELECT * FROM companies;'); + expect(spanToJSON(sentrySpan3!).description).toEqual('SELECT * FROM locations;'); span1.end(); span2.end(); @@ -245,7 +255,7 @@ describe('SentrySpanProcessor', () => { expect(parentSpan?.endTimestamp).toBeDefined(); expect(childSpan?.endTimestamp).toBeDefined(); expect(parentSpan?.parentSpanId).toBeUndefined(); - expect(childSpan?.parentSpanId).toEqual(parentSpan?.spanId); + expect(childSpan?.parentSpanId).toEqual(parentSpan?.spanContext().spanId); }); }); }); @@ -310,11 +320,11 @@ describe('SentrySpanProcessor', () => { const sentrySpan = getSpanForOtelSpan(child); - expect(sentrySpan?.data).toEqual({}); + expect(spanToJSON(sentrySpan!).data).toEqual(undefined); child.end(); - expect(sentrySpan?.data).toEqual({ + expect(spanToJSON(sentrySpan!).data).toEqual({ 'otel.kind': 'INTERNAL', 'test-attribute': 'test-value', 'test-attribute-2': [1, 2, 3], @@ -449,12 +459,12 @@ describe('SentrySpanProcessor', () => { child.updateName('new name'); expect(sentrySpan?.op).toBe(undefined); - expect(sentrySpan?.description).toBe('SELECT * FROM users;'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('SELECT * FROM users;'); child.end(); expect(sentrySpan?.op).toBe(undefined); - expect(sentrySpan?.description).toBe('new name'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('new name'); parentOtelSpan.end(); }); @@ -508,7 +518,7 @@ describe('SentrySpanProcessor', () => { child.end(); - expect(sentrySpan?.description).toBe('HTTP GET'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('HTTP GET'); parentOtelSpan.end(); }); @@ -529,8 +539,10 @@ describe('SentrySpanProcessor', () => { child.end(); - expect(sentrySpan?.description).toBe('GET /my/route/{id}'); - expect(sentrySpan?.data).toEqual({ + const { description, data } = spanToJSON(sentrySpan!); + + expect(description).toBe('GET /my/route/{id}'); + expect(data).toEqual({ 'http.method': 'GET', 'http.route': '/my/route/{id}', 'http.target': '/my/route/123', @@ -557,8 +569,10 @@ describe('SentrySpanProcessor', () => { child.end(); - expect(sentrySpan?.description).toBe('GET http://example.com/my/route/123'); - expect(sentrySpan?.data).toEqual({ + const { description, data } = spanToJSON(sentrySpan!); + + expect(description).toBe('GET http://example.com/my/route/123'); + expect(data).toEqual({ 'http.method': 'GET', 'http.target': '/my/route/123', 'http.url': 'http://example.com/my/route/123', @@ -584,8 +598,10 @@ describe('SentrySpanProcessor', () => { child.end(); - expect(sentrySpan?.description).toBe('GET http://example.com/my/route/123'); - expect(sentrySpan?.data).toEqual({ + const { description, data } = spanToJSON(sentrySpan!); + + expect(description).toBe('GET http://example.com/my/route/123'); + expect(data).toEqual({ 'http.method': 'GET', 'http.target': '/my/route/123', 'http.url': 'http://example.com/my/route/123?what=123#myHash', @@ -611,6 +627,7 @@ describe('SentrySpanProcessor', () => { otelSpan.end(); + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.transaction?.metadata.source).toBe('url'); }); }); @@ -626,6 +643,7 @@ describe('SentrySpanProcessor', () => { otelSpan.end(); + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.transaction?.metadata.source).toBe('route'); }); }); @@ -641,6 +659,7 @@ describe('SentrySpanProcessor', () => { otelSpan.end(); + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.transaction?.metadata.source).toBe('route'); }); }); @@ -658,7 +677,7 @@ describe('SentrySpanProcessor', () => { child.end(); expect(sentrySpan?.op).toBe('db'); - expect(sentrySpan?.description).toBe('SELECT * FROM users'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('SELECT * FROM users'); parentOtelSpan.end(); }); @@ -677,7 +696,7 @@ describe('SentrySpanProcessor', () => { child.end(); expect(sentrySpan?.op).toBe('db'); - expect(sentrySpan?.description).toBe('fetch users from DB'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('fetch users from DB'); parentOtelSpan.end(); }); @@ -696,7 +715,7 @@ describe('SentrySpanProcessor', () => { child.end(); expect(sentrySpan?.op).toBe('rpc'); - expect(sentrySpan?.description).toBe('test operation'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('test operation'); parentOtelSpan.end(); }); @@ -715,7 +734,7 @@ describe('SentrySpanProcessor', () => { child.end(); expect(sentrySpan?.op).toBe('message'); - expect(sentrySpan?.description).toBe('test operation'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('test operation'); parentOtelSpan.end(); }); @@ -734,7 +753,7 @@ describe('SentrySpanProcessor', () => { child.end(); expect(sentrySpan?.op).toBe('test faas trigger'); - expect(sentrySpan?.description).toBe('test operation'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('test operation'); parentOtelSpan.end(); }); @@ -750,8 +769,8 @@ describe('SentrySpanProcessor', () => { parentOtelSpan.setAttribute(SemanticAttributes.FAAS_TRIGGER, 'test faas trigger'); parentOtelSpan.end(); - expect(transaction?.op).toBe('test faas trigger'); - expect(transaction?.name).toBe('test operation'); + expect(transaction.op).toBe('test faas trigger'); + expect(spanToJSON(transaction).description).toBe('test operation'); }); }); }); @@ -888,6 +907,7 @@ describe('SentrySpanProcessor', () => { tracer.startActiveSpan('GET /users', parentOtelSpan => { tracer.startActiveSpan('SELECT * FROM users;', child => { + // eslint-disable-next-line deprecation/deprecation hub.captureException(new Error('oh nooooo!')); otelSpan = child as OtelSpan; child.end(); diff --git a/packages/opentelemetry/src/custom/scope.ts b/packages/opentelemetry/src/custom/scope.ts index c6bdfb164900..2455d616ff39 100644 --- a/packages/opentelemetry/src/custom/scope.ts +++ b/packages/opentelemetry/src/custom/scope.ts @@ -46,6 +46,7 @@ export class OpenTelemetryScope extends Scope { newScope._attachments = [...this['_attachments']]; newScope._sdkProcessingMetadata = { ...this['_sdkProcessingMetadata'] }; newScope._propagationContext = { ...this['_propagationContext'] }; + newScope._client = this._client; return newScope; } diff --git a/packages/opentelemetry/src/custom/transaction.ts b/packages/opentelemetry/src/custom/transaction.ts index b1f84d01aec7..161901c5348e 100644 --- a/packages/opentelemetry/src/custom/transaction.ts +++ b/packages/opentelemetry/src/custom/transaction.ts @@ -11,6 +11,7 @@ export function startTransaction(hub: HubInterface, transactionContext: Transact const client = hub.getClient(); const options: Partial = (client && client.getOptions()) || {}; + // eslint-disable-next-line deprecation/deprecation const transaction = new OpenTelemetryTransaction(transactionContext, hub as Hub); // Since we do not do sampling here, we assume that this is _always_ sampled // Any sampling decision happens in OpenTelemetry's sampler diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index 8c515fc0afc9..7394e413e7ad 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -3,8 +3,8 @@ import type { ExportResult } from '@opentelemetry/core'; import { ExportResultCode } from '@opentelemetry/core'; import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { flush } from '@sentry/core'; -import type { DynamicSamplingContext, Span as SentrySpan, SpanOrigin, TransactionSource } from '@sentry/types'; +import { flush, getCurrentScope } from '@sentry/core'; +import type { DynamicSamplingContext, Scope, Span as SentrySpan, SpanOrigin, TransactionSource } from '@sentry/types'; import { logger } from '@sentry/utils'; import { getCurrentHub } from './custom/hub'; @@ -110,7 +110,7 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { // Now finish the transaction, which will send it together with all the spans // We make sure to use the finish scope - const scope = getSpanFinishScope(span); + const scope = getScopeForTransactionFinish(span); transaction.finishWithScope(convertOtelTimeToSeconds(span.endTime), scope); }); @@ -119,6 +119,17 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { .filter((span): span is ReadableSpan => !!span); } +function getScopeForTransactionFinish(span: ReadableSpan): Scope { + // The finish scope should normally always be there (and it is already a clone), + // but for the sake of type safety we fall back to a clone of the current scope + const scope = getSpanFinishScope(span) || getCurrentScope().clone(); + scope.setContext('otel', { + attributes: removeSentryAttributes(span.attributes), + resource: span.resource.attributes, + }); + return scope; +} + function getCompletedRootNodes(nodes: SpanNode[]): SpanNodeCompleted[] { return nodes.filter((node): node is SpanNodeCompleted => !!node.span && !node.parentNode); } @@ -176,11 +187,6 @@ function createTransactionForOtelSpan(span: ReadableSpan): OpenTelemetryTransact sampled: true, }) as OpenTelemetryTransaction; - transaction.setContext('otel', { - attributes: removeSentryAttributes(span.attributes), - resource: span.resource.attributes, - }); - return transaction; } @@ -204,6 +210,7 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, sentryParentSpan: Sentry const { op, description, tags, data, origin } = getSpanData(span); const allData = { ...removeSentryAttributes(attributes), ...data }; + // eslint-disable-next-line deprecation/deprecation const sentrySpan = sentryParentSpan.startChild({ description, op, diff --git a/packages/opentelemetry/src/types.ts b/packages/opentelemetry/src/types.ts index fdab000a6e09..168a9f4893a6 100644 --- a/packages/opentelemetry/src/types.ts +++ b/packages/opentelemetry/src/types.ts @@ -1,6 +1,6 @@ import type { Attributes, Span as WriteableSpan, SpanKind, TimeInput, Tracer } from '@opentelemetry/api'; import type { BasicTracerProvider, ReadableSpan, Span } from '@opentelemetry/sdk-trace-base'; -import type { SpanOrigin, TransactionMetadata, TransactionSource } from '@sentry/types'; +import type { Scope, SpanOrigin, TransactionMetadata, TransactionSource } from '@sentry/types'; export interface OpenTelemetryClient { tracer: Tracer; @@ -13,6 +13,7 @@ export interface OpenTelemetrySpanContext { metadata?: Partial; origin?: SpanOrigin; source?: TransactionSource; + scope?: Scope; // Base SpanOptions we support attributes?: Attributes; diff --git a/packages/opentelemetry/test/custom/hubextensions.test.ts b/packages/opentelemetry/test/custom/hubextensions.test.ts index 44b7b941161d..6e246763a92a 100644 --- a/packages/opentelemetry/test/custom/hubextensions.test.ts +++ b/packages/opentelemetry/test/custom/hubextensions.test.ts @@ -14,6 +14,7 @@ describe('hubextensions', () => { const mockConsole = jest.spyOn(console, 'warn').mockImplementation(() => {}); + // eslint-disable-next-line deprecation/deprecation const transaction = getCurrentHub().startTransaction({ name: 'test' }); expect(transaction).toEqual({}); diff --git a/packages/opentelemetry/test/custom/scope.test.ts b/packages/opentelemetry/test/custom/scope.test.ts index 41827fcd772d..1662f571331c 100644 --- a/packages/opentelemetry/test/custom/scope.test.ts +++ b/packages/opentelemetry/test/custom/scope.test.ts @@ -81,12 +81,14 @@ describe('NodeExperimentalScope', () => { // Pretend we have a _span set scope['_span'] = {} as any; + // eslint-disable-next-line deprecation/deprecation expect(scope.getSpan()).toBeUndefined(); }); it('setSpan is a noop', () => { const scope = new OpenTelemetryScope(); + // eslint-disable-next-line deprecation/deprecation scope.setSpan({} as any); expect(scope['_span']).toBeUndefined(); diff --git a/packages/opentelemetry/test/custom/transaction.test.ts b/packages/opentelemetry/test/custom/transaction.test.ts index 043d76235140..5b1ccc5b8044 100644 --- a/packages/opentelemetry/test/custom/transaction.test.ts +++ b/packages/opentelemetry/test/custom/transaction.test.ts @@ -1,3 +1,4 @@ +import { spanToJSON } from '@sentry/core'; import { getCurrentHub } from '../../src/custom/hub'; import { OpenTelemetryScope } from '../../src/custom/scope'; import { OpenTelemetryTransaction, startTransaction } from '../../src/custom/transaction'; @@ -16,8 +17,8 @@ describe('NodeExperimentalTransaction', () => { const hub = getCurrentHub(); hub.bindClient(client); - const transaction = new OpenTelemetryTransaction({ name: 'test' }, hub); - transaction.sampled = true; + // eslint-disable-next-line deprecation/deprecation + const transaction = new OpenTelemetryTransaction({ name: 'test', sampled: true }, hub); const res = transaction.finishWithScope(); @@ -63,8 +64,8 @@ describe('NodeExperimentalTransaction', () => { const hub = getCurrentHub(); hub.bindClient(client); - const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456 }, hub); - transaction.sampled = true; + // eslint-disable-next-line deprecation/deprecation + const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456, sampled: true }, hub); const res = transaction.finishWithScope(1234567); @@ -88,8 +89,8 @@ describe('NodeExperimentalTransaction', () => { const hub = getCurrentHub(); hub.bindClient(client); - const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456 }, hub); - transaction.sampled = true; + // eslint-disable-next-line deprecation/deprecation + const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456, sampled: true }, hub); const scope = new OpenTelemetryScope(); scope.setTags({ @@ -148,16 +149,16 @@ describe('startTranscation', () => { const transaction = startTransaction(hub, { name: 'test' }); expect(transaction).toBeInstanceOf(OpenTelemetryTransaction); - - expect(transaction.sampled).toBe(undefined); + expect(transaction['_sampled']).toBe(undefined); expect(transaction.spanRecorder).toBeDefined(); expect(transaction.spanRecorder?.spans).toHaveLength(1); + // eslint-disable-next-line deprecation/deprecation expect(transaction.metadata).toEqual({ source: 'custom', spanMetadata: {}, }); - expect(transaction.toJSON()).toEqual( + expect(spanToJSON(transaction)).toEqual( expect.objectContaining({ origin: 'manual', span_id: expect.any(String), @@ -180,13 +181,13 @@ describe('startTranscation', () => { }); expect(transaction).toBeInstanceOf(OpenTelemetryTransaction); - + // eslint-disable-next-line deprecation/deprecation expect(transaction.metadata).toEqual({ source: 'custom', spanMetadata: {}, }); - expect(transaction.toJSON()).toEqual( + expect(spanToJSON(transaction)).toEqual( expect.objectContaining({ origin: 'manual', span_id: 'span1', diff --git a/packages/opentelemetry/test/integration/breadcrumbs.test.ts b/packages/opentelemetry/test/integration/breadcrumbs.test.ts index af095be83f76..096f31c6e9bf 100644 --- a/packages/opentelemetry/test/integration/breadcrumbs.test.ts +++ b/packages/opentelemetry/test/integration/breadcrumbs.test.ts @@ -29,6 +29,7 @@ describe('Integration | breadcrumbs', () => { hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); const error = new Error('test'); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); await client.flush(); @@ -73,6 +74,7 @@ describe('Integration | breadcrumbs', () => { withScope(() => { hub.addBreadcrumb({ timestamp: 123456, message: 'test2' }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); @@ -123,6 +125,7 @@ describe('Integration | breadcrumbs', () => { hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); @@ -173,6 +176,7 @@ describe('Integration | breadcrumbs', () => { hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); @@ -215,6 +219,7 @@ describe('Integration | breadcrumbs', () => { hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); @@ -262,6 +267,7 @@ describe('Integration | breadcrumbs', () => { startSpan({ name: 'inner3' }, () => { hub.addBreadcrumb({ timestamp: 123457, message: 'test4' }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); startSpan({ name: 'inner4' }, () => { @@ -321,6 +327,7 @@ describe('Integration | breadcrumbs', () => { await new Promise(resolve => setTimeout(resolve, 10)); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts index 787d251b0558..de5193292cef 100644 --- a/packages/opentelemetry/test/integration/transactions.test.ts +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -4,6 +4,7 @@ import { addBreadcrumb, setTag } from '@sentry/core'; import type { PropagationContext, TransactionEvent } from '@sentry/types'; import { logger } from '@sentry/utils'; +import { spanToJSON } from '@sentry/core'; import { getCurrentHub } from '../../src/custom/hub'; import { SentrySpanProcessor } from '../../src/spanProcessor'; import { startInactiveSpan, startSpan } from '../../src/trace'; @@ -142,7 +143,7 @@ describe('Integration | Transactions', () => { // note: Currently, spans do not have any context/span added to them // This is the same behavior as for the "regular" SDKs - expect(spans.map(span => span.toJSON())).toEqual([ + expect(spans.map(span => spanToJSON(span))).toEqual([ { data: { 'otel.kind': 'INTERNAL' }, description: 'inner span 1', @@ -393,7 +394,7 @@ describe('Integration | Transactions', () => { // note: Currently, spans do not have any context/span added to them // This is the same behavior as for the "regular" SDKs - expect(spans.map(span => span.toJSON())).toEqual([ + expect(spans.map(span => spanToJSON(span))).toEqual([ { data: { 'otel.kind': 'INTERNAL' }, description: 'inner span 1', diff --git a/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts b/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts index 887a7f6bc803..a73067581b9a 100644 --- a/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts +++ b/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts @@ -45,6 +45,7 @@ describe('setupEventContextTrace', () => { it('works with no active span', async () => { const error = new Error('test'); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); await client.flush(); @@ -79,6 +80,7 @@ describe('setupEventContextTrace', () => { client.tracer.startActiveSpan('inner', innerSpan => { innerId = innerSpan?.spanContext().spanId; + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); }); diff --git a/packages/react/package.json b/packages/react/package.json index 437642015f78..faec21438a92 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@sentry/browser": "7.92.0", + "@sentry/core": "7.92.0", "@sentry/types": "7.92.0", "@sentry/utils": "7.92.0", "hoist-non-react-statics": "^3.3.2" diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index 749da5e23167..d3bdd1ed06aa 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -59,6 +59,7 @@ class Profiler extends React.Component { const activeTransaction = getActiveTransaction(); if (activeTransaction) { + // eslint-disable-next-line deprecation/deprecation this._mountSpan = activeTransaction.startChild({ description: `<${name}>`, op: REACT_MOUNT_OP, @@ -85,6 +86,7 @@ class Profiler extends React.Component { const changedProps = Object.keys(updateProps).filter(k => updateProps[k] !== this.props.updateProps[k]); if (changedProps.length > 0) { const now = timestampInSeconds(); + // eslint-disable-next-line deprecation/deprecation this._updateSpan = this._mountSpan.startChild({ data: { changedProps, @@ -116,6 +118,7 @@ class Profiler extends React.Component { if (this._mountSpan && includeRender) { // If we were able to obtain the spanId of the mount activity, we should set the // next activity as a child to the component mount activity. + // eslint-disable-next-line deprecation/deprecation this._mountSpan.startChild({ description: `<${name}>`, endTimestamp: timestampInSeconds(), @@ -183,6 +186,7 @@ function useProfiler( const activeTransaction = getActiveTransaction(); if (activeTransaction) { + // eslint-disable-next-line deprecation/deprecation return activeTransaction.startChild({ description: `<${name}>`, op: REACT_MOUNT_OP, @@ -201,6 +205,7 @@ function useProfiler( return (): void => { if (mountSpan && options.hasRenderSpan) { + // eslint-disable-next-line deprecation/deprecation mountSpan.startChild({ description: `<${name}>`, endTimestamp: timestampInSeconds(), @@ -222,6 +227,7 @@ export { withProfiler, Profiler, useProfiler }; export function getActiveTransaction(hub: Hub = getCurrentHub()): T | undefined { if (hub) { const scope = hub.getScope(); + // eslint-disable-next-line deprecation/deprecation return scope.getTransaction() as T | undefined; } diff --git a/packages/react/src/reactrouter.tsx b/packages/react/src/reactrouter.tsx index 8a42c5ff96f1..04995ee4bc44 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -1,4 +1,5 @@ import { WINDOW } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import type { Transaction, TransactionSource } from '@sentry/types'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; @@ -166,7 +167,7 @@ export function withSentryRouting

, R extends React const WrappedRoute: React.FC

= (props: P) => { if (activeTransaction && props && props.computedMatch && props.computedMatch.isExact) { activeTransaction.updateName(props.computedMatch.path); - activeTransaction.setMetadata({ source: 'route' }); + activeTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } // @ts-expect-error Setting more specific React Component typing for `R` generic above diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index 920a6b4f8a0d..de87e5bb6881 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -2,6 +2,7 @@ // https://gist.github.com/wontondon/e8c4bdf2888875e4c755712e99279536 import { WINDOW } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import type { Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { getNumberOfUrlSegments, logger } from '@sentry/utils'; import hoistNonReactStatics from 'hoist-non-react-statics'; @@ -136,7 +137,7 @@ function updatePageloadTransaction( if (activeTransaction && branches) { const [name, source] = getNormalizedName(routes, location, branches, basename); activeTransaction.updateName(name); - activeTransaction.setMetadata({ source }); + activeTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); } } diff --git a/packages/react/test/reactrouterv4.test.tsx b/packages/react/test/reactrouterv4.test.tsx index 2b06b3a196a5..5849bb688598 100644 --- a/packages/react/test/reactrouterv4.test.tsx +++ b/packages/react/test/reactrouterv4.test.tsx @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { act, render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; // biome-ignore lint/nursery/noUnusedImports: Need React import for JSX @@ -12,7 +13,7 @@ describe('React Router v4', () => { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; routes?: RouteConfig[]; - }): [jest.Mock, any, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetMetadata: jest.Mock }] { + }): [jest.Mock, any, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetAttribute: jest.Mock }] { const options = { matchPath: _opts && _opts.routes !== undefined ? matchPath : undefined, routes: undefined, @@ -23,16 +24,16 @@ describe('React Router v4', () => { const history = createMemoryHistory(); const mockFinish = jest.fn(); const mockUpdateName = jest.fn(); - const mockSetMetadata = jest.fn(); + const mockSetAttribute = jest.fn(); const mockStartTransaction = jest .fn() - .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setMetadata: mockSetMetadata }); + .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setAttribute: mockSetAttribute }); reactRouterV4Instrumentation(history, options.routes, options.matchPath)( mockStartTransaction, options.startTransactionOnPageLoad, options.startTransactionOnLocationChange, ); - return [mockStartTransaction, history, { mockUpdateName, mockFinish, mockSetMetadata }]; + return [mockStartTransaction, history, { mockUpdateName, mockFinish, mockSetAttribute }]; } it('starts a pageload transaction when instrumentation is started', () => { @@ -169,7 +170,7 @@ describe('React Router v4', () => { }); it('normalizes transaction name with custom Route', () => { - const [mockStartTransaction, history, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, history, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const SentryRoute = withSentryRouting(Route); const { getByText } = render( @@ -196,11 +197,11 @@ describe('React Router v4', () => { }); expect(mockUpdateName).toHaveBeenCalledTimes(2); expect(mockUpdateName).toHaveBeenLastCalledWith('/users/:userid'); - expect(mockSetMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('normalizes nested transaction names with custom Route', () => { - const [mockStartTransaction, history, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, history, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const SentryRoute = withSentryRouting(Route); const { getByText } = render( @@ -227,7 +228,7 @@ describe('React Router v4', () => { }); expect(mockUpdateName).toHaveBeenCalledTimes(2); expect(mockUpdateName).toHaveBeenLastCalledWith('/organizations/:orgid/v1/:teamid'); - expect(mockSetMetadata).toHaveBeenLastCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); act(() => { history.push('/organizations/543'); @@ -244,7 +245,7 @@ describe('React Router v4', () => { }); expect(mockUpdateName).toHaveBeenCalledTimes(3); expect(mockUpdateName).toHaveBeenLastCalledWith('/organizations/:orgid'); - expect(mockSetMetadata).toHaveBeenLastCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('matches with route object', () => { diff --git a/packages/react/test/reactrouterv5.test.tsx b/packages/react/test/reactrouterv5.test.tsx index fba57df9a5f8..c571b3590b8f 100644 --- a/packages/react/test/reactrouterv5.test.tsx +++ b/packages/react/test/reactrouterv5.test.tsx @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { act, render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; // biome-ignore lint/nursery/noUnusedImports: Need React import for JSX @@ -12,7 +13,7 @@ describe('React Router v5', () => { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; routes?: RouteConfig[]; - }): [jest.Mock, any, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetMetadata: jest.Mock }] { + }): [jest.Mock, any, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetAttribute: jest.Mock }] { const options = { matchPath: _opts && _opts.routes !== undefined ? matchPath : undefined, routes: undefined, @@ -23,16 +24,16 @@ describe('React Router v5', () => { const history = createMemoryHistory(); const mockFinish = jest.fn(); const mockUpdateName = jest.fn(); - const mockSetMetadata = jest.fn(); + const mockSetAttribute = jest.fn(); const mockStartTransaction = jest .fn() - .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setMetadata: mockSetMetadata }); + .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setAttribute: mockSetAttribute }); reactRouterV5Instrumentation(history, options.routes, options.matchPath)( mockStartTransaction, options.startTransactionOnPageLoad, options.startTransactionOnLocationChange, ); - return [mockStartTransaction, history, { mockUpdateName, mockFinish, mockSetMetadata }]; + return [mockStartTransaction, history, { mockUpdateName, mockFinish, mockSetAttribute }]; } it('starts a pageload transaction when instrumentation is started', () => { @@ -169,7 +170,7 @@ describe('React Router v5', () => { }); it('normalizes transaction name with custom Route', () => { - const [mockStartTransaction, history, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, history, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const SentryRoute = withSentryRouting(Route); const { getByText } = render( @@ -196,11 +197,11 @@ describe('React Router v5', () => { }); expect(mockUpdateName).toHaveBeenCalledTimes(2); expect(mockUpdateName).toHaveBeenLastCalledWith('/users/:userid'); - expect(mockSetMetadata).toHaveBeenLastCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('normalizes nested transaction names with custom Route', () => { - const [mockStartTransaction, history, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, history, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const SentryRoute = withSentryRouting(Route); const { getByText } = render( @@ -228,7 +229,7 @@ describe('React Router v5', () => { }); expect(mockUpdateName).toHaveBeenCalledTimes(2); expect(mockUpdateName).toHaveBeenLastCalledWith('/organizations/:orgid/v1/:teamid'); - expect(mockSetMetadata).toHaveBeenLastCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); act(() => { history.push('/organizations/543'); diff --git a/packages/react/test/reactrouterv6.4.test.tsx b/packages/react/test/reactrouterv6.4.test.tsx index a89bb50e1f82..d6b9c0c45b49 100644 --- a/packages/react/test/reactrouterv6.4.test.tsx +++ b/packages/react/test/reactrouterv6.4.test.tsx @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { render } from '@testing-library/react'; import { Request } from 'node-fetch'; import * as React from 'react'; @@ -25,7 +26,7 @@ describe('React Router v6.4', () => { function createInstrumentation(_opts?: { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; - }): [jest.Mock, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetMetadata: jest.Mock }] { + }): [jest.Mock, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetAttribute: jest.Mock }] { const options = { matchPath: _opts ? matchPath : undefined, startTransactionOnLocationChange: true, @@ -34,10 +35,10 @@ describe('React Router v6.4', () => { }; const mockFinish = jest.fn(); const mockUpdateName = jest.fn(); - const mockSetMetadata = jest.fn(); + const mockSetAttribute = jest.fn(); const mockStartTransaction = jest .fn() - .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setMetadata: mockSetMetadata }); + .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setAttribute: mockSetAttribute }); reactRouterV6Instrumentation( React.useEffect, @@ -46,7 +47,7 @@ describe('React Router v6.4', () => { createRoutesFromChildren, matchRoutes, )(mockStartTransaction, options.startTransactionOnPageLoad, options.startTransactionOnLocationChange); - return [mockStartTransaction, { mockUpdateName, mockFinish, mockSetMetadata }]; + return [mockStartTransaction, { mockUpdateName, mockFinish, mockSetAttribute }]; } describe('wrapCreateBrowserRouter', () => { @@ -246,7 +247,7 @@ describe('React Router v6.4', () => { }); it('updates pageload transaction to a parameterized route', () => { - const [mockStartTransaction, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); const router = sentryCreateBrowserRouter( @@ -272,7 +273,7 @@ describe('React Router v6.4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockUpdateName).toHaveBeenLastCalledWith('/about/:page'); - expect(mockSetMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('works with `basename` option', () => { diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index 965ce134bb74..df30c4596dbf 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { render } from '@testing-library/react'; import * as React from 'react'; import { @@ -21,7 +22,7 @@ describe('React Router v6', () => { function createInstrumentation(_opts?: { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; - }): [jest.Mock, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetMetadata: jest.Mock }] { + }): [jest.Mock, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetAttribute: jest.Mock }] { const options = { matchPath: _opts ? matchPath : undefined, startTransactionOnLocationChange: true, @@ -30,10 +31,10 @@ describe('React Router v6', () => { }; const mockFinish = jest.fn(); const mockUpdateName = jest.fn(); - const mockSetMetadata = jest.fn(); + const mockSetAttribute = jest.fn(); const mockStartTransaction = jest .fn() - .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setMetadata: mockSetMetadata }); + .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setAttribute: mockSetAttribute }); reactRouterV6Instrumentation( React.useEffect, @@ -42,7 +43,7 @@ describe('React Router v6', () => { createRoutesFromChildren, matchRoutes, )(mockStartTransaction, options.startTransactionOnPageLoad, options.startTransactionOnLocationChange); - return [mockStartTransaction, { mockUpdateName, mockFinish, mockSetMetadata }]; + return [mockStartTransaction, { mockUpdateName, mockFinish, mockSetAttribute }]; } describe('withSentryReactRouterV6Routing', () => { @@ -545,7 +546,7 @@ describe('React Router v6', () => { }); it('does not add double slashes to URLS', () => { - const [mockStartTransaction, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const wrappedUseRoutes = wrapUseRoutes(useRoutes); const Routes = () => @@ -588,11 +589,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); // should be /tests not //tests expect(mockUpdateName).toHaveBeenLastCalledWith('/tests'); - expect(mockSetMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('handles wildcard routes properly', () => { - const [mockStartTransaction, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const wrappedUseRoutes = wrapUseRoutes(useRoutes); const Routes = () => @@ -634,7 +635,7 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockUpdateName).toHaveBeenLastCalledWith('/tests/:testId/*'); - expect(mockSetMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); }); }); diff --git a/packages/remix/src/client/performance.tsx b/packages/remix/src/client/performance.tsx index 597f6daae48f..fc395e8ddedc 100644 --- a/packages/remix/src/client/performance.tsx +++ b/packages/remix/src/client/performance.tsx @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import type { ErrorBoundaryProps } from '@sentry/react'; import { WINDOW, withErrorBoundary } from '@sentry/react'; import type { Transaction, TransactionContext } from '@sentry/types'; @@ -126,7 +127,7 @@ export function withSentry

, R extends React.Co _useEffect(() => { if (activeTransaction && matches && matches.length) { activeTransaction.updateName(matches[matches.length - 1].id); - activeTransaction.setMetadata({ source: 'route' }); + activeTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } isBaseLocation = true; diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index 9bbe5f03641e..f30b5311d0f6 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -25,12 +25,14 @@ export { createTransport, // eslint-disable-next-line deprecation/deprecation extractTraceparentData, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, Hub, makeMain, Scope, + // eslint-disable-next-line deprecation/deprecation startTransaction, SDK_VERSION, setContext, diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index f557542e64ce..f5202d64570a 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -1,10 +1,13 @@ /* eslint-disable max-lines */ import { + getActiveSpan, getActiveTransaction, getClient, getCurrentScope, + getDynamicSamplingContextFromSpan, hasTracingEnabled, runWithAsyncContext, + spanToJSON, spanToTraceHeader, } from '@sentry/core'; import type { Hub } from '@sentry/node'; @@ -140,7 +143,9 @@ export async function captureRemixServerException(err: unknown, name: string, re const objectifiedErr = objectify(err); captureException(isResponse(objectifiedErr) ? await extractResponseError(objectifiedErr) : objectifiedErr, scope => { - const activeTransactionName = getActiveTransaction()?.name; + // eslint-disable-next-line deprecation/deprecation + const transaction = getActiveTransaction(); + const activeTransactionName = transaction ? spanToJSON(transaction) : undefined; scope.setSDKProcessingMetadata({ request: { @@ -181,13 +186,15 @@ function makeWrappedDocumentRequestFunction(remixVersion?: number) { loadContext?: Record, ): Promise { let res: Response; + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); try { + // eslint-disable-next-line deprecation/deprecation const span = activeTransaction?.startChild({ op: 'function.remix.document_request', origin: 'auto.function.remix', - description: activeTransaction.name, + description: spanToJSON(activeTransaction).description, tags: { method: request.method, url: request.url, @@ -235,10 +242,12 @@ function makeWrappedDataFunction( } let res: Response | AppData; + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); const currentScope = getCurrentScope(); try { + // eslint-disable-next-line deprecation/deprecation const span = activeTransaction?.startChild({ op: `function.remix.${name}`, origin: 'auto.ui.remix', @@ -250,11 +259,13 @@ function makeWrappedDataFunction( if (span) { // Assign data function to hub to be able to see `db` transactions (if any) as children. + // eslint-disable-next-line deprecation/deprecation currentScope.setSpan(span); } res = await origFn.call(this, args); + // eslint-disable-next-line deprecation/deprecation currentScope.setSpan(activeTransaction); span?.end(); } catch (err) { @@ -290,14 +301,14 @@ function getTraceAndBaggage(): { sentryTrace?: string; sentryBaggage?: string; } { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); - const currentScope = getCurrentScope(); if (isNodeEnv() && hasTracingEnabled()) { - const span = currentScope.getSpan(); + const span = getActiveSpan(); if (span && transaction) { - const dynamicSamplingContext = transaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction); return { sentryTrace: spanToTraceHeader(span), @@ -394,6 +405,8 @@ export function startRequestHandlerTransaction( ); hub.getScope().setPropagationContext(propagationContext); + // TODO: Refactor this to `startSpan()` + // eslint-disable-next-line deprecation/deprecation const transaction = hub.startTransaction({ name, op: 'http.server', @@ -408,6 +421,7 @@ export function startRequestHandlerTransaction( }, }); + // eslint-disable-next-line deprecation/deprecation hub.getScope().setSpan(transaction); return transaction; } diff --git a/packages/replay/package.json b/packages/replay/package.json index 54e8f7f11988..7cb1051110a6 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -54,8 +54,8 @@ "devDependencies": { "@babel/core": "^7.17.5", "@sentry-internal/replay-worker": "7.92.0", - "@sentry-internal/rrweb": "2.6.0", - "@sentry-internal/rrweb-snapshot": "2.6.0", + "@sentry-internal/rrweb": "2.7.3", + "@sentry-internal/rrweb-snapshot": "2.7.3", "fflate": "^0.8.1", "jsdom-worker": "^0.2.1" }, diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 75ec17f3627e..27513fe5643d 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -1,6 +1,12 @@ /* eslint-disable max-lines */ // TODO: We might want to split this file up import { EventType, record } from '@sentry-internal/rrweb'; -import { captureException, getClient, getCurrentScope } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + captureException, + getClient, + getCurrentScope, + spanToJSON, +} from '@sentry/core'; import type { ReplayRecordingMode, Transaction } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -699,12 +705,16 @@ export class ReplayContainer implements ReplayContainerInterface { * This is only available if performance is enabled, and if an instrumented router is used. */ public getCurrentRoute(): string | undefined { + // eslint-disable-next-line deprecation/deprecation const lastTransaction = this.lastTransaction || getCurrentScope().getTransaction(); - if (!lastTransaction || !['route', 'custom'].includes(lastTransaction.metadata.source)) { + + const attributes = (lastTransaction && spanToJSON(lastTransaction).data) || {}; + const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + if (!lastTransaction || !source || !['route', 'custom'].includes(source)) { return undefined; } - return lastTransaction.name; + return spanToJSON(lastTransaction).description; } /** diff --git a/packages/replay/test/utils/TestClient.ts b/packages/replay/test/utils/TestClient.ts index ad39b82084a9..da131aec8fd2 100644 --- a/packages/replay/test/utils/TestClient.ts +++ b/packages/replay/test/utils/TestClient.ts @@ -1,5 +1,11 @@ import { BaseClient, createTransport, initAndBind } from '@sentry/core'; -import type { BrowserClientReplayOptions, ClientOptions, Event, SeverityLevel } from '@sentry/types'; +import type { + BrowserClientReplayOptions, + ClientOptions, + Event, + ParameterizedString, + SeverityLevel, +} from '@sentry/types'; import { resolvedSyncPromise } from '@sentry/utils'; export interface TestClientOptions extends ClientOptions, BrowserClientReplayOptions {} @@ -24,7 +30,7 @@ export class TestClient extends BaseClient { }); } - public eventFromMessage(message: string, level: SeverityLevel = 'info'): PromiseLike { + public eventFromMessage(message: ParameterizedString, level: SeverityLevel = 'info'): PromiseLike { return resolvedSyncPromise({ message, level }); } } diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index d9cbe177efc8..9cf05355ee93 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -22,6 +22,7 @@ import { isString, logger } from '@sentry/utils'; import type { Context, Handler } from 'aws-lambda'; import { performance } from 'perf_hooks'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { AWSServices } from './awsservices'; import { DEBUG_BUILD } from './debug-build'; import { markEventUnhandled } from './utils'; @@ -223,7 +224,10 @@ function enhanceScopeWithEnvironmentData(scope: Scope, context: Context, startTi * @param context AWS Lambda context that will be used to extract some part of the data */ function enhanceScopeWithTransactionData(scope: Scope, context: Context): void { - scope.setTransactionName(context.functionName); + scope.addEventProcessor(event => { + event.transaction = context.functionName; + return event; + }); scope.setTag('server_name', process.env._AWS_XRAY_DAEMON_ADDRESS || process.env.SENTRY_NAME || hostname()); scope.setTag('url', `awslambda:///${context.functionName}`); } @@ -348,9 +352,8 @@ export function wrapHandler( op: 'function.aws.lambda', origin: 'auto.function.serverless', ...continueTraceContext, - metadata: { - ...continueTraceContext.metadata, - source: 'component', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', }, }, span => { diff --git a/packages/serverless/src/awsservices.ts b/packages/serverless/src/awsservices.ts index e7404b0695c4..41b73f58464e 100644 --- a/packages/serverless/src/awsservices.ts +++ b/packages/serverless/src/awsservices.ts @@ -58,10 +58,12 @@ function wrapMakeRequest( return function (this: TService, operation: string, params?: GenericParams, callback?: MakeRequestCallback) { let span: Span | undefined; const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation const transaction = scope.getTransaction(); const req = orig.call(this, operation, params); req.on('afterBuild', () => { if (transaction) { + // eslint-disable-next-line deprecation/deprecation span = transaction.startChild({ description: describe(this, operation, params), op: 'http.client', diff --git a/packages/serverless/src/gcpfunction/cloud_events.ts b/packages/serverless/src/gcpfunction/cloud_events.ts index cde99b9707b5..92a3eb0e37e7 100644 --- a/packages/serverless/src/gcpfunction/cloud_events.ts +++ b/packages/serverless/src/gcpfunction/cloud_events.ts @@ -1,4 +1,4 @@ -import { handleCallbackErrors } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, handleCallbackErrors } from '@sentry/core'; import { captureException, flush, getCurrentScope, startSpanManual } from '@sentry/node'; import { logger } from '@sentry/utils'; @@ -36,7 +36,7 @@ function _wrapCloudEventFunction( name: context.type || '', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component' }, }, span => { const scope = getCurrentScope(); diff --git a/packages/serverless/src/gcpfunction/events.ts b/packages/serverless/src/gcpfunction/events.ts index 539c0ee80094..79c609e9108c 100644 --- a/packages/serverless/src/gcpfunction/events.ts +++ b/packages/serverless/src/gcpfunction/events.ts @@ -1,4 +1,4 @@ -import { handleCallbackErrors } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, handleCallbackErrors } from '@sentry/core'; import { captureException, flush, getCurrentScope, startSpanManual } from '@sentry/node'; import { logger } from '@sentry/utils'; @@ -39,7 +39,7 @@ function _wrapEventFunction name: context.eventType, op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component' }, }, span => { const scope = getCurrentScope(); diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts index 609a75c81c1e..41fa620779c7 100644 --- a/packages/serverless/src/gcpfunction/http.ts +++ b/packages/serverless/src/gcpfunction/http.ts @@ -1,4 +1,4 @@ -import { Transaction, handleCallbackErrors } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, Transaction, handleCallbackErrors } from '@sentry/core'; import type { AddRequestDataToEventOptions } from '@sentry/node'; import { continueTrace, startSpanManual } from '@sentry/node'; import { getCurrentScope } from '@sentry/node'; @@ -79,10 +79,8 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial { diff --git a/packages/serverless/src/google-cloud-grpc.ts b/packages/serverless/src/google-cloud-grpc.ts index 9f2ea37203b4..74a88f622333 100644 --- a/packages/serverless/src/google-cloud-grpc.ts +++ b/packages/serverless/src/google-cloud-grpc.ts @@ -109,8 +109,10 @@ function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodName: str } let span: Span | undefined; const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation const transaction = scope.getTransaction(); if (transaction) { + // eslint-disable-next-line deprecation/deprecation span = transaction.startChild({ description: `${callType} ${methodName}`, op: `grpc.${serviceIdentifier}`, diff --git a/packages/serverless/src/google-cloud-http.ts b/packages/serverless/src/google-cloud-http.ts index 6b522facf5ae..87687a52f82e 100644 --- a/packages/serverless/src/google-cloud-http.ts +++ b/packages/serverless/src/google-cloud-http.ts @@ -53,9 +53,11 @@ function wrapRequestFunction(orig: RequestFunction): RequestFunction { return function (this: common.Service, reqOpts: RequestOptions, callback: ResponseCallback): void { let span: Span | undefined; const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation const transaction = scope.getTransaction(); if (transaction) { const httpMethod = reqOpts.method || 'GET'; + // eslint-disable-next-line deprecation/deprecation span = transaction.startChild({ description: `${httpMethod} ${reqOpts.uri}`, op: `http.client.${identifyService(this.apiEndpoint)}`, diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index 488ffec7a1ec..749909e39272 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -27,6 +27,7 @@ export { // eslint-disable-next-line deprecation/deprecation configureScope, createTransport, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getCurrentHub, getClient, @@ -41,6 +42,7 @@ export { setTag, setTags, setUser, + // eslint-disable-next-line deprecation/deprecation startTransaction, withScope, NodeClient, diff --git a/packages/serverless/test/__mocks__/@sentry/node.ts b/packages/serverless/test/__mocks__/@sentry/node.ts index d37bbbd2023c..fb929737f8d4 100644 --- a/packages/serverless/test/__mocks__/@sentry/node.ts +++ b/packages/serverless/test/__mocks__/@sentry/node.ts @@ -11,7 +11,6 @@ export const continueTrace = origSentry.continueTrace; export const fakeScope = { addEventProcessor: jest.fn(), - setTransactionName: jest.fn(), setTag: jest.fn(), setContext: jest.fn(), setSpan: jest.fn(), @@ -46,7 +45,6 @@ export const resetMocks = (): void => { fakeSpan.setHttpStatus.mockClear(); fakeScope.addEventProcessor.mockClear(); - fakeScope.setTransactionName.mockClear(); fakeScope.setTag.mockClear(); fakeScope.setContext.mockClear(); fakeScope.setSpan.mockClear(); diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index f2aa18e9cb8d..0da204b31fa4 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -1,4 +1,5 @@ // NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import * as SentryNode from '@sentry/node'; import type { Event } from '@sentry/types'; import type { Callback, Handler } from 'aws-lambda'; @@ -41,7 +42,14 @@ const fakeCallback: Callback = (err, result) => { function expectScopeSettings() { // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setTransactionName).toBeCalledWith('functionName'); + expect(SentryNode.fakeScope.addEventProcessor).toBeCalledTimes(1); + // Test than an event processor to add `transaction` is registered for the scope + // @ts-expect-error see "Why @ts-expect-error" note + const eventProcessor = SentryNode.fakeScope.addEventProcessor.mock.calls[0][0]; + const event: Event = {}; + eventProcessor(event); + expect(event).toEqual({ transaction: 'functionName' }); + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setTag).toBeCalledWith('server_name', expect.anything()); // @ts-expect-error see "Why @ts-expect-error" note @@ -185,7 +193,7 @@ describe('AWSLambda', () => { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setTransactionName).toBeCalledTimes(0); + expect(SentryNode.fakeScope.addEventProcessor).toBeCalledTimes(0); // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setTag).toBeCalledTimes(0); expect(SentryNode.startSpanManual).toBeCalledTimes(0); @@ -194,7 +202,7 @@ describe('AWSLambda', () => { describe('wrapHandler() on sync handler', () => { test('successful execution', async () => { - expect.assertions(9); + expect.assertions(10); const handler: Handler = (_event, _context, callback) => { callback(null, 42); @@ -206,7 +214,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(rv).toStrictEqual(42); @@ -218,7 +229,7 @@ describe('AWSLambda', () => { }); test('unsuccessful execution', async () => { - expect.assertions(9); + expect.assertions(10); const error = new Error('sorry'); const handler: Handler = (_event, _context, callback) => { @@ -233,7 +244,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -274,11 +288,13 @@ describe('AWSLambda', () => { origin: 'auto.function.serverless', name: 'functionName', traceId: '12312012123120121231201212312012', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, metadata: { dynamicSamplingContext: { release: '2.12.1', }, - source: 'component', }, }), expect.any(Function), @@ -292,7 +308,7 @@ describe('AWSLambda', () => { }); test('capture error', async () => { - expect.assertions(9); + expect.assertions(10); const error = new Error('wat'); const handler: Handler = (_event, _context, _callback) => { @@ -311,7 +327,10 @@ describe('AWSLambda', () => { traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', parentSampled: false, - metadata: { dynamicSamplingContext: {}, source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: { dynamicSamplingContext: {} }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -326,7 +345,7 @@ describe('AWSLambda', () => { describe('wrapHandler() on async handler', () => { test('successful execution', async () => { - expect.assertions(9); + expect.assertions(10); const handler: Handler = async (_event, _context) => { return 42; @@ -338,7 +357,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(rv).toStrictEqual(42); @@ -361,7 +383,7 @@ describe('AWSLambda', () => { }); test('capture error', async () => { - expect.assertions(9); + expect.assertions(10); const error = new Error('wat'); const handler: Handler = async (_event, _context) => { @@ -376,7 +398,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -406,7 +431,7 @@ describe('AWSLambda', () => { describe('wrapHandler() on async handler with a callback method (aka incorrect usage)', () => { test('successful execution', async () => { - expect.assertions(9); + expect.assertions(10); const handler: Handler = async (_event, _context, _callback) => { return 42; @@ -418,7 +443,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(rv).toStrictEqual(42); @@ -441,7 +469,7 @@ describe('AWSLambda', () => { }); test('capture error', async () => { - expect.assertions(9); + expect.assertions(10); const error = new Error('wat'); const handler: Handler = async (_event, _context, _callback) => { @@ -456,7 +484,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index 19a3a2565cdd..f4486415988b 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -2,6 +2,7 @@ import * as domain from 'domain'; import * as SentryNode from '@sentry/node'; import type { Event, Integration } from '@sentry/types'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import * as Sentry from '../src'; import { wrapCloudEventFunction, wrapEventFunction, wrapHttpFunction } from '../src/gcpfunction'; import type { @@ -111,7 +112,10 @@ describe('GCPFunction', () => { name: 'POST /path', op: 'function.gcp.http', origin: 'auto.function.serverless.gcp_http', - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, + metadata: {}, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -141,11 +145,13 @@ describe('GCPFunction', () => { traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', parentSampled: false, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, metadata: { dynamicSamplingContext: { release: '2.12.1', }, - source: 'route', }, }; @@ -172,7 +178,10 @@ describe('GCPFunction', () => { traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', parentSampled: false, - metadata: { dynamicSamplingContext: {}, source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, + metadata: { dynamicSamplingContext: {} }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -251,7 +260,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -272,7 +283,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -298,7 +311,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -323,7 +338,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -346,7 +363,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -367,7 +386,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -389,7 +410,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -444,7 +467,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -465,7 +490,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -488,7 +515,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -509,7 +538,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -531,7 +562,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); diff --git a/packages/svelte/src/performance.ts b/packages/svelte/src/performance.ts index e579b453f033..f0aa16d0c961 100644 --- a/packages/svelte/src/performance.ts +++ b/packages/svelte/src/performance.ts @@ -47,6 +47,7 @@ export function trackComponent(options?: TrackComponentOptions): void { } function recordInitSpan(transaction: Transaction, componentName: string): Span { + // eslint-disable-next-line deprecation/deprecation const initSpan = transaction.startChild({ op: UI_SVELTE_INIT, description: componentName, @@ -75,6 +76,7 @@ function recordUpdateSpans(componentName: string, initSpan?: Span): void { const parentSpan = initSpan && !initSpan.endTimestamp && initSpan.transaction === transaction ? initSpan : transaction; + // eslint-disable-next-line deprecation/deprecation updateSpan = parentSpan.startChild({ op: UI_SVELTE_UPDATE, description: componentName, @@ -92,5 +94,6 @@ function recordUpdateSpans(componentName: string, initSpan?: Span): void { } function getActiveTransaction(): Transaction | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentScope().getTransaction(); } diff --git a/packages/sveltekit/src/client/router.ts b/packages/sveltekit/src/client/router.ts index 0d327335326b..2b36d4adb4f2 100644 --- a/packages/sveltekit/src/client/router.ts +++ b/packages/sveltekit/src/client/router.ts @@ -1,4 +1,4 @@ -import { getActiveTransaction } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveTransaction } from '@sentry/core'; import { WINDOW } from '@sentry/svelte'; import type { Span, Transaction, TransactionContext } from '@sentry/types'; @@ -43,8 +43,8 @@ function instrumentPageload(startTransactionFn: (context: TransactionContext) => tags: { ...DEFAULT_TAGS, }, - metadata: { - source: 'url', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, }); @@ -57,7 +57,7 @@ function instrumentPageload(startTransactionFn: (context: TransactionContext) => if (pageloadTransaction && routeId) { pageloadTransaction.updateName(routeId); - pageloadTransaction.setMetadata({ source: 'route' }); + pageloadTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } }); } @@ -98,6 +98,7 @@ function instrumentNavigations(startTransactionFn: (context: TransactionContext) const parameterizedRouteOrigin = from && from.route.id; const parameterizedRouteDestination = to && to.route.id; + // eslint-disable-next-line deprecation/deprecation activeTransaction = getActiveTransaction(); if (!activeTransaction) { @@ -105,7 +106,7 @@ function instrumentNavigations(startTransactionFn: (context: TransactionContext) name: parameterizedRouteDestination || rawRouteDestination || 'unknown', op: 'navigation', origin: 'auto.navigation.sveltekit', - metadata: { source: parameterizedRouteDestination ? 'route' : 'url' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedRouteDestination ? 'route' : 'url' }, tags: { ...DEFAULT_TAGS, }, @@ -117,11 +118,13 @@ function instrumentNavigations(startTransactionFn: (context: TransactionContext) // If a routing span is still open from a previous navigation, we finish it. routingSpan.end(); } + // eslint-disable-next-line deprecation/deprecation routingSpan = activeTransaction.startChild({ op: 'ui.sveltekit.routing', description: 'SvelteKit Route Change', origin: 'auto.ui.sveltekit', }); + // eslint-disable-next-line deprecation/deprecation activeTransaction.setTag('from', parameterizedRouteOrigin); } }); diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index 540de96c6de9..1bb0c485168e 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -1,4 +1,4 @@ -import { getCurrentScope, spanToTraceHeader } from '@sentry/core'; +import { getActiveSpan, getCurrentScope, getDynamicSamplingContextFromSpan, spanToTraceHeader } from '@sentry/core'; import { getActiveTransaction, runWithAsyncContext, startSpan } from '@sentry/core'; import { captureException } from '@sentry/node'; /* eslint-disable @sentry-internal/sdk/no-optional-chaining */ @@ -95,11 +95,12 @@ export function addSentryCodeToPage(options: SentryHandleOptions): NonNullable { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (transaction) { const traceparentData = spanToTraceHeader(transaction); const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader( - transaction.getDynamicSamplingContext(), + getDynamicSamplingContextFromSpan(transaction), ); const contentMeta = ` @@ -142,7 +143,7 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { // if there is an active transaction, we know that this handle call is nested and hence // we don't create a new domain for it. If we created one, nested server calls would // create new transactions instead of adding a child span to the currently active span. - if (getCurrentScope().getSpan()) { + if (getActiveSpan()) { return instrumentHandle(input, options); } return runWithAsyncContext(() => { diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 2086b3515551..e224df7421a2 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -19,6 +19,7 @@ export { createTransport, // eslint-disable-next-line deprecation/deprecation extractTraceparentData, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, @@ -29,6 +30,7 @@ export { Hub, makeMain, Scope, + // eslint-disable-next-line deprecation/deprecation startTransaction, SDK_VERSION, setContext, diff --git a/packages/sveltekit/test/client/router.test.ts b/packages/sveltekit/test/client/router.test.ts index 648e5344e6c8..29037c28461f 100644 --- a/packages/sveltekit/test/client/router.test.ts +++ b/packages/sveltekit/test/client/router.test.ts @@ -6,6 +6,7 @@ import { vi } from 'vitest'; import { navigating, page } from '$app/stores'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { svelteKitRoutingInstrumentation } from '../../src/client/router'; // we have to overwrite the global mock from `vitest.setup.ts` here to reset the @@ -27,7 +28,7 @@ describe('sveltekitRoutingInstrumentation', () => { returnedTransaction = { ...txnCtx, updateName: vi.fn(), - setMetadata: vi.fn(), + setAttribute: vi.fn(), startChild: vi.fn().mockImplementation(ctx => { return { ...mockedRoutingSpan, ...ctx }; }), @@ -59,8 +60,8 @@ describe('sveltekitRoutingInstrumentation', () => { tags: { 'routing.instrumentation': '@sentry/sveltekit', }, - metadata: { - source: 'url', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, }); @@ -71,7 +72,7 @@ describe('sveltekitRoutingInstrumentation', () => { // This should update the transaction name with the parameterized route: expect(returnedTransaction?.updateName).toHaveBeenCalledTimes(1); expect(returnedTransaction?.updateName).toHaveBeenCalledWith('testRoute'); - expect(returnedTransaction?.setMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(returnedTransaction?.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it("doesn't start a pageload transaction if `startTransactionOnPageLoad` is false", () => { @@ -109,20 +110,20 @@ describe('sveltekitRoutingInstrumentation', () => { name: '/users/[id]', op: 'navigation', origin: 'auto.navigation.sveltekit', - metadata: { - source: 'route', - }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route' }, tags: { 'routing.instrumentation': '@sentry/sveltekit', }, }); + // eslint-disable-next-line deprecation/deprecation expect(returnedTransaction?.startChild).toHaveBeenCalledWith({ op: 'ui.sveltekit.routing', origin: 'auto.ui.sveltekit', description: 'SvelteKit Route Change', }); + // eslint-disable-next-line deprecation/deprecation expect(returnedTransaction?.setTag).toHaveBeenCalledWith('from', '/users'); // We emit `null` here to simulate the end of the navigation lifecycle @@ -160,20 +161,20 @@ describe('sveltekitRoutingInstrumentation', () => { name: '/users/[id]', op: 'navigation', origin: 'auto.navigation.sveltekit', - metadata: { - source: 'route', - }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route' }, tags: { 'routing.instrumentation': '@sentry/sveltekit', }, }); + // eslint-disable-next-line deprecation/deprecation expect(returnedTransaction?.startChild).toHaveBeenCalledWith({ op: 'ui.sveltekit.routing', origin: 'auto.ui.sveltekit', description: 'SvelteKit Route Change', }); + // eslint-disable-next-line deprecation/deprecation expect(returnedTransaction?.setTag).toHaveBeenCalledWith('from', '/users/[id]'); }); diff --git a/packages/sveltekit/test/server/handle.test.ts b/packages/sveltekit/test/server/handle.test.ts index cca809006d27..9f974a6bbdd1 100644 --- a/packages/sveltekit/test/server/handle.test.ts +++ b/packages/sveltekit/test/server/handle.test.ts @@ -358,34 +358,34 @@ describe('addSentryCodeToPage', () => { it('adds meta tags and the fetch proxy script if there is an active transaction', () => { const transformPageChunk = addSentryCodeToPage({}); - const transaction = hub.startTransaction({ name: 'test' }); - hub.getScope().setSpan(transaction); - const transformed = transformPageChunk({ html, done: true }) as string; + SentryNode.startSpan({ name: 'test' }, () => { + const transformed = transformPageChunk({ html, done: true }) as string; - expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); + expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); + }); }); it('adds a nonce attribute to the script if the `fetchProxyScriptNonce` option is specified', () => { const transformPageChunk = addSentryCodeToPage({ fetchProxyScriptNonce: '123abc' }); - const transaction = hub.startTransaction({ name: 'test' }); - hub.getScope().setSpan(transaction); - const transformed = transformPageChunk({ html, done: true }) as string; + SentryNode.startSpan({ name: 'test' }, () => { + const transformed = transformPageChunk({ html, done: true }) as string; - expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); + expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); + }); }); it('does not add the fetch proxy script if the `injectFetchProxyScript` option is false', () => { const transformPageChunk = addSentryCodeToPage({ injectFetchProxyScript: false }); - const transaction = hub.startTransaction({ name: 'test' }); - hub.getScope().setSpan(transaction); - const transformed = transformPageChunk({ html, done: true }) as string; + SentryNode.startSpan({ name: 'test' }, () => { + const transformed = transformPageChunk({ html, done: true }) as string; - expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); + expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); + }); }); }); diff --git a/packages/tracing-internal/src/browser/backgroundtab.ts b/packages/tracing-internal/src/browser/backgroundtab.ts index e13b997b16db..849f20ad20de 100644 --- a/packages/tracing-internal/src/browser/backgroundtab.ts +++ b/packages/tracing-internal/src/browser/backgroundtab.ts @@ -12,6 +12,7 @@ import { WINDOW } from './types'; export function registerBackgroundTabDetection(): void { if (WINDOW && WINDOW.document) { WINDOW.document.addEventListener('visibilitychange', () => { + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction() as IdleTransaction; if (WINDOW.document.hidden && activeTransaction) { const statusType: SpanStatusType = 'cancelled'; @@ -25,6 +26,8 @@ export function registerBackgroundTabDetection(): void { if (!activeTransaction.status) { activeTransaction.setStatus(statusType); } + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation activeTransaction.setTag('visibilitychange', 'document.hidden'); activeTransaction.end(); } diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts index 3b99dd9603d6..083e76b08ecf 100644 --- a/packages/tracing-internal/src/browser/browsertracing.ts +++ b/packages/tracing-internal/src/browser/browsertracing.ts @@ -1,6 +1,13 @@ /* eslint-disable max-lines */ import type { Hub, IdleTransaction } from '@sentry/core'; -import { TRACING_DEFAULTS, addTracingExtensions, getActiveTransaction, startIdleTransaction } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + TRACING_DEFAULTS, + addTracingExtensions, + getActiveTransaction, + spanIsSampled, + startIdleTransaction, +} from '@sentry/core'; import type { EventProcessor, Integration, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { getDomElement, logger, tracingContextFromHeaders } from '@sentry/utils'; @@ -312,6 +319,7 @@ export class BrowserTracing implements Integration { ...context, ...traceparentData, metadata: { + // eslint-disable-next-line deprecation/deprecation ...context.metadata, dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, }, @@ -325,14 +333,24 @@ export class BrowserTracing implements Integration { const finalContext = modifiedContext === undefined ? { ...expandedContext, sampled: false } : modifiedContext; // If `beforeNavigate` set a custom name, record that fact + // eslint-disable-next-line deprecation/deprecation finalContext.metadata = finalContext.name !== expandedContext.name - ? { ...finalContext.metadata, source: 'custom' } - : finalContext.metadata; + ? // eslint-disable-next-line deprecation/deprecation + { ...finalContext.metadata, source: 'custom' } + : // eslint-disable-next-line deprecation/deprecation + finalContext.metadata; this._latestRouteName = finalContext.name; - this._latestRouteSource = finalContext.metadata && finalContext.metadata.source; + // eslint-disable-next-line deprecation/deprecation + const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + // eslint-disable-next-line deprecation/deprecation + const sourceFromMetadata = finalContext.metadata && finalContext.metadata.source; + + this._latestRouteSource = sourceFromData || sourceFromMetadata; + + // eslint-disable-next-line deprecation/deprecation if (finalContext.sampled === false) { DEBUG_BUILD && logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`); } @@ -361,10 +379,10 @@ export class BrowserTracing implements Integration { // Navigation transactions should set a new propagation context based on the // created idle transaction. scope.setPropagationContext({ - traceId: idleTransaction.traceId, - spanId: idleTransaction.spanId, + traceId: idleTransaction.spanContext().traceId, + spanId: idleTransaction.spanContext().spanId, parentSpanId: idleTransaction.parentSpanId, - sampled: idleTransaction.sampled, + sampled: spanIsSampled(idleTransaction), }); } @@ -383,6 +401,7 @@ export class BrowserTracing implements Integration { const { idleTimeout, finalTimeout, heartbeatInterval } = this.options; const op = 'ui.action.click'; + // eslint-disable-next-line deprecation/deprecation const currentTransaction = getActiveTransaction(); if (currentTransaction && currentTransaction.op && ['navigation', 'pageload'].includes(currentTransaction.op)) { DEBUG_BUILD && @@ -415,8 +434,8 @@ export class BrowserTracing implements Integration { name: this._latestRouteName, op, trimEnd: true, - metadata: { - source: this._latestRouteSource || 'url', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: this._latestRouteSource || 'url', }, }; diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index 651246dfb688..df7cde3e1703 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -69,6 +69,7 @@ export function startTrackingWebVitals(): () => void { export function startTrackingLongTasks(): void { addPerformanceInstrumentationHandler('longtask', ({ entries }) => { for (const entry of entries) { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction() as IdleTransaction | undefined; if (!transaction) { return; @@ -76,6 +77,7 @@ export function startTrackingLongTasks(): void { const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); const duration = msToSec(entry.duration); + // eslint-disable-next-line deprecation/deprecation transaction.startChild({ description: 'Main UI thread blocked', op: 'ui.long-task', @@ -93,6 +95,7 @@ export function startTrackingLongTasks(): void { export function startTrackingInteractions(): void { addPerformanceInstrumentationHandler('event', ({ entries }) => { for (const entry of entries) { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction() as IdleTransaction | undefined; if (!transaction) { return; @@ -112,9 +115,10 @@ export function startTrackingInteractions(): void { const componentName = getComponentName(entry.target); if (componentName) { - span.data = { 'ui.component_name': componentName }; + span.attributes = { 'ui.component_name': componentName }; } + // eslint-disable-next-line deprecation/deprecation transaction.startChild(span); } } @@ -444,10 +448,14 @@ function _trackNavigator(transaction: Transaction): void { const connection = navigator.connection; if (connection) { if (connection.effectiveType) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('effectiveConnectionType', connection.effectiveType); } if (connection.type) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('connectionType', connection.type); } @@ -457,10 +465,14 @@ function _trackNavigator(transaction: Transaction): void { } if (isMeasurementValue(navigator.deviceMemory)) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('deviceMemory', `${navigator.deviceMemory} GB`); } if (isMeasurementValue(navigator.hardwareConcurrency)) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('hardwareConcurrency', String(navigator.hardwareConcurrency)); } } @@ -473,18 +485,26 @@ function _tagMetricInfo(transaction: Transaction): void { // Capture Properties of the LCP element that contributes to the LCP. if (_lcpEntry.element) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('lcp.element', htmlTreeAsString(_lcpEntry.element)); } if (_lcpEntry.id) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('lcp.id', _lcpEntry.id); } if (_lcpEntry.url) { // Trim URL to the first 200 characters. + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('lcp.url', _lcpEntry.url.trim().slice(0, 200)); } + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('lcp.size', _lcpEntry.size); } @@ -492,6 +512,8 @@ function _tagMetricInfo(transaction: Transaction): void { if (_clsEntry && _clsEntry.sources) { DEBUG_BUILD && logger.log('[Measurements] Adding CLS Data'); _clsEntry.sources.forEach((source, index) => + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag(`cls.source.${index + 1}`, htmlTreeAsString(source.node)), ); } diff --git a/packages/tracing-internal/src/browser/metrics/utils.ts b/packages/tracing-internal/src/browser/metrics/utils.ts index 80bf01b9c333..cebabd9abf1c 100644 --- a/packages/tracing-internal/src/browser/metrics/utils.ts +++ b/packages/tracing-internal/src/browser/metrics/utils.ts @@ -18,6 +18,7 @@ export function _startChild(transaction: Transaction, { startTimestamp, ...ctx } transaction.startTimestamp = startTimestamp; } + // eslint-disable-next-line deprecation/deprecation return transaction.startChild({ startTimestamp, ...ctx, diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index b85d54c96622..abde36a88488 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -1,9 +1,12 @@ /* eslint-disable max-lines */ import { + getActiveSpan, getClient, getCurrentScope, getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, hasTracingEnabled, + spanToJSON, spanToTraceHeader, } from '@sentry/core'; import type { HandlerDataXhr, SentryWrappedXMLHttpRequest, Span } from '@sentry/types'; @@ -145,9 +148,9 @@ function isPerformanceResourceTiming(entry: PerformanceEntry): entry is Performa * @param span A span that has yet to be finished, must contain `url` on data. */ function addHTTPTimings(span: Span): void { - const url = span.data.url; + const { url } = spanToJSON(span).data || {}; - if (!url) { + if (!url || typeof url !== 'string') { return; } @@ -155,7 +158,7 @@ function addHTTPTimings(span: Span): void { entries.forEach(entry => { if (isPerformanceResourceTiming(entry) && entry.name.endsWith(url)) { const spanData = resourceTimingEntryToSpanData(entry); - spanData.forEach(data => span.setData(...data)); + spanData.forEach(data => span.setAttribute(...data)); // In the next tick, clean this handler up // We have to wait here because otherwise this cleans itself up before it is fully done setTimeout(cleanup); @@ -271,11 +274,12 @@ export function xhrCallback( } const scope = getCurrentScope(); - const parentSpan = scope.getSpan(); + const parentSpan = getActiveSpan(); const span = shouldCreateSpanResult && parentSpan - ? parentSpan.startChild({ + ? // eslint-disable-next-line deprecation/deprecation + parentSpan.startChild({ data: { type: 'xhr', 'http.method': sentryXhrData.method, @@ -288,14 +292,14 @@ export function xhrCallback( : undefined; if (span) { - xhr.__sentry_xhr_span_id__ = span.spanId; + xhr.__sentry_xhr_span_id__ = span.spanContext().spanId; spans[xhr.__sentry_xhr_span_id__] = span; } if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url)) { if (span) { const transaction = span && span.transaction; - const dynamicSamplingContext = transaction && transaction.getDynamicSamplingContext(); + const dynamicSamplingContext = transaction && getDynamicSamplingContextFromSpan(transaction); const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); setHeaderOnXhr(xhr, spanToTraceHeader(span), sentryBaggageHeader); } else { diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts index dfc81bff05f7..14cf242cbf51 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/tracing-internal/src/common/fetch.ts @@ -1,7 +1,9 @@ import { + getActiveSpan, getClient, getCurrentScope, getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, hasTracingEnabled, spanToTraceHeader, } from '@sentry/core'; @@ -57,7 +59,7 @@ export function instrumentFetchRequest( if (contentLength) { const contentLengthNum = parseInt(contentLength); if (contentLengthNum > 0) { - span.setData('http.response_content_length', contentLengthNum); + span.setAttribute('http.response_content_length', contentLengthNum); } } } else if (handlerData.error) { @@ -73,13 +75,14 @@ export function instrumentFetchRequest( const scope = getCurrentScope(); const client = getClient(); - const parentSpan = scope.getSpan(); + const parentSpan = getActiveSpan(); const { method, url } = handlerData.fetchData; const span = shouldCreateSpanResult && parentSpan - ? parentSpan.startChild({ + ? // eslint-disable-next-line deprecation/deprecation + parentSpan.startChild({ data: { url, type: 'fetch', @@ -92,8 +95,8 @@ export function instrumentFetchRequest( : undefined; if (span) { - handlerData.fetchData.__span = span.spanId; - spans[span.spanId] = span; + handlerData.fetchData.__span = span.spanContext().spanId; + spans[span.spanContext().spanId] = span; } if (shouldAttachHeaders(handlerData.fetchData.url) && client) { @@ -128,6 +131,7 @@ export function addTracingHeadersToFetchRequest( }, requestSpan?: Span, ): PolymorphicRequestHeaders | undefined { + // eslint-disable-next-line deprecation/deprecation const span = requestSpan || scope.getSpan(); const transaction = span && span.transaction; @@ -136,7 +140,7 @@ export function addTracingHeadersToFetchRequest( const sentryTraceHeader = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled); const dynamicSamplingContext = transaction - ? transaction.getDynamicSamplingContext() + ? getDynamicSamplingContextFromSpan(transaction) : dsc ? dsc : getDynamicSamplingContextFromClient(traceId, client, scope); diff --git a/packages/tracing-internal/src/exports/index.ts b/packages/tracing-internal/src/exports/index.ts index 7b7f81a9caa1..8c10b3165608 100644 --- a/packages/tracing-internal/src/exports/index.ts +++ b/packages/tracing-internal/src/exports/index.ts @@ -1,6 +1,7 @@ export { // eslint-disable-next-line deprecation/deprecation extractTraceparentData, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, hasTracingEnabled, IdleTransaction, diff --git a/packages/tracing-internal/src/node/integrations/apollo.ts b/packages/tracing-internal/src/node/integrations/apollo.ts index f46de137680a..cb87edae9cf2 100644 --- a/packages/tracing-internal/src/node/integrations/apollo.ts +++ b/packages/tracing-internal/src/node/integrations/apollo.ts @@ -189,7 +189,9 @@ function wrapResolver( fill(model[resolverGroupName], resolverName, function (orig: () => unknown | Promise) { return function (this: unknown, ...args: unknown[]) { const scope = getCurrentHub().getScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild({ description: `${resolverGroupName}.${resolverName}`, op: 'graphql.resolve', diff --git a/packages/tracing-internal/src/node/integrations/express.ts b/packages/tracing-internal/src/node/integrations/express.ts index 7754fff3ad5b..7dabd973252e 100644 --- a/packages/tracing-internal/src/node/integrations/express.ts +++ b/packages/tracing-internal/src/node/integrations/express.ts @@ -1,4 +1,5 @@ /* eslint-disable max-lines */ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; import type { Hub, Integration, PolymorphicRequest, Transaction } from '@sentry/types'; import { GLOBAL_OBJ, @@ -157,6 +158,7 @@ function wrap(fn: Function, method: Method): (...args: any[]) => void { return function (this: NodeJS.Global, req: unknown, res: ExpressResponse & SentryTracingResponse): void { const transaction = res.__sentry_transaction; if (transaction) { + // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild({ description: fn.name, op: `middleware.express.${method}`, @@ -177,6 +179,7 @@ function wrap(fn: Function, method: Method): (...args: any[]) => void { next: () => void, ): void { const transaction = res.__sentry_transaction; + // eslint-disable-next-line deprecation/deprecation const span = transaction?.startChild({ description: fn.name, op: `middleware.express.${method}`, @@ -197,6 +200,7 @@ function wrap(fn: Function, method: Method): (...args: any[]) => void { next: () => void, ): void { const transaction = res.__sentry_transaction; + // eslint-disable-next-line deprecation/deprecation const span = transaction?.startChild({ description: fn.name, op: `middleware.express.${method}`, @@ -371,14 +375,15 @@ function instrumentRouter(appOrRouter: ExpressRouter): void { } const transaction = res.__sentry_transaction; - if (transaction && transaction.metadata.source !== 'custom') { + const attributes = (transaction && spanToJSON(transaction).data) || {}; + if (transaction && attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') { // If the request URL is '/' or empty, the reconstructed route will be empty. // Therefore, we fall back to setting the final route to '/' in this case. const finalRoute = req._reconstructedRoute || '/'; const [name, source] = extractPathForTransaction(req, { path: true, method: true, customRoute: finalRoute }); transaction.updateName(name); - transaction.setMetadata({ source }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); } } diff --git a/packages/tracing-internal/src/node/integrations/graphql.ts b/packages/tracing-internal/src/node/integrations/graphql.ts index 16773daf49b6..a87ee071af81 100644 --- a/packages/tracing-internal/src/node/integrations/graphql.ts +++ b/packages/tracing-internal/src/node/integrations/graphql.ts @@ -52,14 +52,17 @@ export class GraphQL implements LazyLoadedIntegration { fill(pkg, 'execute', function (orig: () => void | Promise) { return function (this: unknown, ...args: unknown[]) { const scope = getCurrentHub().getScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild({ description: 'execute', op: 'graphql.execute', origin: 'auto.graphql.graphql', }); + // eslint-disable-next-line deprecation/deprecation scope?.setSpan(span); const rv = orig.call(this, ...args); @@ -67,6 +70,7 @@ export class GraphQL implements LazyLoadedIntegration { if (isThenable(rv)) { return rv.then((res: unknown) => { span?.end(); + // eslint-disable-next-line deprecation/deprecation scope?.setSpan(parentSpan); return res; @@ -74,6 +78,7 @@ export class GraphQL implements LazyLoadedIntegration { } span?.end(); + // eslint-disable-next-line deprecation/deprecation scope?.setSpan(parentSpan); return rv; }; diff --git a/packages/tracing-internal/src/node/integrations/mongo.ts b/packages/tracing-internal/src/node/integrations/mongo.ts index 966231db2f74..3a91da4b327a 100644 --- a/packages/tracing-internal/src/node/integrations/mongo.ts +++ b/packages/tracing-internal/src/node/integrations/mongo.ts @@ -175,11 +175,13 @@ export class Mongo implements LazyLoadedIntegration { return function (this: unknown, ...args: unknown[]) { const lastArg = args[args.length - 1]; const scope = getCurrentHub().getScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); // Check if the operation was passed a callback. (mapReduce requires a different check, as // its (non-callback) arguments can also be functions.) if (typeof lastArg !== 'function' || (operation === 'mapReduce' && args.length === 2)) { + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild(getSpanContext(this, operation, args)); const maybePromiseOrCursor = orig.call(this, ...args); @@ -211,6 +213,7 @@ export class Mongo implements LazyLoadedIntegration { } } + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild(getSpanContext(this, operation, args.slice(0, -1))); return orig.call(this, ...args.slice(0, -1), function (err: Error, result: unknown) { diff --git a/packages/tracing-internal/src/node/integrations/mysql.ts b/packages/tracing-internal/src/node/integrations/mysql.ts index c85b0021d89a..9756b200e06b 100644 --- a/packages/tracing-internal/src/node/integrations/mysql.ts +++ b/packages/tracing-internal/src/node/integrations/mysql.ts @@ -73,7 +73,7 @@ export class Mysql implements LazyLoadedIntegration { DEBUG_BUILD && logger.error('Mysql Integration was unable to instrument `mysql` config.'); } - function spanDataFromConfig(): Record { + function spanDataFromConfig(): Record { if (!mySqlConfig) { return {}; } @@ -91,7 +91,7 @@ export class Mysql implements LazyLoadedIntegration { const data = spanDataFromConfig(); Object.keys(data).forEach(key => { - span.setData(key, data[key]); + span.setAttribute(key, data[key]); }); span.end(); @@ -104,8 +104,10 @@ export class Mysql implements LazyLoadedIntegration { fill(pkg, 'createQuery', function (orig: () => void) { return function (this: unknown, options: unknown, values: unknown, callback: unknown) { const scope = getCurrentHub().getScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild({ description: typeof options === 'string' ? options : (options as { sql: string }).sql, op: 'db', diff --git a/packages/tracing-internal/src/node/integrations/postgres.ts b/packages/tracing-internal/src/node/integrations/postgres.ts index 810f07825653..2f1128683222 100644 --- a/packages/tracing-internal/src/node/integrations/postgres.ts +++ b/packages/tracing-internal/src/node/integrations/postgres.ts @@ -105,6 +105,7 @@ export class Postgres implements LazyLoadedIntegration { fill(Client.prototype, 'query', function (orig: PgClientQuery) { return function (this: PgClientThis, config: unknown, values: unknown, callback: unknown) { const scope = getCurrentHub().getScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); const data: Record = { @@ -128,6 +129,7 @@ export class Postgres implements LazyLoadedIntegration { // ignore } + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild({ description: typeof config === 'string' ? config : (config as { text: string }).text, op: 'db', diff --git a/packages/tracing-internal/test/browser/backgroundtab.test.ts b/packages/tracing-internal/test/browser/backgroundtab.test.ts index 2687d59069c5..215a9c5d6583 100644 --- a/packages/tracing-internal/test/browser/backgroundtab.test.ts +++ b/packages/tracing-internal/test/browser/backgroundtab.test.ts @@ -1,4 +1,4 @@ -import { Hub, makeMain } from '@sentry/core'; +import { Hub, makeMain, startSpan } from '@sentry/core'; import { JSDOM } from 'jsdom'; import { addExtensionMethods } from '../../../tracing/src'; @@ -30,6 +30,7 @@ conditionalTest({ min: 10 })('registerBackgroundTabDetection', () => { afterEach(() => { events = {}; + // eslint-disable-next-line deprecation/deprecation hub.getScope().setSpan(undefined); }); @@ -47,16 +48,16 @@ conditionalTest({ min: 10 })('registerBackgroundTabDetection', () => { it('finishes a transaction on visibility change', () => { registerBackgroundTabDetection(); - const transaction = hub.startTransaction({ name: 'test' }); - hub.getScope().setSpan(transaction); - - // Simulate document visibility hidden event - // @ts-expect-error need to override global document - global.document.hidden = true; - events.visibilitychange(); - - expect(transaction.status).toBe('cancelled'); - expect(transaction.tags.visibilitychange).toBe('document.hidden'); - expect(transaction.endTimestamp).toBeDefined(); + startSpan({ name: 'test' }, span => { + // Simulate document visibility hidden event + // @ts-expect-error need to override global document + global.document.hidden = true; + events.visibilitychange(); + + expect(span?.status).toBe('cancelled'); + // eslint-disable-next-line deprecation/deprecation + expect(span?.tags.visibilitychange).toBe('document.hidden'); + expect(span?.endTimestamp).toBeDefined(); + }); }); }); diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/tracing-internal/test/browser/metrics/index.test.ts index 20444c7bf4c8..f24b6ce4b45a 100644 --- a/packages/tracing-internal/test/browser/metrics/index.test.ts +++ b/packages/tracing-internal/test/browser/metrics/index.test.ts @@ -3,8 +3,10 @@ import type { ResourceEntry } from '../../../src/browser/metrics'; import { _addMeasureSpans, _addResourceSpans } from '../../../src/browser/metrics'; describe('_addMeasureSpans', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ op: 'pageload', name: '/' }); beforeEach(() => { + // eslint-disable-next-line deprecation/deprecation transaction.startChild = jest.fn(); }); @@ -21,12 +23,12 @@ describe('_addMeasureSpans', () => { const startTime = 23; const duration = 356; - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(0); _addMeasureSpans(transaction, entry, startTime, duration, timeOrigin); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith({ description: 'measure-1', startTimestamp: timeOrigin + startTime, @@ -38,8 +40,10 @@ describe('_addMeasureSpans', () => { }); describe('_addResourceSpans', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ op: 'pageload', name: '/' }); beforeEach(() => { + // eslint-disable-next-line deprecation/deprecation transaction.startChild = jest.fn(); }); @@ -54,7 +58,7 @@ describe('_addResourceSpans', () => { }; _addResourceSpans(transaction, entry, '/assets/to/me', 123, 456, 100); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(0); }); @@ -68,7 +72,7 @@ describe('_addResourceSpans', () => { }; _addResourceSpans(transaction, entry, '/assets/to/me', 123, 456, 100); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(0); }); @@ -87,9 +91,9 @@ describe('_addResourceSpans', () => { _addResourceSpans(transaction, entry, '/assets/to/css', startTime, duration, timeOrigin); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith({ data: { ['http.decoded_response_content_length']: entry.decodedBodySize, @@ -135,7 +139,7 @@ describe('_addResourceSpans', () => { }; _addResourceSpans(transaction, entry, '/assets/to/me', 123, 234, 465); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ op, @@ -155,9 +159,9 @@ describe('_addResourceSpans', () => { _addResourceSpans(transaction, entry, '/assets/to/css', 100, 23, 345); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ data: { @@ -180,9 +184,9 @@ describe('_addResourceSpans', () => { _addResourceSpans(transaction, entry, '/assets/to/css', 100, 23, 345); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ data: {}, @@ -202,9 +206,9 @@ describe('_addResourceSpans', () => { _addResourceSpans(transaction, entry, '/assets/to/css', 100, 23, 345); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ data: {}, diff --git a/packages/tracing-internal/test/browser/metrics/utils.test.ts b/packages/tracing-internal/test/browser/metrics/utils.test.ts index 25f03c11af0b..ae614abc41a6 100644 --- a/packages/tracing-internal/test/browser/metrics/utils.test.ts +++ b/packages/tracing-internal/test/browser/metrics/utils.test.ts @@ -1,8 +1,10 @@ +import { spanToJSON } from '@sentry/core'; import { Span, Transaction } from '../../../src'; import { _startChild } from '../../../src/browser/metrics/utils'; describe('_startChild()', () => { it('creates a span with given properties', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'test' }); const span = _startChild(transaction, { description: 'evaluation', @@ -10,11 +12,12 @@ describe('_startChild()', () => { }); expect(span).toBeInstanceOf(Span); - expect(span.description).toBe('evaluation'); + expect(spanToJSON(span).description).toBe('evaluation'); expect(span.op).toBe('script'); }); it('adjusts the start timestamp if child span starts before transaction', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'test', startTimestamp: 123 }); const span = _startChild(transaction, { description: 'script.js', @@ -27,6 +30,7 @@ describe('_startChild()', () => { }); it('does not adjust start timestamp if child span starts after transaction', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'test', startTimestamp: 123 }); const span = _startChild(transaction, { description: 'script.js', diff --git a/packages/tracing-internal/test/browser/request.test.ts b/packages/tracing-internal/test/browser/request.test.ts index 0f3ce191278a..3dabe104c8f6 100644 --- a/packages/tracing-internal/test/browser/request.test.ts +++ b/packages/tracing-internal/test/browser/request.test.ts @@ -256,7 +256,7 @@ describe('callbacks', () => { expect(finishedSpan).toBeDefined(); expect(finishedSpan).toBeInstanceOf(Span); - expect(finishedSpan.data).toEqual({ + expect(sentryCore.spanToJSON(finishedSpan).data).toEqual({ 'http.response_content_length': 123, 'http.method': 'GET', 'http.response.status_code': 404, diff --git a/packages/tracing-internal/test/utils/TestClient.ts b/packages/tracing-internal/test/utils/TestClient.ts index ad39b82084a9..da131aec8fd2 100644 --- a/packages/tracing-internal/test/utils/TestClient.ts +++ b/packages/tracing-internal/test/utils/TestClient.ts @@ -1,5 +1,11 @@ import { BaseClient, createTransport, initAndBind } from '@sentry/core'; -import type { BrowserClientReplayOptions, ClientOptions, Event, SeverityLevel } from '@sentry/types'; +import type { + BrowserClientReplayOptions, + ClientOptions, + Event, + ParameterizedString, + SeverityLevel, +} from '@sentry/types'; import { resolvedSyncPromise } from '@sentry/utils'; export interface TestClientOptions extends ClientOptions, BrowserClientReplayOptions {} @@ -24,7 +30,7 @@ export class TestClient extends BaseClient { }); } - public eventFromMessage(message: string, level: SeverityLevel = 'info'): PromiseLike { + public eventFromMessage(message: ParameterizedString, level: SeverityLevel = 'info'): PromiseLike { return resolvedSyncPromise({ message, level }); } } diff --git a/packages/tracing/src/index.ts b/packages/tracing/src/index.ts index a515db240117..8559188884d7 100644 --- a/packages/tracing/src/index.ts +++ b/packages/tracing/src/index.ts @@ -62,6 +62,7 @@ export const addExtensionMethods = addExtensionMethodsT; * * `getActiveTransaction` can be imported from `@sentry/node`, `@sentry/browser`, or your framework SDK */ +// eslint-disable-next-line deprecation/deprecation export const getActiveTransaction = getActiveTransactionT; /** diff --git a/packages/tracing/test/hub.test.ts b/packages/tracing/test/hub.test.ts index f7cf93cc5e32..a7fbf5e86b62 100644 --- a/packages/tracing/test/hub.test.ts +++ b/packages/tracing/test/hub.test.ts @@ -1,7 +1,7 @@ /* eslint-disable deprecation/deprecation */ /* eslint-disable @typescript-eslint/unbound-method */ import { BrowserClient } from '@sentry/browser'; -import { Hub, makeMain } from '@sentry/core'; +import { Hub, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, makeMain } from '@sentry/core'; import * as utilsModule from '@sentry/utils'; // for mocking import { logger } from '@sentry/utils'; @@ -16,7 +16,7 @@ import { addExtensionMethods(); const mathRandom = jest.spyOn(Math, 'random'); -jest.spyOn(Transaction.prototype, 'setMetadata'); +jest.spyOn(Transaction.prototype, 'setAttribute'); jest.spyOn(logger, 'warn'); jest.spyOn(logger, 'log'); jest.spyOn(logger, 'error'); @@ -286,9 +286,7 @@ describe('Hub', () => { makeMain(hub); hub.startTransaction({ name: 'dogpark', sampled: true }); - expect(Transaction.prototype.setMetadata).toHaveBeenCalledWith({ - sampleRate: 1.0, - }); + expect(Transaction.prototype.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, 1); }); it('should record sampling method and rate when sampling decision comes from tracesSampler', () => { @@ -298,9 +296,7 @@ describe('Hub', () => { makeMain(hub); hub.startTransaction({ name: 'dogpark' }); - expect(Transaction.prototype.setMetadata).toHaveBeenCalledWith({ - sampleRate: 0.1121, - }); + expect(Transaction.prototype.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, 0.1121); }); it('should record sampling method when sampling decision is inherited', () => { @@ -309,7 +305,7 @@ describe('Hub', () => { makeMain(hub); hub.startTransaction({ name: 'dogpark', parentSampled: true }); - expect(Transaction.prototype.setMetadata).toHaveBeenCalledTimes(0); + expect(Transaction.prototype.setAttribute).toHaveBeenCalledTimes(0); }); it('should record sampling method and rate when sampling decision comes from traceSampleRate', () => { @@ -318,9 +314,7 @@ describe('Hub', () => { makeMain(hub); hub.startTransaction({ name: 'dogpark' }); - expect(Transaction.prototype.setMetadata).toHaveBeenCalledWith({ - sampleRate: 0.1121, - }); + expect(Transaction.prototype.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, 0.1121); }); }); diff --git a/packages/tracing/test/idletransaction.test.ts b/packages/tracing/test/idletransaction.test.ts index e0c5dd189cff..f5f7e92595c5 100644 --- a/packages/tracing/test/idletransaction.test.ts +++ b/packages/tracing/test/idletransaction.test.ts @@ -1,7 +1,15 @@ +/* eslint-disable deprecation/deprecation */ import { BrowserClient } from '@sentry/browser'; -import { TRACING_DEFAULTS, Transaction } from '@sentry/core'; - -import { Hub, IdleTransaction, Span } from '../../core/src'; +import { + TRACING_DEFAULTS, + Transaction, + getCurrentScope, + startInactiveSpan, + startSpan, + startSpanManual, +} from '@sentry/core'; + +import { Hub, IdleTransaction, Span, makeMain } from '../../core/src'; import { IdleTransactionSpanRecorder } from '../../core/src/tracing/idletransaction'; import { getDefaultBrowserClientOptions } from './testutils'; @@ -10,6 +18,7 @@ let hub: Hub; beforeEach(() => { const options = getDefaultBrowserClientOptions({ dsn, tracesSampleRate: 1 }); hub = new Hub(new BrowserClient(options)); + makeMain(hub); }); describe('IdleTransaction', () => { @@ -26,6 +35,7 @@ describe('IdleTransaction', () => { transaction.initSpanRecorder(10); const scope = hub.getScope(); + // eslint-disable-next-line deprecation/deprecation expect(scope.getTransaction()).toBe(transaction); }); @@ -34,6 +44,7 @@ describe('IdleTransaction', () => { transaction.initSpanRecorder(10); const scope = hub.getScope(); + // eslint-disable-next-line deprecation/deprecation expect(scope.getTransaction()).toBe(undefined); }); @@ -52,6 +63,7 @@ describe('IdleTransaction', () => { jest.runAllTimers(); const scope = hub.getScope(); + // eslint-disable-next-line deprecation/deprecation expect(scope.getTransaction()).toBe(undefined); }); @@ -69,6 +81,7 @@ describe('IdleTransaction', () => { jest.runAllTimers(); const scope = hub.getScope(); + // eslint-disable-next-line deprecation/deprecation expect(scope.getTransaction()).toBe(undefined); }); @@ -84,13 +97,16 @@ describe('IdleTransaction', () => { transaction.initSpanRecorder(10); // @ts-expect-error need to pass in hub + // eslint-disable-next-line deprecation/deprecation const otherTransaction = new Transaction({ name: 'bar' }, hub); + // eslint-disable-next-line deprecation/deprecation hub.getScope().setSpan(otherTransaction); transaction.end(); jest.runAllTimers(); const scope = hub.getScope(); + // eslint-disable-next-line deprecation/deprecation expect(scope.getTransaction()).toBe(otherTransaction); }); }); @@ -104,9 +120,11 @@ describe('IdleTransaction', () => { const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - const span = transaction.startChild(); - expect(transaction.activities).toMatchObject({ [span.spanId]: true }); + const span = startInactiveSpan({ name: 'inner' })!; + expect(transaction.activities).toMatchObject({ [span.spanContext().spanId]: true }); expect(mockFinish).toHaveBeenCalledTimes(0); @@ -121,8 +139,10 @@ describe('IdleTransaction', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - transaction.startChild({ startTimestamp: 1234, endTimestamp: 5678 }); + startInactiveSpan({ name: 'inner', startTimestamp: 1234, endTimestamp: 5678 }); expect(transaction.activities).toMatchObject({}); }); @@ -131,16 +151,21 @@ describe('IdleTransaction', () => { const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); + + startSpanManual({ name: 'inner1' }, span => { + const childSpan = startInactiveSpan({ name: 'inner2' })!; + expect(transaction.activities).toMatchObject({ + [span!.spanContext().spanId]: true, + [childSpan.spanContext().spanId]: true, + }); + span?.end(); + jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout + 1); - const span = transaction.startChild(); - const childSpan = span.startChild(); - - expect(transaction.activities).toMatchObject({ [span.spanId]: true, [childSpan.spanId]: true }); - span.end(); - jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout + 1); - - expect(mockFinish).toHaveBeenCalledTimes(0); - expect(transaction.activities).toMatchObject({ [childSpan.spanId]: true }); + expect(mockFinish).toHaveBeenCalledTimes(0); + expect(transaction.activities).toMatchObject({ [childSpan.spanContext().spanId]: true }); + }); }); it('calls beforeFinish callback before finishing', () => { @@ -150,12 +175,13 @@ describe('IdleTransaction', () => { transaction.initSpanRecorder(10); transaction.registerBeforeFinishCallback(mockCallback1); transaction.registerBeforeFinishCallback(mockCallback2); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); expect(mockCallback1).toHaveBeenCalledTimes(0); expect(mockCallback2).toHaveBeenCalledTimes(0); - const span = transaction.startChild(); - span.end(); + startSpan({ name: 'inner' }, () => {}); jest.runOnlyPendingTimers(); expect(mockCallback1).toHaveBeenCalledTimes(1); @@ -167,15 +193,17 @@ describe('IdleTransaction', () => { it('filters spans on finish', () => { const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); // regular child - should be kept - const regularSpan = transaction.startChild({ startTimestamp: transaction.startTimestamp + 2 }); + const regularSpan = startInactiveSpan({ name: 'span1', startTimestamp: transaction.startTimestamp + 2 })!; // discardedSpan - startTimestamp is too large - transaction.startChild({ startTimestamp: 645345234 }); + startInactiveSpan({ name: 'span2', startTimestamp: 645345234 }); // Should be cancelled - will not finish - const cancelledSpan = transaction.startChild({ startTimestamp: transaction.startTimestamp + 4 }); + const cancelledSpan = startInactiveSpan({ name: 'span3', startTimestamp: transaction.startTimestamp + 4 })!; regularSpan.end(regularSpan.startTimestamp + 4); transaction.end(transaction.startTimestamp + 10); @@ -184,14 +212,14 @@ describe('IdleTransaction', () => { if (transaction.spanRecorder) { const spans = transaction.spanRecorder.spans; expect(spans).toHaveLength(3); - expect(spans[0].spanId).toBe(transaction.spanId); + expect(spans[0].spanContext().spanId).toBe(transaction.spanContext().spanId); // Regular Span - should not modified - expect(spans[1].spanId).toBe(regularSpan.spanId); + expect(spans[1].spanContext().spanId).toBe(regularSpan.spanContext().spanId); expect(spans[1].endTimestamp).not.toBe(transaction.endTimestamp); // Cancelled Span - has endtimestamp of transaction - expect(spans[2].spanId).toBe(cancelledSpan.spanId); + expect(spans[2].spanContext().spanId).toBe(cancelledSpan.spanContext().spanId); expect(spans[2].status).toBe('cancelled'); expect(spans[2].endTimestamp).toBe(transaction.endTimestamp); } @@ -200,8 +228,10 @@ describe('IdleTransaction', () => { it('filters out spans that exceed final timeout', () => { const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, 1000, 3000); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - const span = transaction.startChild({ startTimestamp: transaction.startTimestamp + 2 }); + const span = startInactiveSpan({ name: 'span', startTimestamp: transaction.startTimestamp + 2 })!; span.end(span.startTimestamp + 10 + 30 + 1); transaction.end(transaction.startTimestamp + 50); @@ -235,7 +265,10 @@ describe('IdleTransaction', () => { it('does not finish if a activity is started', () => { const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub); transaction.initSpanRecorder(10); - transaction.startChild({}); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); + + startInactiveSpan({ name: 'span' }); jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); expect(transaction.endTimestamp).toBeUndefined(); @@ -245,14 +278,14 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - const span = transaction.startChild({}); - span.end(); + startSpan({ name: 'span1' }, () => {}); jest.advanceTimersByTime(2); - const span2 = transaction.startChild({}); - span2.end(); + startSpan({ name: 'span2' }, () => {}); jest.advanceTimersByTime(8); @@ -263,14 +296,14 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - const span = transaction.startChild({}); - span.end(); + startSpan({ name: 'span1' }, () => {}); jest.advanceTimersByTime(2); - const span2 = transaction.startChild({}); - span2.end(); + startSpan({ name: 'span2' }, () => {}); jest.advanceTimersByTime(10); @@ -283,10 +316,12 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - const firstSpan = transaction.startChild({}); + const firstSpan = startInactiveSpan({ name: 'span1' })!; transaction.cancelIdleTimeout(undefined, { restartOnChildSpanChange: false }); - const secondSpan = transaction.startChild({}); + const secondSpan = startInactiveSpan({ name: 'span2' })!; firstSpan.end(); secondSpan.end(); @@ -297,11 +332,13 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - const firstSpan = transaction.startChild({}); + const firstSpan = startInactiveSpan({ name: 'span1' })!; transaction.cancelIdleTimeout(undefined, { restartOnChildSpanChange: false }); - const secondSpan = transaction.startChild({}); - const thirdSpan = transaction.startChild({}); + const secondSpan = startInactiveSpan({ name: 'span2' })!; + const thirdSpan = startInactiveSpan({ name: 'span3' })!; firstSpan.end(); expect(transaction.endTimestamp).toBeUndefined(); @@ -317,9 +354,10 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - const span = transaction.startChild({}); - span.end(); + startSpan({ name: 'span' }, () => {}); jest.advanceTimersByTime(2); @@ -332,16 +370,16 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); - const span = transaction.startChild({}); - span.end(); + startSpan({ name: 'span' }, () => {}); jest.advanceTimersByTime(2); transaction.cancelIdleTimeout(); - const span2 = transaction.startChild({}); - span2.end(); + startSpan({ name: 'span' }, () => {}); jest.advanceTimersByTime(8); expect(transaction.endTimestamp).toBeUndefined(); @@ -380,9 +418,11 @@ describe('IdleTransaction', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub, TRACING_DEFAULTS.idleTimeout); const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); expect(mockFinish).toHaveBeenCalledTimes(0); - transaction.startChild({}); + startInactiveSpan({ name: 'span' }); // Beat 1 jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); @@ -401,15 +441,17 @@ describe('IdleTransaction', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub, TRACING_DEFAULTS.idleTimeout, 50000); const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation + getCurrentScope().setSpan(transaction); expect(mockFinish).toHaveBeenCalledTimes(0); - transaction.startChild({}); + startInactiveSpan({ name: 'span' }); // Beat 1 jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); expect(mockFinish).toHaveBeenCalledTimes(0); - const span = transaction.startChild(); // push activity + const span = startInactiveSpan({ name: 'span' })!; // push activity // Beat 1 jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); @@ -419,8 +461,8 @@ describe('IdleTransaction', () => { jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); expect(mockFinish).toHaveBeenCalledTimes(0); - transaction.startChild(); // push activity - transaction.startChild(); // push activity + startInactiveSpan({ name: 'span' }); // push activity + startInactiveSpan({ name: 'span' }); // push activity // Beat 1 jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); @@ -466,13 +508,13 @@ describe('IdleTransactionSpanRecorder', () => { expect(spanRecorder.spans).toHaveLength(1); expect(mockPushActivity).toHaveBeenCalledTimes(1); - expect(mockPushActivity).toHaveBeenLastCalledWith(span.spanId); + expect(mockPushActivity).toHaveBeenLastCalledWith(span.spanContext().spanId); expect(mockPopActivity).toHaveBeenCalledTimes(0); span.end(); expect(mockPushActivity).toHaveBeenCalledTimes(1); expect(mockPopActivity).toHaveBeenCalledTimes(1); - expect(mockPushActivity).toHaveBeenLastCalledWith(span.spanId); + expect(mockPushActivity).toHaveBeenLastCalledWith(span.spanContext().spanId); }); it('does not push activities if a span has a timestamp', () => { @@ -491,7 +533,12 @@ describe('IdleTransactionSpanRecorder', () => { const mockPopActivity = jest.fn(); const transaction = new IdleTransaction({ name: 'foo' }, hub); - const spanRecorder = new IdleTransactionSpanRecorder(mockPushActivity, mockPopActivity, transaction.spanId, 10); + const spanRecorder = new IdleTransactionSpanRecorder( + mockPushActivity, + mockPopActivity, + transaction.spanContext().spanId, + 10, + ); spanRecorder.add(transaction); expect(mockPushActivity).toHaveBeenCalledTimes(0); diff --git a/packages/tracing/test/index.test.ts b/packages/tracing/test/index.test.ts index 79a61cd66dd6..e30bb92c0e5d 100644 --- a/packages/tracing/test/index.test.ts +++ b/packages/tracing/test/index.test.ts @@ -5,6 +5,7 @@ import { BrowserTracing, Integrations } from '../src'; describe('index', () => { it('patches the global hub to add an implementation for `Hub.startTransaction` as a side effect', () => { const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation const transaction = hub.startTransaction({ name: 'test', endTimestamp: 123 }); expect(transaction).toBeDefined(); }); diff --git a/packages/tracing/test/span.test.ts b/packages/tracing/test/span.test.ts index 011af5ba10d5..e2ebf335ecb5 100644 --- a/packages/tracing/test/span.test.ts +++ b/packages/tracing/test/span.test.ts @@ -1,6 +1,6 @@ /* eslint-disable deprecation/deprecation */ import { BrowserClient } from '@sentry/browser'; -import { Hub, Scope, makeMain } from '@sentry/core'; +import { Hub, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, Scope, makeMain } from '@sentry/core'; import type { BaseTransportOptions, ClientOptions, TransactionSource } from '@sentry/types'; import { Span, TRACEPARENT_REGEXP, Transaction } from '../src'; @@ -506,8 +506,8 @@ describe('Span', () => { sampled: true, }); - expect(span.traceId).toBe('c'); - expect(span.spanId).toBe('d'); + expect(span.spanContext().traceId).toBe('c'); + expect(span.spanContext().spanId).toBe('d'); expect(span.sampled).toBe(true); expect(span.description).toBe(undefined); expect(span.op).toBe(undefined); @@ -541,8 +541,8 @@ describe('Span', () => { span.updateWithContext(newContext); - expect(span.traceId).toBe('a'); - expect(span.spanId).toBe('b'); + expect(span.spanContext().traceId).toBe('a'); + expect(span.spanContext().spanId).toBe('b'); expect(span.description).toBe('new'); expect(span.endTimestamp).toBe(1); expect(span.op).toBe('new-op'); @@ -571,24 +571,19 @@ describe('Span', () => { hub, ); - const hubSpy = jest.spyOn(hub.getClient()!, 'getOptions'); - const dynamicSamplingContext = transaction.getDynamicSamplingContext(); - expect(hubSpy).not.toHaveBeenCalled(); expect(dynamicSamplingContext).toStrictEqual({ environment: 'myEnv' }); }); test('should return new DSC, if no DSC was provided during transaction creation', () => { - const transaction = new Transaction( - { - name: 'tx', - metadata: { - sampleRate: 0.56, - }, + const transaction = new Transaction({ + name: 'tx', + metadata: { + sampleRate: 0.56, }, - hub, - ); + sampled: true, + }); const getOptionsSpy = jest.spyOn(hub.getClient()!, 'getOptions'); @@ -598,6 +593,7 @@ describe('Span', () => { expect(dynamicSamplingContext).toStrictEqual({ release: '1.0.1', environment: 'production', + sampled: 'true', sample_rate: '0.56', trace_id: expect.any(String), transaction: 'tx', @@ -645,9 +641,7 @@ describe('Span', () => { test('is included when transaction metadata is set', () => { const spy = jest.spyOn(hub as any, 'captureEvent') as any; const transaction = hub.startTransaction({ name: 'test', sampled: true }); - transaction.setMetadata({ - source: 'url', - }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); expect(spy).toHaveBeenCalledTimes(0); transaction.end(); diff --git a/packages/tracing/test/transaction.test.ts b/packages/tracing/test/transaction.test.ts index 12c6c799883a..3d048c9a3c3f 100644 --- a/packages/tracing/test/transaction.test.ts +++ b/packages/tracing/test/transaction.test.ts @@ -1,5 +1,6 @@ /* eslint-disable deprecation/deprecation */ import { BrowserClient, Hub } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { Transaction, addExtensionMethods } from '../src'; import { getDefaultBrowserClientOptions } from './testutils'; @@ -65,7 +66,7 @@ describe('`Transaction` class', () => { describe('`updateName` method', () => { it('does not change the source', () => { const transaction = new Transaction({ name: 'dogpark' }); - transaction.setMetadata({ source: 'route' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); transaction.updateName('ballpit'); expect(transaction.name).toEqual('ballpit'); @@ -162,6 +163,7 @@ describe('`Transaction` class', () => { contexts: { foo: { key: 'val' }, trace: { + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1 }, span_id: transaction.spanId, trace_id: transaction.traceId, origin: 'manual', @@ -189,6 +191,7 @@ describe('`Transaction` class', () => { expect.objectContaining({ contexts: { trace: { + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1 }, span_id: transaction.spanId, trace_id: transaction.traceId, origin: 'manual', diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index bfb657d135fa..c9c37349306d 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -10,6 +10,7 @@ import type { FeedbackEvent } from './feedback'; import type { Integration, IntegrationClass } from './integration'; import type { MetricBucketItem } from './metrics'; import type { ClientOptions } from './options'; +import type { ParameterizedString } from './parameterize'; import type { Scope } from './scope'; import type { SdkMetadata } from './sdkmetadata'; import type { Session, SessionAggregates } from './session'; @@ -159,7 +160,7 @@ export interface Client { /** Creates an {@link Event} from primitive inputs to `captureMessage`. */ eventFromMessage( - message: string, + message: ParameterizedString, // eslint-disable-next-line deprecation/deprecation level?: Severity | SeverityLevel, hint?: EventHint, diff --git a/packages/types/src/context.ts b/packages/types/src/context.ts index 8dadb959a97d..4f92ff4c1c6a 100644 --- a/packages/types/src/context.ts +++ b/packages/types/src/context.ts @@ -95,7 +95,6 @@ export interface ResponseContext extends Record { export interface TraceContext extends Record { data?: { [key: string]: any }; - description?: string; op?: string; parent_span_id?: string; span_id: string; diff --git a/packages/types/src/event.ts b/packages/types/src/event.ts index f04386968280..b9e908371f1e 100644 --- a/packages/types/src/event.ts +++ b/packages/types/src/event.ts @@ -20,6 +20,10 @@ import type { User } from './user'; export interface Event { event_id?: string; message?: string; + logentry?: { + message?: string; + params?: string[]; + }; timestamp?: number; start_timestamp?: number; // eslint-disable-next-line deprecation/deprecation diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index f5ec2ea4fbb2..53d689a826bf 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -214,6 +214,8 @@ export interface Hub { * default values). See {@link Options.tracesSampler}. * * @returns The transaction which was just started + * + * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. */ startTransaction(context: TransactionContext, customSamplingContext?: CustomSamplingContext): Transaction; @@ -228,23 +230,33 @@ export interface Hub { * @param context Optional properties of the new `Session`. * * @returns The session which was just started + * + * @deprecated Use top-level `startSession` instead. */ startSession(context?: Session): Session; /** * Ends the session that lives on the current scope and sends it to Sentry + * + * @deprecated Use top-level `endSession` instead. */ endSession(): void; /** * Sends the current session on the scope to Sentry + * * @param endSession If set the session will be marked as exited and removed from the scope + * + * @deprecated Use top-level `captureSession` instead. */ captureSession(endSession?: boolean): void; /** - * Returns if default PII should be sent to Sentry and propagated in ourgoing requests + * Returns if default PII should be sent to Sentry and propagated in outgoing requests * when Tracing is used. + * + * @deprecated Use top-level `getClient().getOptions().sendDefaultPii` instead. This function + * only unnecessarily increased API surface but only wrapped accessing the option. */ shouldSendDefaultPii(): boolean; } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index c74715d0041e..34f668275c94 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -89,7 +89,17 @@ export type { // eslint-disable-next-line deprecation/deprecation export type { Severity, SeverityLevel } from './severity'; -export type { Span, SpanContext, SpanOrigin, SpanAttributeValue, SpanAttributes } from './span'; +export type { + Span, + SpanContext, + SpanOrigin, + SpanAttributeValue, + SpanAttributes, + SpanTimeInput, + SpanJSON, + SpanContextData, + TraceFlag, +} from './span'; export type { StackFrame } from './stackframe'; export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace'; export type { TextEncoderInternal } from './textencoder'; @@ -140,3 +150,4 @@ export type { export type { BrowserClientReplayOptions, BrowserClientProfilingOptions } from './browseroptions'; export type { CheckIn, MonitorConfig, FinishedCheckIn, InProgressCheckIn, SerializedCheckIn } from './checkin'; export type { MetricsAggregator, MetricBucketItem, MetricInstance } from './metrics'; +export type { ParameterizedString } from './parameterize'; diff --git a/packages/types/src/opentelemetry.ts b/packages/types/src/opentelemetry.ts new file mode 100644 index 000000000000..14ceaab0c087 --- /dev/null +++ b/packages/types/src/opentelemetry.ts @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This file contains vendored types from OpenTelemetry + +/** + * Defines High-Resolution Time. + * + * The first number, HrTime[0], is UNIX Epoch time in seconds since 00:00:00 UTC on 1 January 1970. + * The second number, HrTime[1], represents the partial second elapsed since Unix Epoch time represented by first number in nanoseconds. + * For example, 2021-01-01T12:30:10.150Z in UNIX Epoch time in milliseconds is represented as 1609504210150. + * The first number is calculated by converting and truncating the Epoch time in milliseconds to seconds: + * HrTime[0] = Math.trunc(1609504210150 / 1000) = 1609504210. + * The second number is calculated by converting the digits after the decimal point of the subtraction, (1609504210150 / 1000) - HrTime[0], to nanoseconds: + * HrTime[1] = Number((1609504210.150 - HrTime[0]).toFixed(9)) * 1e9 = 150000000. + * This is represented in HrTime format as [1609504210, 150000000]. + */ +export type HrTime = [number, number]; diff --git a/packages/types/src/parameterize.ts b/packages/types/src/parameterize.ts new file mode 100644 index 000000000000..a94daa3684db --- /dev/null +++ b/packages/types/src/parameterize.ts @@ -0,0 +1,4 @@ +export type ParameterizedString = string & { + __sentry_template_string__?: string; + __sentry_template_values__?: string[]; +}; diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index 5dbdee05260d..fd51eae8e5c4 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -2,6 +2,7 @@ import type { Attachment } from './attachment'; import type { Breadcrumb } from './breadcrumb'; import type { Client } from './client'; import type { Context, Contexts } from './context'; +import type { Event, EventHint } from './event'; import type { EventProcessor } from './eventprocessor'; import type { Extra, Extras } from './extra'; import type { Primitive } from './misc'; @@ -40,6 +41,7 @@ export interface ScopeData { sdkProcessingMetadata: { [key: string]: unknown }; fingerprint: string[]; level?: SeverityLevel; + /** @deprecated This will be removed in v8. */ transactionName?: string; span?: Span; } @@ -124,6 +126,7 @@ export interface Scope { /** * Sets the transaction name on the scope for future events. + * @deprecated Use extra or tags instead. */ setTransactionName(name?: string): this; @@ -137,16 +140,19 @@ export interface Scope { /** * Sets the Span on the scope. * @param span Span + * @deprecated Instead of setting a span on a scope, use `startSpan()`/`startSpanManual()` instead. */ setSpan(span?: Span): this; /** - * Returns the `Span` if there is one + * Returns the `Span` if there is one. + * @deprecated Use `getActiveSpan()` instead. */ getSpan(): Span | undefined; /** - * Returns the `Transaction` attached to the scope (if there is one) + * Returns the `Transaction` attached to the scope (if there is one). + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. */ getTransaction(): Transaction | undefined; @@ -229,4 +235,32 @@ export interface Scope { * Get propagation context from the scope, used for distributed tracing */ getPropagationContext(): PropagationContext; + + /** + * Capture an exception for this scope. + * + * @param exception The exception to capture. + * @param hint Optinal additional data to attach to the Sentry event. + * @returns the id of the captured Sentry event. + */ + captureException(exception: unknown, hint?: EventHint): string; + + /** + * Capture a message for this scope. + * + * @param exception The exception to capture. + * @param level An optional severity level to report the message with. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured message. + */ + captureMessage(message: string, level?: SeverityLevel, hint?: EventHint): string; + + /** + * Capture a Sentry event for this scope. + * + * @param exception The event to capture. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured event. + */ + captureEvent(event: Event, hint?: EventHint): string; } diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 3e625dc7dd9f..3f54322886bb 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -1,6 +1,7 @@ import type { TraceContext } from './context'; import type { Instrumenter } from './instrumenter'; import type { Primitive } from './misc'; +import type { HrTime } from './opentelemetry'; import type { Transaction } from './transaction'; type SpanOriginType = 'manual' | 'auto'; @@ -24,10 +25,69 @@ export type SpanAttributeValue = export type SpanAttributes = Record; +/** This type is aligned with the OpenTelemetry TimeInput type. */ +export type SpanTimeInput = HrTime | number | Date; + +/** A JSON representation of a span. */ +export interface SpanJSON { + data?: { [key: string]: any }; + description?: string; + op?: string; + parent_span_id?: string; + span_id: string; + start_timestamp: number; + status?: string; + tags?: { [key: string]: Primitive }; + timestamp?: number; + trace_id: string; + origin?: SpanOrigin; +} + +// These are aligned with OpenTelemetry trace flags +type TraceFlagNone = 0x0; +type TraceFlagSampled = 0x1; +export type TraceFlag = TraceFlagNone | TraceFlagSampled; + +export interface SpanContextData { + /** + * The ID of the trace that this span belongs to. It is worldwide unique + * with practically sufficient probability by being made as 16 randomly + * generated bytes, encoded as a 32 lowercase hex characters corresponding to + * 128 bits. + */ + traceId: string; + + /** + * The ID of the Span. It is globally unique with practically sufficient + * probability by being made as 8 randomly generated bytes, encoded as a 16 + * lowercase hex characters corresponding to 64 bits. + */ + spanId: string; + + /** + * Only true if the SpanContext was propagated from a remote parent. + */ + isRemote?: boolean; + + /** + * Trace flags to propagate. + * + * It is represented as 1 byte (bitmap). Bit to represent whether trace is + * sampled or not. When set, the least significant bit documents that the + * caller may have recorded trace data. A caller who does not record trace + * data out-of-band leaves this flag unset. + */ + traceFlags: TraceFlag; + + // Note: we do not have traceState here, but this is optional in OpenTelemetry anyhow +} + /** Interface holding all properties that can be set on a Span on creation. */ export interface SpanContext { /** * Description of the Span. + * + * @deprecated Use `name` instead. */ description?: string; @@ -69,11 +129,13 @@ export interface SpanContext { /** * Tags of the Span. + * @deprecated Pass `attributes` instead. */ tags?: { [key: string]: Primitive }; /** * Data of the Span. + * @deprecated Pass `attributes` instead. */ data?: { [key: string]: any }; @@ -107,36 +169,48 @@ export interface SpanContext { export interface Span extends SpanContext { /** * Human-readable identifier for the span. Identical to span.description. + * @deprecated Use `spanToJSON(span).description` instead. */ name: string; /** - * @inheritDoc + * The ID of the span. + * @deprecated Use `spanContext().spanId` instead. */ spanId: string; /** - * @inheritDoc + * The ID of the trace. + * @deprecated Use `spanContext().traceId` instead. */ traceId: string; + /** + * Was this span chosen to be sent as part of the sample? + * @deprecated Use `isRecording()` instead. + */ + sampled?: boolean; + /** * @inheritDoc */ startTimestamp: number; /** - * @inheritDoc + * Tags for the span. + * @deprecated Use `getSpanAttributes(span)` instead. */ tags: { [key: string]: Primitive }; /** - * @inheritDoc + * Data for the span. + * @deprecated Use `getSpanAttributes(span)` instead. */ data: { [key: string]: any }; /** - * @inheritDoc + * Attributes for the span. + * @deprecated Use `getSpanAttributes(span)` instead. */ attributes: SpanAttributes; @@ -150,6 +224,12 @@ export interface Span extends SpanContext { */ instrumenter: Instrumenter; + /** + * Get context data for this span. + * This includes the spanId & the traceId. + */ + spanContext(): SpanContextData; + /** * Sets the finish timestamp on the current span. * @param endTimestamp Takes an endTimestamp if the end should not be the time when you call this function. @@ -159,7 +239,7 @@ export interface Span extends SpanContext { /** * End the current span. */ - end(endTimestamp?: number): void; + end(endTimestamp?: SpanTimeInput): void; /** * Sets the tag attribute on the current span. @@ -168,6 +248,7 @@ export interface Span extends SpanContext { * * @param key Tag key * @param value Tag value + * @deprecated Use `setAttribute()` instead. */ setTag(key: string, value: Primitive): this; @@ -175,6 +256,7 @@ export interface Span extends SpanContext { * Sets the data attribute on the current span * @param key Data key * @param value Data value + * @deprecated Use `setAttribute()` instead. */ setData(key: string, value: any): this; @@ -218,6 +300,8 @@ export interface Span extends SpanContext { /** * Creates a new `Span` while setting the current `Span.id` as `parentSpanId`. * Also the `sampled` decision will be inherited. + * + * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. */ startChild(spanContext?: Pick>): Span; @@ -250,18 +334,15 @@ export interface Span extends SpanContext { */ getTraceContext(): TraceContext; - /** Convert the object to JSON */ - toJSON(): { - data?: { [key: string]: any }; - description?: string; - op?: string; - parent_span_id?: string; - span_id: string; - start_timestamp: number; - status?: string; - tags?: { [key: string]: Primitive }; - timestamp?: number; - trace_id: string; - origin?: SpanOrigin; - }; + /** + * Convert the object to JSON. + * @deprecated Use `spanToJSON(span)` instead. + */ + toJSON(): SpanJSON; + + /** + * If this is span is actually recording data. + * This will return false if tracing is disabled, this span was not sampled or if the span is already finished. + */ + isRecording(): boolean; } diff --git a/packages/types/src/transaction.ts b/packages/types/src/transaction.ts index a4bee40983a5..05be3c588c06 100644 --- a/packages/types/src/transaction.ts +++ b/packages/types/src/transaction.ts @@ -30,6 +30,7 @@ export interface TransactionContext extends SpanContext { /** * Metadata associated with the transaction, for internal SDK use. + * @deprecated Use attributes or store data on the scope instead. */ metadata?: Partial; } @@ -44,37 +45,55 @@ export type TraceparentData = Pick { /** - * @inheritDoc + * Human-readable identifier for the transaction. + * @deprecated Use `spanToJSON(span).description` instead. + */ + name: string; + + /** + * The ID of the transaction. + * @deprecated Use `spanContext().spanId` instead. */ spanId: string; /** - * @inheritDoc + * The ID of the trace. + * @deprecated Use `spanContext().traceId` instead. */ traceId: string; + /** + * Was this transaction chosen to be sent as part of the sample? + * @deprecated Use `spanIsSampled(transaction)` instead. + */ + sampled?: boolean; + /** * @inheritDoc */ startTimestamp: number; /** - * @inheritDoc + * Tags for the transaction. + * @deprecated Use `getSpanAttributes(transaction)` instead. */ tags: { [key: string]: Primitive }; /** - * @inheritDoc + * Data for the transaction. + * @deprecated Use `getSpanAttributes(transaction)` instead. */ data: { [key: string]: any }; /** - * @inheritDoc + * Attributes for the transaction. + * @deprecated Use `getSpanAttributes(transaction)` instead. */ attributes: SpanAttributes; /** - * Metadata about the transaction + * Metadata about the transaction. + * @deprecated Use attributes or store data on the scope instead. */ metadata: TransactionMetadata; @@ -89,7 +108,8 @@ export interface Transaction extends TransactionContext, Omit): void; - /** Return the current Dynamic Sampling Context of this transaction */ + /** + * Return the current Dynamic Sampling Context of this transaction + * + * @deprecated Use top-level `getDynamicSamplingContextFromSpan` instead. + */ getDynamicSamplingContext(): Partial; } @@ -160,7 +184,10 @@ export interface SamplingContext extends CustomSamplingContext { } export interface TransactionMetadata { - /** The sample rate used when sampling this transaction */ + /** + * The sample rate used when sampling this transaction. + * @deprecated Use `SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE` attribute instead. + */ sampleRate?: number; /** @@ -182,10 +209,16 @@ export interface TransactionMetadata { /** TODO: If we rm -rf `instrumentServer`, this can go, too */ requestPath?: string; - /** Information on how a transaction name was generated. */ + /** + * Information on how a transaction name was generated. + * @deprecated Use `SEMANTIC_ATTRIBUTE_SENTRY_SOURCE` attribute instead. + */ source: TransactionSource; - /** Metadata for the transaction's spans, keyed by spanId */ + /** + * Metadata for the transaction's spans, keyed by spanId. + * @deprecated This will be removed in v8. + */ spanMetadata: { [spanId: string]: { [key: string]: unknown } }; } diff --git a/packages/utils/src/eventbuilder.ts b/packages/utils/src/eventbuilder.ts index 28b2d94b0c4f..2a65a6d014cf 100644 --- a/packages/utils/src/eventbuilder.ts +++ b/packages/utils/src/eventbuilder.ts @@ -6,13 +6,14 @@ import type { Extras, Hub, Mechanism, + ParameterizedString, Severity, SeverityLevel, StackFrame, StackParser, } from '@sentry/types'; -import { isError, isPlainObject } from './is'; +import { isError, isParameterizedString, isPlainObject } from './is'; import { addExceptionMechanism, addExceptionTypeValue } from './misc'; import { normalizeToSize } from './normalize'; import { extractExceptionKeysForMessage } from './object'; @@ -127,7 +128,7 @@ export function eventFromUnknownInput( */ export function eventFromMessage( stackParser: StackParser, - message: string, + message: ParameterizedString, // eslint-disable-next-line deprecation/deprecation level: Severity | SeverityLevel = 'info', hint?: EventHint, @@ -136,7 +137,6 @@ export function eventFromMessage( const event: Event = { event_id: hint && hint.event_id, level, - message, }; if (attachStacktrace && hint && hint.syntheticException) { @@ -153,5 +153,16 @@ export function eventFromMessage( } } + if (isParameterizedString(message)) { + const { __sentry_template_string__, __sentry_template_values__ } = message; + + event.logentry = { + message: __sentry_template_string__, + params: __sentry_template_values__, + }; + return event; + } + + event.message = message; return event; } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index d19991b7d401..5483f2aa7e41 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -32,6 +32,7 @@ export * from './url'; export * from './userIntegrations'; export * from './cache'; export * from './eventbuilder'; +export * from './parameterize'; export * from './anr'; export * from './lru'; export * from './buildPolyfills'; diff --git a/packages/utils/src/is.ts b/packages/utils/src/is.ts index 61a94053a265..12225b9c8b60 100644 --- a/packages/utils/src/is.ts +++ b/packages/utils/src/is.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import type { PolymorphicEvent, Primitive } from '@sentry/types'; +import type { ParameterizedString, PolymorphicEvent, Primitive } from '@sentry/types'; // eslint-disable-next-line @typescript-eslint/unbound-method const objectToString = Object.prototype.toString; @@ -78,6 +78,22 @@ export function isString(wat: unknown): wat is string { return isBuiltin(wat, 'String'); } +/** + * Checks whether given string is parameterized + * {@link isParameterizedString}. + * + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +export function isParameterizedString(wat: unknown): wat is ParameterizedString { + return ( + typeof wat === 'object' && + wat !== null && + '__sentry_template_string__' in wat && + '__sentry_template_values__' in wat + ); +} + /** * Checks whether given value is a primitive (undefined, null, number, boolean, string, bigint, symbol) * {@link isPrimitive}. @@ -86,7 +102,7 @@ export function isString(wat: unknown): wat is string { * @returns A boolean representing the result. */ export function isPrimitive(wat: unknown): wat is Primitive { - return wat === null || (typeof wat !== 'object' && typeof wat !== 'function'); + return wat === null || isParameterizedString(wat) || (typeof wat !== 'object' && typeof wat !== 'function'); } /** diff --git a/packages/utils/src/parameterize.ts b/packages/utils/src/parameterize.ts new file mode 100644 index 000000000000..2cfa63e92677 --- /dev/null +++ b/packages/utils/src/parameterize.ts @@ -0,0 +1,17 @@ +import type { ParameterizedString } from '@sentry/types'; + +/** + * Tagged template function which returns paramaterized representation of the message + * For example: parameterize`This is a log statement with ${x} and ${y} params`, would return: + * "__sentry_template_string__": "My raw message with interpreted strings like %s", + * "__sentry_template_values__": ["this"] + * @param strings An array of string values splitted between expressions + * @param values Expressions extracted from template string + * @returns String with template information in __sentry_template_string__ and __sentry_template_values__ properties + */ +export function parameterize(strings: TemplateStringsArray, ...values: string[]): ParameterizedString { + const formatted = new String(String.raw(strings, ...values)) as ParameterizedString; + formatted.__sentry_template_string__ = strings.join('\x00').replace(/%/g, '%%').replace(/\0/g, '%s'); + formatted.__sentry_template_values__ = values; + return formatted; +} diff --git a/packages/utils/src/requestdata.ts b/packages/utils/src/requestdata.ts index 0249b7a2b481..aaa1898e1f55 100644 --- a/packages/utils/src/requestdata.ts +++ b/packages/utils/src/requestdata.ts @@ -72,16 +72,21 @@ export function addRequestDataToTransaction( deps?: InjectedNodeDeps, ): void { if (!transaction) return; + // eslint-disable-next-line deprecation/deprecation if (!transaction.metadata.source || transaction.metadata.source === 'url') { // Attempt to grab a parameterized route off of the request const [name, source] = extractPathForTransaction(req, { path: true, method: true }); transaction.updateName(name); + // TODO: SEMANTIC_ATTRIBUTE_SENTRY_SOURCE is in core, align this once we merge utils & core + // eslint-disable-next-line deprecation/deprecation transaction.setMetadata({ source }); } - transaction.setData('url', req.originalUrl || req.url); + transaction.setAttribute('url', req.originalUrl || req.url); if (req.baseUrl) { - transaction.setData('baseUrl', req.baseUrl); + transaction.setAttribute('baseUrl', req.baseUrl); } + // TODO: We need to rewrite this to a flat format? + // eslint-disable-next-line deprecation/deprecation transaction.setData('query', extractQueryParams(req, deps)); } @@ -187,6 +192,7 @@ export function extractRequestData( }, ): ExtractedNodeRequestData { const { include = DEFAULT_REQUEST_INCLUDES, deps } = options || {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const requestData: { [key: string]: any } = {}; // headers: diff --git a/packages/utils/src/time.ts b/packages/utils/src/time.ts index 1c366f09d952..16c7058ae68c 100644 --- a/packages/utils/src/time.ts +++ b/packages/utils/src/time.ts @@ -1,26 +1,6 @@ -import { dynamicRequire, isNodeEnv } from './node'; -import { getGlobalObject } from './worldwide'; +import { GLOBAL_OBJ } from './worldwide'; -// eslint-disable-next-line deprecation/deprecation -const WINDOW = getGlobalObject(); - -/** - * An object that can return the current timestamp in seconds since the UNIX epoch. - */ -interface TimestampSource { - nowSeconds(): number; -} - -/** - * A TimestampSource implementation for environments that do not support the Performance Web API natively. - * - * Note that this TimestampSource does not use a monotonic clock. A call to `nowSeconds` may return a timestamp earlier - * than a previously returned value. We do not try to emulate a monotonic behavior in order to facilitate debugging. It - * is more obvious to explain "why does my span have negative duration" than "why my spans have zero duration". - */ -const dateTimestampSource: TimestampSource = { - nowSeconds: () => Date.now() / 1000, -}; +const ONE_SECOND_IN_MS = 1000; /** * A partial definition of the [Performance Web API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Performance} @@ -37,89 +17,56 @@ interface Performance { now(): number; } +/** + * Returns a timestamp in seconds since the UNIX epoch using the Date API. + * + * TODO(v8): Return type should be rounded. + */ +export function dateTimestampInSeconds(): number { + return Date.now() / ONE_SECOND_IN_MS; +} + /** * Returns a wrapper around the native Performance API browser implementation, or undefined for browsers that do not * support the API. * * Wrapping the native API works around differences in behavior from different browsers. */ -function getBrowserPerformance(): Performance | undefined { - const { performance } = WINDOW; +function createUnixTimestampInSecondsFunc(): () => number { + const { performance } = GLOBAL_OBJ as typeof GLOBAL_OBJ & { performance?: Performance }; if (!performance || !performance.now) { - return undefined; + return dateTimestampInSeconds; } - // Replace performance.timeOrigin with our own timeOrigin based on Date.now(). - // - // This is a partial workaround for browsers reporting performance.timeOrigin such that performance.timeOrigin + - // performance.now() gives a date arbitrarily in the past. - // - // Additionally, computing timeOrigin in this way fills the gap for browsers where performance.timeOrigin is - // undefined. - // - // The assumption that performance.timeOrigin + performance.now() ~= Date.now() is flawed, but we depend on it to - // interact with data coming out of performance entries. - // - // Note that despite recommendations against it in the spec, browsers implement the Performance API with a clock that - // might stop when the computer is asleep (and perhaps under other circumstances). Such behavior causes - // performance.timeOrigin + performance.now() to have an arbitrary skew over Date.now(). In laptop computers, we have - // observed skews that can be as long as days, weeks or months. - // - // See https://github.com/getsentry/sentry-javascript/issues/2590. + // Some browser and environments don't have a timeOrigin, so we fallback to + // using Date.now() to compute the starting time. + const approxStartingTimeOrigin = Date.now() - performance.now(); + const timeOrigin = performance.timeOrigin == undefined ? approxStartingTimeOrigin : performance.timeOrigin; + + // performance.now() is a monotonic clock, which means it starts at 0 when the process begins. To get the current + // wall clock time (actual UNIX timestamp), we need to add the starting time origin and the current time elapsed. // - // BUG: despite our best intentions, this workaround has its limitations. It mostly addresses timings of pageload - // transactions, but ignores the skew built up over time that can aversely affect timestamps of navigation - // transactions of long-lived web pages. - const timeOrigin = Date.now() - performance.now(); - - return { - now: () => performance.now(), - timeOrigin, + // TODO: This does not account for the case where the monotonic clock that powers performance.now() drifts from the + // wall clock time, which causes the returned timestamp to be inaccurate. We should investigate how to detect and + // correct for this. + // See: https://github.com/getsentry/sentry-javascript/issues/2590 + // See: https://github.com/mdn/content/issues/4713 + // See: https://dev.to/noamr/when-a-millisecond-is-not-a-millisecond-3h6 + return () => { + return (timeOrigin + performance.now()) / ONE_SECOND_IN_MS; }; } -/** - * Returns the native Performance API implementation from Node.js. Returns undefined in old Node.js versions that don't - * implement the API. - */ -function getNodePerformance(): Performance | undefined { - try { - const perfHooks = dynamicRequire(module, 'perf_hooks') as { performance: Performance }; - return perfHooks.performance; - } catch (_) { - return undefined; - } -} - -/** - * The Performance API implementation for the current platform, if available. - */ -const platformPerformance: Performance | undefined = isNodeEnv() ? getNodePerformance() : getBrowserPerformance(); - -const timestampSource: TimestampSource = - platformPerformance === undefined - ? dateTimestampSource - : { - nowSeconds: () => (platformPerformance.timeOrigin + platformPerformance.now()) / 1000, - }; - -/** - * Returns a timestamp in seconds since the UNIX epoch using the Date API. - */ -export const dateTimestampInSeconds: () => number = dateTimestampSource.nowSeconds.bind(dateTimestampSource); - /** * Returns a timestamp in seconds since the UNIX epoch using either the Performance or Date APIs, depending on the * availability of the Performance API. * - * See `usingPerformanceAPI` to test whether the Performance API is used. - * * BUG: Note that because of how browsers implement the Performance API, the clock might stop when the computer is * asleep. This creates a skew between `dateTimestampInSeconds` and `timestampInSeconds`. The * skew can grow to arbitrary amounts like days, weeks or months. * See https://github.com/getsentry/sentry-javascript/issues/2590. */ -export const timestampInSeconds: () => number = timestampSource.nowSeconds.bind(timestampSource); +export const timestampInSeconds = createUnixTimestampInSecondsFunc(); /** * Re-exported with an old name for backwards-compatibility. @@ -129,11 +76,6 @@ export const timestampInSeconds: () => number = timestampSource.nowSeconds.bind( */ export const timestampWithMs = timestampInSeconds; -/** - * A boolean that is true when timestampInSeconds uses the Performance API to produce monotonic timestamps. - */ -export const usingPerformanceAPI = platformPerformance !== undefined; - /** * Internal helper to store what is the source of browserPerformanceTimeOrigin below. For debugging only. */ @@ -148,7 +90,7 @@ export const browserPerformanceTimeOrigin = ((): number | undefined => { // performance.timing.navigationStart, which results in poor results in performance data. We only treat time origin // data as reliable if they are within a reasonable threshold of the current time. - const { performance } = WINDOW; + const { performance } = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; if (!performance || !performance.now) { _browserPerformanceTimeOriginMode = 'none'; return undefined; diff --git a/packages/utils/test/parameterize.test.ts b/packages/utils/test/parameterize.test.ts new file mode 100644 index 000000000000..a199e0939271 --- /dev/null +++ b/packages/utils/test/parameterize.test.ts @@ -0,0 +1,27 @@ +import type { ParameterizedString } from '@sentry/types'; + +import { parameterize } from '../src/parameterize'; + +describe('parameterize()', () => { + test('works with empty string', () => { + const string = new String() as ParameterizedString; + string.__sentry_template_string__ = ''; + string.__sentry_template_values__ = []; + + const formatted = parameterize``; + expect(formatted.__sentry_template_string__).toEqual(''); + expect(formatted.__sentry_template_values__).toEqual([]); + }); + + test('works as expected with template literals', () => { + const x = 'first'; + const y = 'second'; + const string = new String() as ParameterizedString; + string.__sentry_template_string__ = 'This is a log statement with %s and %s params'; + string.__sentry_template_values__ = ['first', 'second']; + + const formatted = parameterize`This is a log statement with ${x} and ${y} params`; + expect(formatted.__sentry_template_string__).toEqual(string.__sentry_template_string__); + expect(formatted.__sentry_template_values__).toEqual(string.__sentry_template_values__); + }); +}); diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 0dd3e722c6bf..515b9c8691b7 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -38,6 +38,7 @@ export { // eslint-disable-next-line deprecation/deprecation extractTraceparentData, flush, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, @@ -51,6 +52,7 @@ export { makeMain, runWithAsyncContext, Scope, + // eslint-disable-next-line deprecation/deprecation startTransaction, SDK_VERSION, setContext, diff --git a/packages/vue/src/errorhandler.ts b/packages/vue/src/errorhandler.ts index 9d09bdf8c181..725f9b56c714 100644 --- a/packages/vue/src/errorhandler.ts +++ b/packages/vue/src/errorhandler.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/browser'; +import { captureException } from '@sentry/core'; import { consoleSandbox } from '@sentry/utils'; import type { ViewModel, Vue, VueOptions } from './types'; @@ -30,7 +30,7 @@ export const attachErrorHandler = (app: Vue, options: VueOptions): void => { // Capture exception in the next event loop, to make sure that all breadcrumbs are recorded in time. setTimeout(() => { - getCurrentHub().captureException(error, { + captureException(error, { captureContext: { contexts: { vue: metadata } }, mechanism: { handled: false }, }); diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts index 70117e960fe2..98c18ae80691 100644 --- a/packages/vue/src/router.ts +++ b/packages/vue/src/router.ts @@ -1,4 +1,5 @@ import { WINDOW, captureException } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; import type { Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { getActiveTransaction } from './tracing'; @@ -72,8 +73,8 @@ export function vueRouterInstrumentation( op: 'pageload', origin: 'auto.pageload.vue', tags, - metadata: { - source: 'url', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, }); } @@ -91,7 +92,7 @@ export function vueRouterInstrumentation( // hence only '==' instead of '===', because `undefined == null` evaluates to `true` const isPageLoadNavigation = from.name == null && from.matched.length === 0; - const data = { + const data: Record = { params: to.params, query: to.query, }; @@ -108,27 +109,30 @@ export function vueRouterInstrumentation( } if (startTransactionOnPageLoad && isPageLoadNavigation) { + // eslint-disable-next-line deprecation/deprecation const pageloadTransaction = getActiveTransaction(); if (pageloadTransaction) { - if (pageloadTransaction.metadata.source !== 'custom') { + const attributes = spanToJSON(pageloadTransaction).data || {}; + if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') { pageloadTransaction.updateName(transactionName); - pageloadTransaction.setMetadata({ source: transactionSource }); + pageloadTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource); } + // TODO: We need to flatten these to make them attributes + // eslint-disable-next-line deprecation/deprecation pageloadTransaction.setData('params', data.params); + // eslint-disable-next-line deprecation/deprecation pageloadTransaction.setData('query', data.query); } } if (startTransactionOnLocationChange && !isPageLoadNavigation) { + data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = transactionSource; startTransaction({ name: transactionName, op: 'navigation', origin: 'auto.navigation.vue', tags, data, - metadata: { - source: transactionSource, - }, }); } diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index af93701e74a7..cff758c4b571 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -32,8 +32,13 @@ const HOOKS: { [key in Operation]: Hook[] } = { update: ['beforeUpdate', 'updated'], }; -/** Grabs active transaction off scope, if any */ +/** + * Grabs active transaction off scope. + * + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. + */ export function getActiveTransaction(): Transaction | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentScope().getTransaction(); } @@ -73,10 +78,12 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { const isRoot = this.$root === this; if (isRoot) { + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); if (activeTransaction) { this.$_sentryRootSpan = this.$_sentryRootSpan || + // eslint-disable-next-line deprecation/deprecation activeTransaction.startChild({ description: 'Application Render', op: `${VUE_OP}.render`, @@ -101,6 +108,7 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { // Start a new span if current hook is a 'before' hook. // Otherwise, retrieve the current span and finish it. if (internalHook == internalHooks[0]) { + // eslint-disable-next-line deprecation/deprecation const activeTransaction = (this.$root && this.$root.$_sentryRootSpan) || getActiveTransaction(); if (activeTransaction) { // Cancel old span for this hook operation in case it didn't get cleaned up. We're not actually sure if it @@ -111,6 +119,7 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { oldSpan.end(); } + // eslint-disable-next-line deprecation/deprecation this.$_sentrySpans[operation] = activeTransaction.startChild({ description: `Vue <${name}>`, op: `${VUE_OP}.${operation}`, diff --git a/packages/vue/test/router.test.ts b/packages/vue/test/router.test.ts index 2e937e02f154..061bcdd3e1f9 100644 --- a/packages/vue/test/router.test.ts +++ b/packages/vue/test/router.test.ts @@ -1,4 +1,5 @@ import * as SentryBrowser from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import type { Transaction } from '@sentry/types'; import { vueRouterInstrumentation } from '../src'; @@ -100,10 +101,8 @@ describe('vueRouterInstrumentation()', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenCalledWith({ name: transactionName, - metadata: { - source: transactionSource, - }, data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: transactionSource, params: to.params, query: to.query, }, @@ -128,7 +127,7 @@ describe('vueRouterInstrumentation()', () => { const mockedTxn = { updateName: jest.fn(), setData: jest.fn(), - setMetadata: jest.fn(), + setAttribute: jest.fn(), metadata: {}, }; const customMockStartTxn = { ...mockStartTransaction }.mockImplementation(_ => { @@ -146,8 +145,8 @@ describe('vueRouterInstrumentation()', () => { expect(customMockStartTxn).toHaveBeenCalledTimes(1); expect(customMockStartTxn).toHaveBeenCalledWith({ name: '/', - metadata: { - source: 'url', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, op: 'pageload', origin: 'auto.pageload.vue', @@ -165,7 +164,7 @@ describe('vueRouterInstrumentation()', () => { expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1); expect(mockedTxn.updateName).toHaveBeenCalledWith(transactionName); - expect(mockedTxn.setMetadata).toHaveBeenCalledWith({ source: transactionSource }); + expect(mockedTxn.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource); expect(mockedTxn.setData).toHaveBeenNthCalledWith(1, 'params', to.params); expect(mockedTxn.setData).toHaveBeenNthCalledWith(2, 'query', to.query); @@ -190,10 +189,8 @@ describe('vueRouterInstrumentation()', () => { // first startTx call happens when the instrumentation is initialized (for pageloads) expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/login', - metadata: { - source: 'route', - }, data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', params: to.params, query: to.query, }, @@ -222,10 +219,8 @@ describe('vueRouterInstrumentation()', () => { // first startTx call happens when the instrumentation is initialized (for pageloads) expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: 'login-screen', - metadata: { - source: 'custom', - }, data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', params: to.params, query: to.query, }, @@ -241,10 +236,13 @@ describe('vueRouterInstrumentation()', () => { const mockedTxn = { updateName: jest.fn(), setData: jest.fn(), + setAttribute: jest.fn(), name: '', - metadata: { - source: 'url', - }, + toJSON: () => ({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + }), }; const customMockStartTxn = { ...mockStartTransaction }.mockImplementation(_ => { return mockedTxn; @@ -261,8 +259,8 @@ describe('vueRouterInstrumentation()', () => { expect(customMockStartTxn).toHaveBeenCalledTimes(1); expect(customMockStartTxn).toHaveBeenCalledWith({ name: '/', - metadata: { - source: 'url', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, op: 'pageload', origin: 'auto.pageload.vue', @@ -274,7 +272,11 @@ describe('vueRouterInstrumentation()', () => { // now we give the transaction a custom name, thereby simulating what would // happen when users use the `beforeNavigate` hook mockedTxn.name = 'customTxnName'; - mockedTxn.metadata.source = 'custom'; + mockedTxn.toJSON = () => ({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + }, + }); const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0]; beforeEachCallback(testRoutes['normalRoute1'], testRoutes['initialPageloadRoute'], mockNext); @@ -282,7 +284,7 @@ describe('vueRouterInstrumentation()', () => { expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1); expect(mockedTxn.updateName).not.toHaveBeenCalled(); - expect(mockedTxn.metadata.source).toEqual('custom'); + expect(mockedTxn.setAttribute).not.toHaveBeenCalled(); expect(mockedTxn.name).toEqual('customTxnName'); }); @@ -344,10 +346,8 @@ describe('vueRouterInstrumentation()', () => { // first startTx call happens when the instrumentation is initialized (for pageloads) expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/login', - metadata: { - source: 'route', - }, data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', params: to.params, query: to.query, }, diff --git a/yarn.lock b/yarn.lock index 48857bd5a97d..3f7c2a8141c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5231,33 +5231,33 @@ semver "7.3.2" semver-intersect "1.4.0" -"@sentry-internal/rrdom@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.6.0.tgz#19b5ab7a01ad5031be2d4bcedd4afedb44ee2bed" - integrity sha512-XqxOhLk/CdrKh0toOKeQ6mOcjLDK3B1KY/UVqM9VwhdVhiHeMwPj6GjJUoNkEXh0MwkDM0pzIMv95oSq7hGhPg== +"@sentry-internal/rrdom@2.7.3": + version "2.7.3" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.7.3.tgz#2efe68a9cf23de9a8970acf4303748cdd7866b20" + integrity sha512-XD14G4Lv3ppvJlR7VkkCgHTKu1ylh7yvXdSsN5/FyGTH+IAXQIKL5nINIgWZTN3noNBWV9R0vcHDufXG/WktWA== dependencies: - "@sentry-internal/rrweb-snapshot" "2.6.0" + "@sentry-internal/rrweb-snapshot" "2.7.3" -"@sentry-internal/rrweb-snapshot@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.6.0.tgz#1b214c9ab4645138a02ef255c95d811bd852f996" - integrity sha512-dlduO37avs5HBP8zRxFHlhRb7ZP6p3SrgMSztPCCnfYr/XAB/rn5yeVn9U2FDYdrgyUzPjFWfYWFvm1eJuEMSg== +"@sentry-internal/rrweb-snapshot@2.7.3": + version "2.7.3" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.7.3.tgz#9a7173825a31c07ccf27a5956f154400e11fdd97" + integrity sha512-mSZuBPmWia3x9wCuaJiZMD9ZVDnFv7TSG1Nz9X4ZqWb3DdaxB2MogGUU/2aTVqmRj6F91nl+GHb5NpmNYojUsw== -"@sentry-internal/rrweb-types@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.6.0.tgz#e526994125db6684ce9402d96f64318f062bebb0" - integrity sha512-mPPumdbyNHF24zShvZqzqgkZRsJHhlNpglGTS0cR/PkX2QdG0CtsPVFpaYj6UQAFGpfb2Aj7VdkKuuzX4RX69w== +"@sentry-internal/rrweb-types@2.7.3": + version "2.7.3" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.7.3.tgz#e38737bc5c31aa9dfb8ce8faf46af374c2aa7cbb" + integrity sha512-EqALxhZtvH0rimYfj7J48DRC+fj+AGsZ/VDdOPKh3MQXptTyHncoWBj4ZtB1AaH7foYUr+2wkyxl3HqMVwe+6g== dependencies: - "@sentry-internal/rrweb-snapshot" "2.6.0" + "@sentry-internal/rrweb-snapshot" "2.7.3" -"@sentry-internal/rrweb@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.6.0.tgz#0976e0d021c965b491a5193546a735f78dad9107" - integrity sha512-N+v0cgft/mikwIH5MPIspWNEqHa3E/01rA+IwozTs/TUp2e8sJmF3qxR0+OeBGZU0ln4soG1o18FjsCmadqbeQ== +"@sentry-internal/rrweb@2.7.3": + version "2.7.3" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.7.3.tgz#900ce7b1bd8ec43b5557d73698cc1b4dc9f47f72" + integrity sha512-K2pksQ1FgDumv54r1o0+RAKB3Qx3Zgx6OrQAkU/SI6/7HcBIxe7b4zepg80NyvnfohUs/relw2EoD3K3kqd8tg== dependencies: - "@sentry-internal/rrdom" "2.6.0" - "@sentry-internal/rrweb-snapshot" "2.6.0" - "@sentry-internal/rrweb-types" "2.6.0" + "@sentry-internal/rrdom" "2.7.3" + "@sentry-internal/rrweb-snapshot" "2.7.3" + "@sentry-internal/rrweb-types" "2.7.3" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" @@ -5292,40 +5292,40 @@ magic-string "0.27.0" unplugin "1.0.1" -"@sentry/cli-darwin@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.23.2.tgz#d1fed31063e19bfbdf5d5ab0bb9938f407eb9e33" - integrity sha512-7Jw1yEmJxiNan5WJyiAKXascxoe8uccKVaTvEo0JwzgWhPzS71j3eUlthuQuy0xv5Pqw4d89khAP79X/pzW/dw== - -"@sentry/cli-linux-arm64@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.23.2.tgz#a4171da7de22fd31a359fdd5671b9e445316778c" - integrity sha512-Hs2PbK2++r6Lbss44HIDXJwBSIyw1naLdIpOBi9NVLBGZxO2VLt8sQYDhVDv2ZIUijw1aGc5sg8R7R0/6qqr8Q== - -"@sentry/cli-linux-arm@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.23.2.tgz#3a101ffcc37128eeebd9abdbe033cf9fcbf093ad" - integrity sha512-fQZNHsGO6kRPT7nuv/GZ048rA2aEGHcrTZEN6UhgHoowPGGmfSpOqlpdXLME6WYWzWeSBt5Sy5RcxMvPzuDnRQ== - -"@sentry/cli-linux-i686@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.23.2.tgz#fcbaa045aa8ab2d646d6c27274f88ccc29bb417c" - integrity sha512-emogfai7xCySsTAaixjnh0hgzcb2nhEqz7MRYxGA+rSI8IgP1ZMBfdWHA/4fUap0wLNA6vVgvbHlFcBVQcGchA== - -"@sentry/cli-linux-x64@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.23.2.tgz#30404f32d8f32e33a24fed67f11f11ac84da0a6c" - integrity sha512-VewJmJRUFvKR3YiPp1pZOZJxrFGLgBHLGEP/9wBkkp3cY+rKrzQ3b7Dlh9v+YOkz1qjF1R1FsAzvsYd9/05dLg== - -"@sentry/cli-win32-i686@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.23.2.tgz#6f77749aad856dceaa9b206c6bd51fbc8caca704" - integrity sha512-R8olErQICIV+AdjINxLQYKVGRi49PdSykjs94gfTvJBxb2hvqCpS+LIVS5SFu2UDvT3/9Elq6hXMKxEgYNy0pQ== - -"@sentry/cli-win32-x64@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.23.2.tgz#d68ac046568ca951d5bfe7216ae5c52a07a65ecc" - integrity sha512-GK9xburDBnpBmjtbWrMK+9I7DRKbEhmjfWLdoTQK593xOHPOzy8lhDZ1u9Lp1mUKUcG1xba4BOFZgNppMYG2cA== +"@sentry/cli-darwin@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.24.1.tgz#feae406b2bf9a6a736e5a6f31e5561aaae8ed902" + integrity sha512-L6puTcZn5AarTL9YCVCSSCJoMV7opMx5hwwl0+sQGbLO8BChuC2QZl+j4ftEb3WgnFcT5+OODBlu4ocREtG7sQ== + +"@sentry/cli-linux-arm64@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.24.1.tgz#a3e5339904fcb89167736a4a3c6edcd697972c24" + integrity sha512-47fq/sOZnY8oSnuEurlplHKlcEhCf4Pd3JHmV6N8dYYwPEapoELb3V53BDPhjkj/rwdpJf8T90+LXCQkeF/o+w== + +"@sentry/cli-linux-arm@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.24.1.tgz#3784604555de7baa17b0b7ae8b7912d37d52738e" + integrity sha512-wnOeIl0NzUdSvz7kJKTovksDUwx6TTyV1iBjM19gZGqi+hfNc1bUa1IDGSY0m/T+CpSZiuKL0M36sYet5euDUQ== + +"@sentry/cli-linux-i686@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.24.1.tgz#9ba0c0eaa783bd955060ea105278c2856c42755f" + integrity sha512-OjpP1aRV0cwdtcics0hv8tZR6Bl5+1KIc+0habMeMxfTN7FvGmJb3ZTpuhi8OJLDglppQe6KlWzEFi8UgcE42Q== + +"@sentry/cli-linux-x64@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.24.1.tgz#b7d157b66f76a5ac47a43b934eadeec50e9c5640" + integrity sha512-GfryILChjrgSGBrT90ln46qt6UTI1ebevcDPoWArftTQ0n+P4tPFcfA9bCMV16Jsnc59CtjMFlQknLOAWnezgg== + +"@sentry/cli-win32-i686@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.24.1.tgz#b01158d6e633a2e6dfed59feaeb6979a32d7632c" + integrity sha512-COi7b/g3BbJHlJfF7GA0LAw/foyP3rMfDLQid/4fj7a0DqNjwAJRgazXvvtAY7/3XThHVE/sgLH0UmAgYaBBpA== + +"@sentry/cli-win32-x64@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.24.1.tgz#8b8752606939d229207b1afe6d4a254d24acb47c" + integrity sha512-gJxQw9ppRgZecMZ4t7mi5zWTFssVFbO2V35Nq6qk9bwHACa/LjQbyRqSqOg6zil8QruGvH1oQYClFZHlW8EHuA== "@sentry/cli@^1.74.4", "@sentry/cli@^1.77.1": version "1.77.1" @@ -5340,9 +5340,9 @@ which "^2.0.2" "@sentry/cli@^2.17.0", "@sentry/cli@^2.21.2", "@sentry/cli@^2.23.0": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.23.2.tgz#5b8edd4e6e8fdea05f5d6bb6c84b55d52897c250" - integrity sha512-coQoJnts6E/yN21uQyI7sqa89kixXQuIRodOPnIymQtYJZG3DAwqxcCBLMS3NZyVQ3HemeuhhDnE/KFd1mS53Q== + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.24.1.tgz#9b643ee44c7b2be7cf9b9435b7eea4b92bdfd6cd" + integrity sha512-eXqbKRzychtG8mMfGmqc0DRY677ngHRYa3aVS8f0VVKHK4PPV/ta08ORs0iS73IaasP563r8YEzpYjD74GtSZA== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -5350,13 +5350,13 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.23.2" - "@sentry/cli-linux-arm" "2.23.2" - "@sentry/cli-linux-arm64" "2.23.2" - "@sentry/cli-linux-i686" "2.23.2" - "@sentry/cli-linux-x64" "2.23.2" - "@sentry/cli-win32-i686" "2.23.2" - "@sentry/cli-win32-x64" "2.23.2" + "@sentry/cli-darwin" "2.24.1" + "@sentry/cli-linux-arm" "2.24.1" + "@sentry/cli-linux-arm64" "2.24.1" + "@sentry/cli-linux-i686" "2.24.1" + "@sentry/cli-linux-x64" "2.24.1" + "@sentry/cli-win32-i686" "2.24.1" + "@sentry/cli-win32-x64" "2.24.1" "@sentry/vite-plugin@^0.6.1": version "0.6.1"