Skip to content

Commit 95f1277

Browse files
authored
Merge pull request #6135 from Countly/feat/version-spread
Spread version saving to db
2 parents 29a6642 + fceb523 commit 95f1277

File tree

6 files changed

+267
-21
lines changed

6 files changed

+267
-21
lines changed

api/parts/data/usage.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ usage.returnAllProcessedMetrics = function(params) {
398398
continue;
399399
}
400400

401-
if (recvMetricValue) {
401+
if (recvMetricValue !== undefined && recvMetricValue !== null) {
402402
var escapedMetricVal = (recvMetricValue + "").replace(/^\$/, "").replace(/\./g, ":");
403403
processedMetrics[tmpMetric.short_code] = escapedMetricVal;
404404
}
@@ -705,7 +705,7 @@ function processMetrics(user, uniqueLevelsZero, uniqueLevelsMonth, params, done)
705705
continue;
706706
}
707707

708-
if (recvMetricValue) {
708+
if (recvMetricValue !== undefined && recvMetricValue !== null) {
709709
recvMetricValue = (recvMetricValue + "").replace(/^\$/, "").replace(/\./g, ":");
710710
postfix = common.crypto.createHash("md5").update(recvMetricValue).digest('base64')[0];
711711
metaToFetch[predefinedMetrics[i].db + params.app_id + "_" + dateIds.zero + "_" + postfix] = {
@@ -767,7 +767,7 @@ function processMetrics(user, uniqueLevelsZero, uniqueLevelsMonth, params, done)
767767
continue;
768768
}
769769

770-
if (recvMetricValue) {
770+
if (recvMetricValue !== undefined && recvMetricValue !== null) {
771771
escapedMetricVal = (recvMetricValue + "").replace(/^\$/, "").replace(/\./g, ":");
772772
postfix = common.crypto.createHash("md5").update(escapedMetricVal).digest('base64')[0];
773773

@@ -1110,6 +1110,15 @@ plugins.register("/sdk/user_properties", async function(ob) {
11101110
userProps[key] = up[key];
11111111
}
11121112
}
1113+
1114+
if (params.qstring.metrics._app_version) {
1115+
const versionComponents = common.parseAppVersion(params.qstring.metrics._app_version);
1116+
if (versionComponents.success) {
1117+
userProps.av_major = versionComponents.major;
1118+
userProps.av_minor = versionComponents.minor;
1119+
userProps.av_patch = versionComponents.patch;
1120+
}
1121+
}
11131122
}
11141123

11151124
if (params.qstring.session_duration) {

api/utils/common.js

Lines changed: 146 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
/**
2-
* Module for some common utility functions and references
3-
* @module api/utils/common
4-
*/
5-
2+
* Module for some common utility functions and references
3+
* @module api/utils/common
4+
*/
65
/**
76
* @typedef {import('../../types/requestProcessor').Params} Params
87
* @typedef {import('../../types/common').TimeObject} TimeObject
@@ -12,19 +11,20 @@
1211

1312
/** @lends module:api/utils/common **/
1413
/** @type(import('../../types/common').Common) */
15-
var common = {};
14+
const common = {};
1615

1716
/** @type(import('moment-timezone')) */
18-
var moment = require('moment-timezone');
19-
var crypto = require('crypto'),
20-
logger = require('./log.js'),
21-
mcc_mnc_list = require('mcc-mnc-list'),
22-
plugins = require('../../plugins/pluginManager.js'),
23-
countlyConfig = require('./../config', 'dont-enclose'),
24-
argon2 = require('argon2'),
25-
mongodb = require('mongodb'),
26-
getRandomValues = require('get-random-values'),
27-
_ = require('lodash');
17+
const moment = require('moment-timezone');
18+
const crypto = require('crypto');
19+
const logger = require('./log.js');
20+
const mcc_mnc_list = require('mcc-mnc-list');
21+
const plugins = require('../../plugins/pluginManager.js');
22+
const countlyConfig = require('./../config', 'dont-enclose');
23+
const argon2 = require('argon2');
24+
const mongodb = require('mongodb');
25+
const getRandomValues = require('get-random-values');
26+
const semver = require('semver');
27+
const _ = require('lodash');
2828

2929
var matchHtmlRegExp = /"|'|&(?!amp;|quot;|#39;|lt;|gt;|#46;|#36;)|<|>/;
3030
var matchLessHtmlRegExp = /[<>]/;
@@ -210,6 +210,9 @@ common.dbUserMap = {
210210
'platform': 'p',
211211
'platform_version': 'pv',
212212
'app_version': 'av',
213+
'app_version_major': 'av_major',
214+
'app_version_minor': 'av_minor',
215+
'app_version_patch': 'av_patch',
213216
'last_begin_session_timestamp': 'lbst',
214217
'last_end_session_timestamp': 'lest',
215218
'has_ongoing_session': 'hos',
@@ -2055,6 +2058,134 @@ common.versionCompare = function(v1, v2, options) {
20552058
return compareParts;
20562059
};
20572060

2061+
/**
2062+
* Parse app_version into major, minor, patch components
2063+
* @param {string|number} version - The version to parse
2064+
* @returns {object} Object containing major, minor, patch, original version, and success flag
2065+
*/
2066+
common.parseAppVersion = function(version) {
2067+
try {
2068+
if (typeof version !== 'string') {
2069+
version = String(version);
2070+
}
2071+
2072+
// Ensure version has at least one decimal point
2073+
if (version.indexOf('.') === -1) {
2074+
version += '.0';
2075+
}
2076+
2077+
const parsedVersion = semver.valid(semver.coerce(version));
2078+
if (parsedVersion) {
2079+
const versionObj = semver.parse(parsedVersion);
2080+
if (versionObj) {
2081+
return {
2082+
major: versionObj.major,
2083+
minor: versionObj.minor,
2084+
patch: versionObj.patch,
2085+
original: version,
2086+
success: true
2087+
};
2088+
}
2089+
}
2090+
}
2091+
catch (error) {
2092+
// Silently catch any errors from semver library
2093+
// console.error('Error parsing app version:', error);
2094+
}
2095+
2096+
// Return only original version with success=false if parsing fails or throws an exception
2097+
return {
2098+
original: version,
2099+
success: false
2100+
};
2101+
};
2102+
2103+
/**
2104+
* Check if a version string follows some kind of scheme (there is only semantic versioning (semver) for now)
2105+
* @param {string} inpVersion - an app version string
2106+
* @return {array} [regex.exec result, version scheme name]
2107+
*/
2108+
common.checkAppVersion = function(inpVersion) {
2109+
// Regex is from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
2110+
const semverRgx = /(^v?)(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
2111+
// Half semver is similar to semver but with only one dot
2112+
const halfSemverRgx = /(^v?)(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
2113+
2114+
let execResult = semverRgx.exec(inpVersion);
2115+
2116+
if (execResult) {
2117+
return [execResult, 'semver'];
2118+
}
2119+
2120+
execResult = halfSemverRgx.exec(inpVersion);
2121+
2122+
if (execResult) {
2123+
return [execResult, 'halfSemver'];
2124+
}
2125+
2126+
return [null, null];
2127+
};
2128+
2129+
/**
2130+
* Transform a version string so it will be numerically correct when sorted
2131+
* For example '1.10.2' will be transformed to '100001.100010.100002'
2132+
* So when sorted ascending it will come after '1.2.0' ('100001.100002.100000')
2133+
* @param {string} inpVersion - an app version string
2134+
* @return {string} the transformed app version
2135+
* @note Imported and moved from @module plugins/crashes/api/parts/version (which has now been deprecated)
2136+
*/
2137+
common.transformAppVersion = function(inpVersion) {
2138+
const [execResult, versionScheme] = common.checkAppVersion(inpVersion);
2139+
2140+
if (execResult === null) {
2141+
// Version string does not follow any scheme, just return it
2142+
return inpVersion;
2143+
}
2144+
2145+
// Mark version parts based on semver scheme
2146+
let prefixIdx = 1;
2147+
let majorIdx = 2;
2148+
let minorIdx = 3;
2149+
let patchIdx = 4;
2150+
let preReleaseIdx = 5;
2151+
let buildIdx = 6;
2152+
2153+
if (versionScheme === 'halfSemver') {
2154+
patchIdx -= 1;
2155+
preReleaseIdx -= 1;
2156+
buildIdx -= 1;
2157+
}
2158+
2159+
let transformed = '';
2160+
// Rejoin version parts to a new string
2161+
for (let idx = prefixIdx; idx < buildIdx; idx += 1) {
2162+
let part = execResult[idx];
2163+
2164+
if (part) {
2165+
if (idx >= majorIdx && idx <= patchIdx) {
2166+
part = 100000 + parseInt(part, 10);
2167+
}
2168+
2169+
if (idx >= minorIdx && idx <= patchIdx) {
2170+
part = '.' + part;
2171+
}
2172+
2173+
if (idx === preReleaseIdx) {
2174+
part = '-' + part;
2175+
}
2176+
2177+
if (idx === buildIdx) {
2178+
part = '+' + part;
2179+
}
2180+
2181+
transformed += part;
2182+
}
2183+
}
2184+
2185+
return transformed;
2186+
};
2187+
2188+
20582189
common.adjustTimestampByTimezone = function(ts, tz) {
20592190
var d = moment();
20602191
if (tz) {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"properties-parser": "0.6.0",
8686
"puppeteer": "^23.8.0",
8787
"sass": "1.86.0",
88+
"semver": "^7.7.1",
8889
"sqlite3": "^5.1.7",
8990
"tslib": "^2.6.3",
9091
"uglify-js": "3.19.3",

plugins/crashes/api/api.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ var plugin = {},
88
Duplex = require('stream').Duplex,
99
Promise = require("bluebird"),
1010
trace = require("./parts/stacktrace.js"),
11-
versionUtils = require('./parts/version.js'),
1211
{ DEFAULT_MAX_CUSTOM_FIELD_KEYS } = require('./parts/custom_field.js'),
1312
plugins = require('../../pluginManager.js'),
1413
{ validateCreate, validateRead, validateUpdate, validateDelete } = require('../../../api/utils/rights.js');
@@ -468,6 +467,19 @@ plugins.setConfigs("crashes", {
468467
if ('app_version' in report && typeof report.app_version !== 'string') {
469468
report.app_version += '';
470469
}
470+
471+
// Parse app_version into separate major, minor, patch version fields
472+
if ('app_version' in report) {
473+
const versionComponents = common.parseAppVersion(report.app_version);
474+
475+
// Only store the components as separate fields if parsing was successful
476+
if (versionComponents.success) {
477+
report.app_version_major = versionComponents.major;
478+
report.app_version_minor = versionComponents.minor;
479+
report.app_version_patch = versionComponents.patch;
480+
}
481+
}
482+
471483
let updateData = {$inc: {}};
472484

473485
updateData.$inc["data.crashes"] = 1;
@@ -511,6 +523,9 @@ plugins.setConfigs("crashes", {
511523
{ name: "muted", type: "l" },
512524
{ name: "background", type: "l" },
513525
{ name: "app_version", type: "l" },
526+
{ name: "app_version_major", type: "n" },
527+
{ name: "app_version_minor", type: "n" },
528+
{ name: "app_version_patch", type: "n" },
514529
{ name: "ram_current", type: "n" },
515530
{ name: "ram_total", type: "n" },
516531
{ name: "disk_current", type: "n" },
@@ -600,7 +615,7 @@ plugins.setConfigs("crashes", {
600615
groupInsert.is_resolved = false;
601616
groupInsert.startTs = report.ts;
602617
groupInsert.latest_version = report.app_version;
603-
groupInsert.latest_version_for_sort = versionUtils.transformAppVersion(report.app_version);
618+
groupInsert.latest_version_for_sort = common.transformAppVersion(report.app_version);
604619
groupInsert.error = report.error;
605620
groupInsert.lrid = report._id + "";
606621

@@ -722,7 +737,7 @@ plugins.setConfigs("crashes", {
722737
if (!isNew && crashGroup) {
723738
if (crashGroup.latest_version && common.versionCompare(report.app_version.replace(/\./g, ":"), crashGroup.latest_version.replace(/\./g, ":")) > 0) {
724739
group.latest_version = report.app_version;
725-
group.latest_version_for_sort = versionUtils.transformAppVersion(report.app_version);
740+
group.latest_version_for_sort = common.transformAppVersion(report.app_version);
726741
}
727742
if (plugins.getConfig('crashes').same_app_version_crash_update) {
728743
if (crashGroup.latest_version && common.versionCompare(report.app_version.replace(/\./g, ":"), crashGroup.latest_version.replace(/\./g, ":")) >= 0) {

plugins/crashes/api/parts/version.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ function checkAppVersion(inpVersion) {
3131

3232
module.exports = {
3333
/**
34+
* @deprecated
3435
* Transform a version string so it will be numerically correct when sorted
3536
* For example '1.10.2' will be transformed to '100001.100010.100002'
3637
* So when sorted ascending it will come after '1.2.0' ('100001.100002.100000')
3738
* @param {string} inpVersion - an app version string
3839
* @return {string} the transformed app version
40+
* @note These method are now moved to @module api/utils/common
3941
*/
4042
transformAppVersion: function(inpVersion) {
4143
const [execResult, versionScheme] = checkAppVersion(inpVersion);

0 commit comments

Comments
 (0)