browser_glean_metrics_exist.js (8871B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 /* This list allows pre-existing or 'unfixable' JS issues to remain, while we 5 * detect newly occurring issues in shipping JS. It is a list of regexes 6 * matching files which have errors: 7 */ 8 9 requestLongerTimeout(2); 10 11 const kAllowlist = new Set([ 12 /browser\/content\/browser\/places\/controller.js$/, 13 ]); 14 15 // Normally we would use reflect.sys.mjs to get Reflect.parse. However, if 16 // we do that, then all the AST data is allocated in reflect.sys.mjs's 17 // zone. That exposes a bug in our GC. The GC collects reflect.sys.mjs's 18 // zone but not the zone in which our test code lives (since no new 19 // data is being allocated in it). The cross-compartment wrappers in 20 // our zone that point to the AST data never get collected, and so the 21 // AST data itself is never collected. We need to GC both zones at 22 // once to fix the problem. 23 const init = Cc["@mozilla.org/jsreflect;1"].createInstance(); 24 init(); 25 26 /** 27 * Check if an error should be ignored due to matching one of the allowlist 28 * objects. 29 * 30 * @param uri the uri to check against the allowlist 31 * @return true if the uri should be skipped, false otherwise. 32 */ 33 function uriIsAllowed(uri) { 34 for (let allowlistItem of kAllowlist) { 35 if (allowlistItem.test(uri.spec)) { 36 return true; 37 } 38 } 39 return false; 40 } 41 42 function recursivelyCheckForGleanCalls(obj, parent = null) { 43 if (!obj) { 44 return; 45 } 46 47 if (Array.isArray(obj)) { 48 for (let item of obj) { 49 recursivelyCheckForGleanCalls(item, { obj, parent }); 50 } 51 return; 52 } 53 54 for (let key in obj) { 55 if (key == "loc") { 56 continue; 57 } 58 if (typeof obj[key] == "object") { 59 recursivelyCheckForGleanCalls(obj[key], { obj, parent }); 60 } 61 } 62 63 if (obj.type != "Identifier" || obj.name != "Glean") { 64 return; 65 } 66 67 function getMemberName(object, child) { 68 if ( 69 object.type == "MemberExpression" && 70 !object.computed && 71 object.object === child && 72 object.property.type == "Identifier" 73 ) { 74 return object.property.name; 75 } 76 return ""; 77 } 78 79 function getPrefix(object, child) { 80 if ( 81 object.type == "MemberExpression" && 82 object.computed && 83 object.object === child && 84 object.property.type == "BinaryExpression" && 85 object.property.operator == "+" && 86 object.property.left.type == "Literal" 87 ) { 88 return object.property.left.value; 89 } 90 return ""; 91 } 92 93 let cat = getMemberName(parent.obj, obj); 94 if (cat) { 95 if (Glean.hasOwnProperty(cat)) { 96 ok(true, `The category ${cat} should exist in the global Glean object`); 97 } else { 98 record( 99 false, 100 `The category ${cat} should exist in the global Glean object`, 101 undefined, 102 `${obj.loc.source}:${obj.loc.start.line}` 103 ); 104 return; 105 } 106 107 let name = getMemberName(parent.parent.obj, parent.obj); 108 if (name) { 109 if (Glean[cat].hasOwnProperty(name)) { 110 ok(true, `The metric ${name} should exist in the Glean.${cat} object`); 111 } else { 112 record( 113 false, 114 `The metric ${name} should exist in the Glean.${cat} object`, 115 undefined, 116 `${obj.loc.source}:${obj.loc.start.line}`, 117 // Object metrics are not supported yet in artifact builds (see bug 1883857), 118 // so we expect some failures. 119 Services.prefs.getBoolPref("telemetry.fog.artifact_build", false) 120 ? "fail" 121 : undefined 122 ); 123 return; 124 } 125 126 let methodOrLabel = getMemberName( 127 parent.parent.parent.obj, 128 parent.parent.obj 129 ); 130 if (methodOrLabel) { 131 if (methodOrLabel in Glean[cat][name]) { 132 ok(true, `${methodOrLabel} should exist in Glean.${cat}.${name}`); 133 } else { 134 record( 135 false, 136 `${methodOrLabel} should exist in Glean.${cat}.${name}`, 137 undefined, 138 `${obj.loc.source}:${obj.loc.start.line}` 139 ); 140 return; 141 } 142 143 let object = Glean[cat][name]; 144 let method = methodOrLabel; 145 if (typeof Glean[cat][name][methodOrLabel] == "object") { 146 method = getMemberName( 147 parent.parent.parent.parent.obj, 148 parent.parent.parent.obj 149 ); 150 if (!method) { 151 return; 152 } 153 object = Glean[cat][name][methodOrLabel]; 154 } 155 156 if (method in object) { 157 ok(true, `${method} exists`); 158 is( 159 typeof object[method], 160 "function", 161 `${method} should be a function` 162 ); 163 } else { 164 record( 165 false, 166 `${method} should exist`, 167 undefined, 168 `${obj.loc.source}:${obj.loc.start.line}` 169 ); 170 } 171 } 172 } else { 173 let prefix = getPrefix(parent.parent.obj, parent.obj); 174 if (prefix) { 175 if (Object.keys(Glean[cat]).some(key => key.startsWith(prefix))) { 176 ok( 177 true, 178 `some metric names in Glean.${cat} have the prefix ${prefix}` 179 ); 180 } else { 181 record( 182 false, 183 `some metric names in Glean.${cat} should start with ${prefix}`, 184 undefined, 185 `${obj.loc.source}:${obj.loc.start.line}` 186 ); 187 } 188 } 189 } 190 } 191 } 192 193 function parsePromise(uri, parseTarget) { 194 return new Promise(resolve => { 195 let xhr = new XMLHttpRequest(); 196 xhr.open("GET", uri, true); 197 xhr.onreadystatechange = function () { 198 if (this.readyState == this.DONE) { 199 let scriptText = this.responseText; 200 if (!scriptText.includes("Glean.")) { 201 resolve(); 202 return; 203 } 204 205 try { 206 info(`Checking ${parseTarget} ${uri}`); 207 let parseOpts = { 208 source: uri, 209 target: parseTarget, 210 }; 211 recursivelyCheckForGleanCalls( 212 Reflect.parse(scriptText, parseOpts).body 213 ); 214 } catch (ex) { 215 let errorMsg = "Script error reading " + uri + ": " + ex; 216 ok(false, errorMsg); 217 } 218 resolve(); 219 } 220 }; 221 xhr.onerror = error => { 222 ok(false, "XHR error reading " + uri + ": " + error); 223 resolve(); 224 }; 225 xhr.overrideMimeType("application/javascript"); 226 xhr.send(null); 227 }); 228 } 229 230 add_task(async function checkAllTheJS() { 231 // In debug builds, even on a fast machine, collecting the file list may take 232 // more than 30 seconds, and parsing all files may take four more minutes. 233 // For this reason, this test must be explictly requested in debug builds by 234 // using the "--setpref parse=<filter>" argument to mach. You can specify: 235 // - A case-sensitive substring of the file name to test (slow). 236 // - A single absolute URI printed out by a previous run (fast). 237 // - An empty string to run the test on all files (slowest). 238 let parseRequested = Services.prefs.prefHasUserValue("parse"); 239 let parseValue = parseRequested && Services.prefs.getCharPref("parse"); 240 if (SpecialPowers.isDebugBuild) { 241 if (!parseRequested) { 242 ok( 243 true, 244 "Test disabled on debug build. To run, execute: ./mach" + 245 " mochitest-browser --setpref parse=<case_sensitive_filter>" + 246 " browser/base/content/test/general/browser_parsable_script.js" 247 ); 248 return; 249 } 250 // Request a 15 minutes timeout (30 seconds * 30) for debug builds. 251 requestLongerTimeout(30); 252 } 253 254 let uris; 255 // If an absolute URI is specified on the command line, use it immediately. 256 if (parseValue && parseValue.includes(":")) { 257 uris = [NetUtil.newURI(parseValue)]; 258 } else { 259 let appDir = Services.dirsvc.get("GreD", Ci.nsIFile); 260 // This asynchronously produces a list of URLs (sadly, mostly sync on our 261 // test infrastructure because it runs against jarfiles there, and 262 // our zipreader APIs are all sync) 263 let startTimeMs = Date.now(); 264 info("Collecting URIs"); 265 uris = await generateURIsFromDirTree(appDir, [".js", ".jsm", ".mjs"]); 266 info("Collected URIs in " + (Date.now() - startTimeMs) + "ms"); 267 268 // Apply the filter specified on the command line, if any. 269 if (parseValue) { 270 uris = uris.filter(uri => { 271 if (uri.spec.includes(parseValue)) { 272 return true; 273 } 274 info("Not checking filtered out " + uri.spec); 275 return false; 276 }); 277 } 278 } 279 280 // We create an array of promises so we can parallelize all our parsing 281 // and file loading activity: 282 await PerfTestHelpers.throttledMapPromises(uris, uri => { 283 if (uriIsAllowed(uri)) { 284 info("Not checking allowlisted " + uri.spec); 285 return undefined; 286 } 287 return parsePromise(uri.spec, uriIsESModule(uri) ? "module" : "script"); 288 }); 289 ok(true, "All files parsed"); 290 });