Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(core): Seal SentryTracerProvider spans against mutation after they end#21842
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -48,7 +48,12 @@ import { logSpanEnd } from './logSpans'; | ||
| import { timedEventsToMeasurements } from './measurement'; | ||
| import { getSegmentSpanCaptureStrategy, type SegmentSpanCaptureConvertOptions } from './segmentSpanCaptureStrategy'; | ||
| import { hasSpanStreamingEnabled } from './spans/hasSpanStreamingEnabled'; | ||
| import { getCapturedScopesOnSpan, markSpanSourceAsExplicit, spanShouldInferOtelSource } from './utils'; | ||
| import { | ||
| getCapturedScopesOnSpan, | ||
| markSpanSourceAsExplicit, | ||
| spanIsTracerProviderSpan, | ||
| spanShouldInferOtelSource, | ||
| } from './utils'; | ||
| const MAX_SPAN_COUNT = 1000; | ||
| @@ -75,6 +80,9 @@ export class SentrySpan implements Span { | ||
| /** if true, treat span as a standalone span (not part of a transaction) */ | ||
| private _isStandaloneSpan?: boolean; | ||
| /** if true, the span is sealed and ignores further mutations (set after end for tracer-provider spans) */ | ||
| private _frozen?: boolean; | ||
| /** | ||
| * You should never call the constructor manually, always use `Sentry.startSpan()` | ||
| * or other span methods. | ||
| @@ -120,6 +128,9 @@ export class SentrySpan implements Span { | ||
| /** @inheritDoc */ | ||
| public addLink(link: SpanLink): this { | ||
| if (this._frozen) { | ||
| return this; | ||
| } | ||
| if (this._links) { | ||
| this._links.push(link); | ||
| } else { | ||
| @@ -130,6 +141,9 @@ export class SentrySpan implements Span { | ||
| /** @inheritDoc */ | ||
| public addLinks(links: SpanLink[]): this { | ||
| if (this._frozen) { | ||
| return this; | ||
| } | ||
| if (this._links) { | ||
| this._links.push(...links); | ||
| } else { | ||
| @@ -161,6 +175,10 @@ export class SentrySpan implements Span { | ||
| /** @inheritdoc */ | ||
| public setAttribute(key: string, value: SpanAttributeValue | undefined): this { | ||
| if (this._frozen) { | ||
| return this; | ||
| } | ||
| if (value === undefined) { | ||
| // eslint-disable-next-line @typescript-eslint/no-dynamic-delete | ||
| delete this._attributes[key]; | ||
| @@ -192,13 +210,19 @@ export class SentrySpan implements Span { | ||
| * @internal | ||
| */ | ||
| public updateStartTime(timeInput: SpanTimeInput): void { | ||
| if (this._frozen) { | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (After looking at other functions the following is a bad idea, but I'll keep it here for food for thoughts) q: This might be an issue for a future method when we forget to add E.g. here we would update the MemberAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd rather not add more code to the SentrySpan, see Lukas' original objection. I think this kind of issue of forgetting to add is easily caught by review bots nowadays. Also, realistically, I don't think we'll expose much more api on this class. | ||
| return; | ||
| } | ||
| this._startTime = spanTimeInputToSeconds(timeInput); | ||
| } | ||
| /** | ||
| * @inheritDoc | ||
| */ | ||
| public setStatus(value: SpanStatus): this { | ||
| if (this._frozen) { | ||
| return this; | ||
| } | ||
| this._status = value; | ||
| return this; | ||
| } | ||
| @@ -207,6 +231,9 @@ export class SentrySpan implements Span { | ||
| * @inheritDoc | ||
| */ | ||
| public updateName(name: string): this { | ||
| if (this._frozen) { | ||
| return this; | ||
| } | ||
| this._name = name; | ||
| // Renaming a span marks its name as explicitly chosen, so we stamp `custom`. | ||
| // The exception is spans created by SentryTraceProvider: those are branded for | ||
| @@ -221,15 +248,29 @@ export class SentrySpan implements Span { | ||
| /** @inheritdoc */ | ||
| public end(endTimestamp?: SpanTimeInput): void { | ||
| // If already ended, skip | ||
| // If already ended, skip the end-of-span processing, but still seal a tracer-provider span. The | ||
| // seal at the bottom of this method is skipped on this early return, and `_endTime` may have been | ||
| // set before this first `end()` call (e.g. via the constructor's `endTimestamp`), which would | ||
| // otherwise leave the span mutable after `end()`. End-of-span processing already ran in that case. | ||
| if (this._endTime) { | ||
| this._frozen = spanIsTracerProviderSpan(this); | ||
| return; | ||
| } | ||
| this._endTime = spanTimeInputToSeconds(endTimestamp); | ||
| logSpanEnd(this); | ||
| this._onSpanEnded(); | ||
cursor[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| // A span created by the SentryTracerProvider is handed to OTel instrumentations as an OTel span, | ||
| // so once end-of-span processing is done (including the `spanEnd` hook where `applyOtelSpanData` | ||
| // finalizes status/source) it is sealed against further writes — mirroring the OpenTelemetry SDK, | ||
| // where setters no-op after a span has ended. Without this, an instrumentation that sets | ||
| // status/attributes after `end()` (e.g. Next.js on a render error) would overwrite the finalized | ||
| // values, and the deferred capture would then serialize those late writes. Spans created directly | ||
| // through the core API (e.g. the browser SDK, which backfills resource-timing attributes after a | ||
| // span ends) are not tracer-provider spans and stay mutable. | ||
| this._frozen = spanIsTracerProviderSpan(this); | ||
cursor[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. andreiborza marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| /** | ||
| @@ -298,6 +339,9 @@ export class SentrySpan implements Span { | ||
| attributesOrStartTime?: SpanAttributes | SpanTimeInput, | ||
| startTime?: SpanTimeInput, | ||
| ): this { | ||
| if (this._frozen) { | ||
| return this; | ||
| } | ||
| DEBUG_BUILD && debug.log('[Tracing] Adding an event to span:', name); | ||
| const time = isSpanTimeInput(attributesOrStartTime) ? attributesOrStartTime : startTime || timestampInSeconds(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,18 @@ | ||
| import { describe, expect, it, test, vi } from 'vitest'; | ||
| import { getCurrentScope } from '../../../src/currentScopes'; | ||
| import { setCurrentClient } from '../../../src/sdk'; | ||
| import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../../../src/semanticAttributes'; | ||
| import { | ||
| SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT, | ||
| SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE, | ||
| SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, | ||
| } from '../../../src/semanticAttributes'; | ||
| import { SentrySpan } from '../../../src/tracing/sentrySpan'; | ||
| import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus'; | ||
| import { markSpanForOtelSourceInference, spanSourceWasExplicitlySet } from '../../../src/tracing/utils'; | ||
| import { | ||
| markSpanAsTracerProviderSpan, | ||
| markSpanForOtelSourceInference, | ||
| spanSourceWasExplicitlySet, | ||
| } from '../../../src/tracing/utils'; | ||
| import type { SpanJSON } from '../../../src/types/span'; | ||
| import { spanToJSON, TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '../../../src/utils/spanUtils'; | ||
| import { timestampInSeconds } from '../../../src/utils/time'; | ||
| @@ -132,6 +140,78 @@ describe('SentrySpan', () => { | ||
| }); | ||
| }); | ||
| describe('tracer-provider span sealing', () => { | ||
| it('seals a tracer-provider span against all mutation after it ends', () => { | ||
| const span = new SentrySpan({ name: 'original', startTimestamp: 1, attributes: { key: 'before' } }); | ||
| span.setStatus({ code: SPAN_STATUS_ERROR, message: 'before' }); | ||
| span.addEvent('measurement', { | ||
| [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: 1, | ||
| [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond', | ||
| }); | ||
| const linked = new SentrySpan({ name: 'linked' }); | ||
| markSpanAsTracerProviderSpan(span); | ||
| span.end(); | ||
| // Every mutator must no-op on a tracer-provider span once it has ended, mirroring OTel SDK spans. | ||
| span.setAttribute('key', 'after'); | ||
| span.setAttributes({ key2: 'after' }); | ||
| span.setStatus({ code: SPAN_STATUS_ERROR, message: 'after' }); | ||
| span.updateName('after'); | ||
| span.updateStartTime(999); | ||
| span.addLink({ context: linked.spanContext() }); | ||
| span.addLinks([{ context: linked.spanContext() }]); | ||
| span.addEvent('measurement', { | ||
| [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: 2, | ||
| [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond', | ||
| }); | ||
| const json = spanToJSON(span); | ||
| expect(json.data?.['key']).toBe('before'); | ||
| expect(json.data?.['key2']).toBeUndefined(); | ||
| expect(json.status).toBe('before'); | ||
| expect(json.description).toBe('original'); | ||
| expect(json.start_timestamp).toBe(1); | ||
| expect(json.links).toBeUndefined(); | ||
| expect(json.measurements).toEqual({ measurement: { value: 1, unit: 'millisecond' } }); | ||
| }); | ||
| it('keeps a span that is not a tracer-provider span mutable after it ends', () => { | ||
| const span = new SentrySpan({ name: 'original', startTimestamp: 1, attributes: { key: 'before' } }); | ||
| const linked = new SentrySpan({ name: 'linked' }); | ||
| span.end(); | ||
| span.setAttribute('key', 'after'); | ||
| span.updateName('after'); | ||
| span.updateStartTime(999); | ||
| span.addLink({ context: linked.spanContext() }); | ||
| const json = spanToJSON(span); | ||
| expect(json.data?.['key']).toBe('after'); | ||
| expect(json.description).toBe('after'); | ||
| expect(json.start_timestamp).toBe(999); | ||
| expect(json.links).toHaveLength(1); | ||
| }); | ||
| it('seals a tracer-provider span that ended via the constructor endTimestamp', () => { | ||
| // `_endTime` is set in the constructor, so `end()` early-returns before reaching the seal at the | ||
| // bottom of its body. The span must still be sealed once `end()` is invoked. | ||
| const span = new SentrySpan({ | ||
| name: 'original', | ||
| startTimestamp: 1, | ||
| endTimestamp: 2, | ||
| attributes: { key: 'before' }, | ||
| }); | ||
| markSpanAsTracerProviderSpan(span); | ||
| span.end(); | ||
| span.setAttribute('key', 'after'); | ||
| expect(spanToJSON(span).data?.['key']).toBe('before'); | ||
| }); | ||
| }); | ||
andreiborza marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| describe('end', () => { | ||
| test('simple', () => { | ||
| const span = new SentrySpan({}); | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.