591 строка
20 KiB
JavaScript
591 строка
20 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
var gProductData = {
|
|
firefox: {
|
|
name: "Firefox Desktop",
|
|
full: "Firefox",
|
|
abbr: "ff",
|
|
channels: {
|
|
nightly: {
|
|
name: "Nightly",
|
|
version: "33.0a1",
|
|
adu: { low: 7e4, min: 4e4 }, // ADUs
|
|
rate: { high: 2.0, max: 2.5 }, // crashes per 100 ADU
|
|
sigcnt: { high: 1e3, max: 1.5e3 }, // # of signatures
|
|
startup: { high: 20, max: 30 }, // percent of total crashes
|
|
flashhang: { high: .1, max: .3 }, // Flash hangs per 100 ADU
|
|
flashcrash: { high: .1, max: .2 }, // Flash crashes per 100 ADU
|
|
opentracking: { high: 10, max: 20 },
|
|
},
|
|
aurora: {
|
|
name: "Aurora",
|
|
version: "32.0a2",
|
|
adu: { low: 2.5e5, min: 1e5 },
|
|
rate: { high: 1.8, max: 2.2 },
|
|
sigcnt: { high: 2e3, max: 3e3 },
|
|
startup: { high: 20, max: 30 },
|
|
flashhang: { high: .1, max: .3 },
|
|
flashcrash: { high: .1, max: .2 },
|
|
opentracking: { high: 10, max: 30 },
|
|
},
|
|
beta: {
|
|
name: "Beta",
|
|
version: "31.0b",
|
|
appendver: true,
|
|
adu: { low: 2e6, min: 1e6 },
|
|
rate: { high: 1.6, max: 1.9 },
|
|
sigcnt: { high: 8e3, max: 1e4 },
|
|
startup: { high: 20, max: 25 },
|
|
flashhang: { high: .1, max: .3 },
|
|
flashcrash: { high: .1, max: .2 },
|
|
opentracking: { high: 5, max: 20 },
|
|
},
|
|
release: {
|
|
name: "Release",
|
|
version: "30.0",
|
|
appendver: true,
|
|
adu: { low: 7e7, min: 2e7 },
|
|
rate: { factor: 1, high: 1.5, max: 1.75 },
|
|
sigcnt: { high: 2e4, max: 2.5e4 },
|
|
startup: { high: 15, max: 20 },
|
|
flashhang: { high: .1, max: .3 },
|
|
flashcrash: { high: .1, max: .2 },
|
|
opentracking: { high: 2, max: 5 },
|
|
},
|
|
},
|
|
},
|
|
fennecandroid: {
|
|
name: "Firefox for Android",
|
|
full: "FennecAndroid",
|
|
abbr: "fna",
|
|
noflash: true,
|
|
channels: {
|
|
nightly: {
|
|
name: "Nightly",
|
|
version: "33.0a1",
|
|
adu: { low: 1000, min: 500 },
|
|
rate: { high: 2, max: 5 },
|
|
sigcnt: { high: 150, max: 250 },
|
|
startup: { high: 25, max: 40 },
|
|
opentracking: { high: 10, max: 20 },
|
|
},
|
|
aurora: {
|
|
name: "Aurora",
|
|
version: "32.0a2",
|
|
adu: { low: 4e3, min: 1e3 },
|
|
rate: { high: 2, max: 4 },
|
|
sigcnt: { high: 250, max: 400 },
|
|
startup: { high: 25, max: 40 },
|
|
opentracking: { high: 10, max: 30 },
|
|
},
|
|
beta: {
|
|
name: "Beta",
|
|
version: "31.0b8",
|
|
appendver: true,
|
|
adu: { low: 6e4, min: 3e4 },
|
|
rate: { high: 2, max: 2.5 },
|
|
sigcnt: { high: 2e3, max: 3e3 },
|
|
startup: { high: 25, max: 40 },
|
|
opentracking: { high: 5, max: 20 },
|
|
},
|
|
release: {
|
|
name: "Release",
|
|
version: "30.0",
|
|
appendver: true,
|
|
adu: { low: 2e6, min: 5e5 },
|
|
rate: { high: 1.5, max: 2.0 },
|
|
sigcnt: { high: 7e3, max: 1e4 },
|
|
startup: { high: 25, max: 40 },
|
|
opentracking: { high: 2, max: 5 },
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
var gSources = {
|
|
adu: {
|
|
precision: null,
|
|
unit: "kMG",
|
|
lowLimits: true,
|
|
getPrettyVersion: function(aProd, aChannel) {
|
|
return aProd.full + " " + aProd.channels[aChannel].version;
|
|
},
|
|
getDataFile: function(aProd, aChannel) {
|
|
return gAnalysisPath + aProd.full + "-daily.json";
|
|
},
|
|
getLinkURL: function(aProd, aChannel) {
|
|
return false;
|
|
},
|
|
getValue: function(aProd, aChannel, aCallback, aCBData) {
|
|
fetchFile(this.getDataFile(aProd, aChannel), "json",
|
|
function(aData) {
|
|
if (!aData || !aData[aProd.channels[aChannel].version] ||
|
|
!aData[aProd.channels[aChannel].version][gDay])
|
|
aCallback(null, aCBData);
|
|
else
|
|
aCallback(aData[aProd.channels[aChannel].version][gDay].adu,
|
|
aCBData);
|
|
}
|
|
);
|
|
},
|
|
},
|
|
rate: {
|
|
precision: 2,
|
|
unit: "",
|
|
lowLimits: false,
|
|
getPrettyVersion: function(aProd, aChannel) {
|
|
return aProd.full + " " + aProd.channels[aChannel].version;
|
|
},
|
|
getDataFile: function(aProd, aChannel) {
|
|
return gAnalysisPath + aProd.full + "-daily.json";
|
|
},
|
|
getLinkURL: function(aProd, aChannel) {
|
|
return false;
|
|
},
|
|
getValue: function(aProd, aChannel, aCallback, aCBData) {
|
|
fetchFile(this.getDataFile(aProd, aChannel), "json",
|
|
function(aData) {
|
|
if (!aData || !aData[aProd.channels[aChannel].version] ||
|
|
!aData[aProd.channels[aChannel].version][gDay]) {
|
|
aCallback(null, aCBData);
|
|
}
|
|
else {
|
|
var crashes = parseInt(aData[aProd.channels[aChannel].version][gDay].crashes);
|
|
var adu = parseInt(aData[aProd.channels[aChannel].version][gDay].adu);
|
|
var factor = aProd.channels[aChannel].rate.factor ?
|
|
aProd.channels[aChannel].rate.factor : 1;
|
|
aCallback(adu ? (crashes / adu) * 100 * factor : null, aCBData);
|
|
}
|
|
}
|
|
);
|
|
},
|
|
},
|
|
sigcnt: {
|
|
precision: 0,
|
|
unit: "",
|
|
lowLimits: false,
|
|
getPrettyVersion: function(aProd, aChannel) {
|
|
return aProd.full + " " +
|
|
(aProd.channels[aChannel].appendver ?
|
|
majVer(aProd.channels[aChannel].version) : aChannel);
|
|
},
|
|
getSigCntFile: function(aProd, aChannel) {
|
|
return gAnalysisPath + gDay + "/" + aProd.abbr + "-" +
|
|
(aProd.channels[aChannel].appendver ?
|
|
majVer(aProd.channels[aChannel].version) : aChannel) +
|
|
"-sigcount.csv";
|
|
},
|
|
getLinkURL: function(aProd, aChannel) {
|
|
return false;
|
|
},
|
|
getValue: function(aProd, aChannel, aCallback, aCBData) {
|
|
fetchFile(this.getSigCntFile(aProd, aChannel), "",
|
|
function(aSigCnt) {
|
|
if (!aSigCnt)
|
|
aCallback(null, aCBData);
|
|
else
|
|
aCallback(parseInt(aSigCnt), aCBData);
|
|
}
|
|
);
|
|
},
|
|
},
|
|
startup: {
|
|
precision: 0,
|
|
unit: "%",
|
|
lowLimits: false,
|
|
getPrettyVersion: function(aProd, aChannel) {
|
|
if (aProd.channels[aChannel].appendver)
|
|
return aProd.full + " " + repVer(aProd.channels[aChannel].version) +
|
|
" " + aChannel;
|
|
else
|
|
return aProd.full + " " + aChannel;
|
|
},
|
|
getStartupFile: function(aProd, aChannel) {
|
|
if (aProd.channels[aChannel].appendver)
|
|
return gAnalysisPath + aProd.abbr + "-" + aChannel + "-" +
|
|
repVer(aProd.channels[aChannel].version) + ".startup.json";
|
|
else
|
|
return gAnalysisPath + aProd.abbr + "-" + aChannel + ".startup.json";
|
|
},
|
|
getLinkURL: function(aProd, aChannel) {
|
|
return false;
|
|
},
|
|
getValue: function(aProd, aChannel, aCallback, aCBData) {
|
|
fetchFile(this.getStartupFile(aProd, aChannel), "json",
|
|
function(aData) {
|
|
if (!aData || !aData[gDay])
|
|
aCallback(null, aCBData);
|
|
else
|
|
aCallback((aData[gDay].startup.browser && aData[gDay].total) ?
|
|
aData[gDay].startup.browser / aData[gDay].total * 100 :
|
|
0,
|
|
aCBData);
|
|
}
|
|
);
|
|
},
|
|
},
|
|
flashhang: {
|
|
precision: 2,
|
|
unit: "",
|
|
lowLimits: false,
|
|
getPrettyVersion: function(aProd, aChannel) {
|
|
return aProd.full + " " + repVer(aProd.channels[aChannel].version);
|
|
},
|
|
getFlashHangFile: function(aProd, aChannel) {
|
|
return gAnalysisPath + aProd.abbr + "-" +
|
|
repVer(aProd.channels[aChannel].version) + ".flashhang.json";
|
|
},
|
|
getLinkURL: function(aProd, aChannel) {
|
|
return false;
|
|
},
|
|
getValue: function(aProd, aChannel, aCallback, aCBData) {
|
|
fetchFile(this.getFlashHangFile(aProd, aChannel), "json",
|
|
function(aData) {
|
|
if (!aData || !aData[gDay])
|
|
aCallback(null, aCBData);
|
|
else
|
|
aCallback(aData[gDay].adu ?
|
|
aData[gDay].total_flash.hang / aData[gDay].adu * 100 :
|
|
null,
|
|
aCBData);
|
|
}
|
|
);
|
|
},
|
|
},
|
|
flashcrash: {
|
|
precision: 2,
|
|
unit: "",
|
|
lowLimits: false,
|
|
getPrettyVersion: function(aProd, aChannel) {
|
|
return aProd.full + " " + repVer(aProd.channels[aChannel].version);
|
|
},
|
|
getFlashHangFile: function(aProd, aChannel) {
|
|
return gAnalysisPath + aProd.abbr + "-" +
|
|
repVer(aProd.channels[aChannel].version) + ".flashhang.json";
|
|
},
|
|
getLinkURL: function(aProd, aChannel) {
|
|
return false;
|
|
},
|
|
getValue: function(aProd, aChannel, aCallback, aCBData) {
|
|
fetchFile(this.getFlashHangFile(aProd, aChannel), "json",
|
|
function(aData) {
|
|
if (!aData || !aData[gDay])
|
|
aCallback(null, aCBData);
|
|
else
|
|
aCallback(aData[gDay].adu ?
|
|
aData[gDay].total_flash.crash / aData[gDay].adu * 100 :
|
|
null,
|
|
aCBData);
|
|
}
|
|
);
|
|
},
|
|
},
|
|
opentracking: {
|
|
precision: 0,
|
|
unit: "",
|
|
lowLimits: false,
|
|
getPrettyVersion: function(aProd, aChannel) {
|
|
return aProd.full + " + Core " + majVer(aProd.channels[aChannel].version);
|
|
},
|
|
getBugzillaQuery: function(aProd, aChannel) {
|
|
var mver = majVer(aProd.channels[aChannel].version);
|
|
var bugprod = aProd.full;
|
|
if (bugprod == "FennecAndroid") { bugprod = "Firefox%20for%20Android"; }
|
|
if (bugprod == "MetroFirefox") { bugprod = "Firefox%20for%20Metro"; }
|
|
return "?keywords=crash&keywords_type=anywords" +
|
|
"&product=Core&product=Toolkit&product=" + bugprod +
|
|
"&field0-0-0=cf_tracking_firefox" + mver + "&type0-0-0=equals&value0-0-0=%2B" +
|
|
"&type0-1-0=nowordssubstr&field0-1-0=cf_status_firefox" + mver
|
|
+ "&query_format=advanced;value0-1-0=fixed%20verified%20disabled%20wontfix%20unaffected";
|
|
},
|
|
getTrackersFile: function(aProd, aChannel) {
|
|
return gBzAPIPath + "count" + this.getBugzillaQuery(aProd, aChannel);
|
|
},
|
|
getLinkURL: function(aProd, aChannel) {
|
|
return gBzBasePath + "buglist.cgi" + this.getBugzillaQuery(aProd, aChannel);
|
|
},
|
|
getValue: function(aProd, aChannel, aCallback, aCBData) {
|
|
fetchFile(this.getTrackersFile(aProd, aChannel), "json",
|
|
function(aData) {
|
|
if (!aData)
|
|
aCallback(null, aCBData);
|
|
else
|
|
aCallback(aData.data, aCBData);
|
|
}
|
|
);
|
|
},
|
|
},
|
|
}
|
|
|
|
var gDebug, gLog;
|
|
var gDay;
|
|
//var gAnalysisPath = "https://crash-analysis.mozilla.com/rkaiser/";
|
|
var gAnalysisPath = "../../";
|
|
var gBzAPIPath = "https://bugzilla.mozilla.org/bzapi/";
|
|
var gBzBasePath = "https://bugzilla.mozilla.org/";
|
|
|
|
window.onload = function() {
|
|
gDebug = document.getElementById("debug");
|
|
gLog = document.getElementById("debugLog");
|
|
|
|
// Get date to analyze.
|
|
fetchFile(gAnalysisPath + "latestdate.txt", "",
|
|
function(aData) {
|
|
if (aData) {
|
|
gDay = aData.trim();
|
|
document.getElementById("repDay").textContent = gDay;
|
|
processData();
|
|
}
|
|
else {
|
|
gDay = null;
|
|
document.getElementById("repDay").textContent = "ERROR - couldn't find latest analyzed date!"
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
function processData() {
|
|
var db = document.getElementById("dashboard");
|
|
var baseSection = document.getElementById("none");
|
|
for (var product in gProductData) {
|
|
var sect = db.appendChild(baseSection.cloneNode(true));
|
|
sect.setAttribute("id", product);
|
|
var ptitle = sect.getElementsByTagName("h2")[0];
|
|
ptitle.textContent = gProductData[product].name;
|
|
|
|
for (var channel in gProductData[product].channels) {
|
|
var cdata = gProductData[product].channels[channel];
|
|
var headrow = sect.getElementsByClassName("headers")[0];
|
|
headrow.appendChild(createCell(cdata.name, true));
|
|
var versionrow = sect.getElementsByClassName("versions")[0];
|
|
versionrow.appendChild(createCell(cdata.version));
|
|
for (var source in gSources) {
|
|
var sourcerow = sect.getElementsByClassName(source)[0];
|
|
if (source.indexOf("flash") !== -1 && gProductData[product].noflash) {
|
|
sourcerow.classList.add("hidden");
|
|
}
|
|
else {
|
|
var sourceCell = sourcerow.appendChild(createCell(""));
|
|
sourceCell.addEventListener("mouseover", infoEvent, false);
|
|
sourceCell.addEventListener("mouseout", infoEvent, false);
|
|
sourceCell.addEventListener("touchstart", infoEvent, false);
|
|
sourceCell.addEventListener("touchleave", infoEvent, false);
|
|
sourceCell.dataset.product = product;
|
|
sourceCell.dataset.channel = channel;
|
|
sourceCell.dataset.source = source;
|
|
gSources[source].getValue(gProductData[product], channel, valueCallback,
|
|
{cell: sourceCell, data: cdata, type: source});
|
|
}
|
|
}
|
|
//gDebug.textContent = channel.name;
|
|
}
|
|
}
|
|
}
|
|
|
|
function createCell(aText, aHeader) {
|
|
var cell = document.createElement(aHeader ? "th" : "td");
|
|
cell.appendChild(document.createTextNode(aText));
|
|
return cell;
|
|
}
|
|
|
|
function infoEvent(event) {
|
|
var info = document.getElementById("infobox");
|
|
|
|
var cell = event.target;
|
|
if (cell.tagName == "A") { cell = cell.parentNode; }
|
|
|
|
if (info.getElementsByClassName(cell.dataset.source)[0]) {
|
|
if (event.type == "mouseover" || event.type == "touchstart") {
|
|
// Position: The parent is the table and has the offset within the body.
|
|
info.style.left = (cell.offsetParent.offsetLeft +
|
|
cell.offsetLeft - 1) + "px";
|
|
info.style.top = (cell.offsetParent.offsetTop +
|
|
cell.offsetTop + cell.offsetHeight - 1) + "px";
|
|
|
|
// Set info to show.
|
|
info.getElementsByClassName(cell.dataset.source)[0].classList.add("current");
|
|
|
|
var limits = gProductData[cell.dataset.product]
|
|
.channels[cell.dataset.channel][cell.dataset.source];
|
|
if (gSources[cell.dataset.source].lowLimits) {
|
|
info.getElementsByClassName("limits")[0].classList.add("low");
|
|
document.getElementById("limit1").textContent =
|
|
formatValue(limits.low,
|
|
gSources[cell.dataset.source].precision,
|
|
gSources[cell.dataset.source].unit);
|
|
document.getElementById("limit2").textContent =
|
|
formatValue(limits.min,
|
|
gSources[cell.dataset.source].precision,
|
|
gSources[cell.dataset.source].unit);
|
|
}
|
|
else {
|
|
info.getElementsByClassName("limits")[0].classList.add("high");
|
|
document.getElementById("limit1").textContent =
|
|
formatValue(limits.high,
|
|
gSources[cell.dataset.source].precision,
|
|
gSources[cell.dataset.source].unit);
|
|
document.getElementById("limit2").textContent =
|
|
formatValue(limits.max,
|
|
gSources[cell.dataset.source].precision,
|
|
gSources[cell.dataset.source].unit);
|
|
}
|
|
|
|
document.getElementById("verinfo").textContent =
|
|
gSources[cell.dataset.source]
|
|
.getPrettyVersion(gProductData[cell.dataset.product],
|
|
cell.dataset.channel);
|
|
|
|
// Finally actually display the box.
|
|
info.style.display = "block";
|
|
}
|
|
else {
|
|
// Hide the box.
|
|
info.style.display = "none";
|
|
|
|
// Reset info where needed.
|
|
info.getElementsByClassName(cell.dataset.source)[0].classList.remove("current");
|
|
if (gSources[cell.dataset.source].lowLimits) {
|
|
info.getElementsByClassName("limits")[0].classList.remove("low");
|
|
}
|
|
else {
|
|
info.getElementsByClassName("limits")[0].classList.remove("high");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function fetchFile(aURL, aFormat, aCallback) {
|
|
var XHR = new XMLHttpRequest();
|
|
XHR.onreadystatechange = function() {
|
|
if (XHR.readyState == 4) {/*
|
|
gLog.appendChild(document.createElement("li"))
|
|
.appendChild(document.createTextNode(aURL + " - " + XHR.status +
|
|
" " + XHR.statusText));*/
|
|
}
|
|
if (XHR.readyState == 4 && XHR.status == 200) {
|
|
// so far so good
|
|
if (XHR.responseXML != null && aFormat == "xml" &&
|
|
XHR.responseXML.getElementById('test').firstChild.data)
|
|
aCallback(aXHR.responseXML.getElementById('test').firstChild.data);
|
|
else if (XHR.responseText != null && aFormat == "json")
|
|
aCallback(JSON.parse(XHR.responseText));
|
|
else
|
|
aCallback(XHR.responseText);
|
|
} else if (XHR.readyState == 4 && XHR.status != 200) {
|
|
// fetched the wrong page or network error...
|
|
aCallback(null);
|
|
}
|
|
};
|
|
XHR.open("GET", aURL);
|
|
if (aFormat == "json") { XHR.setRequestHeader("Accept", "application/json"); }
|
|
else if (aFormat == "xml") { XHR.setRequestHeader("Accept", "application/xml"); }
|
|
try {
|
|
XHR.send();
|
|
}
|
|
catch (e) {
|
|
aCallback(null);
|
|
}
|
|
}
|
|
|
|
function valueCallback(aValue, aCBData) {
|
|
aCBData.data[aCBData.type].current = aValue;
|
|
if (aValue !== null) {
|
|
var url = gSources[aCBData.type]
|
|
.getLinkURL(gProductData[aCBData.cell.dataset.product],
|
|
aCBData.cell.dataset.channel);
|
|
var value = formatValue(aValue, gSources[aCBData.type].precision,
|
|
gSources[aCBData.type].unit);
|
|
if (url) {
|
|
var link = document.createElement("a");
|
|
link.setAttribute("href", url);
|
|
link.setAttribute("target", "_blank");
|
|
link.textContent = value;
|
|
aCBData.cell.appendChild(link);
|
|
}
|
|
else {
|
|
aCBData.cell.textContent = value;
|
|
}
|
|
aCBData.cell.classList.add("num");
|
|
if (((gSources[aCBData.type].lowLimits) &&
|
|
(aValue < aCBData.data[aCBData.type].min)) ||
|
|
(aValue > aCBData.data[aCBData.type].max))
|
|
aCBData.cell.classList.add("faroff");
|
|
else if (((gSources[aCBData.type].lowLimits) &&
|
|
(aValue < aCBData.data[aCBData.type].low)) ||
|
|
(aValue > aCBData.data[aCBData.type].high))
|
|
aCBData.cell.classList.add("outside");
|
|
else
|
|
aCBData.cell.classList.add("normal");
|
|
}
|
|
else {
|
|
aCBData.cell.textContent = "ERR";
|
|
aCBData.cell.classList.add("fail");
|
|
}
|
|
}
|
|
|
|
function formatValue(aValue, aPrecision, aUnit) {
|
|
var formatted;
|
|
if (aUnit == "kMG") {
|
|
var val = aValue;
|
|
var prec = aPrecision;
|
|
var unit = "";
|
|
if (aValue > 1e10) {
|
|
prec = (prec === null) ? 0 : prec;
|
|
val = (val / 1e9).toFixed(prec);
|
|
unit = "G";
|
|
}
|
|
else if (aValue > 1e9) {
|
|
prec = (prec === null) ? 1 : prec;
|
|
val = (val / 1e9).toFixed(prec);
|
|
unit = "G";
|
|
}
|
|
else if (aValue > 1e7) {
|
|
prec = (prec === null) ? 0 : prec;
|
|
val = (val / 1e6).toFixed(prec);
|
|
unit = "M";
|
|
}
|
|
else if (aValue > 1e6) {
|
|
prec = (prec === null) ? 1 : prec;
|
|
val = (val / 1e6).toFixed(prec);
|
|
unit = "M";
|
|
}
|
|
else if (aValue > 1e4) {
|
|
prec = (prec === null) ? 0 : prec;
|
|
val = (val / 1e3).toFixed(prec);
|
|
unit = "k";
|
|
}
|
|
else if (aValue > 1e3) {
|
|
prec = (prec === null) ? 1 : prec;
|
|
val = (val / 1e3).toFixed(prec);
|
|
unit = "k";
|
|
}
|
|
else if (prec !== null) {
|
|
val = val.toFixed(prec);
|
|
}
|
|
formatted = val + unit;
|
|
}
|
|
else {
|
|
formatted = aValue.toFixed(aPrecision);
|
|
if (aUnit)
|
|
formatted += aUnit;
|
|
}
|
|
return formatted;
|
|
}
|
|
|
|
function majVer(aVersion) {
|
|
// returns major version, e.g. 11 for 11.0b3, 13 for 13.0a1, 10 for 10.0.2
|
|
return aVersion.match(/^\d+/);
|
|
}
|
|
|
|
function repVer(aVersion) {
|
|
// returns a report version, e.g. 11.0 for 11.0b3, 13.0a1 for 13.0a1, 10.0 for 10.0.2
|
|
return aVersion.match(/^\d+\.[\da]+/);
|
|
}
|
|
|
|
function intVer(aVersion) {
|
|
// returns the internal version, e.g. 11.0 for 11.0b3, 13.0a1 for 13.0a1, 10.0.2 for 10.0.2
|
|
return aVersion.match(/^[\d\.a]+/);
|
|
}
|