diff --git a/core/packages/gax/src/clientInterface.ts b/core/packages/gax/src/clientInterface.ts index bf4571d6268..ab0dd2f1543 100644 --- a/core/packages/gax/src/clientInterface.ts +++ b/core/packages/gax/src/clientInterface.ts @@ -42,6 +42,9 @@ export interface ClientOptions universe_domain?: string; /** * Whether to enable telemetry tracing for the client. + * + * The `GOOGLE_SDK_NODE_ENABLE_TRACING` environment variable overrides this + * option whenever it is set. */ enableTelemetryTracing?: boolean; } diff --git a/core/packages/gax/src/util.ts b/core/packages/gax/src/util.ts index de388fe4c83..893ff766880 100644 --- a/core/packages/gax/src/util.ts +++ b/core/packages/gax/src/util.ts @@ -23,16 +23,45 @@ const randomUUID = () => globalThis.crypto?.randomUUID() || require('crypto').randomUUID(); /** - * Checks if telemetry tracing is enabled + * Checks if telemetry tracing is enabled. + * + * Tracing is opt-in, and a caller opts in either with the + * `enableTelemetryTracing` client option or with the + * `GOOGLE_SDK_NODE_ENABLE_TRACING` environment variable. The environment + * variable wins whenever it is set, so tracing can be switched on or off for a + * process without touching the code that constructs the client. + * + * Two further conditions apply while the feature is experimental: + * `GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED` must be `true`, and the client + * must have supplied `internalTelemetryInfo` — a client generated without + * tracing has no span metadata to report, so there is nothing to trace. + * * @param settings * @returns true if telemetry tracing is enabled, false otherwise */ export function checkTelemetryEnabled(settings?: CallSettings): boolean { - const tracingEnabled = - Boolean(settings?.enableTelemetryTracing) && - process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED === 'true' && - settings?.otherArgs?.internalTelemetryInfo !== undefined; - return Boolean(tracingEnabled); + // `process` is undeclared in browsers and some edge runtimes, where reading + // it would throw a ReferenceError rather than yield undefined, so it is + // reached through a `typeof` guard and stands in as an empty environment. + // Tracing is then simply off there, since the environment cannot opt in. + const env: Record = + typeof process === 'object' && typeof process.env === 'object' + ? process.env + : {}; + + // An absent or empty environment variable counts as unset, which is what + // separates it from an explicit `false`: only an explicit value overrides + // the opt-in the caller passed in `clientOptions`. + const envOptIn = env.GOOGLE_SDK_NODE_ENABLE_TRACING?.trim(); + const tracingRequested = envOptIn + ? envOptIn.toLowerCase() === 'true' + : Boolean(settings?.enableTelemetryTracing); + + return ( + tracingRequested && + env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED === 'true' && + settings?.otherArgs?.internalTelemetryInfo !== undefined + ); } function words(str: string, normalize = false) { diff --git a/core/packages/gax/test/unit/util.ts b/core/packages/gax/test/unit/util.ts index 89659ac3a47..ae93a29e08b 100644 --- a/core/packages/gax/test/unit/util.ts +++ b/core/packages/gax/test/unit/util.ts @@ -203,6 +203,7 @@ describe('util.ts', () => { describe('checkTelemetryEnabled', () => { afterEach(() => { delete process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED; + delete process.env.GOOGLE_SDK_NODE_ENABLE_TRACING; }); const mockTelemetryInfo: StaticTraceContext = { @@ -257,5 +258,75 @@ describe('util.ts', () => { process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; assert.strictEqual(checkTelemetryEnabled(undefined), false); }); + + it('returns true when GOOGLE_SDK_NODE_ENABLE_TRACING=true and the client option is not set', () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + process.env.GOOGLE_SDK_NODE_ENABLE_TRACING = 'true'; + const noOptInSettings = new CallSettings({ + otherArgs: { + internalTelemetryInfo: mockTelemetryInfo, + }, + }); + assert.strictEqual(checkTelemetryEnabled(noOptInSettings), true); + }); + + it('accepts GOOGLE_SDK_NODE_ENABLE_TRACING case-insensitively', () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + process.env.GOOGLE_SDK_NODE_ENABLE_TRACING = 'TRUE'; + const noOptInSettings = new CallSettings({ + otherArgs: { + internalTelemetryInfo: mockTelemetryInfo, + }, + }); + assert.strictEqual(checkTelemetryEnabled(noOptInSettings), true); + }); + + it('returns false when GOOGLE_SDK_NODE_ENABLE_TRACING=false overrides the client option', () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + process.env.GOOGLE_SDK_NODE_ENABLE_TRACING = 'false'; + assert.strictEqual(checkTelemetryEnabled(mockSettings), false); + }); + + it('falls back to the client option when GOOGLE_SDK_NODE_ENABLE_TRACING is empty', () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + process.env.GOOGLE_SDK_NODE_ENABLE_TRACING = ''; + assert.strictEqual(checkTelemetryEnabled(mockSettings), true); + }); + + it('treats an unrecognized GOOGLE_SDK_NODE_ENABLE_TRACING value as disabled', () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + process.env.GOOGLE_SDK_NODE_ENABLE_TRACING = 'yes'; + assert.strictEqual(checkTelemetryEnabled(mockSettings), false); + }); + + it('returns false when GOOGLE_SDK_NODE_ENABLE_TRACING=true but the experimental flag is unset', () => { + process.env.GOOGLE_SDK_NODE_ENABLE_TRACING = 'true'; + assert.strictEqual(checkTelemetryEnabled(mockSettings), false); + }); + + it('returns false when GOOGLE_SDK_NODE_ENABLE_TRACING=true but internalTelemetryInfo is not set', () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + process.env.GOOGLE_SDK_NODE_ENABLE_TRACING = 'true'; + assert.strictEqual(checkTelemetryEnabled(new CallSettings({})), false); + }); + + it('does not throw when process is undefined, as in a browser', () => { + const globals = globalThis as {process?: NodeJS.Process}; + const originalProcess = globals.process; + // The result is captured and `process` restored before asserting, + // because mocha and assert need `process` themselves. + let result: boolean | undefined; + let thrown: unknown; + delete globals.process; + try { + result = checkTelemetryEnabled(mockSettings); + } catch (e) { + thrown = e; + } finally { + globals.process = originalProcess; + } + assert.strictEqual(thrown, undefined); + assert.strictEqual(result, false); + }); }); });