Skip to content

Commit 7958cba

Browse files
📝 Add docstrings to report-per-http-call
Docstrings generation was requested by @SoulPancake. * #303 (comment) The following files were modified: * `common.ts`
1 parent c7f612f commit 7958cba

File tree

1 file changed

+82
-7
lines changed

1 file changed

+82
-7
lines changed

common.ts

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from "./errors";
1515
import { setNotEnumerableProperty } from "./utils";
1616
import { TelemetryAttribute, TelemetryAttributes } from "./telemetry/attributes";
17+
import { TelemetryConfiguration } from "./telemetry/configuration";
1718
import { 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+
*/
236247
export 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

Comments
 (0)