treblle
Version:
Treblle Javascript SDK
390 lines (351 loc) • 10.6 kB
JavaScript
const { maskSensitiveValues } = require("./maskFields");
const os = require("os");
const fetch = require("node-fetch");
const stackTrace = require("stack-trace");
const VERSION = require("../package.json").version;
/**
* Prepares the payload which is sent to Treblle.
*
* @param {object} Express request object
* @param {object} Express response object
* @param {object} settings
* @param {string} settings.apiKey Treblle API Key
* @param {string} settings.projectId Treblle Project ID
* @param {number[]} settings.requestStartTime when the request started
* @param {object} settings.fieldsToMaskMap map of fields to mask
*/
const generateTrebllePayload = function (
req,
res,
{ apiKey, projectId, requestStartTime, error, fieldsToMaskMap }
) {
const payload = req.method === "GET" ? req.query : req.body;
const parsedPayload = getPayload(payload);
const maskedRequestPayload = maskSensitiveValues(
parsedPayload,
fieldsToMaskMap
);
const responseHeaders = res.getHeaders();
let errors = [];
// We should be able to parse this, but you never know if users will try doing something weird...
let maskedResponseBody;
try {
let originalResponseBody = res.__treblle_body_response;
// if the response is streamed it could be a buffer
// so we'll convert it to a string first
if (Buffer.isBuffer(originalResponseBody)) {
originalResponseBody = originalResponseBody.toString("utf8");
}
if (typeof originalResponseBody === "string") {
let parsedResponseBody = JSON.parse(originalResponseBody);
maskedResponseBody = maskSensitiveValues(
parsedResponseBody,
fieldsToMaskMap
);
} else if (typeof originalResponseBody === "object") {
maskedResponseBody = maskSensitiveValues(
originalResponseBody,
fieldsToMaskMap
);
}
} catch {
// if we can't parse the body we'll leave it empty and set an error
errors.push({
source: "onShutdown",
type: "INVALID_JSON",
message: "Invalid JSON format",
file: null,
line: null,
});
}
const protocol = `${req.protocol.toUpperCase()}/${req.httpVersion}`;
// get rid of the workaround body, we don't need it anymore
res.__treblle_body_response = null;
if (error) {
const trace = stackTrace.parse(error);
errors.push({
source: "onException",
type: "UNHANDLED_EXCEPTION",
message: error.message,
file: trace[0].getFileName(),
line: trace[0].getLineNumber(),
});
}
let dataToSend = {
api_key: apiKey,
project_id: projectId,
version: VERSION,
sdk: "node",
data: {
server: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
os: {
name: os.platform(),
release: os.release(),
architecture: os.arch(),
},
software: null,
signature: null,
protocol: protocol,
},
language: {
name: "node",
version: process.version,
},
request: {
timestamp: new Date().toISOString().replace("T", " ").substr(0, 19),
ip: req.ip,
url: getRequestUrl(req),
user_agent: req.get("user-agent"),
method: req.method,
headers: maskSensitiveValues(req.headers, fieldsToMaskMap),
body: maskedRequestPayload !== undefined ? maskedRequestPayload : null,
},
response: {
headers: maskSensitiveValues(responseHeaders, fieldsToMaskMap),
code: res.statusCode,
size: res._contentLength,
load_time: getRequestDuration(requestStartTime),
body: maskedResponseBody !== undefined ? maskedResponseBody : null,
},
errors: errors,
},
};
return dataToSend;
};
/**
* Prepares the payload which is sent to Treblle.
*
* @param {object} Koa context object
* @param {object} settings
* @param {string} settings.apiKey Treblle API Key
* @param {string} settings.projectId Treblle Project ID
* @param {number[]} settings.requestStartTime when the request started
* @param {object} settings.fieldsToMaskMap map of fields to mask
*/
const generateKoaTrebllePayload = function (
koaContext,
{ apiKey, projectId, requestStartTime, error, fieldsToMaskMap }
) {
const payload =
koaContext.request.method === "GET"
? koaContext.request.query
: koaContext.request.body;
const parsedPayload = getPayload(payload);
const maskedRequestPayload = maskSensitiveValues(
parsedPayload,
fieldsToMaskMap
);
const responseHeaders = koaContext.response.headers;
let errors = [];
// We should be able to parse this, but you never know if users will try doing something weird...
// TODO - figure out Koa buffers & streaming
let maskedResponseBody;
try {
let originalResponseBody = koaContext.response.body;
// if the response is streamed it could be a buffer
// so we'll convert it to a string first
if (Buffer.isBuffer(originalResponseBody)) {
originalResponseBody = originalResponseBody.toString("utf8");
}
if (typeof originalResponseBody === "string") {
let parsedResponseBody = JSON.parse(originalResponseBody);
maskedResponseBody = maskSensitiveValues(
parsedResponseBody,
fieldsToMaskMap
);
} else if (typeof originalResponseBody === "object") {
maskedResponseBody = maskSensitiveValues(
originalResponseBody,
fieldsToMaskMap
);
}
} catch {
// if we can't parse the body we'll leave it empty and set an error
errors.push({
source: "onShutdown",
type: "INVALID_JSON",
message: "Invalid JSON format",
file: null,
line: null,
});
}
const protocol = `${koaContext.request.protocol.toUpperCase()}/${
koaContext.request.req.httpVersion
}`;
if (error) {
const trace = stackTrace.parse(error);
errors.push({
source: "onException",
type: "UNHANDLED_EXCEPTION",
message: error.message,
file: trace[0].getFileName(),
line: trace[0].getLineNumber(),
});
}
let dataToSend = {
api_key: apiKey,
project_id: projectId,
version: VERSION,
sdk: "node",
data: {
server: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
os: {
name: os.platform(),
release: os.release(),
architecture: os.arch(),
},
software: null,
signature: null,
protocol: protocol,
},
language: {
name: "node",
version: process.version,
},
request: {
timestamp: new Date().toISOString().replace("T", " ").substr(0, 19),
ip: koaContext.request.ip,
url: getRequestUrl(koaContext.request),
user_agent: koaContext.request.header["user-agent"],
method: koaContext.request.method,
headers: maskSensitiveValues(
koaContext.request.headers,
fieldsToMaskMap
),
body: maskedRequestPayload !== undefined ? maskedRequestPayload : null,
},
response: {
headers: maskSensitiveValues(responseHeaders, fieldsToMaskMap),
code: koaContext.response.status,
size: koaContext.response.length || null,
load_time: getRequestDuration(requestStartTime),
body: maskedResponseBody !== undefined ? maskedResponseBody : null,
},
errors: errors,
},
};
return dataToSend;
};
function sendExpressPayloadToTreblle(
req,
res,
{ apiKey, projectId, requestStartTime, error, fieldsToMaskMap, showErrors }
) {
let trebllePayload = generateTrebllePayload(req, res, {
apiKey,
projectId,
requestStartTime,
error,
fieldsToMaskMap,
});
sendPayloadToTreblleApi({ apiKey, trebllePayload, showErrors });
}
function sendKoaPayloadToTreblle(
koaContext,
{ apiKey, projectId, requestStartTime, fieldsToMaskMap, showErrors, error }
) {
let trebllePayload = generateKoaTrebllePayload(koaContext, {
apiKey,
projectId,
requestStartTime,
error,
fieldsToMaskMap,
});
sendPayloadToTreblleApi({ apiKey, trebllePayload, showErrors });
}
function sendPayloadToTreblleApi({ apiKey, trebllePayload, showErrors }) {
let f;
if (typeof fetch === "function") {
f = fetch;
} else if (fetch && typeof fetch.default === "function") {
f = fetch.default;
} else {
if (showErrors) {
console.warn("Treblle error: fetch is not defined");
}
return;
}
f("https://rocknrolla.treblle.com", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify(trebllePayload),
}).then(
(response) => {
if (showErrors && response.ok === false) {
logTreblleResponseError(response);
}
},
(error) => {
if (showErrors) {
logRequestFailed(error);
}
}
);
}
async function logTreblleResponseError(response) {
try {
const responseBody = await response.json();
logError(response, responseBody);
return;
} catch (_error) {
// ignore _error here, it means the response wasn't JSON
}
try {
const responseBody = await response.text();
logError(response, responseBody);
return;
} catch (_error) {
// ignore _error here, it means the response wasn't text
}
logError(response);
}
function logError(response, responseBody) {
console.log(
`[error] Sending data to Treblle failed - status: ${response.statusText} (${response.status})`,
responseBody
);
}
function logRequestFailed(error) {
console.error(
"[error] Sending data to Treblle failed (it's possibly a network error)",
error
);
}
/**
* Calculates the request duration.
*
* @param {number[]} startTime
* @returns {number}
*/
function getRequestDuration(startTime) {
const NS_PER_SEC = 1e9;
const NS_TO_MICRO = 1e3;
const diff = process.hrtime(startTime);
const microseconds = (diff[0] * NS_PER_SEC + diff[1]) / NS_TO_MICRO;
return Math.ceil(microseconds);
}
function getRequestUrl(req) {
const fullUrl = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
return fullUrl;
}
function getPayload(payload) {
if (typeof payload === "object") return payload;
if (typeof payload === "string") {
try {
return JSON.parse(payload);
} catch (error) {
// if we can't parse it we'll just return null
return null;
}
}
}
module.exports = {
sendExpressPayloadToTreblle,
sendKoaPayloadToTreblle,
sendPayloadToTreblleApi,
};