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() { ...@@ -75,6 +75,13 @@ content::WebUIDataSource* CreateUntrustedCameraAppUIHTMLSource() {
} }
untrusted_source->AddFrameAncestor(GURL(kChromeUICameraAppURL)); 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; return untrusted_source;
} }
......
...@@ -375,6 +375,7 @@ module.exports = { ...@@ -375,6 +375,7 @@ module.exports = {
'chromeosCamera': 'readable', 'chromeosCamera': 'readable',
'blink': 'readable', 'blink': 'readable',
'cros': 'readable', 'cros': 'readable',
'trustedTypes': 'readable',
'webkitRequestFileSystem': 'readable', 'webkitRequestFileSystem': 'readable',
}, },
// Generally, the rules should be compatible to both bundled and the newest // Generally, the rules should be compatible to both bundled and the newest
......
...@@ -118,6 +118,8 @@ copy("chrome_camera_app_js") { ...@@ -118,6 +118,8 @@ copy("chrome_camera_app_js") {
"js/toast.js", "js/toast.js",
"js/tooltip.js", "js/tooltip.js",
"js/type.js", "js/type.js",
"js/untrusted_ga_helper.js",
"js/untrusted_helper_interfaces.js",
"js/untrusted_script_loader.js", "js/untrusted_script_loader.js",
"js/util.js", "js/util.js",
"js/waitable_event.js", "js/waitable_event.js",
......
...@@ -73,6 +73,8 @@ ...@@ -73,6 +73,8 @@
<structure name="IDR_CAMERA_TOAST_JS" file="js/toast.js" type="chrome_html" /> <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_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_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_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_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" /> <structure name="IDR_CAMERA_UTIL_JS" file="js/util.js" type="chrome_html" />
......
...@@ -65,6 +65,8 @@ js_library("compile_resources") { ...@@ -65,6 +65,8 @@ js_library("compile_resources") {
"toast.js", "toast.js",
"tooltip.js", "tooltip.js",
"type.js", "type.js",
"untrusted_ga_helper.js",
"untrusted_helper_interfaces.js",
"untrusted_script_loader.js", "untrusted_script_loader.js",
"util.js", "util.js",
"views/camera.js", "views/camera.js",
......
...@@ -151,10 +151,8 @@ class ChromeAppBrowserProxy { ...@@ -151,10 +151,8 @@ class ChromeAppBrowserProxy {
} }
/** @override */ /** @override */
addDummyHistoryIfNotAvailable() { shouldAddFakeHistory() {
// Since GA will use history.length to generate hash but it is not available return true;
// in platform apps, set it to 1 manually.
window.history.length = 1;
} }
/** @override */ /** @override */
......
...@@ -107,9 +107,10 @@ export class BrowserProxy { ...@@ -107,9 +107,10 @@ export class BrowserProxy {
getTextDirection() {} getTextDirection() {}
/** /**
* @return {boolean}
* @abstract * @abstract
*/ */
addDummyHistoryIfNotAvailable() {} shouldAddFakeHistory() {}
/** /**
* @return {boolean} * @return {boolean}
......
...@@ -144,8 +144,8 @@ class WebUIBrowserProxy { ...@@ -144,8 +144,8 @@ class WebUIBrowserProxy {
} }
/** @override */ /** @override */
addDummyHistoryIfNotAvailable() { shouldAddFakeHistory() {
// no-ops return false;
} }
/** @override */ /** @override */
......
...@@ -39,6 +39,9 @@ import {Warning} from './views/warning.js'; ...@@ -39,6 +39,9 @@ import {Warning} from './views/warning.js';
import {windowController} from './window_controller/window_controller.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} * @type {?AppWindow}
*/ */
const appWindow = window['appWindow']; const appWindow = window['appWindow'];
...@@ -304,7 +307,7 @@ let instance = null; ...@@ -304,7 +307,7 @@ let instance = null;
const testErrorCallback = bgOps.getTestingErrorCallback(); const testErrorCallback = bgOps.getTestingErrorCallback();
metrics.initMetrics(); metrics.initMetrics();
if (testErrorCallback !== null) { if (testErrorCallback !== null || appWindow !== null) {
metrics.setMetricsEnabled(false); metrics.setMetricsEnabled(false);
} }
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import {browserProxy} from './browser_proxy/browser_proxy.js'; import {browserProxy} from './browser_proxy/browser_proxy.js';
import {assert} from './chrome_util.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 * as state from './state.js';
import { import {
Facing, // eslint-disable-line no-unused-vars Facing, // eslint-disable-line no-unused-vars
...@@ -13,6 +13,10 @@ import { ...@@ -13,6 +13,10 @@ import {
PerfInformation, // eslint-disable-line no-unused-vars PerfInformation, // eslint-disable-line no-unused-vars
Resolution, // eslint-disable-line no-unused-vars Resolution, // eslint-disable-line no-unused-vars
} from './type.js'; } 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. * The tracker ID of the GA metrics.
...@@ -26,25 +30,17 @@ const GA_ID = 'UA-134822711-1'; ...@@ -26,25 +30,17 @@ const GA_ID = 'UA-134822711-1';
let baseDimen = null; let baseDimen = null;
/** /**
* @type {?Promise} * @type {!WaitableEvent}
*/ */
let ready = null; const ready = new WaitableEvent();
/** /**
* @type {boolean} * @type {!Promise<!GAHelperInterface>}
*/ */
let isMetricsEnabled = false; const gaHelper = (async () => {
return /** @type {!GAHelperInterface} */ (await util.createUntrustedJSModule(
/** '/js/untrusted_ga_helper.js', browserProxy.getUntrustedOrigin()));
* Disable metrics sending if either the logging consent option is disabled or })();
* metrics is disabled for current session. (e.g. Running tests)
* @return {!Promise}
*/
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;
}
/** /**
* Send the event to GA backend. * Send the event to GA backend.
...@@ -53,11 +49,6 @@ async function disableMetricsIfNotAllowed() { ...@@ -53,11 +49,6 @@ async function disableMetricsIfNotAllowed() {
* information. * information.
*/ */
async function sendEvent(event, dimen = null) { async function sendEvent(event, dimen = null) {
assert(window.ga !== null);
assert(ready !== null);
await ready;
await disableMetricsIfNotAllowed();
const assignDimension = (e, d) => { const assignDimension = (e, d) => {
d.forEach((value, key) => e[`dimension${key}`] = value); d.forEach((value, key) => e[`dimension${key}`] = value);
}; };
...@@ -67,7 +58,14 @@ async function sendEvent(event, dimen = null) { ...@@ -67,7 +58,14 @@ async function sendEvent(event, dimen = null) {
if (dimen !== null) { if (dimen !== null) {
assignDimension(event, dimen); 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,68 +73,38 @@ async function sendEvent(event, dimen = null) { ...@@ -75,68 +73,38 @@ async function sendEvent(event, dimen = null) {
* is enabled AND the logging consent option is enabled in OS settings. * is enabled AND the logging consent option is enabled in OS settings.
* @param {boolean} enabled True if the metrics is enabled. * @param {boolean} enabled True if the metrics is enabled.
*/ */
export function setMetricsEnabled(enabled) { export async function setMetricsEnabled(enabled) {
assert(ready !== null); await ready.wait();
isMetricsEnabled = enabled; await (await gaHelper).setMetricsEnabled(GA_ID, enabled);
} }
/** /**
* Initializes metrics with parameters. * Initializes metrics with parameters.
*/ */
export function initMetrics() { export async function initMetrics() {
ready = (async () => { const board = await browserProxy.getBoard();
browserProxy.addDummyHistoryIfNotAvailable(); const boardName = /^(x86-)?(\w*)/.exec(board)[0];
const match = navigator.appVersion.match(/CrOS\s+\S+\s+([\d.]+)/);
// GA initialization function which is mostly copied from const osVer = match ? match[1] : '';
// https://developers.google.com/analytics/devguides/collection/analyticsjs. baseDimen = new Map([
(function(i, s, o, g, r) { [1, boardName],
i['GoogleAnalyticsObject'] = r; [2, osVer],
i[r] = i[r] || function(...args) { ]);
(i[r].q = i[r].q || []).push(args);
}, i[r].l = new Date().getTime(); const GA_LOCAL_STORAGE_KEY = 'google-analytics.analytics.user-id';
const a = s.createElement(o); const gaLocalStorage =
const m = s.getElementsByTagName(o)[0]; await browserProxy.localStorageGet({[GA_LOCAL_STORAGE_KEY]: null});
a['async'] = 1; const clientId = gaLocalStorage[GA_LOCAL_STORAGE_KEY];
a['src'] = g;
m.parentNode.insertBefore(a, m); const setClientId = (id) => {
})(window, document, 'script', '../js/lib/analytics.js', 'ga'); browserProxy.localStorageSet({[GA_LOCAL_STORAGE_KEY]: id});
};
const board = await browserProxy.getBoard();
const boardName = /^(x86-)?(\w*)/.exec(board)[0]; await (await gaHelper)
const match = navigator.appVersion.match(/CrOS\s+\S+\s+([\d.]+)/); .initGA(
const osVer = match ? match[1] : ''; GA_ID, clientId, browserProxy.shouldAddFakeHistory(),
baseDimen = new Map([ Comlink.proxy(setClientId));
[1, boardName], ready.signal();
[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);
});
} }
/** /**
......
// 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