Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 0 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,8 +453,6 @@ export type {
SpanContextData,
TraceFlag,
StreamedSpanJSON,
SerializedSpanContainer,
Comment thread
Lms24 marked this conversation as resolved.
SerializedSpan,
} from './types-hoist/span';
export type { SpanStatus } from './types-hoist/spanStatus';
export type { Log, LogSeverityLevel } from './types-hoist/log';
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/tracing/spans/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
For now, all span streaming related tracing code is in this sub directory.
Once we get rid of transaction-based tracing, we can clean up and flatten the entire tracing directory.
36 changes: 36 additions & 0 deletions packages/core/src/tracing/spans/envelope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Client } from '../../client';
import type { DynamicSamplingContext, SpanContainerItem, SpanV2Envelope } from '../../types-hoist/envelope';
import type { SerializedSpan } from '../../types-hoist/span';
import { dsnToString } from '../../utils/dsn';
import { createEnvelope } from '../../utils/envelope';

/**
* Creates a span v2 span streaming envelope
*/
export function createSpanV2Envelope(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: Just that we don't forget: #19140 (comment)

serializedSpans: Array<SerializedSpan>,
dsc: Partial<DynamicSamplingContext>,
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 }),
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
...(!!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<SpanV2Envelope>(headers, [spanContainer]);
}

function dscHasRequiredProps(dsc: Partial<DynamicSamplingContext>): dsc is DynamicSamplingContext {
return !!dsc.trace_id && !!dsc.public_key;
}
232 changes: 232 additions & 0 deletions packages/core/test/lib/tracing/spans/envelope.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { describe, expect, it } from 'vitest';
import { createSpanV2Envelope } from '../../../../src/tracing/spans/envelope';
import type { DynamicSamplingContext } from '../../../../src/types-hoist/envelope';
import type { SerializedSpan } from '../../../../src/types-hoist/span';
import { getDefaultTestClientOptions, TestClient } from '../../../mocks/client';

function createMockSerializedSpan(overrides: Partial<SerializedSpan> = {}): SerializedSpan {
return {
trace_id: 'abc123',
span_id: 'def456',
name: 'test-span',
start_timestamp: 1713859200,
end_timestamp: 1713859201,
status: 'ok',
is_segment: false,
...overrides,
};
}

describe('createSpanV2Envelope', () => {
describe('envelope headers', () => {
it('creates an envelope with sent_at header', () => {
const mockSpan = createMockSerializedSpan();
const mockClient = new TestClient(getDefaultTestClientOptions());
const dsc: Partial<DynamicSamplingContext> = {};

const result = createSpanV2Envelope([mockSpan], dsc, mockClient);

expect(result[0]).toHaveProperty('sent_at', expect.any(String));
});

it('includes trace header when DSC has required props (trace_id and public_key)', () => {
const mockSpan = createMockSerializedSpan();
const mockClient = new TestClient(getDefaultTestClientOptions());
const dsc: DynamicSamplingContext = {
trace_id: 'trace-123',
public_key: 'public-key-abc',
sample_rate: '1.0',
release: 'v1.0.0',
};

const result = createSpanV2Envelope([mockSpan], dsc, mockClient);

expect(result[0]).toHaveProperty('trace', dsc);
});

it("does't include trace header when DSC is missing trace_id", () => {
const mockSpan = createMockSerializedSpan();
const mockClient = new TestClient(getDefaultTestClientOptions());
const dsc: Partial<DynamicSamplingContext> = {
public_key: 'public-key-abc',
};

const result = createSpanV2Envelope([mockSpan], dsc, mockClient);

expect(result[0]).not.toHaveProperty('trace');
});

it("does't include trace header when DSC is missing public_key", () => {
const mockSpan = createMockSerializedSpan();
const mockClient = new TestClient(getDefaultTestClientOptions());
const dsc: Partial<DynamicSamplingContext> = {
trace_id: 'trace-123',
};

const result = createSpanV2Envelope([mockSpan], dsc, mockClient);

expect(result[0]).not.toHaveProperty('trace');
});

it('includes SDK info when available in client options', () => {
const mockSpan = createMockSerializedSpan();
const mockClient = new TestClient(
getDefaultTestClientOptions({
_metadata: {
sdk: { name: 'sentry.javascript.browser', version: '8.0.0' },
},
}),
);
const dsc: Partial<DynamicSamplingContext> = {};

const result = createSpanV2Envelope([mockSpan], dsc, mockClient);

expect(result[0]).toHaveProperty('sdk', { name: 'sentry.javascript.browser', version: '8.0.0' });
});

it("does't include SDK info when not available", () => {
const mockSpan = createMockSerializedSpan();
const mockClient = new TestClient(getDefaultTestClientOptions());
const dsc: Partial<DynamicSamplingContext> = {};

const result = createSpanV2Envelope([mockSpan], dsc, mockClient);

expect(result[0]).not.toHaveProperty('sdk');
});

it('includes DSN when tunnel and DSN are configured', () => {
const mockSpan = createMockSerializedSpan();
const mockClient = new TestClient(
getDefaultTestClientOptions({
dsn: 'https://abc123@example.sentry.io/456',
tunnel: 'https://tunnel.example.com',
}),
);
const dsc: Partial<DynamicSamplingContext> = {};

const result = createSpanV2Envelope([mockSpan], dsc, mockClient);

expect(result[0]).toHaveProperty('dsn', 'https://abc123@example.sentry.io/456');
});

it("does't include DSN when tunnel is not configured", () => {
const mockSpan = createMockSerializedSpan();
const mockClient = new TestClient(
getDefaultTestClientOptions({
dsn: 'https://abc123@example.sentry.io/456',
}),
);
const dsc: Partial<DynamicSamplingContext> = {};

const result = createSpanV2Envelope([mockSpan], dsc, mockClient);

expect(result[0]).not.toHaveProperty('dsn');
});

it("does't include DSN when DSN is not available", () => {
const mockSpan = createMockSerializedSpan();
const mockClient = new TestClient(
getDefaultTestClientOptions({
tunnel: 'https://tunnel.example.com',
}),
);
const dsc: Partial<DynamicSamplingContext> = {};

const result = createSpanV2Envelope([mockSpan], dsc, mockClient);

expect(result[0]).not.toHaveProperty('dsn');
});

it('includes all headers when all options are provided', () => {
const mockSpan = createMockSerializedSpan();
const mockClient = new TestClient(
getDefaultTestClientOptions({
dsn: 'https://abc123@example.sentry.io/456',
tunnel: 'https://tunnel.example.com',
_metadata: {
sdk: { name: 'sentry.javascript.node', version: '10.38.0' },
},
}),
);
const dsc: DynamicSamplingContext = {
trace_id: 'trace-123',
public_key: 'public-key-abc',
environment: 'production',
};

const result = createSpanV2Envelope([mockSpan], dsc, mockClient);

expect(result[0]).toEqual({
sent_at: expect.any(String),
trace: dsc,
sdk: { name: 'sentry.javascript.node', version: '10.38.0' },
dsn: 'https://abc123@example.sentry.io/456',
});
});
});

describe('envelope item', () => {
it('creates a span container item with correct structure', () => {
const mockSpan = createMockSerializedSpan({ name: 'span-1' });
const mockClient = new TestClient(getDefaultTestClientOptions());
const dsc: Partial<DynamicSamplingContext> = {};

const envelopeItems = createSpanV2Envelope([mockSpan], dsc, mockClient)[1];

expect(envelopeItems).toEqual([
[
{
content_type: 'application/vnd.sentry.items.span.v2+json',
item_count: 1,
type: 'span',
},
{
items: [mockSpan],
},
],
]);
});

it('sets correct item_count for multiple spans', () => {
const mockSpan1 = createMockSerializedSpan({ span_id: 'span-1' });
const mockSpan2 = createMockSerializedSpan({ span_id: 'span-2' });
const mockSpan3 = createMockSerializedSpan({ span_id: 'span-3' });
const mockClient = new TestClient(getDefaultTestClientOptions());
const dsc: Partial<DynamicSamplingContext> = {};

const envelopeItems = createSpanV2Envelope([mockSpan1, mockSpan2, mockSpan3], dsc, mockClient)[1];

expect(envelopeItems).toEqual([
[
{ type: 'span', item_count: 3, content_type: 'application/vnd.sentry.items.span.v2+json' },
{ items: [mockSpan1, mockSpan2, mockSpan3] },
],
]);
});

it('handles empty spans array', () => {
const mockClient = new TestClient(getDefaultTestClientOptions());
const dsc: Partial<DynamicSamplingContext> = {};

const result = createSpanV2Envelope([], dsc, mockClient);

expect(result).toEqual([
{
sent_at: expect.any(String),
},
[
[
{
content_type: 'application/vnd.sentry.items.span.v2+json',
item_count: 0,
type: 'span',
},
{
items: [],
},
],
],
]);
});
});
});
Comment thread
Lms24 marked this conversation as resolved.
Loading