test_search_telemetry_config_validation.js (5025B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 ChromeUtils.defineESModuleGetters(this, { 7 AppConstants: "resource://gre/modules/AppConstants.sys.mjs", 8 RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", 9 TELEMETRY_SETTINGS_KEY: 10 "moz-src:///browser/components/search/SearchSERPTelemetry.sys.mjs", 11 JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs", 12 SearchEngineSelector: 13 "moz-src:///toolkit/components/search/SearchEngineSelector.sys.mjs", 14 }); 15 16 /** 17 * Checks to see if a value is an object or not. 18 * 19 * @param {*} value 20 * The value to check. 21 * @returns {boolean} 22 */ 23 function isObject(value) { 24 return value != null && typeof value == "object" && !Array.isArray(value); 25 } 26 27 /** 28 * This function modifies the schema to prevent allowing additional properties 29 * on objects. This is used to enforce that the schema contains everything that 30 * we deliver via the search configuration. 31 * 32 * These checks are not enabled in-product, as we want to allow older versions 33 * to keep working if we add new properties for whatever reason. 34 * 35 * @param {object} section 36 * The section to check to see if an additionalProperties flag should be added. 37 */ 38 function disallowAdditionalProperties(section) { 39 // It is generally acceptable for new properties to be added to the 40 // configuration as older builds will ignore them. 41 // 42 // As a result, we only check for new properties on nightly builds, and this 43 // avoids us having to uplift schema changes. This also helps preserve the 44 // schemas as documentation of "what was supported in this version". 45 if (!AppConstants.NIGHTLY_BUILD) { 46 info("Skipping additional properties validation."); 47 return; 48 } 49 50 if (section.type == "object" && (!"additionalProperties") in section) { 51 section.additionalProperties = false; 52 } 53 for (let value of Object.values(section)) { 54 if (isObject(value)) { 55 disallowAdditionalProperties(value); 56 } 57 } 58 } 59 60 add_task(async function test_search_telemetry_validates_to_schema() { 61 let schema = await IOUtils.readJSON( 62 PathUtils.join(do_get_cwd().path, "search-telemetry-v2-schema.json") 63 ); 64 disallowAdditionalProperties(schema); 65 66 let data = await RemoteSettings(TELEMETRY_SETTINGS_KEY).get(); 67 68 let validator = new JsonSchema.Validator(schema); 69 70 for (let entry of data) { 71 // Records in Remote Settings contain additional properties independent of 72 // the schema. Hence, we don't want to validate their presence. 73 delete entry.schema; 74 delete entry.id; 75 delete entry.last_modified; 76 delete entry.filter_expression; 77 78 let result = validator.validate(entry); 79 let message = `Should validate ${entry.telemetryId}`; 80 if (!result.valid) { 81 message += `:\n${JSON.stringify(result.errors, null, 2)}`; 82 } 83 Assert.ok(result.valid, message); 84 } 85 }); 86 87 add_task(async function test_search_config_codes_in_search_telemetry() { 88 let searchTelemetry = await RemoteSettings(TELEMETRY_SETTINGS_KEY).get(); 89 90 let selector = new SearchEngineSelector(() => {}); 91 let searchConfig = await selector.getEngineConfiguration(); 92 93 const telemetryIdToSearchEngineIdMap = new Map([["duckduckgo", "ddg"]]); 94 95 for (let telemetryEntry of searchTelemetry) { 96 info(`Checking: ${telemetryEntry.telemetryId}`); 97 let engine; 98 for (let entry of searchConfig) { 99 if (entry.recordType != "engine") { 100 continue; 101 } 102 if ( 103 entry.identifier == telemetryEntry.telemetryId || 104 entry.identifier == 105 telemetryIdToSearchEngineIdMap.get(telemetryEntry.telemetryId) 106 ) { 107 engine = entry; 108 } 109 } 110 Assert.ok( 111 !!engine, 112 `Should have associated engine data for telemetry id ${telemetryEntry.telemetryId}` 113 ); 114 115 if (engine.base.partnerCode) { 116 Assert.ok( 117 telemetryEntry.taggedCodes.includes(engine.base.partnerCode), 118 `Should have the base partner code ${engine.base.partnerCode} listed in the search telemetry 'taggedCodes'` 119 ); 120 } else { 121 Assert.ok( 122 ["google", "baidu"].includes(telemetryEntry.telemetryId), 123 "Should only not have a base partner code for Google and Baidu" 124 ); 125 } 126 127 if (engine.variants) { 128 for (let variant of engine.variants) { 129 if ("partnerCode" in variant) { 130 Assert.ok( 131 telemetryEntry.taggedCodes.includes(variant.partnerCode), 132 `Should have the partner code ${variant.partnerCode} listed in the search telemetry 'taggedCodes'` 133 ); 134 } 135 136 if (variant.subVariants) { 137 for (let subVariant of variant.subVariants) { 138 if ("partnerCode" in subVariant) { 139 Assert.ok( 140 telemetryEntry.taggedCodes.includes(subVariant.partnerCode), 141 `Should have the partner code ${subVariant.partnerCode} listed in the search telemetry 'taggedCodes'` 142 ); 143 } 144 } 145 } 146 } 147 } 148 } 149 });