* Iframe support

* Fix linting errors

* Disable legacy extension

* Process translations in the top frame

* Do not load translation scripts to all frames

* Add tests for iframe

* Fix command name

* Prevent beforeunload dialogue

* Integrate QE telemetry

* Limit precision of QE metrics

* Do not report QE metrics if it's disabled

* Fix tab telemetry clearing

* Fix unloading event

* Increase waiting time

* Fix closing notification

* Report initial QE enabled state

* Increase test timeout
This commit is contained in:
Evgeny Pavlov 2022-03-24 11:33:59 -07:00 коммит произвёл GitHub
Родитель 25d7060880
Коммит f8182d85fd
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 443 добавлений и 284 удалений

3
.gitignore поставляемый
Просмотреть файл

@ -1,3 +1,4 @@
node_modules
web-ext-artifacts
extension/settings.js
extension/settings.js
gecko

Просмотреть файл

@ -1,4 +1,4 @@
/* global LanguageDetection, browser, PingSender, loadFastText, FastText */
/* global LanguageDetection, browser, PingSender, BERGAMOT_VERSION_FULL, Telemetry, loadFastText, FastText */
/*
* we need the background script in order to have full access to the
@ -15,6 +15,24 @@ let languageDetection = null;
// as soon we load, we should turn off the legacy prefs to avoid UI conflicts
browser.experiments.translationbar.switchOnPreferences();
let telemetryByTab = new Map();
const init = async () => {
cachedEnvInfo = await browser.experiments.telemetryEnvironment.getFxTelemetryMetrics();
telemetryByTab.forEach(t => t.environment(cachedEnvInfo));
}
const getTelemetry = tabId => {
if (!telemetryByTab.has(tabId)) {
let telemetry = new Telemetry(pingSender);
telemetryByTab.set(tabId, telemetry);
telemetry.versions(browser.runtime.getManifest().version, "?", BERGAMOT_VERSION_FULL);
if (cachedEnvInfo) {
telemetry.environment(cachedEnvInfo);
}
}
return telemetryByTab.get(tabId);
}
// eslint-disable-next-line max-lines-per-function
const messageListener = async function(message, sender) {
@ -115,30 +133,82 @@ const messageListener = async function(message, sender) {
);
break;
case "translate":
// propagate translation message from iframe to top frame
message.frameId = sender.frameId;
browser.tabs.sendMessage(
message.tabId,
message,
{ frameId: 0 }
);
break;
case "translationComplete":
// propagate translation message from top frame to the source frame
browser.tabs.sendMessage(
message.tabId,
message,
{ frameId: message.translationMessage.frameId }
);
break;
case "displayOutboundTranslation":
// propagate "display outbound" command from top frame to other frames
browser.tabs.sendMessage(
message.tabId,
message
);
break;
case "recordTelemetry":
case "loadTelemetryInfo":
if (cachedEnvInfo === null) {
// eslint-disable-next-line require-atomic-updates
cachedEnvInfo = await browser.experiments.telemetryEnvironment.getFxTelemetryMetrics();
/*
* if the event was to close the infobar, we notify the api as well
* we don't need another redundant loop by informing the mediator,
* to then inform this script again
*/
if (message.name === "closed") {
browser.experiments.translationbar.closeInfobar(message.tabId);
}
browser.tabs.sendMessage(sender.tab.id, { command: "telemetryInfoLoaded", env: cachedEnvInfo })
getTelemetry(message.tabId).record(message.type, message.category, message.name, message.value);
break;
case "sendPing":
pingSender.submit(message.pingName, message.data)
.catch(e => console.error(`Telemetry: ping submission has failed: ${e}`));
break;
case "reportTranslationStats": {
let wps = getTelemetry(message.tabId).addAndGetTranslationTimeStamp(message.numWords, message.engineTimeElapsed);
browser.tabs.sendMessage(
message.tabId,
{
command: "updateStats",
tabId: message.tabId,
wps
}
);
}
break;
case "translationRequested":
case "reportOutboundStats":
getTelemetry(message.tabId).addOutboundTranslation(message.textAreaId, message.text);
break;
case "reportQeStats":
getTelemetry(message.tabId).addQualityEstimation(message.wordScores, message.sentScores);
break;
case "submitPing":
getTelemetry(message.tabId).submit();
telemetryByTab.delete(message.tabId);
break;
case "translationRequested":
// requested for translation received. let's inform the mediator
browser.tabs.sendMessage(
message.tabId,
{ command: "translationRequested",
tabId: message.tabId,
from: message.from,
to: message.to,
withOutboundTranslation: message.withOutboundTranslation,
withQualityEstimation: message.withQualityEstimation }
{
command: "translationRequested",
tabId: message.tabId,
from: message.from,
to: message.to,
withOutboundTranslation: message.withOutboundTranslation,
withQualityEstimation: message.withQualityEstimation
}
);
break;
case "updateProgress":
@ -161,28 +231,6 @@ const messageListener = async function(message, sender) {
from: message.to, // we switch the requests directions here
to: message.from }
);
break;
case "onInfobarEvent":
/*
* inform the mediator that a UI event occurred in Infobar
*/
browser.tabs.sendMessage(
message.tabId,
{ command: "onInfobarEvent",
tabId: message.tabId,
name: message.name }
);
/*
* if the event was to close the infobar, we notify the api as well
* we don't need another redundant loop by informing the mediator,
* to then inform this script again
*/
if (message.name === "closed") {
browser.experiments.translationbar.closeInfobar(message.tabId);
}
break;
case "displayStatistics":
@ -206,6 +254,7 @@ const messageListener = async function(message, sender) {
browser.runtime.onMessage.addListener(messageListener);
browser.experiments.translationbar.onTranslationRequest.addListener(messageListener);
init().catch(error => console.error("bgScript initialization failed: ", error.message));
// loads fasttext (language detection) wasm module and model
fetch(browser

Просмотреть файл

@ -59,17 +59,17 @@ class Translation {
payload: null
});
break;
case "onError":
case "reportError":
this.mediator.contentScriptsMessageListener(this, {
command: "onError",
command: "reportError",
payload: translationMessage.data[1]
});
break;
case "onModelEvent":
case "reportPerformanceTimespan":
this.mediator.contentScriptsMessageListener(this, {
command: "onModelEvent",
payload: { type: translationMessage.data[1], timeMs: translationMessage.data[2] }
command: "reportPerformanceTimespan",
payload: { metric: translationMessage.data[1], timeMs: translationMessage.data[2] }
});
break;
@ -135,6 +135,7 @@ class Translation {
sourceParagraph,
type,
tabId,
frameId,
navigatorLanguage,
pageLanguage,
attrId,
@ -173,6 +174,7 @@ class Translation {
break;
}
translationMessage.tabId = tabId;
translationMessage.frameId = frameId;
translationMessage.type = type;
translationMessage.attrId = attrId;
translationMessage.withOutboundTranslation = withOutboundTranslation;

Просмотреть файл

@ -7,6 +7,7 @@ class TranslationMessage {
this.sourceLanguage = null;
this.targetLanguage = null;
this.tabId = null;
this.frameId = null;
this.type = null;
}
}

Просмотреть файл

@ -49,7 +49,7 @@ class TranslationHelper {
engineRegistry.bergamotTranslatorWasm.sha256
);
if (!wasmArrayBuffer) {
postMessage(["onError", "engine_download"]);
postMessage(["reportError", "engine_download"]);
console.log("Error loading engine from cache or web.");
return;
}
@ -76,7 +76,7 @@ class TranslationHelper {
this.WasmEngineModule = Module;
} catch (e) {
console.log("Error loading wasm module:", e);
postMessage(["onError", "engine_load"]);
postMessage(["reportError", "engine_load"]);
postMessage(["updateProgress", "errorLoadingWasm"]);
}
}
@ -173,7 +173,7 @@ class TranslationHelper {
timeElapsed
]);
} catch (e) {
postMessage(["onError", "translation"]);
postMessage(["reportError", "translation"]);
postMessage(["updateProgress", "translationLoadedWithErrors"]);
console.error("Translation error: ", e)
throw e;
@ -263,14 +263,14 @@ class TranslationHelper {
let finish = Date.now();
console.log(`Model '${sourceLanguage}${targetLanguage}' successfully constructed. Time taken: ${(finish - start) / 1000} secs`);
postMessage([
"onModelEvent",
"loaded",
"reportPerformanceTimespan",
"model_load_time_num",
finish-start
]);
} catch (error) {
console.log(`Model '${sourceLanguage}${targetLanguage}' construction failed: '${error.message} - ${error.stack}'`);
postMessage(["onError", "model_load"]);
postMessage(["reportError", "model_load"]);
postMessage(["updateProgress", "errorLoadingWasm"]);
return;
}
@ -367,13 +367,13 @@ class TranslationHelper {
const shortListBuffer = downloadedBuffers[1];
if (!modelBuffer || !shortListBuffer) {
console.log("Error loading models from cache or web (models)");
postMessage(["onError", "model_download"]);
postMessage(["reportError", "model_download"]);
throw new Error("Error loading models from cache or web (models)");
}
const vocabAsArrayBuffer = await this.getItemFromCacheOrWeb(vocabFile, vocabFileSize, vocabFileChecksum);
if (!vocabAsArrayBuffer) {
console.log("Error loading models from cache or web (vocab)");
postMessage(["onError", "model_download"]);
postMessage(["reportError", "model_download"]);
throw new Error("Error loading models from cache or web (vocab)");
}
const downloadedVocabBuffers = [];
@ -382,8 +382,8 @@ class TranslationHelper {
let finish = Date.now();
console.log(`Total Download time for all files of '${languagePair}': ${(finish - start) / 1000} secs`);
postMessage([
"onModelEvent",
"downloaded",
"reportPerformanceTimespan",
"model_download_time_num",
finish-start
]);
@ -583,7 +583,7 @@ class TranslationHelper {
return listTranslatedText;
} catch (e) {
console.error("Error in translation engine ", e)
postMessage(["onError", "marian"]);
postMessage(["reportError", "marian"]);
postMessage(["updateProgress", "translationLoadedWithErrors"]);
throw e; // to do: Should we re-throw?
} finally {

Просмотреть файл

@ -26,11 +26,14 @@
"persistent": true,
"scripts": [
"settings.js",
"controller/translation/bergamotTranslatorVersion.js",
"model/telemetry/schema.js",
"model/telemetry/PingSender.js",
"controller/languageDetection/LanguageDetection.js",
"controller/languageDetection/fasttext_wasm.js",
"controller/languageDetection/fasttext.js",
"model/telemetry/Metrics.js",
"model/telemetry/Telemetry.js",
"view/js/TranslationNotificationManager.js",
"controller/backgroundScript.js"
]
@ -42,23 +45,29 @@
],
"js": [
"settings.js",
"model/modelRegistry.js",
"controller/languageDetection/LanguageDetection.js",
"model/Queue.js",
"controller/translation/Translation.js",
"controller/translation/TranslationMessage.js",
"controller/translation/translationWorker.js",
"controller/translation/bergamotTranslatorVersion.js",
"view/js/OutboundTranslation.js",
"view/js/InPageTranslation.js",
"model/telemetry/Telemetry.js",
"model/telemetry/Metrics.js",
"model/telemetry/schema.js",
"model/modelRegistry.js",
"mediator.js"
],
"css": [
"view/static/outboundTranslation.css"
],
"all_frames": true,
"run_at": "document_idle"
},
{
"matches": [
"<all_urls>"
],
"js": [
"model/Queue.js",
"controller/translation/Translation.js",
"controller/translation/TranslationMessage.js",
"controller/translation/translationWorker.js"
],
"all_frames": false,
"run_at": "document_idle"
}
],

Просмотреть файл

@ -4,55 +4,66 @@
*/
/* global LanguageDetection, OutboundTranslation, Translation , browser,
InPageTranslation, browser, Telemetry, BERGAMOT_VERSION_FULL */
InPageTranslation, browser */
/* eslint-disable max-lines */
class Mediator {
constructor() {
this.messagesSenderLookupTable = new Map();
this.translation = null;
this.translationsCounter = 0;
this.languageDetection = new LanguageDetection();
this.inPageTranslation = new InPageTranslation(this);
this.telemetry = new Telemetry((pingName, data) => browser.runtime.sendMessage({
command: "sendPing",
pingName,
data
}));
this.telemetry.versions(browser.runtime.getManifest().version, "?", BERGAMOT_VERSION_FULL);
this.outboundTranslation = null;
browser.runtime.onMessage.addListener(this.bgScriptsMessageListener.bind(this));
this.translationBarDisplayed = false;
this.statsMode = false;
// if we are in the protected mochitest page, we flag it.
if (window.location.href ===
"https://example.com/browser/browser/extensions/translations/test/browser/browser_translation_test.html") {
if ((window.location.href ===
"https://example.com/browser/browser/extensions/translations/test/browser/browser_translation_test.html") ||
(window.location.href ===
"https://example.com/browser/browser/extensions/translations/test/browser/frame.html")) {
this.isMochitest = true;
}
}
init() {
browser.runtime.sendMessage({ command: "monitorTabLoad" });
browser.runtime.sendMessage({ command: "loadTelemetryUploadPref" });
browser.runtime.sendMessage({ command: "loadTelemetryInfo" });
if (window.self === window.top) { // is main frame
browser.runtime.sendMessage({ command: "monitorTabLoad" });
}
}
// main entrypoint to handle the extension's load
start(tabId) {
this.tabId = tabId;
// request the language detection class to extract a page's snippet
this.languageDetection.extractPageContent();
if (window.self === window.top) { // is main frame
// request the language detection class to extract a page's snippet
this.languageDetection.extractPageContent();
/*
* request the background script to detect the page's language and
* determine if the infobar should be displayed
*/
browser.runtime.sendMessage({
command: "detectPageLanguage",
languageDetection: this.languageDetection
})
/*
* request the background script to detect the page's language and
* determine if the infobar should be displayed
*/
browser.runtime.sendMessage({
command: "detectPageLanguage",
languageDetection: this.languageDetection
})
}
}
recordTelemetry(type, category, name, value) {
browser.runtime.sendMessage({
command: "recordTelemetry",
tabId: this.tabId,
type,
category,
name,
value
});
}
// eslint-disable-next-line max-lines-per-function
determineIfTranslationisRequired() {
/*
@ -73,17 +84,28 @@ class Mediator {
*/
if (this.translationBarDisplayed) return;
// is iframe
if ((window.self !== window.top) && this.languageDetection.shouldDisplayTranslation()) {
this.translationBarDisplayed = true;
return;
}
// is main frame
const pageLang = this.languageDetection.pageLanguage;
const navLang = this.languageDetection.navigatorLanguage;
this.telemetry.langPair(pageLang, navLang);
this.telemetry.langMismatch();
window.onbeforeunload = () => {
browser.runtime.sendMessage({
command: "reportClosedInfobar",
tabId: this.tabId
});
this.telemetry.pageClosed();
}
this.recordTelemetry("string", "metadata", "from_lang", pageLang);
this.recordTelemetry("string", "metadata", "to_lang", navLang);
this.recordTelemetry("counter", "service", "lang_mismatch");
window.addEventListener("beforeunload", () => {
/*
* it is recommended to use visibilitychange event for this use case,
* but it triggers some errors because of communication with bgScript, so let's use beforeunload for now
*/
browser.runtime.sendMessage({ command: "reportClosedInfobar", tabId: this.tabId });
browser.runtime.sendMessage({ command: "submitPing", tabId: this.tabId });
});
if (this.languageDetection.shouldDisplayTranslation()) {
// request the backgroundscript to display the translationbar
@ -95,7 +117,7 @@ class Mediator {
// create the translation object
this.translation = new Translation(this);
} else {
this.telemetry.langNotSupported();
this.recordTelemetry("counter", "service", "not_supported");
}
}
}
@ -104,32 +126,31 @@ class Mediator {
* handles all requests received from the content scripts
* (views and controllers)
*/
// eslint-disable-next-line max-lines-per-function
contentScriptsMessageListener(sender, message) {
switch (message.command) {
case "translate":
if (!this.translation) {
this.translation = new Translation(this);
if (window.self === window.top) {
// eslint-disable-next-line no-case-declarations
this.translate(message);
} else {
// pass to the worker in top frame through bgScript
browser.runtime.sendMessage({
command: "translate",
tabId: this.tabId,
payload: message.payload
});
}
// eslint-disable-next-line no-case-declarations
const translationMessage = this.translation.constructTranslationMessage(
message.payload.text,
message.payload.type,
message.tabId,
this.languageDetection.navigatorLanguage,
this.languageDetection.pageLanguage,
message.payload.attrId,
message.payload.withOutboundTranslation,
message.payload.withQualityEstimation
);
this.messagesSenderLookupTable.set(translationMessage.messageID, sender);
this.translation.translate(translationMessage);
// console.log("new translation message sent:", translationMessage, "msg sender lookuptable size:", this.messagesSenderLookupTable.size);
this.telemetry.infobarState("outbound_enabled", message.payload.withOutboundTranslation === true);
if (message.payload.type === "outbound") {
this.telemetry.addOutboundTranslation(sender.selectedTextArea, message.payload.text);
if (!sender.selectedTextArea.id) sender.selectedTextArea.id = self.crypto.randomUUID();
browser.runtime.sendMessage({
command: "reportOutboundStats",
tabId: this.tabId,
textAreaId: sender.selectedTextArea.id,
text: message.payload.text
});
}
break;
case "translationComplete":
@ -140,25 +161,26 @@ class Mediator {
* in order to route the response back, which can be
* OutbountTranslation, InPageTranslation etc....
*/
message.payload[1].forEach(translationMessage => {
this.messagesSenderLookupTable.get(translationMessage.messageID)
.mediatorNotification(translationMessage);
this.messagesSenderLookupTable.delete(translationMessage.messageID);
// if this message is originated from another frame, pass it back through bgScript
if ((window.self === window.top) && (typeof translationMessage.frameId !== "undefined")) {
browser.runtime.sendMessage({
command: "translationComplete",
tabId: this.tabId,
translationMessage
});
} else {
this.updateElements(translationMessage);
}
});
// eslint-disable-next-line no-case-declarations
const wordsPerSecond = this.telemetry
.addAndGetTranslationTimeStamp(message.payload[2][0], message.payload[2][1]);
if (this.statsMode) {
// if the user chose to see stats in the infobar, we display them
browser.runtime.sendMessage({
command: "updateProgress",
progressMessage: browser.i18n.getMessage("statsMessage", wordsPerSecond),
tabId: this.tabId
});
}
browser.runtime.sendMessage({
command: "reportTranslationStats",
tabId: this.tabId,
numWords: message.payload[2][0],
engineTimeElapsed: message.payload[2][1]
});
// console.log("translation complete rcvd:", message, "msg sender lookuptable size:", this.messagesSenderLookupTable.size);
break;
case "updateProgress":
@ -184,43 +206,37 @@ class Mediator {
});
break;
case "displayOutboundTranslation":
/* display the outboundstranslation widget */
this.outboundTranslation = new OutboundTranslation(this);
this.outboundTranslation.start(
this.localizedNavigatorLanguage,
this.localizedPageLanguage
);
this.startOutbound();
// broadcast to all frames through bgScript
browser.runtime.sendMessage({
command: "displayOutboundTranslation",
tabId: this.tabId
});
break;
case "onError":
case "reportError":
// payload is a metric name from metrics.yaml
this.telemetry.error(message.payload);
this.recordTelemetry("counter", "errors", message.payload);
break;
case "viewPortWordsNum":
this.telemetry.wordsInViewport(message.payload);
case "reportViewPortWordsNum":
this.recordTelemetry("quantity", "performance", "word_count_visible_in_viewport", message.payload);
break;
case "onModelEvent":
// eslint-disable-next-line no-case-declarations
let metric = null;
if (message.payload.type === "downloaded") {
metric = "model_download_time_num";
} else if (message.payload.type === "loaded") {
metric = "model_load_time_num";
// start timer when the model is fully loaded
this.telemetry.translationStarted();
} else {
throw new Error(`Unexpected event type: ${message.payload.type}`)
}
this.telemetry.performanceTime(metric, message.payload.timeMs);
case "reportPerformanceTimespan":
this.recordTelemetry("timespan", "performance", message.payload.metric, message.payload.timeMs);
break;
case "onFormsEvent":
this.telemetry.formsEvent(message.payload);
case "reportFormsEvent":
// payload is a metric name from metrics.yaml
this.recordTelemetry("event", "forms", message.payload);
break;
case "reportQeMetrics":
this.telemetry.addQualityEstimation(message.payload.wordScores, message.payload.sentScores, false);
this.recordTelemetry("boolean", "quality", "is_supervised", false);
browser.runtime.sendMessage({
command: "reportQeStats",
tabId: this.tabId,
wordScores: message.payload.wordScores,
sentScores: message.payload.sentScores
});
break;
case "domMutation":
if (this.outboundTranslation) {
this.outboundTranslation.updateZIndex(message.payload);
}
@ -229,19 +245,57 @@ class Mediator {
}
}
translate(message) {
if (!this.translation) {
this.translation = new Translation(this);
}
const translationMessage = this.translation.constructTranslationMessage(
message.payload.text,
message.payload.type,
this.tabId,
message.frameId,
this.languageDetection.navigatorLanguage,
this.languageDetection.pageLanguage,
message.payload.attrId,
message.payload.withOutboundTranslation,
message.payload.withQualityEstimation
);
this.translation.translate(translationMessage);
// console.log("new translation message sent:", translationMessage, "msg sender lookuptable size:", this.messagesSenderLookupTable.size);
}
startOutbound() {
if (this.outboundTranslation !== null) return;
/* display the outboundstranslation widget */
this.outboundTranslation = new OutboundTranslation(this);
this.outboundTranslation.start(
this.localizedNavigatorLanguage,
this.localizedPageLanguage
);
}
updateElements(translationMessage) {
if (translationMessage.type === "inpage") {
this.inPageTranslation.mediatorNotification(translationMessage);
} else if ((translationMessage.type === "outbound") || (translationMessage.type === "backTranslation")) {
this.outboundTranslation.mediatorNotification(translationMessage);
} else {
throw new Error("Unexpected type: ", translationMessage.type);
}
}
/*
* handles all communication received from the background script
* and properly delegates the calls to the responsible methods
*/
// eslint-disable-next-line max-lines-per-function
bgScriptsMessageListener(message) {
switch (message.command) {
case "responseMonitorTabLoad":
this.start(message.tabId);
break;
case "telemetryInfoLoaded":
this.telemetry.environment(message.env);
break;
case "responseDetectPageLanguage":
this.languageDetection = Object.assign(new LanguageDetection(), message.languageDetection);
this.determineIfTranslationisRequired();
@ -261,13 +315,28 @@ class Mediator {
this.inPageTranslation.start(this.languageDetection.pageLanguage);
}
break;
case "translate":
this.translate(message)
break
case "translationComplete":
this.updateElements(message.translationMessage);
break;
case "displayOutboundTranslation":
this.startOutbound();
break;
case "displayStatistics":
this.statsMode = true;
document.querySelector("html").setAttribute("x-bergamot-debug", true);
break;
case "onInfobarEvent":
// 'name' is a metric name from metrics.yaml
this.telemetry.infobarEvent(message.name);
case "updateStats":
if (this.statsMode) {
// if the user chose to see stats in the infobar, we display them
browser.runtime.sendMessage({
command: "updateProgress",
progressMessage: browser.i18n.getMessage("statsMessage", message.wps),
tabId: this.tabId
});
}
break;
case "localizedLanguages":
this.localizedPageLanguage = message.localizedPageLanguage;

Просмотреть файл

@ -87,6 +87,9 @@ class Metrics {
if (typeof val !== "string") {
throw new Error(`Telemetry: ${category}.${name} must be a string, value: ${val}`)
}
if (val.length > 100) {
this._log(`warning: string ${category}.${name} is longer that 100 character will be truncated`);
}
for (const pingName of this._getPings(category, name, "string")) {
let ping = this._build_ping(pingName);
if (!("string" in ping.metrics)) {

Просмотреть файл

@ -6,8 +6,8 @@
// eslint-disable-next-line
class Telemetry {
constructor(submitCallback) {
this._client = new Metrics(submitCallback);
constructor(pingSender) {
this._client = new Metrics((pingName, data) => pingSender.submit(pingName, data));
this._totalWords = 0;
this._totalEngineMs = 0;
this._translationStartTimestamp = null;
@ -17,13 +17,35 @@ class Telemetry {
this._sentScores = [];
}
translationStarted() {
this._translationStartTimestamp = window.performance.now();
this._updateUsageTime();
}
record(type, category, name, value) {
switch (type) {
case "event":
this._client.event(category, name);
this._updateUsageTime();
break;
case "counter":
this._client.increment(category, name)
break;
case "string":
this._client.string(category, name, value)
break;
case "quantity":
this._client.quantity(category, name, value)
break;
case "timespan":
if (name === "model_load_time_num") {
this._translationStartTimestamp = window.performance.now();
this._updateUsageTime();
}
pageClosed() {
this._client.submit("custom");
this._client.timespan(category, name, value);
break;
case "boolean":
this._client.boolean(category, name, value);
break;
default:
throw new Error(`Metric type is not supported: ${type}`);
}
}
addAndGetTranslationTimeStamp(numWords, engineTimeElapsed) {
@ -44,11 +66,11 @@ class Telemetry {
return engineWps;
}
addOutboundTranslation(textArea, textToTranslate) {
this._otLenthPerTextArea.set(textArea, {
chars: textToTranslate.length,
words: textToTranslate.trim().split(" ").length
});
addOutboundTranslation(textAreaId, textToTranslate) {
this._otLenthPerTextArea.set(textAreaId, {
chars: textToTranslate.length,
words: textToTranslate.trim().split(" ").length
});
let charLengthSum = 0;
let wordLengthSum = 0;
this._otLenthPerTextArea.forEach(v => {
@ -61,9 +83,7 @@ class Telemetry {
this._updateUsageTime();
}
addQualityEstimation(wordScores, sentScores, isSupervised) {
this._client.boolean("quality", "is_supervised", isSupervised);
addQualityEstimation(wordScores, sentScores) {
// scores are log probabilities, convert back to probabilities
for (const score of wordScores) this._wordScores.push(Math.exp(score));
for (const score of sentScores) this._sentScores.push(Math.exp(score));
@ -87,9 +107,9 @@ class Telemetry {
_calcStats(array) {
array.sort();
const sum = array.reduce((a, b) => a + b, 0);
const avg = (sum / array.length) || 0;
const median = array[Math.floor(array.length/2)-1] || 0;
const perc90 = array[Math.floor(array.length*0.9)-1] || 0;
const avg = ((sum / array.length) || 0).toFixed(3);
const median = (array[Math.floor(array.length/2)-1] || 0).toFixed(3);
const perc90 = (array[Math.floor(array.length*0.9)-1] || 0).toFixed(3);
return { avg, median, perc90 }
}
@ -119,45 +139,8 @@ class Telemetry {
this._client.string("metadata", "bergamot_translator_version", engineVersion);
}
infobarEvent(name) {
this._client.event("infobar", name);
this._updateUsageTime();
/* event corresponds to user action, but boolean value is useful to report the state and to filter */
if (name === "outbound_checked") {
this.infobarState("outbound_enabled", true);
} else if (name === "outbound_unchecked") {
this.infobarState("outbound_enabled", false);
}
}
infobarState(name, val) {
this._client.boolean("infobar", name, val);
}
formsEvent(name) {
this._client.event("forms", name);
this._updateUsageTime();
}
error(name) {
this._client.increment("errors", name);
}
langMismatch() {
this._client.increment("service", "lang_mismatch");
}
langNotSupported() {
this._client.increment("service", "not_supported");
}
performanceTime(metric, timeMs) {
this._client.timespan("performance", metric, timeMs);
}
wordsInViewport(val) {
this._client.quantity("performance", "word_count_visible_in_viewport", val);
submit() {
this._client.submit("custom");
}
_updateUsageTime() {

Просмотреть файл

@ -353,7 +353,7 @@ class InPageTranslation {
viewPortWordsNum += value.textContent.trim().split(/\s+/).length;
}
this.notifyMediator("viewPortWordsNum", viewPortWordsNum);
this.notifyMediator("reportViewPortWordsNum", viewPortWordsNum);
// report words in viewport only for initially loaded content
this.initialWordsInViewportReported = true;
}
@ -596,7 +596,12 @@ class InPageTranslation {
currentNode = nodeIterator.nextNode();
}
}
this.notifyMediator("reportQeMetrics", { wordScores: Array.from(wordScores.values()), sentScores: Array.from(sentScores.values()) });
if ((sentScores.size > 0) || (wordScores.size > 0)) {
this.notifyMediator("reportQeMetrics", {
wordScores: Array.from(wordScores.values()),
sentScores: Array.from(sentScores.values())
});
}
}
enqueueElement(translationMessage) {

Просмотреть файл

@ -121,7 +121,7 @@ class OutboundTranslation {
// and clear its forms
this.otTextArea.value = "";
this.backTranslationsTextArea.value = "";
this.notifyMediator("onFormsEvent", "hidden");
this.notifyMediator("reportFormsEvent", "hidden");
}
});
@ -131,7 +131,7 @@ class OutboundTranslation {
this.otTextArea.value = widgetContent.typedContent;
this.backTranslationsTextArea.value = widgetContent.translatedContent;
}
this.notifyMediator("onFormsEvent", "displayed");
this.notifyMediator("reportFormsEvent", "displayed");
}
sendTextToTranslation() {

Просмотреть файл

@ -51,7 +51,7 @@ class TranslationNotificationManager {
STATE_TRANSLATED: 2,
STATE_ERROR: 3,
STATE_UNAVAILABLE: 4,
};
};
}
set localizedLabels(val) {
@ -63,9 +63,9 @@ class TranslationNotificationManager {
}
loadLanguages() {
for (const languagePair of Object.keys(this.modelRegistry)){
const firstLang = languagePair.substring(0,2);
const secondLang = languagePair.substring(2,4);
for (const languagePair of Object.keys(this.modelRegistry)) {
const firstLang = languagePair.substring(0, 2);
const secondLang = languagePair.substring(2, 4);
this.languageSet.add(firstLang);
this.languageSet.add(secondLang);
@ -79,32 +79,40 @@ class TranslationNotificationManager {
}
}
reportInfobarEvent(name) {
reportInfobarMetric(type, name, value) {
/*
* propagate UI event to bgScript
* to have the mediator notified
*/
const message = { command: "onInfobarEvent", tabId: this.tabId, name };
const message = {
command: "recordTelemetry",
tabId: this.tabId,
type,
category: "infobar",
name,
value
};
this.bgScriptListenerCallback(message);
}
requestInPageTranslation(from, to, withOutboundTranslation, withQualityEstimation){
requestInPageTranslation(from, to, withOutboundTranslation, withQualityEstimation) {
/*
* request received. let's forward to the background script in order
* to have the mediator notified
*/
const message = { command: "translationRequested",
from,
to,
withOutboundTranslation,
withQualityEstimation,
tabId: this.tabId };
const message = {
command: "translationRequested",
from,
to,
withOutboundTranslation,
withQualityEstimation,
tabId: this.tabId
};
this.bgScriptListenerCallback(message);
}
enableStats(){
enableStats() {
/*
* notify the mediator that the user wants to see statistics

Просмотреть файл

@ -130,7 +130,15 @@ window.MozTranslationNotification = class extends MozElements.Notification {
}
this.state = this.translationNotificationManager.TranslationInfoBarStates.STATE_OFFER;
this.translationNotificationManager.reportInfobarEvent("displayed");
this.translationNotificationManager.reportInfobarMetric("event", "displayed");
this.translationNotificationManager.reportInfobarMetric(
"boolean", "outbound_enabled",
this._getAnonElt("outboundtranslations-check").checked === true
);
this.translationNotificationManager.reportInfobarMetric(
"boolean", "qe_enabled",
this._getAnonElt("qualityestimations-check").checked === true
);
}
_getAnonElt(anonId) {
@ -138,12 +146,12 @@ window.MozTranslationNotification = class extends MozElements.Notification {
}
fromLanguageChanged() {
this.translationNotificationManager.reportInfobarEvent("change_lang");
this.translationNotificationManager.reportInfobarMetric("event","change_lang");
this._getAnonElt("translate").disabled = false;
}
translate() {
this.translationNotificationManager.reportInfobarEvent("translate");
this.translationNotificationManager.reportInfobarMetric("event","translate");
const from = this._getSourceLang();
const to = this._getTargetLang();
this.translationNotificationManager.requestInPageTranslation(
@ -159,19 +167,22 @@ window.MozTranslationNotification = class extends MozElements.Notification {
onOutboundClick() {
if (this._getAnonElt("outboundtranslations-check").checked) {
this.translationNotificationManager.reportInfobarEvent("outbound_checked");
this.translationNotificationManager.reportInfobarMetric("event", "outbound_checked");
this.translationNotificationManager.reportInfobarMetric("boolean", "outbound_enabled", true);
} else {
this.translationNotificationManager.reportInfobarEvent("outbound_unchecked");
this.translationNotificationManager.reportInfobarMetric("event", "outbound_unchecked");
this.translationNotificationManager.reportInfobarMetric("boolean", "outbound_enabled", false);
}
}
onQeClick() {
// eslint-disable-next-line no-warning-comments
// todo: report boolean qe_enabled after iframe support is merged
if (this._getAnonElt("qualityestimations-check").checked) {
this.translationNotificationManager.reportInfobarEvent("qe_checked");
this.translationNotificationManager.reportInfobarMetric("event","qe_checked");
this.translationNotificationManager.reportInfobarMetric("boolean", "qe_enabled", true);
} else {
this.translationNotificationManager.reportInfobarEvent("qe_unchecked");
this.translationNotificationManager.reportInfobarMetric("event","qe_unchecked");
this.translationNotificationManager.reportInfobarMetric("boolean", "qe_enabled", false);
}
}
@ -180,7 +191,7 @@ window.MozTranslationNotification = class extends MozElements.Notification {
* by clicking the notification's close button, the not now button or choosing never to translate)
*/
closeCommand() {
this.translationNotificationManager.reportInfobarEvent("closed");
this.translationNotificationManager.reportInfobarMetric("event","closed");
this.close();
}
@ -242,7 +253,7 @@ window.MozTranslationNotification = class extends MozElements.Notification {
}
neverForLanguage() {
this.translationNotificationManager.reportInfobarEvent("never_translate_lang");
this.translationNotificationManager.reportInfobarMetric("event","never_translate_lang");
const kPrefName = "browser.translation.neverForLanguages";
const sourceLang = this._getSourceLang();
@ -258,7 +269,7 @@ window.MozTranslationNotification = class extends MozElements.Notification {
}
neverForSite() {
this.translationNotificationManager.reportInfobarEvent("never_translate_site");
this.translationNotificationManager.reportInfobarMetric("event","never_translate_site");
const principal = this.translationNotificationManager.browser.contentPrincipal;
const perms = Services.perms;
perms.addFromPrincipal(principal, "translate", perms.DENY_ACTION);

Просмотреть файл

@ -7,6 +7,7 @@ prefs =
[browser_translation_test.js]
support-files =
browser_translation_test.html
frame.html
esen/lex.50.50.esen.s2t.bin
esen/model.esen.intgemm.alphas.bin
esen/vocab.esen.spm

Просмотреть файл

@ -7,5 +7,6 @@
<body>
<div id="translationDiv">Hola mundo. Eso es una prueba de testes de traducciones.</div>
<textarea id="mainTextarea"></textarea>
<iframe id="iframe" src="frame.html" title="Iframe test page"></iframe>
</body>
</html>

Просмотреть файл

@ -2,6 +2,7 @@
/* eslint-disable no-undef */
/* eslint-disable max-lines-per-function */
requestLongerTimeout(2);
const baseURL = getRootDirectory(gTestPath).replace(
"chrome://mochitests/content",
@ -63,37 +64,40 @@ add_task(async function testTranslationBarDisplayed() {
// and check if the translation happened
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
await new Promise(resolve => content.setTimeout(resolve, 5000));
const checkTranslation = async (document, message) => {
is(
document.getElementById("translationDiv").innerHTML,
"Hello world. That's a test of translation tests.",
`Text was correctly translated. (${message})`
);
is(
content.document.getElementById("translationDiv").innerHTML,
"Hello world. That's a test of translation tests.",
"Text was correctly translated."
);
/*
* let's now select the outbound translation form
* await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
*/
document.getElementById("mainTextarea").focus();
document.getElementById("OTapp").querySelectorAll("textarea")[0].value = "Hello World";
document.getElementById("OTapp").querySelectorAll("textarea")[0].dispatchEvent(new content.KeyboardEvent('keydown', {'key': 'Enter'}));
await new Promise(resolve => content.setTimeout(resolve, 5000));
is(
document.getElementById("mainTextarea").value.trim(),
"Hola Mundo",
`Form translation text was correctly translated. (${message})`
);
is(
document.getElementById("OTapp").querySelectorAll("textarea")[1].value.trim(),
"Hello World",
`Back Translation text was correctly translated. (${message})`
);
}
await new Promise(resolve => content.setTimeout(resolve, 10000));
await checkTranslation(content.document, "main frame");
await checkTranslation(content.document.getElementById("iframe").contentWindow.document, "iframe");
});
// let's now select the outbound translation form
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
content.document.getElementById("mainTextarea").focus();
content.document.getElementById("OTapp").querySelectorAll("textarea")[0].value = "Hello World";
content.document.getElementById("OTapp").querySelectorAll("textarea")[0].dispatchEvent(new content.KeyboardEvent('keydown', {'key': 'Enter'}));
await new Promise(resolve => content.setTimeout(resolve, 5000));
is(
content.document.getElementById("mainTextarea").value.trim(),
"Hola Mundo",
"Form translation text was correctly translated."
);
is(
content.document.getElementById("OTapp").querySelectorAll("textarea")[1].value.trim(),
"Hello World",
"Back Translation text was correctly translated."
);
});
delete window.MozTranslationNotification;
delete window.now;
notification.close();

Просмотреть файл

@ -24,6 +24,7 @@ subprocess.call("unzip web-ext-artifacts/firefox_translations.xpi -d gecko/brows
subprocess.call("mkdir -p gecko/browser/extensions/translations/test/browser/".split(), cwd=root)
subprocess.call("cp scripts/tests/browser.ini gecko/browser/extensions/translations/test/browser/".split(), cwd=root)
subprocess.call("cp scripts/tests/browser_translation_test.html gecko/browser/extensions/translations/test/browser/".split(), cwd=root)
subprocess.call("cp scripts/tests/frame.html gecko/browser/extensions/translations/test/browser/".split(), cwd=root)
subprocess.call("cp scripts/tests/browser_translation_test.js gecko/browser/extensions/translations/test/browser/".split(), cwd=root)
subprocess.call("cp -r scripts/tests/esen/ gecko/browser/extensions/translations/test/browser/esen/".split(), cwd=root)
subprocess.call("cp -r scripts/tests/enes/ gecko/browser/extensions/translations/test/browser/enes/".split(), cwd=root)

11
scripts/tests/frame.html Normal file
Просмотреть файл

@ -0,0 +1,11 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Iframe test.</title>
</head>
<body>
<div id="translationDiv">Hola mundo. Eso es una prueba de testes de traducciones.</div>
<textarea id="mainTextarea"></textarea>
</body>
</html>