diff --git a/.speakeasy/gen.lock b/.speakeasy/gen.lock index 2e71a3a..cc840dc 100755 --- a/.speakeasy/gen.lock +++ b/.speakeasy/gen.lock @@ -3,10 +3,10 @@ id: 599a9576-4665-431e-be1e-44cc13ef28aa management: docChecksum: dd4d04e62622de8f631720b5be68964d docVersion: latest - speakeasyVersion: 1.358.0 - generationVersion: 2.390.6 - releaseVersion: 0.5.0 - configChecksum: 76366dee2b6c919409101b279e0d2575 + speakeasyVersion: 1.360.0 + generationVersion: 2.392.0 + releaseVersion: 0.6.0 + configChecksum: 10b03678b9f493d15f149a0580702c40 repoURL: https://github.com/LukeHagar/discoursejs.git repoSubDirectory: . installationURL: https://github.com/LukeHagar/discoursejs @@ -16,7 +16,7 @@ features: additionalDependencies: 0.1.0 additionalProperties: 0.1.1 constsAndDefaults: 0.1.6 - core: 3.12.3 + core: 3.13.0 defaultEnabledRetries: 0.1.0 deprecations: 2.81.1 envVarSecurityUsage: 0.1.1 @@ -157,6 +157,7 @@ generatedFiles: - src/lib/encodings.ts - src/lib/http.ts - src/lib/is-plain-object.ts + - src/lib/logger.ts - src/lib/matchers.ts - src/lib/primitives.ts - src/lib/retries.ts diff --git a/README.md b/README.md index 4cff033..68f40cc 100644 --- a/README.md +++ b/README.md @@ -460,6 +460,21 @@ run(); ``` + +## Debugging + +To log HTTP requests and responses, you can pass a logger that matches `console`'s interface as an SDK option. + +> [!WARNING] +> Beware that debug logging will reveal secrets, like API tokens in headers, in log messages printed to a console or files. It's recommended to use this feature only during local development and not in production. + +```typescript +import { SDK } from "@lukehagar/discoursejs"; + +const sdk = new SDK({ debugLogger: console }); +``` + + # Development diff --git a/RELEASES.md b/RELEASES.md index 69cf3bb..b45a2cc 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -178,4 +178,14 @@ Based on: ### Generated - [typescript v0.5.0] . ### Releases -- [NPM v0.5.0] https://www.npmjs.com/package/@lukehagar/discoursejs/v/0.5.0 - . \ No newline at end of file +- [NPM v0.5.0] https://www.npmjs.com/package/@lukehagar/discoursejs/v/0.5.0 - . + +## 2024-08-10 00:22:12 +### Changes +Based on: +- OpenAPI Doc latest +- Speakeasy CLI 1.360.0 (2.392.0) https://github.com/speakeasy-api/speakeasy +### Generated +- [typescript v0.6.0] . +### Releases +- [NPM v0.6.0] https://www.npmjs.com/package/@lukehagar/discoursejs/v/0.6.0 - . \ No newline at end of file diff --git a/gen.yaml b/gen.yaml index 8fdcb07..a28cc1f 100644 --- a/gen.yaml +++ b/gen.yaml @@ -11,7 +11,7 @@ generation: auth: oAuth2ClientCredentialsEnabled: false typescript: - version: 0.5.0 + version: 0.6.0 additionalDependencies: dependencies: {} devDependencies: {} diff --git a/jsr.json b/jsr.json index 42140be..a5e57ea 100644 --- a/jsr.json +++ b/jsr.json @@ -2,7 +2,7 @@ { "name": "@lukehagar/discoursejs", - "version": "0.5.0", + "version": "0.6.0", "exports": { ".": "./src/index.ts", "./sdk/models/errors": "./src/sdk/models/errors/index.ts", diff --git a/package.json b/package.json index 2cb7ccd..682f0ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lukehagar/discoursejs", - "version": "0.5.0", + "version": "0.6.0", "author": "LukeHagar", "main": "./index.js", "sideEffects": false, diff --git a/src/lib/config.ts b/src/lib/config.ts index ef58e2f..0ffbce8 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -3,6 +3,7 @@ */ import { HTTPClient } from "./http.js"; +import { Logger } from "./logger.js"; import { RetryConfig } from "./retries.js"; import { Params, pathToFunc } from "./url.js"; @@ -30,6 +31,7 @@ export type SDKOptions = { */ retryConfig?: RetryConfig; timeoutMs?: number; + debugLogger?: Logger; }; export function serverURLFromOptions(options: SDKOptions): URL | null { @@ -58,7 +60,7 @@ export function serverURLFromOptions(options: SDKOptions): URL | null { export const SDK_METADATA = { language: "typescript", openapiDocVersion: "latest", - sdkVersion: "0.5.0", - genVersion: "2.390.6", - userAgent: "speakeasy-sdk/typescript 0.5.0 2.390.6 latest @lukehagar/discoursejs", + sdkVersion: "0.6.0", + genVersion: "2.392.0", + userAgent: "speakeasy-sdk/typescript 0.6.0 2.392.0 latest @lukehagar/discoursejs", } as const; diff --git a/src/lib/http.ts b/src/lib/http.ts index f8c544d..13cf1fd 100644 --- a/src/lib/http.ts +++ b/src/lib/http.ts @@ -157,7 +157,7 @@ export type StatusCodePredicate = number | string | (number | string)[]; // segments in a media type string. const mediaParamSeparator = /\s*;\s*/g; -function matchContentType(response: Response, pattern: string): boolean { +export function matchContentType(response: Response, pattern: string): boolean { // `*` is a special case which means anything is acceptable. if (pattern === "*") { return true; diff --git a/src/lib/logger.ts b/src/lib/logger.ts new file mode 100644 index 0000000..d181f29 --- /dev/null +++ b/src/lib/logger.ts @@ -0,0 +1,9 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +export interface Logger { + group(label?: string): void; + groupEnd(): void; + log(message: any, ...args: any[]): void; +} diff --git a/src/lib/sdks.ts b/src/lib/sdks.ts index 530ca70..08399af 100644 --- a/src/lib/sdks.ts +++ b/src/lib/sdks.ts @@ -4,6 +4,7 @@ import { HTTPClient, + matchContentType, matchStatusCode, isAbortError, isTimeoutError, @@ -11,6 +12,7 @@ import { } from "./http.js"; import { SecurityState } from "./security.js"; import { retry, RetryConfig } from "./retries.js"; +import { Logger } from "./logger.js"; import { encodeForm } from "./encodings.js"; import { stringToBase64 } from "./base64.js"; import { SDKOptions, SDK_METADATA, serverURLFromOptions } from "./config.js"; @@ -74,6 +76,7 @@ export class ClientSDK { private readonly httpClient: HTTPClient; protected readonly baseURL: URL | null; protected readonly hooks$: SDKHooks; + protected readonly logger?: Logger | undefined; public readonly options$: SDKOptions & { hooks?: SDKHooks }; constructor(options: SDKOptions = {}) { @@ -100,6 +103,7 @@ export class ClientSDK { }); this.baseURL = baseURL; this.httpClient = client; + this.logger = options.debugLogger; } public createRequest$( @@ -217,12 +221,13 @@ export class ClientSDK { return retry( async () => { - const req = request.clone(); - - let response = await this.httpClient.request( - await this.hooks$.beforeRequest(context, req) + const req = await this.hooks$.beforeRequest(context, request.clone()); + await logRequest(this.logger, req).catch((e) => + this.logger?.log("Failed to log request:", e) ); + let response = await this.httpClient.request(req); + if (matchStatusCode(response, errorCodes)) { const result = await this.hooks$.afterError(context, response, null); if (result.error) { @@ -233,6 +238,10 @@ export class ClientSDK { response = await this.hooks$.afterSuccess(context, response); } + await logResponse(this.logger, response, req).catch((e) => + this.logger?.log("Failed to log response:", e) + ); + return response; }, { config: retryConfig, statusCodes: retryCodes } @@ -259,3 +268,90 @@ export class ClientSDK { ); } } + +const jsonLikeContentTypeRE = /^application\/(?:.{0,100}\+)?json/; +async function logRequest(logger: Logger | undefined, req: Request) { + if (!logger) { + return; + } + + const contentType = req.headers.get("content-type"); + const ct = contentType?.split(";")[0] || ""; + + logger.group(`> Request: ${req.method} ${req.url}`); + + logger.group("Headers:"); + for (const [k, v] of req.headers.entries()) { + logger.log(`${k}: ${v}`); + } + logger.groupEnd(); + + logger.group("Body:"); + switch (true) { + case jsonLikeContentTypeRE.test(ct): + logger.log(await req.clone().json()); + break; + case ct.startsWith("text/"): + logger.log(await req.clone().text()); + break; + case ct === "multipart/form-data": { + const body = await req.clone().formData(); + for (const [k, v] of body) { + const vlabel = v instanceof Blob ? "" : v; + logger.log(`${k}: ${vlabel}`); + } + break; + } + default: + logger.log(`<${contentType}>`); + break; + } + logger.groupEnd(); + + logger.groupEnd(); +} + +async function logResponse(logger: Logger | undefined, res: Response, req: Request) { + if (!logger) { + return; + } + + const contentType = res.headers.get("content-type"); + const ct = contentType?.split(";")[0] || ""; + + logger.group(`< Response: ${req.method} ${req.url}`); + logger.log("Status Code:", res.status, res.statusText); + + logger.group("Headers:"); + for (const [k, v] of res.headers.entries()) { + logger.log(`${k}: ${v}`); + } + logger.groupEnd(); + + logger.group("Body:"); + switch (true) { + case matchContentType(res, "application/json") || jsonLikeContentTypeRE.test(ct): + logger.log(await res.clone().json()); + break; + case matchContentType(res, "text/event-stream"): + logger.log(`<${contentType}>`); + break; + case matchContentType(res, "text/*"): + logger.log(await res.clone().text()); + break; + case matchContentType(res, "multipart/form-data"): { + const body = await res.clone().formData(); + for (const [k, v] of body) { + const vlabel = v instanceof Blob ? "" : v; + logger.log(`${k}: ${vlabel}`); + } + break; + } + default: + logger.log(`<${contentType}>`); + break; + } + logger.groupEnd(); + + logger.groupEnd(); +}