diff --git a/packages/node-experimental/src/sdk/init.ts b/packages/node-experimental/src/sdk/init.ts index 80b69112f261..5e62a01de6cd 100644 --- a/packages/node-experimental/src/sdk/init.ts +++ b/packages/node-experimental/src/sdk/init.ts @@ -11,7 +11,7 @@ import { requestDataIntegration, startSession, } from '@sentry/core'; -import { setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry'; +import { openTelemetrySetupCheck, setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry'; import type { Client, Integration, Options } from '@sentry/types'; import { consoleSandbox, @@ -117,8 +117,36 @@ export function init(options: NodeOptions | undefined = {}): void { ); } - // Always init Otel, even if tracing is disabled, because we need it for trace propagation & the HTTP integration - initOtel(); + // If users opt-out of this, they _have_ to set up OpenTelemetry themselves + // There is no way to use this SDK without OpenTelemetry! + if (!options.skipOpenTelemetrySetup) { + initOtel(); + } + + validateOpenTelemetrySetup(); +} + +function validateOpenTelemetrySetup(): void { + if (!DEBUG_BUILD) { + return; + } + + const setup = openTelemetrySetupCheck(); + + const required = ['SentrySpanProcessor', 'SentryContextManager', 'SentryPropagator'] as const; + for (const k of required) { + if (!setup.includes(k)) { + logger.error( + `You have to set up the ${k}. Without this, the OpenTelemetry & Sentry integration will not work properly.`, + ); + } + } + + if (!setup.includes('SentrySampler')) { + logger.warn( + 'You have to set up the SentrySampler. Without this, the OpenTelemetry & Sentry integration may still work, but sample rates set for the Sentry SDK will not be respected.', + ); + } } function getClientOptions(options: NodeOptions): NodeClientOptions { diff --git a/packages/node-experimental/src/types.ts b/packages/node-experimental/src/types.ts index 9ac45c54b6ae..19a825e176f4 100644 --- a/packages/node-experimental/src/types.ts +++ b/packages/node-experimental/src/types.ts @@ -64,6 +64,16 @@ export interface BaseNodeOptions { */ spotlight?: boolean | string; + /** + * If this is set to true, the SDK will not set up OpenTelemetry automatically. + * In this case, you _have_ to ensure to set it up correctly yourself, including: + * * The `SentrySpanProcessor` + * * The `SentryPropagator` + * * The `SentryContextManager` + * * The `SentrySampler` + */ + skipOpenTelemetrySetup?: boolean; + /** Callback that is executed when a fatal global error occurs. */ onFatalError?(this: void, error: Error): void; } diff --git a/packages/node-experimental/test/sdk/init.test.ts b/packages/node-experimental/test/sdk/init.test.ts index 7f0c3a8d71ec..d6e9bc7ee81d 100644 --- a/packages/node-experimental/test/sdk/init.test.ts +++ b/packages/node-experimental/test/sdk/init.test.ts @@ -2,6 +2,7 @@ import type { Integration } from '@sentry/types'; import * as auto from '../../src/integrations/tracing'; import { getClient } from '../../src/sdk/api'; +import type { NodeClient } from '../../src/sdk/client'; import { init } from '../../src/sdk/init'; import { cleanupOtel } from '../helpers/mockSdkInit'; @@ -119,4 +120,20 @@ describe('init()', () => { }), ); }); + + it('sets up OpenTelemetry by default', () => { + init({ dsn: PUBLIC_DSN }); + + const client = getClient(); + + expect(client.traceProvider).toBeDefined(); + }); + + it('allows to opt-out of OpenTelemetry setup', () => { + init({ dsn: PUBLIC_DSN, skipOpenTelemetrySetup: true }); + + const client = getClient(); + + expect(client.traceProvider).not.toBeDefined(); + }); }); diff --git a/packages/opentelemetry/src/contextManager.ts b/packages/opentelemetry/src/contextManager.ts index 73bbd145b978..9036667c2b40 100644 --- a/packages/opentelemetry/src/contextManager.ts +++ b/packages/opentelemetry/src/contextManager.ts @@ -9,6 +9,7 @@ import { } from './constants'; import { getCurrentHub } from './custom/getCurrentHub'; import { getScopesFromContext, setContextOnScope, setHubOnContext, setScopesOnContext } from './utils/contextData'; +import { setIsSetup } from './utils/setupCheck'; /** * Wrap an OpenTelemetry ContextManager in a way that ensures the context is kept in sync with the Sentry Hub. @@ -31,6 +32,10 @@ export function wrapContextManagerClass(); + +/** Get all the OpenTelemetry elements that have been set up. */ +export function openTelemetrySetupCheck(): OpenTelemetryElement[] { + return Array.from(setupElements); +} + +/** Mark an OpenTelemetry element as setup. */ +export function setIsSetup(element: OpenTelemetryElement): void { + setupElements.add(element); +} + +/** Only exported for tests. */ +export function clearOpenTelemetrySetupCheck(): void { + setupElements.clear(); +} diff --git a/packages/opentelemetry/test/helpers/mockSdkInit.ts b/packages/opentelemetry/test/helpers/mockSdkInit.ts index d4a41f90959e..6a3d9b171833 100644 --- a/packages/opentelemetry/test/helpers/mockSdkInit.ts +++ b/packages/opentelemetry/test/helpers/mockSdkInit.ts @@ -4,6 +4,7 @@ import type { ClientOptions, Options } from '@sentry/types'; import { getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core'; import { setOpenTelemetryContextAsyncContextStrategy } from '../../src/asyncContextStrategy'; +import { clearOpenTelemetrySetupCheck } from '../../src/utils/setupCheck'; import { init as initTestClient } from './TestClient'; import { initOtel } from './initOtel'; @@ -32,6 +33,7 @@ export function mockSdkInit(options?: Partial) { } export function cleanupOtel(_provider?: BasicTracerProvider): void { + clearOpenTelemetrySetupCheck(); const provider = getProvider(_provider); if (!provider) { diff --git a/packages/opentelemetry/test/utils/setupCheck.test.ts b/packages/opentelemetry/test/utils/setupCheck.test.ts new file mode 100644 index 000000000000..86eca3688731 --- /dev/null +++ b/packages/opentelemetry/test/utils/setupCheck.test.ts @@ -0,0 +1,44 @@ +import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; + +import { SentrySampler } from '../../src/sampler'; +import { SentrySpanProcessor } from '../../src/spanProcessor'; +import { openTelemetrySetupCheck } from '../../src/utils/setupCheck'; +import { TestClient, getDefaultTestClientOptions } from '../helpers/TestClient'; +import { setupOtel } from '../helpers/initOtel'; +import { cleanupOtel } from '../helpers/mockSdkInit'; + +describe('openTelemetrySetupCheck', () => { + let provider: BasicTracerProvider | undefined; + + beforeEach(() => { + cleanupOtel(provider); + }); + + afterEach(() => { + cleanupOtel(provider); + }); + + it('returns empty array by default', () => { + const setup = openTelemetrySetupCheck(); + expect(setup).toEqual([]); + }); + + it('returns all setup parts', () => { + const client = new TestClient(getDefaultTestClientOptions()); + provider = setupOtel(client); + + const setup = openTelemetrySetupCheck(); + expect(setup).toEqual(['SentrySampler', 'SentrySpanProcessor', 'SentryPropagator', 'SentryContextManager']); + }); + + it('returns partial setup parts', () => { + const client = new TestClient(getDefaultTestClientOptions()); + provider = new BasicTracerProvider({ + sampler: new SentrySampler(client), + }); + provider.addSpanProcessor(new SentrySpanProcessor()); + + const setup = openTelemetrySetupCheck(); + expect(setup).toEqual(['SentrySampler', 'SentrySpanProcessor']); + }); +});