From b7dcec2b2e2116bd8d263fb469a80963af4d8aae Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Mon, 10 Aug 2026 15:41:31 +0530 Subject: [PATCH 01/10] add telemetry header --- README.md | 14 ++ src/RequestWrapper.ts | 38 +++- src/createChargebee.ts | 5 + src/telemetry/index.ts | 24 +++ src/telemetry/sdkTelemetryEmitter.ts | 221 +++++++++++++++++++++ src/telemetry/sdkTelemetryHeader.ts | 20 ++ src/telemetry/sdkTelemetryHeaderBuilder.ts | 178 +++++++++++++++++ src/telemetry/sdkTelemetrySnapshot.ts | 20 ++ src/telemetry/sdkTelemetryState.ts | 30 +++ src/types.d.ts | 7 +- test/requestWrapper.test.ts | 110 +++++++++- types/index.d.ts | 11 +- 12 files changed, 673 insertions(+), 5 deletions(-) create mode 100644 src/telemetry/sdkTelemetryEmitter.ts create mode 100644 src/telemetry/sdkTelemetryHeader.ts create mode 100644 src/telemetry/sdkTelemetryHeaderBuilder.ts create mode 100644 src/telemetry/sdkTelemetrySnapshot.ts create mode 100644 src/telemetry/sdkTelemetryState.ts diff --git a/README.md b/README.md index cb7b7a5..b0991eb 100644 --- a/README.md +++ b/README.md @@ -602,6 +602,20 @@ const chargebee = new Chargebee({ These examples demonstrate how to implement and inject custom clients using `axios` and `ky`, respectively. +### SDK telemetry + +By default, the library sends anonymous usage telemetry to Chargebee. This helps us improve the SDK and API. + +You can disable this behavior if you prefer: + +```javascript +const chargebee = new Chargebee({ + site: 'your-site', + apiKey: 'your-api-key', + sdkTelemetryEnabled: false, +}); +``` + ### Telemetry (OpenTelemetry) Optional. Pass a `telemetryAdapter` when you want Chargebee API calls traced in your observability stack (Datadog, Splunk, Honeycomb, Jaeger, etc.). The SDK ships a ready-to-use OpenTelemetry adapter, so for most setups you only need to add `@opentelemetry/api` and wire the adapter on the client. diff --git a/src/RequestWrapper.ts b/src/RequestWrapper.ts index 988d109..d06364e 100644 --- a/src/RequestWrapper.ts +++ b/src/RequestWrapper.ts @@ -22,6 +22,9 @@ import { extractHttpStatusCode, extractRequestTelemetryError, resolveChargebeeApiVersion, + attachSdkTelemetryHeader, + recordSdkTelemetryFailure, + recordSdkTelemetrySuccess, type TelemetryAdapter, } from './telemetry/index.js'; import { handleResponse } from './coreCommon.js'; @@ -115,6 +118,15 @@ export class RequestWrapper { if (this.envArg.telemetryAdapter !== undefined) { _env.telemetryAdapter = this.envArg.telemetryAdapter; } + if (this.envArg.sdkTelemetryState !== undefined) { + _env.sdkTelemetryState = this.envArg.sdkTelemetryState; + } + if (this.envArg.sdkTelemetryEnabled !== undefined) { + _env.sdkTelemetryEnabled = this.envArg.sdkTelemetryEnabled; + } + if (this.envArg.httpClientIsCustom !== undefined) { + _env.httpClientIsCustom = this.envArg.httpClientIsCustom; + } const env = _env as EnvType; @@ -233,6 +245,7 @@ export class RequestWrapper { ...this.httpHeaders, ...telemetryHeaders, }; + attachSdkTelemetryHeader(env, requestHeaders); const contentType = this.apiCall.isJsonRequest ? 'application/json;charset=UTF-8' @@ -385,10 +398,33 @@ export class RequestWrapper { } }; - const promise = + const callMetadata = { + resource: this.apiCall.resource, + operation: this.apiCall.methodName, + }; + + const executeCall = () => telemetryAdapter !== undefined ? runWithTelemetry(telemetryAdapter) : withRetry(0, requestStartTime); + + const promise = executeCall() + .then((result) => { + recordSdkTelemetrySuccess( + env, + callMetadata, + requestStartTime, + typeof result?.httpStatusCode === 'number' + ? result.httpStatusCode + : 200, + result?.headers, + ); + return result; + }) + .catch((err) => { + recordSdkTelemetryFailure(env, callMetadata, requestStartTime, err); + throw err; + }); return callbackifyPromise(promise); } diff --git a/src/createChargebee.ts b/src/createChargebee.ts index f6b9bd7..09cde5b 100644 --- a/src/createChargebee.ts +++ b/src/createChargebee.ts @@ -16,6 +16,7 @@ import { type WebhookHandlerOptions, createDefaultHandler, } from './resources/webhook/handler.js'; +import { SdkTelemetryState } from './telemetry/index.js'; export const CreateChargebee = (httpClient: HttpClientInterface) => { const Chargebee = function (this: ChargebeeType, conf: Config) { @@ -23,12 +24,16 @@ export const CreateChargebee = (httpClient: HttpClientInterface) => { const { telemetryAdapter, httpClient: configHttpClient, + sdkTelemetryEnabled, ...confToMerge } = conf; extend(true, this._env, confToMerge); // @ts-ignore this._env.httpClient = configHttpClient != null ? configHttpClient : httpClient; + this._env.sdkTelemetryState = new SdkTelemetryState(); + this._env.sdkTelemetryEnabled = sdkTelemetryEnabled !== false; + this._env.httpClientIsCustom = configHttpClient != null; if (telemetryAdapter !== undefined) { this._env.telemetryAdapter = telemetryAdapter; } diff --git a/src/telemetry/index.ts b/src/telemetry/index.ts index cd2d882..d23832c 100644 --- a/src/telemetry/index.ts +++ b/src/telemetry/index.ts @@ -34,3 +34,27 @@ export { extractRequestTelemetryError, resolveChargebeeApiVersion, } from './TelemetryAdapter.js'; + +export { + SDK_TELEMETRY_FT_CUSTOM_TRANSPORT, + SDK_TELEMETRY_FT_RETRY_CONFIG, + SDK_TELEMETRY_FT_TELEMETRY_ADAPTER, + SDK_TELEMETRY_HEADER_NAME, + SDK_TELEMETRY_MAX_HEADER_BYTES, + SDK_TELEMETRY_REQUEST_ID_HEADER, + SDK_TELEMETRY_RUNTIME, +} from './sdkTelemetryHeader.js'; + +export type { SdkTelemetrySnapshot } from './sdkTelemetrySnapshot.js'; +export { SdkTelemetryState } from './sdkTelemetryState.js'; +export { + buildSdkTelemetryHeader, + escapeSfString, +} from './sdkTelemetryHeaderBuilder.js'; +export { + attachSdkTelemetryHeader, + recordSdkTelemetryFailure, + recordSdkTelemetrySuccess, + type SdkTelemetryCallMetadata, + type SdkTelemetryEnv, +} from './sdkTelemetryEmitter.js'; diff --git a/src/telemetry/sdkTelemetryEmitter.ts b/src/telemetry/sdkTelemetryEmitter.ts new file mode 100644 index 0000000..3029521 --- /dev/null +++ b/src/telemetry/sdkTelemetryEmitter.ts @@ -0,0 +1,221 @@ +/* + * This file is auto-generated by Chargebee. + * For more information on how to make changes to this file, please see the README. + * Reach out to dx@chargebee.com for any questions. + * Copyright 2026 Chargebee Inc. + */ + +import { CHARGEBEE_SDK_NAME } from './types.js'; +import { + extractHttpStatusCode, + type TelemetryAdapter, +} from './TelemetryAdapter.js'; +import { buildSdkTelemetryHeader } from './sdkTelemetryHeaderBuilder.js'; +import { + SDK_TELEMETRY_FT_CUSTOM_TRANSPORT, + SDK_TELEMETRY_FT_RETRY_CONFIG, + SDK_TELEMETRY_FT_TELEMETRY_ADAPTER, + SDK_TELEMETRY_HEADER_NAME, + SDK_TELEMETRY_REQUEST_ID_HEADER, +} from './sdkTelemetryHeader.js'; +import type { SdkTelemetrySnapshot } from './sdkTelemetrySnapshot.js'; +import type { SdkTelemetryState } from './sdkTelemetryState.js'; + +export type SdkTelemetryEnv = { + sdkTelemetryEnabled?: boolean; + sdkTelemetryState?: SdkTelemetryState; + clientVersion: string; + telemetryAdapter?: TelemetryAdapter; + httpClientIsCustom?: boolean; + retryConfig?: { enabled?: boolean }; +}; + +export type SdkTelemetryCallMetadata = { + resource: string; + operation: string; +}; + +export type RequestHeadersForSdkTelemetry = Record; + +/** + * Emits the anonymous SDK telemetry request header, independently of any customer telemetry + * adapter. + * + * Uses an N+1 scheme: the header sent with a call describes the previous completed call on the + * same client, so the first call of a client never carries the header. Every failure path is + * swallowed and logged at WARNING: telemetry must never fail an API call. + */ +export function attachSdkTelemetryHeader( + env: SdkTelemetryEnv, + headers: RequestHeadersForSdkTelemetry, +): void { + if (env.sdkTelemetryEnabled === false) { + return; + } + + try { + const previousCall = env.sdkTelemetryState?.lastCall(); + if (!previousCall) { + return; + } + const headerValue = buildSdkTelemetryHeader(previousCall); + if (!headerValue) { + return; + } + headers[SDK_TELEMETRY_HEADER_NAME] = headerValue; + } catch (err) { + logSuppressed('attach header', err); + } +} + +export function recordSdkTelemetrySuccess( + env: SdkTelemetryEnv, + call: SdkTelemetryCallMetadata, + startTimeMs: number, + httpStatus: number | undefined, + responseHeaders: Record | undefined, +): void { + if (!hasTelemetryMetadata(call)) { + return; + } + + try { + record( + env, + buildSnapshot( + env, + call, + startTimeMs, + httpStatus, + undefined, + extractRequestId(responseHeaders), + ), + ); + } catch (err) { + logSuppressed('record success', err); + } +} + +export function recordSdkTelemetryFailure( + env: SdkTelemetryEnv, + call: SdkTelemetryCallMetadata, + startTimeMs: number, + callError: unknown, +): void { + if (!hasTelemetryMetadata(call)) { + return; + } + + try { + const httpStatus = extractHttpStatusCode(callError); + const errorObj = + callError != null && typeof callError === 'object' + ? (callError as Record) + : undefined; + const errorCode = + typeof errorObj?.api_error_code === 'string' + ? errorObj.api_error_code + : undefined; + const responseHeaders = + errorObj?.headers != null && typeof errorObj.headers === 'object' + ? (errorObj.headers as Record) + : undefined; + + record( + env, + buildSnapshot( + env, + call, + startTimeMs, + httpStatus, + errorCode, + extractRequestId(responseHeaders), + ), + ); + } catch (err) { + logSuppressed('record failure', err); + } +} + +function record(env: SdkTelemetryEnv, snapshot: SdkTelemetrySnapshot): void { + env.sdkTelemetryState?.record(snapshot); +} + +function buildSnapshot( + env: SdkTelemetryEnv, + call: SdkTelemetryCallMetadata, + startTimeMs: number, + httpStatus: number | undefined, + errorCode: string | undefined, + requestId: string | undefined, +): SdkTelemetrySnapshot { + return { + sdkName: CHARGEBEE_SDK_NAME, + sdkVersion: env.clientVersion, + resource: call.resource, + operation: call.operation, + startTimeEpochSeconds: Math.floor(startTimeMs / 1000), + timeMs: elapsedMs(startTimeMs), + httpStatus, + errorCode, + requestId, + featureTokens: resolveFeatureTokens(env), + }; +} + +function resolveFeatureTokens(env: SdkTelemetryEnv): string[] { + const features: string[] = []; + if (env.telemetryAdapter !== undefined) { + features.push(SDK_TELEMETRY_FT_TELEMETRY_ADAPTER); + } + if (env.httpClientIsCustom) { + features.push(SDK_TELEMETRY_FT_CUSTOM_TRANSPORT); + } + if (isRetryConfigActive(env)) { + features.push(SDK_TELEMETRY_FT_RETRY_CONFIG); + } + return features; +} + +function isRetryConfigActive(env: SdkTelemetryEnv): boolean { + return env.retryConfig?.enabled === true; +} + +function extractRequestId( + headers: Record | undefined, +): string | undefined { + if (!headers) { + return undefined; + } + const value = headers[SDK_TELEMETRY_REQUEST_ID_HEADER]; + if (typeof value === 'string') { + return value; + } + if (Array.isArray(value) && value.length > 0) { + return String(value[0]); + } + if (typeof value === 'number') { + return String(value); + } + return undefined; +} + +function hasTelemetryMetadata(call: SdkTelemetryCallMetadata): boolean { + return isNotBlank(call.resource) && isNotBlank(call.operation); +} + +function elapsedMs(startTimeMs: number): number { + return Math.max(0, Date.now() - startTimeMs); +} + +function isNotBlank(value: string | undefined): boolean { + return value != null && value.trim().length > 0; +} + +function logSuppressed(step: string, err: unknown): void { + const message = err instanceof Error ? err.message : String(err); + console.warn( + `SDK telemetry could not ${step} (${message}); API call unaffected.`, + err, + ); +} diff --git a/src/telemetry/sdkTelemetryHeader.ts b/src/telemetry/sdkTelemetryHeader.ts new file mode 100644 index 0000000..d5ddb3c --- /dev/null +++ b/src/telemetry/sdkTelemetryHeader.ts @@ -0,0 +1,20 @@ +/* + * This file is auto-generated by Chargebee. + * For more information on how to make changes to this file, please see the README. + * Reach out to dx@chargebee.com for any questions. + * Copyright 2026 Chargebee Inc. + */ + +/** Constants for the anonymous SDK telemetry request header. */ +export const SDK_TELEMETRY_HEADER_NAME = 'x-chargebee-sdk-telemetry'; + +/** Server drops larger values, so the SDK omits the header rather than sending a truncated one. */ +export const SDK_TELEMETRY_MAX_HEADER_BYTES = 4096; + +export const SDK_TELEMETRY_REQUEST_ID_HEADER = 'chargebee-request-id'; +export const SDK_TELEMETRY_RUNTIME = 'nodejs'; +export const SDK_TELEMETRY_SEGMENT = 'sdk'; + +export const SDK_TELEMETRY_FT_TELEMETRY_ADAPTER = 'ft-telemetry_adapter'; +export const SDK_TELEMETRY_FT_CUSTOM_TRANSPORT = 'ft-custom_transport'; +export const SDK_TELEMETRY_FT_RETRY_CONFIG = 'ft-retry_config'; diff --git a/src/telemetry/sdkTelemetryHeaderBuilder.ts b/src/telemetry/sdkTelemetryHeaderBuilder.ts new file mode 100644 index 0000000..a48ddc2 --- /dev/null +++ b/src/telemetry/sdkTelemetryHeaderBuilder.ts @@ -0,0 +1,178 @@ +/* + * This file is auto-generated by Chargebee. + * For more information on how to make changes to this file, please see the README. + * Reach out to dx@chargebee.com for any questions. + * Copyright 2026 Chargebee Inc. + */ + +import type { SdkTelemetrySnapshot } from './sdkTelemetrySnapshot.js'; +import { + SDK_TELEMETRY_MAX_HEADER_BYTES, + SDK_TELEMETRY_RUNTIME, + SDK_TELEMETRY_SEGMENT, +} from './sdkTelemetryHeader.js'; + +/** Builds RFC 9651 sf-list values for {@link SDK_TELEMETRY_HEADER_NAME}. */ +export function buildSdkTelemetryHeader( + snapshot: SdkTelemetrySnapshot | undefined, +): string | undefined { + if (!snapshot) { + return undefined; + } + + const segmentParts: string[] = [SDK_TELEMETRY_SEGMENT]; + const segment = { + appendToken(key: string, value: string | undefined) { + appendTokenParam(segmentParts, key, value); + }, + appendBare(key: string, value: string | undefined) { + appendBareParam(segmentParts, key, value); + }, + appendString(key: string, value: string | undefined) { + appendStringParam(segmentParts, key, value); + }, + appendInteger(key: string, value: number) { + appendIntegerParam(segmentParts, key, value); + }, + appendDate(key: string, epochSeconds: number) { + appendDateParam(segmentParts, key, epochSeconds); + }, + }; + + segment.appendToken('name', snapshot.sdkName); + segment.appendBare('version', snapshot.sdkVersion); + segment.appendToken('runtime', SDK_TELEMETRY_RUNTIME); + segment.appendToken('resource', snapshot.resource); + segment.appendToken('operation', snapshot.operation); + if (snapshot.startTimeEpochSeconds > 0) { + segment.appendDate('start_time', snapshot.startTimeEpochSeconds); + } + segment.appendInteger('time_ms', snapshot.timeMs); + if (snapshot.httpStatus != null) { + segment.appendInteger('http_status', snapshot.httpStatus); + } + if (isNotBlank(snapshot.errorCode)) { + segment.appendString('error_code', snapshot.errorCode); + } + if (isNotBlank(snapshot.requestId)) { + segment.appendString('request_id', snapshot.requestId); + } + + const items = [segmentParts.join('')]; + for (const featureToken of snapshot.featureTokens) { + if (isNotBlank(featureToken)) { + items.push(featureToken); + } + } + + const headerValue = items.join(', '); + if ( + new TextEncoder().encode(headerValue).length > + SDK_TELEMETRY_MAX_HEADER_BYTES + ) { + return undefined; + } + return headerValue; +} + +export function escapeSfString(value: string): string { + let escaped = '"'; + for (const ch of value) { + if (ch === '\\' || ch === '"') { + escaped += '\\'; + } + escaped += ch; + } + escaped += '"'; + return escaped; +} + +function appendTokenParam( + parts: string[], + key: string, + value: string | undefined, +): void { + if (!isNotBlank(value)) { + return; + } + const trimmed = value!.trim(); + parts.push(`;${key}=`); + parts.push(isSfToken(trimmed) ? trimmed : escapeSfString(trimmed)); +} + +function isSfToken(value: string): boolean { + const first = value.charAt(0); + if (!isAsciiLetter(first) && first !== '*') { + return false; + } + for (const ch of value) { + const allowed = + isAsciiLetter(ch) || + (ch >= '0' && ch <= '9') || + "!#$%&'*+-.^_`|~:/".includes(ch); + if (!allowed) { + return false; + } + } + return true; +} + +function isAsciiLetter(ch: string): boolean { + return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); +} + +function appendBareParam( + parts: string[], + key: string, + value: string | undefined, +): void { + if (!isNotBlank(value)) { + return; + } + const trimmed = value!.trim(); + parts.push(`;${key}=`); + parts.push(isBareSafe(trimmed) ? trimmed : escapeSfString(trimmed)); +} + +function isBareSafe(value: string): boolean { + for (const ch of value) { + if ( + ch === '"' || + ch === '\\' || + ch === ',' || + ch === ';' || + ch === '=' || + /\s/.test(ch) + ) { + return false; + } + } + return value.length > 0; +} + +function appendStringParam( + parts: string[], + key: string, + value: string | undefined, +): void { + if (!isNotBlank(value)) { + return; + } + parts.push(`;${key}=${escapeSfString(value!.trim())}`); +} + +function appendIntegerParam(parts: string[], key: string, value: number): void { + parts.push(`;${key}=${value}`); +} + +function appendDateParam( + parts: string[], + key: string, + epochSeconds: number, +): void { + parts.push(`;${key}=@${epochSeconds}`); +} + +function isNotBlank(value: string | undefined): boolean { + return value != null && value.trim().length > 0; +} diff --git a/src/telemetry/sdkTelemetrySnapshot.ts b/src/telemetry/sdkTelemetrySnapshot.ts new file mode 100644 index 0000000..ab4cfd0 --- /dev/null +++ b/src/telemetry/sdkTelemetrySnapshot.ts @@ -0,0 +1,20 @@ +/* + * This file is auto-generated by Chargebee. + * For more information on how to make changes to this file, please see the README. + * Reach out to dx@chargebee.com for any questions. + * Copyright 2026 Chargebee Inc. + */ + +/** Immutable snapshot of a completed SDK API call for N+1 header emission. */ +export type SdkTelemetrySnapshot = { + sdkName: string; + sdkVersion: string; + resource: string; + operation: string; + startTimeEpochSeconds: number; + timeMs: number; + httpStatus?: number; + errorCode?: string; + requestId?: string; + featureTokens: string[]; +}; diff --git a/src/telemetry/sdkTelemetryState.ts b/src/telemetry/sdkTelemetryState.ts new file mode 100644 index 0000000..7d62ce8 --- /dev/null +++ b/src/telemetry/sdkTelemetryState.ts @@ -0,0 +1,30 @@ +/* + * This file is auto-generated by Chargebee. + * For more information on how to make changes to this file, please see the README. + * Reach out to dx@chargebee.com for any questions. + * Copyright 2026 Chargebee Inc. + */ + +import type { SdkTelemetrySnapshot } from './sdkTelemetrySnapshot.js'; + +/** + * Per-client holder for the last completed call, used by the N+1 SDK telemetry header. + * + * Internal SDK type: applications must not depend on it. It is public only so that the + * Chargebee client can own one instance. + */ +export class SdkTelemetryState { + private lastCallSnapshot: SdkTelemetrySnapshot | undefined; + + lastCall(): SdkTelemetrySnapshot | undefined { + return this.lastCallSnapshot; + } + + record(snapshot: SdkTelemetrySnapshot): void { + this.lastCallSnapshot = snapshot; + } + + clear(): void { + this.lastCallSnapshot = undefined; + } +} diff --git a/src/types.d.ts b/src/types.d.ts index 07a3939..a91f789 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,4 +1,4 @@ -import type { TelemetryAdapter } from './telemetry/index.js'; +import type { TelemetryAdapter, SdkTelemetryState } from './telemetry/index.js'; interface HttpClientInterface { makeApiRequest: (props: Request, timeout: number) => Promise; @@ -20,6 +20,9 @@ export type EnvType = { enableDebugLogs?: boolean; userAgentSuffix?: string; telemetryAdapter?: TelemetryAdapter; + sdkTelemetryEnabled?: boolean; + sdkTelemetryState?: SdkTelemetryState; + httpClientIsCustom?: boolean; /** When true, request parameters are validated against Zod schemas before each HTTP call (where a schema exists). */ enableValidation?: boolean; }; @@ -47,6 +50,8 @@ export type Config = { userAgentSuffix?: string; httpClient?: HttpClientInterface; telemetryAdapter?: TelemetryAdapter; + /** When false, the SDK does not send the anonymous x-chargebee-sdk-telemetry header. Default true. */ + sdkTelemetryEnabled?: boolean; /** When true, request parameters are validated against Zod schemas before each HTTP call (where a schema exists). */ enableValidation?: boolean; }; diff --git a/test/requestWrapper.test.ts b/test/requestWrapper.test.ts index 023e465..f0d9cc7 100644 --- a/test/requestWrapper.test.ts +++ b/test/requestWrapper.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { CreateChargebee } from '../src/createChargebee.js'; import { Environment } from '../src/environment.js'; -import { TelemetryAttributeKeys } from '../src/chargebee.esm.js'; +import { TelemetryAttributeKeys, SDK_TELEMETRY_HEADER_NAME } from '../src/chargebee.esm.js'; let capturedRequests: Request[] = []; let responseFactory: ((attempt: number) => Response) | null = null; @@ -591,6 +591,114 @@ describe('RequestWrapper - telemetry adapter', () => { }); }); +describe('RequestWrapper - SDK telemetry header', () => { + it('should omit header on first call and attach N+1 header on second call', async () => { + responseFactory = () => + new Response(JSON.stringify({ list: [], next_offset: null }), { + status: 200, + headers: { + 'Content-Type': 'application/json', + 'chargebee-request-id': 'req_abc123', + }, + }); + + const chargebee = createChargebee({ + retryConfig: { enabled: false }, + }); + await chargebee.customer.list({ limit: 1 }); + await chargebee.customer.list({ limit: 1 }); + + expect(capturedRequests.length).to.equal(2); + expect( + capturedRequests[0].headers.get(SDK_TELEMETRY_HEADER_NAME), + ).to.equal(null); + + const header = capturedRequests[1].headers.get(SDK_TELEMETRY_HEADER_NAME); + expect(header).to.be.a('string'); + expect(header).to.include('resource=customer;operation=list'); + expect(header).to.include('start_time=@'); + expect(header).to.include('http_status=200'); + expect(header).to.include('request_id="req_abc123"'); + }); + + it('should not attach header when sdk telemetry is disabled', async () => { + const chargebee = createChargebee({ + sdkTelemetryEnabled: false, + retryConfig: { enabled: false }, + }); + await chargebee.customer.list({ limit: 1 }); + await chargebee.customer.list({ limit: 1 }); + + expect(capturedRequests.length).to.equal(2); + expect( + capturedRequests[0].headers.get(SDK_TELEMETRY_HEADER_NAME), + ).to.equal(null); + expect( + capturedRequests[1].headers.get(SDK_TELEMETRY_HEADER_NAME), + ).to.equal(null); + }); + + it('should record failure details for the next header', async () => { + responseFactory = (attempt) => { + if (attempt === 0) { + return new Response( + JSON.stringify({ + message: 'Not found', + type: 'invalid_request', + api_error_code: 'resource_not_found', + }), + { + status: 404, + headers: { + 'Content-Type': 'application/json', + 'chargebee-request-id': 'req_fail', + }, + }, + ); + } + return new Response(JSON.stringify({ list: [], next_offset: null }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }; + + const chargebee = createChargebee({ + retryConfig: { enabled: false }, + }); + + try { + await chargebee.customer.retrieve('cust_missing'); + } catch (_err) { + // expected + } + await chargebee.customer.list({ limit: 1 }); + + const header = capturedRequests[1].headers.get(SDK_TELEMETRY_HEADER_NAME); + expect(header).to.include('operation=retrieve'); + expect(header).to.include('http_status=404'); + expect(header).to.include('error_code="resource_not_found"'); + }); + + it('should emit feature tokens for retry config and telemetry adapter', async () => { + const chargebee = createChargebee({ + httpClient: mockHttpClient, + retryConfig: { enabled: true, maxRetries: 1, delayMs: 0, retryOn: [500] }, + telemetryAdapter: { + onRequestStart: () => ({}), + onRequestEnd: () => {}, + }, + }); + + await chargebee.customer.list({ limit: 1 }); + await chargebee.customer.list({ limit: 1 }); + + const header = capturedRequests[1].headers.get(SDK_TELEMETRY_HEADER_NAME); + expect(header).to.include('ft-retry_config'); + expect(header).to.include('ft-telemetry_adapter'); + expect(header).to.include('ft-custom_transport'); + }); +}); + describe('Chargebee telemetry exports', () => { it('should export TelemetryAttributeKeys at runtime', () => { expect(TelemetryAttributeKeys.URL_FULL).to.equal('url.full'); diff --git a/types/index.d.ts b/types/index.d.ts index f4023de..6d532d6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -185,14 +185,17 @@ declare module 'chargebee' { * @telemetryAdapter optional telemetry adapter for observability (e.g. OpenTelemetry) */ telemetryAdapter?: TelemetryAdapter; + + /** + * @sdkTelemetryEnabled when false, the SDK does not send the anonymous x-chargebee-sdk-telemetry header. Default true. + */ + sdkTelemetryEnabled?: boolean; }; export interface HttpClientInterface { makeApiRequest: (request: Request, timeout: number) => Promise; } - export type RequestTelemetryHandle = unknown; - export const TelemetryAttributeKeys: { readonly URL_FULL: 'url.full'; readonly HTTP_REQUEST_METHOD: 'http.request.method'; @@ -210,6 +213,10 @@ declare module 'chargebee' { readonly CHARGEBEE_ERROR_PARAM: 'chargebee.error.param'; }; + export const SDK_TELEMETRY_HEADER_NAME: 'x-chargebee-sdk-telemetry'; + + export type RequestTelemetryHandle = unknown; + export type RequestTelemetryContext = { spanName: string; resource: string; From edbf46faff7669aaf5fe965dca002a39c701a95d Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Mon, 10 Aug 2026 15:49:05 +0530 Subject: [PATCH 02/10] export type --- src/chargebee.cjs.ts | 3 ++- src/chargebee.esm.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/chargebee.cjs.ts b/src/chargebee.cjs.ts index 79684c0..e728875 100644 --- a/src/chargebee.cjs.ts +++ b/src/chargebee.cjs.ts @@ -10,7 +10,7 @@ import { } from './resources/webhook/handler.js'; import { basicAuthValidator } from './resources/webhook/auth.js'; import { ChargebeeZodValidationError } from './chargebeeZodValidationError.js'; -import { TelemetryAttributeKeys } from './telemetry/index.js'; +import { TelemetryAttributeKeys, SDK_TELEMETRY_HEADER_NAME } from './telemetry/index.js'; const httpClient = new FetchHttpClient(); const Chargebee = CreateChargebee(httpClient); @@ -29,6 +29,7 @@ module.exports.WebhookAuthenticationError = WebhookAuthenticationError; module.exports.WebhookPayloadValidationError = WebhookPayloadValidationError; module.exports.WebhookPayloadParseError = WebhookPayloadParseError; module.exports.TelemetryAttributeKeys = TelemetryAttributeKeys; +module.exports.SDK_TELEMETRY_HEADER_NAME = SDK_TELEMETRY_HEADER_NAME; // Export validation error class module.exports.ChargebeeZodValidationError = ChargebeeZodValidationError; diff --git a/src/chargebee.esm.ts b/src/chargebee.esm.ts index 4f30d5c..72333b2 100644 --- a/src/chargebee.esm.ts +++ b/src/chargebee.esm.ts @@ -18,7 +18,7 @@ export { WebhookPayloadValidationError, WebhookPayloadParseError, } from './resources/webhook/handler.js'; -export { TelemetryAttributeKeys } from './telemetry/index.js'; +export { TelemetryAttributeKeys, SDK_TELEMETRY_HEADER_NAME } from './telemetry/index.js'; // Export validation error class export { ChargebeeZodValidationError } from './chargebeeZodValidationError.js'; From 8a92dea0791c51e6755dcf91134bb07111cd3641 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Mon, 10 Aug 2026 16:00:10 +0530 Subject: [PATCH 03/10] append prefer header --- src/telemetry/sdkTelemetryHeaderBuilder.ts | 74 +++++++++++++++++----- test/sdkTelemetryHeaderBuilder.test.ts | 58 +++++++++++++++++ 2 files changed, 115 insertions(+), 17 deletions(-) create mode 100644 test/sdkTelemetryHeaderBuilder.test.ts diff --git a/src/telemetry/sdkTelemetryHeaderBuilder.ts b/src/telemetry/sdkTelemetryHeaderBuilder.ts index a48ddc2..cc0d3b0 100644 --- a/src/telemetry/sdkTelemetryHeaderBuilder.ts +++ b/src/telemetry/sdkTelemetryHeaderBuilder.ts @@ -22,11 +22,11 @@ export function buildSdkTelemetryHeader( const segmentParts: string[] = [SDK_TELEMETRY_SEGMENT]; const segment = { - appendToken(key: string, value: string | undefined) { - appendTokenParam(segmentParts, key, value); + appendToken(key: string, value: string | undefined): boolean { + return appendTokenParam(segmentParts, key, value); }, - appendBare(key: string, value: string | undefined) { - appendBareParam(segmentParts, key, value); + appendBare(key: string, value: string | undefined): boolean { + return appendBareParam(segmentParts, key, value); }, appendString(key: string, value: string | undefined) { appendStringParam(segmentParts, key, value); @@ -39,11 +39,21 @@ export function buildSdkTelemetryHeader( }, }; - segment.appendToken('name', snapshot.sdkName); - segment.appendBare('version', snapshot.sdkVersion); - segment.appendToken('runtime', SDK_TELEMETRY_RUNTIME); - segment.appendToken('resource', snapshot.resource); - segment.appendToken('operation', snapshot.operation); + if (!segment.appendToken('name', snapshot.sdkName)) { + return undefined; + } + if (!segment.appendBare('version', snapshot.sdkVersion)) { + return undefined; + } + if (!segment.appendToken('runtime', SDK_TELEMETRY_RUNTIME)) { + return undefined; + } + if (!segment.appendToken('resource', snapshot.resource)) { + return undefined; + } + if (!segment.appendToken('operation', snapshot.operation)) { + return undefined; + } if (snapshot.startTimeEpochSeconds > 0) { segment.appendDate('start_time', snapshot.startTimeEpochSeconds); } @@ -75,7 +85,10 @@ export function buildSdkTelemetryHeader( return headerValue; } -export function escapeSfString(value: string): string { +export function escapeSfString(value: string): string | undefined { + if (containsInvalidSfStringChar(value)) { + return undefined; + } let escaped = '"'; for (const ch of value) { if (ch === '\\' || ch === '"') { @@ -87,17 +100,32 @@ export function escapeSfString(value: string): string { return escaped; } +function containsInvalidSfStringChar(value: string): boolean { + for (const ch of value) { + const code = ch.charCodeAt(0); + if (code === 0 || code === 10 || code === 13) { + return true; + } + } + return false; +} + function appendTokenParam( parts: string[], key: string, value: string | undefined, -): void { +): boolean { if (!isNotBlank(value)) { - return; + return false; } const trimmed = value!.trim(); + const serialized = isSfToken(trimmed) ? trimmed : escapeSfString(trimmed); + if (serialized == null) { + return false; + } parts.push(`;${key}=`); - parts.push(isSfToken(trimmed) ? trimmed : escapeSfString(trimmed)); + parts.push(serialized); + return true; } function isSfToken(value: string): boolean { @@ -125,13 +153,21 @@ function appendBareParam( parts: string[], key: string, value: string | undefined, -): void { +): boolean { if (!isNotBlank(value)) { - return; + return false; } const trimmed = value!.trim(); + const serialized = + isBareSafe(trimmed) && !containsInvalidSfStringChar(trimmed) + ? trimmed + : escapeSfString(trimmed); + if (serialized == null) { + return false; + } parts.push(`;${key}=`); - parts.push(isBareSafe(trimmed) ? trimmed : escapeSfString(trimmed)); + parts.push(serialized); + return true; } function isBareSafe(value: string): boolean { @@ -158,7 +194,11 @@ function appendStringParam( if (!isNotBlank(value)) { return; } - parts.push(`;${key}=${escapeSfString(value!.trim())}`); + const escaped = escapeSfString(value!.trim()); + if (escaped == null) { + return; + } + parts.push(`;${key}=${escaped}`); } function appendIntegerParam(parts: string[], key: string, value: number): void { diff --git a/test/sdkTelemetryHeaderBuilder.test.ts b/test/sdkTelemetryHeaderBuilder.test.ts new file mode 100644 index 0000000..785d2fc --- /dev/null +++ b/test/sdkTelemetryHeaderBuilder.test.ts @@ -0,0 +1,58 @@ +import { expect } from 'chai'; +import { + buildSdkTelemetryHeader, + escapeSfString, +} from '../src/telemetry/sdkTelemetryHeaderBuilder.js'; +import type { SdkTelemetrySnapshot } from '../src/telemetry/sdkTelemetrySnapshot.js'; + +function baseSnapshot( + overrides: Partial = {}, +): SdkTelemetrySnapshot { + return { + sdkName: 'chargebee-node', + sdkVersion: '3.30.0', + resource: 'customer', + operation: 'list', + startTimeEpochSeconds: 1781280400, + timeMs: 100, + featureTokens: [], + ...overrides, + }; +} + +describe('sdkTelemetryHeaderBuilder', () => { + it('should escape valid sf-string values', () => { + expect(escapeSfString('hello')).to.equal('"hello"'); + expect(escapeSfString('say "hi"')).to.equal('"say \\"hi\\""'); + expect(escapeSfString('path\\to')).to.equal('"path\\\\to"'); + }); + + it('should reject sf-string values containing CR, LF, or NUL', () => { + expect(escapeSfString('bad\rvalue')).to.equal(undefined); + expect(escapeSfString('bad\nvalue')).to.equal(undefined); + expect(escapeSfString('bad\0value')).to.equal(undefined); + }); + + it('should omit invalid error_code while keeping the rest of the header', () => { + const header = buildSdkTelemetryHeader( + baseSnapshot({ + httpStatus: 404, + errorCode: 'resource_not_found\rinjected', + }), + ); + + expect(header).to.be.a('string'); + expect(header).to.include('http_status=404'); + expect(header).to.not.include('error_code='); + }); + + it('should omit the entire header when a required field contains invalid characters', () => { + const header = buildSdkTelemetryHeader( + baseSnapshot({ + sdkVersion: '3.30.0\0', + }), + ); + + expect(header).to.equal(undefined); + }); +}); From a81defd5424f47c2b0aa4e4d04477a3058797272 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Mon, 10 Aug 2026 17:30:00 +0530 Subject: [PATCH 04/10] Valid feature token --- src/telemetry/sdkTelemetryHeaderBuilder.ts | 15 +++++++++-- test/requestWrapper.test.ts | 29 ++++++++++++++++++++++ test/sdkTelemetryHeaderBuilder.test.ts | 20 +++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/telemetry/sdkTelemetryHeaderBuilder.ts b/src/telemetry/sdkTelemetryHeaderBuilder.ts index cc0d3b0..276497a 100644 --- a/src/telemetry/sdkTelemetryHeaderBuilder.ts +++ b/src/telemetry/sdkTelemetryHeaderBuilder.ts @@ -70,8 +70,8 @@ export function buildSdkTelemetryHeader( const items = [segmentParts.join('')]; for (const featureToken of snapshot.featureTokens) { - if (isNotBlank(featureToken)) { - items.push(featureToken); + if (isValidFeatureToken(featureToken)) { + items.push(featureToken!.trim()); } } @@ -213,6 +213,17 @@ function appendDateParam( parts.push(`;${key}=@${epochSeconds}`); } +function isValidFeatureToken(value: string | undefined): boolean { + if (!isNotBlank(value)) { + return false; + } + if (containsInvalidSfStringChar(value!)) { + return false; + } + const trimmed = value!.trim(); + return isSfToken(trimmed); +} + function isNotBlank(value: string | undefined): boolean { return value != null && value.trim().length > 0; } diff --git a/test/requestWrapper.test.ts b/test/requestWrapper.test.ts index f0d9cc7..288d641 100644 --- a/test/requestWrapper.test.ts +++ b/test/requestWrapper.test.ts @@ -697,6 +697,35 @@ describe('RequestWrapper - SDK telemetry header', () => { expect(header).to.include('ft-telemetry_adapter'); expect(header).to.include('ft-custom_transport'); }); + + it('should skip invalid feature tokens without failing the API request', async () => { + const chargebee = createChargebee({ + retryConfig: { enabled: false }, + }); + await chargebee.customer.list({ limit: 1 }); + + const state = (chargebee as any)._env.sdkTelemetryState; + const previousCall = state.lastCall(); + state.record({ + ...previousCall, + featureTokens: [ + 'ft-retry_config', + 'ft-bad\rinjected', + 'ft-telemetry_adapter', + ], + }); + + const result = await chargebee.customer.list({ limit: 1 }); + + expect(result).to.have.property('list'); + expect(capturedRequests.length).to.equal(2); + const header = capturedRequests[1].headers.get(SDK_TELEMETRY_HEADER_NAME); + expect(header).to.be.a('string'); + expect(header).to.include('ft-retry_config'); + expect(header).to.include('ft-telemetry_adapter'); + expect(header).to.not.include('ft-bad'); + expect(header).to.not.match(/\r|\n/); + }); }); describe('Chargebee telemetry exports', () => { diff --git a/test/sdkTelemetryHeaderBuilder.test.ts b/test/sdkTelemetryHeaderBuilder.test.ts index 785d2fc..2b2200f 100644 --- a/test/sdkTelemetryHeaderBuilder.test.ts +++ b/test/sdkTelemetryHeaderBuilder.test.ts @@ -55,4 +55,24 @@ describe('sdkTelemetryHeaderBuilder', () => { expect(header).to.equal(undefined); }); + + it('should skip invalid feature tokens while keeping valid ones', () => { + const header = buildSdkTelemetryHeader( + baseSnapshot({ + featureTokens: [ + 'ft-retry_config', + 'ft-bad\rinjected', + 'ft-telemetry_adapter', + 'ft-bad\0', + 'ft-bad\n', + ], + }), + ); + + expect(header).to.be.a('string'); + expect(header).to.include('ft-retry_config'); + expect(header).to.include('ft-telemetry_adapter'); + expect(header).to.not.include('ft-bad'); + expect(header).to.not.match(/\r|\n/); + }); }); From a6faabd87228f393872f9d7a7f439d7401e4a67c Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Mon, 10 Aug 2026 17:56:54 +0530 Subject: [PATCH 05/10] run prettier --- src/chargebee.cjs.ts | 5 ++++- src/chargebee.esm.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/chargebee.cjs.ts b/src/chargebee.cjs.ts index e728875..49345de 100644 --- a/src/chargebee.cjs.ts +++ b/src/chargebee.cjs.ts @@ -10,7 +10,10 @@ import { } from './resources/webhook/handler.js'; import { basicAuthValidator } from './resources/webhook/auth.js'; import { ChargebeeZodValidationError } from './chargebeeZodValidationError.js'; -import { TelemetryAttributeKeys, SDK_TELEMETRY_HEADER_NAME } from './telemetry/index.js'; +import { + TelemetryAttributeKeys, + SDK_TELEMETRY_HEADER_NAME, +} from './telemetry/index.js'; const httpClient = new FetchHttpClient(); const Chargebee = CreateChargebee(httpClient); diff --git a/src/chargebee.esm.ts b/src/chargebee.esm.ts index 72333b2..7e9f165 100644 --- a/src/chargebee.esm.ts +++ b/src/chargebee.esm.ts @@ -18,7 +18,10 @@ export { WebhookPayloadValidationError, WebhookPayloadParseError, } from './resources/webhook/handler.js'; -export { TelemetryAttributeKeys, SDK_TELEMETRY_HEADER_NAME } from './telemetry/index.js'; +export { + TelemetryAttributeKeys, + SDK_TELEMETRY_HEADER_NAME, +} from './telemetry/index.js'; // Export validation error class export { ChargebeeZodValidationError } from './chargebeeZodValidationError.js'; From 5b993dcfcc41f57535372fed0a8ff1cc0f917e06 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Mon, 10 Aug 2026 18:08:01 +0530 Subject: [PATCH 06/10] update sdk telemetry condition --- src/telemetry/sdkTelemetryEmitter.ts | 4 ++-- test/requestWrapper.test.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/telemetry/sdkTelemetryEmitter.ts b/src/telemetry/sdkTelemetryEmitter.ts index 3029521..49e9763 100644 --- a/src/telemetry/sdkTelemetryEmitter.ts +++ b/src/telemetry/sdkTelemetryEmitter.ts @@ -75,7 +75,7 @@ export function recordSdkTelemetrySuccess( httpStatus: number | undefined, responseHeaders: Record | undefined, ): void { - if (!hasTelemetryMetadata(call)) { + if (env.sdkTelemetryEnabled === false || !hasTelemetryMetadata(call)) { return; } @@ -102,7 +102,7 @@ export function recordSdkTelemetryFailure( startTimeMs: number, callError: unknown, ): void { - if (!hasTelemetryMetadata(call)) { + if (env.sdkTelemetryEnabled === false || !hasTelemetryMetadata(call)) { return; } diff --git a/test/requestWrapper.test.ts b/test/requestWrapper.test.ts index 288d641..e09518d 100644 --- a/test/requestWrapper.test.ts +++ b/test/requestWrapper.test.ts @@ -636,6 +636,9 @@ describe('RequestWrapper - SDK telemetry header', () => { expect( capturedRequests[1].headers.get(SDK_TELEMETRY_HEADER_NAME), ).to.equal(null); + expect((chargebee as any)._env.sdkTelemetryState.lastCall()).to.equal( + undefined, + ); }); it('should record failure details for the next header', async () => { From dc1d48c6b0662bebf853061011e30e5f41443387 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Mon, 10 Aug 2026 18:24:22 +0530 Subject: [PATCH 07/10] add docs --- src/telemetry/sdkTelemetryEmitter.ts | 14 ++++++++++++++ src/telemetry/sdkTelemetryHeader.ts | 10 ++++++++++ src/telemetry/sdkTelemetryHeaderBuilder.ts | 12 ++++++++++++ src/telemetry/sdkTelemetryState.ts | 3 +++ 4 files changed, 39 insertions(+) diff --git a/src/telemetry/sdkTelemetryEmitter.ts b/src/telemetry/sdkTelemetryEmitter.ts index 49e9763..e915ae1 100644 --- a/src/telemetry/sdkTelemetryEmitter.ts +++ b/src/telemetry/sdkTelemetryEmitter.ts @@ -21,6 +21,7 @@ import { import type { SdkTelemetrySnapshot } from './sdkTelemetrySnapshot.js'; import type { SdkTelemetryState } from './sdkTelemetryState.js'; +/** Client env fields needed to emit SDK telemetry. */ export type SdkTelemetryEnv = { sdkTelemetryEnabled?: boolean; sdkTelemetryState?: SdkTelemetryState; @@ -30,11 +31,13 @@ export type SdkTelemetryEnv = { retryConfig?: { enabled?: boolean }; }; +/** Resource/operation metadata for the call being recorded. */ export type SdkTelemetryCallMetadata = { resource: string; operation: string; }; +/** Mutable request-header map used when attaching the telemetry header. */ export type RequestHeadersForSdkTelemetry = Record; /** @@ -68,6 +71,7 @@ export function attachSdkTelemetryHeader( } } +/** Records a successful call for the next N+1 header. */ export function recordSdkTelemetrySuccess( env: SdkTelemetryEnv, call: SdkTelemetryCallMetadata, @@ -96,6 +100,7 @@ export function recordSdkTelemetrySuccess( } } +/** Records a failed call for the next N+1 header. */ export function recordSdkTelemetryFailure( env: SdkTelemetryEnv, call: SdkTelemetryCallMetadata, @@ -137,10 +142,12 @@ export function recordSdkTelemetryFailure( } } +/** Stores {@code snapshot} on the client. */ function record(env: SdkTelemetryEnv, snapshot: SdkTelemetrySnapshot): void { env.sdkTelemetryState?.record(snapshot); } +/** Builds an immutable snapshot of the completed call. */ function buildSnapshot( env: SdkTelemetryEnv, call: SdkTelemetryCallMetadata, @@ -163,6 +170,7 @@ function buildSnapshot( }; } +/** Collects {@code ft-*} tokens for the current client configuration. */ function resolveFeatureTokens(env: SdkTelemetryEnv): string[] { const features: string[] = []; if (env.telemetryAdapter !== undefined) { @@ -177,10 +185,12 @@ function resolveFeatureTokens(env: SdkTelemetryEnv): string[] { return features; } +/** Whether retries are enabled on the client. */ function isRetryConfigActive(env: SdkTelemetryEnv): boolean { return env.retryConfig?.enabled === true; } +/** Reads {@code chargebee-request-id} from response headers, if present. */ function extractRequestId( headers: Record | undefined, ): string | undefined { @@ -200,18 +210,22 @@ function extractRequestId( return undefined; } +/** Whether the call has resource and operation metadata. */ function hasTelemetryMetadata(call: SdkTelemetryCallMetadata): boolean { return isNotBlank(call.resource) && isNotBlank(call.operation); } +/** Elapsed wall time of the call in milliseconds. */ function elapsedMs(startTimeMs: number): number { return Math.max(0, Date.now() - startTimeMs); } +/** Whether {@code value} is non-null and non-blank. */ function isNotBlank(value: string | undefined): boolean { return value != null && value.trim().length > 0; } +/** Logs a suppressed telemetry failure without affecting the API call. */ function logSuppressed(step: string, err: unknown): void { const message = err instanceof Error ? err.message : String(err); console.warn( diff --git a/src/telemetry/sdkTelemetryHeader.ts b/src/telemetry/sdkTelemetryHeader.ts index d5ddb3c..f5c7cc0 100644 --- a/src/telemetry/sdkTelemetryHeader.ts +++ b/src/telemetry/sdkTelemetryHeader.ts @@ -11,10 +11,20 @@ export const SDK_TELEMETRY_HEADER_NAME = 'x-chargebee-sdk-telemetry'; /** Server drops larger values, so the SDK omits the header rather than sending a truncated one. */ export const SDK_TELEMETRY_MAX_HEADER_BYTES = 4096; +/** Response header that carries the Chargebee request id. */ export const SDK_TELEMETRY_REQUEST_ID_HEADER = 'chargebee-request-id'; + +/** Runtime token emitted in the sdk segment. */ export const SDK_TELEMETRY_RUNTIME = 'nodejs'; + +/** First item name in the RFC 9651 sf-list. */ export const SDK_TELEMETRY_SEGMENT = 'sdk'; +/** Feature token: a TelemetryAdapter is configured. */ export const SDK_TELEMETRY_FT_TELEMETRY_ADAPTER = 'ft-telemetry_adapter'; + +/** Feature token: a custom HTTP client is configured. */ export const SDK_TELEMETRY_FT_CUSTOM_TRANSPORT = 'ft-custom_transport'; + +/** Feature token: retries are enabled. */ export const SDK_TELEMETRY_FT_RETRY_CONFIG = 'ft-retry_config'; diff --git a/src/telemetry/sdkTelemetryHeaderBuilder.ts b/src/telemetry/sdkTelemetryHeaderBuilder.ts index 276497a..beffef1 100644 --- a/src/telemetry/sdkTelemetryHeaderBuilder.ts +++ b/src/telemetry/sdkTelemetryHeaderBuilder.ts @@ -85,6 +85,7 @@ export function buildSdkTelemetryHeader( return headerValue; } +/** Quotes an sf-string; returns undefined for CR/LF/NUL. */ export function escapeSfString(value: string): string | undefined { if (containsInvalidSfStringChar(value)) { return undefined; @@ -100,6 +101,7 @@ export function escapeSfString(value: string): string | undefined { return escaped; } +/** Whether {@code value} contains CR, LF, or NUL. */ function containsInvalidSfStringChar(value: string): boolean { for (const ch of value) { const code = ch.charCodeAt(0); @@ -110,6 +112,7 @@ function containsInvalidSfStringChar(value: string): boolean { return false; } +/** Emits an sf-token, falling back to an sf-string. */ function appendTokenParam( parts: string[], key: string, @@ -128,6 +131,7 @@ function appendTokenParam( return true; } +/** Whether {@code value} is a valid RFC 9651 sf-token. */ function isSfToken(value: string): boolean { const first = value.charAt(0); if (!isAsciiLetter(first) && first !== '*') { @@ -145,10 +149,12 @@ function isSfToken(value: string): boolean { return true; } +/** Whether {@code ch} is an ASCII letter. */ function isAsciiLetter(ch: string): boolean { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); } +/** Emits a bare key=value, or a quoted sf-string when the value needs escaping. */ function appendBareParam( parts: string[], key: string, @@ -170,6 +176,7 @@ function appendBareParam( return true; } +/** Whether {@code value} can be emitted unquoted without corrupting the sf-list. */ function isBareSafe(value: string): boolean { for (const ch of value) { if ( @@ -186,6 +193,7 @@ function isBareSafe(value: string): boolean { return value.length > 0; } +/** Emits a quoted sf-string parameter, skipping it when the value is invalid. */ function appendStringParam( parts: string[], key: string, @@ -201,10 +209,12 @@ function appendStringParam( parts.push(`;${key}=${escaped}`); } +/** Emits an integer parameter. */ function appendIntegerParam(parts: string[], key: string, value: number): void { parts.push(`;${key}=${value}`); } +/** Emits an RFC 9651 sf-date parameter. */ function appendDateParam( parts: string[], key: string, @@ -213,6 +223,7 @@ function appendDateParam( parts.push(`;${key}=@${epochSeconds}`); } +/** Whether {@code value} is a valid bare feature-token item. */ function isValidFeatureToken(value: string | undefined): boolean { if (!isNotBlank(value)) { return false; @@ -224,6 +235,7 @@ function isValidFeatureToken(value: string | undefined): boolean { return isSfToken(trimmed); } +/** Whether {@code value} is non-null and non-blank. */ function isNotBlank(value: string | undefined): boolean { return value != null && value.trim().length > 0; } diff --git a/src/telemetry/sdkTelemetryState.ts b/src/telemetry/sdkTelemetryState.ts index 7d62ce8..84c09f7 100644 --- a/src/telemetry/sdkTelemetryState.ts +++ b/src/telemetry/sdkTelemetryState.ts @@ -16,14 +16,17 @@ import type { SdkTelemetrySnapshot } from './sdkTelemetrySnapshot.js'; export class SdkTelemetryState { private lastCallSnapshot: SdkTelemetrySnapshot | undefined; + /** Returns the last recorded call, or undefined if none. */ lastCall(): SdkTelemetrySnapshot | undefined { return this.lastCallSnapshot; } + /** Stores {@code snapshot} as the last completed call. */ record(snapshot: SdkTelemetrySnapshot): void { this.lastCallSnapshot = snapshot; } + /** Clears the last completed call. */ clear(): void { this.lastCallSnapshot = undefined; } From 5ed084cbb1f095a086b28605e73bf95a0af65bc5 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Tue, 11 Aug 2026 16:25:38 +0530 Subject: [PATCH 08/10] updated telemetry header format --- src/RequestWrapper.ts | 25 +-- src/telemetry/index.ts | 16 +- src/telemetry/sdkTelemetryEmitter.ts | 181 ++-------------- src/telemetry/sdkTelemetryFeature.ts | 21 ++ src/telemetry/sdkTelemetryHeader.ts | 21 +- src/telemetry/sdkTelemetryHeaderBuilder.ts | 233 ++------------------- src/telemetry/sdkTelemetrySnapshot.ts | 20 -- src/telemetry/sdkTelemetryState.ts | 28 +-- test/requestWrapper.test.ts | 115 +++------- test/sdkTelemetryHeaderBuilder.test.ts | 85 ++------ 10 files changed, 123 insertions(+), 622 deletions(-) create mode 100644 src/telemetry/sdkTelemetryFeature.ts delete mode 100644 src/telemetry/sdkTelemetrySnapshot.ts diff --git a/src/RequestWrapper.ts b/src/RequestWrapper.ts index d06364e..73a951e 100644 --- a/src/RequestWrapper.ts +++ b/src/RequestWrapper.ts @@ -23,8 +23,6 @@ import { extractRequestTelemetryError, resolveChargebeeApiVersion, attachSdkTelemetryHeader, - recordSdkTelemetryFailure, - recordSdkTelemetrySuccess, type TelemetryAdapter, } from './telemetry/index.js'; import { handleResponse } from './coreCommon.js'; @@ -398,33 +396,12 @@ export class RequestWrapper { } }; - const callMetadata = { - resource: this.apiCall.resource, - operation: this.apiCall.methodName, - }; - const executeCall = () => telemetryAdapter !== undefined ? runWithTelemetry(telemetryAdapter) : withRetry(0, requestStartTime); - const promise = executeCall() - .then((result) => { - recordSdkTelemetrySuccess( - env, - callMetadata, - requestStartTime, - typeof result?.httpStatusCode === 'number' - ? result.httpStatusCode - : 200, - result?.headers, - ); - return result; - }) - .catch((err) => { - recordSdkTelemetryFailure(env, callMetadata, requestStartTime, err); - throw err; - }); + const promise = executeCall(); return callbackifyPromise(promise); } diff --git a/src/telemetry/index.ts b/src/telemetry/index.ts index d23832c..b07605e 100644 --- a/src/telemetry/index.ts +++ b/src/telemetry/index.ts @@ -36,25 +36,15 @@ export { } from './TelemetryAdapter.js'; export { - SDK_TELEMETRY_FT_CUSTOM_TRANSPORT, - SDK_TELEMETRY_FT_RETRY_CONFIG, - SDK_TELEMETRY_FT_TELEMETRY_ADAPTER, + SDK_TELEMETRY_FEATURES_KEY, SDK_TELEMETRY_HEADER_NAME, SDK_TELEMETRY_MAX_HEADER_BYTES, - SDK_TELEMETRY_REQUEST_ID_HEADER, - SDK_TELEMETRY_RUNTIME, } from './sdkTelemetryHeader.js'; -export type { SdkTelemetrySnapshot } from './sdkTelemetrySnapshot.js'; +export { SdkTelemetryFeature } from './sdkTelemetryFeature.js'; export { SdkTelemetryState } from './sdkTelemetryState.js'; -export { - buildSdkTelemetryHeader, - escapeSfString, -} from './sdkTelemetryHeaderBuilder.js'; +export { buildSdkTelemetryHeader } from './sdkTelemetryHeaderBuilder.js'; export { attachSdkTelemetryHeader, - recordSdkTelemetryFailure, - recordSdkTelemetrySuccess, - type SdkTelemetryCallMetadata, type SdkTelemetryEnv, } from './sdkTelemetryEmitter.js'; diff --git a/src/telemetry/sdkTelemetryEmitter.ts b/src/telemetry/sdkTelemetryEmitter.ts index e915ae1..c15777a 100644 --- a/src/telemetry/sdkTelemetryEmitter.ts +++ b/src/telemetry/sdkTelemetryEmitter.ts @@ -5,38 +5,21 @@ * Copyright 2026 Chargebee Inc. */ -import { CHARGEBEE_SDK_NAME } from './types.js'; -import { - extractHttpStatusCode, - type TelemetryAdapter, -} from './TelemetryAdapter.js'; +import type { TelemetryAdapter } from './TelemetryAdapter.js'; import { buildSdkTelemetryHeader } from './sdkTelemetryHeaderBuilder.js'; -import { - SDK_TELEMETRY_FT_CUSTOM_TRANSPORT, - SDK_TELEMETRY_FT_RETRY_CONFIG, - SDK_TELEMETRY_FT_TELEMETRY_ADAPTER, - SDK_TELEMETRY_HEADER_NAME, - SDK_TELEMETRY_REQUEST_ID_HEADER, -} from './sdkTelemetryHeader.js'; -import type { SdkTelemetrySnapshot } from './sdkTelemetrySnapshot.js'; +import { SdkTelemetryFeature } from './sdkTelemetryFeature.js'; +import { SDK_TELEMETRY_HEADER_NAME } from './sdkTelemetryHeader.js'; import type { SdkTelemetryState } from './sdkTelemetryState.js'; /** Client env fields needed to emit SDK telemetry. */ export type SdkTelemetryEnv = { sdkTelemetryEnabled?: boolean; sdkTelemetryState?: SdkTelemetryState; - clientVersion: string; telemetryAdapter?: TelemetryAdapter; httpClientIsCustom?: boolean; retryConfig?: { enabled?: boolean }; }; -/** Resource/operation metadata for the call being recorded. */ -export type SdkTelemetryCallMetadata = { - resource: string; - operation: string; -}; - /** Mutable request-header map used when attaching the telemetry header. */ export type RequestHeadersForSdkTelemetry = Record; @@ -44,9 +27,10 @@ export type RequestHeadersForSdkTelemetry = Record; * Emits the anonymous SDK telemetry request header, independently of any customer telemetry * adapter. * - * Uses an N+1 scheme: the header sent with a call describes the previous completed call on the - * same client, so the first call of a client never carries the header. Every failure path is - * swallowed and logged at WARNING: telemetry must never fail an API call. + * On the first API call of a client instance, attaches {@code f;…} with enabled feature codes when + * any are present; omits the header when none are enabled. Later calls on the same client never + * attach again. SDK identity is correlated via User-Agent. Every failure path is swallowed and + * logged at WARNING: telemetry must never fail an API call. */ export function attachSdkTelemetryHeader( env: SdkTelemetryEnv, @@ -57,11 +41,10 @@ export function attachSdkTelemetryHeader( } try { - const previousCall = env.sdkTelemetryState?.lastCall(); - if (!previousCall) { + if (!env.sdkTelemetryState?.tryMarkEmitted()) { return; } - const headerValue = buildSdkTelemetryHeader(previousCall); + const headerValue = buildSdkTelemetryHeader(resolveFeatures(env)); if (!headerValue) { return; } @@ -71,116 +54,17 @@ export function attachSdkTelemetryHeader( } } -/** Records a successful call for the next N+1 header. */ -export function recordSdkTelemetrySuccess( - env: SdkTelemetryEnv, - call: SdkTelemetryCallMetadata, - startTimeMs: number, - httpStatus: number | undefined, - responseHeaders: Record | undefined, -): void { - if (env.sdkTelemetryEnabled === false || !hasTelemetryMetadata(call)) { - return; - } - - try { - record( - env, - buildSnapshot( - env, - call, - startTimeMs, - httpStatus, - undefined, - extractRequestId(responseHeaders), - ), - ); - } catch (err) { - logSuppressed('record success', err); - } -} - -/** Records a failed call for the next N+1 header. */ -export function recordSdkTelemetryFailure( - env: SdkTelemetryEnv, - call: SdkTelemetryCallMetadata, - startTimeMs: number, - callError: unknown, -): void { - if (env.sdkTelemetryEnabled === false || !hasTelemetryMetadata(call)) { - return; - } - - try { - const httpStatus = extractHttpStatusCode(callError); - const errorObj = - callError != null && typeof callError === 'object' - ? (callError as Record) - : undefined; - const errorCode = - typeof errorObj?.api_error_code === 'string' - ? errorObj.api_error_code - : undefined; - const responseHeaders = - errorObj?.headers != null && typeof errorObj.headers === 'object' - ? (errorObj.headers as Record) - : undefined; - - record( - env, - buildSnapshot( - env, - call, - startTimeMs, - httpStatus, - errorCode, - extractRequestId(responseHeaders), - ), - ); - } catch (err) { - logSuppressed('record failure', err); - } -} - -/** Stores {@code snapshot} on the client. */ -function record(env: SdkTelemetryEnv, snapshot: SdkTelemetrySnapshot): void { - env.sdkTelemetryState?.record(snapshot); -} - -/** Builds an immutable snapshot of the completed call. */ -function buildSnapshot( - env: SdkTelemetryEnv, - call: SdkTelemetryCallMetadata, - startTimeMs: number, - httpStatus: number | undefined, - errorCode: string | undefined, - requestId: string | undefined, -): SdkTelemetrySnapshot { - return { - sdkName: CHARGEBEE_SDK_NAME, - sdkVersion: env.clientVersion, - resource: call.resource, - operation: call.operation, - startTimeEpochSeconds: Math.floor(startTimeMs / 1000), - timeMs: elapsedMs(startTimeMs), - httpStatus, - errorCode, - requestId, - featureTokens: resolveFeatureTokens(env), - }; -} - -/** Collects {@code ft-*} tokens for the current client configuration. */ -function resolveFeatureTokens(env: SdkTelemetryEnv): string[] { - const features: string[] = []; +/** Collects enabled feature codes for the current client configuration. */ +function resolveFeatures(env: SdkTelemetryEnv): SdkTelemetryFeature[] { + const features: SdkTelemetryFeature[] = []; if (env.telemetryAdapter !== undefined) { - features.push(SDK_TELEMETRY_FT_TELEMETRY_ADAPTER); + features.push(SdkTelemetryFeature.TELEMETRY_ADAPTER); } if (env.httpClientIsCustom) { - features.push(SDK_TELEMETRY_FT_CUSTOM_TRANSPORT); + features.push(SdkTelemetryFeature.CUSTOM_TRANSPORT); } if (isRetryConfigActive(env)) { - features.push(SDK_TELEMETRY_FT_RETRY_CONFIG); + features.push(SdkTelemetryFeature.RETRY_CONFIG); } return features; } @@ -190,41 +74,6 @@ function isRetryConfigActive(env: SdkTelemetryEnv): boolean { return env.retryConfig?.enabled === true; } -/** Reads {@code chargebee-request-id} from response headers, if present. */ -function extractRequestId( - headers: Record | undefined, -): string | undefined { - if (!headers) { - return undefined; - } - const value = headers[SDK_TELEMETRY_REQUEST_ID_HEADER]; - if (typeof value === 'string') { - return value; - } - if (Array.isArray(value) && value.length > 0) { - return String(value[0]); - } - if (typeof value === 'number') { - return String(value); - } - return undefined; -} - -/** Whether the call has resource and operation metadata. */ -function hasTelemetryMetadata(call: SdkTelemetryCallMetadata): boolean { - return isNotBlank(call.resource) && isNotBlank(call.operation); -} - -/** Elapsed wall time of the call in milliseconds. */ -function elapsedMs(startTimeMs: number): number { - return Math.max(0, Date.now() - startTimeMs); -} - -/** Whether {@code value} is non-null and non-blank. */ -function isNotBlank(value: string | undefined): boolean { - return value != null && value.trim().length > 0; -} - /** Logs a suppressed telemetry failure without affecting the API call. */ function logSuppressed(step: string, err: unknown): void { const message = err instanceof Error ? err.message : String(err); diff --git a/src/telemetry/sdkTelemetryFeature.ts b/src/telemetry/sdkTelemetryFeature.ts new file mode 100644 index 0000000..d64a03f --- /dev/null +++ b/src/telemetry/sdkTelemetryFeature.ts @@ -0,0 +1,21 @@ +/* + * This file is auto-generated by Chargebee. + * For more information on how to make changes to this file, please see the README. + * Reach out to dx@chargebee.com for any questions. + * Copyright 2026 Chargebee Inc. + */ + +/** + * SDK configuration features reported under the {@code f} segment of + * {@link SDK_TELEMETRY_HEADER_NAME}. Wire codes are maintained in sdk-generator. + */ +export enum SdkTelemetryFeature { + /** Customer TelemetryAdapter configured. */ + TELEMETRY_ADAPTER = 'ta', + + /** Custom HTTP client configured. */ + CUSTOM_TRANSPORT = 'ct', + + /** Retries enabled on the client. */ + RETRY_CONFIG = 'rc', +} diff --git a/src/telemetry/sdkTelemetryHeader.ts b/src/telemetry/sdkTelemetryHeader.ts index f5c7cc0..dfada96 100644 --- a/src/telemetry/sdkTelemetryHeader.ts +++ b/src/telemetry/sdkTelemetryHeader.ts @@ -8,23 +8,8 @@ /** Constants for the anonymous SDK telemetry request header. */ export const SDK_TELEMETRY_HEADER_NAME = 'x-chargebee-sdk-telemetry'; -/** Server drops larger values, so the SDK omits the header rather than sending a truncated one. */ +/** Defensive size cap; feature-only values are tiny. */ export const SDK_TELEMETRY_MAX_HEADER_BYTES = 4096; -/** Response header that carries the Chargebee request id. */ -export const SDK_TELEMETRY_REQUEST_ID_HEADER = 'chargebee-request-id'; - -/** Runtime token emitted in the sdk segment. */ -export const SDK_TELEMETRY_RUNTIME = 'nodejs'; - -/** First item name in the RFC 9651 sf-list. */ -export const SDK_TELEMETRY_SEGMENT = 'sdk'; - -/** Feature token: a TelemetryAdapter is configured. */ -export const SDK_TELEMETRY_FT_TELEMETRY_ADAPTER = 'ft-telemetry_adapter'; - -/** Feature token: a custom HTTP client is configured. */ -export const SDK_TELEMETRY_FT_CUSTOM_TRANSPORT = 'ft-custom_transport'; - -/** Feature token: retries are enabled. */ -export const SDK_TELEMETRY_FT_RETRY_CONFIG = 'ft-retry_config'; +/** RFC 9651 sf-list item name for the features segment. */ +export const SDK_TELEMETRY_FEATURES_KEY = 'f'; diff --git a/src/telemetry/sdkTelemetryHeaderBuilder.ts b/src/telemetry/sdkTelemetryHeaderBuilder.ts index beffef1..16fe4d9 100644 --- a/src/telemetry/sdkTelemetryHeaderBuilder.ts +++ b/src/telemetry/sdkTelemetryHeaderBuilder.ts @@ -5,237 +5,38 @@ * Copyright 2026 Chargebee Inc. */ -import type { SdkTelemetrySnapshot } from './sdkTelemetrySnapshot.js'; +import { SdkTelemetryFeature } from './sdkTelemetryFeature.js'; import { + SDK_TELEMETRY_FEATURES_KEY, SDK_TELEMETRY_MAX_HEADER_BYTES, - SDK_TELEMETRY_RUNTIME, - SDK_TELEMETRY_SEGMENT, } from './sdkTelemetryHeader.js'; -/** Builds RFC 9651 sf-list values for {@link SDK_TELEMETRY_HEADER_NAME}. */ +/** + * Builds RFC 9651 values for {@link SDK_TELEMETRY_HEADER_NAME}: a features segment keyed by + * {@link SDK_TELEMETRY_FEATURES_KEY} with enabled feature codes as boolean params + * (for example {@code f;ta;rc}). Correlate SDK identity via User-Agent. + */ export function buildSdkTelemetryHeader( - snapshot: SdkTelemetrySnapshot | undefined, + features: ReadonlyArray | undefined, ): string | undefined { - if (!snapshot) { + if (!features || features.length === 0) { return undefined; } - const segmentParts: string[] = [SDK_TELEMETRY_SEGMENT]; - const segment = { - appendToken(key: string, value: string | undefined): boolean { - return appendTokenParam(segmentParts, key, value); - }, - appendBare(key: string, value: string | undefined): boolean { - return appendBareParam(segmentParts, key, value); - }, - appendString(key: string, value: string | undefined) { - appendStringParam(segmentParts, key, value); - }, - appendInteger(key: string, value: number) { - appendIntegerParam(segmentParts, key, value); - }, - appendDate(key: string, epochSeconds: number) { - appendDateParam(segmentParts, key, epochSeconds); - }, - }; - - if (!segment.appendToken('name', snapshot.sdkName)) { - return undefined; - } - if (!segment.appendBare('version', snapshot.sdkVersion)) { - return undefined; - } - if (!segment.appendToken('runtime', SDK_TELEMETRY_RUNTIME)) { - return undefined; - } - if (!segment.appendToken('resource', snapshot.resource)) { - return undefined; - } - if (!segment.appendToken('operation', snapshot.operation)) { - return undefined; - } - if (snapshot.startTimeEpochSeconds > 0) { - segment.appendDate('start_time', snapshot.startTimeEpochSeconds); - } - segment.appendInteger('time_ms', snapshot.timeMs); - if (snapshot.httpStatus != null) { - segment.appendInteger('http_status', snapshot.httpStatus); - } - if (isNotBlank(snapshot.errorCode)) { - segment.appendString('error_code', snapshot.errorCode); - } - if (isNotBlank(snapshot.requestId)) { - segment.appendString('request_id', snapshot.requestId); - } - - const items = [segmentParts.join('')]; - for (const featureToken of snapshot.featureTokens) { - if (isValidFeatureToken(featureToken)) { - items.push(featureToken!.trim()); + let value = SDK_TELEMETRY_FEATURES_KEY; + for (const feature of features) { + if (feature == null) { + continue; } + value += `;${feature}`; } - const headerValue = items.join(', '); - if ( - new TextEncoder().encode(headerValue).length > - SDK_TELEMETRY_MAX_HEADER_BYTES - ) { + if (value.length === SDK_TELEMETRY_FEATURES_KEY.length) { return undefined; } - return headerValue; -} -/** Quotes an sf-string; returns undefined for CR/LF/NUL. */ -export function escapeSfString(value: string): string | undefined { - if (containsInvalidSfStringChar(value)) { + if (new TextEncoder().encode(value).length > SDK_TELEMETRY_MAX_HEADER_BYTES) { return undefined; } - let escaped = '"'; - for (const ch of value) { - if (ch === '\\' || ch === '"') { - escaped += '\\'; - } - escaped += ch; - } - escaped += '"'; - return escaped; -} - -/** Whether {@code value} contains CR, LF, or NUL. */ -function containsInvalidSfStringChar(value: string): boolean { - for (const ch of value) { - const code = ch.charCodeAt(0); - if (code === 0 || code === 10 || code === 13) { - return true; - } - } - return false; -} - -/** Emits an sf-token, falling back to an sf-string. */ -function appendTokenParam( - parts: string[], - key: string, - value: string | undefined, -): boolean { - if (!isNotBlank(value)) { - return false; - } - const trimmed = value!.trim(); - const serialized = isSfToken(trimmed) ? trimmed : escapeSfString(trimmed); - if (serialized == null) { - return false; - } - parts.push(`;${key}=`); - parts.push(serialized); - return true; -} - -/** Whether {@code value} is a valid RFC 9651 sf-token. */ -function isSfToken(value: string): boolean { - const first = value.charAt(0); - if (!isAsciiLetter(first) && first !== '*') { - return false; - } - for (const ch of value) { - const allowed = - isAsciiLetter(ch) || - (ch >= '0' && ch <= '9') || - "!#$%&'*+-.^_`|~:/".includes(ch); - if (!allowed) { - return false; - } - } - return true; -} - -/** Whether {@code ch} is an ASCII letter. */ -function isAsciiLetter(ch: string): boolean { - return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); -} - -/** Emits a bare key=value, or a quoted sf-string when the value needs escaping. */ -function appendBareParam( - parts: string[], - key: string, - value: string | undefined, -): boolean { - if (!isNotBlank(value)) { - return false; - } - const trimmed = value!.trim(); - const serialized = - isBareSafe(trimmed) && !containsInvalidSfStringChar(trimmed) - ? trimmed - : escapeSfString(trimmed); - if (serialized == null) { - return false; - } - parts.push(`;${key}=`); - parts.push(serialized); - return true; -} - -/** Whether {@code value} can be emitted unquoted without corrupting the sf-list. */ -function isBareSafe(value: string): boolean { - for (const ch of value) { - if ( - ch === '"' || - ch === '\\' || - ch === ',' || - ch === ';' || - ch === '=' || - /\s/.test(ch) - ) { - return false; - } - } - return value.length > 0; -} - -/** Emits a quoted sf-string parameter, skipping it when the value is invalid. */ -function appendStringParam( - parts: string[], - key: string, - value: string | undefined, -): void { - if (!isNotBlank(value)) { - return; - } - const escaped = escapeSfString(value!.trim()); - if (escaped == null) { - return; - } - parts.push(`;${key}=${escaped}`); -} - -/** Emits an integer parameter. */ -function appendIntegerParam(parts: string[], key: string, value: number): void { - parts.push(`;${key}=${value}`); -} - -/** Emits an RFC 9651 sf-date parameter. */ -function appendDateParam( - parts: string[], - key: string, - epochSeconds: number, -): void { - parts.push(`;${key}=@${epochSeconds}`); -} - -/** Whether {@code value} is a valid bare feature-token item. */ -function isValidFeatureToken(value: string | undefined): boolean { - if (!isNotBlank(value)) { - return false; - } - if (containsInvalidSfStringChar(value!)) { - return false; - } - const trimmed = value!.trim(); - return isSfToken(trimmed); -} - -/** Whether {@code value} is non-null and non-blank. */ -function isNotBlank(value: string | undefined): boolean { - return value != null && value.trim().length > 0; + return value; } diff --git a/src/telemetry/sdkTelemetrySnapshot.ts b/src/telemetry/sdkTelemetrySnapshot.ts deleted file mode 100644 index ab4cfd0..0000000 --- a/src/telemetry/sdkTelemetrySnapshot.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * This file is auto-generated by Chargebee. - * For more information on how to make changes to this file, please see the README. - * Reach out to dx@chargebee.com for any questions. - * Copyright 2026 Chargebee Inc. - */ - -/** Immutable snapshot of a completed SDK API call for N+1 header emission. */ -export type SdkTelemetrySnapshot = { - sdkName: string; - sdkVersion: string; - resource: string; - operation: string; - startTimeEpochSeconds: number; - timeMs: number; - httpStatus?: number; - errorCode?: string; - requestId?: string; - featureTokens: string[]; -}; diff --git a/src/telemetry/sdkTelemetryState.ts b/src/telemetry/sdkTelemetryState.ts index 84c09f7..1a18865 100644 --- a/src/telemetry/sdkTelemetryState.ts +++ b/src/telemetry/sdkTelemetryState.ts @@ -5,29 +5,33 @@ * Copyright 2026 Chargebee Inc. */ -import type { SdkTelemetrySnapshot } from './sdkTelemetrySnapshot.js'; - /** - * Per-client holder for the last completed call, used by the N+1 SDK telemetry header. + * Per-client gate so the SDK telemetry header is considered at most once per client instance. * * Internal SDK type: applications must not depend on it. It is public only so that the * Chargebee client can own one instance. */ export class SdkTelemetryState { - private lastCallSnapshot: SdkTelemetrySnapshot | undefined; + private emitted = false; - /** Returns the last recorded call, or undefined if none. */ - lastCall(): SdkTelemetrySnapshot | undefined { - return this.lastCallSnapshot; + /** + * Claims the single emission slot for this client. Returns true only for the first caller. + */ + tryMarkEmitted(): boolean { + if (this.emitted) { + return false; + } + this.emitted = true; + return true; } - /** Stores {@code snapshot} as the last completed call. */ - record(snapshot: SdkTelemetrySnapshot): void { - this.lastCallSnapshot = snapshot; + /** Whether this client has already considered emitting the telemetry header. */ + hasEmitted(): boolean { + return this.emitted; } - /** Clears the last completed call. */ + /** Clears the emission gate (tests only). */ clear(): void { - this.lastCallSnapshot = undefined; + this.emitted = false; } } diff --git a/test/requestWrapper.test.ts b/test/requestWrapper.test.ts index e09518d..fd8eab9 100644 --- a/test/requestWrapper.test.ts +++ b/test/requestWrapper.test.ts @@ -592,16 +592,7 @@ describe('RequestWrapper - telemetry adapter', () => { }); describe('RequestWrapper - SDK telemetry header', () => { - it('should omit header on first call and attach N+1 header on second call', async () => { - responseFactory = () => - new Response(JSON.stringify({ list: [], next_offset: null }), { - status: 200, - headers: { - 'Content-Type': 'application/json', - 'chargebee-request-id': 'req_abc123', - }, - }); - + it('should omit header when no features are enabled', async () => { const chargebee = createChargebee({ retryConfig: { enabled: false }, }); @@ -612,19 +603,16 @@ describe('RequestWrapper - SDK telemetry header', () => { expect( capturedRequests[0].headers.get(SDK_TELEMETRY_HEADER_NAME), ).to.equal(null); - - const header = capturedRequests[1].headers.get(SDK_TELEMETRY_HEADER_NAME); - expect(header).to.be.a('string'); - expect(header).to.include('resource=customer;operation=list'); - expect(header).to.include('start_time=@'); - expect(header).to.include('http_status=200'); - expect(header).to.include('request_id="req_abc123"'); + expect( + capturedRequests[1].headers.get(SDK_TELEMETRY_HEADER_NAME), + ).to.equal(null); }); it('should not attach header when sdk telemetry is disabled', async () => { const chargebee = createChargebee({ sdkTelemetryEnabled: false, - retryConfig: { enabled: false }, + httpClient: mockHttpClient, + retryConfig: { enabled: true, maxRetries: 1, delayMs: 0, retryOn: [500] }, }); await chargebee.customer.list({ limit: 1 }); await chargebee.customer.list({ limit: 1 }); @@ -636,53 +624,12 @@ describe('RequestWrapper - SDK telemetry header', () => { expect( capturedRequests[1].headers.get(SDK_TELEMETRY_HEADER_NAME), ).to.equal(null); - expect((chargebee as any)._env.sdkTelemetryState.lastCall()).to.equal( - undefined, + expect((chargebee as any)._env.sdkTelemetryState.hasEmitted()).to.equal( + false, ); }); - it('should record failure details for the next header', async () => { - responseFactory = (attempt) => { - if (attempt === 0) { - return new Response( - JSON.stringify({ - message: 'Not found', - type: 'invalid_request', - api_error_code: 'resource_not_found', - }), - { - status: 404, - headers: { - 'Content-Type': 'application/json', - 'chargebee-request-id': 'req_fail', - }, - }, - ); - } - return new Response(JSON.stringify({ list: [], next_offset: null }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - }; - - const chargebee = createChargebee({ - retryConfig: { enabled: false }, - }); - - try { - await chargebee.customer.retrieve('cust_missing'); - } catch (_err) { - // expected - } - await chargebee.customer.list({ limit: 1 }); - - const header = capturedRequests[1].headers.get(SDK_TELEMETRY_HEADER_NAME); - expect(header).to.include('operation=retrieve'); - expect(header).to.include('http_status=404'); - expect(header).to.include('error_code="resource_not_found"'); - }); - - it('should emit feature tokens for retry config and telemetry adapter', async () => { + it('should emit keyed feature codes once on the first call only', async () => { const chargebee = createChargebee({ httpClient: mockHttpClient, retryConfig: { enabled: true, maxRetries: 1, delayMs: 0, retryOn: [500] }, @@ -695,39 +642,29 @@ describe('RequestWrapper - SDK telemetry header', () => { await chargebee.customer.list({ limit: 1 }); await chargebee.customer.list({ limit: 1 }); - const header = capturedRequests[1].headers.get(SDK_TELEMETRY_HEADER_NAME); - expect(header).to.include('ft-retry_config'); - expect(header).to.include('ft-telemetry_adapter'); - expect(header).to.include('ft-custom_transport'); + expect(capturedRequests[0].headers.get(SDK_TELEMETRY_HEADER_NAME)).to.equal( + 'f;ta;ct;rc', + ); + expect( + capturedRequests[1].headers.get(SDK_TELEMETRY_HEADER_NAME), + ).to.equal(null); }); - it('should skip invalid feature tokens without failing the API request', async () => { + it('should emit ct and rc without telemetry adapter', async () => { const chargebee = createChargebee({ - retryConfig: { enabled: false }, - }); - await chargebee.customer.list({ limit: 1 }); - - const state = (chargebee as any)._env.sdkTelemetryState; - const previousCall = state.lastCall(); - state.record({ - ...previousCall, - featureTokens: [ - 'ft-retry_config', - 'ft-bad\rinjected', - 'ft-telemetry_adapter', - ], + httpClient: mockHttpClient, + retryConfig: { enabled: true, maxRetries: 1, delayMs: 0, retryOn: [500] }, }); - const result = await chargebee.customer.list({ limit: 1 }); + await chargebee.customer.list({ limit: 1 }); + await chargebee.customer.list({ limit: 1 }); - expect(result).to.have.property('list'); - expect(capturedRequests.length).to.equal(2); - const header = capturedRequests[1].headers.get(SDK_TELEMETRY_HEADER_NAME); - expect(header).to.be.a('string'); - expect(header).to.include('ft-retry_config'); - expect(header).to.include('ft-telemetry_adapter'); - expect(header).to.not.include('ft-bad'); - expect(header).to.not.match(/\r|\n/); + expect(capturedRequests[0].headers.get(SDK_TELEMETRY_HEADER_NAME)).to.equal( + 'f;ct;rc', + ); + expect( + capturedRequests[1].headers.get(SDK_TELEMETRY_HEADER_NAME), + ).to.equal(null); }); }); diff --git a/test/sdkTelemetryHeaderBuilder.test.ts b/test/sdkTelemetryHeaderBuilder.test.ts index 2b2200f..0d78774 100644 --- a/test/sdkTelemetryHeaderBuilder.test.ts +++ b/test/sdkTelemetryHeaderBuilder.test.ts @@ -1,78 +1,35 @@ import { expect } from 'chai'; -import { - buildSdkTelemetryHeader, - escapeSfString, -} from '../src/telemetry/sdkTelemetryHeaderBuilder.js'; -import type { SdkTelemetrySnapshot } from '../src/telemetry/sdkTelemetrySnapshot.js'; - -function baseSnapshot( - overrides: Partial = {}, -): SdkTelemetrySnapshot { - return { - sdkName: 'chargebee-node', - sdkVersion: '3.30.0', - resource: 'customer', - operation: 'list', - startTimeEpochSeconds: 1781280400, - timeMs: 100, - featureTokens: [], - ...overrides, - }; -} +import { buildSdkTelemetryHeader } from '../src/telemetry/sdkTelemetryHeaderBuilder.js'; +import { SdkTelemetryFeature } from '../src/telemetry/sdkTelemetryFeature.js'; describe('sdkTelemetryHeaderBuilder', () => { - it('should escape valid sf-string values', () => { - expect(escapeSfString('hello')).to.equal('"hello"'); - expect(escapeSfString('say "hi"')).to.equal('"say \\"hi\\""'); - expect(escapeSfString('path\\to')).to.equal('"path\\\\to"'); - }); + it('should serialize features under the f key', () => { + const header = buildSdkTelemetryHeader([ + SdkTelemetryFeature.TELEMETRY_ADAPTER, + SdkTelemetryFeature.CUSTOM_TRANSPORT, + ]); - it('should reject sf-string values containing CR, LF, or NUL', () => { - expect(escapeSfString('bad\rvalue')).to.equal(undefined); - expect(escapeSfString('bad\nvalue')).to.equal(undefined); - expect(escapeSfString('bad\0value')).to.equal(undefined); + expect(header).to.equal('f;ta;ct'); }); - it('should omit invalid error_code while keeping the rest of the header', () => { - const header = buildSdkTelemetryHeader( - baseSnapshot({ - httpStatus: 404, - errorCode: 'resource_not_found\rinjected', - }), - ); - - expect(header).to.be.a('string'); - expect(header).to.include('http_status=404'); - expect(header).to.not.include('error_code='); + it('should return undefined when no features are enabled', () => { + expect(buildSdkTelemetryHeader([])).to.equal(undefined); + expect(buildSdkTelemetryHeader(undefined)).to.equal(undefined); }); - it('should omit the entire header when a required field contains invalid characters', () => { - const header = buildSdkTelemetryHeader( - baseSnapshot({ - sdkVersion: '3.30.0\0', - }), - ); + it('should serialize all feature codes in enum order', () => { + const header = buildSdkTelemetryHeader([ + SdkTelemetryFeature.TELEMETRY_ADAPTER, + SdkTelemetryFeature.CUSTOM_TRANSPORT, + SdkTelemetryFeature.RETRY_CONFIG, + ]); - expect(header).to.equal(undefined); + expect(header).to.equal('f;ta;ct;rc'); }); - it('should skip invalid feature tokens while keeping valid ones', () => { - const header = buildSdkTelemetryHeader( - baseSnapshot({ - featureTokens: [ - 'ft-retry_config', - 'ft-bad\rinjected', - 'ft-telemetry_adapter', - 'ft-bad\0', - 'ft-bad\n', - ], - }), + it('should serialize a single feature', () => { + expect(buildSdkTelemetryHeader([SdkTelemetryFeature.RETRY_CONFIG])).to.equal( + 'f;rc', ); - - expect(header).to.be.a('string'); - expect(header).to.include('ft-retry_config'); - expect(header).to.include('ft-telemetry_adapter'); - expect(header).to.not.include('ft-bad'); - expect(header).to.not.match(/\r|\n/); }); }); From 867acb1c69451db479a3f20756fdbabd2a32ac52 Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Wed, 12 Aug 2026 10:16:26 +0530 Subject: [PATCH 09/10] prettier to biome --- Makefile | 2 +- biome.json | 26 +++++++ package-lock.json | 180 +++++++++++++++++++++++++++++++++++++++++----- package.json | 10 +-- 4 files changed, 194 insertions(+), 24 deletions(-) create mode 100644 biome.json diff --git a/Makefile b/Makefile index 586024e..95e7156 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ test: install npm test format: - npm run prettier + npm run format build: install npm run build diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..e060997 --- /dev/null +++ b/biome.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.7/schema.json", + "files": { + "includes": ["src/**/*.ts", "types/**/*.d.ts"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80, + "lineEnding": "lf" + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "always", + "trailingCommas": "all" + } + }, + "linter": { + "enabled": false + }, + "assist": { + "enabled": false + } +} diff --git a/package-lock.json b/package-lock.json index 20597dc..b08e225 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,13 +11,13 @@ "zod": "^4.3.6" }, "devDependencies": { + "@biomejs/biome": "^2.5.7", "@opentelemetry/api": "^1.9.0", "@types/chai": "^4.3.5", "@types/mocha": "^10.0.10", "@types/node": "20.12.0", "chai": "^4.3.7", "mocha": "^10.2.0", - "prettier": "^3.3.3", "ts-node": "^10.9.1", "typescript": "^5.5.4", "undici-types": "^7.16.0" @@ -34,6 +34,169 @@ } } }, + "node_modules/@biomejs/biome": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.7.tgz", + "integrity": "sha512-zr8K/DcY5tYsQOQwqMJ0AWElo6QgmgNI7idXgXLhevVszlt8RGVpesEJPqx3ThazLaOwjJ5Y8fz3BtH5fGZNsw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.5.7", + "@biomejs/cli-darwin-x64": "2.5.7", + "@biomejs/cli-linux-arm64": "2.5.7", + "@biomejs/cli-linux-arm64-musl": "2.5.7", + "@biomejs/cli-linux-x64": "2.5.7", + "@biomejs/cli-linux-x64-musl": "2.5.7", + "@biomejs/cli-win32-arm64": "2.5.7", + "@biomejs/cli-win32-x64": "2.5.7" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.7.tgz", + "integrity": "sha512-vxo/Ls3/PYdQWyLhYYcgMOCzQypAjcY+iihS8M0wW03l16TCLW4zqZzGo75gm1VdCMj38hTVZ31KBWrZ4G9dJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.7.tgz", + "integrity": "sha512-Cd3Ga61amT/Yl/0x8elP5hhGYaFy4bw6WuysTgf7oo8TA5tJ5A1k+DkVoJ2BHbTVil51gTX9VPzArnrlLJ3Kyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.7.tgz", + "integrity": "sha512-rR2QE0yF2GYSuYuKIa7pKvODGJqnOH+2eDREAM8wV+mWKSkMQKdAp4zXEZfTaxY8PMoNONnpgSWcBCyLDPDOKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.7.tgz", + "integrity": "sha512-xPI5yB6XlpDbNkS+bm1t42olw5c4l3UrlOmLg7KtLJvjvkNF/1V4tnUgfkylGIeb3u/T+BzMGYqgQhzjAoJzuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.7.tgz", + "integrity": "sha512-FQgqJhscrqJUFptGaRSUJWlXAExwWcDwLuK49dvKfkQ1bB5SEEyFssnsxQY83Xm6jR0EbbX3+8+D5bfvYqUG2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.7.tgz", + "integrity": "sha512-rE5VZi+qtmPgQH+l7jVxYoZ18b/TiHEhulhMpjmCZH1PltSbjRcxNWywC3HZ9tYottG7ORkeTtoscBilKSBm0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.7.tgz", + "integrity": "sha512-Oq4x0CCwP4jirrcTywXs5kOGZ4v5vuEP+gWrbtjApOA2CL9F3F9GlIdQIci8AKSCa/zURanMRpX/4wQ7Am6hHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.7.tgz", + "integrity": "sha512-V+0wu/nrj2S+MhP4EQ0uHNolP0IALEsz45pg0WoKkHfDeh0+ItHwP/p7bX5RPoMOl9NkpHYWdYPhIcy2mACHvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -955,21 +1118,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", diff --git a/package.json b/package.json index 153d29a..96da087 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build": "npm run build-esm && npm run build-cjs", "build-esm": "rm -rf esm && mkdir -p esm && tsc -p tsconfig.esm.json && echo '{\"type\":\"module\"}' > esm/package.json", "build-cjs": "rm -rf cjs && mkdir -p cjs && tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > cjs/package.json", - "prettier": "prettier --write \"src/**/*.ts\" \"types/**/*.d.ts\"" + "format": "biome format --write", + "format:check": "biome format" }, "types": "./types/index.d.ts", "keywords": [ @@ -76,22 +77,17 @@ } }, "devDependencies": { + "@biomejs/biome": "^2.5.7", "@opentelemetry/api": "^1.9.0", "@types/chai": "^4.3.5", "@types/mocha": "^10.0.10", "@types/node": "20.12.0", "chai": "^4.3.7", "mocha": "^10.2.0", - "prettier": "^3.3.3", "ts-node": "^10.9.1", "typescript": "^5.5.4", "undici-types": "^7.16.0" }, - "prettier": { - "semi": true, - "singleQuote": true, - "parser": "typescript" - }, "dependencies": { "zod": "^4.3.6" } From 294e33b822020fdd01131dcfab460e7fecb408ad Mon Sep 17 00:00:00 2001 From: cb-karthikp Date: Wed, 12 Aug 2026 11:20:15 +0530 Subject: [PATCH 10/10] docs update --- src/telemetry/sdkTelemetryState.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/telemetry/sdkTelemetryState.ts b/src/telemetry/sdkTelemetryState.ts index 1a18865..09de285 100644 --- a/src/telemetry/sdkTelemetryState.ts +++ b/src/telemetry/sdkTelemetryState.ts @@ -7,9 +7,6 @@ /** * Per-client gate so the SDK telemetry header is considered at most once per client instance. - * - * Internal SDK type: applications must not depend on it. It is public only so that the - * Chargebee client can own one instance. */ export class SdkTelemetryState { private emitted = false;