Skip to content

Commit 6bd700e

Browse files
committed
capturespan
1 parent f42d562 commit 6bd700e

File tree

1 file changed

+137
-0
lines changed

1 file changed

+137
-0
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { type RawAttributes, isAttributeObject } from '../attributes';
2+
import type { Client } from '../client';
3+
import { getClient, getGlobalScope } from '../currentScopes';
4+
import { DEBUG_BUILD } from '../debug-build';
5+
import type { Scope, ScopeData } from '../scope';
6+
import {
7+
SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT,
8+
SEMANTIC_ATTRIBUTE_SENTRY_RELEASE,
9+
SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME,
10+
SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION,
11+
SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID,
12+
SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME,
13+
SEMANTIC_ATTRIBUTE_USER_EMAIL,
14+
SEMANTIC_ATTRIBUTE_USER_ID,
15+
SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS,
16+
SEMANTIC_ATTRIBUTE_USER_USERNAME,
17+
} from '../semanticAttributes';
18+
import { getCapturedScopesOnSpan } from '../tracing/utils';
19+
import type { Span, SpanV2JSON } from '../types-hoist/span';
20+
import { mergeScopeData } from '../utils/applyScopeDataToEvent';
21+
import { debug } from '../utils/debug-logger';
22+
import { INTERNAL_getSegmentSpan, spanToV2JSON } from '../utils/spanUtils';
23+
24+
/**
25+
* Captures a span and returns it to the caller, to be enqueued for sending.
26+
*/
27+
export function captureSpan(span: Span, client = getClient()): void {
28+
if (!client) {
29+
DEBUG_BUILD && debug.warn('No client available to capture span.');
30+
return;
31+
}
32+
33+
const segmentSpan = INTERNAL_getSegmentSpan(span);
34+
const serializedSegmentSpan = spanToV2JSON(segmentSpan);
35+
36+
const { isolationScope: spanIsolationScope, scope: spanScope } = getCapturedScopesOnSpan(span);
37+
const finalScopeData = getFinalScopeData(spanIsolationScope, spanScope);
38+
39+
const originalAttributeKeys = Object.keys(serializedSegmentSpan.attributes ?? {});
40+
41+
applyCommonSpanAttributes(span, serializedSegmentSpan, client, finalScopeData, originalAttributeKeys);
42+
43+
if (span === segmentSpan) {
44+
applyScopeToSegmentSpan(span, finalScopeData, originalAttributeKeys);
45+
}
46+
47+
// Wondering where we apply the beforeSendSpan callback?
48+
// We apply it directly before sending the span,
49+
// so whenever the buffer this span gets enqueued in is being flushed.
50+
// Why? Because we have to enqueue the span instance itself, not a JSON object.
51+
// We could temporarily convert to JSON here but this means that we'd then again
52+
// have to mutate the `span` instance (doesn't work for every kind of object mutation)
53+
// or construct a fully new span object. The latter is risky because users (or we) could hold
54+
// references to the original span instance.
55+
client.emit('enqueueSpan', span);
56+
}
57+
58+
function applyScopeToSegmentSpan(segmentSpan: Span, scopeData: ScopeData, originalAttributeKeys: string[]): void {
59+
// TODO: Apply all scope data from auto instrumentation (contexts, request) to segment span
60+
const { attributes } = scopeData;
61+
if (attributes) {
62+
setAttributesIfNotPresent(segmentSpan, originalAttributeKeys, attributes);
63+
}
64+
}
65+
66+
function applyCommonSpanAttributes(
67+
span: Span,
68+
serializedSegmentSpan: SpanV2JSON,
69+
client: Client,
70+
scopeData: ScopeData,
71+
originalAttributeKeys: string[],
72+
): void {
73+
const sdk = client.getSdkMetadata();
74+
const { release, environment, sendDefaultPii } = client.getOptions();
75+
76+
// avoid overwriting any previously set attributes (from users or potentially our SDK instrumentation)
77+
setAttributesIfNotPresent(span, originalAttributeKeys, {
78+
[SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: release,
79+
[SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: environment,
80+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: serializedSegmentSpan.name,
81+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: serializedSegmentSpan.span_id,
82+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: sdk?.sdk?.name,
83+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: sdk?.sdk?.version,
84+
...(sendDefaultPii
85+
? {
86+
[SEMANTIC_ATTRIBUTE_USER_ID]: scopeData.user?.id,
87+
[SEMANTIC_ATTRIBUTE_USER_EMAIL]: scopeData.user?.email,
88+
[SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS]: scopeData.user?.ip_address ?? undefined,
89+
[SEMANTIC_ATTRIBUTE_USER_USERNAME]: scopeData.user?.username,
90+
}
91+
: {}),
92+
});
93+
}
94+
95+
// TODO: Extract this to a helper in core. It's used in multiple places.
96+
function getFinalScopeData(isolationScope: Scope | undefined, scope: Scope | undefined): ScopeData {
97+
const finalScopeData = getGlobalScope().getScopeData();
98+
if (isolationScope) {
99+
mergeScopeData(finalScopeData, isolationScope.getScopeData());
100+
}
101+
if (scope) {
102+
mergeScopeData(finalScopeData, scope.getScopeData());
103+
}
104+
return finalScopeData;
105+
}
106+
107+
function setAttributesIfNotPresent(
108+
span: Span,
109+
originalAttributeKeys: string[],
110+
newAttributes: RawAttributes<Record<string, unknown>>,
111+
): void {
112+
Object.keys(newAttributes).forEach(key => {
113+
if (!originalAttributeKeys.includes(key)) {
114+
setAttributeOnSpanWithMaybeUnit(span, key, newAttributes[key]);
115+
}
116+
});
117+
}
118+
119+
function setAttributeOnSpanWithMaybeUnit(span: Span, attributeKey: string, attributeValue: unknown): void {
120+
if (isAttributeObject(attributeValue)) {
121+
const { value, unit } = attributeValue;
122+
123+
if (isSupportedAttributeType(value)) {
124+
span.setAttribute(attributeKey, value);
125+
}
126+
127+
if (unit) {
128+
span.setAttribute(`${attributeKey}.unit`, unit);
129+
}
130+
} else if (isSupportedAttributeType(attributeValue)) {
131+
span.setAttribute(attributeKey, attributeValue);
132+
}
133+
}
134+
135+
function isSupportedAttributeType(value: unknown): value is Parameters<Span['setAttribute']>[1] {
136+
return ['string', 'number', 'boolean'].includes(typeof value) || Array.isArray(value);
137+
}

0 commit comments

Comments
 (0)