blob: a132e88a4222bbef2214628d3b8334bd202fff5e [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/cr_elements/cr_tab_box/cr_tab_box.js';
import './attribution_detail_table.js';
import './attribution_internals_table.js';
import type {Origin} from 'chrome://resources/mojo/url/mojom/origin.mojom-webui.js';
import {AggregatableResult} from './aggregatable_result.mojom-webui.js';
import {AttributionSupport} from './attribution.mojom-webui.js';
import type {AttributionDetailTableElement} from './attribution_detail_table.js';
import type {HandlerInterface, NetworkStatus, ObserverInterface, ReportID, ReportStatus, WebUIAggregatableDebugReport, WebUIDebugReport, WebUIOsRegistration, WebUIRegistration, WebUIReport, WebUISource, WebUISourceRegistration, WebUITrigger} from './attribution_internals.mojom-webui.js';
import {Factory, HandlerRemote, ObserverReceiver, WebUISource_Attributability} from './attribution_internals.mojom-webui.js';
import type {AttributionInternalsTableElement, CompareFunc, DataColumn, InitOpts, RenderFunc} from './attribution_internals_table.js';
import {OsRegistrationResult, RegistrationType} from './attribution_reporting.mojom-webui.js';
import {EventLevelResult} from './event_level_result.mojom-webui.js';
import {ProcessAggregatableDebugReportResult} from './process_aggregatable_debug_report_result.mojom-webui.js';
import {SourceType} from './source_type.mojom-webui.js';
import {StoreSourceResult} from './store_source_result.mojom-webui.js';
import {TriggerDataMatching} from './trigger_data_matching.mojom-webui.js';
// If kAttributionAggregatableBudgetPerSource changes, update this value
const BUDGET_PER_SOURCE = 65536;
type Comparable = bigint|number|string|boolean|Date;
function compareDefault<T extends Comparable>(a: T, b: T): number {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
}
function undefinedFirst<V>(f: CompareFunc<V>): CompareFunc<V|undefined> {
return (a: V|undefined, b: V|undefined): number => {
if (a === undefined && b === undefined) {
return 0;
}
if (a === undefined) {
return -1;
}
if (b === undefined) {
return 1;
}
return f(a, b);
};
}
function compareLexicographic<V>(f: CompareFunc<V>): CompareFunc<V[]> {
return (a: V[], b: V[]): number => {
for (let i = 0; i < a.length && i < b.length; ++i) {
const r = f(a[i]!, b[i]!);
if (r !== 0) {
return r;
}
}
return compareDefault(a.length, b.length);
};
}
function bigintReplacer(_key: string, value: any): any {
return typeof value === 'bigint' ? value.toString() : value;
}
interface Valuable<V> {
readonly compare?: CompareFunc<V>;
readonly render: RenderFunc<V>;
}
function allowingUndefined<V>({render, compare}: Valuable<V>):
Valuable<V|undefined> {
return {
compare: compare ? undefinedFirst(compare) : undefined,
render: (td: HTMLElement, v: V|undefined) => {
if (v !== undefined) {
render(td, v);
}
},
};
}
function valueColumn<T, K extends keyof T>(
label: string, key: K, {render, compare}: Valuable<T[K]>,
defaultSort: boolean = false): DataColumn<T> {
return {
label,
render: (td, data) => render(td, data[key]),
compare: compare ? (a, b) => compare(a[key], b[key]) : undefined,
defaultSort,
};
}
const asDate: Valuable<Date> = {
compare: compareDefault,
render: (td: HTMLElement, v: Date) => {
const time = td.ownerDocument.createElement('time');
time.dateTime = v.toISOString();
time.innerText = v.toLocaleString();
td.replaceChildren(time);
},
};
const numberClass: string = 'number';
const asNumber: Valuable<bigint|number> = {
compare: compareDefault,
render: (td: HTMLElement, v: bigint|number) => {
td.classList.add(numberClass);
td.innerText = v.toString();
},
};
function asCustomNumber<V extends bigint|number>(fmt: (v: V) => string):
Valuable<V> {
return {
compare: compareDefault,
render: (td: HTMLElement, v: V) => {
td.classList.add(numberClass);
td.innerText = fmt(v);
},
};
}
const asStringOrBool: Valuable<string|boolean> = {
compare: compareDefault,
render: (td: HTMLElement, v: string|boolean) => td.innerText = v.toString(),
};
const asCode: Valuable<string> = {
render: (td: HTMLElement, v: string) => {
const code = td.ownerDocument.createElement('code');
code.innerText = v;
const pre = td.ownerDocument.createElement('pre');
pre.append(code);
td.replaceChildren(pre);
},
};
function asList<V>({render, compare}: Valuable<V>): Valuable<V[]> {
return {
compare: compare ? compareLexicographic(compare) : undefined,
render: (td: HTMLElement, vs: V[]) => {
if (vs.length === 0) {
td.replaceChildren();
return;
}
const ul = td.ownerDocument.createElement('ul');
for (const v of vs) {
const li = td.ownerDocument.createElement('li');
render(li, v);
ul.append(li);
}
td.replaceChildren(ul);
},
};
}
function renderUrl(td: HTMLElement, url: string): void {
const a = td.ownerDocument.createElement('a');
a.target = '_blank';
a.href = url;
a.innerText = url;
td.replaceChildren(a);
}
const asUrl: Valuable<string> = {
compare: compareDefault,
render: renderUrl,
};
function isAttributionSuccessDebugReport(url: string): boolean {
return url.includes('/.well-known/attribution-reporting/debug/');
}
interface Source {
id: bigint;
sourceEventId: bigint;
sourceOrigin: string;
destinations: string[];
reportingOrigin: string;
sourceTime: Date;
expiryTime: Date;
triggerData: number[];
eventReportWindowsStart: bigint;
eventReportWindowsEnds: bigint[];
maxEventLevelReports: number;
aggregatableReportWindowTime: Date;
sourceType: string;
filterData: string;
aggregationKeys: string;
debugKey?: bigint;
dedupKeys: bigint[];
priority: bigint;
status: string;
remainingAggregatableAttributionBudget: number;
aggregatableDedupKeys: bigint[];
triggerDataMatching: string;
eventLevelEpsilon: number;
cookieBasedDebugAllowed: boolean;
remainingAggregatableDebugBudget: number;
aggregatableDebugKeyPiece: string;
attributionScopesData: string;
aggregatableNamedBudgets: string;
}
function newSource(mojo: WebUISource): Source {
return {
id: mojo.id,
sourceEventId: mojo.sourceEventId,
sourceOrigin: originToText(mojo.sourceOrigin),
destinations:
mojo.destinations.destinations.map(d => originToText(d.siteAsOrigin))
.sort(compareDefault),
reportingOrigin: originToText(mojo.reportingOrigin),
sourceTime: new Date(mojo.sourceTime),
expiryTime: new Date(mojo.expiryTime),
triggerData: mojo.triggerData,
eventReportWindowsStart:
mojo.eventReportWindows.startTime.microseconds / 1000000n,
eventReportWindowsEnds:
mojo.eventReportWindows.endTimes.map(t => t.microseconds / 1000000n),
maxEventLevelReports: mojo.maxEventLevelReports,
aggregatableReportWindowTime: new Date(mojo.aggregatableReportWindowTime),
sourceType: sourceTypeText[mojo.sourceType],
priority: mojo.priority,
filterData: JSON.stringify(mojo.filterData.filterValues, null, ' '),
aggregationKeys: JSON.stringify(mojo.aggregationKeys, bigintReplacer, ' '),
debugKey: mojo.debugKey ?? undefined,
dedupKeys: mojo.dedupKeys.sort(compareDefault),
remainingAggregatableAttributionBudget:
mojo.remainingAggregatableAttributionBudget,
aggregatableDedupKeys: mojo.aggregatableDedupKeys.sort(compareDefault),
triggerDataMatching: triggerDataMatchingText[mojo.triggerDataMatching],
eventLevelEpsilon: mojo.eventLevelEpsilon,
status: attributabilityText[mojo.attributability],
cookieBasedDebugAllowed: mojo.cookieBasedDebugAllowed,
remainingAggregatableDebugBudget: mojo.remainingAggregatableDebugBudget,
aggregatableDebugKeyPiece: mojo.aggregatableDebugKeyPiece,
attributionScopesData: mojo.attributionScopesDataJson,
aggregatableNamedBudgets: mojo.aggregatableNamedBudgets,
};
}
function initSourceTable(panel: HTMLElement):
AttributionInternalsTableElement<Source> {
return initPanel(
panel,
[
valueColumn('Source Event ID', 'sourceEventId', asNumber),
valueColumn('Status', 'status', asStringOrBool),
valueColumn('Source Origin', 'sourceOrigin', asUrl),
valueColumn('Destinations', 'destinations', asList(asUrl)),
valueColumn('Reporting Origin', 'reportingOrigin', asUrl),
valueColumn(
'Registration Time', 'sourceTime', asDate, /*defaultSort=*/ true),
valueColumn('Expiry', 'expiryTime', asDate),
valueColumn('Source Type', 'sourceType', asStringOrBool),
valueColumn('Debug Key', 'debugKey', allowingUndefined(asNumber)),
],
{
getId: source => source.id,
isSelectable: true,
},
[
valueColumn('Priority', 'priority', asNumber),
valueColumn('Filter Data', 'filterData', asCode),
valueColumn(
'Cookie-Based Debug Allowed', 'cookieBasedDebugAllowed',
asStringOrBool),
valueColumn('Attribution Scopes Data', 'attributionScopesData', asCode),
valueColumn(
'Remaining Aggregatable Debug Budget',
'remainingAggregatableDebugBudget',
asCustomNumber((v) => `${v} / ${BUDGET_PER_SOURCE}`)),
valueColumn(
'Aggregatable Debug Key Piece', 'aggregatableDebugKeyPiece',
asStringOrBool),
'Event-Level Fields',
valueColumn(
'Epsilon', 'eventLevelEpsilon',
asCustomNumber((v: number) => v.toFixed(3))),
valueColumn(
'Trigger Data Matching', 'triggerDataMatching', asStringOrBool),
valueColumn('Trigger Data', 'triggerData', asList(asNumber)),
valueColumn('Report Start', 'eventReportWindowsStart', asNumber),
valueColumn(
'Report Windows', 'eventReportWindowsEnds', asList(asNumber)),
valueColumn('Max Reports', 'maxEventLevelReports', asNumber),
valueColumn('Dedup Keys', 'dedupKeys', asList(asNumber)),
'Aggregatable Fields',
valueColumn(
'Report Window Time', 'aggregatableReportWindowTime', asDate),
valueColumn(
'Remaining Aggregatable Attribution Budget',
'remainingAggregatableAttributionBudget',
asCustomNumber((v) => `${v} / ${BUDGET_PER_SOURCE}`)),
valueColumn('Named Budgets', 'aggregatableNamedBudgets', asCode),
valueColumn('Aggregation Keys', 'aggregationKeys', asCode),
valueColumn('Dedup Keys', 'aggregatableDedupKeys', asList(asNumber)),
]);
}
class Registration {
readonly time: Date;
readonly contextOrigin: string;
readonly reportingOrigin: string;
readonly registrationJson: string;
readonly clearedDebugKey?: bigint;
constructor(mojo: WebUIRegistration) {
this.time = new Date(mojo.time);
this.contextOrigin = originToText(mojo.contextOrigin);
this.reportingOrigin = originToText(mojo.reportingOrigin);
this.registrationJson = mojo.registrationJson;
this.clearedDebugKey = mojo.clearedDebugKey ?? undefined;
}
}
function initRegistrationTableModel<T extends Registration>(
panel: HTMLElement, contextOriginTitle: string,
cols: Iterable<DataColumn<T>>): AttributionInternalsTableElement<T> {
return initPanel(
panel,
[
valueColumn('Time', 'time', asDate, /*defaultSort=*/ true),
valueColumn(contextOriginTitle, 'contextOrigin', asUrl),
valueColumn('Reporting Origin', 'reportingOrigin', asUrl),
valueColumn(
'Cleared Debug Key', 'clearedDebugKey',
allowingUndefined(asNumber)),
...cols,
],
{isSelectable: true},
[valueColumn('Registration JSON', 'registrationJson', asCode)]);
}
class Trigger extends Registration {
readonly eventLevelResult: string;
readonly aggregatableResult: string;
constructor(mojo: WebUITrigger) {
super(mojo.registration);
this.eventLevelResult = eventLevelResultText[mojo.eventLevelResult];
this.aggregatableResult = aggregatableResultText[mojo.aggregatableResult];
}
}
function initTriggerTable(panel: HTMLElement):
AttributionInternalsTableElement<Trigger> {
return initRegistrationTableModel(panel, 'Destination', [
valueColumn('Event-Level Result', 'eventLevelResult', asStringOrBool),
valueColumn('Aggregatable Result', 'aggregatableResult', asStringOrBool),
]);
}
class SourceRegistration extends Registration {
readonly type: string;
readonly status: string;
constructor(mojo: WebUISourceRegistration) {
super(mojo.registration);
this.type = sourceTypeText[mojo.type];
this.status = sourceRegistrationStatusText[mojo.status];
}
}
function initSourceRegistrationTable(panel: HTMLElement):
AttributionInternalsTableElement<SourceRegistration> {
return initRegistrationTableModel(panel, 'Source Origin', [
valueColumn('Type', 'type', asStringOrBool),
valueColumn('Status', 'status', asStringOrBool),
]);
}
function isHttpError(code: number): boolean {
return code < 200 || code >= 400;
}
const reportStatusColumn: DataColumn<{status: string, sendFailed: boolean}> = {
label: 'Status',
compare: (a, b) => compareDefault(a.status, b.status),
render: (td, report) => {
td.classList.toggle('send-error', report.sendFailed);
td.innerText = report.status;
},
};
function networkStatusToString(status: NetworkStatus, sentPrefix: string):
[status: string, sendFailed: boolean] {
if (status.httpResponseCode !== undefined) {
return [
`${sentPrefix}HTTP ${status.httpResponseCode}`,
isHttpError(status.httpResponseCode),
];
} else if (status.networkError !== undefined) {
return [`Network error: ${status.networkError}`, true];
} else {
throw new Error('invalid NetworkStatus union');
}
}
class Report {
id: ReportID;
reportBody: string;
reportUrl: string;
triggerTime: Date;
reportTime: Date;
status: string;
sendFailed: boolean;
constructor(mojo: WebUIReport) {
this.id = mojo.id;
this.reportBody = mojo.reportBody;
this.reportUrl = mojo.reportUrl.url;
this.triggerTime = new Date(mojo.triggerTime);
this.reportTime = new Date(mojo.reportTime);
[this.status, this.sendFailed] =
Report.statusToString(mojo.status, 'Sent: ');
}
isPending(): boolean {
return this.status === 'Pending';
}
static statusToString(status: ReportStatus, sentPrefix: string):
[status: string, sendFailed: boolean] {
if (status.networkStatus !== undefined) {
return networkStatusToString(status.networkStatus, sentPrefix);
} else if (status.pending !== undefined) {
return ['Pending', false];
} else if (status.replacedByHigherPriorityReport !== undefined) {
return [
`Replaced by higher-priority report: ${
status.replacedByHigherPriorityReport}`,
false,
];
} else if (status.expired !== undefined) {
return ['Expired', false];
} else if (status.prohibitedByBrowserPolicy !== undefined) {
return ['Prohibited by browser policy', false];
} else if (status.failedToAssemble !== undefined) {
return ['Dropped due to assembly failure', false];
} else {
throw new Error('invalid ReportStatus union');
}
}
}
class EventLevelReport extends Report {
reportPriority: bigint;
randomizedReport: boolean;
constructor(mojo: WebUIReport) {
super(mojo);
this.reportPriority = mojo.data.eventLevelData!.priority;
this.randomizedReport = !mojo.data.eventLevelData!.attributedTruthfully;
}
}
class AggregatableReport extends Report {
contributions: string;
aggregationCoordinator: string;
isNullReport: boolean;
constructor(mojo: WebUIReport) {
super(mojo);
this.contributions = JSON.stringify(
mojo.data.aggregatableAttributionData!.contributions, bigintReplacer,
' ');
this.aggregationCoordinator =
mojo.data.aggregatableAttributionData!.aggregationCoordinator;
this.isNullReport = mojo.data.aggregatableAttributionData!.isNullReport;
}
}
function initPanel<T>(
panel: HTMLElement, cols: Iterable<DataColumn<T>>, initOpts: InitOpts<T>,
detailCols: Iterable<string|DataColumn<T>>,
onSelectionChange: (data: T|undefined) => void =
() => {}): AttributionInternalsTableElement<T> {
const t = panel.querySelector<AttributionInternalsTableElement<T>>(
'attribution-internals-table')!;
t.init(cols, initOpts);
const d = panel.querySelector<AttributionDetailTableElement<T>>(
'attribution-detail-table')!;
d.init([...cols, ...detailCols]);
t.addEventListener(
'selection-change', (e: CustomEvent<{data: T | undefined}>) => {
onSelectionChange(e.detail.data);
d.update(e.detail.data);
});
d.addEventListener('close', () => t.clearSelection());
return t;
}
function initReportTable<T extends Report>(
panel: HTMLElement, handler: HandlerInterface,
cols: Iterable<DataColumn<T>>): AttributionInternalsTableElement<T> {
const sendReportButton = panel.querySelector('button')!;
const t = initPanel<T>(
panel,
[
reportStatusColumn,
valueColumn('URL', 'reportUrl', asUrl),
valueColumn('Trigger Time', 'triggerTime', asDate),
valueColumn('Report Time', 'reportTime', asDate, /*defaultSort=*/ true),
...cols,
],
{
// Prevent sent/dropped reports from being removed by returning
// undefined.
getId: (report, updated) =>
(report.isPending() || updated) ? report.id.value : undefined,
isSelectable: true,
},
[valueColumn('Body', 'reportBody', asCode)],
(report: T|undefined) => sendReportButton.disabled =
!(report?.isPending()));
sendReportButton.addEventListener(
'click', () => sendReport(t, sendReportButton, handler));
return t;
}
/**
* Sends the selected report.
* Disables the button while the report is still being sent.
* Observer.onReportsChanged and Observer.onSourcesChanged will be called
* automatically as the report is deleted, so there's no need to manually
* refresh the data on completion.
*/
function sendReport<T extends Report>(
t: AttributionInternalsTableElement<T>, sendReportButton: HTMLButtonElement,
handler: HandlerInterface): void {
const id = t.selectedData()?.id;
if (id === undefined) {
return;
}
const previousText = sendReportButton.innerText;
sendReportButton.disabled = true;
sendReportButton.innerText = 'Sending...';
handler.sendReport(id).then(() => {
sendReportButton.innerText = previousText;
});
}
const registrationTypeText: Readonly<Record<RegistrationType, string>> = {
[RegistrationType.kSource]: 'Source',
[RegistrationType.kTrigger]: 'Trigger',
};
const osRegistrationResultText:
Readonly<Record<OsRegistrationResult, string>> = {
[OsRegistrationResult.kPassedToOs]: 'Passed to OS',
[OsRegistrationResult.kInvalidRegistrationUrl]:
'Invalid registration URL',
[OsRegistrationResult.kProhibitedByBrowserPolicy]:
'Prohibited by browser policy',
[OsRegistrationResult.kRejectedByOs]: 'Rejected by OS',
};
interface OsRegistration {
time: Date;
registrationUrl: string;
topLevelOrigin: string;
registrationType: string;
debugKeyAllowed: boolean;
debugReporting: boolean;
result: string;
}
function newOsRegistration(mojo: WebUIOsRegistration): OsRegistration {
return {
time: new Date(mojo.time),
registrationUrl: mojo.registrationUrl.url,
topLevelOrigin: originToText(mojo.topLevelOrigin),
debugKeyAllowed: mojo.isDebugKeyAllowed,
debugReporting: mojo.debugReporting,
registrationType: `OS ${registrationTypeText[mojo.type]}`,
result: osRegistrationResultText[mojo.result],
};
}
function initOsRegistrationTable(
t: AttributionInternalsTableElement<OsRegistration>):
AttributionInternalsTableElement<OsRegistration> {
t.init([
valueColumn('Time', 'time', asDate, /*defaultSort=*/ true),
valueColumn('Type', 'registrationType', asStringOrBool),
valueColumn('URL', 'registrationUrl', asUrl),
valueColumn('Top-Level Origin', 'topLevelOrigin', asUrl),
valueColumn('Debug Key Allowed', 'debugKeyAllowed', asStringOrBool),
valueColumn('Debug Reporting', 'debugReporting', asStringOrBool),
valueColumn('Result', 'result', asStringOrBool),
]);
return t;
}
interface DebugReport {
body: string;
url: string;
time: Date;
status: string;
sendFailed: boolean;
}
function verboseDebugReport(mojo: WebUIDebugReport): DebugReport {
const report: DebugReport = {
body: mojo.body,
url: mojo.url.url,
time: new Date(mojo.time),
status: '',
sendFailed: false,
};
[report.status, report.sendFailed] =
networkStatusToString(mojo.status, /*sentPrefix=*/ '');
return report;
}
function attributionSuccessDebugReport(mojo: WebUIReport): DebugReport {
const [status, sendFailed] =
Report.statusToString(mojo.status, /*sentPrefix=*/ '');
return {
body: mojo.reportBody,
url: mojo.reportUrl.url,
time: new Date(mojo.reportTime),
status,
sendFailed,
};
}
const processAggregatableDebugReportResultText:
Readonly<Record<ProcessAggregatableDebugReportResult, string>> = {
[ProcessAggregatableDebugReportResult.kSuccess]: 'Success',
[ProcessAggregatableDebugReportResult.kNoDebugData]: 'No debug data',
[ProcessAggregatableDebugReportResult.kInsufficientBudget]:
'Insufficient budget',
[ProcessAggregatableDebugReportResult.kExcessiveReports]:
'Excessive reports',
[ProcessAggregatableDebugReportResult.kGlobalRateLimitReached]:
'Global rate-limit reached',
[ProcessAggregatableDebugReportResult.kReportingSiteRateLimitReached]:
'Per reporting site rate-limit reached',
[ProcessAggregatableDebugReportResult.kBothRateLimitsReached]:
'Both rate-limits reached',
[ProcessAggregatableDebugReportResult.kInternalError]: 'Internal error',
};
function aggregatableDebugReport(mojo: WebUIAggregatableDebugReport):
DebugReport {
const report: DebugReport = {
body: mojo.body,
url: mojo.url.url,
time: new Date(mojo.time),
status: '',
sendFailed: false,
};
const processStatus =
processAggregatableDebugReportResultText[mojo.processResult];
let sendStatus;
if (mojo.sendResult.networkStatus !== undefined) {
[sendStatus, report.sendFailed] = networkStatusToString(
mojo.sendResult.networkStatus, /*sentPrefix=*/ '');
} else if (mojo.sendResult.assemblyFailed !== undefined) {
sendStatus = 'Assembly failure';
} else {
throw new Error('invalid AggregatableDebugReportStatus union');
}
report.status = `${processStatus}, ${sendStatus}`;
return report;
}
function initDebugReportTable(panel: HTMLElement):
AttributionInternalsTableElement<DebugReport> {
return initPanel(
panel,
[
valueColumn('Time', 'time', asDate, /*defaultSort=*/ true),
valueColumn('URL', 'url', asUrl),
reportStatusColumn,
],
{isSelectable: true}, [
valueColumn('Body', 'body', asCode),
]);
}
// Converts a mojo origin into a user-readable string, omitting default ports.
function originToText(origin: Origin): string {
if (origin.host.length === 0) {
return 'Null';
}
let result = origin.scheme + '://' + origin.host;
if ((origin.scheme === 'https' && origin.port !== 443) ||
(origin.scheme === 'http' && origin.port !== 80)) {
result += ':' + origin.port;
}
return result;
}
const sourceTypeText: Readonly<Record<SourceType, string>> = {
[SourceType.kNavigation]: 'Navigation',
[SourceType.kEvent]: 'Event',
};
const triggerDataMatchingText: Readonly<Record<TriggerDataMatching, string>> = {
[TriggerDataMatching.kModulus]: 'modulus',
[TriggerDataMatching.kExact]: 'exact',
};
const attributabilityText:
Readonly<Record<WebUISource_Attributability, string>> = {
[WebUISource_Attributability.kAttributable]: 'Attributable',
[WebUISource_Attributability.kNoisedNever]:
'Unattributable: noised with no reports',
[WebUISource_Attributability.kNoisedFalsely]:
'Unattributable: noised with fake reports',
[WebUISource_Attributability.kReachedEventLevelAttributionLimit]:
'Attributable: reached event-level attribution limit',
};
const sourceRegistrationStatusText:
Readonly<Record<StoreSourceResult, string>> = {
[StoreSourceResult.kSuccess]: 'Success',
[StoreSourceResult.kSuccessNoised]: 'Success',
[StoreSourceResult.kInternalError]: 'Rejected: internal error',
[StoreSourceResult.kInsufficientSourceCapacity]:
'Rejected: insufficient source capacity',
[StoreSourceResult.kInsufficientUniqueDestinationCapacity]:
'Rejected: insufficient unique destination capacity',
[StoreSourceResult.kExcessiveReportingOrigins]:
'Rejected: excessive reporting origins',
[StoreSourceResult.kProhibitedByBrowserPolicy]:
'Rejected: prohibited by browser policy',
[StoreSourceResult.kDestinationReportingLimitReached]:
'Rejected: destination reporting limit reached',
[StoreSourceResult.kDestinationGlobalLimitReached]:
'Rejected: destination global limit reached',
[StoreSourceResult.kDestinationBothLimitsReached]:
'Rejected: destination both limits reached',
[StoreSourceResult.kExceedsMaxChannelCapacity]:
'Rejected: channel capacity exceeds max allowed',
[StoreSourceResult.kReportingOriginsPerSiteLimitReached]:
'Rejected: reached reporting origins per site limit',
[StoreSourceResult.kExceedsMaxTriggerStateCardinality]:
'Rejected: trigger state cardinality exceeds limit',
[StoreSourceResult.kDestinationPerDayReportingLimitReached]:
'Rejected: destination per day reporting limit reached',
[StoreSourceResult.kExceedsMaxScopesChannelCapacity]:
'Rejected: scopes channel capacity exceeds max allowed',
[StoreSourceResult.kExceedsMaxEventStatesLimit]:
'Rejected: event states exceeds limit',
};
const commonResult = {
success: 'Success: Report stored',
internalError: 'Failure: Internal error',
noMatchingImpressions: 'Failure: No matching sources',
noMatchingSourceFilterData: 'Failure: No matching source filter data',
deduplicated: 'Failure: Deduplicated against an earlier report',
noCapacityForConversionDestination:
'Failure: No report capacity for destination site',
excessiveAttributions: 'Failure: Excessive attributions',
excessiveReportingOrigins: 'Failure: Excessive reporting origins',
reportWindowPassed: 'Failure: Report window has passed',
excessiveReports: 'Failure: Excessive reports',
prohibitedByBrowserPolicy: 'Failure: Prohibited by browser policy',
};
const eventLevelResultText: Readonly<Record<EventLevelResult, string>> = {
[EventLevelResult.kSuccess]: commonResult.success,
[EventLevelResult.kSuccessDroppedLowerPriority]: commonResult.success,
[EventLevelResult.kInternalError]: commonResult.internalError,
[EventLevelResult.kNoMatchingImpressions]: commonResult.noMatchingImpressions,
[EventLevelResult.kNoMatchingSourceFilterData]:
commonResult.noMatchingSourceFilterData,
[EventLevelResult.kNoCapacityForConversionDestination]:
commonResult.noCapacityForConversionDestination,
[EventLevelResult.kExcessiveAttributions]: commonResult.excessiveAttributions,
[EventLevelResult.kExcessiveReportingOrigins]:
commonResult.excessiveReportingOrigins,
[EventLevelResult.kDeduplicated]: commonResult.deduplicated,
[EventLevelResult.kReportWindowNotStarted]:
'Failure: Report window has not started',
[EventLevelResult.kReportWindowPassed]: commonResult.reportWindowPassed,
[EventLevelResult.kPriorityTooLow]: 'Failure: Priority too low',
[EventLevelResult.kNeverAttributedSource]: 'Failure: Noised',
[EventLevelResult.kFalselyAttributedSource]: 'Failure: Noised',
[EventLevelResult.kNotRegistered]: 'Failure: No event-level data present',
[EventLevelResult.kProhibitedByBrowserPolicy]:
commonResult.prohibitedByBrowserPolicy,
[EventLevelResult.kNoMatchingConfigurations]:
'Failure: no matching event-level configurations',
[EventLevelResult.kExcessiveReports]: commonResult.excessiveReports,
[EventLevelResult.kNoMatchingTriggerData]:
'Failure: no matching trigger data',
};
const aggregatableResultText: Readonly<Record<AggregatableResult, string>> = {
[AggregatableResult.kSuccess]: commonResult.success,
[AggregatableResult.kInternalError]: commonResult.internalError,
[AggregatableResult.kNoMatchingImpressions]:
commonResult.noMatchingImpressions,
[AggregatableResult.kNoMatchingSourceFilterData]:
commonResult.noMatchingSourceFilterData,
[AggregatableResult.kNoCapacityForConversionDestination]:
commonResult.noCapacityForConversionDestination,
[AggregatableResult.kExcessiveAttributions]:
commonResult.excessiveAttributions,
[AggregatableResult.kExcessiveReportingOrigins]:
commonResult.excessiveReportingOrigins,
[AggregatableResult.kDeduplicated]: commonResult.deduplicated,
[AggregatableResult.kReportWindowPassed]: commonResult.reportWindowPassed,
[AggregatableResult.kNoHistograms]: 'Failure: No source histograms',
[AggregatableResult.kInsufficientBudget]: 'Failure: Insufficient budget',
[AggregatableResult.kNotRegistered]: 'Failure: No aggregatable data present',
[AggregatableResult.kProhibitedByBrowserPolicy]:
commonResult.prohibitedByBrowserPolicy,
[AggregatableResult.kExcessiveReports]: commonResult.excessiveReports,
[AggregatableResult.kInsufficientNamedBudget]:
'Failure: Insufficient budget with selected name',
};
const attributionSupportText: Readonly<Record<AttributionSupport, string>> = {
[AttributionSupport.kWeb]: 'web',
[AttributionSupport.kWebAndOs]: 'os, web',
[AttributionSupport.kOs]: 'os',
[AttributionSupport.kNone]: '',
[AttributionSupport.kUnset]: 'unset',
};
class AttributionInternals implements ObserverInterface {
private readonly sources: AttributionInternalsTableElement<Source>;
private readonly sourceRegistrations:
AttributionInternalsTableElement<SourceRegistration>;
private readonly triggers: AttributionInternalsTableElement<Trigger>;
private readonly debugReports: AttributionInternalsTableElement<DebugReport>;
private readonly osRegistrations:
AttributionInternalsTableElement<OsRegistration>;
private readonly eventLevelReports:
AttributionInternalsTableElement<EventLevelReport>;
private readonly aggregatableReports:
AttributionInternalsTableElement<AggregatableReport>;
private readonly handler = new HandlerRemote();
constructor() {
this.eventLevelReports = initReportTable<EventLevelReport>(
document.querySelector('#event-level-report-panel')!, this.handler, [
valueColumn('Priority', 'reportPriority', asNumber),
valueColumn('Randomized', 'randomizedReport', asStringOrBool),
]);
this.aggregatableReports = initReportTable<AggregatableReport>(
document.querySelector('#aggregatable-report-panel')!, this.handler, [
valueColumn('Histograms', 'contributions', asCode),
valueColumn(
'Aggregation Coordinator', 'aggregationCoordinator', asUrl),
valueColumn('Null', 'isNullReport', asStringOrBool),
]);
this.sources =
initSourceTable(document.querySelector('#active-source-panel')!);
this.sourceRegistrations = initSourceRegistrationTable(
document.querySelector('#source-registration-panel')!);
this.triggers = initTriggerTable(
document.querySelector('#trigger-registration-panel')!);
this.debugReports =
initDebugReportTable(document.querySelector('#debug-report-panel')!);
this.osRegistrations = initOsRegistrationTable(
document.querySelector('#osRegistrationTable')!);
const tabs = document.querySelectorAll<HTMLElement>('div[slot="tab"]');
const panels = document.querySelectorAll<HTMLElement>('div[slot="panel"]');
for (let i = 0; i < panels.length && i < tabs.length; ++i) {
const tab = tabs[i]!;
panels[i]!.addEventListener(
'rows-change',
e => tab.classList.toggle(
'unread',
!tab.hasAttribute('selected') && e.detail.rowCount > 0));
}
Factory.getRemote().create(
new ObserverReceiver(this).$.bindNewPipeAndPassRemote(),
this.handler.$.bindNewPipeAndPassReceiver());
}
onReportHandled(mojo: WebUIReport): void {
this.addSentOrDroppedReport(mojo);
}
onDebugReportSent(mojo: WebUIDebugReport): void {
this.debugReports.addRow(verboseDebugReport(mojo));
}
onAggregatableDebugReportSent(mojo: WebUIAggregatableDebugReport): void {
this.debugReports.addRow(aggregatableDebugReport(mojo));
}
onSourceHandled(mojo: WebUISourceRegistration): void {
this.sourceRegistrations.addRow(new SourceRegistration(mojo));
}
onTriggerHandled(mojo: WebUITrigger): void {
this.triggers.addRow(new Trigger(mojo));
}
onOsRegistration(mojo: WebUIOsRegistration): void {
this.osRegistrations.addRow(newOsRegistration(mojo));
}
private addSentOrDroppedReport(mojo: WebUIReport): void {
if (isAttributionSuccessDebugReport(mojo.reportUrl.url)) {
this.debugReports.addRow(attributionSuccessDebugReport(mojo));
} else if (mojo.data.eventLevelData !== undefined) {
this.eventLevelReports.addRow(new EventLevelReport(mojo));
} else {
this.aggregatableReports.addRow(new AggregatableReport(mojo));
}
}
/**
* Deletes all data stored by the conversions backend.
* onReportsChanged and onSourcesChanged will be called
* automatically as data is deleted, so there's no need to manually refresh
* the data on completion.
*/
clearStorage(): void {
this.sourceRegistrations.clearRows();
this.triggers.clearRows();
this.eventLevelReports.clearRows(report => !report.isPending());
this.aggregatableReports.clearRows(report => !report.isPending());
this.debugReports.clearRows();
this.osRegistrations.clearRows();
this.handler.clearStorage();
}
onDebugModeChanged(debugMode: boolean): void {
const reportDelaysContent =
document.querySelector<HTMLElement>('#report-delays')!;
const noiseContent = document.querySelector<HTMLElement>('#noise')!;
if (debugMode) {
reportDelaysContent.innerText = 'disabled';
noiseContent.innerText = 'disabled';
} else {
reportDelaysContent.innerText = 'enabled';
noiseContent.innerText = 'enabled';
}
}
refresh(): void {
this.handler.isAttributionReportingEnabled().then((response) => {
const featureStatus =
document.querySelector<HTMLElement>('#feature-status')!;
featureStatus.innerText = response.enabled ? 'enabled' : 'disabled';
const attributionSupport = document.querySelector<HTMLElement>('#attribution-support')!;
attributionSupport.innerText =
attributionSupportText[response.attributionSupport];
});
}
onSourcesChanged(sources: WebUISource[]): void {
this.sources.updateRows(function*() {
for (const source of sources) {
yield newSource(source);
}
}());
}
onReportsChanged(reports: WebUIReport[]): void {
this.eventLevelReports.updateRows(function*() {
for (const report of reports) {
if (report.data.eventLevelData !== undefined) {
yield new EventLevelReport(report);
}
}
}());
this.aggregatableReports.updateRows(function*() {
for (const report of reports) {
if (report.data.aggregatableAttributionData !== undefined) {
yield new AggregatableReport(report);
}
}
}());
}
}
document.addEventListener('DOMContentLoaded', function() {
const tabBox = document.querySelector('cr-tab-box')!;
tabBox.addEventListener('selected-index-change', e => {
const tabs = document.querySelectorAll<HTMLElement>('div[slot="tab"]');
tabs[e.detail]!.classList.remove('unread');
});
const internals = new AttributionInternals();
document.querySelector('#refresh')!.addEventListener(
'click', () => internals.refresh());
document.querySelector('#clear-data')!.addEventListener(
'click', () => internals.clearStorage());
tabBox.hidden = false;
internals.refresh();
});