Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'browserTracingIntegration'),
gzip: true,
limit: '41.3 KB',
limit: '42 KB',
},
{
name: '@sentry/browser (incl. Tracing, Profiling)',
Expand All @@ -47,6 +47,13 @@ module.exports = [
gzip: true,
limit: '48 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)',
path: 'packages/browser/build/npm/esm/prod/index.js',
Expand Down Expand Up @@ -89,7 +96,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration'),
gzip: true,
limit: '97 KB',
limit: '98 KB',
},
{
name: '@sentry/browser (incl. Feedback)',
Expand All @@ -103,7 +110,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'sendFeedback'),
gzip: true,
limit: '30 KB',
limit: '31 KB',
},
{
name: '@sentry/browser (incl. FeedbackAsync)',
Expand All @@ -127,22 +134,22 @@ module.exports = [
import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'),
ignore: ['react/jsx-runtime'],
gzip: true,
limit: '43.3 KB',
limit: '44 KB',
},
// Vue SDK (ESM)
{
name: '@sentry/vue',
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.1 KB',
limit: '44 KB',
},
// Svelte SDK (ESM)
{
Expand Down Expand Up @@ -222,7 +229,7 @@ module.exports = [
import: createImport('init'),
ignore: ['$app/stores'],
gzip: true,
limit: '42 KB',
limit: '43 KB',
},
// Node-Core SDK (ESM)
{
Expand All @@ -240,7 +247,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
limit: '158 KB',
limit: '160 KB',
},
{
name: '@sentry/node - without tracing',
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,16 @@ Work in this release was contributed by @hanseo0507. Thank you for your contribu

Work in this release was contributed by @0xbad0c0d3. Thank you for your contribution!

## 10.21.0-alpha.1

This release is a preview release for sending spans in browser via spanV2 instead of transaction event envelopes. All of this is experimental and subject to change. Use at your own risk. [More Details.](https://github.com/getsentry/sentry-javascript/pull/17852)

- export withStreamSpan from `@sentry/browser`

## 10.21.0-alpha.0

This release is a preview release for sending spans in browser via spanV2 instead of transaction event envelopes. All of this is experimental and subject to change. Use at your own risk. [More Details.](https://github.com/getsentry/sentry-javascript/pull/17852)

## 10.20.0

### Important Changes
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/server/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export {
startInactiveSpan,
startSpanManual,
withActiveSpan,
withStreamSpan,
startNewTrace,
getSpanDescendants,
setMeasurement,
Expand Down Expand Up @@ -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';
2 changes: 2 additions & 0 deletions packages/browser/src/integrations/httpcontext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
249 changes: 249 additions & 0 deletions packages/browser/src/integrations/spanstreaming.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import type { Client, IntegrationFn, Scope, ScopeData, Span, SpanAttributes, SpanV2JSON } from '@sentry/core';
import {
createSpanV2Envelope,
debug,
defineIntegration,
getCapturedScopesOnSpan,
getDynamicSamplingContextFromSpan,
getGlobalScope,
INTERNAL_getSegmentSpan,
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_ID,
SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME,
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';

export interface SpanStreamingOptions {
batchLimit: number;
}

export const spanStreamingIntegration = defineIntegration(((userOptions?: Partial<SpanStreamingOptions>) => {
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<string, Set<Span>>();

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<string, Set<Span>>;
batchLimit: number;
beforeSendSpan: ((span: SpanV2JSON) => SpanV2JSON) | undefined;
}

/**
* Just the traceid alone isn't enough because there can be multiple span trees with the same traceid.
*/
function getSpanTreeMapKey(span: Span): string {
return `${span.spanContext().traceId}-${INTERNAL_getSegmentSpan(span).spanContext().spanId}`;
}

function 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);
}

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
// TODO: validate beforeSendSpan result/pass in a copy and merge afterwards
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_SEGMENT_ID]: serializedSegmentSpan.span_id,
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: sdk?.sdk?.name,
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: sdk?.sdk?.version,
...(sendDefaultPii
? {
[SEMANTIC_ATTRIBUTE_USER_ID]: 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,
}
: {}),
});
}

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;
}
1 change: 1 addition & 0 deletions packages/browser/src/tracing/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
if (traceFetch) {
// Keeping track of http requests, whose body payloads resolved later than the initial resolved request
// e.g. streaming using server sent events (SSE)
// TODO (span-streaming): replace with client hook - do we need client.on('processSpan')?
client.addEventProcessor(event => {
if (event.type === 'transaction' && event.spans) {
event.spans.forEach(span => {
Expand Down
Loading
Loading