apitally
Version:
Simple API monitoring & analytics for REST APIs built with Express, Fastify, NestJS, AdonisJS, Hono, H3, Elysia, Hapi, and Koa.
1,089 lines (1,077 loc) • 34.4 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/common/client.ts
var client_exports = {};
__export(client_exports, {
ApitallyClient: () => ApitallyClient
});
module.exports = __toCommonJS(client_exports);
var import_fetch_retry = __toESM(require("fetch-retry"), 1);
var import_node_crypto5 = require("crypto");
// src/common/consumerRegistry.ts
var _ConsumerRegistry = class _ConsumerRegistry {
consumers;
updated;
constructor() {
this.consumers = /* @__PURE__ */ new Map();
this.updated = /* @__PURE__ */ new Set();
}
addOrUpdateConsumer(consumer) {
if (!consumer || !consumer.name && !consumer.group) {
return;
}
const existing = this.consumers.get(consumer.identifier);
if (!existing) {
this.consumers.set(consumer.identifier, consumer);
this.updated.add(consumer.identifier);
} else {
if (consumer.name && consumer.name !== existing.name) {
existing.name = consumer.name;
this.updated.add(consumer.identifier);
}
if (consumer.group && consumer.group !== existing.group) {
existing.group = consumer.group;
this.updated.add(consumer.identifier);
}
}
}
getAndResetUpdatedConsumers() {
const data = [];
this.updated.forEach((identifier) => {
const consumer = this.consumers.get(identifier);
if (consumer) {
data.push(consumer);
}
});
this.updated.clear();
return data;
}
};
__name(_ConsumerRegistry, "ConsumerRegistry");
var ConsumerRegistry = _ConsumerRegistry;
// src/common/logging.ts
var import_winston = require("winston");
function getLogger() {
return (0, import_winston.createLogger)({
level: process.env.APITALLY_DEBUG ? "debug" : "warn",
format: import_winston.format.combine(import_winston.format.colorize(), import_winston.format.timestamp(), import_winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`)),
transports: [
new import_winston.transports.Console()
]
});
}
__name(getLogger, "getLogger");
// src/common/paramValidation.ts
function isValidClientId(clientId) {
const regexExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return regexExp.test(clientId);
}
__name(isValidClientId, "isValidClientId");
function isValidEnv(env) {
const regexExp = /^[\w-]{1,32}$/;
return regexExp.test(env);
}
__name(isValidEnv, "isValidEnv");
// src/common/requestCounter.ts
var _RequestCounter = class _RequestCounter {
requestCounts;
requestSizeSums;
responseSizeSums;
responseTimes;
requestSizes;
responseSizes;
constructor() {
this.requestCounts = /* @__PURE__ */ new Map();
this.requestSizeSums = /* @__PURE__ */ new Map();
this.responseSizeSums = /* @__PURE__ */ new Map();
this.responseTimes = /* @__PURE__ */ new Map();
this.requestSizes = /* @__PURE__ */ new Map();
this.responseSizes = /* @__PURE__ */ new Map();
}
getKey(requestInfo) {
return [
requestInfo.consumer || "",
requestInfo.method.toUpperCase(),
requestInfo.path,
requestInfo.statusCode
].join("|");
}
addRequest(requestInfo) {
const key = this.getKey(requestInfo);
this.requestCounts.set(key, (this.requestCounts.get(key) || 0) + 1);
if (!this.responseTimes.has(key)) {
this.responseTimes.set(key, /* @__PURE__ */ new Map());
}
const responseTimeMap = this.responseTimes.get(key);
const responseTimeMsBin = Math.floor(requestInfo.responseTime / 10) * 10;
responseTimeMap.set(responseTimeMsBin, (responseTimeMap.get(responseTimeMsBin) || 0) + 1);
if (requestInfo.requestSize !== void 0) {
requestInfo.requestSize = Number(requestInfo.requestSize);
this.requestSizeSums.set(key, (this.requestSizeSums.get(key) || 0) + requestInfo.requestSize);
if (!this.requestSizes.has(key)) {
this.requestSizes.set(key, /* @__PURE__ */ new Map());
}
const requestSizeMap = this.requestSizes.get(key);
const requestSizeKbBin = Math.floor(requestInfo.requestSize / 1e3);
requestSizeMap.set(requestSizeKbBin, (requestSizeMap.get(requestSizeKbBin) || 0) + 1);
}
if (requestInfo.responseSize !== void 0) {
requestInfo.responseSize = Number(requestInfo.responseSize);
this.responseSizeSums.set(key, (this.responseSizeSums.get(key) || 0) + requestInfo.responseSize);
if (!this.responseSizes.has(key)) {
this.responseSizes.set(key, /* @__PURE__ */ new Map());
}
const responseSizeMap = this.responseSizes.get(key);
const responseSizeKbBin = Math.floor(requestInfo.responseSize / 1e3);
responseSizeMap.set(responseSizeKbBin, (responseSizeMap.get(responseSizeKbBin) || 0) + 1);
}
}
getAndResetRequests() {
const data = [];
this.requestCounts.forEach((count, key) => {
const [consumer, method, path, statusCodeStr] = key.split("|");
const responseTimes = this.responseTimes.get(key) || /* @__PURE__ */ new Map();
const requestSizes = this.requestSizes.get(key) || /* @__PURE__ */ new Map();
const responseSizes = this.responseSizes.get(key) || /* @__PURE__ */ new Map();
data.push({
consumer: consumer || null,
method,
path,
status_code: parseInt(statusCodeStr),
request_count: count,
request_size_sum: this.requestSizeSums.get(key) || 0,
response_size_sum: this.responseSizeSums.get(key) || 0,
response_times: Object.fromEntries(responseTimes),
request_sizes: Object.fromEntries(requestSizes),
response_sizes: Object.fromEntries(responseSizes)
});
});
this.requestCounts.clear();
this.requestSizeSums.clear();
this.responseSizeSums.clear();
this.responseTimes.clear();
this.requestSizes.clear();
this.responseSizes.clear();
return data;
}
};
__name(_RequestCounter, "RequestCounter");
var RequestCounter = _RequestCounter;
// src/common/requestLogger.ts
var import_async_lock = __toESM(require("async-lock"), 1);
var import_node_buffer2 = require("buffer");
var import_node_crypto3 = require("crypto");
var import_node_fs2 = require("fs");
var import_node_os2 = require("os");
var import_node_path2 = require("path");
// src/common/sentry.ts
var sentry;
(async () => {
try {
sentry = await import("@sentry/node");
} catch (e) {
}
})();
function getSentryEventId() {
if (sentry && sentry.lastEventId) {
return sentry.lastEventId();
}
return void 0;
}
__name(getSentryEventId, "getSentryEventId");
// src/common/serverErrorCounter.ts
var import_node_crypto = require("crypto");
var MAX_MSG_LENGTH = 2048;
var MAX_STACKTRACE_LENGTH = 65536;
var _ServerErrorCounter = class _ServerErrorCounter {
errorCounts;
errorDetails;
sentryEventIds;
constructor() {
this.errorCounts = /* @__PURE__ */ new Map();
this.errorDetails = /* @__PURE__ */ new Map();
this.sentryEventIds = /* @__PURE__ */ new Map();
}
addServerError(serverError) {
const key = this.getKey(serverError);
if (!this.errorDetails.has(key)) {
this.errorDetails.set(key, serverError);
}
this.errorCounts.set(key, (this.errorCounts.get(key) || 0) + 1);
const sentryEventId = getSentryEventId();
if (sentryEventId) {
this.sentryEventIds.set(key, sentryEventId);
}
}
getAndResetServerErrors() {
const data = [];
this.errorCounts.forEach((count, key) => {
const serverError = this.errorDetails.get(key);
if (serverError) {
data.push({
consumer: serverError.consumer || null,
method: serverError.method,
path: serverError.path,
type: serverError.type,
msg: truncateExceptionMessage(serverError.msg),
traceback: truncateExceptionStackTrace(serverError.traceback),
sentry_event_id: this.sentryEventIds.get(key) || null,
error_count: count
});
}
});
this.errorCounts.clear();
this.errorDetails.clear();
this.sentryEventIds.clear();
return data;
}
getKey(serverError) {
const hashInput = [
serverError.consumer || "",
serverError.method.toUpperCase(),
serverError.path,
serverError.type,
serverError.msg.trim(),
serverError.traceback.trim()
].join("|");
return (0, import_node_crypto.createHash)("md5").update(hashInput).digest("hex");
}
};
__name(_ServerErrorCounter, "ServerErrorCounter");
var ServerErrorCounter = _ServerErrorCounter;
function truncateExceptionMessage(msg) {
if (msg.length <= MAX_MSG_LENGTH) {
return msg;
}
const suffix = "... (truncated)";
const cutoff = MAX_MSG_LENGTH - suffix.length;
return msg.substring(0, cutoff) + suffix;
}
__name(truncateExceptionMessage, "truncateExceptionMessage");
function truncateExceptionStackTrace(stack) {
const suffix = "... (truncated) ...";
const cutoff = MAX_STACKTRACE_LENGTH - suffix.length;
const lines = stack.trim().split("\n");
const truncatedLines = [];
let length = 0;
for (const line of lines) {
if (length + line.length + 1 > cutoff) {
truncatedLines.push(suffix);
break;
}
truncatedLines.push(line);
length += line.length + 1;
}
return truncatedLines.join("\n");
}
__name(truncateExceptionStackTrace, "truncateExceptionStackTrace");
// src/common/tempGzipFile.ts
var import_node_buffer = require("buffer");
var import_node_crypto2 = require("crypto");
var import_node_fs = require("fs");
var import_promises = require("fs/promises");
var import_node_os = require("os");
var import_node_path = require("path");
var import_node_zlib = require("zlib");
var _TempGzipFile = class _TempGzipFile {
uuid;
filePath;
gzip;
writeStream;
readyPromise;
closedPromise;
constructor() {
this.uuid = (0, import_node_crypto2.randomUUID)();
this.filePath = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `apitally-${this.uuid}.gz`);
this.writeStream = (0, import_node_fs.createWriteStream)(this.filePath);
this.readyPromise = new Promise((resolve, reject) => {
this.writeStream.once("ready", resolve);
this.writeStream.once("error", reject);
});
this.closedPromise = new Promise((resolve, reject) => {
this.writeStream.once("close", resolve);
this.writeStream.once("error", reject);
});
this.gzip = (0, import_node_zlib.createGzip)();
this.gzip.pipe(this.writeStream);
}
get size() {
return this.writeStream.bytesWritten;
}
async writeLine(data) {
await this.readyPromise;
return new Promise((resolve, reject) => {
this.gzip.write(import_node_buffer.Buffer.concat([
data,
import_node_buffer.Buffer.from("\n")
]), (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
async getContent() {
return new Promise((resolve, reject) => {
(0, import_node_fs.readFile)(this.filePath, (error, data) => {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
}
async close() {
await new Promise((resolve) => {
this.gzip.end(() => {
resolve();
});
});
await this.closedPromise;
}
async delete() {
await this.close();
await (0, import_promises.unlink)(this.filePath);
}
};
__name(_TempGzipFile, "TempGzipFile");
var TempGzipFile = _TempGzipFile;
// src/common/requestLogger.ts
var MAX_BODY_SIZE = 5e4;
var MAX_FILE_SIZE = 1e6;
var MAX_FILES = 50;
var MAX_PENDING_WRITES = 100;
var MAX_LOG_MSG_LENGTH = 2048;
var BODY_TOO_LARGE = import_node_buffer2.Buffer.from("<body too large>");
var BODY_MASKED = import_node_buffer2.Buffer.from("<masked>");
var MASKED = "******";
var ALLOWED_CONTENT_TYPES = [
"application/json",
"application/problem+json",
"application/vnd.api+json",
"text/plain",
"text/html"
];
var EXCLUDE_PATH_PATTERNS = [
/\/_?healthz?$/i,
/\/_?health[_-]?checks?$/i,
/\/_?heart[_-]?beats?$/i,
/\/ping$/i,
/\/ready$/i,
/\/live$/i
];
var EXCLUDE_USER_AGENT_PATTERNS = [
/health[-_ ]?check/i,
/microsoft-azure-application-lb/i,
/googlehc/i,
/kube-probe/i
];
var MASK_QUERY_PARAM_PATTERNS = [
/auth/i,
/api-?key/i,
/secret/i,
/token/i,
/password/i,
/pwd/i
];
var MASK_HEADER_PATTERNS = [
/auth/i,
/api-?key/i,
/secret/i,
/token/i,
/cookie/i
];
var MASK_BODY_FIELD_PATTERNS = [
/password/i,
/pwd/i,
/token/i,
/secret/i,
/auth/i,
/card[-_ ]?number/i,
/ccv/i,
/ssn/i
];
var DEFAULT_CONFIG = {
enabled: false,
logQueryParams: true,
logRequestHeaders: false,
logRequestBody: false,
logResponseHeaders: true,
logResponseBody: false,
logException: true,
captureLogs: false,
maskQueryParams: [],
maskHeaders: [],
maskBodyFields: [],
excludePaths: []
};
var _RequestLogger = class _RequestLogger {
config;
enabled;
suspendUntil = null;
pendingWrites = [];
currentFile = null;
files = [];
maintainIntervalId;
lock = new import_async_lock.default();
constructor(config) {
this.config = {
...DEFAULT_CONFIG,
...config
};
this.enabled = this.config.enabled && checkWritableFs();
if (this.enabled) {
this.maintainIntervalId = setInterval(() => {
this.maintain();
}, 1e3);
}
}
get maxBodySize() {
return MAX_BODY_SIZE;
}
shouldExcludePath(urlPath) {
const patterns = [
...this.config.excludePaths,
...EXCLUDE_PATH_PATTERNS
];
return matchPatterns(urlPath, patterns);
}
shouldExcludeUserAgent(userAgent) {
return userAgent ? matchPatterns(userAgent, EXCLUDE_USER_AGENT_PATTERNS) : false;
}
shouldMaskQueryParam(name) {
const patterns = [
...this.config.maskQueryParams,
...MASK_QUERY_PARAM_PATTERNS
];
return matchPatterns(name, patterns);
}
shouldMaskHeader(name) {
const patterns = [
...this.config.maskHeaders,
...MASK_HEADER_PATTERNS
];
return matchPatterns(name, patterns);
}
shouldMaskBodyField(name) {
const patterns = [
...this.config.maskBodyFields,
...MASK_BODY_FIELD_PATTERNS
];
return matchPatterns(name, patterns);
}
hasSupportedContentType(headers) {
var _a2;
const contentType = (_a2 = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a2[1];
return this.isSupportedContentType(contentType);
}
hasJsonContentType(headers) {
var _a2;
const contentType = (_a2 = headers.find(([k]) => k.toLowerCase() === "content-type")) == null ? void 0 : _a2[1];
return contentType ? /\bjson\b/i.test(contentType) : null;
}
isSupportedContentType(contentType) {
return typeof contentType === "string" && ALLOWED_CONTENT_TYPES.some((t) => contentType.startsWith(t));
}
maskQueryParams(search) {
const params = new URLSearchParams(search);
for (const [key] of params) {
if (this.shouldMaskQueryParam(key)) {
params.set(key, MASKED);
}
}
return params.toString();
}
maskHeaders(headers) {
return headers.map(([k, v]) => [
k,
this.shouldMaskHeader(k) ? MASKED : v
]);
}
maskBody(data) {
if (typeof data === "object" && data !== null && !Array.isArray(data)) {
const result = {};
for (const [key, value] of Object.entries(data)) {
if (typeof value === "string" && this.shouldMaskBodyField(key)) {
result[key] = MASKED;
} else {
result[key] = this.maskBody(value);
}
}
return result;
}
if (Array.isArray(data)) {
return data.map((item) => this.maskBody(item));
}
return data;
}
applyMasking(item) {
if (this.config.maskRequestBodyCallback && item.request.body && item.request.body !== BODY_TOO_LARGE) {
try {
const maskedBody = this.config.maskRequestBodyCallback(item.request);
item.request.body = maskedBody ?? BODY_MASKED;
} catch {
item.request.body = void 0;
}
}
if (this.config.maskResponseBodyCallback && item.response.body && item.response.body !== BODY_TOO_LARGE) {
try {
const maskedBody = this.config.maskResponseBodyCallback(item.request, item.response);
item.response.body = maskedBody ?? BODY_MASKED;
} catch {
item.response.body = void 0;
}
}
if (item.request.body && item.request.body.length > MAX_BODY_SIZE) {
item.request.body = BODY_TOO_LARGE;
}
if (item.response.body && item.response.body.length > MAX_BODY_SIZE) {
item.response.body = BODY_TOO_LARGE;
}
for (const key of [
"request",
"response"
]) {
const bodyData = item[key].body;
if (!bodyData || bodyData === BODY_TOO_LARGE || bodyData === BODY_MASKED) {
continue;
}
const headers = item[key].headers;
const hasJsonContent = this.hasJsonContentType(headers);
if (hasJsonContent === null || hasJsonContent) {
try {
const parsedBody = JSON.parse(bodyData.toString());
const maskedBody = this.maskBody(parsedBody);
item[key].body = import_node_buffer2.Buffer.from(JSON.stringify(maskedBody));
} catch {
}
}
}
item.request.headers = this.config.logRequestHeaders ? this.maskHeaders(item.request.headers) : [];
item.response.headers = this.config.logResponseHeaders ? this.maskHeaders(item.response.headers) : [];
const url = new URL(item.request.url);
url.search = this.config.logQueryParams ? this.maskQueryParams(url.search) : "";
item.request.url = url.toString();
return item;
}
logRequest(request, response, error, logs) {
var _a2, _b, _c;
if (!this.enabled || this.suspendUntil !== null) return;
const url = new URL(request.url);
const path = request.path ?? url.pathname;
const userAgent = (_a2 = request.headers.find(([k]) => k.toLowerCase() === "user-agent")) == null ? void 0 : _a2[1];
if (this.shouldExcludePath(path) || this.shouldExcludeUserAgent(userAgent) || (((_c = (_b = this.config).excludeCallback) == null ? void 0 : _c.call(_b, request, response)) ?? false)) {
return;
}
if (!this.config.logRequestBody || !this.hasSupportedContentType(request.headers)) {
request.body = void 0;
}
if (!this.config.logResponseBody || !this.hasSupportedContentType(response.headers)) {
response.body = void 0;
}
if (request.size !== void 0 && request.size < 0) {
request.size = void 0;
}
if (response.size !== void 0 && response.size < 0) {
response.size = void 0;
}
const item = {
uuid: (0, import_node_crypto3.randomUUID)(),
request,
response,
exception: error && this.config.logException ? {
type: error.name,
message: truncateExceptionMessage(error.message),
stacktrace: truncateExceptionStackTrace(error.stack || ""),
sentryEventId: getSentryEventId()
} : void 0
};
if (logs && logs.length > 0) {
item.logs = logs.map((log) => ({
timestamp: log.timestamp,
logger: log.logger,
level: log.level,
message: truncateLogMessage(log.message)
}));
}
this.pendingWrites.push(item);
if (this.pendingWrites.length > MAX_PENDING_WRITES) {
this.pendingWrites.shift();
}
}
async writeToFile() {
if (!this.enabled || this.pendingWrites.length === 0) {
return;
}
return this.lock.acquire("file", async () => {
if (!this.currentFile) {
this.currentFile = new TempGzipFile();
}
while (this.pendingWrites.length > 0) {
let item = this.pendingWrites.shift();
if (item) {
item = this.applyMasking(item);
const finalItem = {
uuid: item.uuid,
request: skipEmptyValues(item.request),
response: skipEmptyValues(item.response),
exception: item.exception,
logs: item.logs
};
[
finalItem.request.body,
finalItem.response.body
].forEach((body) => {
if (body) {
body.toJSON = function() {
return this.toString("base64");
};
}
});
await this.currentFile.writeLine(import_node_buffer2.Buffer.from(JSON.stringify(finalItem)));
}
}
});
}
getFile() {
return this.files.shift();
}
retryFileLater(file) {
this.files.unshift(file);
}
async rotateFile() {
return this.lock.acquire("file", async () => {
if (this.currentFile) {
await this.currentFile.close();
this.files.push(this.currentFile);
this.currentFile = null;
}
});
}
async maintain() {
await this.writeToFile();
if (this.currentFile && this.currentFile.size > MAX_FILE_SIZE) {
await this.rotateFile();
}
while (this.files.length > MAX_FILES) {
const file = this.files.shift();
file == null ? void 0 : file.delete();
}
if (this.suspendUntil !== null && this.suspendUntil < Date.now()) {
this.suspendUntil = null;
}
}
async clear() {
this.pendingWrites = [];
await this.rotateFile();
this.files.forEach((file) => {
file.delete();
});
this.files = [];
}
async close() {
this.enabled = false;
await this.clear();
if (this.maintainIntervalId) {
clearInterval(this.maintainIntervalId);
}
}
};
__name(_RequestLogger, "RequestLogger");
var RequestLogger = _RequestLogger;
function matchPatterns(value, patterns) {
return patterns.some((pattern) => {
return pattern.test(value);
});
}
__name(matchPatterns, "matchPatterns");
function skipEmptyValues(data) {
return Object.fromEntries(Object.entries(data).filter(([_, v]) => {
if (v == null || Number.isNaN(v)) return false;
if (Array.isArray(v) || import_node_buffer2.Buffer.isBuffer(v) || typeof v === "string") {
return v.length > 0;
}
return true;
}));
}
__name(skipEmptyValues, "skipEmptyValues");
function truncateLogMessage(msg) {
if (msg.length > MAX_LOG_MSG_LENGTH) {
const suffix = "... (truncated)";
return msg.slice(0, MAX_LOG_MSG_LENGTH - suffix.length) + suffix;
}
return msg;
}
__name(truncateLogMessage, "truncateLogMessage");
function checkWritableFs() {
try {
const testPath = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `apitally-${(0, import_node_crypto3.randomUUID)()}`);
(0, import_node_fs2.writeFileSync)(testPath, "test");
(0, import_node_fs2.unlinkSync)(testPath);
return true;
} catch (error) {
return false;
}
}
__name(checkWritableFs, "checkWritableFs");
// src/common/validationErrorCounter.ts
var import_node_crypto4 = require("crypto");
var _ValidationErrorCounter = class _ValidationErrorCounter {
errorCounts;
errorDetails;
constructor() {
this.errorCounts = /* @__PURE__ */ new Map();
this.errorDetails = /* @__PURE__ */ new Map();
}
addValidationError(validationError) {
const key = this.getKey(validationError);
if (!this.errorDetails.has(key)) {
this.errorDetails.set(key, validationError);
}
this.errorCounts.set(key, (this.errorCounts.get(key) || 0) + 1);
}
getAndResetValidationErrors() {
const data = [];
this.errorCounts.forEach((count, key) => {
const validationError = this.errorDetails.get(key);
if (validationError) {
data.push({
consumer: validationError.consumer || null,
method: validationError.method,
path: validationError.path,
loc: validationError.loc.split(".").filter(Boolean),
msg: validationError.msg,
type: validationError.type,
error_count: count
});
}
});
this.errorCounts.clear();
this.errorDetails.clear();
return data;
}
getKey(validationError) {
const hashInput = [
validationError.consumer || "",
validationError.method.toUpperCase(),
validationError.path,
validationError.loc.split(".").filter(Boolean),
validationError.msg.trim(),
validationError.type
].join("|");
return (0, import_node_crypto4.createHash)("md5").update(hashInput).digest("hex");
}
};
__name(_ValidationErrorCounter, "ValidationErrorCounter");
var ValidationErrorCounter = _ValidationErrorCounter;
// src/common/client.ts
var SYNC_INTERVAL = 6e4;
var INITIAL_SYNC_INTERVAL = 1e4;
var INITIAL_SYNC_INTERVAL_DURATION = 36e5;
var MAX_QUEUE_TIME = 36e5;
var _a;
var HTTPError = (_a = class extends Error {
response;
constructor(response) {
const reason = response.status ? `status code ${response.status}` : "an unknown error";
super(`Request failed with ${reason}`);
this.response = response;
}
}, __name(_a, "HTTPError"), _a);
var _ApitallyClient = class _ApitallyClient {
clientId;
env;
instanceUuid;
syncDataQueue;
syncIntervalId;
startupData;
startupDataSent = false;
enabled = true;
requestCounter;
requestLogger;
validationErrorCounter;
serverErrorCounter;
consumerRegistry;
logger;
constructor({ clientId, env = "dev", requestLogging, requestLoggingConfig, logger }) {
if (_ApitallyClient.instance) {
throw new Error("Apitally client is already initialized");
}
if (!isValidClientId(clientId)) {
throw new Error(`Invalid Apitally client ID '${clientId}' (expecting hexadecimal UUID format)`);
}
if (!isValidEnv(env)) {
throw new Error(`Invalid env '${env}' (expecting 1-32 alphanumeric lowercase characters and hyphens only)`);
}
if (requestLoggingConfig && !requestLogging) {
console.warn("requestLoggingConfig is deprecated, use requestLogging instead.");
}
_ApitallyClient.instance = this;
this.clientId = clientId;
this.env = env;
this.instanceUuid = (0, import_node_crypto5.randomUUID)();
this.syncDataQueue = [];
this.requestCounter = new RequestCounter();
this.requestLogger = new RequestLogger(requestLogging ?? requestLoggingConfig);
this.validationErrorCounter = new ValidationErrorCounter();
this.serverErrorCounter = new ServerErrorCounter();
this.consumerRegistry = new ConsumerRegistry();
this.logger = logger ?? getLogger();
this.startSync();
this.handleShutdown = this.handleShutdown.bind(this);
}
static getInstance() {
if (!_ApitallyClient.instance) {
throw new Error("Apitally client is not initialized");
}
return _ApitallyClient.instance;
}
isEnabled() {
return this.enabled;
}
static async shutdown() {
if (_ApitallyClient.instance) {
await _ApitallyClient.instance.handleShutdown();
}
}
async handleShutdown() {
this.enabled = false;
this.stopSync();
await this.sendSyncData();
await this.sendLogData();
await this.requestLogger.close();
_ApitallyClient.instance = void 0;
}
getHubUrlPrefix() {
const baseURL = process.env.APITALLY_HUB_BASE_URL || "https://hub.apitally.io";
const version = "v2";
return `${baseURL}/${version}/${this.clientId}/${this.env}/`;
}
async sendData(url, payload) {
const fetchWithRetry = (0, import_fetch_retry.default)(fetch, {
retries: 3,
retryDelay: 1e3,
retryOn: [
408,
429,
500,
502,
503,
504
]
});
const response = await fetchWithRetry(this.getHubUrlPrefix() + url, {
method: "POST",
body: JSON.stringify(payload),
headers: {
"Content-Type": "application/json"
}
});
if (!response.ok) {
throw new HTTPError(response);
}
}
startSync() {
this.sync();
this.syncIntervalId = setInterval(() => {
this.sync();
}, INITIAL_SYNC_INTERVAL);
setTimeout(() => {
clearInterval(this.syncIntervalId);
this.syncIntervalId = setInterval(() => {
this.sync();
}, SYNC_INTERVAL);
}, INITIAL_SYNC_INTERVAL_DURATION);
}
async sync() {
try {
const promises = [
this.sendSyncData(),
this.sendLogData()
];
if (!this.startupDataSent) {
promises.push(this.sendStartupData());
}
await Promise.all(promises);
} catch (error) {
this.logger.error("Error while syncing with Apitally Hub", {
error
});
}
}
stopSync() {
if (this.syncIntervalId) {
clearInterval(this.syncIntervalId);
this.syncIntervalId = void 0;
}
}
setStartupData(data) {
this.startupData = data;
this.startupDataSent = false;
this.sendStartupData();
}
async sendStartupData() {
if (this.startupData) {
this.logger.debug("Sending startup data to Apitally Hub");
const payload = {
instance_uuid: this.instanceUuid,
message_uuid: (0, import_node_crypto5.randomUUID)(),
...this.startupData
};
try {
await this.sendData("startup", payload);
this.startupDataSent = true;
} catch (error) {
const handled = this.handleHubError(error);
if (!handled) {
this.logger.error(error.message);
this.logger.debug("Error while sending startup data to Apitally Hub (will retry)", {
error
});
}
}
}
}
async sendSyncData() {
this.logger.debug("Synchronizing data with Apitally Hub");
const newPayload = {
timestamp: Date.now() / 1e3,
instance_uuid: this.instanceUuid,
message_uuid: (0, import_node_crypto5.randomUUID)(),
requests: this.requestCounter.getAndResetRequests(),
validation_errors: this.validationErrorCounter.getAndResetValidationErrors(),
server_errors: this.serverErrorCounter.getAndResetServerErrors(),
consumers: this.consumerRegistry.getAndResetUpdatedConsumers()
};
this.syncDataQueue.push(newPayload);
let i = 0;
while (this.syncDataQueue.length > 0) {
const payload = this.syncDataQueue.shift();
if (payload) {
try {
if (Date.now() - payload.timestamp * 1e3 <= MAX_QUEUE_TIME) {
if (i > 0) {
await this.randomDelay();
}
await this.sendData("sync", payload);
i += 1;
}
} catch (error) {
const handled = this.handleHubError(error);
if (!handled) {
this.logger.debug("Error while synchronizing data with Apitally Hub (will retry)", {
error
});
this.syncDataQueue.push(payload);
break;
}
}
}
}
}
async sendLogData() {
this.logger.debug("Sending request log data to Apitally Hub");
await this.requestLogger.rotateFile();
const fetchWithRetry = (0, import_fetch_retry.default)(fetch, {
retries: 3,
retryDelay: 1e3,
retryOn: [
408,
429,
500,
502,
503,
504
]
});
let i = 0;
let logFile;
while (logFile = this.requestLogger.getFile()) {
if (i > 0) {
await this.randomDelay();
}
try {
const response = await fetchWithRetry(`${this.getHubUrlPrefix()}log?uuid=${logFile.uuid}`, {
method: "POST",
body: await logFile.getContent()
});
if (response.status === 402 && response.headers.has("Retry-After")) {
const retryAfter = parseInt(response.headers.get("Retry-After") ?? "0");
if (retryAfter > 0) {
this.requestLogger.suspendUntil = Date.now() + retryAfter * 1e3;
this.requestLogger.clear();
return;
}
}
if (!response.ok) {
throw new HTTPError(response);
}
logFile.delete();
} catch (error) {
this.requestLogger.retryFileLater(logFile);
break;
}
i++;
if (i >= 10) break;
}
}
handleHubError(error) {
if (error instanceof HTTPError) {
if (error.response.status === 404) {
this.logger.error(`Invalid Apitally client ID: '${this.clientId}'`);
this.enabled = false;
this.stopSync();
return true;
}
if (error.response.status === 422) {
this.logger.error("Received validation error from Apitally Hub");
return true;
}
}
return false;
}
async randomDelay() {
const delay = 100 + Math.random() * 400;
await new Promise((resolve) => setTimeout(resolve, delay));
}
};
__name(_ApitallyClient, "ApitallyClient");
__publicField(_ApitallyClient, "instance");
var ApitallyClient = _ApitallyClient;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ApitallyClient
});
//# sourceMappingURL=client.cjs.map