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",