Skip to content

Commit 257c9fb

Browse files
committed
Pre-check wether transactions have gen_ai spans when we construct transactions
to lessen the extraction performance impact
1 parent 69b5cf8 commit 257c9fb

5 files changed

Lines changed: 58 additions & 39 deletions

File tree

packages/core/src/tracing/sentrySpan.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,12 @@ export class SentrySpan implements Span {
392392
// remove internal root span attributes we don't need to send.
393393
/* eslint-disable @typescript-eslint/no-dynamic-delete */
394394
delete this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME];
395+
let hasGenAiSpans = false;
395396
spans.forEach(span => {
396397
delete span.data[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME];
398+
if (span.op?.startsWith('gen_ai.')) {
399+
hasGenAiSpans = true;
400+
}
397401
});
398402
// eslint-enabled-next-line @typescript-eslint/no-dynamic-delete
399403

@@ -415,6 +419,7 @@ export class SentrySpan implements Span {
415419
capturedSpanScope,
416420
capturedSpanIsolationScope,
417421
dynamicSamplingContext: getDynamicSamplingContextFromSpan(this),
422+
hasGenAiSpans,
418423
},
419424
request: normalizedRequest,
420425
...(source && {

packages/core/src/tracing/spans/extractGenAiSpans.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ import { spanJsonToSerializedStreamedSpan } from './spanJsonToStreamedSpan';
1515
* item does not include the extracted spans.
1616
*/
1717
export function extractGenAiSpansFromEvent(event: Event, client: Client): SpanContainerItem | undefined {
18-
if (event.type !== 'transaction' || !event.spans?.length || hasSpanStreamingEnabled(client)) {
18+
if (
19+
event.type !== 'transaction' ||
20+
!event.spans?.length ||
21+
!event.sdkProcessingMetadata?.hasGenAiSpans ||
22+
hasSpanStreamingEnabled(client)
23+
) {
1924
return undefined;
2025
}
2126

packages/core/test/lib/tracing/sentrySpan.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ describe('SentrySpan', () => {
234234
trace_id: expect.stringMatching(/^[a-f0-9]{32}$/),
235235
transaction: 'test',
236236
},
237+
hasGenAiSpans: false,
237238
},
238239
spans: [],
239240
start_timestamp: 1,

packages/core/test/lib/tracing/spans/extractGenAiSpans.test.ts

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ function makeSpanJSON(overrides: Partial<SpanJSON> = {}): SpanJSON {
1414
};
1515
}
1616

17-
function makeTransactionEvent(spans: SpanJSON[]): Event {
17+
function makeTransactionEvent(spans: SpanJSON[], hasGenAiSpans = false): Event {
1818
return {
1919
type: 'transaction',
2020
transaction: 'GET /api/chat',
@@ -26,6 +26,9 @@ function makeTransactionEvent(spans: SpanJSON[]): Event {
2626
trace_id: '00112233445566778899aabbccddeeff',
2727
},
2828
},
29+
sdkProcessingMetadata: {
30+
...(hasGenAiSpans && { hasGenAiSpans: true }),
31+
},
2932
spans,
3033
};
3134
}
@@ -54,12 +57,9 @@ describe('extractGenAiSpansFromEvent', () => {
5457
timestamp: 1002,
5558
});
5659

57-
const event = makeTransactionEvent([genAiSpan, httpSpan]);
58-
const client = makeClient();
59-
60-
const result = extractGenAiSpansFromEvent(event, client);
60+
const event = makeTransactionEvent([genAiSpan, httpSpan], true);
61+
const result = extractGenAiSpansFromEvent(event, makeClient());
6162

62-
// gen_ai spans should be in the container item
6363
expect(result).toBeDefined();
6464
const [headers, payload] = result!;
6565
expect(headers.type).toBe('span');
@@ -69,7 +69,6 @@ describe('extractGenAiSpansFromEvent', () => {
6969
expect(payload.items[0]!.span_id).toBe('genai001');
7070
expect(payload.items[0]!.name).toBe('chat gpt-4');
7171

72-
// gen_ai spans should be removed from the event
7372
expect(event.spans).toHaveLength(1);
7473
expect(event.spans![0]!.span_id).toBe('http001');
7574
});
@@ -80,54 +79,47 @@ describe('extractGenAiSpansFromEvent', () => {
8079
const agentSpan = makeSpanJSON({ span_id: 'agent001', op: 'gen_ai.invoke_agent', description: 'agent' });
8180
const dbSpan = makeSpanJSON({ span_id: 'db001', op: 'db.query', description: 'SELECT *' });
8281

83-
const event = makeTransactionEvent([chatSpan, embeddingsSpan, dbSpan, agentSpan]);
84-
const client = makeClient();
85-
86-
const result = extractGenAiSpansFromEvent(event, client);
82+
const event = makeTransactionEvent([chatSpan, embeddingsSpan, dbSpan, agentSpan], true);
83+
const result = extractGenAiSpansFromEvent(event, makeClient());
8784

8885
expect(result).toBeDefined();
8986
expect(result![0].item_count).toBe(3);
9087
expect(result![1].items).toHaveLength(3);
9188
expect(result![1].items.map(s => s.span_id)).toEqual(['chat001', 'embed001', 'agent001']);
9289

93-
// Only the db span should remain
9490
expect(event.spans).toHaveLength(1);
9591
expect(event.spans![0]!.span_id).toBe('db001');
9692
});
9793

98-
it('returns undefined when there are no gen_ai spans', () => {
99-
const httpSpan = makeSpanJSON({ op: 'http.client' });
100-
const dbSpan = makeSpanJSON({ op: 'db.query' });
94+
it('returns undefined when hasGenAiSpans flag is not set', () => {
95+
const event = makeTransactionEvent([makeSpanJSON({ op: 'gen_ai.chat' })], false);
10196

102-
const event = makeTransactionEvent([httpSpan, dbSpan]);
103-
const client = makeClient();
97+
expect(extractGenAiSpansFromEvent(event, makeClient())).toBeUndefined();
98+
expect(event.spans).toHaveLength(1);
99+
});
104100

105-
const result = extractGenAiSpansFromEvent(event, client);
101+
it('returns undefined when there are no gen_ai spans', () => {
102+
const event = makeTransactionEvent([makeSpanJSON({ op: 'http.client' }), makeSpanJSON({ op: 'db.query' })], true);
106103

107-
expect(result).toBeUndefined();
104+
expect(extractGenAiSpansFromEvent(event, makeClient())).toBeUndefined();
108105
expect(event.spans).toHaveLength(2);
109106
});
110107

111108
it('returns undefined when event has no spans', () => {
112109
const event = makeTransactionEvent([]);
113-
const client = makeClient();
114-
115-
expect(extractGenAiSpansFromEvent(event, client)).toBeUndefined();
110+
expect(extractGenAiSpansFromEvent(event, makeClient())).toBeUndefined();
116111
});
117112

118113
it('returns undefined when event is not a transaction', () => {
119114
const event: Event = { type: undefined, spans: [makeSpanJSON({ op: 'gen_ai.chat' })] };
120-
const client = makeClient();
121-
122-
expect(extractGenAiSpansFromEvent(event, client)).toBeUndefined();
115+
expect(extractGenAiSpansFromEvent(event, makeClient())).toBeUndefined();
123116
});
124117

125118
it('returns undefined when span streaming is enabled', () => {
126-
const event = makeTransactionEvent([makeSpanJSON({ op: 'gen_ai.chat' })]);
119+
const event = makeTransactionEvent([makeSpanJSON({ op: 'gen_ai.chat' })], true);
127120
const client = makeClient({ traceLifecycle: 'stream' });
128121

129122
expect(extractGenAiSpansFromEvent(event, client)).toBeUndefined();
130-
// Spans should not be modified
131123
expect(event.spans).toHaveLength(1);
132124
});
133125

@@ -142,14 +134,10 @@ describe('extractGenAiSpansFromEvent', () => {
142134
op: 'http.client',
143135
});
144136

145-
const event = makeTransactionEvent([httpSpan, genAiSpan]);
146-
const client = makeClient();
147-
148-
const result = extractGenAiSpansFromEvent(event, client);
137+
const event = makeTransactionEvent([httpSpan, genAiSpan], true);
138+
const result = extractGenAiSpansFromEvent(event, makeClient());
149139

150-
// The v2 span should still reference the v1 parent
151140
expect(result![1].items[0]!.parent_span_id).toBe('http001');
152-
// The v1 parent should remain in the transaction
153141
expect(event.spans).toHaveLength(1);
154142
expect(event.spans![0]!.span_id).toBe('http001');
155143
});

packages/opentelemetry/src/spanExporter.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,11 @@ export class SentrySpanExporter {
203203
// We'll recursively add all the child spans to this array
204204
const spans = transactionEvent.spans || [];
205205

206+
let hasGenAiSpans = false;
206207
for (const child of root.children) {
207-
createAndFinishSpanForOtelSpan(child, spans, sentSpans);
208+
if (createAndFinishSpanForOtelSpan(child, spans, sentSpans)) {
209+
hasGenAiSpans = true;
210+
}
208211
}
209212

210213
// spans.sort() mutates the array, but we do not use this anymore after this point
@@ -214,6 +217,13 @@ export class SentrySpanExporter {
214217
? spans.sort((a, b) => a.start_timestamp - b.start_timestamp).slice(0, MAX_SPAN_COUNT)
215218
: spans;
216219

220+
if (hasGenAiSpans) {
221+
transactionEvent.sdkProcessingMetadata = {
222+
...transactionEvent.sdkProcessingMetadata,
223+
hasGenAiSpans: true,
224+
};
225+
}
226+
217227
const measurements = timedEventsToMeasurements(span.events);
218228
if (measurements) {
219229
transactionEvent.measurements = measurements;
@@ -330,7 +340,10 @@ export function createTransactionForOtelSpan(span: ReadableSpan): TransactionEve
330340
return transactionEvent;
331341
}
332342

333-
function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], sentSpans: Set<ReadableSpan>): void {
343+
/**
344+
* Returns `true` if this span or any descendant is a gen_ai span.
345+
*/
346+
function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], sentSpans: Set<ReadableSpan>): boolean {
334347
const span = node.span;
335348

336349
if (span) {
@@ -341,10 +354,13 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], sentS
341354

342355
// If this span should be dropped, we still want to create spans for the children of this
343356
if (shouldDrop) {
357+
let hasGenAiSpans = false;
344358
node.children.forEach(child => {
345-
createAndFinishSpanForOtelSpan(child, spans, sentSpans);
359+
if (createAndFinishSpanForOtelSpan(child, spans, sentSpans)) {
360+
hasGenAiSpans = true;
361+
}
346362
});
347-
return;
363+
return hasGenAiSpans;
348364
}
349365

350366
const span_id = span.spanContext().spanId;
@@ -381,9 +397,13 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], sentS
381397

382398
spans.push(spanJSON);
383399

400+
let hasGenAiSpans = !!op?.startsWith('gen_ai.');
384401
node.children.forEach(child => {
385-
createAndFinishSpanForOtelSpan(child, spans, sentSpans);
402+
if (createAndFinishSpanForOtelSpan(child, spans, sentSpans)) {
403+
hasGenAiSpans = true;
404+
}
386405
});
406+
return hasGenAiSpans;
387407
}
388408

389409
function getSpanData(span: ReadableSpan): {

0 commit comments

Comments
 (0)