Source code

Revision control

Copy as Markdown

Other Tools

/* 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<Record<UpdateChannel, number>> & { 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
);
});
}
}