tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit d21bddc27e27912b00ad5d99e4f1ad6f83b97134
parent 30b7a8312f9f3ed624ddc2f1d43e068a76e6f180
Author: Erik Nordin <enordin@mozilla.com>
Date:   Tue,  4 Nov 2025 21:51:33 +0000

Bug 1997612 - Introduce Translations telemetry rate limits r=translations-reviewers,gregtatum

This patch introduces a set of rate limits for certain kinds
of metrics collected by the Translations telemetry, based on
prior data usage, that I believe will still get us sufficient
sample sizes without collecting excessive amounts of samples.

Important metrics, such as `translation_request` and `error`
are left untouched.

Differential Revision: https://phabricator.services.mozilla.com/D270876

Diffstat:
Mtoolkit/components/translations/TranslationsTelemetry.sys.mjs | 992++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
1 file changed, 655 insertions(+), 337 deletions(-)

diff --git a/toolkit/components/translations/TranslationsTelemetry.sys.mjs b/toolkit/components/translations/TranslationsTelemetry.sys.mjs @@ -4,6 +4,8 @@ // @ts-check +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + /** * @typedef {object} Lazy * @property {typeof console} console @@ -12,6 +14,12 @@ /** @type {Lazy} */ const lazy = /** @type {any} */ ({}); +/** + * @typedef {{ flowId: string, randomRoll: number }} FlowContext + * @typedef {"default" | "nightly" | "beta" | "esr" | "release" | "unofficial"} UpdateChannel + * @typedef {Partial<Record<UpdateChannel, number>>} SampleRates + */ + ChromeUtils.defineLazyGetter(lazy, "console", () => { return console.createInstance({ maxLogLevelPref: "toolkit.telemetry.translations.logLevel", @@ -24,9 +32,67 @@ ChromeUtils.defineLazyGetter(lazy, "console", () => { */ export class TranslationsTelemetry { /** - * A cached value to hold the current flowId. + * The context of the current UI flow of events. + * + * This contains the flowId, which is a randomly generated UUID to + * link telemetry UI events to one another. + * + * This also contains a random number in the range [0, 1) that will + * be associated with the flowId. This allows us to skip all events + * in the same flow based on our rate limits. + * + * @type {FlowContext} + */ + static #flowContext; + + /** + * Page loads happen quite frequently, so we want to avoid firing a telemetry + * event for every page load that occurs. These rate limits will help keep our + * page-load telemetry to a reasonable size, without affecting significance. + * + * @type {SampleRates} + */ + // prettier-ignore + static #PAGE_LOAD_RATE_LIMITS = { + nightly: 1 / 1_000, + beta: 1 / 10_000, + esr: 1 / 10_000, + release: 1 / 100_000, + }; + + /** + * TranslationsEngine performance metrics occur every time an active engine is + * destroyed, but this is aggregate performance data, and we don't need to have + * a sample for every single engine that is created. + * + * @type {SampleRates} */ - static #flowId = null; + // prettier-ignore + static #ENGINE_PERFORMANCE_RATE_LIMITS = { + beta: 1 / 100, + esr: 1 / 100, + release: 1 / 1_000, + }; + + /** + * An array of a single unsigned 32-bit integer that we will use to generate random values. + * + * @type {Uint32Array} + */ + static #RANDOM_VALUE = new Uint32Array(1); + + /** + * Generates a random number in the range [0, 1). + * + * @returns {number} + */ + static #randomRoll() { + // Generate a uniformly-distributed, random u32 in #RANDOM_VALUE. + crypto.getRandomValues(TranslationsTelemetry.#RANDOM_VALUE); + + // Scale the random u32 to the range [0, 1). + return TranslationsTelemetry.#RANDOM_VALUE[0] / Math.pow(2, 32); + } /** * Logs the telemetry event to the console if enabled by toolkit.telemetry.translations.logLevel @@ -35,7 +101,8 @@ export class TranslationsTelemetry { * @param {object} [data] - Optional data passed to telemetry. */ static logEventToConsole(caller, data) { - const logId = TranslationsTelemetry.getOrCreateFlowId().substring(0, 5); + const flowContext = TranslationsTelemetry.getOrCreateFlowContext(); + const logId = flowContext.flowId.substring(0, 5); lazy.console?.log( `flowId[${logId}]: ${caller.name}`, ...(data ? [data] : []) @@ -43,6 +110,86 @@ export class TranslationsTelemetry { } /** + * Determines whether the caller should skip telemetry based on the current update channel. + * + * Passing the optional FlowContext will use the randomRoll associated with that flowId, + * ensuring that each event for the entire flow adheres to the same rate-limit threshold. + * + * @param {SampleRates} sampleRates + * - A mapping of update-channel names to sampling probabilities in the range [0, 1). + * Channels omitted from the mapping are never skipped. + * @param {FlowContext} [flowContext] + * - The context that contains the flow id and a random roll for the entire flow. + * + * @returns {boolean} True if the event should be skipped for this channel, otherwise false. + */ + static shouldSkipSample(sampleRates, flowContext) { + const channel = AppConstants.MOZ_UPDATE_CHANNEL; + const sampleRate = sampleRates[channel]; + + let randomRoll; + + if (lazy.console?.shouldLog("Debug")) { + randomRoll = + flowContext?.randomRoll ?? TranslationsTelemetry.#randomRoll(); + + lazy.console.debug({ + randomRoll: randomRoll.toFixed(8), + default: { + skip: randomRoll >= (sampleRates.default ?? 1), + threshold: sampleRates.default, + }, + nightly: { + skip: randomRoll >= (sampleRates.nightly ?? 1), + threshold: sampleRates.nightly, + }, + beta: { + skip: randomRoll >= (sampleRates.beta ?? 1), + threshold: sampleRates.beta, + }, + esr: { + skip: randomRoll >= (sampleRates.esr ?? 1), + threshold: sampleRates.esr, + }, + release: { + skip: randomRoll >= (sampleRates.release ?? 1), + threshold: sampleRates.release, + }, + }); + } + + if (sampleRate === undefined) { + // Never skip when a rate limit is unspecified. + return false; + } + + if (randomRoll === undefined) { + randomRoll = + flowContext?.randomRoll ?? TranslationsTelemetry.#randomRoll(); + } + + return randomRoll >= sampleRate; + } + + /** + * Invokes or skips the given callback function according to the provided rate limits. + * + * @param {SampleRates} sampleRates + * - A mapping of update-channel names to sampling probabilities in the range [0, 1). + * @param {function(FlowContext): void} callback + * - The function invoked when the event should be recorded. + */ + static withRateLimits(sampleRates, callback) { + const flowContext = TranslationsTelemetry.getOrCreateFlowContext(); + + if (TranslationsTelemetry.shouldSkipSample(sampleRates, flowContext)) { + return; + } + + callback(flowContext); + } + + /** * Telemetry functions for the Full Page Translations panel. * * @returns {FullPageTranslationsPanelTelemetry} @@ -72,28 +219,33 @@ export class TranslationsTelemetry { /** * Forces the creation of a new Translations telemetry flowId and returns it. * - * @returns {string} + * @returns {FlowContext} */ - static createFlowId() { - const flowId = crypto.randomUUID(); - TranslationsTelemetry.#flowId = flowId; - return flowId; + static createFlowContext() { + const flowContext = { + flowId: crypto.randomUUID(), + randomRoll: TranslationsTelemetry.#randomRoll(), + }; + + TranslationsTelemetry.#flowContext = flowContext; + + return flowContext; } /** * Returns a Translations telemetry flowId by retrieving the cached value * if available, or creating a new one otherwise. * - * @returns {string} + * @returns {FlowContext} */ - static getOrCreateFlowId() { + static getOrCreateFlowContext() { // If we have the flowId cached, return it. - if (TranslationsTelemetry.#flowId) { - return TranslationsTelemetry.#flowId; + if (TranslationsTelemetry.#flowContext) { + return TranslationsTelemetry.#flowContext; } // If no flowId exists, create one. - return TranslationsTelemetry.createFlowId(); + return TranslationsTelemetry.createFlowContext(); } /** @@ -111,31 +263,37 @@ export class TranslationsTelemetry { * @param {boolean} data.confident */ static onIdentifyPageLanguage(data) { - const { - htmlLangAttribute, - identifiedLanguage, - langTagsMatch, - isLangAttributeValid, - extractedCodeUnits, - extractionTime, - identificationTime, - totalTime, - confident, - } = data; - Glean.translations.identifyPageLanguage.record({ - html_lang_attribute: htmlLangAttribute, - identified_language: identifiedLanguage, - lang_tags_match: langTagsMatch, - is_lang_attribute_valid: isLangAttributeValid, - extracted_code_units: extractedCodeUnits, - extraction_time: extractionTime, - identification_time: identificationTime, - total_time: totalTime, - confident, - }); - TranslationsTelemetry.logEventToConsole( - TranslationsTelemetry.onIdentifyPageLanguage, - data + TranslationsTelemetry.withRateLimits( + TranslationsTelemetry.#PAGE_LOAD_RATE_LIMITS, + () => { + const { + htmlLangAttribute, + identifiedLanguage, + langTagsMatch, + isLangAttributeValid, + extractedCodeUnits, + extractionTime, + identificationTime, + totalTime, + confident, + } = data; + + Glean.translations.identifyPageLanguage.record({ + html_lang_attribute: htmlLangAttribute, + identified_language: identifiedLanguage, + lang_tags_match: langTagsMatch, + is_lang_attribute_valid: isLangAttributeValid, + extracted_code_units: extractedCodeUnits, + extraction_time: extractionTime, + identification_time: identificationTime, + total_time: totalTime, + confident, + }); + TranslationsTelemetry.logEventToConsole( + TranslationsTelemetry.onIdentifyPageLanguage, + data + ); + } ); } @@ -146,7 +304,7 @@ export class TranslationsTelemetry { */ static onError(errorMessage) { Glean.translations.error.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + flow_id: TranslationsTelemetry.getOrCreateFlowContext().flowId, reason: errorMessage, }); TranslationsTelemetry.logEventToConsole(TranslationsTelemetry.onError, { @@ -180,7 +338,7 @@ export class TranslationsTelemetry { } = data; Glean.translations.requestCount[requestTarget ?? "full_page"].add(1); Glean.translations.translationRequest.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + flow_id: TranslationsTelemetry.getOrCreateFlowContext().flowId, from_language: sourceLanguage, to_language: targetLanguage, auto_translate: autoTranslate, @@ -198,7 +356,7 @@ export class TranslationsTelemetry { static onRestorePage() { Glean.translations.restorePage.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + flow_id: TranslationsTelemetry.getOrCreateFlowContext().flowId, }); TranslationsTelemetry.logEventToConsole( TranslationsTelemetry.onRestorePage @@ -216,37 +374,43 @@ export class TranslationsTelemetry { * @param {number} data.totalTranslatedWords */ static onReportEnginePerformance(data) { - const { - sourceLanguage, - targetLanguage, - totalCompletedRequests, - totalInferenceSeconds, - totalTranslatedWords, - } = data; + TranslationsTelemetry.withRateLimits( + TranslationsTelemetry.#ENGINE_PERFORMANCE_RATE_LIMITS, + () => { + const { + sourceLanguage, + targetLanguage, + totalCompletedRequests, + totalInferenceSeconds, + totalTranslatedWords, + } = data; - const averageWordsPerRequest = - totalTranslatedWords / totalCompletedRequests; - const averageWordsPerSecond = totalTranslatedWords / totalInferenceSeconds; + const averageWordsPerRequest = + totalTranslatedWords / totalCompletedRequests; + const averageWordsPerSecond = + totalTranslatedWords / totalInferenceSeconds; - Glean.translations.enginePerformance.record({ - from_language: sourceLanguage, - to_language: targetLanguage, - average_words_per_request: averageWordsPerRequest, - average_words_per_second: averageWordsPerSecond, - total_completed_requests: totalCompletedRequests, - total_inference_seconds: totalInferenceSeconds, - total_translated_words: totalTranslatedWords, - }); - TranslationsTelemetry.logEventToConsole( - TranslationsTelemetry.onReportEnginePerformance, - { - sourceLanguage, - targetLanguage, - averageWordsPerSecond, - averageWordsPerRequest, - totalCompletedRequests, - totalInferenceSeconds, - totalTranslatedWords, + Glean.translations.enginePerformance.record({ + from_language: sourceLanguage, + to_language: targetLanguage, + average_words_per_request: averageWordsPerRequest, + average_words_per_second: averageWordsPerSecond, + total_completed_requests: totalCompletedRequests, + total_inference_seconds: totalInferenceSeconds, + total_translated_words: totalTranslatedWords, + }); + TranslationsTelemetry.logEventToConsole( + TranslationsTelemetry.onReportEnginePerformance, + { + sourceLanguage, + targetLanguage, + averageWordsPerSecond, + averageWordsPerRequest, + totalCompletedRequests, + totalInferenceSeconds, + totalTranslatedWords, + } + ); } ); } @@ -257,6 +421,30 @@ export class TranslationsTelemetry { */ class FullPageTranslationsPanelTelemetry { /** + * The Full-Page panel events help us determine how users interact with the panel UI. + * The panel can be opened quite frequently, and we don't need to collect data every + * single time the panel opens in order to get a good idea of interaction behavior. + * + * @type {SampleRates} + */ + // prettier-ignore + static #FULL_PAGE_PANEL_RATE_LIMITS = { + release: 1 / 100, + }; + + /** + * Invokes a callback function with #FULL_PAGE_PANEL_RATE_LIMITS. + * + * @param {function(FlowContext): void} callback + */ + static #withRateLimits(callback) { + TranslationsTelemetry.withRateLimits( + FullPageTranslationsPanelTelemetry.#FULL_PAGE_PANEL_RATE_LIMITS, + callback + ); + } + + /** * Records a telemetry event when the FullPageTranslationsPanel is opened. * * @param {object} data @@ -267,236 +455,284 @@ class FullPageTranslationsPanelTelemetry { * @param {boolean} data.openedFromAppMenu */ static onOpen(data) { - Glean.translationsPanel.open.record({ - flow_id: data.maintainFlow - ? TranslationsTelemetry.getOrCreateFlowId() - : TranslationsTelemetry.createFlowId(), - auto_show: data.autoShow, - view_name: data.viewName, - document_language: data.docLangTag, - opened_from: data.openedFromAppMenu ? "appMenu" : "translationsButton", + if (!data.maintainFlow) { + // Create a new flow context when the panel opens, + // unless explicitly instructed to maintain the current flow. + TranslationsTelemetry.createFlowContext(); + } + + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.open.record({ + flow_id: flowId, + auto_show: data.autoShow, + view_name: data.viewName, + document_language: data.docLangTag, + opened_from: data.openedFromAppMenu ? "appMenu" : "translationsButton", + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onOpen, + data + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onOpen, - data - ); } static onClose() { - Glean.translationsPanel.close.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.close.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onClose + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onClose - ); } static onOpenFromLanguageMenu() { - Glean.translationsPanel.openFromLanguageMenu.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.openFromLanguageMenu.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onOpenFromLanguageMenu + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onOpenFromLanguageMenu - ); } static onChangeFromLanguage(langTag) { - Glean.translationsPanel.changeFromLanguage.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), - language: langTag, + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.changeFromLanguage.record({ + flow_id: flowId, + language: langTag, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onChangeFromLanguage, + { + langTag, + } + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onChangeFromLanguage, - { - langTag, - } - ); } static onCloseFromLanguageMenu() { - Glean.translationsPanel.closeFromLanguageMenu.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.closeFromLanguageMenu.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onCloseFromLanguageMenu + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onCloseFromLanguageMenu - ); } static onOpenToLanguageMenu() { - Glean.translationsPanel.openToLanguageMenu.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.openToLanguageMenu.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onOpenToLanguageMenu + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onOpenToLanguageMenu - ); } static onChangeToLanguage(langTag) { - Glean.translationsPanel.changeToLanguage.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), - language: langTag, + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.changeToLanguage.record({ + flow_id: flowId, + language: langTag, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onChangeToLanguage, + { + langTag, + } + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onChangeToLanguage, - { - langTag, - } - ); } static onCloseToLanguageMenu() { - Glean.translationsPanel.closeToLanguageMenu.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.closeToLanguageMenu.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onCloseToLanguageMenu + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onChangeToLanguage - ); } static onOpenSettingsMenu() { - Glean.translationsPanel.openSettingsMenu.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.openSettingsMenu.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onOpenSettingsMenu + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onOpenSettingsMenu - ); } static onCloseSettingsMenu() { - Glean.translationsPanel.closeSettingsMenu.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.closeSettingsMenu.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onCloseSettingsMenu + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onCloseSettingsMenu - ); } static onCancelButton() { - Glean.translationsPanel.cancelButton.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.cancelButton.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onCancelButton + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onCancelButton - ); } static onChangeSourceLanguageButton() { - Glean.translationsPanel.changeSourceLanguageButton.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.changeSourceLanguageButton.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onChangeSourceLanguageButton + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onChangeSourceLanguageButton - ); } static onDismissErrorButton() { - Glean.translationsPanel.dismissErrorButton.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.dismissErrorButton.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onDismissErrorButton + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onDismissErrorButton - ); } static onRestorePageButton() { - Glean.translationsPanel.restorePageButton.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.restorePageButton.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onRestorePageButton + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onRestorePageButton - ); } static onTranslateButton() { - Glean.translationsPanel.translateButton.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.translateButton.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onTranslateButton + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onTranslateButton - ); } static onAlwaysOfferTranslations(toggledOn) { - Glean.translationsPanel.alwaysOfferTranslations.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), - toggled_on: toggledOn, + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.alwaysOfferTranslations.record({ + flow_id: flowId, + toggled_on: toggledOn, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onAlwaysOfferTranslations, + { + toggledOn, + } + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onAlwaysOfferTranslations, - { - toggledOn, - } - ); } static onAlwaysTranslateLanguage(langTag, toggledOn) { - Glean.translationsPanel.alwaysTranslateLanguage.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), - language: langTag, - toggled_on: toggledOn, + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.alwaysTranslateLanguage.record({ + flow_id: flowId, + language: langTag, + toggled_on: toggledOn, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onAlwaysTranslateLanguage, + { + langTag, + toggledOn, + } + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onAlwaysTranslateLanguage, - { - langTag, - toggledOn, - } - ); } static onNeverTranslateLanguage(langTag, toggledOn) { - Glean.translationsPanel.neverTranslateLanguage.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), - language: langTag, - toggled_on: toggledOn, + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.neverTranslateLanguage.record({ + flow_id: flowId, + language: langTag, + toggled_on: toggledOn, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onNeverTranslateLanguage, + { + langTag, + toggledOn, + } + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onNeverTranslateLanguage, - { - langTag, - toggledOn, - } - ); } static onNeverTranslateSite(toggledOn) { - Glean.translationsPanel.neverTranslateSite.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), - toggled_on: toggledOn, + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.neverTranslateSite.record({ + flow_id: flowId, + toggled_on: toggledOn, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onNeverTranslateSite, + { + toggledOn, + } + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onNeverTranslateSite, - { - toggledOn, - } - ); } static onManageLanguages() { - Glean.translationsPanel.manageLanguages.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.manageLanguages.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onManageLanguages + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onManageLanguages - ); } static onAboutTranslations() { - Glean.translationsPanel.aboutTranslations.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.aboutTranslations.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onAboutTranslations + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onAboutTranslations - ); } static onLearnMoreLink() { - Glean.translationsPanel.learnMore.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsPanel.learnMore.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + FullPageTranslationsPanelTelemetry.onLearnMoreLink + ); }); - TranslationsTelemetry.logEventToConsole( - FullPageTranslationsPanelTelemetry.onLearnMoreLink - ); } } @@ -505,6 +741,25 @@ class FullPageTranslationsPanelTelemetry { */ class SelectTranslationsPanelTelemetry { /** + * These events do not yet occur frequently enough to benefit from rate limiting. + * + * @type {SampleRates} + */ + static #SELECT_PANEL_RATE_LIMITS = {}; + + /** + * Invokes a callback function with #SELECT_PANEL_RATE_LIMITS. + * + * @param {function(FlowContext): void} callback + */ + static #withRateLimits(callback) { + TranslationsTelemetry.withRateLimits( + SelectTranslationsPanelTelemetry.#SELECT_PANEL_RATE_LIMITS, + callback + ); + } + + /** * Records a telemetry event when the SelectTranslationsPanel is opened. * * @param {object} data @@ -516,56 +771,70 @@ class SelectTranslationsPanelTelemetry { * @param {string} data.textSource */ static onOpen(data) { - Glean.translationsSelectTranslationsPanel.open.record({ - flow_id: data.maintainFlow - ? TranslationsTelemetry.getOrCreateFlowId() - : TranslationsTelemetry.createFlowId(), - document_language: data.docLangTag, - from_language: data.sourceLanguage, - to_language: data.targetLanguage, - top_preferred_language: data.topPreferredLanguage, - text_source: data.textSource, + if (!data.maintainFlow) { + // Create a new flow context when the panel opens, + // unless explicitly instructed to maintain the current flow. + TranslationsTelemetry.createFlowContext(); + } + + SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsSelectTranslationsPanel.open.record({ + flow_id: flowId, + document_language: data.docLangTag, + from_language: data.sourceLanguage, + to_language: data.targetLanguage, + top_preferred_language: data.topPreferredLanguage, + text_source: data.textSource, + }); + TranslationsTelemetry.logEventToConsole( + SelectTranslationsPanelTelemetry.onOpen, + data + ); }); - TranslationsTelemetry.logEventToConsole( - SelectTranslationsPanelTelemetry.onOpen, - data - ); } static onClose() { - Glean.translationsSelectTranslationsPanel.close.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsSelectTranslationsPanel.close.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + SelectTranslationsPanelTelemetry.onClose + ); }); - TranslationsTelemetry.logEventToConsole( - SelectTranslationsPanelTelemetry.onClose - ); } static onCancelButton() { - Glean.translationsSelectTranslationsPanel.cancelButton.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsSelectTranslationsPanel.cancelButton.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + SelectTranslationsPanelTelemetry.onCancelButton + ); }); - TranslationsTelemetry.logEventToConsole( - SelectTranslationsPanelTelemetry.onCancelButton - ); } static onCopyButton() { - Glean.translationsSelectTranslationsPanel.copyButton.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsSelectTranslationsPanel.copyButton.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + SelectTranslationsPanelTelemetry.onCopyButton + ); }); - TranslationsTelemetry.logEventToConsole( - SelectTranslationsPanelTelemetry.onCopyButton - ); } static onDoneButton() { - Glean.translationsSelectTranslationsPanel.doneButton.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsSelectTranslationsPanel.doneButton.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + SelectTranslationsPanelTelemetry.onDoneButton + ); }); - TranslationsTelemetry.logEventToConsole( - SelectTranslationsPanelTelemetry.onDoneButton - ); } static onTranslateButton({ @@ -573,100 +842,118 @@ class SelectTranslationsPanelTelemetry { sourceLanguage, targetLanguage, }) { - Glean.translationsSelectTranslationsPanel.translateButton.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), - detected_language: detectedLanguage, - from_language: sourceLanguage, - to_language: targetLanguage, + SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsSelectTranslationsPanel.translateButton.record({ + flow_id: flowId, + detected_language: detectedLanguage, + from_language: sourceLanguage, + to_language: targetLanguage, + }); + TranslationsTelemetry.logEventToConsole( + SelectTranslationsPanelTelemetry.onTranslateButton, + { + detectedLanguage, + sourceLanguage, + targetLanguage, + } + ); }); - TranslationsTelemetry.logEventToConsole( - SelectTranslationsPanelTelemetry.onTranslateButton, - { - detectedLanguage, - sourceLanguage, - targetLanguage, - } - ); } static onTranslateFullPageButton() { - Glean.translationsSelectTranslationsPanel.translateFullPageButton.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsSelectTranslationsPanel.translateFullPageButton.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + SelectTranslationsPanelTelemetry.onTranslateFullPageButton + ); }); - TranslationsTelemetry.logEventToConsole( - SelectTranslationsPanelTelemetry.onTranslateFullPageButton - ); } static onTryAgainButton() { - Glean.translationsSelectTranslationsPanel.tryAgainButton.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsSelectTranslationsPanel.tryAgainButton.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + SelectTranslationsPanelTelemetry.onTryAgainButton + ); }); - TranslationsTelemetry.logEventToConsole( - SelectTranslationsPanelTelemetry.onTryAgainButton - ); } static onChangeFromLanguage({ previousLangTag, currentLangTag, docLangTag }) { - Glean.translationsSelectTranslationsPanel.changeFromLanguage.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), - document_language: docLangTag, - previous_language: previousLangTag, - language: currentLangTag, + SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsSelectTranslationsPanel.changeFromLanguage.record({ + flow_id: flowId, + document_language: docLangTag, + previous_language: previousLangTag, + language: currentLangTag, + }); + TranslationsTelemetry.logEventToConsole( + SelectTranslationsPanelTelemetry.onChangeFromLanguage, + { previousLangTag, currentLangTag, docLangTag } + ); }); - TranslationsTelemetry.logEventToConsole( - SelectTranslationsPanelTelemetry.onChangeFromLanguage, - { previousLangTag, currentLangTag, docLangTag } - ); } static onChangeToLanguage(langTag) { - Glean.translationsSelectTranslationsPanel.changeToLanguage.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), - language: langTag, + SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsSelectTranslationsPanel.changeToLanguage.record({ + flow_id: flowId, + language: langTag, + }); + TranslationsTelemetry.logEventToConsole( + SelectTranslationsPanelTelemetry.onChangeToLanguage, + { langTag } + ); }); - TranslationsTelemetry.logEventToConsole( - SelectTranslationsPanelTelemetry.onChangeToLanguage, - { langTag } - ); } static onOpenSettingsMenu() { - Glean.translationsSelectTranslationsPanel.openSettingsMenu.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsSelectTranslationsPanel.openSettingsMenu.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + SelectTranslationsPanelTelemetry.onOpenSettingsMenu + ); }); - TranslationsTelemetry.logEventToConsole( - SelectTranslationsPanelTelemetry.onOpenSettingsMenu - ); } static onTranslationSettings() { - Glean.translationsSelectTranslationsPanel.translationSettings.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsSelectTranslationsPanel.translationSettings.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + SelectTranslationsPanelTelemetry.onTranslationSettings + ); }); - TranslationsTelemetry.logEventToConsole( - SelectTranslationsPanelTelemetry.onTranslationSettings - ); } static onAboutTranslations() { - Glean.translationsSelectTranslationsPanel.aboutTranslations.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), + SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsSelectTranslationsPanel.aboutTranslations.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + SelectTranslationsPanelTelemetry.onAboutTranslations + ); }); - TranslationsTelemetry.logEventToConsole( - SelectTranslationsPanelTelemetry.onAboutTranslations - ); } static onInitializationFailureMessage() { - Glean.translationsSelectTranslationsPanel.initializationFailureMessage.record( - { - flow_id: TranslationsTelemetry.getOrCreateFlowId(), - } - ); - TranslationsTelemetry.logEventToConsole( - SelectTranslationsPanelTelemetry.onInitializationFailureMessage - ); + SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsSelectTranslationsPanel.initializationFailureMessage.record( + { + flow_id: flowId, + } + ); + TranslationsTelemetry.logEventToConsole( + SelectTranslationsPanelTelemetry.onInitializationFailureMessage + ); + }); } /** @@ -677,15 +964,19 @@ class SelectTranslationsPanelTelemetry { * @param {string} data.targetLanguage */ static onTranslationFailureMessage(data) { - Glean.translationsSelectTranslationsPanel.translationFailureMessage.record({ - flow_id: TranslationsTelemetry.getOrCreateFlowId(), - from_language: data.sourceLanguage, - to_language: data.targetLanguage, + SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsSelectTranslationsPanel.translationFailureMessage.record( + { + flow_id: flowId, + from_language: data.sourceLanguage, + to_language: data.targetLanguage, + } + ); + TranslationsTelemetry.logEventToConsole( + SelectTranslationsPanelTelemetry.onTranslationFailureMessage, + data + ); }); - TranslationsTelemetry.logEventToConsole( - SelectTranslationsPanelTelemetry.onTranslationFailureMessage, - data - ); } /** @@ -696,17 +987,19 @@ class SelectTranslationsPanelTelemetry { * @param {string} data.detectedLanguage */ static onUnsupportedLanguageMessage(data) { - Glean.translationsSelectTranslationsPanel.unsupportedLanguageMessage.record( - { - flow_id: TranslationsTelemetry.getOrCreateFlowId(), - document_language: data.docLangTag, - detected_language: data.detectedLanguage, - } - ); - TranslationsTelemetry.logEventToConsole( - SelectTranslationsPanelTelemetry.onUnsupportedLanguageMessage, - data - ); + SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsSelectTranslationsPanel.unsupportedLanguageMessage.record( + { + flow_id: flowId, + document_language: data.docLangTag, + detected_language: data.detectedLanguage, + } + ); + TranslationsTelemetry.logEventToConsole( + SelectTranslationsPanelTelemetry.onUnsupportedLanguageMessage, + data + ); + }); } } @@ -715,20 +1008,45 @@ class SelectTranslationsPanelTelemetry { */ class AboutTranslationsPageTelemetry { /** + * These events do not yet occur frequently enough to benefit from rate limiting. + * + * @type {SampleRates} + */ + static #ABOUT_TRANSLATIONS_RATE_LIMITS = {}; + + /** + * Invokes a callback function with #ABOUT_TRANSLATIONS_RATE_LIMITS. + * + * @param {function(FlowContext): void} callback + */ + static #withRateLimits(callback) { + TranslationsTelemetry.withRateLimits( + AboutTranslationsPageTelemetry.#ABOUT_TRANSLATIONS_RATE_LIMITS, + callback + ); + } + + /** * Records when the about:translations page is opened. * * @param {object} data * @param {boolean} data.maintainFlow */ static onOpen(data) { - Glean.translationsAboutTranslationsPage.open.record({ - flow_id: data.maintainFlow - ? TranslationsTelemetry.getOrCreateFlowId() - : TranslationsTelemetry.createFlowId(), + if (!data.maintainFlow) { + // Create a new flow context when the panel opens, unless we + // are explicitly instructed to maintain the current flow. + TranslationsTelemetry.createFlowContext(); + } + + AboutTranslationsPageTelemetry.#withRateLimits(({ flowId }) => { + Glean.translationsAboutTranslationsPage.open.record({ + flow_id: flowId, + }); + TranslationsTelemetry.logEventToConsole( + AboutTranslationsPageTelemetry.onOpen, + data + ); }); - TranslationsTelemetry.logEventToConsole( - AboutTranslationsPageTelemetry.onOpen, - data - ); } }