diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f24414286c8a..62b713e73def 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -913,6 +913,7 @@ jobs:
'generic-ts3.8',
'node-experimental-fastify-app',
'node-hapi-app',
+ 'node-exports-test-app',
]
build-command:
- false
@@ -946,6 +947,9 @@ jobs:
uses: actions/setup-node@v4
with:
node-version-file: 'dev-packages/e2e-tests/package.json'
+ - name: Set up Bun
+ if: matrix.test-application == 'node-exports-test-app'
+ uses: oven-sh/setup-bun@v1
- name: Restore caches
uses: ./.github/actions/restore-cache
env:
diff --git a/.size-limit.js b/.size-limit.js
index 1a60e556e3e8..5e94a923e656 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -47,6 +47,13 @@ module.exports = [
gzip: true,
limit: '35 KB',
},
+ {
+ name: '@sentry/browser (incl. browserTracingIntegration) - Webpack (gzipped)',
+ path: 'packages/browser/build/npm/esm/index.js',
+ import: '{ init, browserTracingIntegration }',
+ gzip: true,
+ limit: '35 KB',
+ },
{
name: '@sentry/browser (incl. Feedback) - Webpack (gzipped)',
path: 'packages/browser/build/npm/esm/index.js',
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 98ddfdfb5b45..f7548880d5f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,43 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 7.99.0
+
+### Important Changes
+
+#### Deprecations
+
+This release includes some deprecations for span related methods and integrations in our Deno SDK, `@sentry/deno`. For
+more details please look at our
+[migration guide](https://github.com/getsentry/sentry-javascript/blob/develop/MIGRATION.md).
+
+- feat(core): Deprecate `Span.setHttpStatus` in favor of `setHttpStatus` (#10268)
+- feat(core): Deprecate `spanStatusfromHttpCode` in favour of `getSpanStatusFromHttpCode` (#10361)
+- feat(core): Deprecate `StartSpanOptions.origin` in favour of passing attribute (#10274)
+- feat(deno): Expose functional integrations to replace classes (#10355)
+
+### Other Changes
+
+- feat(bun): Add missing `@sentry/node` re-exports (#10396)
+- feat(core): Add `afterAllSetup` hook for integrations (#10345)
+- feat(core): Ensure `startSpan()` can handle spans that require parent (#10386)
+- feat(core): Read propagation context off scopes in `startSpan` APIs (#10300)
+- feat(remix): Export missing `@sentry/node` functions (#10385, #10391)
+- feat(serverless): Add missing `@sentry/node` re-exports (#10390)
+- feat(sveltekit): Add more missing `@sentry/node` re-exports (#10392)
+- feat(tracing): Export proper type for browser tracing (#10411)
+- feat(tracing): Expose new `browserTracingIntegration` (#10351)
+- fix: Ensure `afterAllSetup` is called when using `addIntegration()` (#10372)
+- fix(core): Export `spanToTraceContext` function from span utils (#10364)
+- fix(core): Make `FunctionToString` integration use SETUP_CLIENTS weakmap (#10358)
+- fix(deno): Call function if client is not setup (#10354)
+- fix(react): Fix attachReduxState option (#10381)
+- fix(spotlight): Use unpatched http.request (#10369)
+- fix(tracing): Only create request span if there is active span (#10375)
+- ref: Read propagation context off of scope and isolation scope when propagating and applying trace context (#10297)
+
+Work in this release contributed by @AleshaOleg. Thank you for your contribution!
+
## 7.98.0
This release primarily fixes some type declaration errors:
@@ -20,7 +57,7 @@ Note: The 7.96.0 release was incomplete. This release is partially encompassing
## 7.96.0
-Note: This release was incomplete. Not all Sentry SDK packages were released for this version. Please upgrade to 7.97.0
+Note: This release was incomplete. Not all Sentry SDK packages were released for this version. Please upgrade to 7.98.0
directly.
### Important Changes
@@ -1103,7 +1140,7 @@ finished. This is useful for event emitters or similar.
function middleware(_req, res, next) {
return Sentry.startSpanManual({ name: 'middleware' }, (span, finish) => {
res.once('finish', () => {
- span?.setHttpStatus(res.status);
+ setHttpStatus(span, res.status);
finish();
});
return next();
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 148045151aac..812aaa870df5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -37,6 +37,12 @@ able to use it. From the top level of the repo, there are three commands availab
dependencies (`utils`, `core`, `browser`, etc), and all packages which depend on it (currently `gatsby` and `nextjs`))
- `yarn build:dev:watch`, which runs `yarn build:dev` in watch mode (recommended)
+You can also run a production build via `yarn build`, which will build everything except for the tarballs for publishing
+to NPM. You can use this if you want to bundle Sentry yourself. The build output can be found in the packages `build/`
+folder, e.g. `packages/browser/build`. Bundled files can be found in `packages/browser/build/bundles`. Note that there
+are no guarantees about the produced file names etc., so make sure to double check which files are generated after
+upgrading.
+
## Testing SDK Packages Locally
To test local versions of SDK packages, for instance in test projects, you have a couple of options:
diff --git a/MIGRATION.md b/MIGRATION.md
index 4c0ea3eddc91..f92022cc4690 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -53,12 +53,15 @@ The following list shows how integrations should be migrated:
| `new RewriteFrames()` | `rewriteFramesIntegration()` | `@sentry/integrations` |
| `new SessionTiming()` | `sessionTimingIntegration()` | `@sentry/integrations` |
| `new HttpClient()` | `httpClientIntegration()` | `@sentry/integrations` |
-| `new ContextLines()` | `contextLinesIntegration()` | `@sentry/browser` |
+| `new ContextLines()` | `contextLinesIntegration()` | `@sentry/browser`, `@sentry/deno` |
| `new Breadcrumbs()` | `breadcrumbsIntegration()` | `@sentry/browser`, `@sentry/deno` |
-| `new GlobalHandlers()` | `globalHandlersIntegration()` | `@sentry/browser` |
+| `new GlobalHandlers()` | `globalHandlersIntegration()` | `@sentry/browser` , `@sentry/deno` |
| `new HttpContext()` | `httpContextIntegration()` | `@sentry/browser` |
| `new TryCatch()` | `browserApiErrorsIntegration()` | `@sentry/browser`, `@sentry/deno` |
| `new VueIntegration()` | `vueIntegration()` | `@sentry/vue` |
+| `new DenoContext()` | `denoContextIntegration()` | `@sentry/deno` |
+| `new DenoCron()` | `denoCronIntegration()` | `@sentry/deno` |
+| `new NormalizePaths()` | `normalizePathsIntegration()` | `@sentry/deno` |
## Deprecate `hub.bindClient()` and `makeMain()`
diff --git a/README.md b/README.md
index e1f87e04a07a..b167b682655c 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,7 @@ convenient interface and improved consistency between various JavaScript environ
- [Supported Platforms](#supported-platforms)
- [Installation and Usage](#installation-and-usage)
- [Other Packages](#other-packages)
+- [Bug Bounty Program](#bug-bounty-program)
## Supported Platforms
@@ -104,3 +105,12 @@ below:
utility functions useful for various SDKs.
- [`@sentry/types`](https://github.com/getsentry/sentry-javascript/tree/master/packages/types): Types used in all
packages.
+
+## Bug Bounty Program
+
+Our bug bounty program aims to improve the security of our open source projects by encouraging the community to identify and report potential security vulnerabilities. Your reward will depend on the severity of the identified vulnerability.
+
+Our program is currently running on an invitation basis. If you're interested in participating, please send us an email to security@sentry.io and tell us, that you are interested in auditing this repository.
+
+For more details, please have a look at https://sentry.io/security/#vulnerability-disclosure.
+
diff --git a/biome.json b/biome.json
index 8d1b11d84859..ff5a6ac17286 100644
--- a/biome.json
+++ b/biome.json
@@ -47,8 +47,11 @@
"dev-packages/browser-integration-tests/suites/**/*.json",
"dev-packages/browser-integration-tests/loader-suites/**/*.js",
"dev-packages/browser-integration-tests/suites/stacktraces/**/*.js",
+ ".next/**/*",
"**/fixtures/*/*.json",
- "**/*.min.js"
+ "**/*.min.js",
+ ".next/**",
+ ".svelte-kit/**"
]
},
"javascript": {
diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /init.js b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /init.js
new file mode 100644
index 000000000000..9200b5771ec6
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /init.js
@@ -0,0 +1,23 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+// Replay should not actually work, but still not error out
+window.Replay = new Sentry.replayIntegration({
+ flushMinDelay: 200,
+ flushMaxDelay: 200,
+ minReplayDuration: 0,
+});
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1,
+ replaysSessionSampleRate: 1.0,
+ replaysOnErrorSampleRate: 0.0,
+ integrations: [window.Replay],
+});
+
+// Ensure none of these break
+window.Replay.start();
+window.Replay.stop();
+window.Replay.flush();
diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /template.html b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /template.html
new file mode 100644
index 000000000000..2b3e2f0b27b4
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+ Click me
+
+
diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /test.ts b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /test.ts
new file mode 100644
index 000000000000..6817367ee68d
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /test.ts
@@ -0,0 +1,35 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../utils/fixtures';
+
+sentryTest(
+ 'exports a shim replayIntegration integration for non-replay bundles',
+ async ({ getLocalTestPath, page, forceFlushReplay }) => {
+ const bundle = process.env.PW_BUNDLE;
+
+ if (!bundle || !bundle.startsWith('bundle_') || bundle.includes('replay')) {
+ sentryTest.skip();
+ }
+
+ const consoleMessages: string[] = [];
+ page.on('console', msg => consoleMessages.push(msg.text()));
+
+ let requestCount = 0;
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ requestCount++;
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ await page.goto(url);
+ await forceFlushReplay();
+
+ expect(requestCount).toBe(0);
+ expect(consoleMessages).toEqual(['You are using new Replay() even though this bundle does not include replay.']);
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/init.js
new file mode 100644
index 000000000000..e5453b648509
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/init.js
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration({ idleTimeout: 9000 })],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/subject.js
new file mode 100644
index 000000000000..5355521f1655
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/subject.js
@@ -0,0 +1,11 @@
+document.getElementById('go-background').addEventListener('click', () => {
+ Object.defineProperty(document, 'hidden', { value: true, writable: true });
+ const ev = document.createEvent('Event');
+ ev.initEvent('visibilitychange');
+ document.dispatchEvent(ev);
+});
+
+document.getElementById('start-transaction').addEventListener('click', () => {
+ window.transaction = Sentry.startTransaction({ name: 'test-transaction' });
+ Sentry.getCurrentHub().configureScope(scope => scope.setSpan(window.transaction));
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/template.html
new file mode 100644
index 000000000000..fac45ecebfaf
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/template.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+ Start Transaction
+ New Tab
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts
new file mode 100644
index 000000000000..de1cd552ccab
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts
@@ -0,0 +1,45 @@
+import type { JSHandle } from '@playwright/test';
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+async function getPropertyValue(handle: JSHandle, prop: string) {
+ return (await handle.getProperty(prop))?.jsonValue();
+}
+
+sentryTest('should finish a custom transaction when the page goes background', async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const pageloadTransaction = await getFirstSentryEnvelopeRequest(page, url);
+ expect(pageloadTransaction).toBeDefined();
+
+ await page.locator('#start-transaction').click();
+ const transactionHandle = await page.evaluateHandle('window.transaction');
+
+ const id_before = await getPropertyValue(transactionHandle, 'span_id');
+ const name_before = await getPropertyValue(transactionHandle, 'name');
+ const status_before = await getPropertyValue(transactionHandle, 'status');
+ const tags_before = await getPropertyValue(transactionHandle, 'tags');
+
+ expect(name_before).toBe('test-transaction');
+ expect(status_before).toBeUndefined();
+ expect(tags_before).toStrictEqual({});
+
+ await page.locator('#go-background').click();
+
+ const id_after = await getPropertyValue(transactionHandle, 'span_id');
+ const name_after = await getPropertyValue(transactionHandle, 'name');
+ const status_after = await getPropertyValue(transactionHandle, 'status');
+ const tags_after = await getPropertyValue(transactionHandle, 'tags');
+
+ expect(id_before).toBe(id_after);
+ expect(name_after).toBe(name_before);
+ expect(status_after).toBe('cancelled');
+ expect(tags_after).toStrictEqual({ visibilitychange: 'document.hidden' });
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/subject.js
new file mode 100644
index 000000000000..b657f38ac009
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/subject.js
@@ -0,0 +1,8 @@
+document.getElementById('go-background').addEventListener('click', () => {
+ setTimeout(() => {
+ Object.defineProperty(document, 'hidden', { value: true, writable: true });
+ const ev = document.createEvent('Event');
+ ev.initEvent('visibilitychange');
+ document.dispatchEvent(ev);
+ }, 250);
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/template.html
new file mode 100644
index 000000000000..31cfc73ec3c3
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+ New Tab
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/test.ts
new file mode 100644
index 000000000000..8432245f9c9b
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/test.ts
@@ -0,0 +1,23 @@
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest('should finish pageload transaction when the page goes background', async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ await page.goto(url);
+ await page.locator('#go-background').click();
+
+ const pageloadTransaction = await getFirstSentryEnvelopeRequest(page);
+
+ expect(pageloadTransaction.contexts?.trace?.op).toBe('pageload');
+ expect(pageloadTransaction.contexts?.trace?.status).toBe('cancelled');
+ expect(pageloadTransaction.contexts?.trace?.tags).toMatchObject({
+ visibilitychange: 'document.hidden',
+ });
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/init.js
new file mode 100644
index 000000000000..e32d09a13fab
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/init.js
@@ -0,0 +1,16 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ Sentry.browserTracingIntegration({
+ idleTimeout: 1000,
+ _experiments: {
+ enableHTTPTimings: true,
+ },
+ }),
+ ],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/subject.js
new file mode 100644
index 000000000000..f62499b1e9c5
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/subject.js
@@ -0,0 +1 @@
+fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2')));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts
new file mode 100644
index 000000000000..b6da7522d82c
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts
@@ -0,0 +1,58 @@
+import { expect } from '@playwright/test';
+import type { SerializedEvent } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest('should create fetch spans with http timing @firefox', async ({ browserName, getLocalTestPath, page }) => {
+ const supportedBrowsers = ['chromium', 'firefox'];
+
+ if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
+ sentryTest.skip();
+ }
+ await page.route('http://example.com/*', async route => {
+ const request = route.request();
+ const postData = await request.postDataJSON();
+
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify(Object.assign({ id: 1 }, postData)),
+ });
+ });
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const envelopes = await getMultipleSentryEnvelopeRequests(page, 2, { url, timeout: 10000 });
+ const tracingEvent = envelopes[envelopes.length - 1]; // last envelope contains tracing data on all browsers
+
+ // eslint-disable-next-line deprecation/deprecation
+ const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client');
+
+ expect(requestSpans).toHaveLength(3);
+
+ await page.pause();
+ requestSpans?.forEach((span, index) =>
+ expect(span).toMatchObject({
+ description: `GET http://example.com/${index}`,
+ parent_span_id: tracingEvent.contexts?.trace?.span_id,
+ span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: tracingEvent.contexts?.trace?.trace_id,
+ data: expect.objectContaining({
+ 'http.request.redirect_start': expect.any(Number),
+ 'http.request.fetch_start': expect.any(Number),
+ 'http.request.domain_lookup_start': expect.any(Number),
+ 'http.request.domain_lookup_end': expect.any(Number),
+ 'http.request.connect_start': expect.any(Number),
+ 'http.request.secure_connection_start': expect.any(Number),
+ 'http.request.connection_end': expect.any(Number),
+ 'http.request.request_start': expect.any(Number),
+ 'http.request.response_start': expect.any(Number),
+ 'http.request.response_end': expect.any(Number),
+ 'network.protocol.version': expect.any(String),
+ }),
+ }),
+ );
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/init.js
new file mode 100644
index 000000000000..83076460599f
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/init.js
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration()],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js
new file mode 100644
index 000000000000..a37a2c70ad27
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js
@@ -0,0 +1,17 @@
+const delay = e => {
+ const startTime = Date.now();
+
+ function getElasped() {
+ const time = Date.now();
+ return time - startTime;
+ }
+
+ while (getElasped() < 70) {
+ //
+ }
+
+ e.target.classList.add('clicked');
+};
+
+document.querySelector('[data-test-id=interaction-button]').addEventListener('click', delay);
+document.querySelector('[data-test-id=annotated-button]').addEventListener('click', delay);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js
new file mode 100644
index 000000000000..846538e7f3f0
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js
@@ -0,0 +1,17 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ Sentry.browserTracingIntegration({
+ idleTimeout: 1000,
+ enableLongTask: false,
+ _experiments: {
+ enableInteractions: true,
+ },
+ }),
+ ],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html
new file mode 100644
index 000000000000..3357fb20a94e
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ Rendered Before Long Task
+ Click Me
+ Click Me
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts
new file mode 100644
index 000000000000..131403756251
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts
@@ -0,0 +1,114 @@
+import type { Route } from '@playwright/test';
+import { expect } from '@playwright/test';
+import type { Event, Span, SpanContext, Transaction } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import {
+ getFirstSentryEnvelopeRequest,
+ getMultipleSentryEnvelopeRequests,
+ shouldSkipTracingTest,
+} from '../../../../utils/helpers';
+
+type TransactionJSON = ReturnType & {
+ spans: ReturnType[];
+ contexts: SpanContext;
+ platform: string;
+ type: string;
+};
+
+const wait = (time: number) => new Promise(res => setTimeout(res, time));
+
+sentryTest('should capture interaction transaction. @firefox', async ({ browserName, getLocalTestPath, page }) => {
+ const supportedBrowsers = ['chromium', 'firefox'];
+
+ if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
+ sentryTest.skip();
+ }
+
+ await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ await page.goto(url);
+ await getFirstSentryEnvelopeRequest(page);
+
+ await page.locator('[data-test-id=interaction-button]').click();
+ await page.locator('.clicked[data-test-id=interaction-button]').isVisible();
+
+ const envelopes = await getMultipleSentryEnvelopeRequests(page, 1);
+ expect(envelopes).toHaveLength(1);
+
+ const eventData = envelopes[0];
+
+ expect(eventData.contexts).toMatchObject({ trace: { op: 'ui.action.click' } });
+ expect(eventData.platform).toBe('javascript');
+ expect(eventData.type).toBe('transaction');
+ expect(eventData.spans).toHaveLength(1);
+
+ const interactionSpan = eventData.spans![0];
+ expect(interactionSpan.op).toBe('ui.interaction.click');
+ expect(interactionSpan.description).toBe('body > button.clicked');
+ expect(interactionSpan.timestamp).toBeDefined();
+
+ const interactionSpanDuration = (interactionSpan.timestamp! - interactionSpan.start_timestamp) * 1000;
+ expect(interactionSpanDuration).toBeGreaterThan(70);
+ expect(interactionSpanDuration).toBeLessThan(200);
+});
+
+sentryTest(
+ 'should create only one transaction per interaction @firefox',
+ async ({ browserName, getLocalTestPath, page }) => {
+ const supportedBrowsers = ['chromium', 'firefox'];
+
+ if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
+ sentryTest.skip();
+ }
+
+ await page.route('**/path/to/script.js', (route: Route) =>
+ route.fulfill({ path: `${__dirname}/assets/script.js` }),
+ );
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+ await page.goto(url);
+ await getFirstSentryEnvelopeRequest(page);
+
+ for (let i = 0; i < 4; i++) {
+ await wait(100);
+ await page.locator('[data-test-id=interaction-button]').click();
+ const envelope = await getMultipleSentryEnvelopeRequests(page, 1);
+ expect(envelope[0].spans).toHaveLength(1);
+ }
+ },
+);
+
+sentryTest(
+ 'should use the component name for a clicked element when it is available',
+ async ({ browserName, getLocalTestPath, page }) => {
+ const supportedBrowsers = ['chromium', 'firefox'];
+
+ if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
+ sentryTest.skip();
+ }
+
+ await page.route('**/path/to/script.js', (route: Route) =>
+ route.fulfill({ path: `${__dirname}/assets/script.js` }),
+ );
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ await page.goto(url);
+ await getFirstSentryEnvelopeRequest(page);
+
+ await page.locator('[data-test-id=annotated-button]').click();
+
+ const envelopes = await getMultipleSentryEnvelopeRequests(page, 1);
+ expect(envelopes).toHaveLength(1);
+ const eventData = envelopes[0];
+
+ expect(eventData.spans).toHaveLength(1);
+
+ const interactionSpan = eventData.spans![0];
+ expect(interactionSpan.op).toBe('ui.interaction.click');
+ expect(interactionSpan.description).toBe('body > AnnotatedButton');
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/assets/script.js
new file mode 100644
index 000000000000..9ac3d6fb33d2
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/assets/script.js
@@ -0,0 +1,12 @@
+(() => {
+ const startTime = Date.now();
+
+ function getElasped() {
+ const time = Date.now();
+ return time - startTime;
+ }
+
+ while (getElasped() < 101) {
+ //
+ }
+})();
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js
new file mode 100644
index 000000000000..bde12a1304ed
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration({ enableLongTask: false, idleTimeout: 9000 })],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/template.html
new file mode 100644
index 000000000000..5c3a14114991
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/template.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+ Rendered Before Long Task
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/test.ts
new file mode 100644
index 000000000000..1f7bb54bb36a
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/test.ts
@@ -0,0 +1,23 @@
+import type { Route } from '@playwright/test';
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest('should not capture long task when flag is disabled.', async ({ browserName, getLocalTestPath, page }) => {
+ // Long tasks only work on chrome
+ if (shouldSkipTracingTest() || browserName !== 'chromium') {
+ sentryTest.skip();
+ }
+
+ await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+ // eslint-disable-next-line deprecation/deprecation
+ const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui'));
+
+ expect(uiSpans?.length).toBe(0);
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/assets/script.js
new file mode 100644
index 000000000000..5a2aef02028d
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/assets/script.js
@@ -0,0 +1,12 @@
+(() => {
+ const startTime = Date.now();
+
+ function getElasped() {
+ const time = Date.now();
+ return time - startTime;
+ }
+
+ while (getElasped() < 105) {
+ //
+ }
+})();
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js
new file mode 100644
index 000000000000..ad1d8832b228
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ Sentry.browserTracingIntegration({
+ idleTimeout: 9000,
+ }),
+ ],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/template.html
new file mode 100644
index 000000000000..5c3a14114991
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/template.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+ Rendered Before Long Task
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/test.ts
new file mode 100644
index 000000000000..32819fd784e0
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/test.ts
@@ -0,0 +1,38 @@
+import type { Route } from '@playwright/test';
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest('should capture long task.', async ({ browserName, getLocalTestPath, page }) => {
+ // Long tasks only work on chrome
+ if (shouldSkipTracingTest() || browserName !== 'chromium') {
+ sentryTest.skip();
+ }
+
+ await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+ // eslint-disable-next-line deprecation/deprecation
+ const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui'));
+
+ expect(uiSpans?.length).toBeGreaterThan(0);
+
+ const [firstUISpan] = uiSpans || [];
+ expect(firstUISpan).toEqual(
+ expect.objectContaining({
+ op: 'ui.long-task',
+ description: 'Main UI thread blocked',
+ parent_span_id: eventData.contexts?.trace?.span_id,
+ }),
+ );
+ const start = (firstUISpan as Event)['start_timestamp'] ?? 0;
+ const end = (firstUISpan as Event)['timestamp'] ?? 0;
+ const duration = end - start;
+
+ expect(duration).toBeGreaterThanOrEqual(0.1);
+ expect(duration).toBeLessThanOrEqual(0.15);
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/init.js
new file mode 100644
index 000000000000..d4c7810ef518
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/init.js
@@ -0,0 +1,10 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration()],
+ tracesSampleRate: 1,
+ environment: 'staging',
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/template.html
new file mode 100644
index 000000000000..09984cb0c488
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/template.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts
new file mode 100644
index 000000000000..ae89fd383cbb
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts
@@ -0,0 +1,96 @@
+import { expect } from '@playwright/test';
+import type { Event, EventEnvelopeHeaders } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import {
+ envelopeHeaderRequestParser,
+ getFirstSentryEnvelopeRequest,
+ shouldSkipTracingTest,
+} from '../../../../utils/helpers';
+
+sentryTest(
+ 'should create a pageload transaction based on `sentry-trace` ',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+
+ expect(eventData.contexts?.trace).toMatchObject({
+ op: 'pageload',
+ parent_span_id: '1121201211212012',
+ trace_id: '12312012123120121231201212312012',
+ });
+
+ expect(eventData.spans?.length).toBeGreaterThan(0);
+ },
+);
+
+sentryTest(
+ 'should pick up `baggage` tag, propagate the content in transaction and not add own data',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const envHeader = await getFirstSentryEnvelopeRequest(page, url, envelopeHeaderRequestParser);
+
+ expect(envHeader.trace).toBeDefined();
+ expect(envHeader.trace).toEqual({
+ release: '2.1.12',
+ sample_rate: '0.3232',
+ trace_id: '123',
+ public_key: 'public',
+ });
+ },
+);
+
+sentryTest(
+ "should create a navigation that's not influenced by `sentry-trace` ",
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const pageloadRequest = await getFirstSentryEnvelopeRequest(page, url);
+ const navigationRequest = await getFirstSentryEnvelopeRequest(page, `${url}#foo`);
+
+ expect(pageloadRequest.contexts?.trace).toMatchObject({
+ op: 'pageload',
+ parent_span_id: '1121201211212012',
+ trace_id: '12312012123120121231201212312012',
+ });
+
+ expect(navigationRequest.contexts?.trace?.op).toBe('navigation');
+ expect(navigationRequest.contexts?.trace?.trace_id).toBeDefined();
+ expect(navigationRequest.contexts?.trace?.trace_id).not.toBe(pageloadRequest.contexts?.trace?.trace_id);
+
+ const pageloadSpans = pageloadRequest.spans;
+ const navigationSpans = navigationRequest.spans;
+
+ const pageloadSpanId = pageloadRequest.contexts?.trace?.span_id;
+ const navigationSpanId = navigationRequest.contexts?.trace?.span_id;
+
+ expect(pageloadSpanId).toBeDefined();
+ expect(navigationSpanId).toBeDefined();
+
+ pageloadSpans?.forEach(span =>
+ expect(span).toMatchObject({
+ parent_span_id: pageloadSpanId,
+ }),
+ );
+
+ navigationSpans?.forEach(span =>
+ expect(span).toMatchObject({
+ parent_span_id: navigationSpanId,
+ }),
+ );
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts
new file mode 100644
index 000000000000..5a46a65a4392
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts
@@ -0,0 +1,51 @@
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest('should create a navigation transaction on page navigation', async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const pageloadRequest = await getFirstSentryEnvelopeRequest(page, url);
+ const navigationRequest = await getFirstSentryEnvelopeRequest(page, `${url}#foo`);
+
+ expect(pageloadRequest.contexts?.trace?.op).toBe('pageload');
+ expect(navigationRequest.contexts?.trace?.op).toBe('navigation');
+
+ expect(navigationRequest.transaction_info?.source).toEqual('url');
+
+ const pageloadTraceId = pageloadRequest.contexts?.trace?.trace_id;
+ const navigationTraceId = navigationRequest.contexts?.trace?.trace_id;
+
+ expect(pageloadTraceId).toBeDefined();
+ expect(navigationTraceId).toBeDefined();
+ expect(pageloadTraceId).not.toEqual(navigationTraceId);
+
+ const pageloadSpans = pageloadRequest.spans;
+ const navigationSpans = navigationRequest.spans;
+
+ const pageloadSpanId = pageloadRequest.contexts?.trace?.span_id;
+ const navigationSpanId = navigationRequest.contexts?.trace?.span_id;
+
+ expect(pageloadSpanId).toBeDefined();
+ expect(navigationSpanId).toBeDefined();
+
+ pageloadSpans?.forEach(span =>
+ expect(span).toMatchObject({
+ parent_span_id: pageloadSpanId,
+ }),
+ );
+
+ navigationSpans?.forEach(span =>
+ expect(span).toMatchObject({
+ parent_span_id: navigationSpanId,
+ }),
+ );
+
+ expect(pageloadSpanId).not.toEqual(navigationSpanId);
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/init.js
new file mode 100644
index 000000000000..1f0b64911a75
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/init.js
@@ -0,0 +1,10 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+window._testBaseTimestamp = performance.timeOrigin / 1000;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration()],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts
new file mode 100644
index 000000000000..6a186b63b02a
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts
@@ -0,0 +1,24 @@
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest('should create a pageload transaction', async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+ const timeOrigin = await page.evaluate('window._testBaseTimestamp');
+
+ const { start_timestamp: startTimestamp } = eventData;
+
+ expect(startTimestamp).toBeCloseTo(timeOrigin, 1);
+
+ expect(eventData.contexts?.trace?.op).toBe('pageload');
+ expect(eventData.spans?.length).toBeGreaterThan(0);
+ expect(eventData.transaction_info?.source).toEqual('url');
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/init.js
new file mode 100644
index 000000000000..2c5a44a7f76d
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/init.js
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+window._testBaseTimestamp = performance.timeOrigin / 1000;
+
+setTimeout(() => {
+ window._testTimeoutTimestamp = (performance.timeOrigin + performance.now()) / 1000;
+ Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration()],
+ tracesSampleRate: 1,
+ });
+}, 250);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/test.ts
new file mode 100644
index 000000000000..882c08d23c5e
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/test.ts
@@ -0,0 +1,26 @@
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest('should create a pageload transaction when initialized delayed', async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+ const timeOrigin = await page.evaluate('window._testBaseTimestamp');
+ const timeoutTimestamp = await page.evaluate('window._testTimeoutTimestamp');
+
+ const { start_timestamp: startTimestamp } = eventData;
+
+ expect(startTimestamp).toBeCloseTo(timeOrigin, 1);
+ expect(startTimestamp).toBeLessThan(timeoutTimestamp);
+
+ expect(eventData.contexts?.trace?.op).toBe('pageload');
+ expect(eventData.spans?.length).toBeGreaterThan(0);
+ expect(eventData.transaction_info?.source).toEqual('url');
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/init.js
new file mode 100644
index 000000000000..8b12fe807d7b
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/init.js
@@ -0,0 +1,14 @@
+import * as Sentry from '@sentry/browser';
+import { startSpanManual } from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration()],
+ tracesSampleRate: 1,
+});
+
+setTimeout(() => {
+ startSpanManual({ name: 'pageload-child-span' }, () => {});
+}, 200);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/test.ts
new file mode 100644
index 000000000000..dbb284aecb3b
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/test.ts
@@ -0,0 +1,27 @@
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+// This tests asserts that the pageload transaction will finish itself after about 15 seconds (3x5s of heartbeats) if it
+// has a child span without adding any additional ones or finishing any of them finishing. All of the child spans that
+// are still running should have the status "cancelled".
+sentryTest(
+ 'should send a pageload transaction terminated via heartbeat timeout',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+
+ expect(eventData.contexts?.trace?.op).toBe('pageload');
+ expect(
+ // eslint-disable-next-line deprecation/deprecation
+ eventData.spans?.find(span => span.description === 'pageload-child-span' && span.status === 'cancelled'),
+ ).toBeDefined();
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/init.js
new file mode 100644
index 000000000000..ad48a291386e
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/init.js
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration({ tracePropagationTargets: ['http://example.com'] })],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/subject.js
new file mode 100644
index 000000000000..f62499b1e9c5
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/subject.js
@@ -0,0 +1 @@
+fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2')));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/test.ts
new file mode 100644
index 000000000000..fb6e9e540c46
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/test.ts
@@ -0,0 +1,33 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
+
+sentryTest(
+ 'should attach `sentry-trace` and `baggage` header to request matching tracePropagationTargets',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const requests = (
+ await Promise.all([
+ page.goto(url),
+ Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))),
+ ])
+ )[1];
+
+ expect(requests).toHaveLength(3);
+
+ for (const request of requests) {
+ const requestHeaders = request.headers();
+
+ expect(requestHeaders).toMatchObject({
+ 'sentry-trace': expect.any(String),
+ baggage: expect.any(String),
+ });
+ }
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/init.js
new file mode 100644
index 000000000000..572b8c69d4dc
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/init.js
@@ -0,0 +1,11 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ Sentry.browserTracingIntegration({ tracePropagationTargets: [], tracingOrigins: ['http://example.com'] }),
+ ],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/subject.js
new file mode 100644
index 000000000000..f62499b1e9c5
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/subject.js
@@ -0,0 +1 @@
+fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2')));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/test.ts
new file mode 100644
index 000000000000..a6cc58ca46ff
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/test.ts
@@ -0,0 +1,32 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
+
+sentryTest(
+ '[pre-v8] should prefer custom tracePropagationTargets over tracingOrigins',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const requests = (
+ await Promise.all([
+ page.goto(url),
+ Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))),
+ ])
+ )[1];
+
+ expect(requests).toHaveLength(3);
+
+ for (const request of requests) {
+ const requestHeaders = request.headers();
+ expect(requestHeaders).not.toMatchObject({
+ 'sentry-trace': expect.any(String),
+ baggage: expect.any(String),
+ });
+ }
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/init.js
new file mode 100644
index 000000000000..45e5237e4c24
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/init.js
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration({ tracingOrigins: ['http://example.com'] })],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/subject.js
new file mode 100644
index 000000000000..f62499b1e9c5
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/subject.js
@@ -0,0 +1 @@
+fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2')));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/test.ts
new file mode 100644
index 000000000000..9f32b7b1ad28
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/test.ts
@@ -0,0 +1,32 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
+
+sentryTest(
+ '[pre-v8] should attach `sentry-trace` and `baggage` header to request matching tracingOrigins',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const requests = (
+ await Promise.all([
+ page.goto(url),
+ Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))),
+ ])
+ )[1];
+
+ expect(requests).toHaveLength(3);
+
+ for (const request of requests) {
+ const requestHeaders = request.headers();
+ expect(requestHeaders).toMatchObject({
+ 'sentry-trace': expect.any(String),
+ baggage: expect.any(String),
+ });
+ }
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/init.js
new file mode 100644
index 000000000000..83076460599f
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/init.js
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration()],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/subject.js
new file mode 100644
index 000000000000..4e9cf0d01004
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/subject.js
@@ -0,0 +1 @@
+fetch('http://localhost:4200/0').then(fetch('http://localhost:4200/1').then(fetch('http://localhost:4200/2')));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/test.ts
new file mode 100644
index 000000000000..120b36ec88db
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/test.ts
@@ -0,0 +1,32 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
+
+sentryTest(
+ 'should attach `sentry-trace` and `baggage` header to request matching default tracePropagationTargets',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const requests = (
+ await Promise.all([
+ page.goto(url),
+ Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://localhost:4200/${idx}`))),
+ ])
+ )[1];
+
+ expect(requests).toHaveLength(3);
+
+ for (const request of requests) {
+ const requestHeaders = request.headers();
+ expect(requestHeaders).toMatchObject({
+ 'sentry-trace': expect.any(String),
+ baggage: expect.any(String),
+ });
+ }
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js
new file mode 100644
index 000000000000..83076460599f
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration()],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/subject.js
new file mode 100644
index 000000000000..f62499b1e9c5
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/subject.js
@@ -0,0 +1 @@
+fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2')));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/test.ts
new file mode 100644
index 000000000000..116319259101
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/test.ts
@@ -0,0 +1,32 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../../utils/fixtures';
+import { shouldSkipTracingTest } from '../../../../../utils/helpers';
+
+sentryTest(
+ 'should not attach `sentry-trace` and `baggage` header to request not matching default tracePropagationTargets',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const requests = (
+ await Promise.all([
+ page.goto(url),
+ Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))),
+ ])
+ )[1];
+
+ expect(requests).toHaveLength(3);
+
+ for (const request of requests) {
+ const requestHeaders = request.headers();
+ expect(requestHeaders).not.toMatchObject({
+ 'sentry-trace': expect.any(String),
+ baggage: expect.any(String),
+ });
+ }
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/init.js
new file mode 100644
index 000000000000..cd05f29615bb
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/init.js
@@ -0,0 +1,12 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1,
+ integrations: [new Sentry.Integrations.BrowserTracing()],
+});
+
+// This should not fail
+Sentry.addTracingExtensions();
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/template.html
new file mode 100644
index 000000000000..2b3e2f0b27b4
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+ Click me
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/test.ts
new file mode 100644
index 000000000000..e37181ee815b
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/test.ts
@@ -0,0 +1,36 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../utils/fixtures';
+import { shouldSkipTracingTest } from '../../../utils/helpers';
+
+sentryTest(
+ 'exports a shim Integrations.BrowserTracing integration for non-tracing bundles',
+ async ({ getLocalTestPath, page }) => {
+ // Skip in tracing tests
+ if (!shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const consoleMessages: string[] = [];
+ page.on('console', msg => consoleMessages.push(msg.text()));
+
+ let requestCount = 0;
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ requestCount++;
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ await page.goto(url);
+
+ expect(requestCount).toBe(0);
+ expect(consoleMessages).toEqual([
+ 'You are using new BrowserTracing() even though this bundle does not include tracing.',
+ ]);
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/init.js
index cd05f29615bb..e8ba5702cff8 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/init.js
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/init.js
@@ -5,7 +5,7 @@ window.Sentry = Sentry;
Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
sampleRate: 1,
- integrations: [new Sentry.Integrations.BrowserTracing()],
+ integrations: [new Sentry.browserTracingIntegration()],
});
// This should not fail
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts
index e37181ee815b..71510468a513 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts
@@ -4,7 +4,7 @@ import { sentryTest } from '../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../utils/helpers';
sentryTest(
- 'exports a shim Integrations.BrowserTracing integration for non-tracing bundles',
+ 'exports a shim browserTracingIntegration() integration for non-tracing bundles',
async ({ getLocalTestPath, page }) => {
// Skip in tracing tests
if (!shouldSkipTracingTest()) {
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browsertracing/interactions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browsertracing/interactions/test.ts
index 131403756251..fa9d2889bae3 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browsertracing/interactions/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browsertracing/interactions/test.ts
@@ -1,6 +1,6 @@
import type { Route } from '@playwright/test';
import { expect } from '@playwright/test';
-import type { Event, Span, SpanContext, Transaction } from '@sentry/types';
+import type { SerializedEvent, Span, SpanContext, Transaction } from '@sentry/types';
import { sentryTest } from '../../../../utils/fixtures';
import {
@@ -30,7 +30,7 @@ sentryTest('should capture interaction transaction. @firefox', async ({ browserN
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
- await getFirstSentryEnvelopeRequest(page);
+ await getFirstSentryEnvelopeRequest(page);
await page.locator('[data-test-id=interaction-button]').click();
await page.locator('.clicked[data-test-id=interaction-button]').isVisible();
@@ -51,7 +51,7 @@ sentryTest('should capture interaction transaction. @firefox', async ({ browserN
expect(interactionSpan.timestamp).toBeDefined();
const interactionSpanDuration = (interactionSpan.timestamp! - interactionSpan.start_timestamp) * 1000;
- expect(interactionSpanDuration).toBeGreaterThan(70);
+ expect(interactionSpanDuration).toBeGreaterThan(65);
expect(interactionSpanDuration).toBeLessThan(200);
});
@@ -70,12 +70,12 @@ sentryTest(
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
- await getFirstSentryEnvelopeRequest(page);
+ await getFirstSentryEnvelopeRequest(page);
for (let i = 0; i < 4; i++) {
await wait(100);
await page.locator('[data-test-id=interaction-button]').click();
- const envelope = await getMultipleSentryEnvelopeRequests(page, 1);
+ const envelope = await getMultipleSentryEnvelopeRequests(page, 1);
expect(envelope[0].spans).toHaveLength(1);
}
},
@@ -97,11 +97,11 @@ sentryTest(
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
- await getFirstSentryEnvelopeRequest(page);
+ await getFirstSentryEnvelopeRequest(page);
await page.locator('[data-test-id=annotated-button]').click();
- const envelopes = await getMultipleSentryEnvelopeRequests(page, 1);
+ const envelopes = await getMultipleSentryEnvelopeRequests(page, 1);
expect(envelopes).toHaveLength(1);
const eventData = envelopes[0];
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/init.js
new file mode 100644
index 000000000000..92152554ea57
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/init.js
@@ -0,0 +1,10 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ // disable pageload transaction
+ integrations: [Sentry.BrowserTracing({ tracingOrigins: ['http://example.com'], startTransactionOnPageLoad: false })],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/subject.js
new file mode 100644
index 000000000000..f62499b1e9c5
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/subject.js
@@ -0,0 +1 @@
+fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2')));
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/test.ts
new file mode 100644
index 000000000000..4dc5a0ac4e0a
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/test.ts
@@ -0,0 +1,35 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { envelopeUrlRegex, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest(
+ 'there should be no span created for fetch requests with no active span',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ let requestCount = 0;
+ page.on('request', request => {
+ expect(envelopeUrlRegex.test(request.url())).toBe(false);
+ requestCount++;
+ });
+
+ await page.goto(url);
+
+ // Here are the requests that should exist:
+ // 1. HTML page
+ // 2. Init JS bundle
+ // 3. Subject JS bundle
+ // 4 [OPTIONAl] CDN JS bundle
+ // and then 3 fetch requests
+ if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle_')) {
+ expect(requestCount).toBe(7);
+ } else {
+ expect(requestCount).toBe(6);
+ }
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/init.js
new file mode 100644
index 000000000000..92152554ea57
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/init.js
@@ -0,0 +1,10 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ // disable pageload transaction
+ integrations: [Sentry.BrowserTracing({ tracingOrigins: ['http://example.com'], startTransactionOnPageLoad: false })],
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/subject.js
new file mode 100644
index 000000000000..5790c230aa66
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/subject.js
@@ -0,0 +1,11 @@
+const xhr_1 = new XMLHttpRequest();
+xhr_1.open('GET', 'http://example.com/0');
+xhr_1.send();
+
+const xhr_2 = new XMLHttpRequest();
+xhr_2.open('GET', 'http://example.com/1');
+xhr_2.send();
+
+const xhr_3 = new XMLHttpRequest();
+xhr_3.open('GET', 'http://example.com/2');
+xhr_3.send();
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/test.ts
new file mode 100644
index 000000000000..19c1f5891a39
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/test.ts
@@ -0,0 +1,35 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import { envelopeUrlRegex, shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest(
+ 'there should be no span created for xhr requests with no active span',
+ async ({ getLocalTestPath, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ let requestCount = 0;
+ page.on('request', request => {
+ expect(envelopeUrlRegex.test(request.url())).toBe(false);
+ requestCount++;
+ });
+
+ await page.goto(url);
+
+ // Here are the requests that should exist:
+ // 1. HTML page
+ // 2. Init JS bundle
+ // 3. Subject JS bundle
+ // 4 [OPTIONAl] CDN JS bundle
+ // and then 3 fetch requests
+ if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle_')) {
+ expect(requestCount).toBe(7);
+ } else {
+ expect(requestCount).toBe(6);
+ }
+ },
+);
diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts
index d00f125a90c1..6027695746e9 100644
--- a/dev-packages/browser-integration-tests/utils/helpers.ts
+++ b/dev-packages/browser-integration-tests/utils/helpers.ts
@@ -1,7 +1,7 @@
import type { Page, Request } from '@playwright/test';
import type { EnvelopeItemType, Event, EventEnvelopeHeaders } from '@sentry/types';
-const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//;
+export const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//;
export const envelopeParser = (request: Request | null): unknown[] => {
// https://develop.sentry.dev/sdk/envelopes/
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx
new file mode 100644
index 000000000000..7a226868d1bd
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx
@@ -0,0 +1,13 @@
+import http from 'http';
+
+export const dynamic = 'force-dynamic';
+
+export default async function Page() {
+ await fetch('http://example.com/');
+ await new Promise(resolve => {
+ http.get('http://example.com/', () => {
+ resolve();
+ });
+ });
+ return Hello World!
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts
new file mode 100644
index 000000000000..ce17f725cf79
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts
@@ -0,0 +1,32 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '../event-proxy-server';
+
+test('Should send a transaction with a fetch span', async ({ page }) => {
+ const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => {
+ return transactionEvent?.transaction === 'Page Server Component (/request-instrumentation)';
+ });
+
+ await page.goto(`/request-instrumentation`);
+
+ expect((await transactionPromise).spans).toContainEqual(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'http.method': 'GET',
+ 'sentry.op': 'http.client',
+ 'sentry.origin': 'auto.http.node.undici',
+ }),
+ description: 'GET http://example.com/',
+ }),
+ );
+
+ expect((await transactionPromise).spans).toContainEqual(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'http.method': 'GET',
+ 'sentry.op': 'http.client',
+ 'sentry.origin': 'auto.http.node.http',
+ }),
+ description: 'GET http://example.com/',
+ }),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/.npmrc b/dev-packages/e2e-tests/test-applications/node-exports-test-app/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/README.md b/dev-packages/e2e-tests/test-applications/node-exports-test-app/README.md
new file mode 100644
index 000000000000..8af36f6c6dad
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/README.md
@@ -0,0 +1,18 @@
+# Consistent Node Export Test
+
+This test "app" ensures that we consistently re-export exports from `@sentry/node` in packages depending on
+`@sentry/node`.
+
+## How to add new package
+
+1. Add package as a dependency to the test app
+2. In `scripts/consistentExports.ts`:
+ - add namespace import
+ - add `DEPENDENTS` entry
+ - add any ignores/exclusion entries as necessary
+ - if the package is still under development, you can also set `skip: true`
+
+## Limitations:
+
+- This script only checks top-level exports for now (e.g. `metrics` but no sub-exports like `metrics.increment`)
+- This script only checks ESM transpiled code for now, not CJS
diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json b/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json
new file mode 100644
index 000000000000..8965bb7de982
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "node-express-app",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "tsc",
+ "start": "pnpm build && bun run ./dist/consistentExports.js",
+ "test": " bun run ./dist/consistentExports.js",
+ "clean": "npx rimraf node_modules,pnpm-lock.yaml,dist",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm test"
+ },
+ "dependencies": {
+ "@sentry/node": "latest || *",
+ "@sentry/sveltekit": "latest || *",
+ "@sentry/remix": "latest || *",
+ "@sentry/astro": "latest || *",
+ "@sentry/nextjs": "latest || *",
+ "@sentry/serverless": "latest || *",
+ "@sentry/bun": "latest || *",
+ "@sentry/types": "latest || *",
+ "@types/node": "18.15.1",
+ "typescript": "4.9.5"
+ },
+ "devDependencies": {
+ "ts-node": "10.9.1"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts
new file mode 100644
index 000000000000..cf8233680c11
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts
@@ -0,0 +1,96 @@
+import * as SentryAstro from '@sentry/astro';
+import * as SentryBun from '@sentry/bun';
+import * as SentryNextJs from '@sentry/nextjs';
+import * as SentryNode from '@sentry/node';
+import * as SentryRemix from '@sentry/remix';
+import * as SentryServerless from '@sentry/serverless';
+import * as SentrySvelteKit from '@sentry/sveltekit';
+
+/* List of exports that are safe to ignore / we don't require in any depending package */
+const NODE_EXPORTS_IGNORE = [
+ 'default',
+ // Probably generated by transpilation, no need to require it
+ '__esModule',
+ // this function was deprecated almost immediately after it was introduced
+ // due to a name change (startSpan). No need to re-export it IMHO.
+ 'startActiveSpan',
+ // this was never meant for external use (and documented as such)
+ 'trace',
+ // These Node exports were only made for type definition fixes (see #10339)
+ 'Undici',
+ 'Http',
+ 'DebugSession',
+ 'AnrIntegrationOptions',
+ 'LocalVariablesIntegrationOptions',
+];
+
+type Dependent = {
+ package: string;
+ exports: string[];
+ ignoreExports?: string[];
+ skip?: boolean;
+};
+
+const DEPENDENTS: Dependent[] = [
+ {
+ package: '@sentry/astro',
+ exports: Object.keys(SentryAstro),
+ },
+ {
+ package: '@sentry/bun',
+ exports: Object.keys(SentryBun),
+ ignoreExports: [
+ // not supported in bun:
+ 'Handlers',
+ 'NodeClient',
+ 'hapiErrorPlugin',
+ 'makeNodeTransport',
+ ],
+ },
+ {
+ package: '@sentry/nextjs',
+ // Next.js doesn't require explicit exports, so we can just merge top level and `default` exports:
+ // @ts-expect-error: `default` is not in the type definition but it's defined
+ exports: Object.keys({ ...SentryNextJs, ...SentryNextJs.default }),
+ },
+ {
+ package: '@sentry/remix',
+ exports: Object.keys(SentryRemix),
+ },
+ {
+ package: '@sentry/serverless',
+ exports: Object.keys(SentryServerless),
+ ignoreExports: ['cron', 'hapiErrorPlugin', 'enableAnrDetection'],
+ },
+ {
+ package: '@sentry/sveltekit',
+ exports: Object.keys(SentrySvelteKit),
+ },
+];
+
+/* Sanitized list of node exports */
+const nodeExports = Object.keys(SentryNode).filter(e => !NODE_EXPORTS_IGNORE.includes(e));
+
+console.log('🔎 Checking for consistent exports of @sentry/node exports in depending packages');
+
+const missingExports: Record = {};
+const dependentsToCheck = DEPENDENTS.filter(d => !d.skip);
+
+for (const nodeExport of nodeExports) {
+ for (const dependent of dependentsToCheck) {
+ if (dependent.ignoreExports?.includes(nodeExport)) {
+ continue;
+ }
+ if (!dependent.exports.includes(nodeExport)) {
+ missingExports[dependent.package] = [...(missingExports[dependent.package] ?? []), nodeExport];
+ }
+ }
+}
+
+if (Object.keys(missingExports).length > 0) {
+ console.error('\n❌ Found missing exports from @sentry/node in the following packages:\n');
+ console.log(JSON.stringify(missingExports, null, 2));
+ process.exit(1);
+}
+
+console.log('✅ All good :)');
diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-exports-test-app/tsconfig.json
new file mode 100644
index 000000000000..fc22710d69dc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "types": ["node"],
+ "esModuleInterop": true,
+ "lib": ["ES6"],
+ "strict": true,
+ "outDir": "dist",
+ "target": "ESNext",
+ "moduleResolution": "node",
+ "skipLibCheck": true
+ },
+ "include": ["scripts/**/*.ts"]
+}
diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json
index ed76bfcbec4f..a3b1a5c968d5 100644
--- a/dev-packages/node-integration-tests/package.json
+++ b/dev-packages/node-integration-tests/package.json
@@ -14,7 +14,8 @@
"build:dev": "yarn build",
"build:transpile": "rollup -c rollup.npm.config.mjs",
"build:types": "tsc -p tsconfig.types.json",
- "clean": "rimraf -g **/node_modules",
+ "clean": "rimraf -g **/node_modules && run-p clean:docker:*",
+ "clean:docker:mysql2": "cd suites/tracing-experimental/mysql2 && docker-compose down --volumes",
"prisma:init": "(cd suites/tracing/prisma-orm && ts-node ./setup.ts)",
"prisma:init:new": "(cd suites/tracing-new/prisma-orm && ts-node ./setup.ts)",
"lint": "eslint . --format stylish",
@@ -26,6 +27,7 @@
"test:watch": "yarn test --watch"
},
"dependencies": {
+ "@hapi/hapi": "^20.3.0",
"@prisma/client": "3.15.2",
"@sentry/node": "7.98.0",
"@sentry/tracing": "7.98.0",
@@ -44,6 +46,7 @@
"mongodb-memory-server-global": "^7.6.3",
"mongoose": "^5.13.22",
"mysql": "^2.18.1",
+ "mysql2": "^3.7.1",
"nock": "^13.1.0",
"pg": "^8.7.3",
"proxy": "^2.1.1",
diff --git a/dev-packages/node-integration-tests/src/index.ts b/dev-packages/node-integration-tests/src/index.ts
index 423b1ac3d93f..aba4e76caa7e 100644
--- a/dev-packages/node-integration-tests/src/index.ts
+++ b/dev-packages/node-integration-tests/src/index.ts
@@ -29,3 +29,11 @@ export function startExpressServerAndSendPortToRunner(app: Express): void {
console.log(`{"port":${address.port}}`);
});
}
+
+/**
+ * Sends the port to the runner
+ */
+export function sendPortToRunner(port: number): void {
+ // eslint-disable-next-line no-console
+ console.log(`{"port":${port}}`);
+}
diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts
index 190f458ea76c..4ec29414868c 100644
--- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts
+++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts
@@ -5,7 +5,7 @@ afterAll(() => {
cleanupChildProcesses();
});
-test('Should not overwrite baggage if the incoming request already has Sentry baggage data.', async () => {
+test('Should overwrite baggage if the incoming request already has Sentry baggage data but no sentry-trace', async () => {
const runner = createRunner(__dirname, '..', 'server.ts').start();
const response = await runner.makeRequest('get', '/test/express', {
@@ -13,7 +13,7 @@ test('Should not overwrite baggage if the incoming request already has Sentry ba
});
expect(response).toBeDefined();
- expect(response).toMatchObject({
+ expect(response).not.toMatchObject({
test_data: {
host: 'somewhere.not.sentry',
baggage: 'sentry-release=2.0.0,sentry-environment=myEnv',
@@ -25,7 +25,7 @@ test('Should propagate sentry trace baggage data from an incoming to an outgoing
const runner = createRunner(__dirname, '..', 'server.ts').start();
const response = await runner.makeRequest('get', '/test/express', {
- 'sentry-trace': '',
+ 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1',
baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,dogs=great',
});
@@ -38,11 +38,28 @@ test('Should propagate sentry trace baggage data from an incoming to an outgoing
});
});
-test('Should not propagate baggage if sentry-trace header is present in incoming request but no baggage header', async () => {
+test('Should not propagate baggage data from an incoming to an outgoing request if sentry-trace is faulty.', async () => {
const runner = createRunner(__dirname, '..', 'server.ts').start();
const response = await runner.makeRequest('get', '/test/express', {
'sentry-trace': '',
+ baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,dogs=great',
+ });
+
+ expect(response).toBeDefined();
+ expect(response).not.toMatchObject({
+ test_data: {
+ host: 'somewhere.not.sentry',
+ baggage: 'sentry-release=2.0.0,sentry-environment=myEnv',
+ },
+ });
+});
+
+test('Should not propagate baggage if sentry-trace header is present in incoming request but no baggage header', async () => {
+ const runner = createRunner(__dirname, '..', 'server.ts').start();
+
+ const response = await runner.makeRequest('get', '/test/express', {
+ 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1',
});
expect(response).toBeDefined();
@@ -57,7 +74,7 @@ test('Should not propagate baggage and ignore original 3rd party baggage entries
const runner = createRunner(__dirname, '..', 'server.ts').start();
const response = await runner.makeRequest('get', '/test/express', {
- 'sentry-trace': '',
+ 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1',
baggage: 'foo=bar',
});
diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts
index 1accfce01316..b7b6c08c0f3e 100644
--- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts
+++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts
@@ -9,7 +9,7 @@ test('should ignore sentry-values in `baggage` header of a third party vendor an
const runner = createRunner(__dirname, 'server.ts').start();
const response = await runner.makeRequest('get', '/test/express', {
- 'sentry-trace': '',
+ 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1',
baggage: 'sentry-release=2.1.0,sentry-environment=myEnv',
});
diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts
index bc93b2886a19..41b2b9d7cf19 100644
--- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts
+++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts
@@ -9,7 +9,7 @@ test('should merge `baggage` header of a third party vendor with the Sentry DSC
const runner = createRunner(__dirname, 'server.ts').start();
const response = await runner.makeRequest('get', '/test/express', {
- 'sentry-trace': '',
+ 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1',
baggage: 'sentry-release=2.0.0,sentry-environment=myEnv',
});
diff --git a/dev-packages/node-integration-tests/suites/proxy/test.ts b/dev-packages/node-integration-tests/suites/proxy/test.ts
index 5e4619d3948d..dc709f5251c6 100644
--- a/dev-packages/node-integration-tests/suites/proxy/test.ts
+++ b/dev-packages/node-integration-tests/suites/proxy/test.ts
@@ -1,4 +1,8 @@
-import { createRunner } from '../../utils/runner';
+import { cleanupChildProcesses, createRunner } from '../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
test('proxies sentry requests', done => {
createRunner(__dirname, 'basic.js')
diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario-mutation.js b/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario-mutation.js
new file mode 100644
index 000000000000..ebe4f7cd3e4d
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario-mutation.js
@@ -0,0 +1,63 @@
+const Sentry = require('@sentry/node-experimental');
+const { loggingTransport } = require('@sentry-internal/node-integration-tests');
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+// Stop the process from exiting before the transaction is sent
+setInterval(() => {}, 1000);
+
+async function run() {
+ const { ApolloServer, gql } = require('apollo-server');
+
+ await Sentry.startSpan(
+ {
+ name: 'Test Transaction',
+ op: 'transaction',
+ },
+ async span => {
+ const server = new ApolloServer({
+ typeDefs: gql`
+ type Query {
+ hello: String
+ }
+ type Mutation {
+ login(email: String): String
+ }
+ `,
+ resolvers: {
+ Query: {
+ hello: () => {
+ return 'Hello world!';
+ },
+ },
+ Mutation: {
+ login: async (_, { email }) => {
+ return `${email}--token`;
+ },
+ },
+ },
+ });
+
+ // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation
+ await server.executeOperation({
+ query: gql`mutation Mutation($email: String){
+ login(email: $email)
+ }`,
+ variables: { email: 'test@email.com' },
+ });
+
+ setTimeout(() => {
+ span.end();
+ server.stop();
+ }, 500);
+ },
+ );
+}
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+run();
diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario.js b/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario-query.js
similarity index 100%
rename from dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario.js
rename to dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario-query.js
diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts b/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts
index dc7c304484f9..96018c12ebeb 100644
--- a/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts
@@ -2,37 +2,61 @@ import { conditionalTest } from '../../../utils';
import { createRunner } from '../../../utils/runner';
conditionalTest({ min: 14 })('GraphQL/Apollo Tests', () => {
- const EXPECTED_TRANSACTION = {
- transaction: 'Test Transaction',
- spans: expect.arrayContaining([
- expect.objectContaining({
- data: {
- 'graphql.operation.type': 'query',
- 'graphql.source': '{hello}',
- 'otel.kind': 'INTERNAL',
- 'sentry.origin': 'auto.graphql.otel.graphql',
- },
- description: 'query',
- status: 'ok',
- origin: 'auto.graphql.otel.graphql',
- }),
- expect.objectContaining({
- data: {
- 'graphql.field.name': 'hello',
- 'graphql.field.path': 'hello',
- 'graphql.field.type': 'String',
- 'graphql.source': 'hello',
- 'otel.kind': 'INTERNAL',
- 'sentry.origin': 'manual',
- },
- description: 'graphql.resolve',
- status: 'ok',
- origin: 'manual',
- }),
- ]),
- };
-
test('CJS - should instrument GraphQL queries used from Apollo Server.', done => {
- createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done);
+ const EXPECTED_TRANSACTION = {
+ transaction: 'Test Transaction',
+ spans: expect.arrayContaining([
+ expect.objectContaining({
+ data: {
+ 'graphql.operation.type': 'query',
+ 'graphql.source': '{hello}',
+ 'otel.kind': 'INTERNAL',
+ 'sentry.origin': 'auto.graphql.otel.graphql',
+ },
+ description: 'query',
+ status: 'ok',
+ origin: 'auto.graphql.otel.graphql',
+ }),
+ expect.objectContaining({
+ data: {
+ 'graphql.field.name': 'hello',
+ 'graphql.field.path': 'hello',
+ 'graphql.field.type': 'String',
+ 'graphql.source': 'hello',
+ 'otel.kind': 'INTERNAL',
+ 'sentry.origin': 'manual',
+ },
+ description: 'graphql.resolve',
+ status: 'ok',
+ origin: 'manual',
+ }),
+ ]),
+ };
+
+ createRunner(__dirname, 'scenario-query.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done);
+ });
+
+ test('CJS - should instrument GraphQL mutations used from Apollo Server.', done => {
+ const EXPECTED_TRANSACTION = {
+ transaction: 'Test Transaction',
+ spans: expect.arrayContaining([
+ expect.objectContaining({
+ data: {
+ 'graphql.operation.name': 'Mutation',
+ 'graphql.operation.type': 'mutation',
+ 'graphql.source': `mutation Mutation($email: String) {
+ login(email: $email)
+}`,
+ 'otel.kind': 'INTERNAL',
+ 'sentry.origin': 'auto.graphql.otel.graphql',
+ },
+ description: 'mutation Mutation',
+ status: 'ok',
+ origin: 'auto.graphql.otel.graphql',
+ }),
+ ]),
+ };
+
+ createRunner(__dirname, 'scenario-mutation.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done);
});
});
diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/hapi/scenario.js b/dev-packages/node-integration-tests/suites/tracing-experimental/hapi/scenario.js
new file mode 100644
index 000000000000..5f2c898fad60
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing-experimental/hapi/scenario.js
@@ -0,0 +1,35 @@
+const { loggingTransport, sendPortToRunner } = require('@sentry-internal/node-integration-tests');
+const Sentry = require('@sentry/node-experimental');
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ debug: true,
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+const Hapi = require('@hapi/hapi');
+
+const port = 5999;
+
+const init = async () => {
+ const server = Hapi.server({
+ host: 'localhost',
+ port,
+ });
+
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: (_request, _h) => {
+ return 'Hello World!';
+ },
+ });
+
+ await server.start();
+
+ sendPortToRunner(port);
+};
+
+init();
diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/hapi/test.ts b/dev-packages/node-integration-tests/suites/tracing-experimental/hapi/test.ts
new file mode 100644
index 000000000000..148bf83bb397
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing-experimental/hapi/test.ts
@@ -0,0 +1,35 @@
+import { conditionalTest } from '../../../utils';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+jest.setTimeout(20000);
+
+conditionalTest({ min: 14 })('hapi auto-instrumentation', () => {
+ afterAll(async () => {
+ cleanupChildProcesses();
+ });
+
+ const EXPECTED_TRANSACTION = {
+ transaction: 'GET /',
+ spans: expect.arrayContaining([
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'http.route': '/',
+ 'http.method': 'GET',
+ 'hapi.type': 'router',
+ 'sentry.origin': 'manual',
+ 'sentry.op': 'http',
+ }),
+ description: 'GET /',
+ op: 'http',
+ status: 'ok',
+ }),
+ ]),
+ };
+
+ test('CJS - should auto-instrument `@hapi/hapi` package.', done => {
+ createRunner(__dirname, 'scenario.js')
+ .expect({ transaction: EXPECTED_TRANSACTION })
+ .start(done)
+ .makeRequest('get', '/');
+ });
+});
diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql/test.ts b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql/test.ts
index 43c67c9c8b07..84c63a30ff68 100644
--- a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql/test.ts
@@ -1,7 +1,11 @@
import { conditionalTest } from '../../../utils';
-import { createRunner } from '../../../utils/runner';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
conditionalTest({ min: 14 })('mysql auto instrumentation', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
test('should auto-instrument `mysql` package when using connection.connect()', done => {
const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/docker-compose.yml
new file mode 100644
index 000000000000..71ea54ad7e70
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/docker-compose.yml
@@ -0,0 +1,9 @@
+services:
+ db:
+ image: mysql:8
+ restart: always
+ container_name: integration-tests-mysql
+ ports:
+ - '3306:3306'
+ environment:
+ MYSQL_ROOT_PASSWORD: password
diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/scenario.js b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/scenario.js
new file mode 100644
index 000000000000..8858e4ef587f
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/scenario.js
@@ -0,0 +1,34 @@
+const { loggingTransport } = require('@sentry-internal/node-integration-tests');
+const Sentry = require('@sentry/node-experimental');
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+// Stop the process from exiting before the transaction is sent
+setInterval(() => {}, 1000);
+
+const mysql = require('mysql2/promise');
+
+mysql
+ .createConnection({
+ user: 'root',
+ password: 'password',
+ host: 'localhost',
+ port: 3306,
+ })
+ .then(connection => {
+ return Sentry.startSpan(
+ {
+ op: 'transaction',
+ name: 'Test Transaction',
+ },
+ async _ => {
+ await connection.query('SELECT 1 + 1 AS solution');
+ await connection.query('SELECT NOW()', ['1', '2']);
+ },
+ );
+ });
diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/test.ts b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/test.ts
new file mode 100644
index 000000000000..28209009b03e
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/test.ts
@@ -0,0 +1,41 @@
+import { conditionalTest } from '../../../utils';
+import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+
+conditionalTest({ min: 14 })('mysql2 auto instrumentation', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ test('should auto-instrument `mysql` package without connection.connect()', done => {
+ const EXPECTED_TRANSACTION = {
+ transaction: 'Test Transaction',
+ spans: expect.arrayContaining([
+ expect.objectContaining({
+ description: 'SELECT 1 + 1 AS solution',
+ op: 'db',
+ data: expect.objectContaining({
+ 'db.system': 'mysql',
+ 'net.peer.name': 'localhost',
+ 'net.peer.port': 3306,
+ 'db.user': 'root',
+ }),
+ }),
+ expect.objectContaining({
+ description: 'SELECT NOW()',
+ op: 'db',
+ data: expect.objectContaining({
+ 'db.system': 'mysql',
+ 'net.peer.name': 'localhost',
+ 'net.peer.port': 3306,
+ 'db.user': 'root',
+ }),
+ }),
+ ]),
+ };
+
+ createRunner(__dirname, 'scenario.js')
+ .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port: 3306'] })
+ .expect({ transaction: EXPECTED_TRANSACTION })
+ .start(done);
+ });
+});
diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/docker-compose.yml
new file mode 100644
index 000000000000..dac954ad81d7
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/docker-compose.yml
@@ -0,0 +1,13 @@
+version: '3.9'
+
+services:
+ db:
+ image: postgres:13
+ restart: always
+ container_name: integration-tests-postgres
+ ports:
+ - '5444:5432'
+ environment:
+ POSTGRES_USER: test
+ POSTGRES_PASSWORD: test
+ POSTGRES_DB: tests
diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/scenario.js b/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/scenario.js
new file mode 100644
index 000000000000..fa81bd00b938
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/scenario.js
@@ -0,0 +1,46 @@
+const { loggingTransport } = require('@sentry-internal/node-integration-tests');
+const Sentry = require('@sentry/node-experimental');
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+// Stop the process from exiting before the transaction is sent
+setInterval(() => {}, 1000);
+
+const { Client } = require('pg');
+
+const client = new Client({ port: 5444, user: 'test', password: 'test', database: 'tests' });
+
+async function run() {
+ await Sentry.startSpan(
+ {
+ name: 'Test Transaction',
+ op: 'transaction',
+ },
+ async () => {
+ try {
+ await client.connect();
+
+ await client
+ .query(
+ 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"));',
+ )
+ .catch(() => {
+ // if this is not a fresh database, the table might already exist
+ });
+
+ await client.query('INSERT INTO "User" ("email", "name") VALUES ($1, $2)', ['tim', 'tim@domain.com']);
+ await client.query('SELECT * FROM "User"');
+ } finally {
+ await client.end();
+ }
+ },
+ );
+}
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+run();
diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/test.ts b/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/test.ts
new file mode 100644
index 000000000000..117a5d80ac02
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/test.ts
@@ -0,0 +1,54 @@
+import { conditionalTest } from '../../../utils';
+import { createRunner } from '../../../utils/runner';
+
+conditionalTest({ min: 14 })('postgres auto instrumentation', () => {
+ test('should auto-instrument `pg` package', done => {
+ const EXPECTED_TRANSACTION = {
+ transaction: 'Test Transaction',
+ spans: expect.arrayContaining([
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'db.system': 'postgresql',
+ 'db.name': 'tests',
+ 'sentry.origin': 'manual',
+ 'sentry.op': 'db',
+ }),
+ description: 'pg.connect',
+ op: 'db',
+ status: 'ok',
+ }),
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'db.system': 'postgresql',
+ 'db.name': 'tests',
+ 'db.statement': 'INSERT INTO "User" ("email", "name") VALUES ($1, $2)',
+ 'sentry.origin': 'auto.db.otel.postgres',
+ 'sentry.op': 'db',
+ }),
+ description: 'INSERT INTO "User" ("email", "name") VALUES ($1, $2)',
+ op: 'db',
+ status: 'ok',
+ origin: 'auto.db.otel.postgres',
+ }),
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'db.system': 'postgresql',
+ 'db.name': 'tests',
+ 'db.statement': 'SELECT * FROM "User"',
+ 'sentry.origin': 'auto.db.otel.postgres',
+ 'sentry.op': 'db',
+ }),
+ description: 'SELECT * FROM "User"',
+ op: 'db',
+ status: 'ok',
+ origin: 'auto.db.otel.postgres',
+ }),
+ ]),
+ };
+
+ createRunner(__dirname, 'scenario.js')
+ .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] })
+ .expect({ transaction: EXPECTED_TRANSACTION })
+ .start(done);
+ });
+});
diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts
index 31969452ba74..515a2627acf8 100644
--- a/dev-packages/node-integration-tests/utils/runner.ts
+++ b/dev-packages/node-integration-tests/utils/runner.ts
@@ -1,5 +1,4 @@
-import type { ChildProcess } from 'child_process';
-import { spawn } from 'child_process';
+import { spawn, spawnSync } from 'child_process';
import { join } from 'path';
import type { Envelope, EnvelopeItemType, Event, SerializedSession } from '@sentry/types';
import axios from 'axios';
@@ -30,14 +29,17 @@ export function assertSentryTransaction(actual: Event, expected: Partial)
});
}
-const CHILD_PROCESSES = new Set();
+const CLEANUP_STEPS = new Set();
export function cleanupChildProcesses(): void {
- for (const child of CHILD_PROCESSES) {
- child.kill();
+ for (const step of CLEANUP_STEPS) {
+ step();
}
+ CLEANUP_STEPS.clear();
}
+process.on('exit', cleanupChildProcesses);
+
/** Promise only resolves when fn returns true */
async function waitFor(fn: () => boolean, timeout = 10_000): Promise {
let remaining = timeout;
@@ -50,6 +52,58 @@ async function waitFor(fn: () => boolean, timeout = 10_000): Promise {
}
}
+type VoidFunction = () => void;
+
+interface DockerOptions {
+ /**
+ * The working directory to run docker compose in
+ */
+ workingDirectory: string[];
+ /**
+ * The strings to look for in the output to know that the docker compose is ready for the test to be run
+ */
+ readyMatches: string[];
+}
+
+/**
+ * Runs docker compose up and waits for the readyMatches to appear in the output
+ *
+ * Returns a function that can be called to docker compose down
+ */
+async function runDockerCompose(options: DockerOptions): Promise {
+ return new Promise((resolve, reject) => {
+ const cwd = join(...options.workingDirectory);
+ const close = (): void => {
+ spawnSync('docker', ['compose', 'down', '--volumes'], { cwd });
+ };
+
+ // ensure we're starting fresh
+ close();
+
+ const child = spawn('docker', ['compose', 'up'], { cwd });
+
+ const timeout = setTimeout(() => {
+ close();
+ reject(new Error('Timed out waiting for docker-compose'));
+ }, 60_000);
+
+ function newData(data: Buffer): void {
+ const text = data.toString('utf8');
+
+ for (const match of options.readyMatches) {
+ if (text.includes(match)) {
+ child.stdout.removeAllListeners();
+ clearTimeout(timeout);
+ resolve(close);
+ }
+ }
+ }
+
+ child.stdout.on('data', newData);
+ child.stderr.on('data', newData);
+ });
+}
+
type Expected =
| {
event: Partial | ((event: Event) => void);
@@ -70,6 +124,7 @@ export function createRunner(...paths: string[]) {
const flags: string[] = [];
const ignored: EnvelopeItemType[] = [];
let withSentryServer = false;
+ let dockerOptions: DockerOptions | undefined;
let ensureNoErrorOutput = false;
if (testPath.endsWith('.ts')) {
@@ -93,6 +148,10 @@ export function createRunner(...paths: string[]) {
ignored.push(...types);
return this;
},
+ withDockerCompose: function (options: DockerOptions) {
+ dockerOptions = options;
+ return this;
+ },
ensureNoErrorOutput: function () {
ensureNoErrorOutput = true;
return this;
@@ -182,80 +241,94 @@ export function createRunner(...paths: string[]) {
? createBasicSentryServer(newEnvelope)
: Promise.resolve(undefined);
+ const dockerStartup: Promise = dockerOptions
+ ? runDockerCompose(dockerOptions)
+ : Promise.resolve(undefined);
+
+ const startup = Promise.all([dockerStartup, serverStartup]);
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
- serverStartup.then(mockServerPort => {
- const env = mockServerPort
- ? { ...process.env, SENTRY_DSN: `http://public@localhost:${mockServerPort}/1337` }
- : process.env;
+ startup
+ .then(([dockerChild, mockServerPort]) => {
+ if (dockerChild) {
+ CLEANUP_STEPS.add(dockerChild);
+ }
- // eslint-disable-next-line no-console
- if (process.env.DEBUG) console.log('starting scenario', testPath, flags, env.SENTRY_DSN);
+ const env = mockServerPort
+ ? { ...process.env, SENTRY_DSN: `http://public@localhost:${mockServerPort}/1337` }
+ : process.env;
- child = spawn('node', [...flags, testPath], { env });
+ // eslint-disable-next-line no-console
+ if (process.env.DEBUG) console.log('starting scenario', testPath, flags, env.SENTRY_DSN);
- CHILD_PROCESSES.add(child);
+ child = spawn('node', [...flags, testPath], { env });
- if (ensureNoErrorOutput) {
- child.stderr.on('data', (data: Buffer) => {
- const output = data.toString();
- complete(new Error(`Expected no error output but got: '${output}'`));
+ CLEANUP_STEPS.add(() => {
+ child?.kill();
});
- }
-
- child.on('close', () => {
- hasExited = true;
if (ensureNoErrorOutput) {
- complete();
+ child.stderr.on('data', (data: Buffer) => {
+ const output = data.toString();
+ complete(new Error(`Expected no error output but got: '${output}'`));
+ });
}
- });
- // Pass error to done to end the test quickly
- child.on('error', e => {
- // eslint-disable-next-line no-console
- if (process.env.DEBUG) console.log('scenario error', e);
- complete(e);
- });
-
- function tryParseEnvelopeFromStdoutLine(line: string): void {
- // Lines can have leading '[something] [{' which we need to remove
- const cleanedLine = line.replace(/^.*?] \[{"/, '[{"');
-
- // See if we have a port message
- if (cleanedLine.startsWith('{"port":')) {
- const { port } = JSON.parse(cleanedLine) as { port: number };
- scenarioServerPort = port;
- return;
- }
+ child.on('close', () => {
+ hasExited = true;
- // Skip any lines that don't start with envelope JSON
- if (!cleanedLine.startsWith('[{')) {
- return;
- }
+ if (ensureNoErrorOutput) {
+ complete();
+ }
+ });
- try {
- const envelope = JSON.parse(cleanedLine) as Envelope;
- newEnvelope(envelope);
- } catch (_) {
- //
- }
- }
+ // Pass error to done to end the test quickly
+ child.on('error', e => {
+ // eslint-disable-next-line no-console
+ if (process.env.DEBUG) console.log('scenario error', e);
+ complete(e);
+ });
- let buffer = Buffer.alloc(0);
- child.stdout.on('data', (data: Buffer) => {
- // This is horribly memory inefficient but it's only for tests
- buffer = Buffer.concat([buffer, data]);
+ function tryParseEnvelopeFromStdoutLine(line: string): void {
+ // Lines can have leading '[something] [{' which we need to remove
+ const cleanedLine = line.replace(/^.*?] \[{"/, '[{"');
- let splitIndex = -1;
- while ((splitIndex = buffer.indexOf(0xa)) >= 0) {
- const line = buffer.subarray(0, splitIndex).toString();
- buffer = Buffer.from(buffer.subarray(splitIndex + 1));
- // eslint-disable-next-line no-console
- if (process.env.DEBUG) console.log('line', line);
- tryParseEnvelopeFromStdoutLine(line);
+ // See if we have a port message
+ if (cleanedLine.startsWith('{"port":')) {
+ const { port } = JSON.parse(cleanedLine) as { port: number };
+ scenarioServerPort = port;
+ return;
+ }
+
+ // Skip any lines that don't start with envelope JSON
+ if (!cleanedLine.startsWith('[{')) {
+ return;
+ }
+
+ try {
+ const envelope = JSON.parse(cleanedLine) as Envelope;
+ newEnvelope(envelope);
+ } catch (_) {
+ //
+ }
}
- });
- });
+
+ let buffer = Buffer.alloc(0);
+ child.stdout.on('data', (data: Buffer) => {
+ // This is horribly memory inefficient but it's only for tests
+ buffer = Buffer.concat([buffer, data]);
+
+ let splitIndex = -1;
+ while ((splitIndex = buffer.indexOf(0xa)) >= 0) {
+ const line = buffer.subarray(0, splitIndex).toString();
+ buffer = Buffer.from(buffer.subarray(splitIndex + 1));
+ // eslint-disable-next-line no-console
+ if (process.env.DEBUG) console.log('line', line);
+ tryParseEnvelopeFromStdoutLine(line);
+ }
+ });
+ })
+ .catch(e => complete(e));
return {
childHasExited: function (): boolean {
diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts
index 2197c47381da..8d4bb2a7e371 100644
--- a/packages/astro/src/index.server.ts
+++ b/packages/astro/src/index.server.ts
@@ -46,7 +46,9 @@ export {
setTag,
setTags,
setUser,
+ // eslint-disable-next-line deprecation/deprecation
spanStatusfromHttpCode,
+ getSpanStatusFromHttpCode,
// eslint-disable-next-line deprecation/deprecation
trace,
withScope,
diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts
index d5cc61b73e95..8a7cc3d90384 100644
--- a/packages/astro/src/server/middleware.ts
+++ b/packages/astro/src/server/middleware.ts
@@ -1,4 +1,4 @@
-import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setHttpStatus } from '@sentry/core';
import {
captureException,
continueTrace,
@@ -119,9 +119,11 @@ async function instrumentRequest(
const res = await startSpan(
{
...traceCtx,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.astro',
+ },
name: `${method} ${interpolatedRoute || ctx.url.pathname}`,
op: 'http.server',
- origin: 'auto.http.astro',
status: 'ok',
metadata: {
// eslint-disable-next-line deprecation/deprecation
@@ -140,7 +142,7 @@ async function instrumentRequest(
const originalResponse = await next();
if (span && originalResponse.status) {
- span.setHttpStatus(originalResponse.status);
+ setHttpStatus(span, originalResponse.status);
}
const scope = getCurrentScope();
diff --git a/packages/astro/test/client/sdk.test.ts b/packages/astro/test/client/sdk.test.ts
index 2e10d4210953..3960c25eccd3 100644
--- a/packages/astro/test/client/sdk.test.ts
+++ b/packages/astro/test/client/sdk.test.ts
@@ -1,5 +1,6 @@
import type { BrowserClient } from '@sentry/browser';
-import { getCurrentScope } from '@sentry/browser';
+import { getActiveSpan } from '@sentry/browser';
+import { browserTracingIntegration, getCurrentScope } from '@sentry/browser';
import * as SentryBrowser from '@sentry/browser';
import { BrowserTracing, SDK_VERSION, WINDOW, getClient } from '@sentry/browser';
import { vi } from 'vitest';
@@ -100,7 +101,7 @@ describe('Sentry client SDK', () => {
delete globalThis.__SENTRY_TRACING__;
});
- it('Overrides the automatically default BrowserTracing instance with a a user-provided instance', () => {
+ it('Overrides the automatically default BrowserTracing instance with a a user-provided BrowserTracing instance', () => {
init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [new BrowserTracing({ finalTimeout: 10, startTransactionOnLocationChange: false })],
@@ -118,6 +119,22 @@ describe('Sentry client SDK', () => {
// This shows that the user-configured options are still here
expect(options.finalTimeout).toEqual(10);
});
+
+ it('Overrides the automatically default BrowserTracing instance with a a user-provided browserTracingIntergation instance', () => {
+ init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ browserTracingIntegration({ finalTimeout: 10, instrumentNavigation: false, instrumentPageLoad: false }),
+ ],
+ enableTracing: true,
+ });
+
+ const browserTracing = getClient()?.getIntegrationByName('BrowserTracing');
+ expect(browserTracing).toBeDefined();
+
+ // no active span means the settings were respected
+ expect(getActiveSpan()).toBeUndefined();
+ });
});
});
});
diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts
index 9fa5bc430c90..c641f5ac6177 100644
--- a/packages/astro/test/server/middleware.test.ts
+++ b/packages/astro/test/server/middleware.test.ts
@@ -58,6 +58,9 @@ describe('sentryMiddleware', () => {
expect(startSpanSpy).toHaveBeenCalledWith(
{
+ attributes: {
+ 'sentry.origin': 'auto.http.astro',
+ },
data: {
method: 'GET',
url: 'https://mydomain.io/users/123/details',
@@ -66,7 +69,6 @@ describe('sentryMiddleware', () => {
metadata: {},
name: 'GET /users/[id]/details',
op: 'http.server',
- origin: 'auto.http.astro',
status: 'ok',
},
expect.any(Function), // the `next` function
@@ -94,6 +96,9 @@ describe('sentryMiddleware', () => {
expect(startSpanSpy).toHaveBeenCalledWith(
{
+ attributes: {
+ 'sentry.origin': 'auto.http.astro',
+ },
data: {
method: 'GET',
url: 'http://localhost:1234/a%xx',
@@ -102,7 +107,6 @@ describe('sentryMiddleware', () => {
metadata: {},
name: 'GET a%xx',
op: 'http.server',
- origin: 'auto.http.astro',
status: 'ok',
},
expect.any(Function), // the `next` function
diff --git a/packages/browser/src/helpers.ts b/packages/browser/src/helpers.ts
index 1bc58b780748..5fff014eaa8d 100644
--- a/packages/browser/src/helpers.ts
+++ b/packages/browser/src/helpers.ts
@@ -1,5 +1,7 @@
+import type { browserTracingIntegration } from '@sentry-internal/tracing';
+import { BrowserTracing } from '@sentry-internal/tracing';
import { captureException, withScope } from '@sentry/core';
-import type { DsnLike, Mechanism, WrappedFunction } from '@sentry/types';
+import type { DsnLike, Integration, Mechanism, WrappedFunction } from '@sentry/types';
import {
GLOBAL_OBJ,
addExceptionMechanism,
@@ -185,3 +187,33 @@ export interface ReportDialogOptions {
/** Callback after reportDialog closed */
onClose?(this: void): void;
}
+
+/**
+ * This is a slim shim of `browserTracingIntegration` for the CDN bundles.
+ * Since the actual functional integration uses a different code from `BrowserTracing`,
+ * we want to avoid shipping both of them in the CDN bundles, as that would blow up the size.
+ * Instead, we provide a functional integration with the same API, but the old implementation.
+ * This means that it's not possible to register custom routing instrumentation, but that's OK for now.
+ * We also don't expose the utilities for this anyhow in the CDN bundles.
+ * For users that need custom routing in CDN bundles, they have to continue using `new BrowserTracing()` until v8.
+ */
+export function bundleBrowserTracingIntegration(
+ options: Parameters[0] = {},
+): Integration {
+ // Migrate some options from the old integration to the new one
+ const opts: ConstructorParameters[0] = options;
+
+ if (typeof options.markBackgroundSpan === 'boolean') {
+ opts.markBackgroundTransactions = options.markBackgroundSpan;
+ }
+
+ if (typeof options.instrumentPageLoad === 'boolean') {
+ opts.startTransactionOnPageLoad = options.instrumentPageLoad;
+ }
+
+ if (typeof options.instrumentNavigation === 'boolean') {
+ opts.startTransactionOnLocationChange = options.instrumentNavigation;
+ }
+
+ return new BrowserTracing(opts);
+}
diff --git a/packages/browser/src/index.bundle.feedback.ts b/packages/browser/src/index.bundle.feedback.ts
index 5d3612106286..af4de5ea063d 100644
--- a/packages/browser/src/index.bundle.feedback.ts
+++ b/packages/browser/src/index.bundle.feedback.ts
@@ -1,6 +1,12 @@
// This is exported so the loader does not fail when switching off Replay/Tracing
import { Feedback, feedbackIntegration } from '@sentry-internal/feedback';
-import { BrowserTracing, Replay, addTracingExtensions, replayIntegration } from '@sentry-internal/integration-shims';
+import {
+ BrowserTracing,
+ Replay,
+ addTracingExtensions,
+ browserTracingIntegration,
+ replayIntegration,
+} from '@sentry-internal/integration-shims';
import * as Sentry from './index.bundle.base';
@@ -13,6 +19,7 @@ Sentry.Integrations.BrowserTracing = BrowserTracing;
export * from './index.bundle.base';
export {
BrowserTracing,
+ browserTracingIntegration,
addTracingExtensions,
// eslint-disable-next-line deprecation/deprecation
Replay,
diff --git a/packages/browser/src/index.bundle.replay.ts b/packages/browser/src/index.bundle.replay.ts
index 2609e7d9b48c..175a435fadcf 100644
--- a/packages/browser/src/index.bundle.replay.ts
+++ b/packages/browser/src/index.bundle.replay.ts
@@ -3,6 +3,7 @@ import {
BrowserTracing,
Feedback,
addTracingExtensions,
+ browserTracingIntegration,
feedbackIntegration,
} from '@sentry-internal/integration-shims';
import { Replay, replayIntegration } from '@sentry/replay';
@@ -18,6 +19,7 @@ Sentry.Integrations.BrowserTracing = BrowserTracing;
export * from './index.bundle.base';
export {
BrowserTracing,
+ browserTracingIntegration,
addTracingExtensions,
// eslint-disable-next-line deprecation/deprecation
Replay,
diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts
index e17c7de4159a..df151bba0a8f 100644
--- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts
+++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts
@@ -1,6 +1,7 @@
import { Feedback, feedbackIntegration } from '@sentry-internal/feedback';
import { BrowserTracing, Span, addExtensionMethods } from '@sentry-internal/tracing';
import { Replay, replayIntegration } from '@sentry/replay';
+import { bundleBrowserTracingIntegration as browserTracingIntegration } from './helpers';
import * as Sentry from './index.bundle.base';
@@ -23,6 +24,7 @@ export {
feedbackIntegration,
replayIntegration,
BrowserTracing,
+ browserTracingIntegration,
Span,
addExtensionMethods,
};
diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts
index 5dc0537be064..2437a8546d5c 100644
--- a/packages/browser/src/index.bundle.tracing.replay.ts
+++ b/packages/browser/src/index.bundle.tracing.replay.ts
@@ -1,6 +1,7 @@
import { Feedback, feedbackIntegration } from '@sentry-internal/integration-shims';
import { BrowserTracing, Span, addExtensionMethods } from '@sentry-internal/tracing';
import { Replay, replayIntegration } from '@sentry/replay';
+import { bundleBrowserTracingIntegration as browserTracingIntegration } from './helpers';
import * as Sentry from './index.bundle.base';
@@ -23,6 +24,7 @@ export {
replayIntegration,
feedbackIntegration,
BrowserTracing,
+ browserTracingIntegration,
Span,
addExtensionMethods,
};
diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts
index f810b61b92a7..2ca0613146f0 100644
--- a/packages/browser/src/index.bundle.tracing.ts
+++ b/packages/browser/src/index.bundle.tracing.ts
@@ -1,6 +1,7 @@
// This is exported so the loader does not fail when switching off Replay
import { Feedback, Replay, feedbackIntegration, replayIntegration } from '@sentry-internal/integration-shims';
import { BrowserTracing, Span, addExtensionMethods } from '@sentry-internal/tracing';
+import { bundleBrowserTracingIntegration as browserTracingIntegration } from './helpers';
import * as Sentry from './index.bundle.base';
@@ -23,6 +24,7 @@ export {
feedbackIntegration,
replayIntegration,
BrowserTracing,
+ browserTracingIntegration,
Span,
addExtensionMethods,
};
diff --git a/packages/browser/src/index.bundle.ts b/packages/browser/src/index.bundle.ts
index a92ff6bf66ec..93a0b0cb498a 100644
--- a/packages/browser/src/index.bundle.ts
+++ b/packages/browser/src/index.bundle.ts
@@ -4,6 +4,7 @@ import {
Feedback,
Replay,
addTracingExtensions,
+ browserTracingIntegration,
feedbackIntegration,
replayIntegration,
} from '@sentry-internal/integration-shims';
@@ -24,6 +25,7 @@ export {
Replay,
// eslint-disable-next-line deprecation/deprecation
Feedback,
+ browserTracingIntegration,
feedbackIntegration,
replayIntegration,
};
diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts
index 19c377fc5931..408a64081a02 100644
--- a/packages/browser/src/index.ts
+++ b/packages/browser/src/index.ts
@@ -57,6 +57,9 @@ export {
BrowserTracing,
defaultRequestInstrumentationOptions,
instrumentOutgoingRequests,
+ browserTracingIntegration,
+ startBrowserTracingNavigationSpan,
+ startBrowserTracingPageLoadSpan,
} from '@sentry-internal/tracing';
export type { RequestInstrumentationOptions } from '@sentry-internal/tracing';
export {
@@ -66,7 +69,9 @@ export {
extractTraceparentData,
// eslint-disable-next-line deprecation/deprecation
getActiveTransaction,
+ // eslint-disable-next-line deprecation/deprecation
spanStatusfromHttpCode,
+ getSpanStatusFromHttpCode,
// eslint-disable-next-line deprecation/deprecation
trace,
makeMultiplexedTransport,
diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts
index 393e534e12ee..5742597485e0 100644
--- a/packages/bun/src/index.ts
+++ b/packages/bun/src/index.ts
@@ -66,7 +66,9 @@ export {
setTag,
setTags,
setUser,
+ // eslint-disable-next-line deprecation/deprecation
spanStatusfromHttpCode,
+ getSpanStatusFromHttpCode,
// eslint-disable-next-line deprecation/deprecation
trace,
withScope,
@@ -87,7 +89,22 @@ export {
parameterize,
} from '@sentry/core';
export type { SpanStatusType } from '@sentry/core';
-export { autoDiscoverNodePerformanceMonitoringIntegrations, cron } from '@sentry/node';
+export {
+ // eslint-disable-next-line deprecation/deprecation
+ deepReadDirSync,
+ // eslint-disable-next-line deprecation/deprecation
+ enableAnrDetection,
+ // eslint-disable-next-line deprecation/deprecation
+ getModuleFromFilename,
+ DEFAULT_USER_INCLUDES,
+ autoDiscoverNodePerformanceMonitoringIntegrations,
+ cron,
+ createGetModuleFromFilename,
+ defaultStackParser,
+ extractRequestData,
+ getSentryRelease,
+ addRequestDataToEvent,
+} from '@sentry/node';
export { BunClient } from './client';
export {
diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts
index fb12cf94432b..b1dc4c6892e0 100644
--- a/packages/bun/src/integrations/bunserver.ts
+++ b/packages/bun/src/integrations/bunserver.ts
@@ -1,4 +1,5 @@
import {
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
Transaction,
captureException,
@@ -6,6 +7,7 @@ import {
convertIntegrationFnToClass,
getCurrentScope,
runWithAsyncContext,
+ setHttpStatus,
startSpan,
} from '@sentry/core';
import type { IntegrationFn } from '@sentry/types';
@@ -69,9 +71,11 @@ function instrumentBunServeOptions(serveOptions: Parameters[0]
ctx => {
return startSpan(
{
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.bun.serve',
+ },
op: 'http.server',
name: `${request.method} ${parsedUrl.path || '/'}`,
- origin: 'auto.http.bun.serve',
...ctx,
data,
metadata: {
@@ -90,8 +94,9 @@ function instrumentBunServeOptions(serveOptions: Parameters[0]
typeof serveOptions.fetch
>);
if (response && response.status) {
- span?.setHttpStatus(response.status);
- span?.setAttribute('http.response.status_code', response.status);
+ if (span) {
+ setHttpStatus(span, response.status);
+ }
if (span instanceof Transaction) {
const scope = getCurrentScope();
scope.setContext('response', {
diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts
index d59d596e0b82..b3eb10f686d7 100644
--- a/packages/core/src/baseclient.ts
+++ b/packages/core/src/baseclient.ts
@@ -20,12 +20,12 @@ import type {
MetricsAggregator,
Outcome,
ParameterizedString,
- PropagationContext,
SdkMetadata,
Session,
SessionAggregates,
Severity,
SeverityLevel,
+ StartSpanOptions,
Transaction,
TransactionEvent,
Transport,
@@ -53,6 +53,7 @@ import { createEventEnvelope, createSessionEnvelope } from './envelope';
import { getClient } from './exports';
import { getIsolationScope } from './hub';
import type { IntegrationIndex } from './integration';
+import { afterSetupIntegrations } from './integration';
import { setupIntegration, setupIntegrations } from './integration';
import { createMetricEnvelope } from './metrics/envelope';
import type { Scope } from './scope';
@@ -366,7 +367,14 @@ export abstract class BaseClient implements Client {
* @inheritDoc
*/
public addIntegration(integration: Integration): void {
+ const isAlreadyInstalled = this._integrations[integration.name];
+
+ // This hook takes care of only installing if not already installed
setupIntegration(this, integration, this._integrations);
+ // Here we need to check manually to make sure to not run this multiple times
+ if (!isAlreadyInstalled) {
+ afterSetupIntegrations(this, [integration]);
+ }
}
/**
@@ -481,6 +489,12 @@ export abstract class BaseClient implements Client {
callback: (feedback: FeedbackEvent, options?: { includeReplay: boolean }) => void,
): void;
+ /** @inheritdoc */
+ public on(hook: 'startPageLoadSpan', callback: (options: StartSpanOptions) => void): void;
+
+ /** @inheritdoc */
+ public on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void;
+
/** @inheritdoc */
public on(hook: string, callback: unknown): void {
if (!this._hooks[hook]) {
@@ -521,6 +535,12 @@ export abstract class BaseClient implements Client {
/** @inheritdoc */
public emit(hook: 'beforeSendFeedback', feedback: FeedbackEvent, options?: { includeReplay: boolean }): void;
+ /** @inheritdoc */
+ public emit(hook: 'startPageLoadSpan', options: StartSpanOptions): void;
+
+ /** @inheritdoc */
+ public emit(hook: 'startNavigationSpan', options: StartSpanOptions): void;
+
/** @inheritdoc */
public emit(hook: string, ...rest: unknown[]): void {
if (this._hooks[hook]) {
@@ -532,7 +552,10 @@ export abstract class BaseClient implements Client {
/** Setup integrations for this client. */
protected _setupIntegrations(): void {
- this._integrations = setupIntegrations(this, this._options.integrations);
+ const { integrations } = this._options;
+ this._integrations = setupIntegrations(this, integrations);
+ afterSetupIntegrations(this, integrations);
+
// TODO v8: We don't need this flag anymore
this._integrationsInitialized = true;
}
@@ -638,13 +661,14 @@ export abstract class BaseClient implements Client {
return evt;
}
- // If a trace context is not set on the event, we use the propagationContext set on the event to
- // generate a trace context. If the propagationContext does not have a dynamic sampling context, we
- // also generate one for it.
- const { propagationContext } = evt.sdkProcessingMetadata || {};
+ const propagationContext = {
+ ...isolationScope.getPropagationContext(),
+ ...(scope ? scope.getPropagationContext() : undefined),
+ };
+
const trace = evt.contexts && evt.contexts.trace;
if (!trace && propagationContext) {
- const { traceId: trace_id, spanId, parentSpanId, dsc } = propagationContext as PropagationContext;
+ const { traceId: trace_id, spanId, parentSpanId, dsc } = propagationContext;
evt.contexts = {
trace: {
trace_id,
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 261a40a1fa8f..849e34f6c92b 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -87,6 +87,7 @@ export {
spanToTraceHeader,
spanToJSON,
spanIsSampled,
+ spanToTraceContext,
} from './utils/spanUtils';
export { getRootSpan } from './utils/getRootSpan';
export { applySdkMetadata } from './utils/sdkMetadata';
diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts
index 5b126459390b..8a91fa10e303 100644
--- a/packages/core/src/integration.ts
+++ b/packages/core/src/integration.ts
@@ -108,6 +108,18 @@ export function setupIntegrations(client: Client, integrations: Integration[]):
return integrationIndex;
}
+/**
+ * Execute the `afterAllSetup` hooks of the given integrations.
+ */
+export function afterSetupIntegrations(client: Client, integrations: Integration[]): void {
+ for (const integration of integrations) {
+ // guard against empty provided integrations
+ if (integration && integration.afterAllSetup) {
+ integration.afterAllSetup(client);
+ }
+ }
+}
+
/** Setup a single integration. */
export function setupIntegration(client: Client, integration: Integration, integrationIndex: IntegrationIndex): void {
if (integrationIndex[integration.name]) {
diff --git a/packages/core/src/integrations/functiontostring.ts b/packages/core/src/integrations/functiontostring.ts
index 7a50aec67f2d..0f3e9f08b59e 100644
--- a/packages/core/src/integrations/functiontostring.ts
+++ b/packages/core/src/integrations/functiontostring.ts
@@ -1,11 +1,14 @@
-import type { Integration, IntegrationClass, IntegrationFn, WrappedFunction } from '@sentry/types';
+import type { Client, Integration, IntegrationClass, IntegrationFn, WrappedFunction } from '@sentry/types';
import { getOriginalFunction } from '@sentry/utils';
+import { getClient } from '../exports';
import { convertIntegrationFnToClass, defineIntegration } from '../integration';
let originalFunctionToString: () => void;
const INTEGRATION_NAME = 'FunctionToString';
+const SETUP_CLIENTS = new WeakMap();
+
const _functionToStringIntegration = (() => {
return {
name: INTEGRATION_NAME,
@@ -18,20 +21,37 @@ const _functionToStringIntegration = (() => {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Function.prototype.toString = function (this: WrappedFunction, ...args: any[]): string {
- const context = getOriginalFunction(this) || this;
+ const originalFunction = getOriginalFunction(this);
+ const context =
+ SETUP_CLIENTS.has(getClient() as Client) && originalFunction !== undefined ? originalFunction : this;
return originalFunctionToString.apply(context, args);
};
} catch {
// ignore errors here, just don't patch this
}
},
+ setup(client) {
+ SETUP_CLIENTS.set(client, true);
+ },
};
}) satisfies IntegrationFn;
+/**
+ * Patch toString calls to return proper name for wrapped functions.
+ *
+ * ```js
+ * Sentry.init({
+ * integrations: [
+ * functionToStringIntegration(),
+ * ],
+ * });
+ * ```
+ */
export const functionToStringIntegration = defineIntegration(_functionToStringIntegration);
/**
* Patch toString calls to return proper name for wrapped functions.
+ *
* @deprecated Use `functionToStringIntegration()` instead.
*/
// eslint-disable-next-line deprecation/deprecation
@@ -39,3 +59,6 @@ export const FunctionToString = convertIntegrationFnToClass(
INTEGRATION_NAME,
functionToStringIntegration,
) as IntegrationClass void }>;
+
+// eslint-disable-next-line deprecation/deprecation
+export type FunctionToString = typeof FunctionToString;
diff --git a/packages/core/src/tracing/errors.ts b/packages/core/src/tracing/errors.ts
index 5a885dd1f090..229695afc58c 100644
--- a/packages/core/src/tracing/errors.ts
+++ b/packages/core/src/tracing/errors.ts
@@ -5,7 +5,7 @@ import {
} from '@sentry/utils';
import { DEBUG_BUILD } from '../debug-build';
-import type { SpanStatusType } from './span';
+import type { SpanStatusType } from './spanstatus';
import { getActiveTransaction } from './utils';
let errorsInstrumented = false;
diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts
index ecdc5f595095..d1e1c7f65b44 100644
--- a/packages/core/src/tracing/index.ts
+++ b/packages/core/src/tracing/index.ts
@@ -1,13 +1,19 @@
export { startIdleTransaction, addTracingExtensions } from './hubextensions';
export { IdleTransaction, TRACING_DEFAULTS } from './idletransaction';
export type { BeforeFinishCallback } from './idletransaction';
-export { Span, spanStatusfromHttpCode } from './span';
+export { Span } from './span';
export { Transaction } from './transaction';
// eslint-disable-next-line deprecation/deprecation
export { extractTraceparentData, getActiveTransaction } from './utils';
// eslint-disable-next-line deprecation/deprecation
export { SpanStatus } from './spanstatus';
-export type { SpanStatusType } from './span';
+export {
+ setHttpStatus,
+ // eslint-disable-next-line deprecation/deprecation
+ spanStatusfromHttpCode,
+ getSpanStatusFromHttpCode,
+} from './spanstatus';
+export type { SpanStatusType } from './spanstatus';
export {
// eslint-disable-next-line deprecation/deprecation
trace,
diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts
index 8ddfddb3b353..165677455d7f 100644
--- a/packages/core/src/tracing/span.ts
+++ b/packages/core/src/tracing/span.ts
@@ -26,6 +26,8 @@ import {
spanToTraceContext,
spanToTraceHeader,
} from '../utils/spanUtils';
+import type { SpanStatusType } from './spanstatus';
+import { setHttpStatus } from './spanstatus';
/**
* Keeps track of finished spans for a given transaction
@@ -130,11 +132,15 @@ export class Span implements SpanInterface {
this.tags = spanContext.tags ? { ...spanContext.tags } : {};
// eslint-disable-next-line deprecation/deprecation
this.data = spanContext.data ? { ...spanContext.data } : {};
- this._attributes = spanContext.attributes ? { ...spanContext.attributes } : {};
// eslint-disable-next-line deprecation/deprecation
this.instrumenter = spanContext.instrumenter || 'sentry';
- this.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanContext.origin || 'manual');
+ this._attributes = {};
+ this.setAttributes({
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanContext.origin || 'manual',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: spanContext.op,
+ ...spanContext.attributes,
+ });
// eslint-disable-next-line deprecation/deprecation
this._name = spanContext.name || spanContext.description;
@@ -146,9 +152,6 @@ export class Span implements SpanInterface {
if ('sampled' in spanContext) {
this._sampled = spanContext.sampled;
}
- if (spanContext.op) {
- this.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, spanContext.op);
- }
if (spanContext.status) {
this._status = spanContext.status;
}
@@ -469,16 +472,10 @@ export class Span implements SpanInterface {
/**
* @inheritDoc
+ * @deprecated Use top-level `setHttpStatus()` instead.
*/
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') {
- this.setStatus(spanStatus);
- }
+ setHttpStatus(this, httpStatus);
return this;
}
@@ -674,85 +671,3 @@ export class Span implements SpanInterface {
return hasData ? data : attributes;
}
}
-
-export type SpanStatusType =
- /** The operation completed successfully. */
- | 'ok'
- /** Deadline expired before operation could complete. */
- | 'deadline_exceeded'
- /** 401 Unauthorized (actually does mean unauthenticated according to RFC 7235) */
- | 'unauthenticated'
- /** 403 Forbidden */
- | 'permission_denied'
- /** 404 Not Found. Some requested entity (file or directory) was not found. */
- | 'not_found'
- /** 429 Too Many Requests */
- | 'resource_exhausted'
- /** Client specified an invalid argument. 4xx. */
- | 'invalid_argument'
- /** 501 Not Implemented */
- | 'unimplemented'
- /** 503 Service Unavailable */
- | 'unavailable'
- /** Other/generic 5xx. */
- | 'internal_error'
- /** Unknown. Any non-standard HTTP status code. */
- | 'unknown_error'
- /** The operation was cancelled (typically by the user). */
- | 'cancelled'
- /** Already exists (409) */
- | 'already_exists'
- /** Operation was rejected because the system is not in a state required for the operation's */
- | 'failed_precondition'
- /** The operation was aborted, typically due to a concurrency issue. */
- | 'aborted'
- /** Operation was attempted past the valid range. */
- | 'out_of_range'
- /** Unrecoverable data loss or corruption */
- | 'data_loss';
-
-/**
- * Converts a HTTP status code into a {@link SpanStatusType}.
- *
- * @param httpStatus The HTTP response status code.
- * @returns The span status or unknown_error.
- */
-export function spanStatusfromHttpCode(httpStatus: number): SpanStatusType {
- if (httpStatus < 400 && httpStatus >= 100) {
- return 'ok';
- }
-
- if (httpStatus >= 400 && httpStatus < 500) {
- switch (httpStatus) {
- case 401:
- return 'unauthenticated';
- case 403:
- return 'permission_denied';
- case 404:
- return 'not_found';
- case 409:
- return 'already_exists';
- case 413:
- return 'failed_precondition';
- case 429:
- return 'resource_exhausted';
- default:
- return 'invalid_argument';
- }
- }
-
- if (httpStatus >= 500 && httpStatus < 600) {
- switch (httpStatus) {
- case 501:
- return 'unimplemented';
- case 503:
- return 'unavailable';
- case 504:
- return 'deadline_exceeded';
- default:
- return 'internal_error';
- }
- }
-
- return 'unknown_error';
-}
diff --git a/packages/core/src/tracing/spanstatus.ts b/packages/core/src/tracing/spanstatus.ts
index 6a758d95ee84..aa0d1639a70c 100644
--- a/packages/core/src/tracing/spanstatus.ts
+++ b/packages/core/src/tracing/spanstatus.ts
@@ -1,3 +1,5 @@
+import type { Span } from '@sentry/types';
+
/** The status of an Span.
*
* @deprecated Use string literals - if you require type casting, cast to SpanStatusType type
@@ -38,3 +40,119 @@ export enum SpanStatus {
/** Unrecoverable data loss or corruption */
DataLoss = 'data_loss',
}
+
+export type SpanStatusType =
+ /** The operation completed successfully. */
+ | 'ok'
+ /** Deadline expired before operation could complete. */
+ | 'deadline_exceeded'
+ /** 401 Unauthorized (actually does mean unauthenticated according to RFC 7235) */
+ | 'unauthenticated'
+ /** 403 Forbidden */
+ | 'permission_denied'
+ /** 404 Not Found. Some requested entity (file or directory) was not found. */
+ | 'not_found'
+ /** 429 Too Many Requests */
+ | 'resource_exhausted'
+ /** Client specified an invalid argument. 4xx. */
+ | 'invalid_argument'
+ /** 501 Not Implemented */
+ | 'unimplemented'
+ /** 503 Service Unavailable */
+ | 'unavailable'
+ /** Other/generic 5xx. */
+ | 'internal_error'
+ /** Unknown. Any non-standard HTTP status code. */
+ | 'unknown_error'
+ /** The operation was cancelled (typically by the user). */
+ | 'cancelled'
+ /** Already exists (409) */
+ | 'already_exists'
+ /** Operation was rejected because the system is not in a state required for the operation's */
+ | 'failed_precondition'
+ /** The operation was aborted, typically due to a concurrency issue. */
+ | 'aborted'
+ /** Operation was attempted past the valid range. */
+ | 'out_of_range'
+ /** Unrecoverable data loss or corruption */
+ | 'data_loss';
+
+/**
+ * Converts a HTTP status code into a {@link SpanStatusType}.
+ *
+ * @param httpStatus The HTTP response status code.
+ * @returns The span status or unknown_error.
+ */
+export function getSpanStatusFromHttpCode(httpStatus: number): SpanStatusType {
+ if (httpStatus < 400 && httpStatus >= 100) {
+ return 'ok';
+ }
+
+ if (httpStatus >= 400 && httpStatus < 500) {
+ switch (httpStatus) {
+ case 401:
+ return 'unauthenticated';
+ case 403:
+ return 'permission_denied';
+ case 404:
+ return 'not_found';
+ case 409:
+ return 'already_exists';
+ case 413:
+ return 'failed_precondition';
+ case 429:
+ return 'resource_exhausted';
+ default:
+ return 'invalid_argument';
+ }
+ }
+
+ if (httpStatus >= 500 && httpStatus < 600) {
+ switch (httpStatus) {
+ case 501:
+ return 'unimplemented';
+ case 503:
+ return 'unavailable';
+ case 504:
+ return 'deadline_exceeded';
+ default:
+ return 'internal_error';
+ }
+ }
+
+ return 'unknown_error';
+}
+
+/**
+ * Converts a HTTP status code into a {@link SpanStatusType}.
+ *
+ * @deprecated Use {@link spanStatusFromHttpCode} instead.
+ * This export will be removed in v8 as the signature contains a typo.
+ *
+ * @param httpStatus The HTTP response status code.
+ * @returns The span status or unknown_error.
+ */
+export const spanStatusfromHttpCode = getSpanStatusFromHttpCode;
+
+/**
+ * Sets the Http status attributes on the current span based on the http code.
+ * Additionally, the span's status is updated, depending on the http code.
+ */
+export function setHttpStatus(span: Span, httpStatus: number): void {
+ // TODO (v8): Remove these calls
+ // Relay does not require us to send the status code as a tag
+ // For now, just because users might expect it to land as a tag we keep sending it.
+ // Same with data.
+ // In v8, we replace both, simply with
+ // span.setAttribute('http.response.status_code', httpStatus);
+
+ // eslint-disable-next-line deprecation/deprecation
+ span.setTag('http.status_code', String(httpStatus));
+ // eslint-disable-next-line deprecation/deprecation
+ span.setData('http.response.status_code', httpStatus);
+
+ const spanStatus = getSpanStatusFromHttpCode(httpStatus);
+ if (spanStatus !== 'unknown_error') {
+ span.setStatus(spanStatus);
+ }
+}
diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts
index eb070510a3b7..dc822a2bab7d 100644
--- a/packages/core/src/tracing/trace.ts
+++ b/packages/core/src/tracing/trace.ts
@@ -1,124 +1,17 @@
-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 type { Span, SpanTimeInput, StartSpanOptions, TransactionContext } from '@sentry/types';
+
import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils';
import { DEBUG_BUILD } from '../debug-build';
import { getCurrentScope, withScope } from '../exports';
import type { Hub } from '../hub';
+import { runWithAsyncContext } from '../hub';
+import { getIsolationScope } from '../hub';
import { getCurrentHub } from '../hub';
import { handleCallbackErrors } from '../utils/handleCallbackErrors';
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
import { spanTimeInputToSeconds, spanToJSON } 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.
*
@@ -182,29 +75,33 @@ export function trace(
export function startSpan(context: StartSpanOptions, callback: (span: Span | undefined) => T): T {
const ctx = normalizeContext(context);
- return withScope(context.scope, scope => {
- // eslint-disable-next-line deprecation/deprecation
- const hub = getCurrentHub();
- // eslint-disable-next-line deprecation/deprecation
- const parentSpan = scope.getSpan();
+ return runWithAsyncContext(() => {
+ return withScope(context.scope, scope => {
+ // eslint-disable-next-line deprecation/deprecation
+ 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(
- () => callback(activeSpan),
- () => {
- // Only update the span status if it hasn't been changed yet
- if (activeSpan) {
- const { status } = spanToJSON(activeSpan);
- if (!status || status === 'ok') {
- activeSpan.setStatus('internal_error');
+ const shouldSkipSpan = context.onlyIfParent && !parentSpan;
+ const activeSpan = shouldSkipSpan ? undefined : createChildSpanOrTransaction(hub, parentSpan, ctx);
+
+ // eslint-disable-next-line deprecation/deprecation
+ scope.setSpan(activeSpan);
+
+ return handleCallbackErrors(
+ () => callback(activeSpan),
+ () => {
+ // Only update the span status if it hasn't been changed yet
+ if (activeSpan) {
+ const { status } = spanToJSON(activeSpan);
+ if (!status || status === 'ok') {
+ activeSpan.setStatus('internal_error');
+ }
}
- }
- },
- () => activeSpan && activeSpan.end(),
- );
+ },
+ () => activeSpan && activeSpan.end(),
+ );
+ });
});
}
@@ -230,32 +127,36 @@ export function startSpanManual(
): T {
const ctx = normalizeContext(context);
- return withScope(context.scope, scope => {
- // eslint-disable-next-line deprecation/deprecation
- const hub = getCurrentHub();
- // eslint-disable-next-line deprecation/deprecation
- const parentSpan = scope.getSpan();
+ return runWithAsyncContext(() => {
+ return withScope(context.scope, scope => {
+ // eslint-disable-next-line deprecation/deprecation
+ 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);
+ const shouldSkipSpan = context.onlyIfParent && !parentSpan;
+ const activeSpan = shouldSkipSpan ? undefined : createChildSpanOrTransaction(hub, parentSpan, ctx);
- function finishAndSetSpan(): void {
- activeSpan && activeSpan.end();
- }
-
- return handleCallbackErrors(
- () => callback(activeSpan, finishAndSetSpan),
- () => {
- // Only update the span status if it hasn't been changed yet, and the span is not yet finished
- if (activeSpan && activeSpan.isRecording()) {
- const { status } = spanToJSON(activeSpan);
- if (!status || status === 'ok') {
- activeSpan.setStatus('internal_error');
+ // eslint-disable-next-line deprecation/deprecation
+ scope.setSpan(activeSpan);
+
+ function finishAndSetSpan(): void {
+ activeSpan && activeSpan.end();
+ }
+
+ return handleCallbackErrors(
+ () => callback(activeSpan, finishAndSetSpan),
+ () => {
+ // Only update the span status if it hasn't been changed yet, and the span is not yet finished
+ if (activeSpan && activeSpan.isRecording()) {
+ const { status } = spanToJSON(activeSpan);
+ if (!status || status === 'ok') {
+ activeSpan.setStatus('internal_error');
+ }
}
- }
- },
- );
+ },
+ );
+ });
});
}
@@ -281,11 +182,38 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined {
? // 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);
+
+ const shouldSkipSpan = context.onlyIfParent && !parentSpan;
+
+ if (shouldSkipSpan) {
+ return undefined;
+ }
+
+ if (parentSpan) {
+ // eslint-disable-next-line deprecation/deprecation
+ return parentSpan.startChild(ctx);
+ } else {
+ const isolationScope = getIsolationScope();
+ const scope = getCurrentScope();
+
+ const { traceId, dsc, parentSpanId, sampled } = {
+ ...isolationScope.getPropagationContext(),
+ ...scope.getPropagationContext(),
+ };
+
+ // eslint-disable-next-line deprecation/deprecation
+ return hub.startTransaction({
+ traceId,
+ parentSpanId,
+ parentSampled: sampled,
+ ...ctx,
+ metadata: {
+ dynamicSamplingContext: dsc,
+ // eslint-disable-next-line deprecation/deprecation
+ ...ctx.metadata,
+ },
+ });
+ }
}
/**
@@ -346,7 +274,7 @@ export function continueTrace(
const transactionContext: Partial = {
...traceparentData,
metadata: dropUndefinedKeys({
- dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
+ dynamicSamplingContext,
}),
};
@@ -365,11 +293,32 @@ function createChildSpanOrTransaction(
if (!hasTracingEnabled()) {
return undefined;
}
- return parentSpan
- ? // eslint-disable-next-line deprecation/deprecation
- parentSpan.startChild(ctx)
- : // eslint-disable-next-line deprecation/deprecation
- hub.startTransaction(ctx);
+
+ if (parentSpan) {
+ // eslint-disable-next-line deprecation/deprecation
+ return parentSpan.startChild(ctx);
+ } else {
+ const isolationScope = getIsolationScope();
+ const scope = getCurrentScope();
+
+ const { traceId, dsc, parentSpanId, sampled } = {
+ ...isolationScope.getPropagationContext(),
+ ...scope.getPropagationContext(),
+ };
+
+ // eslint-disable-next-line deprecation/deprecation
+ return hub.startTransaction({
+ traceId,
+ parentSpanId,
+ parentSampled: sampled,
+ ...ctx,
+ metadata: {
+ dynamicSamplingContext: dsc,
+ // eslint-disable-next-line deprecation/deprecation
+ ...ctx.metadata,
+ },
+ });
+ }
}
/**
diff --git a/packages/core/src/utils/applyScopeDataToEvent.ts b/packages/core/src/utils/applyScopeDataToEvent.ts
index bdf60497cff1..92a5e6747cd8 100644
--- a/packages/core/src/utils/applyScopeDataToEvent.ts
+++ b/packages/core/src/utils/applyScopeDataToEvent.ts
@@ -1,4 +1,4 @@
-import type { Breadcrumb, Event, PropagationContext, ScopeData, Span } from '@sentry/types';
+import type { Breadcrumb, Event, ScopeData, Span } from '@sentry/types';
import { arrayify, dropUndefinedKeys } from '@sentry/utils';
import { getDynamicSamplingContextFromSpan } from '../tracing/dynamicSamplingContext';
import { getRootSpan } from './getRootSpan';
@@ -8,7 +8,7 @@ import { spanToJSON, spanToTraceContext } from './spanUtils';
* Applies data from the scope to the event and runs all event processors on it.
*/
export function applyScopeDataToEvent(event: Event, data: ScopeData): void {
- const { fingerprint, span, breadcrumbs, sdkProcessingMetadata, propagationContext } = data;
+ const { fingerprint, span, breadcrumbs, sdkProcessingMetadata } = data;
// Apply general data
applyDataToEvent(event, data);
@@ -22,7 +22,7 @@ export function applyScopeDataToEvent(event: Event, data: ScopeData): void {
applyFingerprintToEvent(event, fingerprint);
applyBreadcrumbsToEvent(event, breadcrumbs);
- applySdkMetadataToEvent(event, sdkProcessingMetadata, propagationContext);
+ applySdkMetadataToEvent(event, sdkProcessingMetadata);
}
/** Merge data of two scopes together. */
@@ -163,15 +163,10 @@ function applyBreadcrumbsToEvent(event: Event, breadcrumbs: Breadcrumb[]): void
event.breadcrumbs = mergedBreadcrumbs.length ? mergedBreadcrumbs : undefined;
}
-function applySdkMetadataToEvent(
- event: Event,
- sdkProcessingMetadata: ScopeData['sdkProcessingMetadata'],
- propagationContext: PropagationContext,
-): void {
+function applySdkMetadataToEvent(event: Event, sdkProcessingMetadata: ScopeData['sdkProcessingMetadata']): void {
event.sdkProcessingMetadata = {
...event.sdkProcessingMetadata,
...sdkProcessingMetadata,
- propagationContext: propagationContext,
};
}
diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts
index ccc6476c14d8..e819f9413aec 100644
--- a/packages/core/test/lib/integration.test.ts
+++ b/packages/core/test/lib/integration.test.ts
@@ -646,6 +646,63 @@ describe('addIntegration', () => {
expect(warnings).toHaveBeenCalledTimes(1);
expect(warnings).toHaveBeenCalledWith('Cannot add integration "test" because no SDK Client is available.');
});
+
+ it('triggers all hooks', () => {
+ const setup = jest.fn();
+ const setupOnce = jest.fn();
+ const setupAfterAll = jest.fn();
+
+ class CustomIntegration implements Integration {
+ name = 'test';
+ setupOnce = setupOnce;
+ setup = setup;
+ afterAllSetup = setupAfterAll;
+ }
+
+ const client = getTestClient();
+ const hub = new Hub(client);
+ // eslint-disable-next-line deprecation/deprecation
+ makeMain(hub);
+
+ const integration = new CustomIntegration();
+ addIntegration(integration);
+
+ expect(setupOnce).toHaveBeenCalledTimes(1);
+ expect(setup).toHaveBeenCalledTimes(1);
+ expect(setupAfterAll).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not trigger hooks if already installed', () => {
+ const logs = jest.spyOn(logger, 'log');
+
+ class CustomIntegration implements Integration {
+ name = 'test';
+ setupOnce = jest.fn();
+ setup = jest.fn();
+ afterAllSetup = jest.fn();
+ }
+
+ const client = getTestClient();
+ const hub = new Hub(client);
+ // eslint-disable-next-line deprecation/deprecation
+ makeMain(hub);
+
+ const integration1 = new CustomIntegration();
+ const integration2 = new CustomIntegration();
+ addIntegration(integration1);
+
+ expect(integration1.setupOnce).toHaveBeenCalledTimes(1);
+ expect(integration1.setup).toHaveBeenCalledTimes(1);
+ expect(integration1.afterAllSetup).toHaveBeenCalledTimes(1);
+
+ addIntegration(integration2);
+
+ expect(integration2.setupOnce).toHaveBeenCalledTimes(0);
+ expect(integration2.setup).toHaveBeenCalledTimes(0);
+ expect(integration2.afterAllSetup).toHaveBeenCalledTimes(0);
+
+ expect(logs).toHaveBeenCalledWith('Integration skipped because it was already installed: test');
+ });
});
describe('convertIntegrationFnToClass', () => {
diff --git a/packages/core/test/lib/integrations/functiontostring.test.ts b/packages/core/test/lib/integrations/functiontostring.test.ts
index bb3b62d11915..c0e2a22cd6ed 100644
--- a/packages/core/test/lib/integrations/functiontostring.test.ts
+++ b/packages/core/test/lib/integrations/functiontostring.test.ts
@@ -1,7 +1,18 @@
-import { fill } from '../../../../utils/src/object';
-import { FunctionToString } from '../../../src/integrations/functiontostring';
+import { fill } from '@sentry/utils';
+import { getClient, getCurrentScope, setCurrentClient } from '../../../src';
+import { functionToStringIntegration } from '../../../src/integrations/functiontostring';
+import { TestClient, getDefaultTestClientOptions } from '../../mocks/client';
describe('FunctionToString', () => {
+ beforeEach(() => {
+ const testClient = new TestClient(getDefaultTestClientOptions({}));
+ setCurrentClient(testClient);
+ });
+
+ afterAll(() => {
+ getCurrentScope().setClient(undefined);
+ });
+
it('it works as expected', () => {
const foo = {
bar(wat: boolean): boolean {
@@ -17,10 +28,31 @@ describe('FunctionToString', () => {
expect(foo.bar.toString()).not.toBe(originalFunction);
- // eslint-disable-next-line deprecation/deprecation
- const fts = new FunctionToString();
- fts.setupOnce();
+ const fts = functionToStringIntegration();
+ getClient()?.addIntegration?.(fts);
expect(foo.bar.toString()).toBe(originalFunction);
});
+
+ it('does not activate when client is not active', () => {
+ const foo = {
+ bar(wat: boolean): boolean {
+ return wat;
+ },
+ };
+ const originalFunction = foo.bar.toString();
+ fill(foo, 'bar', function wat(whatever: boolean): () => void {
+ return function watwat(): boolean {
+ return whatever;
+ };
+ });
+
+ expect(foo.bar.toString()).not.toBe(originalFunction);
+
+ const testClient = new TestClient(getDefaultTestClientOptions({}));
+ const fts = functionToStringIntegration();
+ testClient.addIntegration(fts);
+
+ expect(foo.bar.toString()).not.toBe(originalFunction);
+ });
});
diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts
index 22f62bbcf89c..cb3c616b7390 100644
--- a/packages/core/test/lib/prepareEvent.test.ts
+++ b/packages/core/test/lib/prepareEvent.test.ts
@@ -228,12 +228,7 @@ describe('prepareEvent', () => {
event_id: expect.any(String),
environment: 'production',
message: 'foo',
- sdkProcessingMetadata: {
- propagationContext: {
- spanId: expect.any(String),
- traceId: expect.any(String),
- },
- },
+ sdkProcessingMetadata: {},
});
});
@@ -309,16 +304,15 @@ describe('prepareEvent', () => {
user: { id: '1', email: 'test@example.com' },
tags: { tag1: 'aa', tag2: 'aa' },
extra: { extra1: 'aa', extra2: 'aa' },
- contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } },
+ contexts: {
+ os: { name: 'os1' },
+ culture: { display_name: 'name1' },
+ },
fingerprint: ['dd', 'aa'],
breadcrumbs: [breadcrumb4, breadcrumb2, breadcrumb3, breadcrumb1],
sdkProcessingMetadata: {
aa: 'aa',
bb: 'bb',
- propagationContext: {
- spanId: '1',
- traceId: '1',
- },
},
});
});
@@ -382,7 +376,6 @@ describe('prepareEvent', () => {
sdkProcessingMetadata: {
aa: 'aa',
bb: 'bb',
- propagationContext: isolationScope.getPropagationContext(),
},
});
});
diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts
index e4510ef58e3b..4b4242ce7dc6 100644
--- a/packages/core/test/lib/scope.test.ts
+++ b/packages/core/test/lib/scope.test.ts
@@ -132,12 +132,7 @@ describe('Scope', () => {
expect(event).toEqual({
message: 'foo',
- sdkProcessingMetadata: {
- propagationContext: {
- spanId: expect.any(String),
- traceId: expect.any(String),
- },
- },
+ sdkProcessingMetadata: {},
});
});
@@ -166,15 +161,14 @@ describe('Scope', () => {
user: { id: '1', email: 'test@example.com' },
tags: { tag1: 'aa', tag2: 'aa' },
extra: { extra1: 'aa', extra2: 'aa' },
- contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } },
+ contexts: {
+ os: { name: 'os1' },
+ culture: { display_name: 'name1' },
+ },
fingerprint: ['dd', 'aa'],
breadcrumbs: [breadcrumb2, breadcrumb1],
sdkProcessingMetadata: {
aa: 'aa',
- propagationContext: {
- spanId: '1',
- traceId: '1',
- },
},
});
});
diff --git a/packages/core/test/lib/sdk.test.ts b/packages/core/test/lib/sdk.test.ts
index 1484971babf7..c9d18c02c78e 100644
--- a/packages/core/test/lib/sdk.test.ts
+++ b/packages/core/test/lib/sdk.test.ts
@@ -1,5 +1,5 @@
import { Hub, captureCheckIn, makeMain, setCurrentClient } from '@sentry/core';
-import type { Client, Integration } from '@sentry/types';
+import type { Client, Integration, IntegrationFnResult } from '@sentry/types';
import { installedIntegrations } from '../../src/integration';
import { initAndBind } from '../../src/sdk';
@@ -35,6 +35,53 @@ describe('SDK', () => {
expect((integrations[0].setupOnce as jest.Mock).mock.calls.length).toBe(1);
expect((integrations[1].setupOnce as jest.Mock).mock.calls.length).toBe(1);
});
+
+ test('calls hooks in the correct order', () => {
+ const list: string[] = [];
+
+ const integration1 = {
+ name: 'integration1',
+ setupOnce: jest.fn(() => list.push('setupOnce1')),
+ afterAllSetup: jest.fn(() => list.push('afterAllSetup1')),
+ } satisfies IntegrationFnResult;
+
+ const integration2 = {
+ name: 'integration2',
+ setupOnce: jest.fn(() => list.push('setupOnce2')),
+ setup: jest.fn(() => list.push('setup2')),
+ afterAllSetup: jest.fn(() => list.push('afterAllSetup2')),
+ } satisfies IntegrationFnResult;
+
+ const integration3 = {
+ name: 'integration3',
+ setupOnce: jest.fn(() => list.push('setupOnce3')),
+ setup: jest.fn(() => list.push('setup3')),
+ } satisfies IntegrationFnResult;
+
+ const integrations: Integration[] = [integration1, integration2, integration3];
+ const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, integrations });
+ initAndBind(TestClient, options);
+
+ expect(integration1.setupOnce).toHaveBeenCalledTimes(1);
+ expect(integration2.setupOnce).toHaveBeenCalledTimes(1);
+ expect(integration3.setupOnce).toHaveBeenCalledTimes(1);
+
+ expect(integration2.setup).toHaveBeenCalledTimes(1);
+ expect(integration3.setup).toHaveBeenCalledTimes(1);
+
+ expect(integration1.afterAllSetup).toHaveBeenCalledTimes(1);
+ expect(integration2.afterAllSetup).toHaveBeenCalledTimes(1);
+
+ expect(list).toEqual([
+ 'setupOnce1',
+ 'setupOnce2',
+ 'setup2',
+ 'setupOnce3',
+ 'setup3',
+ 'afterAllSetup1',
+ 'afterAllSetup2',
+ ]);
+ });
});
});
diff --git a/packages/core/test/lib/tracing/spanstatus.test.ts b/packages/core/test/lib/tracing/spanstatus.test.ts
new file mode 100644
index 000000000000..559aa101c642
--- /dev/null
+++ b/packages/core/test/lib/tracing/spanstatus.test.ts
@@ -0,0 +1,41 @@
+import { Span, setHttpStatus, spanToJSON } from '../../../src/index';
+
+describe('setHttpStatus', () => {
+ it.each([
+ [200, 'ok'],
+ [300, 'ok'],
+ [401, 'unauthenticated'],
+ [403, 'permission_denied'],
+ [404, 'not_found'],
+ [409, 'already_exists'],
+ [413, 'failed_precondition'],
+ [429, 'resource_exhausted'],
+ [455, 'invalid_argument'],
+ [501, 'unimplemented'],
+ [503, 'unavailable'],
+ [504, 'deadline_exceeded'],
+ [520, 'internal_error'],
+ ])('applies the correct span status and http status code to the span (%s - $%s)', (code, status) => {
+ const span = new Span({ name: 'test' });
+
+ setHttpStatus(span!, code);
+
+ const { status: spanStatus, data, tags } = spanToJSON(span!);
+
+ expect(spanStatus).toBe(status);
+ expect(data).toMatchObject({ 'http.response.status_code': code });
+ expect(tags).toMatchObject({ 'http.status_code': String(code) });
+ });
+
+ it("doesn't set the status for an unknown http status code", () => {
+ const span = new Span({ name: 'test' });
+
+ setHttpStatus(span!, 600);
+
+ const { status: spanStatus, data, tags } = spanToJSON(span!);
+
+ expect(spanStatus).toBeUndefined();
+ expect(data).toMatchObject({ 'http.response.status_code': 600 });
+ expect(tags).toMatchObject({ 'http.status_code': '600' });
+ });
+});
diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts
index a2bdac43b6dd..fca086f10c94 100644
--- a/packages/core/test/lib/tracing/trace.test.ts
+++ b/packages/core/test/lib/tracing/trace.test.ts
@@ -5,6 +5,7 @@ import {
getCurrentScope,
makeMain,
spanToJSON,
+ withScope,
} from '../../../src';
import { Scope } from '../../../src/scope';
import {
@@ -229,6 +230,40 @@ describe('startSpan', () => {
expect(ref.spanRecorder.spans).toHaveLength(2);
expect(spanToJSON(ref.spanRecorder.spans[1]).op).toEqual('db.query');
});
+
+ it.each([
+ { origin: 'auto.http.browser' },
+ { attributes: { 'sentry.origin': 'auto.http.browser' } },
+ // attribute should take precedence over top level origin
+ { origin: 'manual', attributes: { 'sentry.origin': 'auto.http.browser' } },
+ ])('correctly sets the span origin', async () => {
+ let ref: any = undefined;
+ client.on('finishTransaction', transaction => {
+ ref = transaction;
+ });
+ try {
+ await startSpan({ name: 'GET users/[id]', origin: 'auto.http.browser' }, () => {
+ return callback();
+ });
+ } catch (e) {
+ //
+ }
+
+ const jsonSpan = spanToJSON(ref);
+ expect(jsonSpan).toEqual({
+ data: {
+ 'sentry.origin': 'auto.http.browser',
+ 'sentry.sample_rate': 0,
+ },
+ origin: 'auto.http.browser',
+ description: 'GET users/[id]',
+ span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ status: isError ? 'internal_error' : undefined,
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ });
+ });
});
it('creates & finishes span', async () => {
@@ -284,6 +319,44 @@ describe('startSpan', () => {
expect(getCurrentScope()).toBe(initialScope);
expect(getActiveSpan()).toBe(undefined);
});
+
+ it("picks up the trace id off the parent scope's propagation context", () => {
+ expect.assertions(1);
+ withScope(scope => {
+ scope.setPropagationContext({
+ traceId: '99999999999999999999999999999999',
+ spanId: '1212121212121212',
+ dsc: {},
+ parentSpanId: '4242424242424242',
+ });
+
+ startSpan({ name: 'span' }, span => {
+ expect(span?.spanContext().traceId).toBe('99999999999999999999999999999999');
+ });
+ });
+ });
+
+ describe('onlyIfParent', () => {
+ it('does not create a span if there is no parent', () => {
+ const span = startSpan({ name: 'test span', onlyIfParent: true }, span => {
+ return span;
+ });
+
+ expect(span).toBeUndefined();
+ });
+
+ it('creates a span if there is a parent', () => {
+ const span = startSpan({ name: 'parent span' }, () => {
+ const span = startSpan({ name: 'test span', onlyIfParent: true }, span => {
+ return span;
+ });
+
+ return span;
+ });
+
+ expect(span).toBeDefined();
+ });
+ });
});
describe('startSpanManual', () => {
@@ -347,6 +420,45 @@ describe('startSpanManual', () => {
expect(start).toEqual(1234);
});
+
+ it("picks up the trace id off the parent scope's propagation context", () => {
+ expect.assertions(1);
+ withScope(scope => {
+ scope.setPropagationContext({
+ traceId: '99999999999999999999999999999991',
+ spanId: '1212121212121212',
+ dsc: {},
+ parentSpanId: '4242424242424242',
+ });
+
+ startSpanManual({ name: 'span' }, span => {
+ expect(span?.spanContext().traceId).toBe('99999999999999999999999999999991');
+ span?.end();
+ });
+ });
+ });
+
+ describe('onlyIfParent', () => {
+ it('does not create a span if there is no parent', () => {
+ const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => {
+ return span;
+ });
+
+ expect(span).toBeUndefined();
+ });
+
+ it('creates a span if there is a parent', () => {
+ const span = startSpan({ name: 'parent span' }, () => {
+ const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => {
+ return span;
+ });
+
+ return span;
+ });
+
+ expect(span).toBeDefined();
+ });
+ });
});
describe('startInactiveSpan', () => {
@@ -395,6 +507,40 @@ describe('startInactiveSpan', () => {
const span = startInactiveSpan({ name: 'outer', startTime: [1234, 0] });
expect(spanToJSON(span!).start_timestamp).toEqual(1234);
});
+
+ it("picks up the trace id off the parent scope's propagation context", () => {
+ expect.assertions(1);
+ withScope(scope => {
+ scope.setPropagationContext({
+ traceId: '99999999999999999999999999999991',
+ spanId: '1212121212121212',
+ dsc: {},
+ parentSpanId: '4242424242424242',
+ });
+
+ const span = startInactiveSpan({ name: 'span' });
+ expect(span?.spanContext().traceId).toBe('99999999999999999999999999999991');
+ span?.end();
+ });
+ });
+
+ describe('onlyIfParent', () => {
+ it('does not create a span if there is no parent', () => {
+ const span = startInactiveSpan({ name: 'test span', onlyIfParent: true });
+
+ expect(span).toBeUndefined();
+ });
+
+ it('creates a span if there is a parent', () => {
+ const span = startSpan({ name: 'parent span' }, () => {
+ const span = startInactiveSpan({ name: 'test span', onlyIfParent: true });
+
+ return span;
+ });
+
+ expect(span).toBeDefined();
+ });
+ });
});
describe('continueTrace', () => {
@@ -455,6 +601,7 @@ describe('continueTrace', () => {
const scope = getCurrentScope();
expect(scope.getPropagationContext()).toEqual({
+ dsc: {}, // DSC should be an empty object (frozen), because there was an incoming trace
sampled: false,
parentSpanId: '1121201211212012',
spanId: expect.any(String),
diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts
index 65d82a0e6779..f5ed9651bf94 100644
--- a/packages/deno/src/index.ts
+++ b/packages/deno/src/index.ts
@@ -65,7 +65,9 @@ export {
setTag,
setTags,
setUser,
+ // eslint-disable-next-line deprecation/deprecation
spanStatusfromHttpCode,
+ getSpanStatusFromHttpCode,
// eslint-disable-next-line deprecation/deprecation
trace,
withScope,
@@ -97,12 +99,17 @@ export {
export { breadcrumbsIntegration, dedupeIntegration } from '@sentry/browser';
import { Integrations as CoreIntegrations } from '@sentry/core';
+export { denoContextIntegration } from './integrations/context';
+export { globalHandlersIntegration } from './integrations/globalhandlers';
+export { normalizePathsIntegration } from './integrations/normalizepaths';
+export { contextLinesIntegration } from './integrations/contextlines';
+export { denoCronIntegration } from './integrations/deno-cron';
+
import * as DenoIntegrations from './integrations';
-const INTEGRATIONS = {
+/** @deprecated Import the integration function directly, e.g. `inboundFiltersIntegration()` instead of `new Integrations.InboundFilter(). */
+export const Integrations = {
// eslint-disable-next-line deprecation/deprecation
...CoreIntegrations,
...DenoIntegrations,
};
-
-export { INTEGRATIONS as Integrations };
diff --git a/packages/deno/src/integrations/context.ts b/packages/deno/src/integrations/context.ts
index 199da80d9b4b..f844b80be6c8 100644
--- a/packages/deno/src/integrations/context.ts
+++ b/packages/deno/src/integrations/context.ts
@@ -1,4 +1,4 @@
-import { convertIntegrationFnToClass } from '@sentry/core';
+import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core';
import type { Event, Integration, IntegrationClass, IntegrationFn } from '@sentry/types';
const INTEGRATION_NAME = 'DenoContext';
@@ -52,7 +52,7 @@ async function addDenoRuntimeContext(event: Event): Promise {
return event;
}
-const denoContextIntegration = (() => {
+const _denoContextIntegration = (() => {
return {
name: INTEGRATION_NAME,
// TODO v8: Remove this
@@ -63,8 +63,16 @@ const denoContextIntegration = (() => {
};
}) satisfies IntegrationFn;
-/** Adds Deno context to events. */
+export const denoContextIntegration = defineIntegration(_denoContextIntegration);
+
+/**
+ * Adds Deno context to events.
+ * @deprecated Use `denoContextintegration()` instead.
+ */
// eslint-disable-next-line deprecation/deprecation
export const DenoContext = convertIntegrationFnToClass(INTEGRATION_NAME, denoContextIntegration) as IntegrationClass<
Integration & { processEvent: (event: Event) => Promise }
>;
+
+// eslint-disable-next-line deprecation/deprecation
+export type DenoContext = typeof DenoContext;
diff --git a/packages/deno/src/integrations/contextlines.ts b/packages/deno/src/integrations/contextlines.ts
index 1b3b413699f6..fc51e4ad2d57 100644
--- a/packages/deno/src/integrations/contextlines.ts
+++ b/packages/deno/src/integrations/contextlines.ts
@@ -1,4 +1,4 @@
-import { convertIntegrationFnToClass } from '@sentry/core';
+import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core';
import type { Event, Integration, IntegrationClass, IntegrationFn, StackFrame } from '@sentry/types';
import { LRUMap, addContextToFrame } from '@sentry/utils';
@@ -47,7 +47,7 @@ interface ContextLinesOptions {
frameContextLines?: number;
}
-const denoContextLinesIntegration = ((options: ContextLinesOptions = {}) => {
+const _contextLinesIntegration = ((options: ContextLinesOptions = {}) => {
const contextLines = options.frameContextLines !== undefined ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT;
return {
@@ -60,12 +60,19 @@ const denoContextLinesIntegration = ((options: ContextLinesOptions = {}) => {
};
}) satisfies IntegrationFn;
-/** Add node modules / packages to the event */
+export const contextLinesIntegration = defineIntegration(_contextLinesIntegration);
+
+/**
+ * Add node modules / packages to the event.
+ * @deprecated Use `contextLinesIntegration()` instead.
+ */
+// eslint-disable-next-line deprecation/deprecation
+export const ContextLines = convertIntegrationFnToClass(INTEGRATION_NAME, contextLinesIntegration) as IntegrationClass<
+ Integration & { processEvent: (event: Event) => Promise }
+>;
+
// eslint-disable-next-line deprecation/deprecation
-export const ContextLines = convertIntegrationFnToClass(
- INTEGRATION_NAME,
- denoContextLinesIntegration,
-) as IntegrationClass Promise }>;
+export type ContextLines = typeof ContextLines;
/** Processes an event and adds context lines */
async function addSourceContext(event: Event, contextLines: number): Promise {
diff --git a/packages/deno/src/integrations/deno-cron.ts b/packages/deno/src/integrations/deno-cron.ts
index 3b337b004405..89030629864c 100644
--- a/packages/deno/src/integrations/deno-cron.ts
+++ b/packages/deno/src/integrations/deno-cron.ts
@@ -1,4 +1,4 @@
-import { convertIntegrationFnToClass, getClient, withMonitor } from '@sentry/core';
+import { convertIntegrationFnToClass, defineIntegration, getClient, withMonitor } from '@sentry/core';
import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types';
import { parseScheduleToString } from './deno-cron-format';
@@ -11,7 +11,7 @@ const INTEGRATION_NAME = 'DenoCron';
const SETUP_CLIENTS = new WeakMap();
-const denoCronIntegration = (() => {
+const _denoCronIntegration = (() => {
return {
name: INTEGRATION_NAME,
setupOnce() {
@@ -37,8 +37,8 @@ const denoCronIntegration = (() => {
}
async function cronCalled(): Promise {
- if (SETUP_CLIENTS.has(getClient() as Client)) {
- return;
+ if (!SETUP_CLIENTS.has(getClient() as Client)) {
+ return fn();
}
await withMonitor(monitorSlug, async () => fn(), {
@@ -60,8 +60,16 @@ const denoCronIntegration = (() => {
};
}) satisfies IntegrationFn;
-/** Instruments Deno.cron to automatically capture cron check-ins */
+export const denoCronIntegration = defineIntegration(_denoCronIntegration);
+
+/**
+ * Instruments Deno.cron to automatically capture cron check-ins.
+ * @deprecated Use `denoCronIntegration()` instead.
+ */
// eslint-disable-next-line deprecation/deprecation
export const DenoCron = convertIntegrationFnToClass(INTEGRATION_NAME, denoCronIntegration) as IntegrationClass<
Integration & { setup: (client: Client) => void }
>;
+
+// eslint-disable-next-line deprecation/deprecation
+export type DenoCron = typeof DenoCron;
diff --git a/packages/deno/src/integrations/globalhandlers.ts b/packages/deno/src/integrations/globalhandlers.ts
index 895c52ee59e4..0c830c40da25 100644
--- a/packages/deno/src/integrations/globalhandlers.ts
+++ b/packages/deno/src/integrations/globalhandlers.ts
@@ -1,4 +1,5 @@
import type { ServerRuntimeClient } from '@sentry/core';
+import { defineIntegration } from '@sentry/core';
import { convertIntegrationFnToClass } from '@sentry/core';
import { captureEvent } from '@sentry/core';
import { getClient } from '@sentry/core';
@@ -21,7 +22,7 @@ type GlobalHandlersIntegrations = Record {
+const _globalHandlersIntegration = ((options?: GlobalHandlersIntegrations) => {
const _options = {
error: true,
unhandledrejection: true,
@@ -43,13 +44,21 @@ const globalHandlersIntegration = ((options?: GlobalHandlersIntegrations) => {
};
}) satisfies IntegrationFn;
-/** Global handlers */
+export const globalHandlersIntegration = defineIntegration(_globalHandlersIntegration);
+
+/**
+ * Global handlers.
+ * @deprecated Use `globalHandlersIntergation()` instead.
+ */
// eslint-disable-next-line deprecation/deprecation
export const GlobalHandlers = convertIntegrationFnToClass(
INTEGRATION_NAME,
globalHandlersIntegration,
) as IntegrationClass void }>;
+// eslint-disable-next-line deprecation/deprecation
+export type GlobalHandlers = typeof GlobalHandlers;
+
function installGlobalErrorHandler(client: Client): void {
globalThis.addEventListener('error', data => {
if (getClient() !== client || isExiting) {
diff --git a/packages/deno/src/integrations/index.ts b/packages/deno/src/integrations/index.ts
index 065e16770109..6870606066eb 100644
--- a/packages/deno/src/integrations/index.ts
+++ b/packages/deno/src/integrations/index.ts
@@ -1,3 +1,4 @@
+/* eslint-disable deprecation/deprecation */
export { DenoContext } from './context';
export { GlobalHandlers } from './globalhandlers';
export { NormalizePaths } from './normalizepaths';
diff --git a/packages/deno/src/integrations/normalizepaths.ts b/packages/deno/src/integrations/normalizepaths.ts
index 68ba3986e805..a9b8f3dbb0e3 100644
--- a/packages/deno/src/integrations/normalizepaths.ts
+++ b/packages/deno/src/integrations/normalizepaths.ts
@@ -1,4 +1,4 @@
-import { convertIntegrationFnToClass } from '@sentry/core';
+import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core';
import type { Event, Integration, IntegrationClass, IntegrationFn } from '@sentry/types';
import { createStackParser, dirname, nodeStackLineParser } from '@sentry/utils';
@@ -55,7 +55,7 @@ function getCwd(): string | undefined {
return undefined;
}
-const normalizePathsIntegration = (() => {
+const _normalizePathsIntegration = (() => {
// Cached here
let appRoot: string | undefined;
@@ -98,9 +98,17 @@ const normalizePathsIntegration = (() => {
};
}) satisfies IntegrationFn;
-/** Normalises paths to the app root directory. */
+export const normalizePathsIntegration = defineIntegration(_normalizePathsIntegration);
+
+/**
+ * Normalises paths to the app root directory.
+ * @deprecated Use `normalizePathsIntegration()` instead.
+ */
// eslint-disable-next-line deprecation/deprecation
export const NormalizePaths = convertIntegrationFnToClass(
INTEGRATION_NAME,
normalizePathsIntegration,
) as IntegrationClass Event }>;
+
+// eslint-disable-next-line deprecation/deprecation
+export type NormalizePaths = typeof NormalizePaths;
diff --git a/packages/deno/src/sdk.ts b/packages/deno/src/sdk.ts
index 990eb8146039..c5bd3a1d002f 100644
--- a/packages/deno/src/sdk.ts
+++ b/packages/deno/src/sdk.ts
@@ -6,7 +6,10 @@ import type { Integration, Options, StackParser } from '@sentry/types';
import { createStackParser, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils';
import { DenoClient } from './client';
-import { ContextLines, DenoContext, GlobalHandlers, NormalizePaths } from './integrations';
+import { denoContextIntegration } from './integrations/context';
+import { contextLinesIntegration } from './integrations/contextlines';
+import { globalHandlersIntegration } from './integrations/globalhandlers';
+import { normalizePathsIntegration } from './integrations/normalizepaths';
import { makeFetchTransport } from './transports';
import type { DenoOptions } from './types';
@@ -24,10 +27,10 @@ export const defaultIntegrations = [
xhr: false,
}),
// Deno Specific
- new DenoContext(),
- new ContextLines(),
- new NormalizePaths(),
- new GlobalHandlers(),
+ denoContextIntegration(),
+ contextLinesIntegration(),
+ normalizePathsIntegration(),
+ globalHandlersIntegration(),
];
/** Get the default integrations for the Deno SDK. */
diff --git a/packages/ember/addon/index.ts b/packages/ember/addon/index.ts
index 4f1f399654e4..b3ccfffa404f 100644
--- a/packages/ember/addon/index.ts
+++ b/packages/ember/addon/index.ts
@@ -5,7 +5,7 @@ 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 { applySdkMetadata } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, applySdkMetadata } from '@sentry/core';
import { GLOBAL_OBJ } from '@sentry/utils';
import Ember from 'ember';
@@ -82,9 +82,11 @@ export const instrumentRoutePerformance = (BaseRoute
): Promise> => {
return startSpan(
{
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'ember',
+ },
op,
name: description,
- origin: 'auto.ui.ember',
},
() => {
return fn(...args);
diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts
index b25125b28da6..acabe5334cad 100644
--- a/packages/ember/addon/instance-initializers/sentry-performance.ts
+++ b/packages/ember/addon/instance-initializers/sentry-performance.ts
@@ -11,6 +11,7 @@ import type { ExtendedBackburner } from '@sentry/ember/runloop';
import type { Span, Transaction } from '@sentry/types';
import { GLOBAL_OBJ, browserPerformanceTimeOrigin, timestampInSeconds } from '@sentry/utils';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import type { BrowserClient } from '..';
import { getActiveSpan, startInactiveSpan } from '..';
import type { EmberRouterMain, EmberSentryConfig, GlobalConfig, OwnConfig, StartTransactionFunction } from '../types';
@@ -150,9 +151,11 @@ export function _instrumentEmberRouter(
},
});
transitionSpan = startInactiveSpan({
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
+ },
op: 'ui.ember.transition',
name: `route:${fromRoute} -> route:${toRoute}`,
- origin: 'auto.ui.ember',
});
});
@@ -212,9 +215,11 @@ function _instrumentEmberRunloop(config: EmberSentryConfig): void {
if ((now - currentQueueStart) * 1000 >= minQueueDuration) {
startInactiveSpan({
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
+ },
name: 'runloop',
op: `ui.ember.runloop.${queue}`,
- origin: 'auto.ui.ember',
startTimestamp: currentQueueStart,
})?.end(now);
}
@@ -370,7 +375,9 @@ function _instrumentInitialLoad(config: EmberSentryConfig): void {
startInactiveSpan({
op: 'ui.ember.init',
name: 'init',
- origin: 'auto.ui.ember',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
+ },
startTimestamp,
})?.end(endTimestamp);
performance.clearMarks(startName);
diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts
index b8cfaf1914e0..08f4a73abcb2 100644
--- a/packages/hub/test/scope.test.ts
+++ b/packages/hub/test/scope.test.ts
@@ -261,8 +261,6 @@ describe('Scope', () => {
expect(processedEvent!.contexts).toEqual({ os: { id: '1' } });
expect(processedEvent!.sdkProcessingMetadata).toEqual({
dogs: 'are great!',
- // @ts-expect-error accessing private property for test
- propagationContext: scope._propagationContext,
});
});
diff --git a/packages/integration-shims/src/BrowserTracing.ts b/packages/integration-shims/src/BrowserTracing.ts
index 310dc589afe9..8e3d61bae58f 100644
--- a/packages/integration-shims/src/BrowserTracing.ts
+++ b/packages/integration-shims/src/BrowserTracing.ts
@@ -33,7 +33,16 @@ class BrowserTracingShim implements Integration {
}
}
-export { BrowserTracingShim as BrowserTracing };
+/**
+ * This is a shim for the BrowserTracing integration.
+ * It is needed in order for the CDN bundles to continue working when users add/remove tracing
+ * from it, without changing their config. This is necessary for the loader mechanism.
+ */
+function browserTracingIntegrationShim(_options: unknown): Integration {
+ return new BrowserTracingShim({});
+}
+
+export { BrowserTracingShim as BrowserTracing, browserTracingIntegrationShim as browserTracingIntegration };
/** Shim function */
export function addTracingExtensions(): void {
diff --git a/packages/integration-shims/src/index.ts b/packages/integration-shims/src/index.ts
index 43243f69a194..bffdf82c99f7 100644
--- a/packages/integration-shims/src/index.ts
+++ b/packages/integration-shims/src/index.ts
@@ -3,9 +3,16 @@ export {
Feedback,
feedbackIntegration,
} from './Feedback';
+
export {
// eslint-disable-next-line deprecation/deprecation
Replay,
replayIntegration,
} from './Replay';
-export { BrowserTracing, addTracingExtensions } from './BrowserTracing';
+
+export {
+ // eslint-disable-next-line deprecation/deprecation
+ BrowserTracing,
+ browserTracingIntegration,
+ addTracingExtensions,
+} from './BrowserTracing';
diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts
index d1d5e1db7ff5..a1c20937f578 100644
--- a/packages/nextjs/src/client/index.ts
+++ b/packages/nextjs/src/client/index.ts
@@ -1,5 +1,5 @@
import { applySdkMetadata, hasTracingEnabled } from '@sentry/core';
-import type { BrowserOptions } from '@sentry/react';
+import type { BrowserOptions, browserTracingIntegration } from '@sentry/react';
import {
Integrations as OriginalIntegrations,
getCurrentScope,
@@ -86,13 +86,30 @@ function fixBrowserTracingIntegration(options: BrowserOptions): void {
}
}
+function isNewBrowserTracingIntegration(
+ integration: Integration,
+): integration is Integration & { options?: Parameters[0] } {
+ return !!integration.afterAllSetup && !!(integration as BrowserTracing).options;
+}
+
function maybeUpdateBrowserTracingIntegration(integrations: Integration[]): Integration[] {
const browserTracing = integrations.find(integration => integration.name === 'BrowserTracing');
+
+ if (!browserTracing) {
+ return integrations;
+ }
+
+ // If `browserTracingIntegration()` was added, we need to force-convert it to our custom one
+ if (isNewBrowserTracingIntegration(browserTracing)) {
+ const { options } = browserTracing;
+ integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options);
+ }
+
// If BrowserTracing was added, but it is not our forked version,
// replace it with our forked version with the same options
- if (browserTracing && !(browserTracing instanceof BrowserTracing)) {
+ if (!(browserTracing instanceof BrowserTracing)) {
const options: ConstructorParameters[0] = (browserTracing as BrowserTracing).options;
- // These two options are overwritten by the custom integration
+ // This option is overwritten by the custom integration
delete options.routingInstrumentation;
// eslint-disable-next-line deprecation/deprecation
delete options.tracingOrigins;
diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts
index 59114ddee709..fd98fe2328ee 100644
--- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts
+++ b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts
@@ -1,9 +1,11 @@
import {
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
addTracingExtensions,
captureException,
continueTrace,
handleCallbackErrors,
+ setHttpStatus,
startSpan,
} from '@sentry/core';
import { winterCGRequestToRequestData } from '@sentry/utils';
@@ -40,8 +42,10 @@ export function withEdgeWrapping(
...transactionContext,
name: options.spanDescription,
op: options.spanOp,
- origin: 'auto.function.nextjs.withEdgeWrapping',
- attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route' },
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping',
+ },
metadata: {
// eslint-disable-next-line deprecation/deprecation
...transactionContext.metadata,
@@ -64,10 +68,12 @@ export function withEdgeWrapping(
},
);
- if (handlerResult instanceof Response) {
- span?.setHttpStatus(handlerResult.status);
- } else {
- span?.setStatus('ok');
+ if (span) {
+ if (handlerResult instanceof Response) {
+ setHttpStatus(span, handlerResult.status);
+ } else {
+ span.setStatus('ok');
+ }
}
return handlerResult;
diff --git a/packages/nextjs/src/common/utils/responseEnd.ts b/packages/nextjs/src/common/utils/responseEnd.ts
index e59a99fb0ebb..c12d19f1c6fa 100644
--- a/packages/nextjs/src/common/utils/responseEnd.ts
+++ b/packages/nextjs/src/common/utils/responseEnd.ts
@@ -1,5 +1,5 @@
import type { ServerResponse } from 'http';
-import { flush } from '@sentry/core';
+import { flush, setHttpStatus } from '@sentry/core';
import type { Transaction } from '@sentry/types';
import { fill, logger } from '@sentry/utils';
@@ -41,7 +41,7 @@ export function autoEndTransactionOnResponseEnd(transaction: Transaction, res: S
/** Finish the given response's transaction and set HTTP status data */
export function finishTransaction(transaction: Transaction | undefined, res: ServerResponse): void {
if (transaction) {
- transaction.setHttpStatus(res.statusCode);
+ setHttpStatus(transaction, res.statusCode);
transaction.end();
}
}
diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts
index 16228aa0cda8..61a08ba18891 100644
--- a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts
+++ b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts
@@ -5,10 +5,12 @@ import {
continueTrace,
getCurrentScope,
runWithAsyncContext,
+ setHttpStatus,
startSpanManual,
} from '@sentry/core';
import { consoleSandbox, isString, logger, objectify, stripUrlQueryAndFragment } from '@sentry/utils';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import type { AugmentedNextApiRequest, AugmentedNextApiResponse, NextApiHandler } from './types';
import { platformSupportsStreaming } from './utils/platformSupportsStreaming';
import { flushQueue } from './utils/responseEnd';
@@ -108,9 +110,9 @@ export function withSentry(apiHandler: NextApiHandler, parameterizedRoute?: stri
...transactionContext,
name: `${reqMethod}${reqPath}`,
op: 'http.server',
- origin: 'auto.http.nextjs',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs',
},
metadata: {
// eslint-disable-next-line deprecation/deprecation
@@ -122,8 +124,10 @@ export function withSentry(apiHandler: NextApiHandler, parameterizedRoute?: stri
// eslint-disable-next-line @typescript-eslint/unbound-method
res.end = new Proxy(res.end, {
apply(target, thisArg, argArray) {
- span?.setHttpStatus(res.statusCode);
- span?.end();
+ if (span) {
+ setHttpStatus(span, res.statusCode);
+ span.end();
+ }
if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) {
target.apply(thisArg, argArray);
} else {
@@ -179,8 +183,10 @@ export function withSentry(apiHandler: NextApiHandler, parameterizedRoute?: stri
res.statusCode = 500;
res.statusMessage = 'Internal Server Error';
- span?.setHttpStatus(res.statusCode);
- span?.end();
+ if (span) {
+ setHttpStatus(span, res.statusCode);
+ span.end();
+ }
// Make sure we have a chance to finish the transaction and flush events to Sentry before the handler errors
// out. (Apps which are deployed on Vercel run their API routes in lambdas, and those lambdas will shut down the
diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts
index f2e829704dd6..5e6a051ffcfb 100644
--- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts
+++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts
@@ -12,6 +12,7 @@ import {
import type { WebFetchHeaders } from '@sentry/types';
import { winterCGHeadersToDict } from '@sentry/utils';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import type { GenerationFunctionContext } from '../common/types';
import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils';
import { commonObjectToPropagationContext } from './utils/commonObjectTracing';
@@ -67,11 +68,11 @@ export function wrapGenerationFunctionWithSentry a
{
op: 'function.nextjs',
name: `${componentType}.${generationFunctionIdentifier} (${componentRoute})`,
- origin: 'auto.function.nextjs',
...transactionContext,
data,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs',
},
metadata: {
// eslint-disable-next-line deprecation/deprecation
diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts
index a1067b1577e4..99b9cf99b9b9 100644
--- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts
+++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts
@@ -4,6 +4,7 @@ import {
getCurrentScope,
handleCallbackErrors,
runWithAsyncContext,
+ setHttpStatus,
startSpan,
} from '@sentry/core';
import { tracingContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils';
@@ -66,7 +67,7 @@ export function wrapRouteHandlerWithSentry any>(
);
try {
- span?.setHttpStatus(response.status);
+ span && setHttpStatus(span, response.status);
} catch {
// best effort
}
diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts
index a0a1ae2f77aa..f8b6c5698550 100644
--- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts
+++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts
@@ -10,6 +10,7 @@ import {
} from '@sentry/core';
import { winterCGHeadersToDict } from '@sentry/utils';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils';
import type { ServerComponentContext } from '../common/types';
import { commonObjectToPropagationContext } from './utils/commonObjectTracing';
@@ -61,9 +62,9 @@ export function wrapServerComponentWithSentry any>
op: 'function.nextjs',
name: `${componentType} Server Component (${componentRoute})`,
status: 'ok',
- origin: 'auto.function.nextjs',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs',
},
metadata: {
// eslint-disable-next-line deprecation/deprecation
diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts
index 464b7db14dc7..f4ec99c3cc71 100644
--- a/packages/nextjs/test/clientSdk.test.ts
+++ b/packages/nextjs/test/clientSdk.test.ts
@@ -1,6 +1,7 @@
import { BaseClient } from '@sentry/core';
import * as SentryReact from '@sentry/react';
import type { BrowserClient } from '@sentry/react';
+import { browserTracingIntegration } from '@sentry/react';
import { WINDOW, getClient, getCurrentScope } from '@sentry/react';
import type { Integration } from '@sentry/types';
import { logger } from '@sentry/utils';
@@ -166,7 +167,7 @@ describe('Client init()', () => {
init({
dsn: TEST_DSN,
tracesSampleRate: 1.0,
- integrations: [new BrowserTracing({ startTransactionOnLocationChange: false })],
+ integrations: [new BrowserTracing({ finalTimeout: 10 })],
});
const client = getClient()!;
@@ -177,7 +178,27 @@ describe('Client init()', () => {
expect.objectContaining({
routingInstrumentation: nextRouterInstrumentation,
// This proves it's still the user's copy
- startTransactionOnLocationChange: false,
+ finalTimeout: 10,
+ }),
+ );
+ });
+
+ it('forces correct router instrumentation if user provides `browserTracingIntegration`', () => {
+ init({
+ dsn: TEST_DSN,
+ integrations: [browserTracingIntegration({ finalTimeout: 10 })],
+ enableTracing: true,
+ });
+
+ const client = getClient()!;
+ const integration = client.getIntegrationByName('BrowserTracing');
+
+ expect(integration).toBeDefined();
+ expect(integration?.options).toEqual(
+ expect.objectContaining({
+ routingInstrumentation: nextRouterInstrumentation,
+ // This proves it's still the user's copy
+ finalTimeout: 10,
}),
);
});
diff --git a/packages/nextjs/test/config/withSentry.test.ts b/packages/nextjs/test/config/withSentry.test.ts
index 91b61516a240..1ae933549b17 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 { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions } from '@sentry/core';
import type { NextApiRequest, NextApiResponse } from 'next';
import type { AugmentedNextApiResponse, NextApiHandler } from '../../src/common/types';
@@ -44,9 +44,9 @@ describe('withSentry', () => {
{
name: 'GET http://dogs.are.great',
op: 'http.server',
- origin: 'auto.http.nextjs',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs',
},
metadata: {
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 97d6e7b103e1..495e6336e2cd 100644
--- a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts
+++ b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts
@@ -87,10 +87,10 @@ describe('withEdgeWrapping', () => {
},
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [coreSdk.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping',
},
name: 'some label',
op: 'some op',
- origin: 'auto.function.nextjs.withEdgeWrapping',
}),
expect.any(Function),
);
diff --git a/packages/nextjs/test/edge/withSentryAPI.test.ts b/packages/nextjs/test/edge/withSentryAPI.test.ts
index ea5e7c4319f0..071bdda93952 100644
--- a/packages/nextjs/test/edge/withSentryAPI.test.ts
+++ b/packages/nextjs/test/edge/withSentryAPI.test.ts
@@ -1,5 +1,5 @@
import * as coreSdk from '@sentry/core';
-import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
import { wrapApiHandlerWithSentry } from '../../src/edge';
@@ -58,10 +58,10 @@ describe('wrapApiHandlerWithSentry', () => {
},
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping',
},
name: 'POST /user/[userId]/post/[postId]',
op: 'http.server',
- origin: 'auto.function.nextjs.withEdgeWrapping',
}),
expect.any(Function),
);
@@ -80,10 +80,10 @@ describe('wrapApiHandlerWithSentry', () => {
metadata: {},
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [coreSdk.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping',
},
name: 'handler (/user/[userId]/post/[postId])',
op: 'http.server',
- origin: 'auto.function.nextjs.withEdgeWrapping',
}),
expect.any(Function),
);
diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts
index b19dad4adfd7..2fcb4ee1b166 100644
--- a/packages/node-experimental/src/index.ts
+++ b/packages/node-experimental/src/index.ts
@@ -66,7 +66,9 @@ export {
Hub,
runWithAsyncContext,
SDK_VERSION,
+ // eslint-disable-next-line deprecation/deprecation
spanStatusfromHttpCode,
+ getSpanStatusFromHttpCode,
// eslint-disable-next-line deprecation/deprecation
trace,
captureCheckIn,
diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts
index b1038a53a276..929b286452f3 100644
--- a/packages/node-experimental/test/integration/transactions.test.ts
+++ b/packages/node-experimental/test/integration/transactions.test.ts
@@ -108,11 +108,6 @@ describe('Integration | Transactions', () => {
trace_id: expect.any(String),
transaction: 'test name',
}),
- propagationContext: {
- sampled: undefined,
- spanId: expect.any(String),
- traceId: expect.any(String),
- },
sampleRate: 1,
source: 'task',
spanMetadata: expect.any(Object),
diff --git a/packages/node-experimental/test/sdk/scope.test.ts b/packages/node-experimental/test/sdk/scope.test.ts
index e3919654e920..076188d9c9b7 100644
--- a/packages/node-experimental/test/sdk/scope.test.ts
+++ b/packages/node-experimental/test/sdk/scope.test.ts
@@ -130,12 +130,7 @@ describe('Unit | Scope', () => {
event_id: expect.any(String),
environment: 'production',
message: 'foo',
- sdkProcessingMetadata: {
- propagationContext: {
- spanId: expect.any(String),
- traceId: expect.any(String),
- },
- },
+ sdkProcessingMetadata: {},
});
});
@@ -213,16 +208,15 @@ describe('Unit | Scope', () => {
user: { id: '1', email: 'test@example.com' },
tags: { tag1: 'aa', tag2: 'aa' },
extra: { extra1: 'aa', extra2: 'aa' },
- contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } },
+ contexts: {
+ os: { name: 'os1' },
+ culture: { display_name: 'name1' },
+ },
fingerprint: ['dd', 'aa'],
breadcrumbs: [breadcrumb4, breadcrumb2, breadcrumb3, breadcrumb1],
sdkProcessingMetadata: {
aa: 'aa',
bb: 'bb',
- propagationContext: {
- spanId: '1',
- traceId: '1',
- },
},
});
});
diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts
index 892aabd2dd84..b1140d0d9c28 100644
--- a/packages/node/src/handlers.ts
+++ b/packages/node/src/handlers.ts
@@ -10,6 +10,7 @@ import {
getCurrentScope,
hasTracingEnabled,
runWithAsyncContext,
+ setHttpStatus,
startTransaction,
withScope,
} from '@sentry/core';
@@ -105,7 +106,7 @@ export function tracingHandler(): (
// closes
setImmediate(() => {
addRequestDataToTransaction(transaction, req);
- transaction.setHttpStatus(res.statusCode);
+ setHttpStatus(transaction, res.statusCode);
transaction.end();
});
});
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index 09fc72a5d382..79edd5eddd89 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -65,7 +65,9 @@ export {
setTag,
setTags,
setUser,
+ // eslint-disable-next-line deprecation/deprecation
spanStatusfromHttpCode,
+ getSpanStatusFromHttpCode,
// eslint-disable-next-line deprecation/deprecation
trace,
withScope,
diff --git a/packages/node/src/integrations/hapi/index.ts b/packages/node/src/integrations/hapi/index.ts
index 42335f7c4ce5..c80265926ce4 100644
--- a/packages/node/src/integrations/hapi/index.ts
+++ b/packages/node/src/integrations/hapi/index.ts
@@ -6,6 +6,7 @@ import {
getActiveTransaction,
getCurrentScope,
getDynamicSamplingContextFromSpan,
+ setHttpStatus,
spanToTraceHeader,
startTransaction,
} from '@sentry/core';
@@ -117,11 +118,11 @@ export const hapiTracingPlugin = {
// eslint-disable-next-line deprecation/deprecation
const transaction = getActiveTransaction();
- if (request.response && isResponseObject(request.response) && transaction) {
- transaction.setHttpStatus(request.response.statusCode);
- }
-
if (transaction) {
+ if (request.response && isResponseObject(request.response)) {
+ setHttpStatus(transaction, request.response.statusCode);
+ }
+
transaction.end();
}
diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts
index e3f6d164d991..de013541257e 100644
--- a/packages/node/src/integrations/http.ts
+++ b/packages/node/src/integrations/http.ts
@@ -1,6 +1,7 @@
import type * as http from 'http';
import type * as https from 'https';
import type { Hub } from '@sentry/core';
+import { getIsolationScope } from '@sentry/core';
import {
addBreadcrumb,
getActiveSpan,
@@ -10,16 +11,11 @@ import {
getDynamicSamplingContextFromClient,
getDynamicSamplingContextFromSpan,
isSentryRequestUrl,
+ setHttpStatus,
spanToJSON,
spanToTraceHeader,
} from '@sentry/core';
-import type {
- DynamicSamplingContext,
- EventProcessor,
- Integration,
- SanitizedRequestData,
- TracePropagationTargets,
-} from '@sentry/types';
+import type { EventProcessor, Integration, SanitizedRequestData, TracePropagationTargets } from '@sentry/types';
import {
LRUMap,
dynamicSamplingContextToSentryBaggageHeader,
@@ -250,13 +246,15 @@ function _createWrappedRequestMethodFactory(
// eslint-disable-next-line deprecation/deprecation
const rawRequestUrl = extractRawUrl(requestOptions);
const requestUrl = extractUrl(requestOptions);
+ const client = getClient();
// we don't want to record requests to Sentry as either breadcrumbs or spans, so just use the original method
- if (isSentryRequestUrl(requestUrl, getClient())) {
+ if (isSentryRequestUrl(requestUrl, client)) {
return originalRequestMethod.apply(httpModule, requestArgs);
}
const scope = getCurrentScope();
+ const isolationScope = getIsolationScope();
const parentSpan = getActiveSpan();
const data = getRequestSpanData(requestUrl, requestOptions);
@@ -271,19 +269,24 @@ function _createWrappedRequestMethodFactory(
})
: undefined;
- if (shouldAttachTraceData(rawRequestUrl)) {
- if (requestSpan) {
- const sentryTraceHeader = spanToTraceHeader(requestSpan);
- const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestSpan);
- addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, dynamicSamplingContext);
- } else {
- const client = getClient();
- const { traceId, sampled, dsc } = scope.getPropagationContext();
- const sentryTraceHeader = generateSentryTraceHeader(traceId, undefined, sampled);
- const dynamicSamplingContext =
- dsc || (client ? getDynamicSamplingContextFromClient(traceId, client, scope) : undefined);
- addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, dynamicSamplingContext);
- }
+ if (client && shouldAttachTraceData(rawRequestUrl)) {
+ const { traceId, spanId, sampled, dsc } = {
+ ...isolationScope.getPropagationContext(),
+ ...scope.getPropagationContext(),
+ };
+
+ const sentryTraceHeader = requestSpan
+ ? spanToTraceHeader(requestSpan)
+ : generateSentryTraceHeader(traceId, spanId, sampled);
+
+ const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(
+ dsc ||
+ (requestSpan
+ ? getDynamicSamplingContextFromSpan(requestSpan)
+ : getDynamicSamplingContextFromClient(traceId, client, scope)),
+ );
+
+ addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, sentryBaggageHeader);
} else {
DEBUG_BUILD &&
logger.log(
@@ -302,7 +305,7 @@ function _createWrappedRequestMethodFactory(
}
if (requestSpan) {
if (res.statusCode) {
- requestSpan.setHttpStatus(res.statusCode);
+ setHttpStatus(requestSpan, res.statusCode);
}
requestSpan.updateName(
cleanSpanDescription(spanToJSON(requestSpan).description || '', requestOptions, req) || '',
@@ -318,7 +321,7 @@ function _createWrappedRequestMethodFactory(
addRequestBreadcrumb('error', data, req);
}
if (requestSpan) {
- requestSpan.setHttpStatus(500);
+ setHttpStatus(requestSpan, 500);
requestSpan.updateName(
cleanSpanDescription(spanToJSON(requestSpan).description || '', requestOptions, req) || '',
);
@@ -333,7 +336,7 @@ function addHeadersToRequestOptions(
requestOptions: RequestOptions,
requestUrl: string,
sentryTraceHeader: string,
- dynamicSamplingContext: Partial | undefined,
+ sentryBaggageHeader: string | undefined,
): void {
// Don't overwrite sentry-trace and baggage header if it's already set.
const headers = requestOptions.headers || {};
@@ -343,15 +346,13 @@ function addHeadersToRequestOptions(
DEBUG_BUILD &&
logger.log(`[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to "${requestUrl}": `);
- const sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
- const sentryBaggageHeader =
- sentryBaggage && sentryBaggage.length > 0 ? normalizeBaggageHeader(requestOptions, sentryBaggage) : undefined;
requestOptions.headers = {
...requestOptions.headers,
'sentry-trace': sentryTraceHeader,
// Setting a header to `undefined` will crash in node so we only set the baggage header when it's defined
- ...(sentryBaggageHeader && { baggage: sentryBaggageHeader }),
+ ...(sentryBaggageHeader &&
+ sentryBaggageHeader.length > 0 && { baggage: normalizeBaggageHeader(requestOptions, sentryBaggageHeader) }),
};
}
diff --git a/packages/node/src/integrations/spotlight.ts b/packages/node/src/integrations/spotlight.ts
index f6e5c1e45adc..ab27f860c97b 100644
--- a/packages/node/src/integrations/spotlight.ts
+++ b/packages/node/src/integrations/spotlight.ts
@@ -71,7 +71,8 @@ function connectToSpotlight(client: Client, options: Required {
parentSpanId: '1121201211212012',
spanId: expect.any(String),
sampled: false,
+ dsc: {}, // There is an incoming trace but no baggage header, so the DSC must be frozen (empty object)
});
// since we have no tracesSampler defined, the default behavior (inherit if possible) applies
diff --git a/packages/node/test/performance.test.ts b/packages/node/test/performance.test.ts
new file mode 100644
index 000000000000..0f57dd4166e6
--- /dev/null
+++ b/packages/node/test/performance.test.ts
@@ -0,0 +1,150 @@
+import { setAsyncContextStrategy, setCurrentClient, startSpan, startSpanManual } from '@sentry/core';
+import type { TransactionEvent } from '@sentry/types';
+import { NodeClient, defaultStackParser } from '../src';
+import { setNodeAsyncContextStrategy } from '../src/async';
+import { getDefaultNodeClientOptions } from './helper/node-client-options';
+
+const dsn = 'https://53039209a22b4ec1bcc296a3c9fdecd6@sentry.io/4291';
+
+beforeAll(() => {
+ setNodeAsyncContextStrategy();
+});
+
+afterAll(() => {
+ setAsyncContextStrategy(undefined);
+});
+
+describe('startSpan()', () => {
+ it('should correctly separate spans when called after one another with interwoven timings', async () => {
+ const transactionEventPromise = new Promise(resolve => {
+ setCurrentClient(
+ new NodeClient(
+ getDefaultNodeClientOptions({
+ stackParser: defaultStackParser,
+ tracesSampleRate: 1,
+ beforeSendTransaction: event => {
+ resolve(event);
+ return null;
+ },
+ dsn,
+ }),
+ ),
+ );
+ });
+
+ startSpan({ name: 'first' }, () => {
+ return new Promise(resolve => {
+ setTimeout(resolve, 500);
+ });
+ });
+
+ startSpan({ name: 'second' }, () => {
+ return new Promise(resolve => {
+ setTimeout(resolve, 250);
+ });
+ });
+
+ const transactionEvent = await transactionEventPromise;
+
+ // Any transaction events happening shouldn't have any child spans
+ expect(transactionEvent.spans).toStrictEqual([]);
+ });
+
+ it('should correctly nest spans when called within one another', async () => {
+ const transactionEventPromise = new Promise(resolve => {
+ setCurrentClient(
+ new NodeClient(
+ getDefaultNodeClientOptions({
+ stackParser: defaultStackParser,
+ tracesSampleRate: 1,
+ beforeSendTransaction: event => {
+ resolve(event);
+ return null;
+ },
+ dsn,
+ }),
+ ),
+ );
+ });
+
+ startSpan({ name: 'first' }, () => {
+ startSpan({ name: 'second' }, () => undefined);
+ });
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.spans).toContainEqual(expect.objectContaining({ description: 'second' }));
+ });
+});
+
+describe('startSpanManual()', () => {
+ it('should correctly separate spans when called after one another with interwoven timings', async () => {
+ const transactionEventPromise = new Promise(resolve => {
+ setCurrentClient(
+ new NodeClient(
+ getDefaultNodeClientOptions({
+ stackParser: defaultStackParser,
+ tracesSampleRate: 1,
+ beforeSendTransaction: event => {
+ resolve(event);
+ return null;
+ },
+ dsn,
+ }),
+ ),
+ );
+ });
+
+ startSpanManual({ name: 'first' }, span => {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ span?.end();
+ resolve();
+ }, 500);
+ });
+ });
+
+ startSpanManual({ name: 'second' }, span => {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ span?.end();
+ resolve();
+ }, 500);
+ });
+ });
+
+ const transactionEvent = await transactionEventPromise;
+
+ // Any transaction events happening shouldn't have any child spans
+ expect(transactionEvent.spans).toStrictEqual([]);
+ });
+
+ it('should correctly nest spans when called within one another', async () => {
+ const transactionEventPromise = new Promise(resolve => {
+ setCurrentClient(
+ new NodeClient(
+ getDefaultNodeClientOptions({
+ stackParser: defaultStackParser,
+ tracesSampleRate: 1,
+ beforeSendTransaction: event => {
+ resolve(event);
+ return null;
+ },
+ dsn,
+ }),
+ ),
+ );
+ });
+
+ startSpanManual({ name: 'first' }, span1 => {
+ startSpanManual({ name: 'second' }, span2 => {
+ span2?.end();
+ });
+ span1?.end();
+ });
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.spans).toContainEqual(expect.objectContaining({ description: 'second' }));
+ });
+});
diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts
index 0768541fbfb9..bbeb2744e501 100644
--- a/packages/opentelemetry/src/propagator.ts
+++ b/packages/opentelemetry/src/propagator.ts
@@ -83,7 +83,7 @@ function getDsc(
context: Context,
propagationContext: PropagationContext,
traceId: string | undefined,
-): DynamicSamplingContext | undefined {
+): Partial | undefined {
// If we have a DSC on the propagation context, we just use it
if (propagationContext.dsc) {
return propagationContext.dsc;
diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts
index ca2f1deb4fc0..0d57d1009e31 100644
--- a/packages/opentelemetry/src/spanExporter.ts
+++ b/packages/opentelemetry/src/spanExporter.ts
@@ -4,7 +4,7 @@ import { ExportResultCode } from '@opentelemetry/core';
import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import { flush, getCurrentScope } from '@sentry/core';
-import type { DynamicSamplingContext, Scope, Span as SentrySpan, SpanOrigin, TransactionSource } from '@sentry/types';
+import type { Scope, Span as SentrySpan, SpanOrigin, TransactionSource } from '@sentry/types';
import { logger } from '@sentry/utils';
import { getCurrentHub } from './custom/hub';
@@ -158,9 +158,7 @@ function createTransactionForOtelSpan(span: ReadableSpan): OpenTelemetryTransact
const parentSpanId = span.parentSpanId;
const parentSampled = span.attributes[InternalSentrySemanticAttributes.PARENT_SAMPLED] as boolean | undefined;
- const dynamicSamplingContext: DynamicSamplingContext | undefined = scope
- ? scope.getPropagationContext().dsc
- : undefined;
+ const dynamicSamplingContext = scope ? scope.getPropagationContext().dsc : undefined;
const { op, description, tags, data, origin, source } = getSpanData(span);
const metadata = getSpanMetadata(span);
diff --git a/packages/opentelemetry/src/trace.ts b/packages/opentelemetry/src/trace.ts
index e2f517900114..1047d77f39fd 100644
--- a/packages/opentelemetry/src/trace.ts
+++ b/packages/opentelemetry/src/trace.ts
@@ -1,5 +1,7 @@
import type { Span, Tracer } from '@opentelemetry/api';
+import { context } from '@opentelemetry/api';
import { SpanStatusCode, trace } from '@opentelemetry/api';
+import { suppressTracing } from '@opentelemetry/core';
import { SDK_VERSION, handleCallbackErrors } from '@sentry/core';
import type { Client } from '@sentry/types';
@@ -22,7 +24,11 @@ export function startSpan(spanContext: OpenTelemetrySpanContext, callback: (s
const { name } = spanContext;
- return tracer.startActiveSpan(name, spanContext, span => {
+ const activeCtx = context.active();
+ const shouldSkipSpan = spanContext.onlyIfParent && !trace.getSpan(activeCtx);
+ const ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx;
+
+ return tracer.startActiveSpan(name, spanContext, ctx, span => {
_applySentryAttributesToSpan(span, spanContext);
return handleCallbackErrors(
@@ -49,7 +55,11 @@ export function startSpanManual(spanContext: OpenTelemetrySpanContext, callba
const { name } = spanContext;
- return tracer.startActiveSpan(name, spanContext, span => {
+ const activeCtx = context.active();
+ const shouldSkipSpan = spanContext.onlyIfParent && !trace.getSpan(activeCtx);
+ const ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx;
+
+ return tracer.startActiveSpan(name, spanContext, ctx, span => {
_applySentryAttributesToSpan(span, spanContext);
return handleCallbackErrors(
@@ -81,7 +91,11 @@ export function startInactiveSpan(spanContext: OpenTelemetrySpanContext): Span {
const { name } = spanContext;
- const span = tracer.startSpan(name, spanContext);
+ const activeCtx = context.active();
+ const shouldSkipSpan = spanContext.onlyIfParent && !trace.getSpan(activeCtx);
+ const ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx;
+
+ const span = tracer.startSpan(name, spanContext, ctx);
_applySentryAttributesToSpan(span, spanContext);
diff --git a/packages/opentelemetry/src/types.ts b/packages/opentelemetry/src/types.ts
index 168a9f4893a6..d01d80b64e82 100644
--- a/packages/opentelemetry/src/types.ts
+++ b/packages/opentelemetry/src/types.ts
@@ -14,6 +14,7 @@ export interface OpenTelemetrySpanContext {
origin?: SpanOrigin;
source?: TransactionSource;
scope?: Scope;
+ onlyIfParent?: boolean;
// Base SpanOptions we support
attributes?: Attributes;
diff --git a/packages/opentelemetry/test/integration/scope.test.ts b/packages/opentelemetry/test/integration/scope.test.ts
index 6ab7c6d35621..d0234b27a140 100644
--- a/packages/opentelemetry/test/integration/scope.test.ts
+++ b/packages/opentelemetry/test/integration/scope.test.ts
@@ -69,7 +69,6 @@ describe('Integration | Scope', () => {
? {
span_id: spanId,
trace_id: traceId,
- parent_span_id: undefined,
}
: expect.any(Object),
}),
@@ -190,7 +189,6 @@ describe('Integration | Scope', () => {
? {
span_id: spanId1,
trace_id: traceId1,
- parent_span_id: undefined,
}
: expect.any(Object),
}),
diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts
index a87816691a45..5c4742df5f97 100644
--- a/packages/opentelemetry/test/integration/transactions.test.ts
+++ b/packages/opentelemetry/test/integration/transactions.test.ts
@@ -106,11 +106,6 @@ describe('Integration | Transactions', () => {
trace_id: expect.any(String),
transaction: 'test name',
}),
- propagationContext: {
- sampled: undefined,
- spanId: expect.any(String),
- traceId: expect.any(String),
- },
sampleRate: 1,
source: 'task',
spanMetadata: expect.any(Object),
diff --git a/packages/opentelemetry/test/propagator.test.ts b/packages/opentelemetry/test/propagator.test.ts
index 1663021b6224..a4eb98ab2126 100644
--- a/packages/opentelemetry/test/propagator.test.ts
+++ b/packages/opentelemetry/test/propagator.test.ts
@@ -310,6 +310,7 @@ describe('SentryPropagator', () => {
parentSpanId: '6e0c63257de34c92',
spanId: expect.any(String),
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
+ dsc: {}, // Frozen DSC
});
// Ensure spanId !== parentSpanId - it should be a new random ID
@@ -321,19 +322,21 @@ describe('SentryPropagator', () => {
carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader;
const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
expect(getPropagationContextFromContext(context)).toEqual({
- sampled: undefined,
spanId: expect.any(String),
traceId: expect.any(String),
});
});
it('sets defined dynamic sampling context on context', () => {
+ const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1';
const baggage =
'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=dsc-transaction';
+ carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader;
carrier[SENTRY_BAGGAGE_HEADER] = baggage;
const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
expect(getPropagationContextFromContext(context)).toEqual({
- sampled: undefined,
+ sampled: true,
+ parentSpanId: expect.any(String),
spanId: expect.any(String),
traceId: expect.any(String), // Note: This is not automatically taken from the DSC (in reality, this should be aligned)
dsc: {
diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts
index af8625a6534e..34e27c3c62f3 100644
--- a/packages/opentelemetry/test/trace.test.ts
+++ b/packages/opentelemetry/test/trace.test.ts
@@ -2,6 +2,7 @@ import type { Span } from '@opentelemetry/api';
import { SpanKind } from '@opentelemetry/api';
import { TraceFlags, context, trace } from '@opentelemetry/api';
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
+import { Span as SpanClass } from '@opentelemetry/sdk-trace-base';
import type { PropagationContext } from '@sentry/types';
import { getClient } from '../src/custom/hub';
@@ -260,6 +261,28 @@ describe('trace', () => {
},
);
});
+
+ describe('onlyIfParent', () => {
+ it('does not create a span if there is no parent', () => {
+ const span = startSpan({ name: 'test span', onlyIfParent: true }, span => {
+ return span;
+ });
+
+ expect(span).not.toBeInstanceOf(SpanClass);
+ });
+
+ it('creates a span if there is a parent', () => {
+ const span = startSpan({ name: 'parent span' }, () => {
+ const span = startSpan({ name: 'test span', onlyIfParent: true }, span => {
+ return span;
+ });
+
+ return span;
+ });
+
+ expect(span).toBeInstanceOf(SpanClass);
+ });
+ });
});
describe('startInactiveSpan', () => {
@@ -349,6 +372,24 @@ describe('trace', () => {
});
expect(getSpanKind(span)).toEqual(SpanKind.CLIENT);
});
+
+ describe('onlyIfParent', () => {
+ it('does not create a span if there is no parent', () => {
+ const span = startInactiveSpan({ name: 'test span', onlyIfParent: true });
+
+ expect(span).not.toBeInstanceOf(SpanClass);
+ });
+
+ it('creates a span if there is a parent', () => {
+ const span = startSpan({ name: 'parent span' }, () => {
+ const span = startInactiveSpan({ name: 'test span', onlyIfParent: true });
+
+ return span;
+ });
+
+ expect(span).toBeInstanceOf(SpanClass);
+ });
+ });
});
describe('startSpanManual', () => {
@@ -419,6 +460,28 @@ describe('trace', () => {
);
});
});
+
+ describe('onlyIfParent', () => {
+ it('does not create a span if there is no parent', () => {
+ const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => {
+ return span;
+ });
+
+ expect(span).not.toBeInstanceOf(SpanClass);
+ });
+
+ it('creates a span if there is a parent', () => {
+ const span = startSpan({ name: 'parent span' }, () => {
+ const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => {
+ return span;
+ });
+
+ return span;
+ });
+
+ expect(span).toBeInstanceOf(SpanClass);
+ });
+ });
});
describe('trace (tracing disabled)', () => {
diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx
index d4f942e33d64..54ed3fa541c8 100644
--- a/packages/react/src/profiler.tsx
+++ b/packages/react/src/profiler.tsx
@@ -57,6 +57,7 @@ class Profiler extends React.Component {
this._mountSpan = startInactiveSpan({
name: `<${name}>`,
+ onlyIfParent: true,
op: REACT_MOUNT_OP,
origin: 'auto.ui.react.profiler',
attributes: { 'ui.component_name': name },
@@ -83,6 +84,7 @@ class Profiler extends React.Component {
this._updateSpan = withActiveSpan(this._mountSpan, () => {
return startInactiveSpan({
name: `<${this.props.name}>`,
+ onlyIfParent: true,
op: REACT_UPDATE_OP,
origin: 'auto.ui.react.profiler',
startTimestamp: now,
@@ -115,6 +117,7 @@ class Profiler extends React.Component {
const startTimestamp = spanToJSON(this._mountSpan).timestamp;
withActiveSpan(this._mountSpan, () => {
const renderSpan = startInactiveSpan({
+ onlyIfParent: true,
name: `<${name}>`,
op: REACT_RENDER_OP,
origin: 'auto.ui.react.profiler',
@@ -187,6 +190,7 @@ function useProfiler(
return startInactiveSpan({
name: `<${name}>`,
+ onlyIfParent: true,
op: REACT_MOUNT_OP,
origin: 'auto.ui.react.profiler',
attributes: { 'ui.component_name': name },
@@ -205,6 +209,7 @@ function useProfiler(
const renderSpan = startInactiveSpan({
name: `<${name}>`,
+ onlyIfParent: true,
op: REACT_RENDER_OP,
origin: 'auto.ui.react.profiler',
startTimestamp,
diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts
index 38f99d7af825..fb3ca2fa073f 100644
--- a/packages/react/src/redux.ts
+++ b/packages/react/src/redux.ts
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-import { addEventProcessor, getClient, getCurrentScope } from '@sentry/browser';
+import { getClient, getCurrentScope, getGlobalScope } from '@sentry/core';
import type { Scope } from '@sentry/types';
import { addNonEnumerableProperty } from '@sentry/utils';
@@ -97,7 +97,7 @@ function createReduxEnhancer(enhancerOptions?: Partial):
return (next: StoreEnhancerStoreCreator): StoreEnhancerStoreCreator =>
(reducer: Reducer, initialState?: PreloadedState) => {
options.attachReduxState &&
- addEventProcessor((event, hint) => {
+ getGlobalScope().addEventProcessor((event, hint) => {
try {
// @ts-expect-error try catch to reduce bundle size
if (event.type === undefined && event.contexts.state.state.type === 'redux') {
@@ -117,6 +117,7 @@ function createReduxEnhancer(enhancerOptions?: Partial):
const newState = reducer(state, action);
const scope = getCurrentScope();
+
/* Action breadcrumbs */
const transformedAction = options.actionTransformer(action);
if (typeof transformedAction !== 'undefined' && transformedAction !== null) {
diff --git a/packages/react/test/profiler.test.tsx b/packages/react/test/profiler.test.tsx
index 2cdbae0e9320..5d399f342535 100644
--- a/packages/react/test/profiler.test.tsx
+++ b/packages/react/test/profiler.test.tsx
@@ -74,6 +74,7 @@ describe('withProfiler', () => {
expect(mockStartInactiveSpan).toHaveBeenCalledTimes(1);
expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({
name: `<${UNKNOWN_COMPONENT}>`,
+ onlyIfParent: true,
op: REACT_MOUNT_OP,
origin: 'auto.ui.react.profiler',
attributes: { 'ui.component_name': 'unknown' },
@@ -92,6 +93,7 @@ describe('withProfiler', () => {
expect(mockStartInactiveSpan).toHaveBeenCalledTimes(2);
expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({
name: `<${UNKNOWN_COMPONENT}>`,
+ onlyIfParent: true,
op: REACT_RENDER_OP,
origin: 'auto.ui.react.profiler',
startTimestamp: undefined,
@@ -125,6 +127,7 @@ describe('withProfiler', () => {
expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({
attributes: { 'ui.react.changed_props': ['num'], 'ui.component_name': 'unknown' },
name: `<${UNKNOWN_COMPONENT}>`,
+ onlyIfParent: true,
op: REACT_UPDATE_OP,
origin: 'auto.ui.react.profiler',
startTimestamp: expect.any(Number),
@@ -136,6 +139,7 @@ describe('withProfiler', () => {
expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({
attributes: { 'ui.react.changed_props': ['num'], 'ui.component_name': 'unknown' },
name: `<${UNKNOWN_COMPONENT}>`,
+ onlyIfParent: true,
op: REACT_UPDATE_OP,
origin: 'auto.ui.react.profiler',
startTimestamp: expect.any(Number),
@@ -175,6 +179,7 @@ describe('useProfiler()', () => {
expect(mockStartInactiveSpan).toHaveBeenCalledTimes(1);
expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({
name: '',
+ onlyIfParent: true,
op: REACT_MOUNT_OP,
origin: 'auto.ui.react.profiler',
attributes: { 'ui.component_name': 'Example' },
@@ -199,6 +204,7 @@ describe('useProfiler()', () => {
expect(mockStartInactiveSpan).toHaveBeenLastCalledWith(
expect.objectContaining({
name: '',
+ onlyIfParent: true,
op: REACT_RENDER_OP,
origin: 'auto.ui.react.profiler',
attributes: { 'ui.component_name': 'Example' },
diff --git a/packages/react/test/redux.test.ts b/packages/react/test/redux.test.ts
index 0ce064365eeb..537c133cd3fd 100644
--- a/packages/react/test/redux.test.ts
+++ b/packages/react/test/redux.test.ts
@@ -5,24 +5,28 @@ import { createReduxEnhancer } from '../src/redux';
const mockAddBreadcrumb = jest.fn();
const mockSetContext = jest.fn();
+const mockGlobalScopeAddEventProcessor = jest.fn();
-jest.mock('@sentry/browser', () => ({
- ...jest.requireActual('@sentry/browser'),
+jest.mock('@sentry/core', () => ({
+ ...jest.requireActual('@sentry/core'),
getCurrentScope() {
return {
addBreadcrumb: mockAddBreadcrumb,
setContext: mockSetContext,
};
},
+ getGlobalScope() {
+ return {
+ addEventProcessor: mockGlobalScopeAddEventProcessor,
+ };
+ },
addEventProcessor: jest.fn(),
}));
-const mockAddEventProcessor = Sentry.addEventProcessor as jest.Mock;
-
afterEach(() => {
mockAddBreadcrumb.mockReset();
mockSetContext.mockReset();
- mockAddEventProcessor.mockReset();
+ mockGlobalScopeAddEventProcessor.mockReset();
});
describe('createReduxEnhancer', () => {
@@ -257,9 +261,9 @@ describe('createReduxEnhancer', () => {
Redux.createStore((state = initialState) => state, enhancer);
- expect(mockAddEventProcessor).toHaveBeenCalledTimes(1);
+ expect(mockGlobalScopeAddEventProcessor).toHaveBeenCalledTimes(1);
- const callbackFunction = mockAddEventProcessor.mock.calls[0][0];
+ const callbackFunction = mockGlobalScopeAddEventProcessor.mock.calls[0][0];
const mockEvent = {
contexts: {
@@ -306,7 +310,7 @@ describe('createReduxEnhancer', () => {
Redux.createStore((state = initialState) => state, enhancer);
- expect(mockAddEventProcessor).toHaveBeenCalledTimes(0);
+ expect(mockGlobalScopeAddEventProcessor).toHaveBeenCalledTimes(0);
});
it('does not attach when state.type is not redux', () => {
@@ -318,9 +322,9 @@ describe('createReduxEnhancer', () => {
Redux.createStore((state = initialState) => state, enhancer);
- expect(mockAddEventProcessor).toHaveBeenCalledTimes(1);
+ expect(mockGlobalScopeAddEventProcessor).toHaveBeenCalledTimes(1);
- const callbackFunction = mockAddEventProcessor.mock.calls[0][0];
+ const callbackFunction = mockGlobalScopeAddEventProcessor.mock.calls[0][0];
const mockEvent = {
contexts: {
@@ -353,9 +357,9 @@ describe('createReduxEnhancer', () => {
Redux.createStore((state = initialState) => state, enhancer);
- expect(mockAddEventProcessor).toHaveBeenCalledTimes(1);
+ expect(mockGlobalScopeAddEventProcessor).toHaveBeenCalledTimes(1);
- const callbackFunction = mockAddEventProcessor.mock.calls[0][0];
+ const callbackFunction = mockGlobalScopeAddEventProcessor.mock.calls[0][0];
const mockEvent = {
contexts: {
@@ -385,9 +389,9 @@ describe('createReduxEnhancer', () => {
Redux.createStore((state = initialState) => state, enhancer);
- expect(mockAddEventProcessor).toHaveBeenCalledTimes(1);
+ expect(mockGlobalScopeAddEventProcessor).toHaveBeenCalledTimes(1);
- const callbackFunction = mockAddEventProcessor.mock.calls[0][0];
+ const callbackFunction = mockGlobalScopeAddEventProcessor.mock.calls[0][0];
const mockEvent = {
type: 'not_redux',
diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts
index 1cd04720b4dc..da1e794690de 100644
--- a/packages/remix/src/index.server.ts
+++ b/packages/remix/src/index.server.ts
@@ -15,6 +15,7 @@ export {
addGlobalEventProcessor,
addEventProcessor,
addBreadcrumb,
+ addIntegration,
captureCheckIn,
withMonitor,
captureException,
@@ -30,10 +31,15 @@ export {
getHubFromCarrier,
// eslint-disable-next-line deprecation/deprecation
getCurrentHub,
+ getClient,
+ getCurrentScope,
+ getGlobalScope,
+ getIsolationScope,
Hub,
// eslint-disable-next-line deprecation/deprecation
makeMain,
setCurrentClient,
+ NodeClient,
Scope,
// eslint-disable-next-line deprecation/deprecation
startTransaction,
@@ -44,7 +50,9 @@ export {
setTag,
setTags,
setUser,
+ // eslint-disable-next-line deprecation/deprecation
spanStatusfromHttpCode,
+ getSpanStatusFromHttpCode,
// eslint-disable-next-line deprecation/deprecation
trace,
withScope,
@@ -67,8 +75,27 @@ export {
deepReadDirSync,
Integrations,
Handlers,
+ setMeasurement,
+ getActiveSpan,
+ startSpan,
+ startSpanManual,
+ startInactiveSpan,
+ continueTrace,
+ isInitialized,
cron,
parameterize,
+ metrics,
+ // eslint-disable-next-line deprecation/deprecation
+ getModuleFromFilename,
+ createGetModuleFromFilename,
+ functionToStringIntegration,
+ hapiErrorPlugin,
+ inboundFiltersIntegration,
+ linkedErrorsIntegration,
+ requestDataIntegration,
+ runWithAsyncContext,
+ // eslint-disable-next-line deprecation/deprecation
+ enableAnrDetection,
} from '@sentry/node';
// Keeping the `*` exports for backwards compatibility and types
diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts
index 22171153a534..1ed13e9f28ec 100644
--- a/packages/remix/src/utils/instrumentServer.ts
+++ b/packages/remix/src/utils/instrumentServer.ts
@@ -1,5 +1,6 @@
/* eslint-disable max-lines */
import {
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
getActiveSpan,
getActiveTransaction,
getClient,
@@ -7,6 +8,7 @@ import {
getDynamicSamplingContextFromSpan,
hasTracingEnabled,
runWithAsyncContext,
+ setHttpStatus,
spanToJSON,
spanToTraceHeader,
} from '@sentry/core';
@@ -411,7 +413,9 @@ export function startRequestHandlerTransaction(
const transaction = hub.startTransaction({
name,
op: 'http.server',
- origin: 'auto.http.remix',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.remix',
+ },
tags: {
method: request.method,
},
@@ -492,7 +496,7 @@ function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBui
const res = (await origRequestHandler.call(this, request, loadContext)) as Response;
if (isResponse(res)) {
- transaction.setHttpStatus(res.status);
+ setHttpStatus(transaction, res.status);
}
transaction.end();
diff --git a/packages/remix/src/utils/serverAdapters/express.ts b/packages/remix/src/utils/serverAdapters/express.ts
index d3acd7633132..8f05de780381 100644
--- a/packages/remix/src/utils/serverAdapters/express.ts
+++ b/packages/remix/src/utils/serverAdapters/express.ts
@@ -1,4 +1,11 @@
-import { getClient, getCurrentHub, getCurrentScope, hasTracingEnabled, runWithAsyncContext } from '@sentry/core';
+import {
+ getClient,
+ getCurrentHub,
+ getCurrentScope,
+ hasTracingEnabled,
+ runWithAsyncContext,
+ setHttpStatus,
+} from '@sentry/core';
import { flush } from '@sentry/node';
import type { Transaction } from '@sentry/types';
import { extractRequestData, fill, isString, logger } from '@sentry/utils';
@@ -151,7 +158,7 @@ async function finishSentryProcessing(res: AugmentedExpressResponse): Promise(
{
name: context.functionName,
op: 'function.aws.lambda',
- origin: 'auto.function.serverless',
...continueTraceContext,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless',
},
},
span => {
diff --git a/packages/serverless/src/awsservices.ts b/packages/serverless/src/awsservices.ts
index 8992b7a7adb0..084fdbf0238a 100644
--- a/packages/serverless/src/awsservices.ts
+++ b/packages/serverless/src/awsservices.ts
@@ -1,5 +1,6 @@
-import { startInactiveSpan } from '@sentry/node';
-import type { Integration, Span } from '@sentry/types';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, convertIntegrationFnToClass, defineIntegration } from '@sentry/core';
+import { getClient, startInactiveSpan } from '@sentry/node';
+import type { Client, Integration, IntegrationClass, IntegrationFn, Span } from '@sentry/types';
import { fill } from '@sentry/utils';
// 'aws-sdk/global' import is expected to be type-only so it's erased in the final .js file.
// When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here.
@@ -15,61 +16,74 @@ interface AWSService {
serviceIdentifier: string;
}
-/** AWS service requests tracking */
-export class AWSServices implements Integration {
- /**
- * @inheritDoc
- */
- public static id: string = 'AWSServices';
+const INTEGRATION_NAME = 'AWSServices';
- /**
- * @inheritDoc
- */
- public name: string;
+const SETUP_CLIENTS = new WeakMap();
- private readonly _optional: boolean;
+const _awsServicesIntegration = ((options: { optional?: boolean } = {}) => {
+ const optional = options.optional || false;
+ return {
+ name: INTEGRATION_NAME,
+ setupOnce() {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const awsModule = require('aws-sdk/global') as typeof AWS;
+ fill(awsModule.Service.prototype, 'makeRequest', wrapMakeRequest);
+ } catch (e) {
+ if (!optional) {
+ throw e;
+ }
+ }
+ },
+ setup(client) {
+ SETUP_CLIENTS.set(client, true);
+ },
+ };
+}) satisfies IntegrationFn;
- public constructor(options: { optional?: boolean } = {}) {
- this.name = AWSServices.id;
+export const awsServicesIntegration = defineIntegration(_awsServicesIntegration);
- this._optional = options.optional || false;
- }
+/**
+ * AWS Service Request Tracking.
+ *
+ * @deprecated Use `awsServicesIntegration()` instead.
+ */
+// eslint-disable-next-line deprecation/deprecation
+export const AWSServices = convertIntegrationFnToClass(
+ INTEGRATION_NAME,
+ awsServicesIntegration,
+) as IntegrationClass;
- /**
- * @inheritDoc
- */
- public setupOnce(): void {
- try {
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const awsModule = require('aws-sdk/global') as typeof AWS;
- fill(awsModule.Service.prototype, 'makeRequest', wrapMakeRequest);
- } catch (e) {
- if (!this._optional) {
- throw e;
- }
- }
- }
-}
+// eslint-disable-next-line deprecation/deprecation
+export type AWSServices = typeof AWSServices;
-/** */
+/**
+ * Patches AWS SDK request to create `http.client` spans.
+ */
function wrapMakeRequest(
orig: MakeRequestFunction,
): MakeRequestFunction {
return function (this: TService, operation: string, params?: GenericParams, callback?: MakeRequestCallback) {
let span: Span | undefined;
const req = orig.call(this, operation, params);
- req.on('afterBuild', () => {
- span = startInactiveSpan({
- name: describe(this, operation, params),
- op: 'http.client',
- origin: 'auto.http.serverless',
+
+ if (SETUP_CLIENTS.has(getClient() as Client)) {
+ req.on('afterBuild', () => {
+ span = startInactiveSpan({
+ name: describe(this, operation, params),
+ onlyIfParent: true,
+ op: 'http.client',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless',
+ },
+ });
});
- });
- req.on('complete', () => {
- if (span) {
- span.end();
- }
- });
+ req.on('complete', () => {
+ if (span) {
+ span.end();
+ }
+ });
+ }
if (callback) {
req.send(callback);
diff --git a/packages/serverless/src/gcpfunction/cloud_events.ts b/packages/serverless/src/gcpfunction/cloud_events.ts
index 92a3eb0e37e7..533c74bb7653 100644
--- a/packages/serverless/src/gcpfunction/cloud_events.ts
+++ b/packages/serverless/src/gcpfunction/cloud_events.ts
@@ -1,4 +1,4 @@
-import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, handleCallbackErrors } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, handleCallbackErrors } from '@sentry/core';
import { captureException, flush, getCurrentScope, startSpanManual } from '@sentry/node';
import { logger } from '@sentry/utils';
@@ -35,8 +35,10 @@ function _wrapCloudEventFunction(
{
name: context.type || '',
op: 'function.gcp.cloud_event',
- origin: 'auto.function.serverless.gcp_cloud_event',
- attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component' },
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event',
+ },
},
span => {
const scope = getCurrentScope();
diff --git a/packages/serverless/src/gcpfunction/events.ts b/packages/serverless/src/gcpfunction/events.ts
index 79c609e9108c..501b6f7d6da3 100644
--- a/packages/serverless/src/gcpfunction/events.ts
+++ b/packages/serverless/src/gcpfunction/events.ts
@@ -1,4 +1,4 @@
-import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, handleCallbackErrors } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, handleCallbackErrors } from '@sentry/core';
import { captureException, flush, getCurrentScope, startSpanManual } from '@sentry/node';
import { logger } from '@sentry/utils';
@@ -38,8 +38,10 @@ function _wrapEventFunction
{
name: context.eventType,
op: 'function.gcp.event',
- origin: 'auto.function.serverless.gcp_event',
- attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component' },
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event',
+ },
},
span => {
const scope = getCurrentScope();
diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts
index 41fa620779c7..a90dd3a0423c 100644
--- a/packages/serverless/src/gcpfunction/http.ts
+++ b/packages/serverless/src/gcpfunction/http.ts
@@ -1,4 +1,10 @@
-import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, Transaction, handleCallbackErrors } from '@sentry/core';
+import {
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+ Transaction,
+ handleCallbackErrors,
+ setHttpStatus,
+} from '@sentry/core';
import type { AddRequestDataToEventOptions } from '@sentry/node';
import { continueTrace, startSpanManual } from '@sentry/node';
import { getCurrentScope } from '@sentry/node';
@@ -78,9 +84,9 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial {
@@ -101,8 +107,10 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial void), encoding?: string | (() => void), cb?: () => void): any {
- span?.setHttpStatus(res.statusCode);
- span?.end();
+ if (span) {
+ setHttpStatus(span, res.statusCode);
+ span.end();
+ }
// eslint-disable-next-line @typescript-eslint/no-floating-promises
flush(options.flushTimeout)
diff --git a/packages/serverless/src/gcpfunction/index.ts b/packages/serverless/src/gcpfunction/index.ts
index 3907e84aabc8..50556634c5fc 100644
--- a/packages/serverless/src/gcpfunction/index.ts
+++ b/packages/serverless/src/gcpfunction/index.ts
@@ -7,8 +7,8 @@ import {
} from '@sentry/node';
import type { Integration, Options, SdkMetadata } from '@sentry/types';
-import { GoogleCloudGrpc } from '../google-cloud-grpc';
-import { GoogleCloudHttp } from '../google-cloud-http';
+import { googleCloudGrpcIntegration } from '../google-cloud-grpc';
+import { googleCloudHttpIntegration } from '../google-cloud-http';
export * from './http';
export * from './events';
@@ -18,16 +18,16 @@ export * from './cloud_events';
export const defaultIntegrations: Integration[] = [
// eslint-disable-next-line deprecation/deprecation
...defaultNodeIntegrations,
- new GoogleCloudHttp({ optional: true }), // We mark this integration optional since '@google-cloud/common' module could be missing.
- new GoogleCloudGrpc({ optional: true }), // We mark this integration optional since 'google-gax' module could be missing.
+ googleCloudHttpIntegration({ optional: true }), // We mark this integration optional since '@google-cloud/common' module could be missing.
+ googleCloudGrpcIntegration({ optional: true }), // We mark this integration optional since 'google-gax' module could be missing.
];
/** Get the default integrations for the GCP SDK. */
export function getDefaultIntegrations(options: Options): Integration[] {
return [
...getDefaultNodeIntegrations(options),
- new GoogleCloudHttp({ optional: true }), // We mark this integration optional since '@google-cloud/common' module could be missing.
- new GoogleCloudGrpc({ optional: true }), // We mark this integration optional since 'google-gax' module could be missing.
+ googleCloudHttpIntegration({ optional: true }), // We mark this integration optional since '@google-cloud/common' module could be missing.
+ googleCloudGrpcIntegration({ optional: true }), // We mark this integration optional since 'google-gax' module could be missing.
];
}
diff --git a/packages/serverless/src/google-cloud-grpc.ts b/packages/serverless/src/google-cloud-grpc.ts
index 458af78872f7..a32ac9dae9ac 100644
--- a/packages/serverless/src/google-cloud-grpc.ts
+++ b/packages/serverless/src/google-cloud-grpc.ts
@@ -1,6 +1,12 @@
import type { EventEmitter } from 'events';
+import {
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ convertIntegrationFnToClass,
+ defineIntegration,
+ getClient,
+} from '@sentry/core';
import { startInactiveSpan } from '@sentry/node';
-import type { Integration } from '@sentry/types';
+import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types';
import { fill } from '@sentry/utils';
interface GrpcFunction extends CallableFunction {
@@ -25,45 +31,52 @@ interface Stub {
[key: string]: GrpcFunctionObject;
}
-/** Google Cloud Platform service requests tracking for GRPC APIs */
-export class GoogleCloudGrpc implements Integration {
- /**
- * @inheritDoc
- */
- public static id: string = 'GoogleCloudGrpc';
+const SERVICE_PATH_REGEX = /^(\w+)\.googleapis.com$/;
- /**
- * @inheritDoc
- */
- public name: string;
+const INTEGRATION_NAME = 'GoogleCloudGrpc';
- private readonly _optional: boolean;
+const SETUP_CLIENTS = new WeakMap();
- public constructor(options: { optional?: boolean } = {}) {
- this.name = GoogleCloudGrpc.id;
+const _googleCloudGrpcIntegration = ((options: { optional?: boolean } = {}) => {
+ const optional = options.optional || false;
+ return {
+ name: INTEGRATION_NAME,
+ setupOnce() {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const gaxModule = require('google-gax');
+ fill(
+ gaxModule.GrpcClient.prototype, // eslint-disable-line @typescript-eslint/no-unsafe-member-access
+ 'createStub',
+ wrapCreateStub,
+ );
+ } catch (e) {
+ if (!optional) {
+ throw e;
+ }
+ }
+ },
+ setup(client) {
+ SETUP_CLIENTS.set(client, true);
+ },
+ };
+}) satisfies IntegrationFn;
- this._optional = options.optional || false;
- }
+export const googleCloudGrpcIntegration = defineIntegration(_googleCloudGrpcIntegration);
- /**
- * @inheritDoc
- */
- public setupOnce(): void {
- try {
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const gaxModule = require('google-gax');
- fill(
- gaxModule.GrpcClient.prototype, // eslint-disable-line @typescript-eslint/no-unsafe-member-access
- 'createStub',
- wrapCreateStub,
- );
- } catch (e) {
- if (!this._optional) {
- throw e;
- }
- }
- }
-}
+/**
+ * Google Cloud Platform service requests tracking for GRPC APIs.
+ *
+ * @deprecated Use `googleCloudGrpcIntegration()` instead.
+ */
+// eslint-disable-next-line deprecation/deprecation
+export const GoogleCloudGrpc = convertIntegrationFnToClass(
+ INTEGRATION_NAME,
+ googleCloudGrpcIntegration,
+) as IntegrationClass;
+
+// eslint-disable-next-line deprecation/deprecation
+export type GoogleCloudGrpc = typeof GoogleCloudGrpc;
/** Returns a wrapped function that returns a stub with tracing enabled */
function wrapCreateStub(origCreate: CreateStubFunc): CreateStubFunc {
@@ -104,13 +117,16 @@ function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodName: str
(orig: GrpcFunction): GrpcFunction =>
(...args) => {
const ret = orig.apply(stub, args);
- if (typeof ret?.on !== 'function') {
+ if (typeof ret?.on !== 'function' || !SETUP_CLIENTS.has(getClient() as Client)) {
return ret;
}
const span = startInactiveSpan({
name: `${callType} ${methodName}`,
+ onlyIfParent: true,
op: `grpc.${serviceIdentifier}`,
- origin: 'auto.grpc.serverless',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.grpc.serverless',
+ },
});
ret.on('status', () => {
if (span) {
@@ -124,6 +140,6 @@ function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodName: str
/** Identifies service by its address */
function identifyService(servicePath: string): string {
- const match = servicePath.match(/^(\w+)\.googleapis.com$/);
+ const match = servicePath.match(SERVICE_PATH_REGEX);
return match ? match[1] : servicePath;
}
diff --git a/packages/serverless/src/google-cloud-http.ts b/packages/serverless/src/google-cloud-http.ts
index 369fa6ad230d..5e7558f4299d 100644
--- a/packages/serverless/src/google-cloud-http.ts
+++ b/packages/serverless/src/google-cloud-http.ts
@@ -1,8 +1,12 @@
-// '@google-cloud/common' import is expected to be type-only so it's erased in the final .js file.
-// When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here.
import type * as common from '@google-cloud/common';
+import {
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ convertIntegrationFnToClass,
+ defineIntegration,
+ getClient,
+} from '@sentry/core';
import { startInactiveSpan } from '@sentry/node';
-import type { Integration } from '@sentry/types';
+import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types';
import { fill } from '@sentry/utils';
type RequestOptions = common.DecorateRequestOptions;
@@ -12,51 +16,61 @@ interface RequestFunction extends CallableFunction {
(reqOpts: RequestOptions, callback: ResponseCallback): void;
}
-/** Google Cloud Platform service requests tracking for RESTful APIs */
-export class GoogleCloudHttp implements Integration {
- /**
- * @inheritDoc
- */
- public static id: string = 'GoogleCloudHttp';
+const INTEGRATION_NAME = 'GoogleCloudHttp';
- /**
- * @inheritDoc
- */
- public name: string;
+const SETUP_CLIENTS = new WeakMap();
- private readonly _optional: boolean;
+const _googleCloudHttpIntegration = ((options: { optional?: boolean } = {}) => {
+ const optional = options.optional || false;
+ return {
+ name: INTEGRATION_NAME,
+ setupOnce() {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const commonModule = require('@google-cloud/common') as typeof common;
+ fill(commonModule.Service.prototype, 'request', wrapRequestFunction);
+ } catch (e) {
+ if (!optional) {
+ throw e;
+ }
+ }
+ },
+ setup(client) {
+ SETUP_CLIENTS.set(client, true);
+ },
+ };
+}) satisfies IntegrationFn;
- public constructor(options: { optional?: boolean } = {}) {
- this.name = GoogleCloudHttp.id;
+export const googleCloudHttpIntegration = defineIntegration(_googleCloudHttpIntegration);
- this._optional = options.optional || false;
- }
+/**
+ * Google Cloud Platform service requests tracking for RESTful APIs.
+ *
+ * @deprecated Use `googleCloudHttpIntegration()` instead.
+ */
+// eslint-disable-next-line deprecation/deprecation
+export const GoogleCloudHttp = convertIntegrationFnToClass(
+ INTEGRATION_NAME,
+ googleCloudHttpIntegration,
+) as IntegrationClass;
- /**
- * @inheritDoc
- */
- public setupOnce(): void {
- try {
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const commonModule = require('@google-cloud/common') as typeof common;
- fill(commonModule.Service.prototype, 'request', wrapRequestFunction);
- } catch (e) {
- if (!this._optional) {
- throw e;
- }
- }
- }
-}
+// eslint-disable-next-line deprecation/deprecation
+export type GoogleCloudHttp = typeof GoogleCloudHttp;
/** Returns a wrapped function that makes a request with tracing enabled */
function wrapRequestFunction(orig: RequestFunction): RequestFunction {
return function (this: common.Service, reqOpts: RequestOptions, callback: ResponseCallback): void {
const httpMethod = reqOpts.method || 'GET';
- const span = startInactiveSpan({
- name: `${httpMethod} ${reqOpts.uri}`,
- op: `http.client.${identifyService(this.apiEndpoint)}`,
- origin: 'auto.http.serverless',
- });
+ const span = SETUP_CLIENTS.has(getClient() as Client)
+ ? startInactiveSpan({
+ name: `${httpMethod} ${reqOpts.uri}`,
+ onlyIfParent: true,
+ op: `http.client.${identifyService(this.apiEndpoint)}`,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless',
+ },
+ })
+ : undefined;
orig.call(this, reqOpts, (...args: Parameters) => {
if (span) {
span.end();
diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts
index 3fef9fb94283..abc135a6b750 100644
--- a/packages/serverless/src/index.ts
+++ b/packages/serverless/src/index.ts
@@ -3,7 +3,8 @@ import * as AWSLambda from './awslambda';
import * as GCPFunction from './gcpfunction';
export { AWSLambda, GCPFunction };
-export { AWSServices } from './awsservices';
+// eslint-disable-next-line deprecation/deprecation
+export { AWSServices, awsServicesIntegration } from './awsservices';
// TODO(v8): We have to explicitly export these because of the namespace exports
// above. This is because just doing `export * from '@sentry/node'` will not
@@ -38,6 +39,9 @@ export {
getIsolationScope,
getHubFromCarrier,
// eslint-disable-next-line deprecation/deprecation
+ spanStatusfromHttpCode,
+ getSpanStatusFromHttpCode,
+ // eslint-disable-next-line deprecation/deprecation
makeMain,
setCurrentClient,
setContext,
@@ -78,4 +82,15 @@ export {
startSpanManual,
continueTrace,
parameterize,
+ requestDataIntegration,
+ linkedErrorsIntegration,
+ inboundFiltersIntegration,
+ functionToStringIntegration,
+ // eslint-disable-next-line deprecation/deprecation
+ getModuleFromFilename,
+ createGetModuleFromFilename,
+ metrics,
+ // eslint-disable-next-line deprecation/deprecation
+ extractTraceparentData,
+ runWithAsyncContext,
} from '@sentry/node';
diff --git a/packages/serverless/test/__mocks__/@sentry/node.ts b/packages/serverless/test/__mocks__/@sentry/node.ts
deleted file mode 100644
index 5181b8a0a535..000000000000
--- a/packages/serverless/test/__mocks__/@sentry/node.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-const origSentry = jest.requireActual('@sentry/node');
-export const defaultIntegrations = origSentry.defaultIntegrations; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
-export const getDefaultIntegrations = origSentry.getDefaultIntegrations; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
-export const Handlers = origSentry.Handlers; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
-export const Integrations = origSentry.Integrations;
-export const addRequestDataToEvent = origSentry.addRequestDataToEvent;
-export const SDK_VERSION = '6.6.6';
-export const Severity = {
- Warning: 'warning',
-};
-export const continueTrace = origSentry.continueTrace;
-
-export const fakeScope = {
- addEventProcessor: jest.fn(),
- setTag: jest.fn(),
- setContext: jest.fn(),
- setSpan: jest.fn(),
- setSDKProcessingMetadata: jest.fn(),
- setPropagationContext: jest.fn(),
-};
-export const fakeSpan = {
- end: jest.fn(),
- setHttpStatus: jest.fn(),
-};
-export const init = jest.fn();
-export const addGlobalEventProcessor = jest.fn();
-export const getCurrentScope = jest.fn(() => fakeScope);
-export const captureException = jest.fn();
-export const captureMessage = jest.fn();
-export const withScope = jest.fn(cb => cb(fakeScope));
-export const flush = jest.fn(() => Promise.resolve());
-export const getClient = jest.fn(() => ({}));
-export const startSpanManual = jest.fn((ctx, callback: (span: any) => any) => callback(fakeSpan));
-export const startInactiveSpan = jest.fn(() => fakeSpan);
-
-export const resetMocks = (): void => {
- fakeSpan.end.mockClear();
- fakeSpan.setHttpStatus.mockClear();
-
- fakeScope.addEventProcessor.mockClear();
- fakeScope.setTag.mockClear();
- fakeScope.setContext.mockClear();
- fakeScope.setSpan.mockClear();
-
- init.mockClear();
- addGlobalEventProcessor.mockClear();
-
- captureException.mockClear();
- captureMessage.mockClear();
- withScope.mockClear();
- flush.mockClear();
- getClient.mockClear();
- startSpanManual.mockClear();
-};
diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts
index 0d923074067f..57feede5a102 100644
--- a/packages/serverless/test/awslambda.test.ts
+++ b/packages/serverless/test/awslambda.test.ts
@@ -1,12 +1,59 @@
-// 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 { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
+
import type { Event } from '@sentry/types';
import type { Callback, Handler } from 'aws-lambda';
-import * as Sentry from '../src';
+import { init, wrapHandler } from '../src/awslambda';
+
+const mockSpanEnd = jest.fn();
+const mockStartInactiveSpan = jest.fn((...spanArgs) => ({ ...spanArgs }));
+const mockStartSpanManual = jest.fn((...spanArgs) => ({ ...spanArgs }));
+const mockFlush = jest.fn((...args) => Promise.resolve(args));
+const mockWithScope = jest.fn();
+const mockCaptureMessage = jest.fn();
+const mockCaptureException = jest.fn();
+const mockInit = jest.fn();
+
+const mockScope = {
+ setTag: jest.fn(),
+ setContext: jest.fn(),
+ addEventProcessor: jest.fn(),
+};
-const { wrapHandler } = Sentry.AWSLambda;
+jest.mock('@sentry/node', () => {
+ const original = jest.requireActual('@sentry/node');
+ return {
+ ...original,
+ init: (options: unknown) => {
+ mockInit(options);
+ },
+ startInactiveSpan: (...args: unknown[]) => {
+ mockStartInactiveSpan(...args);
+ return { end: mockSpanEnd };
+ },
+ startSpanManual: (...args: unknown[]) => {
+ mockStartSpanManual(...args);
+ mockSpanEnd();
+ return original.startSpanManual(...args);
+ },
+ getCurrentScope: () => {
+ return mockScope;
+ },
+ flush: (...args: unknown[]) => {
+ return mockFlush(...args);
+ },
+ withScope: (fn: (scope: unknown) => void) => {
+ mockWithScope(fn);
+ fn(mockScope);
+ },
+ captureMessage: (...args: unknown[]) => {
+ mockCaptureMessage(...args);
+ },
+ captureException: (...args: unknown[]) => {
+ mockCaptureException(...args);
+ },
+ };
+});
// Default `timeoutWarningLimit` is 500ms so leaving some space for it to trigger when necessary
const DEFAULT_EXECUTION_TIME = 100;
@@ -34,21 +81,18 @@ const fakeCallback: Callback = (err, result) => {
};
function expectScopeSettings() {
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeScope.addEventProcessor).toBeCalledTimes(1);
+ expect(mockScope.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 eventProcessor = mockScope.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
- expect(SentryNode.fakeScope.setTag).toBeCalledWith('url', 'awslambda:///functionName');
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeScope.setContext).toBeCalledWith(
+ expect(mockScope.setTag).toBeCalledWith('server_name', expect.anything());
+
+ expect(mockScope.setTag).toBeCalledWith('url', 'awslambda:///functionName');
+
+ expect(mockScope.setContext).toBeCalledWith(
'aws.lambda',
expect.objectContaining({
aws_request_id: 'awsRequestId',
@@ -58,8 +102,8 @@ function expectScopeSettings() {
remaining_time_in_millis: 100,
}),
);
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeScope.setContext).toBeCalledWith(
+
+ expect(mockScope.setContext).toBeCalledWith(
'aws.cloudwatch.logs',
expect.objectContaining({
log_group: 'logGroupName',
@@ -73,11 +117,8 @@ describe('AWSLambda', () => {
fakeEvent = {
fortySix: 'o_O',
};
- });
- afterEach(() => {
- // @ts-expect-error see "Why @ts-expect-error" note
- SentryNode.resetMocks();
+ jest.clearAllMocks();
});
describe('wrapHandler() options', () => {
@@ -88,7 +129,7 @@ describe('AWSLambda', () => {
const wrappedHandler = wrapHandler(handler, { flushTimeout: 1337 });
await wrappedHandler(fakeEvent, fakeContext, fakeCallback);
- expect(SentryNode.flush).toBeCalledWith(1337);
+ expect(mockFlush).toBeCalledWith(1337);
});
test('captureTimeoutWarning enabled (default)', async () => {
@@ -100,10 +141,9 @@ describe('AWSLambda', () => {
const wrappedHandler = wrapHandler(handler);
await wrappedHandler(fakeEvent, fakeContext, fakeCallback);
- expect(Sentry.withScope).toBeCalledTimes(1);
- expect(Sentry.captureMessage).toBeCalled();
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeScope.setTag).toBeCalledWith('timeout', '1s');
+ expect(mockWithScope).toBeCalledTimes(1);
+ expect(mockCaptureMessage).toBeCalled();
+ expect(mockScope.setTag).toBeCalledWith('timeout', '1s');
});
test('captureTimeoutWarning disabled', async () => {
@@ -117,10 +157,9 @@ describe('AWSLambda', () => {
});
await wrappedHandler(fakeEvent, fakeContext, fakeCallback);
- expect(Sentry.withScope).toBeCalledTimes(0);
- expect(Sentry.captureMessage).not.toBeCalled();
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeScope.setTag).not.toBeCalledWith('timeout', '1s');
+ expect(mockWithScope).toBeCalledTimes(0);
+ expect(mockCaptureMessage).not.toBeCalled();
+ expect(mockScope.setTag).not.toBeCalledWith('timeout', '1s');
});
test('captureTimeoutWarning with configured timeoutWarningLimit', async () => {
@@ -149,16 +188,15 @@ describe('AWSLambda', () => {
fakeCallback,
);
- expect(Sentry.captureMessage).toBeCalled();
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeScope.setTag).toBeCalledWith('timeout', '1m40s');
+ expect(mockCaptureMessage).toBeCalled();
+ expect(mockScope.setTag).toBeCalledWith('timeout', '1m40s');
});
test('captureAllSettledReasons disabled (default)', async () => {
const handler = () => Promise.resolve([{ status: 'rejected', reason: new Error() }]);
const wrappedHandler = wrapHandler(handler, { flushTimeout: 1337 });
await wrappedHandler(fakeEvent, fakeContext, fakeCallback);
- expect(SentryNode.captureException).toBeCalledTimes(0);
+ expect(mockCaptureException).toBeCalledTimes(0);
});
test('captureAllSettledReasons enable', async () => {
@@ -172,9 +210,9 @@ describe('AWSLambda', () => {
]);
const wrappedHandler = wrapHandler(handler, { flushTimeout: 1337, captureAllSettledReasons: true });
await wrappedHandler(fakeEvent, fakeContext, fakeCallback);
- expect(SentryNode.captureException).toHaveBeenNthCalledWith(1, error, expect.any(Function));
- expect(SentryNode.captureException).toHaveBeenNthCalledWith(2, error2, expect.any(Function));
- expect(SentryNode.captureException).toBeCalledTimes(2);
+ expect(mockCaptureException).toHaveBeenNthCalledWith(1, error, expect.any(Function));
+ expect(mockCaptureException).toHaveBeenNthCalledWith(2, error2, expect.any(Function));
+ expect(mockCaptureException).toBeCalledTimes(2);
});
// "wrapHandler() ... successful execution" tests the default of startTrace enabled
@@ -185,11 +223,10 @@ describe('AWSLambda', () => {
const wrappedHandler = wrapHandler(handler, { startTrace: false });
await wrappedHandler(fakeEvent, fakeContext, fakeCallback);
- // @ts-expect-error see "Why @ts-expect-error" note
- 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);
+ expect(mockScope.addEventProcessor).toBeCalledTimes(0);
+
+ expect(mockScope.setTag).toBeCalledTimes(0);
+ expect(mockStartSpanManual).toBeCalledTimes(0);
});
});
@@ -206,19 +243,19 @@ describe('AWSLambda', () => {
const fakeTransactionContext = {
name: 'functionName',
op: 'function.aws.lambda',
- origin: 'auto.function.serverless',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless',
},
metadata: {},
};
expect(rv).toStrictEqual(42);
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
expectScopeSettings();
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalledWith(2000);
+
+ expect(mockSpanEnd).toBeCalled();
+ expect(mockFlush).toBeCalledWith(2000);
});
test('unsuccessful execution', async () => {
@@ -236,19 +273,19 @@ describe('AWSLambda', () => {
const fakeTransactionContext = {
name: 'functionName',
op: 'function.aws.lambda',
- origin: 'auto.function.serverless',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless',
},
metadata: {},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
expectScopeSettings();
- expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalledWith(2000);
+ expect(mockCaptureException).toBeCalledWith(error, expect.any(Function));
+
+ expect(mockSpanEnd).toBeCalled();
+ expect(mockFlush).toBeCalledWith(2000);
}
});
@@ -273,16 +310,16 @@ describe('AWSLambda', () => {
};
const handler: Handler = (_event, _context, callback) => {
- expect(SentryNode.startSpanManual).toBeCalledWith(
+ expect(mockStartSpanManual).toBeCalledWith(
expect.objectContaining({
parentSpanId: '1121201211212012',
parentSampled: false,
op: 'function.aws.lambda',
- origin: 'auto.function.serverless',
name: 'functionName',
traceId: '12312012123120121231201212312012',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless',
},
metadata: {
dynamicSamplingContext: {
@@ -316,22 +353,22 @@ describe('AWSLambda', () => {
const fakeTransactionContext = {
name: 'functionName',
op: 'function.aws.lambda',
- origin: 'auto.function.serverless',
traceId: '12312012123120121231201212312012',
parentSpanId: '1121201211212012',
parentSampled: false,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless',
},
metadata: { dynamicSamplingContext: {} },
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
expectScopeSettings();
- expect(SentryNode.captureException).toBeCalledWith(e, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalled();
+ expect(mockCaptureException).toBeCalledWith(e, expect.any(Function));
+
+ expect(mockSpanEnd).toBeCalled();
+ expect(mockFlush).toBeCalled();
}
});
});
@@ -349,19 +386,19 @@ describe('AWSLambda', () => {
const fakeTransactionContext = {
name: 'functionName',
op: 'function.aws.lambda',
- origin: 'auto.function.serverless',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless',
},
metadata: {},
};
expect(rv).toStrictEqual(42);
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
expectScopeSettings();
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalled();
+
+ expect(mockSpanEnd).toBeCalled();
+ expect(mockFlush).toBeCalled();
});
test('event and context are correctly passed to the original handler', async () => {
@@ -390,19 +427,19 @@ describe('AWSLambda', () => {
const fakeTransactionContext = {
name: 'functionName',
op: 'function.aws.lambda',
- origin: 'auto.function.serverless',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless',
},
metadata: {},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
expectScopeSettings();
- expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalled();
+ expect(mockCaptureException).toBeCalledWith(error, expect.any(Function));
+
+ expect(mockSpanEnd).toBeCalled();
+ expect(mockFlush).toBeCalled();
}
});
@@ -414,9 +451,7 @@ describe('AWSLambda', () => {
const wrappedHandler = wrapHandler(handler);
- jest.spyOn(Sentry, 'flush').mockImplementationOnce(async () => {
- throw new Error();
- });
+ mockFlush.mockImplementationOnce(() => Promise.reject(new Error('wat')));
await expect(wrappedHandler(fakeEvent, fakeContext, fakeCallback)).resolves.toBe('some string');
});
@@ -435,19 +470,19 @@ describe('AWSLambda', () => {
const fakeTransactionContext = {
name: 'functionName',
op: 'function.aws.lambda',
- origin: 'auto.function.serverless',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless',
},
metadata: {},
};
expect(rv).toStrictEqual(42);
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
expectScopeSettings();
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalled();
+
+ expect(mockSpanEnd).toBeCalled();
+ expect(mockFlush).toBeCalled();
});
test('event and context are correctly passed to the original handler', async () => {
@@ -476,19 +511,19 @@ describe('AWSLambda', () => {
const fakeTransactionContext = {
name: 'functionName',
op: 'function.aws.lambda',
- origin: 'auto.function.serverless',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless',
},
metadata: {},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
expectScopeSettings();
- expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalled();
+ expect(mockCaptureException).toBeCalledWith(error, expect.any(Function));
+
+ expect(mockSpanEnd).toBeCalled();
+ expect(mockFlush).toBeCalled();
}
});
});
@@ -505,9 +540,9 @@ describe('AWSLambda', () => {
try {
await wrappedHandler(fakeEvent, fakeContext, fakeCallback);
} catch (e) {
- expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- const scopeFunction = SentryNode.captureException.mock.calls[0][1];
+ expect(mockCaptureException).toBeCalledWith(error, expect.any(Function));
+
+ const scopeFunction = mockCaptureException.mock.calls[0][1];
const event: Event = { exception: { values: [{}] } };
let evtProcessor: ((e: Event) => Event) | undefined = undefined;
scopeFunction({ addEventProcessor: jest.fn().mockImplementation(proc => (evtProcessor = proc)) });
@@ -523,9 +558,9 @@ describe('AWSLambda', () => {
describe('init()', () => {
test('calls Sentry.init with correct sdk info metadata', () => {
- Sentry.AWSLambda.init({});
+ init({});
- expect(Sentry.init).toBeCalledWith(
+ expect(mockInit).toBeCalledWith(
expect.objectContaining({
_metadata: {
sdk: {
@@ -534,10 +569,10 @@ describe('AWSLambda', () => {
packages: [
{
name: 'npm:@sentry/serverless',
- version: '6.6.6',
+ version: expect.any(String),
},
],
- version: '6.6.6',
+ version: expect.any(String),
},
},
}),
diff --git a/packages/serverless/test/awsservices.test.ts b/packages/serverless/test/awsservices.test.ts
index 16464e315fa6..3170f9056ec0 100644
--- a/packages/serverless/test/awsservices.test.ts
+++ b/packages/serverless/test/awsservices.test.ts
@@ -1,21 +1,53 @@
-import * as SentryNode from '@sentry/node';
+import { NodeClient, createTransport, setCurrentClient } from '@sentry/node';
import * as AWS from 'aws-sdk';
import * as nock from 'nock';
-import { AWSServices } from '../src/awsservices';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
+import { awsServicesIntegration } from '../src/awsservices';
-describe('AWSServices', () => {
- beforeAll(() => {
- new AWSServices().setupOnce();
+const mockSpanEnd = jest.fn();
+const mockStartInactiveSpan = jest.fn(spanArgs => ({ ...spanArgs }));
+
+jest.mock('@sentry/node', () => {
+ return {
+ ...jest.requireActual('@sentry/node'),
+ startInactiveSpan: (ctx: unknown) => {
+ mockStartInactiveSpan(ctx);
+ return { end: mockSpanEnd };
+ },
+ };
+});
+
+describe('awsServicesIntegration', () => {
+ const mockClient = new NodeClient({
+ tracesSampleRate: 1.0,
+ integrations: [],
+ dsn: 'https://withAWSServices@domain/123',
+ transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})),
+ stackParser: () => [],
});
- afterEach(() => {
- // @ts-expect-error see "Why @ts-expect-error" note
- SentryNode.resetMocks();
+
+ const integration = awsServicesIntegration();
+ mockClient.addIntegration(integration);
+
+ const mockClientWithoutIntegration = new NodeClient({
+ tracesSampleRate: 1.0,
+ integrations: [],
+ dsn: 'https://withoutAWSServices@domain/123',
+ transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})),
+ stackParser: () => [],
});
+
afterAll(() => {
nock.restore();
});
+ beforeEach(() => {
+ setCurrentClient(mockClient);
+ mockSpanEnd.mockClear();
+ mockStartInactiveSpan.mockClear();
+ });
+
describe('S3 tracing', () => {
const s3 = new AWS.S3({ accessKeyId: '-', secretAccessKey: '-' });
@@ -23,13 +55,23 @@ describe('AWSServices', () => {
nock('https://foo.s3.amazonaws.com').get('/bar').reply(200, 'contents');
const data = await s3.getObject({ Bucket: 'foo', Key: 'bar' }).promise();
expect(data.Body?.toString('utf-8')).toEqual('contents');
- expect(SentryNode.startInactiveSpan).toBeCalledWith({
+ expect(mockStartInactiveSpan).toBeCalledWith({
op: 'http.client',
- origin: 'auto.http.serverless',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless',
+ },
name: 'aws.s3.getObject foo',
+ onlyIfParent: true,
});
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
+
+ expect(mockSpanEnd).toHaveBeenCalledTimes(1);
+ });
+
+ test('getObject with integration-less client', async () => {
+ setCurrentClient(mockClientWithoutIntegration);
+ nock('https://foo.s3.amazonaws.com').get('/bar').reply(200, 'contents');
+ await s3.getObject({ Bucket: 'foo', Key: 'bar' }).promise();
+ expect(mockStartInactiveSpan).not.toBeCalled();
});
test('getObject with callback', done => {
@@ -40,11 +82,24 @@ describe('AWSServices', () => {
expect(data.Body?.toString('utf-8')).toEqual('contents');
done();
});
- expect(SentryNode.startInactiveSpan).toBeCalledWith({
+ expect(mockStartInactiveSpan).toBeCalledWith({
op: 'http.client',
- origin: 'auto.http.serverless',
name: 'aws.s3.getObject foo',
+ onlyIfParent: true,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless',
+ },
+ });
+ });
+
+ test('getObject with callback with integration-less client', done => {
+ setCurrentClient(mockClientWithoutIntegration);
+ expect.assertions(1);
+ nock('https://foo.s3.amazonaws.com').get('/bar').reply(200, 'contents');
+ s3.getObject({ Bucket: 'foo', Key: 'bar' }, () => {
+ done();
});
+ expect(mockStartInactiveSpan).not.toBeCalled();
});
});
@@ -55,11 +110,22 @@ describe('AWSServices', () => {
nock('https://lambda.eu-north-1.amazonaws.com').post('/2015-03-31/functions/foo/invocations').reply(201, 'reply');
const data = await lambda.invoke({ FunctionName: 'foo' }).promise();
expect(data.Payload?.toString('utf-8')).toEqual('reply');
- expect(SentryNode.startInactiveSpan).toBeCalledWith({
+ expect(mockStartInactiveSpan).toBeCalledWith({
op: 'http.client',
- origin: 'auto.http.serverless',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless',
+ },
name: 'aws.lambda.invoke foo',
+ onlyIfParent: true,
});
+ expect(mockSpanEnd).toHaveBeenCalledTimes(1);
+ });
+
+ test('invoke with integration-less client', async () => {
+ setCurrentClient(mockClientWithoutIntegration);
+ nock('https://lambda.eu-north-1.amazonaws.com').post('/2015-03-31/functions/foo/invocations').reply(201, 'reply');
+ await lambda.invoke({ FunctionName: 'foo' }).promise();
+ expect(mockStartInactiveSpan).not.toBeCalled();
});
});
});
diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts
index 29cfe0541a0c..1fc58c37fdce 100644
--- a/packages/serverless/test/gcpfunction.test.ts
+++ b/packages/serverless/test/gcpfunction.test.ts
@@ -1,9 +1,9 @@
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 { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
+
import { wrapCloudEventFunction, wrapEventFunction, wrapHttpFunction } from '../src/gcpfunction';
import type {
CloudEventFunction,
@@ -15,10 +15,65 @@ import type {
Response,
} from '../src/gcpfunction/general';
+import { init } from '../src/gcpfunction';
+
+const mockStartInactiveSpan = jest.fn((...spanArgs) => ({ ...spanArgs }));
+const mockStartSpanManual = jest.fn((...spanArgs) => ({ ...spanArgs }));
+const mockFlush = jest.fn((...args) => Promise.resolve(args));
+const mockWithScope = jest.fn();
+const mockCaptureMessage = jest.fn();
+const mockCaptureException = jest.fn();
+const mockInit = jest.fn();
+
+const mockScope = {
+ setTag: jest.fn(),
+ setContext: jest.fn(),
+ addEventProcessor: jest.fn(),
+ setSDKProcessingMetadata: jest.fn(),
+};
+
+const mockSpan = {
+ end: jest.fn(),
+};
+
+jest.mock('@sentry/node', () => {
+ const original = jest.requireActual('@sentry/node');
+ return {
+ ...original,
+ init: (options: unknown) => {
+ mockInit(options);
+ },
+ startInactiveSpan: (...args: unknown[]) => {
+ mockStartInactiveSpan(...args);
+ return mockSpan;
+ },
+ startSpanManual: (...args: unknown[]) => {
+ mockStartSpanManual(...args);
+ mockSpan.end();
+ return original.startSpanManual(...args);
+ },
+ getCurrentScope: () => {
+ return mockScope;
+ },
+ flush: (...args: unknown[]) => {
+ return mockFlush(...args);
+ },
+ withScope: (fn: (scope: unknown) => void) => {
+ mockWithScope(fn);
+ fn(mockScope);
+ },
+ captureMessage: (...args: unknown[]) => {
+ mockCaptureMessage(...args);
+ },
+ captureException: (...args: unknown[]) => {
+ mockCaptureException(...args);
+ },
+ };
+});
+
describe('GCPFunction', () => {
- afterEach(() => {
- // @ts-expect-error see "Why @ts-expect-error" note
- SentryNode.resetMocks();
+ beforeEach(() => {
+ jest.clearAllMocks();
});
async function handleHttp(fn: HttpFunction, trace_headers: { [key: string]: string } | null = null): Promise {
@@ -89,7 +144,7 @@ describe('GCPFunction', () => {
const wrappedHandler = wrapHttpFunction(handler, { flushTimeout: 1337 });
await handleHttp(wrappedHandler);
- expect(SentryNode.flush).toBeCalledWith(1337);
+ expect(mockFlush).toBeCalledWith(1337);
});
});
@@ -105,19 +160,16 @@ describe('GCPFunction', () => {
const fakeTransactionContext = {
name: 'POST /path',
op: 'function.gcp.http',
- origin: 'auto.function.serverless.gcp_http',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http',
},
metadata: {},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.setHttpStatus).toBeCalledWith(200);
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalledWith(2000);
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockSpan.end).toBeCalled();
+ expect(mockFlush).toBeCalledWith(2000);
});
test('incoming trace headers are correctly parsed and used', async () => {
@@ -135,12 +187,12 @@ describe('GCPFunction', () => {
const fakeTransactionContext = {
name: 'POST /path',
op: 'function.gcp.http',
- origin: 'auto.function.serverless.gcp_http',
traceId: '12312012123120121231201212312012',
parentSpanId: '1121201211212012',
parentSampled: false,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http',
},
metadata: {
dynamicSamplingContext: {
@@ -149,7 +201,7 @@ describe('GCPFunction', () => {
},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
});
test('capture error', async () => {
@@ -168,21 +220,20 @@ describe('GCPFunction', () => {
const fakeTransactionContext = {
name: 'POST /path',
op: 'function.gcp.http',
- origin: 'auto.function.serverless.gcp_http',
traceId: '12312012123120121231201212312012',
parentSpanId: '1121201211212012',
parentSampled: false,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http',
},
metadata: { dynamicSamplingContext: {} },
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
- expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalled();
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockCaptureException).toBeCalledWith(error, expect.any(Function));
+ expect(mockSpan.end).toBeCalled();
+ expect(mockFlush).toBeCalled();
});
test('should not throw when flush rejects', async () => {
@@ -203,7 +254,7 @@ describe('GCPFunction', () => {
const mockEnd = jest.fn();
const response = { end: mockEnd } as unknown as Response;
- jest.spyOn(Sentry, 'flush').mockImplementationOnce(async () => {
+ mockFlush.mockImplementationOnce(async () => {
throw new Error();
});
@@ -216,7 +267,7 @@ describe('GCPFunction', () => {
// integration is included in the defaults and the necessary data is stored in `sdkProcessingMetadata`. The
// integration's tests cover testing that it uses that data correctly.
test('wrapHttpFunction request data prereqs', async () => {
- Sentry.GCPFunction.init({});
+ init({});
const handler: HttpFunction = (_req, res) => {
res.end();
@@ -225,13 +276,12 @@ describe('GCPFunction', () => {
await handleHttp(wrappedHandler);
- const initOptions = (SentryNode.init as unknown as jest.SpyInstance).mock.calls[0];
+ const initOptions = (mockInit as unknown as jest.SpyInstance).mock.calls[0];
const defaultIntegrations = initOptions[0].defaultIntegrations.map((i: Integration) => i.name);
expect(defaultIntegrations).toContain('RequestData');
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeScope.setSDKProcessingMetadata).toHaveBeenCalledWith({
+ expect(mockScope.setSDKProcessingMetadata).toHaveBeenCalledWith({
request: {
method: 'POST',
url: '/path?q=query',
@@ -253,16 +303,15 @@ describe('GCPFunction', () => {
const fakeTransactionContext = {
name: 'event.type',
op: 'function.gcp.event',
- origin: 'auto.function.serverless.gcp_event',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event',
},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalledWith(2000);
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockSpan.end).toBeCalled();
+ expect(mockFlush).toBeCalledWith(2000);
});
test('capture error', async () => {
@@ -276,17 +325,16 @@ describe('GCPFunction', () => {
const fakeTransactionContext = {
name: 'event.type',
op: 'function.gcp.event',
- origin: 'auto.function.serverless.gcp_event',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event',
},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
- expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalled();
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockCaptureException).toBeCalledWith(error, expect.any(Function));
+ expect(mockSpan.end).toBeCalled();
+ expect(mockFlush).toBeCalled();
});
});
@@ -304,16 +352,15 @@ describe('GCPFunction', () => {
const fakeTransactionContext = {
name: 'event.type',
op: 'function.gcp.event',
- origin: 'auto.function.serverless.gcp_event',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event',
},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalledWith(2000);
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockSpan.end).toBeCalled();
+ expect(mockFlush).toBeCalledWith(2000);
});
test('capture error', async () => {
@@ -331,17 +378,16 @@ describe('GCPFunction', () => {
const fakeTransactionContext = {
name: 'event.type',
op: 'function.gcp.event',
- origin: 'auto.function.serverless.gcp_event',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event',
},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
- expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalled();
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockCaptureException).toBeCalledWith(error, expect.any(Function));
+ expect(mockSpan.end).toBeCalled();
+ expect(mockFlush).toBeCalled();
});
});
@@ -356,16 +402,15 @@ describe('GCPFunction', () => {
const fakeTransactionContext = {
name: 'event.type',
op: 'function.gcp.event',
- origin: 'auto.function.serverless.gcp_event',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event',
},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalledWith(2000);
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockSpan.end).toBeCalled();
+ expect(mockFlush).toBeCalledWith(2000);
});
test('capture error', async () => {
@@ -379,17 +424,16 @@ describe('GCPFunction', () => {
const fakeTransactionContext = {
name: 'event.type',
op: 'function.gcp.event',
- origin: 'auto.function.serverless.gcp_event',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event',
},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
- expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalled();
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockCaptureException).toBeCalledWith(error, expect.any(Function));
+ expect(mockSpan.end).toBeCalled();
+ expect(mockFlush).toBeCalled();
});
test('capture exception', async () => {
@@ -403,14 +447,14 @@ describe('GCPFunction', () => {
const fakeTransactionContext = {
name: 'event.type',
op: 'function.gcp.event',
- origin: 'auto.function.serverless.gcp_event',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event',
},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
- expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function));
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockCaptureException).toBeCalledWith(error, expect.any(Function));
});
});
@@ -422,10 +466,9 @@ describe('GCPFunction', () => {
const wrappedHandler = wrapEventFunction(handler);
await expect(handleEvent(wrappedHandler)).rejects.toThrowError(error);
- expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function));
+ expect(mockCaptureException).toBeCalledWith(error, expect.any(Function));
- // @ts-expect-error just mocking around...
- const scopeFunction = SentryNode.captureException.mock.calls[0][1];
+ const scopeFunction = mockCaptureException.mock.calls[0][1];
const event: Event = { exception: { values: [{}] } };
let evtProcessor: ((e: Event) => Event) | undefined = undefined;
scopeFunction({ addEventProcessor: jest.fn().mockImplementation(proc => (evtProcessor = proc)) });
@@ -442,8 +485,7 @@ describe('GCPFunction', () => {
const handler: EventFunction = (_data, _context) => 42;
const wrappedHandler = wrapEventFunction(handler);
await handleEvent(wrappedHandler);
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeScope.setContext).toBeCalledWith('gcp.function.context', {
+ expect(mockScope.setContext).toBeCalledWith('gcp.function.context', {
eventType: 'event.type',
resource: 'some.resource',
});
@@ -460,16 +502,15 @@ describe('GCPFunction', () => {
const fakeTransactionContext = {
name: 'event.type',
op: 'function.gcp.cloud_event',
- origin: 'auto.function.serverless.gcp_cloud_event',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event',
},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalledWith(2000);
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockSpan.end).toBeCalled();
+ expect(mockFlush).toBeCalledWith(2000);
});
test('capture error', async () => {
@@ -483,17 +524,16 @@ describe('GCPFunction', () => {
const fakeTransactionContext = {
name: 'event.type',
op: 'function.gcp.cloud_event',
- origin: 'auto.function.serverless.gcp_cloud_event',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event',
},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
- expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalled();
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockCaptureException).toBeCalledWith(error, expect.any(Function));
+ expect(mockSpan.end).toBeCalled();
+ expect(mockFlush).toBeCalled();
});
});
@@ -508,16 +548,15 @@ describe('GCPFunction', () => {
const fakeTransactionContext = {
name: 'event.type',
op: 'function.gcp.cloud_event',
- origin: 'auto.function.serverless.gcp_cloud_event',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event',
},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalledWith(2000);
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockSpan.end).toBeCalled();
+ expect(mockFlush).toBeCalledWith(2000);
});
test('capture error', async () => {
@@ -531,17 +570,16 @@ describe('GCPFunction', () => {
const fakeTransactionContext = {
name: 'event.type',
op: 'function.gcp.cloud_event',
- origin: 'auto.function.serverless.gcp_cloud_event',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event',
},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
- expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function));
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeSpan.end).toBeCalled();
- expect(SentryNode.flush).toBeCalled();
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockCaptureException).toBeCalledWith(error, expect.any(Function));
+ expect(mockSpan.end).toBeCalled();
+ expect(mockFlush).toBeCalled();
});
test('capture exception', async () => {
@@ -555,14 +593,14 @@ describe('GCPFunction', () => {
const fakeTransactionContext = {
name: 'event.type',
op: 'function.gcp.cloud_event',
- origin: 'auto.function.serverless.gcp_cloud_event',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event',
},
};
- expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
- expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function));
+ expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function));
+ expect(mockCaptureException).toBeCalledWith(error, expect.any(Function));
});
});
@@ -570,15 +608,14 @@ describe('GCPFunction', () => {
const handler: CloudEventFunction = _context => 42;
const wrappedHandler = wrapCloudEventFunction(handler);
await handleCloudEvent(wrappedHandler);
- // @ts-expect-error see "Why @ts-expect-error" note
- expect(SentryNode.fakeScope.setContext).toBeCalledWith('gcp.function.context', { type: 'event.type' });
+ expect(mockScope.setContext).toBeCalledWith('gcp.function.context', { type: 'event.type' });
});
describe('init()', () => {
test('calls Sentry.init with correct sdk info metadata', () => {
- Sentry.GCPFunction.init({});
+ init({});
- expect(Sentry.init).toBeCalledWith(
+ expect(mockInit).toBeCalledWith(
expect.objectContaining({
_metadata: {
sdk: {
@@ -587,10 +624,10 @@ describe('GCPFunction', () => {
packages: [
{
name: 'npm:@sentry/serverless',
- version: '6.6.6',
+ version: expect.any(String),
},
],
- version: '6.6.6',
+ version: expect.any(String),
},
},
}),
diff --git a/packages/serverless/test/google-cloud-grpc.test.ts b/packages/serverless/test/google-cloud-grpc.test.ts
index 39ebb4a54ecd..0ce19ee7db8b 100644
--- a/packages/serverless/test/google-cloud-grpc.test.ts
+++ b/packages/serverless/test/google-cloud-grpc.test.ts
@@ -5,14 +5,28 @@ import { EventEmitter } from 'events';
import * as fs from 'fs';
import * as path from 'path';
import { PubSub } from '@google-cloud/pubsub';
-import * as SentryNode from '@sentry/node';
import * as http2 from 'http2';
import * as nock from 'nock';
-import { GoogleCloudGrpc } from '../src/google-cloud-grpc';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
+import { NodeClient, createTransport, setCurrentClient } from '@sentry/node';
+import { googleCloudGrpcIntegration } from '../src/google-cloud-grpc';
const spyConnect = jest.spyOn(http2, 'connect');
+const mockSpanEnd = jest.fn();
+const mockStartInactiveSpan = jest.fn(spanArgs => ({ ...spanArgs }));
+
+jest.mock('@sentry/node', () => {
+ return {
+ ...jest.requireActual('@sentry/node'),
+ startInactiveSpan: (ctx: unknown) => {
+ mockStartInactiveSpan(ctx);
+ return { end: mockSpanEnd };
+ },
+ };
+});
+
/** Fake HTTP2 stream */
class FakeStream extends EventEmitter {
public rstCode: number = 0;
@@ -70,18 +84,24 @@ function mockHttp2Session(): FakeSession {
}
describe('GoogleCloudGrpc tracing', () => {
- beforeAll(() => {
- new GoogleCloudGrpc().setupOnce();
+ const mockClient = new NodeClient({
+ tracesSampleRate: 1.0,
+ integrations: [],
+ dsn: 'https://withAWSServices@domain/123',
+ transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})),
+ stackParser: () => [],
});
+ const integration = googleCloudGrpcIntegration();
+ mockClient.addIntegration(integration);
+
beforeEach(() => {
nock('https://www.googleapis.com').post('/oauth2/v4/token').reply(200, []);
+ setCurrentClient(mockClient);
+ mockSpanEnd.mockClear();
+ mockStartInactiveSpan.mockClear();
});
- afterEach(() => {
- // @ts-expect-error see "Why @ts-expect-error" note
- SentryNode.resetMocks();
- spyConnect.mockClear();
- });
+
afterAll(() => {
nock.restore();
spyConnect.mockRestore();
@@ -115,16 +135,22 @@ describe('GoogleCloudGrpc tracing', () => {
resolveTxt.mockReset();
});
+ afterAll(async () => {
+ await pubsub.close();
+ });
+
test('publish', async () => {
mockHttp2Session().mockUnaryRequest(Buffer.from('00000000120a1031363337303834313536363233383630', 'hex'));
const resp = await pubsub.topic('nicetopic').publish(Buffer.from('data'));
expect(resp).toEqual('1637084156623860');
- expect(SentryNode.startInactiveSpan).toBeCalledWith({
+ expect(mockStartInactiveSpan).toBeCalledWith({
op: 'grpc.pubsub',
- origin: 'auto.grpc.serverless',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.grpc.serverless',
+ },
name: 'unary call publish',
+ onlyIfParent: true,
});
- await pubsub.close();
});
});
});
diff --git a/packages/serverless/test/google-cloud-http.test.ts b/packages/serverless/test/google-cloud-http.test.ts
index 0ef1466647a5..3389130565f9 100644
--- a/packages/serverless/test/google-cloud-http.test.ts
+++ b/packages/serverless/test/google-cloud-http.test.ts
@@ -1,24 +1,46 @@
import * as fs from 'fs';
import * as path from 'path';
import { BigQuery } from '@google-cloud/bigquery';
-import * as SentryNode from '@sentry/node';
import * as nock from 'nock';
-import { GoogleCloudHttp } from '../src/google-cloud-http';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
+import { NodeClient, createTransport, setCurrentClient } from '@sentry/node';
+import { googleCloudHttpIntegration } from '../src/google-cloud-http';
+
+const mockSpanEnd = jest.fn();
+const mockStartInactiveSpan = jest.fn(spanArgs => ({ ...spanArgs }));
+
+jest.mock('@sentry/node', () => {
+ return {
+ ...jest.requireActual('@sentry/node'),
+ startInactiveSpan: (ctx: unknown) => {
+ mockStartInactiveSpan(ctx);
+ return { end: mockSpanEnd };
+ },
+ };
+});
describe('GoogleCloudHttp tracing', () => {
- beforeAll(() => {
- new GoogleCloudHttp().setupOnce();
+ const mockClient = new NodeClient({
+ tracesSampleRate: 1.0,
+ integrations: [],
+ dsn: 'https://withAWSServices@domain/123',
+ transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})),
+ stackParser: () => [],
});
+
+ const integration = googleCloudHttpIntegration();
+ mockClient.addIntegration(integration);
+
beforeEach(() => {
nock('https://www.googleapis.com')
.post('/oauth2/v4/token')
.reply(200, '{"access_token":"a.b.c","expires_in":3599,"token_type":"Bearer"}');
+ setCurrentClient(mockClient);
+ mockSpanEnd.mockClear();
+ mockStartInactiveSpan.mockClear();
});
- afterEach(() => {
- // @ts-expect-error see "Why @ts-expect-error" note
- SentryNode.resetMocks();
- });
+
afterAll(() => {
nock.restore();
});
@@ -50,15 +72,21 @@ describe('GoogleCloudHttp tracing', () => {
);
const resp = await bigquery.query('SELECT true AS foo');
expect(resp).toEqual([[{ foo: true }]]);
- expect(SentryNode.startInactiveSpan).toBeCalledWith({
+ expect(mockStartInactiveSpan).toBeCalledWith({
op: 'http.client.bigquery',
- origin: 'auto.http.serverless',
name: 'POST /jobs',
+ onlyIfParent: true,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless',
+ },
});
- expect(SentryNode.startInactiveSpan).toBeCalledWith({
+ expect(mockStartInactiveSpan).toBeCalledWith({
op: 'http.client.bigquery',
- origin: 'auto.http.serverless',
name: expect.stringMatching(/^GET \/queries\/.+/),
+ onlyIfParent: true,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless',
+ },
});
});
});
diff --git a/packages/sveltekit/src/client/load.ts b/packages/sveltekit/src/client/load.ts
index 545105e49c43..14528959d34e 100644
--- a/packages/sveltekit/src/client/load.ts
+++ b/packages/sveltekit/src/client/load.ts
@@ -1,4 +1,4 @@
-import { handleCallbackErrors, startSpan } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, handleCallbackErrors, startSpan } from '@sentry/core';
import { captureException } from '@sentry/svelte';
import { addNonEnumerableProperty, objectify } from '@sentry/utils';
import type { LoadEvent } from '@sveltejs/kit';
@@ -80,7 +80,9 @@ export function wrapLoadWithSentry any>(origLoad: T)
return startSpan(
{
op: 'function.sveltekit.load',
- origin: 'auto.function.sveltekit',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
+ },
name: routeId ? routeId : event.url.pathname,
status: 'ok',
metadata: {
diff --git a/packages/sveltekit/src/client/sdk.ts b/packages/sveltekit/src/client/sdk.ts
index 7b9c608a862d..920b2db75193 100644
--- a/packages/sveltekit/src/client/sdk.ts
+++ b/packages/sveltekit/src/client/sdk.ts
@@ -1,5 +1,5 @@
import { applySdkMetadata, hasTracingEnabled } from '@sentry/core';
-import type { BrowserOptions } from '@sentry/svelte';
+import type { BrowserOptions, browserTracingIntegration } from '@sentry/svelte';
import { getDefaultIntegrations as getDefaultSvelteIntegrations } from '@sentry/svelte';
import { WINDOW, getCurrentScope, init as initSvelteSdk } from '@sentry/svelte';
import type { Integration } from '@sentry/types';
@@ -61,11 +61,28 @@ function fixBrowserTracingIntegration(options: BrowserOptions): void {
}
}
+function isNewBrowserTracingIntegration(
+ integration: Integration,
+): integration is Integration & { options?: Parameters[0] } {
+ return !!integration.afterAllSetup && !!(integration as BrowserTracing).options;
+}
+
function maybeUpdateBrowserTracingIntegration(integrations: Integration[]): Integration[] {
const browserTracing = integrations.find(integration => integration.name === 'BrowserTracing');
+
+ if (!browserTracing) {
+ return integrations;
+ }
+
+ // If `browserTracingIntegration()` was added, we need to force-convert it to our custom one
+ if (isNewBrowserTracingIntegration(browserTracing)) {
+ const { options } = browserTracing;
+ integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options);
+ }
+
// If BrowserTracing was added, but it is not our forked version,
// replace it with our forked version with the same options
- if (browserTracing && !(browserTracing instanceof BrowserTracing)) {
+ if (!(browserTracing instanceof BrowserTracing)) {
const options: ConstructorParameters[0] = (browserTracing as BrowserTracing).options;
// This option is overwritten by the custom integration
delete options.routingInstrumentation;
diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts
index 1bb0c485168e..42f95c905ca5 100644
--- a/packages/sveltekit/src/server/handle.ts
+++ b/packages/sveltekit/src/server/handle.ts
@@ -1,4 +1,11 @@
-import { getActiveSpan, getCurrentScope, getDynamicSamplingContextFromSpan, spanToTraceHeader } from '@sentry/core';
+import {
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ getActiveSpan,
+ getCurrentScope,
+ getDynamicSamplingContextFromSpan,
+ setHttpStatus,
+ spanToTraceHeader,
+} from '@sentry/core';
import { getActiveTransaction, runWithAsyncContext, startSpan } from '@sentry/core';
import { captureException } from '@sentry/node';
/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
@@ -169,7 +176,9 @@ async function instrumentHandle(
const resolveResult = await startSpan(
{
op: 'http.server',
- origin: 'auto.http.sveltekit',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
+ },
name: `${event.request.method} ${event.route?.id || event.url.pathname}`,
status: 'ok',
...traceparentData,
@@ -183,7 +192,7 @@ async function instrumentHandle(
transformPageChunk: addSentryCodeToPage(options),
});
if (span) {
- span.setHttpStatus(res.status);
+ setHttpStatus(span, res.status);
}
return res;
},
diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts
index 7d523ee55fd9..32fcec426df4 100644
--- a/packages/sveltekit/src/server/index.ts
+++ b/packages/sveltekit/src/server/index.ts
@@ -30,6 +30,7 @@ export {
getGlobalScope,
getIsolationScope,
Hub,
+ NodeClient,
// eslint-disable-next-line deprecation/deprecation
makeMain,
setCurrentClient,
@@ -43,7 +44,9 @@ export {
setTag,
setTags,
setUser,
+ // eslint-disable-next-line deprecation/deprecation
spanStatusfromHttpCode,
+ getSpanStatusFromHttpCode,
// eslint-disable-next-line deprecation/deprecation
trace,
withScope,
@@ -76,6 +79,18 @@ export {
continueTrace,
cron,
parameterize,
+ // eslint-disable-next-line deprecation/deprecation
+ getModuleFromFilename,
+ createGetModuleFromFilename,
+ functionToStringIntegration,
+ hapiErrorPlugin,
+ inboundFiltersIntegration,
+ linkedErrorsIntegration,
+ requestDataIntegration,
+ metrics,
+ runWithAsyncContext,
+ // eslint-disable-next-line deprecation/deprecation
+ enableAnrDetection,
} from '@sentry/node';
// We can still leave this for the carrier init and type exports
diff --git a/packages/sveltekit/src/server/load.ts b/packages/sveltekit/src/server/load.ts
index 5d0cd3c1cb90..9728dcf47b5b 100644
--- a/packages/sveltekit/src/server/load.ts
+++ b/packages/sveltekit/src/server/load.ts
@@ -1,10 +1,10 @@
/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
-import { getCurrentScope, startSpan } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getCurrentScope, startSpan } from '@sentry/core';
import { captureException } from '@sentry/node';
-import type { TransactionContext } from '@sentry/types';
import { addNonEnumerableProperty, objectify } from '@sentry/utils';
import type { LoadEvent, ServerLoadEvent } from '@sveltejs/kit';
+import type { TransactionContext } from '@sentry/types';
import type { SentryWrappedFlag } from '../common/utils';
import { isHttpError, isRedirect } from '../common/utils';
import { flushIfServerless, getTracePropagationData } from './utils';
@@ -67,7 +67,9 @@ export function wrapLoadWithSentry any>(origLoad: T)
const traceLoadContext: TransactionContext = {
op: 'function.sveltekit.load',
- origin: 'auto.function.sveltekit',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
+ },
name: routeId ? routeId : event.url.pathname,
status: 'ok',
metadata: {
@@ -134,7 +136,9 @@ export function wrapServerLoadWithSentry any>(origSe
const traceLoadContext: TransactionContext = {
op: 'function.sveltekit.server.load',
- origin: 'auto.function.sveltekit',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
+ },
name: routeId ? routeId : event.url.pathname,
status: 'ok',
metadata: {
diff --git a/packages/sveltekit/test/client/load.test.ts b/packages/sveltekit/test/client/load.test.ts
index 15d2850b4a8c..e839b5a9cba5 100644
--- a/packages/sveltekit/test/client/load.test.ts
+++ b/packages/sveltekit/test/client/load.test.ts
@@ -3,6 +3,7 @@ import type { Load } from '@sveltejs/kit';
import { redirect } from '@sveltejs/kit';
import { vi } from 'vitest';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import { wrapLoadWithSentry } from '../../src/client/load';
const mockCaptureException = vi.spyOn(SentrySvelte, 'captureException').mockImplementation(() => 'xx');
@@ -82,8 +83,10 @@ describe('wrapLoadWithSentry', () => {
expect(mockStartSpan).toHaveBeenCalledTimes(1);
expect(mockStartSpan).toHaveBeenCalledWith(
{
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
+ },
op: 'function.sveltekit.load',
- origin: 'auto.function.sveltekit',
name: '/users/[id]',
status: 'ok',
metadata: {
@@ -110,8 +113,10 @@ describe('wrapLoadWithSentry', () => {
expect(mockStartSpan).toHaveBeenCalledTimes(1);
expect(mockStartSpan).toHaveBeenCalledWith(
{
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
+ },
op: 'function.sveltekit.load',
- origin: 'auto.function.sveltekit',
name: '/users/123',
status: 'ok',
metadata: {
diff --git a/packages/sveltekit/test/client/sdk.test.ts b/packages/sveltekit/test/client/sdk.test.ts
index 10292658bc54..4b0afb85bcd8 100644
--- a/packages/sveltekit/test/client/sdk.test.ts
+++ b/packages/sveltekit/test/client/sdk.test.ts
@@ -1,7 +1,7 @@
import { getClient, getCurrentScope } from '@sentry/core';
import type { BrowserClient } from '@sentry/svelte';
import * as SentrySvelte from '@sentry/svelte';
-import { SDK_VERSION, WINDOW } from '@sentry/svelte';
+import { SDK_VERSION, WINDOW, browserTracingIntegration } from '@sentry/svelte';
import { vi } from 'vitest';
import { BrowserTracing, init } from '../../src/client';
@@ -100,7 +100,26 @@ describe('Sentry client SDK', () => {
it('Merges a user-provided BrowserTracing integration with the automatically added one', () => {
init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
- integrations: [new BrowserTracing({ finalTimeout: 10, startTransactionOnLocationChange: false })],
+ integrations: [new BrowserTracing({ finalTimeout: 10 })],
+ enableTracing: true,
+ });
+
+ const browserTracing = getClient()?.getIntegrationByName('BrowserTracing') as BrowserTracing;
+ const options = browserTracing.options;
+
+ expect(browserTracing).toBeDefined();
+
+ // This shows that the user-configured options are still here
+ expect(options.finalTimeout).toEqual(10);
+
+ // But we force the routing instrumentation to be ours
+ expect(options.routingInstrumentation).toEqual(svelteKitRoutingInstrumentation);
+ });
+
+ it('Merges a user-provided browserTracingIntegration with the automatically added one', () => {
+ init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [browserTracingIntegration({ finalTimeout: 10 })],
enableTracing: true,
});
diff --git a/packages/sveltekit/test/server/load.test.ts b/packages/sveltekit/test/server/load.test.ts
index 6b86ca6b32f6..2656b22f685a 100644
--- a/packages/sveltekit/test/server/load.test.ts
+++ b/packages/sveltekit/test/server/load.test.ts
@@ -1,4 +1,4 @@
-import { addTracingExtensions } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, addTracingExtensions } from '@sentry/core';
import * as SentryNode from '@sentry/node';
import type { Load, ServerLoad } from '@sveltejs/kit';
import { error, redirect } from '@sveltejs/kit';
@@ -197,8 +197,10 @@ describe('wrapLoadWithSentry calls trace', () => {
expect(mockStartSpan).toHaveBeenCalledTimes(1);
expect(mockStartSpan).toHaveBeenCalledWith(
{
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
+ },
op: 'function.sveltekit.load',
- origin: 'auto.function.sveltekit',
name: '/users/[id]',
status: 'ok',
metadata: {
@@ -216,8 +218,10 @@ describe('wrapLoadWithSentry calls trace', () => {
expect(mockStartSpan).toHaveBeenCalledTimes(1);
expect(mockStartSpan).toHaveBeenCalledWith(
{
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
+ },
op: 'function.sveltekit.load',
- origin: 'auto.function.sveltekit',
name: '/users/123',
status: 'ok',
metadata: {
@@ -250,8 +254,10 @@ describe('wrapServerLoadWithSentry calls trace', () => {
expect(mockStartSpan).toHaveBeenCalledTimes(1);
expect(mockStartSpan).toHaveBeenCalledWith(
{
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
+ },
op: 'function.sveltekit.server.load',
- origin: 'auto.function.sveltekit',
name: '/users/[id]',
parentSampled: true,
parentSpanId: '1234567890abcdef',
@@ -284,8 +290,10 @@ describe('wrapServerLoadWithSentry calls trace', () => {
expect(mockStartSpan).toHaveBeenCalledTimes(1);
expect(mockStartSpan).toHaveBeenCalledWith(
{
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
+ },
op: 'function.sveltekit.server.load',
- origin: 'auto.function.sveltekit',
name: '/users/[id]',
status: 'ok',
data: {
@@ -306,8 +314,10 @@ describe('wrapServerLoadWithSentry calls trace', () => {
expect(mockStartSpan).toHaveBeenCalledTimes(1);
expect(mockStartSpan).toHaveBeenCalledWith(
{
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
+ },
op: 'function.sveltekit.server.load',
- origin: 'auto.function.sveltekit',
name: '/users/[id]',
parentSampled: true,
parentSpanId: '1234567890abcdef',
@@ -335,8 +345,10 @@ describe('wrapServerLoadWithSentry calls trace', () => {
expect(mockStartSpan).toHaveBeenCalledTimes(1);
expect(mockStartSpan).toHaveBeenCalledWith(
{
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
+ },
op: 'function.sveltekit.server.load',
- origin: 'auto.function.sveltekit',
name: '/users/123',
parentSampled: true,
parentSpanId: '1234567890abcdef',
diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts
new file mode 100644
index 000000000000..34fe4a7b13d2
--- /dev/null
+++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts
@@ -0,0 +1,496 @@
+/* eslint-disable max-lines, complexity */
+import type { IdleTransaction } from '@sentry/core';
+import { getCurrentHub } from '@sentry/core';
+import {
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+ TRACING_DEFAULTS,
+ addTracingExtensions,
+ getActiveTransaction,
+ spanToJSON,
+ startIdleTransaction,
+} from '@sentry/core';
+import type {
+ Client,
+ IntegrationFn,
+ StartSpanOptions,
+ Transaction,
+ TransactionContext,
+ TransactionSource,
+} from '@sentry/types';
+import type { Span } from '@sentry/types';
+import {
+ addHistoryInstrumentationHandler,
+ browserPerformanceTimeOrigin,
+ getDomElement,
+ logger,
+ tracingContextFromHeaders,
+} from '@sentry/utils';
+
+import { DEBUG_BUILD } from '../common/debug-build';
+import { registerBackgroundTabDetection } from './backgroundtab';
+import {
+ addPerformanceEntries,
+ startTrackingInteractions,
+ startTrackingLongTasks,
+ startTrackingWebVitals,
+} from './metrics';
+import type { RequestInstrumentationOptions } from './request';
+import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request';
+import { WINDOW } from './types';
+
+export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing';
+
+/** Options for Browser Tracing integration */
+export interface BrowserTracingOptions extends RequestInstrumentationOptions {
+ /**
+ * The time to wait in ms until the transaction will be finished during an idle state. An idle state is defined
+ * by a moment where there are no in-progress spans.
+ *
+ * The transaction will use the end timestamp of the last finished span as the endtime for the transaction.
+ * If there are still active spans when this the `idleTimeout` is set, the `idleTimeout` will get reset.
+ * Time is in ms.
+ *
+ * Default: 1000
+ */
+ idleTimeout: number;
+
+ /**
+ * The max duration for a transaction. If a transaction duration hits the `finalTimeout` value, it
+ * will be finished.
+ * Time is in ms.
+ *
+ * Default: 30000
+ */
+ finalTimeout: number;
+
+ /**
+ * The heartbeat interval. If no new spans are started or open spans are finished within 3 heartbeats,
+ * the transaction will be finished.
+ * Time is in ms.
+ *
+ * Default: 5000
+ */
+ heartbeatInterval: number;
+
+ /**
+ * If a span should be created on page load.
+ * If this is set to `false`, this integration will not start the default page load span.
+ * Default: true
+ */
+ instrumentPageLoad: boolean;
+
+ /**
+ * If a span should be created on navigation (history change).
+ * If this is set to `false`, this integration will not start the default navigation spans.
+ * Default: true
+ */
+ instrumentNavigation: boolean;
+
+ /**
+ * Flag spans where tabs moved to background with "cancelled". Browser background tab timing is
+ * not suited towards doing precise measurements of operations. By default, we recommend that this option
+ * be enabled as background transactions can mess up your statistics in nondeterministic ways.
+ *
+ * Default: true
+ */
+ markBackgroundSpan: boolean;
+
+ /**
+ * If true, Sentry will capture long tasks and add them to the corresponding transaction.
+ *
+ * Default: true
+ */
+ enableLongTask: boolean;
+
+ /**
+ * _metricOptions allows the user to send options to change how metrics are collected.
+ *
+ * _metricOptions is currently experimental.
+ *
+ * Default: undefined
+ */
+ _metricOptions?: Partial<{
+ /**
+ * @deprecated This property no longer has any effect and will be removed in v8.
+ */
+ _reportAllChanges: boolean;
+ }>;
+
+ /**
+ * _experiments allows the user to send options to define how this integration works.
+ * Note that the `enableLongTask` options is deprecated in favor of the option at the top level, and will be removed in v8.
+ *
+ * TODO (v8): Remove enableLongTask
+ *
+ * Default: undefined
+ */
+ _experiments: Partial<{
+ enableInteractions: boolean;
+ }>;
+
+ /**
+ * A callback which is called before a span for a pageload or navigation is started.
+ * It receives the options passed to `startSpan`, and expects to return an updated options object.
+ */
+ beforeStartSpan?: (options: StartSpanOptions) => StartSpanOptions;
+}
+
+const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {
+ ...TRACING_DEFAULTS,
+ instrumentNavigation: true,
+ instrumentPageLoad: true,
+ markBackgroundSpan: true,
+ enableLongTask: true,
+ _experiments: {},
+ ...defaultRequestInstrumentationOptions,
+};
+
+/**
+ * The Browser Tracing integration automatically instruments browser pageload/navigation
+ * actions as transactions, and captures requests, metrics and errors as spans.
+ *
+ * The integration can be configured with a variety of options, and can be extended to use
+ * any routing library. This integration uses {@see IdleTransaction} to create transactions.
+ *
+ * We explicitly export the proper type here, as this has to be extended in some cases.
+ */
+export const browserTracingIntegration = ((_options: Partial = {}) => {
+ const _hasSetTracePropagationTargets = DEBUG_BUILD
+ ? !!(
+ // eslint-disable-next-line deprecation/deprecation
+ (_options.tracePropagationTargets || _options.tracingOrigins)
+ )
+ : false;
+
+ addTracingExtensions();
+
+ // TODO (v8): remove this block after tracingOrigins is removed
+ // Set tracePropagationTargets to tracingOrigins if specified by the user
+ // In case both are specified, tracePropagationTargets takes precedence
+ // eslint-disable-next-line deprecation/deprecation
+ if (!_options.tracePropagationTargets && _options.tracingOrigins) {
+ // eslint-disable-next-line deprecation/deprecation
+ _options.tracePropagationTargets = _options.tracingOrigins;
+ }
+
+ const options = {
+ ...DEFAULT_BROWSER_TRACING_OPTIONS,
+ ..._options,
+ };
+
+ const _collectWebVitals = startTrackingWebVitals();
+
+ if (options.enableLongTask) {
+ startTrackingLongTasks();
+ }
+ if (options._experiments.enableInteractions) {
+ startTrackingInteractions();
+ }
+
+ let latestRouteName: string | undefined;
+ let latestRouteSource: TransactionSource | undefined;
+
+ /** Create routing idle transaction. */
+ function _createRouteTransaction(context: TransactionContext): Transaction | undefined {
+ // eslint-disable-next-line deprecation/deprecation
+ const hub = getCurrentHub();
+
+ const { beforeStartSpan, idleTimeout, finalTimeout, heartbeatInterval } = options;
+
+ const isPageloadTransaction = context.op === 'pageload';
+
+ let expandedContext: TransactionContext;
+ if (isPageloadTransaction) {
+ const sentryTrace = isPageloadTransaction ? getMetaContent('sentry-trace') : '';
+ const baggage = isPageloadTransaction ? getMetaContent('baggage') : undefined;
+ const { traceparentData, dynamicSamplingContext } = tracingContextFromHeaders(sentryTrace, baggage);
+ expandedContext = {
+ ...context,
+ ...traceparentData,
+ metadata: {
+ // eslint-disable-next-line deprecation/deprecation
+ ...context.metadata,
+ dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
+ },
+ trimEnd: true,
+ };
+ } else {
+ expandedContext = {
+ ...context,
+ trimEnd: true,
+ };
+ }
+
+ const finalContext = beforeStartSpan ? beforeStartSpan(expandedContext) : expandedContext;
+
+ // If `beforeStartSpan` set a custom name, record that fact
+ // eslint-disable-next-line deprecation/deprecation
+ finalContext.metadata =
+ finalContext.name !== expandedContext.name
+ ? // eslint-disable-next-line deprecation/deprecation
+ { ...finalContext.metadata, source: 'custom' }
+ : // eslint-disable-next-line deprecation/deprecation
+ finalContext.metadata;
+
+ latestRouteName = finalContext.name;
+ latestRouteSource = getSource(finalContext);
+
+ // eslint-disable-next-line deprecation/deprecation
+ if (finalContext.sampled === false) {
+ DEBUG_BUILD && logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`);
+ }
+
+ DEBUG_BUILD && logger.log(`[Tracing] Starting ${finalContext.op} transaction on scope`);
+
+ const { location } = WINDOW;
+
+ const idleTransaction = startIdleTransaction(
+ hub,
+ finalContext,
+ idleTimeout,
+ finalTimeout,
+ true,
+ { location }, // for use in the tracesSampler
+ heartbeatInterval,
+ isPageloadTransaction, // should wait for finish signal if it's a pageload transaction
+ );
+
+ if (isPageloadTransaction) {
+ WINDOW.document.addEventListener('readystatechange', () => {
+ if (['interactive', 'complete'].includes(WINDOW.document.readyState)) {
+ idleTransaction.sendAutoFinishSignal();
+ }
+ });
+
+ if (['interactive', 'complete'].includes(WINDOW.document.readyState)) {
+ idleTransaction.sendAutoFinishSignal();
+ }
+ }
+
+ idleTransaction.registerBeforeFinishCallback(transaction => {
+ _collectWebVitals();
+ addPerformanceEntries(transaction);
+ });
+
+ return idleTransaction as Transaction;
+ }
+
+ return {
+ name: BROWSER_TRACING_INTEGRATION_ID,
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ setupOnce: () => {},
+ afterAllSetup(client) {
+ const clientOptions = client.getOptions();
+
+ const { markBackgroundSpan, traceFetch, traceXHR, shouldCreateSpanForRequest, enableHTTPTimings, _experiments } =
+ options;
+
+ const clientOptionsTracePropagationTargets = clientOptions && clientOptions.tracePropagationTargets;
+ // There are three ways to configure tracePropagationTargets:
+ // 1. via top level client option `tracePropagationTargets`
+ // 2. via BrowserTracing option `tracePropagationTargets`
+ // 3. via BrowserTracing option `tracingOrigins` (deprecated)
+ //
+ // To avoid confusion, favour top level client option `tracePropagationTargets`, and fallback to
+ // BrowserTracing option `tracePropagationTargets` and then `tracingOrigins` (deprecated).
+ // This is done as it minimizes bundle size (we don't have to have undefined checks).
+ //
+ // If both 1 and either one of 2 or 3 are set (from above), we log out a warning.
+ // eslint-disable-next-line deprecation/deprecation
+ const tracePropagationTargets = clientOptionsTracePropagationTargets || options.tracePropagationTargets;
+ if (DEBUG_BUILD && _hasSetTracePropagationTargets && clientOptionsTracePropagationTargets) {
+ logger.warn(
+ '[Tracing] The `tracePropagationTargets` option was set in the BrowserTracing integration and top level `Sentry.init`. The top level `Sentry.init` value is being used.',
+ );
+ }
+
+ let activeSpan: Span | undefined;
+ let startingUrl: string | undefined = WINDOW.location.href;
+
+ if (client.on) {
+ client.on('startNavigationSpan', (context: StartSpanOptions) => {
+ if (activeSpan) {
+ DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${spanToJSON(activeSpan).op}`);
+ // If there's an open transaction on the scope, we need to finish it before creating an new one.
+ activeSpan.end();
+ }
+ activeSpan = _createRouteTransaction(context);
+ });
+
+ client.on('startPageLoadSpan', (context: StartSpanOptions) => {
+ if (activeSpan) {
+ DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${spanToJSON(activeSpan).op}`);
+ // If there's an open transaction on the scope, we need to finish it before creating an new one.
+ activeSpan.end();
+ }
+ activeSpan = _createRouteTransaction(context);
+ });
+ }
+
+ if (options.instrumentPageLoad && client.emit) {
+ const context: StartSpanOptions = {
+ name: WINDOW.location.pathname,
+ // pageload should always start at timeOrigin (and needs to be in s, not ms)
+ startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,
+ op: 'pageload',
+ origin: 'auto.pageload.browser',
+ metadata: { source: 'url' },
+ };
+ startBrowserTracingPageLoadSpan(client, context);
+ }
+
+ if (options.instrumentNavigation && client.emit) {
+ addHistoryInstrumentationHandler(({ to, from }) => {
+ /**
+ * This early return is there to account for some cases where a navigation transaction starts right after
+ * long-running pageload. We make sure that if `from` is undefined and a valid `startingURL` exists, we don't
+ * create an uneccessary navigation transaction.
+ *
+ * This was hard to duplicate, but this behavior stopped as soon as this fix was applied. This issue might also
+ * only be caused in certain development environments where the usage of a hot module reloader is causing
+ * errors.
+ */
+ if (from === undefined && startingUrl && startingUrl.indexOf(to) !== -1) {
+ startingUrl = undefined;
+ return;
+ }
+
+ if (from !== to) {
+ startingUrl = undefined;
+ const context: StartSpanOptions = {
+ name: WINDOW.location.pathname,
+ op: 'navigation',
+ origin: 'auto.navigation.browser',
+ metadata: { source: 'url' },
+ };
+
+ startBrowserTracingNavigationSpan(client, context);
+ }
+ });
+ }
+
+ if (markBackgroundSpan) {
+ registerBackgroundTabDetection();
+ }
+
+ if (_experiments.enableInteractions) {
+ registerInteractionListener(options, latestRouteName, latestRouteSource);
+ }
+
+ instrumentOutgoingRequests({
+ traceFetch,
+ traceXHR,
+ tracePropagationTargets,
+ shouldCreateSpanForRequest,
+ enableHTTPTimings,
+ });
+ },
+ // TODO v8: Remove this again
+ // This is private API that we use to fix converted BrowserTracing integrations in Next.js & SvelteKit
+ options,
+ };
+}) satisfies IntegrationFn;
+
+/**
+ * Manually start a page load span.
+ * This will only do something if the BrowserTracing integration has been setup.
+ */
+export function startBrowserTracingPageLoadSpan(client: Client, spanOptions: StartSpanOptions): void {
+ if (!client.emit) {
+ return;
+ }
+
+ client.emit('startPageLoadSpan', spanOptions);
+}
+
+/**
+ * Manually start a navigation span.
+ * This will only do something if the BrowserTracing integration has been setup.
+ */
+export function startBrowserTracingNavigationSpan(client: Client, spanOptions: StartSpanOptions): void {
+ if (!client.emit) {
+ return;
+ }
+
+ client.emit('startNavigationSpan', spanOptions);
+}
+
+/** Returns the value of a meta tag */
+export function getMetaContent(metaName: string): string | undefined {
+ // Can't specify generic to `getDomElement` because tracing can be used
+ // in a variety of environments, have to disable `no-unsafe-member-access`
+ // as a result.
+ const metaTag = getDomElement(`meta[name=${metaName}]`);
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ return metaTag ? metaTag.getAttribute('content') : undefined;
+}
+
+/** Start listener for interaction transactions */
+function registerInteractionListener(
+ options: BrowserTracingOptions,
+ latestRouteName: string | undefined,
+ latestRouteSource: TransactionSource | undefined,
+): void {
+ let inflightInteractionTransaction: IdleTransaction | undefined;
+ const registerInteractionTransaction = (): void => {
+ const { idleTimeout, finalTimeout, heartbeatInterval } = 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 &&
+ logger.warn(
+ `[Tracing] Did not create ${op} transaction because a pageload or navigation transaction is in progress.`,
+ );
+ return undefined;
+ }
+
+ if (inflightInteractionTransaction) {
+ inflightInteractionTransaction.setFinishReason('interactionInterrupted');
+ inflightInteractionTransaction.end();
+ inflightInteractionTransaction = undefined;
+ }
+
+ if (!latestRouteName) {
+ DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`);
+ return undefined;
+ }
+
+ const { location } = WINDOW;
+
+ const context: TransactionContext = {
+ name: latestRouteName,
+ op,
+ trimEnd: true,
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRouteSource || 'url',
+ },
+ };
+
+ inflightInteractionTransaction = startIdleTransaction(
+ // eslint-disable-next-line deprecation/deprecation
+ getCurrentHub(),
+ context,
+ idleTimeout,
+ finalTimeout,
+ true,
+ { location }, // for use in the tracesSampler
+ heartbeatInterval,
+ );
+ };
+
+ ['click'].forEach(type => {
+ addEventListener(type, registerInteractionTransaction, { once: false, capture: true });
+ });
+}
+
+function getSource(context: TransactionContext): TransactionSource | undefined {
+ const sourceFromAttributes = context.attributes && context.attributes[SEMANTIC_ATTRIBUTE_SENTRY_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 = context.metadata && context.metadata.source;
+
+ return sourceFromAttributes || sourceFromData || sourceFromMetadata;
+}
diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts
index e9f61c73c0f3..83897724fa5e 100644
--- a/packages/tracing-internal/src/browser/browsertracing.ts
+++ b/packages/tracing-internal/src/browser/browsertracing.ts
@@ -5,8 +5,6 @@ import {
TRACING_DEFAULTS,
addTracingExtensions,
getActiveTransaction,
- spanIsSampled,
- spanToJSON,
startIdleTransaction,
} from '@sentry/core';
import type { EventProcessor, Integration, Transaction, TransactionContext, TransactionSource } from '@sentry/types';
@@ -310,23 +308,27 @@ export class BrowserTracing implements Integration {
const isPageloadTransaction = context.op === 'pageload';
- const sentryTrace = isPageloadTransaction ? getMetaContent('sentry-trace') : '';
- const baggage = isPageloadTransaction ? getMetaContent('baggage') : '';
- const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders(
- sentryTrace,
- baggage,
- );
-
- const expandedContext: TransactionContext = {
- ...context,
- ...traceparentData,
- metadata: {
- // eslint-disable-next-line deprecation/deprecation
- ...context.metadata,
- dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
- },
- trimEnd: true,
- };
+ let expandedContext: TransactionContext;
+ if (isPageloadTransaction) {
+ const sentryTrace = isPageloadTransaction ? getMetaContent('sentry-trace') : '';
+ const baggage = isPageloadTransaction ? getMetaContent('baggage') : undefined;
+ const { traceparentData, dynamicSamplingContext } = tracingContextFromHeaders(sentryTrace, baggage);
+ expandedContext = {
+ ...context,
+ ...traceparentData,
+ metadata: {
+ // eslint-disable-next-line deprecation/deprecation
+ ...context.metadata,
+ dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
+ },
+ trimEnd: true,
+ };
+ } else {
+ expandedContext = {
+ ...context,
+ trimEnd: true,
+ };
+ }
const modifiedContext = typeof beforeNavigate === 'function' ? beforeNavigate(expandedContext) : expandedContext;
@@ -344,13 +346,7 @@ export class BrowserTracing implements Integration {
finalContext.metadata;
this._latestRouteName = finalContext.name;
-
- // 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;
+ this._latestRouteSource = getSource(finalContext);
// eslint-disable-next-line deprecation/deprecation
if (finalContext.sampled === false) {
@@ -384,24 +380,6 @@ export class BrowserTracing implements Integration {
}
}
- // eslint-disable-next-line deprecation/deprecation
- const scope = hub.getScope();
-
- // If it's a pageload and there is a meta tag set
- // use the traceparentData as the propagation context
- if (isPageloadTransaction && traceparentData) {
- scope.setPropagationContext(propagationContext);
- } else {
- // Navigation transactions should set a new propagation context based on the
- // created idle transaction.
- scope.setPropagationContext({
- traceId: idleTransaction.spanContext().traceId,
- spanId: idleTransaction.spanContext().spanId,
- parentSpanId: spanToJSON(idleTransaction).parent_span_id,
- sampled: spanIsSampled(idleTransaction),
- });
- }
-
idleTransaction.registerBeforeFinishCallback(transaction => {
this._collectWebVitals();
addPerformanceEntries(transaction);
@@ -481,3 +459,13 @@ export function getMetaContent(metaName: string): string | undefined {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return metaTag ? metaTag.getAttribute('content') : undefined;
}
+
+function getSource(context: TransactionContext): TransactionSource | undefined {
+ const sourceFromAttributes = context.attributes && context.attributes[SEMANTIC_ATTRIBUTE_SENTRY_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 = context.metadata && context.metadata.source;
+
+ return sourceFromAttributes || sourceFromData || sourceFromMetadata;
+}
diff --git a/packages/tracing-internal/src/browser/index.ts b/packages/tracing-internal/src/browser/index.ts
index 5b30bc519404..7d29d2f11e9e 100644
--- a/packages/tracing-internal/src/browser/index.ts
+++ b/packages/tracing-internal/src/browser/index.ts
@@ -3,6 +3,12 @@ export * from '../exports';
export type { RequestInstrumentationOptions } from './request';
export { BrowserTracing, BROWSER_TRACING_INTEGRATION_ID } from './browsertracing';
+export {
+ browserTracingIntegration,
+ startBrowserTracingNavigationSpan,
+ startBrowserTracingPageLoadSpan,
+} from './browserTracingIntegration';
+
export { instrumentOutgoingRequests, defaultRequestInstrumentationOptions } from './request';
export {
diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts
index c84d0545054b..d072188bb2af 100644
--- a/packages/tracing-internal/src/browser/request.ts
+++ b/packages/tracing-internal/src/browser/request.ts
@@ -1,11 +1,13 @@
/* eslint-disable max-lines */
import {
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
getClient,
getCurrentScope,
getDynamicSamplingContextFromClient,
getDynamicSamplingContextFromSpan,
- getRootSpan,
+ getIsolationScope,
hasTracingEnabled,
+ setHttpStatus,
spanToJSON,
spanToTraceHeader,
startInactiveSpan,
@@ -265,7 +267,7 @@ export function xhrCallback(
const span = spans[spanId];
if (span && sentryXhrData.status_code !== undefined) {
- span.setHttpStatus(sentryXhrData.status_code);
+ setHttpStatus(span, sentryXhrData.status_code);
span.end();
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
@@ -275,17 +277,19 @@ export function xhrCallback(
}
const scope = getCurrentScope();
+ const isolationScope = getIsolationScope();
const span = shouldCreateSpanResult
? startInactiveSpan({
+ name: `${sentryXhrData.method} ${sentryXhrData.url}`,
+ onlyIfParent: true,
attributes: {
type: 'xhr',
'http.method': sentryXhrData.method,
url: sentryXhrData.url,
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser',
},
- name: `${sentryXhrData.method} ${sentryXhrData.url}`,
op: 'http.client',
- origin: 'auto.http.browser',
})
: undefined;
@@ -294,21 +298,22 @@ export function xhrCallback(
spans[xhr.__sentry_xhr_span_id__] = span;
}
- if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url)) {
- if (span) {
- const transaction = span && getRootSpan(span);
- const dynamicSamplingContext = transaction && getDynamicSamplingContextFromSpan(transaction);
- const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
- setHeaderOnXhr(xhr, spanToTraceHeader(span), sentryBaggageHeader);
- } else {
- const client = getClient();
- const { traceId, sampled, dsc } = scope.getPropagationContext();
- const sentryTraceHeader = generateSentryTraceHeader(traceId, undefined, sampled);
- const dynamicSamplingContext =
- dsc || (client ? getDynamicSamplingContextFromClient(traceId, client, scope) : undefined);
- const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
- setHeaderOnXhr(xhr, sentryTraceHeader, sentryBaggageHeader);
- }
+ const client = getClient();
+
+ if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url) && client) {
+ const { traceId, spanId, sampled, dsc } = {
+ ...isolationScope.getPropagationContext(),
+ ...scope.getPropagationContext(),
+ };
+
+ const sentryTraceHeader = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, spanId, sampled);
+
+ const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(
+ dsc ||
+ (span ? getDynamicSamplingContextFromSpan(span) : getDynamicSamplingContextFromClient(traceId, client, scope)),
+ );
+
+ setHeaderOnXhr(xhr, sentryTraceHeader, sentryBaggageHeader);
}
return span;
diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts
index c96778f8cd35..e12ca3cf1b97 100644
--- a/packages/tracing-internal/src/common/fetch.ts
+++ b/packages/tracing-internal/src/common/fetch.ts
@@ -1,10 +1,12 @@
import {
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
getClient,
getCurrentScope,
getDynamicSamplingContextFromClient,
getDynamicSamplingContextFromSpan,
- getRootSpan,
+ getIsolationScope,
hasTracingEnabled,
+ setHttpStatus,
spanToTraceHeader,
startInactiveSpan,
} from '@sentry/core';
@@ -52,7 +54,7 @@ export function instrumentFetchRequest(
const span = spans[spanId];
if (span) {
if (handlerData.response) {
- span.setHttpStatus(handlerData.response.status);
+ setHttpStatus(span, handlerData.response.status);
const contentLength =
handlerData.response && handlerData.response.headers && handlerData.response.headers.get('content-length');
@@ -81,14 +83,15 @@ export function instrumentFetchRequest(
const span = shouldCreateSpanResult
? startInactiveSpan({
+ name: `${method} ${url}`,
+ onlyIfParent: true,
attributes: {
url,
type: 'fetch',
'http.method': method,
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin,
},
- name: `${method} ${url}`,
op: 'http.client',
- origin: spanOrigin,
})
: undefined;
@@ -132,18 +135,19 @@ export function addTracingHeadersToFetchRequest(
// eslint-disable-next-line deprecation/deprecation
const span = requestSpan || scope.getSpan();
- const transaction = span && getRootSpan(span);
+ const isolationScope = getIsolationScope();
- const { traceId, sampled, dsc } = scope.getPropagationContext();
+ const { traceId, spanId, sampled, dsc } = {
+ ...isolationScope.getPropagationContext(),
+ ...scope.getPropagationContext(),
+ };
- const sentryTraceHeader = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled);
- const dynamicSamplingContext = transaction
- ? getDynamicSamplingContextFromSpan(transaction)
- : dsc
- ? dsc
- : getDynamicSamplingContextFromClient(traceId, client, scope);
+ const sentryTraceHeader = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, spanId, sampled);
- const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
+ const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(
+ dsc ||
+ (span ? getDynamicSamplingContextFromSpan(span) : getDynamicSamplingContextFromClient(traceId, client, scope)),
+ );
const headers =
options.headers ||
diff --git a/packages/tracing-internal/src/exports/index.ts b/packages/tracing-internal/src/exports/index.ts
index 8c10b3165608..96cd3b85ac89 100644
--- a/packages/tracing-internal/src/exports/index.ts
+++ b/packages/tracing-internal/src/exports/index.ts
@@ -8,6 +8,7 @@ export {
Span,
// eslint-disable-next-line deprecation/deprecation
SpanStatus,
+ // eslint-disable-next-line deprecation/deprecation
spanStatusfromHttpCode,
startIdleTransaction,
Transaction,
diff --git a/packages/tracing-internal/src/index.ts b/packages/tracing-internal/src/index.ts
index 495d8dbb26b9..233baae9c308 100644
--- a/packages/tracing-internal/src/index.ts
+++ b/packages/tracing-internal/src/index.ts
@@ -14,6 +14,9 @@ export type { LazyLoadedIntegration } from './node';
export {
BrowserTracing,
+ browserTracingIntegration,
+ startBrowserTracingNavigationSpan,
+ startBrowserTracingPageLoadSpan,
BROWSER_TRACING_INTEGRATION_ID,
instrumentOutgoingRequests,
defaultRequestInstrumentationOptions,
diff --git a/packages/tracing-internal/src/node/integrations/prisma.ts b/packages/tracing-internal/src/node/integrations/prisma.ts
index f51bcd6eef32..2e2aa7dab17a 100644
--- a/packages/tracing-internal/src/node/integrations/prisma.ts
+++ b/packages/tracing-internal/src/node/integrations/prisma.ts
@@ -1,4 +1,4 @@
-import { getCurrentHub, startSpan } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getCurrentHub, startSpan } from '@sentry/core';
import type { Integration } from '@sentry/types';
import { addNonEnumerableProperty, logger } from '@sentry/utils';
@@ -103,8 +103,11 @@ export class Prisma implements Integration {
return startSpan(
{
name: model ? `${model} ${action}` : action,
+ onlyIfParent: true,
op: 'db.prisma',
- origin: 'auto.db.prisma',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.prisma',
+ },
data: { ...clientData, 'db.operation': action },
},
() => next(params),
diff --git a/packages/tracing/src/index.ts b/packages/tracing/src/index.ts
index 8559188884d7..d789a2b68520 100644
--- a/packages/tracing/src/index.ts
+++ b/packages/tracing/src/index.ts
@@ -78,6 +78,7 @@ export const extractTraceparentData = extractTraceparentDataT;
*
* `spanStatusfromHttpCode` can be imported from `@sentry/node`, `@sentry/browser`, or your framework SDK
*/
+// eslint-disable-next-line deprecation/deprecation
export const spanStatusfromHttpCode = spanStatusfromHttpCodeT;
/**
diff --git a/packages/tracing/test/hub.test.ts b/packages/tracing/test/hub.test.ts
index 7b73d9bb2d51..02841dbd5aca 100644
--- a/packages/tracing/test/hub.test.ts
+++ b/packages/tracing/test/hub.test.ts
@@ -305,8 +305,8 @@ describe('Hub', () => {
makeMain(hub);
hub.startTransaction({ name: 'dogpark', parentSampled: true });
- // length 1 because `sentry.origin` is set on span initialization
- expect(Transaction.prototype.setAttribute).toHaveBeenCalledTimes(1);
+ // length 2 because origin and op are set as attributes on span initialization
+ expect(Transaction.prototype.setAttribute).toHaveBeenCalledTimes(2);
});
it('should record sampling method and rate when sampling decision comes from traceSampleRate', () => {
diff --git a/packages/tracing/test/integrations/node/prisma.test.ts b/packages/tracing/test/integrations/node/prisma.test.ts
index 552edb1b78c2..2541eebf8f91 100644
--- a/packages/tracing/test/integrations/node/prisma.test.ts
+++ b/packages/tracing/test/integrations/node/prisma.test.ts
@@ -54,9 +54,12 @@ describe('setupOnce', function () {
expect(mockStartSpan).toHaveBeenCalledTimes(1);
expect(mockStartSpan).toHaveBeenLastCalledWith(
{
+ attributes: {
+ 'sentry.origin': 'auto.db.prisma',
+ },
name: 'user create',
+ onlyIfParent: true,
op: 'db.prisma',
- origin: 'auto.db.prisma',
data: { 'db.system': 'postgresql', 'db.prisma.version': '3.1.2', 'db.operation': 'create' },
},
expect.any(Function),
diff --git a/packages/tracing/test/span.test.ts b/packages/tracing/test/span.test.ts
index 4a7e1537f394..ae13f42ea698 100644
--- a/packages/tracing/test/span.test.ts
+++ b/packages/tracing/test/span.test.ts
@@ -98,6 +98,7 @@ describe('Span', () => {
expect((span.getTraceContext() as any).status).toBe('permission_denied');
});
+ // TODO (v8): Remove
test('setHttpStatus', () => {
const span = new Span({});
span.setHttpStatus(404);
diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts
index d8d09ec1431b..5db008b0ba37 100644
--- a/packages/types/src/client.ts
+++ b/packages/types/src/client.ts
@@ -15,6 +15,7 @@ import type { Scope } from './scope';
import type { SdkMetadata } from './sdkmetadata';
import type { Session, SessionAggregates } from './session';
import type { Severity, SeverityLevel } from './severity';
+import type { StartSpanOptions } from './startSpanOptions';
import type { Transaction } from './transaction';
import type { Transport, TransportMakeRequestResponse } from './transport';
@@ -272,6 +273,16 @@ export interface Client {
callback: (feedback: FeedbackEvent, options?: { includeReplay?: boolean }) => void,
): void;
+ /**
+ * A hook for BrowserTracing to trigger a span start for a page load.
+ */
+ on?(hook: 'startPageLoadSpan', callback: (options: StartSpanOptions) => void): void;
+
+ /**
+ * A hook for BrowserTracing to trigger a span for a navigation.
+ */
+ on?(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void;
+
/**
* Fire a hook event for transaction start.
* Expects to be given a transaction as the second argument.
@@ -333,5 +344,15 @@ export interface Client {
*/
emit?(hook: 'beforeSendFeedback', feedback: FeedbackEvent, options?: { includeReplay?: boolean }): void;
+ /**
+ * Emit a hook event for BrowserTracing to trigger a span start for a page load.
+ */
+ emit?(hook: 'startPageLoadSpan', options: StartSpanOptions): void;
+
+ /**
+ * Emit a hook event for BrowserTracing to trigger a span for a navigation.
+ */
+ emit?(hook: 'startNavigationSpan', options: StartSpanOptions): void;
+
/* eslint-enable @typescript-eslint/unified-signatures */
}
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index 7f9d66c904fa..5970383febc3 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -104,6 +104,7 @@ export type { StackFrame } from './stackframe';
export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace';
export type { TextEncoderInternal } from './textencoder';
export type { PropagationContext, TracePropagationTargets } from './tracing';
+export type { StartSpanOptions } from './startSpanOptions';
export type {
CustomSamplingContext,
SamplingContext,
diff --git a/packages/types/src/integration.ts b/packages/types/src/integration.ts
index 44c49ab375aa..3c3b44eb0ed8 100644
--- a/packages/types/src/integration.ts
+++ b/packages/types/src/integration.ts
@@ -39,6 +39,12 @@ export interface IntegrationFnResult {
*/
setup?(client: Client): void;
+ /**
+ * This hook is triggered after `setupOnce()` and `setup()` have been called for all integrations.
+ * You can use it if it is important that all other integrations have been run before.
+ */
+ afterAllSetup?(client: Client): void;
+
/**
* An optional hook that allows to preprocess an event _before_ it is passed to all other event processors.
*/
@@ -83,6 +89,12 @@ export interface Integration {
*/
setup?(client: Client): void;
+ /**
+ * This hook is triggered after `setupOnce()` and `setup()` have been called for all integrations.
+ * You can use it if it is important that all other integrations have been run before.
+ */
+ afterAllSetup?(client: Client): void;
+
/**
* An optional hook that allows to preprocess an event _before_ it is passed to all other event processors.
*/
diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts
index e34940b35d65..73c2fbdaaaa8 100644
--- a/packages/types/src/span.ts
+++ b/packages/types/src/span.ts
@@ -23,7 +23,13 @@ export type SpanAttributeValue =
| Array
| Array;
-export type SpanAttributes = Record;
+export type SpanAttributes = Partial<{
+ 'sentry.origin': string;
+ 'sentry.op': string;
+ 'sentry.source': string;
+ 'sentry.sample_rate': number;
+}> &
+ Record;
/** This type is aligned with the OpenTelemetry TimeInput type. */
export type SpanTimeInput = HrTime | number | Date;
@@ -326,6 +332,7 @@ export interface Span extends Omit {
/**
* Sets the status attribute on the current span based on the http code
* @param httpStatus http code used to set the status
+ * @deprecated Use top-level `setHttpStatus()` instead.
*/
setHttpStatus(httpStatus: number): this;
diff --git a/packages/types/src/startSpanOptions.ts b/packages/types/src/startSpanOptions.ts
new file mode 100644
index 000000000000..57ff96b3169f
--- /dev/null
+++ b/packages/types/src/startSpanOptions.ts
@@ -0,0 +1,111 @@
+import type { Instrumenter } from './instrumenter';
+import type { Primitive } from './misc';
+import type { Scope } from './scope';
+import type { SpanAttributes, SpanOrigin, SpanTimeInput } from './span';
+import type { TransactionContext, TransactionMetadata, TransactionSource } from './transaction';
+
+export 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;
+
+ /** If set to true, only start a span if a parent span exists. */
+ onlyIfParent?: boolean;
+
+ /** An op for the span. This is a categorization for spans. */
+ op?: string;
+
+ /**
+ * The origin of the span - if it comes from auto instrumentation or manual instrumentation.
+ *
+ * @deprecated Set `attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]` instead.
+ */
+ 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;
+}
diff --git a/packages/types/src/tracing.ts b/packages/types/src/tracing.ts
index 7c5b02c45596..701f9930d314 100644
--- a/packages/types/src/tracing.ts
+++ b/packages/types/src/tracing.ts
@@ -2,10 +2,43 @@ import type { DynamicSamplingContext } from './envelope';
export type TracePropagationTargets = (string | RegExp)[];
+/**
+ * `PropagationContext` represents the data from an incoming trace. It should be constructed from incoming trace data,
+ * usually represented by `sentry-trace` and `baggage` HTTP headers.
+ *
+ * There is always a propagation context present in the SDK (or rather on Scopes), holding at least a `traceId`. This is
+ * to ensure that there is always a trace we can attach events onto, even if performance monitoring is disabled. If
+ * there was no incoming `traceId`, the `traceId` will be generated by the current SDK.
+ */
export interface PropagationContext {
+ /**
+ * Either represents the incoming `traceId` or the `traceId` generated by the current SDK, if there was no incoming trace.
+ */
traceId: string;
+ /**
+ * Represents the execution context of the current SDK. This acts as a fallback value to associate events with a
+ * particular execution context when performance monitoring is disabled.
+ *
+ * The ID of a current span (if one exists) should have precedence over this value when propagating trace data.
+ */
spanId: string;
+ /**
+ * Represents the sampling decision of the incoming trace.
+ *
+ * The current SDK should not modify this value!
+ */
sampled?: boolean;
+ /**
+ * The `parentSpanId` denotes the ID of the incoming client span. If there is no `parentSpanId` on the propagation
+ * context, it means that the the incoming trace didn't come from a span.
+ *
+ * The current SDK should not modify this value!
+ */
parentSpanId?: string;
- dsc?: DynamicSamplingContext;
+ /**
+ * An undefined dsc in the propagation context means that the current SDK invocation is the head of trace and still free to modify and set the DSC for outgoing requests.
+ *
+ * The current SDK should not modify this value!
+ */
+ dsc?: Partial;
}
diff --git a/packages/utils/src/tracing.ts b/packages/utils/src/tracing.ts
index 438af21ac744..b0c25e7f1935 100644
--- a/packages/utils/src/tracing.ts
+++ b/packages/utils/src/tracing.ts
@@ -1,4 +1,4 @@
-import type { DynamicSamplingContext, PropagationContext, TraceparentData } from '@sentry/types';
+import type { PropagationContext, TraceparentData } from '@sentry/types';
import { baggageHeaderToDynamicSamplingContext } from './baggage';
import { uuid4 } from './misc';
@@ -59,25 +59,28 @@ export function tracingContextFromHeaders(
const { traceId, parentSpanId, parentSampled } = traceparentData || {};
- const propagationContext: PropagationContext = {
- traceId: traceId || uuid4(),
- spanId: uuid4().substring(16),
- sampled: parentSampled,
- };
-
- if (parentSpanId) {
- propagationContext.parentSpanId = parentSpanId;
- }
-
- if (dynamicSamplingContext) {
- propagationContext.dsc = dynamicSamplingContext as DynamicSamplingContext;
+ if (!traceparentData) {
+ return {
+ traceparentData,
+ dynamicSamplingContext: undefined,
+ propagationContext: {
+ traceId: traceId || uuid4(),
+ spanId: uuid4().substring(16),
+ },
+ };
+ } else {
+ return {
+ traceparentData,
+ dynamicSamplingContext: dynamicSamplingContext || {}, // If we have traceparent data but no DSC it means we are not head of trace and we must freeze it
+ propagationContext: {
+ traceId: traceId || uuid4(),
+ parentSpanId: parentSpanId || uuid4().substring(16),
+ spanId: uuid4().substring(16),
+ sampled: parentSampled,
+ dsc: dynamicSamplingContext || {}, // If we have traceparent data but no DSC it means we are not head of trace and we must freeze it
+ },
+ };
}
-
- return {
- traceparentData,
- dynamicSamplingContext,
- propagationContext,
- };
}
/**
diff --git a/packages/utils/test/tracing.test.ts b/packages/utils/test/tracing.test.ts
new file mode 100644
index 000000000000..2e7cc4d3d5a5
--- /dev/null
+++ b/packages/utils/test/tracing.test.ts
@@ -0,0 +1,9 @@
+import { tracingContextFromHeaders } from '../src/tracing';
+
+describe('tracingContextFromHeaders()', () => {
+ it('should produce a frozen baggage (empty object) when there is an incoming trace but no baggage header', () => {
+ const tracingContext = tracingContextFromHeaders('12312012123120121231201212312012-1121201211212012-1', undefined);
+ expect(tracingContext.dynamicSamplingContext).toEqual({});
+ expect(tracingContext.propagationContext.dsc).toEqual({});
+ });
+});
diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts
index abeb6d54b870..2ff971fde287 100644
--- a/packages/vercel-edge/src/index.ts
+++ b/packages/vercel-edge/src/index.ts
@@ -65,7 +65,9 @@ export {
setTag,
setTags,
setUser,
+ // eslint-disable-next-line deprecation/deprecation
spanStatusfromHttpCode,
+ getSpanStatusFromHttpCode,
// eslint-disable-next-line deprecation/deprecation
trace,
withScope,
diff --git a/yarn.lock b/yarn.lock
index 12799a710b48..bcbeead21f52 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3541,6 +3541,21 @@
"@hapi/boom" "9.x.x"
"@hapi/hoek" "9.x.x"
+"@hapi/accept@^5.0.1":
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.2.tgz#ab7043b037e68b722f93f376afb05e85c0699523"
+ integrity sha512-CmzBx/bXUR8451fnZRuZAJRlzgm0Jgu5dltTX/bszmR2lheb9BpyN47Q1RbaGTsvFzn0PXAEs+lXDKfshccYZw==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/hoek" "9.x.x"
+
+"@hapi/ammo@^5.0.1":
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/ammo/-/ammo-5.0.1.tgz#9d34560f5c214eda563d838c01297387efaab490"
+ integrity sha512-FbCNwcTbnQP4VYYhLNGZmA76xb2aHg9AMPiy18NZyWMG310P5KdFGyA9v2rm5ujrIny77dEEIkMOwl0Xv+fSSA==
+ dependencies:
+ "@hapi/hoek" "9.x.x"
+
"@hapi/b64@5.x.x":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@hapi/b64/-/b64-5.0.0.tgz#b8210cbd72f4774985e78569b77e97498d24277d"
@@ -3555,18 +3570,59 @@
dependencies:
"@hapi/hoek" "9.x.x"
-"@hapi/boom@^9.0.0":
+"@hapi/boom@^9.0.0", "@hapi/boom@^9.1.0":
version "9.1.4"
resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.4.tgz#1f9dad367c6a7da9f8def24b4a986fc5a7bd9db6"
integrity sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==
dependencies:
"@hapi/hoek" "9.x.x"
+"@hapi/bounce@2.x.x", "@hapi/bounce@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/bounce/-/bounce-2.0.0.tgz#e6ef56991c366b1e2738b2cd83b01354d938cf3d"
+ integrity sha512-JesW92uyzOOyuzJKjoLHM1ThiOvHPOLDHw01YV8yh5nCso7sDwJho1h0Ad2N+E62bZyz46TG3xhAi/78Gsct6A==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/hoek" "9.x.x"
+
"@hapi/bourne@2.x.x":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.1.0.tgz#66aff77094dc3080bd5df44ec63881f2676eb020"
integrity sha512-i1BpaNDVLJdRBEKeJWkVO6tYX6DMFBuwMhSuWqLsY4ufeTKGVuV5rBsUhxPayXqnnWHgXUAmWK16H/ykO5Wj4Q==
+"@hapi/call@^8.0.0":
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/call/-/call-8.0.1.tgz#9e64cd8ba6128eb5be6e432caaa572b1ed8cd7c0"
+ integrity sha512-bOff6GTdOnoe5b8oXRV3lwkQSb/LAWylvDMae6RgEWWntd0SHtkYbQukDHKlfaYtVnSAgIavJ0kqszF/AIBb6g==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/hoek" "9.x.x"
+
+"@hapi/catbox-memory@^5.0.0":
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/catbox-memory/-/catbox-memory-5.0.1.tgz#cb63fca0ded01d445a2573b38eb2688df67f70ac"
+ integrity sha512-QWw9nOYJq5PlvChLWV8i6hQHJYfvdqiXdvTupJFh0eqLZ64Xir7mKNi96d5/ZMUAqXPursfNDIDxjFgoEDUqeQ==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/hoek" "9.x.x"
+
+"@hapi/catbox@^11.1.1":
+ version "11.1.1"
+ resolved "https://registry.yarnpkg.com/@hapi/catbox/-/catbox-11.1.1.tgz#d277e2d5023fd69cddb33d05b224ea03065fec0c"
+ integrity sha512-u/8HvB7dD/6X8hsZIpskSDo4yMKpHxFd7NluoylhGrL6cUfYxdQPnvUp9YU2C6F9hsyBVLGulBd9vBN1ebfXOQ==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/hoek" "9.x.x"
+ "@hapi/podium" "4.x.x"
+ "@hapi/validate" "1.x.x"
+
+"@hapi/content@^5.0.2":
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/@hapi/content/-/content-5.0.2.tgz#ae57954761de570392763e64cdd75f074176a804"
+ integrity sha512-mre4dl1ygd4ZyOH3tiYBrOUBzV7Pu/EOs8VLGf58vtOEECWed8Uuw6B4iR9AN/8uQt42tB04qpVaMyoMQh0oMw==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+
"@hapi/cryptiles@5.x.x":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@hapi/cryptiles/-/cryptiles-5.1.0.tgz#655de4cbbc052c947f696148c83b187fc2be8f43"
@@ -3574,17 +3630,55 @@
dependencies:
"@hapi/boom" "9.x.x"
+"@hapi/file@2.x.x":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/file/-/file-2.0.0.tgz#2ecda37d1ae9d3078a67c13b7da86e8c3237dfb9"
+ integrity sha512-WSrlgpvEqgPWkI18kkGELEZfXr0bYLtr16iIN4Krh9sRnzBZN6nnWxHFxtsnP684wueEySBbXPDg/WfA9xJdBQ==
+
+"@hapi/hapi@^20.3.0":
+ version "20.3.0"
+ resolved "https://registry.yarnpkg.com/@hapi/hapi/-/hapi-20.3.0.tgz#1d620005afeebcb2c8170352286a4664b0107c15"
+ integrity sha512-zvPSRvaQyF3S6Nev9aiAzko2/hIFZmNSJNcs07qdVaVAvj8dGJSV4fVUuQSnufYJAGiSau+U5LxMLhx79se5WA==
+ dependencies:
+ "@hapi/accept" "^5.0.1"
+ "@hapi/ammo" "^5.0.1"
+ "@hapi/boom" "^9.1.0"
+ "@hapi/bounce" "^2.0.0"
+ "@hapi/call" "^8.0.0"
+ "@hapi/catbox" "^11.1.1"
+ "@hapi/catbox-memory" "^5.0.0"
+ "@hapi/heavy" "^7.0.1"
+ "@hapi/hoek" "^9.0.4"
+ "@hapi/mimos" "^6.0.0"
+ "@hapi/podium" "^4.1.1"
+ "@hapi/shot" "^5.0.5"
+ "@hapi/somever" "^3.0.0"
+ "@hapi/statehood" "^7.0.3"
+ "@hapi/subtext" "^7.1.0"
+ "@hapi/teamwork" "^5.1.0"
+ "@hapi/topo" "^5.0.0"
+ "@hapi/validate" "^1.1.1"
+
+"@hapi/heavy@^7.0.1":
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/heavy/-/heavy-7.0.1.tgz#73315ae33b6e7682a0906b7a11e8ca70e3045874"
+ integrity sha512-vJ/vzRQ13MtRzz6Qd4zRHWS3FaUc/5uivV2TIuExGTM9Qk+7Zzqj0e2G7EpE6KztO9SalTbiIkTh7qFKj/33cA==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/hoek" "9.x.x"
+ "@hapi/validate" "1.x.x"
+
"@hapi/hoek@9.x.x":
version "9.2.0"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131"
integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug==
-"@hapi/hoek@^9.0.0":
+"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.0.4":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==
-"@hapi/iron@^6.0.0":
+"@hapi/iron@6.x.x", "@hapi/iron@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@hapi/iron/-/iron-6.0.0.tgz#ca3f9136cda655bdd6028de0045da0de3d14436f"
integrity sha512-zvGvWDufiTGpTJPG1Y/McN8UqWBu0k/xs/7l++HVU535NLHXsHhy54cfEMdW7EjwKfbBfM9Xy25FmTiobb7Hvw==
@@ -3595,7 +3689,34 @@
"@hapi/cryptiles" "5.x.x"
"@hapi/hoek" "9.x.x"
-"@hapi/podium@^4.1.3":
+"@hapi/mimos@^6.0.0":
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/mimos/-/mimos-6.0.0.tgz#daa523d9c07222c7e8860cb7c9c5501fd6506484"
+ integrity sha512-Op/67tr1I+JafN3R3XN5DucVSxKRT/Tc+tUszDwENoNpolxeXkhrJ2Czt6B6AAqrespHoivhgZBWYSuANN9QXg==
+ dependencies:
+ "@hapi/hoek" "9.x.x"
+ mime-db "1.x.x"
+
+"@hapi/nigel@4.x.x":
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/@hapi/nigel/-/nigel-4.0.2.tgz#8f84ef4bca4fb03b2376463578f253b0b8e863c4"
+ integrity sha512-ht2KoEsDW22BxQOEkLEJaqfpoKPXxi7tvabXy7B/77eFtOyG5ZEstfZwxHQcqAiZhp58Ae5vkhEqI03kawkYNw==
+ dependencies:
+ "@hapi/hoek" "^9.0.4"
+ "@hapi/vise" "^4.0.0"
+
+"@hapi/pez@^5.1.0":
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/@hapi/pez/-/pez-5.1.0.tgz#c03a5e01f8be01cfabc4c0017631e619586321c1"
+ integrity sha512-YfB0btnkLB3lb6Ry/1KifnMPBm5ZPfaAHWFskzOMAgDgXgcBgA+zjpIynyEiBfWEz22DBT8o1e2tAaBdlt8zbw==
+ dependencies:
+ "@hapi/b64" "5.x.x"
+ "@hapi/boom" "9.x.x"
+ "@hapi/content" "^5.0.2"
+ "@hapi/hoek" "9.x.x"
+ "@hapi/nigel" "4.x.x"
+
+"@hapi/podium@4.x.x", "@hapi/podium@^4.1.1", "@hapi/podium@^4.1.3":
version "4.1.3"
resolved "https://registry.yarnpkg.com/@hapi/podium/-/podium-4.1.3.tgz#91e20838fc2b5437f511d664aabebbb393578a26"
integrity sha512-ljsKGQzLkFqnQxE7qeanvgGj4dejnciErYd30dbrYzUOF/FyS/DOF97qcrT3bhoVwCYmxa6PEMhxfCPlnUcD2g==
@@ -3604,7 +3725,49 @@
"@hapi/teamwork" "5.x.x"
"@hapi/validate" "1.x.x"
-"@hapi/teamwork@5.x.x":
+"@hapi/shot@^5.0.5":
+ version "5.0.5"
+ resolved "https://registry.yarnpkg.com/@hapi/shot/-/shot-5.0.5.tgz#a25c23d18973bec93c7969c51bf9579632a5bebd"
+ integrity sha512-x5AMSZ5+j+Paa8KdfCoKh+klB78otxF+vcJR/IoN91Vo2e5ulXIW6HUsFTCU+4W6P/Etaip9nmdAx2zWDimB2A==
+ dependencies:
+ "@hapi/hoek" "9.x.x"
+ "@hapi/validate" "1.x.x"
+
+"@hapi/somever@^3.0.0":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/somever/-/somever-3.0.1.tgz#9961cd5bdbeb5bb1edc0b2acdd0bb424066aadcc"
+ integrity sha512-4ZTSN3YAHtgpY/M4GOtHUXgi6uZtG9nEZfNI6QrArhK0XN/RDVgijlb9kOmXwCR5VclDSkBul9FBvhSuKXx9+w==
+ dependencies:
+ "@hapi/bounce" "2.x.x"
+ "@hapi/hoek" "9.x.x"
+
+"@hapi/statehood@^7.0.3":
+ version "7.0.4"
+ resolved "https://registry.yarnpkg.com/@hapi/statehood/-/statehood-7.0.4.tgz#6acb9d0817b5c657089356f7d9fd60af0bce4f41"
+ integrity sha512-Fia6atroOVmc5+2bNOxF6Zv9vpbNAjEXNcUbWXavDqhnJDlchwUUwKS5LCi5mGtCTxRhUKKHwuxuBZJkmLZ7fw==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/bounce" "2.x.x"
+ "@hapi/bourne" "2.x.x"
+ "@hapi/cryptiles" "5.x.x"
+ "@hapi/hoek" "9.x.x"
+ "@hapi/iron" "6.x.x"
+ "@hapi/validate" "1.x.x"
+
+"@hapi/subtext@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@hapi/subtext/-/subtext-7.1.0.tgz#b4d1ea2aeab1923ac130a24e75921e38fab5b15b"
+ integrity sha512-n94cU6hlvsNRIpXaROzBNEJGwxC+HA69q769pChzej84On8vsU14guHDub7Pphr/pqn5b93zV3IkMPDU5AUiXA==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/bourne" "2.x.x"
+ "@hapi/content" "^5.0.2"
+ "@hapi/file" "2.x.x"
+ "@hapi/hoek" "9.x.x"
+ "@hapi/pez" "^5.1.0"
+ "@hapi/wreck" "17.x.x"
+
+"@hapi/teamwork@5.x.x", "@hapi/teamwork@^5.1.0":
version "5.1.1"
resolved "https://registry.yarnpkg.com/@hapi/teamwork/-/teamwork-5.1.1.tgz#4d2ba3cac19118a36c44bf49a3a47674de52e4e4"
integrity sha512-1oPx9AE5TIv+V6Ih54RP9lTZBso3rP8j4Xhb6iSVwPXtAM+sDopl5TFMv5Paw73UnpZJ9gjcrTE1BXrWt9eQrg==
@@ -3616,7 +3779,7 @@
dependencies:
"@hapi/hoek" "^9.0.0"
-"@hapi/validate@1.x.x":
+"@hapi/validate@1.x.x", "@hapi/validate@^1.1.1":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@hapi/validate/-/validate-1.1.3.tgz#f750a07283929e09b51aa16be34affb44e1931ad"
integrity sha512-/XMR0N0wjw0Twzq2pQOzPBZlDzkekGcoCtzO314BpIEsbXdYGthQUbxgkGDf4nhk1+IPDAsXqWjMohRQYO06UA==
@@ -3624,6 +3787,22 @@
"@hapi/hoek" "^9.0.0"
"@hapi/topo" "^5.0.0"
+"@hapi/vise@^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/vise/-/vise-4.0.0.tgz#c6a94fe121b94a53bf99e7489f7fcc74c104db02"
+ integrity sha512-eYyLkuUiFZTer59h+SGy7hUm+qE9p+UemePTHLlIWppEd+wExn3Df5jO04bFQTm7nleF5V8CtuYQYb+VFpZ6Sg==
+ dependencies:
+ "@hapi/hoek" "9.x.x"
+
+"@hapi/wreck@17.x.x":
+ version "17.2.0"
+ resolved "https://registry.yarnpkg.com/@hapi/wreck/-/wreck-17.2.0.tgz#a5b69b724fa8fa25550fb02f55c649becfc59f63"
+ integrity sha512-pJ5kjYoRPYDv+eIuiLQqhGon341fr2bNIYZjuotuPJG/3Ilzr/XtI+JAp0A86E2bYfsS3zBPABuS2ICkaXFT8g==
+ dependencies:
+ "@hapi/boom" "9.x.x"
+ "@hapi/bourne" "2.x.x"
+ "@hapi/hoek" "9.x.x"
+
"@humanwhocodes/config-array@^0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
@@ -12744,6 +12923,11 @@ denque@^1.4.1:
resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf"
integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==
+denque@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
+ integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
+
depd@2.0.0, depd@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
@@ -16129,6 +16313,13 @@ gcp-metadata@^4.2.0:
gaxios "^4.0.0"
json-bigint "^1.0.0"
+generate-function@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f"
+ integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==
+ dependencies:
+ is-property "^1.0.2"
+
genfun@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537"
@@ -18407,6 +18598,11 @@ is-potential-custom-element-name@^1.0.1:
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
+is-property@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
+ integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==
+
is-reference@1.2.1, is-reference@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7"
@@ -20640,6 +20836,11 @@ long@^4.0.0:
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
+long@^5.2.1:
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1"
+ integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==
+
longest-streak@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4"
@@ -20708,6 +20909,16 @@ lru-cache@^7.10.1, lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.1.tgz#8da8d2f5f59827edb388e63e459ac23d6d408fea"
integrity sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==
+lru-cache@^7.14.1:
+ version "7.18.3"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
+ integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
+
+lru-cache@^8.0.0:
+ version "8.0.5"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-8.0.5.tgz#983fe337f3e176667f8e567cfcce7cb064ea214e"
+ integrity sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==
+
lru-cache@^9.0.0:
version "9.0.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.0.1.tgz#ac061ed291f8b9adaca2b085534bb1d3b61bef83"
@@ -21674,7 +21885,7 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
-mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
+mime-db@1.52.0, mime-db@1.x.x, "mime-db@>= 1.43.0 < 2":
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
@@ -22331,6 +22542,20 @@ mute-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e"
integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==
+mysql2@^3.7.1:
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.7.1.tgz#bb088fa3f01deefbfe04adaf0d3ec18571b33410"
+ integrity sha512-4EEqYu57mnkW5+Bvp5wBebY7PpfyrmvJ3knHcmLkp8FyBu4kqgrF2GxIjsC2tbLNZWqJaL21v/MYH7bU5f03oA==
+ dependencies:
+ denque "^2.1.0"
+ generate-function "^2.3.1"
+ iconv-lite "^0.6.3"
+ long "^5.2.1"
+ lru-cache "^8.0.0"
+ named-placeholders "^1.1.3"
+ seq-queue "^0.0.5"
+ sqlstring "^2.3.2"
+
mysql@^2.18.1:
version "2.18.1"
resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717"
@@ -22350,6 +22575,13 @@ mz@^2.7.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
+named-placeholders@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351"
+ integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==
+ dependencies:
+ lru-cache "^7.14.1"
+
nan@^2.12.1:
version "2.14.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
@@ -27953,6 +28185,11 @@ send@0.18.0:
range-parser "~1.2.1"
statuses "2.0.1"
+seq-queue@^0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e"
+ integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==
+
serialize-javascript@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
@@ -28813,6 +29050,11 @@ sqlstring@2.3.1:
resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40"
integrity sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=
+sqlstring@^2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c"
+ integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==
+
sri-toolbox@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/sri-toolbox/-/sri-toolbox-0.2.0.tgz#a7fea5c3fde55e675cf1c8c06f3ebb5c2935835e"