feat: Add configurable response data storage for notifications (#6684)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Frank Elsinga <frank@elsinga.de> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
034b8641c8
commit
751fe1bbf5
@ -0,0 +1,15 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.alterTable("monitor", function (table) {
|
||||||
|
table.boolean("save_response").notNullable().defaultTo(false);
|
||||||
|
table.boolean("save_error_response").notNullable().defaultTo(true);
|
||||||
|
table.integer("response_max_length").notNullable().defaultTo(10240); // Default 10KB
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.alterTable("monitor", function (table) {
|
||||||
|
table.dropColumn("save_response");
|
||||||
|
table.dropColumn("save_error_response");
|
||||||
|
table.dropColumn("response_max_length");
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.alterTable("heartbeat", function (table) {
|
||||||
|
table.text("response").nullable().defaultTo(null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.alterTable("heartbeat", function (table) {
|
||||||
|
table.dropColumn("response");
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -87,10 +87,12 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
|
|||||||
|
|
||||||
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
|
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`);
|
||||||
|
|
||||||
|
const result = list.map((bean) => bean.toJSON());
|
||||||
|
|
||||||
if (toUser) {
|
if (toUser) {
|
||||||
io.to(socket.userID).emit("importantHeartbeatList", monitorID, list, overwrite);
|
io.to(socket.userID).emit("importantHeartbeatList", monitorID, result, overwrite);
|
||||||
} else {
|
} else {
|
||||||
socket.emit("importantHeartbeatList", monitorID, list, overwrite);
|
socket.emit("importantHeartbeatList", monitorID, result, overwrite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
const zlib = require("node:zlib");
|
||||||
|
const { promisify } = require("node:util");
|
||||||
|
const gunzip = promisify(zlib.gunzip);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* status:
|
* status:
|
||||||
@ -36,8 +39,46 @@ class Heartbeat extends BeanModel {
|
|||||||
important: this._important,
|
important: this._important,
|
||||||
duration: this._duration,
|
duration: this._duration,
|
||||||
retries: this._retries,
|
retries: this._retries,
|
||||||
|
response: this._response,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object that ready to parse to JSON
|
||||||
|
* @param {{ decodeResponse?: boolean }} opts Options for JSON serialization
|
||||||
|
* @returns {Promise<object>} Object ready to parse
|
||||||
|
*/
|
||||||
|
async toJSONAsync(opts) {
|
||||||
|
return {
|
||||||
|
monitorID: this._monitorId,
|
||||||
|
status: this._status,
|
||||||
|
time: this._time,
|
||||||
|
msg: this._msg,
|
||||||
|
ping: this._ping,
|
||||||
|
important: this._important,
|
||||||
|
duration: this._duration,
|
||||||
|
retries: this._retries,
|
||||||
|
response: opts?.decodeResponse ? await Heartbeat.decodeResponseValue(this._response) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode compressed response payload stored in database.
|
||||||
|
* @param {string|null} response Encoded response payload.
|
||||||
|
* @returns {string|null} Decoded response payload.
|
||||||
|
*/
|
||||||
|
static async decodeResponseValue(response) {
|
||||||
|
if (!response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Offload gzip decode from main event loop to libuv thread pool
|
||||||
|
return (await gunzip(Buffer.from(response, "base64"))).toString("utf8");
|
||||||
|
} catch (error) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Heartbeat;
|
module.exports = Heartbeat;
|
||||||
|
|||||||
@ -24,6 +24,8 @@ const {
|
|||||||
PING_PER_REQUEST_TIMEOUT_MIN,
|
PING_PER_REQUEST_TIMEOUT_MIN,
|
||||||
PING_PER_REQUEST_TIMEOUT_MAX,
|
PING_PER_REQUEST_TIMEOUT_MAX,
|
||||||
PING_PER_REQUEST_TIMEOUT_DEFAULT,
|
PING_PER_REQUEST_TIMEOUT_DEFAULT,
|
||||||
|
RESPONSE_BODY_LENGTH_DEFAULT,
|
||||||
|
RESPONSE_BODY_LENGTH_MAX,
|
||||||
} = require("../../src/util");
|
} = require("../../src/util");
|
||||||
const {
|
const {
|
||||||
ping,
|
ping,
|
||||||
@ -56,6 +58,9 @@ const { CookieJar } = require("tough-cookie");
|
|||||||
const { HttpsCookieAgent } = require("http-cookie-agent/http");
|
const { HttpsCookieAgent } = require("http-cookie-agent/http");
|
||||||
const https = require("https");
|
const https = require("https");
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
|
const zlib = require("node:zlib");
|
||||||
|
const { promisify } = require("node:util");
|
||||||
|
const gzip = promisify(zlib.gzip);
|
||||||
const DomainExpiry = require("./domain_expiry");
|
const DomainExpiry = require("./domain_expiry");
|
||||||
|
|
||||||
const rootCertificates = rootCertificatesFingerprints();
|
const rootCertificates = rootCertificatesFingerprints();
|
||||||
@ -203,6 +208,11 @@ class Monitor extends BeanModel {
|
|||||||
ping_numeric: this.isPingNumeric(),
|
ping_numeric: this.isPingNumeric(),
|
||||||
ping_count: this.ping_count,
|
ping_count: this.ping_count,
|
||||||
ping_per_request_timeout: this.ping_per_request_timeout,
|
ping_per_request_timeout: this.ping_per_request_timeout,
|
||||||
|
|
||||||
|
// response saving options
|
||||||
|
saveResponse: this.getSaveResponse(),
|
||||||
|
saveErrorResponse: this.getSaveErrorResponse(),
|
||||||
|
responseMaxLength: this.response_max_length ?? RESPONSE_BODY_LENGTH_DEFAULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeSensitiveData) {
|
if (includeSensitiveData) {
|
||||||
@ -386,6 +396,22 @@ class Monitor extends BeanModel {
|
|||||||
return Boolean(this.kafkaProducerAllowAutoTopicCreation);
|
return Boolean(this.kafkaProducerAllowAutoTopicCreation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to boolean
|
||||||
|
* @returns {boolean} Should save response data on success?
|
||||||
|
*/
|
||||||
|
getSaveResponse() {
|
||||||
|
return Boolean(this.save_response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to boolean
|
||||||
|
* @returns {boolean} Should save response data on error?
|
||||||
|
*/
|
||||||
|
getSaveErrorResponse() {
|
||||||
|
return Boolean(this.save_error_response);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start monitor
|
* Start monitor
|
||||||
* @param {Server} io Socket server instance
|
* @param {Server} io Socket server instance
|
||||||
@ -620,6 +646,11 @@ class Monitor extends BeanModel {
|
|||||||
bean.msg = `${res.status} - ${res.statusText}`;
|
bean.msg = `${res.status} - ${res.statusText}`;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
|
||||||
|
// in the frontend, the save response is only shown if the saveErrorResponse is set
|
||||||
|
if (this.getSaveResponse() && this.getSaveErrorResponse()) {
|
||||||
|
await this.saveResponseData(bean, res.data);
|
||||||
|
}
|
||||||
|
|
||||||
// fallback for if kelog event is not emitted, but we may still have tlsInfo,
|
// fallback for if kelog event is not emitted, but we may still have tlsInfo,
|
||||||
// e.g. if the connection is made through a proxy
|
// e.g. if the connection is made through a proxy
|
||||||
if (this.getUrl()?.protocol === "https:" && tlsInfo.valid === undefined) {
|
if (this.getUrl()?.protocol === "https:" && tlsInfo.valid === undefined) {
|
||||||
@ -931,6 +962,10 @@ class Monitor extends BeanModel {
|
|||||||
bean.msg = error.message;
|
bean.msg = error.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.getSaveErrorResponse() && error?.response?.data !== undefined) {
|
||||||
|
await this.saveResponseData(bean, error.response.data);
|
||||||
|
}
|
||||||
|
|
||||||
// If UP come in here, it must be upside down mode
|
// If UP come in here, it must be upside down mode
|
||||||
// Just reset the retries
|
// Just reset the retries
|
||||||
if (this.isUpsideDown() && bean.status === UP) {
|
if (this.isUpsideDown() && bean.status === UP) {
|
||||||
@ -1114,6 +1149,35 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save response body to a heartbeat if response saving is enabled.
|
||||||
|
* @param {import("redbean-node").Bean} bean Heartbeat bean to populate.
|
||||||
|
* @param {unknown} data Response payload.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
async saveResponseData(bean, data) {
|
||||||
|
if (data === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let responseData = data;
|
||||||
|
if (typeof responseData !== "string") {
|
||||||
|
try {
|
||||||
|
responseData = JSON.stringify(responseData);
|
||||||
|
} catch (error) {
|
||||||
|
responseData = String(responseData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxSize = this.response_max_length ?? RESPONSE_BODY_LENGTH_DEFAULT;
|
||||||
|
if (responseData.length > maxSize) {
|
||||||
|
responseData = responseData.substring(0, maxSize) + "... (truncated)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offload gzip compression from main event loop to libuv thread pool
|
||||||
|
bean.response = (await gzip(Buffer.from(responseData, "utf8"))).toString("base64");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a request using axios
|
* Make a request using axios
|
||||||
* @param {object} options Options for Axios
|
* @param {object} options Options for Axios
|
||||||
@ -1417,7 +1481,7 @@ class Monitor extends BeanModel {
|
|||||||
* Send a notification about a monitor
|
* Send a notification about a monitor
|
||||||
* @param {boolean} isFirstBeat Is this beat the first of this monitor?
|
* @param {boolean} isFirstBeat Is this beat the first of this monitor?
|
||||||
* @param {Monitor} monitor The monitor to send a notification about
|
* @param {Monitor} monitor The monitor to send a notification about
|
||||||
* @param {Bean} bean Status information about monitor
|
* @param {import("./heartbeat")} bean Status information about monitor
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async sendNotification(isFirstBeat, monitor, bean) {
|
static async sendNotification(isFirstBeat, monitor, bean) {
|
||||||
@ -1435,7 +1499,7 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
for (let notification of notificationList) {
|
for (let notification of notificationList) {
|
||||||
try {
|
try {
|
||||||
const heartbeatJSON = bean.toJSON();
|
const heartbeatJSON = await bean.toJSONAsync({ decodeResponse: true });
|
||||||
const monitorData = [{ id: monitor.id, active: monitor.active, name: monitor.name }];
|
const monitorData = [{ id: monitor.id, active: monitor.active, name: monitor.name }];
|
||||||
const preloadData = await Monitor.preparePreloadData(monitorData);
|
const preloadData = await Monitor.preparePreloadData(monitorData);
|
||||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||||
@ -1642,6 +1706,16 @@ class Monitor extends BeanModel {
|
|||||||
throw new Error(`Retry interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
|
throw new Error(`Retry interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.response_max_length !== undefined) {
|
||||||
|
if (this.response_max_length < 0) {
|
||||||
|
throw new Error(`Response max length cannot be less than 0`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.response_max_length > RESPONSE_BODY_LENGTH_MAX) {
|
||||||
|
throw new Error(`Response max length cannot be more than ${RESPONSE_BODY_LENGTH_MAX} bytes`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.type === "ping") {
|
if (this.type === "ping") {
|
||||||
// ping parameters validation
|
// ping parameters validation
|
||||||
if (this.packetSize && (this.packetSize < PING_PACKET_SIZE_MIN || this.packetSize > PING_PACKET_SIZE_MAX)) {
|
if (this.packetSize && (this.packetSize < PING_PACKET_SIZE_MIN || this.packetSize > PING_PACKET_SIZE_MAX)) {
|
||||||
|
|||||||
@ -863,6 +863,9 @@ let needSetup = false;
|
|||||||
bean.packetSize = monitor.packetSize;
|
bean.packetSize = monitor.packetSize;
|
||||||
bean.maxredirects = monitor.maxredirects;
|
bean.maxredirects = monitor.maxredirects;
|
||||||
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
||||||
|
bean.save_response = monitor.saveResponse;
|
||||||
|
bean.save_error_response = monitor.saveErrorResponse;
|
||||||
|
bean.response_max_length = monitor.responseMaxLength;
|
||||||
bean.dns_resolve_type = monitor.dns_resolve_type;
|
bean.dns_resolve_type = monitor.dns_resolve_type;
|
||||||
bean.dns_resolve_server = monitor.dns_resolve_server;
|
bean.dns_resolve_server = monitor.dns_resolve_server;
|
||||||
bean.pushToken = monitor.pushToken;
|
bean.pushToken = monitor.pushToken;
|
||||||
|
|||||||
@ -100,6 +100,11 @@
|
|||||||
"maxRedirectDescription": "Maximum number of redirects to follow. Set to 0 to disable redirects.",
|
"maxRedirectDescription": "Maximum number of redirects to follow. Set to 0 to disable redirects.",
|
||||||
"Upside Down Mode": "Upside Down Mode",
|
"Upside Down Mode": "Upside Down Mode",
|
||||||
"Max. Redirects": "Max. Redirects",
|
"Max. Redirects": "Max. Redirects",
|
||||||
|
"saveResponseForNotifications": "Save HTTP Success Response for Notifications",
|
||||||
|
"saveErrorResponseForNotifications": "Save HTTP Error Response for Notifications",
|
||||||
|
"saveResponseDescription": "Stores the HTTP response and makes it available to notification templates as {templateVariable}",
|
||||||
|
"responseMaxLength": "Response Max Length (bytes)",
|
||||||
|
"responseMaxLengthDescription": "Maximum size of response data to store. Set to 0 for unlimited. Larger responses will be truncated. Default: 10240 (10KB)",
|
||||||
"Accepted Status Codes": "Accepted Status Codes",
|
"Accepted Status Codes": "Accepted Status Codes",
|
||||||
"Push URL": "Push URL",
|
"Push URL": "Push URL",
|
||||||
"needPushEvery": "You should call this URL every {0} seconds.",
|
"needPushEvery": "You should call this URL every {0} seconds.",
|
||||||
|
|||||||
@ -1514,6 +1514,89 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
monitor.type === 'http' ||
|
||||||
|
monitor.type === 'keyword' ||
|
||||||
|
monitor.type === 'json-query'
|
||||||
|
"
|
||||||
|
class="my-3"
|
||||||
|
>
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
id="saveErrorResponse"
|
||||||
|
v-model="monitor.saveErrorResponse"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="saveErrorResponse">
|
||||||
|
{{ $t("saveErrorResponseForNotifications") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
<i18n-t keypath="saveResponseDescription" tag="div" class="form-text">
|
||||||
|
<template #templateVariable>
|
||||||
|
<code>heartbeatJSON.response</code>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
(monitor.type === 'http' ||
|
||||||
|
monitor.type === 'keyword' ||
|
||||||
|
monitor.type === 'json-query') &&
|
||||||
|
monitor.saveErrorResponse
|
||||||
|
"
|
||||||
|
class="my-3"
|
||||||
|
>
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
id="saveResponse"
|
||||||
|
v-model="monitor.saveResponse"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="saveResponse">
|
||||||
|
{{ $t("saveResponseForNotifications") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
<i18n-t keypath="saveResponseDescription" tag="div" class="form-text">
|
||||||
|
<template #templateVariable>
|
||||||
|
<code>heartbeatJSON.response</code>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
(monitor.type === 'http' ||
|
||||||
|
monitor.type === 'keyword' ||
|
||||||
|
monitor.type === 'json-query') &&
|
||||||
|
(monitor.saveResponse || monitor.saveErrorResponse)
|
||||||
|
"
|
||||||
|
class="my-3"
|
||||||
|
>
|
||||||
|
<label for="responseMaxLength" class="form-label">
|
||||||
|
{{ $t("responseMaxLength") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="responseMaxLength"
|
||||||
|
v-model="monitor.responseMaxLength"
|
||||||
|
type="number"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
/>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("responseMaxLengthDescription") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="acceptedStatusCodes" class="form-label">
|
<label for="acceptedStatusCodes" class="form-label">
|
||||||
{{ $t("Accepted Status Codes") }}
|
{{ $t("Accepted Status Codes") }}
|
||||||
@ -2184,6 +2267,9 @@ const monitorDefaults = {
|
|||||||
domainExpiryNotification: true,
|
domainExpiryNotification: true,
|
||||||
maxredirects: 10,
|
maxredirects: 10,
|
||||||
accepted_statuscodes: ["200-299"],
|
accepted_statuscodes: ["200-299"],
|
||||||
|
saveResponse: false,
|
||||||
|
saveErrorResponse: true,
|
||||||
|
responseMaxLength: 10240,
|
||||||
dns_resolve_type: "A",
|
dns_resolve_type: "A",
|
||||||
dns_resolve_server: "1.1.1.1",
|
dns_resolve_server: "1.1.1.1",
|
||||||
docker_container: "",
|
docker_container: "",
|
||||||
|
|||||||
@ -10,8 +10,8 @@
|
|||||||
*/
|
*/
|
||||||
var _a;
|
var _a;
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_MAX = exports.PING_PER_REQUEST_TIMEOUT_MIN = exports.PING_COUNT_DEFAULT = exports.PING_COUNT_MAX = exports.PING_COUNT_MIN = exports.PING_GLOBAL_TIMEOUT_DEFAULT = exports.PING_GLOBAL_TIMEOUT_MAX = exports.PING_GLOBAL_TIMEOUT_MIN = exports.PING_PACKET_SIZE_DEFAULT = exports.PING_PACKET_SIZE_MAX = exports.PING_PACKET_SIZE_MIN = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0;
|
exports.CONSOLE_STYLE_FgViolet = exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.RESPONSE_BODY_LENGTH_MAX = exports.RESPONSE_BODY_LENGTH_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_MAX = exports.PING_PER_REQUEST_TIMEOUT_MIN = exports.PING_COUNT_DEFAULT = exports.PING_COUNT_MAX = exports.PING_COUNT_MIN = exports.PING_GLOBAL_TIMEOUT_DEFAULT = exports.PING_GLOBAL_TIMEOUT_MAX = exports.PING_GLOBAL_TIMEOUT_MIN = exports.PING_PACKET_SIZE_DEFAULT = exports.PING_PACKET_SIZE_MAX = exports.PING_PACKET_SIZE_MIN = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0;
|
||||||
exports.TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD = exports.evaluateJsonQuery = exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = void 0;
|
exports.TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD = exports.evaluateJsonQuery = exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = void 0;
|
||||||
const dayjs_1 = require("dayjs");
|
const dayjs_1 = require("dayjs");
|
||||||
const jsonata = require("jsonata");
|
const jsonata = require("jsonata");
|
||||||
exports.isDev = process.env.NODE_ENV === "development";
|
exports.isDev = process.env.NODE_ENV === "development";
|
||||||
@ -43,6 +43,8 @@ exports.PING_COUNT_DEFAULT = 1;
|
|||||||
exports.PING_PER_REQUEST_TIMEOUT_MIN = 1;
|
exports.PING_PER_REQUEST_TIMEOUT_MIN = 1;
|
||||||
exports.PING_PER_REQUEST_TIMEOUT_MAX = 60;
|
exports.PING_PER_REQUEST_TIMEOUT_MAX = 60;
|
||||||
exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = 2;
|
exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = 2;
|
||||||
|
exports.RESPONSE_BODY_LENGTH_DEFAULT = 1024 * 10;
|
||||||
|
exports.RESPONSE_BODY_LENGTH_MAX = 1024 * 1024;
|
||||||
exports.CONSOLE_STYLE_Reset = "\x1b[0m";
|
exports.CONSOLE_STYLE_Reset = "\x1b[0m";
|
||||||
exports.CONSOLE_STYLE_Bright = "\x1b[1m";
|
exports.CONSOLE_STYLE_Bright = "\x1b[1m";
|
||||||
exports.CONSOLE_STYLE_Dim = "\x1b[2m";
|
exports.CONSOLE_STYLE_Dim = "\x1b[2m";
|
||||||
|
|||||||
11
src/util.ts
11
src/util.ts
@ -66,6 +66,17 @@ export const PING_PER_REQUEST_TIMEOUT_MIN = 1;
|
|||||||
export const PING_PER_REQUEST_TIMEOUT_MAX = 60;
|
export const PING_PER_REQUEST_TIMEOUT_MAX = 60;
|
||||||
export const PING_PER_REQUEST_TIMEOUT_DEFAULT = 2;
|
export const PING_PER_REQUEST_TIMEOUT_DEFAULT = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response body length cutoff used by default (10kb)
|
||||||
|
* (measured in bytes)
|
||||||
|
*/
|
||||||
|
export const RESPONSE_BODY_LENGTH_DEFAULT = 1024 * 10;
|
||||||
|
/**
|
||||||
|
* Maximum allowed response body length to store (1mb)
|
||||||
|
* (measured in bytes)
|
||||||
|
*/
|
||||||
|
export const RESPONSE_BODY_LENGTH_MAX = 1024 * 1024;
|
||||||
|
|
||||||
// Console colors
|
// Console colors
|
||||||
// https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color
|
// https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color
|
||||||
export const CONSOLE_STYLE_Reset = "\x1b[0m";
|
export const CONSOLE_STYLE_Reset = "\x1b[0m";
|
||||||
|
|||||||
36
test/backend-test/test-monitor-response.js
Normal file
36
test/backend-test/test-monitor-response.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
const { describe, test } = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const Monitor = require("../../server/model/monitor");
|
||||||
|
const Heartbeat = require("../../server/model/heartbeat");
|
||||||
|
const { RESPONSE_BODY_LENGTH_DEFAULT } = require("../../src/util");
|
||||||
|
|
||||||
|
describe("Monitor response saving", () => {
|
||||||
|
test("getSaveResponse and getSaveErrorResponse parse booleans", () => {
|
||||||
|
const monitor = Object.create(Monitor.prototype);
|
||||||
|
monitor.save_response = 1;
|
||||||
|
monitor.save_error_response = 0;
|
||||||
|
|
||||||
|
assert.strictEqual(monitor.getSaveResponse(), true);
|
||||||
|
assert.strictEqual(monitor.getSaveErrorResponse(), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("saveResponseData stores and truncates response", async () => {
|
||||||
|
const monitor = Object.create(Monitor.prototype);
|
||||||
|
monitor.response_max_length = 5;
|
||||||
|
|
||||||
|
const bean = {};
|
||||||
|
await monitor.saveResponseData(bean, "abcdef");
|
||||||
|
|
||||||
|
assert.strictEqual(await Heartbeat.decodeResponseValue(bean.response), "abcde... (truncated)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("saveResponseData stringifies objects", async () => {
|
||||||
|
const monitor = Object.create(Monitor.prototype);
|
||||||
|
monitor.response_max_length = RESPONSE_BODY_LENGTH_DEFAULT;
|
||||||
|
|
||||||
|
const bean = {};
|
||||||
|
await monitor.saveResponseData(bean, { ok: true });
|
||||||
|
|
||||||
|
assert.strictEqual(await Heartbeat.decodeResponseValue(bean.response), JSON.stringify({ ok: true }));
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -105,4 +105,34 @@ test.describe("Monitor Form", () => {
|
|||||||
|
|
||||||
await screenshot(testInfo, page);
|
await screenshot(testInfo, page);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("save response settings persist", async ({ page }, testInfo) => {
|
||||||
|
await page.goto("./add");
|
||||||
|
await login(page);
|
||||||
|
await selectMonitorType(page, "http");
|
||||||
|
|
||||||
|
const friendlyName = "Example HTTP Save Response";
|
||||||
|
await page.getByTestId("friendly-name-input").fill(friendlyName);
|
||||||
|
await page.getByTestId("url-input").fill("https://www.example.com/");
|
||||||
|
|
||||||
|
// Expect error response save enabled by default
|
||||||
|
await expect(page.getByLabel("Save HTTP Error Response for Notifications")).toBeChecked();
|
||||||
|
|
||||||
|
await page.getByLabel("Save HTTP Success Response for Notifications").check();
|
||||||
|
await page.getByLabel("Save HTTP Error Response for Notifications").uncheck();
|
||||||
|
await page.getByLabel("Response Max Length (bytes)").fill("2048");
|
||||||
|
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
await page.getByTestId("save-button").click();
|
||||||
|
await page.waitForURL("/dashboard/*");
|
||||||
|
|
||||||
|
await page.getByRole("link", { name: "Edit" }).click();
|
||||||
|
await page.waitForURL("/edit/*");
|
||||||
|
|
||||||
|
await expect(page.getByLabel("Save HTTP Success Response for Notifications")).toBeHidden();
|
||||||
|
await expect(page.getByLabel("Save HTTP Error Response for Notifications")).not.toBeChecked();
|
||||||
|
await expect(page.getByLabel("Response Max Length (bytes)")).toHaveValue("2048");
|
||||||
|
|
||||||
|
await screenshot(testInfo, page);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user