apitally 0.15.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/adonisjs/middleware.cjs.map +1 -1
  2. package/dist/adonisjs/middleware.js.map +1 -1
  3. package/dist/adonisjs/provider.cjs +118 -46
  4. package/dist/adonisjs/provider.cjs.map +1 -1
  5. package/dist/adonisjs/provider.js +118 -46
  6. package/dist/adonisjs/provider.js.map +1 -1
  7. package/dist/common/client.cjs +118 -46
  8. package/dist/common/client.cjs.map +1 -1
  9. package/dist/common/client.js +118 -46
  10. package/dist/common/client.js.map +1 -1
  11. package/dist/common/requestLogger.cjs +118 -46
  12. package/dist/common/requestLogger.cjs.map +1 -1
  13. package/dist/common/requestLogger.d.cts +5 -0
  14. package/dist/common/requestLogger.d.ts +5 -0
  15. package/dist/common/requestLogger.js +118 -46
  16. package/dist/common/requestLogger.js.map +1 -1
  17. package/dist/express/index.cjs +118 -46
  18. package/dist/express/index.cjs.map +1 -1
  19. package/dist/express/index.js +118 -46
  20. package/dist/express/index.js.map +1 -1
  21. package/dist/express/middleware.cjs +118 -46
  22. package/dist/express/middleware.cjs.map +1 -1
  23. package/dist/express/middleware.js +118 -46
  24. package/dist/express/middleware.js.map +1 -1
  25. package/dist/fastify/index.cjs +118 -46
  26. package/dist/fastify/index.cjs.map +1 -1
  27. package/dist/fastify/index.js +118 -46
  28. package/dist/fastify/index.js.map +1 -1
  29. package/dist/fastify/plugin.cjs +118 -46
  30. package/dist/fastify/plugin.cjs.map +1 -1
  31. package/dist/fastify/plugin.js +118 -46
  32. package/dist/fastify/plugin.js.map +1 -1
  33. package/dist/h3/index.cjs +118 -46
  34. package/dist/h3/index.cjs.map +1 -1
  35. package/dist/h3/index.js +118 -46
  36. package/dist/h3/index.js.map +1 -1
  37. package/dist/h3/plugin.cjs +118 -46
  38. package/dist/h3/plugin.cjs.map +1 -1
  39. package/dist/h3/plugin.js +118 -46
  40. package/dist/h3/plugin.js.map +1 -1
  41. package/dist/hono/index.cjs +118 -46
  42. package/dist/hono/index.cjs.map +1 -1
  43. package/dist/hono/index.js +118 -46
  44. package/dist/hono/index.js.map +1 -1
  45. package/dist/hono/middleware.cjs +118 -46
  46. package/dist/hono/middleware.cjs.map +1 -1
  47. package/dist/hono/middleware.js +118 -46
  48. package/dist/hono/middleware.js.map +1 -1
  49. package/dist/koa/index.cjs +118 -46
  50. package/dist/koa/index.cjs.map +1 -1
  51. package/dist/koa/index.js +118 -46
  52. package/dist/koa/index.js.map +1 -1
  53. package/dist/koa/middleware.cjs +118 -46
  54. package/dist/koa/middleware.cjs.map +1 -1
  55. package/dist/koa/middleware.js +118 -46
  56. package/dist/koa/middleware.js.map +1 -1
  57. package/dist/nestjs/index.cjs +118 -46
  58. package/dist/nestjs/index.cjs.map +1 -1
  59. package/dist/nestjs/index.js +118 -46
  60. package/dist/nestjs/index.js.map +1 -1
  61. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adonisjs/middleware.ts","../../src/common/consumerRegistry.ts","../../src/common/headers.ts","../../src/common/requestLogger.ts","../../src/common/sentry.ts"],"sourcesContent":["import { HttpContext } from \"@adonisjs/core/http\";\nimport { NextFn } from \"@adonisjs/core/types/http\";\nimport type { OutgoingHttpHeaders } from \"http\";\nimport { performance } from \"perf_hooks\";\n\nimport type { ApitallyClient } from \"../common/client.js\";\nimport { consumerFromStringOrObject } from \"../common/consumerRegistry.js\";\nimport { parseContentLength } from \"../common/headers.js\";\nimport { convertHeaders } from \"../common/requestLogger.js\";\nimport type { ApitallyConsumer } from \"../common/types.js\";\n\ndeclare module \"@adonisjs/core/http\" {\n interface HttpContext {\n apitallyConsumer?: ApitallyConsumer | string;\n apitallyError?: Error;\n }\n}\n\nexport default class ApitallyMiddleware {\n async handle(ctx: HttpContext, next: NextFn) {\n const client: ApitallyClient =\n await ctx.containerResolver.make(\"apitallyClient\");\n\n if (!client.isEnabled()) {\n await next();\n return;\n }\n\n const path = ctx.route?.pattern;\n const timestamp = Date.now() / 1000;\n const startTime = performance.now();\n\n await next();\n\n const responseTime = performance.now() - startTime;\n const requestSize = parseContentLength(\n ctx.request.header(\"content-length\"),\n );\n const requestContentType = ctx.request.header(\"content-type\")?.toString();\n let responseStatus = ctx.response.getStatus();\n let responseHeaders = ctx.response.getHeaders();\n let responseSize: number | undefined;\n let responseContentType: string | undefined;\n\n const consumer = ctx.apitallyConsumer\n ? consumerFromStringOrObject(ctx.apitallyConsumer)\n : null;\n client.consumerRegistry.addOrUpdateConsumer(consumer);\n\n const onWriteHead = (statusCode: number, headers: OutgoingHttpHeaders) => {\n responseStatus = statusCode;\n responseHeaders = headers;\n responseSize = parseContentLength(headers[\"content-length\"]);\n responseContentType = headers[\"content-type\"]?.toString();\n if (path) {\n client.requestCounter.addRequest({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n statusCode: responseStatus,\n responseTime,\n requestSize,\n responseSize,\n });\n\n if (\n responseStatus === 422 &&\n ctx.apitallyError &&\n \"code\" in ctx.apitallyError &&\n \"messages\" in ctx.apitallyError &&\n ctx.apitallyError.code === \"E_VALIDATION_ERROR\" &&\n Array.isArray(ctx.apitallyError.messages)\n ) {\n ctx.apitallyError.messages.forEach((message) => {\n client.validationErrorCounter.addValidationError({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n loc: message.field,\n msg: message.message,\n type: message.rule,\n });\n });\n }\n\n if (responseStatus === 500 && ctx.apitallyError) {\n client.serverErrorCounter.addServerError({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n type: ctx.apitallyError.name,\n msg: ctx.apitallyError.message,\n traceback: ctx.apitallyError.stack || \"\",\n });\n }\n }\n };\n\n // Capture the final status code and response headers just before they are sent\n const originalWriteHead = ctx.response.response.writeHead;\n ctx.response.response.writeHead = (...args: any) => {\n originalWriteHead.apply(ctx.response.response, args);\n onWriteHead(args[0], typeof args[1] === \"string\" ? args[2] : args[1]);\n return ctx.response.response;\n };\n\n if (client.requestLogger.enabled) {\n const onEnd = (chunk: any) => {\n const requestBody =\n client.requestLogger.config.logRequestBody &&\n client.requestLogger.isSupportedContentType(requestContentType)\n ? ctx.request.raw()\n : undefined;\n const responseBody =\n client.requestLogger.config.logResponseBody &&\n client.requestLogger.isSupportedContentType(responseContentType)\n ? chunk\n : undefined;\n\n client.requestLogger.logRequest(\n {\n timestamp,\n method: ctx.request.method(),\n path,\n url: ctx.request.completeUrl(true),\n headers: convertHeaders(ctx.request.headers()),\n size: requestSize,\n consumer: consumer?.identifier,\n body: requestBody ? Buffer.from(requestBody) : undefined,\n },\n {\n statusCode: responseStatus,\n responseTime: responseTime / 1000,\n headers: convertHeaders(responseHeaders),\n size: responseSize,\n body: responseBody ? Buffer.from(responseBody) : undefined,\n },\n ctx.apitallyError,\n );\n };\n\n // Capture the final response body just before it is sent\n const originalEnd = ctx.response.response.end;\n ctx.response.response.end = (...args: any) => {\n originalEnd.apply(ctx.response.response, args);\n onEnd(typeof args[0] !== \"function\" ? args[0] : undefined);\n return ctx.response.response;\n };\n }\n }\n}\n","import { ApitallyConsumer } from \"./types.js\";\n\nexport const consumerFromStringOrObject = (\n consumer: ApitallyConsumer | string,\n) => {\n if (typeof consumer === \"string\") {\n consumer = String(consumer).trim().substring(0, 128);\n return consumer ? { identifier: consumer } : null;\n } else {\n consumer.identifier = String(consumer.identifier).trim().substring(0, 128);\n consumer.name = consumer.name?.trim().substring(0, 64);\n consumer.group = consumer.group?.trim().substring(0, 64);\n return consumer.identifier ? consumer : null;\n }\n};\n\nexport default class ConsumerRegistry {\n private consumers: Map<string, ApitallyConsumer>;\n private updated: Set<string>;\n\n constructor() {\n this.consumers = new Map();\n this.updated = new Set();\n }\n\n public addOrUpdateConsumer(consumer?: ApitallyConsumer | null) {\n if (!consumer || (!consumer.name && !consumer.group)) {\n return;\n }\n const existing = this.consumers.get(consumer.identifier);\n if (!existing) {\n this.consumers.set(consumer.identifier, consumer);\n this.updated.add(consumer.identifier);\n } else {\n if (consumer.name && consumer.name !== existing.name) {\n existing.name = consumer.name;\n this.updated.add(consumer.identifier);\n }\n if (consumer.group && consumer.group !== existing.group) {\n existing.group = consumer.group;\n this.updated.add(consumer.identifier);\n }\n }\n }\n\n public getAndResetUpdatedConsumers() {\n const data: Array<ApitallyConsumer> = [];\n this.updated.forEach((identifier) => {\n const consumer = this.consumers.get(identifier);\n if (consumer) {\n data.push(consumer);\n }\n });\n this.updated.clear();\n return data;\n }\n}\n","import { OutgoingHttpHeader } from \"http\";\n\nexport function parseContentLength(\n contentLength: OutgoingHttpHeader | undefined | null,\n): number | undefined {\n if (contentLength === undefined || contentLength === null) {\n return undefined;\n }\n if (typeof contentLength === \"number\") {\n return contentLength;\n }\n if (typeof contentLength === \"string\") {\n const parsed = parseInt(contentLength);\n return isNaN(parsed) ? undefined : parsed;\n }\n if (Array.isArray(contentLength)) {\n return parseContentLength(contentLength[0]);\n }\n return undefined;\n}\n\nexport function mergeHeaders(base: Headers, merge: Headers) {\n const mergedHeaders = new Headers(base);\n for (const [name, value] of merge)\n if (name === \"set-cookie\") mergedHeaders.append(name, value);\n else mergedHeaders.set(name, value);\n return mergedHeaders;\n}\n","import AsyncLock from \"async-lock\";\nimport { Buffer } from \"buffer\";\nimport { randomUUID } from \"crypto\";\nimport { unlinkSync, writeFileSync } from \"fs\";\nimport { IncomingHttpHeaders, OutgoingHttpHeaders } from \"http\";\nimport { tmpdir } from \"os\";\nimport { join } from \"path\";\n\nimport { getSentryEventId } from \"./sentry.js\";\nimport {\n truncateExceptionMessage,\n truncateExceptionStackTrace,\n} from \"./serverErrorCounter.js\";\nimport TempGzipFile from \"./tempGzipFile.js\";\n\nconst MAX_BODY_SIZE = 50_000; // 50 KB (uncompressed)\nconst MAX_FILE_SIZE = 1_000_000; // 1 MB (compressed)\nconst MAX_FILES = 50;\nconst MAX_PENDING_WRITES = 100;\nconst BODY_TOO_LARGE = Buffer.from(\"<body too large>\");\nconst BODY_MASKED = Buffer.from(\"<masked>\");\nconst MASKED = \"******\";\nconst ALLOWED_CONTENT_TYPES = [\n \"application/json\",\n \"application/problem+json\",\n \"application/vnd.api+json\",\n \"text/plain\",\n];\nconst EXCLUDE_PATH_PATTERNS = [\n /\\/_?healthz?$/i,\n /\\/_?health[_-]?checks?$/i,\n /\\/_?heart[_-]?beats?$/i,\n /\\/ping$/i,\n /\\/ready$/i,\n /\\/live$/i,\n];\nconst EXCLUDE_USER_AGENT_PATTERNS = [\n /health[-_ ]?check/i,\n /microsoft-azure-application-lb/i,\n /googlehc/i,\n /kube-probe/i,\n];\nconst MASK_QUERY_PARAM_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /password/i,\n /pwd/i,\n];\nconst MASK_HEADER_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /cookie/i,\n];\n\nexport type Request = {\n timestamp: number;\n method: string;\n path?: string;\n url: string;\n headers: [string, string][];\n size?: number;\n consumer?: string;\n body?: Buffer;\n};\n\nexport type Response = {\n statusCode: number;\n responseTime: number;\n headers: [string, string][];\n size?: number;\n body?: Buffer;\n};\n\nexport type RequestLoggingConfig = {\n enabled: boolean;\n logQueryParams: boolean;\n logRequestHeaders: boolean;\n logRequestBody: boolean;\n logResponseHeaders: boolean;\n logResponseBody: boolean;\n logException: boolean;\n maskQueryParams: RegExp[];\n maskHeaders: RegExp[];\n maskRequestBodyCallback?: (request: Request) => Buffer | null | undefined;\n maskResponseBodyCallback?: (\n request: Request,\n response: Response,\n ) => Buffer | null | undefined;\n excludePaths: RegExp[];\n excludeCallback?: (request: Request, response: Response) => boolean;\n};\n\nconst DEFAULT_CONFIG: RequestLoggingConfig = {\n enabled: false,\n logQueryParams: true,\n logRequestHeaders: false,\n logRequestBody: false,\n logResponseHeaders: true,\n logResponseBody: false,\n logException: true,\n maskQueryParams: [],\n maskHeaders: [],\n excludePaths: [],\n};\n\nexport default class RequestLogger {\n public config: RequestLoggingConfig;\n public enabled: boolean;\n public suspendUntil: number | null = null;\n private pendingWrites: string[] = [];\n private currentFile: TempGzipFile | null = null;\n private files: TempGzipFile[] = [];\n private maintainIntervalId?: NodeJS.Timeout;\n private lock = new AsyncLock();\n\n constructor(config?: Partial<RequestLoggingConfig>) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.enabled = this.config.enabled && checkWritableFs();\n\n if (this.enabled) {\n this.maintainIntervalId = setInterval(() => {\n this.maintain();\n }, 1000);\n }\n }\n\n get maxBodySize() {\n return MAX_BODY_SIZE;\n }\n\n private shouldExcludePath(urlPath: string) {\n const patterns = [...this.config.excludePaths, ...EXCLUDE_PATH_PATTERNS];\n return matchPatterns(urlPath, patterns);\n }\n\n private shouldExcludeUserAgent(userAgent?: string) {\n return userAgent\n ? matchPatterns(userAgent, EXCLUDE_USER_AGENT_PATTERNS)\n : false;\n }\n\n private shouldMaskQueryParam(name: string) {\n const patterns = [\n ...this.config.maskQueryParams,\n ...MASK_QUERY_PARAM_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskHeader(name: string) {\n const patterns = [...this.config.maskHeaders, ...MASK_HEADER_PATTERNS];\n return matchPatterns(name, patterns);\n }\n\n private hasSupportedContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return this.isSupportedContentType(contentType);\n }\n\n public isSupportedContentType(contentType?: string | null) {\n return (\n typeof contentType === \"string\" &&\n ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t))\n );\n }\n\n private maskQueryParams(search: string) {\n const params = new URLSearchParams(search);\n for (const [key] of params) {\n if (this.shouldMaskQueryParam(key)) {\n params.set(key, MASKED);\n }\n }\n return params.toString();\n }\n\n private maskHeaders(headers: [string, string][]): [string, string][] {\n return headers.map(([k, v]) => [k, this.shouldMaskHeader(k) ? MASKED : v]);\n }\n\n logRequest(request: Request, response: Response, error?: Error) {\n if (!this.enabled || this.suspendUntil !== null) return;\n\n const url = new URL(request.url);\n const path = request.path ?? url.pathname;\n const userAgent = request.headers.find(\n ([k]) => k.toLowerCase() === \"user-agent\",\n )?.[1];\n\n if (\n this.shouldExcludePath(path) ||\n this.shouldExcludeUserAgent(userAgent) ||\n (this.config.excludeCallback?.(request, response) ?? false)\n ) {\n return;\n }\n\n // Process query params\n url.search = this.config.logQueryParams\n ? this.maskQueryParams(url.search)\n : \"\";\n request.url = url.toString();\n\n // Process request body\n if (\n !this.config.logRequestBody ||\n !this.hasSupportedContentType(request.headers)\n ) {\n request.body = undefined;\n } else if (request.body) {\n if (request.body.length > MAX_BODY_SIZE) {\n request.body = BODY_TOO_LARGE;\n } else if (this.config.maskRequestBodyCallback) {\n try {\n request.body =\n this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;\n if (request.body.length > MAX_BODY_SIZE) {\n request.body = BODY_TOO_LARGE;\n }\n } catch {\n request.body = undefined;\n }\n }\n }\n\n // Process response body\n if (\n !this.config.logResponseBody ||\n !this.hasSupportedContentType(response.headers)\n ) {\n response.body = undefined;\n } else if (response.body) {\n if (response.body.length > MAX_BODY_SIZE) {\n response.body = BODY_TOO_LARGE;\n } else if (this.config.maskResponseBodyCallback) {\n try {\n response.body =\n this.config.maskResponseBodyCallback(request, response) ??\n BODY_MASKED;\n if (response.body.length > MAX_BODY_SIZE) {\n response.body = BODY_TOO_LARGE;\n }\n } catch {\n response.body = undefined;\n }\n }\n }\n\n // Process headers\n request.headers = this.config.logRequestHeaders\n ? this.maskHeaders(request.headers)\n : [];\n response.headers = this.config.logResponseHeaders\n ? this.maskHeaders(response.headers)\n : [];\n\n const item = {\n uuid: randomUUID(),\n request: skipEmptyValues(request),\n response: skipEmptyValues(response),\n exception:\n error && this.config.logException\n ? {\n type: error.name,\n message: truncateExceptionMessage(error.message),\n stacktrace: truncateExceptionStackTrace(error.stack || \"\"),\n sentryEventId: getSentryEventId(),\n }\n : null,\n };\n [item.request.body, item.response.body].forEach((body) => {\n if (body) {\n // @ts-expect-error Different return type\n body.toJSON = function () {\n return this.toString(\"base64\");\n };\n }\n });\n this.pendingWrites.push(JSON.stringify(item));\n\n if (this.pendingWrites.length > MAX_PENDING_WRITES) {\n this.pendingWrites.shift();\n }\n }\n\n async writeToFile() {\n if (!this.enabled || this.pendingWrites.length === 0) {\n return;\n }\n return this.lock.acquire(\"file\", async () => {\n if (!this.currentFile) {\n this.currentFile = new TempGzipFile();\n }\n while (this.pendingWrites.length > 0) {\n const item = this.pendingWrites.shift();\n if (item) {\n await this.currentFile.writeLine(Buffer.from(item));\n }\n }\n });\n }\n\n getFile() {\n return this.files.shift();\n }\n\n retryFileLater(file: TempGzipFile) {\n this.files.unshift(file);\n }\n\n async rotateFile() {\n return this.lock.acquire(\"file\", async () => {\n if (this.currentFile) {\n await this.currentFile.close();\n this.files.push(this.currentFile);\n this.currentFile = null;\n }\n });\n }\n\n async maintain() {\n await this.writeToFile();\n if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {\n await this.rotateFile();\n }\n while (this.files.length > MAX_FILES) {\n const file = this.files.shift();\n file?.delete();\n }\n if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {\n this.suspendUntil = null;\n }\n }\n\n async clear() {\n this.pendingWrites = [];\n await this.rotateFile();\n this.files.forEach((file) => {\n file.delete();\n });\n this.files = [];\n }\n\n async close() {\n this.enabled = false;\n await this.clear();\n if (this.maintainIntervalId) {\n clearInterval(this.maintainIntervalId);\n }\n }\n}\n\nexport function convertHeaders(\n headers:\n | Headers\n | IncomingHttpHeaders\n | OutgoingHttpHeaders\n | Record<string, string | string[] | number | undefined>,\n) {\n if (headers instanceof Headers) {\n return Array.from(headers.entries());\n }\n return Object.entries(headers).flatMap(([key, value]) => {\n if (value === undefined) {\n return [];\n }\n if (Array.isArray(value)) {\n return value.map((v) => [key, v]);\n }\n return [[key, value.toString()]];\n }) as [string, string][];\n}\n\nexport function convertBody(body: any, contentType?: string | null) {\n if (!body || !contentType) {\n return;\n }\n try {\n if (contentType.startsWith(\"application/json\")) {\n if (isValidJsonString(body)) {\n return Buffer.from(body);\n } else {\n return Buffer.from(JSON.stringify(body));\n }\n }\n if (contentType.startsWith(\"text/\") && typeof body === \"string\") {\n return Buffer.from(body);\n }\n } catch (error) {\n return;\n }\n}\n\nfunction isValidJsonString(body: any) {\n if (typeof body !== \"string\") {\n return false;\n }\n try {\n JSON.parse(body);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction matchPatterns(value: string, patterns: RegExp[]) {\n return patterns.some((pattern) => {\n return pattern.test(value);\n });\n}\n\nfunction skipEmptyValues<T extends Record<string, any>>(data: T) {\n return Object.fromEntries(\n Object.entries(data).filter(([_, v]) => {\n if (v == null || Number.isNaN(v)) return false;\n if (Array.isArray(v) || Buffer.isBuffer(v) || typeof v === \"string\") {\n return v.length > 0;\n }\n return true;\n }),\n ) as Partial<T>;\n}\n\nfunction checkWritableFs() {\n try {\n const testPath = join(tmpdir(), `apitally-${randomUUID()}`);\n writeFileSync(testPath, \"test\");\n unlinkSync(testPath);\n return true;\n } catch (error) {\n return false;\n }\n}\n","import type * as Sentry from \"@sentry/node\";\n\nlet sentry: typeof Sentry | undefined;\n\n// Initialize Sentry when the module is loaded\n(async () => {\n try {\n sentry = await import(\"@sentry/node\");\n } catch (e) {\n // Sentry SDK is not installed, ignore\n }\n})();\n\n/**\n * Returns the last Sentry event ID if available\n */\nexport function getSentryEventId(): string | undefined {\n if (sentry && sentry.lastEventId) {\n return sentry.lastEventId();\n }\n return undefined;\n}\n"],"mappings":"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","names":["consumerFromStringOrObject","consumer","String","trim","substring","identifier","name","group","parseContentLength","contentLength","undefined","parsed","parseInt","isNaN","Array","isArray","sentry","e","BODY_TOO_LARGE","Buffer","from","BODY_MASKED","convertHeaders","headers","Headers","Array","from","entries","Object","flatMap","key","value","undefined","isArray","map","v","toString","ApitallyMiddleware","handle","ctx","next","client","containerResolver","make","isEnabled","path","route","pattern","timestamp","Date","now","startTime","performance","responseTime","requestSize","parseContentLength","request","header","requestContentType","toString","responseStatus","response","getStatus","responseHeaders","getHeaders","responseSize","responseContentType","consumer","apitallyConsumer","consumerFromStringOrObject","consumerRegistry","addOrUpdateConsumer","onWriteHead","statusCode","headers","_a","requestCounter","addRequest","identifier","method","apitallyError","code","Array","isArray","messages","forEach","message","validationErrorCounter","addValidationError","loc","field","msg","type","rule","serverErrorCounter","addServerError","name","traceback","stack","originalWriteHead","writeHead","args","apply","requestLogger","enabled","onEnd","chunk","requestBody","config","logRequestBody","isSupportedContentType","raw","undefined","responseBody","logResponseBody","logRequest","url","completeUrl","convertHeaders","size","body","Buffer","from","originalEnd","end"]}
1
+ {"version":3,"sources":["../../src/adonisjs/middleware.ts","../../src/common/consumerRegistry.ts","../../src/common/headers.ts","../../src/common/requestLogger.ts","../../src/common/sentry.ts"],"sourcesContent":["import { HttpContext } from \"@adonisjs/core/http\";\nimport { NextFn } from \"@adonisjs/core/types/http\";\nimport type { OutgoingHttpHeaders } from \"http\";\nimport { performance } from \"perf_hooks\";\n\nimport type { ApitallyClient } from \"../common/client.js\";\nimport { consumerFromStringOrObject } from \"../common/consumerRegistry.js\";\nimport { parseContentLength } from \"../common/headers.js\";\nimport { convertHeaders } from \"../common/requestLogger.js\";\nimport type { ApitallyConsumer } from \"../common/types.js\";\n\ndeclare module \"@adonisjs/core/http\" {\n interface HttpContext {\n apitallyConsumer?: ApitallyConsumer | string;\n apitallyError?: Error;\n }\n}\n\nexport default class ApitallyMiddleware {\n async handle(ctx: HttpContext, next: NextFn) {\n const client: ApitallyClient =\n await ctx.containerResolver.make(\"apitallyClient\");\n\n if (!client.isEnabled()) {\n await next();\n return;\n }\n\n const path = ctx.route?.pattern;\n const timestamp = Date.now() / 1000;\n const startTime = performance.now();\n\n await next();\n\n const responseTime = performance.now() - startTime;\n const requestSize = parseContentLength(\n ctx.request.header(\"content-length\"),\n );\n const requestContentType = ctx.request.header(\"content-type\")?.toString();\n let responseStatus = ctx.response.getStatus();\n let responseHeaders = ctx.response.getHeaders();\n let responseSize: number | undefined;\n let responseContentType: string | undefined;\n\n const consumer = ctx.apitallyConsumer\n ? consumerFromStringOrObject(ctx.apitallyConsumer)\n : null;\n client.consumerRegistry.addOrUpdateConsumer(consumer);\n\n const onWriteHead = (statusCode: number, headers: OutgoingHttpHeaders) => {\n responseStatus = statusCode;\n responseHeaders = headers;\n responseSize = parseContentLength(headers[\"content-length\"]);\n responseContentType = headers[\"content-type\"]?.toString();\n if (path) {\n client.requestCounter.addRequest({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n statusCode: responseStatus,\n responseTime,\n requestSize,\n responseSize,\n });\n\n if (\n responseStatus === 422 &&\n ctx.apitallyError &&\n \"code\" in ctx.apitallyError &&\n \"messages\" in ctx.apitallyError &&\n ctx.apitallyError.code === \"E_VALIDATION_ERROR\" &&\n Array.isArray(ctx.apitallyError.messages)\n ) {\n ctx.apitallyError.messages.forEach((message) => {\n client.validationErrorCounter.addValidationError({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n loc: message.field,\n msg: message.message,\n type: message.rule,\n });\n });\n }\n\n if (responseStatus === 500 && ctx.apitallyError) {\n client.serverErrorCounter.addServerError({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n type: ctx.apitallyError.name,\n msg: ctx.apitallyError.message,\n traceback: ctx.apitallyError.stack || \"\",\n });\n }\n }\n };\n\n // Capture the final status code and response headers just before they are sent\n const originalWriteHead = ctx.response.response.writeHead;\n ctx.response.response.writeHead = (...args: any) => {\n originalWriteHead.apply(ctx.response.response, args);\n onWriteHead(args[0], typeof args[1] === \"string\" ? args[2] : args[1]);\n return ctx.response.response;\n };\n\n if (client.requestLogger.enabled) {\n const onEnd = (chunk: any) => {\n const requestBody =\n client.requestLogger.config.logRequestBody &&\n client.requestLogger.isSupportedContentType(requestContentType)\n ? ctx.request.raw()\n : undefined;\n const responseBody =\n client.requestLogger.config.logResponseBody &&\n client.requestLogger.isSupportedContentType(responseContentType)\n ? chunk\n : undefined;\n\n client.requestLogger.logRequest(\n {\n timestamp,\n method: ctx.request.method(),\n path,\n url: ctx.request.completeUrl(true),\n headers: convertHeaders(ctx.request.headers()),\n size: requestSize,\n consumer: consumer?.identifier,\n body: requestBody ? Buffer.from(requestBody) : undefined,\n },\n {\n statusCode: responseStatus,\n responseTime: responseTime / 1000,\n headers: convertHeaders(responseHeaders),\n size: responseSize,\n body: responseBody ? Buffer.from(responseBody) : undefined,\n },\n ctx.apitallyError,\n );\n };\n\n // Capture the final response body just before it is sent\n const originalEnd = ctx.response.response.end;\n ctx.response.response.end = (...args: any) => {\n originalEnd.apply(ctx.response.response, args);\n onEnd(typeof args[0] !== \"function\" ? args[0] : undefined);\n return ctx.response.response;\n };\n }\n }\n}\n","import { ApitallyConsumer } from \"./types.js\";\n\nexport const consumerFromStringOrObject = (\n consumer: ApitallyConsumer | string,\n) => {\n if (typeof consumer === \"string\") {\n consumer = String(consumer).trim().substring(0, 128);\n return consumer ? { identifier: consumer } : null;\n } else {\n consumer.identifier = String(consumer.identifier).trim().substring(0, 128);\n consumer.name = consumer.name?.trim().substring(0, 64);\n consumer.group = consumer.group?.trim().substring(0, 64);\n return consumer.identifier ? consumer : null;\n }\n};\n\nexport default class ConsumerRegistry {\n private consumers: Map<string, ApitallyConsumer>;\n private updated: Set<string>;\n\n constructor() {\n this.consumers = new Map();\n this.updated = new Set();\n }\n\n public addOrUpdateConsumer(consumer?: ApitallyConsumer | null) {\n if (!consumer || (!consumer.name && !consumer.group)) {\n return;\n }\n const existing = this.consumers.get(consumer.identifier);\n if (!existing) {\n this.consumers.set(consumer.identifier, consumer);\n this.updated.add(consumer.identifier);\n } else {\n if (consumer.name && consumer.name !== existing.name) {\n existing.name = consumer.name;\n this.updated.add(consumer.identifier);\n }\n if (consumer.group && consumer.group !== existing.group) {\n existing.group = consumer.group;\n this.updated.add(consumer.identifier);\n }\n }\n }\n\n public getAndResetUpdatedConsumers() {\n const data: Array<ApitallyConsumer> = [];\n this.updated.forEach((identifier) => {\n const consumer = this.consumers.get(identifier);\n if (consumer) {\n data.push(consumer);\n }\n });\n this.updated.clear();\n return data;\n }\n}\n","import { OutgoingHttpHeader } from \"http\";\n\nexport function parseContentLength(\n contentLength: OutgoingHttpHeader | undefined | null,\n): number | undefined {\n if (contentLength === undefined || contentLength === null) {\n return undefined;\n }\n if (typeof contentLength === \"number\") {\n return contentLength;\n }\n if (typeof contentLength === \"string\") {\n const parsed = parseInt(contentLength);\n return isNaN(parsed) ? undefined : parsed;\n }\n if (Array.isArray(contentLength)) {\n return parseContentLength(contentLength[0]);\n }\n return undefined;\n}\n\nexport function mergeHeaders(base: Headers, merge: Headers) {\n const mergedHeaders = new Headers(base);\n for (const [name, value] of merge)\n if (name === \"set-cookie\") mergedHeaders.append(name, value);\n else mergedHeaders.set(name, value);\n return mergedHeaders;\n}\n","import AsyncLock from \"async-lock\";\nimport { Buffer } from \"buffer\";\nimport { randomUUID } from \"crypto\";\nimport { unlinkSync, writeFileSync } from \"fs\";\nimport { IncomingHttpHeaders, OutgoingHttpHeaders } from \"http\";\nimport { tmpdir } from \"os\";\nimport { join } from \"path\";\n\nimport { getSentryEventId } from \"./sentry.js\";\nimport {\n truncateExceptionMessage,\n truncateExceptionStackTrace,\n} from \"./serverErrorCounter.js\";\nimport TempGzipFile from \"./tempGzipFile.js\";\n\nconst MAX_BODY_SIZE = 50_000; // 50 KB (uncompressed)\nconst MAX_FILE_SIZE = 1_000_000; // 1 MB (compressed)\nconst MAX_FILES = 50;\nconst MAX_PENDING_WRITES = 100;\nconst BODY_TOO_LARGE = Buffer.from(\"<body too large>\");\nconst BODY_MASKED = Buffer.from(\"<masked>\");\nconst MASKED = \"******\";\nconst ALLOWED_CONTENT_TYPES = [\n \"application/json\",\n \"application/problem+json\",\n \"application/vnd.api+json\",\n \"text/plain\",\n];\nconst EXCLUDE_PATH_PATTERNS = [\n /\\/_?healthz?$/i,\n /\\/_?health[_-]?checks?$/i,\n /\\/_?heart[_-]?beats?$/i,\n /\\/ping$/i,\n /\\/ready$/i,\n /\\/live$/i,\n];\nconst EXCLUDE_USER_AGENT_PATTERNS = [\n /health[-_ ]?check/i,\n /microsoft-azure-application-lb/i,\n /googlehc/i,\n /kube-probe/i,\n];\nconst MASK_QUERY_PARAM_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /password/i,\n /pwd/i,\n];\nconst MASK_HEADER_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /cookie/i,\n];\nconst MASK_BODY_FIELD_PATTERNS = [\n /password/i,\n /pwd/i,\n /token/i,\n /secret/i,\n /auth/i,\n /card[-_ ]?number/i,\n /ccv/i,\n /ssn/i,\n];\n\nexport type Request = {\n timestamp: number;\n method: string;\n path?: string;\n url: string;\n headers: [string, string][];\n size?: number;\n consumer?: string;\n body?: Buffer;\n};\n\nexport type Response = {\n statusCode: number;\n responseTime: number;\n headers: [string, string][];\n size?: number;\n body?: Buffer;\n};\n\nexport type RequestLoggingConfig = {\n enabled: boolean;\n logQueryParams: boolean;\n logRequestHeaders: boolean;\n logRequestBody: boolean;\n logResponseHeaders: boolean;\n logResponseBody: boolean;\n logException: boolean;\n maskQueryParams: RegExp[];\n maskHeaders: RegExp[];\n maskBodyFields: RegExp[];\n maskRequestBodyCallback?: (request: Request) => Buffer | null | undefined;\n maskResponseBodyCallback?: (\n request: Request,\n response: Response,\n ) => Buffer | null | undefined;\n excludePaths: RegExp[];\n excludeCallback?: (request: Request, response: Response) => boolean;\n};\n\nconst DEFAULT_CONFIG: RequestLoggingConfig = {\n enabled: false,\n logQueryParams: true,\n logRequestHeaders: false,\n logRequestBody: false,\n logResponseHeaders: true,\n logResponseBody: false,\n logException: true,\n maskQueryParams: [],\n maskHeaders: [],\n maskBodyFields: [],\n excludePaths: [],\n};\n\ntype RequestLogItem = {\n uuid: string;\n request: Request;\n response: Response;\n exception?: {\n type: string;\n message: string;\n stacktrace: string;\n sentryEventId?: string;\n };\n};\n\nexport default class RequestLogger {\n public config: RequestLoggingConfig;\n public enabled: boolean;\n public suspendUntil: number | null = null;\n private pendingWrites: RequestLogItem[] = [];\n private currentFile: TempGzipFile | null = null;\n private files: TempGzipFile[] = [];\n private maintainIntervalId?: NodeJS.Timeout;\n private lock = new AsyncLock();\n\n constructor(config?: Partial<RequestLoggingConfig>) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.enabled = this.config.enabled && checkWritableFs();\n\n if (this.enabled) {\n this.maintainIntervalId = setInterval(() => {\n this.maintain();\n }, 1000);\n }\n }\n\n get maxBodySize() {\n return MAX_BODY_SIZE;\n }\n\n private shouldExcludePath(urlPath: string) {\n const patterns = [...this.config.excludePaths, ...EXCLUDE_PATH_PATTERNS];\n return matchPatterns(urlPath, patterns);\n }\n\n private shouldExcludeUserAgent(userAgent?: string) {\n return userAgent\n ? matchPatterns(userAgent, EXCLUDE_USER_AGENT_PATTERNS)\n : false;\n }\n\n private shouldMaskQueryParam(name: string) {\n const patterns = [\n ...this.config.maskQueryParams,\n ...MASK_QUERY_PARAM_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskHeader(name: string) {\n const patterns = [...this.config.maskHeaders, ...MASK_HEADER_PATTERNS];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskBodyField(name: string) {\n const patterns = [\n ...this.config.maskBodyFields,\n ...MASK_BODY_FIELD_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private hasSupportedContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return this.isSupportedContentType(contentType);\n }\n\n private hasJsonContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return contentType ? /\\bjson\\b/i.test(contentType) : null;\n }\n\n public isSupportedContentType(contentType?: string | null) {\n return (\n typeof contentType === \"string\" &&\n ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t))\n );\n }\n\n private maskQueryParams(search: string) {\n const params = new URLSearchParams(search);\n for (const [key] of params) {\n if (this.shouldMaskQueryParam(key)) {\n params.set(key, MASKED);\n }\n }\n return params.toString();\n }\n\n private maskHeaders(headers: [string, string][]): [string, string][] {\n return headers.map(([k, v]) => [k, this.shouldMaskHeader(k) ? MASKED : v]);\n }\n\n private maskBody(data: any): any {\n if (typeof data === \"object\" && data !== null && !Array.isArray(data)) {\n const result: any = {};\n for (const [key, value] of Object.entries(data)) {\n if (typeof value === \"string\" && this.shouldMaskBodyField(key)) {\n result[key] = MASKED;\n } else {\n result[key] = this.maskBody(value);\n }\n }\n return result;\n }\n if (Array.isArray(data)) {\n return data.map((item) => this.maskBody(item));\n }\n return data;\n }\n\n private applyMasking(item: RequestLogItem) {\n // Apply user-provided maskRequestBodyCallback function\n if (\n this.config.maskRequestBodyCallback &&\n item.request.body &&\n item.request.body !== BODY_TOO_LARGE\n ) {\n try {\n const maskedBody = this.config.maskRequestBodyCallback(item.request);\n item.request.body = maskedBody ?? BODY_MASKED;\n } catch {\n item.request.body = undefined;\n }\n }\n\n // Apply user-provided maskResponseBodyCallback function\n if (\n this.config.maskResponseBodyCallback &&\n item.response.body &&\n item.response.body !== BODY_TOO_LARGE\n ) {\n try {\n const maskedBody = this.config.maskResponseBodyCallback(\n item.request,\n item.response,\n );\n item.response.body = maskedBody ?? BODY_MASKED;\n } catch {\n item.response.body = undefined;\n }\n }\n\n // Check request and response body sizes\n if (item.request.body && item.request.body.length > MAX_BODY_SIZE) {\n item.request.body = BODY_TOO_LARGE;\n }\n if (item.response.body && item.response.body.length > MAX_BODY_SIZE) {\n item.response.body = BODY_TOO_LARGE;\n }\n\n // Mask request and response body fields\n for (const key of [\"request\", \"response\"] as const) {\n const bodyData = item[key].body;\n if (\n !bodyData ||\n bodyData === BODY_TOO_LARGE ||\n bodyData === BODY_MASKED\n ) {\n continue;\n }\n\n const headers = item[key].headers;\n const hasJsonContent = this.hasJsonContentType(headers);\n if (hasJsonContent === null || hasJsonContent) {\n try {\n const parsedBody = JSON.parse(bodyData.toString());\n const maskedBody = this.maskBody(parsedBody);\n item[key].body = Buffer.from(JSON.stringify(maskedBody));\n } catch {\n // If parsing fails, leave body as is\n }\n }\n }\n\n // Mask request and response headers\n item.request.headers = this.config.logRequestHeaders\n ? this.maskHeaders(item.request.headers)\n : [];\n item.response.headers = this.config.logResponseHeaders\n ? this.maskHeaders(item.response.headers)\n : [];\n\n // Mask query params\n const url = new URL(item.request.url);\n url.search = this.config.logQueryParams\n ? this.maskQueryParams(url.search)\n : \"\";\n item.request.url = url.toString();\n\n return item;\n }\n\n logRequest(request: Request, response: Response, error?: Error) {\n if (!this.enabled || this.suspendUntil !== null) return;\n\n const url = new URL(request.url);\n const path = request.path ?? url.pathname;\n const userAgent = request.headers.find(\n ([k]) => k.toLowerCase() === \"user-agent\",\n )?.[1];\n\n if (\n this.shouldExcludePath(path) ||\n this.shouldExcludeUserAgent(userAgent) ||\n (this.config.excludeCallback?.(request, response) ?? false)\n ) {\n return;\n }\n\n if (\n !this.config.logRequestBody ||\n !this.hasSupportedContentType(request.headers)\n ) {\n request.body = undefined;\n }\n if (\n !this.config.logResponseBody ||\n !this.hasSupportedContentType(response.headers)\n ) {\n response.body = undefined;\n }\n\n if (request.size !== undefined && request.size < 0) {\n request.size = undefined;\n }\n if (response.size !== undefined && response.size < 0) {\n response.size = undefined;\n }\n\n const item: RequestLogItem = {\n uuid: randomUUID(),\n request: request,\n response: response,\n exception:\n error && this.config.logException\n ? {\n type: error.name,\n message: truncateExceptionMessage(error.message),\n stacktrace: truncateExceptionStackTrace(error.stack || \"\"),\n sentryEventId: getSentryEventId(),\n }\n : undefined,\n };\n this.pendingWrites.push(item);\n\n if (this.pendingWrites.length > MAX_PENDING_WRITES) {\n this.pendingWrites.shift();\n }\n }\n\n async writeToFile() {\n if (!this.enabled || this.pendingWrites.length === 0) {\n return;\n }\n return this.lock.acquire(\"file\", async () => {\n if (!this.currentFile) {\n this.currentFile = new TempGzipFile();\n }\n while (this.pendingWrites.length > 0) {\n let item = this.pendingWrites.shift();\n if (item) {\n item = this.applyMasking(item);\n\n const finalItem = {\n uuid: item.uuid,\n request: skipEmptyValues(item.request),\n response: skipEmptyValues(item.response),\n exception: item.exception,\n };\n\n // Set up body serialization for JSON\n [finalItem.request.body, finalItem.response.body].forEach((body) => {\n if (body) {\n // @ts-expect-error Override Buffer's default JSON serialization\n body.toJSON = function () {\n return this.toString(\"base64\");\n };\n }\n });\n\n await this.currentFile.writeLine(\n Buffer.from(JSON.stringify(finalItem)),\n );\n }\n }\n });\n }\n\n getFile() {\n return this.files.shift();\n }\n\n retryFileLater(file: TempGzipFile) {\n this.files.unshift(file);\n }\n\n async rotateFile() {\n return this.lock.acquire(\"file\", async () => {\n if (this.currentFile) {\n await this.currentFile.close();\n this.files.push(this.currentFile);\n this.currentFile = null;\n }\n });\n }\n\n async maintain() {\n await this.writeToFile();\n if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {\n await this.rotateFile();\n }\n while (this.files.length > MAX_FILES) {\n const file = this.files.shift();\n file?.delete();\n }\n if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {\n this.suspendUntil = null;\n }\n }\n\n async clear() {\n this.pendingWrites = [];\n await this.rotateFile();\n this.files.forEach((file) => {\n file.delete();\n });\n this.files = [];\n }\n\n async close() {\n this.enabled = false;\n await this.clear();\n if (this.maintainIntervalId) {\n clearInterval(this.maintainIntervalId);\n }\n }\n}\n\nexport function convertHeaders(\n headers:\n | Headers\n | IncomingHttpHeaders\n | OutgoingHttpHeaders\n | Record<string, string | string[] | number | undefined>,\n) {\n if (headers instanceof Headers) {\n return Array.from(headers.entries());\n }\n return Object.entries(headers).flatMap(([key, value]) => {\n if (value === undefined) {\n return [];\n }\n if (Array.isArray(value)) {\n return value.map((v) => [key, v]);\n }\n return [[key, value.toString()]];\n }) as [string, string][];\n}\n\nexport function convertBody(body: any, contentType?: string | null) {\n if (!body || !contentType) {\n return;\n }\n try {\n if (contentType.startsWith(\"application/json\")) {\n if (isValidJsonString(body)) {\n return Buffer.from(body);\n } else {\n return Buffer.from(JSON.stringify(body));\n }\n }\n if (contentType.startsWith(\"text/\") && typeof body === \"string\") {\n return Buffer.from(body);\n }\n } catch (error) {\n return;\n }\n}\n\nfunction isValidJsonString(body: any) {\n if (typeof body !== \"string\") {\n return false;\n }\n try {\n JSON.parse(body);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction matchPatterns(value: string, patterns: RegExp[]) {\n return patterns.some((pattern) => {\n return pattern.test(value);\n });\n}\n\nfunction skipEmptyValues<T extends Record<string, any>>(data: T) {\n return Object.fromEntries(\n Object.entries(data).filter(([_, v]) => {\n if (v == null || Number.isNaN(v)) return false;\n if (Array.isArray(v) || Buffer.isBuffer(v) || typeof v === \"string\") {\n return v.length > 0;\n }\n return true;\n }),\n ) as Partial<T>;\n}\n\nfunction checkWritableFs() {\n try {\n const testPath = join(tmpdir(), `apitally-${randomUUID()}`);\n writeFileSync(testPath, \"test\");\n unlinkSync(testPath);\n return true;\n } catch (error) {\n return false;\n }\n}\n","import type * as Sentry from \"@sentry/node\";\n\nlet sentry: typeof Sentry | undefined;\n\n// Initialize Sentry when the module is loaded\n(async () => {\n try {\n sentry = await import(\"@sentry/node\");\n } catch (e) {\n // Sentry SDK is not installed, ignore\n }\n})();\n\n/**\n * Returns the last Sentry event ID if available\n */\nexport function getSentryEventId(): string | undefined {\n if (sentry && sentry.lastEventId) {\n return sentry.lastEventId();\n }\n return undefined;\n}\n"],"mappings":"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","names":["consumerFromStringOrObject","consumer","String","trim","substring","identifier","name","group","parseContentLength","contentLength","undefined","parsed","parseInt","isNaN","Array","isArray","sentry","e","BODY_TOO_LARGE","Buffer","from","BODY_MASKED","convertHeaders","headers","Headers","Array","from","entries","Object","flatMap","key","value","undefined","isArray","map","v","toString","ApitallyMiddleware","handle","ctx","next","client","containerResolver","make","isEnabled","path","route","pattern","timestamp","Date","now","startTime","performance","responseTime","requestSize","parseContentLength","request","header","requestContentType","toString","responseStatus","response","getStatus","responseHeaders","getHeaders","responseSize","responseContentType","consumer","apitallyConsumer","consumerFromStringOrObject","consumerRegistry","addOrUpdateConsumer","onWriteHead","statusCode","headers","_a","requestCounter","addRequest","identifier","method","apitallyError","code","Array","isArray","messages","forEach","message","validationErrorCounter","addValidationError","loc","field","msg","type","rule","serverErrorCounter","addServerError","name","traceback","stack","originalWriteHead","writeHead","args","apply","requestLogger","enabled","onEnd","chunk","requestBody","config","logRequestBody","isSupportedContentType","raw","undefined","responseBody","logResponseBody","logRequest","url","completeUrl","convertHeaders","size","body","Buffer","from","originalEnd","end"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adonisjs/middleware.ts","../../src/common/consumerRegistry.ts","../../src/common/headers.ts","../../src/common/requestLogger.ts","../../src/common/sentry.ts"],"sourcesContent":["import { HttpContext } from \"@adonisjs/core/http\";\nimport { NextFn } from \"@adonisjs/core/types/http\";\nimport type { OutgoingHttpHeaders } from \"http\";\nimport { performance } from \"perf_hooks\";\n\nimport type { ApitallyClient } from \"../common/client.js\";\nimport { consumerFromStringOrObject } from \"../common/consumerRegistry.js\";\nimport { parseContentLength } from \"../common/headers.js\";\nimport { convertHeaders } from \"../common/requestLogger.js\";\nimport type { ApitallyConsumer } from \"../common/types.js\";\n\ndeclare module \"@adonisjs/core/http\" {\n interface HttpContext {\n apitallyConsumer?: ApitallyConsumer | string;\n apitallyError?: Error;\n }\n}\n\nexport default class ApitallyMiddleware {\n async handle(ctx: HttpContext, next: NextFn) {\n const client: ApitallyClient =\n await ctx.containerResolver.make(\"apitallyClient\");\n\n if (!client.isEnabled()) {\n await next();\n return;\n }\n\n const path = ctx.route?.pattern;\n const timestamp = Date.now() / 1000;\n const startTime = performance.now();\n\n await next();\n\n const responseTime = performance.now() - startTime;\n const requestSize = parseContentLength(\n ctx.request.header(\"content-length\"),\n );\n const requestContentType = ctx.request.header(\"content-type\")?.toString();\n let responseStatus = ctx.response.getStatus();\n let responseHeaders = ctx.response.getHeaders();\n let responseSize: number | undefined;\n let responseContentType: string | undefined;\n\n const consumer = ctx.apitallyConsumer\n ? consumerFromStringOrObject(ctx.apitallyConsumer)\n : null;\n client.consumerRegistry.addOrUpdateConsumer(consumer);\n\n const onWriteHead = (statusCode: number, headers: OutgoingHttpHeaders) => {\n responseStatus = statusCode;\n responseHeaders = headers;\n responseSize = parseContentLength(headers[\"content-length\"]);\n responseContentType = headers[\"content-type\"]?.toString();\n if (path) {\n client.requestCounter.addRequest({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n statusCode: responseStatus,\n responseTime,\n requestSize,\n responseSize,\n });\n\n if (\n responseStatus === 422 &&\n ctx.apitallyError &&\n \"code\" in ctx.apitallyError &&\n \"messages\" in ctx.apitallyError &&\n ctx.apitallyError.code === \"E_VALIDATION_ERROR\" &&\n Array.isArray(ctx.apitallyError.messages)\n ) {\n ctx.apitallyError.messages.forEach((message) => {\n client.validationErrorCounter.addValidationError({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n loc: message.field,\n msg: message.message,\n type: message.rule,\n });\n });\n }\n\n if (responseStatus === 500 && ctx.apitallyError) {\n client.serverErrorCounter.addServerError({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n type: ctx.apitallyError.name,\n msg: ctx.apitallyError.message,\n traceback: ctx.apitallyError.stack || \"\",\n });\n }\n }\n };\n\n // Capture the final status code and response headers just before they are sent\n const originalWriteHead = ctx.response.response.writeHead;\n ctx.response.response.writeHead = (...args: any) => {\n originalWriteHead.apply(ctx.response.response, args);\n onWriteHead(args[0], typeof args[1] === \"string\" ? args[2] : args[1]);\n return ctx.response.response;\n };\n\n if (client.requestLogger.enabled) {\n const onEnd = (chunk: any) => {\n const requestBody =\n client.requestLogger.config.logRequestBody &&\n client.requestLogger.isSupportedContentType(requestContentType)\n ? ctx.request.raw()\n : undefined;\n const responseBody =\n client.requestLogger.config.logResponseBody &&\n client.requestLogger.isSupportedContentType(responseContentType)\n ? chunk\n : undefined;\n\n client.requestLogger.logRequest(\n {\n timestamp,\n method: ctx.request.method(),\n path,\n url: ctx.request.completeUrl(true),\n headers: convertHeaders(ctx.request.headers()),\n size: requestSize,\n consumer: consumer?.identifier,\n body: requestBody ? Buffer.from(requestBody) : undefined,\n },\n {\n statusCode: responseStatus,\n responseTime: responseTime / 1000,\n headers: convertHeaders(responseHeaders),\n size: responseSize,\n body: responseBody ? Buffer.from(responseBody) : undefined,\n },\n ctx.apitallyError,\n );\n };\n\n // Capture the final response body just before it is sent\n const originalEnd = ctx.response.response.end;\n ctx.response.response.end = (...args: any) => {\n originalEnd.apply(ctx.response.response, args);\n onEnd(typeof args[0] !== \"function\" ? args[0] : undefined);\n return ctx.response.response;\n };\n }\n }\n}\n","import { ApitallyConsumer } from \"./types.js\";\n\nexport const consumerFromStringOrObject = (\n consumer: ApitallyConsumer | string,\n) => {\n if (typeof consumer === \"string\") {\n consumer = String(consumer).trim().substring(0, 128);\n return consumer ? { identifier: consumer } : null;\n } else {\n consumer.identifier = String(consumer.identifier).trim().substring(0, 128);\n consumer.name = consumer.name?.trim().substring(0, 64);\n consumer.group = consumer.group?.trim().substring(0, 64);\n return consumer.identifier ? consumer : null;\n }\n};\n\nexport default class ConsumerRegistry {\n private consumers: Map<string, ApitallyConsumer>;\n private updated: Set<string>;\n\n constructor() {\n this.consumers = new Map();\n this.updated = new Set();\n }\n\n public addOrUpdateConsumer(consumer?: ApitallyConsumer | null) {\n if (!consumer || (!consumer.name && !consumer.group)) {\n return;\n }\n const existing = this.consumers.get(consumer.identifier);\n if (!existing) {\n this.consumers.set(consumer.identifier, consumer);\n this.updated.add(consumer.identifier);\n } else {\n if (consumer.name && consumer.name !== existing.name) {\n existing.name = consumer.name;\n this.updated.add(consumer.identifier);\n }\n if (consumer.group && consumer.group !== existing.group) {\n existing.group = consumer.group;\n this.updated.add(consumer.identifier);\n }\n }\n }\n\n public getAndResetUpdatedConsumers() {\n const data: Array<ApitallyConsumer> = [];\n this.updated.forEach((identifier) => {\n const consumer = this.consumers.get(identifier);\n if (consumer) {\n data.push(consumer);\n }\n });\n this.updated.clear();\n return data;\n }\n}\n","import { OutgoingHttpHeader } from \"http\";\n\nexport function parseContentLength(\n contentLength: OutgoingHttpHeader | undefined | null,\n): number | undefined {\n if (contentLength === undefined || contentLength === null) {\n return undefined;\n }\n if (typeof contentLength === \"number\") {\n return contentLength;\n }\n if (typeof contentLength === \"string\") {\n const parsed = parseInt(contentLength);\n return isNaN(parsed) ? undefined : parsed;\n }\n if (Array.isArray(contentLength)) {\n return parseContentLength(contentLength[0]);\n }\n return undefined;\n}\n\nexport function mergeHeaders(base: Headers, merge: Headers) {\n const mergedHeaders = new Headers(base);\n for (const [name, value] of merge)\n if (name === \"set-cookie\") mergedHeaders.append(name, value);\n else mergedHeaders.set(name, value);\n return mergedHeaders;\n}\n","import AsyncLock from \"async-lock\";\nimport { Buffer } from \"buffer\";\nimport { randomUUID } from \"crypto\";\nimport { unlinkSync, writeFileSync } from \"fs\";\nimport { IncomingHttpHeaders, OutgoingHttpHeaders } from \"http\";\nimport { tmpdir } from \"os\";\nimport { join } from \"path\";\n\nimport { getSentryEventId } from \"./sentry.js\";\nimport {\n truncateExceptionMessage,\n truncateExceptionStackTrace,\n} from \"./serverErrorCounter.js\";\nimport TempGzipFile from \"./tempGzipFile.js\";\n\nconst MAX_BODY_SIZE = 50_000; // 50 KB (uncompressed)\nconst MAX_FILE_SIZE = 1_000_000; // 1 MB (compressed)\nconst MAX_FILES = 50;\nconst MAX_PENDING_WRITES = 100;\nconst BODY_TOO_LARGE = Buffer.from(\"<body too large>\");\nconst BODY_MASKED = Buffer.from(\"<masked>\");\nconst MASKED = \"******\";\nconst ALLOWED_CONTENT_TYPES = [\n \"application/json\",\n \"application/problem+json\",\n \"application/vnd.api+json\",\n \"text/plain\",\n];\nconst EXCLUDE_PATH_PATTERNS = [\n /\\/_?healthz?$/i,\n /\\/_?health[_-]?checks?$/i,\n /\\/_?heart[_-]?beats?$/i,\n /\\/ping$/i,\n /\\/ready$/i,\n /\\/live$/i,\n];\nconst EXCLUDE_USER_AGENT_PATTERNS = [\n /health[-_ ]?check/i,\n /microsoft-azure-application-lb/i,\n /googlehc/i,\n /kube-probe/i,\n];\nconst MASK_QUERY_PARAM_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /password/i,\n /pwd/i,\n];\nconst MASK_HEADER_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /cookie/i,\n];\n\nexport type Request = {\n timestamp: number;\n method: string;\n path?: string;\n url: string;\n headers: [string, string][];\n size?: number;\n consumer?: string;\n body?: Buffer;\n};\n\nexport type Response = {\n statusCode: number;\n responseTime: number;\n headers: [string, string][];\n size?: number;\n body?: Buffer;\n};\n\nexport type RequestLoggingConfig = {\n enabled: boolean;\n logQueryParams: boolean;\n logRequestHeaders: boolean;\n logRequestBody: boolean;\n logResponseHeaders: boolean;\n logResponseBody: boolean;\n logException: boolean;\n maskQueryParams: RegExp[];\n maskHeaders: RegExp[];\n maskRequestBodyCallback?: (request: Request) => Buffer | null | undefined;\n maskResponseBodyCallback?: (\n request: Request,\n response: Response,\n ) => Buffer | null | undefined;\n excludePaths: RegExp[];\n excludeCallback?: (request: Request, response: Response) => boolean;\n};\n\nconst DEFAULT_CONFIG: RequestLoggingConfig = {\n enabled: false,\n logQueryParams: true,\n logRequestHeaders: false,\n logRequestBody: false,\n logResponseHeaders: true,\n logResponseBody: false,\n logException: true,\n maskQueryParams: [],\n maskHeaders: [],\n excludePaths: [],\n};\n\nexport default class RequestLogger {\n public config: RequestLoggingConfig;\n public enabled: boolean;\n public suspendUntil: number | null = null;\n private pendingWrites: string[] = [];\n private currentFile: TempGzipFile | null = null;\n private files: TempGzipFile[] = [];\n private maintainIntervalId?: NodeJS.Timeout;\n private lock = new AsyncLock();\n\n constructor(config?: Partial<RequestLoggingConfig>) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.enabled = this.config.enabled && checkWritableFs();\n\n if (this.enabled) {\n this.maintainIntervalId = setInterval(() => {\n this.maintain();\n }, 1000);\n }\n }\n\n get maxBodySize() {\n return MAX_BODY_SIZE;\n }\n\n private shouldExcludePath(urlPath: string) {\n const patterns = [...this.config.excludePaths, ...EXCLUDE_PATH_PATTERNS];\n return matchPatterns(urlPath, patterns);\n }\n\n private shouldExcludeUserAgent(userAgent?: string) {\n return userAgent\n ? matchPatterns(userAgent, EXCLUDE_USER_AGENT_PATTERNS)\n : false;\n }\n\n private shouldMaskQueryParam(name: string) {\n const patterns = [\n ...this.config.maskQueryParams,\n ...MASK_QUERY_PARAM_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskHeader(name: string) {\n const patterns = [...this.config.maskHeaders, ...MASK_HEADER_PATTERNS];\n return matchPatterns(name, patterns);\n }\n\n private hasSupportedContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return this.isSupportedContentType(contentType);\n }\n\n public isSupportedContentType(contentType?: string | null) {\n return (\n typeof contentType === \"string\" &&\n ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t))\n );\n }\n\n private maskQueryParams(search: string) {\n const params = new URLSearchParams(search);\n for (const [key] of params) {\n if (this.shouldMaskQueryParam(key)) {\n params.set(key, MASKED);\n }\n }\n return params.toString();\n }\n\n private maskHeaders(headers: [string, string][]): [string, string][] {\n return headers.map(([k, v]) => [k, this.shouldMaskHeader(k) ? MASKED : v]);\n }\n\n logRequest(request: Request, response: Response, error?: Error) {\n if (!this.enabled || this.suspendUntil !== null) return;\n\n const url = new URL(request.url);\n const path = request.path ?? url.pathname;\n const userAgent = request.headers.find(\n ([k]) => k.toLowerCase() === \"user-agent\",\n )?.[1];\n\n if (\n this.shouldExcludePath(path) ||\n this.shouldExcludeUserAgent(userAgent) ||\n (this.config.excludeCallback?.(request, response) ?? false)\n ) {\n return;\n }\n\n // Process query params\n url.search = this.config.logQueryParams\n ? this.maskQueryParams(url.search)\n : \"\";\n request.url = url.toString();\n\n // Process request body\n if (\n !this.config.logRequestBody ||\n !this.hasSupportedContentType(request.headers)\n ) {\n request.body = undefined;\n } else if (request.body) {\n if (request.body.length > MAX_BODY_SIZE) {\n request.body = BODY_TOO_LARGE;\n } else if (this.config.maskRequestBodyCallback) {\n try {\n request.body =\n this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;\n if (request.body.length > MAX_BODY_SIZE) {\n request.body = BODY_TOO_LARGE;\n }\n } catch {\n request.body = undefined;\n }\n }\n }\n\n // Process response body\n if (\n !this.config.logResponseBody ||\n !this.hasSupportedContentType(response.headers)\n ) {\n response.body = undefined;\n } else if (response.body) {\n if (response.body.length > MAX_BODY_SIZE) {\n response.body = BODY_TOO_LARGE;\n } else if (this.config.maskResponseBodyCallback) {\n try {\n response.body =\n this.config.maskResponseBodyCallback(request, response) ??\n BODY_MASKED;\n if (response.body.length > MAX_BODY_SIZE) {\n response.body = BODY_TOO_LARGE;\n }\n } catch {\n response.body = undefined;\n }\n }\n }\n\n // Process headers\n request.headers = this.config.logRequestHeaders\n ? this.maskHeaders(request.headers)\n : [];\n response.headers = this.config.logResponseHeaders\n ? this.maskHeaders(response.headers)\n : [];\n\n const item = {\n uuid: randomUUID(),\n request: skipEmptyValues(request),\n response: skipEmptyValues(response),\n exception:\n error && this.config.logException\n ? {\n type: error.name,\n message: truncateExceptionMessage(error.message),\n stacktrace: truncateExceptionStackTrace(error.stack || \"\"),\n sentryEventId: getSentryEventId(),\n }\n : null,\n };\n [item.request.body, item.response.body].forEach((body) => {\n if (body) {\n // @ts-expect-error Different return type\n body.toJSON = function () {\n return this.toString(\"base64\");\n };\n }\n });\n this.pendingWrites.push(JSON.stringify(item));\n\n if (this.pendingWrites.length > MAX_PENDING_WRITES) {\n this.pendingWrites.shift();\n }\n }\n\n async writeToFile() {\n if (!this.enabled || this.pendingWrites.length === 0) {\n return;\n }\n return this.lock.acquire(\"file\", async () => {\n if (!this.currentFile) {\n this.currentFile = new TempGzipFile();\n }\n while (this.pendingWrites.length > 0) {\n const item = this.pendingWrites.shift();\n if (item) {\n await this.currentFile.writeLine(Buffer.from(item));\n }\n }\n });\n }\n\n getFile() {\n return this.files.shift();\n }\n\n retryFileLater(file: TempGzipFile) {\n this.files.unshift(file);\n }\n\n async rotateFile() {\n return this.lock.acquire(\"file\", async () => {\n if (this.currentFile) {\n await this.currentFile.close();\n this.files.push(this.currentFile);\n this.currentFile = null;\n }\n });\n }\n\n async maintain() {\n await this.writeToFile();\n if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {\n await this.rotateFile();\n }\n while (this.files.length > MAX_FILES) {\n const file = this.files.shift();\n file?.delete();\n }\n if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {\n this.suspendUntil = null;\n }\n }\n\n async clear() {\n this.pendingWrites = [];\n await this.rotateFile();\n this.files.forEach((file) => {\n file.delete();\n });\n this.files = [];\n }\n\n async close() {\n this.enabled = false;\n await this.clear();\n if (this.maintainIntervalId) {\n clearInterval(this.maintainIntervalId);\n }\n }\n}\n\nexport function convertHeaders(\n headers:\n | Headers\n | IncomingHttpHeaders\n | OutgoingHttpHeaders\n | Record<string, string | string[] | number | undefined>,\n) {\n if (headers instanceof Headers) {\n return Array.from(headers.entries());\n }\n return Object.entries(headers).flatMap(([key, value]) => {\n if (value === undefined) {\n return [];\n }\n if (Array.isArray(value)) {\n return value.map((v) => [key, v]);\n }\n return [[key, value.toString()]];\n }) as [string, string][];\n}\n\nexport function convertBody(body: any, contentType?: string | null) {\n if (!body || !contentType) {\n return;\n }\n try {\n if (contentType.startsWith(\"application/json\")) {\n if (isValidJsonString(body)) {\n return Buffer.from(body);\n } else {\n return Buffer.from(JSON.stringify(body));\n }\n }\n if (contentType.startsWith(\"text/\") && typeof body === \"string\") {\n return Buffer.from(body);\n }\n } catch (error) {\n return;\n }\n}\n\nfunction isValidJsonString(body: any) {\n if (typeof body !== \"string\") {\n return false;\n }\n try {\n JSON.parse(body);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction matchPatterns(value: string, patterns: RegExp[]) {\n return patterns.some((pattern) => {\n return pattern.test(value);\n });\n}\n\nfunction skipEmptyValues<T extends Record<string, any>>(data: T) {\n return Object.fromEntries(\n Object.entries(data).filter(([_, v]) => {\n if (v == null || Number.isNaN(v)) return false;\n if (Array.isArray(v) || Buffer.isBuffer(v) || typeof v === \"string\") {\n return v.length > 0;\n }\n return true;\n }),\n ) as Partial<T>;\n}\n\nfunction checkWritableFs() {\n try {\n const testPath = join(tmpdir(), `apitally-${randomUUID()}`);\n writeFileSync(testPath, \"test\");\n unlinkSync(testPath);\n return true;\n } catch (error) {\n return false;\n }\n}\n","import type * as Sentry from \"@sentry/node\";\n\nlet sentry: typeof Sentry | undefined;\n\n// Initialize Sentry when the module is loaded\n(async () => {\n try {\n sentry = await import(\"@sentry/node\");\n } catch (e) {\n // Sentry SDK is not installed, ignore\n }\n})();\n\n/**\n * Returns the last Sentry event ID if available\n */\nexport function getSentryEventId(): string | undefined {\n if (sentry && sentry.lastEventId) {\n return sentry.lastEventId();\n }\n return undefined;\n}\n"],"mappings":"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","names":["performance","consumerFromStringOrObject","consumer","String","trim","substring","identifier","name","group","parseContentLength","contentLength","undefined","parsed","parseInt","isNaN","Array","isArray","AsyncLock","Buffer","sentry","e","BODY_TOO_LARGE","Buffer","from","BODY_MASKED","convertHeaders","headers","Headers","Array","from","entries","Object","flatMap","key","value","undefined","isArray","map","v","toString","ApitallyMiddleware","handle","ctx","next","client","containerResolver","make","isEnabled","path","route","pattern","timestamp","Date","now","startTime","performance","responseTime","requestSize","parseContentLength","request","header","requestContentType","toString","responseStatus","response","getStatus","responseHeaders","getHeaders","responseSize","responseContentType","consumer","apitallyConsumer","consumerFromStringOrObject","consumerRegistry","addOrUpdateConsumer","onWriteHead","statusCode","headers","_a","requestCounter","addRequest","identifier","method","apitallyError","code","Array","isArray","messages","forEach","message","validationErrorCounter","addValidationError","loc","field","msg","type","rule","serverErrorCounter","addServerError","name","traceback","stack","originalWriteHead","writeHead","args","apply","requestLogger","enabled","onEnd","chunk","requestBody","config","logRequestBody","isSupportedContentType","raw","undefined","responseBody","logResponseBody","logRequest","url","completeUrl","convertHeaders","size","body","Buffer","from","originalEnd","end"]}
1
+ {"version":3,"sources":["../../src/adonisjs/middleware.ts","../../src/common/consumerRegistry.ts","../../src/common/headers.ts","../../src/common/requestLogger.ts","../../src/common/sentry.ts"],"sourcesContent":["import { HttpContext } from \"@adonisjs/core/http\";\nimport { NextFn } from \"@adonisjs/core/types/http\";\nimport type { OutgoingHttpHeaders } from \"http\";\nimport { performance } from \"perf_hooks\";\n\nimport type { ApitallyClient } from \"../common/client.js\";\nimport { consumerFromStringOrObject } from \"../common/consumerRegistry.js\";\nimport { parseContentLength } from \"../common/headers.js\";\nimport { convertHeaders } from \"../common/requestLogger.js\";\nimport type { ApitallyConsumer } from \"../common/types.js\";\n\ndeclare module \"@adonisjs/core/http\" {\n interface HttpContext {\n apitallyConsumer?: ApitallyConsumer | string;\n apitallyError?: Error;\n }\n}\n\nexport default class ApitallyMiddleware {\n async handle(ctx: HttpContext, next: NextFn) {\n const client: ApitallyClient =\n await ctx.containerResolver.make(\"apitallyClient\");\n\n if (!client.isEnabled()) {\n await next();\n return;\n }\n\n const path = ctx.route?.pattern;\n const timestamp = Date.now() / 1000;\n const startTime = performance.now();\n\n await next();\n\n const responseTime = performance.now() - startTime;\n const requestSize = parseContentLength(\n ctx.request.header(\"content-length\"),\n );\n const requestContentType = ctx.request.header(\"content-type\")?.toString();\n let responseStatus = ctx.response.getStatus();\n let responseHeaders = ctx.response.getHeaders();\n let responseSize: number | undefined;\n let responseContentType: string | undefined;\n\n const consumer = ctx.apitallyConsumer\n ? consumerFromStringOrObject(ctx.apitallyConsumer)\n : null;\n client.consumerRegistry.addOrUpdateConsumer(consumer);\n\n const onWriteHead = (statusCode: number, headers: OutgoingHttpHeaders) => {\n responseStatus = statusCode;\n responseHeaders = headers;\n responseSize = parseContentLength(headers[\"content-length\"]);\n responseContentType = headers[\"content-type\"]?.toString();\n if (path) {\n client.requestCounter.addRequest({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n statusCode: responseStatus,\n responseTime,\n requestSize,\n responseSize,\n });\n\n if (\n responseStatus === 422 &&\n ctx.apitallyError &&\n \"code\" in ctx.apitallyError &&\n \"messages\" in ctx.apitallyError &&\n ctx.apitallyError.code === \"E_VALIDATION_ERROR\" &&\n Array.isArray(ctx.apitallyError.messages)\n ) {\n ctx.apitallyError.messages.forEach((message) => {\n client.validationErrorCounter.addValidationError({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n loc: message.field,\n msg: message.message,\n type: message.rule,\n });\n });\n }\n\n if (responseStatus === 500 && ctx.apitallyError) {\n client.serverErrorCounter.addServerError({\n consumer: consumer?.identifier,\n method: ctx.request.method(),\n path,\n type: ctx.apitallyError.name,\n msg: ctx.apitallyError.message,\n traceback: ctx.apitallyError.stack || \"\",\n });\n }\n }\n };\n\n // Capture the final status code and response headers just before they are sent\n const originalWriteHead = ctx.response.response.writeHead;\n ctx.response.response.writeHead = (...args: any) => {\n originalWriteHead.apply(ctx.response.response, args);\n onWriteHead(args[0], typeof args[1] === \"string\" ? args[2] : args[1]);\n return ctx.response.response;\n };\n\n if (client.requestLogger.enabled) {\n const onEnd = (chunk: any) => {\n const requestBody =\n client.requestLogger.config.logRequestBody &&\n client.requestLogger.isSupportedContentType(requestContentType)\n ? ctx.request.raw()\n : undefined;\n const responseBody =\n client.requestLogger.config.logResponseBody &&\n client.requestLogger.isSupportedContentType(responseContentType)\n ? chunk\n : undefined;\n\n client.requestLogger.logRequest(\n {\n timestamp,\n method: ctx.request.method(),\n path,\n url: ctx.request.completeUrl(true),\n headers: convertHeaders(ctx.request.headers()),\n size: requestSize,\n consumer: consumer?.identifier,\n body: requestBody ? Buffer.from(requestBody) : undefined,\n },\n {\n statusCode: responseStatus,\n responseTime: responseTime / 1000,\n headers: convertHeaders(responseHeaders),\n size: responseSize,\n body: responseBody ? Buffer.from(responseBody) : undefined,\n },\n ctx.apitallyError,\n );\n };\n\n // Capture the final response body just before it is sent\n const originalEnd = ctx.response.response.end;\n ctx.response.response.end = (...args: any) => {\n originalEnd.apply(ctx.response.response, args);\n onEnd(typeof args[0] !== \"function\" ? args[0] : undefined);\n return ctx.response.response;\n };\n }\n }\n}\n","import { ApitallyConsumer } from \"./types.js\";\n\nexport const consumerFromStringOrObject = (\n consumer: ApitallyConsumer | string,\n) => {\n if (typeof consumer === \"string\") {\n consumer = String(consumer).trim().substring(0, 128);\n return consumer ? { identifier: consumer } : null;\n } else {\n consumer.identifier = String(consumer.identifier).trim().substring(0, 128);\n consumer.name = consumer.name?.trim().substring(0, 64);\n consumer.group = consumer.group?.trim().substring(0, 64);\n return consumer.identifier ? consumer : null;\n }\n};\n\nexport default class ConsumerRegistry {\n private consumers: Map<string, ApitallyConsumer>;\n private updated: Set<string>;\n\n constructor() {\n this.consumers = new Map();\n this.updated = new Set();\n }\n\n public addOrUpdateConsumer(consumer?: ApitallyConsumer | null) {\n if (!consumer || (!consumer.name && !consumer.group)) {\n return;\n }\n const existing = this.consumers.get(consumer.identifier);\n if (!existing) {\n this.consumers.set(consumer.identifier, consumer);\n this.updated.add(consumer.identifier);\n } else {\n if (consumer.name && consumer.name !== existing.name) {\n existing.name = consumer.name;\n this.updated.add(consumer.identifier);\n }\n if (consumer.group && consumer.group !== existing.group) {\n existing.group = consumer.group;\n this.updated.add(consumer.identifier);\n }\n }\n }\n\n public getAndResetUpdatedConsumers() {\n const data: Array<ApitallyConsumer> = [];\n this.updated.forEach((identifier) => {\n const consumer = this.consumers.get(identifier);\n if (consumer) {\n data.push(consumer);\n }\n });\n this.updated.clear();\n return data;\n }\n}\n","import { OutgoingHttpHeader } from \"http\";\n\nexport function parseContentLength(\n contentLength: OutgoingHttpHeader | undefined | null,\n): number | undefined {\n if (contentLength === undefined || contentLength === null) {\n return undefined;\n }\n if (typeof contentLength === \"number\") {\n return contentLength;\n }\n if (typeof contentLength === \"string\") {\n const parsed = parseInt(contentLength);\n return isNaN(parsed) ? undefined : parsed;\n }\n if (Array.isArray(contentLength)) {\n return parseContentLength(contentLength[0]);\n }\n return undefined;\n}\n\nexport function mergeHeaders(base: Headers, merge: Headers) {\n const mergedHeaders = new Headers(base);\n for (const [name, value] of merge)\n if (name === \"set-cookie\") mergedHeaders.append(name, value);\n else mergedHeaders.set(name, value);\n return mergedHeaders;\n}\n","import AsyncLock from \"async-lock\";\nimport { Buffer } from \"buffer\";\nimport { randomUUID } from \"crypto\";\nimport { unlinkSync, writeFileSync } from \"fs\";\nimport { IncomingHttpHeaders, OutgoingHttpHeaders } from \"http\";\nimport { tmpdir } from \"os\";\nimport { join } from \"path\";\n\nimport { getSentryEventId } from \"./sentry.js\";\nimport {\n truncateExceptionMessage,\n truncateExceptionStackTrace,\n} from \"./serverErrorCounter.js\";\nimport TempGzipFile from \"./tempGzipFile.js\";\n\nconst MAX_BODY_SIZE = 50_000; // 50 KB (uncompressed)\nconst MAX_FILE_SIZE = 1_000_000; // 1 MB (compressed)\nconst MAX_FILES = 50;\nconst MAX_PENDING_WRITES = 100;\nconst BODY_TOO_LARGE = Buffer.from(\"<body too large>\");\nconst BODY_MASKED = Buffer.from(\"<masked>\");\nconst MASKED = \"******\";\nconst ALLOWED_CONTENT_TYPES = [\n \"application/json\",\n \"application/problem+json\",\n \"application/vnd.api+json\",\n \"text/plain\",\n];\nconst EXCLUDE_PATH_PATTERNS = [\n /\\/_?healthz?$/i,\n /\\/_?health[_-]?checks?$/i,\n /\\/_?heart[_-]?beats?$/i,\n /\\/ping$/i,\n /\\/ready$/i,\n /\\/live$/i,\n];\nconst EXCLUDE_USER_AGENT_PATTERNS = [\n /health[-_ ]?check/i,\n /microsoft-azure-application-lb/i,\n /googlehc/i,\n /kube-probe/i,\n];\nconst MASK_QUERY_PARAM_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /password/i,\n /pwd/i,\n];\nconst MASK_HEADER_PATTERNS = [\n /auth/i,\n /api-?key/i,\n /secret/i,\n /token/i,\n /cookie/i,\n];\nconst MASK_BODY_FIELD_PATTERNS = [\n /password/i,\n /pwd/i,\n /token/i,\n /secret/i,\n /auth/i,\n /card[-_ ]?number/i,\n /ccv/i,\n /ssn/i,\n];\n\nexport type Request = {\n timestamp: number;\n method: string;\n path?: string;\n url: string;\n headers: [string, string][];\n size?: number;\n consumer?: string;\n body?: Buffer;\n};\n\nexport type Response = {\n statusCode: number;\n responseTime: number;\n headers: [string, string][];\n size?: number;\n body?: Buffer;\n};\n\nexport type RequestLoggingConfig = {\n enabled: boolean;\n logQueryParams: boolean;\n logRequestHeaders: boolean;\n logRequestBody: boolean;\n logResponseHeaders: boolean;\n logResponseBody: boolean;\n logException: boolean;\n maskQueryParams: RegExp[];\n maskHeaders: RegExp[];\n maskBodyFields: RegExp[];\n maskRequestBodyCallback?: (request: Request) => Buffer | null | undefined;\n maskResponseBodyCallback?: (\n request: Request,\n response: Response,\n ) => Buffer | null | undefined;\n excludePaths: RegExp[];\n excludeCallback?: (request: Request, response: Response) => boolean;\n};\n\nconst DEFAULT_CONFIG: RequestLoggingConfig = {\n enabled: false,\n logQueryParams: true,\n logRequestHeaders: false,\n logRequestBody: false,\n logResponseHeaders: true,\n logResponseBody: false,\n logException: true,\n maskQueryParams: [],\n maskHeaders: [],\n maskBodyFields: [],\n excludePaths: [],\n};\n\ntype RequestLogItem = {\n uuid: string;\n request: Request;\n response: Response;\n exception?: {\n type: string;\n message: string;\n stacktrace: string;\n sentryEventId?: string;\n };\n};\n\nexport default class RequestLogger {\n public config: RequestLoggingConfig;\n public enabled: boolean;\n public suspendUntil: number | null = null;\n private pendingWrites: RequestLogItem[] = [];\n private currentFile: TempGzipFile | null = null;\n private files: TempGzipFile[] = [];\n private maintainIntervalId?: NodeJS.Timeout;\n private lock = new AsyncLock();\n\n constructor(config?: Partial<RequestLoggingConfig>) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.enabled = this.config.enabled && checkWritableFs();\n\n if (this.enabled) {\n this.maintainIntervalId = setInterval(() => {\n this.maintain();\n }, 1000);\n }\n }\n\n get maxBodySize() {\n return MAX_BODY_SIZE;\n }\n\n private shouldExcludePath(urlPath: string) {\n const patterns = [...this.config.excludePaths, ...EXCLUDE_PATH_PATTERNS];\n return matchPatterns(urlPath, patterns);\n }\n\n private shouldExcludeUserAgent(userAgent?: string) {\n return userAgent\n ? matchPatterns(userAgent, EXCLUDE_USER_AGENT_PATTERNS)\n : false;\n }\n\n private shouldMaskQueryParam(name: string) {\n const patterns = [\n ...this.config.maskQueryParams,\n ...MASK_QUERY_PARAM_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskHeader(name: string) {\n const patterns = [...this.config.maskHeaders, ...MASK_HEADER_PATTERNS];\n return matchPatterns(name, patterns);\n }\n\n private shouldMaskBodyField(name: string) {\n const patterns = [\n ...this.config.maskBodyFields,\n ...MASK_BODY_FIELD_PATTERNS,\n ];\n return matchPatterns(name, patterns);\n }\n\n private hasSupportedContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return this.isSupportedContentType(contentType);\n }\n\n private hasJsonContentType(headers: [string, string][]) {\n const contentType = headers.find(\n ([k]) => k.toLowerCase() === \"content-type\",\n )?.[1];\n return contentType ? /\\bjson\\b/i.test(contentType) : null;\n }\n\n public isSupportedContentType(contentType?: string | null) {\n return (\n typeof contentType === \"string\" &&\n ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t))\n );\n }\n\n private maskQueryParams(search: string) {\n const params = new URLSearchParams(search);\n for (const [key] of params) {\n if (this.shouldMaskQueryParam(key)) {\n params.set(key, MASKED);\n }\n }\n return params.toString();\n }\n\n private maskHeaders(headers: [string, string][]): [string, string][] {\n return headers.map(([k, v]) => [k, this.shouldMaskHeader(k) ? MASKED : v]);\n }\n\n private maskBody(data: any): any {\n if (typeof data === \"object\" && data !== null && !Array.isArray(data)) {\n const result: any = {};\n for (const [key, value] of Object.entries(data)) {\n if (typeof value === \"string\" && this.shouldMaskBodyField(key)) {\n result[key] = MASKED;\n } else {\n result[key] = this.maskBody(value);\n }\n }\n return result;\n }\n if (Array.isArray(data)) {\n return data.map((item) => this.maskBody(item));\n }\n return data;\n }\n\n private applyMasking(item: RequestLogItem) {\n // Apply user-provided maskRequestBodyCallback function\n if (\n this.config.maskRequestBodyCallback &&\n item.request.body &&\n item.request.body !== BODY_TOO_LARGE\n ) {\n try {\n const maskedBody = this.config.maskRequestBodyCallback(item.request);\n item.request.body = maskedBody ?? BODY_MASKED;\n } catch {\n item.request.body = undefined;\n }\n }\n\n // Apply user-provided maskResponseBodyCallback function\n if (\n this.config.maskResponseBodyCallback &&\n item.response.body &&\n item.response.body !== BODY_TOO_LARGE\n ) {\n try {\n const maskedBody = this.config.maskResponseBodyCallback(\n item.request,\n item.response,\n );\n item.response.body = maskedBody ?? BODY_MASKED;\n } catch {\n item.response.body = undefined;\n }\n }\n\n // Check request and response body sizes\n if (item.request.body && item.request.body.length > MAX_BODY_SIZE) {\n item.request.body = BODY_TOO_LARGE;\n }\n if (item.response.body && item.response.body.length > MAX_BODY_SIZE) {\n item.response.body = BODY_TOO_LARGE;\n }\n\n // Mask request and response body fields\n for (const key of [\"request\", \"response\"] as const) {\n const bodyData = item[key].body;\n if (\n !bodyData ||\n bodyData === BODY_TOO_LARGE ||\n bodyData === BODY_MASKED\n ) {\n continue;\n }\n\n const headers = item[key].headers;\n const hasJsonContent = this.hasJsonContentType(headers);\n if (hasJsonContent === null || hasJsonContent) {\n try {\n const parsedBody = JSON.parse(bodyData.toString());\n const maskedBody = this.maskBody(parsedBody);\n item[key].body = Buffer.from(JSON.stringify(maskedBody));\n } catch {\n // If parsing fails, leave body as is\n }\n }\n }\n\n // Mask request and response headers\n item.request.headers = this.config.logRequestHeaders\n ? this.maskHeaders(item.request.headers)\n : [];\n item.response.headers = this.config.logResponseHeaders\n ? this.maskHeaders(item.response.headers)\n : [];\n\n // Mask query params\n const url = new URL(item.request.url);\n url.search = this.config.logQueryParams\n ? this.maskQueryParams(url.search)\n : \"\";\n item.request.url = url.toString();\n\n return item;\n }\n\n logRequest(request: Request, response: Response, error?: Error) {\n if (!this.enabled || this.suspendUntil !== null) return;\n\n const url = new URL(request.url);\n const path = request.path ?? url.pathname;\n const userAgent = request.headers.find(\n ([k]) => k.toLowerCase() === \"user-agent\",\n )?.[1];\n\n if (\n this.shouldExcludePath(path) ||\n this.shouldExcludeUserAgent(userAgent) ||\n (this.config.excludeCallback?.(request, response) ?? false)\n ) {\n return;\n }\n\n if (\n !this.config.logRequestBody ||\n !this.hasSupportedContentType(request.headers)\n ) {\n request.body = undefined;\n }\n if (\n !this.config.logResponseBody ||\n !this.hasSupportedContentType(response.headers)\n ) {\n response.body = undefined;\n }\n\n if (request.size !== undefined && request.size < 0) {\n request.size = undefined;\n }\n if (response.size !== undefined && response.size < 0) {\n response.size = undefined;\n }\n\n const item: RequestLogItem = {\n uuid: randomUUID(),\n request: request,\n response: response,\n exception:\n error && this.config.logException\n ? {\n type: error.name,\n message: truncateExceptionMessage(error.message),\n stacktrace: truncateExceptionStackTrace(error.stack || \"\"),\n sentryEventId: getSentryEventId(),\n }\n : undefined,\n };\n this.pendingWrites.push(item);\n\n if (this.pendingWrites.length > MAX_PENDING_WRITES) {\n this.pendingWrites.shift();\n }\n }\n\n async writeToFile() {\n if (!this.enabled || this.pendingWrites.length === 0) {\n return;\n }\n return this.lock.acquire(\"file\", async () => {\n if (!this.currentFile) {\n this.currentFile = new TempGzipFile();\n }\n while (this.pendingWrites.length > 0) {\n let item = this.pendingWrites.shift();\n if (item) {\n item = this.applyMasking(item);\n\n const finalItem = {\n uuid: item.uuid,\n request: skipEmptyValues(item.request),\n response: skipEmptyValues(item.response),\n exception: item.exception,\n };\n\n // Set up body serialization for JSON\n [finalItem.request.body, finalItem.response.body].forEach((body) => {\n if (body) {\n // @ts-expect-error Override Buffer's default JSON serialization\n body.toJSON = function () {\n return this.toString(\"base64\");\n };\n }\n });\n\n await this.currentFile.writeLine(\n Buffer.from(JSON.stringify(finalItem)),\n );\n }\n }\n });\n }\n\n getFile() {\n return this.files.shift();\n }\n\n retryFileLater(file: TempGzipFile) {\n this.files.unshift(file);\n }\n\n async rotateFile() {\n return this.lock.acquire(\"file\", async () => {\n if (this.currentFile) {\n await this.currentFile.close();\n this.files.push(this.currentFile);\n this.currentFile = null;\n }\n });\n }\n\n async maintain() {\n await this.writeToFile();\n if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {\n await this.rotateFile();\n }\n while (this.files.length > MAX_FILES) {\n const file = this.files.shift();\n file?.delete();\n }\n if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {\n this.suspendUntil = null;\n }\n }\n\n async clear() {\n this.pendingWrites = [];\n await this.rotateFile();\n this.files.forEach((file) => {\n file.delete();\n });\n this.files = [];\n }\n\n async close() {\n this.enabled = false;\n await this.clear();\n if (this.maintainIntervalId) {\n clearInterval(this.maintainIntervalId);\n }\n }\n}\n\nexport function convertHeaders(\n headers:\n | Headers\n | IncomingHttpHeaders\n | OutgoingHttpHeaders\n | Record<string, string | string[] | number | undefined>,\n) {\n if (headers instanceof Headers) {\n return Array.from(headers.entries());\n }\n return Object.entries(headers).flatMap(([key, value]) => {\n if (value === undefined) {\n return [];\n }\n if (Array.isArray(value)) {\n return value.map((v) => [key, v]);\n }\n return [[key, value.toString()]];\n }) as [string, string][];\n}\n\nexport function convertBody(body: any, contentType?: string | null) {\n if (!body || !contentType) {\n return;\n }\n try {\n if (contentType.startsWith(\"application/json\")) {\n if (isValidJsonString(body)) {\n return Buffer.from(body);\n } else {\n return Buffer.from(JSON.stringify(body));\n }\n }\n if (contentType.startsWith(\"text/\") && typeof body === \"string\") {\n return Buffer.from(body);\n }\n } catch (error) {\n return;\n }\n}\n\nfunction isValidJsonString(body: any) {\n if (typeof body !== \"string\") {\n return false;\n }\n try {\n JSON.parse(body);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction matchPatterns(value: string, patterns: RegExp[]) {\n return patterns.some((pattern) => {\n return pattern.test(value);\n });\n}\n\nfunction skipEmptyValues<T extends Record<string, any>>(data: T) {\n return Object.fromEntries(\n Object.entries(data).filter(([_, v]) => {\n if (v == null || Number.isNaN(v)) return false;\n if (Array.isArray(v) || Buffer.isBuffer(v) || typeof v === \"string\") {\n return v.length > 0;\n }\n return true;\n }),\n ) as Partial<T>;\n}\n\nfunction checkWritableFs() {\n try {\n const testPath = join(tmpdir(), `apitally-${randomUUID()}`);\n writeFileSync(testPath, \"test\");\n unlinkSync(testPath);\n return true;\n } catch (error) {\n return false;\n }\n}\n","import type * as Sentry from \"@sentry/node\";\n\nlet sentry: typeof Sentry | undefined;\n\n// Initialize Sentry when the module is loaded\n(async () => {\n try {\n sentry = await import(\"@sentry/node\");\n } catch (e) {\n // Sentry SDK is not installed, ignore\n }\n})();\n\n/**\n * Returns the last Sentry event ID if available\n */\nexport function getSentryEventId(): string | undefined {\n if (sentry && sentry.lastEventId) {\n return sentry.lastEventId();\n }\n return undefined;\n}\n"],"mappings":"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","names":["performance","consumerFromStringOrObject","consumer","String","trim","substring","identifier","name","group","parseContentLength","contentLength","undefined","parsed","parseInt","isNaN","Array","isArray","AsyncLock","Buffer","sentry","e","BODY_TOO_LARGE","Buffer","from","BODY_MASKED","convertHeaders","headers","Headers","Array","from","entries","Object","flatMap","key","value","undefined","isArray","map","v","toString","ApitallyMiddleware","handle","ctx","next","client","containerResolver","make","isEnabled","path","route","pattern","timestamp","Date","now","startTime","performance","responseTime","requestSize","parseContentLength","request","header","requestContentType","toString","responseStatus","response","getStatus","responseHeaders","getHeaders","responseSize","responseContentType","consumer","apitallyConsumer","consumerFromStringOrObject","consumerRegistry","addOrUpdateConsumer","onWriteHead","statusCode","headers","_a","requestCounter","addRequest","identifier","method","apitallyError","code","Array","isArray","messages","forEach","message","validationErrorCounter","addValidationError","loc","field","msg","type","rule","serverErrorCounter","addServerError","name","traceback","stack","originalWriteHead","writeHead","args","apply","requestLogger","enabled","onEnd","chunk","requestBody","config","logRequestBody","isSupportedContentType","raw","undefined","responseBody","logResponseBody","logRequest","url","completeUrl","convertHeaders","size","body","Buffer","from","originalEnd","end"]}
@@ -420,6 +420,16 @@ var MASK_HEADER_PATTERNS = [
420
420
  /token/i,
421
421
  /cookie/i
422
422
  ];
423
+ var MASK_BODY_FIELD_PATTERNS = [
424
+ /password/i,
425
+ /pwd/i,
426
+ /token/i,
427
+ /secret/i,
428
+ /auth/i,
429
+ /card[-_ ]?number/i,
430
+ /ccv/i,
431
+ /ssn/i
432
+ ];
423
433
  var DEFAULT_CONFIG = {
424
434
  enabled: false,
425
435
  logQueryParams: true,
@@ -430,6 +440,7 @@ var DEFAULT_CONFIG = {
430
440
  logException: true,
431
441
  maskQueryParams: [],
432
442
  maskHeaders: [],
443
+ maskBodyFields: [],
433
444
  excludePaths: []
434
445
  };
435
446
  var _RequestLogger = class _RequestLogger {
@@ -480,11 +491,23 @@ var _RequestLogger = class _RequestLogger {
480
491
  ];
481
492
  return matchPatterns(name, patterns);
482
493
  }
494
+ shouldMaskBodyField(name) {
495
+ const patterns = [
496
+ ...this.config.maskBodyFields,
497
+ ...MASK_BODY_FIELD_PATTERNS
498
+ ];
499
+ return matchPatterns(name, patterns);
500
+ }
483
501
  hasSupportedContentType(headers) {
484
502
  var _a2;
485
503
  const contentType = (_a2 = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a2[1];
486
504
  return this.isSupportedContentType(contentType);
487
505
  }
506
+ hasJsonContentType(headers) {
507
+ var _a2;
508
+ const contentType = (_a2 = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a2[1];
509
+ return contentType ? /\bjson\b/i.test(contentType) : null;
510
+ }
488
511
  isSupportedContentType(contentType) {
489
512
  return typeof contentType === "string" && ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t));
490
513
  }
@@ -503,6 +526,72 @@ var _RequestLogger = class _RequestLogger {
503
526
  this.shouldMaskHeader(k) ? MASKED : v
504
527
  ]);
505
528
  }
529
+ maskBody(data) {
530
+ if (typeof data === "object" && data !== null && !Array.isArray(data)) {
531
+ const result = {};
532
+ for (const [key, value] of Object.entries(data)) {
533
+ if (typeof value === "string" && this.shouldMaskBodyField(key)) {
534
+ result[key] = MASKED;
535
+ } else {
536
+ result[key] = this.maskBody(value);
537
+ }
538
+ }
539
+ return result;
540
+ }
541
+ if (Array.isArray(data)) {
542
+ return data.map((item) => this.maskBody(item));
543
+ }
544
+ return data;
545
+ }
546
+ applyMasking(item) {
547
+ if (this.config.maskRequestBodyCallback && item.request.body && item.request.body !== BODY_TOO_LARGE) {
548
+ try {
549
+ const maskedBody = this.config.maskRequestBodyCallback(item.request);
550
+ item.request.body = maskedBody ?? BODY_MASKED;
551
+ } catch {
552
+ item.request.body = void 0;
553
+ }
554
+ }
555
+ if (this.config.maskResponseBodyCallback && item.response.body && item.response.body !== BODY_TOO_LARGE) {
556
+ try {
557
+ const maskedBody = this.config.maskResponseBodyCallback(item.request, item.response);
558
+ item.response.body = maskedBody ?? BODY_MASKED;
559
+ } catch {
560
+ item.response.body = void 0;
561
+ }
562
+ }
563
+ if (item.request.body && item.request.body.length > MAX_BODY_SIZE) {
564
+ item.request.body = BODY_TOO_LARGE;
565
+ }
566
+ if (item.response.body && item.response.body.length > MAX_BODY_SIZE) {
567
+ item.response.body = BODY_TOO_LARGE;
568
+ }
569
+ for (const key of [
570
+ "request",
571
+ "response"
572
+ ]) {
573
+ const bodyData = item[key].body;
574
+ if (!bodyData || bodyData === BODY_TOO_LARGE || bodyData === BODY_MASKED) {
575
+ continue;
576
+ }
577
+ const headers = item[key].headers;
578
+ const hasJsonContent = this.hasJsonContentType(headers);
579
+ if (hasJsonContent === null || hasJsonContent) {
580
+ try {
581
+ const parsedBody = JSON.parse(bodyData.toString());
582
+ const maskedBody = this.maskBody(parsedBody);
583
+ item[key].body = import_buffer2.Buffer.from(JSON.stringify(maskedBody));
584
+ } catch {
585
+ }
586
+ }
587
+ }
588
+ item.request.headers = this.config.logRequestHeaders ? this.maskHeaders(item.request.headers) : [];
589
+ item.response.headers = this.config.logResponseHeaders ? this.maskHeaders(item.response.headers) : [];
590
+ const url = new URL(item.request.url);
591
+ url.search = this.config.logQueryParams ? this.maskQueryParams(url.search) : "";
592
+ item.request.url = url.toString();
593
+ return item;
594
+ }
506
595
  logRequest(request, response, error) {
507
596
  var _a2, _b, _c;
508
597
  if (!this.enabled || this.suspendUntil !== null) return;
@@ -512,64 +601,30 @@ var _RequestLogger = class _RequestLogger {
512
601
  if (this.shouldExcludePath(path) || this.shouldExcludeUserAgent(userAgent) || (((_c = (_b = this.config).excludeCallback) == null ? void 0 : _c.call(_b, request, response)) ?? false)) {
513
602
  return;
514
603
  }
515
- url.search = this.config.logQueryParams ? this.maskQueryParams(url.search) : "";
516
- request.url = url.toString();
517
604
  if (!this.config.logRequestBody || !this.hasSupportedContentType(request.headers)) {
518
605
  request.body = void 0;
519
- } else if (request.body) {
520
- if (request.body.length > MAX_BODY_SIZE) {
521
- request.body = BODY_TOO_LARGE;
522
- } else if (this.config.maskRequestBodyCallback) {
523
- try {
524
- request.body = this.config.maskRequestBodyCallback(request) ?? BODY_MASKED;
525
- if (request.body.length > MAX_BODY_SIZE) {
526
- request.body = BODY_TOO_LARGE;
527
- }
528
- } catch {
529
- request.body = void 0;
530
- }
531
- }
532
606
  }
533
607
  if (!this.config.logResponseBody || !this.hasSupportedContentType(response.headers)) {
534
608
  response.body = void 0;
535
- } else if (response.body) {
536
- if (response.body.length > MAX_BODY_SIZE) {
537
- response.body = BODY_TOO_LARGE;
538
- } else if (this.config.maskResponseBodyCallback) {
539
- try {
540
- response.body = this.config.maskResponseBodyCallback(request, response) ?? BODY_MASKED;
541
- if (response.body.length > MAX_BODY_SIZE) {
542
- response.body = BODY_TOO_LARGE;
543
- }
544
- } catch {
545
- response.body = void 0;
546
- }
547
- }
548
609
  }
549
- request.headers = this.config.logRequestHeaders ? this.maskHeaders(request.headers) : [];
550
- response.headers = this.config.logResponseHeaders ? this.maskHeaders(response.headers) : [];
610
+ if (request.size !== void 0 && request.size < 0) {
611
+ request.size = void 0;
612
+ }
613
+ if (response.size !== void 0 && response.size < 0) {
614
+ response.size = void 0;
615
+ }
551
616
  const item = {
552
617
  uuid: (0, import_crypto3.randomUUID)(),
553
- request: skipEmptyValues(request),
554
- response: skipEmptyValues(response),
618
+ request,
619
+ response,
555
620
  exception: error && this.config.logException ? {
556
621
  type: error.name,
557
622
  message: truncateExceptionMessage(error.message),
558
623
  stacktrace: truncateExceptionStackTrace(error.stack || ""),
559
624
  sentryEventId: getSentryEventId()
560
- } : null
625
+ } : void 0
561
626
  };
562
- [
563
- item.request.body,
564
- item.response.body
565
- ].forEach((body) => {
566
- if (body) {
567
- body.toJSON = function() {
568
- return this.toString("base64");
569
- };
570
- }
571
- });
572
- this.pendingWrites.push(JSON.stringify(item));
627
+ this.pendingWrites.push(item);
573
628
  if (this.pendingWrites.length > MAX_PENDING_WRITES) {
574
629
  this.pendingWrites.shift();
575
630
  }
@@ -583,9 +638,26 @@ var _RequestLogger = class _RequestLogger {
583
638
  this.currentFile = new TempGzipFile();
584
639
  }
585
640
  while (this.pendingWrites.length > 0) {
586
- const item = this.pendingWrites.shift();
641
+ let item = this.pendingWrites.shift();
587
642
  if (item) {
588
- await this.currentFile.writeLine(import_buffer2.Buffer.from(item));
643
+ item = this.applyMasking(item);
644
+ const finalItem = {
645
+ uuid: item.uuid,
646
+ request: skipEmptyValues(item.request),
647
+ response: skipEmptyValues(item.response),
648
+ exception: item.exception
649
+ };
650
+ [
651
+ finalItem.request.body,
652
+ finalItem.response.body
653
+ ].forEach((body) => {
654
+ if (body) {
655
+ body.toJSON = function() {
656
+ return this.toString("base64");
657
+ };
658
+ }
659
+ });
660
+ await this.currentFile.writeLine(import_buffer2.Buffer.from(JSON.stringify(finalItem)));
589
661
  }
590
662
  }
591
663
  });