apitally 1.0.0-beta.2 → 1.0.0-beta.4

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 (74) hide show
  1. package/README.md +7 -25
  2. package/dist/activation.cjs +2 -1
  3. package/dist/activation.cjs.map +1 -1
  4. package/dist/activation.d.cts +1 -0
  5. package/dist/activation.d.ts +1 -0
  6. package/dist/activation.js +2 -1
  7. package/dist/activation.js.map +1 -1
  8. package/dist/consumers.cjs +96 -0
  9. package/dist/consumers.cjs.map +1 -0
  10. package/dist/consumers.d.cts +13 -0
  11. package/dist/consumers.d.ts +13 -0
  12. package/dist/consumers.js +93 -0
  13. package/dist/consumers.js.map +1 -0
  14. package/dist/context.cjs +2 -8
  15. package/dist/context.cjs.map +1 -1
  16. package/dist/context.d.cts +5 -5
  17. package/dist/context.d.ts +5 -5
  18. package/dist/context.js +3 -7
  19. package/dist/context.js.map +1 -1
  20. package/dist/exportWorker.cjs +6 -5
  21. package/dist/exportWorker.cjs.map +1 -1
  22. package/dist/exportWorker.js +6 -5
  23. package/dist/exportWorker.js.map +1 -1
  24. package/dist/index.cjs +2 -2
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/metrics.cjs +164 -32
  31. package/dist/metrics.cjs.map +1 -1
  32. package/dist/metrics.d.cts +4 -6
  33. package/dist/metrics.d.ts +4 -6
  34. package/dist/metrics.js +165 -33
  35. package/dist/metrics.js.map +1 -1
  36. package/dist/nestjs/index.cjs +6 -0
  37. package/dist/nestjs/index.cjs.map +1 -1
  38. package/dist/nestjs/index.js +6 -0
  39. package/dist/nestjs/index.js.map +1 -1
  40. package/dist/providers.cjs +0 -8
  41. package/dist/providers.cjs.map +1 -1
  42. package/dist/providers.d.cts +0 -2
  43. package/dist/providers.d.ts +0 -2
  44. package/dist/providers.js +1 -8
  45. package/dist/providers.js.map +1 -1
  46. package/dist/requestObservation.cjs +6 -4
  47. package/dist/requestObservation.cjs.map +1 -1
  48. package/dist/requestObservation.js +7 -5
  49. package/dist/requestObservation.js.map +1 -1
  50. package/dist/serverErrors.cjs +30 -28
  51. package/dist/serverErrors.cjs.map +1 -1
  52. package/dist/serverErrors.js +30 -28
  53. package/dist/serverErrors.js.map +1 -1
  54. package/dist/spanProcessor.cjs +0 -2
  55. package/dist/spanProcessor.cjs.map +1 -1
  56. package/dist/spanProcessor.js +0 -2
  57. package/dist/spanProcessor.js.map +1 -1
  58. package/dist/spool.cjs +5 -0
  59. package/dist/spool.cjs.map +1 -1
  60. package/dist/spool.d.cts +2 -1
  61. package/dist/spool.d.ts +2 -1
  62. package/dist/spool.js +5 -0
  63. package/dist/spool.js.map +1 -1
  64. package/dist/validationErrors.cjs +29 -26
  65. package/dist/validationErrors.cjs.map +1 -1
  66. package/dist/validationErrors.js +29 -26
  67. package/dist/validationErrors.js.map +1 -1
  68. package/package.json +2 -2
  69. package/dist/consumer.cjs +0 -61
  70. package/dist/consumer.cjs.map +0 -1
  71. package/dist/consumer.d.cts +0 -12
  72. package/dist/consumer.d.ts +0 -12
  73. package/dist/consumer.js +0 -59
  74. package/dist/consumer.js.map +0 -1
@@ -2,14 +2,13 @@ import { MAX_BODY_SIZE } from "./bodyCapture.js";
2
2
  import { brotliDecompressSync, unzipSync } from "node:zlib";
3
3
  //#region src/validationErrors.ts
4
4
  const VALIDATION_ERROR_EVENT_NAME = "apitally.request.validation_error";
5
- const MAX_GROUPS = 100;
5
+ const MAX_ERRORS = 100;
6
6
  const MAX_CONSUMER_LENGTH = 128;
7
7
  const MAX_PATH_LENGTH = 2e3;
8
8
  const MAX_SOURCE_LENGTH = 32;
9
9
  const MAX_FIELD_LENGTH = 2048;
10
10
  const MAX_MESSAGE_LENGTH = 2048;
11
11
  const MAX_TYPE_LENGTH = 128;
12
- const MAX_COUNT = 2 ** 32 - 1;
13
12
  const SOURCE_ALIASES = {
14
13
  body: "body",
15
14
  query: "query",
@@ -22,48 +21,52 @@ const SOURCE_ALIASES = {
22
21
  headers: "header",
23
22
  cookies: "cookie"
24
23
  };
25
- const VALIDATION_ERROR_GROUPS_KEY = Symbol.for("apitally.validationErrorGroups");
26
- const validationErrorGroupsHolder = globalThis;
27
- const validationErrorGroups = validationErrorGroupsHolder[VALIDATION_ERROR_GROUPS_KEY] ?? /* @__PURE__ */ new Map();
28
- validationErrorGroupsHolder[VALIDATION_ERROR_GROUPS_KEY] = validationErrorGroups;
24
+ const VALIDATION_ERROR_AGGREGATES_KEY = Symbol.for("apitally.validationErrorAggregates");
25
+ const validationErrorAggregatesHolder = globalThis;
26
+ const validationErrorAggregates = validationErrorAggregatesHolder[VALIDATION_ERROR_AGGREGATES_KEY] ?? /* @__PURE__ */ new Map();
27
+ validationErrorAggregatesHolder[VALIDATION_ERROR_AGGREGATES_KEY] = validationErrorAggregates;
29
28
  function isValidationResponseStatus(statusCode) {
30
29
  return statusCode === 400 || statusCode === 422;
31
30
  }
32
31
  function addValidationErrors(consumer, method, path, details) {
33
32
  method = method.toUpperCase();
34
33
  if (method === "OPTIONS" || !path) return;
34
+ consumer = consumer?.slice(0, MAX_CONSUMER_LENGTH);
35
35
  for (const detail of details) {
36
- const group = {
36
+ const fields = {
37
37
  method,
38
38
  path: path.slice(0, MAX_PATH_LENGTH),
39
39
  source: detail.source.slice(0, MAX_SOURCE_LENGTH),
40
40
  field: detail.field.slice(0, MAX_FIELD_LENGTH),
41
41
  message: detail.message.slice(0, MAX_MESSAGE_LENGTH),
42
- type: detail.type.slice(0, MAX_TYPE_LENGTH),
43
- count: 1
42
+ type: detail.type.slice(0, MAX_TYPE_LENGTH)
44
43
  };
45
- if (consumer !== void 0) group.consumer = consumer.slice(0, MAX_CONSUMER_LENGTH);
46
- const key = [
47
- group.consumer ?? "",
48
- group.method,
49
- group.path,
50
- group.source,
51
- group.field,
52
- group.message,
53
- group.type
54
- ].join("\0");
55
- const existing = validationErrorGroups.get(key);
56
- if (existing) existing.count = Math.min(existing.count + 1, MAX_COUNT);
57
- else if (validationErrorGroups.size < MAX_GROUPS) validationErrorGroups.set(key, group);
44
+ const key = Object.values(fields).join("\0");
45
+ let aggregate = validationErrorAggregates.get(key);
46
+ if (!aggregate) {
47
+ if (validationErrorAggregates.size >= MAX_ERRORS) continue;
48
+ aggregate = {
49
+ ...fields,
50
+ counts: /* @__PURE__ */ new Map()
51
+ };
52
+ validationErrorAggregates.set(key, aggregate);
53
+ }
54
+ aggregate.counts.set(consumer, (aggregate.counts.get(consumer) ?? 0) + 1);
58
55
  }
59
56
  }
60
57
  function drainValidationErrors() {
61
- const groups = [...validationErrorGroups.values()];
62
- validationErrorGroups.clear();
63
- return groups;
58
+ const aggregates = [...validationErrorAggregates.values()];
59
+ validationErrorAggregates.clear();
60
+ return aggregates.map(({ counts, ...fields }) => ({
61
+ ...fields,
62
+ counts: [...counts].map(([consumer, count]) => consumer === void 0 ? { count } : {
63
+ consumer,
64
+ count
65
+ })
66
+ }));
64
67
  }
65
68
  function resetValidationErrors() {
66
- validationErrorGroups.clear();
69
+ validationErrorAggregates.clear();
67
70
  }
68
71
  function normalizeSource(source) {
69
72
  return typeof source === "string" ? SOURCE_ALIASES[source.toLowerCase()] ?? "" : "";
@@ -1 +1 @@
1
- {"version":3,"file":"validationErrors.js","names":[],"sources":["../src/validationErrors.ts"],"sourcesContent":["import { brotliDecompressSync, unzipSync } from \"node:zlib\";\nimport type { AnyValueMap } from \"@opentelemetry/api-logs\";\nimport { MAX_BODY_SIZE } from \"./bodyCapture.js\";\n\nexport const VALIDATION_ERROR_EVENT_NAME = \"apitally.request.validation_error\";\n\nconst MAX_GROUPS = 100;\nconst MAX_CONSUMER_LENGTH = 128;\nconst MAX_PATH_LENGTH = 2_000;\nconst MAX_SOURCE_LENGTH = 32;\nconst MAX_FIELD_LENGTH = 2_048;\nconst MAX_MESSAGE_LENGTH = 2_048;\nconst MAX_TYPE_LENGTH = 128;\nconst MAX_COUNT = 2 ** 32 - 1;\n\nconst SOURCE_ALIASES: Record<string, string> = {\n body: \"body\",\n query: \"query\",\n path: \"path\",\n header: \"header\",\n cookie: \"cookie\",\n querystring: \"query\",\n params: \"path\",\n path_params: \"path\",\n headers: \"header\",\n cookies: \"cookie\",\n};\n\nexport type ValidationErrorDetail = {\n source: string;\n field: string;\n message: string;\n type: string;\n};\n\ntype ValidationErrorGroup = ValidationErrorDetail & {\n consumer?: string;\n method: string;\n path: string;\n count: number;\n};\n\nconst VALIDATION_ERROR_GROUPS_KEY = Symbol.for(\"apitally.validationErrorGroups\");\nconst validationErrorGroupsHolder = globalThis as Record<\n symbol,\n Map<string, ValidationErrorGroup> | undefined\n>;\nconst validationErrorGroups =\n validationErrorGroupsHolder[VALIDATION_ERROR_GROUPS_KEY] ??\n new Map<string, ValidationErrorGroup>();\nvalidationErrorGroupsHolder[VALIDATION_ERROR_GROUPS_KEY] = validationErrorGroups;\n\nexport function isValidationResponseStatus(statusCode: number): boolean {\n return statusCode === 400 || statusCode === 422;\n}\n\nexport function addValidationErrors(\n consumer: string | undefined,\n method: string,\n path: string,\n details: ValidationErrorDetail[],\n): void {\n method = method.toUpperCase();\n if (method === \"OPTIONS\" || !path) {\n return;\n }\n for (const detail of details) {\n const group: ValidationErrorGroup = {\n method,\n path: path.slice(0, MAX_PATH_LENGTH),\n source: detail.source.slice(0, MAX_SOURCE_LENGTH),\n field: detail.field.slice(0, MAX_FIELD_LENGTH),\n message: detail.message.slice(0, MAX_MESSAGE_LENGTH),\n type: detail.type.slice(0, MAX_TYPE_LENGTH),\n count: 1,\n };\n if (consumer !== undefined) {\n group.consumer = consumer.slice(0, MAX_CONSUMER_LENGTH);\n }\n const key = [\n group.consumer ?? \"\",\n group.method,\n group.path,\n group.source,\n group.field,\n group.message,\n group.type,\n ].join(\"\\0\");\n const existing = validationErrorGroups.get(key);\n if (existing) {\n existing.count = Math.min(existing.count + 1, MAX_COUNT);\n } else if (validationErrorGroups.size < MAX_GROUPS) {\n validationErrorGroups.set(key, group);\n }\n }\n}\n\nexport function drainValidationErrors(): AnyValueMap[] {\n const groups = [...validationErrorGroups.values()];\n validationErrorGroups.clear();\n return groups;\n}\n\nexport function resetValidationErrors(): void {\n validationErrorGroups.clear();\n}\n\nexport function normalizeSource(source: unknown): string {\n return typeof source === \"string\" ? (SOURCE_ALIASES[source.toLowerCase()] ?? \"\") : \"\";\n}\n\n// Zod, Standard Schema, and TypeBox issues share this shape. A Standard Schema\n// path segment may be an object carrying the key; a TypeBox path is a JSON pointer.\nexport function formatIssues(issues: unknown, source = \"\"): ValidationErrorDetail[] {\n if (!Array.isArray(issues)) {\n return [];\n }\n const details: ValidationErrorDetail[] = [];\n for (const issue of issues) {\n if (!isRecord(issue) || typeof issue.message !== \"string\") {\n continue;\n }\n const path = Array.isArray(issue.path)\n ? issue.path\n : typeof issue.path === \"string\"\n ? issue.path\n .split(\"/\")\n .filter(Boolean)\n .map((segment) => segment.replace(/~1/g, \"/\").replace(/~0/g, \"~\"))\n : [];\n details.push({\n source,\n field: path\n .map((segment: unknown) => (isRecord(segment) ? segment.key : segment))\n .filter((segment) => typeof segment === \"string\" || typeof segment === \"number\")\n .join(\".\"),\n message: issue.message,\n type: typeof issue.code === \"string\" ? issue.code : \"\",\n });\n }\n return details;\n}\n\n// zod 3 serializes issues as `error.issues`; zod 4 hides them as a JSON string\n// in `error.message`.\nexport function extractZodValidationErrors(body: unknown): ValidationErrorDetail[] {\n if (!isRecord(body)) {\n return [];\n }\n const error = isRecord(body.error) ? body.error : undefined;\n let issues = body.issues ?? error?.issues;\n if (issues === undefined && error?.name === \"ZodError\" && typeof error.message === \"string\") {\n try {\n issues = JSON.parse(error.message);\n } catch {\n return [];\n }\n }\n return formatIssues(issues);\n}\n\n// Recognized response shapes: zod issues, express-validator, and celebrate.\nexport function extractExpressValidationErrors(body: unknown): ValidationErrorDetail[] {\n if (!isRecord(body)) {\n return [];\n }\n const zodDetails = extractZodValidationErrors(body);\n if (zodDetails.length > 0) {\n return zodDetails;\n }\n if (Array.isArray(body.errors)) {\n return extractExpressValidatorErrors(body.errors);\n }\n if (isRecord(body.validation)) {\n return extractCelebrateErrors(body.validation);\n }\n return [];\n}\n\nfunction extractExpressValidatorErrors(errors: unknown[]): ValidationErrorDetail[] {\n const details: ValidationErrorDetail[] = [];\n for (const error of errors) {\n if (\n !isRecord(error) ||\n error.type !== \"field\" ||\n typeof error.path !== \"string\" ||\n typeof error.msg !== \"string\"\n ) {\n continue;\n }\n const source = normalizeSource(error.location);\n if (source) {\n details.push({ source, field: error.path, message: error.msg, type: \"\" });\n }\n }\n return details;\n}\n\n// Celebrate joins the messages of all failed keys into one sentence list, so\n// each key takes the sentence naming it.\nfunction extractCelebrateErrors(validation: Record<string, unknown>): ValidationErrorDetail[] {\n const details: ValidationErrorDetail[] = [];\n for (const error of Object.values(validation)) {\n if (!isRecord(error) || !Array.isArray(error.keys) || typeof error.message !== \"string\") {\n continue;\n }\n const sentences = error.message.split(\". \");\n for (const key of error.keys) {\n if (typeof key === \"string\") {\n details.push({\n source: normalizeSource(error.source),\n field: key,\n message: sentences.find((sentence) => sentence.includes(`\"${key}\"`)) ?? error.message,\n type: \"\",\n });\n }\n }\n }\n return details;\n}\n\nexport function parseJsonResponseBody(body: Buffer | undefined, contentEncoding: unknown): unknown {\n if (!body) {\n return undefined;\n }\n try {\n const encoding =\n typeof contentEncoding === \"string\" ? contentEncoding.trim().toLowerCase() : \"\";\n let decoded = body;\n if (encoding === \"br\") {\n decoded = brotliDecompressSync(body, { maxOutputLength: MAX_BODY_SIZE });\n } else if (encoding === \"gzip\" || encoding === \"deflate\") {\n decoded = unzipSync(body, { maxOutputLength: MAX_BODY_SIZE });\n } else if (encoding !== \"\" && encoding !== \"identity\") {\n return undefined;\n }\n return JSON.parse(decoded.toString());\n } catch {\n // A body that cannot be decoded as JSON carries no recognizable validation details.\n }\n return undefined;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"validationErrors.js","names":[],"sources":["../src/validationErrors.ts"],"sourcesContent":["import { brotliDecompressSync, unzipSync } from \"node:zlib\";\nimport type { AnyValueMap } from \"@opentelemetry/api-logs\";\nimport { MAX_BODY_SIZE } from \"./bodyCapture.js\";\n\nexport const VALIDATION_ERROR_EVENT_NAME = \"apitally.request.validation_error\";\n\nconst MAX_ERRORS = 100;\nconst MAX_CONSUMER_LENGTH = 128;\nconst MAX_PATH_LENGTH = 2_000;\nconst MAX_SOURCE_LENGTH = 32;\nconst MAX_FIELD_LENGTH = 2_048;\nconst MAX_MESSAGE_LENGTH = 2_048;\nconst MAX_TYPE_LENGTH = 128;\n\nconst SOURCE_ALIASES: Record<string, string> = {\n body: \"body\",\n query: \"query\",\n path: \"path\",\n header: \"header\",\n cookie: \"cookie\",\n querystring: \"query\",\n params: \"path\",\n path_params: \"path\",\n headers: \"header\",\n cookies: \"cookie\",\n};\n\nexport type ValidationErrorDetail = {\n source: string;\n field: string;\n message: string;\n type: string;\n};\n\ntype ValidationErrorAggregate = ValidationErrorDetail & {\n method: string;\n path: string;\n counts: Map<string | undefined, number>;\n};\n\nconst VALIDATION_ERROR_AGGREGATES_KEY = Symbol.for(\"apitally.validationErrorAggregates\");\nconst validationErrorAggregatesHolder = globalThis as Record<\n symbol,\n Map<string, ValidationErrorAggregate> | undefined\n>;\nconst validationErrorAggregates =\n validationErrorAggregatesHolder[VALIDATION_ERROR_AGGREGATES_KEY] ??\n new Map<string, ValidationErrorAggregate>();\nvalidationErrorAggregatesHolder[VALIDATION_ERROR_AGGREGATES_KEY] = validationErrorAggregates;\n\nexport function isValidationResponseStatus(statusCode: number): boolean {\n return statusCode === 400 || statusCode === 422;\n}\n\nexport function addValidationErrors(\n consumer: string | undefined,\n method: string,\n path: string,\n details: ValidationErrorDetail[],\n): void {\n method = method.toUpperCase();\n if (method === \"OPTIONS\" || !path) {\n return;\n }\n consumer = consumer?.slice(0, MAX_CONSUMER_LENGTH);\n for (const detail of details) {\n const fields = {\n method,\n path: path.slice(0, MAX_PATH_LENGTH),\n source: detail.source.slice(0, MAX_SOURCE_LENGTH),\n field: detail.field.slice(0, MAX_FIELD_LENGTH),\n message: detail.message.slice(0, MAX_MESSAGE_LENGTH),\n type: detail.type.slice(0, MAX_TYPE_LENGTH),\n };\n const key = Object.values(fields).join(\"\\0\");\n let aggregate = validationErrorAggregates.get(key);\n if (!aggregate) {\n if (validationErrorAggregates.size >= MAX_ERRORS) {\n continue;\n }\n aggregate = { ...fields, counts: new Map() };\n validationErrorAggregates.set(key, aggregate);\n }\n aggregate.counts.set(consumer, (aggregate.counts.get(consumer) ?? 0) + 1);\n }\n}\n\nexport function drainValidationErrors(): AnyValueMap[] {\n const aggregates = [...validationErrorAggregates.values()];\n validationErrorAggregates.clear();\n return aggregates.map(({ counts, ...fields }) => ({\n ...fields,\n counts: [...counts].map(([consumer, count]) =>\n consumer === undefined ? { count } : { consumer, count },\n ),\n }));\n}\n\nexport function resetValidationErrors(): void {\n validationErrorAggregates.clear();\n}\n\nexport function normalizeSource(source: unknown): string {\n return typeof source === \"string\" ? (SOURCE_ALIASES[source.toLowerCase()] ?? \"\") : \"\";\n}\n\n// Zod, Standard Schema, and TypeBox issues share this shape. A Standard Schema\n// path segment may be an object carrying the key; a TypeBox path is a JSON pointer.\nexport function formatIssues(issues: unknown, source = \"\"): ValidationErrorDetail[] {\n if (!Array.isArray(issues)) {\n return [];\n }\n const details: ValidationErrorDetail[] = [];\n for (const issue of issues) {\n if (!isRecord(issue) || typeof issue.message !== \"string\") {\n continue;\n }\n const path = Array.isArray(issue.path)\n ? issue.path\n : typeof issue.path === \"string\"\n ? issue.path\n .split(\"/\")\n .filter(Boolean)\n .map((segment) => segment.replace(/~1/g, \"/\").replace(/~0/g, \"~\"))\n : [];\n details.push({\n source,\n field: path\n .map((segment: unknown) => (isRecord(segment) ? segment.key : segment))\n .filter((segment) => typeof segment === \"string\" || typeof segment === \"number\")\n .join(\".\"),\n message: issue.message,\n type: typeof issue.code === \"string\" ? issue.code : \"\",\n });\n }\n return details;\n}\n\n// zod 3 serializes issues as `error.issues`; zod 4 hides them as a JSON string\n// in `error.message`.\nexport function extractZodValidationErrors(body: unknown): ValidationErrorDetail[] {\n if (!isRecord(body)) {\n return [];\n }\n const error = isRecord(body.error) ? body.error : undefined;\n let issues = body.issues ?? error?.issues;\n if (issues === undefined && error?.name === \"ZodError\" && typeof error.message === \"string\") {\n try {\n issues = JSON.parse(error.message);\n } catch {\n return [];\n }\n }\n return formatIssues(issues);\n}\n\n// Recognized response shapes: zod issues, express-validator, and celebrate.\nexport function extractExpressValidationErrors(body: unknown): ValidationErrorDetail[] {\n if (!isRecord(body)) {\n return [];\n }\n const zodDetails = extractZodValidationErrors(body);\n if (zodDetails.length > 0) {\n return zodDetails;\n }\n if (Array.isArray(body.errors)) {\n return extractExpressValidatorErrors(body.errors);\n }\n if (isRecord(body.validation)) {\n return extractCelebrateErrors(body.validation);\n }\n return [];\n}\n\nfunction extractExpressValidatorErrors(errors: unknown[]): ValidationErrorDetail[] {\n const details: ValidationErrorDetail[] = [];\n for (const error of errors) {\n if (\n !isRecord(error) ||\n error.type !== \"field\" ||\n typeof error.path !== \"string\" ||\n typeof error.msg !== \"string\"\n ) {\n continue;\n }\n const source = normalizeSource(error.location);\n if (source) {\n details.push({ source, field: error.path, message: error.msg, type: \"\" });\n }\n }\n return details;\n}\n\n// Celebrate joins the messages of all failed keys into one sentence list, so\n// each key takes the sentence naming it.\nfunction extractCelebrateErrors(validation: Record<string, unknown>): ValidationErrorDetail[] {\n const details: ValidationErrorDetail[] = [];\n for (const error of Object.values(validation)) {\n if (!isRecord(error) || !Array.isArray(error.keys) || typeof error.message !== \"string\") {\n continue;\n }\n const sentences = error.message.split(\". \");\n for (const key of error.keys) {\n if (typeof key === \"string\") {\n details.push({\n source: normalizeSource(error.source),\n field: key,\n message: sentences.find((sentence) => sentence.includes(`\"${key}\"`)) ?? error.message,\n type: \"\",\n });\n }\n }\n }\n return details;\n}\n\nexport function parseJsonResponseBody(body: Buffer | undefined, contentEncoding: unknown): unknown {\n if (!body) {\n return undefined;\n }\n try {\n const encoding =\n typeof contentEncoding === \"string\" ? contentEncoding.trim().toLowerCase() : \"\";\n let decoded = body;\n if (encoding === \"br\") {\n decoded = brotliDecompressSync(body, { maxOutputLength: MAX_BODY_SIZE });\n } else if (encoding === \"gzip\" || encoding === \"deflate\") {\n decoded = unzipSync(body, { maxOutputLength: MAX_BODY_SIZE });\n } else if (encoding !== \"\" && encoding !== \"identity\") {\n return undefined;\n }\n return JSON.parse(decoded.toString());\n } catch {\n // A body that cannot be decoded as JSON carries no recognizable validation details.\n }\n return undefined;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apitally",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0-beta.4",
4
4
  "description": "Simple API monitoring, analytics and request logging for REST APIs built with AdonisJS, Elysia, Express, Fastify, H3, Hapi, Hono, Koa and NestJS.",
5
5
  "author": "Apitally <hello@apitally.io>",
6
6
  "license": "MIT",
@@ -289,7 +289,7 @@
289
289
  "elysia": "^1.4.29",
290
290
  "express": "^5.2.1",
291
291
  "fastify": "^5.11.0",
292
- "h3": "2.0.1-rc.31",
292
+ "h3": "2.0.1-rc.32",
293
293
  "hono": "^4.12.31",
294
294
  "knip": "^6.29.0",
295
295
  "koa": "^3.2.1",
package/dist/consumer.cjs DELETED
@@ -1,61 +0,0 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_logger = require("./logger.cjs");
3
- const require_context = require("./context.cjs");
4
- const require_requestAttributes = require("./requestAttributes.cjs");
5
- //#region src/consumer.ts
6
- const CONSUMER_IDENTIFIER_ATTRIBUTE = "apitally.consumer.identifier";
7
- const CONSUMER_NAME_ATTRIBUTE = "apitally.consumer.name";
8
- const CONSUMER_GROUP_ATTRIBUTE = "apitally.consumer.group";
9
- function setConsumer(consumer) {
10
- try {
11
- const holder = require_context.getConsumerHolder();
12
- const record = require_context.getRequestRecord();
13
- const normalized = normalizeConsumer(consumer);
14
- if (!holder && !record || !normalized) return;
15
- if (holder) {
16
- holder.identifier = normalized.identifier;
17
- holder.name = normalized.name;
18
- holder.group = normalized.group;
19
- }
20
- writeConsumerAttributes(require_context.getServerSpan(), record, normalized);
21
- } catch (error) {
22
- require_logger.logDebug(`Error setting consumer: ${String(error)}`);
23
- }
24
- }
25
- function writeConsumerAttributesFromContext(parentContext, span, record) {
26
- const holder = parentContext.getValue(require_context.CONSUMER_HOLDER_KEY);
27
- if (!holder?.identifier) return;
28
- const consumer = normalizeConsumer({
29
- identifier: holder.identifier,
30
- name: holder.name,
31
- group: holder.group
32
- });
33
- if (consumer) writeConsumerAttributes(span, record, consumer);
34
- }
35
- function writeConsumerAttributes(span, record, consumer) {
36
- require_requestAttributes.writeRequestAttribute(span, record, CONSUMER_IDENTIFIER_ATTRIBUTE, consumer.identifier);
37
- if (consumer.name) require_requestAttributes.writeRequestAttribute(span, record, CONSUMER_NAME_ATTRIBUTE, consumer.name);
38
- if (consumer.group) require_requestAttributes.writeRequestAttribute(span, record, CONSUMER_GROUP_ATTRIBUTE, consumer.group);
39
- }
40
- function normalizeConsumer(consumer) {
41
- if (typeof consumer === "object" && consumer !== null) {
42
- const identifier = trimAndCap(consumer.identifier, 128);
43
- if (!identifier) return;
44
- return {
45
- identifier,
46
- name: trimAndCap(consumer.name, 64),
47
- group: trimAndCap(consumer.group, 64)
48
- };
49
- }
50
- const identifier = trimAndCap(consumer, 128);
51
- return identifier ? { identifier } : void 0;
52
- }
53
- function trimAndCap(value, maxLength) {
54
- if (typeof value !== "string" && typeof value !== "number") return;
55
- return String(value).trim().slice(0, maxLength) || void 0;
56
- }
57
- //#endregion
58
- exports.setConsumer = setConsumer;
59
- exports.writeConsumerAttributesFromContext = writeConsumerAttributesFromContext;
60
-
61
- //# sourceMappingURL=consumer.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"consumer.cjs","names":["getConsumerHolder","getRequestRecord","getServerSpan","CONSUMER_HOLDER_KEY"],"sources":["../src/consumer.ts"],"sourcesContent":["import type { Context, Span } from \"@opentelemetry/api\";\nimport {\n CONSUMER_HOLDER_KEY,\n type ConsumerHolder,\n getConsumerHolder,\n getRequestRecord,\n getServerSpan,\n type RequestRecord,\n} from \"./context.js\";\nimport { logDebug } from \"./logger.js\";\nimport { writeRequestAttribute } from \"./requestAttributes.js\";\n\nexport interface ApitallyConsumer {\n identifier: string;\n name?: string | null;\n group?: string | null;\n}\n\nconst CONSUMER_IDENTIFIER_ATTRIBUTE = \"apitally.consumer.identifier\";\nconst CONSUMER_NAME_ATTRIBUTE = \"apitally.consumer.name\";\nconst CONSUMER_GROUP_ATTRIBUTE = \"apitally.consumer.group\";\n\nexport function setConsumer(consumer: ApitallyConsumer | string | null | undefined): void {\n try {\n const holder = getConsumerHolder();\n const record = getRequestRecord();\n const normalized = normalizeConsumer(consumer);\n if ((!holder && !record) || !normalized) {\n return;\n }\n if (holder) {\n holder.identifier = normalized.identifier;\n holder.name = normalized.name;\n holder.group = normalized.group;\n }\n writeConsumerAttributes(getServerSpan(), record, normalized);\n } catch (error) {\n logDebug(`Error setting consumer: ${String(error)}`);\n }\n}\n\n// Applies a consumer inherited in the parent context when a SERVER span starts.\nexport function writeConsumerAttributesFromContext(\n parentContext: Context,\n span: Span,\n record: RequestRecord | undefined,\n): void {\n const holder = parentContext.getValue(CONSUMER_HOLDER_KEY) as ConsumerHolder | undefined;\n if (!holder?.identifier) {\n return;\n }\n const consumer = normalizeConsumer({\n identifier: holder.identifier,\n name: holder.name,\n group: holder.group,\n });\n if (consumer) {\n writeConsumerAttributes(span, record, consumer);\n }\n}\n\nfunction writeConsumerAttributes(\n span: Span | undefined,\n record: RequestRecord | undefined,\n consumer: ApitallyConsumer,\n): void {\n writeRequestAttribute(span, record, CONSUMER_IDENTIFIER_ATTRIBUTE, consumer.identifier);\n if (consumer.name) {\n writeRequestAttribute(span, record, CONSUMER_NAME_ATTRIBUTE, consumer.name);\n }\n if (consumer.group) {\n writeRequestAttribute(span, record, CONSUMER_GROUP_ATTRIBUTE, consumer.group);\n }\n}\n\nfunction normalizeConsumer(consumer: ApitallyConsumer | string | number | null | undefined) {\n if (typeof consumer === \"object\" && consumer !== null) {\n const identifier = trimAndCap(consumer.identifier, 128);\n if (!identifier) {\n return undefined;\n }\n return {\n identifier,\n name: trimAndCap(consumer.name, 64),\n group: trimAndCap(consumer.group, 64),\n };\n }\n const identifier = trimAndCap(consumer, 128);\n return identifier ? { identifier } : undefined;\n}\n\nfunction trimAndCap(value: unknown, maxLength: number): string | undefined {\n if (typeof value !== \"string\" && typeof value !== \"number\") {\n return undefined;\n }\n return String(value).trim().slice(0, maxLength) || undefined;\n}\n"],"mappings":";;;;;AAkBA,MAAM,gCAAgC;AACtC,MAAM,0BAA0B;AAChC,MAAM,2BAA2B;AAEjC,SAAgB,YAAY,UAA8D;CACxF,IAAI;EACF,MAAM,SAASA,gBAAAA,kBAAkB;EACjC,MAAM,SAASC,gBAAAA,iBAAiB;EAChC,MAAM,aAAa,kBAAkB,QAAQ;EAC7C,IAAK,CAAC,UAAU,CAAC,UAAW,CAAC,YAC3B;EAEF,IAAI,QAAQ;GACV,OAAO,aAAa,WAAW;GAC/B,OAAO,OAAO,WAAW;GACzB,OAAO,QAAQ,WAAW;EAC5B;EACA,wBAAwBC,gBAAAA,cAAc,GAAG,QAAQ,UAAU;CAC7D,SAAS,OAAO;EACd,eAAA,SAAS,2BAA2B,OAAO,KAAK,GAAG;CACrD;AACF;AAGA,SAAgB,mCACd,eACA,MACA,QACM;CACN,MAAM,SAAS,cAAc,SAASC,gBAAAA,mBAAmB;CACzD,IAAI,CAAC,QAAQ,YACX;CAEF,MAAM,WAAW,kBAAkB;EACjC,YAAY,OAAO;EACnB,MAAM,OAAO;EACb,OAAO,OAAO;CAChB,CAAC;CACD,IAAI,UACF,wBAAwB,MAAM,QAAQ,QAAQ;AAElD;AAEA,SAAS,wBACP,MACA,QACA,UACM;CACN,0BAAA,sBAAsB,MAAM,QAAQ,+BAA+B,SAAS,UAAU;CACtF,IAAI,SAAS,MACX,0BAAA,sBAAsB,MAAM,QAAQ,yBAAyB,SAAS,IAAI;CAE5E,IAAI,SAAS,OACX,0BAAA,sBAAsB,MAAM,QAAQ,0BAA0B,SAAS,KAAK;AAEhF;AAEA,SAAS,kBAAkB,UAAiE;CAC1F,IAAI,OAAO,aAAa,YAAY,aAAa,MAAM;EACrD,MAAM,aAAa,WAAW,SAAS,YAAY,GAAG;EACtD,IAAI,CAAC,YACH;EAEF,OAAO;GACL;GACA,MAAM,WAAW,SAAS,MAAM,EAAE;GAClC,OAAO,WAAW,SAAS,OAAO,EAAE;EACtC;CACF;CACA,MAAM,aAAa,WAAW,UAAU,GAAG;CAC3C,OAAO,aAAa,EAAE,WAAW,IAAI,KAAA;AACvC;AAEA,SAAS,WAAW,OAAgB,WAAuC;CACzE,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UAChD;CAEF,OAAO,OAAO,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,SAAS,KAAK,KAAA;AACrD"}
@@ -1,12 +0,0 @@
1
- import { RequestRecord } from "./context.cjs";
2
- import { Context, Span } from "@opentelemetry/api";
3
- //#region src/consumer.d.ts
4
- export interface ApitallyConsumer {
5
- identifier: string;
6
- name?: string | null;
7
- group?: string | null;
8
- }
9
- export declare function setConsumer(consumer: ApitallyConsumer | string | null | undefined): void;
10
- export declare function writeConsumerAttributesFromContext(parentContext: Context, span: Span, record: RequestRecord | undefined): void;
11
- //#endregion
12
- //# sourceMappingURL=consumer.d.cts.map
@@ -1,12 +0,0 @@
1
- import { RequestRecord } from "./context.js";
2
- import { Context, Span } from "@opentelemetry/api";
3
- //#region src/consumer.d.ts
4
- export interface ApitallyConsumer {
5
- identifier: string;
6
- name?: string | null;
7
- group?: string | null;
8
- }
9
- export declare function setConsumer(consumer: ApitallyConsumer | string | null | undefined): void;
10
- export declare function writeConsumerAttributesFromContext(parentContext: Context, span: Span, record: RequestRecord | undefined): void;
11
- //#endregion
12
- //# sourceMappingURL=consumer.d.ts.map
package/dist/consumer.js DELETED
@@ -1,59 +0,0 @@
1
- import { logDebug } from "./logger.js";
2
- import { CONSUMER_HOLDER_KEY, getConsumerHolder, getRequestRecord, getServerSpan } from "./context.js";
3
- import { writeRequestAttribute } from "./requestAttributes.js";
4
- //#region src/consumer.ts
5
- const CONSUMER_IDENTIFIER_ATTRIBUTE = "apitally.consumer.identifier";
6
- const CONSUMER_NAME_ATTRIBUTE = "apitally.consumer.name";
7
- const CONSUMER_GROUP_ATTRIBUTE = "apitally.consumer.group";
8
- function setConsumer(consumer) {
9
- try {
10
- const holder = getConsumerHolder();
11
- const record = getRequestRecord();
12
- const normalized = normalizeConsumer(consumer);
13
- if (!holder && !record || !normalized) return;
14
- if (holder) {
15
- holder.identifier = normalized.identifier;
16
- holder.name = normalized.name;
17
- holder.group = normalized.group;
18
- }
19
- writeConsumerAttributes(getServerSpan(), record, normalized);
20
- } catch (error) {
21
- logDebug(`Error setting consumer: ${String(error)}`);
22
- }
23
- }
24
- function writeConsumerAttributesFromContext(parentContext, span, record) {
25
- const holder = parentContext.getValue(CONSUMER_HOLDER_KEY);
26
- if (!holder?.identifier) return;
27
- const consumer = normalizeConsumer({
28
- identifier: holder.identifier,
29
- name: holder.name,
30
- group: holder.group
31
- });
32
- if (consumer) writeConsumerAttributes(span, record, consumer);
33
- }
34
- function writeConsumerAttributes(span, record, consumer) {
35
- writeRequestAttribute(span, record, CONSUMER_IDENTIFIER_ATTRIBUTE, consumer.identifier);
36
- if (consumer.name) writeRequestAttribute(span, record, CONSUMER_NAME_ATTRIBUTE, consumer.name);
37
- if (consumer.group) writeRequestAttribute(span, record, CONSUMER_GROUP_ATTRIBUTE, consumer.group);
38
- }
39
- function normalizeConsumer(consumer) {
40
- if (typeof consumer === "object" && consumer !== null) {
41
- const identifier = trimAndCap(consumer.identifier, 128);
42
- if (!identifier) return;
43
- return {
44
- identifier,
45
- name: trimAndCap(consumer.name, 64),
46
- group: trimAndCap(consumer.group, 64)
47
- };
48
- }
49
- const identifier = trimAndCap(consumer, 128);
50
- return identifier ? { identifier } : void 0;
51
- }
52
- function trimAndCap(value, maxLength) {
53
- if (typeof value !== "string" && typeof value !== "number") return;
54
- return String(value).trim().slice(0, maxLength) || void 0;
55
- }
56
- //#endregion
57
- export { setConsumer, writeConsumerAttributesFromContext };
58
-
59
- //# sourceMappingURL=consumer.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"consumer.js","names":[],"sources":["../src/consumer.ts"],"sourcesContent":["import type { Context, Span } from \"@opentelemetry/api\";\nimport {\n CONSUMER_HOLDER_KEY,\n type ConsumerHolder,\n getConsumerHolder,\n getRequestRecord,\n getServerSpan,\n type RequestRecord,\n} from \"./context.js\";\nimport { logDebug } from \"./logger.js\";\nimport { writeRequestAttribute } from \"./requestAttributes.js\";\n\nexport interface ApitallyConsumer {\n identifier: string;\n name?: string | null;\n group?: string | null;\n}\n\nconst CONSUMER_IDENTIFIER_ATTRIBUTE = \"apitally.consumer.identifier\";\nconst CONSUMER_NAME_ATTRIBUTE = \"apitally.consumer.name\";\nconst CONSUMER_GROUP_ATTRIBUTE = \"apitally.consumer.group\";\n\nexport function setConsumer(consumer: ApitallyConsumer | string | null | undefined): void {\n try {\n const holder = getConsumerHolder();\n const record = getRequestRecord();\n const normalized = normalizeConsumer(consumer);\n if ((!holder && !record) || !normalized) {\n return;\n }\n if (holder) {\n holder.identifier = normalized.identifier;\n holder.name = normalized.name;\n holder.group = normalized.group;\n }\n writeConsumerAttributes(getServerSpan(), record, normalized);\n } catch (error) {\n logDebug(`Error setting consumer: ${String(error)}`);\n }\n}\n\n// Applies a consumer inherited in the parent context when a SERVER span starts.\nexport function writeConsumerAttributesFromContext(\n parentContext: Context,\n span: Span,\n record: RequestRecord | undefined,\n): void {\n const holder = parentContext.getValue(CONSUMER_HOLDER_KEY) as ConsumerHolder | undefined;\n if (!holder?.identifier) {\n return;\n }\n const consumer = normalizeConsumer({\n identifier: holder.identifier,\n name: holder.name,\n group: holder.group,\n });\n if (consumer) {\n writeConsumerAttributes(span, record, consumer);\n }\n}\n\nfunction writeConsumerAttributes(\n span: Span | undefined,\n record: RequestRecord | undefined,\n consumer: ApitallyConsumer,\n): void {\n writeRequestAttribute(span, record, CONSUMER_IDENTIFIER_ATTRIBUTE, consumer.identifier);\n if (consumer.name) {\n writeRequestAttribute(span, record, CONSUMER_NAME_ATTRIBUTE, consumer.name);\n }\n if (consumer.group) {\n writeRequestAttribute(span, record, CONSUMER_GROUP_ATTRIBUTE, consumer.group);\n }\n}\n\nfunction normalizeConsumer(consumer: ApitallyConsumer | string | number | null | undefined) {\n if (typeof consumer === \"object\" && consumer !== null) {\n const identifier = trimAndCap(consumer.identifier, 128);\n if (!identifier) {\n return undefined;\n }\n return {\n identifier,\n name: trimAndCap(consumer.name, 64),\n group: trimAndCap(consumer.group, 64),\n };\n }\n const identifier = trimAndCap(consumer, 128);\n return identifier ? { identifier } : undefined;\n}\n\nfunction trimAndCap(value: unknown, maxLength: number): string | undefined {\n if (typeof value !== \"string\" && typeof value !== \"number\") {\n return undefined;\n }\n return String(value).trim().slice(0, maxLength) || undefined;\n}\n"],"mappings":";;;;AAkBA,MAAM,gCAAgC;AACtC,MAAM,0BAA0B;AAChC,MAAM,2BAA2B;AAEjC,SAAgB,YAAY,UAA8D;CACxF,IAAI;EACF,MAAM,SAAS,kBAAkB;EACjC,MAAM,SAAS,iBAAiB;EAChC,MAAM,aAAa,kBAAkB,QAAQ;EAC7C,IAAK,CAAC,UAAU,CAAC,UAAW,CAAC,YAC3B;EAEF,IAAI,QAAQ;GACV,OAAO,aAAa,WAAW;GAC/B,OAAO,OAAO,WAAW;GACzB,OAAO,QAAQ,WAAW;EAC5B;EACA,wBAAwB,cAAc,GAAG,QAAQ,UAAU;CAC7D,SAAS,OAAO;EACd,SAAS,2BAA2B,OAAO,KAAK,GAAG;CACrD;AACF;AAGA,SAAgB,mCACd,eACA,MACA,QACM;CACN,MAAM,SAAS,cAAc,SAAS,mBAAmB;CACzD,IAAI,CAAC,QAAQ,YACX;CAEF,MAAM,WAAW,kBAAkB;EACjC,YAAY,OAAO;EACnB,MAAM,OAAO;EACb,OAAO,OAAO;CAChB,CAAC;CACD,IAAI,UACF,wBAAwB,MAAM,QAAQ,QAAQ;AAElD;AAEA,SAAS,wBACP,MACA,QACA,UACM;CACN,sBAAsB,MAAM,QAAQ,+BAA+B,SAAS,UAAU;CACtF,IAAI,SAAS,MACX,sBAAsB,MAAM,QAAQ,yBAAyB,SAAS,IAAI;CAE5E,IAAI,SAAS,OACX,sBAAsB,MAAM,QAAQ,0BAA0B,SAAS,KAAK;AAEhF;AAEA,SAAS,kBAAkB,UAAiE;CAC1F,IAAI,OAAO,aAAa,YAAY,aAAa,MAAM;EACrD,MAAM,aAAa,WAAW,SAAS,YAAY,GAAG;EACtD,IAAI,CAAC,YACH;EAEF,OAAO;GACL;GACA,MAAM,WAAW,SAAS,MAAM,EAAE;GAClC,OAAO,WAAW,SAAS,OAAO,EAAE;EACtC;CACF;CACA,MAAM,aAAa,WAAW,UAAU,GAAG;CAC3C,OAAO,aAAa,EAAE,WAAW,IAAI,KAAA;AACvC;AAEA,SAAS,WAAW,OAAgB,WAAuC;CACzE,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UAChD;CAEF,OAAO,OAAO,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,SAAS,KAAK,KAAA;AACrD"}