diff --git a/.size-limit.js b/.size-limit.js
index 6e6ee0f68303..e6c0f5f3b1ab 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -47,12 +47,19 @@ module.exports = [
gzip: true,
limit: '48 KB',
},
+ // {
+ // name: '@sentry/browser (incl. Tracing Span-First)',
+ // path: 'packages/browser/build/npm/esm/index.js',
+ // import: createImport('init', 'browserTracingIntegration', 'spanStreamingIntegration'),
+ // gzip: true,
+ // limit: '44 KB',
+ // },
{
name: '@sentry/browser (incl. Tracing, Replay)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration'),
gzip: true,
- limit: '80 KB',
+ limit: '82 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay) - with treeshaking flags',
@@ -82,14 +89,14 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'),
gzip: true,
- limit: '85 KB',
+ limit: '86 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay, Feedback)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration'),
gzip: true,
- limit: '97 KB',
+ limit: '98 KB',
},
{
name: '@sentry/browser (incl. Feedback)',
@@ -103,7 +110,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'sendFeedback'),
gzip: true,
- limit: '30 KB',
+ limit: '31 KB',
},
{
name: '@sentry/browser (incl. FeedbackAsync)',
@@ -127,7 +134,7 @@ module.exports = [
import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'),
ignore: ['react/jsx-runtime'],
gzip: true,
- limit: '44 KB',
+ limit: '45 KB',
},
// Vue SDK (ESM)
{
@@ -135,7 +142,7 @@ module.exports = [
path: 'packages/vue/build/esm/index.js',
import: createImport('init'),
gzip: true,
- limit: '30 KB',
+ limit: '31 KB',
},
{
name: '@sentry/vue (incl. Tracing)',
@@ -163,7 +170,7 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing)',
path: createCDNPath('bundle.tracing.min.js'),
gzip: true,
- limit: '42.5 KB',
+ limit: '43 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay)',
@@ -213,7 +220,7 @@ module.exports = [
import: createImport('init'),
ignore: ['next/router', 'next/constants'],
gzip: true,
- limit: '46 KB',
+ limit: '47 KB',
},
// SvelteKit SDK (ESM)
{
@@ -222,7 +229,7 @@ module.exports = [
import: createImport('init'),
ignore: ['$app/stores'],
gzip: true,
- limit: '42 KB',
+ limit: '43 KB',
},
// Node-Core SDK (ESM)
{
@@ -240,7 +247,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
- limit: '160 KB',
+ limit: '161 KB',
},
{
name: '@sentry/node - without tracing',
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58e2cf7bd830..3456bb599692 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,17 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 10.28.0-alpha.0
+
+This release is a preview release for sending spans in browser via spanV2 instead of transaction event envelopes. All of this is experimental and subject to change. Use at your own risk. [More Details.](https://github.com/getsentry/sentry-javascript/pull/17852)
+
+Changes from 10.24-alpha.1:
+
+- add `sentry.segment.id` common span attribute
+- rename attribute `user.username` to `user.name`
+- remove `is_remote`, add `is_segment` span attributes
+- remove `span.kind` field
+
## 10.27.0
### Important Changes
@@ -353,6 +364,16 @@ Work in this release was contributed by @hanseo0507. Thank you for your contribu
Work in this release was contributed by @0xbad0c0d3. Thank you for your contribution!
+## 10.21.0-alpha.1
+
+This release is a preview release for sending spans in browser via spanV2 instead of transaction event envelopes. All of this is experimental and subject to change. Use at your own risk. [More Details.](https://github.com/getsentry/sentry-javascript/pull/17852)
+
+- export withStreamSpan from `@sentry/browser`
+
+## 10.21.0-alpha.0
+
+This release is a preview release for sending spans in browser via spanV2 instead of transaction event envelopes. All of this is experimental and subject to change. Use at your own risk. [More Details.](https://github.com/getsentry/sentry-javascript/pull/17852)
+
## 10.20.0
### Important Changes
diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json
index e5ff62a58084..92943f0a6cef 100644
--- a/dev-packages/browser-integration-tests/package.json
+++ b/dev-packages/browser-integration-tests/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/browser-integration-tests",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"main": "index.js",
"license": "MIT",
"engines": {
@@ -43,7 +43,7 @@
"@babel/preset-typescript": "^7.16.7",
"@playwright/test": "~1.53.2",
"@sentry-internal/rrweb": "2.34.0",
- "@sentry/browser": "10.27.0",
+ "@sentry/browser": "10.28.0-alpha.0",
"@supabase/supabase-js": "2.49.3",
"axios": "^1.12.2",
"babel-loader": "^8.2.2",
diff --git a/dev-packages/browser-integration-tests/suites/span-first/backgroundtab-pageload/init.js b/dev-packages/browser-integration-tests/suites/span-first/backgroundtab-pageload/init.js
new file mode 100644
index 000000000000..5541015d7585
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/span-first/backgroundtab-pageload/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',
+ traceLifecycle: 'stream',
+ integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()],
+ tracePropagationTargets: ['http://sentry-test-site.example'],
+ tracesSampleRate: 1,
+ sendDefaultPii: true,
+});
diff --git a/dev-packages/browser-integration-tests/suites/span-first/backgroundtab-pageload/subject.js b/dev-packages/browser-integration-tests/suites/span-first/backgroundtab-pageload/subject.js
new file mode 100644
index 000000000000..b657f38ac009
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/span-first/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/span-first/backgroundtab-pageload/template.html b/dev-packages/browser-integration-tests/suites/span-first/backgroundtab-pageload/template.html
new file mode 100644
index 000000000000..31cfc73ec3c3
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/span-first/backgroundtab-pageload/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/span-first/backgroundtab-pageload/test.ts b/dev-packages/browser-integration-tests/suites/span-first/backgroundtab-pageload/test.ts
new file mode 100644
index 000000000000..368820e754fb
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/span-first/backgroundtab-pageload/test.ts
@@ -0,0 +1,22 @@
+import { expect } from '@playwright/test';
+import { sentryTest } from '../../../utils/fixtures';
+import { shouldSkipTracingTest } from '../../../utils/helpers';
+import { getSpanOp, waitForV2Spans } from '../../../utils/spanFirstUtils';
+
+sentryTest('ends pageload span when the page goes to background', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ const spanPromise = waitForV2Spans(page, spans => !!spans.find(span => getSpanOp(span) === 'pageload'));
+
+ await page.goto(url);
+ await page.locator('#go-background').click();
+
+ const pageloadSpan = (await spanPromise).find(span => getSpanOp(span) === 'pageload');
+
+ expect(pageloadSpan?.status).toBe('error'); // a cancelled span previously mapped to status error with message cancelled.
+ expect(pageloadSpan?.attributes?.['sentry.op']?.value).toBe('pageload');
+ expect(pageloadSpan?.attributes?.['sentry.cancellation_reason']?.value).toBe('document.hidden');
+});
diff --git a/dev-packages/browser-integration-tests/suites/span-first/error/init.js b/dev-packages/browser-integration-tests/suites/span-first/error/init.js
new file mode 100644
index 000000000000..853d9ec8f605
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/span-first/error/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',
+ traceLifecycle: 'stream',
+ integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()],
+ tracePropagationTargets: ['http://sentry-test-site.example'],
+ tracesSampleRate: 1,
+ sendDefaultPii: true,
+ debug: true,
+});
diff --git a/dev-packages/browser-integration-tests/suites/span-first/error/test.ts b/dev-packages/browser-integration-tests/suites/span-first/error/test.ts
new file mode 100644
index 000000000000..682cece57172
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/span-first/error/test.ts
@@ -0,0 +1,50 @@
+import { expect } from '@playwright/test';
+import type { Event } from '@sentry/core';
+import { sentryTest } from '../../../utils/fixtures';
+import {
+ envelopeRequestParser,
+ runScriptInSandbox,
+ shouldSkipTracingTest,
+ waitForErrorRequest,
+} from '../../../utils/helpers';
+import { getSpanOp, waitForV2Spans } from '../../../utils/spanFirstUtils';
+
+sentryTest(
+ 'puts the pageload span name onto an error event caught during pageload',
+ async ({ getLocalTestUrl, page, browserName }) => {
+ if (browserName === 'webkit') {
+ // This test fails on Webkit as errors thrown from `runScriptInSandbox` are Script Errors and skipped by Sentry
+ sentryTest.skip();
+ }
+
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ const errorEventPromise = waitForErrorRequest(page);
+ const spanPromise = waitForV2Spans(page, spans => !!spans.find(span => getSpanOp(span) === 'pageload'));
+
+ await page.goto(url);
+
+ await runScriptInSandbox(page, {
+ content: `
+ throw new Error('Error during pageload');
+ `,
+ });
+
+ const errorEvent = envelopeRequestParser(await errorEventPromise);
+ const pageloadSpan = (await spanPromise).find(span => getSpanOp(span) === 'pageload');
+
+ expect(pageloadSpan?.attributes?.['sentry.op']?.value).toEqual('pageload');
+ expect(errorEvent.exception?.values?.[0]).toBeDefined();
+
+ expect(pageloadSpan?.name).toEqual('/index.html');
+
+ expect(pageloadSpan?.status).toBe('error');
+ expect(pageloadSpan?.attributes?.['sentry.idle_span_finish_reason']?.value).toBe('idleTimeout');
+
+ expect(errorEvent.transaction).toEqual(pageloadSpan?.name);
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/span-first/init.js b/dev-packages/browser-integration-tests/suites/span-first/init.js
new file mode 100644
index 000000000000..5541015d7585
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/span-first/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',
+ traceLifecycle: 'stream',
+ integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()],
+ tracePropagationTargets: ['http://sentry-test-site.example'],
+ tracesSampleRate: 1,
+ sendDefaultPii: true,
+});
diff --git a/dev-packages/browser-integration-tests/suites/span-first/linked-traces/test.ts b/dev-packages/browser-integration-tests/suites/span-first/linked-traces/test.ts
new file mode 100644
index 000000000000..5fd9df3dbae1
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/span-first/linked-traces/test.ts
@@ -0,0 +1,100 @@
+import { expect } from '@playwright/test';
+import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
+import { sentryTest } from '../../../utils/fixtures';
+import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers';
+import { getSpanOp, waitForV2Spans } from '../../../utils/spanFirstUtils';
+
+sentryTest("navigation spans link back to previous trace's root span", async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ const pageloadSpan = await sentryTest.step('Initial pageload', async () => {
+ const pageloadSpanPromise = waitForV2Spans(page, spans => !!spans.find(span => getSpanOp(span) === 'pageload'));
+ await page.goto(url);
+ return (await pageloadSpanPromise).find(span => getSpanOp(span) === 'pageload');
+ });
+
+ const navigation1Span = await sentryTest.step('First navigation', async () => {
+ const navigation1SpanPromise = waitForV2Spans(
+ page,
+ spans => !!spans.find(span => getSpanOp(span) === 'navigation'),
+ );
+ await page.goto(`${url}#foo`);
+ return (await navigation1SpanPromise).find(span => getSpanOp(span) === 'navigation');
+ });
+
+ const navigation2Span = await sentryTest.step('Second navigation', async () => {
+ const navigation2SpanPromise = waitForV2Spans(
+ page,
+ spans => !!spans.find(span => getSpanOp(span) === 'navigation'),
+ );
+ await page.goto(`${url}#bar`);
+ return (await navigation2SpanPromise).find(span => getSpanOp(span) === 'navigation');
+ });
+
+ const pageloadTraceId = pageloadSpan?.trace_id;
+ const navigation1TraceId = navigation1Span?.trace_id;
+ const navigation2TraceId = navigation2Span?.trace_id;
+
+ expect(pageloadSpan?.links).toBeUndefined();
+
+ expect(navigation1Span?.links).toEqual([
+ {
+ trace_id: pageloadTraceId,
+ span_id: pageloadSpan?.span_id,
+ sampled: true,
+ attributes: {
+ [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: { value: 'previous_trace', type: 'string' },
+ },
+ },
+ ]);
+
+ expect(navigation1Span?.attributes).toMatchObject({
+ 'sentry.previous_trace': { type: 'string', value: `${pageloadTraceId}-${pageloadSpan?.span_id}-1` },
+ });
+
+ expect(navigation2Span?.links).toEqual([
+ {
+ trace_id: navigation1TraceId,
+ span_id: navigation1Span?.span_id,
+ sampled: true,
+ attributes: {
+ [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: { value: 'previous_trace', type: 'string' },
+ },
+ },
+ ]);
+
+ expect(navigation2Span?.attributes).toMatchObject({
+ 'sentry.previous_trace': { type: 'string', value: `${navigation1TraceId}-${navigation1Span?.span_id}-1` },
+ });
+
+ expect(pageloadTraceId).not.toEqual(navigation1TraceId);
+ expect(navigation1TraceId).not.toEqual(navigation2TraceId);
+ expect(pageloadTraceId).not.toEqual(navigation2TraceId);
+});
+
+sentryTest("doesn't link between hard page reloads by default", async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ await sentryTest.step('First pageload', async () => {
+ const pageloadRequestPromise = waitForV2Spans(page, spans => !!spans.find(span => getSpanOp(span) === 'pageload'));
+ await page.goto(url);
+ return (await pageloadRequestPromise).find(span => getSpanOp(span) === 'pageload');
+ });
+
+ await sentryTest.step('Second pageload', async () => {
+ const pageload2RequestPromise = waitForV2Spans(page, spans => !!spans.find(span => getSpanOp(span) === 'pageload'));
+ await page.reload();
+ const pageload2Span = (await pageload2RequestPromise).find(span => getSpanOp(span) === 'pageload');
+
+ expect(pageload2Span?.trace_id).toBeDefined();
+ expect(pageload2Span?.links).toBeUndefined();
+ });
+});
diff --git a/dev-packages/browser-integration-tests/suites/span-first/pageload/init.js b/dev-packages/browser-integration-tests/suites/span-first/pageload/init.js
new file mode 100644
index 000000000000..5541015d7585
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/span-first/pageload/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',
+ traceLifecycle: 'stream',
+ integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()],
+ tracePropagationTargets: ['http://sentry-test-site.example'],
+ tracesSampleRate: 1,
+ sendDefaultPii: true,
+});
diff --git a/dev-packages/browser-integration-tests/suites/span-first/pageload/test.ts b/dev-packages/browser-integration-tests/suites/span-first/pageload/test.ts
new file mode 100644
index 000000000000..1092f53a8698
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/span-first/pageload/test.ts
@@ -0,0 +1,118 @@
+import { expect } from '@playwright/test';
+import { sentryTest } from '../../../utils/fixtures';
+import { shouldSkipTracingTest } from '../../../utils/helpers';
+import { getSpanOp, waitForSpanV2Envelope } from '../../../utils/spanFirstUtils';
+
+sentryTest('sends a span v2 envelope for the pageload', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const spanEnvelopePromise = waitForSpanV2Envelope(page);
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ await page.goto(url);
+
+ const spanEnvelope = await spanEnvelopePromise;
+
+ const envelopeHeaders = spanEnvelope[0];
+
+ const envelopeItem0 = spanEnvelope[1][0];
+ const envelopeItemHeader = envelopeItem0[0];
+ const envelopeItem = envelopeItem0[1];
+
+ expect(envelopeHeaders).toEqual({
+ sent_at: expect.any(String),
+ trace: {
+ environment: 'production',
+ public_key: 'public',
+ trace_id: expect.stringMatching(/^[\da-f]{32}$/),
+ sampled: 'true',
+ sample_rand: expect.any(String),
+ sample_rate: '1',
+ },
+ sdk: {
+ name: 'sentry.javascript.browser',
+ packages: [
+ {
+ name: 'npm:@sentry/browser',
+ version: expect.any(String),
+ },
+ ],
+ version: expect.any(String),
+ settings: {
+ infer_ip: 'auto',
+ },
+ },
+ });
+
+ expect(envelopeItemHeader).toEqual({
+ content_type: 'application/vnd.sentry.items.span.v2+json',
+ item_count: expect.any(Number),
+ type: 'span',
+ });
+
+ // test the shape of the item first, then the content
+ expect(envelopeItem).toEqual({
+ items: expect.any(Array),
+ });
+
+ expect(envelopeItem.items.length).toBe(envelopeItemHeader.item_count);
+
+ const pageloadSpan = envelopeItem.items.find(item => getSpanOp(item) === 'pageload');
+
+ expect(pageloadSpan).toBeDefined();
+
+ expect(pageloadSpan).toEqual({
+ attributes: expect.objectContaining({
+ 'performance.activationStart': {
+ type: 'integer',
+ value: 0,
+ },
+ 'performance.timeOrigin': {
+ type: 'double',
+ value: expect.any(Number),
+ },
+ 'sentry.op': {
+ type: 'string',
+ value: 'pageload',
+ },
+ 'sentry.origin': {
+ type: 'string',
+ value: 'auto.pageload.browser',
+ },
+ 'sentry.sample_rate': {
+ type: 'integer',
+ value: 1,
+ },
+ 'sentry.sdk.name': {
+ type: 'string',
+ value: 'sentry.javascript.browser',
+ },
+ 'sentry.sdk.version': {
+ type: 'string',
+ value: expect.any(String),
+ },
+ 'sentry.segment.id': {
+ type: 'string',
+ value: pageloadSpan?.span_id, // pageload is always the segment
+ },
+ 'sentry.segment.name': {
+ type: 'string',
+ value: '/index.html',
+ },
+ 'sentry.source': {
+ type: 'string',
+ value: 'url',
+ },
+ }),
+ trace_id: expect.stringMatching(/^[a-f\d]{32}$/),
+ span_id: expect.stringMatching(/^[a-f\d]{16}$/),
+ name: '/index.html',
+ status: 'ok',
+ is_segment: true,
+ start_timestamp: expect.any(Number),
+ end_timestamp: expect.any(Number),
+ });
+});
diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts
index dd75d2f6ee86..0495a539ff53 100644
--- a/dev-packages/browser-integration-tests/utils/helpers.ts
+++ b/dev-packages/browser-integration-tests/utils/helpers.ts
@@ -62,7 +62,7 @@ export const eventAndTraceHeaderRequestParser = (request: Request | null): Event
return getEventAndTraceHeader(envelope);
};
-const properFullEnvelopeParser = (request: Request | null): T => {
+export const properFullEnvelopeParser = (request: Request | null): T => {
// https://develop.sentry.dev/sdk/envelopes/
const envelope = request?.postData() || '';
diff --git a/dev-packages/browser-integration-tests/utils/spanFirstUtils.ts b/dev-packages/browser-integration-tests/utils/spanFirstUtils.ts
new file mode 100644
index 000000000000..212355f5e780
--- /dev/null
+++ b/dev-packages/browser-integration-tests/utils/spanFirstUtils.ts
@@ -0,0 +1,62 @@
+import type { Page } from '@playwright/test';
+import type { SpanV2Envelope, SpanV2JSON } from '@sentry/core';
+import { properFullEnvelopeParser } from './helpers';
+
+/**
+ * Wait for a span v2 envelope
+ */
+export async function waitForSpanV2Envelope(
+ page: Page,
+ callback?: (spanEnvelope: SpanV2Envelope) => boolean,
+): Promise {
+ const req = await page.waitForRequest(req => {
+ const postData = req.postData();
+ if (!postData) {
+ return false;
+ }
+
+ try {
+ const spanEnvelope = properFullEnvelopeParser(req);
+
+ const envelopeItemHeader = spanEnvelope[1][0][0];
+
+ if (
+ envelopeItemHeader?.type !== 'span' ||
+ envelopeItemHeader?.content_type !== 'application/vnd.sentry.items.span.v2+json'
+ ) {
+ return false;
+ }
+
+ if (callback) {
+ return callback(spanEnvelope);
+ }
+
+ return true;
+ } catch {
+ return false;
+ }
+ });
+
+ return properFullEnvelopeParser(req);
+}
+
+/**
+ * Wait for v2 spans sent in one envelope.
+ * (We might need a more sophisticated helper that waits for N envelopes and buckets by traceId)
+ * For now, this should do.
+ * @param page
+ * @param callback - Callback being called with all spans
+ */
+export async function waitForV2Spans(page: Page, callback?: (spans: SpanV2JSON[]) => boolean): Promise {
+ const spanEnvelope = await waitForSpanV2Envelope(page, envelope => {
+ if (callback) {
+ return callback(envelope[1][0][1].items);
+ }
+ return true;
+ });
+ return spanEnvelope[1][0][1].items;
+}
+
+export function getSpanOp(span: SpanV2JSON): string | undefined {
+ return span.attributes?.['sentry.op']?.type === 'string' ? span.attributes?.['sentry.op']?.value : undefined;
+}
diff --git a/dev-packages/bundle-analyzer-scenarios/package.json b/dev-packages/bundle-analyzer-scenarios/package.json
index 518da1c1e8af..6f031fd35231 100644
--- a/dev-packages/bundle-analyzer-scenarios/package.json
+++ b/dev-packages/bundle-analyzer-scenarios/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/bundle-analyzer-scenarios",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Scenarios to test bundle analysis with",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/dev-packages/bundle-analyzer-scenarios",
diff --git a/dev-packages/bundler-tests/package.json b/dev-packages/bundler-tests/package.json
index b2a307291c90..c1d829609771 100644
--- a/dev-packages/bundler-tests/package.json
+++ b/dev-packages/bundler-tests/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/bundler-tests",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Bundler tests for Sentry Browser SDK",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/bundler-tests",
@@ -13,7 +13,7 @@
},
"dependencies": {
"@rollup/plugin-node-resolve": "^15.2.3",
- "@sentry/browser": "10.27.0",
+ "@sentry/browser": "10.28.0-alpha.0",
"rollup": "^4.0.0",
"vite": "^5.0.0",
"vitest": "^3.2.4",
diff --git a/dev-packages/clear-cache-gh-action/package.json b/dev-packages/clear-cache-gh-action/package.json
index 9592fba2a84d..0738eb261acd 100644
--- a/dev-packages/clear-cache-gh-action/package.json
+++ b/dev-packages/clear-cache-gh-action/package.json
@@ -1,7 +1,7 @@
{
"name": "@sentry-internal/clear-cache-gh-action",
"description": "An internal Github Action to clear GitHub caches.",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"license": "MIT",
"engines": {
"node": ">=18"
diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json
index 89c3abb7d5e8..a11005b34132 100644
--- a/dev-packages/cloudflare-integration-tests/package.json
+++ b/dev-packages/cloudflare-integration-tests/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/cloudflare-integration-tests",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -14,11 +14,11 @@
},
"dependencies": {
"@langchain/langgraph": "^1.0.1",
- "@sentry/cloudflare": "10.27.0"
+ "@sentry/cloudflare": "10.28.0-alpha.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250922.0",
- "@sentry-internal/test-utils": "10.27.0",
+ "@sentry-internal/test-utils": "10.28.0-alpha.0",
"eslint-plugin-regexp": "^1.15.0",
"vitest": "^3.2.4",
"wrangler": "4.22.0"
diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json
index be48359c7045..98a6cce88105 100644
--- a/dev-packages/e2e-tests/package.json
+++ b/dev-packages/e2e-tests/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/e2e-tests",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"license": "MIT",
"private": true,
"scripts": {
diff --git a/dev-packages/external-contributor-gh-action/package.json b/dev-packages/external-contributor-gh-action/package.json
index 8c839a63b4aa..57028e8745b7 100644
--- a/dev-packages/external-contributor-gh-action/package.json
+++ b/dev-packages/external-contributor-gh-action/package.json
@@ -1,7 +1,7 @@
{
"name": "@sentry-internal/external-contributor-gh-action",
"description": "An internal Github Action to add external contributors to the CHANGELOG.md file.",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"license": "MIT",
"engines": {
"node": ">=18"
diff --git a/dev-packages/node-core-integration-tests/package.json b/dev-packages/node-core-integration-tests/package.json
index a5be477c7334..411c4a93685f 100644
--- a/dev-packages/node-core-integration-tests/package.json
+++ b/dev-packages/node-core-integration-tests/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/node-core-integration-tests",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -34,8 +34,8 @@
"@opentelemetry/resources": "^2.2.0",
"@opentelemetry/sdk-trace-base": "^2.2.0",
"@opentelemetry/semantic-conventions": "^1.37.0",
- "@sentry/core": "10.27.0",
- "@sentry/node-core": "10.27.0",
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node-core": "10.28.0-alpha.0",
"body-parser": "^1.20.3",
"cors": "^2.8.5",
"cron": "^3.1.6",
diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json
index 8a81e262d33f..bb2e9aeaad69 100644
--- a/dev-packages/node-integration-tests/package.json
+++ b/dev-packages/node-integration-tests/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/node-integration-tests",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -36,9 +36,9 @@
"@nestjs/core": "^11",
"@nestjs/platform-express": "^11",
"@prisma/client": "6.15.0",
- "@sentry/aws-serverless": "10.27.0",
- "@sentry/core": "10.27.0",
- "@sentry/node": "10.27.0",
+ "@sentry/aws-serverless": "10.28.0-alpha.0",
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node": "10.28.0-alpha.0",
"@types/mongodb": "^3.6.20",
"@types/mysql": "^2.15.21",
"@types/pg": "^8.6.5",
@@ -83,7 +83,7 @@
"yargs": "^16.2.0"
},
"devDependencies": {
- "@sentry-internal/test-utils": "10.27.0",
+ "@sentry-internal/test-utils": "10.28.0-alpha.0",
"@types/amqplib": "^0.10.5",
"@types/node-cron": "^3.0.11",
"@types/node-schedule": "^2.1.7",
diff --git a/dev-packages/node-integration-tests/suites/tracing/meta-tags-twp-errors/test.ts b/dev-packages/node-integration-tests/suites/tracing/meta-tags-twp-errors/test.ts
index d58f35b02972..c1859a3a67b6 100644
--- a/dev-packages/node-integration-tests/suites/tracing/meta-tags-twp-errors/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/meta-tags-twp-errors/test.ts
@@ -1,5 +1,6 @@
import { afterAll, describe, expect, test } from 'vitest';
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
+import { run } from 'node:test';
describe('errors in TwP mode have same trace in trace context and getTraceData()', () => {
afterAll(() => {
@@ -8,11 +9,16 @@ describe('errors in TwP mode have same trace in trace context and getTraceData()
// In a request handler, the spanId is consistent inside of the request
test('in incoming request', async () => {
+ let firstTraceId: string | undefined;
+
const runner = createRunner(__dirname, 'server.js')
.expect({
event: event => {
const { contexts } = event;
const { trace_id, span_id } = contexts?.trace || {};
+ if (!firstTraceId) {
+ firstTraceId = trace_id;
+ }
expect(trace_id).toMatch(/^[a-f\d]{32}$/);
expect(span_id).toMatch(/^[a-f\d]{16}$/);
@@ -28,8 +34,15 @@ describe('errors in TwP mode have same trace in trace context and getTraceData()
expect(traceData.metaTags).not.toContain('sentry-sampled=');
},
})
+ .expect({
+ event: event => {
+ expect(event.contexts?.trace?.trace_id).toBeDefined();
+ expect(event.contexts?.trace?.trace_id).toBe(firstTraceId);
+ },
+ })
.start();
runner.makeRequest('get', '/test');
+ runner.makeRequest('get', '/test');
await runner.completed();
});
diff --git a/dev-packages/node-overhead-gh-action/package.json b/dev-packages/node-overhead-gh-action/package.json
index d044b8047bcc..6114135d6372 100644
--- a/dev-packages/node-overhead-gh-action/package.json
+++ b/dev-packages/node-overhead-gh-action/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/node-overhead-gh-action",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -23,7 +23,7 @@
"fix": "eslint . --format stylish --fix"
},
"dependencies": {
- "@sentry/node": "10.27.0",
+ "@sentry/node": "10.28.0-alpha.0",
"express": "^4.21.1",
"mysql2": "^3.14.4"
},
diff --git a/dev-packages/rollup-utils/package.json b/dev-packages/rollup-utils/package.json
index 3c8e3a029d28..e8abf4524a8e 100644
--- a/dev-packages/rollup-utils/package.json
+++ b/dev-packages/rollup-utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/rollup-utils",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Rollup utilities used at Sentry for the Sentry JavaScript SDK",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/rollup-utils",
diff --git a/dev-packages/size-limit-gh-action/package.json b/dev-packages/size-limit-gh-action/package.json
index 59d885937a9f..0b387059de8b 100644
--- a/dev-packages/size-limit-gh-action/package.json
+++ b/dev-packages/size-limit-gh-action/package.json
@@ -1,7 +1,7 @@
{
"name": "@sentry-internal/size-limit-gh-action",
"description": "An internal Github Action to compare the current size of a PR against the one on develop.",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"license": "MIT",
"engines": {
"node": ">=18"
diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json
index 3df8bcc441ea..9356acdc5e38 100644
--- a/dev-packages/test-utils/package.json
+++ b/dev-packages/test-utils/package.json
@@ -1,6 +1,6 @@
{
"private": true,
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"name": "@sentry-internal/test-utils",
"author": "Sentry",
"license": "MIT",
@@ -48,7 +48,7 @@
},
"devDependencies": {
"@playwright/test": "~1.53.2",
- "@sentry/core": "10.27.0",
+ "@sentry/core": "10.28.0-alpha.0",
"eslint-plugin-regexp": "^1.15.0"
},
"volta": {
diff --git a/lerna.json b/lerna.json
index 9ad4c26bf2a7..b98a81054dc9 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"npmClient": "yarn"
}
diff --git a/packages/angular/package.json b/packages/angular/package.json
index 72d29ca6b272..7e5e5bc78d3c 100644
--- a/packages/angular/package.json
+++ b/packages/angular/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/angular",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for Angular",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular",
@@ -21,8 +21,8 @@
"rxjs": "^6.5.5 || ^7.x"
},
"dependencies": {
- "@sentry/browser": "10.27.0",
- "@sentry/core": "10.27.0",
+ "@sentry/browser": "10.28.0-alpha.0",
+ "@sentry/core": "10.28.0-alpha.0",
"tslib": "^2.4.1"
},
"devDependencies": {
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 99d5eabae3e9..7cd0fc448cf3 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/astro",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for Astro",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/astro",
@@ -56,9 +56,9 @@
"astro": ">=3.x || >=4.0.0-beta || >=5.x"
},
"dependencies": {
- "@sentry/browser": "10.27.0",
- "@sentry/core": "10.27.0",
- "@sentry/node": "10.27.0",
+ "@sentry/browser": "10.28.0-alpha.0",
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node": "10.28.0-alpha.0",
"@sentry/vite-plugin": "^4.1.0"
},
"devDependencies": {
diff --git a/packages/astro/src/server/sdk.ts b/packages/astro/src/server/sdk.ts
index 25dbb9416fe6..7e9597f9f39e 100644
--- a/packages/astro/src/server/sdk.ts
+++ b/packages/astro/src/server/sdk.ts
@@ -15,6 +15,7 @@ export function init(options: NodeOptions): NodeClient | undefined {
const client = initNodeSdk(opts);
+ // TODO (span-streaming): remove this event processor. In this case, can probably just disable http integration server spans
client?.addEventProcessor(
Object.assign(
(event: Event) => {
diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json
index 2aaaa292cd93..a5716771303c 100644
--- a/packages/aws-serverless/package.json
+++ b/packages/aws-serverless/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/aws-serverless",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for AWS Lambda and AWS Serverless Environments",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/aws-serverless",
@@ -69,9 +69,9 @@
"@opentelemetry/instrumentation": "^0.208.0",
"@opentelemetry/instrumentation-aws-sdk": "0.64.0",
"@opentelemetry/semantic-conventions": "^1.37.0",
- "@sentry/core": "10.27.0",
- "@sentry/node": "10.27.0",
- "@sentry/node-core": "10.27.0",
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node": "10.28.0-alpha.0",
+ "@sentry/node-core": "10.28.0-alpha.0",
"@types/aws-lambda": "^8.10.62"
},
"devDependencies": {
diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json
index 37ac4a317bc8..7d43623493d1 100644
--- a/packages/browser-utils/package.json
+++ b/packages/browser-utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/browser-utils",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Browser Utilities for all Sentry JavaScript SDKs",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser-utils",
@@ -39,7 +39,7 @@
"access": "public"
},
"dependencies": {
- "@sentry/core": "10.27.0"
+ "@sentry/core": "10.28.0-alpha.0"
},
"scripts": {
"build": "run-p build:transpile build:types",
diff --git a/packages/browser/package.json b/packages/browser/package.json
index 2ecab4e6119e..ec061d50efe0 100644
--- a/packages/browser/package.json
+++ b/packages/browser/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/browser",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for browsers",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser",
@@ -44,14 +44,14 @@
"access": "public"
},
"dependencies": {
- "@sentry-internal/browser-utils": "10.27.0",
- "@sentry-internal/feedback": "10.27.0",
- "@sentry-internal/replay": "10.27.0",
- "@sentry-internal/replay-canvas": "10.27.0",
- "@sentry/core": "10.27.0"
+ "@sentry-internal/browser-utils": "10.28.0-alpha.0",
+ "@sentry-internal/feedback": "10.28.0-alpha.0",
+ "@sentry-internal/replay": "10.28.0-alpha.0",
+ "@sentry-internal/replay-canvas": "10.28.0-alpha.0",
+ "@sentry/core": "10.28.0-alpha.0"
},
"devDependencies": {
- "@sentry-internal/integration-shims": "10.27.0",
+ "@sentry-internal/integration-shims": "10.28.0-alpha.0",
"fake-indexeddb": "^6.2.4"
},
"scripts": {
diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts
index 03416fa41af7..4869a894edfc 100644
--- a/packages/browser/src/index.ts
+++ b/packages/browser/src/index.ts
@@ -51,6 +51,7 @@ export {
startInactiveSpan,
startSpanManual,
withActiveSpan,
+ withStreamSpan,
startNewTrace,
getSpanDescendants,
setMeasurement,
@@ -80,3 +81,4 @@ export { growthbookIntegration } from './integrations/featureFlags/growthbook';
export { statsigIntegration } from './integrations/featureFlags/statsig';
export { diagnoseSdkConnectivity } from './diagnose-sdk';
export { webWorkerIntegration, registerWebWorker } from './integrations/webWorker';
+export { spanStreamingIntegration } from './integrations/spanstreaming';
diff --git a/packages/browser/src/integrations/httpcontext.ts b/packages/browser/src/integrations/httpcontext.ts
index 9517b2364e83..254e867301af 100644
--- a/packages/browser/src/integrations/httpcontext.ts
+++ b/packages/browser/src/integrations/httpcontext.ts
@@ -8,6 +8,8 @@ import { getHttpRequestData, WINDOW } from '../helpers';
export const httpContextIntegration = defineIntegration(() => {
return {
name: 'HttpContext',
+ // TODO (span-streaming): probably fine to omit this in favour of us globally
+ // already adding request context data but should double-check this
preprocessEvent(event) {
// if none of the information we want exists, don't bother
if (!WINDOW.navigator && !WINDOW.location && !WINDOW.document) {
diff --git a/packages/browser/src/integrations/spanstreaming.ts b/packages/browser/src/integrations/spanstreaming.ts
new file mode 100644
index 000000000000..a3d0d0d326e0
--- /dev/null
+++ b/packages/browser/src/integrations/spanstreaming.ts
@@ -0,0 +1,152 @@
+import type { Client, IntegrationFn, Span, SpanV2JSON } from '@sentry/core';
+import {
+ captureSpan,
+ createSpanV2Envelope,
+ debug,
+ defineIntegration,
+ getDynamicSamplingContextFromSpan,
+ INTERNAL_getSegmentSpan,
+ isV2BeforeSendSpanCallback,
+ showSpanDropWarning,
+ spanToV2JSON,
+} from '@sentry/core';
+import { DEBUG_BUILD } from '../debug-build';
+
+export interface SpanStreamingOptions {
+ batchLimit: number;
+}
+
+export const spanStreamingIntegration = defineIntegration(((userOptions?: Partial) => {
+ const validatedUserProvidedBatchLimit =
+ userOptions?.batchLimit && userOptions.batchLimit <= 1000 && userOptions.batchLimit >= 1
+ ? userOptions.batchLimit
+ : undefined;
+
+ if (DEBUG_BUILD && userOptions?.batchLimit && !validatedUserProvidedBatchLimit) {
+ debug.warn('SpanStreaming batchLimit must be between 1 and 1000, defaulting to 1000');
+ }
+
+ const options: SpanStreamingOptions = {
+ ...userOptions,
+ batchLimit:
+ userOptions?.batchLimit && userOptions.batchLimit <= 1000 && userOptions.batchLimit >= 1
+ ? userOptions.batchLimit
+ : 1000,
+ };
+
+ // key: traceId-segmentSpanId
+ const spanTreeMap = new Map>();
+
+ return {
+ name: 'SpanStreaming',
+ setup(client) {
+ const clientOptions = client.getOptions();
+ const beforeSendSpan = clientOptions.beforeSendSpan;
+
+ const initialMessage = 'spanStreamingIntegration requires';
+ const fallbackMsg = 'Falling back to static trace lifecycle.';
+
+ if (clientOptions.traceLifecycle !== 'stream') {
+ DEBUG_BUILD && debug.warn(`${initialMessage} \`traceLifecycle\` to be set to "stream"! ${fallbackMsg}`);
+ return;
+ }
+
+ if (beforeSendSpan && !isV2BeforeSendSpanCallback(beforeSendSpan)) {
+ client.getOptions().traceLifecycle = 'static';
+ debug.warn(`${initialMessage} a beforeSendSpan callback using \`withStreamSpan\`! ${fallbackMsg}`);
+ return;
+ }
+
+ client.on('enqueueSpan', span => {
+ const spanTreeMapKey = getSpanTreeMapKey(span);
+ const spanBuffer = spanTreeMap.get(spanTreeMapKey);
+ if (spanBuffer) {
+ spanBuffer.add(span);
+ } else {
+ spanTreeMap.set(spanTreeMapKey, new Set([span]));
+ }
+ });
+
+ client.on('afterSpanEnd', span => {
+ captureSpan(span, client);
+ });
+
+ // For now, we send all spans on local segment (root) span end.
+ // TODO: This will change once we have more concrete ideas about a universal SDK data buffer.
+ client.on('afterSegmentSpanEnd', segmentSpan => {
+ sendSegment(segmentSpan, {
+ spanTreeMap: spanTreeMap,
+ client,
+ batchLimit: options.batchLimit,
+ beforeSendSpan,
+ });
+ });
+ },
+ };
+}) satisfies IntegrationFn);
+
+interface SpanProcessingOptions {
+ client: Client;
+ spanTreeMap: Map>;
+ batchLimit: number;
+ beforeSendSpan: ((span: SpanV2JSON) => SpanV2JSON) | undefined;
+}
+
+/**
+ * Just the traceid alone isn't enough because there can be multiple span trees with the same traceid.
+ */
+function getSpanTreeMapKey(span: Span): string {
+ return `${span.spanContext().traceId}-${INTERNAL_getSegmentSpan(span).spanContext().spanId}`;
+}
+
+function sendSegment(
+ segmentSpan: Span,
+ { client, spanTreeMap, batchLimit, beforeSendSpan }: SpanProcessingOptions,
+): void {
+ const traceId = segmentSpan.spanContext().traceId;
+ const spanTreeMapKey = getSpanTreeMapKey(segmentSpan);
+ const spansOfTrace = spanTreeMap.get(spanTreeMapKey);
+
+ if (!spansOfTrace?.size) {
+ spanTreeMap.delete(spanTreeMapKey);
+ return;
+ }
+
+ const finalSpans = Array.from(spansOfTrace).map(span => {
+ const spanJson = spanToV2JSON(span);
+ if (beforeSendSpan) {
+ return applyBeforeSendSpanCallback(spanJson, beforeSendSpan);
+ }
+ return spanJson;
+ });
+
+ const batches: SpanV2JSON[][] = [];
+ for (let i = 0; i < finalSpans.length; i += batchLimit) {
+ batches.push(finalSpans.slice(i, i + batchLimit));
+ }
+
+ DEBUG_BUILD && debug.log(`Sending trace ${traceId} in ${batches.length} batch${batches.length === 1 ? '' : 'es'}`);
+
+ const dsc = getDynamicSamplingContextFromSpan(segmentSpan);
+
+ for (const batch of batches) {
+ const envelope = createSpanV2Envelope(batch, dsc, client);
+ // no need to handle client reports for network errors,
+ // buffer overflows or rate limiting here. All of this is handled
+ // by client and transport.
+ client.sendEnvelope(envelope).then(null, reason => {
+ DEBUG_BUILD && debug.error('Error while sending span stream envelope:', reason);
+ });
+ }
+
+ spanTreeMap.delete(spanTreeMapKey);
+}
+
+function applyBeforeSendSpanCallback(span: SpanV2JSON, beforeSendSpan: (span: SpanV2JSON) => SpanV2JSON): SpanV2JSON {
+ const modifedSpan = beforeSendSpan(span);
+ if (!modifedSpan) {
+ showSpanDropWarning();
+ return span;
+ }
+ return modifedSpan;
+}
diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts
index 025b08b12168..692fc131230b 100644
--- a/packages/browser/src/tracing/request.ts
+++ b/packages/browser/src/tracing/request.ts
@@ -156,6 +156,7 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial {
if (event.type === 'transaction' && event.spans) {
event.spans.forEach(span => {
diff --git a/packages/bun/package.json b/packages/bun/package.json
index f1464af7a732..371734066768 100644
--- a/packages/bun/package.json
+++ b/packages/bun/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/bun",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for bun",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/bun",
@@ -39,8 +39,8 @@
"access": "public"
},
"dependencies": {
- "@sentry/core": "10.27.0",
- "@sentry/node": "10.27.0"
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node": "10.28.0-alpha.0"
},
"devDependencies": {
"bun-types": "^1.2.9"
diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json
index 163afddca271..57f524d663e7 100644
--- a/packages/cloudflare/package.json
+++ b/packages/cloudflare/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/cloudflare",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for Cloudflare Workers and Pages",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/cloudflare",
@@ -50,7 +50,7 @@
},
"dependencies": {
"@opentelemetry/api": "^1.9.0",
- "@sentry/core": "10.27.0"
+ "@sentry/core": "10.28.0-alpha.0"
},
"peerDependencies": {
"@cloudflare/workers-types": "^4.x"
diff --git a/packages/core/package.json b/packages/core/package.json
index 76ac4bc3e96f..e6c1c0d4bcd7 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/core",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Base implementation for all Sentry JavaScript SDKs",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/core",
diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts
index 805d8e596528..f39e2d05ddd5 100644
--- a/packages/core/src/client.ts
+++ b/packages/core/src/client.ts
@@ -34,6 +34,7 @@ import type { SeverityLevel } from './types-hoist/severity';
import type { Span, SpanAttributes, SpanContextData, SpanJSON } from './types-hoist/span';
import type { StartSpanOptions } from './types-hoist/startSpanOptions';
import type { Transport, TransportMakeRequestResponse } from './types-hoist/transport';
+import { isV2BeforeSendSpanCallback } from './utils/beforeSendSpan';
import { createClientReportEnvelope } from './utils/clientreport';
import { debug } from './utils/debug-logger';
import { dsnToString, makeDsn } from './utils/dsn';
@@ -607,6 +608,20 @@ export abstract class Client {
*/
public on(hook: 'spanEnd', callback: (span: Span) => void): () => void;
+ // Hooks reserved for Span-First span processing:
+ /**
+ * Register a callback for after a span is ended.
+ */
+ public on(hook: 'afterSpanEnd', callback: (span: Span) => void): () => void;
+ /**
+ * Register a callback for after a segment span is ended.
+ */
+ public on(hook: 'afterSegmentSpanEnd', callback: (span: Span) => void): () => void;
+ /**
+ * Register a callback for when the span is ready to be enqueued into the span buffer.
+ */
+ public on(hook: 'enqueueSpan', callback: (span: Span) => void): () => void;
+
/**
* Register a callback for when an idle span is allowed to auto-finish.
* @returns {() => void} A function that, when executed, removes the registered callback.
@@ -879,6 +894,14 @@ export abstract class Client {
/** Fire a hook whenever a span ends. */
public emit(hook: 'spanEnd', span: Span): void;
+ // Hooks reserved for Span-First span processing:
+ /** Fire a hook after the `spanEnd` hook */
+ public emit(hook: 'afterSpanEnd', span: Span): void;
+ /** Fire a hook after the `segmentSpanEnd` hook is fired. */
+ public emit(hook: 'afterSegmentSpanEnd', span: Span): void;
+ /** Fire a hook after a span ready to be enqueued into the span buffer. */
+ public emit(hook: 'enqueueSpan', span: Span): void;
+
/**
* Fire a hook indicating that an idle span is allowed to auto finish.
*/
@@ -1492,13 +1515,17 @@ function _validateBeforeSendResult(
/**
* Process the matching `beforeSendXXX` callback.
*/
+// eslint-disable-next-line complexity
function processBeforeSend(
client: Client,
options: ClientOptions,
event: Event,
hint: EventHint,
): PromiseLike | Event | null {
- const { beforeSend, beforeSendTransaction, beforeSendSpan, ignoreSpans } = options;
+ const { beforeSend, beforeSendTransaction, ignoreSpans } = options;
+
+ const beforeSendSpan = !isV2BeforeSendSpanCallback(options.beforeSendSpan) && options.beforeSendSpan;
+
let processedEvent = event;
if (isErrorEvent(processedEvent) && beforeSend) {
diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts
index 875056890e0e..aa392314d1db 100644
--- a/packages/core/src/envelope.ts
+++ b/packages/core/src/envelope.ts
@@ -11,13 +11,17 @@ import type {
RawSecurityItem,
SessionEnvelope,
SessionItem,
+ SpanContainerItem,
SpanEnvelope,
SpanItem,
+ SpanV2Envelope,
} from './types-hoist/envelope';
import type { Event } from './types-hoist/event';
import type { SdkInfo } from './types-hoist/sdkinfo';
import type { SdkMetadata } from './types-hoist/sdkmetadata';
import type { Session, SessionAggregates } from './types-hoist/session';
+import type { SpanV2JSON } from './types-hoist/span';
+import { isV2BeforeSendSpanCallback } from './utils/beforeSendSpan';
import { dsnToString } from './utils/dsn';
import {
createEnvelope,
@@ -120,10 +124,6 @@ export function createEventEnvelope(
* Takes an optional client and runs spans through `beforeSendSpan` if available.
*/
export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client?: Client): SpanEnvelope {
- function dscHasRequiredProps(dsc: Partial): dsc is DynamicSamplingContext {
- return !!dsc.trace_id && !!dsc.public_key;
- }
-
// For the moment we'll obtain the DSC from the first span in the array
// This might need to be changed if we permit sending multiple spans from
// different segments in one envelope
@@ -138,7 +138,8 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client?
...(!!tunnel && dsn && { dsn: dsnToString(dsn) }),
};
- const { beforeSendSpan, ignoreSpans } = client?.getOptions() || {};
+ const options = client?.getOptions();
+ const ignoreSpans = options?.ignoreSpans;
const filteredSpans = ignoreSpans?.length
? spans.filter(span => !shouldIgnoreSpan(spanToJSON(span), ignoreSpans))
@@ -149,10 +150,14 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client?
client?.recordDroppedEvent('before_send', 'span', droppedSpans);
}
- const convertToSpanJSON = beforeSendSpan
+ // checking against traceLifeCycle so that TS can infer the correct type for
+ // beforeSendSpan. This is a workaround for now as most likely, this entire function
+ // will be removed in the future (once we send standalone spans as spans v2)
+ const convertToSpanJSON = options?.beforeSendSpan
? (span: SentrySpan) => {
const spanJson = spanToJSON(span);
- const processedSpan = beforeSendSpan(spanJson);
+ const processedSpan =
+ !isV2BeforeSendSpanCallback(options?.beforeSendSpan) && options?.beforeSendSpan?.(spanJson);
if (!processedSpan) {
showSpanDropWarning();
@@ -174,6 +179,33 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client?
return createEnvelope(headers, items);
}
+/**
+ * Creates a span v2 envelope
+ */
+export function createSpanV2Envelope(
+ serializedSpans: SpanV2JSON[],
+ dsc: Partial,
+ client: Client,
+): SpanV2Envelope {
+ const dsn = client?.getDsn();
+ const tunnel = client?.getOptions().tunnel;
+ const sdk = client?.getOptions()._metadata?.sdk;
+
+ const headers: SpanV2Envelope[0] = {
+ sent_at: new Date().toISOString(),
+ ...(dscHasRequiredProps(dsc) && { trace: dsc }),
+ ...(sdk && { sdk: sdk }),
+ ...(!!tunnel && dsn && { dsn: dsnToString(dsn) }),
+ };
+
+ const spanContainer: SpanContainerItem = [
+ { type: 'span', item_count: serializedSpans.length, content_type: 'application/vnd.sentry.items.span.v2+json' },
+ { items: serializedSpans },
+ ];
+
+ return createEnvelope(headers, [spanContainer]);
+}
+
/**
* Create an Envelope from a CSP report.
*/
@@ -196,3 +228,7 @@ export function createRawSecurityEnvelope(
return createEnvelope(envelopeHeaders, [eventItem]);
}
+
+function dscHasRequiredProps(dsc: Partial): dsc is DynamicSamplingContext {
+ return !!dsc.trace_id && !!dsc.public_key;
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 014a411d0265..6234e6e31ad4 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -9,7 +9,7 @@ export type { IntegrationIndex } from './integration';
export * from './tracing';
export * from './semanticAttributes';
-export { createEventEnvelope, createSessionEnvelope, createSpanEnvelope } from './envelope';
+export { createEventEnvelope, createSessionEnvelope, createSpanEnvelope, createSpanV2Envelope } from './envelope';
export {
captureCheckIn,
withMonitor,
@@ -81,11 +81,16 @@ export {
getSpanDescendants,
getStatusMessage,
getRootSpan,
+ INTERNAL_getSegmentSpan,
getActiveSpan,
addChildSpanToSpan,
spanTimeInputToSeconds,
updateSpanName,
+ spanToV2JSON,
+ showSpanDropWarning,
} from './utils/spanUtils';
+export { captureSpan } from './spans/captureSpan';
+export { attributesFromObject } from './utils/attributes';
export { _setSpanForScope as _INTERNAL_setSpanForScope } from './utils/spanOnScope';
export { parseSampleRate } from './utils/parseSampleRate';
export { applySdkMetadata } from './utils/sdkMetadata';
@@ -321,6 +326,8 @@ export { flushIfServerless } from './utils/flushIfServerless';
export { SDK_VERSION } from './utils/version';
export { getDebugImagesForResources, getFilenameToDebugIdMap } from './utils/debug-ids';
export { escapeStringForRegex } from './vendor/escapeStringForRegex';
+export { isV2BeforeSendSpanCallback, withStreamSpan } from './utils/beforeSendSpan';
+export { shouldIgnoreSpan, reparentChildSpans } from './utils/should-ignore-span';
export type { Attachment } from './types-hoist/attachment';
export type {
@@ -372,6 +379,7 @@ export type {
ProfileChunkEnvelope,
ProfileChunkItem,
SpanEnvelope,
+ SpanV2Envelope,
SpanItem,
LogEnvelope,
MetricEnvelope,
@@ -439,6 +447,7 @@ export type {
SpanJSON,
SpanContextData,
TraceFlag,
+ SpanV2JSON,
} from './types-hoist/span';
export type { SpanStatus } from './types-hoist/spanStatus';
export type { Log, LogSeverityLevel } from './types-hoist/log';
diff --git a/packages/core/src/integrations/eventFilters.ts b/packages/core/src/integrations/eventFilters.ts
index 84ae5d4c4139..4278d234a0f9 100644
--- a/packages/core/src/integrations/eventFilters.ts
+++ b/packages/core/src/integrations/eventFilters.ts
@@ -145,7 +145,7 @@ function _shouldDropEvent(event: Event, options: Partial):
}
} else if (event.type === 'transaction') {
// Filter transactions
-
+ // TODO (span-streaming): replace with ignoreSpans defaults (if we have any)
if (_isIgnoredTransaction(event, options.ignoreTransactions)) {
DEBUG_BUILD &&
debug.warn(
diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts
index a72fbed70d7e..5a45bc9c9861 100644
--- a/packages/core/src/integrations/requestdata.ts
+++ b/packages/core/src/integrations/requestdata.ts
@@ -40,6 +40,8 @@ const _requestDataIntegration = ((options: RequestDataIntegrationOptions = {}) =
return {
name: INTEGRATION_NAME,
+ // TODO (span-streaming): probably fine to leave as-is for errors.
+ // For spans, we go through global context -> attribute conversion or omit this completely (TBD)
processEvent(event, _hint, client) {
const { sdkProcessingMetadata = {} } = event;
const { normalizedRequest, ipAddress } = sdkProcessingMetadata;
diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts
index 9b90809c0091..ac7bc2c3b188 100644
--- a/packages/core/src/semanticAttributes.ts
+++ b/packages/core/src/semanticAttributes.ts
@@ -77,3 +77,27 @@ export const SEMANTIC_ATTRIBUTE_URL_FULL = 'url.full';
* @see https://develop.sentry.dev/sdk/telemetry/traces/span-links/#link-types
*/
export const SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE = 'sentry.link.type';
+
+// some attributes for now exclusively used for span streaming
+// @see https://develop.sentry.dev/sdk/telemetry/spans/span-protocol/#common-attribute-keys
+
+/** The release version of the application */
+export const SEMANTIC_ATTRIBUTE_SENTRY_RELEASE = 'sentry.release';
+/** The environment name (e.g., "production", "staging", "development") */
+export const SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT = 'sentry.environment';
+/** The segment name (e.g., "GET /users") */
+export const SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME = 'sentry.segment.name';
+/** The id of the segment that this span belongs to. */
+export const SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID = 'sentry.segment.id';
+/** The user ID (gated by sendDefaultPii) */
+export const SEMANTIC_ATTRIBUTE_USER_ID = 'user.id';
+/** The user email (gated by sendDefaultPii) */
+export const SEMANTIC_ATTRIBUTE_USER_EMAIL = 'user.email';
+/** The user IP address (gated by sendDefaultPii) */
+export const SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS = 'user.ip_address';
+/** The user username (gated by sendDefaultPii) */
+export const SEMANTIC_ATTRIBUTE_USER_USERNAME = 'user.name';
+/** The name of the Sentry SDK (e.g., "sentry.php", "sentry.javascript") */
+export const SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME = 'sentry.sdk.name';
+/** The version of the Sentry SDK */
+export const SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION = 'sentry.sdk.version';
diff --git a/packages/core/src/spans/captureSpan.ts b/packages/core/src/spans/captureSpan.ts
new file mode 100644
index 000000000000..6f43588f6cb1
--- /dev/null
+++ b/packages/core/src/spans/captureSpan.ts
@@ -0,0 +1,137 @@
+import { type RawAttributes, isAttributeObject } from '../attributes';
+import type { Client } from '../client';
+import { getClient, getGlobalScope } from '../currentScopes';
+import { DEBUG_BUILD } from '../debug-build';
+import type { Scope, ScopeData } from '../scope';
+import {
+ SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT,
+ SEMANTIC_ATTRIBUTE_SENTRY_RELEASE,
+ SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME,
+ SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION,
+ SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID,
+ SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME,
+ SEMANTIC_ATTRIBUTE_USER_EMAIL,
+ SEMANTIC_ATTRIBUTE_USER_ID,
+ SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS,
+ SEMANTIC_ATTRIBUTE_USER_USERNAME,
+} from '../semanticAttributes';
+import { getCapturedScopesOnSpan } from '../tracing/utils';
+import type { Span, SpanV2JSON } from '../types-hoist/span';
+import { mergeScopeData } from '../utils/applyScopeDataToEvent';
+import { debug } from '../utils/debug-logger';
+import { INTERNAL_getSegmentSpan, spanToV2JSON } from '../utils/spanUtils';
+
+/**
+ * Captures a span and returns it to the caller, to be enqueued for sending.
+ */
+export function captureSpan(span: Span, client = getClient()): void {
+ if (!client) {
+ DEBUG_BUILD && debug.warn('No client available to capture span.');
+ return;
+ }
+
+ const segmentSpan = INTERNAL_getSegmentSpan(span);
+ const serializedSegmentSpan = spanToV2JSON(segmentSpan);
+
+ const { isolationScope: spanIsolationScope, scope: spanScope } = getCapturedScopesOnSpan(span);
+ const finalScopeData = getFinalScopeData(spanIsolationScope, spanScope);
+
+ const originalAttributeKeys = Object.keys(serializedSegmentSpan.attributes ?? {});
+
+ applyCommonSpanAttributes(span, serializedSegmentSpan, client, finalScopeData, originalAttributeKeys);
+
+ if (span === segmentSpan) {
+ applyScopeToSegmentSpan(span, finalScopeData, originalAttributeKeys);
+ }
+
+ // Wondering where we apply the beforeSendSpan callback?
+ // We apply it directly before sending the span,
+ // so whenever the buffer this span gets enqueued in is being flushed.
+ // Why? Because we have to enqueue the span instance itself, not a JSON object.
+ // We could temporarily convert to JSON here but this means that we'd then again
+ // have to mutate the `span` instance (doesn't work for every kind of object mutation)
+ // or construct a fully new span object. The latter is risky because users (or we) could hold
+ // references to the original span instance.
+ client.emit('enqueueSpan', span);
+}
+
+function applyScopeToSegmentSpan(segmentSpan: Span, scopeData: ScopeData, originalAttributeKeys: string[]): void {
+ // TODO: Apply all scope data from auto instrumentation (contexts, request) to segment span
+ const { attributes } = scopeData;
+ if (attributes) {
+ setAttributesIfNotPresent(segmentSpan, originalAttributeKeys, attributes);
+ }
+}
+
+function applyCommonSpanAttributes(
+ span: Span,
+ serializedSegmentSpan: SpanV2JSON,
+ client: Client,
+ scopeData: ScopeData,
+ originalAttributeKeys: string[],
+): void {
+ const sdk = client.getSdkMetadata();
+ const { release, environment, sendDefaultPii } = client.getOptions();
+
+ // avoid overwriting any previously set attributes (from users or potentially our SDK instrumentation)
+ setAttributesIfNotPresent(span, originalAttributeKeys, {
+ [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: release,
+ [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: environment,
+ [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: serializedSegmentSpan.name,
+ [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: serializedSegmentSpan.span_id,
+ [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: sdk?.sdk?.name,
+ [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: sdk?.sdk?.version,
+ ...(sendDefaultPii
+ ? {
+ [SEMANTIC_ATTRIBUTE_USER_ID]: scopeData.user?.id,
+ [SEMANTIC_ATTRIBUTE_USER_EMAIL]: scopeData.user?.email,
+ [SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS]: scopeData.user?.ip_address ?? undefined,
+ [SEMANTIC_ATTRIBUTE_USER_USERNAME]: scopeData.user?.username,
+ }
+ : {}),
+ });
+}
+
+// TODO: Extract this to a helper in core. It's used in multiple places.
+function getFinalScopeData(isolationScope: Scope | undefined, scope: Scope | undefined): ScopeData {
+ const finalScopeData = getGlobalScope().getScopeData();
+ if (isolationScope) {
+ mergeScopeData(finalScopeData, isolationScope.getScopeData());
+ }
+ if (scope) {
+ mergeScopeData(finalScopeData, scope.getScopeData());
+ }
+ return finalScopeData;
+}
+
+function setAttributesIfNotPresent(
+ span: Span,
+ originalAttributeKeys: string[],
+ newAttributes: RawAttributes>,
+): void {
+ Object.keys(newAttributes).forEach(key => {
+ if (!originalAttributeKeys.includes(key)) {
+ setAttributeOnSpanWithMaybeUnit(span, key, newAttributes[key]);
+ }
+ });
+}
+
+function setAttributeOnSpanWithMaybeUnit(span: Span, attributeKey: string, attributeValue: unknown): void {
+ if (isAttributeObject(attributeValue)) {
+ const { value, unit } = attributeValue;
+
+ if (isSupportedAttributeType(value)) {
+ span.setAttribute(attributeKey, value);
+ }
+
+ if (unit) {
+ span.setAttribute(`${attributeKey}.unit`, unit);
+ }
+ } else if (isSupportedAttributeType(attributeValue)) {
+ span.setAttribute(attributeKey, attributeValue);
+ }
+}
+
+function isSupportedAttributeType(value: unknown): value is Parameters[1] {
+ return ['string', 'number', 'boolean'].includes(typeof value) || Array.isArray(value);
+}
diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts
index 9bd98b9741c6..574ba9ab2478 100644
--- a/packages/core/src/tracing/sentrySpan.ts
+++ b/packages/core/src/tracing/sentrySpan.ts
@@ -1,3 +1,4 @@
+/* eslint-disable max-lines */
import { getClient, getCurrentScope } from '../currentScopes';
import { DEBUG_BUILD } from '../debug-build';
import { createSpanEnvelope } from '../envelope';
@@ -21,6 +22,7 @@ import type {
SpanJSON,
SpanOrigin,
SpanTimeInput,
+ SpanV2JSON,
} from '../types-hoist/span';
import type { SpanStatus } from '../types-hoist/spanStatus';
import type { TimedEvent } from '../types-hoist/timedEvent';
@@ -31,6 +33,9 @@ import {
getRootSpan,
getSpanDescendants,
getStatusMessage,
+ getV2Attributes,
+ getV2SpanLinks,
+ getV2StatusMessage,
spanTimeInputToSeconds,
spanToJSON,
spanToTransactionTraceContext,
@@ -241,6 +246,30 @@ export class SentrySpan implements Span {
};
}
+ /**
+ * Get SpanV2JSON representation of this span.
+ *
+ * @hidden
+ * @internal This method is purely for internal purposes and should not be used outside
+ * of SDK code. If you need to get a JSON representation of a span,
+ * use `spanToV2JSON(span)` instead.
+ */
+ public getSpanV2JSON(): SpanV2JSON {
+ return {
+ name: this._name ?? '',
+ span_id: this._spanId,
+ trace_id: this._traceId,
+ parent_span_id: this._parentSpanId,
+ start_timestamp: this._startTime,
+ // just in case _endTime is not set, we use the start time (i.e. duration 0)
+ end_timestamp: this._endTime ?? this._startTime,
+ is_segment: this._isStandaloneSpan || this === getRootSpan(this),
+ status: getV2StatusMessage(this._status),
+ attributes: getV2Attributes(this._attributes),
+ links: getV2SpanLinks(this._links),
+ };
+ }
+
/** @inheritdoc */
public isRecording(): boolean {
return !this._endTime && !!this._sampled;
@@ -287,6 +316,7 @@ export class SentrySpan implements Span {
const client = getClient();
if (client) {
client.emit('spanEnd', this);
+ client.emit('afterSpanEnd', this);
}
// A segment span is basically the root span of a local span tree.
@@ -310,6 +340,10 @@ export class SentrySpan implements Span {
}
}
return;
+ } else if (client?.getOptions().traceLifecycle === 'stream') {
+ // TODO (spans): Remove standalone span custom logic in favor of sending simple v2 web vital spans
+ client?.emit('afterSegmentSpanEnd', this);
+ return;
}
const transactionEvent = this._convertSpanToTransaction();
diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts
index 98c3a33a8a79..8aa254437879 100644
--- a/packages/core/src/tracing/trace.ts
+++ b/packages/core/src/tracing/trace.ts
@@ -490,6 +490,7 @@ function _startChildSpan(parentSpan: Span, scope: Scope, spanArguments: SentrySp
// If it has an endTimestamp, it's already ended
if (spanArguments.endTimestamp) {
client.emit('spanEnd', childSpan);
+ client.emit('afterSpanEnd', childSpan);
}
}
diff --git a/packages/core/src/tracing/vercel-ai/index.ts b/packages/core/src/tracing/vercel-ai/index.ts
index f07244088ff9..8f51e3996fe7 100644
--- a/packages/core/src/tracing/vercel-ai/index.ts
+++ b/packages/core/src/tracing/vercel-ai/index.ts
@@ -70,6 +70,7 @@ function onVercelAiSpanStart(span: Span): void {
processGenerateSpan(span, name, attributes);
}
+// TODO (span-streaming): move to client hook. What to do about parent modifications?
function vercelAiEventProcessor(event: Event): Event {
if (event.type === 'transaction' && event.spans) {
// Map to accumulate token data by parent span ID
diff --git a/packages/core/src/types-hoist/attributes.ts b/packages/core/src/types-hoist/attributes.ts
new file mode 100644
index 000000000000..56b3658f8c20
--- /dev/null
+++ b/packages/core/src/types-hoist/attributes.ts
@@ -0,0 +1,20 @@
+export type SerializedAttributes = Record;
+export type SerializedAttribute = (
+ | {
+ type: 'string';
+ value: string;
+ }
+ | {
+ type: 'integer';
+ value: number;
+ }
+ | {
+ type: 'double';
+ value: number;
+ }
+ | {
+ type: 'boolean';
+ value: boolean;
+ }
+) & { unit?: 'ms' | 's' | 'bytes' | 'count' | 'percent' };
+export type SerializedAttributeType = 'string' | 'integer' | 'double' | 'boolean';
diff --git a/packages/core/src/types-hoist/envelope.ts b/packages/core/src/types-hoist/envelope.ts
index 272f8cde9f62..7251f85b5df0 100644
--- a/packages/core/src/types-hoist/envelope.ts
+++ b/packages/core/src/types-hoist/envelope.ts
@@ -11,7 +11,7 @@ import type { Profile, ProfileChunk } from './profiling';
import type { ReplayEvent, ReplayRecordingData } from './replay';
import type { SdkInfo } from './sdkinfo';
import type { SerializedSession, SessionAggregates } from './session';
-import type { SpanJSON } from './span';
+import type { SerializedSpanContainer, SpanJSON } from './span';
// Based on: https://develop.sentry.dev/sdk/envelopes/
@@ -91,6 +91,21 @@ type CheckInItemHeaders = { type: 'check_in' };
type ProfileItemHeaders = { type: 'profile' };
type ProfileChunkItemHeaders = { type: 'profile_chunk' };
type SpanItemHeaders = { type: 'span' };
+type SpanContainerItemHeaders = {
+ /**
+ * Same as v1 span item type but this envelope is distinguished by {@link SpanContainerItemHeaders.content_type}.
+ */
+ type: 'span';
+ /**
+ * The number of span items in the container. This must be the same as the number of span items in the payload.
+ */
+ item_count: number;
+ /**
+ * The content type of the span items. This must be `application/vnd.sentry.items.span.v2+json`.
+ * (the presence of this field also distinguishes the span item from the v1 span item)
+ */
+ content_type: 'application/vnd.sentry.items.span.v2+json';
+};
type LogContainerItemHeaders = {
type: 'log';
/**
@@ -123,6 +138,7 @@ export type FeedbackItem = BaseEnvelopeItem;
export type ProfileItem = BaseEnvelopeItem;
export type ProfileChunkItem = BaseEnvelopeItem;
export type SpanItem = BaseEnvelopeItem>;
+export type SpanContainerItem = BaseEnvelopeItem;
export type LogContainerItem = BaseEnvelopeItem;
export type MetricContainerItem = BaseEnvelopeItem;
export type RawSecurityItem = BaseEnvelopeItem;
@@ -133,6 +149,7 @@ type CheckInEnvelopeHeaders = { trace?: DynamicSamplingContext };
type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders;
type ReplayEnvelopeHeaders = BaseEnvelopeHeaders;
type SpanEnvelopeHeaders = BaseEnvelopeHeaders & { trace?: DynamicSamplingContext };
+type SpanV2EnvelopeHeaders = BaseEnvelopeHeaders & { trace?: DynamicSamplingContext };
type LogEnvelopeHeaders = BaseEnvelopeHeaders;
type MetricEnvelopeHeaders = BaseEnvelopeHeaders;
export type EventEnvelope = BaseEnvelope<
@@ -144,6 +161,7 @@ export type ClientReportEnvelope = BaseEnvelope;
export type SpanEnvelope = BaseEnvelope;
+export type SpanV2Envelope = BaseEnvelope;
export type ProfileChunkEnvelope = BaseEnvelope;
export type RawSecurityEnvelope = BaseEnvelope;
export type LogEnvelope = BaseEnvelope;
@@ -157,6 +175,7 @@ export type Envelope =
| ReplayEnvelope
| CheckInEnvelope
| SpanEnvelope
+ | SpanV2Envelope
| RawSecurityEnvelope
| LogEnvelope
| MetricEnvelope;
diff --git a/packages/core/src/types-hoist/link.ts b/packages/core/src/types-hoist/link.ts
index a330dc108b00..9a117258200b 100644
--- a/packages/core/src/types-hoist/link.ts
+++ b/packages/core/src/types-hoist/link.ts
@@ -22,9 +22,9 @@ export interface SpanLink {
* Link interface for the event envelope item. It's a flattened representation of `SpanLink`.
* Can include additional fields defined by OTel.
*/
-export interface SpanLinkJSON extends Record {
+export interface SpanLinkJSON extends Record {
span_id: string;
trace_id: string;
sampled?: boolean;
- attributes?: SpanLinkAttributes;
+ attributes?: TAttributes;
}
diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts
index c33d0107df5f..80d57ae8cea8 100644
--- a/packages/core/src/types-hoist/options.ts
+++ b/packages/core/src/types-hoist/options.ts
@@ -1,3 +1,4 @@
+import { RawAttributes } from '../attributes';
import type { CaptureContext } from '../scope';
import type { Breadcrumb, BreadcrumbHint } from './breadcrumb';
import type { ErrorEvent, EventHint, TransactionEvent } from './event';
@@ -6,7 +7,7 @@ import type { Log } from './log';
import type { Metric } from './metric';
import type { TracesSamplerSamplingContext } from './samplingcontext';
import type { SdkMetadata } from './sdkmetadata';
-import type { SpanJSON } from './span';
+import type { SpanJSON, SpanV2JSON } from './span';
import type { StackLineParser, StackParser } from './stacktrace';
import type { TracePropagationTargets } from './tracing';
import type { BaseTransportOptions, Transport } from './transport';
@@ -382,6 +383,16 @@ export interface ClientOptions SpanJSON;
+ beforeSendSpan?: ((span: SpanJSON) => SpanJSON) | SpanV2CompatibleBeforeSendSpanCallback;
/**
* An event-processing callback for transaction events, guaranteed to be invoked after all other event
@@ -497,6 +508,12 @@ export interface ClientOptions Breadcrumb | null;
}
+/**
+ * A callback that is known to be compatible with actually receiving and returning a span v2 JSON object.
+ * Only useful in conjunction with the {@link CoreOptions.traceLifecycle} option.
+ */
+export type SpanV2CompatibleBeforeSendSpanCallback = ((span: SpanV2JSON) => SpanV2JSON) & { _v2: true };
+
/** Base configuration options for every SDK. */
export interface CoreOptions
extends Omit>, 'integrations' | 'transport' | 'stackParser'> {
diff --git a/packages/core/src/types-hoist/span.ts b/packages/core/src/types-hoist/span.ts
index d82463768b7f..0295f21d19dd 100644
--- a/packages/core/src/types-hoist/span.ts
+++ b/packages/core/src/types-hoist/span.ts
@@ -1,3 +1,4 @@
+import type { SerializedAttributes } from './attributes';
import type { SpanLink, SpanLinkJSON } from './link';
import type { Measurements } from './measurement';
import type { HrTime } from './opentelemetry';
@@ -34,6 +35,23 @@ export type SpanAttributes = Partial<{
/** This type is aligned with the OpenTelemetry TimeInput type. */
export type SpanTimeInput = HrTime | number | Date;
+export interface SpanV2JSON {
+ trace_id: string;
+ parent_span_id?: string;
+ span_id: string;
+ name: string;
+ start_timestamp: number;
+ end_timestamp: number;
+ status: 'ok' | 'error';
+ is_segment: boolean;
+ attributes?: SerializedAttributes;
+ links?: SpanLinkJSON[];
+}
+
+export type SerializedSpanContainer = {
+ items: Array;
+};
+
/** A JSON representation of a span. */
export interface SpanJSON {
data: SpanAttributes;
diff --git a/packages/core/src/utils/attributes.ts b/packages/core/src/utils/attributes.ts
new file mode 100644
index 000000000000..99419ce1afbd
--- /dev/null
+++ b/packages/core/src/utils/attributes.ts
@@ -0,0 +1,98 @@
+import type { SerializedAttribute } from '../types-hoist/attributes';
+import type { SpanAttributes } from '../types-hoist/span';
+import { normalize } from '../utils/normalize';
+
+/**
+ * Converts an attribute value to a serialized attribute value object, containing
+ * a type descriptor as well as the value.
+ *
+ * TODO: dedupe this with the logs version of the function (didn't do this yet to avoid
+ * dependance on logs/spans for the open questions RE array and object attribute types)
+ *
+ * @param value - The value of the log attribute.
+ * @returns The serialized log attribute.
+ */
+export function attributeValueToSerializedAttribute(value: unknown): SerializedAttribute {
+ switch (typeof value) {
+ case 'number':
+ if (Number.isInteger(value)) {
+ return {
+ value,
+ type: 'integer',
+ };
+ }
+ return {
+ value,
+ type: 'double',
+ };
+ case 'boolean':
+ return {
+ value,
+ type: 'boolean',
+ };
+ case 'string':
+ return {
+ value,
+ type: 'string',
+ };
+ default: {
+ let stringValue = '';
+ try {
+ stringValue = JSON.stringify(value) ?? '';
+ } catch {
+ // Do nothing
+ }
+ return {
+ value: stringValue,
+ type: 'string',
+ };
+ }
+ }
+}
+
+/**
+ * Given an object that might contain keys with primitive, array, or object values,
+ * return a SpanAttributes object that flattens the object into a single level.
+ * - Nested keys are separated by '.'.
+ * - arrays are stringified (TODO: might change, depending on how we support array attributes)
+ * - objects are flattened
+ * - primitives are added directly
+ * - nullish values are ignored
+ * - maxDepth is the maximum depth to flatten the object to
+ *
+ * @param obj - The object to flatten into span attributes
+ * @returns The span attribute object
+ */
+export function attributesFromObject(obj: Record, maxDepth = 3): SpanAttributes {
+ const result: Record = {};
+
+ function primitiveOrToString(current: unknown): number | boolean | string {
+ if (typeof current === 'number' || typeof current === 'boolean' || typeof current === 'string') {
+ return current;
+ }
+ return String(current);
+ }
+
+ function flatten(current: unknown, prefix: string, depth: number): void {
+ if (current == null) {
+ return;
+ } else if (depth >= maxDepth) {
+ result[prefix] = primitiveOrToString(current);
+ return;
+ } else if (Array.isArray(current)) {
+ result[prefix] = JSON.stringify(current);
+ } else if (typeof current === 'number' || typeof current === 'string' || typeof current === 'boolean') {
+ result[prefix] = current;
+ } else if (typeof current === 'object' && current !== null && !Array.isArray(current) && depth < maxDepth) {
+ for (const [key, value] of Object.entries(current as Record)) {
+ flatten(value, prefix ? `${prefix}.${key}` : key, depth + 1);
+ }
+ }
+ }
+
+ const normalizedObj = normalize(obj, maxDepth);
+
+ flatten(normalizedObj, '', 0);
+
+ return result;
+}
diff --git a/packages/core/src/utils/beforeSendSpan.ts b/packages/core/src/utils/beforeSendSpan.ts
new file mode 100644
index 000000000000..3bfe2fa0c301
--- /dev/null
+++ b/packages/core/src/utils/beforeSendSpan.ts
@@ -0,0 +1,32 @@
+import type { ClientOptions, SpanV2CompatibleBeforeSendSpanCallback } from '../types-hoist/options';
+import type { SpanV2JSON } from '../types-hoist/span';
+import { addNonEnumerableProperty } from './object';
+
+/**
+ * A wrapper to use the new span format in your `beforeSendSpan` callback.
+ *
+ * @example
+ *
+ * Sentry.init({
+ * beforeSendSpan: withStreamSpan((span) => {
+ * return span;
+ * }),
+ * });
+ *
+ * @param callback
+ * @returns
+ */
+export function withStreamSpan(callback: (span: SpanV2JSON) => SpanV2JSON): SpanV2CompatibleBeforeSendSpanCallback {
+ addNonEnumerableProperty(callback, '_v2', true);
+ // type-casting here because TS can't infer the type correctly
+ return callback as SpanV2CompatibleBeforeSendSpanCallback;
+}
+
+/**
+ * Typesafe check to identify the expected span json format of the `beforeSendSpan` callback.
+ */
+export function isV2BeforeSendSpanCallback(
+ callback: ClientOptions['beforeSendSpan'],
+): callback is SpanV2CompatibleBeforeSendSpanCallback {
+ return !!callback && '_v2' in callback && !!callback._v2;
+}
diff --git a/packages/core/src/utils/featureFlags.ts b/packages/core/src/utils/featureFlags.ts
index 4fa3cdc5ac8d..671f615b32cf 100644
--- a/packages/core/src/utils/featureFlags.ts
+++ b/packages/core/src/utils/featureFlags.ts
@@ -27,6 +27,7 @@ const SPAN_FLAG_ATTRIBUTE_PREFIX = 'flag.evaluation.';
/**
* Copies feature flags that are in current scope context to the event context
*/
+// TODO (span-streaming): should flags be added to (segment) spans? If so, probably do this via globally applying context data to spans
export function _INTERNAL_copyFlagsFromScopeToEvent(event: Event): Event {
const scope = getCurrentScope();
const flagContext = scope.getScopeData().contexts.flags;
diff --git a/packages/core/src/utils/should-ignore-span.ts b/packages/core/src/utils/should-ignore-span.ts
index a8d3ac0211c7..f05f0dc5402e 100644
--- a/packages/core/src/utils/should-ignore-span.ts
+++ b/packages/core/src/utils/should-ignore-span.ts
@@ -1,28 +1,47 @@
import { DEBUG_BUILD } from '../debug-build';
+import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '../semanticAttributes';
import type { ClientOptions } from '../types-hoist/options';
-import type { SpanJSON } from '../types-hoist/span';
+import type { SpanJSON, SpanV2JSON } from '../types-hoist/span';
import { debug } from './debug-logger';
import { isMatchingPattern } from './string';
-function logIgnoredSpan(droppedSpan: Pick): void {
- debug.log(`Ignoring span ${droppedSpan.op} - ${droppedSpan.description} because it matches \`ignoreSpans\`.`);
+function logIgnoredSpan(spanName: string, spanOp: string | undefined): void {
+ debug.log(`Ignoring span ${spanOp ? `${spanOp} - ` : ''}${spanName} because it matches \`ignoreSpans\`.`);
}
/**
* Check if a span should be ignored based on the ignoreSpans configuration.
*/
export function shouldIgnoreSpan(
- span: Pick,
+ span: Pick | Pick,
ignoreSpans: Required['ignoreSpans'],
): boolean {
- if (!ignoreSpans?.length || !span.description) {
+ if (!ignoreSpans?.length) {
+ return false;
+ }
+
+ const { spanName, spanOp: spanOpAttributeOrString } =
+ 'description' in span
+ ? { spanName: span.description, spanOp: span.op }
+ : 'name' in span
+ ? { spanName: span.name, spanOp: span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] }
+ : { spanName: '', spanOp: '' };
+
+ const spanOp =
+ typeof spanOpAttributeOrString === 'string'
+ ? spanOpAttributeOrString
+ : spanOpAttributeOrString?.type === 'string'
+ ? spanOpAttributeOrString.value
+ : undefined;
+
+ if (!spanName) {
return false;
}
for (const pattern of ignoreSpans) {
if (isStringOrRegExp(pattern)) {
- if (isMatchingPattern(span.description, pattern)) {
- DEBUG_BUILD && logIgnoredSpan(span);
+ if (isMatchingPattern(spanName, pattern)) {
+ DEBUG_BUILD && logIgnoredSpan(spanName, spanOp);
return true;
}
continue;
@@ -32,15 +51,15 @@ export function shouldIgnoreSpan(
continue;
}
- const nameMatches = pattern.name ? isMatchingPattern(span.description, pattern.name) : true;
- const opMatches = pattern.op ? span.op && isMatchingPattern(span.op, pattern.op) : true;
+ const nameMatches = pattern.name ? isMatchingPattern(spanName, pattern.name) : true;
+ const opMatches = pattern.op ? spanOp && isMatchingPattern(spanOp, pattern.op) : true;
// This check here is only correct because we can guarantee that we ran `isMatchingPattern`
// for at least one of `nameMatches` and `opMatches`. So in contrary to how this looks,
// not both op and name actually have to match. This is the most efficient way to check
// for all combinations of name and op patterns.
if (nameMatches && opMatches) {
- DEBUG_BUILD && logIgnoredSpan(span);
+ DEBUG_BUILD && logIgnoredSpan(spanName, spanOp);
return true;
}
}
@@ -52,7 +71,10 @@ export function shouldIgnoreSpan(
* Takes a list of spans, and a span that was dropped, and re-parents the child spans of the dropped span to the parent of the dropped span, if possible.
* This mutates the spans array in place!
*/
-export function reparentChildSpans(spans: SpanJSON[], dropSpan: SpanJSON): void {
+export function reparentChildSpans(
+ spans: Pick[],
+ dropSpan: Pick,
+): void {
const droppedSpanParentId = dropSpan.parent_span_id;
const droppedSpanId = dropSpan.span_id;
diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts
index d7c261ecd73c..7fbe4ac695e1 100644
--- a/packages/core/src/utils/spanUtils.ts
+++ b/packages/core/src/utils/spanUtils.ts
@@ -8,16 +8,18 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from '../semanticAttributes';
import type { SentrySpan } from '../tracing/sentrySpan';
-import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus';
+import { SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus';
import { getCapturedScopesOnSpan } from '../tracing/utils';
+import type { SerializedAttributes } from '../types-hoist/attributes';
import type { TraceContext } from '../types-hoist/context';
import type { SpanLink, SpanLinkJSON } from '../types-hoist/link';
-import type { Span, SpanAttributes, SpanJSON, SpanOrigin, SpanTimeInput } from '../types-hoist/span';
+import type { Span, SpanAttributes, SpanJSON, SpanOrigin, SpanTimeInput, SpanV2JSON } from '../types-hoist/span';
import type { SpanStatus } from '../types-hoist/spanStatus';
import { addNonEnumerableProperty } from '../utils/object';
import { generateSpanId } from '../utils/propagationContext';
import { timestampInSeconds } from '../utils/time';
import { generateSentryTraceHeader, generateTraceparentHeader } from '../utils/tracing';
+import { attributeValueToSerializedAttribute } from './attributes';
import { consoleSandbox } from './debug-logger';
import { _getSpanForScope } from './spanOnScope';
@@ -92,7 +94,7 @@ export function spanToTraceparentHeader(span: Span): string {
* If the links array is empty, it returns `undefined` so the empty value can be dropped before it's sent.
*/
export function convertSpanLinksForEnvelope(links?: SpanLink[]): SpanLinkJSON[] | undefined {
- if (links && links.length > 0) {
+ if (links?.length) {
return links.map(({ context: { spanId, traceId, traceFlags, ...restContext }, attributes }) => ({
span_id: spanId,
trace_id: traceId,
@@ -104,6 +106,24 @@ export function convertSpanLinksForEnvelope(links?: SpanLink[]): SpanLinkJSON[]
return undefined;
}
}
+/**
+ *
+ * @param links
+ * @returns
+ */
+export function getV2SpanLinks(links?: SpanLink[]): SpanLinkJSON[] | undefined {
+ if (links?.length) {
+ return links.map(({ context: { spanId, traceId, traceFlags, ...restContext }, attributes }) => ({
+ span_id: spanId,
+ trace_id: traceId,
+ sampled: traceFlags === TRACE_FLAG_SAMPLED,
+ ...(attributes && { attributes: getV2Attributes(attributes) }),
+ ...restContext,
+ }));
+ } else {
+ return undefined;
+ }
+}
/**
* Convert a span time input into a timestamp in seconds.
@@ -187,6 +207,59 @@ export function spanToJSON(span: Span): SpanJSON {
};
}
+/**
+ * Convert a span to a SpanV2JSON representation.
+ * @returns
+ */
+export function spanToV2JSON(span: Span): SpanV2JSON {
+ if (spanIsSentrySpan(span)) {
+ return span.getSpanV2JSON();
+ }
+
+ const { spanId: span_id, traceId: trace_id } = span.spanContext();
+
+ // Handle a span from @opentelemetry/sdk-base-trace's `Span` class
+ if (spanIsOpenTelemetrySdkTraceBaseSpan(span)) {
+ const { attributes, startTime, name, endTime, status, links } = span;
+
+ // In preparation for the next major of OpenTelemetry, we want to support
+ // looking up the parent span id according to the new API
+ // In OTel v1, the parent span id is accessed as `parentSpanId`
+ // In OTel v2, the parent span id is accessed as `spanId` on the `parentSpanContext`
+ const parentSpanId =
+ 'parentSpanId' in span
+ ? span.parentSpanId
+ : 'parentSpanContext' in span
+ ? (span.parentSpanContext as { spanId?: string } | undefined)?.spanId
+ : undefined;
+
+ return {
+ name,
+ span_id,
+ trace_id,
+ parent_span_id: parentSpanId,
+ start_timestamp: spanTimeInputToSeconds(startTime),
+ end_timestamp: spanTimeInputToSeconds(endTime),
+ is_segment: span === INTERNAL_getSegmentSpan(span),
+ status: getV2StatusMessage(status),
+ attributes: getV2Attributes(attributes),
+ links: getV2SpanLinks(links),
+ };
+ }
+
+ // Finally, as a fallback, at least we have `spanContext()`....
+ // This should not actually happen in reality, but we need to handle it for type safety.
+ return {
+ span_id,
+ trace_id,
+ start_timestamp: 0,
+ name: '',
+ end_timestamp: 0,
+ status: 'ok',
+ is_segment: span === INTERNAL_getSegmentSpan(span),
+ };
+}
+
function spanIsOpenTelemetrySdkTraceBaseSpan(span: Span): span is OpenTelemetrySdkTraceBaseSpan {
const castSpan = span as Partial;
return !!castSpan.attributes && !!castSpan.startTime && !!castSpan.name && !!castSpan.endTime && !!castSpan.status;
@@ -237,6 +310,27 @@ export function getStatusMessage(status: SpanStatus | undefined): string | undef
return status.message || 'internal_error';
}
+/**
+ * Convert the various statuses to the ones expected by Sentry ('ok' is default)
+ */
+export function getV2StatusMessage(status: SpanStatus | undefined): 'ok' | 'error' {
+ return !status ||
+ status.code === SPAN_STATUS_UNSET ||
+ (status.code === SPAN_STATUS_ERROR && status.message === 'unknown_error')
+ ? 'ok'
+ : 'error';
+}
+
+/**
+ * Convert the attributes to the ones expected by Sentry, including the type annotation
+ */
+export function getV2Attributes(attributes: SpanAttributes): SerializedAttributes {
+ return Object.entries(attributes).reduce((acc, [key, value]) => {
+ acc[key] = attributeValueToSerializedAttribute(value);
+ return acc;
+ }, {} as SerializedAttributes);
+}
+
const CHILD_SPANS_FIELD = '_sentryChildSpans';
const ROOT_SPAN_FIELD = '_sentryRootSpan';
@@ -298,7 +392,12 @@ export function getSpanDescendants(span: SpanWithPotentialChildren): Span[] {
/**
* Returns the root span of a given span.
*/
-export function getRootSpan(span: SpanWithPotentialChildren): Span {
+export const getRootSpan = INTERNAL_getSegmentSpan;
+
+/**
+ * Returns the segment span of a given span.
+ */
+export function INTERNAL_getSegmentSpan(span: SpanWithPotentialChildren): Span {
return span[ROOT_SPAN_FIELD] || span;
}
diff --git a/packages/core/test/lib/utils/attributes.test.ts b/packages/core/test/lib/utils/attributes.test.ts
new file mode 100644
index 000000000000..9dd05e0e5b28
--- /dev/null
+++ b/packages/core/test/lib/utils/attributes.test.ts
@@ -0,0 +1,99 @@
+import { describe, expect, it } from 'vitest';
+import { attributesFromObject } from '../../../src/utils/attributes';
+
+describe('attributesFromObject', () => {
+ it('flattens an object', () => {
+ const context = {
+ a: 1,
+ b: { c: { d: 2 } },
+ };
+
+ const result = attributesFromObject(context);
+
+ expect(result).toEqual({
+ a: 1,
+ 'b.c.d': 2,
+ });
+ });
+
+ it('flattens an object with a max depth', () => {
+ const context = {
+ a: 1,
+ b: { c: { d: 2 } },
+ };
+
+ const result = attributesFromObject(context, 2);
+
+ expect(result).toEqual({
+ a: 1,
+ 'b.c': '[Object]',
+ });
+ });
+
+ it('flattens an object an array', () => {
+ const context = {
+ a: 1,
+ b: { c: { d: 2 } },
+ integrations: ['foo', 'bar'],
+ };
+
+ const result = attributesFromObject(context);
+
+ expect(result).toEqual({
+ a: 1,
+ 'b.c.d': 2,
+ integrations: '["foo","bar"]',
+ });
+ });
+
+ it('handles a circular object', () => {
+ const context = {
+ a: 1,
+ b: { c: { d: 2 } },
+ };
+ context.b.c.e = context.b;
+
+ const result = attributesFromObject(context, 5);
+
+ expect(result).toEqual({
+ a: 1,
+ 'b.c.d': 2,
+ 'b.c.e': '[Circular ~]',
+ });
+ });
+
+ it('handles a circular object in an array', () => {
+ const context = {
+ a: 1,
+ b: { c: { d: 2 } },
+ integrations: ['foo', 'bar'],
+ };
+
+ // @ts-expect-error - this is fine
+ context.integrations[0] = context.integrations;
+
+ const result = attributesFromObject(context, 5);
+
+ expect(result).toEqual({
+ a: 1,
+ 'b.c.d': 2,
+ integrations: '["[Circular ~]","bar"]',
+ });
+ });
+
+ it('handles objects in arrays', () => {
+ const context = {
+ a: 1,
+ b: { c: { d: 2 } },
+ integrations: [{ name: 'foo' }, { name: 'bar' }],
+ };
+
+ const result = attributesFromObject(context);
+
+ expect(result).toEqual({
+ a: 1,
+ 'b.c.d': 2,
+ integrations: '[{"name":"foo"},{"name":"bar"}]',
+ });
+ });
+});
diff --git a/packages/deno/package.json b/packages/deno/package.json
index cc40ededd6e7..14092d7967e4 100644
--- a/packages/deno/package.json
+++ b/packages/deno/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/deno",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for Deno",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/deno",
@@ -25,7 +25,7 @@
],
"dependencies": {
"@opentelemetry/api": "^1.9.0",
- "@sentry/core": "10.27.0"
+ "@sentry/core": "10.28.0-alpha.0"
},
"scripts": {
"deno-types": "node ./scripts/download-deno-types.mjs",
diff --git a/packages/deno/src/integrations/context.ts b/packages/deno/src/integrations/context.ts
index 979ffff7d0e8..4dc9c723fbeb 100644
--- a/packages/deno/src/integrations/context.ts
+++ b/packages/deno/src/integrations/context.ts
@@ -56,6 +56,7 @@ const _denoContextIntegration = (() => {
return {
name: INTEGRATION_NAME,
processEvent(event) {
+ // TODO (span-streaming): we probably need to apply this to spans via a hook IF we decide to apply contexts to (segment) spans
return addDenoRuntimeContext(event);
},
};
diff --git a/packages/ember/package.json b/packages/ember/package.json
index a2307bb11013..a16c48cc1b32 100644
--- a/packages/ember/package.json
+++ b/packages/ember/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/ember",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for Ember.js",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember",
@@ -32,8 +32,8 @@
"dependencies": {
"@babel/core": "^7.27.7",
"@embroider/macros": "^1.16.0",
- "@sentry/browser": "10.27.0",
- "@sentry/core": "10.27.0",
+ "@sentry/browser": "10.28.0-alpha.0",
+ "@sentry/core": "10.28.0-alpha.0",
"ember-auto-import": "^2.7.2",
"ember-cli-babel": "^8.2.0",
"ember-cli-htmlbars": "^6.1.1",
diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json
index 8982ce90c026..0522ce601baa 100644
--- a/packages/eslint-config-sdk/package.json
+++ b/packages/eslint-config-sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/eslint-config-sdk",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK eslint config",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-config-sdk",
@@ -22,8 +22,8 @@
"access": "public"
},
"dependencies": {
- "@sentry-internal/eslint-plugin-sdk": "10.27.0",
- "@sentry-internal/typescript": "10.27.0",
+ "@sentry-internal/eslint-plugin-sdk": "10.28.0-alpha.0",
+ "@sentry-internal/typescript": "10.28.0-alpha.0",
"@typescript-eslint/eslint-plugin": "^5.48.0",
"@typescript-eslint/parser": "^5.48.0",
"eslint-config-prettier": "^6.11.0",
diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json
index 2ee8a4be4925..19dffa00d3c3 100644
--- a/packages/eslint-plugin-sdk/package.json
+++ b/packages/eslint-plugin-sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/eslint-plugin-sdk",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK eslint plugin",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-plugin-sdk",
diff --git a/packages/feedback/package.json b/packages/feedback/package.json
index b2b17bad3033..7fadc1543a8c 100644
--- a/packages/feedback/package.json
+++ b/packages/feedback/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/feedback",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Sentry SDK integration for user feedback",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback",
@@ -39,7 +39,7 @@
"access": "public"
},
"dependencies": {
- "@sentry/core": "10.27.0"
+ "@sentry/core": "10.28.0-alpha.0"
},
"devDependencies": {
"preact": "^10.19.4"
diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json
index 8f5059ac9792..702480331aad 100644
--- a/packages/gatsby/package.json
+++ b/packages/gatsby/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/gatsby",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for Gatsby.js",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby",
@@ -45,8 +45,8 @@
"access": "public"
},
"dependencies": {
- "@sentry/core": "10.27.0",
- "@sentry/react": "10.27.0",
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/react": "10.28.0-alpha.0",
"@sentry/webpack-plugin": "^4.6.1"
},
"peerDependencies": {
diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json
index 3495aff8de55..8a0308d2b509 100644
--- a/packages/google-cloud-serverless/package.json
+++ b/packages/google-cloud-serverless/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/google-cloud-serverless",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for Google Cloud Functions",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/google-cloud-serverless",
@@ -48,8 +48,8 @@
"access": "public"
},
"dependencies": {
- "@sentry/core": "10.27.0",
- "@sentry/node": "10.27.0",
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node": "10.28.0-alpha.0",
"@types/express": "^4.17.14"
},
"devDependencies": {
diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json
index 437b12de6c33..f66732c9ccd5 100644
--- a/packages/integration-shims/package.json
+++ b/packages/integration-shims/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/integration-shims",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Shims for integrations in Sentry SDK.",
"main": "build/cjs/index.js",
"module": "build/esm/index.js",
@@ -56,7 +56,7 @@
"url": "https://github.com/getsentry/sentry-javascript/issues"
},
"dependencies": {
- "@sentry/core": "10.27.0"
+ "@sentry/core": "10.28.0-alpha.0"
},
"engines": {
"node": ">=18"
diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json
index 2b4e04b097b9..78a1a59cf09a 100644
--- a/packages/nestjs/package.json
+++ b/packages/nestjs/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/nestjs",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for NestJS",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nestjs",
@@ -49,8 +49,8 @@
"@opentelemetry/instrumentation": "^0.208.0",
"@opentelemetry/instrumentation-nestjs-core": "0.55.0",
"@opentelemetry/semantic-conventions": "^1.37.0",
- "@sentry/core": "10.27.0",
- "@sentry/node": "10.27.0"
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node": "10.28.0-alpha.0"
},
"devDependencies": {
"@nestjs/common": "^10.0.0",
diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json
index 710a7dd33623..f371417270cd 100644
--- a/packages/nextjs/package.json
+++ b/packages/nextjs/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/nextjs",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for Next.js",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs",
@@ -79,13 +79,13 @@
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@rollup/plugin-commonjs": "28.0.1",
- "@sentry-internal/browser-utils": "10.27.0",
+ "@sentry-internal/browser-utils": "10.28.0-alpha.0",
"@sentry/bundler-plugin-core": "^4.6.1",
- "@sentry/core": "10.27.0",
- "@sentry/node": "10.27.0",
- "@sentry/opentelemetry": "10.27.0",
- "@sentry/react": "10.27.0",
- "@sentry/vercel-edge": "10.27.0",
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node": "10.28.0-alpha.0",
+ "@sentry/opentelemetry": "10.28.0-alpha.0",
+ "@sentry/react": "10.28.0-alpha.0",
+ "@sentry/vercel-edge": "10.28.0-alpha.0",
"@sentry/webpack-plugin": "^4.6.1",
"resolve": "1.22.8",
"rollup": "^4.35.0",
diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts
index a171652b7221..6d47ca909425 100644
--- a/packages/nextjs/src/client/index.ts
+++ b/packages/nextjs/src/client/index.ts
@@ -60,11 +60,13 @@ export function init(options: BrowserOptions): Client | undefined {
const client = reactInit(opts);
+ // TODO (span-streaming): replace with ignoreSpans default?
const filterTransactions: EventProcessor = event =>
event.type === 'transaction' && event.transaction === '/404' ? null : event;
filterTransactions.id = 'NextClient404Filter';
addEventProcessor(filterTransactions);
+ // TODO (span-streaming): replace with ignoreSpans default?
const filterIncompleteNavigationTransactions: EventProcessor = event =>
event.type === 'transaction' && event.transaction === INCOMPLETE_APP_ROUTER_INSTRUMENTATION_TRANSACTION_NAME
? null
diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts
index caec9a9f1af1..c955cd1b0ac5 100644
--- a/packages/nextjs/src/server/index.ts
+++ b/packages/nextjs/src/server/index.ts
@@ -227,6 +227,9 @@ export function init(options: NodeOptions): NodeClient | undefined {
}
});
+ // TODO (span-streaming):
+ // - replace with ignoreSpans default
+ // - allow ignoreSpans to filter on arbitrary span attributes (not just op)
getGlobalScope().addEventProcessor(
Object.assign(
(event => {
diff --git a/packages/node-core/package.json b/packages/node-core/package.json
index 57100fc16cf3..8466dd34a2ef 100644
--- a/packages/node-core/package.json
+++ b/packages/node-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/node-core",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Sentry Node-Core SDK",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node-core",
@@ -67,8 +67,8 @@
},
"dependencies": {
"@apm-js-collab/tracing-hooks": "^0.3.1",
- "@sentry/core": "10.27.0",
- "@sentry/opentelemetry": "10.27.0",
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/opentelemetry": "10.28.0-alpha.0",
"import-in-the-middle": "^2"
},
"devDependencies": {
diff --git a/packages/node-core/src/integrations/context.ts b/packages/node-core/src/integrations/context.ts
index cad8a1c4a443..16cdadd9383b 100644
--- a/packages/node-core/src/integrations/context.ts
+++ b/packages/node-core/src/integrations/context.ts
@@ -107,6 +107,7 @@ const _nodeContextIntegration = ((options: ContextOptions = {}) => {
return {
name: INTEGRATION_NAME,
+ // TODO (span-streaming): we probably need to apply this to spans via a hook IF we decide to apply contexts to (segment) spans
processEvent(event) {
return addContext(event);
},
diff --git a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts
index 34741e95c912..9dd2db5b095c 100644
--- a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts
+++ b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts
@@ -216,6 +216,7 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions
},
processEvent(event) {
// Drop transaction if it has a status code that should be ignored
+ // TODO (span-streaming): port this logic to spans via a hook or ignoreSpans default
if (event.type === 'transaction') {
const statusCode = event.contexts?.trace?.data?.['http.response.status_code'];
if (typeof statusCode === 'number') {
diff --git a/packages/node-core/src/integrations/http/index.ts b/packages/node-core/src/integrations/http/index.ts
index 19859b68f3c0..30ae4a468323 100644
--- a/packages/node-core/src/integrations/http/index.ts
+++ b/packages/node-core/src/integrations/http/index.ts
@@ -167,6 +167,7 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) =>
instrumentSentryHttp(httpInstrumentationOptions);
},
+ // TODO (span-streaming): port this logic to spans via a hook or ignoreSpans default; check with serverSpans migration strategy
processEvent(event) {
// Note: We always run this, even if spans are disabled
// The reason being that e.g. the remix integration disables span creation here but still wants to use the ignore status codes option
diff --git a/packages/node-native/package.json b/packages/node-native/package.json
index e193f26ae795..5582eff0d671 100644
--- a/packages/node-native/package.json
+++ b/packages/node-native/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/node-native",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Native Tools for the Official Sentry Node.js SDK",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node-native",
@@ -64,8 +64,8 @@
},
"dependencies": {
"@sentry-internal/node-native-stacktrace": "^0.2.2",
- "@sentry/core": "10.27.0",
- "@sentry/node": "10.27.0"
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node": "10.28.0-alpha.0"
},
"devDependencies": {
"@types/node": "^18.19.1"
diff --git a/packages/node/package.json b/packages/node/package.json
index 95e0610c9828..f2f9fdcd2352 100644
--- a/packages/node/package.json
+++ b/packages/node/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/node",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Sentry Node SDK using OpenTelemetry for performance instrumentation",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node",
@@ -95,9 +95,9 @@
"@opentelemetry/sdk-trace-base": "^2.2.0",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@prisma/instrumentation": "6.19.0",
- "@sentry/core": "10.27.0",
- "@sentry/node-core": "10.27.0",
- "@sentry/opentelemetry": "10.27.0",
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node-core": "10.28.0-alpha.0",
+ "@sentry/opentelemetry": "10.28.0-alpha.0",
"import-in-the-middle": "^2",
"minimatch": "^9.0.0"
},
diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json
index 0ce5df7bb8d5..1cbf841c0e07 100644
--- a/packages/nuxt/package.json
+++ b/packages/nuxt/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/nuxt",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for Nuxt",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nuxt",
@@ -49,13 +49,13 @@
},
"dependencies": {
"@nuxt/kit": "^3.13.2",
- "@sentry/browser": "10.27.0",
- "@sentry/cloudflare": "10.27.0",
- "@sentry/core": "10.27.0",
- "@sentry/node": "10.27.0",
+ "@sentry/browser": "10.28.0-alpha.0",
+ "@sentry/cloudflare": "10.28.0-alpha.0",
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node": "10.28.0-alpha.0",
"@sentry/rollup-plugin": "^4.3.0",
"@sentry/vite-plugin": "^4.3.0",
- "@sentry/vue": "10.27.0"
+ "@sentry/vue": "10.28.0-alpha.0"
},
"devDependencies": {
"@nuxt/module-builder": "^0.8.4",
diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts
index edbd26b3d707..142c4edf1bbd 100644
--- a/packages/nuxt/src/server/sdk.ts
+++ b/packages/nuxt/src/server/sdk.ts
@@ -37,6 +37,7 @@ export function init(options: SentryNuxtServerOptions): Client | undefined {
*
* Only exported for testing
*/
+// TODO (span-streaming): replace with ignoreSpans default
export function lowQualityTransactionsFilter(options: SentryNuxtServerOptions): EventProcessor {
return Object.assign(
(event => {
diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json
index 4a21c884155f..876f5a624ee1 100644
--- a/packages/opentelemetry/package.json
+++ b/packages/opentelemetry/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/opentelemetry",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry utilities for OpenTelemetry",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/opentelemetry",
@@ -39,7 +39,7 @@
"access": "public"
},
"dependencies": {
- "@sentry/core": "10.27.0"
+ "@sentry/core": "10.28.0-alpha.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.9.0",
diff --git a/packages/opentelemetry/src/spanProcessor.ts b/packages/opentelemetry/src/spanProcessor.ts
index 3430456caaee..07d3c92269ae 100644
--- a/packages/opentelemetry/src/spanProcessor.ts
+++ b/packages/opentelemetry/src/spanProcessor.ts
@@ -56,6 +56,7 @@ function onSpanEnd(span: Span): void {
const client = getClient();
client?.emit('spanEnd', span);
+ client?.emit('afterSpanEnd', span);
}
/**
diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json
index f1082191997e..1c830658cc42 100644
--- a/packages/profiling-node/package.json
+++ b/packages/profiling-node/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/profiling-node",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for Node.js Profiling",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/profiling-node",
@@ -63,8 +63,8 @@
},
"dependencies": {
"@sentry-internal/node-cpu-profiler": "^2.2.0",
- "@sentry/core": "10.27.0",
- "@sentry/node": "10.27.0"
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node": "10.28.0-alpha.0"
},
"devDependencies": {
"@types/node": "^18.19.1"
diff --git a/packages/react-router/package.json b/packages/react-router/package.json
index fe662890fa7a..c4e0ea39d187 100644
--- a/packages/react-router/package.json
+++ b/packages/react-router/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/react-router",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for React Router (Framework)",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react-router",
@@ -49,11 +49,11 @@
"@opentelemetry/core": "^2.2.0",
"@opentelemetry/instrumentation": "^0.208.0",
"@opentelemetry/semantic-conventions": "^1.37.0",
- "@sentry/browser": "10.27.0",
+ "@sentry/browser": "10.28.0-alpha.0",
"@sentry/cli": "^2.58.2",
- "@sentry/core": "10.27.0",
- "@sentry/node": "10.27.0",
- "@sentry/react": "10.27.0",
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node": "10.28.0-alpha.0",
+ "@sentry/react": "10.28.0-alpha.0",
"@sentry/vite-plugin": "^4.1.0",
"glob": "11.1.0"
},
diff --git a/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts b/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts
index cc16b03076ec..1f711cf0070a 100644
--- a/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts
+++ b/packages/react-router/src/server/integration/lowQualityTransactionsFilterIntegration.ts
@@ -15,6 +15,7 @@ function _lowQualityTransactionsFilterIntegration(options: NodeOptions): {
return {
name: 'LowQualityTransactionsFilter',
+ // TODO (span-streaming): port this logic to spans via a hook or ignoreSpans default;
processEvent(event: Event, _hint: EventHint, _client: Client): Event | null {
if (event.type !== 'transaction' || !event.transaction) {
return event;
diff --git a/packages/react-router/src/server/integration/reactRouterServer.ts b/packages/react-router/src/server/integration/reactRouterServer.ts
index 4625d1cb979e..10b3fe0ddbd7 100644
--- a/packages/react-router/src/server/integration/reactRouterServer.ts
+++ b/packages/react-router/src/server/integration/reactRouterServer.ts
@@ -30,6 +30,7 @@ export const reactRouterServerIntegration = defineIntegration(() => {
instrumentReactRouterServer();
}
},
+ // TODO (span-streaming): port this logic to spans via a hook or ignoreSpans default;
processEvent(event) {
// Express generates bogus `*` routes for data loaders, which we want to remove here
// we cannot do this earlier because some OTEL instrumentation adds this at some unexpected point
diff --git a/packages/react/package.json b/packages/react/package.json
index 06d56f58b6f5..f7824d6a3ab5 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/react",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for React.js",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react",
@@ -39,8 +39,8 @@
"access": "public"
},
"dependencies": {
- "@sentry/browser": "10.27.0",
- "@sentry/core": "10.27.0",
+ "@sentry/browser": "10.28.0-alpha.0",
+ "@sentry/core": "10.28.0-alpha.0",
"hoist-non-react-statics": "^3.3.2"
},
"peerDependencies": {
diff --git a/packages/remix/package.json b/packages/remix/package.json
index 7230064df963..a846ad04da96 100644
--- a/packages/remix/package.json
+++ b/packages/remix/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/remix",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for Remix",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/remix",
@@ -69,9 +69,9 @@
"@opentelemetry/semantic-conventions": "^1.37.0",
"@remix-run/router": "1.x",
"@sentry/cli": "^2.58.2",
- "@sentry/core": "10.27.0",
- "@sentry/node": "10.27.0",
- "@sentry/react": "10.27.0",
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node": "10.28.0-alpha.0",
+ "@sentry/react": "10.28.0-alpha.0",
"glob": "^10.3.4",
"yargs": "^17.6.0"
},
diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json
index f66b691a42fa..834558b010e3 100644
--- a/packages/replay-canvas/package.json
+++ b/packages/replay-canvas/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/replay-canvas",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Replay canvas integration",
"main": "build/npm/cjs/index.js",
"module": "build/npm/esm/index.js",
@@ -69,8 +69,8 @@
"@sentry-internal/rrweb": "2.40.0"
},
"dependencies": {
- "@sentry-internal/replay": "10.27.0",
- "@sentry/core": "10.27.0"
+ "@sentry-internal/replay": "10.28.0-alpha.0",
+ "@sentry/core": "10.28.0-alpha.0"
},
"engines": {
"node": ">=18"
diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json
index 1cccb4c2ef50..2dffeb6a6edf 100644
--- a/packages/replay-internal/package.json
+++ b/packages/replay-internal/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/replay",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "User replays for Sentry",
"main": "build/npm/cjs/index.js",
"module": "build/npm/esm/index.js",
@@ -81,7 +81,7 @@
"homepage": "https://docs.sentry.io/platforms/javascript/session-replay/",
"devDependencies": {
"@babel/core": "^7.27.7",
- "@sentry-internal/replay-worker": "10.27.0",
+ "@sentry-internal/replay-worker": "10.28.0-alpha.0",
"@sentry-internal/rrweb": "2.40.0",
"@sentry-internal/rrweb-snapshot": "2.40.0",
"fflate": "0.8.2",
@@ -89,8 +89,8 @@
"jsdom-worker": "^0.3.0"
},
"dependencies": {
- "@sentry-internal/browser-utils": "10.27.0",
- "@sentry/core": "10.27.0"
+ "@sentry-internal/browser-utils": "10.28.0-alpha.0",
+ "@sentry/core": "10.28.0-alpha.0"
},
"engines": {
"node": ">=18"
diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json
index cdd3dd4059b1..06e62055a93a 100644
--- a/packages/replay-worker/package.json
+++ b/packages/replay-worker/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/replay-worker",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Worker for @sentry-internal/replay",
"main": "build/esm/index.js",
"module": "build/esm/index.js",
diff --git a/packages/solid/package.json b/packages/solid/package.json
index 9d6138147612..c54037a37403 100644
--- a/packages/solid/package.json
+++ b/packages/solid/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/solid",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for Solid",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solid",
@@ -54,8 +54,8 @@
"access": "public"
},
"dependencies": {
- "@sentry/browser": "10.27.0",
- "@sentry/core": "10.27.0"
+ "@sentry/browser": "10.28.0-alpha.0",
+ "@sentry/core": "10.28.0-alpha.0"
},
"peerDependencies": {
"@solidjs/router": "^0.13.4",
diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json
index 1069905423fa..53e54b38105d 100644
--- a/packages/solidstart/package.json
+++ b/packages/solidstart/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/solidstart",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for Solid Start",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solidstart",
@@ -66,9 +66,9 @@
}
},
"dependencies": {
- "@sentry/core": "10.27.0",
- "@sentry/node": "10.27.0",
- "@sentry/solid": "10.27.0",
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node": "10.28.0-alpha.0",
+ "@sentry/solid": "10.28.0-alpha.0",
"@sentry/vite-plugin": "^4.1.0"
},
"devDependencies": {
diff --git a/packages/solidstart/src/server/utils.ts b/packages/solidstart/src/server/utils.ts
index 8276c32da9e0..0d838c601827 100644
--- a/packages/solidstart/src/server/utils.ts
+++ b/packages/solidstart/src/server/utils.ts
@@ -44,5 +44,6 @@ export function lowQualityTransactionsFilter(options: Options): EventProcessor {
* e.g. to filter out transactions for build assets
*/
export function filterLowQualityTransactions(options: Options): void {
+ // TODO (span-streaming): replace with ignoreSpans defaults
getGlobalScope().addEventProcessor(lowQualityTransactionsFilter(options));
}
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index 83af1efee4a5..93d9cc476444 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/svelte",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for Svelte",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/svelte",
@@ -39,8 +39,8 @@
"access": "public"
},
"dependencies": {
- "@sentry/browser": "10.27.0",
- "@sentry/core": "10.27.0",
+ "@sentry/browser": "10.28.0-alpha.0",
+ "@sentry/core": "10.28.0-alpha.0",
"magic-string": "^0.30.0"
},
"peerDependencies": {
diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json
index a39ef877222e..5645cf317c7f 100644
--- a/packages/sveltekit/package.json
+++ b/packages/sveltekit/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/sveltekit",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for SvelteKit",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/sveltekit",
@@ -48,10 +48,10 @@
},
"dependencies": {
"@babel/parser": "7.26.9",
- "@sentry/cloudflare": "10.27.0",
- "@sentry/core": "10.27.0",
- "@sentry/node": "10.27.0",
- "@sentry/svelte": "10.27.0",
+ "@sentry/cloudflare": "10.28.0-alpha.0",
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node": "10.28.0-alpha.0",
+ "@sentry/svelte": "10.28.0-alpha.0",
"@sentry/vite-plugin": "^4.1.0",
"magic-string": "0.30.7",
"recast": "0.23.11",
diff --git a/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts b/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts
index 5ab24a731279..696b158da912 100644
--- a/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts
+++ b/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts
@@ -11,6 +11,7 @@ export function svelteKitSpansIntegration(): Integration {
name: 'SvelteKitSpansEnhancement',
// Using preprocessEvent to ensure the processing happens before user-configured
// event processors are executed
+ // TODO (span-streaming): replace with client hook
preprocessEvent(event) {
// only iterate over the spans if the root span was emitted by SvelteKit
// TODO: Right now, we can't optimize this to only check traces with a kit-emitted root span
diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json
index dac0c980b1f5..b6d73d7091af 100644
--- a/packages/tanstackstart-react/package.json
+++ b/packages/tanstackstart-react/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/tanstackstart-react",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for TanStack Start React",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tanstackstart-react",
@@ -52,10 +52,10 @@
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/semantic-conventions": "^1.37.0",
- "@sentry-internal/browser-utils": "10.27.0",
- "@sentry/core": "10.27.0",
- "@sentry/node": "10.27.0",
- "@sentry/react": "10.27.0"
+ "@sentry-internal/browser-utils": "10.28.0-alpha.0",
+ "@sentry/core": "10.28.0-alpha.0",
+ "@sentry/node": "10.28.0-alpha.0",
+ "@sentry/react": "10.28.0-alpha.0"
},
"scripts": {
"build": "run-p build:transpile build:types",
diff --git a/packages/tanstackstart/package.json b/packages/tanstackstart/package.json
index 6a99cfefdb99..25bda5f78f17 100644
--- a/packages/tanstackstart/package.json
+++ b/packages/tanstackstart/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/tanstackstart",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Utilities for the Sentry TanStack Start SDKs",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tanstackstart",
diff --git a/packages/types/package.json b/packages/types/package.json
index 70cd501b4eb6..d2c4c5c997db 100644
--- a/packages/types/package.json
+++ b/packages/types/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/types",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Types for all Sentry JavaScript SDKs",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/types",
@@ -57,7 +57,7 @@
"yalc:publish": "yalc publish --push --sig"
},
"dependencies": {
- "@sentry/core": "10.27.0"
+ "@sentry/core": "10.28.0-alpha.0"
},
"volta": {
"extends": "../../package.json"
diff --git a/packages/typescript/package.json b/packages/typescript/package.json
index f1d5c80f4954..d5d0a1c960f5 100644
--- a/packages/typescript/package.json
+++ b/packages/typescript/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry-internal/typescript",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Typescript configuration used at Sentry",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/typescript",
diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json
index 90d61f964f34..70442b9dc5b1 100644
--- a/packages/vercel-edge/package.json
+++ b/packages/vercel-edge/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/vercel-edge",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for the Vercel Edge Runtime",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vercel-edge",
@@ -41,14 +41,14 @@
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/resources": "^2.2.0",
- "@sentry/core": "10.27.0"
+ "@sentry/core": "10.28.0-alpha.0"
},
"devDependencies": {
"@edge-runtime/types": "3.0.1",
"@opentelemetry/core": "^2.2.0",
"@opentelemetry/sdk-trace-base": "^2.2.0",
"@opentelemetry/semantic-conventions": "^1.37.0",
- "@sentry/opentelemetry": "10.27.0"
+ "@sentry/opentelemetry": "10.28.0-alpha.0"
},
"scripts": {
"build": "run-p build:transpile build:types",
diff --git a/packages/vue/package.json b/packages/vue/package.json
index 6cb37663ebbf..0d6802a733ec 100644
--- a/packages/vue/package.json
+++ b/packages/vue/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/vue",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Official Sentry SDK for Vue.js",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vue",
@@ -39,8 +39,8 @@
"access": "public"
},
"dependencies": {
- "@sentry/browser": "10.27.0",
- "@sentry/core": "10.27.0"
+ "@sentry/browser": "10.28.0-alpha.0",
+ "@sentry/core": "10.28.0-alpha.0"
},
"peerDependencies": {
"pinia": "2.x || 3.x",
diff --git a/packages/wasm/package.json b/packages/wasm/package.json
index a864eca972f7..1eb810c031f4 100644
--- a/packages/wasm/package.json
+++ b/packages/wasm/package.json
@@ -1,6 +1,6 @@
{
"name": "@sentry/wasm",
- "version": "10.27.0",
+ "version": "10.28.0-alpha.0",
"description": "Support for WASM.",
"repository": "git://github.com/getsentry/sentry-javascript.git",
"homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/wasm",
@@ -39,8 +39,8 @@
"access": "public"
},
"dependencies": {
- "@sentry/browser": "10.27.0",
- "@sentry/core": "10.27.0"
+ "@sentry/browser": "10.28.0-alpha.0",
+ "@sentry/core": "10.28.0-alpha.0"
},
"scripts": {
"build": "run-p build:transpile build:bundle build:types",