@@ -14,6 +14,7 @@ import {
1414} from "./errors" ;
1515import { setNotEnumerableProperty } from "./utils" ;
1616import { TelemetryAttribute , TelemetryAttributes } from "./telemetry/attributes" ;
17+ import { TelemetryConfiguration } from "./telemetry/configuration" ;
1718import { TelemetryHistograms } from "./telemetry/histograms" ;
1819
1920/**
@@ -233,37 +234,108 @@ function checkIfRetryableError(
233234 }
234235}
235236
237+ /**
238+ * Perform an HTTP request with retry logic and optional per-request telemetry emission.
239+ *
240+ * @param request - Axios request configuration for the HTTP call
241+ * @param config - Retry configuration containing `maxRetry` (maximum retry attempts) and `minWaitInMs` (base wait time for backoff)
242+ * @param axiosInstance - Axios instance used to execute the request
243+ * @param telemetryConfig - Optional telemetry configuration and original User-Agent to emit per-request metrics
244+ * @returns A `WrappedAxiosResponse<R>` with the successful Axios response and the number of retries performed, or `undefined` if no attempt succeeded
245+ * @throws A domain-specific mapped error when a non-retryable failure occurs or when retries are exhausted and the error is final
246+ */
236247export async function attemptHttpRequest < B , R > (
237248 request : AxiosRequestConfig < B > ,
238249 config : {
239250 maxRetry : number ;
240251 minWaitInMs : number ;
241252 } ,
242253 axiosInstance : AxiosInstance ,
254+ telemetryConfig ?: {
255+ telemetry ?: TelemetryConfiguration ;
256+ userAgent ?: string ;
257+ } ,
243258) : Promise < WrappedAxiosResponse < R > | undefined > {
244259 let iterationCount = 0 ;
245260 do {
246261 iterationCount ++ ;
262+
263+ // Track HTTP request duration for this specific call
264+ const httpRequestStart = performance . now ( ) ;
265+ let response : AxiosResponse < R > | undefined ;
266+ let httpRequestError : any ;
267+
247268 try {
248- const response = await axiosInstance ( request ) ;
269+ response = await axiosInstance ( request ) ;
270+ } catch ( err : any ) {
271+ httpRequestError = err ;
272+ }
273+
274+ // Calculate duration for this individual HTTP call
275+ const httpRequestDuration = Math . round ( performance . now ( ) - httpRequestStart ) ;
276+
277+ // Emit per-HTTP-request metric if telemetry is configured
278+ if ( telemetryConfig ?. telemetry ?. metrics ?. histogramHttpRequestDuration ) {
279+ const httpAttrs : Record < string , string | number > = { } ;
280+
281+ // Build attributes from the request
282+ if ( request . url ) {
283+ try {
284+ const parsedUrl = new URL ( request . url ) ;
285+ httpAttrs [ TelemetryAttribute . HttpHost ] = parsedUrl . hostname ;
286+ httpAttrs [ TelemetryAttribute . UrlScheme ] = parsedUrl . protocol ;
287+ httpAttrs [ TelemetryAttribute . UrlFull ] = request . url ;
288+ } catch {
289+ // URL parsing failed, still include the raw URL
290+ httpAttrs [ TelemetryAttribute . UrlFull ] = request . url ;
291+ }
292+ }
293+ if ( request . method ) {
294+ httpAttrs [ TelemetryAttribute . HttpRequestMethod ] = request . method . toUpperCase ( ) ;
295+ }
296+ if ( telemetryConfig . userAgent ) {
297+ httpAttrs [ TelemetryAttribute . UserAgentOriginal ] = telemetryConfig . userAgent ;
298+ }
299+
300+ // Add response status code if available
301+ const responseStatus = response ?. status || httpRequestError ?. response ?. status ;
302+ if ( responseStatus ) {
303+ httpAttrs [ TelemetryAttribute . HttpResponseStatusCode ] = responseStatus ;
304+ }
305+
306+ telemetryConfig . telemetry . recorder . histogram (
307+ TelemetryHistograms . httpRequestDuration ,
308+ httpRequestDuration ,
309+ TelemetryAttributes . prepare (
310+ httpAttrs ,
311+ telemetryConfig . telemetry . metrics . histogramHttpRequestDuration . attributes
312+ )
313+ ) ;
314+ }
315+
316+ // Handle successful response
317+ if ( response && ! httpRequestError ) {
249318 return {
250319 response : response ,
251320 retries : iterationCount - 1 ,
252321 } ;
253- } catch ( err : any ) {
254- const { retryable, error } = checkIfRetryableError ( err , iterationCount , config . maxRetry ) ;
322+ }
323+
324+ // Handle error
325+ if ( httpRequestError ) {
326+ const { retryable, error } = checkIfRetryableError ( httpRequestError , iterationCount , config . maxRetry ) ;
255327
256328 if ( ! retryable ) {
257329 throw error ;
258330 }
259331
260- const status = ( err as any ) ?. response ?. status ;
332+ const status = httpRequestError ?. response ?. status ;
261333 let retryDelayMs : number | undefined ;
262334
263335 if ( ( status &&
264336 ( status === 429 || ( status >= 500 && status !== 501 ) ) ) &&
265- err . response ?. headers ) {
266- retryDelayMs = parseRetryAfterHeader ( err . response . headers ) ;
337+ httpRequestError . response ?. headers ) {
338+ retryDelayMs = parseRetryAfterHeader ( httpRequestError . response . headers ) ;
267339 }
268340 if ( ! retryDelayMs ) {
269341 retryDelayMs = calculateExponentialBackoffWithJitter ( iterationCount , config . minWaitInMs ) ;
@@ -295,7 +367,10 @@ export const createRequestFunction = function (axiosArgs: RequestArgs, axiosInst
295367 const wrappedResponse = await attemptHttpRequest ( axiosRequestArgs , {
296368 maxRetry,
297369 minWaitInMs,
298- } , axios ) ;
370+ } , axios , {
371+ telemetry : configuration . telemetry ,
372+ userAgent : configuration . baseOptions ?. headers ?. [ "User-Agent" ] ,
373+ } ) ;
299374 const response = wrappedResponse ?. response ;
300375 const data = typeof response ?. data === "undefined" ? { } : response ?. data ;
301376 const result : CallResult < any > = { ...data } ;
0 commit comments