Bug 1943642 - NewTab JOG dynamic metrics registration r=home-newtab-reviewers,mconley
Differential Revision: https://phabricator.services.mozilla.com/D244846
This commit is contained in:
270
browser/extensions/newtab/lib/NewTabGleanUtils.sys.mjs
Normal file
270
browser/extensions/newtab/lib/NewTabGleanUtils.sys.mjs
Normal file
@@ -0,0 +1,270 @@
|
||||
/* 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/. */
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
|
||||
return console.createInstance({
|
||||
prefix: "NewTabGleanUtils",
|
||||
maxLogLevel: Services.prefs.getBoolPref(
|
||||
"browser.newtab.builtin-addon.log",
|
||||
false
|
||||
)
|
||||
? "Debug"
|
||||
: "Warn",
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Module for managing Glean telemetry metrics and pings in the New Tab page.
|
||||
* This object provides functionality to:
|
||||
* - Read and parse JSON configuration files containing metrics and ping definitions
|
||||
* - Register metrics and pings at runtime
|
||||
* - Convert between different naming conventions (dotted snake case, kebab case, camel case)
|
||||
* - Handle metric and ping registration with proper error handling and logging
|
||||
*/
|
||||
export const NewTabGleanUtils = {
|
||||
/**
|
||||
* Fetches and parses a JSON file from a given resource URI.
|
||||
*
|
||||
* @param {string} resourceURI - The URI of the JSON file to fetch and parse
|
||||
* @returns {Promise<Object>} A promise that resolves to the parsed JSON object
|
||||
*/
|
||||
async readJSON(resourceURI) {
|
||||
let result = await fetch(resourceURI);
|
||||
return result.json();
|
||||
},
|
||||
/**
|
||||
* Processes and registers Glean metrics and pings from a JSON configuration file.
|
||||
* This method performs two main operations:
|
||||
* 1. Registers all pings defined in the configuration
|
||||
* 2. Registers all metrics under their respective categories
|
||||
* Example: await NewTabGleanUtils.registerMetricsAndPings("resource://path/to/metrics.json");
|
||||
*
|
||||
* @param {string} resourceURI - The URI of the JSON file containing metrics and pings definitions
|
||||
* @returns {Promise<boolean>} A promise that resolves when all metrics and pings are registered
|
||||
* If a metric or ping registration fails, all further registration halts and this Promise
|
||||
* will still resolve (errors will be logged to the console).
|
||||
*/
|
||||
async registerMetricsAndPings(resourceURI) {
|
||||
try {
|
||||
const data = await this.readJSON(resourceURI);
|
||||
|
||||
// Check if data exists and has either metrics or pings to register
|
||||
if (!data || (!data.metrics && !data.pings)) {
|
||||
lazy.logConsole.log("No metrics or pings found in the JSON file");
|
||||
return false;
|
||||
}
|
||||
|
||||
// First register all pings from the JSON file
|
||||
if (data.pings) {
|
||||
for (const [pingName, pingConfig] of Object.entries(data.pings)) {
|
||||
await this.registerPingIfNeeded({
|
||||
name: pingName,
|
||||
...pingConfig,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Then register all metrics under their respective categories
|
||||
if (data.metrics) {
|
||||
for (const [category, metrics] of Object.entries(data.metrics)) {
|
||||
for (const [name, config] of Object.entries(metrics)) {
|
||||
await this.registerMetricIfNeeded({
|
||||
...config,
|
||||
category,
|
||||
name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
lazy.logConsole.debug(
|
||||
"Successfully registered metrics and pings found in the JSON file"
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
lazy.logConsole.error(
|
||||
"Failed to complete registration of metrics and pings found in runtime metrics JSON:",
|
||||
e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers a metric in Glean if it doesn't already exist.
|
||||
* @param {Object} options - The metric configuration options
|
||||
* @param {string} options.type - The type of metric (e.g., "text", "counter")
|
||||
* @param {string} options.category - The category the metric belongs to
|
||||
* @param {string} options.name - The name of the metric
|
||||
* @param {string[]} options.pings - Array of ping names this metric belongs to
|
||||
* @param {string} options.lifetime - The lifetime of the metric
|
||||
* @param {boolean} [options.disabled] - Whether the metric is disabled
|
||||
* @param {Object} [options.extraArgs] - Additional arguments for the metric
|
||||
* @throws {Error} If a new metrics registration fails and error will be logged in console
|
||||
*/
|
||||
registerMetricIfNeeded(options) {
|
||||
const { type, category, name, pings, lifetime, disabled, extraArgs } =
|
||||
options;
|
||||
|
||||
try {
|
||||
let categoryName = this.dottedSnakeToCamel(category);
|
||||
let metricName = this.dottedSnakeToCamel(name);
|
||||
|
||||
if (categoryName in Glean && metricName in Glean[categoryName]) {
|
||||
lazy.logConsole.warn(
|
||||
`Fail to register metric ${name} in category ${category} as it already exists`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert extraArgs to JSON string for metrics type event
|
||||
let extraArgsJson = null;
|
||||
if (type === "event" && extraArgs && Object.keys(extraArgs).length) {
|
||||
extraArgsJson = JSON.stringify(extraArgs);
|
||||
}
|
||||
|
||||
// Metric doesn't exist, register it
|
||||
lazy.logConsole.debug(`Registering metric ${name} at runtime`);
|
||||
|
||||
// Register the metric
|
||||
Services.fog.testRegisterRuntimeMetric(
|
||||
type,
|
||||
category,
|
||||
name,
|
||||
pings,
|
||||
`"${lifetime}"`,
|
||||
disabled,
|
||||
extraArgsJson
|
||||
);
|
||||
} catch (e) {
|
||||
lazy.logConsole.error(`Error registering metric ${name}: ${e}`);
|
||||
throw new Error(`Failure while registering metrics ${name} `);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers a ping in Glean if it doesn't already exist.
|
||||
* @param {Object} options - The ping configuration options
|
||||
* @param {string} options.name - The name of the ping
|
||||
* @param {boolean} [options.includeClientId] - Whether to include client ID
|
||||
* @param {boolean} [options.sendIfEmpty] - Whether to send ping if empty
|
||||
* @param {boolean} [options.preciseTimestamps] - Whether to use precise timestamps
|
||||
* @param {boolean} [options.includeInfoSections] - Whether to include info sections
|
||||
* @param {boolean} [options.enabled] - Whether the ping is enabled
|
||||
* @param {string[]} [options.schedulesPings] - Array of scheduled ping times
|
||||
* @param {string[]} [options.reasonCodes] - Array of valid reason codes
|
||||
* @param {boolean} [options.followsCollectionEnabled] - Whether ping follows collection enabled state
|
||||
* @param {string[]} [options.uploaderCapabilities] - Array of uploader capabilities for this ping
|
||||
* @throws {Error} If a new ping registration fails and error will be logged in console
|
||||
*/
|
||||
registerPingIfNeeded(options) {
|
||||
const {
|
||||
name,
|
||||
includeClientId,
|
||||
sendIfEmpty,
|
||||
preciseTimestamps,
|
||||
includeInfoSections,
|
||||
enabled,
|
||||
schedulesPings,
|
||||
reasonCodes,
|
||||
followsCollectionEnabled,
|
||||
uploaderCapabilities,
|
||||
} = options;
|
||||
|
||||
try {
|
||||
let pingName = this.kebabToCamel(name);
|
||||
if (pingName in GleanPings) {
|
||||
lazy.logConsole.warn(
|
||||
`Fail to register ping ${name} as it already exists`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ping doesn't exist, register it
|
||||
lazy.logConsole.debug(`Registering ping ${name} at runtime`);
|
||||
|
||||
Services.fog.testRegisterRuntimePing(
|
||||
name,
|
||||
includeClientId,
|
||||
sendIfEmpty,
|
||||
preciseTimestamps,
|
||||
includeInfoSections,
|
||||
enabled,
|
||||
schedulesPings,
|
||||
reasonCodes,
|
||||
followsCollectionEnabled,
|
||||
uploaderCapabilities
|
||||
);
|
||||
} catch (e) {
|
||||
lazy.logConsole.error(`Error registering ping ${name}: ${e}`);
|
||||
throw new Error(`Failure while registering ping ${name} `);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts a dotted snake case string to camel case.
|
||||
* Example: "foo.bar_baz" becomes "fooBarBaz"
|
||||
* @param {string} metricNameOrCategory - The string in dotted snake case format
|
||||
* @returns {string} The converted camel case string
|
||||
*/
|
||||
dottedSnakeToCamel(metricNameOrCategory) {
|
||||
if (!metricNameOrCategory) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let camel = "";
|
||||
// Split by underscore and then by dots
|
||||
const segments = metricNameOrCategory.split("_");
|
||||
for (const segment of segments) {
|
||||
const parts = segment.split(".");
|
||||
for (const part of parts) {
|
||||
if (!camel) {
|
||||
camel += part;
|
||||
} else if (part.length) {
|
||||
const firstChar = part.charAt(0);
|
||||
if (firstChar >= "a" && firstChar <= "z") {
|
||||
// Capitalize first letter and append rest of the string
|
||||
camel += firstChar.toUpperCase() + part.slice(1);
|
||||
} else {
|
||||
// If first char is not a-z, append as is
|
||||
camel += part;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return camel;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts a kebab case string to camel case.
|
||||
* Example: "foo-bar-baz" becomes "fooBarBaz"
|
||||
* @param {string} pingName - The string in kebab case format
|
||||
* @returns {string} The converted camel case string
|
||||
*/
|
||||
kebabToCamel(pingName) {
|
||||
if (!pingName) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let camel = "";
|
||||
// Split by hyphens
|
||||
const segments = pingName.split("-");
|
||||
for (const segment of segments) {
|
||||
if (!camel) {
|
||||
camel += segment;
|
||||
} else if (segment.length) {
|
||||
const firstChar = segment.charAt(0);
|
||||
if (firstChar >= "a" && firstChar <= "z") {
|
||||
// Capitalize first letter and append rest of the string
|
||||
camel += firstChar.toUpperCase() + segment.slice(1);
|
||||
} else {
|
||||
// If first char is not a-z, append as is
|
||||
camel += segment;
|
||||
}
|
||||
}
|
||||
}
|
||||
return camel;
|
||||
},
|
||||
};
|
||||
@@ -22,6 +22,7 @@ const SUPPORTED_LOCALES =
|
||||
const lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
AboutHomeStartupCache: "resource:///modules/AboutHomeStartupCache.sys.mjs",
|
||||
NewTabGleanUtils: "resource://newtab/lib/NewTabGleanUtils.sys.mjs",
|
||||
});
|
||||
|
||||
const ResourceSubstitution = "newtab";
|
||||
@@ -29,6 +30,15 @@ const ResourceSubstitution = "newtab";
|
||||
this.builtin_newtab = class extends ExtensionAPI {
|
||||
#chromeHandle = null;
|
||||
|
||||
async registerMetricsFromJson() {
|
||||
// The metrics we need to process were placed in webext-glue/metrics/runtime-metrics-<version>.json
|
||||
// That file will be generated by build scipt getting implemented with Bug 1960111
|
||||
const version = AppConstants.MOZ_APP_VERSION.match(/\d+/)[0];
|
||||
const metricsPath = `webext-glue/metrics/runtime-metrics-${version}.json`;
|
||||
|
||||
lazy.NewTabGleanUtils.registerMetricsAndPings(`resource://newtab/${metricsPath}`);
|
||||
}
|
||||
|
||||
onStartup() {
|
||||
if (!AppConstants.BROWSER_NEWTAB_AS_ADDON) {
|
||||
// If we're here, this must be the first launch of a profile where this
|
||||
@@ -73,9 +83,9 @@ this.builtin_newtab = class extends ExtensionAPI {
|
||||
);
|
||||
L10nRegistry.getInstance().registerSources([newtabFileSource]);
|
||||
|
||||
// TODO: Dynamically register any Glean pings/metrics here.
|
||||
// Dynamically register any Glean pings/metrics here.
|
||||
this.registerMetricsFromJson();
|
||||
}
|
||||
|
||||
redirector.builtInAddonInitialized();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"metrics": {
|
||||
"newtab": {
|
||||
"wallpaper_click": {
|
||||
"type": "text",
|
||||
"description": "Number of clicks",
|
||||
"lifetime": "ping",
|
||||
"pings": ["newtab"],
|
||||
"disabled": false
|
||||
},
|
||||
"load_time": {
|
||||
"type": "event",
|
||||
"description": "Time taken to load and render",
|
||||
"lifetime": "ping",
|
||||
"pings": ["spoc-1"],
|
||||
"disabled": false,
|
||||
"extraArgs": {
|
||||
"allowed_extra_keys": ["is_page_followed", "newtab_visit_id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pings": {
|
||||
"messaging-system": {
|
||||
"includeClientId": false,
|
||||
"sendIfEmpty": false,
|
||||
"preciseTimestamps": true,
|
||||
"includeInfoSections": true,
|
||||
"enabled": true,
|
||||
"schedulesPings": [],
|
||||
"reasonCodes": [],
|
||||
"followsCollectionEnabled": true
|
||||
},
|
||||
"spoc-1": {
|
||||
"includeClientId": false,
|
||||
"sendIfEmpty": false,
|
||||
"preciseTimestamps": true,
|
||||
"includeInfoSections": true,
|
||||
"enabled": true,
|
||||
"schedulesPings": [],
|
||||
"reasonCodes": ["click", "impression", "save"],
|
||||
"followsCollectionEnabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"metrics": {
|
||||
"newtab": {
|
||||
"wallpaper_click": {
|
||||
"type": "text",
|
||||
"description": "Number of clicks",
|
||||
"lifetime": "ping",
|
||||
"pings": ["newtab"],
|
||||
"disabled": false
|
||||
},
|
||||
"load_time": {
|
||||
"type": "event",
|
||||
"description": "Time taken to load and render",
|
||||
"lifetime": "ping",
|
||||
"pings": ["spoc-1"],
|
||||
"disabled": false,
|
||||
"extraArgs": {
|
||||
"allowed_extra_keys": ["is_page_followed", "newtab_visit_id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pings": {
|
||||
"messaging-system": {
|
||||
"includeClientId": false,
|
||||
"sendIfEmpty": false,
|
||||
"preciseTimestamps": true,
|
||||
"includeInfoSections": true,
|
||||
"enabled": true,
|
||||
"schedulesPings": [],
|
||||
"reasonCodes": [],
|
||||
"followsCollectionEnabled": true,
|
||||
"uploaderCapabilities": []
|
||||
},
|
||||
"spoc-1": {
|
||||
"includeClientId": false,
|
||||
"sendIfEmpty": false,
|
||||
"preciseTimestamps": true,
|
||||
"includeInfoSections": true,
|
||||
"enabled": true,
|
||||
"schedulesPings": [],
|
||||
"reasonCodes": ["click", "impression", "save"],
|
||||
"followsCollectionEnabled": true,
|
||||
"uploaderCapabilities": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ FINAL_TARGET_FILES += [
|
||||
]
|
||||
FINAL_TARGET_FILES["webext-glue"] += [
|
||||
"background.js",
|
||||
"metrics/**",
|
||||
"schema.json",
|
||||
]
|
||||
FINAL_TARGET_PP_FILES["webext-glue"] += [
|
||||
|
||||
Reference in New Issue
Block a user