From 13386204613389f785f625426f2624ad7e1b7490 Mon Sep 17 00:00:00 2001 From: Shivanee Persaud Date: Mon, 14 Sep 2026 17:51:17 -0700 Subject: [PATCH 1/3] feat(gax): trace gRPC api calls via traceCall in createApiCall Wire TracerHelper.traceCall into createApiCall so gRPC calls emit spans when telemetry tracing is enabled. Covers unary, streaming, and callback-style calls, passing the isStreamingCall flag and the maxDurationMs backstop, and keeps the _fallback parameter type intact. Adds unit tests for the createApiCall tracing branch, stream retries, listener cleanup, and premature span closure. Squashed from 42 commits (24 of which were stale duplicates of shivaneep-o11y-tracer-helper-updates work) to restore linear history across the stack. Content is identical to the previous branch tip. --- core/packages/gax/src/createApiCall.ts | 50 +- core/packages/gax/test/unit/apiCallable.ts | 662 ++++++++++++++++++++- 2 files changed, 699 insertions(+), 13 deletions(-) diff --git a/core/packages/gax/src/createApiCall.ts b/core/packages/gax/src/createApiCall.ts index e161879d5c93..ed85cb573005 100644 --- a/core/packages/gax/src/createApiCall.ts +++ b/core/packages/gax/src/createApiCall.ts @@ -33,6 +33,12 @@ import {retryable} from './normalCalls/retries'; import {addTimeoutArg} from './normalCalls/timeout'; import {StreamingApiCaller} from './streamingCalls/streamingApiCaller'; import {warn} from './warnings'; +import { + traceCall, + StaticTraceContext, + DynamicTraceContext, +} from './observability/TracerHelper'; +import {checkTelemetryEnabled} from './util'; /** * Converts an rpc call into an API call governed by the settings. @@ -66,6 +72,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 +177,44 @@ 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 isFallback = Boolean(_fallback); + const dynamicArgs: DynamicTraceContext = { + clientName: serviceName ? `${serviceName}Client` : '', + methodName: settings.otherArgs?.internalMethodName ?? '', + rpcType: isFallback ? 'http' : 'grpc', + }; + const isStreamingCall = apiCaller instanceof StreamingApiCaller; + return ( + request: RequestType, + callOptions?: CallOptions, + callback?: APICallback, + ) => { + return traceCall( + dynamicArgs, + staticArgs, + (tracedCallback?: APICallback) => { + // `traceCall` only supplies a traced callback for callback-style, + // non-streaming invocations. When it is undefined the span is bound + // to the returned promise or stream instead, so pass the user's + // callback straight through. + return invokeCall(request, callOptions, tracedCallback ?? callback); + }, + isStreamingCall, + callback, + ); + }; + } else { + return invokeCall; + } } diff --git a/core/packages/gax/test/unit/apiCallable.ts b/core/packages/gax/test/unit/apiCallable.ts index f7cfaed41485..6bdc57d71649 100644 --- a/core/packages/gax/test/unit/apiCallable.ts +++ b/core/packages/gax/test/unit/apiCallable.ts @@ -15,13 +15,20 @@ */ import assert from 'assert'; +import {PassThrough} from 'stream'; 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 {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'; +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'; @@ -331,27 +338,658 @@ 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; + }); + + it('calls traceCall with dynamicArgs, staticArgs, and isStreamingCall when tracing is enabled', async () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const traceCallSpy = sinon.spy(tracerHelper, 'traceCall'); + + 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); + await apiCall({param: 'test'}, undefined); + + assert.strictEqual(traceCallSpy.calledOnce, true); + const [dynamicArgs, staticArgs, fn, isStreamingCall] = + traceCallSpy.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 traceCall for streaming calls when tracing is enabled', () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const traceCallSpy = sinon.spy(tracerHelper, 'traceCall'); + + 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(traceCallSpy.calledOnce, true); + const [, , , isStreamingCall] = traceCallSpy.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 traceCallSpy = sinon.spy(tracerHelper, 'traceCall'); + + 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(traceCallSpy.calledOnce, true); + const [dynamicArgs] = traceCallSpy.firstCall.args; + assert.deepStrictEqual(dynamicArgs, { + clientName: '', + methodName: '', + rpcType: 'grpc', + }); + }); + + it('returns invokeCall directly without calling traceCall when tracing is disabled', async () => { delete process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED; + const traceCallSpy = sinon.spy(tracerHelper, 'traceCall'); + + 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(traceCallSpy.called, false); + assert.strictEqual(harness.getSpans('google-gax').length, 0); + }); + + 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 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', + 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, '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'], 'http'); }); - it('creates an api call when GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED and CallSettings field is set', () => { + it('sets rpcType to http when _fallback is "proto"', async () => { process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; - const mockCallOptions: gax.CallOptions = { + const settings = new gax.CallSettings({ + apiName: 'google.example.v1.Echo', enableTelemetryTracing: true, otherArgs: { - internalTelemetryInfo: { - gcpClientService: 'test.googleapis.com', + 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 () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const serviceName = 'google.example.v1.Echo'; + const defaults = gax.constructSettings( + serviceName, + { + interfaces: { + [serviceName]: { + methods: { + Echo: {}, + }, + }, }, }, - }; - const apiCall = createApiCall(() => {}, {settings: mockCallOptions}); - assert.strictEqual(typeof apiCall, 'function'); + {}, + {}, + 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'); + setImmediate(() => { + callback(error); + }); + return { + cancel: () => {}, + }; + } + + const apiCall = realCreateApiCall(failingFunc, settings); + const promise = apiCall({}, undefined); + assert.strictEqual(harness.getSpans('google-gax').length, 0); + + await assert.rejects( + async () => { + await promise; + }, + (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('creates an api call when GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED is not set', () => { - const apiCall = createApiCall(() => {}); - assert.strictEqual(typeof apiCall, 'function'); + 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({ + 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); + }); + + 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); + // Span must remain active while streaming chunks + assert.strictEqual(harness.getSpans('google-gax').length, 0); + }); + 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 215e224bb396bec60c6a996fe5eabb0db1d1de73 Mon Sep 17 00:00:00 2001 From: Shivanee Persaud Date: Mon, 14 Sep 2026 18:02:22 -0700 Subject: [PATCH 2/3] refactor(gax): guard otherArgs in internalTelemetryInfo optional chain The staticArgs block started its optional chain at internalTelemetryInfo, leaving otherArgs itself unguarded, while internalMethodName a few lines below already used settings.otherArgs?.* This is not currently reachable: checkTelemetryEnabled(settings) guarantees otherArgs is defined before the tracing branch runs. It is also invisible to the compiler, since CallSettings declares otherArgs as required (CallOptions declares it optional), so tsc accepts the unguarded access. That combination means a refactor of the gating would surface this as a runtime TypeError with no compile-time warning. No behavior change. --- 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 ed85cb573005..1c3ffde595fd 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 1b77f4ef5f33136b6e1fa8ed5a030133ac87e1ea Mon Sep 17 00:00:00 2001 From: Shivanee Persaud Date: Tue, 15 Sep 2026 09:23:14 -0700 Subject: [PATCH 3/3] docs(gax): correct the traced callback contract in createApiCall traceCall now wraps the user's callback for stream calls as well, so the comment describing it as non-streaming only no longer holds. The tracedCallback ?? callback fallback is unchanged and still correct. --- core/packages/gax/src/createApiCall.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/packages/gax/src/createApiCall.ts b/core/packages/gax/src/createApiCall.ts index 1c3ffde595fd..80fbed668c56 100644 --- a/core/packages/gax/src/createApiCall.ts +++ b/core/packages/gax/src/createApiCall.ts @@ -204,10 +204,11 @@ export function createApiCall( dynamicArgs, staticArgs, (tracedCallback?: APICallback) => { - // `traceCall` only supplies a traced callback for callback-style, - // non-streaming invocations. When it is undefined the span is bound - // to the returned promise or stream instead, so pass the user's - // callback straight through. + // `traceCall` wraps the user's callback whenever one was supplied, + // for stream and non-stream calls alike, and that wrapper is what + // closes the span. It is undefined only when there is no callback to + // wrap, in which case the span is bound to the returned promise or + // stream instead; the fallback keeps this correct either way. return invokeCall(request, callOptions, tracedCallback ?? callback); }, isStreamingCall,