diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1a8fdff3a32..9bcc5b0c4e8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,26 @@
+## Version 25.03.12
+Features:
+- [plugins] Add configuration warning tags to settings UI
+- [white-labeling] Add sidebar footer label setting to white labeling
+
+Fixes:
+- [core] Use correct rights validation for loyality
+- [crashes] Fix free session for home widget
+- [crashes] Fix trend and change calculation for crash stats
+- [crashes] Use na for free session and free user when there's no data
+- [push] Show segmentation, geo and cohorts related components in push drawer on editing draft.
+
+Enterprise Fixes:
+- [ldap] Error handling in ldap plugin on search error
+- [license] Display notification for non global admin user
+- [users] Load table data from report if user table calculation goes to report manager
+
+Dependencies:
+- Bump mongodb from 6.17.0 to 6.18.0
+- Bump puppeteer from 24.14.0 to 24.15.0
+- Bump supertest from 7.1.3 to 7.1.4
+
+
## Version 25.03.11
Fixes:
- [core] Fix mongo connection url parsing
@@ -5,6 +28,8 @@ Fixes:
- [crashes] Fix free session and free user calculation
- [dashboards] Delete associated widgets and reports when a dashboard is removed
- [star-rating] Fix widget close post message
+- [core] Adjust level and update content of app version log
+- [populator] Update getVersion to generate valid semantic version
Enterprise Fixes:
- [crash_symbolication] Remove auto symbolication setting
diff --git a/LICENSE b/LICENSE.md
similarity index 92%
rename from LICENSE
rename to LICENSE.md
index 1901e4fc150..0bf57dc9a67 100644
--- a/LICENSE
+++ b/LICENSE.md
@@ -3,7 +3,7 @@ Countly Product Analytics - Countly Lite License
© Countly, https://count.ly
-Countly is provided under AGPL v3 with modified Section 7. In accordance
+Countly is provided under AGPL-3.0 with modified Section 7. In accordance
with Section 7 of the AGPL, the Works included in this package or repository
(excluding 3rd party Software), are subject to the following additional terms:
diff --git a/README.md b/README.md
index 3f4ad6978b4..0ea8dc3416a 100644
--- a/README.md
+++ b/README.md
@@ -116,3 +116,11 @@ If you like Countly, why not use one of our badges and give a link back to us?
+
+
+## License
+This project is licensed under **AGPL-3.0** with modified Section 7., see the [LICENSE](LICENSE) file for more details.
+
+## 💚 Thanks
+
+This project is tested with BrowserStack.
diff --git a/api/parts/data/usage.js b/api/parts/data/usage.js
index ce3b2a847d3..0b03e0599de 100644
--- a/api/parts/data/usage.js
+++ b/api/parts/data/usage.js
@@ -1121,7 +1121,7 @@ plugins.register("/sdk/user_properties", async function(ob) {
userProps.av_build = versionComponents.build;
}
else {
- log.w("Invalid app version format: %s", params.qstring.metrics._app_version);
+ log.d("App version %s is not a valid semantic version. It cannot be separated into semantic version parts", params.qstring.metrics._app_version);
userProps.av_major = null;
userProps.av_minor = null;
userProps.av_patch = null;
diff --git a/api/utils/requestProcessor.js b/api/utils/requestProcessor.js
index 6a6450f00cc..707eca00467 100644
--- a/api/utils/requestProcessor.js
+++ b/api/utils/requestProcessor.js
@@ -1567,7 +1567,7 @@ const processRequest = (params) => {
common.returnMessage(params, 400, 'Missing parameter "app_id"');
return false;
}
- validateUserForMgmtReadAPI(countlyApi.mgmt.appUsers.loyalty, params);
+ validateUserForRead(params, countlyApi.mgmt.appUsers.loyalty);
break;
}
/**
diff --git a/bin/scripts/export-data/setting_limits_and_real_values.js b/bin/scripts/export-data/setting_limits_and_real_values.js
index 116e97c1744..7cbb0e0bfa3 100644
--- a/bin/scripts/export-data/setting_limits_and_real_values.js
+++ b/bin/scripts/export-data/setting_limits_and_real_values.js
@@ -4,7 +4,6 @@
* Path: $(countly dir)/bin/scripts/export-data
* Command: node setting_limits_and_real_values.js
*/
-
const fs = require('fs');
const crypto = require('crypto');
const common = require('../../../api/utils/common.js');
@@ -20,10 +19,10 @@ const DEFAULT_LIMITS = {
view_name_limit: 128,
view_segment_limit: 100,
view_segment_value_limit: 10,
- custom_prop_limit: 20,
+ //custom_prop_limit: 20,
+ custom_property_limit: 20,
custom_prop_value_limit: 50,
};
-
Promise.all([pluginManager.dbConnection("countly"), pluginManager.dbConnection("countly_drill")]).then(async function([countlyDb, drillDb]) {
console.log("Connected to databases...");
common.db = countlyDb;
@@ -36,21 +35,17 @@ Promise.all([pluginManager.dbConnection("countly"), pluginManager.dbConnection("
else {
// WRITE START OF ARRAY
WriteStream.write('[\n', 'utf8');
-
// GETTING DATA FOR SET LIMITS FOR EVENTS, VIEWS, AND CUSTOM PROPERTIES
var pluginsCollectionPlugins = await countlyDb.collection("plugins").findOne({"_id": 'plugins'});
-
// LOOP APPS FOR EACH REQUIREMENT
for (let i = 0; i < apps.length; i++) {
var app = apps[i];
console.log(i + 1, ") Processing app:", app.name);
-
try {
var app_results = { "App Name": app.name },
defaultVal,
realVal,
currentVal;
-
// SETTING UP CURRENT SET LIMITS PER APP
var appsCollectionPerApp = await countlyDb.collection("apps").findOne({"_id": common.db.ObjectID(app._id)});
var CURRENT_LIMITS = {
@@ -61,10 +56,10 @@ Promise.all([pluginManager.dbConnection("countly"), pluginManager.dbConnection("
view_name_limit: pluginsCollectionPlugins?.views?.view_name_limit || DEFAULT_LIMITS.view_name_limit,
view_segment_limit: pluginsCollectionPlugins?.views?.segment_limit || DEFAULT_LIMITS.view_segment_limit,
view_segment_value_limit: pluginsCollectionPlugins?.views?.segment_value_limit || DEFAULT_LIMITS.view_segment_value_limit,
- custom_prop_limit: pluginsCollectionPlugins?.users?.custom_prop_limit || DEFAULT_LIMITS.custom_prop_limit,
+ //custom_prop_limit: pluginsCollectionPlugins?.users?.custom_prop_limit || DEFAULT_LIMITS.custom_prop_limit,
+ custom_property_limit: pluginsCollectionPlugins?.drill?.custom_property_limit || DEFAULT_LIMITS.custom_property_limit,
custom_prop_value_limit: pluginsCollectionPlugins?.users?.custom_set_limit || DEFAULT_LIMITS.custom_prop_value_limit,
};
-
// GETTING REAL DATA PER APP
var eventsCollectionPerApp = await countlyDb.collection("events").findOne({"_id": common.db.ObjectID(app._id)});
var viewsCountsPerApp = await countlyDb.collection("app_viewsmeta" + app._id).countDocuments();
@@ -140,22 +135,15 @@ Promise.all([pluginManager.dbConnection("countly"), pluginManager.dbConnection("
catch (err) {
console.log("Mongodb operation failed for app: ", app.name, err);
}
-
// EVENT KEYS
defaultVal = DEFAULT_LIMITS.event_limit;
-
currentVal = CURRENT_LIMITS.event_limit;
-
let realEvents = eventsCollectionPerApp?.list || [];
realVal = realEvents.length;
-
app_results['Event Keys'] = {"default": defaultVal, "set": currentVal, "real": realVal};
-
// SEGMENTS IN ONE EVENT
defaultVal = DEFAULT_LIMITS.event_segment_limit;
-
currentVal = CURRENT_LIMITS.event_segment_limit;
-
let eventSegments = eventsCollectionPerApp && eventsCollectionPerApp.segments || {};
realVal = Object.entries(eventSegments)
.sort((a, b) => b[1].length - a[1].length)
@@ -163,23 +151,18 @@ Promise.all([pluginManager.dbConnection("countly"), pluginManager.dbConnection("
acc[key] = value.length;
return acc;
}, {});
-
app_results['Event Segments'] = {"default": defaultVal, "set": currentVal, "real": realVal};
-
// UNIQUE EVENT SEGMENT VALUES FOR 1 SEGMENT
defaultVal = DEFAULT_LIMITS.event_segment_value_limit;
-
currentVal = CURRENT_LIMITS.event_segment_value_limit;
-
realVal = {};
await Promise.all(realEvents.map(async(event) => {
var shortEventName = common.fixEventKey(event);
- var eventCollectionName = "events" + crypto.createHash('sha1').update(shortEventName + app._id).digest('hex');
-
+ var hash = crypto.createHash('sha1').update(shortEventName + app._id).digest('hex');
+ var eventCollectionName = "events_data";
try {
var regexes = [
- "^no-segment_2023:0.*",
- "^no-segment_2024:0.*"
+ "^" + app._id + "_" + hash + "_no-segment_2025:0.*"
];
var eventsSegmentsValues = await countlyDb.collection(eventCollectionName).aggregate([
{
@@ -218,7 +201,6 @@ Promise.all([pluginManager.dbConnection("countly"), pluginManager.dbConnection("
}
}
]).toArray();
-
// Use reduce to transform array
eventsSegmentsValues = eventsSegmentsValues.reduce((acc, item) => {
const key = item.meta_v2.k;
@@ -228,7 +210,6 @@ Promise.all([pluginManager.dbConnection("countly"), pluginManager.dbConnection("
acc[key] += item.meta_v2.v;
return acc;
}, {});
-
if (Object.keys(eventsSegmentsValues).length > 0) {
realVal[event] = eventsSegmentsValues;
}
@@ -238,41 +219,26 @@ Promise.all([pluginManager.dbConnection("countly"), pluginManager.dbConnection("
}
}));
app_results['Unique Event Segment Values'] = {"default": defaultVal, "set": currentVal, "real": realVal};
-
// UNIQUE VIEV NAMES
defaultVal = DEFAULT_LIMITS.view_limit;
-
currentVal = CURRENT_LIMITS.view_limit;
-
realVal = viewsCountsPerApp;
-
app_results['Unique View Names'] = {"default": defaultVal, "set": currentVal, "real": realVal};
-
// VIEW NAME LENGTH LIMIT
defaultVal = DEFAULT_LIMITS.view_name_limit;
-
currentVal = CURRENT_LIMITS.view_name_limit;
-
realVal = {longestViewName: "", longestViewLength: 0};
realVal.longestViewName = viewsCollectionPerApp && viewsCollectionPerApp[0] && viewsCollectionPerApp[0]?.view;
realVal.longestViewLength = viewsCollectionPerApp && viewsCollectionPerApp[0] && viewsCollectionPerApp[0]?.max_length;
-
app_results['View Name Length Limit'] = {"default": defaultVal, "set": currentVal, "real": realVal};
-
// SEGMENTS IN ONE VIEW
defaultVal = DEFAULT_LIMITS.view_segment_limit;
-
currentVal = CURRENT_LIMITS.view_segment_limit;
-
realVal = viewsSegmentsPerApp && viewsSegmentsPerApp[0]?.numberOfSegments || 0;
-
app_results['View Segments'] = {"default": defaultVal, "set": currentVal, "real": realVal};
-
// VIEW SEGMENT'S UNIQUE VALUES
defaultVal = DEFAULT_LIMITS.view_segment_value_limit;
-
currentVal = CURRENT_LIMITS.view_segment_value_limit;
-
realVal = viewsSegmentsPerApp && viewsSegmentsPerApp[0]?.segments || 0;
Object.keys(realVal).forEach(key => {
if (realVal[key] === 0) {
@@ -280,23 +246,18 @@ Promise.all([pluginManager.dbConnection("countly"), pluginManager.dbConnection("
}
});
app_results['View Segments Unique Values'] = {"default": defaultVal, "set": currentVal, "real": realVal};
-
// USER PROPERTIES
- defaultVal = DEFAULT_LIMITS.custom_prop_limit;
-
- currentVal = CURRENT_LIMITS.custom_prop_limit;
-
+ //defaultVal = DEFAULT_LIMITS.custom_prop_limit;
+ //currentVal = CURRENT_LIMITS.custom_prop_limit;
+ defaultVal = DEFAULT_LIMITS.custom_property_limit;
+ currentVal = CURRENT_LIMITS.custom_property_limit;
realVal = customPropsPerApp && customPropsPerApp[0]?.customPropertiesCount || 0;
- app_results['Custom User Properties'] = {"default": defaultVal, "set": currentVal, "real": realVal};
-
+ app_results['Max user custom properties'] = {"default": defaultVal, "set": currentVal, "real": realVal};
// VALUES IN AN ARRAY FOR ONE USER PROPERTY
defaultVal = DEFAULT_LIMITS.custom_prop_value_limit;
-
currentVal = CURRENT_LIMITS.custom_prop_value_limit;
-
realVal = valueFieldCounts || undefined;
app_results['Values In Array For One User Property'] = {"default": defaultVal, "set": currentVal, "real": realVal};
-
// WRITE RESULTS PER APP TO FILE
WriteStream.write(JSON.stringify(app_results, null, 2), 'utf8');
if (i + 1 < apps.length) {
@@ -315,7 +276,6 @@ Promise.all([pluginManager.dbConnection("countly"), pluginManager.dbConnection("
finally {
close();
}
-
async function getAppList(options) {
var query = {};
if (app_list && app_list.length > 0) {
@@ -325,7 +285,6 @@ Promise.all([pluginManager.dbConnection("countly"), pluginManager.dbConnection("
}
query = {_id: {$in: listed}};
}
-
try {
let apps = await options.db.collection("apps").find(query).toArray();
return apps;
@@ -334,9 +293,7 @@ Promise.all([pluginManager.dbConnection("countly"), pluginManager.dbConnection("
console.log("Error getting apps: ", err);
return [];
}
-
}
-
function close(err) {
if (err) {
console.log("Error: ", err);
diff --git a/frontend/express/app.js b/frontend/express/app.js
index 194ccd85ff6..482b7d91ce1 100644
--- a/frontend/express/app.js
+++ b/frontend/express/app.js
@@ -422,6 +422,14 @@ Promise.all([plugins.dbConnection(countlyConfig), plugins.dbConnection("countly_
app.loadThemeFiles(curTheme);
app.dashboard_headers = plugins.getConfig("security").dashboard_additional_headers;
+ var overriddenCountlyNamedType = COUNTLY_NAMED_TYPE;
+ var whiteLabelingConfig = plugins.getConfig("white-labeling");
+ if (whiteLabelingConfig && whiteLabelingConfig.footerLabel && whiteLabelingConfig.footerLabel.length) {
+ overriddenCountlyNamedType = whiteLabelingConfig.footerLabel;
+ }
+
+ COUNTLY_NAMED_TYPE = overriddenCountlyNamedType;
+
if (typeof plugins.getConfig('frontend').countly_tracking !== 'boolean' && plugins.isPluginEnabled('tracker')) {
plugins.updateConfigs(countlyDb, 'frontend', { countly_tracking: true });
}
@@ -929,6 +937,12 @@ Promise.all([plugins.dbConnection(countlyConfig), plugins.dbConnection("countly_
licenseNotification, licenseError;
var isLocked = false;
configs.export_limit = plugins.getConfig("api").export_limit;
+
+ var currentWhiteLabelingConfig = plugins.getConfig("white-labeling");
+ var overriddenCountlyNamedType = COUNTLY_NAMED_TYPE;
+ if (currentWhiteLabelingConfig && currentWhiteLabelingConfig.footerLabel && currentWhiteLabelingConfig.footerLabel.length) {
+ overriddenCountlyNamedType = currentWhiteLabelingConfig.footerLabel;
+ }
app.loadThemeFiles(configs.theme, async function(theme) {
if (configs._user.theme) {
res.cookie("theme", configs.theme);
@@ -1004,7 +1018,7 @@ Promise.all([plugins.dbConnection(countlyConfig), plugins.dbConnection("countly_
licenseError,
ssr: serverSideRendering,
timezones: timezones,
- countlyTypeName: COUNTLY_NAMED_TYPE,
+ countlyTypeName: overriddenCountlyNamedType,
countlyTypeTrack: COUNTLY_TRACK_TYPE,
countlyTypeCE: COUNTLY_TYPE_CE,
countly_tracking,
@@ -1037,7 +1051,7 @@ Promise.all([plugins.dbConnection(countlyConfig), plugins.dbConnection("countly_
countlyVersion: req.countly.version,
countlyType: COUNTLY_TYPE_CE,
countlyTrial: COUNTLY_TRIAL,
- countlyTypeName: COUNTLY_NAMED_TYPE,
+ countlyTypeName: overriddenCountlyNamedType,
feedbackLink: COUNTLY_FEEDBACK_LINK,
documentationLink: COUNTLY_DOCUMENTATION_LINK,
helpCenterLink: COUNTLY_HELPCENTER_LINK,
diff --git a/frontend/express/public/javascripts/countly/vue/data/vuex.js b/frontend/express/public/javascripts/countly/vue/data/vuex.js
index 6809cd60631..72248ebc465 100644
--- a/frontend/express/public/javascripts/countly/vue/data/vuex.js
+++ b/frontend/express/public/javascripts/countly/vue/data/vuex.js
@@ -274,14 +274,23 @@
options.onOverrideResponse(context, res);
}
var convertedResponse = _dataTableAdapters.toStandardResponse(res, requestOptions);
- if (!Object.prototype.hasOwnProperty.call(convertedResponse, "echo") ||
- convertedResponse.echo >= context.state[echoField]) {
- if (typeof options.onReady === 'function') {
- convertedResponse.rows = options.onReady(context, convertedResponse.rows);
+ if (res.task_id) {
+ if (typeof options.onTask === 'function') {
+ options.onTask(context, res.task_id);
}
context.commit(_capitalized("set", resourceName), convertedResponse);
context.commit(_capitalized("set", lastSuccessfulRequestKey), requestOptions);
}
+ else {
+ if (!Object.prototype.hasOwnProperty.call(convertedResponse, "echo") ||
+ convertedResponse.echo >= context.state[echoField]) {
+ if (typeof options.onReady === 'function') {
+ convertedResponse.rows = options.onReady(context, convertedResponse.rows);
+ }
+ context.commit(_capitalized("set", resourceName), convertedResponse);
+ context.commit(_capitalized("set", lastSuccessfulRequestKey), requestOptions);
+ }
+ }
})
.catch(function(err) {
if (typeof options.onError === 'function') {
diff --git a/package-lock.json b/package-lock.json
index bb676d5c182..f421c67b686 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -53,7 +53,7 @@
"method-override": "3.0.0",
"moment": "2.30.1",
"moment-timezone": "0.6.0",
- "mongodb": "6.17.0",
+ "mongodb": "6.18.0",
"nginx-conf": "2.1.0",
"nodemailer": "7.0.5",
"object-hash": "3.0.0",
@@ -83,7 +83,7 @@
"mocha": "10.8.2",
"nyc": "17.1.0",
"should": "13.2.3",
- "supertest": "7.1.3",
+ "supertest": "7.1.4",
"typescript": "^5.8.2"
}
},
@@ -3629,9 +3629,9 @@
}
},
"node_modules/chromium-bidi": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-7.1.1.tgz",
- "integrity": "sha512-L2BKQ0rSLADgbPMIdDh3wnYHs3EiUiMay2Sq0CTolheaADmWIf6Pe+T9LJRcnh5rcMz0U7MVk0cQVvKsGRMa1g==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-7.2.0.tgz",
+ "integrity": "sha512-gREyhyBstermK+0RbcJLbFhcQctg92AGgDe/h/taMJEOLRdtSswBAO9KmvltFSQWgM2LrwWu5SIuEUbdm3JsyQ==",
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
@@ -3931,6 +3931,7 @@
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
"integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
"dev": true,
+ "license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
@@ -4071,7 +4072,8 @@
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/core-util-is": {
"version": "1.0.3",
@@ -5500,7 +5502,8 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/fastq": {
"version": "1.19.1",
@@ -9528,9 +9531,9 @@
}
},
"node_modules/mongodb": {
- "version": "6.17.0",
- "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.17.0.tgz",
- "integrity": "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==",
+ "version": "6.18.0",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz",
+ "integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==",
"license": "Apache-2.0",
"dependencies": {
"@mongodb-js/saslprep": "^1.1.9",
@@ -11103,17 +11106,17 @@
}
},
"node_modules/puppeteer": {
- "version": "24.14.0",
- "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.14.0.tgz",
- "integrity": "sha512-GB7suRDkp9pUnxpNGAORICQCtw11KFbg6U2iJXVTflzJLK5D1qzq8xOOmLgN/QnDBpDMdpn96ri52XkuN83Giw==",
+ "version": "24.15.0",
+ "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.15.0.tgz",
+ "integrity": "sha512-HPSOTw+DFsU/5s2TUUWEum9WjFbyjmvFDuGHtj2X4YUz2AzOzvKMkT3+A3FR+E+ZefiX/h3kyLyXzWJWx/eMLQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.10.6",
- "chromium-bidi": "7.1.1",
+ "chromium-bidi": "7.2.0",
"cosmiconfig": "^9.0.0",
"devtools-protocol": "0.0.1464554",
- "puppeteer-core": "24.14.0",
+ "puppeteer-core": "24.15.0",
"typed-query-selector": "^2.12.0"
},
"bin": {
@@ -11124,13 +11127,13 @@
}
},
"node_modules/puppeteer-core": {
- "version": "24.14.0",
- "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.14.0.tgz",
- "integrity": "sha512-NO9XpCl+i8oB0zJp81iPhzMo2QK8/JTj4ramSvTpGCo9CPCNo4AZ8qVOGpSgXzlcOfOT3VHOkzTfPo08GOE5jA==",
+ "version": "24.15.0",
+ "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.15.0.tgz",
+ "integrity": "sha512-2iy0iBeWbNyhgiCGd/wvGrDSo73emNFjSxYOcyAqYiagkYt5q4cPfVXaVDKBsukgc2fIIfLAalBZlaxldxdDYg==",
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.10.6",
- "chromium-bidi": "7.1.1",
+ "chromium-bidi": "7.2.0",
"debug": "^4.4.1",
"devtools-protocol": "0.0.1464554",
"typed-query-selector": "^2.12.0",
@@ -12449,20 +12452,21 @@
}
},
"node_modules/superagent": {
- "version": "10.2.2",
- "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.2.tgz",
- "integrity": "sha512-vWMq11OwWCC84pQaFPzF/VO3BrjkCeewuvJgt1jfV0499Z1QSAWN4EqfMM5WlFDDX9/oP8JjlDKpblrmEoyu4Q==",
+ "version": "10.2.3",
+ "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz",
+ "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "component-emitter": "^1.3.0",
+ "component-emitter": "^1.3.1",
"cookiejar": "^2.1.4",
- "debug": "^4.3.4",
+ "debug": "^4.3.7",
"fast-safe-stringify": "^2.1.1",
- "form-data": "^4.0.0",
+ "form-data": "^4.0.4",
"formidable": "^3.5.4",
"methods": "^1.1.2",
"mime": "2.6.0",
- "qs": "^6.11.0"
+ "qs": "^6.11.2"
},
"engines": {
"node": ">=14.18.0"
@@ -12473,6 +12477,7 @@
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
"integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@paralleldrive/cuid2": "^2.2.2",
"dezalgo": "^1.0.4",
@@ -12490,6 +12495,7 @@
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
"dev": true,
+ "license": "MIT",
"bin": {
"mime": "cli.js"
},
@@ -12498,13 +12504,14 @@
}
},
"node_modules/supertest": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.3.tgz",
- "integrity": "sha512-ORY0gPa6ojmg/C74P/bDoS21WL6FMXq5I8mawkEz30/zkwdu0gOeqstFy316vHG6OKxqQ+IbGneRemHI8WraEw==",
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz",
+ "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"methods": "^1.1.2",
- "superagent": "^10.2.2"
+ "superagent": "^10.2.3"
},
"engines": {
"node": ">=14.18.0"
diff --git a/package.json b/package.json
index eeaadca6484..95ddf4ee808 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
"mocha": "10.8.2",
"nyc": "17.1.0",
"should": "13.2.3",
- "supertest": "7.1.3",
+ "supertest": "7.1.4",
"typescript": "^5.8.2"
},
"dependencies": {
@@ -85,7 +85,7 @@
"method-override": "3.0.0",
"moment": "2.30.1",
"moment-timezone": "0.6.0",
- "mongodb": "6.17.0",
+ "mongodb": "6.18.0",
"nginx-conf": "2.1.0",
"nodemailer": "7.0.5",
"object-hash": "3.0.0",
diff --git a/plugins/crashes/frontend/public/javascripts/countly.models.js b/plugins/crashes/frontend/public/javascripts/countly.models.js
index 52a14407986..058f6f11aac 100644
--- a/plugins/crashes/frontend/public/javascripts/countly.models.js
+++ b/plugins/crashes/frontend/public/javascripts/countly.models.js
@@ -152,7 +152,10 @@ function transformAppVersion(inpVersion) {
* @param {bool} isPercent - Flag to just format the metric's total values as percentages.
*/
function populateMetric(metric, isPercent) {
- if (dashboard[metric].total !== 0 && dashboard[metric]["prev-total"] !== 0) {
+ if (dashboard[metric].total === 'NA' || dashboard[metric]['prev-total'] === 'NA') {
+ dashboard[metric].change = 'NA';
+ }
+ else if (dashboard[metric].total !== 0 && dashboard[metric]["prev-total"] !== 0) {
if (isPercent) {
dashboard[metric].change = (dashboard[metric].total - dashboard[metric]["prev-total"]).toFixed(1) + "%";
}
@@ -166,12 +169,18 @@ function transformAppVersion(inpVersion) {
else if (dashboard[metric].total !== 0 && dashboard[metric]["prev-total"] === 0) {
dashboard[metric].change = "∞";
}
+ else if (dashboard[metric].total === dashboard[metric]["prev-total"]) {
+ dashboard[metric].change = "0";
+ }
if (dashboard[metric].total === dashboard[metric]["prev-total"]) {
dashboard[metric].trend = "n";
}
else {
- dashboard[metric].trend = dashboard[metric].total >= dashboard[metric]["prev-total"] ? "u" : "d";
+ var totl = dashboard[metric].total === 'NA' ? -1 : dashboard[metric].total;
+ var prevTotl = dashboard[metric]['prev-total'] === 'NA' ? -1 : dashboard[metric]['prev-total'];
+
+ dashboard[metric].trend = totl >= prevTotl ? 'u' : 'd';
}
if (metric in derivations) {
@@ -182,7 +191,9 @@ function transformAppVersion(inpVersion) {
if (isPercent && ["crses", "crnfses", "crfses", "crau", "craunf", "crauf", 'crinv', 'crfinv', 'crnfinv', 'crauinv', 'craufinv', 'craunfinv'].includes(metric)) {
["total", "prev-total"].forEach(function(prop) {
- dashboard[metric][prop] = dashboard[metric][prop].toFixed(2) + '%';
+ if (dashboard[metric][prop] !== 'NA') {
+ dashboard[metric][prop] = dashboard[metric][prop].toFixed(2) + '%';
+ }
});
}
}
@@ -219,10 +230,13 @@ function transformAppVersion(inpVersion) {
}
// derive user count from whole users
- if (dashboard.crau[prop] > dashboard.cr_u[prop] && 'users' in state.rawData) {
+ if (dashboard.crau[prop] > dashboard.cr_u[prop] && 'users' in state.rawData && wholeUsers.total > 0) {
dashboard.crauf[prop] = dashboard.cr_u[prop] * ((wholeUsers.fatal / wholeUsers.total) - (dashboard.crf[prop] / dashboard.cr_s[prop]));
+ dashboard.crauf.isEstimate = true;
dashboard.craunf[prop] = dashboard.cr_u[prop] * ((wholeUsers.nonfatal / wholeUsers.total) - (dashboard.crnf[prop] / dashboard.cr_s[prop]));
+ dashboard.craunf.isEstimate = true;
dashboard.crau[prop] = dashboard.crauf[prop] + dashboard.craunf[prop];
+ dashboard.crau.isEstimate = true;
}
dashboard.crinv[prop] = Math.max(0, dashboard.cr_s[prop] - dashboard.cr[prop]);
@@ -234,9 +248,14 @@ function transformAppVersion(inpVersion) {
populateMetric(metric);
});
- ["crau", "craunf", "crauf"].forEach(function(name) {
- ["total", "prev-total"].forEach(function(prop) {
- dashboard[name][prop] = Math.min(100, (dashboard.cr_u[prop] === 0 || dashboard[name][prop] === 0) ? 100 : (Math.abs(dashboard.cr_u[prop] - dashboard[name][prop]) / dashboard.cr_u[prop] * 100));
+ ['crau', 'craunf', 'crauf'].forEach(function(name) {
+ ['total', 'prev-total'].forEach(function(prop) {
+ if (dashboard.cr_u[prop] === 0 || !Number.isFinite(dashboard[name][prop])) {
+ dashboard[name][prop] = 'NA';
+ }
+ else {
+ dashboard[name][prop] = Math.abs(dashboard.cr_u[prop] - dashboard[name][prop]) / dashboard.cr_u[prop] * 100;
+ }
});
populateMetric(name, true);
});
@@ -264,17 +283,13 @@ function transformAppVersion(inpVersion) {
});
['crinv', 'crfinv', 'crnfinv'].forEach(function(name) {
- ["total", "prev-total"].forEach(function(prop) {
- var propValue = 0;
-
+ ['total', 'prev-total'].forEach(function(prop) {
if (dashboard.cr_s[prop] === 0) {
- propValue = 100;
+ dashboard[name][prop] = 'NA';
}
else {
- propValue = dashboard[name][prop] / dashboard.cr_s[prop] * 100;
+ dashboard[name][prop] = dashboard[name][prop] / dashboard.cr_s[prop] * 100;
}
-
- dashboard[name][prop] = Math.min(100, propValue);
});
populateMetric(name, true);
});
diff --git a/plugins/crashes/frontend/public/javascripts/countly.views.js b/plugins/crashes/frontend/public/javascripts/countly.views.js
index 2f32ce9f589..dcfded97912 100644
--- a/plugins/crashes/frontend/public/javascripts/countly.views.js
+++ b/plugins/crashes/frontend/public/javascripts/countly.views.js
@@ -1338,7 +1338,7 @@
{"name": CV.i18n('crashes.unique'), "info": CV.i18n('crashes.home.unique'), "prop": "cru", "r": true},
{"name": CV.i18n('crashes.total-per-session'), "info": CV.i18n('crashes.home.per-session'), "prop": "cr-session", "r": true},
{"name": CV.i18n('crashes.free-users'), "info": CV.i18n('crashes.help-free-users'), "prop": "crau", "p": true},
- {"name": CV.i18n('crashes.free-sessions'), "info": CV.i18n('crashes.help-free-sessions'), "prop": "crses", "p": true}
+ {"name": CV.i18n('crashes.free-sessions'), "info": CV.i18n('crashes.help-free-sessions'), "prop": "crinv", "p": true}
];
for (var k = 0; k < getUs.length; k++) {
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 9c01f0da1f8..27bae3d1761 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 227e38e26ef..eb10290d9c6 100644
--- a/plugins/plugins/frontend/public/localization/plugins.properties
+++ b/plugins/plugins/frontend/public/localization/plugins.properties
@@ -243,5 +243,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
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 e0c933ceb65..49250590e55 100755
--- a/plugins/plugins/frontend/public/templates/configurations.html
+++ b/plugins/plugins/frontend/public/templates/configurations.html
@@ -51,8 +51,35 @@
{{getLabelName(key)}}
- +