From cca4bd694a851b9a72c0f0778f5723855d531a23 Mon Sep 17 00:00:00 2001 From: Shivanee Persaud Date: Tue, 8 Sep 2026 14:37:44 -0700 Subject: [PATCH 01/10] feat(gax): wrap api calls with traceAttempt when tracing is enabled --- core/packages/gax/src/createApiCall.ts | 43 +++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/core/packages/gax/src/createApiCall.ts b/core/packages/gax/src/createApiCall.ts index e161879d5c9..6a86bd2bcd5 100644 --- a/core/packages/gax/src/createApiCall.ts +++ b/core/packages/gax/src/createApiCall.ts @@ -22,6 +22,7 @@ import {createAPICaller} from './apiCaller'; import { APICallback, GaxCall, + GaxCallResult, GRPCCall, GRPCCallOtherArgs, RequestType, @@ -33,6 +34,12 @@ import {retryable} from './normalCalls/retries'; import {addTimeoutArg} from './normalCalls/timeout'; import {StreamingApiCaller} from './streamingCalls/streamingApiCaller'; import {warn} from './warnings'; +import { + traceAttempt, + StaticTraceContext, + DynamicTraceContext, +} from './observability/TracerHelper'; +import {checkTelemetryEnabled} from './util'; /** * Converts an rpc call into an API call governed by the settings. @@ -66,6 +73,9 @@ export function createApiCall( const funcPromise = typeof func === 'function' ? Promise.resolve(func) : func; // the following apiCaller will be used for all calls of this function... const apiCaller = createAPICaller(settings, descriptor); + + const tracingEnabled = checkTelemetryEnabled(settings); + const invokeCall = ( request: RequestType, callOptions?: CallOptions, @@ -168,5 +178,36 @@ export function createApiCall( // or to cancel the ongoing call. return currentApiCaller.result(ongoingCall); }; - return invokeCall; + + if (tracingEnabled) { + const staticArgs: StaticTraceContext = { + gcpClientService: + settings.otherArgs?.internalTelemetryInfo.gcpClientService, + gcpVersion: settings.otherArgs?.internalTelemetryInfo.gcpVersion, + gcpRepo: settings.otherArgs?.internalTelemetryInfo.gcpRepo, + gcpArtifact: settings.otherArgs?.internalTelemetryInfo.gcpArtifact, + }; + + const serviceName = settings.apiName?.split('.').pop() ?? ''; + const dynamicArgs: DynamicTraceContext = { + clientName: serviceName ? `${serviceName}Client` : '', + methodName: settings.otherArgs?.internalMethodName ?? '', + rpcType: _fallback ? 'http' : 'grpc', + }; + return ( + request: RequestType, + callOptions?: CallOptions, + callback?: APICallback, + ) => { + return traceAttempt(dynamicArgs, staticArgs, async () => { + return (await invokeCall( + request, + callOptions, + callback, + )) as GaxCallResult; + }) as unknown as GaxCallResult; + }; + } else { + return invokeCall; + } } From 480dfb8231777f728c26c5c3b8a4b1c3b81ef356 Mon Sep 17 00:00:00 2001 From: Shivanee Persaud Date: Tue, 8 Sep 2026 15:18:16 -0700 Subject: [PATCH 02/10] refactor(gax): simplify traceAttempt and remove type casts in createApiCall --- core/packages/gax/src/createApiCall.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/core/packages/gax/src/createApiCall.ts b/core/packages/gax/src/createApiCall.ts index 6a86bd2bcd5..97c83409bbd 100644 --- a/core/packages/gax/src/createApiCall.ts +++ b/core/packages/gax/src/createApiCall.ts @@ -22,7 +22,6 @@ import {createAPICaller} from './apiCaller'; import { APICallback, GaxCall, - GaxCallResult, GRPCCall, GRPCCallOtherArgs, RequestType, @@ -199,13 +198,9 @@ export function createApiCall( callOptions?: CallOptions, callback?: APICallback, ) => { - return traceAttempt(dynamicArgs, staticArgs, async () => { - return (await invokeCall( - request, - callOptions, - callback, - )) as GaxCallResult; - }) as unknown as GaxCallResult; + return traceAttempt(dynamicArgs, staticArgs, () => { + return invokeCall(request, callOptions, callback); + }); }; } else { return invokeCall; From e6fcde025169a654757632e2422246bbacd35380 Mon Sep 17 00:00:00 2001 From: Shivanee Persaud Date: Tue, 8 Sep 2026 15:26:56 -0700 Subject: [PATCH 03/10] test(gax): add unit tests for telemetry tracing and handle false fallback --- core/packages/gax/src/createApiCall.ts | 5 +- core/packages/gax/test/unit/apiCallable.ts | 342 ++++++++++++++++++++- 2 files changed, 344 insertions(+), 3 deletions(-) diff --git a/core/packages/gax/src/createApiCall.ts b/core/packages/gax/src/createApiCall.ts index 97c83409bbd..f33a4065ca8 100644 --- a/core/packages/gax/src/createApiCall.ts +++ b/core/packages/gax/src/createApiCall.ts @@ -64,7 +64,7 @@ export function createApiCall( settings: CallSettings, descriptor?: Descriptor, // eslint-disable-next-line @typescript-eslint/no-unused-vars - _fallback?: boolean | 'proto' | 'rest', // unused here, used in fallback.ts implementation + _fallback?: boolean | 'proto' | 'rest' | 'false' | string, ): GaxCall { // we want to be able to accept both promise resolving to a function and a // function. Currently client librares are only calling this method with a @@ -188,10 +188,11 @@ export function createApiCall( }; const serviceName = settings.apiName?.split('.').pop() ?? ''; + const isFallback = Boolean(_fallback && _fallback !== 'false'); const dynamicArgs: DynamicTraceContext = { clientName: serviceName ? `${serviceName}Client` : '', methodName: settings.otherArgs?.internalMethodName ?? '', - rpcType: _fallback ? 'http' : 'grpc', + rpcType: isFallback ? 'http' : 'grpc', }; return ( request: RequestType, diff --git a/core/packages/gax/test/unit/apiCallable.ts b/core/packages/gax/test/unit/apiCallable.ts index f7cfaed4148..31bf118c22c 100644 --- a/core/packages/gax/test/unit/apiCallable.ts +++ b/core/packages/gax/test/unit/apiCallable.ts @@ -16,12 +16,15 @@ import assert from 'assert'; import {status} from '@grpc/grpc-js'; -import {afterEach, describe, it} from 'mocha'; +import {afterEach, beforeEach, describe, it} from 'mocha'; import * as sinon from 'sinon'; import {RequestType} from '../../src/apitypes'; +import {createApiCall as realCreateApiCall} from '../../src/createApiCall'; import * as gax from '../../src/gax'; import {GoogleError} from '../../src/googleError'; +import {OtelHarness} from './otelHarness'; +import {StaticTraceContext} from '../../src/observability/TracerHelper'; import * as utils from './utils'; import * as retries from '../../src/normalCalls/retries'; @@ -331,7 +334,22 @@ describe('createApiCall', () => { }); describe('in regards to OpenTelemetry Tracing', () => { + let harness: OtelHarness; + + const telemetryInfo: StaticTraceContext = { + gcpClientService: 'echo.googleapis.com', + gcpVersion: '1.2.3', + gcpRepo: 'googleapis/google-cloud-node', + gcpArtifact: '@google-cloud/echo', + }; + + beforeEach(() => { + harness = new OtelHarness(); + harness.setup(); + }); + afterEach(() => { + harness.teardown(); delete process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED; }); @@ -353,6 +371,328 @@ describe('createApiCall', () => { const apiCall = createApiCall(() => {}); assert.strictEqual(typeof apiCall, 'function'); }); + + it('correctly pipes telemetry information into the active span for gRPC calls', async () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const settings = new gax.CallSettings({ + apiName: 'google.example.v1.Echo', + enableTelemetryTracing: true, + otherArgs: { + internalTelemetryInfo: telemetryInfo, + internalMethodName: 'Echo', + }, + }); + + function func( + argument: {}, + metadata: {}, + options: {}, + callback: (err: GoogleError | null, resp?: unknown) => void, + ) { + callback(null, {data: 'hello'}); + return { + cancel: () => {}, + }; + } + + const apiCall = realCreateApiCall(func, settings); + const [response] = (await apiCall({}, undefined)) as [ + {data: string}, + unknown, + unknown, + ]; + assert.deepStrictEqual(response, {data: 'hello'}); + + const spans = harness.getSpans('google-gax'); + assert.strictEqual(spans.length, 1); + const span = spans[0]; + assert.strictEqual(span.name, 'EchoClient.Echo'); + assert.strictEqual(span.ended, true); + assert.strictEqual( + span.attributes['gcp.client.service'], + 'echo.googleapis.com', + ); + assert.strictEqual(span.attributes['gcp.client.version'], '1.2.3'); + assert.strictEqual( + span.attributes['gcp.repo'], + 'googleapis/google-cloud-node', + ); + assert.strictEqual(span.attributes['gcp.artifact'], '@google-cloud/echo'); + assert.strictEqual(span.attributes['gcp.method.name'], 'Echo'); + assert.strictEqual(span.attributes['gcp.method.type'], 'grpc'); + }); + + it('correctly pipes telemetry information for HTTP fallback calls', async () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const settings = new gax.CallSettings({ + apiName: 'google.example.v1.Echo', + enableTelemetryTracing: true, + otherArgs: { + internalTelemetryInfo: telemetryInfo, + internalMethodName: 'Echo', + }, + }); + + function func( + argument: {}, + metadata: {}, + options: {}, + callback: (err: GoogleError | null, resp?: unknown) => void, + ) { + callback(null, {data: 'hello'}); + return { + cancel: () => {}, + }; + } + + const apiCall = realCreateApiCall(func, settings, undefined, true); + await apiCall({}, undefined); + + const spans = harness.getSpans('google-gax'); + assert.strictEqual(spans.length, 1); + const span = spans[0]; + assert.strictEqual(span.name, 'EchoClient.Echo'); + assert.strictEqual(span.ended, true); + assert.strictEqual(span.attributes['gcp.method.type'], 'http'); + }); + + it('sets rpcType to grpc when _fallback is boolean false', async () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const settings = new gax.CallSettings({ + apiName: 'google.example.v1.Echo', + enableTelemetryTracing: true, + otherArgs: { + internalTelemetryInfo: telemetryInfo, + internalMethodName: 'Echo', + }, + }); + + function func( + argument: {}, + metadata: {}, + options: {}, + callback: (err: GoogleError | null, resp?: unknown) => void, + ) { + callback(null, {data: 'hello'}); + return { + cancel: () => {}, + }; + } + + const apiCall = realCreateApiCall(func, settings, undefined, false); + await apiCall({}, undefined); + + const spans = harness.getSpans('google-gax'); + assert.strictEqual(spans.length, 1); + const span = spans[0]; + assert.strictEqual(span.attributes['gcp.method.type'], 'grpc'); + }); + + it('sets rpcType to grpc when _fallback is string "false"', async () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const settings = new gax.CallSettings({ + apiName: 'google.example.v1.Echo', + enableTelemetryTracing: true, + otherArgs: { + internalTelemetryInfo: telemetryInfo, + internalMethodName: 'Echo', + }, + }); + + function func( + argument: {}, + metadata: {}, + options: {}, + callback: (err: GoogleError | null, resp?: unknown) => void, + ) { + callback(null, {data: 'hello'}); + return { + cancel: () => {}, + }; + } + + const apiCall = realCreateApiCall(func, settings, undefined, 'false'); + await apiCall({}, undefined); + + const spans = harness.getSpans('google-gax'); + assert.strictEqual(spans.length, 1); + const span = spans[0]; + assert.strictEqual(span.attributes['gcp.method.type'], 'grpc'); + }); + + it('pipes telemetry information configured via constructSettings', async () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const serviceName = 'google.example.v1.Echo'; + const defaults = gax.constructSettings( + serviceName, + { + interfaces: { + [serviceName]: { + methods: { + Echo: {}, + }, + }, + }, + }, + {}, + {}, + undefined, + true, + telemetryInfo, + ); + + function func( + argument: {}, + metadata: {}, + options: {}, + callback: (err: GoogleError | null, resp?: unknown) => void, + ) { + callback(null, {data: 'hello'}); + return { + cancel: () => {}, + }; + } + + const apiCall = realCreateApiCall(func, defaults.echo); + await apiCall({}, undefined); + + const spans = harness.getSpans('google-gax'); + assert.strictEqual(spans.length, 1); + const span = spans[0]; + assert.strictEqual(span.name, 'EchoClient.Echo'); + assert.strictEqual(span.ended, true); + assert.strictEqual( + span.attributes['gcp.client.service'], + 'echo.googleapis.com', + ); + assert.strictEqual(span.attributes['gcp.client.version'], '1.2.3'); + assert.strictEqual( + span.attributes['gcp.repo'], + 'googleapis/google-cloud-node', + ); + assert.strictEqual(span.attributes['gcp.artifact'], '@google-cloud/echo'); + assert.strictEqual(span.attributes['gcp.method.name'], 'Echo'); + assert.strictEqual(span.attributes['gcp.method.type'], 'grpc'); + }); + + it('records error details on the span when the API call fails', async () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const settings = new gax.CallSettings({ + apiName: 'google.example.v1.Echo', + enableTelemetryTracing: true, + otherArgs: { + internalTelemetryInfo: telemetryInfo, + internalMethodName: 'Echo', + }, + }); + + function failingFunc( + argument: {}, + metadata: {}, + options: {}, + callback: (err: GoogleError | null, resp?: unknown) => void, + ) { + const error = new GoogleError('RPC test failure'); + callback(error); + return { + cancel: () => {}, + }; + } + + const apiCall = realCreateApiCall(failingFunc, settings); + await assert.rejects( + async () => { + await apiCall({}, undefined); + }, + (err: GoogleError) => { + assert.strictEqual(err.message, 'RPC test failure'); + return true; + }, + ); + + const spans = harness.getSpans('google-gax'); + assert.strictEqual(spans.length, 1); + const span = spans[0]; + assert.strictEqual(span.ended, true); + assert.strictEqual(span.attributes['error.message'], 'RPC test failure'); + assert.strictEqual(span.events.length, 1); + assert.strictEqual(span.events[0].name, 'exception'); + }); + + it('cancels the call and ends the span', async () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const settings = new gax.CallSettings({ + apiName: 'google.example.v1.Echo', + enableTelemetryTracing: true, + otherArgs: { + internalTelemetryInfo: telemetryInfo, + internalMethodName: 'Echo', + }, + }); + + function cancellableFunc( + argument: {}, + metadata: {}, + options: {}, + callback: (err: GoogleError | null, resp?: unknown) => void, + ) { + const timeoutId = setTimeout(() => { + callback(null, {data: 'done'}); + }, 5000); + return { + cancel: () => { + clearTimeout(timeoutId); + const err = new GoogleError('cancelled'); + err.code = status.CANCELLED; + callback(err); + }, + }; + } + + const apiCall = realCreateApiCall(cancellableFunc, settings); + const promise = apiCall({}, undefined); + assert.strictEqual(typeof promise.cancel, 'function'); + promise.cancel(); + + await assert.rejects(async () => { + await promise; + }); + + const spans = harness.getSpans('google-gax'); + assert.strictEqual(spans.length, 1); + const span = spans[0]; + assert.strictEqual(span.ended, true); + }); + + it('does not create any spans when tracing is disabled', async () => { + delete process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED; + const settings = new gax.CallSettings({ + apiName: 'google.example.v1.Echo', + enableTelemetryTracing: false, + otherArgs: { + internalTelemetryInfo: telemetryInfo, + internalMethodName: 'Echo', + }, + }); + + function func( + argument: {}, + metadata: {}, + options: {}, + callback: (err: GoogleError | null, resp?: unknown) => void, + ) { + callback(null, {data: 'hello'}); + return { + cancel: () => {}, + }; + } + + const apiCall = realCreateApiCall(func, settings); + await apiCall({}, undefined); + + const spans = harness.getSpans('google-gax'); + assert.strictEqual(spans.length, 0); + }); }); }); From eaf845069d497d16ee98f45454c980e7ac696bdd Mon Sep 17 00:00:00 2001 From: Shivanee Persaud Date: Tue, 8 Sep 2026 16:08:17 -0700 Subject: [PATCH 04/10] fix(gax): support custom thenables and streams in traceAttempt --- core/packages/gax/test/unit/apiCallable.ts | 107 ++++++++++++++++++++- 1 file changed, 105 insertions(+), 2 deletions(-) diff --git a/core/packages/gax/test/unit/apiCallable.ts b/core/packages/gax/test/unit/apiCallable.ts index 31bf118c22c..5c2b4b067d7 100644 --- a/core/packages/gax/test/unit/apiCallable.ts +++ b/core/packages/gax/test/unit/apiCallable.ts @@ -15,12 +15,15 @@ */ import assert from 'assert'; +import {PassThrough} from 'stream'; import {status} from '@grpc/grpc-js'; import {afterEach, beforeEach, describe, it} from 'mocha'; import * as sinon from 'sinon'; -import {RequestType} from '../../src/apitypes'; +import {CancellableStream, GRPCCall, RequestType} from '../../src/apitypes'; import {createApiCall as realCreateApiCall} from '../../src/createApiCall'; +import {StreamDescriptor} from '../../src/descriptor'; +import {StreamType} from '../../src/streamingCalls/streaming'; import * as gax from '../../src/gax'; import {GoogleError} from '../../src/googleError'; import {OtelHarness} from './otelHarness'; @@ -593,7 +596,9 @@ describe('createApiCall', () => { callback: (err: GoogleError | null, resp?: unknown) => void, ) { const error = new GoogleError('RPC test failure'); - callback(error); + setImmediate(() => { + callback(error); + }); return { cancel: () => {}, }; @@ -693,6 +698,104 @@ describe('createApiCall', () => { const spans = harness.getSpans('google-gax'); assert.strictEqual(spans.length, 0); }); + + it('manages span lifetime for streaming API calls until stream ends', done => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const settings = new gax.CallSettings({ + apiName: 'google.example.v1.Echo', + enableTelemetryTracing: true, + otherArgs: { + internalTelemetryInfo: telemetryInfo, + internalMethodName: 'Echo', + }, + }); + + const spy = sinon.spy(() => { + const s = new PassThrough({ + objectMode: true, + }); + s.push({data: 'chunk1'}); + s.push({data: 'chunk2'}); + s.push(null); + return Object.assign(s, {cancel: () => {}}); + }); + + const apiCall = realCreateApiCall( + spy as unknown as GRPCCall, + settings, + new StreamDescriptor(StreamType.SERVER_STREAMING, true), + ); + const stream = apiCall({}, undefined) as CancellableStream; + assert.strictEqual(harness.getSpans('google-gax').length, 0); + + const received: unknown[] = []; + stream.on('data', chunk => { + received.push(chunk); + }); + stream.on('end', () => { + try { + assert.strictEqual(received.length, 2); + const spans = harness.getSpans('google-gax'); + assert.strictEqual(spans.length, 1); + const span = spans[0]; + assert.strictEqual(span.ended, true); + assert.strictEqual(span.name, 'EchoClient.Echo'); + assert.strictEqual(span.attributes['gcp.method.type'], 'grpc'); + done(); + } catch (e) { + done(e); + } + }); + }); + + it('records error details on the span when a streaming API call errors', done => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const settings = new gax.CallSettings({ + apiName: 'google.example.v1.Echo', + enableTelemetryTracing: true, + otherArgs: { + internalTelemetryInfo: telemetryInfo, + internalMethodName: 'Echo', + }, + }); + + const spy = sinon.spy(() => { + const s = new PassThrough({ + objectMode: true, + }); + setImmediate(() => { + s.emit('error', new GoogleError('streaming test failure')); + }); + return Object.assign(s, {cancel: () => {}}); + }); + + const apiCall = realCreateApiCall( + spy as unknown as GRPCCall, + settings, + new StreamDescriptor(StreamType.SERVER_STREAMING, true), + ); + const stream = apiCall({}, undefined) as CancellableStream; + assert.strictEqual(harness.getSpans('google-gax').length, 0); + + stream.on('error', (err: GoogleError) => { + try { + assert.strictEqual(err.message, 'streaming test failure'); + const spans = harness.getSpans('google-gax'); + assert.strictEqual(spans.length, 1); + const span = spans[0]; + assert.strictEqual(span.ended, true); + assert.strictEqual( + span.attributes['error.message'], + 'streaming test failure', + ); + assert.strictEqual(span.events.length, 1); + assert.strictEqual(span.events[0].name, 'exception'); + done(); + } catch (e) { + done(e); + } + }); + }); }); }); From e51732b7b6b013f617d0318f25c0d86a4955acee Mon Sep 17 00:00:00 2001 From: Shivanee Persaud Date: Tue, 8 Sep 2026 16:21:33 -0700 Subject: [PATCH 05/10] refactor(gax): use standard EventEmitter and Promise checks and add premature span closure tests --- core/packages/gax/test/unit/apiCallable.ts | 49 +++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/core/packages/gax/test/unit/apiCallable.ts b/core/packages/gax/test/unit/apiCallable.ts index 5c2b4b067d7..ce65c81d97d 100644 --- a/core/packages/gax/test/unit/apiCallable.ts +++ b/core/packages/gax/test/unit/apiCallable.ts @@ -605,9 +605,12 @@ describe('createApiCall', () => { } const apiCall = realCreateApiCall(failingFunc, settings); + const promise = apiCall({}, undefined); + assert.strictEqual(harness.getSpans('google-gax').length, 0); + await assert.rejects( async () => { - await apiCall({}, undefined); + await promise; }, (err: GoogleError) => { assert.strictEqual(err.message, 'RPC test failure'); @@ -624,6 +627,48 @@ describe('createApiCall', () => { assert.strictEqual(span.events[0].name, 'exception'); }); + it('does not end span prematurely for successful asynchronous API calls', async () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const settings = new gax.CallSettings({ + apiName: 'google.example.v1.Echo', + enableTelemetryTracing: true, + otherArgs: { + internalTelemetryInfo: telemetryInfo, + internalMethodName: 'Echo', + }, + }); + + function asyncFunc( + argument: {}, + metadata: {}, + options: {}, + callback: (err: GoogleError | null, resp?: unknown) => void, + ) { + setImmediate(() => { + callback(null, {data: 'hello'}); + }); + return { + cancel: () => {}, + }; + } + + const apiCall = realCreateApiCall(asyncFunc, settings); + const promise = apiCall({}, undefined); + + // Verify the span is not ended prematurely while the call is in flight + assert.strictEqual(harness.getSpans('google-gax').length, 0); + + const [response] = (await promise) as [{data: string}, unknown, unknown]; + assert.deepStrictEqual(response, {data: 'hello'}); + + // Span must only be ended after completion + const spans = harness.getSpans('google-gax'); + assert.strictEqual(spans.length, 1); + const span = spans[0]; + assert.strictEqual(span.ended, true); + assert.strictEqual(span.name, 'EchoClient.Echo'); + }); + it('cancels the call and ends the span', async () => { process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; const settings = new gax.CallSettings({ @@ -731,6 +776,8 @@ describe('createApiCall', () => { const received: unknown[] = []; stream.on('data', chunk => { received.push(chunk); + // Span must remain active while streaming chunks + assert.strictEqual(harness.getSpans('google-gax').length, 0); }); stream.on('end', () => { try { From 5619c336ef75935b22129c28762de74b1e81987e Mon Sep 17 00:00:00 2001 From: Shivanee Persaud Date: Tue, 8 Sep 2026 17:03:03 -0700 Subject: [PATCH 06/10] feat(gax): pass isStreaming flag to traceAttempt in createApiCall --- core/packages/gax/src/createApiCall.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/core/packages/gax/src/createApiCall.ts b/core/packages/gax/src/createApiCall.ts index f33a4065ca8..8d44ffa01ae 100644 --- a/core/packages/gax/src/createApiCall.ts +++ b/core/packages/gax/src/createApiCall.ts @@ -194,14 +194,20 @@ export function createApiCall( methodName: settings.otherArgs?.internalMethodName ?? '', rpcType: isFallback ? 'http' : 'grpc', }; + const isStreaming = apiCaller instanceof StreamingApiCaller; return ( request: RequestType, callOptions?: CallOptions, callback?: APICallback, ) => { - return traceAttempt(dynamicArgs, staticArgs, () => { - return invokeCall(request, callOptions, callback); - }); + return traceAttempt( + dynamicArgs, + staticArgs, + () => { + return invokeCall(request, callOptions, callback); + }, + isStreaming, + ); }; } else { return invokeCall; From a867c15016e1c70e9cac1f38d5d69b2f158d53ee Mon Sep 17 00:00:00 2001 From: Shivanee Persaud Date: Wed, 9 Sep 2026 13:34:51 -0700 Subject: [PATCH 07/10] refactor(gax): rename isStreaming to isStreamingCall in createApiCall --- core/packages/gax/src/createApiCall.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/packages/gax/src/createApiCall.ts b/core/packages/gax/src/createApiCall.ts index 8d44ffa01ae..50da87fc895 100644 --- a/core/packages/gax/src/createApiCall.ts +++ b/core/packages/gax/src/createApiCall.ts @@ -194,7 +194,7 @@ export function createApiCall( methodName: settings.otherArgs?.internalMethodName ?? '', rpcType: isFallback ? 'http' : 'grpc', }; - const isStreaming = apiCaller instanceof StreamingApiCaller; + const isStreamingCall = apiCaller instanceof StreamingApiCaller; return ( request: RequestType, callOptions?: CallOptions, @@ -206,7 +206,7 @@ export function createApiCall( () => { return invokeCall(request, callOptions, callback); }, - isStreaming, + isStreamingCall, ); }; } else { From e6ec979c03abb998e5ab6f479966f963f484e28e Mon Sep 17 00:00:00 2001 From: Shivanee Persaud Date: Wed, 9 Sep 2026 13:53:51 -0700 Subject: [PATCH 08/10] test(gax): add unit tests for createApiCall tracing branch --- core/packages/gax/test/unit/apiCallable.ts | 138 +++++++++++++++++++-- 1 file changed, 127 insertions(+), 11 deletions(-) diff --git a/core/packages/gax/test/unit/apiCallable.ts b/core/packages/gax/test/unit/apiCallable.ts index ce65c81d97d..3abb449e71e 100644 --- a/core/packages/gax/test/unit/apiCallable.ts +++ b/core/packages/gax/test/unit/apiCallable.ts @@ -27,6 +27,7 @@ import {StreamType} from '../../src/streamingCalls/streaming'; import * as gax from '../../src/gax'; import {GoogleError} from '../../src/googleError'; import {OtelHarness} from './otelHarness'; +import * as tracerHelper from '../../src/observability/TracerHelper'; import {StaticTraceContext} from '../../src/observability/TracerHelper'; import * as utils from './utils'; import * as retries from '../../src/normalCalls/retries'; @@ -356,23 +357,138 @@ describe('createApiCall', () => { delete process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED; }); - it('creates an api call when GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED and CallSettings field is set', () => { + it('calls traceAttempt with dynamicArgs, staticArgs, and isStreamingCall when tracing is enabled', async () => { process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; - const mockCallOptions: gax.CallOptions = { + const traceAttemptSpy = sinon.spy(tracerHelper, 'traceAttempt'); + + const settings = new gax.CallSettings({ + apiName: 'google.example.v1.Echo', enableTelemetryTracing: true, otherArgs: { - internalTelemetryInfo: { - gcpClientService: 'test.googleapis.com', - }, + internalTelemetryInfo: telemetryInfo, + internalMethodName: 'Echo', }, - }; - const apiCall = createApiCall(() => {}, {settings: mockCallOptions}); - assert.strictEqual(typeof apiCall, 'function'); + }); + + function func( + argument: {}, + metadata: {}, + options: {}, + callback: (err: GoogleError | null, resp?: unknown) => void, + ) { + callback(null, {data: 'hello'}); + return {cancel: () => {}}; + } + + const apiCall = realCreateApiCall(func, settings); + await apiCall({param: 'test'}, undefined); + + assert.strictEqual(traceAttemptSpy.calledOnce, true); + const [dynamicArgs, staticArgs, fn, isStreamingCall] = + traceAttemptSpy.firstCall.args; + + assert.deepStrictEqual(dynamicArgs, { + clientName: 'EchoClient', + methodName: 'Echo', + rpcType: 'grpc', + }); + assert.deepStrictEqual(staticArgs, telemetryInfo); + assert.strictEqual(typeof fn, 'function'); + assert.strictEqual(isStreamingCall, false); + }); + + it('passes isStreamingCall as true to traceAttempt for streaming calls when tracing is enabled', () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const traceAttemptSpy = sinon.spy(tracerHelper, 'traceAttempt'); + + const settings = new gax.CallSettings({ + apiName: 'google.example.v1.Echo', + enableTelemetryTracing: true, + otherArgs: { + internalTelemetryInfo: telemetryInfo, + internalMethodName: 'Echo', + }, + }); + + const spy = sinon.spy(() => { + const s = new PassThrough({objectMode: true}); + s.push(null); + return Object.assign(s, {cancel: () => {}}); + }); + + const apiCall = realCreateApiCall( + spy as unknown as GRPCCall, + settings, + new StreamDescriptor(StreamType.SERVER_STREAMING, true), + ); + void apiCall({}, undefined); + + assert.strictEqual(traceAttemptSpy.calledOnce, true); + const [, , , isStreamingCall] = traceAttemptSpy.firstCall.args; + assert.strictEqual(isStreamingCall, true); + }); + + it('gracefully handles missing apiName and internalMethodName when tracing is enabled', async () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const traceAttemptSpy = sinon.spy(tracerHelper, 'traceAttempt'); + + const settings = new gax.CallSettings({ + enableTelemetryTracing: true, + otherArgs: { + internalTelemetryInfo: telemetryInfo, + }, + }); + + function func( + argument: {}, + metadata: {}, + options: {}, + callback: (err: GoogleError | null, resp?: unknown) => void, + ) { + callback(null, {data: 'hello'}); + return {cancel: () => {}}; + } + + const apiCall = realCreateApiCall(func, settings); + await apiCall({}, undefined); + + assert.strictEqual(traceAttemptSpy.calledOnce, true); + const [dynamicArgs] = traceAttemptSpy.firstCall.args; + assert.deepStrictEqual(dynamicArgs, { + clientName: '', + methodName: '', + rpcType: 'grpc', + }); }); - it('creates an api call when GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED is not set', () => { - const apiCall = createApiCall(() => {}); - assert.strictEqual(typeof apiCall, 'function'); + it('returns invokeCall directly without calling traceAttempt when tracing is disabled', async () => { + delete process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED; + const traceAttemptSpy = sinon.spy(tracerHelper, 'traceAttempt'); + + const settings = new gax.CallSettings({ + apiName: 'google.example.v1.Echo', + enableTelemetryTracing: false, + otherArgs: { + internalTelemetryInfo: telemetryInfo, + internalMethodName: 'Echo', + }, + }); + + function func( + argument: {}, + metadata: {}, + options: {}, + callback: (err: GoogleError | null, resp?: unknown) => void, + ) { + callback(null, {data: 'hello'}); + return {cancel: () => {}}; + } + + const apiCall = realCreateApiCall(func, settings); + await apiCall({}, undefined); + + assert.strictEqual(traceAttemptSpy.called, false); + assert.strictEqual(harness.getSpans('google-gax').length, 0); }); it('correctly pipes telemetry information into the active span for gRPC calls', async () => { From d6d6def3fbecb6001e95592b56752f7ded6cf946 Mon Sep 17 00:00:00 2001 From: Shivanee <46910562+shivanee-p@users.noreply.github.com> Date: Wed, 9 Sep 2026 14:32:57 -0700 Subject: [PATCH 09/10] Update core/packages/gax/src/createApiCall.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- core/packages/gax/src/createApiCall.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/packages/gax/src/createApiCall.ts b/core/packages/gax/src/createApiCall.ts index 50da87fc895..e5603e7fcf8 100644 --- a/core/packages/gax/src/createApiCall.ts +++ b/core/packages/gax/src/createApiCall.ts @@ -181,10 +181,10 @@ export function createApiCall( if (tracingEnabled) { const staticArgs: StaticTraceContext = { gcpClientService: - settings.otherArgs?.internalTelemetryInfo.gcpClientService, - gcpVersion: settings.otherArgs?.internalTelemetryInfo.gcpVersion, - gcpRepo: settings.otherArgs?.internalTelemetryInfo.gcpRepo, - gcpArtifact: settings.otherArgs?.internalTelemetryInfo.gcpArtifact, + settings.otherArgs.internalTelemetryInfo?.gcpClientService, + gcpVersion: settings.otherArgs.internalTelemetryInfo?.gcpVersion, + gcpRepo: settings.otherArgs.internalTelemetryInfo?.gcpRepo, + gcpArtifact: settings.otherArgs.internalTelemetryInfo?.gcpArtifact, }; const serviceName = settings.apiName?.split('.').pop() ?? ''; From 9ded6a05510883c6acc6be8927a8afff30aa96ab Mon Sep 17 00:00:00 2001 From: Shivanee Persaud Date: Thu, 10 Sep 2026 11:53:52 -0700 Subject: [PATCH 10/10] refactor(gax): keep _fallback parameter type as boolean | 'proto' | 'rest' in createApiCall --- core/packages/gax/src/createApiCall.ts | 4 +-- core/packages/gax/test/unit/apiCallable.ts | 38 ++++++++++++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/core/packages/gax/src/createApiCall.ts b/core/packages/gax/src/createApiCall.ts index e5603e7fcf8..43e8d56968e 100644 --- a/core/packages/gax/src/createApiCall.ts +++ b/core/packages/gax/src/createApiCall.ts @@ -64,7 +64,7 @@ export function createApiCall( settings: CallSettings, descriptor?: Descriptor, // eslint-disable-next-line @typescript-eslint/no-unused-vars - _fallback?: boolean | 'proto' | 'rest' | 'false' | string, + _fallback?: boolean | 'proto' | 'rest', // unused here, used in fallback.ts implementation ): GaxCall { // we want to be able to accept both promise resolving to a function and a // function. Currently client librares are only calling this method with a @@ -188,7 +188,7 @@ export function createApiCall( }; const serviceName = settings.apiName?.split('.').pop() ?? ''; - const isFallback = Boolean(_fallback && _fallback !== 'false'); + const isFallback = Boolean(_fallback); const dynamicArgs: DynamicTraceContext = { clientName: serviceName ? `${serviceName}Client` : '', methodName: settings.otherArgs?.internalMethodName ?? '', diff --git a/core/packages/gax/test/unit/apiCallable.ts b/core/packages/gax/test/unit/apiCallable.ts index 3abb449e71e..d1c7bb20183 100644 --- a/core/packages/gax/test/unit/apiCallable.ts +++ b/core/packages/gax/test/unit/apiCallable.ts @@ -607,7 +607,7 @@ describe('createApiCall', () => { assert.strictEqual(span.attributes['gcp.method.type'], 'grpc'); }); - it('sets rpcType to grpc when _fallback is string "false"', async () => { + it('sets rpcType to http when _fallback is "rest"', async () => { process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; const settings = new gax.CallSettings({ apiName: 'google.example.v1.Echo', @@ -630,13 +630,45 @@ describe('createApiCall', () => { }; } - const apiCall = realCreateApiCall(func, settings, undefined, 'false'); + const apiCall = realCreateApiCall(func, settings, undefined, 'rest'); await apiCall({}, undefined); const spans = harness.getSpans('google-gax'); assert.strictEqual(spans.length, 1); const span = spans[0]; - assert.strictEqual(span.attributes['gcp.method.type'], 'grpc'); + assert.strictEqual(span.attributes['gcp.method.type'], 'http'); + }); + + it('sets rpcType to http when _fallback is "proto"', async () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const settings = new gax.CallSettings({ + apiName: 'google.example.v1.Echo', + enableTelemetryTracing: true, + otherArgs: { + internalTelemetryInfo: telemetryInfo, + internalMethodName: 'Echo', + }, + }); + + function func( + argument: {}, + metadata: {}, + options: {}, + callback: (err: GoogleError | null, resp?: unknown) => void, + ) { + callback(null, {data: 'hello'}); + return { + cancel: () => {}, + }; + } + + const apiCall = realCreateApiCall(func, settings, undefined, 'proto'); + await apiCall({}, undefined); + + const spans = harness.getSpans('google-gax'); + assert.strictEqual(spans.length, 1); + const span = spans[0]; + assert.strictEqual(span.attributes['gcp.method.type'], 'http'); }); it('pipes telemetry information configured via constructSettings', async () => {