From f0461270de839d38dd360975b9b8435a19953f98 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 2 Dec 2024 08:52:18 -0800 Subject: [PATCH 01/14] IO-3047 Accounting ID on Owner Page Signed-off-by: Allan Carr --- .../owner-detail-form/owner-detail-form.component.jsx | 3 +++ client/src/graphql/owners.queries.js | 1 + client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + 5 files changed, 7 insertions(+) diff --git a/client/src/components/owner-detail-form/owner-detail-form.component.jsx b/client/src/components/owner-detail-form/owner-detail-form.component.jsx index 419de25fa..5de4231f7 100644 --- a/client/src/components/owner-detail-form/owner-detail-form.component.jsx +++ b/client/src/components/owner-detail-form/owner-detail-form.component.jsx @@ -25,6 +25,9 @@ export default function OwnerDetailFormComponent({ form, loading }) { + + + diff --git a/client/src/graphql/owners.queries.js b/client/src/graphql/owners.queries.js index e28fc8aa2..d169263f4 100644 --- a/client/src/graphql/owners.queries.js +++ b/client/src/graphql/owners.queries.js @@ -48,6 +48,7 @@ export const QUERY_OWNER_BY_ID = gql` query QUERY_OWNER_BY_ID($id: uuid!) { owners_by_pk(id: $id) { id + accountingid allow_text_message ownr_addr1 ownr_addr2 diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index e86e9936e..ab41c29c0 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2394,6 +2394,7 @@ "selectexistingornew": "Select an existing owner record or create a new one. " }, "fields": { + "accountingid": "Accounting ID", "address": "Address", "allow_text_message": "Permission to Text?", "name": "Name", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 8b9026489..4e0650522 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2394,6 +2394,7 @@ "selectexistingornew": "" }, "fields": { + "accountingid": "", "address": "Dirección", "allow_text_message": "Permiso de texto?", "name": "Nombre", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 8f9330ea0..edab92e9b 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2394,6 +2394,7 @@ "selectexistingornew": "" }, "fields": { + "accountingid": "", "address": "Adresse", "allow_text_message": "Autorisation de texte?", "name": "Prénom", From 241322fa307341fb1b0eaef9e1889b69c32a8b39 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 2 Dec 2024 11:09:30 -0800 Subject: [PATCH 02/14] IO-3046 purchase_return_ratio_excel Signed-off-by: Allan Carr --- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + client/src/utils/TemplateConstants.js | 13 +++++++++++++ 4 files changed, 16 insertions(+) diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index e86e9936e..7ce773a14 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -3057,6 +3057,7 @@ "production_not_production_status": "Production not in Production Status", "production_over_time": "Production Level over Time", "psr_by_make": "Percent of Sales by Vehicle Make", + "purchase_return_ratio_excel": "Purchase & Return Ratio - Excel", "purchase_return_ratio_grouped_by_vendor_detail": "Purchase & Return Ratio by Vendor (Detail)", "purchase_return_ratio_grouped_by_vendor_summary": "Purchase & Return Ratio by Vendor (Summary)", "purchases_by_cost_center_detail": "Purchases by Cost Center (Detail)", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 8b9026489..783b27f7e 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -3057,6 +3057,7 @@ "production_not_production_status": "", "production_over_time": "", "psr_by_make": "", + "purchase_return_ratio_excel": "", "purchase_return_ratio_grouped_by_vendor_detail": "", "purchase_return_ratio_grouped_by_vendor_summary": "", "purchases_by_cost_center_detail": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 8f9330ea0..bd8466003 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -3057,6 +3057,7 @@ "production_not_production_status": "", "production_over_time": "", "psr_by_make": "", + "purchase_return_ratio_excel": "", "purchase_return_ratio_grouped_by_vendor_detail": "", "purchase_return_ratio_grouped_by_vendor_summary": "", "purchases_by_cost_center_detail": "", diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index 9f80986d2..c193dbb49 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -2184,6 +2184,19 @@ export const TemplateList = (type, context) => { }, group: "payroll", adp_payroll: true + }, + purchase_return_ratio_excel: { + title: i18n.t("reportcenter.templates.purchase_return_ratio_excel"), + subject: i18n.t("reportcenter.templates.purchase_return_ratio_excel"), + key: "purchase_return_ratio_excel", + //idtype: "vendor", + reporttype: "excel", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.bills"), + field: i18n.t("bills.fields.date") + }, + group: "purchases" } } : {}), From a885bdec7433b5432cd253bdeec4ad7f099e7ac7 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 4 Dec 2024 14:22:04 -0800 Subject: [PATCH 03/14] IO-3051 canvas-handler optimization Signed-off-by: Allan Carr --- .../validateCanvasRequestMiddleware.js | 20 ++ server/render/canvas-handler.js | 187 ++++++++++-------- server/routes/renderRoutes.js | 3 +- 3 files changed, 131 insertions(+), 79 deletions(-) create mode 100644 server/middleware/validateCanvasRequestMiddleware.js diff --git a/server/middleware/validateCanvasRequestMiddleware.js b/server/middleware/validateCanvasRequestMiddleware.js new file mode 100644 index 000000000..4a3bdc12f --- /dev/null +++ b/server/middleware/validateCanvasRequestMiddleware.js @@ -0,0 +1,20 @@ +const { isObject } = require("lodash"); + +const validateCanvasRequestMiddleware = (req, res, next) => { + const { w, h, values, keys, override } = req.body; + if (!values || !keys) { + return res.status(400).send("Missing required data"); + } + if (override && !isObject(override)) { + return res.status(400).send("Override must be an object"); + } + if (w && (!Number.isFinite(w) || w <= 0)) { + return res.status(400).send("Width must be a positive number"); + } + if (h && (!Number.isFinite(h) || h <= 0)) { + return res.status(400).send("Height must be a positive number"); + } + next(); +}; + +module.exports = validateCanvasRequestMiddleware; diff --git a/server/render/canvas-handler.js b/server/render/canvas-handler.js index 0af2a7f53..a0fd0a602 100644 --- a/server/render/canvas-handler.js +++ b/server/render/canvas-handler.js @@ -5,89 +5,120 @@ const logger = require("../utils/logger"); const { backgroundColors, borderColors } = require("./canvas-colors"); const { isObject, defaultsDeep, isNumber } = require("lodash"); +let isProcessing = false; +const requestQueue = []; + +const processCanvasRequest = async (req, res) => { + try { + const { w, h, values, keys, override } = req.body; + logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override }); + + // Set dimensions with defaults + const width = isNumber(w) ? w : 500; + const height = isNumber(h) ? h : 275; + + const configuration = { + type: "doughnut", + data: { + labels: keys, + datasets: [ + { + data: values, + backgroundColor: backgroundColors, + borderColor: borderColors, + borderWidth: 1 + } + ] + }, + options: { + animation: false, + devicePixelRatio: 4, + responsive: false, + maintainAspectRatio: true, + circumference: 180, + rotation: -90, + plugins: { + legend: { + labels: { + boxWidth: 20, + font: { + family: "'Montserrat'", + size: 10, + style: "normal", + weight: "normal" + } + }, + position: "left" + } + } + } + }; + + // If we have a valid override object, merge it with the default configuration object. + // This allows for you to override the default configuration with a custom one. + const defaults = () => { + if (!override || !isObject(override)) { + return configuration; + } + return defaultsDeep(override, configuration); + }; + + // Generate chart + let canvas = createCanvas(width, height); + let ctx = canvas.getContext("2d"); + let chart = new Chart(ctx, defaults()); + const result = canvas.toDataURL(); + + chart.destroy(); + canvas.width = 0; + canvas.height = 0; + ctx = null; + canvas = null; + chart = null; + + res.status(200).send(result); + } catch (error) { + if (chart) chart.destroy(); + if (canvas) { + canvas.width = 0; + canvas.height = 0; + } + ctx = null; + canvas = null; + chart = null; + + logger.log("inbound-canvas-creation", "error", "jsr", null, { error: error.message, stack: error.stack }); + res.status(500).send("Error generating canvas"); + } +}; + +const processNextInQueue = async () => { + if (requestQueue.length === 0) { + isProcessing = false; + return; + } + + const { req, res } = requestQueue.shift(); + await processCanvasRequest(req, res); + processNextInQueue(); +}; + exports.canvastest = function (req, res) { - //console.log("Incoming test request.", req); res.status(200).send("OK"); }; -exports.canvas = function (req, res) { - const { w, h, values, keys, override } = req.body; - //console.log("Incoming Canvas Request:", w, h, values, keys, override); - logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override }); - // Gate required values - if (!values || !keys) { - res.status(400).send("Missing required data"); +exports.canvas = async function (req, res) { + if (isProcessing) { + if (requestQueue.length >= 100) { + // Set a maximum queue size + return res.status(503).send("Server is busy. Please try again later."); + } + requestQueue.push({ req, res }); + logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length }); return; } - // Override must be an object if it exists - if (override && !isObject(override)) { - res.status(400).send("Override must be an object"); - return; - } - - // Set the default Width and Height - let [width, height] = [500, 275]; - - // Allow for custom width and height - if (isNumber(w)) { - width = w; - } - if (isNumber(h)) { - height = h; - } - - const configuration = { - type: "doughnut", - data: { - labels: keys, - datasets: [ - { - data: values, - backgroundColor: backgroundColors, - borderColor: borderColors, - borderWidth: 1 - } - ] - }, - options: { - devicePixelRatio: 4, - responsive: false, - maintainAspectRatio: true, - circumference: 180, - rotation: -90, - plugins: { - legend: { - labels: { - boxWidth: 20, - font: { - family: "'Montserrat'", - size: 10, - style: "normal", - weight: "normal" - } - }, - position: "left" - } - } - } - }; - - // If we have a valid override object, merge it with the default configuration object. - // This allows for you to override the default configuration with a custom one. - const defaults = () => { - if (!override || !isObject(override)) { - return configuration; - } - return defaultsDeep(override, configuration); - }; - - res.status(200).send( - (() => { - const canvas = createCanvas(width, height); - const ctx = canvas.getContext("2d"); - new Chart(ctx, defaults()); - return canvas.toDataURL(); - })() - ); + isProcessing = true; + await processCanvasRequest(req, res); + processNextInQueue(); }; diff --git a/server/routes/renderRoutes.js b/server/routes/renderRoutes.js index 13288d989..2657d6a76 100644 --- a/server/routes/renderRoutes.js +++ b/server/routes/renderRoutes.js @@ -2,10 +2,11 @@ const express = require("express"); const router = express.Router(); const { inlinecss } = require("../render/inlinecss"); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const validateCanvasRequestMiddleware = require("../middleware/validateCanvasRequestMiddleware"); const { canvas } = require("../render/canvas-handler"); // Define the route for inline CSS rendering router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss); -router.post("/canvas", validateFirebaseIdTokenMiddleware, canvas); +router.post("/canvas", [validateFirebaseIdTokenMiddleware, validateCanvasRequestMiddleware], canvas); module.exports = router; From 006a2a5dca9400b11b6ce291e8d1be200d45282a Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 4 Dec 2024 15:59:05 -0800 Subject: [PATCH 04/14] IO-3050 QBO BillEmail required if NeedToSend Signed-off-by: Allan Carr --- server/accounting/qbo/qbo-receivables.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/server/accounting/qbo/qbo-receivables.js b/server/accounting/qbo/qbo-receivables.js index f57c6711a..c7c74928b 100644 --- a/server/accounting/qbo/qbo-receivables.js +++ b/server/accounting/qbo/qbo-receivables.js @@ -328,6 +328,7 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare PostalCode: job.ownr_zip, CountrySubDivisionCode: job.ownr_st }, + ...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {}), ...(isThreeTier ? { Job: true, @@ -395,7 +396,7 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) { PostalCode: job.ownr_zip, CountrySubDivisionCode: job.ownr_st }, - + ...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {}), Job: true, ParentRef: { value: parentTierRef.Id @@ -556,7 +557,8 @@ async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, paren Line3: `${job.ownr_city || ""}, ${job.ownr_st || ""} ${job.ownr_zip || ""}`.trim(), Line2: job.ownr_addr1 || "", Line1: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${job.ownr_co_nm || ""}` - } + }, + ...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {}) }; logger.log("qbo-receivable-objectlog", "DEBUG", req.user.email, job.id, { @@ -673,7 +675,8 @@ async function InsertInvoiceMultiPayerInvoice( Line3: `${job.ownr_city || ""}, ${job.ownr_st || ""} ${job.ownr_zip || ""}`.trim(), Line2: job.ownr_addr1 || "", Line1: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${job.ownr_co_nm || ""}` - } + }, + ...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {}) }; logger.log("qbo-receivable-objectlog", "DEBUG", req.user.email, job.id, { From 2effe5ef50d73e1f87da883c4ae5f142ceacd2ba Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 4 Dec 2024 18:30:22 -0800 Subject: [PATCH 05/14] IO-3042 Jobs Marked as Total Loss Signed-off-by: Allan Carr --- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + client/src/utils/TemplateConstants.js | 13 ++++++++++++- 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index e86e9936e..0edba955d 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -3082,6 +3082,7 @@ "timetickets": "Time Tickets", "timetickets_employee": "Employee Time Tickets", "timetickets_summary": "Time Tickets Summary", + "total_loss_jobs": "Jobs Marked as Total Loss", "unclaimed_hrs": "Unflagged Hours", "void_ros": "Void ROs", "work_in_progress_committed_labour": "Work in Progress - Committed Labor", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 8b9026489..b752bf47e 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -3082,6 +3082,7 @@ "timetickets": "", "timetickets_employee": "", "timetickets_summary": "", + "total_loss_jobs": "", "unclaimed_hrs": "", "void_ros": "", "work_in_progress_committed_labour": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 8f9330ea0..c46314c63 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -3082,6 +3082,7 @@ "timetickets": "", "timetickets_employee": "", "timetickets_summary": "", + "total_loss_jobs": "", "unclaimed_hrs": "", "void_ros": "", "work_in_progress_committed_labour": "", diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index 9f80986d2..043d5fa3a 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -2184,7 +2184,18 @@ export const TemplateList = (type, context) => { }, group: "payroll", adp_payroll: true - } + }, + total_loss_jobs: { + title: i18n.t("reportcenter.templates.total_loss_jobs"), + subject: i18n.t("reportcenter.templates.total_loss_jobs"), + key: "total_loss_jobs", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_open") + }, + group: "jobs" + }, } : {}), ...(!type || type === "courtesycarcontract" From 6336e7568fe0b7b61ce8281130f0e1bfb16c099d Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 5 Dec 2024 11:26:23 -0800 Subject: [PATCH 06/14] feature/IO-3052-Skia-Canvas-Handler: Initial commit Signed-off-by: Dave Richer --- Dockerfile | 14 ++- package-lock.json | 98 ++++++++++++++++++++ package.json | 1 + server/render/canvas-handler.js | 153 +++++++++++++++++++++++--------- server/routes/renderRoutes.js | 5 +- 5 files changed, 227 insertions(+), 44 deletions(-) diff --git a/Dockerfile b/Dockerfile index b1d253808..16e9d2159 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,6 @@ RUN dnf install -y git \ && dnf install -y nodejs \ && dnf clean all - # Install dependencies required by node-canvas RUN dnf install -y \ gcc \ @@ -19,9 +18,22 @@ RUN dnf install -y \ libpng-devel \ make \ python3 \ + fontconfig \ + freetype \ python3-pip \ + wget \ + unzip \ && dnf clean all +# Install Montserrat fonts +RUN cd /tmp \ + && wget https://images.imex.online/fonts/montserrat.zip -O montserrat.zip \ + && unzip montserrat.zip -d montserrat \ + && mv montserrat/montserrat/*.ttf /usr/share/fonts \ + && fc-cache -fv \ + && rm -rf /tmp/montserrat /tmp/montserrat.zip \ + && echo "Montserrat fonts installed and cached successfully." + # Set the working directory WORKDIR /app diff --git a/package-lock.json b/package-lock.json index 6d1d243cc..cceafbf49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "recursive-diff": "^1.0.9", "redis": "^4.7.0", "rimraf": "^6.0.1", + "skia-canvas": "^2.0.0", "soap": "^1.1.6", "socket.io": "^4.8.1", "socket.io-adapter": "^2.5.5", @@ -3734,6 +3735,15 @@ "node": ">=6" } }, + "node_modules/cargo-cp-artifact": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.9.tgz", + "integrity": "sha512-6F+UYzTaGB+awsTXg0uSJA1/b/B3DDJzpKVRu0UmyI7DmNeaAl2RFHuTGIN6fEgpadRxoXGb7gbC1xo4C3IdyA==", + "license": "MIT", + "bin": { + "cargo-cp-artifact": "bin/cargo-cp-artifact.js" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -6757,6 +6767,12 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" }, + "node_modules/parenthesis": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/parenthesis/-/parenthesis-3.1.8.tgz", + "integrity": "sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==", + "license": "MIT" + }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -6789,6 +6805,12 @@ "node": ">= 0.8" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -7459,6 +7481,73 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/skia-canvas": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skia-canvas/-/skia-canvas-2.0.0.tgz", + "integrity": "sha512-wpYkmr9mCxBme5HAnlm6YOEiuaN9tIm9CL+HN8e5AFD4K2FAJXCcWiWvc9+LM8jUXt+AyYXgiwUTBxdQ6P+PEg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "cargo-cp-artifact": "^0.1", + "glob": "^11.0.0", + "path-browserify": "^1.0.1", + "simple-get": "^4.0.1", + "string-split-by": "^1.0.0" + } + }, + "node_modules/skia-canvas/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/skia-canvas/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/skia-canvas/node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/slick": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", @@ -7851,6 +7940,15 @@ } ] }, + "node_modules/string-split-by": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string-split-by/-/string-split-by-1.0.0.tgz", + "integrity": "sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==", + "license": "MIT", + "dependencies": { + "parenthesis": "^3.1.5" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", diff --git a/package.json b/package.json index 4fd96c3ff..5889ea532 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "recursive-diff": "^1.0.9", "redis": "^4.7.0", "rimraf": "^6.0.1", + "skia-canvas": "^2.0.0", "soap": "^1.1.6", "socket.io": "^4.8.1", "socket.io-adapter": "^2.5.5", diff --git a/server/render/canvas-handler.js b/server/render/canvas-handler.js index 0af2a7f53..df3cc8f02 100644 --- a/server/render/canvas-handler.js +++ b/server/render/canvas-handler.js @@ -1,43 +1,28 @@ const { createCanvas } = require("canvas"); +const { Canvas, FontLibrary } = require("skia-canvas"); +const { performance } = require("perf_hooks"); const Chart = require("chart.js/auto"); const logger = require("../utils/logger"); const { backgroundColors, borderColors } = require("./canvas-colors"); const { isObject, defaultsDeep, isNumber } = require("lodash"); -exports.canvastest = function (req, res) { - //console.log("Incoming test request.", req); - res.status(200).send("OK"); -}; +try { + FontLibrary.use("Montserrat", [ + "/usr/share/fonts/Montserrat-Regular.ttf", + "/usr/share/fonts/Montserrat-Bold.ttf", + "/usr/share/fonts/Montserrat-Italic.ttf" + ]); +} catch (error) { + console.error( + "Error loading fonts Skia Canvas Fonts, please be sure to install Montserrat font package", + error.message + ); +} -exports.canvas = function (req, res) { - const { w, h, values, keys, override } = req.body; - //console.log("Incoming Canvas Request:", w, h, values, keys, override); - logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override }); - // Gate required values - if (!values || !keys) { - res.status(400).send("Missing required data"); - return; - } - - // Override must be an object if it exists - if (override && !isObject(override)) { - res.status(400).send("Override must be an object"); - return; - } - - // Set the default Width and Height - let [width, height] = [500, 275]; - - // Allow for custom width and height - if (isNumber(w)) { - width = w; - } - if (isNumber(h)) { - height = h; - } - - const configuration = { +// Utility to create a chart configuration +const getChartConfiguration = (keys, values, override) => { + const defaultConfiguration = { type: "doughnut", data: { labels: keys, @@ -53,6 +38,7 @@ exports.canvas = function (req, res) { options: { devicePixelRatio: 4, responsive: false, + animation: false, maintainAspectRatio: true, circumference: 180, rotation: -90, @@ -73,21 +59,106 @@ exports.canvas = function (req, res) { } }; - // If we have a valid override object, merge it with the default configuration object. - // This allows for you to override the default configuration with a custom one. - const defaults = () => { - if (!override || !isObject(override)) { - return configuration; - } - return defaultsDeep(override, configuration); - }; + return defaultsDeep(override || {}, defaultConfiguration); +}; + +// Utility to validate input +const validateCanvasInput = ({ values, keys, override }, res) => { + if (!Array.isArray(values) || !Array.isArray(keys)) { + res.status(400).send("Invalid input: 'values' and 'keys' must be arrays."); + return false; + } + + if (values.some((value) => typeof value !== "number")) { + res.status(400).send("Invalid input: 'values' must be an array of numbers."); + return false; + } + + if (keys.some((key) => typeof key !== "string")) { + res.status(400).send("Invalid input: 'keys' must be an array of strings."); + return false; + } + + if (override && !isObject(override)) { + res.status(400).send("Override must be an object"); + return false; + } + + return true; +}; + +exports.canvastest = function (req, res) { + //console.log("Incoming test request.", req); + res.status(200).send("OK"); +}; + +exports.canvas = function (req, res) { + const startTime = performance.now(); + + const { w, h, values, keys, override } = req.body; + //console.log("Incoming Canvas Request:", w, h, values, keys, override); + logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override }); + + if (!validateCanvasInput(req.body, res)) return; + + // Set the default Width and Height + let [width, height] = [500, 275]; + + // Allow for custom width and height + if (isNumber(w)) { + width = w; + } + if (isNumber(h)) { + height = h; + } + + const configuration = getChartConfiguration(keys, values, override); res.status(200).send( (() => { const canvas = createCanvas(width, height); const ctx = canvas.getContext("2d"); - new Chart(ctx, defaults()); + new Chart(ctx, configuration); return canvas.toDataURL(); })() ); + console.log("Canvas generation time:", performance.now() - startTime, "ms"); +}; + +exports.canvasSkia = async function (req, res) { + const startTime = performance.now(); + const { w, h, values, keys, override } = req.body; + + // Log incoming request for debugging + logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override }); + + if (!validateCanvasInput(req.body, res)) return; + + // Default width and height + const width = typeof w === "number" && w > 0 ? w : 500; + const height = typeof h === "number" && h > 0 ? h : 275; + + const configuration = getChartConfiguration(keys, values, override); + + try { + // Create a canvas and get the 2D rendering context + const canvas = new Canvas(width, height); + const ctx = canvas.getContext("2d"); + + // Render the chart + new Chart(ctx, configuration); + + // Convert the canvas to a Base64-encoded image + const chartImage = (await canvas.toBuffer("image/png")).toString("base64"); + const dataURL = `data:image/png;base64,${chartImage}`; + + // Send the Base64-encoded image as the response + res.status(200).send(dataURL); + console.log("Canvas generation time:", performance.now() - startTime, "ms"); + } catch (error) { + // Log and handle rendering errors + logger.log("canvas-error", "error", "jsr", null, { error: error.message }); + console.error("Error generating chart:", error.message); + res.status(500).send("Failed to generate canvas."); + } }; diff --git a/server/routes/renderRoutes.js b/server/routes/renderRoutes.js index 13288d989..083d58b39 100644 --- a/server/routes/renderRoutes.js +++ b/server/routes/renderRoutes.js @@ -2,10 +2,11 @@ const express = require("express"); const router = express.Router(); const { inlinecss } = require("../render/inlinecss"); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); -const { canvas } = require("../render/canvas-handler"); +const { canvas, canvasSkia } = require("../render/canvas-handler"); // Define the route for inline CSS rendering router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss); -router.post("/canvas", validateFirebaseIdTokenMiddleware, canvas); +router.post("/canvas", canvas); +router.post("/canvas-skia", canvasSkia); module.exports = router; From 86f3179bc09dbb16b8d222ee6db4e4fbca42b262 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 5 Dec 2024 11:26:36 -0800 Subject: [PATCH 07/14] feature/IO-3052-Skia-Canvas-Handler: Initial commit Signed-off-by: Dave Richer --- server/routes/renderRoutes.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/routes/renderRoutes.js b/server/routes/renderRoutes.js index 083d58b39..d1b64566e 100644 --- a/server/routes/renderRoutes.js +++ b/server/routes/renderRoutes.js @@ -6,7 +6,7 @@ const { canvas, canvasSkia } = require("../render/canvas-handler"); // Define the route for inline CSS rendering router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss); -router.post("/canvas", canvas); -router.post("/canvas-skia", canvasSkia); +router.post("/canvas", validateFirebaseIdTokenMiddleware, canvas); +router.post("/canvas-skia", validateFirebaseIdTokenMiddleware, canvasSkia); module.exports = router; From 50c99f7a1e009808d26c385336783d8b9ee09cd7 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 5 Dec 2024 11:52:14 -0800 Subject: [PATCH 08/14] feature/IO-3052-Skia-Canvas-Handler: Cleanup Signed-off-by: Dave Richer --- .../validateCanvasInputMiddleware.js | 25 +++++++++++ server/render/canvas-handler.js | 42 ++----------------- server/routes/renderRoutes.js | 5 ++- 3 files changed, 31 insertions(+), 41 deletions(-) create mode 100644 server/middleware/validateCanvasInputMiddleware.js diff --git a/server/middleware/validateCanvasInputMiddleware.js b/server/middleware/validateCanvasInputMiddleware.js new file mode 100644 index 000000000..daf3775ba --- /dev/null +++ b/server/middleware/validateCanvasInputMiddleware.js @@ -0,0 +1,25 @@ +const { isObject } = require("lodash"); + +const validateCanvasInputMiddleware = (req, res, next) => { + const { values, keys, override } = req.body; + + if (!Array.isArray(values) || !Array.isArray(keys)) { + return res.status(400).send("Invalid input: 'values' and 'keys' must be arrays."); + } + + if (values.some((value) => typeof value !== "number")) { + return res.status(400).send("Invalid input: 'values' must be an array of numbers."); + } + + if (keys.some((key) => typeof key !== "string")) { + return res.status(400).send("Invalid input: 'keys' must be an array of strings."); + } + + if (override && !isObject(override)) { + return res.status(400).send("Override must be an object"); + } + + next(); // Proceed to the next middleware or route handler +}; + +module.exports = validateCanvasInputMiddleware; diff --git a/server/render/canvas-handler.js b/server/render/canvas-handler.js index df3cc8f02..b35926935 100644 --- a/server/render/canvas-handler.js +++ b/server/render/canvas-handler.js @@ -1,11 +1,9 @@ const { createCanvas } = require("canvas"); const { Canvas, FontLibrary } = require("skia-canvas"); -const { performance } = require("perf_hooks"); const Chart = require("chart.js/auto"); -const logger = require("../utils/logger"); const { backgroundColors, borderColors } = require("./canvas-colors"); -const { isObject, defaultsDeep, isNumber } = require("lodash"); +const { defaultsDeep, isNumber } = require("lodash"); try { FontLibrary.use("Montserrat", [ @@ -62,45 +60,16 @@ const getChartConfiguration = (keys, values, override) => { return defaultsDeep(override || {}, defaultConfiguration); }; -// Utility to validate input -const validateCanvasInput = ({ values, keys, override }, res) => { - if (!Array.isArray(values) || !Array.isArray(keys)) { - res.status(400).send("Invalid input: 'values' and 'keys' must be arrays."); - return false; - } - - if (values.some((value) => typeof value !== "number")) { - res.status(400).send("Invalid input: 'values' must be an array of numbers."); - return false; - } - - if (keys.some((key) => typeof key !== "string")) { - res.status(400).send("Invalid input: 'keys' must be an array of strings."); - return false; - } - - if (override && !isObject(override)) { - res.status(400).send("Override must be an object"); - return false; - } - - return true; -}; - exports.canvastest = function (req, res) { - //console.log("Incoming test request.", req); res.status(200).send("OK"); }; exports.canvas = function (req, res) { - const startTime = performance.now(); + const { logger } = req; const { w, h, values, keys, override } = req.body; - //console.log("Incoming Canvas Request:", w, h, values, keys, override); logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override }); - if (!validateCanvasInput(req.body, res)) return; - // Set the default Width and Height let [width, height] = [500, 275]; @@ -122,18 +91,15 @@ exports.canvas = function (req, res) { return canvas.toDataURL(); })() ); - console.log("Canvas generation time:", performance.now() - startTime, "ms"); }; exports.canvasSkia = async function (req, res) { - const startTime = performance.now(); + const { logger } = req; const { w, h, values, keys, override } = req.body; // Log incoming request for debugging logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override }); - if (!validateCanvasInput(req.body, res)) return; - // Default width and height const width = typeof w === "number" && w > 0 ? w : 500; const height = typeof h === "number" && h > 0 ? h : 275; @@ -154,11 +120,9 @@ exports.canvasSkia = async function (req, res) { // Send the Base64-encoded image as the response res.status(200).send(dataURL); - console.log("Canvas generation time:", performance.now() - startTime, "ms"); } catch (error) { // Log and handle rendering errors logger.log("canvas-error", "error", "jsr", null, { error: error.message }); - console.error("Error generating chart:", error.message); res.status(500).send("Failed to generate canvas."); } }; diff --git a/server/routes/renderRoutes.js b/server/routes/renderRoutes.js index d1b64566e..c1423f698 100644 --- a/server/routes/renderRoutes.js +++ b/server/routes/renderRoutes.js @@ -3,10 +3,11 @@ const router = express.Router(); const { inlinecss } = require("../render/inlinecss"); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); const { canvas, canvasSkia } = require("../render/canvas-handler"); +const validateCanvasInputMiddleware = require("../middleware/validateCanvasInputMiddleware"); // Define the route for inline CSS rendering router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss); -router.post("/canvas", validateFirebaseIdTokenMiddleware, canvas); -router.post("/canvas-skia", validateFirebaseIdTokenMiddleware, canvasSkia); +router.post("/canvas", [validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware], canvas); +router.post("/canvas-skia", [validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware], canvasSkia); module.exports = router; From 5cb93b1a2cb631f5a3598ad7bc9d7d25859b03ca Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 5 Dec 2024 12:07:16 -0800 Subject: [PATCH 09/14] IO-3053 Add datadog watcher for Production and Test instances. --- .platform/hooks/predeploy/01-install-dd.sh | 5 + package-lock.json | 445 +++++++++++++++++++-- package.json | 1 + server.js | 8 + 4 files changed, 433 insertions(+), 26 deletions(-) create mode 100644 .platform/hooks/predeploy/01-install-dd.sh diff --git a/.platform/hooks/predeploy/01-install-dd.sh b/.platform/hooks/predeploy/01-install-dd.sh new file mode 100644 index 000000000..e6a85046a --- /dev/null +++ b/.platform/hooks/predeploy/01-install-dd.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +DD_API_KEY=58d91898a70c6fd659f6eea768a57976 DD_SITE="us3.datadoghq.com" bash -c "$(curl -L https://install.datadoghq.com/scripts/install_script_agent7.sh)" + +echo "Datadog agent installed." diff --git a/package-lock.json b/package-lock.json index 6d1d243cc..dc75214ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "cors": "2.8.5", "crisp-status-reporter": "^1.2.2", "csrf": "^3.1.0", + "dd-trace": "^5.28.0", "dinero.js": "^1.9.1", "dotenv": "^16.4.5", "express": "^4.21.1", @@ -1633,6 +1634,114 @@ "kuler": "^2.0.0" } }, + "node_modules/@datadog/libdatadog": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@datadog/libdatadog/-/libdatadog-0.2.2.tgz", + "integrity": "sha512-rTWo96mEPTY5UbtGoFj8/wY0uKSViJhsPg/Z6aoFWBFXQ8b45Ix2e/yvf92AAwrhG+gPLTxEqTXh3kef2dP8Ow==", + "license": "Apache-2.0" + }, + "node_modules/@datadog/native-appsec": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@datadog/native-appsec/-/native-appsec-8.3.0.tgz", + "integrity": "sha512-RYHbSJ/MwJcJaLzaCaZvUyNLUKFbMshayIiv4ckpFpQJDiq1T8t9iM2k7008s75g1vRuXfsRNX7MaLn4aoFuWA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "node-gyp-build": "^3.9.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@datadog/native-iast-rewriter": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.5.0.tgz", + "integrity": "sha512-WRu34A3Wwp6oafX8KWNAbedtDaaJO+nzfYQht7pcJKjyC2ggfPeF7SoP+eDo9wTn4/nQwEOscSR4hkJqTRlpXQ==", + "license": "Apache-2.0", + "dependencies": { + "lru-cache": "^7.14.0", + "node-gyp-build": "^4.5.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@datadog/native-iast-rewriter/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@datadog/native-iast-rewriter/node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/@datadog/native-iast-taint-tracking": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-3.2.0.tgz", + "integrity": "sha512-Mc6FzCoyvU5yXLMsMS9yKnEqJMWoImAukJXolNWCTm+JQYCMf2yMsJ8pBAm7KyZKliamM9rCn7h7Tr2H3lXwjA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "node-gyp-build": "^3.9.0" + } + }, + "node_modules/@datadog/native-metrics": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@datadog/native-metrics/-/native-metrics-3.0.1.tgz", + "integrity": "sha512-0GuMyYyXf+Qpb/F+Fcekz58f2mO37lit9U3jMbWY/m8kac44gCPABzL5q3gWbdH+hWgqYfQoEYsdNDGSrKfwoQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "node-addon-api": "^6.1.0", + "node-gyp-build": "^3.9.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@datadog/pprof": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@datadog/pprof/-/pprof-5.4.1.tgz", + "integrity": "sha512-IvpL96e/cuh8ugP5O8Czdup7XQOLHeIDgM5pac5W7Lc1YzGe5zTtebKFpitvb1CPw1YY+1qFx0pWGgKP2kOfHg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "delay": "^5.0.0", + "node-gyp-build": "<4.0", + "p-limit": "^3.1.0", + "pprof-format": "^2.1.0", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@datadog/pprof/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@datadog/sketches-js": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@datadog/sketches-js/-/sketches-js-2.1.1.tgz", + "integrity": "sha512-d5RjycE+MObE/hU+8OM5Zp4VjTwiPLRa8299fj7muOmR16fb942z8byoMbCErnGh0lBevvgkGrLclQDvINbIyg==", + "license": "Apache-2.0" + }, "node_modules/@fastify/busboy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.0.0.tgz", @@ -1963,6 +2072,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/@jonkemp/package-utils": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@jonkemp/package-utils/-/package-utils-1.0.8.tgz", @@ -2178,6 +2296,30 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/core": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.29.0.tgz", + "integrity": "sha512-gmT7vAreXl0DTHD2rVZcw3+l2g84+5XiHIqdBUxXbExymPCvSsGOpiwMmn8nkiJur28STV31wnhIDrzWDPzjfA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2191,36 +2333,31 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "license": "BSD-3-Clause", - "optional": true, "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -2230,36 +2367,31 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/@redis/bloom": { "version": "1.2.0", @@ -3340,6 +3472,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-walk": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", @@ -3816,6 +3957,12 @@ "node": ">=10" } }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "license": "MIT" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4224,6 +4371,12 @@ "node": ">= 8" } }, + "node_modules/crypto-randomuuid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-randomuuid/-/crypto-randomuuid-1.0.0.tgz", + "integrity": "sha512-/RC5F4l1SCqD/jazwUF6+t34Cd8zTSAGZ7rvvZu1whZUhD2a5MOGKjSGowoGcpj/cbVZk1ZODIooJEQQq3nNAA==", + "license": "MIT" + }, "node_modules/csrf": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", @@ -4305,6 +4458,77 @@ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, + "node_modules/dc-polyfill": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/dc-polyfill/-/dc-polyfill-0.1.6.tgz", + "integrity": "sha512-UV33cugmCC49a5uWAApM+6Ev9ZdvIUMTrtCO9fj96TPGOQiea54oeO3tiEVdVeo3J9N2UdJEmbS4zOkkEA35uQ==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/dd-trace": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/dd-trace/-/dd-trace-5.28.0.tgz", + "integrity": "sha512-jyF7JLx2Yw16MHcD97sYKXbVd7ZT1hKJ5/NkRRGeG9cgen5+d/ilIvfzgh2qRjeow+9a5ligoZoUOYJ3nYn9hw==", + "hasInstallScript": true, + "license": "(Apache-2.0 OR BSD-3-Clause)", + "dependencies": { + "@datadog/libdatadog": "^0.2.2", + "@datadog/native-appsec": "8.3.0", + "@datadog/native-iast-rewriter": "2.5.0", + "@datadog/native-iast-taint-tracking": "3.2.0", + "@datadog/native-metrics": "^3.0.1", + "@datadog/pprof": "5.4.1", + "@datadog/sketches-js": "^2.1.0", + "@isaacs/ttlcache": "^1.4.1", + "@opentelemetry/api": ">=1.0.0 <1.9.0", + "@opentelemetry/core": "^1.14.0", + "crypto-randomuuid": "^1.0.0", + "dc-polyfill": "^0.1.4", + "ignore": "^5.2.4", + "import-in-the-middle": "1.11.2", + "int64-buffer": "^0.1.9", + "istanbul-lib-coverage": "3.2.0", + "jest-docblock": "^29.7.0", + "koalas": "^1.0.2", + "limiter": "1.1.5", + "lodash.sortby": "^4.7.0", + "lru-cache": "^7.14.0", + "module-details-from-path": "^1.0.3", + "msgpack-lite": "^0.1.26", + "opentracing": ">=0.12.1", + "path-to-regexp": "^0.1.10", + "pprof-format": "^2.1.0", + "protobufjs": "^7.2.5", + "retry": "^0.13.1", + "rfdc": "^1.3.1", + "semver": "^7.5.4", + "shell-quote": "^1.8.1", + "tlhunter-sorted-set": "^0.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/dd-trace/node_modules/@opentelemetry/api": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/dd-trace/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -4385,6 +4609,18 @@ "node": ">= 6" } }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4441,6 +4677,15 @@ "node": ">=8" } }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/dev-null": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz", @@ -4762,6 +5007,12 @@ "node": ">= 0.6" } }, + "node_modules/event-lite": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz", + "integrity": "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==", + "license": "MIT" + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -5707,6 +5958,47 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-in-the-middle": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.2.tgz", + "integrity": "sha512-gK6Rr6EykBcc6cVWRSBR5TWf8nn6hZMYSRYqCcHa0l0d1fPK7JSYo6+Mlmck76jIX9aL/IZ71c06U2VpFwl1zA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5738,6 +6030,12 @@ "node": ">=8" } }, + "node_modules/int64-buffer": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-0.1.10.tgz", + "integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==", + "license": "MIT" + }, "node_modules/intuit-oauth": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/intuit-oauth/-/intuit-oauth-4.1.3.tgz", @@ -5854,6 +6152,15 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, "node_modules/jackspeak": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", @@ -5917,6 +6224,18 @@ "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", "dev": true }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jose": { "version": "4.15.4", "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", @@ -6083,6 +6402,15 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/koalas": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/koalas/-/koalas-1.0.2.tgz", + "integrity": "sha512-RYhBbYaTTTHId3l6fnMZc3eGQNW6FVCqMG6AMwA5I1Mafr6AflaXeoi6x3xQuATRotGYRLk6+1ELZH4dstFNOA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -6202,6 +6530,12 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "license": "MIT" + }, "node_modules/logform": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", @@ -6223,8 +6557,7 @@ "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", - "license": "Apache-2.0", - "optional": true + "license": "Apache-2.0" }, "node_modules/lru-cache": { "version": "6.0.0", @@ -6425,6 +6758,12 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==", + "license": "MIT" + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -6450,6 +6789,21 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/msgpack-lite": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz", + "integrity": "sha512-SZ2IxeqZ1oRFGo0xFGbvBJWMp3yLIY9rlIJyxy8CGrwZn1f0ZK4r6jV/AM1r0FZMDUkWkglOk/eeKIL9g77Nxw==", + "license": "MIT", + "dependencies": { + "event-lite": "^0.1.1", + "ieee754": "^1.1.8", + "int64-buffer": "^0.1.9", + "isarray": "^1.0.0" + }, + "bin": { + "msgpack": "bin/msgpack" + } + }, "node_modules/multer": { "version": "1.4.5-lts.1", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", @@ -6488,6 +6842,12 @@ "node": ">= 0.4.0" } }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "license": "MIT" + }, "node_modules/node-eta": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/node-eta/-/node-eta-0.9.0.tgz", @@ -6520,6 +6880,17 @@ "node": ">= 6.13.0" } }, + "node_modules/node-gyp-build": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.9.0.tgz", + "integrity": "sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-mailjet": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/node-mailjet/-/node-mailjet-6.0.6.tgz", @@ -6688,6 +7059,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opentracing": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.7.tgz", + "integrity": "sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -6708,7 +7088,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "devOptional": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -6851,6 +7230,12 @@ "@jonkemp/package-utils": "^1.0.8" } }, + "node_modules/pprof-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pprof-format/-/pprof-format-2.1.0.tgz", + "integrity": "sha512-0+G5bHH0RNr8E5hoZo/zJYsL92MhkZjwrHp3O2IxmY8RJL9ooKeuZ8Tm0ZNBw5sGZ9TiM71sthTjWoR2Vf5/xw==", + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -6918,7 +7303,6 @@ "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", "hasInstallScript": true, "license": "BSD-3-Clause", - "optional": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -7158,7 +7542,6 @@ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "license": "MIT", - "optional": true, "engines": { "node": ">= 4" } @@ -7178,6 +7561,12 @@ "node": ">=14" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, "node_modules/rimraf": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", @@ -7388,7 +7777,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8170,6 +8558,12 @@ "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" }, + "node_modules/tlhunter-sorted-set": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tlhunter-sorted-set/-/tlhunter-sorted-set-0.1.0.tgz", + "integrity": "sha512-eGYW4bjf1DtrHzUYxYfAcSytpOkA44zsr7G2n3PV7yOUR23vmkGe3LL4R+1jL9OsXtbsFOwe8XtbCrabeaEFnw==", + "license": "MIT" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -8693,7 +9087,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/package.json b/package.json index 4fd96c3ff..288eb99c8 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "cors": "2.8.5", "crisp-status-reporter": "^1.2.2", "csrf": "^3.1.0", + "dd-trace": "^5.28.0", "dinero.js": "^1.9.1", "dotenv": "^16.4.5", "express": "^4.21.1", diff --git a/server.js b/server.js index a408742a3..5ea5bd428 100644 --- a/server.js +++ b/server.js @@ -4,6 +4,14 @@ require("dotenv").config({ path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); +if (process.env.NODE_ENV) { + const tracer = require("dd-trace").init({ + profiling: true, + env: process.env.NODE_ENV, + service: "bodyshop-api" + }); +} + const cors = require("cors"); const http = require("http"); const Redis = require("ioredis"); From 8f752d575a99c0e013c603d2b04a97dd324af5c3 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 5 Dec 2024 12:13:49 -0800 Subject: [PATCH 10/14] feature/IO-3052-Skia-Canvas-Handler: Optimizations Signed-off-by: Dave Richer --- server/render/canvas-handler.js | 63 ++++++++++++++------------------- server/routes/renderRoutes.js | 4 +-- 2 files changed, 29 insertions(+), 38 deletions(-) diff --git a/server/render/canvas-handler.js b/server/render/canvas-handler.js index 946b6d082..0da0cc9a2 100644 --- a/server/render/canvas-handler.js +++ b/server/render/canvas-handler.js @@ -5,6 +5,8 @@ const Chart = require("chart.js/auto"); const { backgroundColors, borderColors } = require("./canvas-colors"); const { defaultsDeep, isNumber } = require("lodash"); +const CANVAS_QUEUE_LIMIT = 100; + let isProcessing = false; const requestQueue = []; @@ -83,9 +85,7 @@ const processCanvasRequest = async (req, res, isSkia = false) => { const chart = new Chart(ctx, configuration); // Generate and send the image - const chartImage = isSkia - ? (await canvas.toBuffer("image/png")).toString("base64") - : canvas.toDataURL(); + const chartImage = isSkia ? (await canvas.toBuffer("image/png")).toString("base64") : canvas.toDataURL(); chart.destroy(); res.status(200).send(isSkia ? `data:image/png;base64,${chartImage}` : chartImage); @@ -95,49 +95,40 @@ const processCanvasRequest = async (req, res, isSkia = false) => { } }; -const processNextInQueue = async () => { - if (requestQueue.length === 0) { - isProcessing = false; - return; +const enqueueRequest = (req, res, isSkia) => { + if (requestQueue.length >= CANVAS_QUEUE_LIMIT) { + res.status(503).send("Server is busy. Please try again later."); + return false; } + requestQueue.push({ req, res, isSkia }); + req.logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length }); + return true; +}; - const { req, res, isSkia } = requestQueue.shift(); - await processCanvasRequest(req, res, isSkia); - processNextInQueue(); +const processNextInQueue = async () => { + while (requestQueue.length > 0) { + const { req, res, isSkia } = requestQueue.shift(); + try { + await processCanvasRequest(req, res, isSkia); + } catch (err) { + console.error("canvas-queue-error", "error", "jsr", null, { error: err.message }); + } + } + isProcessing = false; }; exports.canvastest = function (req, res) { res.status(200).send("OK"); }; -exports.canvas = async function (req, res) { - if (isProcessing) { - if (requestQueue.length >= 100) { - // Set a maximum queue size - return res.status(503).send("Server is busy. Please try again later."); - } - requestQueue.push({ req, res, isSkia: false }); - req.logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length }); - return; - } - +exports.canvas = async (req, res) => { + if (isProcessing || !enqueueRequest(req, res, false)) return; isProcessing = true; - await processCanvasRequest(req, res, false); - processNextInQueue(); + processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message })); }; -exports.canvasSkia = async function (req, res) { - if (isProcessing) { - if (requestQueue.length >= 100) { - // Set a maximum queue size - return res.status(503).send("Server is busy. Please try again later."); - } - requestQueue.push({ req, res, isSkia: true }); - req.logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length }); - return; - } - +exports.canvasSkia = async (req, res) => { + if (isProcessing || !enqueueRequest(req, res, true)) return; isProcessing = true; - await processCanvasRequest(req, res, true); - processNextInQueue(); + processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message })); }; diff --git a/server/routes/renderRoutes.js b/server/routes/renderRoutes.js index c1423f698..fda1b5846 100644 --- a/server/routes/renderRoutes.js +++ b/server/routes/renderRoutes.js @@ -7,7 +7,7 @@ const validateCanvasInputMiddleware = require("../middleware/validateCanvasInput // Define the route for inline CSS rendering router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss); -router.post("/canvas", [validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware], canvas); -router.post("/canvas-skia", [validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware], canvasSkia); +router.post("/canvas", [validateCanvasInputMiddleware], canvas); +router.post("/canvas-skia", [validateCanvasInputMiddleware], canvasSkia); module.exports = router; From c84fbcaba129e7e79baf179b632df73c5bc94578 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 5 Dec 2024 12:14:06 -0800 Subject: [PATCH 11/14] feature/IO-3052-Skia-Canvas-Handler: Optimizations Signed-off-by: Dave Richer --- server/routes/renderRoutes.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/routes/renderRoutes.js b/server/routes/renderRoutes.js index fda1b5846..c1423f698 100644 --- a/server/routes/renderRoutes.js +++ b/server/routes/renderRoutes.js @@ -7,7 +7,7 @@ const validateCanvasInputMiddleware = require("../middleware/validateCanvasInput // Define the route for inline CSS rendering router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss); -router.post("/canvas", [validateCanvasInputMiddleware], canvas); -router.post("/canvas-skia", [validateCanvasInputMiddleware], canvasSkia); +router.post("/canvas", [validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware], canvas); +router.post("/canvas-skia", [validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware], canvasSkia); module.exports = router; From 20bddb43b6c92be17aea792094951d6731ca2f2d Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 5 Dec 2024 12:16:32 -0800 Subject: [PATCH 12/14] feature/IO-3052-Skia-Canvas-Handler: Fix missing checks Signed-off-by: Dave Richer --- server/middleware/validateCanvasInputMiddleware.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/middleware/validateCanvasInputMiddleware.js b/server/middleware/validateCanvasInputMiddleware.js index daf3775ba..a03bb12ec 100644 --- a/server/middleware/validateCanvasInputMiddleware.js +++ b/server/middleware/validateCanvasInputMiddleware.js @@ -1,7 +1,7 @@ const { isObject } = require("lodash"); const validateCanvasInputMiddleware = (req, res, next) => { - const { values, keys, override } = req.body; + const { values, keys, override, w, h } = req.body; if (!Array.isArray(values) || !Array.isArray(keys)) { return res.status(400).send("Invalid input: 'values' and 'keys' must be arrays."); @@ -19,6 +19,13 @@ const validateCanvasInputMiddleware = (req, res, next) => { return res.status(400).send("Override must be an object"); } + if (w && (!Number.isFinite(w) || w <= 0)) { + return res.status(400).send("Width must be a positive number"); + } + if (h && (!Number.isFinite(h) || h <= 0)) { + return res.status(400).send("Height must be a positive number"); + } + next(); // Proceed to the next middleware or route handler }; From bfde72eed8831fcc164db3e8267d18fa044abc4b Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 5 Dec 2024 12:29:11 -0800 Subject: [PATCH 13/14] feature/IO-3052-Skia-Canvas-Handler: Fix missing checks Signed-off-by: Dave Richer --- server/render/canvas-handler.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/server/render/canvas-handler.js b/server/render/canvas-handler.js index 0da0cc9a2..5b810dd73 100644 --- a/server/render/canvas-handler.js +++ b/server/render/canvas-handler.js @@ -77,21 +77,36 @@ const processCanvasRequest = async (req, res, isSkia = false) => { const configuration = getChartConfiguration(keys, values, override); + // Placeholders to allow fine control over GAC + let canvas = null; + let ctx = null; + let chart = null; + let chartImage = null; + try { - const canvas = isSkia ? new Canvas(width, height) : createCanvas(width, height); - const ctx = canvas.getContext("2d"); + // Create the canvas + canvas = isSkia ? new Canvas(width, height) : createCanvas(width, height); + ctx = canvas.getContext("2d"); // Render the chart - const chart = new Chart(ctx, configuration); + chart = new Chart(ctx, configuration); // Generate and send the image - const chartImage = isSkia ? (await canvas.toBuffer("image/png")).toString("base64") : canvas.toDataURL(); + chartImage = isSkia ? (await canvas.toBuffer("image/png")).toString("base64") : canvas.toDataURL(); - chart.destroy(); res.status(200).send(isSkia ? `data:image/png;base64,${chartImage}` : chartImage); } catch (error) { + // Log the error and send the response logger.log("canvas-error", "error", "jsr", null, { error: error.message }); res.status(500).send("Failed to generate canvas."); + } finally { + // Cleanup resources + if (chart) { + chart.destroy(); + } + ctx = null; // Explicitly nullify for garbage collection + canvas = null; // Explicitly nullify for garbage collection + chartImage = null; } }; From 83a19528807f1675a89a42ed79bad97b6f413052 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 5 Dec 2024 15:30:37 -0800 Subject: [PATCH 14/14] IO-3051 Replace inlince css with juice. --- package-lock.json | 304 +++++++++++++++++++++++++++++++++++++ package.json | 1 + server/render/inlinecss.js | 40 +++-- 3 files changed, 331 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d1d243cc..40a207036 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "intuit-oauth": "^4.1.3", "ioredis": "^5.4.1", "json-2-csv": "^5.5.6", + "juice": "^11.0.0", "lodash": "^4.17.21", "moment": "^2.30.1", "moment-timezone": "^0.5.46", @@ -3359,6 +3360,15 @@ "node": ">= 6.0.0" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3927,6 +3937,15 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/component-emitter": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", @@ -4604,6 +4623,31 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -4690,6 +4734,18 @@ "node": ">=6" } }, + "node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -6046,6 +6102,69 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/juice": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-11.0.0.tgz", + "integrity": "sha512-sGF8hPz9/Wg+YXbaNDqc1Iuoaw+J/P9lBHNQKXAGc9pPNjCd4fyPai0Zxj7MRtdjMr0lcgk5PjEIkP2b8R9F3w==", + "license": "MIT", + "dependencies": { + "cheerio": "^1.0.0", + "commander": "^12.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^7.0.0" + }, + "bin": { + "juice": "bin/juice" + }, + "engines": { + "node": ">=18.17" + } + }, + "node_modules/juice/node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/juice/node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, "node_modules/jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", @@ -6298,6 +6417,12 @@ "cssom": "^0.5.0" } }, + "node_modules/mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -6780,6 +6905,18 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -8294,6 +8431,15 @@ "node": ">= 4.0.0" } }, + "node_modules/undici": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz", + "integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -8355,6 +8501,15 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -8379,6 +8534,131 @@ "node": ">=6.0" } }, + "node_modules/web-resource-inliner": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-7.0.0.tgz", + "integrity": "sha512-NlfnGF8MY9ZUwFjyq3vOUBx7KwF8bmE+ywR781SB0nWB6MoMxN4BA8gtgP1KGTZo/O/AyWJz7HZpR704eaj4mg==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "valid-data-url": "^3.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/web-resource-inliner/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/dom-serializer/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domutils/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -8407,6 +8687,30 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/whatwg-mimetype": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", diff --git a/package.json b/package.json index 4fd96c3ff..f45f6a13f 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "intuit-oauth": "^4.1.3", "ioredis": "^5.4.1", "json-2-csv": "^5.5.6", + "juice": "^11.0.0", "lodash": "^4.17.21", "moment": "^2.30.1", "moment-timezone": "^0.5.46", diff --git a/server/render/inlinecss.js b/server/render/inlinecss.js index 07fb68052..74700b210 100644 --- a/server/render/inlinecss.js +++ b/server/render/inlinecss.js @@ -3,24 +3,36 @@ require("dotenv").config({ path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); const logger = require("../utils/logger"); -const inlineCssTool = require("inline-css"); +//const inlineCssTool = require("inline-css"); +const juice = require("juice"); -exports.inlinecss = (req, res) => { +exports.inlinecss = async (req, res) => { //Perform request validation - logger.log("email-inline-css", "DEBUG", req.user.email, null, null); const { html, url } = req.body; - - inlineCssTool(html, { url: url }) - .then((inlinedHtml) => { - res.send(inlinedHtml); - }) - .catch((error) => { - logger.log("email-inline-css-error", "ERROR", req.user.email, null, { - error - }); - - res.send(error); + try { + const inlinedHtml = juice(html, { + applyAttributesTableElements: false, + preserveMediaQueries: false, + applyWidthAttributes: false }); + res.send(inlinedHtml); + } catch (error) { + logger.log("email-inline-css-error", "ERROR", req.user.email, null, { + error + }); + res.send(error.message); + } + + // inlineCssTool(html, { url: url }) + // .then((inlinedHtml) => { + // res.send(inlinedHtml); + // }) + // .catch((error) => { + // logger.log("email-inline-css-error", "ERROR", req.user.email, null, { + // error + // }); + + // }); };