/* 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/. */ // @ts-check import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; /** * @typedef {object} Lazy * @property {typeof console} console */ /** @type {Lazy} */ const lazy = /** @type {any} */ ({}); /** * @typedef {{ flowId: string, randomRoll: number }} FlowContext * @typedef {"default" | "nightly" | "beta" | "aurora" | "esr" | "release" | "unofficial"} UpdateChannel * @typedef {Partial> & { applyInAutomation?: boolean }} SampleRates */ ChromeUtils.defineLazyGetter(lazy, "console", () => { return console.createInstance({ maxLogLevelPref: "toolkit.telemetry.translations.logLevel", prefix: "TranslationsTelemetry", }); }); /** * Telemetry functions for Translations actors */ export class TranslationsTelemetry { /** * 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, aurora: 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} */ // prettier-ignore static #ENGINE_PERFORMANCE_RATE_LIMITS = { beta: 1 / 100, aurora: 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 * * @param {Function} caller - The calling function. * @param {object} [data] - Optional data passed to telemetry. */ static logEventToConsole(caller, data) { const flowContext = TranslationsTelemetry.getOrCreateFlowContext(); const logId = flowContext.flowId.substring(0, 5); lazy.console?.log( `flowId[${logId}]: ${caller.name}`, ...(data ? [data] : []) ); } /** * 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. * @param {UpdateChannel} [channel] * - The update channel whose sampling policy should be applied. Defaults to the * application-wide update channel, but may be overridden for testing. * * @returns {boolean} True if the event should be skipped for this channel, otherwise false. */ static shouldSkipSample( sampleRates, flowContext, channel = AppConstants.MOZ_UPDATE_CHANNEL ) { if (Cu.isInAutomation && !sampleRates.applyInAutomation) { // Do no skip any samples in automation, unless it is explicitly requested. return false; } if (channel !== AppConstants.MOZ_UPDATE_CHANNEL && !Cu.isInAutomation) { throw new Error( `Channel "${AppConstants.MOZ_UPDATE_CHANNEL}" was overridden as "${channel}" outside of testing.` ); } 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} */ static fullPagePanel() { return FullPageTranslationsPanelTelemetry; } /** * Telemetry functions for the SelectTranslationsPanel. * * @returns {SelectTranslationsPanelTelemetry} */ static selectTranslationsPanel() { return SelectTranslationsPanelTelemetry; } /** * Telemetry functions for the AboutTranslations page. * * @returns {AboutTranslationsPageTelemetry} */ static aboutTranslationsPage() { return AboutTranslationsPageTelemetry; } /** * Forces the creation of a new Translations telemetry flowId and returns it. * * @returns {FlowContext} */ 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 {FlowContext} */ static getOrCreateFlowContext() { // If we have the flowId cached, return it. if (TranslationsTelemetry.#flowContext) { return TranslationsTelemetry.#flowContext; } // If no flowId exists, create one. return TranslationsTelemetry.createFlowContext(); } /** * Records a telemetry event when the language of a page is identified for Translations. * * @param {object} data * @param {string | null} [data.htmlLangAttribute] * @param {string} data.identifiedLanguage * @param {boolean | null} [data.langTagsMatch] * @param {boolean | null} [data.isLangAttributeValid] * @param {number} data.extractedCodeUnits * @param {number} data.extractionTime * @param {number} data.identificationTime * @param {number} data.totalTime * @param {boolean} data.confident */ static 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 ); } ); } /** * Records a telemetry event when full page translation fails. * * @param {string} errorMessage */ static onError(errorMessage) { Glean.translations.error.record({ flow_id: TranslationsTelemetry.getOrCreateFlowContext().flowId, reason: errorMessage, }); TranslationsTelemetry.logEventToConsole(TranslationsTelemetry.onError, { errorMessage, }); } /** * Records a telemetry event when a translation request is sent. * * @param {object} data * @param {boolean} data.autoTranslate * @param {string} [data.docLangTag] * @param {string} data.sourceLanguage * @param {string} data.targetLanguage * @param {string} data.requestTarget * @param {number} data.sourceTextCodeUnits * @param {number} [data.sourceTextWordCount] */ static onTranslate(data) { const { autoTranslate, docLangTag, sourceLanguage, requestTarget, targetLanguage, sourceTextCodeUnits, sourceTextWordCount, } = data; Glean.translations.requestCount[requestTarget ?? "full_page"].add(1); Glean.translations.translationRequest.record({ flow_id: TranslationsTelemetry.getOrCreateFlowContext().flowId, from_language: sourceLanguage, to_language: targetLanguage, auto_translate: autoTranslate, document_language: docLangTag, request_target: requestTarget ?? "full_page", source_text_code_units: sourceTextCodeUnits, source_text_word_count: sourceTextWordCount, }); TranslationsTelemetry.logEventToConsole( TranslationsTelemetry.onTranslate, data ); } static onRestorePage() { Glean.translations.restorePage.record({ flow_id: TranslationsTelemetry.getOrCreateFlowContext().flowId, }); TranslationsTelemetry.logEventToConsole( TranslationsTelemetry.onRestorePage ); } /** * Records a telemetry event for measuring translation engine performance. * * @param {object} data * @param {string} data.sourceLanguage * @param {string} data.targetLanguage * @param {number} data.totalCompletedRequests * @param {number} data.totalInferenceSeconds * @param {number} data.totalTranslatedWords */ static onReportEnginePerformance(data) { TranslationsTelemetry.withRateLimits( TranslationsTelemetry.#ENGINE_PERFORMANCE_RATE_LIMITS, () => { const { sourceLanguage, targetLanguage, totalCompletedRequests, totalInferenceSeconds, totalTranslatedWords, } = data; 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, } ); } ); } static onFeatureEnable() { Glean.translationsFeature.enable.record(); TranslationsTelemetry.logEventToConsole( TranslationsTelemetry.onFeatureEnable ); } static onFeatureDisable() { Glean.translationsFeature.disable.record(); TranslationsTelemetry.logEventToConsole( TranslationsTelemetry.onFeatureDisable ); } static onFeatureReset() { Glean.translationsFeature.reset.record(); TranslationsTelemetry.logEventToConsole( TranslationsTelemetry.onFeatureReset ); } } /** * Telemetry functions for the FullPageTranslationsPanel UI */ 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 = { nightly: 1 / 10, beta: 1 / 50, aurora: 1 / 50, esr: 1 / 100, release: 1 / 1_000, }; /** * 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 * @param {string} data.viewName * @param {string} data.docLangTag * @param {boolean} data.autoShow * @param {boolean} data.maintainFlow * @param {boolean} data.openedFromAppMenu */ static onOpen(data) { 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 ); }); } static onClose() { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.close.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onClose ); }); } static onOpenFromLanguageMenu() { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.openFromLanguageMenu.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onOpenFromLanguageMenu ); }); } static onChangeFromLanguage(langTag) { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.changeFromLanguage.record({ flow_id: flowId, language: langTag, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onChangeFromLanguage, { langTag, } ); }); } static onCloseFromLanguageMenu() { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.closeFromLanguageMenu.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onCloseFromLanguageMenu ); }); } static onOpenToLanguageMenu() { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.openToLanguageMenu.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onOpenToLanguageMenu ); }); } static onChangeToLanguage(langTag) { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.changeToLanguage.record({ flow_id: flowId, language: langTag, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onChangeToLanguage, { langTag, } ); }); } static onCloseToLanguageMenu() { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.closeToLanguageMenu.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onCloseToLanguageMenu ); }); } static onOpenSettingsMenu() { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.openSettingsMenu.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onOpenSettingsMenu ); }); } static onCloseSettingsMenu() { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.closeSettingsMenu.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onCloseSettingsMenu ); }); } static onCancelButton() { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.cancelButton.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onCancelButton ); }); } static onChangeSourceLanguageButton() { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.changeSourceLanguageButton.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onChangeSourceLanguageButton ); }); } static onDismissErrorButton() { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.dismissErrorButton.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onDismissErrorButton ); }); } static onRestorePageButton() { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.restorePageButton.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onRestorePageButton ); }); } static onTranslateButton() { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.translateButton.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onTranslateButton ); }); } static onAlwaysOfferTranslations(toggledOn) { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.alwaysOfferTranslations.record({ flow_id: flowId, toggled_on: toggledOn, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onAlwaysOfferTranslations, { toggledOn, } ); }); } static onAlwaysTranslateLanguage(langTag, toggledOn) { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.alwaysTranslateLanguage.record({ flow_id: flowId, language: langTag, toggled_on: toggledOn, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onAlwaysTranslateLanguage, { langTag, toggledOn, } ); }); } static onNeverTranslateLanguage(langTag, toggledOn) { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.neverTranslateLanguage.record({ flow_id: flowId, language: langTag, toggled_on: toggledOn, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onNeverTranslateLanguage, { langTag, toggledOn, } ); }); } static onNeverTranslateSite(toggledOn) { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.neverTranslateSite.record({ flow_id: flowId, toggled_on: toggledOn, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onNeverTranslateSite, { toggledOn, } ); }); } static onManageLanguages() { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.manageLanguages.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onManageLanguages ); }); } static onAboutTranslations() { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.aboutTranslations.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onAboutTranslations ); }); } static onLearnMoreLink() { FullPageTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsPanel.learnMore.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( FullPageTranslationsPanelTelemetry.onLearnMoreLink ); }); } } /** * Telemetry functions for the SelectTranslationsPanel UI */ 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 * @param {string} data.docLangTag * @param {boolean} data.maintainFlow * @param {string} data.sourceLanguage * @param {string} data.targetLanguage * @param {string} data.textSource */ static onOpen(data) { 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, text_source: data.textSource, }); TranslationsTelemetry.logEventToConsole( SelectTranslationsPanelTelemetry.onOpen, data ); }); } static onClose() { SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsSelectTranslationsPanel.close.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( SelectTranslationsPanelTelemetry.onClose ); }); } static onCancelButton() { SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsSelectTranslationsPanel.cancelButton.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( SelectTranslationsPanelTelemetry.onCancelButton ); }); } static onCopyButton() { SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsSelectTranslationsPanel.copyButton.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( SelectTranslationsPanelTelemetry.onCopyButton ); }); } static onDoneButton() { SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsSelectTranslationsPanel.doneButton.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( SelectTranslationsPanelTelemetry.onDoneButton ); }); } static onTranslateButton({ detectedLanguage, sourceLanguage, 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, } ); }); } static onTranslateFullPageButton() { SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsSelectTranslationsPanel.translateFullPageButton.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( SelectTranslationsPanelTelemetry.onTranslateFullPageButton ); }); } static onTryAgainButton() { SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsSelectTranslationsPanel.tryAgainButton.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( SelectTranslationsPanelTelemetry.onTryAgainButton ); }); } static onChangeFromLanguage({ previousLangTag, currentLangTag, docLangTag }) { 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 } ); }); } static onChangeToLanguage(langTag) { SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsSelectTranslationsPanel.changeToLanguage.record({ flow_id: flowId, language: langTag, }); TranslationsTelemetry.logEventToConsole( SelectTranslationsPanelTelemetry.onChangeToLanguage, { langTag } ); }); } static onOpenSettingsMenu() { SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsSelectTranslationsPanel.openSettingsMenu.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( SelectTranslationsPanelTelemetry.onOpenSettingsMenu ); }); } static onTranslationSettings() { SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsSelectTranslationsPanel.translationSettings.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( SelectTranslationsPanelTelemetry.onTranslationSettings ); }); } static onAboutTranslations() { SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsSelectTranslationsPanel.aboutTranslations.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( SelectTranslationsPanelTelemetry.onAboutTranslations ); }); } static onInitializationFailureMessage() { SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsSelectTranslationsPanel.initializationFailureMessage.record( { flow_id: flowId, } ); TranslationsTelemetry.logEventToConsole( SelectTranslationsPanelTelemetry.onInitializationFailureMessage ); }); } /** * Records a telemetry event when the translation-failure message is displayed. * * @param {object} data * @param {string} data.sourceLanguage * @param {string} data.targetLanguage */ static onTranslationFailureMessage(data) { SelectTranslationsPanelTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsSelectTranslationsPanel.translationFailureMessage.record( { flow_id: flowId, from_language: data.sourceLanguage, to_language: data.targetLanguage, } ); TranslationsTelemetry.logEventToConsole( SelectTranslationsPanelTelemetry.onTranslationFailureMessage, data ); }); } /** * Records a telemetry event when the unsupported-language message is displayed. * * @param {object} data * @param {string} data.docLangTag * @param {string} data.detectedLanguage */ static 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 ); }); } } /** * Telemetry functions for the AboutTranslations Page */ 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 a translation request is sent from about:translations. * * @param {object} data * @param {boolean} data.autoTranslate * @param {string} data.sourceLanguage * @param {string} data.targetLanguage * @param {number} data.sourceTextCodeUnits * @param {number} [data.sourceTextWordCount] */ static onTranslate(data) { // Translation requests are explicitly excluded from rate limiting. TranslationsTelemetry.onTranslate({ ...data, requestTarget: "about_translations", }); } /** * Records when the about:translations page is opened. * * @param {object} data * @param {boolean} data.maintainFlow */ static onOpen(data) { 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 ); }); } /** * Records when the try-again button is invoked in about:translations. */ static onTryAgainButton() { AboutTranslationsPageTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsAboutTranslationsPage.tryAgainButton.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( AboutTranslationsPageTelemetry.onTryAgainButton ); }); } /** * Records when the copy button is invoked in about:translations. */ static onCopyButton() { AboutTranslationsPageTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsAboutTranslationsPage.copyButton.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( AboutTranslationsPageTelemetry.onCopyButton ); }); } /** * Records when the clear source text button is invoked in about:translations. */ static onClearSourceTextButton() { AboutTranslationsPageTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsAboutTranslationsPage.clearSourceTextButton.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( AboutTranslationsPageTelemetry.onClearSourceTextButton ); }); } /** * Records when the swap button is invoked in about:translations. */ static onSwapButton() { AboutTranslationsPageTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsAboutTranslationsPage.swapButton.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( AboutTranslationsPageTelemetry.onSwapButton ); }); } /** * Records when the language-load-error message is shown in about:translations. */ static onLanguageLoadErrorMessage() { AboutTranslationsPageTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsAboutTranslationsPage.languageLoadErrorMessage.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( AboutTranslationsPageTelemetry.onLanguageLoadErrorMessage ); }); } /** * Records when the unsupported-info message is shown in about:translations. */ static onUnsupportedInfoMessage() { AboutTranslationsPageTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsAboutTranslationsPage.unsupportedInfoMessage.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( AboutTranslationsPageTelemetry.onUnsupportedInfoMessage ); }); } /** * Records when the policy-disabled-info message is shown in about:translations. */ static onPolicyDisabledInfoMessage() { AboutTranslationsPageTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsAboutTranslationsPage.policyDisabledInfoMessage.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( AboutTranslationsPageTelemetry.onPolicyDisabledInfoMessage ); }); } /** * Records when the feature-blocked-info message is shown in about:translations. */ static onFeatureBlockedInfoMessage() { AboutTranslationsPageTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsAboutTranslationsPage.featureBlockedInfoMessage.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( AboutTranslationsPageTelemetry.onFeatureBlockedInfoMessage ); }); } /** * Records when unblock-feature is invoked in about:translations. */ static onUnblockFeature() { AboutTranslationsPageTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsAboutTranslationsPage.unblockFeature.record({ flow_id: flowId, }); TranslationsTelemetry.logEventToConsole( AboutTranslationsPageTelemetry.onUnblockFeature ); }); } /** * Records when the unsupported-language message is shown in about:translations. * * @param {object} data * @param {string} data.detectedLanguage * @param {number} data.sourceTextCodeUnits * @param {number | null} [data.sourceTextWordCount] */ static onUnsupportedLanguageMessage(data) { AboutTranslationsPageTelemetry.#withRateLimits(({ flowId }) => { Glean.translationsAboutTranslationsPage.unsupportedLanguageMessage.record( { flow_id: flowId, detected_language: data.detectedLanguage, source_text_code_units: data.sourceTextCodeUnits, source_text_word_count: data.sourceTextWordCount, } ); TranslationsTelemetry.logEventToConsole( AboutTranslationsPageTelemetry.onUnsupportedLanguageMessage, data ); }); } }