|
| 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