Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions packages/core/src/shared-exports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,6 +103,7 @@ export {
spanToJSON,
spanToStreamedSpanJSON,
spanIsSampled,
spanIsSentrySpan,
spanToTraceContext,
getSpanDescendants,
getStatusMessage,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/tracing/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,10 @@ export {
getCapturedScopesOnSpan,
markSpanForOtelSourceInference,
spanShouldInferOtelSource,
markSpanSourceAsExplicit,
spanSourceWasExplicitlySet,
markSpanAsTracerProviderSpan,
spanIsTracerProviderSpan,
} from './utils';
export { startIdleSpan, TRACING_DEFAULTS } from './idleSpan';
export { SentrySpan } from './sentrySpan';
Expand All@@ -13,12 +17,14 @@ export { SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET } from './spanstat
export {
startSpan,
startInactiveSpan,
_INTERNAL_startInactiveSpan,
startSpanManual,
continueTrace,
withActiveSpan,
suppressTracing,
isTracingSuppressed,
startNewTrace,
spanIsIgnored,
SUPPRESS_TRACING_KEY,
} from './trace';
export { bindScopeToEmitter } from './bindScopeToEmitter';
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/tracing/sentrySpan.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
import { logSpanEnd } from './logSpans';
import { timedEventsToMeasurements } from './measurement';
import { hasSpanStreamingEnabled } from './spans/hasSpanStreamingEnabled';
import { getCapturedScopesOnSpan, spanShouldInferOtelSource } from './utils';
import { getCapturedScopesOnSpan, markSpanSourceAsExplicit, spanShouldInferOtelSource } from './utils';

const MAX_SPAN_COUNT = 1000;

Expand DownExpand Up@@ -144,7 +144,7 @@ export class SentrySpan implements Span {
* @hidden
* @internal
*/
public recordException(_exception: unknown, _time?: number | undefined): void {
public recordException(_exception: unknown, _time?: SpanTimeInput | undefined): void {
// noop
}

Expand All@@ -167,6 +167,12 @@ export class SentrySpan implements Span {
this._attributes[key] = value;
}

// Setting the source on a span branded for OTel-style inference means user code is choosing it
// explicitly, so flag it to keep `applyOtelSpanData` from overriding it with an inferred source.
if (key === SEMANTIC_ATTRIBUTE_SENTRY_SOURCE && value !== undefined && spanShouldInferOtelSource(this)) {
markSpanSourceAsExplicit(this);
}
Comment thread
andreiborza marked this conversation as resolved.

return this;
}

Expand Down
25 changes: 22 additions & 3 deletions packages/core/src/tracing/trace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,7 +82,7 @@ export function startSpan<T>(options: StartSpanOptions, callback: (span: Span) =
// Ignored root spans still need to be set on scope so that `getActiveSpan()` returns them
// and descendants are also non-recording. Ignored child spans don't need this because
// the parent span is already on scope.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
if (!spanIsIgnored(activeSpan) || !parentSpan) {
_setSpanForScope(scope, activeSpan);
}

Expand DownExpand Up@@ -144,7 +144,7 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S

// We don't set ignored child spans onto the scope because there likely is an active,
// unignored span on the scope already.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
if (!spanIsIgnored(activeSpan) || !parentSpan) {
_setSpanForScope(scope, activeSpan);
}

Expand DownExpand Up@@ -181,6 +181,20 @@ export function startInactiveSpan(options: StartSpanOptions): Span {
return acs.startInactiveSpan(options);
}

return _startInactiveSpanImpl(options);
}

/**
* Internal version of startInactiveSpan that bypasses the ACS check.
* Used by SentryTracerProvider to create spans without triggering recursion
* through ACS overrides.
* @hidden
*/
export function _INTERNAL_startInactiveSpan(options: StartSpanOptions): Span {
return _startInactiveSpanImpl(options);
}

function _startInactiveSpanImpl(options: StartSpanOptions): Span {
const spanArguments = parseSentrySpanArguments(options);
const { forceTransaction, parentSpan: customParentSpan } = options;

Expand DownExpand Up@@ -651,6 +665,11 @@ function _shouldIgnoreStreamedSpan(client: Client | undefined, spanArguments: Se
);
}

function _isIgnoredSpan(span: Span): span is SentryNonRecordingSpan {
/**
* Whether a span is an ignored (`ignoreSpans`) placeholder. Such a span must not be set as the active
* span when it has a parent, so its children attach to that parent and get re-parented rather than
* dropped with it. Shared with the OTel-based provider so both span pipelines apply the same rule.
*/
export function spanIsIgnored(span: Span): span is SentryNonRecordingSpan {
return spanIsNonRecordingSpan(span) && span.dropReason === 'ignored';
}
46 changes: 46 additions & 0 deletions packages/core/src/tracing/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,13 +12,30 @@ const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope';
// so the key is shared across duplicated copies of `@sentry/core`.
const OTEL_SOURCE_INFERENCE_SPAN_FIELD = Symbol.for('sentry.otelSourceInference');

// Brand marking a span (otherwise subject to OTel-style source inference, see above) whose
// `sentry.source` was explicitly set by user code after creation, so `applyOtelSpanData` stops
// inferring and respects the chosen source and name. This is what tells a user-set `custom` source
// apart from the default `custom` that `_startRootSpan` stamps on every root span.
const OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD = Symbol.for('sentry.otelSourceExplicitlySet');

// Brand marking a span created by the `SentryTracerProvider` (i.e. via the OTel tracer) rather than
// directly through the core span API. Such a span is handed to OTel instrumentations as an OTel span,
// so it must become immutable after `end()` like a real OTel SDK span (see `SentrySpan.end()`). Spans
// created directly through core (e.g. the browser SDK) are not branded and stay mutable.
const TRACER_PROVIDER_SPAN_FIELD = Symbol.for('sentry.tracerProviderSpan');

type SpanWithScopes = Span & {
[SCOPE_ON_START_SPAN_FIELD]?: Scope;
[ISOLATION_SCOPE_ON_START_SPAN_FIELD]?: MaybeWeakRef<Scope>;
};

type SpanWithOtelSourceInference = Span & {
[OTEL_SOURCE_INFERENCE_SPAN_FIELD]?: boolean;
[OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD]?: boolean;
};

type SpanWithTracerProviderBrand = Span & {
[TRACER_PROVIDER_SPAN_FIELD]?: boolean;
};

/** Store the scope & isolation scope for a span, which can the be used when it is finished. */
Expand DownExpand Up@@ -57,3 +74,32 @@ export function markSpanForOtelSourceInference(span: Span): void {
export function spanShouldInferOtelSource(span: Span): boolean {
return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_INFERENCE_SPAN_FIELD] === true;
}

/**
* Mark that user code explicitly set `sentry.source` on a span subject to OTel-style inference, so
* `applyOtelSpanData` keeps that source (and name) instead of overriding it. Set by `SentrySpan`
* when `setAttribute` writes the source on an already-branded span (the default `custom` source is
* stamped at construction, before the brand, so it doesn't trip this).
*/
export function markSpanSourceAsExplicit(span: Span): void {
addNonEnumerableProperty(span, OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD, true);
}

/** Whether user code explicitly set `sentry.source` on a span (see {@link markSpanSourceAsExplicit}). */
export function spanSourceWasExplicitlySet(span: Span): boolean {
return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD] === true;
}

/**
* Mark a span as created by the `SentryTracerProvider` (via the OTel tracer). Set by `SentryTracer`
* on every span it creates; read by `SentrySpan.end()` to seal the span against further writes once
* it has ended, mirroring OTel SDK spans (which are immutable after `end()`).
*/
export function markSpanAsTracerProviderSpan(span: Span): void {
addNonEnumerableProperty(span, TRACER_PROVIDER_SPAN_FIELD, true);
}

/** Whether a span was created by the `SentryTracerProvider` (see {@link markSpanAsTracerProviderSpan}). */
export function spanIsTracerProviderSpan(span: Span): boolean {
return (span as SpanWithTracerProviderBrand)[TRACER_PROVIDER_SPAN_FIELD] === true;
}
2 changes: 1 addition & 1 deletion packages/core/src/types/span.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -319,5 +319,5 @@ export interface Span {
/**
* NOT USED IN SENTRY, only added for compliance with OTEL Span interface
*/
recordException(exception: unknown, time?: number): void;
recordException(exception: unknown, time?: SpanTimeInput): void;
}
2 changes: 1 addition & 1 deletion packages/core/src/utils/spanUtils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -299,7 +299,7 @@ export interface OpenTelemetrySdkTraceBaseSpan extends Span {
* Sadly, due to circular dependency checks we cannot actually import the Span class here and check for instanceof.
* :( So instead we approximate this by checking if it has the `getSpanJSON` method.
*/
function spanIsSentrySpan(span: Span): span is SentrySpan {
export function spanIsSentrySpan(span: Span): span is SentrySpan {
return typeof (span as SentrySpan).getSpanJSON === 'function';
}

Expand Down
32 changes: 31 additions & 1 deletion packages/core/test/lib/tracing/sentrySpan.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { setCurrentClient } from '../../../src/sdk';
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../../../src/semanticAttributes';
import { SentrySpan } from '../../../src/tracing/sentrySpan';
import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus';
import { markSpanForOtelSourceInference } from '../../../src/tracing/utils';
import { 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';
Expand DownExpand Up@@ -61,6 +61,36 @@ describe('SentrySpan', () => {
});
});

describe('explicit source', () => {
it('flags a source set on a span marked for OTel source inference as explicit', () => {
const span = new SentrySpan({ name: 'original name' });
markSpanForOtelSourceInference(span);
expect(spanSourceWasExplicitlySet(span)).toBe(false);

span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');

expect(spanSourceWasExplicitlySet(span)).toBe(true);
});

it('does not flag the default source set at construction (before the inference brand) as explicit', () => {
const span = new SentrySpan({
name: 'original name',
attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
});
markSpanForOtelSourceInference(span);

expect(spanSourceWasExplicitlySet(span)).toBe(false);
});

it('does not flag a source set on a span that is not marked for OTel source inference', () => {
const span = new SentrySpan({ name: 'original name' });

span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');

expect(spanSourceWasExplicitlySet(span)).toBe(false);
});
});

describe('setters', () => {
test('setName', () => {
const span = new SentrySpan({});
Expand Down
30 changes: 30 additions & 0 deletions packages/opentelemetry/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,36 @@ function setupSentry() {
A full setup example can be found in
[node-experimental](https://github.com/getsentry/sentry-javascript/blob/develop/packages/node-experimental).

## Experimental Sentry Tracer Provider

`SentryTracerProvider` is an experimental minimal OpenTelemetry tracer provider which creates native Sentry spans directly.
It is useful when code uses the global OpenTelemetry API and you do not need the full OpenTelemetry SDK span processor
and exporter pipeline.

```js
import { trace } from '@opentelemetry/api';
import { SentryTracerProvider } from '@sentry/opentelemetry';

trace.setGlobalTracerProvider(new SentryTracerProvider());

const span = trace.getTracer('example').startSpan('work');
span.end();
```

In `@sentry/node`, this provider can be enabled with the experimental option:

```js
Sentry.init({
dsn: 'xxx',
_experiments: {
useSentryTracerProvider: true,
},
});
```

When this provider is enabled, additional OpenTelemetry span processors are ignored because Sentry spans are created
directly. OpenTelemetry logs and metrics are not handled by this provider.

## Links

- [Official SDK Docs](https://docs.sentry.io/quickstart/)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions packages/core/src/shared-exports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,6 +103,7 @@ export {
spanToJSON,
spanToStreamedSpanJSON,
spanIsSampled,
spanIsSentrySpan,
spanToTraceContext,
getSpanDescendants,
getStatusMessage,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/tracing/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,10 @@ export {
getCapturedScopesOnSpan,
markSpanForOtelSourceInference,
spanShouldInferOtelSource,
markSpanSourceAsExplicit,
spanSourceWasExplicitlySet,
markSpanAsTracerProviderSpan,
spanIsTracerProviderSpan,
} from './utils';
export { startIdleSpan, TRACING_DEFAULTS } from './idleSpan';
export { SentrySpan } from './sentrySpan';
Expand All@@ -13,12 +17,14 @@ export { SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET } from './spanstat
export {
startSpan,
startInactiveSpan,
_INTERNAL_startInactiveSpan,
startSpanManual,
continueTrace,
withActiveSpan,
suppressTracing,
isTracingSuppressed,
startNewTrace,
spanIsIgnored,
SUPPRESS_TRACING_KEY,
} from './trace';
export { bindScopeToEmitter } from './bindScopeToEmitter';
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/tracing/sentrySpan.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
import { logSpanEnd } from './logSpans';
import { timedEventsToMeasurements } from './measurement';
import { hasSpanStreamingEnabled } from './spans/hasSpanStreamingEnabled';
import { getCapturedScopesOnSpan, spanShouldInferOtelSource } from './utils';
import { getCapturedScopesOnSpan, markSpanSourceAsExplicit, spanShouldInferOtelSource } from './utils';

const MAX_SPAN_COUNT = 1000;

Expand DownExpand Up@@ -144,7 +144,7 @@ export class SentrySpan implements Span {
* @hidden
* @internal
*/
public recordException(_exception: unknown, _time?: number | undefined): void {
public recordException(_exception: unknown, _time?: SpanTimeInput | undefined): void {
// noop
}

Expand All@@ -167,6 +167,12 @@ export class SentrySpan implements Span {
this._attributes[key] = value;
}

// Setting the source on a span branded for OTel-style inference means user code is choosing it
// explicitly, so flag it to keep `applyOtelSpanData` from overriding it with an inferred source.
if (key === SEMANTIC_ATTRIBUTE_SENTRY_SOURCE && value !== undefined && spanShouldInferOtelSource(this)) {
markSpanSourceAsExplicit(this);
}
Comment thread
andreiborza marked this conversation as resolved.

return this;
}

Expand Down
25 changes: 22 additions & 3 deletions packages/core/src/tracing/trace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,7 +82,7 @@ export function startSpan<T>(options: StartSpanOptions, callback: (span: Span) =
// Ignored root spans still need to be set on scope so that `getActiveSpan()` returns them
// and descendants are also non-recording. Ignored child spans don't need this because
// the parent span is already on scope.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
if (!spanIsIgnored(activeSpan) || !parentSpan) {
_setSpanForScope(scope, activeSpan);
}

Expand DownExpand Up@@ -144,7 +144,7 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S

// We don't set ignored child spans onto the scope because there likely is an active,
// unignored span on the scope already.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
if (!spanIsIgnored(activeSpan) || !parentSpan) {
_setSpanForScope(scope, activeSpan);
}

Expand DownExpand Up@@ -181,6 +181,20 @@ export function startInactiveSpan(options: StartSpanOptions): Span {
return acs.startInactiveSpan(options);
}

return _startInactiveSpanImpl(options);
}

/**
* Internal version of startInactiveSpan that bypasses the ACS check.
* Used by SentryTracerProvider to create spans without triggering recursion
* through ACS overrides.
* @hidden
*/
export function _INTERNAL_startInactiveSpan(options: StartSpanOptions): Span {
return _startInactiveSpanImpl(options);
}

function _startInactiveSpanImpl(options: StartSpanOptions): Span {
const spanArguments = parseSentrySpanArguments(options);
const { forceTransaction, parentSpan: customParentSpan } = options;

Expand DownExpand Up@@ -651,6 +665,11 @@ function _shouldIgnoreStreamedSpan(client: Client | undefined, spanArguments: Se
);
}

function _isIgnoredSpan(span: Span): span is SentryNonRecordingSpan {
/**
* Whether a span is an ignored (`ignoreSpans`) placeholder. Such a span must not be set as the active
* span when it has a parent, so its children attach to that parent and get re-parented rather than
* dropped with it. Shared with the OTel-based provider so both span pipelines apply the same rule.
*/
export function spanIsIgnored(span: Span): span is SentryNonRecordingSpan {
return spanIsNonRecordingSpan(span) && span.dropReason === 'ignored';
}
46 changes: 46 additions & 0 deletions packages/core/src/tracing/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,13 +12,30 @@ const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope';
// so the key is shared across duplicated copies of `@sentry/core`.
const OTEL_SOURCE_INFERENCE_SPAN_FIELD = Symbol.for('sentry.otelSourceInference');

// Brand marking a span (otherwise subject to OTel-style source inference, see above) whose
// `sentry.source` was explicitly set by user code after creation, so `applyOtelSpanData` stops
// inferring and respects the chosen source and name. This is what tells a user-set `custom` source
// apart from the default `custom` that `_startRootSpan` stamps on every root span.
const OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD = Symbol.for('sentry.otelSourceExplicitlySet');

// Brand marking a span created by the `SentryTracerProvider` (i.e. via the OTel tracer) rather than
// directly through the core span API. Such a span is handed to OTel instrumentations as an OTel span,
// so it must become immutable after `end()` like a real OTel SDK span (see `SentrySpan.end()`). Spans
// created directly through core (e.g. the browser SDK) are not branded and stay mutable.
const TRACER_PROVIDER_SPAN_FIELD = Symbol.for('sentry.tracerProviderSpan');

type SpanWithScopes = Span & {
[SCOPE_ON_START_SPAN_FIELD]?: Scope;
[ISOLATION_SCOPE_ON_START_SPAN_FIELD]?: MaybeWeakRef<Scope>;
};

type SpanWithOtelSourceInference = Span & {
[OTEL_SOURCE_INFERENCE_SPAN_FIELD]?: boolean;
[OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD]?: boolean;
};

type SpanWithTracerProviderBrand = Span & {
[TRACER_PROVIDER_SPAN_FIELD]?: boolean;
};

/** Store the scope & isolation scope for a span, which can the be used when it is finished. */
Expand DownExpand Up@@ -57,3 +74,32 @@ export function markSpanForOtelSourceInference(span: Span): void {
export function spanShouldInferOtelSource(span: Span): boolean {
return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_INFERENCE_SPAN_FIELD] === true;
}

/**
* Mark that user code explicitly set `sentry.source` on a span subject to OTel-style inference, so
* `applyOtelSpanData` keeps that source (and name) instead of overriding it. Set by `SentrySpan`
* when `setAttribute` writes the source on an already-branded span (the default `custom` source is
* stamped at construction, before the brand, so it doesn't trip this).
*/
export function markSpanSourceAsExplicit(span: Span): void {
addNonEnumerableProperty(span, OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD, true);
}

/** Whether user code explicitly set `sentry.source` on a span (see {@link markSpanSourceAsExplicit}). */
export function spanSourceWasExplicitlySet(span: Span): boolean {
return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD] === true;
}

/**
* Mark a span as created by the `SentryTracerProvider` (via the OTel tracer). Set by `SentryTracer`
* on every span it creates; read by `SentrySpan.end()` to seal the span against further writes once
* it has ended, mirroring OTel SDK spans (which are immutable after `end()`).
*/
export function markSpanAsTracerProviderSpan(span: Span): void {
addNonEnumerableProperty(span, TRACER_PROVIDER_SPAN_FIELD, true);
}

/** Whether a span was created by the `SentryTracerProvider` (see {@link markSpanAsTracerProviderSpan}). */
export function spanIsTracerProviderSpan(span: Span): boolean {
return (span as SpanWithTracerProviderBrand)[TRACER_PROVIDER_SPAN_FIELD] === true;
}
2 changes: 1 addition & 1 deletion packages/core/src/types/span.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -319,5 +319,5 @@ export interface Span {
/**
* NOT USED IN SENTRY, only added for compliance with OTEL Span interface
*/
recordException(exception: unknown, time?: number): void;
recordException(exception: unknown, time?: SpanTimeInput): void;
}
2 changes: 1 addition & 1 deletion packages/core/src/utils/spanUtils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -299,7 +299,7 @@ export interface OpenTelemetrySdkTraceBaseSpan extends Span {
* Sadly, due to circular dependency checks we cannot actually import the Span class here and check for instanceof.
* :( So instead we approximate this by checking if it has the `getSpanJSON` method.
*/
function spanIsSentrySpan(span: Span): span is SentrySpan {
export function spanIsSentrySpan(span: Span): span is SentrySpan {
return typeof (span as SentrySpan).getSpanJSON === 'function';
}

Expand Down
32 changes: 31 additions & 1 deletion packages/core/test/lib/tracing/sentrySpan.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { setCurrentClient } from '../../../src/sdk';
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../../../src/semanticAttributes';
import { SentrySpan } from '../../../src/tracing/sentrySpan';
import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus';
import { markSpanForOtelSourceInference } from '../../../src/tracing/utils';
import { 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';
Expand DownExpand Up@@ -61,6 +61,36 @@ describe('SentrySpan', () => {
});
});

describe('explicit source', () => {
it('flags a source set on a span marked for OTel source inference as explicit', () => {
const span = new SentrySpan({ name: 'original name' });
markSpanForOtelSourceInference(span);
expect(spanSourceWasExplicitlySet(span)).toBe(false);

span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');

expect(spanSourceWasExplicitlySet(span)).toBe(true);
});

it('does not flag the default source set at construction (before the inference brand) as explicit', () => {
const span = new SentrySpan({
name: 'original name',
attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
});
markSpanForOtelSourceInference(span);

expect(spanSourceWasExplicitlySet(span)).toBe(false);
});

it('does not flag a source set on a span that is not marked for OTel source inference', () => {
const span = new SentrySpan({ name: 'original name' });

span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');

expect(spanSourceWasExplicitlySet(span)).toBe(false);
});
});

describe('setters', () => {
test('setName', () => {
const span = new SentrySpan({});
Expand Down
30 changes: 30 additions & 0 deletions packages/opentelemetry/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,36 @@ function setupSentry() {
A full setup example can be found in
[node-experimental](https://github.com/getsentry/sentry-javascript/blob/develop/packages/node-experimental).

## Experimental Sentry Tracer Provider

`SentryTracerProvider` is an experimental minimal OpenTelemetry tracer provider which creates native Sentry spans directly.
It is useful when code uses the global OpenTelemetry API and you do not need the full OpenTelemetry SDK span processor
and exporter pipeline.

```js
import { trace } from '@opentelemetry/api';
import { SentryTracerProvider } from '@sentry/opentelemetry';

trace.setGlobalTracerProvider(new SentryTracerProvider());

const span = trace.getTracer('example').startSpan('work');
span.end();
```

In `@sentry/node`, this provider can be enabled with the experimental option:

```js
Sentry.init({
dsn: 'xxx',
_experiments: {
useSentryTracerProvider: true,
},
});
```

When this provider is enabled, additional OpenTelemetry span processors are ignored because Sentry spans are created
directly. OpenTelemetry logs and metrics are not handled by this provider.

## Links

- [Official SDK Docs](https://docs.sentry.io/quickstart/)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions packages/core/src/shared-exports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,6 +103,7 @@ export {
spanToJSON,
spanToStreamedSpanJSON,
spanIsSampled,
spanIsSentrySpan,
spanToTraceContext,
getSpanDescendants,
getStatusMessage,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/tracing/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,10 @@ export {
getCapturedScopesOnSpan,
markSpanForOtelSourceInference,
spanShouldInferOtelSource,
markSpanSourceAsExplicit,
spanSourceWasExplicitlySet,
markSpanAsTracerProviderSpan,
spanIsTracerProviderSpan,
} from './utils';
export { startIdleSpan, TRACING_DEFAULTS } from './idleSpan';
export { SentrySpan } from './sentrySpan';
Expand All@@ -13,12 +17,14 @@ export { SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET } from './spanstat
export {
startSpan,
startInactiveSpan,
_INTERNAL_startInactiveSpan,
startSpanManual,
continueTrace,
withActiveSpan,
suppressTracing,
isTracingSuppressed,
startNewTrace,
spanIsIgnored,
SUPPRESS_TRACING_KEY,
} from './trace';
export { bindScopeToEmitter } from './bindScopeToEmitter';
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/tracing/sentrySpan.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
import { logSpanEnd } from './logSpans';
import { timedEventsToMeasurements } from './measurement';
import { hasSpanStreamingEnabled } from './spans/hasSpanStreamingEnabled';
import { getCapturedScopesOnSpan, spanShouldInferOtelSource } from './utils';
import { getCapturedScopesOnSpan, markSpanSourceAsExplicit, spanShouldInferOtelSource } from './utils';

const MAX_SPAN_COUNT = 1000;

Expand DownExpand Up@@ -144,7 +144,7 @@ export class SentrySpan implements Span {
* @hidden
* @internal
*/
public recordException(_exception: unknown, _time?: number | undefined): void {
public recordException(_exception: unknown, _time?: SpanTimeInput | undefined): void {
// noop
}

Expand All@@ -167,6 +167,12 @@ export class SentrySpan implements Span {
this._attributes[key] = value;
}

// Setting the source on a span branded for OTel-style inference means user code is choosing it
// explicitly, so flag it to keep `applyOtelSpanData` from overriding it with an inferred source.
if (key === SEMANTIC_ATTRIBUTE_SENTRY_SOURCE && value !== undefined && spanShouldInferOtelSource(this)) {
markSpanSourceAsExplicit(this);
}
Comment thread
andreiborza marked this conversation as resolved.

return this;
}

Expand Down
25 changes: 22 additions & 3 deletions packages/core/src/tracing/trace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,7 +82,7 @@ export function startSpan<T>(options: StartSpanOptions, callback: (span: Span) =
// Ignored root spans still need to be set on scope so that `getActiveSpan()` returns them
// and descendants are also non-recording. Ignored child spans don't need this because
// the parent span is already on scope.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
if (!spanIsIgnored(activeSpan) || !parentSpan) {
_setSpanForScope(scope, activeSpan);
}

Expand DownExpand Up@@ -144,7 +144,7 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S

// We don't set ignored child spans onto the scope because there likely is an active,
// unignored span on the scope already.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
if (!spanIsIgnored(activeSpan) || !parentSpan) {
_setSpanForScope(scope, activeSpan);
}

Expand DownExpand Up@@ -181,6 +181,20 @@ export function startInactiveSpan(options: StartSpanOptions): Span {
return acs.startInactiveSpan(options);
}

return _startInactiveSpanImpl(options);
}

/**
* Internal version of startInactiveSpan that bypasses the ACS check.
* Used by SentryTracerProvider to create spans without triggering recursion
* through ACS overrides.
* @hidden
*/
export function _INTERNAL_startInactiveSpan(options: StartSpanOptions): Span {
return _startInactiveSpanImpl(options);
}

function _startInactiveSpanImpl(options: StartSpanOptions): Span {
const spanArguments = parseSentrySpanArguments(options);
const { forceTransaction, parentSpan: customParentSpan } = options;

Expand DownExpand Up@@ -651,6 +665,11 @@ function _shouldIgnoreStreamedSpan(client: Client | undefined, spanArguments: Se
);
}

function _isIgnoredSpan(span: Span): span is SentryNonRecordingSpan {
/**
* Whether a span is an ignored (`ignoreSpans`) placeholder. Such a span must not be set as the active
* span when it has a parent, so its children attach to that parent and get re-parented rather than
* dropped with it. Shared with the OTel-based provider so both span pipelines apply the same rule.
*/
export function spanIsIgnored(span: Span): span is SentryNonRecordingSpan {
return spanIsNonRecordingSpan(span) && span.dropReason === 'ignored';
}
46 changes: 46 additions & 0 deletions packages/core/src/tracing/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,13 +12,30 @@ const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope';
// so the key is shared across duplicated copies of `@sentry/core`.
const OTEL_SOURCE_INFERENCE_SPAN_FIELD = Symbol.for('sentry.otelSourceInference');

// Brand marking a span (otherwise subject to OTel-style source inference, see above) whose
// `sentry.source` was explicitly set by user code after creation, so `applyOtelSpanData` stops
// inferring and respects the chosen source and name. This is what tells a user-set `custom` source
// apart from the default `custom` that `_startRootSpan` stamps on every root span.
const OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD = Symbol.for('sentry.otelSourceExplicitlySet');

// Brand marking a span created by the `SentryTracerProvider` (i.e. via the OTel tracer) rather than
// directly through the core span API. Such a span is handed to OTel instrumentations as an OTel span,
// so it must become immutable after `end()` like a real OTel SDK span (see `SentrySpan.end()`). Spans
// created directly through core (e.g. the browser SDK) are not branded and stay mutable.
const TRACER_PROVIDER_SPAN_FIELD = Symbol.for('sentry.tracerProviderSpan');

type SpanWithScopes = Span & {
[SCOPE_ON_START_SPAN_FIELD]?: Scope;
[ISOLATION_SCOPE_ON_START_SPAN_FIELD]?: MaybeWeakRef<Scope>;
};

type SpanWithOtelSourceInference = Span & {
[OTEL_SOURCE_INFERENCE_SPAN_FIELD]?: boolean;
[OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD]?: boolean;
};

type SpanWithTracerProviderBrand = Span & {
[TRACER_PROVIDER_SPAN_FIELD]?: boolean;
};

/** Store the scope & isolation scope for a span, which can the be used when it is finished. */
Expand DownExpand Up@@ -57,3 +74,32 @@ export function markSpanForOtelSourceInference(span: Span): void {
export function spanShouldInferOtelSource(span: Span): boolean {
return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_INFERENCE_SPAN_FIELD] === true;
}

/**
* Mark that user code explicitly set `sentry.source` on a span subject to OTel-style inference, so
* `applyOtelSpanData` keeps that source (and name) instead of overriding it. Set by `SentrySpan`
* when `setAttribute` writes the source on an already-branded span (the default `custom` source is
* stamped at construction, before the brand, so it doesn't trip this).
*/
export function markSpanSourceAsExplicit(span: Span): void {
addNonEnumerableProperty(span, OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD, true);
}

/** Whether user code explicitly set `sentry.source` on a span (see {@link markSpanSourceAsExplicit}). */
export function spanSourceWasExplicitlySet(span: Span): boolean {
return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD] === true;
}

/**
* Mark a span as created by the `SentryTracerProvider` (via the OTel tracer). Set by `SentryTracer`
* on every span it creates; read by `SentrySpan.end()` to seal the span against further writes once
* it has ended, mirroring OTel SDK spans (which are immutable after `end()`).
*/
export function markSpanAsTracerProviderSpan(span: Span): void {
addNonEnumerableProperty(span, TRACER_PROVIDER_SPAN_FIELD, true);
}

/** Whether a span was created by the `SentryTracerProvider` (see {@link markSpanAsTracerProviderSpan}). */
export function spanIsTracerProviderSpan(span: Span): boolean {
return (span as SpanWithTracerProviderBrand)[TRACER_PROVIDER_SPAN_FIELD] === true;
}
2 changes: 1 addition & 1 deletion packages/core/src/types/span.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -319,5 +319,5 @@ export interface Span {
/**
* NOT USED IN SENTRY, only added for compliance with OTEL Span interface
*/
recordException(exception: unknown, time?: number): void;
recordException(exception: unknown, time?: SpanTimeInput): void;
}
2 changes: 1 addition & 1 deletion packages/core/src/utils/spanUtils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -299,7 +299,7 @@ export interface OpenTelemetrySdkTraceBaseSpan extends Span {
* Sadly, due to circular dependency checks we cannot actually import the Span class here and check for instanceof.
* :( So instead we approximate this by checking if it has the `getSpanJSON` method.
*/
function spanIsSentrySpan(span: Span): span is SentrySpan {
export function spanIsSentrySpan(span: Span): span is SentrySpan {
return typeof (span as SentrySpan).getSpanJSON === 'function';
}

Expand Down
32 changes: 31 additions & 1 deletion packages/core/test/lib/tracing/sentrySpan.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { setCurrentClient } from '../../../src/sdk';
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../../../src/semanticAttributes';
import { SentrySpan } from '../../../src/tracing/sentrySpan';
import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus';
import { markSpanForOtelSourceInference } from '../../../src/tracing/utils';
import { 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';
Expand DownExpand Up@@ -61,6 +61,36 @@ describe('SentrySpan', () => {
});
});

describe('explicit source', () => {
it('flags a source set on a span marked for OTel source inference as explicit', () => {
const span = new SentrySpan({ name: 'original name' });
markSpanForOtelSourceInference(span);
expect(spanSourceWasExplicitlySet(span)).toBe(false);

span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');

expect(spanSourceWasExplicitlySet(span)).toBe(true);
});

it('does not flag the default source set at construction (before the inference brand) as explicit', () => {
const span = new SentrySpan({
name: 'original name',
attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
});
markSpanForOtelSourceInference(span);

expect(spanSourceWasExplicitlySet(span)).toBe(false);
});

it('does not flag a source set on a span that is not marked for OTel source inference', () => {
const span = new SentrySpan({ name: 'original name' });

span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');

expect(spanSourceWasExplicitlySet(span)).toBe(false);
});
});

describe('setters', () => {
test('setName', () => {
const span = new SentrySpan({});
Expand Down
30 changes: 30 additions & 0 deletions packages/opentelemetry/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,36 @@ function setupSentry() {
A full setup example can be found in
[node-experimental](https://github.com/getsentry/sentry-javascript/blob/develop/packages/node-experimental).

## Experimental Sentry Tracer Provider

`SentryTracerProvider` is an experimental minimal OpenTelemetry tracer provider which creates native Sentry spans directly.
It is useful when code uses the global OpenTelemetry API and you do not need the full OpenTelemetry SDK span processor
and exporter pipeline.

```js
import { trace } from '@opentelemetry/api';
import { SentryTracerProvider } from '@sentry/opentelemetry';

trace.setGlobalTracerProvider(new SentryTracerProvider());

const span = trace.getTracer('example').startSpan('work');
span.end();
```

In `@sentry/node`, this provider can be enabled with the experimental option:

```js
Sentry.init({
dsn: 'xxx',
_experiments: {
useSentryTracerProvider: true,
},
});
```

When this provider is enabled, additional OpenTelemetry span processors are ignored because Sentry spans are created
directly. OpenTelemetry logs and metrics are not handled by this provider.

## Links

- [Official SDK Docs](https://docs.sentry.io/quickstart/)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions packages/core/src/shared-exports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,6 +103,7 @@ export {
spanToJSON,
spanToStreamedSpanJSON,
spanIsSampled,
spanIsSentrySpan,
spanToTraceContext,
getSpanDescendants,
getStatusMessage,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/tracing/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,10 @@ export {
getCapturedScopesOnSpan,
markSpanForOtelSourceInference,
spanShouldInferOtelSource,
markSpanSourceAsExplicit,
spanSourceWasExplicitlySet,
markSpanAsTracerProviderSpan,
spanIsTracerProviderSpan,
} from './utils';
export { startIdleSpan, TRACING_DEFAULTS } from './idleSpan';
export { SentrySpan } from './sentrySpan';
Expand All@@ -13,12 +17,14 @@ export { SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET } from './spanstat
export {
startSpan,
startInactiveSpan,
_INTERNAL_startInactiveSpan,
startSpanManual,
continueTrace,
withActiveSpan,
suppressTracing,
isTracingSuppressed,
startNewTrace,
spanIsIgnored,
SUPPRESS_TRACING_KEY,
} from './trace';
export { bindScopeToEmitter } from './bindScopeToEmitter';
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/tracing/sentrySpan.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
import { logSpanEnd } from './logSpans';
import { timedEventsToMeasurements } from './measurement';
import { hasSpanStreamingEnabled } from './spans/hasSpanStreamingEnabled';
import { getCapturedScopesOnSpan, spanShouldInferOtelSource } from './utils';
import { getCapturedScopesOnSpan, markSpanSourceAsExplicit, spanShouldInferOtelSource } from './utils';

const MAX_SPAN_COUNT = 1000;

Expand DownExpand Up@@ -144,7 +144,7 @@ export class SentrySpan implements Span {
* @hidden
* @internal
*/
public recordException(_exception: unknown, _time?: number | undefined): void {
public recordException(_exception: unknown, _time?: SpanTimeInput | undefined): void {
// noop
}

Expand All@@ -167,6 +167,12 @@ export class SentrySpan implements Span {
this._attributes[key] = value;
}

// Setting the source on a span branded for OTel-style inference means user code is choosing it
// explicitly, so flag it to keep `applyOtelSpanData` from overriding it with an inferred source.
if (key === SEMANTIC_ATTRIBUTE_SENTRY_SOURCE && value !== undefined && spanShouldInferOtelSource(this)) {
markSpanSourceAsExplicit(this);
}
Comment thread
andreiborza marked this conversation as resolved.

return this;
}

Expand Down
25 changes: 22 additions & 3 deletions packages/core/src/tracing/trace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,7 +82,7 @@ export function startSpan<T>(options: StartSpanOptions, callback: (span: Span) =
// Ignored root spans still need to be set on scope so that `getActiveSpan()` returns them
// and descendants are also non-recording. Ignored child spans don't need this because
// the parent span is already on scope.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
if (!spanIsIgnored(activeSpan) || !parentSpan) {
_setSpanForScope(scope, activeSpan);
}

Expand DownExpand Up@@ -144,7 +144,7 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S

// We don't set ignored child spans onto the scope because there likely is an active,
// unignored span on the scope already.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
if (!spanIsIgnored(activeSpan) || !parentSpan) {
_setSpanForScope(scope, activeSpan);
}

Expand DownExpand Up@@ -181,6 +181,20 @@ export function startInactiveSpan(options: StartSpanOptions): Span {
return acs.startInactiveSpan(options);
}

return _startInactiveSpanImpl(options);
}

/**
* Internal version of startInactiveSpan that bypasses the ACS check.
* Used by SentryTracerProvider to create spans without triggering recursion
* through ACS overrides.
* @hidden
*/
export function _INTERNAL_startInactiveSpan(options: StartSpanOptions): Span {
return _startInactiveSpanImpl(options);
}

function _startInactiveSpanImpl(options: StartSpanOptions): Span {
const spanArguments = parseSentrySpanArguments(options);
const { forceTransaction, parentSpan: customParentSpan } = options;

Expand DownExpand Up@@ -651,6 +665,11 @@ function _shouldIgnoreStreamedSpan(client: Client | undefined, spanArguments: Se
);
}

function _isIgnoredSpan(span: Span): span is SentryNonRecordingSpan {
/**
* Whether a span is an ignored (`ignoreSpans`) placeholder. Such a span must not be set as the active
* span when it has a parent, so its children attach to that parent and get re-parented rather than
* dropped with it. Shared with the OTel-based provider so both span pipelines apply the same rule.
*/
export function spanIsIgnored(span: Span): span is SentryNonRecordingSpan {
return spanIsNonRecordingSpan(span) && span.dropReason === 'ignored';
}
46 changes: 46 additions & 0 deletions packages/core/src/tracing/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,13 +12,30 @@ const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope';
// so the key is shared across duplicated copies of `@sentry/core`.
const OTEL_SOURCE_INFERENCE_SPAN_FIELD = Symbol.for('sentry.otelSourceInference');

// Brand marking a span (otherwise subject to OTel-style source inference, see above) whose
// `sentry.source` was explicitly set by user code after creation, so `applyOtelSpanData` stops
// inferring and respects the chosen source and name. This is what tells a user-set `custom` source
// apart from the default `custom` that `_startRootSpan` stamps on every root span.
const OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD = Symbol.for('sentry.otelSourceExplicitlySet');

// Brand marking a span created by the `SentryTracerProvider` (i.e. via the OTel tracer) rather than
// directly through the core span API. Such a span is handed to OTel instrumentations as an OTel span,
// so it must become immutable after `end()` like a real OTel SDK span (see `SentrySpan.end()`). Spans
// created directly through core (e.g. the browser SDK) are not branded and stay mutable.
const TRACER_PROVIDER_SPAN_FIELD = Symbol.for('sentry.tracerProviderSpan');

type SpanWithScopes = Span & {
[SCOPE_ON_START_SPAN_FIELD]?: Scope;
[ISOLATION_SCOPE_ON_START_SPAN_FIELD]?: MaybeWeakRef<Scope>;
};

type SpanWithOtelSourceInference = Span & {
[OTEL_SOURCE_INFERENCE_SPAN_FIELD]?: boolean;
[OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD]?: boolean;
};

type SpanWithTracerProviderBrand = Span & {
[TRACER_PROVIDER_SPAN_FIELD]?: boolean;
};

/** Store the scope & isolation scope for a span, which can the be used when it is finished. */
Expand DownExpand Up@@ -57,3 +74,32 @@ export function markSpanForOtelSourceInference(span: Span): void {
export function spanShouldInferOtelSource(span: Span): boolean {
return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_INFERENCE_SPAN_FIELD] === true;
}

/**
* Mark that user code explicitly set `sentry.source` on a span subject to OTel-style inference, so
* `applyOtelSpanData` keeps that source (and name) instead of overriding it. Set by `SentrySpan`
* when `setAttribute` writes the source on an already-branded span (the default `custom` source is
* stamped at construction, before the brand, so it doesn't trip this).
*/
export function markSpanSourceAsExplicit(span: Span): void {
addNonEnumerableProperty(span, OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD, true);
}

/** Whether user code explicitly set `sentry.source` on a span (see {@link markSpanSourceAsExplicit}). */
export function spanSourceWasExplicitlySet(span: Span): boolean {
return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD] === true;
}

/**
* Mark a span as created by the `SentryTracerProvider` (via the OTel tracer). Set by `SentryTracer`
* on every span it creates; read by `SentrySpan.end()` to seal the span against further writes once
* it has ended, mirroring OTel SDK spans (which are immutable after `end()`).
*/
export function markSpanAsTracerProviderSpan(span: Span): void {
addNonEnumerableProperty(span, TRACER_PROVIDER_SPAN_FIELD, true);
}

/** Whether a span was created by the `SentryTracerProvider` (see {@link markSpanAsTracerProviderSpan}). */
export function spanIsTracerProviderSpan(span: Span): boolean {
return (span as SpanWithTracerProviderBrand)[TRACER_PROVIDER_SPAN_FIELD] === true;
}
2 changes: 1 addition & 1 deletion packages/core/src/types/span.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -319,5 +319,5 @@ export interface Span {
/**
* NOT USED IN SENTRY, only added for compliance with OTEL Span interface
*/
recordException(exception: unknown, time?: number): void;
recordException(exception: unknown, time?: SpanTimeInput): void;
}
2 changes: 1 addition & 1 deletion packages/core/src/utils/spanUtils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -299,7 +299,7 @@ export interface OpenTelemetrySdkTraceBaseSpan extends Span {
* Sadly, due to circular dependency checks we cannot actually import the Span class here and check for instanceof.
* :( So instead we approximate this by checking if it has the `getSpanJSON` method.
*/
function spanIsSentrySpan(span: Span): span is SentrySpan {
export function spanIsSentrySpan(span: Span): span is SentrySpan {
return typeof (span as SentrySpan).getSpanJSON === 'function';
}

Expand Down
32 changes: 31 additions & 1 deletion packages/core/test/lib/tracing/sentrySpan.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { setCurrentClient } from '../../../src/sdk';
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../../../src/semanticAttributes';
import { SentrySpan } from '../../../src/tracing/sentrySpan';
import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus';
import { markSpanForOtelSourceInference } from '../../../src/tracing/utils';
import { 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';
Expand DownExpand Up@@ -61,6 +61,36 @@ describe('SentrySpan', () => {
});
});

describe('explicit source', () => {
it('flags a source set on a span marked for OTel source inference as explicit', () => {
const span = new SentrySpan({ name: 'original name' });
markSpanForOtelSourceInference(span);
expect(spanSourceWasExplicitlySet(span)).toBe(false);

span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');

expect(spanSourceWasExplicitlySet(span)).toBe(true);
});

it('does not flag the default source set at construction (before the inference brand) as explicit', () => {
const span = new SentrySpan({
name: 'original name',
attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
});
markSpanForOtelSourceInference(span);

expect(spanSourceWasExplicitlySet(span)).toBe(false);
});

it('does not flag a source set on a span that is not marked for OTel source inference', () => {
const span = new SentrySpan({ name: 'original name' });

span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');

expect(spanSourceWasExplicitlySet(span)).toBe(false);
});
});

describe('setters', () => {
test('setName', () => {
const span = new SentrySpan({});
Expand Down
30 changes: 30 additions & 0 deletions packages/opentelemetry/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,36 @@ function setupSentry() {
A full setup example can be found in
[node-experimental](https://github.com/getsentry/sentry-javascript/blob/develop/packages/node-experimental).

## Experimental Sentry Tracer Provider

`SentryTracerProvider` is an experimental minimal OpenTelemetry tracer provider which creates native Sentry spans directly.
It is useful when code uses the global OpenTelemetry API and you do not need the full OpenTelemetry SDK span processor
and exporter pipeline.

```js
import { trace } from '@opentelemetry/api';
import { SentryTracerProvider } from '@sentry/opentelemetry';

trace.setGlobalTracerProvider(new SentryTracerProvider());

const span = trace.getTracer('example').startSpan('work');
span.end();
```

In `@sentry/node`, this provider can be enabled with the experimental option:

```js
Sentry.init({
dsn: 'xxx',
_experiments: {
useSentryTracerProvider: true,
},
});
```

When this provider is enabled, additional OpenTelemetry span processors are ignored because Sentry spans are created
directly. OpenTelemetry logs and metrics are not handled by this provider.

## Links

- [Official SDK Docs](https://docs.sentry.io/quickstart/)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions packages/core/src/shared-exports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,6 +103,7 @@ export {
spanToJSON,
spanToStreamedSpanJSON,
spanIsSampled,
spanIsSentrySpan,
spanToTraceContext,
getSpanDescendants,
getStatusMessage,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/tracing/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,10 @@ export {
getCapturedScopesOnSpan,
markSpanForOtelSourceInference,
spanShouldInferOtelSource,
markSpanSourceAsExplicit,
spanSourceWasExplicitlySet,
markSpanAsTracerProviderSpan,
spanIsTracerProviderSpan,
} from './utils';
export { startIdleSpan, TRACING_DEFAULTS } from './idleSpan';
export { SentrySpan } from './sentrySpan';
Expand All@@ -13,12 +17,14 @@ export { SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET } from './spanstat
export {
startSpan,
startInactiveSpan,
_INTERNAL_startInactiveSpan,
startSpanManual,
continueTrace,
withActiveSpan,
suppressTracing,
isTracingSuppressed,
startNewTrace,
spanIsIgnored,
SUPPRESS_TRACING_KEY,
} from './trace';
export { bindScopeToEmitter } from './bindScopeToEmitter';
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/tracing/sentrySpan.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
import { logSpanEnd } from './logSpans';
import { timedEventsToMeasurements } from './measurement';
import { hasSpanStreamingEnabled } from './spans/hasSpanStreamingEnabled';
import { getCapturedScopesOnSpan, spanShouldInferOtelSource } from './utils';
import { getCapturedScopesOnSpan, markSpanSourceAsExplicit, spanShouldInferOtelSource } from './utils';

const MAX_SPAN_COUNT = 1000;

Expand DownExpand Up@@ -144,7 +144,7 @@ export class SentrySpan implements Span {
* @hidden
* @internal
*/
public recordException(_exception: unknown, _time?: number | undefined): void {
public recordException(_exception: unknown, _time?: SpanTimeInput | undefined): void {
// noop
}

Expand All@@ -167,6 +167,12 @@ export class SentrySpan implements Span {
this._attributes[key] = value;
}

// Setting the source on a span branded for OTel-style inference means user code is choosing it
// explicitly, so flag it to keep `applyOtelSpanData` from overriding it with an inferred source.
if (key === SEMANTIC_ATTRIBUTE_SENTRY_SOURCE && value !== undefined && spanShouldInferOtelSource(this)) {
markSpanSourceAsExplicit(this);
}
Comment thread
andreiborza marked this conversation as resolved.

return this;
}

Expand Down
25 changes: 22 additions & 3 deletions packages/core/src/tracing/trace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,7 +82,7 @@ export function startSpan<T>(options: StartSpanOptions, callback: (span: Span) =
// Ignored root spans still need to be set on scope so that `getActiveSpan()` returns them
// and descendants are also non-recording. Ignored child spans don't need this because
// the parent span is already on scope.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
if (!spanIsIgnored(activeSpan) || !parentSpan) {
_setSpanForScope(scope, activeSpan);
}

Expand DownExpand Up@@ -144,7 +144,7 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S

// We don't set ignored child spans onto the scope because there likely is an active,
// unignored span on the scope already.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
if (!spanIsIgnored(activeSpan) || !parentSpan) {
_setSpanForScope(scope, activeSpan);
}

Expand DownExpand Up@@ -181,6 +181,20 @@ export function startInactiveSpan(options: StartSpanOptions): Span {
return acs.startInactiveSpan(options);
}

return _startInactiveSpanImpl(options);
}

/**
* Internal version of startInactiveSpan that bypasses the ACS check.
* Used by SentryTracerProvider to create spans without triggering recursion
* through ACS overrides.
* @hidden
*/
export function _INTERNAL_startInactiveSpan(options: StartSpanOptions): Span {
return _startInactiveSpanImpl(options);
}

function _startInactiveSpanImpl(options: StartSpanOptions): Span {
const spanArguments = parseSentrySpanArguments(options);
const { forceTransaction, parentSpan: customParentSpan } = options;

Expand DownExpand Up@@ -651,6 +665,11 @@ function _shouldIgnoreStreamedSpan(client: Client | undefined, spanArguments: Se
);
}

function _isIgnoredSpan(span: Span): span is SentryNonRecordingSpan {
/**
* Whether a span is an ignored (`ignoreSpans`) placeholder. Such a span must not be set as the active
* span when it has a parent, so its children attach to that parent and get re-parented rather than
* dropped with it. Shared with the OTel-based provider so both span pipelines apply the same rule.
*/
export function spanIsIgnored(span: Span): span is SentryNonRecordingSpan {
return spanIsNonRecordingSpan(span) && span.dropReason === 'ignored';
}
46 changes: 46 additions & 0 deletions packages/core/src/tracing/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,13 +12,30 @@ const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope';
// so the key is shared across duplicated copies of `@sentry/core`.
const OTEL_SOURCE_INFERENCE_SPAN_FIELD = Symbol.for('sentry.otelSourceInference');

// Brand marking a span (otherwise subject to OTel-style source inference, see above) whose
// `sentry.source` was explicitly set by user code after creation, so `applyOtelSpanData` stops
// inferring and respects the chosen source and name. This is what tells a user-set `custom` source
// apart from the default `custom` that `_startRootSpan` stamps on every root span.
const OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD = Symbol.for('sentry.otelSourceExplicitlySet');

// Brand marking a span created by the `SentryTracerProvider` (i.e. via the OTel tracer) rather than
// directly through the core span API. Such a span is handed to OTel instrumentations as an OTel span,
// so it must become immutable after `end()` like a real OTel SDK span (see `SentrySpan.end()`). Spans
// created directly through core (e.g. the browser SDK) are not branded and stay mutable.
const TRACER_PROVIDER_SPAN_FIELD = Symbol.for('sentry.tracerProviderSpan');

type SpanWithScopes = Span & {
[SCOPE_ON_START_SPAN_FIELD]?: Scope;
[ISOLATION_SCOPE_ON_START_SPAN_FIELD]?: MaybeWeakRef<Scope>;
};

type SpanWithOtelSourceInference = Span & {
[OTEL_SOURCE_INFERENCE_SPAN_FIELD]?: boolean;
[OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD]?: boolean;
};

type SpanWithTracerProviderBrand = Span & {
[TRACER_PROVIDER_SPAN_FIELD]?: boolean;
};

/** Store the scope & isolation scope for a span, which can the be used when it is finished. */
Expand DownExpand Up@@ -57,3 +74,32 @@ export function markSpanForOtelSourceInference(span: Span): void {
export function spanShouldInferOtelSource(span: Span): boolean {
return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_INFERENCE_SPAN_FIELD] === true;
}

/**
* Mark that user code explicitly set `sentry.source` on a span subject to OTel-style inference, so
* `applyOtelSpanData` keeps that source (and name) instead of overriding it. Set by `SentrySpan`
* when `setAttribute` writes the source on an already-branded span (the default `custom` source is
* stamped at construction, before the brand, so it doesn't trip this).
*/
export function markSpanSourceAsExplicit(span: Span): void {
addNonEnumerableProperty(span, OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD, true);
}

/** Whether user code explicitly set `sentry.source` on a span (see {@link markSpanSourceAsExplicit}). */
export function spanSourceWasExplicitlySet(span: Span): boolean {
return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD] === true;
}

/**
* Mark a span as created by the `SentryTracerProvider` (via the OTel tracer). Set by `SentryTracer`
* on every span it creates; read by `SentrySpan.end()` to seal the span against further writes once
* it has ended, mirroring OTel SDK spans (which are immutable after `end()`).
*/
export function markSpanAsTracerProviderSpan(span: Span): void {
addNonEnumerableProperty(span, TRACER_PROVIDER_SPAN_FIELD, true);
}

/** Whether a span was created by the `SentryTracerProvider` (see {@link markSpanAsTracerProviderSpan}). */
export function spanIsTracerProviderSpan(span: Span): boolean {
return (span as SpanWithTracerProviderBrand)[TRACER_PROVIDER_SPAN_FIELD] === true;
}
2 changes: 1 addition & 1 deletion packages/core/src/types/span.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -319,5 +319,5 @@ export interface Span {
/**
* NOT USED IN SENTRY, only added for compliance with OTEL Span interface
*/
recordException(exception: unknown, time?: number): void;
recordException(exception: unknown, time?: SpanTimeInput): void;
}
2 changes: 1 addition & 1 deletion packages/core/src/utils/spanUtils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -299,7 +299,7 @@ export interface OpenTelemetrySdkTraceBaseSpan extends Span {
* Sadly, due to circular dependency checks we cannot actually import the Span class here and check for instanceof.
* :( So instead we approximate this by checking if it has the `getSpanJSON` method.
*/
function spanIsSentrySpan(span: Span): span is SentrySpan {
export function spanIsSentrySpan(span: Span): span is SentrySpan {
return typeof (span as SentrySpan).getSpanJSON === 'function';
}

Expand Down
32 changes: 31 additions & 1 deletion packages/core/test/lib/tracing/sentrySpan.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { setCurrentClient } from '../../../src/sdk';
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../../../src/semanticAttributes';
import { SentrySpan } from '../../../src/tracing/sentrySpan';
import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus';
import { markSpanForOtelSourceInference } from '../../../src/tracing/utils';
import { 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';
Expand DownExpand Up@@ -61,6 +61,36 @@ describe('SentrySpan', () => {
});
});

describe('explicit source', () => {
it('flags a source set on a span marked for OTel source inference as explicit', () => {
const span = new SentrySpan({ name: 'original name' });
markSpanForOtelSourceInference(span);
expect(spanSourceWasExplicitlySet(span)).toBe(false);

span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');

expect(spanSourceWasExplicitlySet(span)).toBe(true);
});

it('does not flag the default source set at construction (before the inference brand) as explicit', () => {
const span = new SentrySpan({
name: 'original name',
attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
});
markSpanForOtelSourceInference(span);

expect(spanSourceWasExplicitlySet(span)).toBe(false);
});

it('does not flag a source set on a span that is not marked for OTel source inference', () => {
const span = new SentrySpan({ name: 'original name' });

span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');

expect(spanSourceWasExplicitlySet(span)).toBe(false);
});
});

describe('setters', () => {
test('setName', () => {
const span = new SentrySpan({});
Expand Down
30 changes: 30 additions & 0 deletions packages/opentelemetry/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,36 @@ function setupSentry() {
A full setup example can be found in
[node-experimental](https://github.com/getsentry/sentry-javascript/blob/develop/packages/node-experimental).

## Experimental Sentry Tracer Provider

`SentryTracerProvider` is an experimental minimal OpenTelemetry tracer provider which creates native Sentry spans directly.
It is useful when code uses the global OpenTelemetry API and you do not need the full OpenTelemetry SDK span processor
and exporter pipeline.

```js
import { trace } from '@opentelemetry/api';
import { SentryTracerProvider } from '@sentry/opentelemetry';

trace.setGlobalTracerProvider(new SentryTracerProvider());

const span = trace.getTracer('example').startSpan('work');
span.end();
```

In `@sentry/node`, this provider can be enabled with the experimental option:

```js
Sentry.init({
dsn: 'xxx',
_experiments: {
useSentryTracerProvider: true,
},
});
```

When this provider is enabled, additional OpenTelemetry span processors are ignored because Sentry spans are created
directly. OpenTelemetry logs and metrics are not handled by this provider.

## Links

- [Official SDK Docs](https://docs.sentry.io/quickstart/)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions packages/core/src/shared-exports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,6 +103,7 @@ export {
spanToJSON,
spanToStreamedSpanJSON,
spanIsSampled,
spanIsSentrySpan,
spanToTraceContext,
getSpanDescendants,
getStatusMessage,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/tracing/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,10 @@ export {
getCapturedScopesOnSpan,
markSpanForOtelSourceInference,
spanShouldInferOtelSource,
markSpanSourceAsExplicit,
spanSourceWasExplicitlySet,
markSpanAsTracerProviderSpan,
spanIsTracerProviderSpan,
} from './utils';
export { startIdleSpan, TRACING_DEFAULTS } from './idleSpan';
export { SentrySpan } from './sentrySpan';
Expand All@@ -13,12 +17,14 @@ export { SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET } from './spanstat
export {
startSpan,
startInactiveSpan,
_INTERNAL_startInactiveSpan,
startSpanManual,
continueTrace,
withActiveSpan,
suppressTracing,
isTracingSuppressed,
startNewTrace,
spanIsIgnored,
SUPPRESS_TRACING_KEY,
} from './trace';
export { bindScopeToEmitter } from './bindScopeToEmitter';
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/tracing/sentrySpan.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
import { logSpanEnd } from './logSpans';
import { timedEventsToMeasurements } from './measurement';
import { hasSpanStreamingEnabled } from './spans/hasSpanStreamingEnabled';
import { getCapturedScopesOnSpan, spanShouldInferOtelSource } from './utils';
import { getCapturedScopesOnSpan, markSpanSourceAsExplicit, spanShouldInferOtelSource } from './utils';

const MAX_SPAN_COUNT = 1000;

Expand DownExpand Up@@ -144,7 +144,7 @@ export class SentrySpan implements Span {
* @hidden
* @internal
*/
public recordException(_exception: unknown, _time?: number | undefined): void {
public recordException(_exception: unknown, _time?: SpanTimeInput | undefined): void {
// noop
}

Expand All@@ -167,6 +167,12 @@ export class SentrySpan implements Span {
this._attributes[key] = value;
}

// Setting the source on a span branded for OTel-style inference means user code is choosing it
// explicitly, so flag it to keep `applyOtelSpanData` from overriding it with an inferred source.
if (key === SEMANTIC_ATTRIBUTE_SENTRY_SOURCE && value !== undefined && spanShouldInferOtelSource(this)) {
markSpanSourceAsExplicit(this);
}
Comment thread
andreiborza marked this conversation as resolved.

return this;
}

Expand Down
25 changes: 22 additions & 3 deletions packages/core/src/tracing/trace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,7 +82,7 @@ export function startSpan<T>(options: StartSpanOptions, callback: (span: Span) =
// Ignored root spans still need to be set on scope so that `getActiveSpan()` returns them
// and descendants are also non-recording. Ignored child spans don't need this because
// the parent span is already on scope.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
if (!spanIsIgnored(activeSpan) || !parentSpan) {
_setSpanForScope(scope, activeSpan);
}

Expand DownExpand Up@@ -144,7 +144,7 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S

// We don't set ignored child spans onto the scope because there likely is an active,
// unignored span on the scope already.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
if (!spanIsIgnored(activeSpan) || !parentSpan) {
_setSpanForScope(scope, activeSpan);
}

Expand DownExpand Up@@ -181,6 +181,20 @@ export function startInactiveSpan(options: StartSpanOptions): Span {
return acs.startInactiveSpan(options);
}

return _startInactiveSpanImpl(options);
}

/**
* Internal version of startInactiveSpan that bypasses the ACS check.
* Used by SentryTracerProvider to create spans without triggering recursion
* through ACS overrides.
* @hidden
*/
export function _INTERNAL_startInactiveSpan(options: StartSpanOptions): Span {
return _startInactiveSpanImpl(options);
}

function _startInactiveSpanImpl(options: StartSpanOptions): Span {
const spanArguments = parseSentrySpanArguments(options);
const { forceTransaction, parentSpan: customParentSpan } = options;

Expand DownExpand Up@@ -651,6 +665,11 @@ function _shouldIgnoreStreamedSpan(client: Client | undefined, spanArguments: Se
);
}

function _isIgnoredSpan(span: Span): span is SentryNonRecordingSpan {
/**
* Whether a span is an ignored (`ignoreSpans`) placeholder. Such a span must not be set as the active
* span when it has a parent, so its children attach to that parent and get re-parented rather than
* dropped with it. Shared with the OTel-based provider so both span pipelines apply the same rule.
*/
export function spanIsIgnored(span: Span): span is SentryNonRecordingSpan {
return spanIsNonRecordingSpan(span) && span.dropReason === 'ignored';
}
46 changes: 46 additions & 0 deletions packages/core/src/tracing/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,13 +12,30 @@ const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope';
// so the key is shared across duplicated copies of `@sentry/core`.
const OTEL_SOURCE_INFERENCE_SPAN_FIELD = Symbol.for('sentry.otelSourceInference');

// Brand marking a span (otherwise subject to OTel-style source inference, see above) whose
// `sentry.source` was explicitly set by user code after creation, so `applyOtelSpanData` stops
// inferring and respects the chosen source and name. This is what tells a user-set `custom` source
// apart from the default `custom` that `_startRootSpan` stamps on every root span.
const OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD = Symbol.for('sentry.otelSourceExplicitlySet');

// Brand marking a span created by the `SentryTracerProvider` (i.e. via the OTel tracer) rather than
// directly through the core span API. Such a span is handed to OTel instrumentations as an OTel span,
// so it must become immutable after `end()` like a real OTel SDK span (see `SentrySpan.end()`). Spans
// created directly through core (e.g. the browser SDK) are not branded and stay mutable.
const TRACER_PROVIDER_SPAN_FIELD = Symbol.for('sentry.tracerProviderSpan');

type SpanWithScopes = Span & {
[SCOPE_ON_START_SPAN_FIELD]?: Scope;
[ISOLATION_SCOPE_ON_START_SPAN_FIELD]?: MaybeWeakRef<Scope>;
};

type SpanWithOtelSourceInference = Span & {
[OTEL_SOURCE_INFERENCE_SPAN_FIELD]?: boolean;
[OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD]?: boolean;
};

type SpanWithTracerProviderBrand = Span & {
[TRACER_PROVIDER_SPAN_FIELD]?: boolean;
};

/** Store the scope & isolation scope for a span, which can the be used when it is finished. */
Expand DownExpand Up@@ -57,3 +74,32 @@ export function markSpanForOtelSourceInference(span: Span): void {
export function spanShouldInferOtelSource(span: Span): boolean {
return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_INFERENCE_SPAN_FIELD] === true;
}

/**
* Mark that user code explicitly set `sentry.source` on a span subject to OTel-style inference, so
* `applyOtelSpanData` keeps that source (and name) instead of overriding it. Set by `SentrySpan`
* when `setAttribute` writes the source on an already-branded span (the default `custom` source is
* stamped at construction, before the brand, so it doesn't trip this).
*/
export function markSpanSourceAsExplicit(span: Span): void {
addNonEnumerableProperty(span, OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD, true);
}

/** Whether user code explicitly set `sentry.source` on a span (see {@link markSpanSourceAsExplicit}). */
export function spanSourceWasExplicitlySet(span: Span): boolean {
return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD] === true;
}

/**
* Mark a span as created by the `SentryTracerProvider` (via the OTel tracer). Set by `SentryTracer`
* on every span it creates; read by `SentrySpan.end()` to seal the span against further writes once
* it has ended, mirroring OTel SDK spans (which are immutable after `end()`).
*/
export function markSpanAsTracerProviderSpan(span: Span): void {
addNonEnumerableProperty(span, TRACER_PROVIDER_SPAN_FIELD, true);
}

/** Whether a span was created by the `SentryTracerProvider` (see {@link markSpanAsTracerProviderSpan}). */
export function spanIsTracerProviderSpan(span: Span): boolean {
return (span as SpanWithTracerProviderBrand)[TRACER_PROVIDER_SPAN_FIELD] === true;
}
2 changes: 1 addition & 1 deletion packages/core/src/types/span.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -319,5 +319,5 @@ export interface Span {
/**
* NOT USED IN SENTRY, only added for compliance with OTEL Span interface
*/
recordException(exception: unknown, time?: number): void;
recordException(exception: unknown, time?: SpanTimeInput): void;
}
2 changes: 1 addition & 1 deletion packages/core/src/utils/spanUtils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -299,7 +299,7 @@ export interface OpenTelemetrySdkTraceBaseSpan extends Span {
* Sadly, due to circular dependency checks we cannot actually import the Span class here and check for instanceof.
* :( So instead we approximate this by checking if it has the `getSpanJSON` method.
*/
function spanIsSentrySpan(span: Span): span is SentrySpan {
export function spanIsSentrySpan(span: Span): span is SentrySpan {
return typeof (span as SentrySpan).getSpanJSON === 'function';
}

Expand Down
32 changes: 31 additions & 1 deletion packages/core/test/lib/tracing/sentrySpan.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { setCurrentClient } from '../../../src/sdk';
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../../../src/semanticAttributes';
import { SentrySpan } from '../../../src/tracing/sentrySpan';
import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus';
import { markSpanForOtelSourceInference } from '../../../src/tracing/utils';
import { 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';
Expand DownExpand Up@@ -61,6 +61,36 @@ describe('SentrySpan', () => {
});
});

describe('explicit source', () => {
it('flags a source set on a span marked for OTel source inference as explicit', () => {
const span = new SentrySpan({ name: 'original name' });
markSpanForOtelSourceInference(span);
expect(spanSourceWasExplicitlySet(span)).toBe(false);

span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');

expect(spanSourceWasExplicitlySet(span)).toBe(true);
});

it('does not flag the default source set at construction (before the inference brand) as explicit', () => {
const span = new SentrySpan({
name: 'original name',
attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
});
markSpanForOtelSourceInference(span);

expect(spanSourceWasExplicitlySet(span)).toBe(false);
});

it('does not flag a source set on a span that is not marked for OTel source inference', () => {
const span = new SentrySpan({ name: 'original name' });

span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');

expect(spanSourceWasExplicitlySet(span)).toBe(false);
});
});

describe('setters', () => {
test('setName', () => {
const span = new SentrySpan({});
Expand Down
30 changes: 30 additions & 0 deletions packages/opentelemetry/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,36 @@ function setupSentry() {
A full setup example can be found in
[node-experimental](https://github.com/getsentry/sentry-javascript/blob/develop/packages/node-experimental).

## Experimental Sentry Tracer Provider

`SentryTracerProvider` is an experimental minimal OpenTelemetry tracer provider which creates native Sentry spans directly.
It is useful when code uses the global OpenTelemetry API and you do not need the full OpenTelemetry SDK span processor
and exporter pipeline.

```js
import { trace } from '@opentelemetry/api';
import { SentryTracerProvider } from '@sentry/opentelemetry';

trace.setGlobalTracerProvider(new SentryTracerProvider());

const span = trace.getTracer('example').startSpan('work');
span.end();
```

In `@sentry/node`, this provider can be enabled with the experimental option:

```js
Sentry.init({
dsn: 'xxx',
_experiments: {
useSentryTracerProvider: true,
},
});
```

When this provider is enabled, additional OpenTelemetry span processors are ignored because Sentry spans are created
directly. OpenTelemetry logs and metrics are not handled by this provider.

## Links

- [Official SDK Docs](https://docs.sentry.io/quickstart/)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions packages/core/src/shared-exports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,6 +103,7 @@ export {
spanToJSON,
spanToStreamedSpanJSON,
spanIsSampled,
spanIsSentrySpan,
spanToTraceContext,
getSpanDescendants,
getStatusMessage,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/tracing/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,10 @@ export {
getCapturedScopesOnSpan,
markSpanForOtelSourceInference,
spanShouldInferOtelSource,
markSpanSourceAsExplicit,
spanSourceWasExplicitlySet,
markSpanAsTracerProviderSpan,
spanIsTracerProviderSpan,
} from './utils';
export { startIdleSpan, TRACING_DEFAULTS } from './idleSpan';
export { SentrySpan } from './sentrySpan';
Expand All@@ -13,12 +17,14 @@ export { SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET } from './spanstat
export {
startSpan,
startInactiveSpan,
_INTERNAL_startInactiveSpan,
startSpanManual,
continueTrace,
withActiveSpan,
suppressTracing,
isTracingSuppressed,
startNewTrace,
spanIsIgnored,
SUPPRESS_TRACING_KEY,
} from './trace';
export { bindScopeToEmitter } from './bindScopeToEmitter';
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/tracing/sentrySpan.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
import { logSpanEnd } from './logSpans';
import { timedEventsToMeasurements } from './measurement';
import { hasSpanStreamingEnabled } from './spans/hasSpanStreamingEnabled';
import { getCapturedScopesOnSpan, spanShouldInferOtelSource } from './utils';
import { getCapturedScopesOnSpan, markSpanSourceAsExplicit, spanShouldInferOtelSource } from './utils';

const MAX_SPAN_COUNT = 1000;

Expand DownExpand Up@@ -144,7 +144,7 @@ export class SentrySpan implements Span {
* @hidden
* @internal
*/
public recordException(_exception: unknown, _time?: number | undefined): void {
public recordException(_exception: unknown, _time?: SpanTimeInput | undefined): void {
// noop
}

Expand All@@ -167,6 +167,12 @@ export class SentrySpan implements Span {
this._attributes[key] = value;
}

// Setting the source on a span branded for OTel-style inference means user code is choosing it
// explicitly, so flag it to keep `applyOtelSpanData` from overriding it with an inferred source.
if (key === SEMANTIC_ATTRIBUTE_SENTRY_SOURCE && value !== undefined && spanShouldInferOtelSource(this)) {
markSpanSourceAsExplicit(this);
}
Comment thread
andreiborza marked this conversation as resolved.

return this;
}

Expand Down
25 changes: 22 additions & 3 deletions packages/core/src/tracing/trace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,7 +82,7 @@ export function startSpan<T>(options: StartSpanOptions, callback: (span: Span) =
// Ignored root spans still need to be set on scope so that `getActiveSpan()` returns them
// and descendants are also non-recording. Ignored child spans don't need this because
// the parent span is already on scope.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
if (!spanIsIgnored(activeSpan) || !parentSpan) {
_setSpanForScope(scope, activeSpan);
}

Expand DownExpand Up@@ -144,7 +144,7 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S

// We don't set ignored child spans onto the scope because there likely is an active,
// unignored span on the scope already.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
if (!spanIsIgnored(activeSpan) || !parentSpan) {
_setSpanForScope(scope, activeSpan);
}

Expand DownExpand Up@@ -181,6 +181,20 @@ export function startInactiveSpan(options: StartSpanOptions): Span {
return acs.startInactiveSpan(options);
}

return _startInactiveSpanImpl(options);
}

/**
* Internal version of startInactiveSpan that bypasses the ACS check.
* Used by SentryTracerProvider to create spans without triggering recursion
* through ACS overrides.
* @hidden
*/
export function _INTERNAL_startInactiveSpan(options: StartSpanOptions): Span {
return _startInactiveSpanImpl(options);
}

function _startInactiveSpanImpl(options: StartSpanOptions): Span {
const spanArguments = parseSentrySpanArguments(options);
const { forceTransaction, parentSpan: customParentSpan } = options;

Expand DownExpand Up@@ -651,6 +665,11 @@ function _shouldIgnoreStreamedSpan(client: Client | undefined, spanArguments: Se
);
}

function _isIgnoredSpan(span: Span): span is SentryNonRecordingSpan {
/**
* Whether a span is an ignored (`ignoreSpans`) placeholder. Such a span must not be set as the active
* span when it has a parent, so its children attach to that parent and get re-parented rather than
* dropped with it. Shared with the OTel-based provider so both span pipelines apply the same rule.
*/
export function spanIsIgnored(span: Span): span is SentryNonRecordingSpan {
return spanIsNonRecordingSpan(span) && span.dropReason === 'ignored';
}
46 changes: 46 additions & 0 deletions packages/core/src/tracing/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,13 +12,30 @@ const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope';
// so the key is shared across duplicated copies of `@sentry/core`.
const OTEL_SOURCE_INFERENCE_SPAN_FIELD = Symbol.for('sentry.otelSourceInference');

// Brand marking a span (otherwise subject to OTel-style source inference, see above) whose
// `sentry.source` was explicitly set by user code after creation, so `applyOtelSpanData` stops
// inferring and respects the chosen source and name. This is what tells a user-set `custom` source
// apart from the default `custom` that `_startRootSpan` stamps on every root span.
const OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD = Symbol.for('sentry.otelSourceExplicitlySet');

// Brand marking a span created by the `SentryTracerProvider` (i.e. via the OTel tracer) rather than
// directly through the core span API. Such a span is handed to OTel instrumentations as an OTel span,
// so it must become immutable after `end()` like a real OTel SDK span (see `SentrySpan.end()`). Spans
// created directly through core (e.g. the browser SDK) are not branded and stay mutable.
const TRACER_PROVIDER_SPAN_FIELD = Symbol.for('sentry.tracerProviderSpan');

type SpanWithScopes = Span & {
[SCOPE_ON_START_SPAN_FIELD]?: Scope;
[ISOLATION_SCOPE_ON_START_SPAN_FIELD]?: MaybeWeakRef<Scope>;
};

type SpanWithOtelSourceInference = Span & {
[OTEL_SOURCE_INFERENCE_SPAN_FIELD]?: boolean;
[OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD]?: boolean;
};

type SpanWithTracerProviderBrand = Span & {
[TRACER_PROVIDER_SPAN_FIELD]?: boolean;
};

/** Store the scope & isolation scope for a span, which can the be used when it is finished. */
Expand DownExpand Up@@ -57,3 +74,32 @@ export function markSpanForOtelSourceInference(span: Span): void {
export function spanShouldInferOtelSource(span: Span): boolean {
return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_INFERENCE_SPAN_FIELD] === true;
}

/**
* Mark that user code explicitly set `sentry.source` on a span subject to OTel-style inference, so
* `applyOtelSpanData` keeps that source (and name) instead of overriding it. Set by `SentrySpan`
* when `setAttribute` writes the source on an already-branded span (the default `custom` source is
* stamped at construction, before the brand, so it doesn't trip this).
*/
export function markSpanSourceAsExplicit(span: Span): void {
addNonEnumerableProperty(span, OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD, true);
}

/** Whether user code explicitly set `sentry.source` on a span (see {@link markSpanSourceAsExplicit}). */
export function spanSourceWasExplicitlySet(span: Span): boolean {
return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD] === true;
}

/**
* Mark a span as created by the `SentryTracerProvider` (via the OTel tracer). Set by `SentryTracer`
* on every span it creates; read by `SentrySpan.end()` to seal the span against further writes once
* it has ended, mirroring OTel SDK spans (which are immutable after `end()`).
*/
export function markSpanAsTracerProviderSpan(span: Span): void {
addNonEnumerableProperty(span, TRACER_PROVIDER_SPAN_FIELD, true);
}

/** Whether a span was created by the `SentryTracerProvider` (see {@link markSpanAsTracerProviderSpan}). */
export function spanIsTracerProviderSpan(span: Span): boolean {
return (span as SpanWithTracerProviderBrand)[TRACER_PROVIDER_SPAN_FIELD] === true;
}
2 changes: 1 addition & 1 deletion packages/core/src/types/span.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -319,5 +319,5 @@ export interface Span {
/**
* NOT USED IN SENTRY, only added for compliance with OTEL Span interface
*/
recordException(exception: unknown, time?: number): void;
recordException(exception: unknown, time?: SpanTimeInput): void;
}
2 changes: 1 addition & 1 deletion packages/core/src/utils/spanUtils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -299,7 +299,7 @@ export interface OpenTelemetrySdkTraceBaseSpan extends Span {
* Sadly, due to circular dependency checks we cannot actually import the Span class here and check for instanceof.
* :( So instead we approximate this by checking if it has the `getSpanJSON` method.
*/
function spanIsSentrySpan(span: Span): span is SentrySpan {
export function spanIsSentrySpan(span: Span): span is SentrySpan {
return typeof (span as SentrySpan).getSpanJSON === 'function';
}

Expand Down
32 changes: 31 additions & 1 deletion packages/core/test/lib/tracing/sentrySpan.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { setCurrentClient } from '../../../src/sdk';
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../../../src/semanticAttributes';
import { SentrySpan } from '../../../src/tracing/sentrySpan';
import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus';
import { markSpanForOtelSourceInference } from '../../../src/tracing/utils';
import { 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';
Expand DownExpand Up@@ -61,6 +61,36 @@ describe('SentrySpan', () => {
});
});

describe('explicit source', () => {
it('flags a source set on a span marked for OTel source inference as explicit', () => {
const span = new SentrySpan({ name: 'original name' });
markSpanForOtelSourceInference(span);
expect(spanSourceWasExplicitlySet(span)).toBe(false);

span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');

expect(spanSourceWasExplicitlySet(span)).toBe(true);
});

it('does not flag the default source set at construction (before the inference brand) as explicit', () => {
const span = new SentrySpan({
name: 'original name',
attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
});
markSpanForOtelSourceInference(span);

expect(spanSourceWasExplicitlySet(span)).toBe(false);
});

it('does not flag a source set on a span that is not marked for OTel source inference', () => {
const span = new SentrySpan({ name: 'original name' });

span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');

expect(spanSourceWasExplicitlySet(span)).toBe(false);
});
});

describe('setters', () => {
test('setName', () => {
const span = new SentrySpan({});
Expand Down
30 changes: 30 additions & 0 deletions packages/opentelemetry/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,36 @@ function setupSentry() {
A full setup example can be found in
[node-experimental](https://github.com/getsentry/sentry-javascript/blob/develop/packages/node-experimental).

## Experimental Sentry Tracer Provider

`SentryTracerProvider` is an experimental minimal OpenTelemetry tracer provider which creates native Sentry spans directly.
It is useful when code uses the global OpenTelemetry API and you do not need the full OpenTelemetry SDK span processor
and exporter pipeline.

```js
import { trace } from '@opentelemetry/api';
import { SentryTracerProvider } from '@sentry/opentelemetry';

trace.setGlobalTracerProvider(new SentryTracerProvider());

const span = trace.getTracer('example').startSpan('work');
span.end();
```

In `@sentry/node`, this provider can be enabled with the experimental option:

```js
Sentry.init({
dsn: 'xxx',
_experiments: {
useSentryTracerProvider: true,
},
});
```

When this provider is enabled, additional OpenTelemetry span processors are ignored because Sentry spans are created
directly. OpenTelemetry logs and metrics are not handled by this provider.

## Links

- [Official SDK Docs](https://docs.sentry.io/quickstart/)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions packages/core/src/shared-exports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,6 +103,7 @@ export {
spanToJSON,
spanToStreamedSpanJSON,
spanIsSampled,
spanIsSentrySpan,
spanToTraceContext,
getSpanDescendants,
getStatusMessage,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/tracing/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,10 @@ export {
getCapturedScopesOnSpan,
markSpanForOtelSourceInference,
spanShouldInferOtelSource,
markSpanSourceAsExplicit,
spanSourceWasExplicitlySet,
markSpanAsTracerProviderSpan,
spanIsTracerProviderSpan,
} from './utils';
export { startIdleSpan, TRACING_DEFAULTS } from './idleSpan';
export { SentrySpan } from './sentrySpan';
Expand All@@ -13,12 +17,14 @@ export { SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET } from './spanstat
export {
startSpan,
startInactiveSpan,
_INTERNAL_startInactiveSpan,
startSpanManual,
continueTrace,
withActiveSpan,
suppressTracing,
isTracingSuppressed,
startNewTrace,
spanIsIgnored,
SUPPRESS_TRACING_KEY,
} from './trace';
export { bindScopeToEmitter } from './bindScopeToEmitter';
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/tracing/sentrySpan.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
import { logSpanEnd } from './logSpans';
import { timedEventsToMeasurements } from './measurement';
import { hasSpanStreamingEnabled } from './spans/hasSpanStreamingEnabled';
import { getCapturedScopesOnSpan, spanShouldInferOtelSource } from './utils';
import { getCapturedScopesOnSpan, markSpanSourceAsExplicit, spanShouldInferOtelSource } from './utils';

const MAX_SPAN_COUNT = 1000;

Expand DownExpand Up@@ -144,7 +144,7 @@ export class SentrySpan implements Span {
* @hidden
* @internal
*/
public recordException(_exception: unknown, _time?: number | undefined): void {
public recordException(_exception: unknown, _time?: SpanTimeInput | undefined): void {
// noop
}

Expand All@@ -167,6 +167,12 @@ export class SentrySpan implements Span {
this._attributes[key] = value;
}

// Setting the source on a span branded for OTel-style inference means user code is choosing it
// explicitly, so flag it to keep `applyOtelSpanData` from overriding it with an inferred source.
if (key === SEMANTIC_ATTRIBUTE_SENTRY_SOURCE && value !== undefined && spanShouldInferOtelSource(this)) {
markSpanSourceAsExplicit(this);
}
Comment thread
andreiborza marked this conversation as resolved.

return this;
}

Expand Down
25 changes: 22 additions & 3 deletions packages/core/src/tracing/trace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,7 +82,7 @@ export function startSpan<T>(options: StartSpanOptions, callback: (span: Span) =
// Ignored root spans still need to be set on scope so that `getActiveSpan()` returns them
// and descendants are also non-recording. Ignored child spans don't need this because
// the parent span is already on scope.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
if (!spanIsIgnored(activeSpan) || !parentSpan) {
_setSpanForScope(scope, activeSpan);
}

Expand DownExpand Up@@ -144,7 +144,7 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S

// We don't set ignored child spans onto the scope because there likely is an active,
// unignored span on the scope already.
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
if (!spanIsIgnored(activeSpan) || !parentSpan) {
_setSpanForScope(scope, activeSpan);
}

Expand DownExpand Up@@ -181,6 +181,20 @@ export function startInactiveSpan(options: StartSpanOptions): Span {
return acs.startInactiveSpan(options);
}

return _startInactiveSpanImpl(options);
}

/**
* Internal version of startInactiveSpan that bypasses the ACS check.
* Used by SentryTracerProvider to create spans without triggering recursion
* through ACS overrides.
* @hidden
*/
export function _INTERNAL_startInactiveSpan(options: StartSpanOptions): Span {
return _startInactiveSpanImpl(options);
}

function _startInactiveSpanImpl(options: StartSpanOptions): Span {
const spanArguments = parseSentrySpanArguments(options);
const { forceTransaction, parentSpan: customParentSpan } = options;

Expand DownExpand Up@@ -651,6 +665,11 @@ function _shouldIgnoreStreamedSpan(client: Client | undefined, spanArguments: Se
);
}

function _isIgnoredSpan(span: Span): span is SentryNonRecordingSpan {
/**
* Whether a span is an ignored (`ignoreSpans`) placeholder. Such a span must not be set as the active
* span when it has a parent, so its children attach to that parent and get re-parented rather than
* dropped with it. Shared with the OTel-based provider so both span pipelines apply the same rule.
*/
export function spanIsIgnored(span: Span): span is SentryNonRecordingSpan {
return spanIsNonRecordingSpan(span) && span.dropReason === 'ignored';
}
46 changes: 46 additions & 0 deletions packages/core/src/tracing/utils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,13 +12,30 @@ const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope';
// so the key is shared across duplicated copies of `@sentry/core`.
const OTEL_SOURCE_INFERENCE_SPAN_FIELD = Symbol.for('sentry.otelSourceInference');

// Brand marking a span (otherwise subject to OTel-style source inference, see above) whose
// `sentry.source` was explicitly set by user code after creation, so `applyOtelSpanData` stops
// inferring and respects the chosen source and name. This is what tells a user-set `custom` source
// apart from the default `custom` that `_startRootSpan` stamps on every root span.
const OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD = Symbol.for('sentry.otelSourceExplicitlySet');

// Brand marking a span created by the `SentryTracerProvider` (i.e. via the OTel tracer) rather than
// directly through the core span API. Such a span is handed to OTel instrumentations as an OTel span,
// so it must become immutable after `end()` like a real OTel SDK span (see `SentrySpan.end()`). Spans
// created directly through core (e.g. the browser SDK) are not branded and stay mutable.
const TRACER_PROVIDER_SPAN_FIELD = Symbol.for('sentry.tracerProviderSpan');

type SpanWithScopes = Span & {
[SCOPE_ON_START_SPAN_FIELD]?: Scope;
[ISOLATION_SCOPE_ON_START_SPAN_FIELD]?: MaybeWeakRef<Scope>;
};

type SpanWithOtelSourceInference = Span & {
[OTEL_SOURCE_INFERENCE_SPAN_FIELD]?: boolean;
[OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD]?: boolean;
};

type SpanWithTracerProviderBrand = Span & {
[TRACER_PROVIDER_SPAN_FIELD]?: boolean;
};

/** Store the scope & isolation scope for a span, which can the be used when it is finished. */
Expand DownExpand Up@@ -57,3 +74,32 @@ export function markSpanForOtelSourceInference(span: Span): void {
export function spanShouldInferOtelSource(span: Span): boolean {
return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_INFERENCE_SPAN_FIELD] === true;
}

/**
* Mark that user code explicitly set `sentry.source` on a span subject to OTel-style inference, so
* `applyOtelSpanData` keeps that source (and name) instead of overriding it. Set by `SentrySpan`
* when `setAttribute` writes the source on an already-branded span (the default `custom` source is
* stamped at construction, before the brand, so it doesn't trip this).
*/
export function markSpanSourceAsExplicit(span: Span): void {
addNonEnumerableProperty(span, OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD, true);
}

/** Whether user code explicitly set `sentry.source` on a span (see {@link markSpanSourceAsExplicit}). */
export function spanSourceWasExplicitlySet(span: Span): boolean {
return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_EXPLICITLY_SET_SPAN_FIELD] === true;
}

/**
* Mark a span as created by the `SentryTracerProvider` (via the OTel tracer). Set by `SentryTracer`
* on every span it creates; read by `SentrySpan.end()` to seal the span against further writes once
* it has ended, mirroring OTel SDK spans (which are immutable after `end()`).
*/
export function markSpanAsTracerProviderSpan(span: Span): void {
addNonEnumerableProperty(span, TRACER_PROVIDER_SPAN_FIELD, true);
}

/** Whether a span was created by the `SentryTracerProvider` (see {@link markSpanAsTracerProviderSpan}). */
export function spanIsTracerProviderSpan(span: Span): boolean {
return (span as SpanWithTracerProviderBrand)[TRACER_PROVIDER_SPAN_FIELD] === true;
}
2 changes: 1 addition & 1 deletion packages/core/src/types/span.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -319,5 +319,5 @@ export interface Span {
/**
* NOT USED IN SENTRY, only added for compliance with OTEL Span interface
*/
recordException(exception: unknown, time?: number): void;
recordException(exception: unknown, time?: SpanTimeInput): void;
}
2 changes: 1 addition & 1 deletion packages/core/src/utils/spanUtils.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -299,7 +299,7 @@ export interface OpenTelemetrySdkTraceBaseSpan extends Span {
* Sadly, due to circular dependency checks we cannot actually import the Span class here and check for instanceof.
* :( So instead we approximate this by checking if it has the `getSpanJSON` method.
*/
function spanIsSentrySpan(span: Span): span is SentrySpan {
export function spanIsSentrySpan(span: Span): span is SentrySpan {
return typeof (span as SentrySpan).getSpanJSON === 'function';
}

Expand Down
32 changes: 31 additions & 1 deletion packages/core/test/lib/tracing/sentrySpan.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { setCurrentClient } from '../../../src/sdk';
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../../../src/semanticAttributes';
import { SentrySpan } from '../../../src/tracing/sentrySpan';
import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus';
import { markSpanForOtelSourceInference } from '../../../src/tracing/utils';
import { 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';
Expand DownExpand Up@@ -61,6 +61,36 @@ describe('SentrySpan', () => {
});
});

describe('explicit source', () => {
it('flags a source set on a span marked for OTel source inference as explicit', () => {
const span = new SentrySpan({ name: 'original name' });
markSpanForOtelSourceInference(span);
expect(spanSourceWasExplicitlySet(span)).toBe(false);

span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');

expect(spanSourceWasExplicitlySet(span)).toBe(true);
});

it('does not flag the default source set at construction (before the inference brand) as explicit', () => {
const span = new SentrySpan({
name: 'original name',
attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' },
});
markSpanForOtelSourceInference(span);

expect(spanSourceWasExplicitlySet(span)).toBe(false);
});

it('does not flag a source set on a span that is not marked for OTel source inference', () => {
const span = new SentrySpan({ name: 'original name' });

span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');

expect(spanSourceWasExplicitlySet(span)).toBe(false);
});
});

describe('setters', () => {
test('setName', () => {
const span = new SentrySpan({});
Expand Down
30 changes: 30 additions & 0 deletions packages/opentelemetry/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,36 @@ function setupSentry() {
A full setup example can be found in
[node-experimental](https://github.com/getsentry/sentry-javascript/blob/develop/packages/node-experimental).

## Experimental Sentry Tracer Provider

`SentryTracerProvider` is an experimental minimal OpenTelemetry tracer provider which creates native Sentry spans directly.
It is useful when code uses the global OpenTelemetry API and you do not need the full OpenTelemetry SDK span processor
and exporter pipeline.

```js
import { trace } from '@opentelemetry/api';
import { SentryTracerProvider } from '@sentry/opentelemetry';

trace.setGlobalTracerProvider(new SentryTracerProvider());

const span = trace.getTracer('example').startSpan('work');
span.end();
```

In `@sentry/node`, this provider can be enabled with the experimental option:

```js
Sentry.init({
dsn: 'xxx',
_experiments: {
useSentryTracerProvider: true,
},
});
```

When this provider is enabled, additional OpenTelemetry span processors are ignored because Sentry spans are created
directly. OpenTelemetry logs and metrics are not handled by this provider.

## Links

- [Official SDK Docs](https://docs.sentry.io/quickstart/)
Loading
Loading