diff --git a/packages/integration-sdk-http-client/src/__tests__/index.test.ts b/packages/integration-sdk-http-client/src/__tests__/index.test.ts index 60997ca7b..5add46f67 100644 --- a/packages/integration-sdk-http-client/src/__tests__/index.test.ts +++ b/packages/integration-sdk-http-client/src/__tests__/index.test.ts @@ -6,6 +6,7 @@ import https from 'node:https'; jest.mock('node-fetch'); import fetch from 'node-fetch'; +import { redactUrlQueryParametersKeys } from '../errors'; const { Response, Headers } = jest.requireActual('node-fetch'); @@ -565,5 +566,28 @@ describe('APIClient', () => { }); }); + describe('redactUrlQueryParametersKeys', () => { + it('should redact the query parameters from the URL', () => { + const url = 'https://api.example.com?key1=value1&key2=value2'; + const keys = ['key1']; + const result = redactUrlQueryParametersKeys(url, keys); + expect(result).toBe('https://api.example.com?key2=value2'); + }); + + it('should redact the query parameters from the URL', () => { + const url = 'https://api.example.com?key1=value1&key2=value2'; + const keys = ['key1', 'key2']; + const result = redactUrlQueryParametersKeys(url, keys); + expect(result).toBe('https://api.example.com'); + }); + + it('should not redact the query parameters from the URL if the keys are not provided', () => { + const url = 'https://api.example.com?key1=value1&key2=value2'; + const keys = []; + const result = redactUrlQueryParametersKeys(url, keys); + expect(result).toBe('https://api.example.com?key1=value1&key2=value2'); + }); + }); + // Add more test cases for other methods as needed }); diff --git a/packages/integration-sdk-http-client/src/client.ts b/packages/integration-sdk-http-client/src/client.ts index dffee8bd4..768a0d439 100644 --- a/packages/integration-sdk-http-client/src/client.ts +++ b/packages/integration-sdk-http-client/src/client.ts @@ -61,6 +61,7 @@ export abstract class BaseAPIClient { protected tokenBucketInitialConfig?: TokenBucketOptions | undefined; protected endpointTokenBuckets: Record = {}; protected refreshAuth?: RefreshAuthOptions; + protected redactUrlQueryParamsKeys?: string[]; protected readonly integrationConfig: IntegrationInstanceConfig; protected readonly agent?: Agent; @@ -97,6 +98,7 @@ export abstract class BaseAPIClient { * if not provided, token bucket will not be enabled * @param {boolean} [config.refreshAuth.enabled] - If true, the auth headers will be refreshed on 401 and 403 errors * @param {number[]} [config.refreshAuth.errorCodes] - If provided, the auth headers will be refreshed on the provided error codes + * @param {string[]} [config.redactUrlQueryParamsKeys] - If provided, the query parameters will be redacted from the URL * * @example * ```typescript @@ -136,6 +138,7 @@ export abstract class BaseAPIClient { }); } this.refreshAuth = config.refreshAuth; + this.redactUrlQueryParamsKeys = config.redactUrlQueryParamsKeys; } protected withBaseUrl(endpoint: string): string { @@ -333,6 +336,7 @@ export abstract class BaseAPIClient { response, logger: this.logger, logErrorBody: this.logErrorBody, + redactUrlQueryParamsKeys: this.redactUrlQueryParamsKeys, }; if (isRetryableRequest(response.status)) { error = await retryableRequestError(requestErrorParams); diff --git a/packages/integration-sdk-http-client/src/errors.ts b/packages/integration-sdk-http-client/src/errors.ts index 95beba9e9..a35d0b8c1 100644 --- a/packages/integration-sdk-http-client/src/errors.ts +++ b/packages/integration-sdk-http-client/src/errors.ts @@ -17,6 +17,7 @@ interface RequestErrorParams { response: Response; logger: IntegrationLogger; logErrorBody: boolean; + redactUrlQueryParamsKeys?: string[]; } export class RetryableIntegrationProviderApiError extends IntegrationProviderAPIError { @@ -101,11 +102,32 @@ export async function retryableRequestError({ }); } +/** + * Redact the query parameters from the URL + * @param url - The URL to redact + * @param keys - The keys to redact + * @returns The URL with the query parameters redacted + */ +export function redactUrlQueryParametersKeys( + url: string, + keys: string[], +): string { + const [base, query] = url.split('?'); + if (!query) return url; + + const params = new URLSearchParams(query); + keys.forEach((key) => params.delete(key)); + + const qs = params.toString(); + return qs ? `${base}?${qs}` : base; +} + export async function fatalRequestError({ endpoint, response, logger, logErrorBody, + redactUrlQueryParamsKeys, }: RequestErrorParams): Promise { const errorBody = await getErrorBody(response, logger, logErrorBody); const headers = headersToRecord(response.headers); @@ -115,7 +137,9 @@ export async function fatalRequestError({ typeof errorBody === 'string' ? new ResponseBodyError(errorBody, headers) : undefined, - endpoint, + endpoint: redactUrlQueryParamsKeys?.length + ? redactUrlQueryParametersKeys(endpoint, redactUrlQueryParamsKeys) + : endpoint, status: response.status, statusText: response.statusText, }; diff --git a/packages/integration-sdk-http-client/src/types.ts b/packages/integration-sdk-http-client/src/types.ts index d11fb65de..e1f68acf7 100644 --- a/packages/integration-sdk-http-client/src/types.ts +++ b/packages/integration-sdk-http-client/src/types.ts @@ -66,6 +66,7 @@ export interface ClientConfig { rateLimitThrottling?: RateLimitThrottlingOptions; tokenBucket?: TokenBucketOptions; refreshAuth?: RefreshAuthOptions; + redactUrlQueryParamsKeys?: string[]; } export interface IterateCallbackResult {