Skip to content

Commit f7a0bd3

Browse files
committed
types, serialization, integration WIP
1 parent 42460ad commit f7a0bd3

File tree

13 files changed

+411
-13
lines changed

13 files changed

+411
-13
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type { Envelope, IntegrationFn, Span, SpanV2JSON } from '@sentry/core';
2+
import { createEnvelope, debug, defineIntegration, isV2BeforeSendSpanCallback, spanToV2JSON } from '@sentry/core';
3+
import { DEBUG_BUILD } from '../debug-build';
4+
5+
export interface SpanStreamingOptions {
6+
batchLimit: number;
7+
}
8+
9+
const _spanStreamingIntegration = ((userOptions?: Partial<SpanStreamingOptions>) => {
10+
const validatedUserProvidedBatchLimit =
11+
userOptions?.batchLimit && userOptions.batchLimit <= 1000 && userOptions.batchLimit >= 1
12+
? userOptions.batchLimit
13+
: undefined;
14+
15+
if (DEBUG_BUILD && userOptions?.batchLimit && !validatedUserProvidedBatchLimit) {
16+
debug.warn('SpanStreaming batchLimit must be between 1 and 1000, defaulting to 1000');
17+
}
18+
19+
const options: SpanStreamingOptions = {
20+
batchLimit:
21+
userOptions?.batchLimit && userOptions.batchLimit <= 1000 && userOptions.batchLimit >= 1
22+
? userOptions.batchLimit
23+
: 1000,
24+
...userOptions,
25+
};
26+
27+
const traceMap = new Map<string, Set<Span>>();
28+
29+
return {
30+
name: 'SpanStreaming',
31+
setup(client) {
32+
const clientOptions = client.getOptions();
33+
const beforeSendSpan = clientOptions.beforeSendSpan;
34+
35+
const initialMessage = 'spanStreamingIntegration requires';
36+
const fallbackMsg = 'Falling back to static trace lifecycle.';
37+
38+
if (DEBUG_BUILD && clientOptions.traceLifecycle !== 'streamed') {
39+
debug.warn(`${initialMessage} \`traceLifecycle\` to be set to "streamed"! ${fallbackMsg}`);
40+
return;
41+
}
42+
43+
if (DEBUG_BUILD && beforeSendSpan && !isV2BeforeSendSpanCallback(beforeSendSpan)) {
44+
debug.warn(`${initialMessage} a beforeSendSpan callback using \`makeV2Callback\`! ${fallbackMsg}`);
45+
return;
46+
}
47+
48+
client.on('spanEnd', span => {
49+
const spanBuffer = traceMap.get(span.spanContext().traceId);
50+
if (spanBuffer) {
51+
spanBuffer.add(span);
52+
} else {
53+
traceMap.set(span.spanContext().traceId, new Set([span]));
54+
}
55+
});
56+
57+
client.on('segmentSpanEnd', segmentSpan => {
58+
const traceId = segmentSpan.spanContext().traceId;
59+
const spansOfTrace = traceMap.get(traceId);
60+
61+
if (!spansOfTrace?.size) {
62+
traceMap.delete(traceId);
63+
return;
64+
}
65+
66+
const serializedSpans = Array.from(spansOfTrace ?? []).map(span => {
67+
const serializedSpan = spanToV2JSON(span);
68+
const finalSpan = beforeSendSpan ? beforeSendSpan(serializedSpan) : serializedSpan;
69+
return finalSpan;
70+
});
71+
72+
const batches: SpanV2JSON[][] = [];
73+
for (let i = 0; i < serializedSpans.length; i += options.batchLimit) {
74+
batches.push(serializedSpans.slice(i, i + options.batchLimit));
75+
}
76+
77+
debug.log(`Sending trace ${traceId} in ${batches.length} batche${batches.length === 1 ? '' : 's'}`);
78+
79+
// TODO: Apply scopes to spans
80+
81+
// TODO: Apply beforeSendSpan to spans
82+
83+
// TODO: Apply ignoreSpans to spans
84+
85+
for (const batch of batches) {
86+
const envelope = createSpanStreamEnvelope(batch);
87+
// no need to handle client reports for network errors,
88+
// buffer overflows or rate limiting here. All of this is handled
89+
// by client and transport.
90+
client.sendEnvelope(envelope).then(null, reason => {
91+
DEBUG_BUILD && debug.error('Error while sending span stream envelope:', reason);
92+
});
93+
}
94+
95+
traceMap.delete(traceId);
96+
});
97+
},
98+
};
99+
}) satisfies IntegrationFn;
100+
101+
export const spanStreamingIntegration = defineIntegration(_spanStreamingIntegration);
102+
103+
function createSpanStreamEnvelope(serializedSpans: StreamedSpanJSON[]): Envelope {
104+
return createEnvelope<SpanEnvelope>(headers, [item]);
105+
}

packages/core/src/client.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import type { SeverityLevel } from './types-hoist/severity';
3232
import type { Span, SpanAttributes, SpanContextData, SpanJSON } from './types-hoist/span';
3333
import type { StartSpanOptions } from './types-hoist/startSpanOptions';
3434
import type { Transport, TransportMakeRequestResponse } from './types-hoist/transport';
35+
import { isV2BeforeSendSpanCallback } from './utils/beforeSendSpan';
3536
import { createClientReportEnvelope } from './utils/clientreport';
3637
import { debug } from './utils/debug-logger';
3738
import { dsnToString, makeDsn } from './utils/dsn';
@@ -1304,13 +1305,17 @@ function _validateBeforeSendResult(
13041305
/**
13051306
* Process the matching `beforeSendXXX` callback.
13061307
*/
1308+
// eslint-disable-next-line complexity
13071309
function processBeforeSend(
13081310
client: Client,
13091311
options: ClientOptions,
13101312
event: Event,
13111313
hint: EventHint,
13121314
): PromiseLike<Event | null> | Event | null {
1313-
const { beforeSend, beforeSendTransaction, beforeSendSpan, ignoreSpans } = options;
1315+
const { beforeSend, beforeSendTransaction, ignoreSpans } = options;
1316+
1317+
const beforeSendSpan = !isV2BeforeSendSpanCallback(options.beforeSendSpan) && options.beforeSendSpan;
1318+
13141319
let processedEvent = event;
13151320

13161321
if (isErrorEvent(processedEvent) && beforeSend) {

packages/core/src/envelope.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { Event } from './types-hoist/event';
1818
import type { SdkInfo } from './types-hoist/sdkinfo';
1919
import type { SdkMetadata } from './types-hoist/sdkmetadata';
2020
import type { Session, SessionAggregates } from './types-hoist/session';
21+
import { isV2BeforeSendSpanCallback } from './utils/beforeSendSpan';
2122
import { dsnToString } from './utils/dsn';
2223
import {
2324
createEnvelope,
@@ -138,7 +139,8 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client?
138139
...(!!tunnel && dsn && { dsn: dsnToString(dsn) }),
139140
};
140141

141-
const { beforeSendSpan, ignoreSpans } = client?.getOptions() || {};
142+
const options = client?.getOptions();
143+
const ignoreSpans = options?.ignoreSpans;
142144

143145
const filteredSpans = ignoreSpans?.length
144146
? spans.filter(span => !shouldIgnoreSpan(spanToJSON(span), ignoreSpans))
@@ -149,10 +151,14 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client?
149151
client?.recordDroppedEvent('before_send', 'span', droppedSpans);
150152
}
151153

152-
const convertToSpanJSON = beforeSendSpan
154+
// checking against traceLifeCycle so that TS can infer the correct type for
155+
// beforeSendSpan. This is a workaround for now as most likely, this entire function
156+
// will be removed in the future (once we send standalone spans as spans v2)
157+
const convertToSpanJSON = options?.beforeSendSpan
153158
? (span: SentrySpan) => {
154159
const spanJson = spanToJSON(span);
155-
const processedSpan = beforeSendSpan(spanJson);
160+
const processedSpan =
161+
!isV2BeforeSendSpanCallback(options?.beforeSendSpan) && options?.beforeSendSpan?.(spanJson);
156162

157163
if (!processedSpan) {
158164
showSpanDropWarning();

packages/core/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export {
8080
addChildSpanToSpan,
8181
spanTimeInputToSeconds,
8282
updateSpanName,
83+
spanToV2JSON,
8384
} from './utils/spanUtils';
8485
export { _setSpanForScope as _INTERNAL_setSpanForScope } from './utils/spanOnScope';
8586
export { parseSampleRate } from './utils/parseSampleRate';
@@ -302,6 +303,7 @@ export { flushIfServerless } from './utils/flushIfServerless';
302303
export { SDK_VERSION } from './utils/version';
303304
export { getDebugImagesForResources, getFilenameToDebugIdMap } from './utils/debug-ids';
304305
export { escapeStringForRegex } from './vendor/escapeStringForRegex';
306+
export { isV2BeforeSendSpanCallback, makeV2Callback } from './utils/beforeSendSpan';
305307

306308
export type { Attachment } from './types-hoist/attachment';
307309
export type {
@@ -413,6 +415,7 @@ export type {
413415
SpanJSON,
414416
SpanContextData,
415417
TraceFlag,
418+
SpanV2JSON,
416419
} from './types-hoist/span';
417420
export type { SpanStatus } from './types-hoist/spanStatus';
418421
export type { Log, LogSeverityLevel } from './types-hoist/log';

packages/core/src/tracing/sentrySpan.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
SpanJSON,
2222
SpanOrigin,
2323
SpanTimeInput,
24+
SpanV2JSON,
2425
} from '../types-hoist/span';
2526
import type { SpanStatus } from '../types-hoist/spanStatus';
2627
import type { TimedEvent } from '../types-hoist/timedEvent';
@@ -31,6 +32,9 @@ import {
3132
getRootSpan,
3233
getSpanDescendants,
3334
getStatusMessage,
35+
getV2Attributes,
36+
getV2SpanLinks,
37+
getV2StatusMessage,
3438
spanTimeInputToSeconds,
3539
spanToJSON,
3640
spanToTransactionTraceContext,
@@ -241,6 +245,31 @@ export class SentrySpan implements Span {
241245
};
242246
}
243247

248+
/**
249+
* Get SpanV2JSON representation of this span.
250+
*
251+
* @hidden
252+
* @internal This method is purely for internal purposes and should not be used outside
253+
* of SDK code. If you need to get a JSON representation of a span,
254+
* use `spanToV2JSON(span)` instead.
255+
*/
256+
public getSpanV2JSON(): SpanV2JSON {
257+
return {
258+
name: this._name ?? '',
259+
span_id: this._spanId,
260+
trace_id: this._traceId,
261+
parent_span_id: this._parentSpanId,
262+
start_timestamp: this._startTime,
263+
// just in case _endTime is not set, we use the start time (i.e. duration 0)
264+
end_timestamp: this._endTime ?? this._startTime,
265+
is_remote: false, // TODO: This has to be inferred from attributes SentrySpans. `false` is the default.
266+
kind: 'internal', // TODO: This has to be inferred from attributes SentrySpans. `internal` is the default.
267+
status: getV2StatusMessage(this._status),
268+
attributes: getV2Attributes(this._attributes),
269+
links: getV2SpanLinks(this._links),
270+
};
271+
}
272+
244273
/** @inheritdoc */
245274
public isRecording(): boolean {
246275
return !this._endTime && !!this._sampled;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export type SerializedAttributes = Record<string, SerializedAttribute>;
2+
export type SerializedAttribute = (
3+
| {
4+
type: 'string';
5+
value: string;
6+
}
7+
| {
8+
type: 'integer';
9+
value: number;
10+
}
11+
| {
12+
type: 'double';
13+
value: number;
14+
}
15+
| {
16+
type: 'boolean';
17+
value: boolean;
18+
}
19+
) & { unit?: 'ms' | 's' | 'bytes' | 'count' | 'percent' };
20+
export type SerializedAttributeType = 'string' | 'integer' | 'double' | 'boolean';

packages/core/src/types-hoist/envelope.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { Profile, ProfileChunk } from './profiling';
1010
import type { ReplayEvent, ReplayRecordingData } from './replay';
1111
import type { SdkInfo } from './sdkinfo';
1212
import type { SerializedSession, SessionAggregates } from './session';
13-
import type { SpanJSON } from './span';
13+
import type { SerializedSpanContainer, SpanJSON } from './span';
1414

1515
// Based on: https://develop.sentry.dev/sdk/envelopes/
1616

@@ -88,6 +88,21 @@ type CheckInItemHeaders = { type: 'check_in' };
8888
type ProfileItemHeaders = { type: 'profile' };
8989
type ProfileChunkItemHeaders = { type: 'profile_chunk' };
9090
type SpanItemHeaders = { type: 'span' };
91+
type SpanV2ItemHeaders = {
92+
/**
93+
* Same as v1 span item type but this envelope is distinguished by {@link SpanV2ItemHeaders.content_type}.
94+
*/
95+
type: 'span';
96+
/**
97+
* The number of span items in the container. This must be the same as the number of span items in the payload.
98+
*/
99+
item_count: number;
100+
/**
101+
* The content type of the span items. This must be `application/vnd.sentry.items.span.v2+json`.
102+
* (the presence of this field also distinguishes the span item from the v1 span item)
103+
*/
104+
content_type: 'application/vnd.sentry.items.span.v2+json';
105+
};
91106
type LogContainerItemHeaders = {
92107
type: 'log';
93108
/**
@@ -115,6 +130,7 @@ export type FeedbackItem = BaseEnvelopeItem<FeedbackItemHeaders, FeedbackEvent>;
115130
export type ProfileItem = BaseEnvelopeItem<ProfileItemHeaders, Profile>;
116131
export type ProfileChunkItem = BaseEnvelopeItem<ProfileChunkItemHeaders, ProfileChunk>;
117132
export type SpanItem = BaseEnvelopeItem<SpanItemHeaders, Partial<SpanJSON>>;
133+
export type SpanV2Item = BaseEnvelopeItem<SpanV2ItemHeaders, SerializedSpanContainer>;
118134
export type LogContainerItem = BaseEnvelopeItem<LogContainerItemHeaders, SerializedLogContainer>;
119135
export type RawSecurityItem = BaseEnvelopeItem<RawSecurityHeaders, LegacyCSPReport>;
120136

@@ -124,6 +140,7 @@ type CheckInEnvelopeHeaders = { trace?: DynamicSamplingContext };
124140
type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders;
125141
type ReplayEnvelopeHeaders = BaseEnvelopeHeaders;
126142
type SpanEnvelopeHeaders = BaseEnvelopeHeaders & { trace?: DynamicSamplingContext };
143+
type SpanV2EnvelopeHeaders = BaseEnvelopeHeaders & { trace: DynamicSamplingContext };
127144
type LogEnvelopeHeaders = BaseEnvelopeHeaders;
128145
export type EventEnvelope = BaseEnvelope<
129146
EventEnvelopeHeaders,
@@ -134,6 +151,7 @@ export type ClientReportEnvelope = BaseEnvelope<ClientReportEnvelopeHeaders, Cli
134151
export type ReplayEnvelope = [ReplayEnvelopeHeaders, [ReplayEventItem, ReplayRecordingItem]];
135152
export type CheckInEnvelope = BaseEnvelope<CheckInEnvelopeHeaders, CheckInItem>;
136153
export type SpanEnvelope = BaseEnvelope<SpanEnvelopeHeaders, SpanItem>;
154+
export type SpanV2Envelope = BaseEnvelope<SpanV2EnvelopeHeaders, SpanV2Item>;
137155
export type ProfileChunkEnvelope = BaseEnvelope<BaseEnvelopeHeaders, ProfileChunkItem>;
138156
export type RawSecurityEnvelope = BaseEnvelope<BaseEnvelopeHeaders, RawSecurityItem>;
139157
export type LogEnvelope = BaseEnvelope<LogEnvelopeHeaders, LogContainerItem>;

packages/core/src/types-hoist/link.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ export interface SpanLink {
2222
* Link interface for the event envelope item. It's a flattened representation of `SpanLink`.
2323
* Can include additional fields defined by OTel.
2424
*/
25-
export interface SpanLinkJSON extends Record<string, unknown> {
25+
export interface SpanLinkJSON<TAttributes = SpanLinkAttributes> extends Record<string, unknown> {
2626
span_id: string;
2727
trace_id: string;
2828
sampled?: boolean;
29-
attributes?: SpanLinkAttributes;
29+
attributes?: TAttributes;
3030
}

packages/core/src/types-hoist/options.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { Integration } from './integration';
55
import type { Log } from './log';
66
import type { TracesSamplerSamplingContext } from './samplingcontext';
77
import type { SdkMetadata } from './sdkmetadata';
8-
import type { SpanJSON } from './span';
8+
import type { SpanJSON, SpanV2JSON } from './span';
99
import type { StackLineParser, StackParser } from './stacktrace';
1010
import type { TracePropagationTargets } from './tracing';
1111
import type { BaseTransportOptions, Transport } from './transport';
@@ -358,6 +358,16 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
358358
*/
359359
strictTraceContinuation?: boolean;
360360

361+
/**
362+
* [Experimental] The trace lifecycle, determining whether spans are sent statically when the entire local span tree is complete, or
363+
* in batches, following interval- and action-based triggers.
364+
*
365+
* @experimental this option is currently still experimental and its type, name, or entire presence is subject to break and change at any time.
366+
*
367+
* @default 'static'
368+
*/
369+
traceLifecycle?: 'static' | 'streamed';
370+
361371
/**
362372
* The organization ID for your Sentry project.
363373
*
@@ -417,13 +427,13 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
417427
* This function can be defined to modify a child span before it's sent.
418428
*
419429
* Note that this function is only called for child spans and not for the root span (formerly known as transaction).
420-
* If you want to modify or drop the root span, use {@link CoreOptions.beforeSendTransaction} instead.
430+
* If you want to modify or drop the root span, use {@link ClientOptions.beforeSendTransaction} instead.
421431
*
422432
* @param span The span generated by the SDK.
423433
*
424434
* @returns The modified span payload that will be sent.
425435
*/
426-
beforeSendSpan?: (span: SpanJSON) => SpanJSON;
436+
beforeSendSpan?: ((span: SpanJSON) => SpanJSON) | SpanV2CompatibleBeforeSendSpanCallback;
427437

428438
/**
429439
* An event-processing callback for transaction events, guaranteed to be invoked after all other event
@@ -455,6 +465,12 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
455465
beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | null;
456466
}
457467

468+
/**
469+
* A callback that is known to be compatible with actually receiving and returning a span v2 JSON object.
470+
* Only useful in conjunction with the {@link CoreOptions.traceLifecycle} option.
471+
*/
472+
export type SpanV2CompatibleBeforeSendSpanCallback = ((span: SpanV2JSON) => SpanV2JSON) & { _v2: true };
473+
458474
/** Base configuration options for every SDK. */
459475
export interface CoreOptions<TO extends BaseTransportOptions = BaseTransportOptions>
460476
extends Omit<Partial<ClientOptions<TO>>, 'integrations' | 'transport' | 'stackParser'> {

0 commit comments

Comments
 (0)