Commit 37cc551c authored by Wei Lee's avatar Wei Lee Committed by Commit Bot

[CCA WebUI] Extracts GA metrics sending to untrusted context

Since we access external websites when sending GA metrics, it should be
extracted to untrusted context.

Bug: 980846
Test: Send GA metrics on CCA and both version (Platform App/SWA) works
normally

Change-Id: I14702129e4301327fd8a918d716420acbb43a94c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2352263
Commit-Queue: Wei Lee <wtlee@chromium.org>
Reviewed-by: default avatarInker Kuo <inker@chromium.org>
Cr-Commit-Position: refs/heads/master@{#822522}
parent 49765c9f
......@@ -75,6 +75,13 @@ content::WebUIDataSource* CreateUntrustedCameraAppUIHTMLSource() {
}
untrusted_source->AddFrameAncestor(GURL(kChromeUICameraAppURL));
untrusted_source->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::ConnectSrc,
std::string("connect-src http://www.google-analytics.com/ 'self';"));
untrusted_source->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::TrustedTypes,
std::string("trusted-types ga-js-static;"));
return untrusted_source;
}
......
......@@ -375,6 +375,7 @@ module.exports = {
'chromeosCamera': 'readable',
'blink': 'readable',
'cros': 'readable',
'trustedTypes': 'readable',
'webkitRequestFileSystem': 'readable',
},
// Generally, the rules should be compatible to both bundled and the newest
......
......@@ -118,6 +118,8 @@ copy("chrome_camera_app_js") {
"js/toast.js",
"js/tooltip.js",
"js/type.js",
"js/untrusted_ga_helper.js",
"js/untrusted_helper_interfaces.js",
"js/untrusted_script_loader.js",
"js/util.js",
"js/waitable_event.js",
......
......@@ -73,6 +73,8 @@
<structure name="IDR_CAMERA_TOAST_JS" file="js/toast.js" type="chrome_html" />
<structure name="IDR_CAMERA_TOOLTIP_JS" file="js/tooltip.js" type="chrome_html" />
<structure name="IDR_CAMERA_TYPE_JS" file="js/type.js" type="chrome_html" />
<structure name="IDR_CAMERA_UNTRUSTED_GA_HELPER_JS" file="js/untrusted_ga_helper.js" type="chrome_html" />
<structure name="IDR_CAMERA_UNTRUSTED_HELPER_INTERFACES_JS" file="js/untrusted_helper_interfaces.js" type="chrome_html" />
<structure name="IDR_CAMERA_UNTRUSTED_SCRIPT_LOADER_HTML" file="views/untrusted_script_loader.html" type="chrome_html" />
<structure name="IDR_CAMERA_UNTRUSTED_SCRIPT_LOADER_JS" file="js/untrusted_script_loader.js" type="chrome_html" />
<structure name="IDR_CAMERA_UTIL_JS" file="js/util.js" type="chrome_html" />
......
......@@ -65,6 +65,8 @@ js_library("compile_resources") {
"toast.js",
"tooltip.js",
"type.js",
"untrusted_ga_helper.js",
"untrusted_helper_interfaces.js",
"untrusted_script_loader.js",
"util.js",
"views/camera.js",
......
......@@ -151,10 +151,8 @@ class ChromeAppBrowserProxy {
}
/** @override */
addDummyHistoryIfNotAvailable() {
// Since GA will use history.length to generate hash but it is not available
// in platform apps, set it to 1 manually.
window.history.length = 1;
shouldAddFakeHistory() {
return true;
}
/** @override */
......
......@@ -107,9 +107,10 @@ export class BrowserProxy {
getTextDirection() {}
/**
* @return {boolean}
* @abstract
*/
addDummyHistoryIfNotAvailable() {}
shouldAddFakeHistory() {}
/**
* @return {boolean}
......
......@@ -144,8 +144,8 @@ class WebUIBrowserProxy {
}
/** @override */
addDummyHistoryIfNotAvailable() {
// no-ops
shouldAddFakeHistory() {
return false;
}
/** @override */
......
......@@ -39,6 +39,9 @@ import {Warning} from './views/warning.js';
import {windowController} from './window_controller/window_controller.js';
/**
* The app window instance which is used for communication with Tast tests. For
* non-test sessions or test sessions but using the legacy communication
* solution (chrome.runtime), it should be null.
* @type {?AppWindow}
*/
const appWindow = window['appWindow'];
......@@ -304,7 +307,7 @@ let instance = null;
const testErrorCallback = bgOps.getTestingErrorCallback();
metrics.initMetrics();
if (testErrorCallback !== null) {
if (testErrorCallback !== null || appWindow !== null) {
metrics.setMetricsEnabled(false);
}
......
......@@ -4,7 +4,7 @@
import {browserProxy} from './browser_proxy/browser_proxy.js';
import {assert} from './chrome_util.js';
// eslint-disable-next-line no-unused-vars
import * as Comlink from './lib/comlink.js';
import * as state from './state.js';
import {
Facing, // eslint-disable-line no-unused-vars
......@@ -13,6 +13,10 @@ import {
PerfInformation, // eslint-disable-line no-unused-vars
Resolution, // eslint-disable-line no-unused-vars
} from './type.js';
// eslint-disable-next-line no-unused-vars
import {GAHelperInterface} from './untrusted_helper_interfaces.js';
import * as util from './util.js';
import {WaitableEvent} from './waitable_event.js';
/**
* The tracker ID of the GA metrics.
......@@ -26,25 +30,17 @@ const GA_ID = 'UA-134822711-1';
let baseDimen = null;
/**
* @type {?Promise}
*/
let ready = null;
/**
* @type {boolean}
* @type {!WaitableEvent}
*/
let isMetricsEnabled = false;
const ready = new WaitableEvent();
/**
* Disable metrics sending if either the logging consent option is disabled or
* metrics is disabled for current session. (e.g. Running tests)
* @return {!Promise}
* @type {!Promise<!GAHelperInterface>}
*/
async function disableMetricsIfNotAllowed() {
// This value reflects the logging constent option in OS settings.
const canSendMetrics = await browserProxy.isMetricsAndCrashReportingEnabled();
window[`ga-disable-${GA_ID}`] = !isMetricsEnabled || !canSendMetrics;
}
const gaHelper = (async () => {
return /** @type {!GAHelperInterface} */ (await util.createUntrustedJSModule(
'/js/untrusted_ga_helper.js', browserProxy.getUntrustedOrigin()));
})();
/**
* Send the event to GA backend.
......@@ -53,11 +49,6 @@ async function disableMetricsIfNotAllowed() {
* information.
*/
async function sendEvent(event, dimen = null) {
assert(window.ga !== null);
assert(ready !== null);
await ready;
await disableMetricsIfNotAllowed();
const assignDimension = (e, d) => {
d.forEach((value, key) => e[`dimension${key}`] = value);
};
......@@ -67,7 +58,14 @@ async function sendEvent(event, dimen = null) {
if (dimen !== null) {
assignDimension(event, dimen);
}
window.ga('send', 'event', event);
await ready.wait();
// This value reflects the logging constent option in OS settings.
const canSendMetrics = await browserProxy.isMetricsAndCrashReportingEnabled();
if (canSendMetrics) {
(await gaHelper).sendGAEvent(event);
}
}
/**
......@@ -75,32 +73,15 @@ async function sendEvent(event, dimen = null) {
* is enabled AND the logging consent option is enabled in OS settings.
* @param {boolean} enabled True if the metrics is enabled.
*/
export function setMetricsEnabled(enabled) {
assert(ready !== null);
isMetricsEnabled = enabled;
export async function setMetricsEnabled(enabled) {
await ready.wait();
await (await gaHelper).setMetricsEnabled(GA_ID, enabled);
}
/**
* Initializes metrics with parameters.
*/
export function initMetrics() {
ready = (async () => {
browserProxy.addDummyHistoryIfNotAvailable();
// GA initialization function which is mostly copied from
// https://developers.google.com/analytics/devguides/collection/analyticsjs.
(function(i, s, o, g, r) {
i['GoogleAnalyticsObject'] = r;
i[r] = i[r] || function(...args) {
(i[r].q = i[r].q || []).push(args);
}, i[r].l = new Date().getTime();
const a = s.createElement(o);
const m = s.getElementsByTagName(o)[0];
a['async'] = 1;
a['src'] = g;
m.parentNode.insertBefore(a, m);
})(window, document, 'script', '../js/lib/analytics.js', 'ga');
export async function initMetrics() {
const board = await browserProxy.getBoard();
const boardName = /^(x86-)?(\w*)/.exec(board)[0];
const match = navigator.appVersion.match(/CrOS\s+\S+\s+([\d.]+)/);
......@@ -110,33 +91,20 @@ export function initMetrics() {
[2, osVer],
]);
// By default GA stores the user ID in cookies. Change to store in local
// storage instead.
const GA_LOCAL_STORAGE_KEY = 'google-analytics.analytics.user-id';
const gaLocalStorage =
await browserProxy.localStorageGet({[GA_LOCAL_STORAGE_KEY]: null});
window.ga('create', GA_ID, {
'storage': 'none',
'clientId': gaLocalStorage[GA_LOCAL_STORAGE_KEY] || null,
});
window.ga(
(tracker) => browserProxy.localStorageSet(
{[GA_LOCAL_STORAGE_KEY]: tracker.get('clientId')}));
// By default GA uses a dummy image and sets its source to the target URL to
// record metrics. Since requesting remote image violates the policy of
// a platform app, use navigator.sendBeacon() instead.
window.ga('set', 'transport', 'beacon');
// By default GA only accepts "http://" and "https://" protocol. Bypass the
// check here since we are "chrome-extension://".
window.ga('set', 'checkProtocolTask', null);
})();
ready.then(async () => {
// The metrics is default enabled.
await setMetricsEnabled(true);
});
const clientId = gaLocalStorage[GA_LOCAL_STORAGE_KEY];
const setClientId = (id) => {
browserProxy.localStorageSet({[GA_LOCAL_STORAGE_KEY]: id});
};
await (await gaHelper)
.initGA(
GA_ID, clientId, browserProxy.shouldAddFakeHistory(),
Comlink.proxy(setClientId));
ready.signal();
}
/**
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// eslint-disable-next-line no-unused-vars
import {GAHelperInterface} from './untrusted_helper_interfaces.js';
/**
* The GA library URL in trusted type.
* @type {!TrustedScriptURL}
*/
const gaLibraryURL = (() => {
const staticUrlPolicy = trustedTypes.createPolicy(
'ga-js-static', {createScriptURL: () => '../js/lib/analytics.js'});
return staticUrlPolicy.createScriptURL('');
})();
/**
* Initializes GA for sending metrics.
* @param {string} id The GA tracker ID to send metrics.
* @param {string} clientId The GA client ID representing the current client.
* @param {boolean} shouldAddFakeHistory True for platform app and false for
* SWA.
* @param {function(string): void} setClientIdCallback Callback to store
* client id.
* @return {!Promise}
*/
async function initGA(id, clientId, shouldAddFakeHistory, setClientIdCallback) {
if (shouldAddFakeHistory) {
// Since GA will use history.length to generate hash but it is not
// available in platform apps, set it to 1 manually.
window.history.length = 1;
}
// GA initialization function which is mostly copied from
// https://developers.google.com/analytics/devguides/collection/analyticsjs.
(function(i, s, o, g, r) {
i['GoogleAnalyticsObject'] = r;
i[r] = i[r] || function(...args) {
(i[r].q = i[r].q || []).push(args);
}, i[r].l = new Date().getTime();
const a = s.createElement(o);
const m = s.getElementsByTagName(o)[0];
a['async'] = 1;
a['src'] = g;
m.parentNode.insertBefore(a, m);
})(window, document, 'script', gaLibraryURL, 'ga');
window.ga('create', id, {
'storage': 'none',
'clientId': clientId,
});
window.ga((tracker) => setClientIdCallback(tracker.get('clientId')));
// By default GA uses a fake image and sets its source to the target URL to
// record metrics. Since requesting remote image violates the policy of
// a platform app, use navigator.sendBeacon() instead.
window.ga('set', 'transport', 'beacon');
// By default GA only accepts "http://" and "https://" protocol. Bypass the
// check here since we are "chrome-extension://".
window.ga('set', 'checkProtocolTask', null);
}
/**
* Sends event to GA.
* @param {!ga.Fields} event Event to send.
* @return {!Promise}
*/
async function sendGAEvent(event) {
window.ga('send', 'event', event);
}
/**
* Sets if GA can send metrics.
* @param {string} id The GA tracker ID.
* @param {boolean} enabled True if the metrics is enabled.
* @return {!Promise}
*/
async function setMetricsEnabled(id, enabled) {
window[`ga-disable-${id}`] = !enabled;
}
export /** !GAHelperInterface */ {initGA, sendGAEvent, setMetricsEnabled};
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @typedef {{
* initGA: function(string, string, boolean, function(string): void):
* !Promise,
* sendGAEvent: function(!ga.Fields): !Promise,
* setMetricsEnabled: function(string, boolean): !Promise,
* }}
*/
export let GAHelperInterface;
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment