From ed7ad2e8b7333409b159c9f6e9be80412159db97 Mon Sep 17 00:00:00 2001 From: coskunaydinoglu Date: Tue, 18 Mar 2025 16:40:55 +0300 Subject: [PATCH 001/203] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66d7cf20164..9eba7a796de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Version 24.05.28 +Enterprise Fixes: +- [cohorts] Fixed issue with combining multiple cohorts + ## Version 24.05.27 Fixes: - [crashes] Remove memory addresses from stack trace grouping From 14c3eaffbb48f46ffb87f2423935705578afc7d6 Mon Sep 17 00:00:00 2001 From: Cookiezaurs <> Date: Sat, 5 Apr 2025 14:34:25 +0300 Subject: [PATCH 002/203] [reports] Correctly match event for email report if event key contains '.' --- CHANGELOG.md | 4 ++++ plugins/reports/api/reports.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eba7a796de..a46c36120f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Version 24.05.X +Fixes: +- [reports] Correctly match event for email report if event key contains '.' + ## Version 24.05.28 Enterprise Fixes: - [cohorts] Fixed issue with combining multiple cohorts diff --git a/plugins/reports/api/reports.js b/plugins/reports/api/reports.js index 7dd1890eb79..1cc57ae2f0c 100644 --- a/plugins/reports/api/reports.js +++ b/plugins/reports/api/reports.js @@ -204,6 +204,10 @@ var metricProps = { } else { event = parts[1]; + for (var z = 2; z < parts.length; z++) { + event += "." + parts[z]; + } + parts[1] = event;//To use it as name also afterwards } if (event) { if (Array.isArray(event)) { From e88578520944bb334014fe2d936d96986d430cbc Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Fri, 4 Apr 2025 11:12:25 +0100 Subject: [PATCH 003/203] correctly parse string to be sent in notification --- plugins/push/api/send/data/pers.js | 43 +++++++++++++++++++----------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/plugins/push/api/send/data/pers.js b/plugins/push/api/send/data/pers.js index 23fb445dd87..0fcb6116dc5 100644 --- a/plugins/push/api/send/data/pers.js +++ b/plugins/push/api/send/data/pers.js @@ -4,36 +4,46 @@ const { dot } = require('../../../../../api/utils/common'); * Personalize function. A factory to create a function which would apply personalization to a given string. * * @param {String} string string to personalize - * @param {Object} personaliztion object of {5: {f: 'fallback', c: false, k: 'key'}, 10: {...}} kind + * @param {Object} personalizations object of {5: {f: 'fallback', c: false, k: 'key'}, 10: {...}} kind * * @returns {function} function with single obejct parameter which returns final string for a given data */ -module.exports = function personalize(string, personaliztion) { - let parts = [], - indicies = personaliztion ? Object.keys(personaliztion).map(n => parseInt(n, 10)) : [], - i = 0, - def; +module.exports = function personalize(string, personalizations) { + let parts = []; + let def; + let i = 0; + let indexes = Object.keys(personalizations || {}).map(n => parseInt(n, 10)); - indicies.forEach(idx => { + indexes.forEach(idx => { if (i < idx) { - parts.push(string.substr(i, idx)); + const subStringLength = idx - i; + // push all the string that appears before the personalization index + parts.push(string.substr(i, subStringLength)); } - parts.push(function(data) { - let pers = personaliztion[idx]; - data = dot(data, pers.k); - if (pers.c && data) { + + // push the personalization function + parts.push(function (data) { + let personalization = personalizations[idx]; + + data = dot(data, personalization.k); + + if (personalization.c && data) { if (typeof data !== 'string') { data = data + ''; } + return data.substr(0, 1).toUpperCase() + data.substr(1); } - return data === null || data === undefined ? pers.f : (data + ''); + + // if data does not exist return fallback value + return data === null || data === undefined ? personalization.f : (data + ''); }); - i = idx + 1; + + i = idx; }); if (i < string.length) { - parts.push(string.substr(i, string.length)); + parts.push(string.substr(i)); } /** @@ -48,6 +58,7 @@ module.exports = function personalize(string, personaliztion) { return parts.map(p => typeof p === 'string' ? p : p(data)).join(''); } } + return def; }; @@ -55,4 +66,4 @@ module.exports = function personalize(string, personaliztion) { def = compile({___dummy: true}); return compile; -}; \ No newline at end of file +}; From eaa8049698adcd3dd1eb55223a95c174a8c4e065 Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Fri, 4 Apr 2025 15:41:11 +0100 Subject: [PATCH 004/203] fix lint error --- plugins/push/api/send/data/pers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/push/api/send/data/pers.js b/plugins/push/api/send/data/pers.js index 0fcb6116dc5..564c39350f3 100644 --- a/plugins/push/api/send/data/pers.js +++ b/plugins/push/api/send/data/pers.js @@ -22,7 +22,7 @@ module.exports = function personalize(string, personalizations) { } // push the personalization function - parts.push(function (data) { + parts.push(function(data) { let personalization = personalizations[idx]; data = dot(data, personalization.k); From a78d8cde96ddcd981e28fbb77f0fea2794dd950a Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Thu, 3 Apr 2025 10:17:28 +0100 Subject: [PATCH 005/203] fix: remove right margin from message editor user property --- .../frontend/public/stylesheets/main.scss | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/plugins/push/frontend/public/stylesheets/main.scss b/plugins/push/frontend/public/stylesheets/main.scss index 958528634bf..06a41e67063 100644 --- a/plugins/push/frontend/public/stylesheets/main.scss +++ b/plugins/push/frontend/public/stylesheets/main.scss @@ -673,18 +673,15 @@ .cly-vue-push-notification-message-editor-with-emoji-picker { - - &__user-property { - padding: 1px 5px 2px; - background: #ECECEC; - border-radius: 2px; - margin-right: 2px; - - &:hover { - cursor: pointer; - } + // .cly-vue-push-notification-message-editor-with-emoji-picker__content + &__content { + word-break: break-all; + overflow-x:auto; + height: 100px; + padding-right:32px; } + // .cly-vue-push-notification-message-editor-with-emoji-picker__title &__title { word-break: break-all; overflow-x: auto; @@ -692,11 +689,16 @@ padding-right:32px; } - &__content { - word-break: break-all; - overflow-x:auto; - height: 100px; - padding-right:32px; + // .cly-vue-push-notification-message-editor-with-emoji-picker__user-property + &__user-property { + padding: 1px 5px 2px; + background: #ECECEC; + border-radius: 2px; + + // .cly-vue-push-notification-message-editor-with-emoji-picker__user-property:hover + &:hover { + cursor: pointer; + } } } @@ -830,4 +832,4 @@ .cly-vue-push-notification-details-chart-bars__item-legend-percentage + .text-medium span i{ margin-left: 6px; cursor: pointer; -} \ No newline at end of file +} From 3acc812c193ef7d122a4d72b9264578a965c2a3b Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Mon, 7 Apr 2025 16:31:30 +0100 Subject: [PATCH 006/203] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a46c36120f1..26745ebd8f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## Version 24.05.X Fixes: +- [push] Fixed push notifications title and content text and variables combination - [reports] Correctly match event for email report if event key contains '.' ## Version 24.05.28 @@ -4503,4 +4504,3 @@ This version provides several features and bugfixes to both server and SDKs. The A user of an application can only view analytics for that application and cannot edit its settings. * Added csfr protection to all methods provided through app.js. - From 0202cc2232feec904a58d89c0819f3bf03a3d277 Mon Sep 17 00:00:00 2001 From: Cookiezaurs Date: Thu, 3 Oct 2024 17:14:11 +0300 Subject: [PATCH 007/203] Allow downloading data also from other databases in dbviewer (cherry picked from commit 4a5b6537813b9c7339055602b6e19bbfb29a01f2) --- api/utils/requestProcessor.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/api/utils/requestProcessor.js b/api/utils/requestProcessor.js index 2dbfd60780d..92c574288c3 100644 --- a/api/utils/requestProcessor.js +++ b/api/utils/requestProcessor.js @@ -2130,8 +2130,16 @@ const processRequest = (params) => { dbUserHasAccessToCollection(params, params.qstring.collection, (hasAccess) => { if (hasAccess) { + var dbs = { countly: common.db, countly_drill: common.drillDb, countly_out: common.outDb, countly_fs: countlyFs.gridfs.getHandler() }; + var db = ""; + if (params.qstring.db && dbs[params.qstring.db]) { + db = dbs[params.qstring.db]; + } + else { + db = common.db; + } countlyApi.data.exports.fromDatabase({ - db: (params.qstring.db === "countly_drill") ? common.drillDb : (params.qstring.dbs === "countly_drill") ? common.drillDb : common.db, + db: db, params: params, collection: params.qstring.collection, query: params.qstring.query, From 7cf70b00d4c2679539449d6ea7e711f31d3a1c17 Mon Sep 17 00:00:00 2001 From: Anna Sosina Date: Tue, 8 Apr 2025 17:50:38 +0300 Subject: [PATCH 008/203] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26745ebd8f2..c41fd37d3ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ -## Version 24.05.X +## Version 24.05.29 Fixes: +- [core] Allow downloading data also from other databases in dbviewer +- [crash_symbolication] Symbolication server api end point test fix - [push] Fixed push notifications title and content text and variables combination - [reports] Correctly match event for email report if event key contains '.' From 120f37e1967761e09d782c185fc7dab47a8327be Mon Sep 17 00:00:00 2001 From: Cookiezaurs <> Date: Thu, 10 Apr 2025 13:14:02 +0300 Subject: [PATCH 009/203] [dashboards] Added the option to set a refresh rate for dashboards, allowing data to update more frequently for selected dashboards. --- CHANGELOG.md | 4 ++ api/utils/taskmanager.js | 9 ++- plugins/dashboards/api/api.js | 49 +++++++++++++- .../dashboards/api/jobs/refreshDashboards.js | 65 ++++++++++++++++++ plugins/dashboards/api/parts/dashboards.js | 66 +++++++++++++++++++ .../public/javascripts/countly.models.js | 8 ++- .../public/localization/dashboards.properties | 2 + .../public/templates/dashboards-drawer.html | 15 +++++ 8 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 plugins/dashboards/api/jobs/refreshDashboards.js diff --git a/CHANGELOG.md b/CHANGELOG.md index c41fd37d3ba..026c164fa14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Version 24.05.x +Features: +- [dashboards] Added the option to set a refresh rate for dashboards, allowing data to update more frequently for selected dashboards. + ## Version 24.05.29 Fixes: - [core] Allow downloading data also from other databases in dbviewer diff --git a/api/utils/taskmanager.js b/api/utils/taskmanager.js index 8be6a3cfc6f..28112c5c5f9 100644 --- a/api/utils/taskmanager.js +++ b/api/utils/taskmanager.js @@ -972,7 +972,13 @@ taskmanager.rerunTask = function(options, callback) { }); } - options.db.collection("long_tasks").findOne({_id: options.id}, function(err, res) { + var qq = {_id: options.id} + if(options.additionalQuery){ + qq = options.additionalQuery; + qq._id = options.id; + } + log.d("Fetching from long_tasks to rerun: "+ JSON.stringify(qq)); + options.db.collection("long_tasks").findOne(qq, function(err, res) { if (!err && res && res.request) { var reqData = {}; try { @@ -982,6 +988,7 @@ taskmanager.rerunTask = function(options, callback) { reqData = {}; } if (reqData.uri) { + reqData.json = reqData.json || {}; reqData.json.task_id = options.id; reqData.strictSSL = false; if (reqData.json && reqData.json.period && Array.isArray(reqData.json.period)) { diff --git a/plugins/dashboards/api/api.js b/plugins/dashboards/api/api.js index 8103fa9769f..67b282acd0d 100644 --- a/plugins/dashboards/api/api.js +++ b/plugins/dashboards/api/api.js @@ -22,6 +22,12 @@ plugins.setConfigs("dashboards", { (function() { + plugins.register("/master", function() { + setTimeout(() => { + require('../../../api/parts/jobs').job('dashboards:refreshDashboards').replace().schedule('every 5 minutes'); + }, 1000); + }); + /** * @api {get} /o/dashboards Get dashboard * @apiName GetDashboard @@ -162,6 +168,11 @@ plugins.setConfigs("dashboards", { } } + if (dashboard.refreshRate) { + dashboard.refreshRate = dashboard.refreshRate / 60; //Convert to minutes + dashboard.use_refresh_rate = true; + } + if (canSeeDashboardShares(params.member, dashboard)) { parallelTasks.push(fetchSharedUsersInfo.bind(null, dashboard)); } @@ -597,6 +608,19 @@ plugins.setConfigs("dashboards", { shareWith = params.qstring.share_with || "", copyDashId = params.qstring.copy_dash_id; + var refreshRate = 0; + if (params.qstring.use_refresh_rate && params.qstring.use_refresh_rate !== "false" && params.qstring.refreshRate > 0) { + try { + refreshRate = parseInt(params.qstring.refreshRate, 10); + refreshRate = refreshRate * 60; //Convert to seconds + } + catch (ex) { + refreshRate = 0; + log.e("passed unexpected refresh rate"); + } + + } + try { sharedEmailEdit = JSON.parse(sharedEmailEdit); } @@ -711,6 +735,9 @@ plugins.setConfigs("dashboards", { theme: theme, created_at: new Date().getTime() }; + if (refreshRate > 0) { + dashData.refreshRate = refreshRate; + } var widgets = dataObj.newWidgetIds; if (widgets && widgets.length) { @@ -846,6 +873,19 @@ plugins.setConfigs("dashboards", { send_email_invitation = params.qstring.send_email_invitation, memberId = params.member._id + ""; + var refreshRate = 0; + if (params.qstring.use_refresh_rate && params.qstring.use_refresh_rate !== "false" && params.qstring.refreshRate > 0) { + try { + refreshRate = parseInt(params.qstring.refreshRate, 10); + refreshRate = refreshRate * 60; //Convert to seconds + } + catch (ex) { + refreshRate = 0; + log.e("passed unexpected refresh rate"); + } + } + + if (!dashboardId || dashboardId.length !== 24) { common.returnMessage(params, 400, 'Invalid parameter: dashboard_id'); return true; @@ -972,12 +1012,19 @@ plugins.setConfigs("dashboards", { changedFields.shared_user_groups_view = sharedUserGroupView; } } + var unset = {shared_with_view: "", shared_with_edit: ""}; + if (refreshRate && refreshRate > 0 && refreshRate !== dashboard.refreshRate) { + changedFields.refreshRate = refreshRate; + } + else if (refreshRate === 0) { + unset.refreshRate = ""; //unset refresh rate for dashboard + } common.db.collection("dashboards").update( filterCond, { $set: changedFields, - $unset: {shared_with_view: "", shared_with_edit: ""} + $unset: unset }, async function(e, res) { if (!e && res) { diff --git a/plugins/dashboards/api/jobs/refreshDashboards.js b/plugins/dashboards/api/jobs/refreshDashboards.js new file mode 100644 index 00000000000..604e94ef787 --- /dev/null +++ b/plugins/dashboards/api/jobs/refreshDashboards.js @@ -0,0 +1,65 @@ +const job = require('../../../../api/parts/jobs/job.js'); +const log = require('../../../../api/utils/log.js')('job:dashboards:refreshDashboards'); +const pluginManager = require('../../../pluginManager.js'); +var customDashboards = require('./../parts/dashboards.js'); + + +/** class RefreshDashboardsJob */ +class RefreshDashboardsJob extends job.Job { + /** function run + * @param {object} countlyDb - db connection object + * @param {function} doneJob - function to call when finishing Job + * @param {function} progressJob - fnction to call while running job + */ + run(countlyDb, doneJob, progressJob) { + var total = 0; + var current = 0; + var bookmark = ''; + log.d('Starting dashboards refresh job'); + + /** + * check job status periodically + */ + function ping() { + log.d('Pinging dashboards refresh job'); + if (pingTimeout) { + progressJob(total, current, bookmark); + pingTimeout = setTimeout(ping, 10000); + } + } + var pingTimeout = setTimeout(ping, 10000); + + /** + * end job + * @returns {varies} job done + */ + function endJob() { + log.d('Ending dashboards refresh job'); + clearTimeout(pingTimeout); + pingTimeout = 0; + return doneJob(); + } + + pluginManager.loadConfigs(countlyDb, async() => { + //Fetch all sashboards. + //Check for the ones that have set refresh rate. + //Trigger regeneration for those dashboards. + try { + var dashboards = await countlyDb.collection('dashboards').find({}).toArray(); + for (var z = 0; z < dashboards.length; z++) { + + if (dashboards[z].refreshRate && dashboards[z].refreshRate > 0) { + log.d('Refreshing dashboard: ' + dashboards[z]._id); + await customDashboards.refreshDashboard(countlyDb, dashboards[z]); + } + } + } + catch (error) { + log.e('Error while refreshing dashboards: ' + error); + } + return endJob(); + }); + } +} + +module.exports = RefreshDashboardsJob; diff --git a/plugins/dashboards/api/parts/dashboards.js b/plugins/dashboards/api/parts/dashboards.js index 5c40bb8e4ea..47cd3552345 100644 --- a/plugins/dashboards/api/parts/dashboards.js +++ b/plugins/dashboards/api/parts/dashboards.js @@ -11,6 +11,8 @@ var countlyModel = require("../../../../api/lib/countly.model.js"), log = common.log('dashboards:api'), plugins = require("../../../pluginManager.js"); +const taskmanager = require('../../../../api/utils/taskmanager.js'); + /** @lends module:api/parts/data/dashboard */ var dashboard = {}; @@ -73,6 +75,70 @@ function toSegment(val) { return val; } +/** + * Triggers widget refresh for drill and formula widgets in dashboards based on refresh rate. + * @param {object} db - db Connection, if not passed will use common.db + * @param {object} dashboard_obj - dashboard object + */ +dashboard.refreshDashboard = async function(db, dashboard_obj) { + //Get lists of widgets which could be refreshed. + db = db || common.db; + if (dashboard_obj.widgets && dashboard_obj.widgets.length) { + try { + for (var p = 0; p < dashboard_obj.widgets.length; p++) { + dashboard_obj.widgets[p] = common.db.ObjectID(dashboard_obj.widgets[p]); + } + var pipeline = [ + {"$match": {"_id": {"$in": dashboard_obj.widgets}, "widget_type": {"$in": ["drill", "formulas"]}}} + ]; + var widgets = await db.collection("widgets").aggregate(pipeline).toArray(); + var tasks = []; + for (var z = 0; z < widgets.length; z++) { + if (widgets[z].widget_type === "drill") { + for (var k = 0; k < widgets[z].drill_report.length; k++) { + tasks.push({ + _id: widgets[z].drill_report[k], + type: "drill" + }); + } + } + else if (widgets[z].widget_type === "formulas") { + for (var kz = 0; kz < widgets[z].cmetrics.length; kz++) { + tasks.push({ + _id: widgets[z].cmetrics[kz], + type: "formula" + }); + } + } + } + //Refresh rate is saved in seconds in database + var maxTS = Date.now().valueOf() - dashboard_obj.refreshRate * 1000; + log.d("collected list with potential tasks for refresh", JSON.stringify(tasks)); + await async.eachSeries(tasks, function(task, next) { + taskmanager.rerunTask({ + db: common.db, + id: task._id, + autoUpdate: true, + additionalQuery: { + status: {"$nin": ["running", "rerunning"]}, + end: {"$lt": maxTS} + }, + subtask_key: "daily" + }, function(e) { + if (e) { + log.e(e, e.stack); + } + next(); + }); + }); + } + catch (e) { + log.d("Error while refreshing dashboard", e); + } + + } + +}; dashboard.mapWidget = function(widget) { var widgetType, visualization, dataType, appcount, breakdowns, isPluginWidget, feature; diff --git a/plugins/dashboards/frontend/public/javascripts/countly.models.js b/plugins/dashboards/frontend/public/javascripts/countly.models.js index e63e1e62493..6cb1206405e 100644 --- a/plugins/dashboards/frontend/public/javascripts/countly.models.js +++ b/plugins/dashboards/frontend/public/javascripts/countly.models.js @@ -15,6 +15,8 @@ theme: 0, is_owner: true, send_email_invitation: false, + use_refresh_rate: false, + refreshRate: 0, }; } }, @@ -88,6 +90,8 @@ "copy_dash_id": settings.copyDashId, "share_with": settings.share_with, "send_email_invitation": settings.send_email_invitation, + "use_refresh_rate": settings.use_refresh_rate, + "refreshRate": settings.refreshRate, "theme": settings.theme }, dataType: "json" @@ -106,7 +110,9 @@ "shared_user_groups_view": JSON.stringify(settings.shared_user_groups_view), "share_with": settings.share_with, "send_email_invitation": settings.send_email_invitation, - "theme": settings.theme + "theme": settings.theme, + "use_refresh_rate": settings.use_refresh_rate, + "refreshRate": settings.refreshRate }, dataType: "json" }, {disableAutoCatch: true}); diff --git a/plugins/dashboards/frontend/public/localization/dashboards.properties b/plugins/dashboards/frontend/public/localization/dashboards.properties index a485a76d726..03a517598fe 100644 --- a/plugins/dashboards/frontend/public/localization/dashboards.properties +++ b/plugins/dashboards/frontend/public/localization/dashboards.properties @@ -31,6 +31,8 @@ dashboards.report = Dashboard report dashboards.period = Period dashboards.custom-period = Use custom time period dashboards.custom-cohort-widget-title= Show overall number of users in the chart +dashboards.custom-refresh-rate = Custom refresh rate +dashboards.custom-refresh-rate-description = The refresh rate should be specified in minutes, with a minimum value of 5 minutes. The dashboard will attempt to refresh report-based widgets (such as funnels and drills) according to the specified refresh rate. However, if the calculation of any widget takes longer than set refresh rate period, it will not refresh as frequently as the set rate. dashboards.email-subject = {0} stats for {1}! dashbaords.dashboard-theme-1 = Charcoal Blue dashbaords.dashboard-theme-2 = Deep Violet diff --git a/plugins/dashboards/frontend/public/templates/dashboards-drawer.html b/plugins/dashboards/frontend/public/templates/dashboards-drawer.html index 4ad035fcd48..bc802cf1bde 100644 --- a/plugins/dashboards/frontend/public/templates/dashboards-drawer.html +++ b/plugins/dashboards/frontend/public/templates/dashboards-drawer.html @@ -100,6 +100,21 @@ {{i18n('dashboards.send-email')}} + + + {{ i18nM("dashboards.custom-refresh-rate") }} + + + + + + + From ad4c79b6b53d373db3dc04e421dc4bb8de8dd829 Mon Sep 17 00:00:00 2001 From: Cookiezaurs <> Date: Thu, 10 Apr 2025 13:33:07 +0300 Subject: [PATCH 010/203] eslint --- api/utils/taskmanager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/utils/taskmanager.js b/api/utils/taskmanager.js index 28112c5c5f9..ce6dcb40238 100644 --- a/api/utils/taskmanager.js +++ b/api/utils/taskmanager.js @@ -972,12 +972,12 @@ taskmanager.rerunTask = function(options, callback) { }); } - var qq = {_id: options.id} - if(options.additionalQuery){ + var qq = {_id: options.id}; + if (options.additionalQuery) { qq = options.additionalQuery; qq._id = options.id; } - log.d("Fetching from long_tasks to rerun: "+ JSON.stringify(qq)); + log.d("Fetching from long_tasks to rerun: " + JSON.stringify(qq)); options.db.collection("long_tasks").findOne(qq, function(err, res) { if (!err && res && res.request) { var reqData = {}; From f886fa775eb63df4b54c7c11eecc93a87e2a3111 Mon Sep 17 00:00:00 2001 From: Cookiezaurs <> Date: Thu, 10 Apr 2025 14:01:36 +0300 Subject: [PATCH 011/203] Enforcing 5 minutes as minimum refresh rate --- plugins/dashboards/api/api.js | 4 ++-- plugins/dashboards/api/jobs/refreshDashboards.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/dashboards/api/api.js b/plugins/dashboards/api/api.js index 67b282acd0d..6152d8010b3 100644 --- a/plugins/dashboards/api/api.js +++ b/plugins/dashboards/api/api.js @@ -612,7 +612,7 @@ plugins.setConfigs("dashboards", { if (params.qstring.use_refresh_rate && params.qstring.use_refresh_rate !== "false" && params.qstring.refreshRate > 0) { try { refreshRate = parseInt(params.qstring.refreshRate, 10); - refreshRate = refreshRate * 60; //Convert to seconds + refreshRate = Math.max(5, refreshRate) * 60; //Convert to seconds } catch (ex) { refreshRate = 0; @@ -877,7 +877,7 @@ plugins.setConfigs("dashboards", { if (params.qstring.use_refresh_rate && params.qstring.use_refresh_rate !== "false" && params.qstring.refreshRate > 0) { try { refreshRate = parseInt(params.qstring.refreshRate, 10); - refreshRate = refreshRate * 60; //Convert to seconds + refreshRate = Math.max(5, refreshRate) * 60; //Convert to seconds } catch (ex) { refreshRate = 0; diff --git a/plugins/dashboards/api/jobs/refreshDashboards.js b/plugins/dashboards/api/jobs/refreshDashboards.js index 604e94ef787..8c3b4464584 100644 --- a/plugins/dashboards/api/jobs/refreshDashboards.js +++ b/plugins/dashboards/api/jobs/refreshDashboards.js @@ -47,8 +47,10 @@ class RefreshDashboardsJob extends job.Job { try { var dashboards = await countlyDb.collection('dashboards').find({}).toArray(); for (var z = 0; z < dashboards.length; z++) { - if (dashboards[z].refreshRate && dashboards[z].refreshRate > 0) { + if (dashboards[z].refreshRate < 300) { + dashboards[z].refreshRate = 300; + } log.d('Refreshing dashboard: ' + dashboards[z]._id); await customDashboards.refreshDashboard(countlyDb, dashboards[z]); } From 994f7942112b97ed1d9816e25e427322102766d8 Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Mon, 14 Apr 2025 11:28:54 +0100 Subject: [PATCH 012/203] fix: unescaped sdk logs --- .../crashes/frontend/public/javascripts/countly.models.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/crashes/frontend/public/javascripts/countly.models.js b/plugins/crashes/frontend/public/javascripts/countly.models.js index 0e382a6fdb5..e216685598e 100644 --- a/plugins/crashes/frontend/public/javascripts/countly.models.js +++ b/plugins/crashes/frontend/public/javascripts/countly.models.js @@ -808,6 +808,9 @@ function transformAppVersion(inpVersion) { } crashgroupJson.data.forEach(function(crash, crashIndex) { + // unescape logs + crashgroupJson.data[crashIndex].logs = countlyCommon.unescapeHtml(crash.logs) + if (crash.uid in userIds) { userIds[crash.uid].push(crashIndex); } @@ -1327,4 +1330,4 @@ function transformAppVersion(inpVersion) { return resultQuery; }; -}(window.countlyCrashes = window.countlyCrashes || {})); \ No newline at end of file +}(window.countlyCrashes = window.countlyCrashes || {})); From 9027ab984a05acb0edd79d34477d7805f3965213 Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Mon, 14 Apr 2025 11:29:26 +0100 Subject: [PATCH 013/203] chore: update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 026c164fa14..f6e99698e5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Version 24.05.x Features: - [dashboards] Added the option to set a refresh rate for dashboards, allowing data to update more frequently for selected dashboards. +- [crashed] Fix unescaped SDK logs ## Version 24.05.29 Fixes: From 1222970da388452e93bc7a996cb3fcc1f29ae564 Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Mon, 14 Apr 2025 11:55:47 +0100 Subject: [PATCH 014/203] fix: lint error --- plugins/crashes/frontend/public/javascripts/countly.models.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/crashes/frontend/public/javascripts/countly.models.js b/plugins/crashes/frontend/public/javascripts/countly.models.js index e216685598e..d64bc446840 100644 --- a/plugins/crashes/frontend/public/javascripts/countly.models.js +++ b/plugins/crashes/frontend/public/javascripts/countly.models.js @@ -809,7 +809,7 @@ function transformAppVersion(inpVersion) { crashgroupJson.data.forEach(function(crash, crashIndex) { // unescape logs - crashgroupJson.data[crashIndex].logs = countlyCommon.unescapeHtml(crash.logs) + crashgroupJson.data[crashIndex].logs = countlyCommon.unescapeHtml(crash.logs); if (crash.uid in userIds) { userIds[crash.uid].push(crashIndex); From 8aee3c7cddd064df427e8d2ea724bde05aedd308 Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Mon, 14 Apr 2025 17:49:28 +0100 Subject: [PATCH 015/203] chore: document no defined variable --- .../express/public/javascripts/countly/vue/components/drawer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/express/public/javascripts/countly/vue/components/drawer.js b/frontend/express/public/javascripts/countly/vue/components/drawer.js index 7e949e20cb0..a66b085dda2 100644 --- a/frontend/express/public/javascripts/countly/vue/components/drawer.js +++ b/frontend/express/public/javascripts/countly/vue/components/drawer.js @@ -62,6 +62,7 @@ 'is-open': this.isOpened, 'has-sidecars': this.hasSidecars }; + // NOTE: currentScreenMode variable seems to be not defined it should be defined or removed classes["cly-vue-drawer--" + this.currentScreenMode + "-screen"] = true; if (this.currentScreenMode === 'half') { classes["cly-vue-drawer--half-screen-" + this.size] = true; From e4c28bb868b00f9e93e38532e3525ab75b85a358 Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Mon, 14 Apr 2025 17:54:41 +0100 Subject: [PATCH 016/203] fix: align drawer title with close icon and remove duplicated code --- .../countly/vue/templates/drawer.html | 50 +++++++++++-------- .../public/stylesheets/vue/clyvue.scss | 24 ++++----- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/frontend/express/public/javascripts/countly/vue/templates/drawer.html b/frontend/express/public/javascripts/countly/vue/templates/drawer.html index 42b6eddddee..f9535187fdb 100644 --- a/frontend/express/public/javascripts/countly/vue/templates/drawer.html +++ b/frontend/express/public/javascripts/countly/vue/templates/drawer.html @@ -20,25 +20,35 @@
-
-
- -
-

{{title}}

-
- - - -
-
-

{{title}}

- - +
+ + + + {{ i18n('plugins.back') }} + +

+ {{ title }} +

+
+
@@ -51,7 +61,7 @@

{{i + 1}} - +

{{currentContent.name}}
@@ -100,4 +110,4 @@

{{title}}

- \ No newline at end of file + diff --git a/frontend/express/public/stylesheets/vue/clyvue.scss b/frontend/express/public/stylesheets/vue/clyvue.scss index 512edb38c66..8f6bd2bd4a0 100644 --- a/frontend/express/public/stylesheets/vue/clyvue.scss +++ b/frontend/express/public/stylesheets/vue/clyvue.scss @@ -1809,14 +1809,9 @@ box-sizing: border-box; } + // .cly-vue-drawer__title-container &__title-container { - width: 100%; - margin-bottom: 24px; - } - - &__title-header { - margin-top: 24px; - margin-left: 32px; + margin: 24px 32px; } &__subtitle { @@ -1824,16 +1819,19 @@ margin-bottom: 24px !important; } + // .cly-vue-drawer__close-button &__close-button { + display: flex; + align-items: center; + justify-content: center; + font-size: 30px; color: #81868D; transition: color 1s; cursor: pointer; - //width: 30px; - height: 15px; - //margin-block: auto; - margin-right: 32px; - margin-top: 24px; + height: 20px; + + // .cly-vue-drawer__close-button:hover &:hover { color: #333; transition: color 1s; @@ -4189,4 +4187,4 @@ position: unset; top:0; } -} \ No newline at end of file +} From f267e21737b1c9236fe1843f7f94ea73b56ca3af Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Mon, 14 Apr 2025 18:06:03 +0100 Subject: [PATCH 017/203] chore: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e99698e5b..92b58abb7e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Features: - [dashboards] Added the option to set a refresh rate for dashboards, allowing data to update more frequently for selected dashboards. - [crashed] Fix unescaped SDK logs +- [ui] Fix alignment of drawers title and close icon ## Version 24.05.29 Fixes: From 9f3f40551904694c868d8be7ea0a0a962d4721e7 Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Tue, 15 Apr 2025 17:04:22 +0100 Subject: [PATCH 018/203] fix: add missing columns on rating table edit --- .../public/javascripts/countly.views.js | 37 ++- .../public/templates/widgets-table.html | 229 ++++++++++++------ 2 files changed, 187 insertions(+), 79 deletions(-) diff --git a/plugins/star-rating/frontend/public/javascripts/countly.views.js b/plugins/star-rating/frontend/public/javascripts/countly.views.js index f8cc504087d..254f44a69f6 100644 --- a/plugins/star-rating/frontend/public/javascripts/countly.views.js +++ b/plugins/star-rating/frontend/public/javascripts/countly.views.js @@ -403,18 +403,43 @@ data: function() { return { cohortsEnabled: countlyGlobal.plugins.indexOf('cohorts') > -1, - persistKey: 'ratingsWidgetsTable_' + countlyCommon.ACTIVE_APP_ID, - tableDynamicCols: [ + persistKey: 'ratingsWidgetsTable_' + countlyCommon.ACTIVE_APP_ID + }; + }, + computed: { + tableDynamicCols() { + const columns = [ + { + value: 'rating_score', + label: CV.i18n('feedback.rating-score'), + default: true, + required: true + }, + { + value: 'responses', + label: CV.i18n('feedback.responses'), + default: true, + required: true + }, { value: "target_pages", label: CV.i18n("feedback.pages"), default: true, required: true } - ], - }; - }, - computed: { + ]; + + if (this.cohortsEnabled) { + columns.unshift({ + value: 'targeting', + label: CV.i18n('feedback.targeting'), + default: true, + required: true + }); + } + + return columns; + }, widgets: function() { for (var i = 0; i < this.rows.length; i++) { var ratingScore = 0; diff --git a/plugins/star-rating/frontend/public/templates/widgets-table.html b/plugins/star-rating/frontend/public/templates/widgets-table.html index 49a7d70ee80..21276589e72 100644 --- a/plugins/star-rating/frontend/public/templates/widgets-table.html +++ b/plugins/star-rating/frontend/public/templates/widgets-table.html @@ -1,76 +1,159 @@ + + From f7eb60d36150f65a84ebafd1b98dc1a36c06245b Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Wed, 23 Apr 2025 14:41:00 +0100 Subject: [PATCH 031/203] chore: Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f44447aad5..413e08e2719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Version 24.05.x - [feedback] Uniformize drawer internal name input texts - [star-rating] Fix rating score and responses table sorting +- [star-rating] Allow bulk update of widget status ## Version 24.05.30 Features: From 93e657dd52c093d08c337bb568e641fc4f9fb4ba Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Wed, 23 Apr 2025 15:49:56 +0100 Subject: [PATCH 032/203] feat: add loading state to widget status badge --- .../public/javascripts/countly.views.js | 8 +++++ .../public/stylesheets/widget-detail.scss | 32 ++++++++++++------- .../public/templates/widget-detail.html | 14 ++++++-- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/plugins/star-rating/frontend/public/javascripts/countly.views.js b/plugins/star-rating/frontend/public/javascripts/countly.views.js index 254f44a69f6..988de560fb9 100644 --- a/plugins/star-rating/frontend/public/javascripts/countly.views.js +++ b/plugins/star-rating/frontend/public/javascripts/countly.views.js @@ -1354,6 +1354,14 @@ timesShown = this.count; } return parseFloat(((this.count / timesShown) * 100).toFixed(2)) || 0; + }, + + widgetStatusClassModifier() { + if (this.isLoading) { + return 'loading'; + } + + return this.widget.status ? 'active' : 'disabled'; } }, mounted: function() { diff --git a/plugins/star-rating/frontend/public/stylesheets/widget-detail.scss b/plugins/star-rating/frontend/public/stylesheets/widget-detail.scss index 1e7be62aef4..bbf774fbf90 100644 --- a/plugins/star-rating/frontend/public/stylesheets/widget-detail.scss +++ b/plugins/star-rating/frontend/public/stylesheets/widget-detail.scss @@ -27,20 +27,30 @@ display: flex; } - &__widget-status-active { - background-color: #EBFAEE !important; - color: #12AF51 !important; + // .ratings-widget-detail-view__widget-status + &__widget-status { height: 19px; padding: 1px 6px 0px 6px; border-radius: 4px; - } - &__widget-status-disabled { - background-color: #d9d9d9 !important; - color: #81868D !important; - padding: 1px 6px 0px 6px; - height: 19px; - border-radius: 4px; + // .ratings-widget-detail-view__widget-status--active + &--active { + background-color: #EBFAEE; + color: #12AF51 !important; + } + + // .ratings-widget-detail-view__widget-status--disabled + &--disabled { + background-color: #d9d9d9; + color: #81868D !important; + } + + //.ratings-widget-detail-view__widget-status--loading + &--loading { + background-color: #d9d9d9; + min-width: 60px; + animation: blinker 1.5s cubic-bezier(0.5, 0, 1, 1) infinite alternate; + } } &__created-at, &__widget-id { @@ -61,4 +71,4 @@ text-transform: uppercase; } } -} \ No newline at end of file +} diff --git a/plugins/star-rating/frontend/public/templates/widget-detail.html b/plugins/star-rating/frontend/public/templates/widget-detail.html index 8ba25e7ade1..d1085c00e97 100644 --- a/plugins/star-rating/frontend/public/templates/widget-detail.html +++ b/plugins/star-rating/frontend/public/templates/widget-detail.html @@ -10,9 +10,17 @@

{{unescapeHtml(widget.popup_header_text)}}

-
- - {{ widget.status ? i18n('feedback.active') : i18n('feedback.disabled') }} +
+
{{ i18n('feedback.created-at') }} From 3cbf5062148010cac9f295cd1d11cd9d3a5902fb Mon Sep 17 00:00:00 2001 From: Savas Date: Thu, 24 Apr 2025 14:06:19 +0000 Subject: [PATCH 033/203] Fixed the code for fixing the SER-1971 --- .../cypress/e2e/dashboard/feedback/ratings/widgets.cy.js | 5 +++-- .../cypress/lib/dashboard/feedback/ratings/demoWidgetPage.js | 2 +- ui-tests/cypress/lib/dashboard/feedback/ratings/widgets.js | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) mode change 100644 => 100755 ui-tests/cypress/lib/dashboard/feedback/ratings/widgets.js diff --git a/ui-tests/cypress/e2e/dashboard/feedback/ratings/widgets.cy.js b/ui-tests/cypress/e2e/dashboard/feedback/ratings/widgets.cy.js index ab9371693ea..e412caee1bf 100755 --- a/ui-tests/cypress/e2e/dashboard/feedback/ratings/widgets.cy.js +++ b/ui-tests/cypress/e2e/dashboard/feedback/ratings/widgets.cy.js @@ -16,7 +16,7 @@ describe('Create New Widget', () => { navigationHelpers.goToFeedbackRatingsWidgetsPage(); }); - it.skip('Verify default values of page and create a widget with that values and then update the widget data', function() { + it('Verify default values of page and create a widget with that values and then update the widget data', function() { widgetsHelpers.clickAddNewWidgetButton(); widgetsHelpers.verifySettingsPageDefaultElements(); widgetsHelpers.typeWidgetName("My New Widget"); @@ -222,7 +222,8 @@ describe('Create New Widget', () => { contactViaCheckboxLabelText: widget.contactViaCheckboxLabelText, contactEmail: widgetRate.contactEmail, submitButtonText: widget.submitButtonText, - hasAggrementCheckbox: true, + //TODO SER-1971 There is no Aggrement Checkbox in the demo page, Also Look at the line 136 in demoWidgetPage.js + //hasAggrementCheckbox: true, selectedMainColor: widget.mainColor, selectedFontColor: widget.FontColor, hasPoweredByLogo: true, diff --git a/ui-tests/cypress/lib/dashboard/feedback/ratings/demoWidgetPage.js b/ui-tests/cypress/lib/dashboard/feedback/ratings/demoWidgetPage.js index 9b6bd94d3d9..e7950bc405b 100755 --- a/ui-tests/cypress/lib/dashboard/feedback/ratings/demoWidgetPage.js +++ b/ui-tests/cypress/lib/dashboard/feedback/ratings/demoWidgetPage.js @@ -33,7 +33,7 @@ const goToDemoWidgetPage = (username, password, appName, widgetID) => { if (response.admin_of[key].name === appName) { const appKey = response.admin_of[key].key; - cy.visit(`/feedback/rating?widget_id=${widgetID}&device_id=test&app_key=${appKey}`); + cy.visit(`/feedback/rating?widget_id=${widgetID.trim()}&device_id=test&app_key=${appKey}`); } } }); diff --git a/ui-tests/cypress/lib/dashboard/feedback/ratings/widgets.js b/ui-tests/cypress/lib/dashboard/feedback/ratings/widgets.js old mode 100644 new mode 100755 index 50916aa4770..e089b49faeb --- a/ui-tests/cypress/lib/dashboard/feedback/ratings/widgets.js +++ b/ui-tests/cypress/lib/dashboard/feedback/ratings/widgets.js @@ -53,11 +53,11 @@ const verifySettingsPageElements = ({ labelText: "Internal Name", element: feedbackRatingWidgetsPageElements.WIDGET_NAME_INPUT, value: widgetName, - elementPlaceHolder: "Widget Name" + elementPlaceHolder: "Enter a internal Name" }); cy.verifyElement({ labelElement: feedbackRatingWidgetsPageElements.WIDGET_NAME_DESC, - labelText: "Name survey for internal purposes. It is not going to be shown on survey.", + labelText: "This name is internal and will not be shown to your end user.", }); cy.verifyElement({ labelElement: feedbackRatingWidgetsPageElements.QUESTION_LABEL, From a19391852346146d383f49693dcb4137b01fa586 Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Tue, 29 Apr 2025 18:13:45 +0100 Subject: [PATCH 034/203] feat: add loading skeleton to status tag --- .../countly/vue/components/helpers.js | 64 ++++++-- .../public/stylesheets/vue/clyvue.scss | 150 ++++++++++++------ 2 files changed, 152 insertions(+), 62 deletions(-) diff --git a/frontend/express/public/javascripts/countly/vue/components/helpers.js b/frontend/express/public/javascripts/countly/vue/components/helpers.js index cebfd52c5a6..c6dd8367ca7 100644 --- a/frontend/express/public/javascripts/countly/vue/components/helpers.js +++ b/frontend/express/public/javascripts/countly/vue/components/helpers.js @@ -86,25 +86,63 @@ } })); - Vue.component("cly-status-tag", countlyBaseComponent.extend({ - template: '
\n' + - '\n' + - '{{text}}\n' + - '
', - mixins: [countlyVue.mixins.i18n], + Vue.component('cly-status-tag', countlyBaseComponent.extend({ props: { - text: { required: true, type: String }, - color: { default: "green", type: String}, - size: { default: "unset", type: String}, + color: { + default: 'green', + type: String + }, + + loading: { + default: false, + type: Boolean + }, + + size: { + default: 'unset', + type: String + }, + + text: { + required: true, + type: String + } }, + computed: { - dynamicClasses: function() { - if (this.size === "small") { - return ["cly-vue-status-tag--small", "cly-vue-status-tag--" + this.color]; + dynamicClasses() { + const classes = []; + + if (this.size === 'small') { + classes.push('cly-vue-status-tag--small'); + } + + if (this.loading) { + classes.push('cly-vue-status-tag--gray'); } - return "cly-vue-status-tag--" + this.color; + else { + classes.push(`cly-vue-status-tag--${this.color}`); + } + + return classes; } }, + + template: ` +
+
-
- -
+
{{ i18n('feedback.created-at') }} {{unescapeHtml(widget.created_at)}}
From 745b813191fd3a43b2e8d0ab5899e69f0cb5e09c Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Wed, 30 Apr 2025 11:05:51 +0100 Subject: [PATCH 036/203] chore: Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 413e08e2719..0f442ef5c8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - [feedback] Uniformize drawer internal name input texts - [star-rating] Fix rating score and responses table sorting - [star-rating] Allow bulk update of widget status +- [feedback] Uniformize feedback widgets status tag ## Version 24.05.30 Features: From f728cb7d5553f4096cdf445269b422c2b44b2d77 Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Wed, 30 Apr 2025 16:43:20 +0100 Subject: [PATCH 037/203] fix: remove background color from rating consent input suffix --- .../frontend/public/stylesheets/ratings.scss | 32 +++++++++++++++++++ .../public/templates/star-consent-link.html | 14 +++++--- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/plugins/star-rating/frontend/public/stylesheets/ratings.scss b/plugins/star-rating/frontend/public/stylesheets/ratings.scss index 85d0efaeb48..f7bbed66d95 100644 --- a/plugins/star-rating/frontend/public/stylesheets/ratings.scss +++ b/plugins/star-rating/frontend/public/stylesheets/ratings.scss @@ -433,6 +433,38 @@ } } +.cly-vue-drawer__input-element { + // .cly-vue-drawer__input-element .el-input__inner + & .el-input__inner { + // right padding = suffix width (40px) + space to border (10px) + padding-right: 50px; + + // .cly-vue-drawer__input-element .el-input__inner:focus + &:focus { + box-shadow: unset; + } + } + + // .cly-vue-drawer__input-element .el-input__inner .el-input__suffix + & .el-input__suffix { + right: 10px; + width: 40px; + + // .cly-vue-drawer__input-element .el-input__inner .el-input__suffix .el-input__suffix-inner + & .el-input__suffix-inner { + width: 100%; + height: 100%; + + // .cly-vue-drawer__input-element .el-input__inner .el-input__suffix .el-input__suffix-inner .el-input__count .el-input__count-inner + & .el-input__count .el-input__count-inner { + background: transparent; + padding: 0; + width: 100%; + } + } + } +} + .ratings-section-title { margin-right: 4px; } diff --git a/plugins/star-rating/frontend/public/templates/star-consent-link.html b/plugins/star-rating/frontend/public/templates/star-consent-link.html index 7941bfd5dce..c731fb6e576 100644 --- a/plugins/star-rating/frontend/public/templates/star-consent-link.html +++ b/plugins/star-rating/frontend/public/templates/star-consent-link.html @@ -3,12 +3,18 @@
{{i18n('rating.drawer.consent.text')}}
+ + {{ value.finalText.length }}/94 +
@@ -38,4 +44,4 @@
{{i18n('rating.drawer.consent.add.link')}}
-
\ No newline at end of file +
From 2d6b6f59bbee46d3d01961191415f62e30af6b38 Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Wed, 30 Apr 2025 16:44:19 +0100 Subject: [PATCH 038/203] chore: update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 413e08e2719..040e23b4e94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - [feedback] Uniformize drawer internal name input texts - [star-rating] Fix rating score and responses table sorting - [star-rating] Allow bulk update of widget status +- [UI] Remove white background from input character amount suffix ## Version 24.05.30 Features: From 320f25305e3f1da0668ebb3ee070f0f391503b3e Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Thu, 8 May 2025 11:10:29 +0100 Subject: [PATCH 039/203] fix: prevent error when ref or dropdown are undefined --- .../public/javascripts/countly/vue/components/dropdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/express/public/javascripts/countly/vue/components/dropdown.js b/frontend/express/public/javascripts/countly/vue/components/dropdown.js index 1f41c7eaca0..37a92472fb4 100644 --- a/frontend/express/public/javascripts/countly/vue/components/dropdown.js +++ b/frontend/express/public/javascripts/countly/vue/components/dropdown.js @@ -580,7 +580,7 @@ handleMenuItemClick: function(command, instance) { if (!this.disabled) { this.$emit('command', command, instance); - this.$refs.dropdown.handleClose(); + this.$refs?.dropdown?.handleClose(); } } }, From d4aac7090963b4e118d841cb7bf1dea1cf05a622 Mon Sep 17 00:00:00 2001 From: Anna Sosina Date: Thu, 8 May 2025 19:24:49 +0300 Subject: [PATCH 040/203] Update CHANGELOG.md --- CHANGELOG.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c01f755ba4..70f80ea4ae4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,16 @@ ## Version 24.05.x + +## Version 24.05.31 +Fixes: - [feedback] Uniformize drawer internal name input texts -- [star-rating] Fix rating score and responses table sorting +- [feedback] Uniformize feedback widgets status tag - [star-rating] Allow bulk update of widget status +- [star-rating] Fix rating score and responses table sorting - [UI] Remove white background from input character amount suffix -- [feedback] Uniformize feedback widgets status tag + +Enterprise Fixes: + - [retention] Fixed report loading + ## Version 24.05.30 Features: From 042ab91cc30c59a06a7fe49c0f02d021edf0d064 Mon Sep 17 00:00:00 2001 From: Artem Salpagarov Date: Mon, 1 May 2023 16:58:13 +0300 Subject: [PATCH 041/203] [push] Scheduling tz messages 24h upfront --- plugins/push/api/send/audience.js | 5 ++++- plugins/push/api/send/data/const.js | 1 + plugins/push/api/send/data/message.js | 7 +++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/plugins/push/api/send/audience.js b/plugins/push/api/send/audience.js index a3cb46a0fb3..44c336c6f0d 100644 --- a/plugins/push/api/send/audience.js +++ b/plugins/push/api/send/audience.js @@ -178,6 +178,9 @@ class Audience { if (!project.tk) { project.tk = 1; } + if (!project.tz) { + project.tz = 1; + } steps.push({$project: project}); } @@ -424,7 +427,7 @@ class PlainApiMapper extends Mapper { map(user, date, c, offset = 0) { let d = date.getTime(); if (this.trigger.tz) { - let utz = (user.tz === undefined || user.tz === null ? this.offset || 0 : user.tz || 0) * 60000; + let utz = (user.tz === undefined || user.tz === null ? this.offset || 0 : parseFloat(user.tz) || 0) * 60000; d = date.getTime() - this.trigger.sctz * 60000 - utz; if (d < Date.now()) { diff --git a/plugins/push/api/send/data/const.js b/plugins/push/api/send/data/const.js index 1587178f33e..6ff1d9e8c9e 100644 --- a/plugins/push/api/send/data/const.js +++ b/plugins/push/api/send/data/const.js @@ -27,6 +27,7 @@ const MEDIA_MIME_ANDROID = [ const DEFAULTS = { schedule_ahead: 5 * 60000, // schedule job needs to be scheduled this much ms prior to the job date + schedule_ahead_tz: 24 * 60 * 60000, // schedule job needs to be scheduled this much ms prior to the job date if we send in users' timezones queue_insert_batch: 100000, // insert into "push" collection in batches of 100 000 records max_media_size: 1024 * 1024 // 1Mb is a very conservative limit for media attachments }; diff --git a/plugins/push/api/send/data/message.js b/plugins/push/api/send/data/message.js index 02c331c667f..b3f7e81afe2 100644 --- a/plugins/push/api/send/data/message.js +++ b/plugins/push/api/send/data/message.js @@ -532,7 +532,7 @@ class Message extends Mongoable { */ get needsScheduling() { return this.state === State.Created && this.triggers.filter(t => t.kind === TriggerKind.Plain && - (!t.delayed || (t.delayed && t.start.getTime() > Date.now() - 5 * 60000))).length > 0; + (!t.delayed || (t.delayed && !t.tz && t.start.getTime() > Date.now() - DEFAULTS.schedule_ahead) || (t.delayed && t.tz && t.start.getTime() > Date.now() - DEFAULTS.schedule_ahead_tz))).length > 0; } /** @@ -659,7 +659,10 @@ class Message extends Mongoable { } }); } - let date = plain.delayed ? plain.start.getTime() - DEFAULTS.schedule_ahead : Date.now(); + let date = Date.now(); + if (plain.delayed) { + date = plain.start.getTime() - (plain.tz ? DEFAULTS.schedule_ahead_tz : DEFAULTS.schedule_ahead); + } await require('../../../../../api/parts/jobs').job('push:schedule', {mid: this._id, aid: this.app}).replace().once(date); } if (this.triggerAutoOrApi() && (this.is(State.Done) || this.state === State.Created)) { From 95308eb0ceddb4e262c8b11a00b8e3f3787f8697 Mon Sep 17 00:00:00 2001 From: Cihad Tekin Date: Tue, 20 May 2025 20:19:38 +0300 Subject: [PATCH 042/203] [push] Enable reschedule option --- plugins/push/api/send/data/trigger.js | 33 ++++++++++++++++--- .../public/javascripts/countly.models.js | 8 +++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/plugins/push/api/send/data/trigger.js b/plugins/push/api/send/data/trigger.js index 9ece4c22ca1..14a6b740fca 100644 --- a/plugins/push/api/send/data/trigger.js +++ b/plugins/push/api/send/data/trigger.js @@ -160,10 +160,11 @@ class Trigger extends Validatable { class PlainTrigger extends Trigger { /** * Constructor - * + * * @param {object} data filter data * @param {boolean} data.tz in case tz = true, sctz is scheduler's timezone offset in minutes (GMT +3 is "-180") * @param {number} data.sctz scheduler's timezone offset in minutes (GMT +3 is "-180") + * @param {boolean} data.reschedule allow rescheduling to next day when sending on "time" is not an option * @param {boolean} delayed true if audience calculation should be done right before sending the message */ constructor(data) { @@ -180,12 +181,13 @@ class PlainTrigger extends Trigger { tz: {type: 'Boolean', required: false}, sctz: {type: 'Number', required: false}, delayed: {type: 'Boolean', required: false}, + reschedule: {type: 'Boolean', required: false}, }); } /** * Getter for sctz - * + * * @returns {number|undefined} in case tz = true, this is scheduler's timezone offset in minutes (GMT +3 is "-180") */ get sctz() { @@ -194,7 +196,7 @@ class PlainTrigger extends Trigger { /** * Set scheduler's timezone offset, effectively setting `tz` prop as well - * + * * @param {number|undefined} sctz scheduler's timezone offset in seconds (GMT +3 = `-180`) */ set sctz(sctz) { @@ -240,6 +242,29 @@ class PlainTrigger extends Trigger { delete this._data.delayed; } } + + /** + * Getter for reschedule + * + * @returns {boolean} allow rescheduling to next day when sending on "time" is not an option + */ + get reschedule() { + return this._data.reschedule || false; + } + + /** + * Setter for reschedule + * + * @param {boolean|undefined} reschedule allow rescheduling to next day when sending on "time" is not an option + */ + set reschedule(reschedule) { + if (reschedule !== null && reschedule !== undefined) { + this._data.reschedule = reschedule; + } + else { + delete this._data.reschedule; + } + } } /** @@ -248,7 +273,7 @@ class PlainTrigger extends Trigger { class AutoTrigger extends Trigger { /** * Constructor - * + * * @param {object|null} data filter data * @param {Date} data.end message end date (don't send anything after this date, set status to Stopped) * @param {boolean} data.actuals whether to use server calculation date (false) or event/cohort entry date for scheduling (true) diff --git a/plugins/push/frontend/public/javascripts/countly.models.js b/plugins/push/frontend/public/javascripts/countly.models.js index c28b13d8314..383db0450d2 100644 --- a/plugins/push/frontend/public/javascripts/countly.models.js +++ b/plugins/push/frontend/public/javascripts/countly.models.js @@ -1863,6 +1863,7 @@ var result = { kind: 'plain', start: model.delivery.startDate, + reschedule: false, }; if (model.delivery.type === SendEnum.LATER) { if (model.timezone === TimezoneEnum.DEVICE) { @@ -1870,6 +1871,9 @@ result.sctz = new Date().getTimezoneOffset(); } } + if (model?.oneTime?.pastSchedule === PastScheduleEnum.NEXT_DAY) { + result.reschedule = true; + } result.delayed = model[TypeEnum.ONE_TIME].audienceSelection === AudienceSelectionEnum.BEFORE; return [result]; }, @@ -1878,6 +1882,7 @@ kind: model.automatic.trigger === TriggerEnum.EVENT ? 'event' : 'cohort', start: model.delivery.startDate, actuals: model.automatic.deliveryDateCalculation === DeliveryDateCalculationEnum.EVENT_DEVICE_DATE, + reschedule: false, }; if (options.isEndDateSet) { result.end = model.delivery.endDate; @@ -1912,6 +1917,9 @@ result.entry = model.automatic.trigger === TriggerEnum.COHORT_ENTRY, result.cancels = model.automatic.triggerNotMet === TriggerNotMetEnum.CANCEL_ON_EXIT; } + if (model?.automatic?.pastSchedule === PastScheduleEnum.NEXT_DAY) { + result.reschedule = true; + } return [result]; }, mapTransactionalTrigger: function(model) { From d547584ce19d1a71106ee7f9c9ed2d1ffd6a58b2 Mon Sep 17 00:00:00 2001 From: Cihad Tekin Date: Tue, 20 May 2025 20:26:49 +0300 Subject: [PATCH 043/203] [push] Deepscan fix: unnecessary null check --- plugins/push/frontend/public/javascripts/countly.models.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/push/frontend/public/javascripts/countly.models.js b/plugins/push/frontend/public/javascripts/countly.models.js index 383db0450d2..e3d7eb6c646 100644 --- a/plugins/push/frontend/public/javascripts/countly.models.js +++ b/plugins/push/frontend/public/javascripts/countly.models.js @@ -1871,7 +1871,7 @@ result.sctz = new Date().getTimezoneOffset(); } } - if (model?.oneTime?.pastSchedule === PastScheduleEnum.NEXT_DAY) { + if (model.oneTime?.pastSchedule === PastScheduleEnum.NEXT_DAY) { result.reschedule = true; } result.delayed = model[TypeEnum.ONE_TIME].audienceSelection === AudienceSelectionEnum.BEFORE; @@ -1917,7 +1917,7 @@ result.entry = model.automatic.trigger === TriggerEnum.COHORT_ENTRY, result.cancels = model.automatic.triggerNotMet === TriggerNotMetEnum.CANCEL_ON_EXIT; } - if (model?.automatic?.pastSchedule === PastScheduleEnum.NEXT_DAY) { + if (model.automatic.pastSchedule === PastScheduleEnum.NEXT_DAY) { result.reschedule = true; } return [result]; From 3eb70c98160b047b375cba89e89ca92dfa6080a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=B1nar=20Gen=C3=A7?= Date: Wed, 11 Jun 2025 15:25:32 +0300 Subject: [PATCH 044/203] Changelog entry for active users calculation bug --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70f80ea4ae4..da41cd665e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ ## Version 24.05.x +Enterprise Fixes: +- [active-users] Fixed bug related to selecting calculation ranges. As a result, some dates were previously calculated on incomplete data set. ## Version 24.05.31 Fixes: From 7a1568a45edf91f0fd946a844406d45396033d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=B1nar=20Gen=C3=A7?= Date: Wed, 18 Jun 2025 15:40:23 +0300 Subject: [PATCH 045/203] Update CHANGELOG.md for v24.05.32 --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da41cd665e4..755440847ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ -## Version 24.05.x +## Version 24.05.32 Enterprise Fixes: - [active-users] Fixed bug related to selecting calculation ranges. As a result, some dates were previously calculated on incomplete data set. + ## Version 24.05.31 Fixes: - [feedback] Uniformize drawer internal name input texts From dc60def4a35a6227e7f3f5fe8161c945eab693c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=B1nar=20Gen=C3=A7?= Date: Wed, 18 Jun 2025 16:58:31 +0300 Subject: [PATCH 046/203] [emails][puppeteer][fix] allow chrome to launch multiple instances --- CHANGELOG.md | 4 ++++ api/utils/render.js | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da41cd665e4..293247e692e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Enterprise Fixes: - [active-users] Fixed bug related to selecting calculation ranges. As a result, some dates were previously calculated on incomplete data set. +Fixes: +- [core] Allow chrome to launch multiple instances to fix blank dashboard emails + + ## Version 24.05.31 Fixes: - [feedback] Uniformize drawer internal name input texts diff --git a/api/utils/render.js b/api/utils/render.js index ee01d946878..e117df24556 100644 --- a/api/utils/render.js +++ b/api/utils/render.js @@ -25,6 +25,7 @@ var chromePath = ""; var countlyFs = require('./countlyFs'); var log = require('./log.js')('core:render'); var countlyConfig = require('./../config', 'dont-enclose'); +var fs = require('fs'); /** @@ -69,7 +70,7 @@ exports.renderView = function(options, cb) { }, args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'], ignoreHTTPSErrors: true, - userDataDir: pathModule.resolve(__dirname, "../../dump/chrome") + userDataDir: pathModule.resolve(__dirname, "../../dump/chrome/" + Date.now()) }; if (chromePath) { @@ -251,6 +252,9 @@ exports.renderView = function(options, cb) { await bodyHandle.dispose(); await browser.close(); + // Remove user data directory after use + fs.rmSync(settings.userDataDir, { recursive: true, force: true }); + var imageData = { image: image, path: path @@ -261,6 +265,8 @@ exports.renderView = function(options, cb) { catch (e) { log.e("Headless chrome browser error", e); await browser.close(); + // Remove user data directory after use + fs.rmSync(settings.userDataDir, { recursive: true, force: true }); return cb(e); } } From f3ee918131613e8f282206ee41bb97caa71aad3a Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Tue, 24 Jun 2025 14:41:34 +0700 Subject: [PATCH 047/203] Update changelog for v24.05.33 --- CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 755440847ec..fa60e4db4bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## Version 24.05.33 +Fixes: +- [emails] [puppeteer] [fix] Allow chrome to launch multiple instances + + ## Version 24.05.32 Enterprise Fixes: - [active-users] Fixed bug related to selecting calculation ranges. As a result, some dates were previously calculated on incomplete data set. @@ -32,7 +37,7 @@ Fixes: ## Version 24.05.28 Enterprise Fixes: - [cohorts] Fixed issue with combining multiple cohorts - + ## Version 24.05.27 Fixes: - [crashes] Remove memory addresses from stack trace grouping @@ -92,7 +97,7 @@ Enterprise fixes: - [drill] [license] Update license loader to enable supplying db client - [users] Format data points displayed in user sidebar - [cohorts] Unescape drill texts in cohort component - + Dependencies: - Bump fs-extra from 11.2.0 to 11.3.0 - Bump nodemailer from 6.9.16 to 6.10.0 From 60d9ae2234cdf9b03453fcd277fe0d008b5eaa86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=B1nar=20Gen=C3=A7?= Date: Mon, 7 Jul 2025 16:58:10 +0300 Subject: [PATCH 048/203] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa60e4db4bd..f16710b097b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Version 24.05.XX +Enterprise Fixes: +- [drill] Fixed typo issue while getting segment values in drill widgets + ## Version 24.05.33 Fixes: - [emails] [puppeteer] [fix] Allow chrome to launch multiple instances From 32555b1f2ea49c2b0487ebe9d4daf2d0079e1829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=B1nar=20Gen=C3=A7?= Date: Tue, 8 Jul 2025 14:34:35 +0300 Subject: [PATCH 049/203] Changelog update for v24.05.34 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f16710b097b..f0031ba0719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Version 24.05.XX +## Version 24.05.34 Enterprise Fixes: - [drill] Fixed typo issue while getting segment values in drill widgets From 7c591213bbab0c5aa1083b1a434b1c778c79d349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=B1nar=20Gen=C3=A7?= Date: Wed, 16 Jul 2025 17:29:59 +0300 Subject: [PATCH 050/203] fix: Delete widgets when a dashboard is removed --- CHANGELOG.md | 5 ++++ plugins/dashboards/api/api.js | 49 +++++++++++++++++++++++++---------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0031ba0719..6d629c01b2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## Version 24.05.XX +Fixes: + -[dashboards] Delete associated widgets and reports when a dashboard is removed + + ## Version 24.05.34 Enterprise Fixes: - [drill] Fixed typo issue while getting segment values in drill widgets diff --git a/plugins/dashboards/api/api.js b/plugins/dashboards/api/api.js index 6152d8010b3..3ebc8d9b3b6 100644 --- a/plugins/dashboards/api/api.js +++ b/plugins/dashboards/api/api.js @@ -1093,30 +1093,53 @@ plugins.setConfigs("dashboards", { filterCond.owner_id = memberId; } - common.db.collection("dashboards").findOne({_id: common.db.ObjectID(dashboardId)}, function(err, dashboard) { + common.db.collection("dashboards").findOne({_id: common.db.ObjectID(dashboardId)}, async function(err, dashboard) { if (err || !dashboard) { common.returnMessage(params, 400, "Dashboard with the given id doesn't exist"); } else { - hasViewAccessToDashboard(params.member, dashboard, function(er, status) { + hasViewAccessToDashboard(params.member, dashboard, async function(er, status) { if (er || !status) { return common.returnOutput(params, {error: true, dashboard_access_denied: true}); } - common.db.collection("dashboards").remove( - filterCond, - function(error, result) { - if (!error && result) { - if (result && result.result && result.result.n === 1) { - plugins.dispatch("/systemlogs", {params: params, action: "dashboard_deleted", data: dashboard}); + try { + // Remove the dashboard + const result = await common.db.collection("dashboards").deleteOne(filterCond); + + if (result && result.deletedCount > 0) { + // Collect widget IDs from the dashboard + const widgetIds = (dashboard.widgets || []).map(w => common.db.ObjectID(w.$oid || w)); + + // Delete widgets from the widgets collection + if (widgetIds.length) { + const widgets = await common.db.collection("widgets").find({_id: {$in: widgetIds}}).toArray(); + const drillReportIds = widgets.reduce((acc, widget) => { + if (Array.isArray(widget.drill_report)) { + acc.push(...widget.drill_report); + } + return acc; + }, []); + + await common.db.collection("widgets").deleteMany({_id: {$in: widgetIds}}); + + // Delete drill_reports from the long_tasks collection + if (drillReportIds.length) { + await common.db.collection("long_tasks").deleteMany({_id: {$in: drillReportIds}}); } - common.returnOutput(params, result); - } - else { - common.returnMessage(params, 500, "Failed to delete dashboard"); } + + plugins.dispatch("/systemlogs", {params: params, action: "dashboard_deleted", data: dashboard}); + common.returnOutput(params, result); } - ); + else { + common.returnMessage(params, 500, "Failed to delete dashboard"); + } + } + catch (error) { + console.error("Error during dashboard deletion:", error); + common.returnMessage(params, 500, "An error occurred while deleting the dashboard"); + } }); } }); From ec2d2eac12c2331eedc8368d37464395cdf15ca0 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Tue, 15 Jul 2025 11:53:58 +0700 Subject: [PATCH 051/203] [core] Fix mongo connection url parsing --- plugins/pluginManager.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/pluginManager.js b/plugins/pluginManager.js index 6684372c347..0fad86840d6 100644 --- a/plugins/pluginManager.js +++ b/plugins/pluginManager.js @@ -11,7 +11,6 @@ var pluginDependencies = require('./pluginDependencies.js'), apiCountlyConfig = require('../api/config', 'dont-enclose'), utils = require('../api/utils/utils.js'), fs = require('fs'), - url = require('url'), querystring = require('querystring'), cp = require('child_process'), async = require("async"), @@ -1869,9 +1868,12 @@ var pluginManager = function pluginManager() { } if (config && typeof config.mongodb === "string") { - var urlParts = url.parse(config.mongodb, true); - if (urlParts && urlParts.query && urlParts.query.maxPoolSize) { - maxPoolSize = urlParts.query.maxPoolSize; + const urlParts = config.mongodb.split('?'); + + if (urlParts.length > 1) { + const queryParams = new URLSearchParams(urlParts[1]); + + maxPoolSize = queryParams.get('maxPoolSize') || maxPoolSize; } } else { From c982e60daa185ca124026021e07a6b8e91c0a7c9 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Wed, 16 Jul 2025 09:33:47 +0700 Subject: [PATCH 052/203] [core] Fix mongo connection url parsing --- plugins/pluginManager.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/plugins/pluginManager.js b/plugins/pluginManager.js index 0fad86840d6..5b44727de75 100644 --- a/plugins/pluginManager.js +++ b/plugins/pluginManager.js @@ -1868,12 +1868,21 @@ var pluginManager = function pluginManager() { } if (config && typeof config.mongodb === "string") { - const urlParts = config.mongodb.split('?'); + try { + const urlObj = new URL(config.mongodb); + // mongo connection string with multiple host like 'mongodb://localhost:30000,localhost:30001' will cause an error + + maxPoolSize = urlObj.searchParams.get('maxPoolSize') !== null ? urlObj.searchParams.get('maxPoolSize') : maxPoolSize; + } + catch (_err) { + // we catch the error here and try to process only the query params part + const urlParts = config.mongodb.split('?'); - if (urlParts.length > 1) { - const queryParams = new URLSearchParams(urlParts[1]); + if (urlParts.length > 1) { + const queryParams = new URLSearchParams(urlParts[1]); - maxPoolSize = queryParams.get('maxPoolSize') || maxPoolSize; + maxPoolSize = queryParams.get('maxPoolSize') !== null ? queryParams.get('maxPoolSize') : maxPoolSize; + } } } else { From ee767f9c1de041d848489ca7050673f2fbbf7bcc Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Thu, 17 Jul 2025 13:19:15 +0700 Subject: [PATCH 053/203] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0031ba0719..b414d57862d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Version 24.05.XX +Fixes: +- [core] Fix mongo connection url parsing + ## Version 24.05.34 Enterprise Fixes: - [drill] Fixed typo issue while getting segment values in drill widgets From ce914459a7cff0f6d8c17b25e5926c0d33a9e371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=B1nar=20Gen=C3=A7?= Date: Mon, 21 Jul 2025 11:30:18 +0300 Subject: [PATCH 054/203] fix: Improve orphan widget and long_task cleanup logic Enhanced the script to accurately identify and delete only unreferenced long_tasks when removing orphan widgets, supporting multiple widget types and their report fields. Updated dashboard deletion API to ensure widgets and their linked long_tasks are deleted individually, and improved error handling for missing dashboards --- .../delete_widgets_of_deleted_dashboards.js | 180 ++++++++++++++++++ plugins/dashboards/api/api.js | 35 ++-- 2 files changed, 196 insertions(+), 19 deletions(-) create mode 100644 bin/scripts/fix-data/delete_widgets_of_deleted_dashboards.js diff --git a/bin/scripts/fix-data/delete_widgets_of_deleted_dashboards.js b/bin/scripts/fix-data/delete_widgets_of_deleted_dashboards.js new file mode 100644 index 00000000000..ed775703105 --- /dev/null +++ b/bin/scripts/fix-data/delete_widgets_of_deleted_dashboards.js @@ -0,0 +1,180 @@ +/** + * Description : Remove orphan widget and long_tasks records left behind after a dashboard is deleted + * Server : countly + * Path : $(countly dir)/bin/scripts/fix-data + * Command : node delete_widgets_of_deleted_dashboards.js [--dry-run] + * Usage : + * # Preview only + * node delete_widgets_of_deleted_dashboards.js --dry-run + * # Actual deletion + * node delete_widgets_of_deleted_dashboards.js + */ +const DRY_RUN = process.argv.includes('--dry-run'); + +const BATCH_SIZE = 1000; +const pluginManager = require('../../../plugins/pluginManager.js'); + +// Widget type configurations for long_tasks relationships +const WIDGET_LONG_TASK_CONFIG = { + 'drill': { + reportField: 'drill_report' + }, + 'users': { + reportField: 'drill_report' + }, + 'formulas': { + reportField: 'cmetrics' + } +}; + +/** + * Deletes documents in batches to avoid oversized commands + * @param {Object} db - MongoDB connection + * @param {String} collection - Collection name + * @param {Array} ids - List of document ids to delete + */ +async function deleteByChunks(db, collection, ids) { + let bucket = []; + + for (const id of ids) { + bucket.push(id); + + if (bucket.length === BATCH_SIZE) { + await runDelete(bucket); + bucket = []; + } + } + + if (bucket.length) { + await runDelete(bucket); + } + + /** + * Executes the delete operation for a batch of ids + * @param {Array} batch - Array of document ids to delete + * @returns {Promise} + * */ + async function runDelete(batch) { + if (DRY_RUN) { + console.log(`[dry-run] ${collection}: would delete ${batch.length}`); + } + else { + const res = await db.collection(collection).deleteMany({ _id: { $in: batch } }); + console.log(`[deleted] ${collection}: ${res.deletedCount}`); + } + } +} + +/** + * Counts references to reports and returns only unreferenced ones + * @param {Object} db - MongoDB connection + * @param {Array} reportIds - Report IDs to be checked + * @param {Array} excludeWidgetIds - Widget IDs to exclude from reference check + * @returns {Array} Unreferenced report IDs + */ +async function getUnreferencedReports(db, reportIds, excludeWidgetIds) { + if (!reportIds || !reportIds.length) { + return []; + } + + let referencedReports = []; + + // Check all widget types that can reference reports + for (const [widgetType, config] of Object.entries(WIDGET_LONG_TASK_CONFIG)) { + const query = { + widget_type: widgetType, + [config.reportField]: { $in: reportIds } + }; + + // Exclude orphan widgets from reference check + if (excludeWidgetIds.length) { + query._id = { $nin: excludeWidgetIds }; + } + + const widgets = await db.collection('widgets').find(query, { [config.reportField]: 1 }).toArray(); + + widgets.forEach(widget => { + const reports = widget[config.reportField] || []; + referencedReports.push(...reports.map(reportId => reportId.toString())); + }); + } + + // Return only those report IDs that are not referenced in any widget + return reportIds.filter(reportId => !referencedReports.includes(reportId.toString())); +} + +/** + * Collects all linked long_task IDs from a widget based on its type + * @param {Object} widget - Widget document + * @returns {Array} Array of long_task IDs + */ +function collectAllLinkedLongTasks(widget) { + const config = WIDGET_LONG_TASK_CONFIG[widget.widget_type]; + if (!config) { + return []; + } + + const reportField = config.reportField; + return Array.isArray(widget[reportField]) ? widget[reportField] : []; +} + + +(async() => { + const db = await pluginManager.dbConnection('countly'); + + try { + const dashboardWidgets = []; + + const dashCursor = db.collection('dashboards').find({widgets: {$exists: true, $not: {$size: 0}}}, {projection: {widgets: 1}}); + + while (await dashCursor.hasNext()) { + const dash = await dashCursor.next(); + for (const w of dash.widgets) { + const idStr = (w && w.$oid) ? w.$oid : (w + ''); + if (idStr && !dashboardWidgets.includes(idStr)) { + dashboardWidgets.push(idStr); + } + } + } + + await dashCursor.close(); + + const orphanWidgetIds = []; + const allLinkedLongTasks = []; + + const widgetCursor = db.collection('widgets').find({}); + + while (await widgetCursor.hasNext()) { + const w = await widgetCursor.next(); + if (!dashboardWidgets.includes(String(w._id))) { + orphanWidgetIds.push(w._id); + + // Find linked long_tasks based on widget type + const linkedTasks = collectAllLinkedLongTasks(w); + allLinkedLongTasks.push(...linkedTasks); + } + } + await widgetCursor.close(); + + console.log(`Orphan widgets found: ${orphanWidgetIds.length}`); + if (DRY_RUN && orphanWidgetIds.length) { + console.log('Orphan widget IDs to be deleted:', orphanWidgetIds.map(id => id.toString())); + } + await deleteByChunks(db, 'widgets', orphanWidgetIds); + + const unreferencedLongTasks = await getUnreferencedReports(db, allLinkedLongTasks, orphanWidgetIds); + console.log(`Unreferenced long_tasks to delete: ${unreferencedLongTasks.length}`); + if (DRY_RUN && unreferencedLongTasks.length) { + console.log('Unreferenced long_task IDs to be deleted:', unreferencedLongTasks.map(id => id.toString())); + } + await deleteByChunks(db, 'long_tasks', unreferencedLongTasks); + + console.log(DRY_RUN ? 'Dry-run finished' : 'Cleanup completed.'); + } + catch (err) { + console.error(err); + } + finally { + db.close(); + } +})(); \ No newline at end of file diff --git a/plugins/dashboards/api/api.js b/plugins/dashboards/api/api.js index 3ebc8d9b3b6..559f9dd3854 100644 --- a/plugins/dashboards/api/api.js +++ b/plugins/dashboards/api/api.js @@ -1104,31 +1104,28 @@ plugins.setConfigs("dashboards", { } try { - // Remove the dashboard - const result = await common.db.collection("dashboards").deleteOne(filterCond); + const dashboardToDelete = await common.db.collection("dashboards").findOne(filterCond); - if (result && result.deletedCount > 0) { - // Collect widget IDs from the dashboard - const widgetIds = (dashboard.widgets || []).map(w => common.db.ObjectID(w.$oid || w)); + if (!dashboardToDelete) { + return common.returnMessage(params, 404, "Dashboard not found"); + } - // Delete widgets from the widgets collection - if (widgetIds.length) { - const widgets = await common.db.collection("widgets").find({_id: {$in: widgetIds}}).toArray(); - const drillReportIds = widgets.reduce((acc, widget) => { - if (Array.isArray(widget.drill_report)) { - acc.push(...widget.drill_report); - } - return acc; - }, []); + // Collect widget IDs from the dashboard + const widgetIds = (dashboardToDelete.widgets || []).map(w => common.db.ObjectID(w.$oid || w)); - await common.db.collection("widgets").deleteMany({_id: {$in: widgetIds}}); + // Delete widgets and linked reports + for (const wid of widgetIds) { + const widget = await common.db.collection("widgets").findOneAndDelete({ _id: wid }); - // Delete drill_reports from the long_tasks collection - if (drillReportIds.length) { - await common.db.collection("long_tasks").deleteMany({_id: {$in: drillReportIds}}); - } + if (widget && widget.value) { + plugins.dispatch("/dashboard/widget/deleted", { params, widget: widget.value }); } + } + + // Remove the dashboard + const result = await common.db.collection("dashboards").deleteOne(filterCond); + if (result && result.deletedCount) { plugins.dispatch("/systemlogs", {params: params, action: "dashboard_deleted", data: dashboard}); common.returnOutput(params, result); } From 716f34837b2c34f264b669ae09b2fdc3edc68f59 Mon Sep 17 00:00:00 2001 From: Anna Sosina Date: Tue, 22 Jul 2025 16:27:54 +0300 Subject: [PATCH 055/203] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2681250c365..db03e0a44fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Fixes: - [core] Fix mongo connection url parsing - [dashboards] Delete associated widgets and reports when a dashboard is removed +Enterprise Fixes: +- [flows] Nullchecks on $size when calculating flows. ## Version 24.05.34 Enterprise Fixes: From 09e93a4a1e43e3df27b294cc2852e090aa01227c Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Pinto Date: Wed, 23 Jul 2025 10:22:15 +0100 Subject: [PATCH 056/203] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db03e0a44fc..920c7acdffa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Version 24.05.XX +## Version 24.05.35 Fixes: - [core] Fix mongo connection url parsing - [dashboards] Delete associated widgets and reports when a dashboard is removed From e1f586ea1a8800ea20ced8d4c1cdf2cf4b5cde82 Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Pinto Date: Wed, 23 Jul 2025 10:36:05 +0100 Subject: [PATCH 057/203] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 920c7acdffa..9d2a1fdf3b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ Fixes: - [core] Fix mongo connection url parsing - [dashboards] Delete associated widgets and reports when a dashboard is removed + Enterprise Fixes: -- [flows] Nullchecks on $size when calculating flows. +- [flows] Null checks on $size when calculating flows. ## Version 24.05.34 Enterprise Fixes: From 9e104ad9b78144bc548051ef3cdc7a86180435b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=B1nar=20Gen=C3=A7?= Date: Fri, 25 Jul 2025 16:09:59 +0300 Subject: [PATCH 058/203] feat: Add configuration warning tags to settings UI for v24.05 --- CHANGELOG.md | 4 + .../public/javascripts/countly.models.js | 234 ++++++++++++++++++ .../public/javascripts/countly.views.js | 12 + .../public/localization/plugins.properties | 7 + .../frontend/public/stylesheets/main.scss | 20 ++ .../public/templates/configurations.html | 31 ++- 6 files changed, 306 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d2a1fdf3b8..99251f54db2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +##Version 24.05.XX +Features: +- [plugins] Add configuration warning tags to settings UI + ## Version 24.05.35 Fixes: - [core] Fix mongo connection url parsing diff --git a/plugins/plugins/frontend/public/javascripts/countly.models.js b/plugins/plugins/frontend/public/javascripts/countly.models.js index 0b8bf80a186..feffedd6b0a 100644 --- a/plugins/plugins/frontend/public/javascripts/countly.models.js +++ b/plugins/plugins/frontend/public/javascripts/countly.models.js @@ -7,6 +7,7 @@ var _userConfigsData = {}; var _themeList = []; var _graph = {}; + var _configWarnings = null; //Public Methods countlyPlugins.initialize = function() { @@ -307,4 +308,237 @@ }); }; + // Warning types and their associated colors + var WARNING_TYPES = { + DATA_INGESTION: 'dataIngestion', + UI_FILTERING: 'uiFiltering', + SERVER_PERFORMANCE: 'serverPerformance' + // SECURITY_IMPACT: 'securityImpact' // for the 2nd phase + }; + + // Tooltip color mappings + var TOOLTIP_COLORS = { + dataIngestion: { bgColor: '#FCF5E5', textColor: '#E49700' }, + serverPerformance: { bgColor: '#FBECE5', textColor: '#D23F00' }, + uiFiltering: { bgColor: '#E1EFFF', textColor: '#0166D6' } + }; + + /** + * Helper function to create warning objects + * @param {string} type - Warning type + * @param {string} textKey - Warning text key + * @returns {Object} Warning object with type and text + */ + function createWarning(type, textKey) { + return { + type: type, + text: textKey + }; + } + + // Predefined warning combinations + var WARNING_COMBINATIONS = { + DATA_INGESTION: [ + createWarning(WARNING_TYPES.DATA_INGESTION, "configs.tooltip.data-ingestion-warning") + ], + UI_FILTERING: [ + createWarning(WARNING_TYPES.UI_FILTERING, "configs.tooltip.ui-filtering-warning") + ], + SERVER_PERFORMANCE: [ + createWarning(WARNING_TYPES.SERVER_PERFORMANCE, "configs.tooltip.server-performance-warning") + ] + }; + + /** + * Initialize configuration warnings + * @returns {Object} Configuration warnings map + */ + function initializeConfigWarnings() { + if (_configWarnings !== null) { + return _configWarnings; + } + + var configWarnings = { + // API Core Configurations + "api.trim_trailing_ending_spaces": [ + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "api.event_limit": [ + ...WARNING_COMBINATIONS.SERVER_PERFORMANCE, + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "api.event_segmentation_limit": [ + ...WARNING_COMBINATIONS.SERVER_PERFORMANCE, + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "api.event_segmentation_value_limit": [ + ...WARNING_COMBINATIONS.SERVER_PERFORMANCE, + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "api.metric_limit": [ + ...WARNING_COMBINATIONS.SERVER_PERFORMANCE, + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "api.session_duration_limit": [ + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "api.array_list_limit": [ + ...WARNING_COMBINATIONS.SERVER_PERFORMANCE, + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "api.city_data": [ + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "api.country_data": [ + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + + // Logging Configurations + "logs.default": WARNING_COMBINATIONS.UI_FILTERING, + + // Plugin-specific Configurations + "attribution.segment_value_limit": [ + ...WARNING_COMBINATIONS.SERVER_PERFORMANCE, + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + + "crashes.report_limit": [ + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "crashes.max_custom_field_keys": [ + ...WARNING_COMBINATIONS.SERVER_PERFORMANCE, + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "crashes.smart_regexes": WARNING_COMBINATIONS.UI_FILTERING, + + "drill.list_limit": [ + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "drill.custom_property_limit": [ + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "drill.projection_limit": [ + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "drill.big_list_limit": [ + ...WARNING_COMBINATIONS.SERVER_PERFORMANCE, + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + + "flows.maxDepth": [ + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "flows.nodesCn": [ + ...WARNING_COMBINATIONS.UI_FILTERING + ], + + "hooks.requestLimit": [ + ...WARNING_COMBINATIONS.DATA_INGESTION + ], + + "logger.limit": [ + ...WARNING_COMBINATIONS.SERVER_PERFORMANCE, + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "logger.state": [ + ...WARNING_COMBINATIONS.SERVER_PERFORMANCE, + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + + "remote-config.conditions_per_paramaeters": [ + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "remote-config.maximum_allowed_parameters": [ + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + + "sources.sources_length_limit": [ + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + + "users.custom_prop_limit": [ + ...WARNING_COMBINATIONS.SERVER_PERFORMANCE, + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "users.custom_set_limit": [ + ...WARNING_COMBINATIONS.SERVER_PERFORMANCE, + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + + "views.segment_limit": [ + ...WARNING_COMBINATIONS.SERVER_PERFORMANCE, + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "views.segment_value_limit": [ + ...WARNING_COMBINATIONS.SERVER_PERFORMANCE, + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "views.view_limit": [ + ...WARNING_COMBINATIONS.SERVER_PERFORMANCE, + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "views.view_name_limit": [ + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + + "data-manager.globalValidationAction": [ + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "data-manager.segmentLevelValidationAction": [ + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ], + "data-manager.enableDataMasking": [ + ...WARNING_COMBINATIONS.DATA_INGESTION, + ...WARNING_COMBINATIONS.UI_FILTERING + ] + }; + + _configWarnings = configWarnings; + return _configWarnings; + } + + countlyPlugins.getConfigWarnings = function(configGroup, key) { + var warnings = initializeConfigWarnings(); + var mapKey = configGroup + "." + key; + return warnings[mapKey] || []; + }; + + countlyPlugins.getTooltipColors = function() { + return TOOLTIP_COLORS; + }; + + countlyPlugins.getTooltipLabel = function(type) { + var labels = { + dataIngestion: CV.i18n('configs.tooltip.data-ingestion'), + uiFiltering: CV.i18n('configs.tooltip.ui-filtering'), + serverPerformance: CV.i18n('configs.tooltip.server-performance') + }; + return labels[type] || 'Unknown Type'; + }; + }(window.countlyPlugins = window.countlyPlugins || {}, jQuery)); diff --git a/plugins/plugins/frontend/public/javascripts/countly.views.js b/plugins/plugins/frontend/public/javascripts/countly.views.js index b1ddb453b0b..1958bf3855d 100644 --- a/plugins/plugins/frontend/public/javascripts/countly.views.js +++ b/plugins/plugins/frontend/public/javascripts/countly.views.js @@ -534,6 +534,18 @@ getConfigType: function(id) { return this.coreDefaults.includes(id) ? "Core" : "Plugins"; }, + getWarningTags(configGroup, key) { + var warnings = countlyPlugins.getConfigWarnings(configGroup, key); + var tooltipColors = countlyPlugins.getTooltipColors(); + return warnings.map(function(warning) { + return { + tooltipText: CV.i18n(warning.text), + bgColor: tooltipColors[warning.type].bgColor, + textColor: tooltipColors[warning.type].textColor, + label: countlyPlugins.getTooltipLabel(warning.type) + }; + }); + }, checkIfOverwritten: function(id, ns) { ns = ns || this.selectedConfig; var configsData = countlyPlugins.getConfigsData(); diff --git a/plugins/plugins/frontend/public/localization/plugins.properties b/plugins/plugins/frontend/public/localization/plugins.properties index a802c965699..df7cde798ea 100644 --- a/plugins/plugins/frontend/public/localization/plugins.properties +++ b/plugins/plugins/frontend/public/localization/plugins.properties @@ -241,5 +241,12 @@ configs.help.push-proxyuser = (if needed) Username for proxy server HTTP Basic a configs.help.push-proxypass = (if needed) Password for proxy server HTTP Basic authentication configs.help.push-proxyunauthorized = (if needed) Allow self signed certificates without CA installed +configs.tooltip.server-performance-warning = Adjusting this value may impact system performance +configs.tooltip.data-ingestion-warning = Adjusting this value may affect data processing +configs.tooltip.ui-filtering-warning = Adjusting this value controls how much data is shown in the UI +configs.tooltip.data-ingestion = Data Processing +configs.tooltip.ui-filtering = Interface +configs.tooltip.server-performance = Performance + systemlogs.action.change_configs = Setting Changed systemlogs.action.change_plugins = Plugins Changed \ No newline at end of file diff --git a/plugins/plugins/frontend/public/stylesheets/main.scss b/plugins/plugins/frontend/public/stylesheets/main.scss index 808c8c2a2bd..df5c9b43d8b 100644 --- a/plugins/plugins/frontend/public/stylesheets/main.scss +++ b/plugins/plugins/frontend/public/stylesheets/main.scss @@ -73,6 +73,26 @@ } } +.config-section { + &__header { + width: max-content; + } +} + +.configuration-warning-container { + border-radius: 4px; + display: flex; + height: 20px; + width: auto; + + &__text { + font-size: 10px; + font-weight: 500; + margin: 4px 6px; + text-transform: uppercase; + } +} + .apikey { .el-input { width: 320px; diff --git a/plugins/plugins/frontend/public/templates/configurations.html b/plugins/plugins/frontend/public/templates/configurations.html index 457baeecbd6..f9600f38cf3 100755 --- a/plugins/plugins/frontend/public/templates/configurations.html +++ b/plugins/plugins/frontend/public/templates/configurations.html @@ -49,8 +49,35 @@

-

{{getLabelName(key)}}

-

+
+
+ {{ getLabelName(key) }} +
+
+
+
+ + {{ tag.label }} + +
+
+
+
+

From cbc3cfdfe04003d559a70c58e027be66f6de932b Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Pinto Date: Wed, 30 Jul 2025 10:24:43 +0100 Subject: [PATCH 059/203] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99251f54db2..f89c7269dfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -##Version 24.05.XX +## Version 24.05.36 Features: - [plugins] Add configuration warning tags to settings UI From 79ac343783d6fd0b89cb9b911c9340095b40d6b7 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Fri, 29 Aug 2025 10:01:45 +0700 Subject: [PATCH 060/203] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f89c7269dfd..236b3c472c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## Version 24.05.X +Enterprise Fixes: +- [license] Update license metric endpoint permission + + ## Version 24.05.36 Features: - [plugins] Add configuration warning tags to settings UI From 3957be82f1a34d3e94e0f5844142539e0f9def3d Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Pinto Date: Wed, 3 Sep 2025 14:12:22 +0100 Subject: [PATCH 061/203] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 236b3c472c6..4abcf33cd12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Version 24.05.X +## Version 24.05.37 Enterprise Fixes: - [license] Update license metric endpoint permission From f1360c033d877bffd8cc5ca00abb220abdba440f Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Tue, 9 Sep 2025 14:31:54 +0100 Subject: [PATCH 062/203] fix: call render function from backend --- api/utils/requestProcessor.js | 40 +++++++++++++++++ frontend/express/app.js | 43 ------------------- .../core/home/javascripts/countly.models.js | 9 ++-- .../core/home/javascripts/countly.views.js | 2 +- .../javascripts/countly/countly.helpers.js | 7 ++- 5 files changed, 52 insertions(+), 49 deletions(-) diff --git a/api/utils/requestProcessor.js b/api/utils/requestProcessor.js index 92c574288c3..c6ca535660d 100644 --- a/api/utils/requestProcessor.js +++ b/api/utils/requestProcessor.js @@ -24,6 +24,7 @@ const validateUserForGlobalAdmin = validateGlobalAdmin; const validateUserForMgmtReadAPI = validateUser; const request = require('countly-request')(plugins.getConfig("security")); const Handle = require('../../api/parts/jobs/index.js'); +const render = require('../../api/utils/render.js'); var loaded_configs_time = 0; @@ -466,6 +467,45 @@ const processRequest = (params) => { } break; } + case '/o/render': { + validateUserForRead(params, function() { + var options = {}; + var view = params.qstring.view || ""; + var route = params.qstring.route || ""; + var id = params.qstring.id || ""; + + options.view = view + "#" + route; + options.id = id ? "#" + id : ""; + + var imageName = "screenshot_" + common.crypto.randomBytes(16).toString("hex") + ".png"; + + options.savePath = path.resolve(__dirname, "../../frontend/express/public/images/screenshots/" + imageName); + options.source = "core"; + + authorize.save({ + db: common.db, + multi: false, + owner: params.member._id, + ttl: 300, + purpose: "LoginAuthToken", + callback: function(err2, token) { + if (err2) { + common.returnMessage(params, 400, 'Error creating token: ' + err2); + return false; + } + options.token = token; + render.renderView(options, function(err3) { + if (err3) { + common.returnMessage(params, 400, 'Error creating screenshot: ' + err3); + return false; + } + common.returnOutput(params, {path: common.config.path + "/images/screenshots/" + imageName}); + }); + } + }); + }); + break; + } case '/i/app_users': { switch (paths[3]) { case 'create': { diff --git a/frontend/express/app.js b/frontend/express/app.js index 4af7bd9c927..60540752e2e 100644 --- a/frontend/express/app.js +++ b/frontend/express/app.js @@ -52,7 +52,6 @@ var versionInfo = require('./version.info'), url = require('url'), authorize = require('../../api/utils/authorizer.js'), //for token validations languages = require('../../frontend/express/locale.conf'), - render = require('../../api/utils/render.js'), rateLimit = require("express-rate-limit"), membersUtility = require("./libs/members.js"), argon2 = require('argon2'), @@ -1831,48 +1830,6 @@ Promise.all([plugins.dbConnection(countlyConfig), plugins.dbConnection("countly_ } }); - app.get(countlyConfig.path + '/render', function(req, res) { - if (!req.session.uid) { - return res.redirect(countlyConfig.path + '/login'); - } - - var options = {}; - var view = req.query.view || ""; - var route = req.query.route || ""; - var id = req.query.id || ""; - - options.view = view + "#" + route; - options.id = id ? "#" + id : ""; - - var randomString = (+new Date()).toString() + (Math.random()).toString(); - var imageName = "screenshot_" + sha1Hash(randomString) + ".png"; - - options.savePath = path.resolve(__dirname, "./public/images/screenshots/" + imageName); - options.source = "core"; - - authorize.save({ - db: countlyDb, - multi: false, - owner: req.session.uid, - ttl: 300, - purpose: "LoginAuthToken", - callback: function(err2, token) { - if (err2) { - console.log(err2); - return res.send(false); - } - options.token = token; - render.renderView(options, function(err3) { - if (err3) { - return res.send(false); - } - - return res.send({path: countlyConfig.path + "/images/screenshots/" + imageName}); - }); - } - }); - }); - app.get(countlyConfig.path + '/login/token/:token', function(req, res) { membersUtility.loginWithToken(req, function(member) { if (member) { diff --git a/frontend/express/public/core/home/javascripts/countly.models.js b/frontend/express/public/core/home/javascripts/countly.models.js index 96ef78c8489..4f514a35374 100644 --- a/frontend/express/public/core/home/javascripts/countly.models.js +++ b/frontend/express/public/core/home/javascripts/countly.models.js @@ -1,4 +1,4 @@ -/* global countlyVue,CV,countlyCommon*/ +/* global countlyVue,CV,countlyCommon,countlyGlobal*/ (function(countlyHomeView) { countlyHomeView.getVuexModule = function() { @@ -12,11 +12,12 @@ var HomeViewActions = { downloadScreen: function(context) { return CV.$.ajax({ - type: "GET", - url: "/render?view=/dashboard&route=/" + countlyCommon.ACTIVE_APP_ID + "/", + type: "POST", + url: "/o/render?view=/dashboard&route=/" + countlyCommon.ACTIVE_APP_ID + "/", data: { app_id: countlyCommon.ACTIVE_APP_ID, "id": "main_home_view", + api_key: countlyGlobal.member.api_key, options: JSON.stringify({"dimensions": {"width": 2000, "padding": 25}}) }, dataType: "json" @@ -92,4 +93,4 @@ mutations: HomeViewMutations }); }; -}(window.countlyHomeView = window.countlyHomeView || {})); \ No newline at end of file +}(window.countlyHomeView = window.countlyHomeView || {})); diff --git a/frontend/express/public/core/home/javascripts/countly.views.js b/frontend/express/public/core/home/javascripts/countly.views.js index 35d099f98e5..ccc449f9a94 100644 --- a/frontend/express/public/core/home/javascripts/countly.views.js +++ b/frontend/express/public/core/home/javascripts/countly.views.js @@ -222,7 +222,7 @@ var HomeViewView = countlyVue.views.create({ CountlyHelpers.notify({type: "ok", title: jQuery.i18n.map["common.success"], message: "Starting the image generation process. You will be notified when it is ready. Please do not leave the website while the process is running.", sticky: true, clearAll: true}); this.$store.dispatch("countlyHomeView/downloadScreen").then(function() { if (self.$store.state.countlyHomeView.image) { - CountlyHelpers.notify({type: "ok", title: jQuery.i18n.map["common.success"], message: "Download", sticky: true, clearAll: true}); + CountlyHelpers.notify({type: "ok", title: jQuery.i18n.map["common.success"], message: "" + jQuery.i18n.map["common.download"] + "", sticky: true, clearAll: true, html: true}); } else { CountlyHelpers.notify({type: "error", title: jQuery.i18n.map["common.success"], message: "ERROR", sticky: false, clearAll: true}); diff --git a/frontend/express/public/javascripts/countly/countly.helpers.js b/frontend/express/public/javascripts/countly/countly.helpers.js index 5161077a326..9ccb6c0c10a 100644 --- a/frontend/express/public/javascripts/countly/countly.helpers.js +++ b/frontend/express/public/javascripts/countly/countly.helpers.js @@ -337,7 +337,12 @@ } var payload = {}; var persistent = msg.persistent; - payload.text = countlyCommon.encodeHtml(msg.message); + if (msg.html) { + payload.text = msg.message; + } + else { + payload.text = countlyCommon.encodeHtml(msg.message); + } payload.autoHide = !msg.sticky; payload.id = msg.id; payload.width = msg.width; From a37ff935c708d1eb92e78be2d80717b58bc43907 Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Pinto Date: Tue, 9 Sep 2025 15:35:43 +0100 Subject: [PATCH 063/203] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4abcf33cd12..81f706ecf9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Version 24.05.38 +Enterprise Fixes: +- [home] Fix home download render issue + ## Version 24.05.37 Enterprise Fixes: - [license] Update license metric endpoint permission From 0c10837de3c13e3dc592c89d85986b8fb71f46b1 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Thu, 18 Sep 2025 12:39:37 +0700 Subject: [PATCH 064/203] Add semver lib --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index a4374fb4979..88708c04ab7 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "properties-parser": "0.6.0", "puppeteer": "^23.8.0", "sass": "1.83.4", + "semver": "^7.7.1", "tslib": "^2.6.3", "uglify-js": "3.19.3", "underscore": "1.13.7", From 48cbbf369c9d616a94085250c9bbf672a269ae4e Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Tue, 8 Jul 2025 17:10:51 +0700 Subject: [PATCH 065/203] [remote-config] Enable app version semver comparison --- plugins/remote-config/api/parts/rc.js | 33 +++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/plugins/remote-config/api/parts/rc.js b/plugins/remote-config/api/parts/rc.js index 5c04137ea9a..65ec7eb11a6 100644 --- a/plugins/remote-config/api/parts/rc.js +++ b/plugins/remote-config/api/parts/rc.js @@ -2,6 +2,7 @@ * Fetching and processing data for remote config * @module plugins/remote-config/api/parts/data/rc */ +const semver = require('semver'); var prng = require('../../../../api/utils/random-sfc32.js'); var globalSeed = "Countly_is_awesome"; @@ -38,7 +39,16 @@ remoteConfig.processFilter = function(user, query) { } if (parts[0] !== "chr") { - if (typeof (value) !== "undefined") { + if (prop === 'up.av') { + if ('av' in user) { + hasValue = true; + queryStatus = queryStatus && processAppVersionValues(user.av, query, prop); + } + else { + hasValue = false; + } + } + else if (typeof (value) !== "undefined") { hasValue = true; queryStatus = queryStatus && processPropertyValues(value, query, prop); } @@ -111,6 +121,25 @@ function processPropertyValues(value, query, prop) { return status; } +/** + * Function to process query property value + * @param {Object} inpUserAv - user app version + * @param {Object} query - filter + * @param {String} prop - query property + * @returns {Boolean} property value status + */ +function processAppVersionValues(inpUserAv, query, prop) { + const userAv = inpUserAv.replace(/:/g, '.'); + const filterType = Object.keys(query[prop])[0]; + const targetAv = query[prop] && query[prop][filterType] && query[prop][filterType].replace(/:/g, '.'); + + if (!semver.valid(userAv) || !semver.valid(targetAv)) { + return false; + } + + return semver[filterType.slice(1)](userAv, targetAv); +} + /** * Function to process query cohort value * @param {Object} user - user data @@ -162,4 +191,4 @@ function processCohortValues(user, query) { return status; } -module.exports = remoteConfig; \ No newline at end of file +module.exports = remoteConfig; From 8416e73815eec602639e30d926c846d08361295e Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Tue, 8 Jul 2025 17:10:58 +0700 Subject: [PATCH 066/203] [remote-config] Add app version comparison test --- .../tests/fetch_remote_config.js | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/plugins/remote-config/tests/fetch_remote_config.js b/plugins/remote-config/tests/fetch_remote_config.js index a4bd77239c4..18e7592536f 100644 --- a/plugins/remote-config/tests/fetch_remote_config.js +++ b/plugins/remote-config/tests/fetch_remote_config.js @@ -188,7 +188,7 @@ describe('Fetch remote config', () => { .expect('Content-Type', /json/); }); - it('Should match targetted user', () => { + it('Should match targetted user (device id)', () => { const targettedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', uid: '13', @@ -199,7 +199,7 @@ describe('Fetch remote config', () => { should(remoteConfig.processFilter(targettedUser, query)).equal(true); }); - it('Should match targetted user', () => { + it('Should match targetted user (country)', () => { const targettedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', uid: '13', @@ -211,6 +211,42 @@ describe('Fetch remote config', () => { should(remoteConfig.processFilter(targettedUser, query)).equal(true); }); + it('Should match targetted user (app version)', () => { + const targettedUser = { + _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', + uid: '13', + did: 'targetted_user', + av: '1:0:0', + }; + const queryGt = { 'up.av': { $gt: '0:0:0' } }; + const queryGte = { 'up.av': { $gte: '1:0:0' } }; + const queryLt = { 'up.av': { $lt: '2:0:0' } }; + const queryLte = { 'up.av': { $lte: '1:0:0' } }; + + should(remoteConfig.processFilter(targettedUser, queryGt)).equal(true); + should(remoteConfig.processFilter(targettedUser, queryGte)).equal(true); + should(remoteConfig.processFilter(targettedUser, queryLt)).equal(true); + should(remoteConfig.processFilter(targettedUser, queryLte)).equal(true); + }); + + it('Should not match non targetted user (app version)', () => { + const nonTargettedUser = { + _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', + uid: '13', + did: 'targetted_user', + av: '1:0:0', + }; + const queryGt = { 'up.av': { $gt: '1:0:0' } }; + const queryGte = { 'up.av': { $gte: '2:0:0' } }; + const queryLt = { 'up.av': { $lt: '1:0:0' } }; + const queryLte = { 'up.av': { $lte: '0:0:0' } }; + + should(remoteConfig.processFilter(nonTargettedUser, queryGt)).equal(false); + should(remoteConfig.processFilter(nonTargettedUser, queryGte)).equal(false); + should(remoteConfig.processFilter(nonTargettedUser, queryLt)).equal(false); + should(remoteConfig.processFilter(nonTargettedUser, queryLte)).equal(false); + }); + it('Should not match non targetted user', () => { const nonTargettedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', From bb40a43c2983fce146fcdbbbea03603eda4ce1c5 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Tue, 8 Jul 2025 17:11:04 +0700 Subject: [PATCH 067/203] [remote-config] Update query matcher to enable complex conditions --- plugins/remote-config/api/parts/rc.js | 113 +++++++-------- .../tests/fetch_remote_config.js | 129 +++++++++++++++++- 2 files changed, 182 insertions(+), 60 deletions(-) diff --git a/plugins/remote-config/api/parts/rc.js b/plugins/remote-config/api/parts/rc.js index 65ec7eb11a6..04763156c1c 100644 --- a/plugins/remote-config/api/parts/rc.js +++ b/plugins/remote-config/api/parts/rc.js @@ -10,77 +10,79 @@ var globalSeed = "Countly_is_awesome"; var remoteConfig = {}; /** - * Function to check if the given query would match the given user - * @param {Object} user - user - * @param {Object} query - query + * Function to check if the given query matches the given user + * @param {Object} inpUser - user object + * @param {Object} inpQuery - condition query * @returns {Boolean} true if the query matches the user */ -remoteConfig.processFilter = function(user, query) { - var queryStatus = false, isCohort = false, hasValue = false; - - if (Object.keys(query).length) { - queryStatus = true; - - for (var prop in query) { - var parts = prop.split("."); - var value; - - if (parts[0] === "up" || parts.length === 1) { - var p = parts[0]; - if (p === "up") { - p = parts[1]; - } - if (user[p]) { - value = user[p]; - } +remoteConfig.processFilter = function(inpUser, inpQuery) { + /** + * Inner function of processFilter for recursion + * @param {Object} user - user object + * @param {Object} query - condition query + * @returns {Boolean} true if the query matches the user + */ + function matchesQuery(user, query) { + for (let key in query) { + if (key === '$or') { + return query[key].some((subQuery) => matchesQuery(user, subQuery)); } - else if (user[parts[0]] && user[parts[0]][parts[1]]) { - value = user[parts[0]][parts[1]]; + else if (key === '$and') { + return query[key].every((subQuery) => matchesQuery(user, subQuery)); } - - if (parts[0] !== "chr") { - if (prop === 'up.av') { - if ('av' in user) { - hasValue = true; - queryStatus = queryStatus && processAppVersionValues(user.av, query, prop); + else if (typeof query[key] === 'object' && query[key] !== null && !Array.isArray(query[key])) { + let qResult = true; + + for (let prop in query) { + let parts = prop.split("."); + let value; + + if (parts[0] === "up" || parts.length === 1) { + var p = parts[0]; + if (p === "up") { + p = parts[1]; + } + if (user[p]) { + value = user[p]; + } } - else { - hasValue = false; + else if (user[parts[0]] && user[parts[0]][parts[1]]) { + value = user[parts[0]][parts[1]]; } - } - else if (typeof (value) !== "undefined") { - hasValue = true; - queryStatus = queryStatus && processPropertyValues(value, query, prop); - } - else { - //If the type of the user prop is undefined, set query status to false, since data is not available - //In such cases only process if $nin is present otherwise we show the default value to the user - if (query[prop].$nin) { - hasValue = true; - queryStatus = queryStatus && processPropertyValues(value, query, prop); + + if (parts[0] !== 'chr') { + if (prop === 'up.av') { + if ('av' in user) { + qResult = qResult && processAppVersionValues(user.av, { [prop]: query[prop] }, prop); + } + } + else if (typeof (value) !== 'undefined') { + qResult = qResult && processPropertyValues(value, { [prop]: query[prop] }, prop); + } + else { + //If data is not available, check for $nin and $exists operator since they can be true + if (query[prop] && (query[prop].$nin || '$exists' in query[prop])) { + qResult = qResult && processPropertyValues(value, { [prop]: query[prop] }, prop); + } // Otherwise return false + else { + qResult = false; + } + } } else { - queryStatus = false; + qResult = qResult && processCohortValues(user, { chr: query[prop] }); } } + + return qResult; } else { - hasValue = true; - isCohort = true; + return false; } } - - if (isCohort) { - queryStatus = queryStatus && processCohortValues(user, query); - } - - if (!hasValue) { - //If the user does not have any user prop value, set query status to false, since data is not available - queryStatus = false; - } } - return queryStatus; + return matchesQuery(inpUser, inpQuery); }; /** @@ -115,6 +117,7 @@ function processPropertyValues(value, query, prop) { case "$lte": status = status && value <= query[prop].$lte; break; case "$regex": status = status && query[prop].$regex.test(value); break; case "$not": status = status && !query[prop].$not.test(value); break; + case '$exists': status = status && (query[prop].$exists === (value !== undefined)); break; } } diff --git a/plugins/remote-config/tests/fetch_remote_config.js b/plugins/remote-config/tests/fetch_remote_config.js index 18e7592536f..d5a0b7259b6 100644 --- a/plugins/remote-config/tests/fetch_remote_config.js +++ b/plugins/remote-config/tests/fetch_remote_config.js @@ -199,6 +199,17 @@ describe('Fetch remote config', () => { should(remoteConfig.processFilter(targettedUser, query)).equal(true); }); + it('Should not match non targetted user (device id)', () => { + const nonTargettedUser = { + _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', + uid: '13', + did: 'non_targetted_user', + }; + const query = { did: { $in: ['targetted_user'] } }; + + should(remoteConfig.processFilter(nonTargettedUser, query)).equal(false); + }); + it('Should match targetted user (country)', () => { const targettedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', @@ -247,15 +258,123 @@ describe('Fetch remote config', () => { should(remoteConfig.processFilter(nonTargettedUser, queryLte)).equal(false); }); - it('Should not match non targetted user', () => { - const nonTargettedUser = { + it('Should match targetted user ($and query)', () => { + const targettedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', uid: '13', - did: 'non_targetted_user', + did: 'targetted_user', + cc: 'UK', + }; + const query = { 'up.cc': { $exists: true }, did: { $in: ['targetted_user'] } }; + const altQuery = { + $and: [ + { 'up.cc': { $exists: true } }, + { did: { $in: ['targetted_user'] } }, + ], }; - const query = { did: { $in: ['targetted_user'] } }; - should(remoteConfig.processFilter(nonTargettedUser, query)).equal(false); + should(remoteConfig.processFilter(targettedUser, query)).equal(true); + should(remoteConfig.processFilter(targettedUser, altQuery)).equal(true); + }); + + it('Should not match targetted user ($and query)', () => { + const targettedUser = { + _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', + uid: '13', + did: 'targetted_user', + cc: 'UK', + }; + const query = { 'up.cc': { $exists: true }, did: { $nin: ['targetted_user'] } }; + const altQuery = { + $and: [ + { 'up.cc': { $exists: true } }, + { did: { $nin: ['targetted_user'] } }, + ], + }; + + should(remoteConfig.processFilter(targettedUser, query)).equal(false); + should(remoteConfig.processFilter(targettedUser, altQuery)).equal(false); + }); + + it('Should match targetted user ($or query)', () => { + const targettedUser = { + _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', + uid: '13', + did: 'targetted_user', + cc: 'UK', + }; + const query = { + $or: [ + { 'up.cc': { $exists: true }}, + { did: { $nin: ['targetted_user'] } }, + ], + }; + + should(remoteConfig.processFilter(targettedUser, query)).equal(true); + }); + + it('Should not match targetted user ($or query)', () => { + const targettedUser = { + _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', + uid: '13', + did: 'targetted_user', + cc: 'UK', + }; + const query = { + $or: [ + { 'up.cc': { $exists: false }}, + { did: { $nin: ['targetted_user'] } }, + ], + }; + + should(remoteConfig.processFilter(targettedUser, query)).equal(false); + }); + + it('Should match targetted user (combination query)', () => { + const targettedUser = { + _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', + uid: '13', + did: 'targetted_user', + cc: 'UK', + chr: { + 'chr_id': { + i: 123, + in: 'true', + }, + }, + }; + const query = { + $or: [ + { 'up.cc': { $in: ['UK'] }, chr: { $in: ['chr_id'] } }, + { did: { $nin: ['targetted_user'] } }, + ], + }; + + should(remoteConfig.processFilter(targettedUser, query)).equal(true); + }); + + it('Should not match targetted user (combination query)', () => { + const targettedUser = { + _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', + uid: '13', + did: 'targetted_user', + cc: 'UK', + av: '1:0:0', + chr: { + 'chr_id': { + i: 123, + in: 'true', + }, + }, + }; + const query = { + $or: [ + { 'up.cc': { $nin: ['UK'] }, chr: { $nin: ['chr_id'] } }, + { did: { $nin: ['targetted_user'] }, 'up.av': { $gt: '2:0:0' } }, + ], + }; + + should(remoteConfig.processFilter(targettedUser, query)).equal(false); }); it('Should fetch remote config with default value', async() => { From 767e15ab9d989dbba7c90b0f3095d156b31fc343 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Tue, 8 Jul 2025 20:50:57 +0700 Subject: [PATCH 068/203] [remote-config] Update condition query matcher --- plugins/remote-config/api/parts/rc.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/remote-config/api/parts/rc.js b/plugins/remote-config/api/parts/rc.js index 04763156c1c..9e155029f0b 100644 --- a/plugins/remote-config/api/parts/rc.js +++ b/plugins/remote-config/api/parts/rc.js @@ -51,21 +51,21 @@ remoteConfig.processFilter = function(inpUser, inpQuery) { } if (parts[0] !== 'chr') { - if (prop === 'up.av') { - if ('av' in user) { + if (typeof (value) !== 'undefined') { + if (prop === 'up.av') { qResult = qResult && processAppVersionValues(user.av, { [prop]: query[prop] }, prop); } - } - else if (typeof (value) !== 'undefined') { - qResult = qResult && processPropertyValues(value, { [prop]: query[prop] }, prop); + else { + qResult = qResult && processPropertyValues(value, { [prop]: query[prop] }, prop); + } } else { //If data is not available, check for $nin and $exists operator since they can be true - if (query[prop] && (query[prop].$nin || '$exists' in query[prop])) { + if (query[prop] && ('$nin' in query[prop] || '$exists' in query[prop])) { qResult = qResult && processPropertyValues(value, { [prop]: query[prop] }, prop); } // Otherwise return false else { - qResult = false; + qResult = qResult && false; } } } From 1bcfe41c474a2fbc6d7ae8dc62ad0c858d9772b5 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Thu, 10 Jul 2025 22:29:48 +0700 Subject: [PATCH 069/203] [remote-config] Modify app version property type in qb --- .../frontend/public/javascripts/countly.views.js | 7 ++++--- .../frontend/public/templates/condition-dialog.html | 13 +++++++++++-- .../public/templates/conditions-drawer.html | 1 + 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/plugins/remote-config/frontend/public/javascripts/countly.views.js b/plugins/remote-config/frontend/public/javascripts/countly.views.js index 60a9a275afe..df2ca57b399 100644 --- a/plugins/remote-config/frontend/public/javascripts/countly.views.js +++ b/plugins/remote-config/frontend/public/javascripts/countly.views.js @@ -211,7 +211,8 @@ value: 1, label: "#6C47FF" }, - colorTag: COLOR_TAG + colorTag: COLOR_TAG, + modifyPropType: { 'up.av': countlyQueryBuilder.PropertyType.NUMERIC_STRING_LIST }, }; }, computed: { @@ -631,7 +632,6 @@ name: CV.i18n("remote-config.conditions.random.percentile"), type: countlyQueryBuilder.PropertyType.NUMBER, group: 'User Properties', - })); return { remoteConfigFilterRules: remoteConfigFilterRules, @@ -646,7 +646,8 @@ value: 1, label: "#6C47FF" }, - colorTag: COLOR_TAG + colorTag: COLOR_TAG, + modifyPropType: { 'up.av': countlyQueryBuilder.PropertyType.NUMERIC_STRING_LIST }, }; }, methods: { diff --git a/plugins/remote-config/frontend/public/templates/condition-dialog.html b/plugins/remote-config/frontend/public/templates/condition-dialog.html index fcd12ac7355..032a6a798b8 100644 --- a/plugins/remote-config/frontend/public/templates/condition-dialog.html +++ b/plugins/remote-config/frontend/public/templates/condition-dialog.html @@ -1,5 +1,13 @@ - + @@ -71,6 +79,7 @@

{{i18n('remote-config.parameter.conditions.add.n :add-empty-row-on-empty-query="true" :allow-breakdown="false" :orGroupsEnabled="true" + :modifyPropType="modifyPropType" show-in-the-last-minutes show-in-the-last-hours v-model="managedPropertySegmentation"> diff --git a/plugins/remote-config/frontend/public/templates/conditions-drawer.html b/plugins/remote-config/frontend/public/templates/conditions-drawer.html index a5c043dc759..54923d9b544 100644 --- a/plugins/remote-config/frontend/public/templates/conditions-drawer.html +++ b/plugins/remote-config/frontend/public/templates/conditions-drawer.html @@ -66,6 +66,7 @@ :add-empty-row-on-empty-query="true" :allow-breakdown="false" :orGroupsEnabled="true" + :modifyPropType="modifyPropType" show-in-the-last-minutes show-in-the-last-hours v-model="managedPropertySegmentation"> From ad9ea3f131244f81b42c798cf386988198d26638 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Fri, 11 Jul 2025 13:36:10 +0700 Subject: [PATCH 070/203] [remote-config] Update tests --- .../tests/fetch_remote_config.js | 134 +++++++++--------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/plugins/remote-config/tests/fetch_remote_config.js b/plugins/remote-config/tests/fetch_remote_config.js index d5a0b7259b6..c5717c32257 100644 --- a/plugins/remote-config/tests/fetch_remote_config.js +++ b/plugins/remote-config/tests/fetch_remote_config.js @@ -13,7 +13,7 @@ const AMOUNT_OF_KEYS = 5; const PARAMETER_PREFIX = 'fetch_remote_config_param_'; const CONDITION_PREFIX = 'fetchremoteconfigcond'; const VALUE_PREFIX = 'value_'; -const TARGETTED_USER_ID = 'targetted_user'; +const TARGETED_USER_ID = 'targeted_user'; describe('Fetch remote config', () => { before(async() => { @@ -136,7 +136,7 @@ describe('Fetch remote config', () => { } }); - describe('Targetting', () => { + describe('Targeting', () => { before(async() => { await request .post('/i/remote-config/add-condition') @@ -147,8 +147,8 @@ describe('Fetch remote config', () => { condition: JSON.stringify({ condition_name: `${CONDITION_PREFIX}0`, condition_color: 1, - condition: { did: { $in: [TARGETTED_USER_ID] } }, - condition_definition: `ID = ${TARGETTED_USER_ID}`, + condition: { did: { $in: [TARGETED_USER_ID] } }, + condition_definition: `ID = ${TARGETED_USER_ID}`, condition_description: '-', seed_value: '', }), @@ -188,45 +188,45 @@ describe('Fetch remote config', () => { .expect('Content-Type', /json/); }); - it('Should match targetted user (device id)', () => { - const targettedUser = { + it('Should match targeted user (device id)', () => { + const targetedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', uid: '13', - did: 'targetted_user', + did: 'targeted_user', }; - const query = { did: { $in: ['targetted_user'] } }; + const query = { did: { $in: ['targeted_user'] } }; - should(remoteConfig.processFilter(targettedUser, query)).equal(true); + should(remoteConfig.processFilter(targetedUser, query)).equal(true); }); - it('Should not match non targetted user (device id)', () => { - const nonTargettedUser = { + it('Should not match non targeted user (device id)', () => { + const nonTargetedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', uid: '13', - did: 'non_targetted_user', + did: 'non_targeted_user', }; - const query = { did: { $in: ['targetted_user'] } }; + const query = { did: { $in: ['targeted_user'] } }; - should(remoteConfig.processFilter(nonTargettedUser, query)).equal(false); + should(remoteConfig.processFilter(nonTargetedUser, query)).equal(false); }); - it('Should match targetted user (country)', () => { - const targettedUser = { + it('Should match targeted user (country)', () => { + const targetedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', uid: '13', - did: 'targetted_user', + did: 'targeted_user', cc: 'UK', }; const query = { 'up.cc': { $exists: true } }; - should(remoteConfig.processFilter(targettedUser, query)).equal(true); + should(remoteConfig.processFilter(targetedUser, query)).equal(true); }); - it('Should match targetted user (app version)', () => { - const targettedUser = { + it('Should match targeted user (app version)', () => { + const targetedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', uid: '13', - did: 'targetted_user', + did: 'targeted_user', av: '1:0:0', }; const queryGt = { 'up.av': { $gt: '0:0:0' } }; @@ -234,17 +234,17 @@ describe('Fetch remote config', () => { const queryLt = { 'up.av': { $lt: '2:0:0' } }; const queryLte = { 'up.av': { $lte: '1:0:0' } }; - should(remoteConfig.processFilter(targettedUser, queryGt)).equal(true); - should(remoteConfig.processFilter(targettedUser, queryGte)).equal(true); - should(remoteConfig.processFilter(targettedUser, queryLt)).equal(true); - should(remoteConfig.processFilter(targettedUser, queryLte)).equal(true); + should(remoteConfig.processFilter(targetedUser, queryGt)).equal(true); + should(remoteConfig.processFilter(targetedUser, queryGte)).equal(true); + should(remoteConfig.processFilter(targetedUser, queryLt)).equal(true); + should(remoteConfig.processFilter(targetedUser, queryLte)).equal(true); }); - it('Should not match non targetted user (app version)', () => { - const nonTargettedUser = { + it('Should not match non targeted user (app version)', () => { + const nonTargetedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', uid: '13', - did: 'targetted_user', + did: 'targeted_user', av: '1:0:0', }; const queryGt = { 'up.av': { $gt: '1:0:0' } }; @@ -252,89 +252,89 @@ describe('Fetch remote config', () => { const queryLt = { 'up.av': { $lt: '1:0:0' } }; const queryLte = { 'up.av': { $lte: '0:0:0' } }; - should(remoteConfig.processFilter(nonTargettedUser, queryGt)).equal(false); - should(remoteConfig.processFilter(nonTargettedUser, queryGte)).equal(false); - should(remoteConfig.processFilter(nonTargettedUser, queryLt)).equal(false); - should(remoteConfig.processFilter(nonTargettedUser, queryLte)).equal(false); + should(remoteConfig.processFilter(nonTargetedUser, queryGt)).equal(false); + should(remoteConfig.processFilter(nonTargetedUser, queryGte)).equal(false); + should(remoteConfig.processFilter(nonTargetedUser, queryLt)).equal(false); + should(remoteConfig.processFilter(nonTargetedUser, queryLte)).equal(false); }); - it('Should match targetted user ($and query)', () => { - const targettedUser = { + it('Should match targeted user ($and query)', () => { + const targetedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', uid: '13', - did: 'targetted_user', + did: 'targeted_user', cc: 'UK', }; - const query = { 'up.cc': { $exists: true }, did: { $in: ['targetted_user'] } }; + const query = { 'up.cc': { $exists: true }, did: { $in: ['targeted_user'] } }; const altQuery = { $and: [ { 'up.cc': { $exists: true } }, - { did: { $in: ['targetted_user'] } }, + { did: { $in: ['targeted_user'] } }, ], }; - should(remoteConfig.processFilter(targettedUser, query)).equal(true); - should(remoteConfig.processFilter(targettedUser, altQuery)).equal(true); + should(remoteConfig.processFilter(targetedUser, query)).equal(true); + should(remoteConfig.processFilter(targetedUser, altQuery)).equal(true); }); - it('Should not match targetted user ($and query)', () => { - const targettedUser = { + it('Should not match targeted user ($and query)', () => { + const targetedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', uid: '13', - did: 'targetted_user', + did: 'targeted_user', cc: 'UK', }; - const query = { 'up.cc': { $exists: true }, did: { $nin: ['targetted_user'] } }; + const query = { 'up.cc': { $exists: true }, did: { $nin: ['targeted_user'] } }; const altQuery = { $and: [ { 'up.cc': { $exists: true } }, - { did: { $nin: ['targetted_user'] } }, + { did: { $nin: ['targeted_user'] } }, ], }; - should(remoteConfig.processFilter(targettedUser, query)).equal(false); - should(remoteConfig.processFilter(targettedUser, altQuery)).equal(false); + should(remoteConfig.processFilter(targetedUser, query)).equal(false); + should(remoteConfig.processFilter(targetedUser, altQuery)).equal(false); }); - it('Should match targetted user ($or query)', () => { - const targettedUser = { + it('Should match targeted user ($or query)', () => { + const targetedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', uid: '13', - did: 'targetted_user', + did: 'targeted_user', cc: 'UK', }; const query = { $or: [ { 'up.cc': { $exists: true }}, - { did: { $nin: ['targetted_user'] } }, + { did: { $nin: ['targeted_user'] } }, ], }; - should(remoteConfig.processFilter(targettedUser, query)).equal(true); + should(remoteConfig.processFilter(targetedUser, query)).equal(true); }); - it('Should not match targetted user ($or query)', () => { - const targettedUser = { + it('Should not match targeted user ($or query)', () => { + const targetedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', uid: '13', - did: 'targetted_user', + did: 'targeted_user', cc: 'UK', }; const query = { $or: [ { 'up.cc': { $exists: false }}, - { did: { $nin: ['targetted_user'] } }, + { did: { $nin: ['targeted_user'] } }, ], }; - should(remoteConfig.processFilter(targettedUser, query)).equal(false); + should(remoteConfig.processFilter(targetedUser, query)).equal(false); }); - it('Should match targetted user (combination query)', () => { - const targettedUser = { + it('Should match targeted user (combination query)', () => { + const targetedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', uid: '13', - did: 'targetted_user', + did: 'targeted_user', cc: 'UK', chr: { 'chr_id': { @@ -346,18 +346,18 @@ describe('Fetch remote config', () => { const query = { $or: [ { 'up.cc': { $in: ['UK'] }, chr: { $in: ['chr_id'] } }, - { did: { $nin: ['targetted_user'] } }, + { did: { $nin: ['targeted_user'] } }, ], }; - should(remoteConfig.processFilter(targettedUser, query)).equal(true); + should(remoteConfig.processFilter(targetedUser, query)).equal(true); }); - it('Should not match targetted user (combination query)', () => { - const targettedUser = { + it('Should not match targeted user (combination query)', () => { + const targetedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', uid: '13', - did: 'targetted_user', + did: 'targeted_user', cc: 'UK', av: '1:0:0', chr: { @@ -370,11 +370,11 @@ describe('Fetch remote config', () => { const query = { $or: [ { 'up.cc': { $nin: ['UK'] }, chr: { $nin: ['chr_id'] } }, - { did: { $nin: ['targetted_user'] }, 'up.av': { $gt: '2:0:0' } }, + { did: { $nin: ['targeted_user'] }, 'up.av': { $gt: '2:0:0' } }, ], }; - should(remoteConfig.processFilter(targettedUser, query)).equal(false); + should(remoteConfig.processFilter(targetedUser, query)).equal(false); }); it('Should fetch remote config with default value', async() => { @@ -399,7 +399,7 @@ describe('Fetch remote config', () => { api_key: API_KEY_ADMIN, app_id: APP_ID, app_key: APP_KEY, - device_id: TARGETTED_USER_ID, + device_id: TARGETED_USER_ID, method: 'fetch_remote_config', }) .expect(200); From 230457b5bb82922d0fc2768721921567ba396a03 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Fri, 11 Jul 2025 15:44:19 +0700 Subject: [PATCH 071/203] [remote-config] Fix delete dialog --- .../frontend/public/javascripts/countly.views.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/remote-config/frontend/public/javascripts/countly.views.js b/plugins/remote-config/frontend/public/javascripts/countly.views.js index df2ca57b399..dba166d3c88 100644 --- a/plugins/remote-config/frontend/public/javascripts/countly.views.js +++ b/plugins/remote-config/frontend/public/javascripts/countly.views.js @@ -829,7 +829,7 @@ self.onSubmit(); }); - }, [this.i18n["common.no-dont-delete"], this.i18n["remote-config.yes-delete-parameter"]], {title: this.i18n["remote-config.delete-parameter-title"], image: "delete-email-report"}); + }, [this.i18n("common.no-dont-delete"), this.i18n("remote-config.yes-delete-parameter")], {title: this.i18n("remote-config.delete-parameter-title"), image: "delete-email-report"}); break; } }, @@ -913,7 +913,7 @@ break; case "remove": - CountlyHelpers.confirm(this.i18n("remote-config.confirm-condition-delete", "" + name + ""), "popStyleGreen", function(result) { + CountlyHelpers.confirm(this.i18n("remote-config.confirm-condition-delete", "" + row.condition_name + ""), "popStyleGreen", function(result) { if (!result) { return false; } @@ -922,7 +922,7 @@ self.onSubmit(); }); - }, [this.i18n["common.no-dont-delete"], this.i18n["remote-config.yes-delete-condition"]], {title: this.i18n["remote-config.delete-condition-title"], image: "delete-email-report"}); + }, [this.i18n("common.no-dont-delete"), this.i18n("remote-config.yes-delete-condition")], {title: this.i18n("remote-config.delete-condition-title"), image: "delete-email-report"}); break; } }, From 6347c9e8f4eb6c697e55b0f5f4a3c1a7792df49e Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Mon, 14 Jul 2025 10:16:26 +0700 Subject: [PATCH 072/203] [remote-config] Update query matcher --- plugins/remote-config/api/parts/rc.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/plugins/remote-config/api/parts/rc.js b/plugins/remote-config/api/parts/rc.js index 9e155029f0b..b6ff123ecf4 100644 --- a/plugins/remote-config/api/parts/rc.js +++ b/plugins/remote-config/api/parts/rc.js @@ -24,16 +24,17 @@ remoteConfig.processFilter = function(inpUser, inpQuery) { */ function matchesQuery(user, query) { for (let key in query) { - if (key === '$or') { - return query[key].some((subQuery) => matchesQuery(user, subQuery)); - } - else if (key === '$and') { - return query[key].every((subQuery) => matchesQuery(user, subQuery)); - } - else if (typeof query[key] === 'object' && query[key] !== null && !Array.isArray(query[key])) { + if (typeof query[key] === 'object' && query[key] !== null && !Array.isArray(query[key])) { let qResult = true; for (let prop in query) { + if (prop === '$or') { + return qResult && query[prop].some((subQuery) => matchesQuery(user, subQuery)); + } + else if (prop === '$and') { + return qResult && query[prop].every((subQuery) => matchesQuery(user, subQuery)); + } + let parts = prop.split("."); let value; @@ -76,6 +77,12 @@ remoteConfig.processFilter = function(inpUser, inpQuery) { return qResult; } + else if (key === '$or') { + return query[key].some((subQuery) => matchesQuery(user, subQuery)); + } + else if (key === '$and') { + return query[key].every((subQuery) => matchesQuery(user, subQuery)); + } else { return false; } From 85c9a16a6af6250858b8fdae517177fb077ca10b Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Fri, 25 Jul 2025 08:45:39 +0700 Subject: [PATCH 073/203] [remote-config] Rename numeric string list to app version list --- .../frontend/public/javascripts/countly.views.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/remote-config/frontend/public/javascripts/countly.views.js b/plugins/remote-config/frontend/public/javascripts/countly.views.js index dba166d3c88..6f6884421a6 100644 --- a/plugins/remote-config/frontend/public/javascripts/countly.views.js +++ b/plugins/remote-config/frontend/public/javascripts/countly.views.js @@ -212,7 +212,7 @@ label: "#6C47FF" }, colorTag: COLOR_TAG, - modifyPropType: { 'up.av': countlyQueryBuilder.PropertyType.NUMERIC_STRING_LIST }, + modifyPropType: { 'up.av': countlyQueryBuilder.PropertyType.APP_VERSION_LIST }, }; }, computed: { @@ -647,7 +647,7 @@ label: "#6C47FF" }, colorTag: COLOR_TAG, - modifyPropType: { 'up.av': countlyQueryBuilder.PropertyType.NUMERIC_STRING_LIST }, + modifyPropType: { 'up.av': countlyQueryBuilder.PropertyType.APP_VERSION_LIST }, }; }, methods: { From e4b9d71dd73ea812dec32c2c8114bd59c137cecc Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Fri, 1 Aug 2025 01:18:31 +0700 Subject: [PATCH 074/203] [remote-config] Add more tests --- plugins/remote-config/api/parts/rc.js | 6 +++++- plugins/remote-config/tests/fetch_remote_config.js | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/remote-config/api/parts/rc.js b/plugins/remote-config/api/parts/rc.js index b6ff123ecf4..43fc4b07713 100644 --- a/plugins/remote-config/api/parts/rc.js +++ b/plugins/remote-config/api/parts/rc.js @@ -53,7 +53,9 @@ remoteConfig.processFilter = function(inpUser, inpQuery) { if (parts[0] !== 'chr') { if (typeof (value) !== 'undefined') { - if (prop === 'up.av') { + const filterType = Object.keys(query[prop])[0]; + + if (prop === 'up.av' && /^\$(gt|lt)/.test(filterType)) { qResult = qResult && processAppVersionValues(user.av, { [prop]: query[prop] }, prop); } else { @@ -139,6 +141,8 @@ function processPropertyValues(value, query, prop) { * @returns {Boolean} property value status */ function processAppVersionValues(inpUserAv, query, prop) { + // app version is stored in mongo like 1:1:0 instead of 1.1.0 + // the colons have to be replaced with dots so that semver lib can compare the app version const userAv = inpUserAv.replace(/:/g, '.'); const filterType = Object.keys(query[prop])[0]; const targetAv = query[prop] && query[prop][filterType] && query[prop][filterType].replace(/:/g, '.'); diff --git a/plugins/remote-config/tests/fetch_remote_config.js b/plugins/remote-config/tests/fetch_remote_config.js index c5717c32257..e09a818d239 100644 --- a/plugins/remote-config/tests/fetch_remote_config.js +++ b/plugins/remote-config/tests/fetch_remote_config.js @@ -233,29 +233,37 @@ describe('Fetch remote config', () => { const queryGte = { 'up.av': { $gte: '1:0:0' } }; const queryLt = { 'up.av': { $lt: '2:0:0' } }; const queryLte = { 'up.av': { $lte: '1:0:0' } }; + const queryIn = { 'up.av': { $in: ['1:0:0'] } }; + const queryNin = { 'up.av': { $nin: ['2:0:0'] } }; should(remoteConfig.processFilter(targetedUser, queryGt)).equal(true); should(remoteConfig.processFilter(targetedUser, queryGte)).equal(true); should(remoteConfig.processFilter(targetedUser, queryLt)).equal(true); should(remoteConfig.processFilter(targetedUser, queryLte)).equal(true); + should(remoteConfig.processFilter(targetedUser, queryIn)).equal(true); + should(remoteConfig.processFilter(targetedUser, queryNin)).equal(true); }); it('Should not match non targeted user (app version)', () => { const nonTargetedUser = { _id: '1c5c91e1dd594d457a656fad1e55d0cf2a3f0601', uid: '13', - did: 'targeted_user', + did: 'non_targeted_user', av: '1:0:0', }; const queryGt = { 'up.av': { $gt: '1:0:0' } }; const queryGte = { 'up.av': { $gte: '2:0:0' } }; const queryLt = { 'up.av': { $lt: '1:0:0' } }; const queryLte = { 'up.av': { $lte: '0:0:0' } }; + const queryIn = { 'up.av': { $in: ['2:0:0'] } }; + const queryNin = { 'up.av': { $nin: ['1:0:0'] } }; should(remoteConfig.processFilter(nonTargetedUser, queryGt)).equal(false); should(remoteConfig.processFilter(nonTargetedUser, queryGte)).equal(false); should(remoteConfig.processFilter(nonTargetedUser, queryLt)).equal(false); should(remoteConfig.processFilter(nonTargetedUser, queryLte)).equal(false); + should(remoteConfig.processFilter(nonTargetedUser, queryIn)).equal(false); + should(remoteConfig.processFilter(nonTargetedUser, queryNin)).equal(false); }); it('Should match targeted user ($and query)', () => { From 0375499a1f70524293ab89c5682a209a6241f4ee Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Fri, 19 Sep 2025 12:31:24 +0700 Subject: [PATCH 075/203] Update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81f706ecf9e..c23fccd3e8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## Version 24.05.X +Features: +- [remote-config] Enable comparing newer/older app version in conditions + +Fixes: +- [remote-config] Fix condition matching with compound conditions + + ## Version 24.05.38 Enterprise Fixes: - [home] Fix home download render issue From 55f832cad7af5f913b801af9b8370a5d837ddeb6 Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Pinto Date: Tue, 23 Sep 2025 11:02:16 +0100 Subject: [PATCH 076/203] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c23fccd3e8a..4fbca8805fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Version 24.05.X +## Version 24.05.39 Features: - [remote-config] Enable comparing newer/older app version in conditions From afe7e3e6081f41dae2dda24a09a8081a716214a8 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Wed, 24 Sep 2025 10:43:05 +0700 Subject: [PATCH 077/203] [populator] Fix nps generator --- .../frontend/public/javascripts/countly.models.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/populator/frontend/public/javascripts/countly.models.js b/plugins/populator/frontend/public/javascripts/countly.models.js index 3e2b5ba4fc9..113e48ad695 100644 --- a/plugins/populator/frontend/public/javascripts/countly.models.js +++ b/plugins/populator/frontend/public/javascripts/countly.models.js @@ -1634,8 +1634,13 @@ }, success: function(json, textStatus, xhr) { if (json && json.result) { - var id = json.result.split(" "); - npsWidgetList.push(id[2]); + if (json.result._id) { + npsWidgetList.push(json.result._id); + } + else if (json.result.text) { + var id = json.result.text.split(" "); + npsWidgetList.push(id[2]); + } } callback(json, textStatus, xhr); }, From f282ef6ebbfad694f169b628cdabfe6cbfc15699 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Wed, 24 Sep 2025 10:51:36 +0700 Subject: [PATCH 078/203] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fbca8805fb..f3f4c7a1847 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## Version 24.05.X +Fixes: +- [populator] Fix nps generator + + ## Version 24.05.39 Features: - [remote-config] Enable comparing newer/older app version in conditions From bfeb1b4036be36df9f897beb8b974b3fa91818da Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Pinto Date: Tue, 30 Sep 2025 15:39:19 +0100 Subject: [PATCH 079/203] Update version number and fix NPS generator typo --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3f4c7a1847..c77e98f7d0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ -## Version 24.05.X +## Version 24.05.40 Fixes: -- [populator] Fix nps generator +- [populator] Fix NPS generator ## Version 24.05.39 From 8dbbb31f5323ef2f2738b0f47ef8f4ae3e5caa25 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Thu, 2 Oct 2025 18:44:12 +0700 Subject: [PATCH 080/203] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c77e98f7d0b..9218ac7aced 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Version 24.05.40 +Enterprise Fixes: +- [users] Update user custom field number formatting +- [users] Fix condition for custom property update + + ## Version 24.05.40 Fixes: - [populator] Fix NPS generator From a5f56001645f5fda205a906d1ca951dd553d0e58 Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Thu, 2 Oct 2025 13:46:47 +0100 Subject: [PATCH 081/203] chore: update changelog next version --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9218ac7aced..bd4f8159c23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Version 24.05.40 +## Version 24.05.XX Enterprise Fixes: - [users] Update user custom field number formatting - [users] Fix condition for custom property update From 1dbb8fff96a95c94e81e027e4ba5616b9d08a23b Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Tue, 7 Oct 2025 12:39:21 +0700 Subject: [PATCH 082/203] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd4f8159c23..ccaafd64142 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Enterprise Fixes: - [users] Update user custom field number formatting - [users] Fix condition for custom property update +- [cohorts] Fix query transformation for chr ## Version 24.05.40 From 051238b269e6f42d4e739523acc1917979f3b8cc Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Pinto Date: Wed, 8 Oct 2025 08:47:47 +0100 Subject: [PATCH 083/203] Update CHANGELOG for version 24.05.41 Updated version number and fixed entries for users and cohorts. --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccaafd64142..d9c1f13a0ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ -## Version 24.05.XX +## Version 24.05.41 Enterprise Fixes: -- [users] Update user custom field number formatting +- [cohorts] Fix query transformation for profile group - [users] Fix condition for custom property update -- [cohorts] Fix query transformation for chr +- [users] Update user custom field number formatting ## Version 24.05.40 From 3dd0f0ddc2631351916e3a844ae9f2d79acabc41 Mon Sep 17 00:00:00 2001 From: Arturs Sosins Date: Wed, 8 Oct 2025 15:28:10 +0300 Subject: [PATCH 084/203] Add mail debug option --- api/parts/mgmt/mail.js | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/api/parts/mgmt/mail.js b/api/parts/mgmt/mail.js index c5330bcfc0b..19e01d517fd 100644 --- a/api/parts/mgmt/mail.js +++ b/api/parts/mgmt/mail.js @@ -11,8 +11,38 @@ var mail = {}, versionInfo = require('../../../frontend/express/version.info'), authorize = require('../../utils/authorizer'), config = require('../../config'), + log = require('../../utils/log.js')('mail'), + util = require('node:util'), ip = require('./ip.js'); +const smtpLogger = {}; + +// Set up logger wrapper +for (let level of ['trace', 'debug', 'info', 'warn', 'error', 'fatal']) { + smtpLogger[level] = (data, message, ...args) => { + if (args && args.length) { + message = util.format(message, ...args); + } + if (level === 'error' || level === 'fatal') { + log.e(message, data || ''); + } + else if (level === 'warn') { + log.w(message, data || ''); + } + else if (level === 'info') { + log.i(message, data || ''); + } + else { + log.d(message, data || ''); + } + }; +} + +if (config.mail && config.mail.config) { + config.mail.config.logger = smtpLogger; + config.mail.config.debug = true; +} + if (config.mail && config.mail.transport && config.mail.transport !== "nodemailer-smtp-transport") { mail.smtpTransport = nodemailer.createTransport(require(config.mail.transport)(config.mail.config)); } @@ -23,7 +53,8 @@ else { mail.smtpTransport = nodemailer.createTransport({ sendmail: true, newline: 'unix', - path: '/usr/sbin/sendmail' + path: '/usr/sbin/sendmail', + logger: smtpLogger }); } From d840b8490fbfef2ddd5e66045a464b05b348b4e1 Mon Sep 17 00:00:00 2001 From: Arturs Sosins Date: Wed, 8 Oct 2025 15:29:52 +0300 Subject: [PATCH 085/203] Add smtp debug option to mail module --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9c1f13a0ed..29acc36e7ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Version 24.05.xx +Fixes: +- [mail] add smtp debug option for mail module + ## Version 24.05.41 Enterprise Fixes: - [cohorts] Fix query transformation for profile group From 6ee9176d33fd6734b37711f19ae9365ea75c3ba9 Mon Sep 17 00:00:00 2001 From: John-Weak Date: Tue, 10 Jun 2025 15:29:38 +0530 Subject: [PATCH 086/203] [fix] return same promise if multiple calls are made instead of aborting duplicate request --- .../public/javascripts/countly/countly.event.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/express/public/javascripts/countly/countly.event.js b/frontend/express/public/javascripts/countly/countly.event.js index 2cf06dbe2aa..fe6b8cda7e1 100644 --- a/frontend/express/public/javascripts/countly/countly.event.js +++ b/frontend/express/public/javascripts/countly/countly.event.js @@ -17,6 +17,7 @@ _period = null; var _activeLoadedEvent = ""; var _activeLoadedSegmentation = ""; + var _refreshEventsPromise = null; countlyEvent.hasLoadedData = function() { if (_activeLoadedEvent && _activeLoadedEvent === _activeEvent && _activeLoadedSegmentation === _activeSegmentation) { @@ -497,7 +498,11 @@ countlyEvent.refreshEvents = function() { if (!countlyCommon.DEBUG) { - return $.ajax({ + if (_refreshEventsPromise) { + return _refreshEventsPromise; + } + + _refreshEventsPromise = $.ajax({ type: "GET", url: countlyCommon.API_PARTS.data.r, data: { @@ -510,8 +515,13 @@ if (!_activeEvent && countlyEvent.getEvents()[0]) { _activeEvent = countlyEvent.getEvents()[0].key; } + }, + complete: function() { + _refreshEventsPromise = null; } }); + + return _refreshEventsPromise; } else { _activeEvents = {}; From c0386f3cd0a97eee7881e9a6b4c850de70d71deb Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Thu, 9 Oct 2025 11:18:31 +0700 Subject: [PATCH 087/203] Update changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29acc36e7ce..c9f8460a99f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ ## Version 24.05.xx Fixes: - [mail] add smtp debug option for mail module - +- [core] Fix multiple ajax request in event selector + +Enterprise Fixes: +- [funnels] Show notification if funnel results are from cache/task manager + + ## Version 24.05.41 Enterprise Fixes: - [cohorts] Fix query transformation for profile group From 7c4f37c30346a02d77533d9d210497a74b9021ed Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Pinto Date: Tue, 14 Oct 2025 15:17:43 +0100 Subject: [PATCH 088/203] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9f8460a99f..87812ff1042 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ -## Version 24.05.xx +## Version 24.05.42 Fixes: -- [mail] add smtp debug option for mail module - [core] Fix multiple ajax request in event selector +- [mail] add smtp debug option for mail module Enterprise Fixes: - [funnels] Show notification if funnel results are from cache/task manager From cfd3a631af6ba24cc70950b6a330da2165459948 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Wed, 15 Oct 2025 12:05:34 +0700 Subject: [PATCH 089/203] [core] Implement notification goto --- .../javascripts/countly/countly.helpers.js | 12 +++++++--- .../countly/vue/components/helpers.js | 23 ++++++++++++++++--- .../public/javascripts/countly/vue/core.js | 2 +- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/frontend/express/public/javascripts/countly/countly.helpers.js b/frontend/express/public/javascripts/countly/countly.helpers.js index 9ccb6c0c10a..6f91afaedb9 100644 --- a/frontend/express/public/javascripts/countly/countly.helpers.js +++ b/frontend/express/public/javascripts/countly/countly.helpers.js @@ -346,6 +346,7 @@ payload.autoHide = !msg.sticky; payload.id = msg.id; payload.width = msg.width; + payload.goTo = msg.goTo; var colorToUse; if (countlyGlobal.ssr) { @@ -398,9 +399,14 @@ * title is the text that will be dispalyed for the backlink url. */ CountlyHelpers.goTo = function(options) { - app.backlinkUrl = options.from; - app.backlinkTitle = options.title; - window.location.hash = options.url; + if (options.url && options.url.startsWith('https://')) { + window.open(options.url, '_blank', 'noopener,noreferrer'); + } + else { + app.backlinkUrl = options.from; + app.backlinkTitle = options.title; + window.location.hash = options.url; + } }; /** diff --git a/frontend/express/public/javascripts/countly/vue/components/helpers.js b/frontend/express/public/javascripts/countly/vue/components/helpers.js index c6dd8367ca7..a8ee0a3c981 100644 --- a/frontend/express/public/javascripts/countly/vue/components/helpers.js +++ b/frontend/express/public/javascripts/countly/vue/components/helpers.js @@ -1036,11 +1036,13 @@ Vue.component("cly-notification", countlyBaseComponent.extend({ template: '
\n' + '
\n' + - '
\n' + + '
\n' + '\n' + - '{{text}}\n' + + '
\n' + + '{{text}}\n' + + '' + + '
\n' + '
\n' + - '' + '
\n' + '
\n' + '\n' + @@ -1069,6 +1071,7 @@ type: Object }, customWidth: { default: "", type: String }, + toast: { default: false, type: Boolean }, }, data: function() { return { @@ -1115,6 +1118,20 @@ return this.text; } return ""; + }, + dynamicStyle: function() { + let style = { + display: 'flex', + 'flex-direction': this.toast ? 'column' : 'row', + width: '100%' + }; + if (this.toast) { + style.gap = '5px'; + } + else { + style['justify-content'] = 'space-between'; + } + return style; } }, methods: { diff --git a/frontend/express/public/javascripts/countly/vue/core.js b/frontend/express/public/javascripts/countly/vue/core.js index 1f8d33c6b4f..3e43d2be68e 100644 --- a/frontend/express/public/javascripts/countly/vue/core.js +++ b/frontend/express/public/javascripts/countly/vue/core.js @@ -688,7 +688,7 @@ var NotificationToastsView = { template: '
\ - \ + \
', store: _vuex.getGlobalStore(), computed: { From bda1156cae581b929044ae42c8335b9ff2f8dbf6 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Wed, 15 Oct 2025 14:23:03 +0700 Subject: [PATCH 090/203] Update changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87812ff1042..779e4e5c233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## Version 24.05.X +Fixes: +- [core] Implement go to link in notification + +Enterprise Fixes: +- [users] Fix add/remove user to profile group +- [users] Remove link to profile group page after removing user from group + + ## Version 24.05.42 Fixes: - [core] Fix multiple ajax request in event selector From 704a1a752c52a831ca73d477f53872feca812d95 Mon Sep 17 00:00:00 2001 From: Anna Sosina Date: Wed, 15 Oct 2025 10:49:40 +0300 Subject: [PATCH 091/203] Update CHANGELOG Added a new version entry for Enterprise fixes. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87812ff1042..40a5b50c383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Version 24.05.XX +Enterprise Fixes: +- [data-manager] Fixed segment data deletion + ## Version 24.05.42 Fixes: - [core] Fix multiple ajax request in event selector From df420e6058c8b4fff73d187e066ec6d7d66c183d Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Wed, 15 Oct 2025 09:14:53 +0100 Subject: [PATCH 092/203] fix: stop mousedown event propagation on widget content --- .../public/templates/helpers/widget/primary-legend.html | 4 ++-- .../public/templates/helpers/widget/secondary-legend.html | 2 +- .../frontend/public/templates/helpers/widget/title.html | 4 ++-- .../frontend/public/templates/widgets/analytics/widget.html | 4 ++-- .../frontend/public/templates/widgets/note/widget.html | 4 ++-- .../frontend/public/templates/widgets/analytics/widget.html | 4 ++-- .../frontend/public/templates/times-of-day-widget.html | 4 ++-- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/dashboards/frontend/public/templates/helpers/widget/primary-legend.html b/plugins/dashboards/frontend/public/templates/helpers/widget/primary-legend.html index 68a134c5977..e74dd6dd3f5 100644 --- a/plugins/dashboards/frontend/public/templates/helpers/widget/primary-legend.html +++ b/plugins/dashboards/frontend/public/templates/helpers/widget/primary-legend.html @@ -1,4 +1,4 @@ -
+
{{unescapeHtml(customText)}} @@ -10,4 +10,4 @@
{{i18n('taskmanager.recalculating')}}
-
\ No newline at end of file +
diff --git a/plugins/dashboards/frontend/public/templates/helpers/widget/secondary-legend.html b/plugins/dashboards/frontend/public/templates/helpers/widget/secondary-legend.html index 5ac995e46bf..c71a5b5e0db 100644 --- a/plugins/dashboards/frontend/public/templates/helpers/widget/secondary-legend.html +++ b/plugins/dashboards/frontend/public/templates/helpers/widget/secondary-legend.html @@ -1,4 +1,4 @@ -
+
diff --git a/plugins/dashboards/frontend/public/templates/helpers/widget/title.html b/plugins/dashboards/frontend/public/templates/helpers/widget/title.html index a7b3d33c242..c9a98b74528 100644 --- a/plugins/dashboards/frontend/public/templates/helpers/widget/title.html +++ b/plugins/dashboards/frontend/public/templates/helpers/widget/title.html @@ -1,4 +1,4 @@ -
+

{{unescapeHtml(title)}}

-
\ No newline at end of file +
diff --git a/plugins/dashboards/frontend/public/templates/widgets/analytics/widget.html b/plugins/dashboards/frontend/public/templates/widgets/analytics/widget.html index 6da38f9f9f6..8aa717fb56a 100644 --- a/plugins/dashboards/frontend/public/templates/widgets/analytics/widget.html +++ b/plugins/dashboards/frontend/public/templates/widgets/analytics/widget.html @@ -24,7 +24,7 @@
-
+