From 7eae8b84e92f79a19ff5f459943ec1dbe86d02fe Mon Sep 17 00:00:00 2001 From: Shivanee Persaud Date: Fri, 18 Sep 2026 11:04:05 -0700 Subject: [PATCH] feat(o11y): let callers enable tracing with GOOGLE_SDK_NODE_ENABLE_TRACING Tracing could only be switched on from code, by passing enableTelemetryTracing in clientOptions, so collecting traces meant editing and redeploying the application. Read GOOGLE_SDK_NODE_ENABLE_TRACING in checkTelemetryEnabled and let it win over the client option whenever it is set, so tracing can be turned on or off per process. An absent or empty value leaves the client option in charge, and GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED is still required while the feature is experimental. --- core/packages/gax/src/clientInterface.ts | 3 + core/packages/gax/src/util.ts | 41 ++++++++++++-- core/packages/gax/test/unit/util.ts | 71 ++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 6 deletions(-) diff --git a/core/packages/gax/src/clientInterface.ts b/core/packages/gax/src/clientInterface.ts index bf4571d62689..ab0dd2f15434 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 de388fe4c834..893ff7668800 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 89659ac3a472..ae93a29e08b1 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); + }); }); });