chromium/chrome/test/data/extensions/api_test/tracing_extension/background.js

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

const PROTOCOL_VERSION = '1.3';

const HIGH_ENTROPY_API_CATEGORY =
    'disabled-by-default-identifiability.high_entropy_api';
// We will only analyse traces from the HIGH_ENTROPY_API_CATEGORY. While we
// could choose any other category, traces from this one are easy to trigger
// (by calling the respective apis in the html document) and contain a lot of
// information by default (like the execution context). Since they are
// disabled by default we check category enabling on the way.
const includedCategories = [HIGH_ENTROPY_API_CATEGORY];
// Exclude all traces but the ones we explicitly define using the
// includedCategories. Even so, '__metadata' traces will be received, for which
// reason we need to filter the traces later using their category field.
const excludedCategories = ['*'];
const traceConfig = {
  includedCategories,
  excludedCategories,
  // Ensure that we get multiple tracing packages in event based mode that we
  // need to join, in combination with the current number of API calls in the
  // html file. This is just to cover more code paths with this test.
  traceBufferSizeInKb: 100 * 1000,  // 100MB
};
const tracingParams = {traceConfig};

// Waits for tab to be completely loaded. This must be true for the API calls to
// be fetched before tracing is stopped.
function updateTab(tabId, pageUrl) {
  chrome.tabs.update(tabId, {url: pageUrl});

  return new Promise((resolve) => {
    const listener = (_, info, t) => {
      if (t.id === tabId && info.status === 'complete') {
        chrome.tabs.onUpdated.removeListener(listener);
        resolve();
      }
    };
    chrome.tabs.onUpdated.addListener(listener);
  });
}

// Filters and returns only traces from the HIGH_ENTROPY_API_CATEGORY.
// After we end tracing using sendCommand, dataCollected flow in followed
// by a final tracingComplete event. The number of dataCollected events
// depends on the amount of traced information; since we limit the buffer size
// in the traceConfig we will receive more than one such event (in combination
// with calling enough APIs in the html page).
async function getIdentifiabilityTraces() {
  let traces = [];
  await new Promise((resolve) => {
    const listener = (source, method, params) => {
      if (method === 'Tracing.dataCollected' && params && params.value) {
        traces = traces.concat(params.value.filter(
            (trace) => trace.cat === HIGH_ENTROPY_API_CATEGORY &&
                       trace.name === "HighEntropyJavaScriptAPICall"));
      } else if (method === 'Tracing.tracingComplete') {
        chrome.debugger.onEvent.removeListener(listener);
        resolve();
      }
    };
    chrome.debugger.onEvent.addListener(listener);
  });
  return traces;
}

chrome.test.getConfig((config) => {
  const crossOriginA = `http://a.test:${config.testServer.port}`;
  const crossOriginB = `http://b.test:${config.testServer.port}`;

  const filterOriginA = (traces) => {
    return traces.filter(
        (trace) => trace.args.high_entropy_api.execution_context.origin ===
            crossOriginA);
  };
  const filterOriginB = (traces) => {
    return traces.filter(
        (trace) => trace.args.high_entropy_api.execution_context.origin ===
            crossOriginB);
  };

  const htmlPath =
      '/extensions/api_test/tracing_extension/test_api_tracing.html';
  const pageA = `${crossOriginA}${htmlPath}`;
  const pageB = `${crossOriginB}${htmlPath}`;

  // Number of API calls triggered by loading the html file one time.
  // Depends on the number of api calls emitted by the inline script in the html
  // file and the number of high entropy traces emitted for each such call.
  const API_CALL_COUNT = 62;

  chrome.test.runTests([
    // The current tab's traces should be receiveable using the default tracing
    // filter, which filters the current tab's processes ('currentTarget').
    async function traceTargetTabEventBased() {
      const targetTab = await chrome.tabs.create({url: 'about:blank'});
      const debuggee = {tabId: targetTab.id};

      await chrome.debugger.attach(debuggee, PROTOCOL_VERSION);

      await chrome.debugger.sendCommand(
          debuggee, 'Tracing.start', tracingParams);

      // API calls are triggered using reload events on the current target.
      await updateTab(targetTab.id, pageB);
      await updateTab(targetTab.id, pageA);

      // Traces are received using events after the end command is send.
      chrome.debugger.sendCommand(debuggee, 'Tracing.end');
      const traces = await getIdentifiabilityTraces();

      // Check if traces can be received from different origins and are
      // attributed to them.
      chrome.test.assertEq(API_CALL_COUNT, filterOriginA(traces).length);
      chrome.test.assertEq(API_CALL_COUNT, filterOriginB(traces).length);

      chrome.debugger.detach(debuggee);
      await chrome.tabs.remove(targetTab.id);
      chrome.test.succeed();
    },

    // Only traces from the target tab should be received.
    async function notTraceOtherTabEventBased() {
      const targetTab = await chrome.tabs.create({url: 'about:blank'});
      const otherTab = await chrome.tabs.create({url: 'about:blank'});
      const debuggee = {tabId: targetTab.id};

      await chrome.debugger.attach(debuggee, PROTOCOL_VERSION);

      await chrome.debugger.sendCommand(
          debuggee, 'Tracing.start', tracingParams);

      // API calls are triggered using reload events on the tabs.
      await updateTab(otherTab.id, pageA);
      await updateTab(targetTab.id, pageB);

      chrome.debugger.sendCommand(debuggee, 'Tracing.end');
      const traces = await getIdentifiabilityTraces();

      // All the API calls triggered in the other tab should be ignored.
      chrome.test.assertEq(0, filterOriginA(traces).length);
      // Only receive the target tabs traces.
      chrome.test.assertEq(API_CALL_COUNT, filterOriginB(traces).length);

      chrome.debugger.detach(debuggee);

      await chrome.tabs.remove(targetTab.id);
      await chrome.tabs.remove(otherTab.id);
      chrome.test.succeed();
    },
  ]);
});