diff --git a/.size-limit.js b/.size-limit.js index 17e33dd7ff21..b5d0644ff088 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -38,7 +38,14 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '41 KB', + limit: '42 KB', + }, + { + name: '@sentry/browser (incl. Tracing with Span Streaming)', + path: 'packages/browser/build/npm/esm/index.js', + import: createImport('init', 'browserTracingIntegration', 'spanStreamingIntegration'), + gzip: true, + limit: '44 KB', }, { name: '@sentry/browser (incl. Tracing, Replay)', @@ -75,14 +82,14 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'), gzip: true, - limit: '84 KB', + limit: '85 KB', }, { name: '@sentry/browser (incl. Tracing, Replay, Feedback)', path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration'), gzip: true, - limit: '97 KB', + limit: '98 KB', }, { name: '@sentry/browser (incl. Feedback)', @@ -96,7 +103,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'sendFeedback'), gzip: true, - limit: '30 KB', + limit: '31 KB', }, { name: '@sentry/browser (incl. FeedbackAsync)', @@ -120,7 +127,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '43 KB', + limit: '44 KB', }, // Vue SDK (ESM) { @@ -128,14 +135,14 @@ 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)', path: 'packages/vue/build/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '43 KB', + limit: '44 KB', }, // Svelte SDK (ESM) { @@ -150,7 +157,7 @@ module.exports = [ name: 'CDN Bundle', path: createCDNPath('bundle.min.js'), gzip: true, - limit: '27 KB', + limit: '27.5 KB', }, { name: 'CDN Bundle (incl. Tracing)', @@ -183,7 +190,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.min.js'), gzip: false, brotli: false, - limit: '124 KB', + limit: '125 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed', @@ -215,7 +222,7 @@ module.exports = [ import: createImport('init'), ignore: ['$app/stores'], gzip: true, - limit: '42 KB', + limit: '43 KB', }, // Node-Core SDK (ESM) { diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c68921b62c4..a419eeb33818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ 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 336ecd045f40..56d920731b43 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.20.0", + "version": "10.21.0-alpha.1", "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.20.0", + "@sentry/browser": "10.21.0-alpha.1", "@supabase/supabase-js": "2.49.3", "axios": "^1.12.2", "babel-loader": "^8.2.2", diff --git a/dev-packages/bundle-analyzer-scenarios/package.json b/dev-packages/bundle-analyzer-scenarios/package.json index 124361baaa25..52cc31047324 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.20.0", + "version": "10.21.0-alpha.1", "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/clear-cache-gh-action/package.json b/dev-packages/clear-cache-gh-action/package.json index 21c4ce680659..216c4065bbc4 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.20.0", + "version": "10.21.0-alpha.1", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index fbc4f1c991a4..ff16ead70047 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.20.0", + "version": "10.21.0-alpha.1", "license": "MIT", "engines": { "node": ">=18" @@ -13,11 +13,11 @@ "test:watch": "yarn test --watch" }, "dependencies": { - "@sentry/cloudflare": "10.20.0" + "@sentry/cloudflare": "10.21.0-alpha.1" }, "devDependencies": { "@cloudflare/workers-types": "^4.20250922.0", - "@sentry-internal/test-utils": "10.20.0", + "@sentry-internal/test-utils": "10.21.0-alpha.1", "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 a5892494bd16..939950ba54de 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.20.0", + "version": "10.21.0-alpha.1", "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 d3848bd8c087..ae85d61891bb 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.20.0", + "version": "10.21.0-alpha.1", "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 f78ace6957b3..aaa2bfa7f45c 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.20.0", + "version": "10.21.0-alpha.1", "license": "MIT", "engines": { "node": ">=18" @@ -34,8 +34,8 @@ "@opentelemetry/resources": "^2.1.0", "@opentelemetry/sdk-trace-base": "^2.1.0", "@opentelemetry/semantic-conventions": "^1.37.0", - "@sentry/core": "10.20.0", - "@sentry/node-core": "10.20.0", + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node-core": "10.21.0-alpha.1", "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 5d7a02f7327c..56175bfef3d2 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.20.0", + "version": "10.21.0-alpha.1", "license": "MIT", "engines": { "node": ">=18" @@ -33,9 +33,9 @@ "@nestjs/core": "^11", "@nestjs/platform-express": "^11", "@prisma/client": "6.15.0", - "@sentry/aws-serverless": "10.20.0", - "@sentry/core": "10.20.0", - "@sentry/node": "10.20.0", + "@sentry/aws-serverless": "10.21.0-alpha.1", + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node": "10.21.0-alpha.1", "@types/mongodb": "^3.6.20", "@types/mysql": "^2.15.21", "@types/pg": "^8.6.5", @@ -80,7 +80,7 @@ "yargs": "^16.2.0" }, "devDependencies": { - "@sentry-internal/test-utils": "10.20.0", + "@sentry-internal/test-utils": "10.21.0-alpha.1", "@types/amqplib": "^0.10.5", "@types/node-cron": "^3.0.11", "@types/node-schedule": "^2.1.7", diff --git a/dev-packages/node-overhead-gh-action/package.json b/dev-packages/node-overhead-gh-action/package.json index 2e48ecfc5d12..985a4e651f46 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.20.0", + "version": "10.21.0-alpha.1", "license": "MIT", "engines": { "node": ">=18" @@ -23,7 +23,7 @@ "fix": "eslint . --format stylish --fix" }, "dependencies": { - "@sentry/node": "10.20.0", + "@sentry/node": "10.21.0-alpha.1", "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 c733d1a7a10b..c5cd52f2dcb3 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.20.0", + "version": "10.21.0-alpha.1", "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 b5da92310446..1136c1089ead 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.20.0", + "version": "10.21.0-alpha.1", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json index 2f1d906fd4e1..3264c557d339 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "10.20.0", + "version": "10.21.0-alpha.1", "name": "@sentry-internal/test-utils", "author": "Sentry", "license": "MIT", @@ -48,7 +48,7 @@ }, "devDependencies": { "@playwright/test": "~1.53.2", - "@sentry/core": "10.20.0" + "@sentry/core": "10.21.0-alpha.1" }, "volta": { "extends": "../../package.json" diff --git a/lerna.json b/lerna.json index d4b1a98ac901..ce2daa9127e3 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "npmClient": "yarn" } diff --git a/packages/angular/package.json b/packages/angular/package.json index 758a6aa17e53..55fb742ab2ff 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "description": "Official Sentry SDK for Angular", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular", @@ -21,8 +21,8 @@ "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "10.20.0", - "@sentry/core": "10.20.0", + "@sentry/browser": "10.21.0-alpha.1", + "@sentry/core": "10.21.0-alpha.1", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/astro/package.json b/packages/astro/package.json index c3ae4e4116ff..77dab33b8e36 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/astro", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0", - "@sentry/core": "10.20.0", - "@sentry/node": "10.20.0", + "@sentry/browser": "10.21.0-alpha.1", + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node": "10.21.0-alpha.1", "@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 2c9d74fcd065..571a7aacf7c1 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/aws-serverless", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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,8 +69,8 @@ "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/instrumentation-aws-sdk": "0.59.0", "@opentelemetry/semantic-conventions": "^1.37.0", - "@sentry/core": "10.20.0", - "@sentry/node": "10.20.0", + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node": "10.21.0-alpha.1", "@types/aws-lambda": "^8.10.62" }, "devDependencies": { diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index 363b725915ba..280cf064464d 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-utils", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0" + "@sentry/core": "10.21.0-alpha.1" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/browser/package.json b/packages/browser/package.json index 3b01f272299f..60c894e76d41 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/browser", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "description": "Official Sentry SDK for browsers", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser", @@ -39,14 +39,14 @@ "access": "public" }, "dependencies": { - "@sentry-internal/browser-utils": "10.20.0", - "@sentry-internal/feedback": "10.20.0", - "@sentry-internal/replay": "10.20.0", - "@sentry-internal/replay-canvas": "10.20.0", - "@sentry/core": "10.20.0" + "@sentry-internal/browser-utils": "10.21.0-alpha.1", + "@sentry-internal/feedback": "10.21.0-alpha.1", + "@sentry-internal/replay": "10.21.0-alpha.1", + "@sentry-internal/replay-canvas": "10.21.0-alpha.1", + "@sentry/core": "10.21.0-alpha.1" }, "devDependencies": { - "@sentry-internal/integration-shims": "10.20.0", + "@sentry-internal/integration-shims": "10.21.0-alpha.1", "fake-indexeddb": "^4.0.1" }, "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..beb0639604a4 --- /dev/null +++ b/packages/browser/src/integrations/spanstreaming.ts @@ -0,0 +1,286 @@ +import type { Client, IntegrationFn, Scope, ScopeData, Span, SpanAttributes, SpanV2JSON } from '@sentry/core'; +import { + attributesFromObject, + createSpanV2Envelope, + debug, + defineIntegration, + getCapturedScopesOnSpan, + getDynamicSamplingContextFromSpan, + getGlobalScope, + getRootSpan as getSegmentSpan, + httpHeadersToSpanAttributes, + isV2BeforeSendSpanCallback, + mergeScopeData, + reparentChildSpans, + SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, + SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT, + SEMANTIC_ATTRIBUTE_SENTRY_RELEASE, + SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME, + SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION, + SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME, + SEMANTIC_ATTRIBUTE_URL_FULL, + SEMANTIC_ATTRIBUTE_USER_EMAIL, + SEMANTIC_ATTRIBUTE_USER_ID, + SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS, + SEMANTIC_ATTRIBUTE_USER_USERNAME, + shouldIgnoreSpan, + showSpanDropWarning, + spanToV2JSON, +} from '@sentry/core'; +import { DEBUG_BUILD } from '../debug-build'; +import { getHttpRequestData } from '../helpers'; + +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('spanEnd', span => { + const spanTreeMapKey = getSpanTreeMapKey(span); + const spanBuffer = spanTreeMap.get(spanTreeMapKey); + if (spanBuffer) { + spanBuffer.add(span); + } else { + spanTreeMap.set(spanTreeMapKey, new Set([span])); + } + }); + + // 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('segmentSpanEnd', segmentSpan => { + processAndSendSpans(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}-${getSegmentSpan(span).spanContext().spanId}`; +} + +function processAndSendSpans( + 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 segmentSpanJson = spanToV2JSON(segmentSpan); + + for (const span of spansOfTrace) { + applyCommonSpanAttributes(span, segmentSpanJson, client); + } + + applyScopeToSegmentSpan(segmentSpan, segmentSpanJson, client); + + // TODO: Apply scope data and contexts to segment span + + const { ignoreSpans } = client.getOptions(); + + // 1. Check if the entire span tree is ignored by ignoreSpans + if (ignoreSpans?.length && shouldIgnoreSpan(segmentSpanJson, ignoreSpans)) { + client.recordDroppedEvent('before_send', 'span', spansOfTrace.size); + spanTreeMap.delete(spanTreeMapKey); + return; + } + + const serializedSpans = Array.from(spansOfTrace ?? []).map(s => { + const serialized = spanToV2JSON(s); + // remove internal span attributes we don't need to send. + delete serialized.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; + return serialized; + }); + + const processedSpans = []; + let ignoredSpanCount = 0; + + for (const span of serializedSpans) { + // 2. Check if child spans should be ignored + const isChildSpan = span.span_id !== segmentSpan.spanContext().spanId; + if (ignoreSpans?.length && isChildSpan && shouldIgnoreSpan(span, ignoreSpans)) { + reparentChildSpans(serializedSpans, span); + ignoredSpanCount++; + // drop this span by not adding it to the processedSpans array + continue; + } + + // 3. Apply beforeSendSpan callback + const processedSpan = beforeSendSpan ? applyBeforeSendSpanCallback(span, beforeSendSpan) : span; + processedSpans.push(processedSpan); + } + + if (ignoredSpanCount) { + client.recordDroppedEvent('before_send', 'span', ignoredSpanCount); + } + + const batches: SpanV2JSON[][] = []; + for (let i = 0; i < processedSpans.length; i += batchLimit) { + batches.push(processedSpans.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 applyCommonSpanAttributes(span: Span, serializedSegmentSpan: SpanV2JSON, client: Client): void { + const sdk = client.getSdkMetadata(); + const { release, environment, sendDefaultPii } = client.getOptions(); + + const { isolationScope: spanIsolationScope, scope: spanScope } = getCapturedScopesOnSpan(span); + + const originalAttributeKeys = Object.keys(spanToV2JSON(span).attributes ?? {}); + + const finalScopeData = getFinalScopeData(spanIsolationScope, spanScope); + + // 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_SDK_NAME]: sdk?.sdk?.name, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: sdk?.sdk?.version, + ...(sendDefaultPii + ? { + [SEMANTIC_ATTRIBUTE_USER_ID]: finalScopeData.user?.id, + [SEMANTIC_ATTRIBUTE_USER_EMAIL]: finalScopeData.user?.email, + [SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS]: finalScopeData.user?.ip_address ?? undefined, + [SEMANTIC_ATTRIBUTE_USER_USERNAME]: finalScopeData.user?.username, + } + : {}), + }); +} + +/** + * Adds span attributes from the scopes' contexts + * TODO: It's not set in stone yet if we actually want to flatmap contexts into span attributes. + * For now we do it but not yet extra or tags. It's still TBD how to proceed here. + */ +function applyScopeToSegmentSpan(segmentSpan: Span, serializedSegmentSpan: SpanV2JSON, client: Client): void { + const { isolationScope, scope } = getCapturedScopesOnSpan(segmentSpan); + const finalScopeData = getFinalScopeData(isolationScope, scope); + + const browserRequestData = getHttpRequestData(); + + const tags = finalScopeData.tags ?? {}; + + let contextAttributes = {}; + Object.keys(finalScopeData.contexts).forEach(key => { + const context = finalScopeData.contexts[key]; + if (context) { + contextAttributes = { ...contextAttributes, ...attributesFromObject(context) }; + } + }); + + const extraAttributes = attributesFromObject(finalScopeData.extra); + + setAttributesIfNotPresent(segmentSpan, Object.keys(serializedSegmentSpan.attributes ?? {}), { + [SEMANTIC_ATTRIBUTE_URL_FULL]: browserRequestData.url, + ...httpHeadersToSpanAttributes(browserRequestData.headers, client.getOptions().sendDefaultPii ?? false), + ...tags, + ...contextAttributes, + ...extraAttributes, + }); +} + +function applyBeforeSendSpanCallback(span: SpanV2JSON, beforeSendSpan: (span: SpanV2JSON) => SpanV2JSON): SpanV2JSON { + const modifedSpan = beforeSendSpan(span); + if (!modifedSpan) { + showSpanDropWarning(); + return span; + } + return modifedSpan; +} + +function setAttributesIfNotPresent(span: Span, originalAttributeKeys: string[], newAttributes: SpanAttributes): void { + Object.keys(newAttributes).forEach(key => { + if (!originalAttributeKeys.includes(key)) { + span.setAttribute(key, newAttributes[key]); + } + }); +} + +// 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; +} 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 15eb9481f99a..a8454b616161 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/bun", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0", - "@sentry/node": "10.20.0" + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node": "10.21.0-alpha.1" }, "devDependencies": { "bun-types": "^1.2.9" diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index df04b7ad30ac..b57448ef4761 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cloudflare", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0" + "@sentry/core": "10.21.0-alpha.1" }, "peerDependencies": { "@cloudflare/workers-types": "^4.x" diff --git a/packages/core/package.json b/packages/core/package.json index 2e7f2d55018a..5d3afc1efeca 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/core", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "description": "Base implementation for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/core", diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 6a269a969c8d..cb16ad025a9b 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -33,6 +33,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'; @@ -579,6 +580,14 @@ export abstract class Client { */ public on(hook: 'spanEnd', callback: (span: Span) => void): () => void; + /** + * Register a callback for after a span is ended. + * NOTE: The span cannot be mutated anymore in this callback. + * Receives the span as argument. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ + public on(hook: 'segmentSpanEnd', 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. @@ -826,6 +835,9 @@ export abstract class Client { /** Fire a hook whenever a span ends. */ public emit(hook: 'spanEnd', span: Span): void; + /** Fire a hook whenever a segment span ends. */ + public emit(hook: 'segmentSpanEnd', span: Span): void; + /** * Fire a hook indicating that an idle span is allowed to auto finish. */ @@ -1410,13 +1422,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 7a6c5c2e17d3..ba4d6171b28e 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, @@ -76,11 +76,15 @@ export { getSpanDescendants, getStatusMessage, getRootSpan, + getSegmentSpan, getActiveSpan, addChildSpanToSpan, spanTimeInputToSeconds, updateSpanName, + spanToV2JSON, + showSpanDropWarning, } from './utils/spanUtils'; +export { attributesFromObject } from './utils/attributes'; export { _setSpanForScope as _INTERNAL_setSpanForScope } from './utils/spanOnScope'; export { parseSampleRate } from './utils/parseSampleRate'; export { applySdkMetadata } from './utils/sdkMetadata'; @@ -310,6 +314,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 { @@ -361,6 +367,7 @@ export type { ProfileChunkEnvelope, ProfileChunkItem, SpanEnvelope, + SpanV2Envelope, SpanItem, LogEnvelope, MetricEnvelope, @@ -428,6 +435,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..df43f510aaaf 100644 --- a/packages/core/src/semanticAttributes.ts +++ b/packages/core/src/semanticAttributes.ts @@ -77,3 +77,33 @@ 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 operating system name (e.g., "Linux", "Windows", "macOS") */ +export const SEMANTIC_ATTRIBUTE_OS_NAME = 'os.name'; +/** The browser name (e.g., "Chrome", "Firefox", "Safari") */ +export const SEMANTIC_ATTRIBUTE_BROWSER_VERSION = 'browser.name'; +/** 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.username'; +/** The thread ID */ +export const SEMANTIC_ATTRIBUTE_THREAD_ID = 'thread.id'; +/** The thread name */ +export const SEMANTIC_ATTRIBUTE_THREAD_NAME = 'thread.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/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 9bd98b9741c6..6a4eaefb21f5 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,31 @@ 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_remote: false, // TODO: This has to be inferred from attributes SentrySpans. `false` is the default. + kind: 'internal', // TODO: This has to be inferred from attributes SentrySpans. `internal` is the default. + status: getV2StatusMessage(this._status), + attributes: getV2Attributes(this._attributes), + links: getV2SpanLinks(this._links), + }; + } + /** @inheritdoc */ public isRecording(): boolean { return !this._endTime && !!this._sampled; @@ -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('segmentSpanEnd', this); + return; } const transactionEvent = this._convertSpanToTransaction(); 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 1f172aaa1f4a..747d48cd20be 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -6,7 +6,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'; @@ -374,6 +374,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 @@ -468,6 +478,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..762d3519d0fe 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,24 @@ 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'; + kind: 'server' | 'client' | 'internal' | 'consumer' | 'producer'; + is_remote: 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 6e7c62c7631a..32eadacd5351 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,61 @@ 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, isRemote } = 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_remote: isRemote || false, + kind: 'internal', // TODO: Figure out how to get this from the OTel span as it's not publicly exposed + 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', + kind: 'internal', + is_remote: isRemote || false, + }; +} + 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 +312,27 @@ export function getStatusMessage(status: SpanStatus | undefined): string | undef return status.message || 'unknown_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 +394,12 @@ export function getSpanDescendants(span: SpanWithPotentialChildren): Span[] { /** * Returns the root span of a given span. */ -export function getRootSpan(span: SpanWithPotentialChildren): Span { +export const getRootSpan = getSegmentSpan; + +/** + * Returns the segment span of a given span. + */ +export function getSegmentSpan(span: SpanWithPotentialChildren): Span { return span[ROOT_SPAN_FIELD] || span; } diff --git a/packages/core/src/utils/vercel-ai/index.ts b/packages/core/src/utils/vercel-ai/index.ts index 9b1cc2bc8aae..715eb774fc98 100644 --- a/packages/core/src/utils/vercel-ai/index.ts +++ b/packages/core/src/utils/vercel-ai/index.ts @@ -64,6 +64,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/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 9b72fb7e82da..2cf6b743878d 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/deno", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0" + "@sentry/core": "10.21.0-alpha.1" }, "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 9fbfc634baaa..fb3bb1b090e2 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/ember", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "description": "Official Sentry SDK for Ember.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember", @@ -32,8 +32,8 @@ "dependencies": { "@babel/core": "^7.27.7", "@embroider/macros": "^1.16.0", - "@sentry/browser": "10.20.0", - "@sentry/core": "10.20.0", + "@sentry/browser": "10.21.0-alpha.1", + "@sentry/core": "10.21.0-alpha.1", "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 f013f8c41e05..abbd1108a92d 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.20.0", + "version": "10.21.0-alpha.1", "description": "Official Sentry SDK eslint config", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-config-sdk", @@ -22,8 +22,8 @@ "access": "public" }, "dependencies": { - "@sentry-internal/eslint-plugin-sdk": "10.20.0", - "@sentry-internal/typescript": "10.20.0", + "@sentry-internal/eslint-plugin-sdk": "10.21.0-alpha.1", + "@sentry-internal/typescript": "10.21.0-alpha.1", "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", "eslint-config-prettier": "^6.11.0", diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index 43a55a198577..41e74dd44c3b 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.20.0", + "version": "10.21.0-alpha.1", "description": "Official Sentry SDK eslint plugin", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-plugin-sdk", diff --git a/packages/feedback/package.json b/packages/feedback/package.json index cfd607c98884..a7c5251947d7 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/feedback", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0" + "@sentry/core": "10.21.0-alpha.1" }, "devDependencies": { "preact": "^10.19.4" diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index e54d827079d9..c206a4ba9834 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/gatsby", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "description": "Official Sentry SDK for Gatsby.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby", @@ -45,8 +45,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "10.20.0", - "@sentry/react": "10.20.0", + "@sentry/core": "10.21.0-alpha.1", + "@sentry/react": "10.21.0-alpha.1", "@sentry/webpack-plugin": "^4.1.1" }, "peerDependencies": { diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index a4507d3482f3..66ca08971354 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.20.0", + "version": "10.21.0-alpha.1", "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.20.0", - "@sentry/node": "10.20.0", + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node": "10.21.0-alpha.1", "@types/express": "^4.17.14" }, "devDependencies": { diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json index b13790dbfb95..0cb71d747f94 100644 --- a/packages/integration-shims/package.json +++ b/packages/integration-shims/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/integration-shims", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0" + "@sentry/core": "10.21.0-alpha.1" }, "engines": { "node": ">=18" diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index db33f3f20025..92c895d1fee9 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nestjs", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.204.0", "@opentelemetry/instrumentation-nestjs-core": "0.50.0", "@opentelemetry/semantic-conventions": "^1.37.0", - "@sentry/core": "10.20.0", - "@sentry/node": "10.20.0" + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node": "10.21.0-alpha.1" }, "devDependencies": { "@nestjs/common": "^10.0.0", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 9464ced6a3ed..005624d32641 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nextjs", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "description": "Official Sentry SDK for Next.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs", @@ -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.20.0", + "@sentry-internal/browser-utils": "10.21.0-alpha.1", "@sentry/bundler-plugin-core": "^4.3.0", - "@sentry/core": "10.20.0", - "@sentry/node": "10.20.0", - "@sentry/opentelemetry": "10.20.0", - "@sentry/react": "10.20.0", - "@sentry/vercel-edge": "10.20.0", + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node": "10.21.0-alpha.1", + "@sentry/opentelemetry": "10.21.0-alpha.1", + "@sentry/react": "10.21.0-alpha.1", + "@sentry/vercel-edge": "10.21.0-alpha.1", "@sentry/webpack-plugin": "^4.3.0", "chalk": "3.0.0", "resolve": "1.22.8", diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 4d09e7e2d170..9154643e7ca5 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -53,11 +53,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 5ce23e6a9460..5ff1dd2c6a52 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -212,6 +212,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 701991d15783..683595ec3b7b 100644 --- a/packages/node-core/package.json +++ b/packages/node-core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node-core", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0", - "@sentry/opentelemetry": "10.20.0", + "@sentry/core": "10.21.0-alpha.1", + "@sentry/opentelemetry": "10.21.0-alpha.1", "import-in-the-middle": "^1.14.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 c24c0c68d1da..c9825de6630f 100644 --- a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts +++ b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts @@ -217,6 +217,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 eaf0b9f4f15e..acf248dcb093 100644 --- a/packages/node-native/package.json +++ b/packages/node-native/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node-native", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0", - "@sentry/node": "10.20.0" + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node": "10.21.0-alpha.1" }, "devDependencies": { "@types/node": "^18.19.1" diff --git a/packages/node/package.json b/packages/node/package.json index a78a670a6cdd..a52722f181e1 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.1.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@prisma/instrumentation": "6.15.0", - "@sentry/core": "10.20.0", - "@sentry/node-core": "10.20.0", - "@sentry/opentelemetry": "10.20.0", + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node-core": "10.21.0-alpha.1", + "@sentry/opentelemetry": "10.21.0-alpha.1", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" }, diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index b43fbf854d2e..2c80bd8635e7 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nuxt", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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", @@ -47,13 +47,13 @@ }, "dependencies": { "@nuxt/kit": "^3.13.2", - "@sentry/browser": "10.20.0", - "@sentry/cloudflare": "10.20.0", - "@sentry/core": "10.20.0", - "@sentry/node": "10.20.0", + "@sentry/browser": "10.21.0-alpha.1", + "@sentry/cloudflare": "10.21.0-alpha.1", + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node": "10.21.0-alpha.1", "@sentry/rollup-plugin": "^4.3.0", "@sentry/vite-plugin": "^4.3.0", - "@sentry/vue": "10.20.0" + "@sentry/vue": "10.21.0-alpha.1" }, "devDependencies": { "@nuxt/module-builder": "^0.8.4", diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index dcd2f46caec9..a5af20393db8 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 8be9d3fe62dd..62cd6ed4eb6f 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/opentelemetry", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0" + "@sentry/core": "10.21.0-alpha.1" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index 177badb3987c..2455185aed16 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/profiling-node", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0", - "@sentry/node": "10.20.0" + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node": "10.21.0-alpha.1" }, "devDependencies": { "@types/node": "^18.19.1" diff --git a/packages/react-router/package.json b/packages/react-router/package.json index ed996110c024..d3c528828f89 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react-router", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.1.0", "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.37.0", - "@sentry/browser": "10.20.0", + "@sentry/browser": "10.21.0-alpha.1", "@sentry/cli": "^2.56.0", - "@sentry/core": "10.20.0", - "@sentry/node": "10.20.0", - "@sentry/react": "10.20.0", + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node": "10.21.0-alpha.1", + "@sentry/react": "10.21.0-alpha.1", "@sentry/vite-plugin": "^4.1.0", "glob": "11.0.1" }, 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 8b4c9be1e4fa..849702ea1ab9 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "description": "Official Sentry SDK for React.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "10.20.0", - "@sentry/core": "10.20.0", + "@sentry/browser": "10.21.0-alpha.1", + "@sentry/core": "10.21.0-alpha.1", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { diff --git a/packages/remix/package.json b/packages/remix/package.json index 49f8547e9e47..f6d783693690 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/remix", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "description": "Official Sentry SDK for Remix", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/remix", @@ -69,9 +69,9 @@ "@opentelemetry/semantic-conventions": "^1.37.0", "@remix-run/router": "1.x", "@sentry/cli": "^2.56.0", - "@sentry/core": "10.20.0", - "@sentry/node": "10.20.0", - "@sentry/react": "10.20.0", + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node": "10.21.0-alpha.1", + "@sentry/react": "10.21.0-alpha.1", "glob": "^10.3.4", "yargs": "^17.6.0" }, diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index 2d5cbe1f4979..c40ace1afc61 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-canvas", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "description": "Replay canvas integration", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -69,8 +69,8 @@ "@sentry-internal/rrweb": "2.37.0" }, "dependencies": { - "@sentry-internal/replay": "10.20.0", - "@sentry/core": "10.20.0" + "@sentry-internal/replay": "10.21.0-alpha.1", + "@sentry/core": "10.21.0-alpha.1" }, "engines": { "node": ">=18" diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index ba9f70516b01..c17d60eea068 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0", + "@sentry-internal/replay-worker": "10.21.0-alpha.1", "@sentry-internal/rrweb": "2.37.0", "@sentry-internal/rrweb-snapshot": "2.37.0", "fflate": "0.8.2", @@ -90,8 +90,8 @@ "node-fetch": "^2.6.7" }, "dependencies": { - "@sentry-internal/browser-utils": "10.20.0", - "@sentry/core": "10.20.0" + "@sentry-internal/browser-utils": "10.21.0-alpha.1", + "@sentry/core": "10.21.0-alpha.1" }, "engines": { "node": ">=18" diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json index 3e02a1ef7fd7..f5265927d0e1 100644 --- a/packages/replay-worker/package.json +++ b/packages/replay-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-worker", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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 74851199d7d7..6523047f9016 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/solid", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0", - "@sentry/core": "10.20.0" + "@sentry/browser": "10.21.0-alpha.1", + "@sentry/core": "10.21.0-alpha.1" }, "peerDependencies": { "@solidjs/router": "^0.13.4", diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index 09cd15891644..f7b1a7f3ec4a 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/solidstart", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0", - "@sentry/node": "10.20.0", - "@sentry/solid": "10.20.0", + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node": "10.21.0-alpha.1", + "@sentry/solid": "10.21.0-alpha.1", "@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 e65fa84fd435..bb70fbf4479b 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/svelte", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "description": "Official Sentry SDK for Svelte", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/svelte", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "10.20.0", - "@sentry/core": "10.20.0", + "@sentry/browser": "10.21.0-alpha.1", + "@sentry/core": "10.21.0-alpha.1", "magic-string": "^0.30.0" }, "peerDependencies": { diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index b0fb07508ef7..1d1cbe316991 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/sveltekit", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "description": "Official Sentry SDK for SvelteKit", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/sveltekit", @@ -48,10 +48,10 @@ }, "dependencies": { "@babel/parser": "7.26.9", - "@sentry/cloudflare": "10.20.0", - "@sentry/core": "10.20.0", - "@sentry/node": "10.20.0", - "@sentry/svelte": "10.20.0", + "@sentry/cloudflare": "10.21.0-alpha.1", + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node": "10.21.0-alpha.1", + "@sentry/svelte": "10.21.0-alpha.1", "@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 71ae6f8c4e53..d68170e74d29 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/tanstackstart-react", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0", - "@sentry/core": "10.20.0", - "@sentry/node": "10.20.0", - "@sentry/react": "10.20.0" + "@sentry-internal/browser-utils": "10.21.0-alpha.1", + "@sentry/core": "10.21.0-alpha.1", + "@sentry/node": "10.21.0-alpha.1", + "@sentry/react": "10.21.0-alpha.1" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/tanstackstart/package.json b/packages/tanstackstart/package.json index b03139402bdb..2be6bff207d8 100644 --- a/packages/tanstackstart/package.json +++ b/packages/tanstackstart/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/tanstackstart", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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 e393534729c7..083d60c29c1a 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/types", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "description": "Types for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/types", @@ -57,7 +57,7 @@ "yalc:publish": "yalc publish --push --sig" }, "dependencies": { - "@sentry/core": "10.20.0" + "@sentry/core": "10.21.0-alpha.1" }, "volta": { "extends": "../../package.json" diff --git a/packages/typescript/package.json b/packages/typescript/package.json index dc465ec207dd..90dc4e0bb0a6 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/typescript", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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 94ce4856d199..b1d4aaf1b444 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vercel-edge", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.1.0", - "@sentry/core": "10.20.0" + "@sentry/core": "10.21.0-alpha.1" }, "devDependencies": { "@edge-runtime/types": "3.0.1", "@opentelemetry/core": "^2.1.0", "@opentelemetry/sdk-trace-base": "^2.1.0", "@opentelemetry/semantic-conventions": "^1.37.0", - "@sentry/opentelemetry": "10.20.0" + "@sentry/opentelemetry": "10.21.0-alpha.1" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/vue/package.json b/packages/vue/package.json index ba35ef861b38..cb4d89234a2b 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vue", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0", - "@sentry/core": "10.20.0" + "@sentry/browser": "10.21.0-alpha.1", + "@sentry/core": "10.21.0-alpha.1" }, "peerDependencies": { "pinia": "2.x || 3.x", diff --git a/packages/wasm/package.json b/packages/wasm/package.json index 35515307262d..66be5ddb964a 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/wasm", - "version": "10.20.0", + "version": "10.21.0-alpha.1", "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.20.0", - "@sentry/core": "10.20.0" + "@sentry/browser": "10.21.0-alpha.1", + "@sentry/core": "10.21.0-alpha.1" }, "scripts": { "build": "run-p build:transpile build:bundle build:types",