Skip to content

Commit 2a50e02

Browse files
Josh KarlinDevtools-frontend LUCI CQ
authored andcommitted
Add SelectivePermissionInterventionIssue support to frontend
Bug: 435214052 Change-Id: I021882fe859922ce1f9b68472fa82020b4d1fecb Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7616323 Reviewed-by: Philip Pfaffe <pfaffe@chromium.org> Reviewed-by: Simon Zünd <szuend@chromium.org> Commit-Queue: Josh Karlin <jkarlin@chromium.org>
1 parent 9a8d33b commit 2a50e02

19 files changed

Lines changed: 581 additions & 0 deletions

config/gni/devtools_grd_files.gni

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,7 @@ grd_files_bundled_sources = [
582582
"front_end/models/issues_manager/descriptions/selectElementAccessibilityInteractiveContentLegendChild.md",
583583
"front_end/models/issues_manager/descriptions/selectElementAccessibilityInteractiveContentOptionChild.md",
584584
"front_end/models/issues_manager/descriptions/selectElementAccessibilityNonPhrasingContentOptionChild.md",
585+
"front_end/models/issues_manager/descriptions/selectivePermissionsIntervention.md",
585586
"front_end/models/issues_manager/descriptions/sharedArrayBuffer.md",
586587
"front_end/models/issues_manager/descriptions/sharedDictionaryUseErrorCrossOriginNoCorsRequest.md",
587588
"front_end/models/issues_manager/descriptions/sharedDictionaryUseErrorDictionaryLoadFailure.md",
@@ -1186,6 +1187,7 @@ grd_files_unbundled_sources = [
11861187
"front_end/models/issues_manager/QuirksModeIssue.js",
11871188
"front_end/models/issues_manager/RelatedIssue.js",
11881189
"front_end/models/issues_manager/SRIMessageSignatureIssue.js",
1190+
"front_end/models/issues_manager/SelectivePermissionsInterventionIssue.js",
11891191
"front_end/models/issues_manager/SharedArrayBufferIssue.js",
11901192
"front_end/models/issues_manager/SharedDictionaryIssue.js",
11911193
"front_end/models/issues_manager/SourceFrameIssuesManager.js",
@@ -1676,6 +1678,7 @@ grd_files_unbundled_sources = [
16761678
"front_end/panels/issues/AffectedPartitioningBlobURLView.js",
16771679
"front_end/panels/issues/AffectedPermissionElementsView.js",
16781680
"front_end/panels/issues/AffectedResourcesView.js",
1681+
"front_end/panels/issues/AffectedSelectivePermissionsInterventionView.js",
16791682
"front_end/panels/issues/AffectedSharedArrayBufferIssueDetailsView.js",
16801683
"front_end/panels/issues/AffectedSourcesView.js",
16811684
"front_end/panels/issues/AffectedTrackingSitesView.js",

front_end/models/bindings/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ devtools_entrypoint("bundle") {
6565
"../../panels/coverage/*",
6666
"../../panels/elements/*",
6767
"../../panels/emulation/*",
68+
"../../panels/issues/*",
6869
"../../panels/linear_memory_inspector/*",
6970
"../../panels/network/*",
7071
"../../panels/profiler/*",

front_end/models/issues_manager/BUILD.gn

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ devtools_module("issues_manager") {
3939
"QuirksModeIssue.ts",
4040
"RelatedIssue.ts",
4141
"SRIMessageSignatureIssue.ts",
42+
"SelectivePermissionsInterventionIssue.ts",
4243
"SharedArrayBufferIssue.ts",
4344
"SharedDictionaryIssue.ts",
4445
"SourceFrameIssuesManager.ts",
@@ -216,6 +217,7 @@ devtools_issue_description_files = [
216217
"selectElementAccessibilityInteractiveContentLegendChild.md",
217218
"selectElementAccessibilityInteractiveContentOptionChild.md",
218219
"selectElementAccessibilityNonPhrasingContentOptionChild.md",
220+
"selectivePermissionsIntervention.md",
219221
"summaryElementAccessibilityInteractiveContentSummaryDescendant.md",
220222
"sharedArrayBuffer.md",
221223
"stylesheetLateImport.md",
@@ -321,6 +323,7 @@ ts_library("unittests") {
321323
sources = [
322324
"CheckFormsIssuesTrigger.test.ts",
323325
"IssuesManager.test.ts",
326+
"SelectivePermissionsInterventionIssue.test.ts",
324327
]
325328

326329
deps = [

front_end/models/issues_manager/Issue.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const enum IssueCategory {
5252
ATTRIBUTION_REPORTING = 'AttributionReporting',
5353
QUIRKS_MODE = 'QuirksMode',
5454
PERMISSION_ELEMENT = 'PermissionElement',
55+
SELECTIVE_PERMISSIONS_INTERVENTION = 'SelectivePermissionsIntervention',
5556
OTHER = 'Other',
5657
}
5758

front_end/models/issues_manager/IssueAggregator.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {MixedContentIssue} from './MixedContentIssue.js';
2222
import {PartitioningBlobURLIssue} from './PartitioningBlobURLIssue.js';
2323
import {PermissionElementIssue} from './PermissionElementIssue.js';
2424
import {QuirksModeIssue} from './QuirksModeIssue.js';
25+
import {SelectivePermissionsInterventionIssue} from './SelectivePermissionsInterventionIssue.js';
2526
import {SharedArrayBufferIssue} from './SharedArrayBufferIssue.js';
2627

2728
export interface IssuesProvider extends Common.EventTarget.EventTarget<IssuesManagerEventsTypes> {
@@ -67,6 +68,7 @@ export class AggregatedIssue extends Issue {
6768
#mixedContentIssues = new Set<MixedContentIssue>();
6869
#partitioningBlobURLIssues = new Set<PartitioningBlobURLIssue>();
6970
#permissionElementIssues = new Set<PermissionElementIssue>();
71+
#selectivePermissionsInterventionIssues = new Set<SelectivePermissionsInterventionIssue>();
7072
#sharedArrayBufferIssues = new Set<SharedArrayBufferIssue>();
7173
#quirksModeIssues = new Set<QuirksModeIssue>();
7274
#attributionReportingIssues = new Set<AttributionReportingIssue>();
@@ -144,6 +146,10 @@ export class AggregatedIssue extends Issue {
144146
return this.#affectedRequests.values();
145147
}
146148

149+
getSelectivePermissionsInterventionIssues(): Iterable<SelectivePermissionsInterventionIssue> {
150+
return this.#selectivePermissionsInterventionIssues;
151+
}
152+
147153
getSharedArrayBufferIssues(): Iterable<SharedArrayBufferIssue> {
148154
return this.#sharedArrayBufferIssues;
149155
}
@@ -282,6 +288,9 @@ export class AggregatedIssue extends Issue {
282288
if (issue instanceof PermissionElementIssue) {
283289
this.#permissionElementIssues.add(issue);
284290
}
291+
if (issue instanceof SelectivePermissionsInterventionIssue) {
292+
this.#selectivePermissionsInterventionIssues.add(issue);
293+
}
285294
}
286295

287296
getKind(): IssueKind {

front_end/models/issues_manager/IssuesManager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {PartitioningBlobURLIssue} from './PartitioningBlobURLIssue.js';
2727
import {PermissionElementIssue} from './PermissionElementIssue.js';
2828
import {PropertyRuleIssue} from './PropertyRuleIssue.js';
2929
import {QuirksModeIssue} from './QuirksModeIssue.js';
30+
import {SelectivePermissionsInterventionIssue} from './SelectivePermissionsInterventionIssue.js';
3031
import {SharedArrayBufferIssue} from './SharedArrayBufferIssue.js';
3132
import {SharedDictionaryIssue} from './SharedDictionaryIssue.js';
3233
import {SourceFrameIssuesManager} from './SourceFrameIssuesManager.js';
@@ -148,6 +149,10 @@ const issueCodeHandlers = new Map<
148149
Protocol.Audits.InspectorIssueCode.PermissionElementIssue,
149150
PermissionElementIssue.fromInspectorIssue,
150151
],
152+
[
153+
Protocol.Audits.InspectorIssueCode.SelectivePermissionsInterventionIssue,
154+
SelectivePermissionsInterventionIssue.fromInspectorIssue,
155+
],
151156
]);
152157

153158
/**
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright 2026 The Chromium Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import type * as SDK from '../../core/sdk/sdk.js';
6+
import * as Protocol from '../../generated/protocol.js';
7+
import {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
8+
import {setupLocaleHooks} from '../../testing/LocaleHelpers.js';
9+
import {MockIssuesModel} from '../../testing/MockIssuesModel.js';
10+
11+
import * as IssuesManager from './issues_manager.js';
12+
13+
describeWithEnvironment('SelectivePermissionsInterventionIssue', () => {
14+
setupLocaleHooks();
15+
16+
const mockModel = new MockIssuesModel([]) as unknown as SDK.IssuesModel.IssuesModel;
17+
const mockTarget = {
18+
id: () => 'fake-id',
19+
isDisposed: () => false,
20+
model: () => null,
21+
};
22+
mockModel.target = () => mockTarget as unknown as SDK.Target.Target;
23+
24+
function createProtocolIssueWithDetails(
25+
selectivePermissionsInterventionIssueDetails: Protocol.Audits.SelectivePermissionsInterventionIssueDetails):
26+
Protocol.Audits.InspectorIssue {
27+
return {
28+
code: Protocol.Audits.InspectorIssueCode.SelectivePermissionsInterventionIssue,
29+
details: {selectivePermissionsInterventionIssueDetails},
30+
};
31+
}
32+
33+
const issueDetails = {
34+
apiName: 'geolocation',
35+
stackTrace: {
36+
callFrames: [
37+
{
38+
functionName: 'foo',
39+
scriptId: '1' as Protocol.Runtime.ScriptId,
40+
url: 'https://example.com/foo.js',
41+
lineNumber: 10,
42+
columnNumber: 5,
43+
},
44+
],
45+
},
46+
adAncestry: {
47+
adAncestryChain: [
48+
{
49+
scriptId: '2' as Protocol.Runtime.ScriptId,
50+
debuggerId: '123' as Protocol.Runtime.UniqueDebuggerId,
51+
name: 'https://ads.com/ad.js',
52+
},
53+
],
54+
rootScriptFilterlistRule: '||ads.com^',
55+
},
56+
};
57+
58+
it('correctly creates an issue with valid details', () => {
59+
const issue = createProtocolIssueWithDetails(issueDetails);
60+
61+
const interventionIssues =
62+
IssuesManager.SelectivePermissionsInterventionIssue.SelectivePermissionsInterventionIssue.fromInspectorIssue(
63+
mockModel, issue);
64+
assert.lengthOf(interventionIssues, 1);
65+
const interventionIssue = interventionIssues[0];
66+
67+
assert.strictEqual(
68+
interventionIssue.getCategory(), IssuesManager.Issue.IssueCategory.SELECTIVE_PERMISSIONS_INTERVENTION);
69+
assert.strictEqual(interventionIssue.getKind(), IssuesManager.Issue.IssueKind.PAGE_ERROR);
70+
const description = interventionIssue.getDescription();
71+
assert.strictEqual(description?.file, 'selectivePermissionsIntervention.md');
72+
assert.lengthOf(description?.links || [], 1);
73+
assert.strictEqual(description?.links[0].link, 'https://crbug.com/435223477');
74+
assert.deepEqual(Array.from([interventionIssue.details()]), [issueDetails]);
75+
});
76+
77+
it('generates a stable primary key', () => {
78+
const issue = createProtocolIssueWithDetails(issueDetails);
79+
const interventionIssue1 =
80+
IssuesManager.SelectivePermissionsInterventionIssue.SelectivePermissionsInterventionIssue.fromInspectorIssue(
81+
mockModel, issue)[0];
82+
const interventionIssue2 =
83+
IssuesManager.SelectivePermissionsInterventionIssue.SelectivePermissionsInterventionIssue.fromInspectorIssue(
84+
mockModel, issue)[0];
85+
86+
assert.strictEqual(interventionIssue1.primaryKey(), interventionIssue2.primaryKey());
87+
});
88+
89+
it('generates different primary keys for different details', () => {
90+
const issue1 = createProtocolIssueWithDetails(issueDetails);
91+
const issue2 = createProtocolIssueWithDetails({...issueDetails, apiName: 'microphone'});
92+
const interventionIssue1 =
93+
IssuesManager.SelectivePermissionsInterventionIssue.SelectivePermissionsInterventionIssue.fromInspectorIssue(
94+
mockModel, issue1)[0];
95+
const interventionIssue2 =
96+
IssuesManager.SelectivePermissionsInterventionIssue.SelectivePermissionsInterventionIssue.fromInspectorIssue(
97+
mockModel, issue2)[0];
98+
99+
assert.notStrictEqual(interventionIssue1.primaryKey(), interventionIssue2.primaryKey());
100+
});
101+
102+
it('handles missing optional fields', () => {
103+
const issueDetailsMinimal = {
104+
apiName: 'geolocation',
105+
adAncestry: {
106+
adAncestryChain: [],
107+
},
108+
} as Protocol.Audits.SelectivePermissionsInterventionIssueDetails;
109+
const issue = createProtocolIssueWithDetails(issueDetailsMinimal);
110+
111+
const interventionIssues =
112+
IssuesManager.SelectivePermissionsInterventionIssue.SelectivePermissionsInterventionIssue.fromInspectorIssue(
113+
mockModel, issue);
114+
assert.lengthOf(interventionIssues, 1);
115+
const interventionIssue = interventionIssues[0];
116+
117+
assert.deepEqual(Array.from([interventionIssue.details()]), [issueDetailsMinimal]);
118+
});
119+
120+
it('returns an empty array when details are missing', () => {
121+
const issue = {
122+
code: Protocol.Audits.InspectorIssueCode.SelectivePermissionsInterventionIssue,
123+
details: {},
124+
};
125+
const interventionIssues =
126+
IssuesManager.SelectivePermissionsInterventionIssue.SelectivePermissionsInterventionIssue.fromInspectorIssue(
127+
mockModel, issue);
128+
assert.lengthOf(interventionIssues, 0);
129+
});
130+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2026 The Chromium Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as i18n from '../../core/i18n/i18n.js';
6+
import type * as SDK from '../../core/sdk/sdk.js';
7+
import * as Protocol from '../../generated/protocol.js';
8+
9+
import {Issue, IssueCategory, IssueKind} from './Issue.js';
10+
import type {MarkdownIssueDescription} from './MarkdownIssueDescription.js';
11+
12+
const UIStrings = {
13+
/**
14+
* @description Title for a learn more link in Selective Permissions Intervention issue description
15+
*/
16+
selectivePermissionsIntervention: 'Selective Permissions Intervention',
17+
} as const;
18+
const str_ = i18n.i18n.registerUIStrings('models/issues_manager/SelectivePermissionsInterventionIssue.ts', UIStrings);
19+
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
20+
21+
export class SelectivePermissionsInterventionIssue extends
22+
Issue<Protocol.Audits.SelectivePermissionsInterventionIssueDetails> {
23+
constructor(
24+
issueDetails: Protocol.Audits.SelectivePermissionsInterventionIssueDetails,
25+
issuesModel: SDK.IssuesModel.IssuesModel|null) {
26+
super(Protocol.Audits.InspectorIssueCode.SelectivePermissionsInterventionIssue, issueDetails, issuesModel);
27+
}
28+
29+
primaryKey(): string {
30+
return `${Protocol.Audits.InspectorIssueCode.SelectivePermissionsInterventionIssue}-${
31+
JSON.stringify(this.details())}`;
32+
}
33+
34+
getDescription(): MarkdownIssueDescription {
35+
return {
36+
file: 'selectivePermissionsIntervention.md',
37+
links: [
38+
{
39+
link: 'https://crbug.com/435223477',
40+
linkTitle: i18nString(UIStrings.selectivePermissionsIntervention),
41+
},
42+
],
43+
};
44+
}
45+
46+
getCategory(): IssueCategory {
47+
return IssueCategory.SELECTIVE_PERMISSIONS_INTERVENTION;
48+
}
49+
50+
getKind(): IssueKind {
51+
return IssueKind.PAGE_ERROR;
52+
}
53+
54+
static fromInspectorIssue(
55+
issuesModel: SDK.IssuesModel.IssuesModel|null,
56+
inspectorIssue: Protocol.Audits.InspectorIssue): SelectivePermissionsInterventionIssue[] {
57+
const selectivePermissionsInterventionIssueDetails =
58+
inspectorIssue.details.selectivePermissionsInterventionIssueDetails;
59+
if (!selectivePermissionsInterventionIssueDetails) {
60+
console.warn('Selective Permissions Intervention issue without details received.');
61+
return [];
62+
}
63+
return [new SelectivePermissionsInterventionIssue(selectivePermissionsInterventionIssueDetails, issuesModel)];
64+
}
65+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Selective Permissions Intervention
2+
3+
The Selective Permissions Intervention blocks calls to privacy-sensitive APIs when they are called from ad scripts in order to align the permission grant with the user's intent. The particular API that was blocked, the call-stack that triggered the intervention, and why the script that called the API is considered ad-related is shown below.
4+
5+
Note that Chrome considers any script with a URL that matches a rule in the [filterlist](ChromeFilterlistRepository) as ad script, and the matching rule is shown in the Ad Ancestry section. In addition, any script loaded while an ad script is in the JavaScript stack will also be considered an ad script by Chrome and is also shown in Ad Ancestry.
6+
7+
If you believe this intervention was in error (e.g., this call would occur even when loading the page with an ad blocker enabled), then please [file a bug](SelectivePermissionsInterventionIssue).

front_end/models/issues_manager/issues_manager.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import * as PermissionElementIssue from './PermissionElementIssue.js';
2727
import * as PropertyRuleIssue from './PropertyRuleIssue.js';
2828
import * as QuirksModeIssue from './QuirksModeIssue.js';
2929
import * as RelatedIssue from './RelatedIssue.js';
30+
import * as SelectivePermissionsInterventionIssue from './SelectivePermissionsInterventionIssue.js';
3031
import * as SharedArrayBufferIssue from './SharedArrayBufferIssue.js';
3132
import * as SharedDictionaryIssue from './SharedDictionaryIssue.js';
3233
import * as SourceFrameIssuesManager from './SourceFrameIssuesManager.js';
@@ -60,6 +61,7 @@ export {
6061
PropertyRuleIssue,
6162
QuirksModeIssue,
6263
RelatedIssue,
64+
SelectivePermissionsInterventionIssue,
6365
SharedArrayBufferIssue,
6466
SharedDictionaryIssue,
6567
SourceFrameIssuesManager,

0 commit comments

Comments
 (0)