chrome-devtools-frontend 1.0.1512147 → 1.0.1512349

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/front_end/Images/src/ai-explorer-badge.svg +114 -0
  2. package/front_end/Images/src/code-whisperer-badge.svg +166 -0
  3. package/front_end/Images/src/devtools-user-badge.svg +129 -0
  4. package/front_end/Images/src/dom-detective-badge.svg +136 -0
  5. package/front_end/Images/src/speedster-badge.svg +166 -0
  6. package/front_end/core/host/GdpClient.ts +38 -2
  7. package/front_end/core/i18n/NumberFormatter.ts +7 -0
  8. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +3 -19
  9. package/front_end/models/ai_assistance/ai_assistance.ts +1 -1
  10. package/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.ts +7 -6
  11. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.snapshot.txt +119 -119
  12. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.ts +43 -52
  13. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +100 -100
  14. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.ts +11 -17
  15. package/front_end/models/ai_assistance/data_formatters/UnitFormatters.ts +151 -0
  16. package/front_end/models/badges/Badge.ts +7 -4
  17. package/front_end/models/badges/DOMDetectiveBadge.ts +20 -0
  18. package/front_end/models/badges/SpeedsterBadge.ts +4 -1
  19. package/front_end/models/badges/StarterBadge.ts +5 -1
  20. package/front_end/models/badges/UserBadges.ts +33 -7
  21. package/front_end/models/trace/ModelImpl.ts +0 -13
  22. package/front_end/panels/common/BadgeNotification.ts +119 -9
  23. package/front_end/panels/common/badgeNotification.css +4 -0
  24. package/front_end/panels/elements/ElementsTreeElement.ts +12 -0
  25. package/front_end/panels/elements/ElementsTreeOutline.ts +3 -0
  26. package/front_end/panels/elements/StylePropertiesSection.ts +3 -0
  27. package/front_end/panels/elements/StylePropertyTreeElement.ts +5 -0
  28. package/front_end/panels/settings/SettingsScreen.ts +3 -9
  29. package/front_end/panels/settings/components/SyncSection.ts +6 -2
  30. package/front_end/panels/timeline/TimelinePanel.ts +21 -9
  31. package/front_end/panels/timeline/TimelineUIUtils.ts +4 -3
  32. package/front_end/ui/legacy/filter.css +1 -1
  33. package/front_end/ui/legacy/inspectorCommon.css +1 -1
  34. package/front_end/ui/legacy/softDropDownButton.css +1 -1
  35. package/package.json +1 -1
  36. package/front_end/models/ai_assistance/data_formatters/Types.ts +0 -9
@@ -0,0 +1,151 @@
1
+ // Copyright 2025 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
+ /**
6
+ * This module contains unit formatters that are only to be used within
7
+ * the AI models because they do not account for locales other than en-US.
8
+ */
9
+ const defaultTimeFormatterOptions: Intl.NumberFormatOptions = {
10
+ style: 'unit',
11
+ unitDisplay: 'narrow',
12
+ minimumFractionDigits: 0,
13
+ maximumFractionDigits: 1,
14
+ } as const;
15
+
16
+ const defaultByteFormatterOptions: Intl.NumberFormatOptions = {
17
+ style: 'unit',
18
+ unitDisplay: 'narrow',
19
+ minimumFractionDigits: 0,
20
+ maximumFractionDigits: 1,
21
+ } as const;
22
+
23
+ const timeFormatters = {
24
+ milli: new Intl.NumberFormat('en-US', {
25
+ ...defaultTimeFormatterOptions,
26
+ unit: 'millisecond',
27
+ }),
28
+ second: new Intl.NumberFormat('en-US', {
29
+ ...defaultTimeFormatterOptions,
30
+ unit: 'second',
31
+ }),
32
+ micro: new Intl.NumberFormat('en-US', {
33
+ ...defaultTimeFormatterOptions,
34
+ unit: 'microsecond',
35
+ }),
36
+ } as const;
37
+
38
+ const byteFormatters = {
39
+ bytes: new Intl.NumberFormat('en-US', {
40
+ ...defaultByteFormatterOptions,
41
+ // Don't need as much precision on bytes.
42
+ minimumFractionDigits: 0,
43
+ maximumFractionDigits: 0,
44
+ unit: 'byte',
45
+ }),
46
+ kilobytes: new Intl.NumberFormat('en-US', {
47
+ ...defaultByteFormatterOptions,
48
+ unit: 'kilobyte',
49
+ }),
50
+ megabytes: new Intl.NumberFormat('en-US', {
51
+ ...defaultByteFormatterOptions,
52
+ unit: 'megabyte',
53
+ }),
54
+ } as const;
55
+
56
+ function numberIsTooLarge(x: number): boolean {
57
+ return !Number.isFinite(x) || x === Number.MAX_VALUE;
58
+ }
59
+
60
+ export function seconds(x: number): string {
61
+ if (numberIsTooLarge(x)) {
62
+ return '-';
63
+ }
64
+ if (x === 0) {
65
+ return formatAndEnsureSpace(timeFormatters.second, x);
66
+ }
67
+
68
+ const asMilli = x * 1_000;
69
+
70
+ if (asMilli < 1) {
71
+ return micros(x * 1_000_000);
72
+ }
73
+
74
+ if (asMilli < 1_000) {
75
+ return millis(asMilli);
76
+ }
77
+ return formatAndEnsureSpace(timeFormatters.second, x);
78
+ }
79
+
80
+ export function millis(x: number): string {
81
+ if (numberIsTooLarge(x)) {
82
+ return '-';
83
+ }
84
+ return formatAndEnsureSpace(timeFormatters.milli, x);
85
+ }
86
+
87
+ export function micros(x: number): string {
88
+ if (numberIsTooLarge(x)) {
89
+ return '-';
90
+ }
91
+
92
+ if (x < 100) {
93
+ return formatAndEnsureSpace(timeFormatters.micro, x);
94
+ }
95
+
96
+ const asMilli = x / 1_000;
97
+ return millis(asMilli);
98
+ }
99
+
100
+ export function bytes(x: number): string {
101
+ if (x < 1_000) {
102
+ return formatAndEnsureSpace(byteFormatters.bytes, x);
103
+ }
104
+ const kilobytes = x / 1_000;
105
+ if (kilobytes < 1_000) {
106
+ return formatAndEnsureSpace(byteFormatters.kilobytes, kilobytes);
107
+ }
108
+
109
+ const megabytes = kilobytes / 1_000;
110
+ return formatAndEnsureSpace(byteFormatters.megabytes, megabytes);
111
+ }
112
+
113
+ /**
114
+ * When using 'narrow' unitDisplay, many locales exclude the space between the literal and the unit.
115
+ * We don't like that, so when there is no space literal we inject the provided separator manually.
116
+ */
117
+ function formatAndEnsureSpace(formatter: Intl.NumberFormat, value: number, separator = '\xA0'): string {
118
+ const parts = formatter.formatToParts(value);
119
+
120
+ let hasSpace = false;
121
+ for (const part of parts) {
122
+ if (part.type === 'literal') {
123
+ if (part.value === ' ') {
124
+ hasSpace = true;
125
+ part.value = separator;
126
+ } else if (part.value === separator) {
127
+ hasSpace = true;
128
+ }
129
+ }
130
+ }
131
+
132
+ if (hasSpace) {
133
+ return parts.map(part => part.value).join('');
134
+ }
135
+
136
+ const unitIndex = parts.findIndex(part => part.type === 'unit');
137
+
138
+ // Unexpected for there to be no unit, but just in case, handle that.
139
+ if (unitIndex === -1) {
140
+ return parts.map(part => part.value).join('');
141
+ }
142
+
143
+ // For locales where the unit comes first (sw), the space has to come after the unit.
144
+ if (unitIndex === 0) {
145
+ return parts[0].value + separator + parts.slice(1).map(part => part.value).join('');
146
+ }
147
+
148
+ // Otherwise, it comes before.
149
+ return parts.slice(0, unitIndex).map(part => part.value).join('') + separator +
150
+ parts.slice(unitIndex).map(part => part.value).join('');
151
+ }
@@ -6,29 +6,32 @@ import * as Common from '../../core/common/common.js';
6
6
 
7
7
  export enum BadgeAction {
8
8
  CSS_RULE_MODIFIED = 'css-rule-modified',
9
+ DOM_ELEMENT_OR_ATTRIBUTE_EDITED = 'dom-element-or-attribute-edited',
10
+ MODERN_DOM_BADGE_CLICKED = 'modern-dom-badge-clicked',
9
11
  PERFORMANCE_INSIGHT_CLICKED = 'performance-insight-clicked',
10
12
  }
11
13
 
12
14
  export type BadgeActionEvents = Record<BadgeAction, void>;
13
15
 
14
16
  export interface BadgeContext {
15
- dispatchBadgeTriggeredEvent: (badge: Badge) => void;
17
+ onTriggerBadge: (badge: Badge) => void;
16
18
  badgeActionEventTarget: Common.ObjectWrapper.ObjectWrapper<BadgeActionEvents>;
17
19
  }
18
20
 
19
21
  export abstract class Badge {
20
- #dispatchBadgeTriggeredEvent: (badge: Badge) => void;
22
+ #onTriggerBadge: (badge: Badge) => void;
21
23
  #badgeActionEventTarget: Common.ObjectWrapper.ObjectWrapper<BadgeActionEvents>;
22
24
  #eventListeners: Common.EventTarget.EventDescriptor[] = [];
23
25
  #triggeredBefore = false;
24
26
 
25
27
  abstract readonly name: string;
26
28
  abstract readonly title: string;
29
+ abstract readonly imageUri: string;
27
30
  abstract readonly interestedActions: readonly BadgeAction[];
28
31
  readonly isStarterBadge: boolean = false;
29
32
 
30
33
  constructor(context: BadgeContext) {
31
- this.#dispatchBadgeTriggeredEvent = context.dispatchBadgeTriggeredEvent;
34
+ this.#onTriggerBadge = context.onTriggerBadge;
32
35
  this.#badgeActionEventTarget = context.badgeActionEventTarget;
33
36
  }
34
37
 
@@ -40,7 +43,7 @@ export abstract class Badge {
40
43
 
41
44
  this.#triggeredBefore = true;
42
45
  this.deactivate();
43
- this.#dispatchBadgeTriggeredEvent(this);
46
+ this.#onTriggerBadge(this);
44
47
  }
45
48
 
46
49
  activate(): void {
@@ -0,0 +1,20 @@
1
+ // Copyright 2025 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 {Badge, BadgeAction} from './Badge.js';
6
+
7
+ const DOM_DETECTIVE_BADGE_IMAGE_URI = new URL('../../Images/gdp-logo-standalone.svg', import.meta.url).toString();
8
+ export class DOMDetectiveBadge extends Badge {
9
+ override readonly name = 'awards/dom-detective-badge';
10
+ override readonly title = 'DOM Detective';
11
+ override readonly imageUri = DOM_DETECTIVE_BADGE_IMAGE_URI;
12
+
13
+ override readonly interestedActions = [
14
+ BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED,
15
+ ] as const;
16
+
17
+ handleAction(_action: BadgeAction): void {
18
+ this.trigger();
19
+ }
20
+ }
@@ -4,10 +4,13 @@
4
4
 
5
5
  import {Badge, BadgeAction} from './Badge.js';
6
6
 
7
+ const SPEEDSTER_BADGE_URI = new URL('../../Images/gdp-logo-standalone.svg', import.meta.url).toString();
7
8
  export class SpeedsterBadge extends Badge {
8
- override readonly name = 'awards/speedster';
9
+ // TODO(ergunsh): Update the name to be the actual badge for DevTools.
10
+ override readonly name = 'profiles/me/awards/developers.google.com%2Fprofile%2Fbadges%2Flegacy%2Ftest';
9
11
  override readonly title = 'Speedster';
10
12
  override readonly interestedActions = [BadgeAction.PERFORMANCE_INSIGHT_CLICKED] as const;
13
+ override readonly imageUri = SPEEDSTER_BADGE_URI;
11
14
 
12
15
  handleAction(_action: BadgeAction): void {
13
16
  this.trigger();
@@ -4,14 +4,18 @@
4
4
 
5
5
  import {Badge, BadgeAction} from './Badge.js';
6
6
 
7
+ const STARTER_BADGE_IMAGE_URI = new URL('../../Images/gdp-logo-standalone.svg', import.meta.url).toString();
7
8
  export class StarterBadge extends Badge {
8
9
  override readonly isStarterBadge = true;
9
- override readonly name = 'awards/chrome-devtools-user';
10
+ // TODO(ergunsh): Update the name to be the actual badge for DevTools.
11
+ override readonly name = 'profiles/me/awards/developers.google.com%2Fprofile%2Fbadges%2Fprofile%2Fcreated-profile';
10
12
  override readonly title = 'Chrome DevTools User';
13
+ override readonly imageUri = STARTER_BADGE_IMAGE_URI;
11
14
 
12
15
  // TODO(ergunsh): Add remaining non-trivial event definitions
13
16
  override readonly interestedActions = [
14
17
  BadgeAction.CSS_RULE_MODIFIED,
18
+ BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED,
15
19
  ] as const;
16
20
 
17
21
  handleAction(_action: BadgeAction): void {
@@ -6,6 +6,7 @@ import * as Common from '../../core/common/common.js';
6
6
  import * as Host from '../../core/host/host.js';
7
7
 
8
8
  import type {Badge, BadgeAction, BadgeActionEvents, BadgeContext} from './Badge.js';
9
+ import {DOMDetectiveBadge} from './DOMDetectiveBadge.js';
9
10
  import {SpeedsterBadge} from './SpeedsterBadge.js';
10
11
  import {StarterBadge} from './StarterBadge.js';
11
12
 
@@ -23,12 +24,13 @@ let userBadgesInstance: UserBadges|undefined = undefined;
23
24
  export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
24
25
  readonly #badgeActionEventTarget = new Common.ObjectWrapper.ObjectWrapper<BadgeActionEvents>();
25
26
 
26
- #receiveBadgesSetting?: Common.Settings.Setting<Boolean>;
27
+ #receiveBadgesSetting: Common.Settings.Setting<Boolean>;
27
28
  #allBadges: Badge[];
28
29
 
29
30
  static readonly BADGE_REGISTRY: BadgeClass[] = [
30
31
  StarterBadge,
31
32
  SpeedsterBadge,
33
+ DOMDetectiveBadge,
32
34
  ];
33
35
 
34
36
  private constructor() {
@@ -37,11 +39,10 @@ export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
37
39
  this.#receiveBadgesSetting = Common.Settings.Settings.instance().moduleSetting('receive-gdp-badges');
38
40
  this.#receiveBadgesSetting.addChangeListener(this.#reconcileBadges, this);
39
41
 
40
- this.#allBadges =
41
- UserBadges.BADGE_REGISTRY.map(badgeCtor => new badgeCtor({
42
- dispatchBadgeTriggeredEvent: this.#dispatchBadgeTriggeredEvent.bind(this),
43
- badgeActionEventTarget: this.#badgeActionEventTarget,
44
- }));
42
+ this.#allBadges = UserBadges.BADGE_REGISTRY.map(badgeCtor => new badgeCtor({
43
+ onTriggerBadge: this.#onTriggerBadge.bind(this),
44
+ badgeActionEventTarget: this.#badgeActionEventTarget,
45
+ }));
45
46
  }
46
47
 
47
48
  static instance({forceNew}: {forceNew: boolean} = {forceNew: false}): UserBadges {
@@ -65,7 +66,28 @@ export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
65
66
  this.#badgeActionEventTarget.dispatchEventToListeners(action);
66
67
  }
67
68
 
68
- #dispatchBadgeTriggeredEvent(badge: Badge): void {
69
+ async #onTriggerBadge(badge: Badge): Promise<void> {
70
+ let shouldAwardBadge = false;
71
+ // By default, we award non-starter badges directly when they are triggered.
72
+ if (!badge.isStarterBadge) {
73
+ shouldAwardBadge = true;
74
+ } else {
75
+ const gdpProfile = await Host.GdpClient.GdpClient.instance().getProfile();
76
+ const receiveBadgesSettingEnabled = Boolean(this.#receiveBadgesSetting.get());
77
+ // If there is a GDP profile and the user has enabled receiving badges, we award the starter badge as well.
78
+ if (gdpProfile && receiveBadgesSettingEnabled) {
79
+ shouldAwardBadge = true;
80
+ }
81
+ }
82
+
83
+ // Awarding was needed and not successful, we don't show the notification
84
+ if (shouldAwardBadge) {
85
+ const result = await Host.GdpClient.GdpClient.instance().createAward({name: badge.name});
86
+ if (!result) {
87
+ return;
88
+ }
89
+ }
90
+
69
91
  this.dispatchEventToListeners(Events.BADGE_TRIGGERED, badge);
70
92
  }
71
93
 
@@ -115,4 +137,8 @@ export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
115
137
 
116
138
  reconcileBadgesFinishedForTest(): void {
117
139
  }
140
+
141
+ isReceiveBadgesSettingEnabled(): boolean {
142
+ return Boolean(this.#receiveBadgesSetting?.get());
143
+ }
118
144
  }
@@ -118,19 +118,6 @@ export class Model extends EventTarget {
118
118
  metadata,
119
119
  resolveSourceMap: config?.resolveSourceMap,
120
120
  };
121
- if (!parseConfig.logger &&
122
- (window.location.href.includes('devtools/bundled') || window.location.search.includes('debugFrontend'))) {
123
- // Someone is debugging DevTools, enable the logger.
124
- const times: Record<string, number> = {};
125
- parseConfig.logger = {
126
- start(id) {
127
- times[id] = performance.now();
128
- },
129
- end(id) {
130
- performance.measure(id, {start: times[id]});
131
- },
132
- };
133
- }
134
121
  await this.#processor.parse(traceEvents, parseConfig);
135
122
  this.#storeParsedFileData(file, this.#processor.parsedTrace, this.#processor.insights);
136
123
  // We only push the file onto this.#traces here once we know it's valid
@@ -2,7 +2,9 @@
2
2
  // Use of this source code is governed by a BSD-style license that can be
3
3
  // found in the LICENSE file.
4
4
 
5
+ import * as Host from '../../core/host/host.js';
5
6
  import * as i18n from '../../core/i18n/i18n.js';
7
+ import * as Badges from '../../models/badges/badges.js';
6
8
  import * as Buttons from '../../ui/components/buttons/buttons.js';
7
9
  import * as UI from '../../ui/legacy/legacy.js';
8
10
  import * as Lit from '../../ui/lit/lit.js';
@@ -17,10 +19,50 @@ const UIStrings = {
17
19
  * @description Title for close button
18
20
  */
19
21
  dismiss: 'Dismiss',
22
+ /**
23
+ * @description Activity based badge award notification text
24
+ * @example {Badge Title} PH1
25
+ */
26
+ activityBasedBadgeAwardMessage: 'You earned the {PH1} badge! It has been added to your Developer Profile.',
27
+ /**
28
+ * @description Action title for navigating to the badge settings in Google Developer Profile section
29
+ */
30
+ badgeSettings: 'Badge settings',
31
+ /**
32
+ * @description Action title for opening the Google Developer Program profile page of the user in a new tab
33
+ */
34
+ viewProfile: 'View profile',
35
+ /**
36
+ * @description Starter badge award notification text when the user has a Google Developer Program profile but did not enable receiving badges in DevTools yet
37
+ * @example {Badge Title} PH1
38
+ * @example {Google Developer Program link} PH2
39
+ */
40
+ starterBadgeAwardMessageSettingDisabled: 'You earned the {PH1} badge for the {PH2}! Turn on badges to claim it.',
41
+ /**
42
+ * @description Starter badge award notification text when the user does not have a Google Developer Program profile.
43
+ * @example {Badge Title} PH1
44
+ * @example {Google Developer Program link} PH2
45
+ */
46
+ starterBadgeAwardMessageNoGdpProfile:
47
+ 'You earned the {PH1} badge for the {PH2}! Create a profile to claim your badge.',
48
+ /**
49
+ * @description Action title for snoozing the starter badge.
50
+ */
51
+ remindMeLater: 'Remind me later',
52
+ /**
53
+ * @description Action title for enabling the "Receive badges" setting
54
+ */
55
+ receiveBadges: 'Receive badges',
56
+ /**
57
+ * @description Action title for creating a Google Developer Program profle
58
+ */
59
+ createProfile: 'Create profile',
20
60
  } as const;
21
61
 
22
62
  const str_ = i18n.i18n.registerUIStrings('panels/common/BadgeNotification.ts', UIStrings);
23
63
  const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
64
+ const i18nFormatString = i18n.i18n.getFormatLocalizedString.bind(undefined, str_);
65
+ const lockedString = i18n.i18n.lockedString;
24
66
 
25
67
  export interface BadgeNotificationAction {
26
68
  label: string;
@@ -30,7 +72,7 @@ export interface BadgeNotificationAction {
30
72
  }
31
73
 
32
74
  export interface BadgeNotificationProperties {
33
- message: Lit.LitTemplate;
75
+ message: HTMLElement|string;
34
76
  imageUri: string;
35
77
  actions: BadgeNotificationAction[];
36
78
  }
@@ -80,7 +122,7 @@ const DEFAULT_VIEW = (input: ViewInput, _output: undefined, target: HTMLElement)
80
122
  type View = typeof DEFAULT_VIEW;
81
123
 
82
124
  export class BadgeNotification extends UI.Widget.Widget {
83
- message: Lit.LitTemplate = html``;
125
+ message: HTMLElement|string = '';
84
126
  imageUri = '';
85
127
  actions: BadgeNotificationAction[] = [];
86
128
 
@@ -91,13 +133,81 @@ export class BadgeNotification extends UI.Widget.Widget {
91
133
  this.#view = view;
92
134
  }
93
135
 
94
- static show(properties: BadgeNotificationProperties): BadgeNotification {
95
- const widget = new BadgeNotification();
96
- widget.message = properties.message;
97
- widget.imageUri = properties.imageUri;
98
- widget.actions = properties.actions;
99
- widget.show(UI.InspectorView.InspectorView.instance().element);
100
- return widget;
136
+ async present(badge: Badges.Badge): Promise<void> {
137
+ if (badge.isStarterBadge) {
138
+ await this.#presentStarterBadge(badge);
139
+ } else {
140
+ this.#presentActivityBasedBadge(badge);
141
+ }
142
+ }
143
+
144
+ #show(properties: BadgeNotificationProperties): void {
145
+ this.message = properties.message;
146
+ this.imageUri = properties.imageUri;
147
+ this.actions = properties.actions;
148
+ this.requestUpdate();
149
+ this.show(UI.InspectorView.InspectorView.instance().element);
150
+ }
151
+
152
+ async #presentStarterBadge(badge: Badges.Badge): Promise<void> {
153
+ const gdpProfile = await Host.GdpClient.GdpClient.instance().getProfile();
154
+ const receiveBadgesSettingEnabled = Badges.UserBadges.instance().isReceiveBadgesSettingEnabled();
155
+ const googleDeveloperProgramLink = UI.XLink.XLink.create(
156
+ 'https://developers.google.com/program', lockedString('Google Developer Program'), 'badge-link', undefined,
157
+ 'gdp.program-link');
158
+
159
+ // If the user already has a GDP profile and the receive badges setting enabled,
160
+ // starter badge behaves as if it's an activity based badge.
161
+ if (gdpProfile && receiveBadgesSettingEnabled) {
162
+ this.#presentActivityBasedBadge(badge);
163
+ return;
164
+ }
165
+
166
+ // If the user already has a GDP profile and the receive badges setting disabled,
167
+ // starter badge behaves as a nudge for opting into receiving badges.
168
+ if (gdpProfile && !receiveBadgesSettingEnabled) {
169
+ this.#show({
170
+ message: i18nFormatString(
171
+ UIStrings.starterBadgeAwardMessageSettingDisabled, {PH1: badge.title, PH2: googleDeveloperProgramLink}),
172
+ actions: [
173
+ {
174
+ label: i18nString(UIStrings.remindMeLater),
175
+ onClick: () => {/* To implement */},
176
+ },
177
+ {label: i18nString(UIStrings.receiveBadges), onClick: () => { /* To implement */ }}
178
+ ],
179
+ imageUri: badge.imageUri,
180
+ });
181
+ return;
182
+ }
183
+
184
+ // The user does not have a GDP profile, starter badge acts as a nudge for creating a GDP profile.
185
+ this.#show({
186
+ message: i18nFormatString(
187
+ UIStrings.starterBadgeAwardMessageNoGdpProfile, {PH1: badge.title, PH2: googleDeveloperProgramLink}),
188
+ actions: [
189
+ {
190
+ label: i18nString(UIStrings.remindMeLater),
191
+ onClick: () => {/* TODO(ergunsh): Implement */},
192
+ },
193
+ {label: i18nString(UIStrings.createProfile), onClick: () => { /* TODO(ergunsh): Implement */ }}
194
+ ],
195
+ imageUri: badge.imageUri,
196
+ });
197
+ }
198
+
199
+ #presentActivityBasedBadge(badge: Badges.Badge): void {
200
+ this.#show({
201
+ message: i18nString(UIStrings.activityBasedBadgeAwardMessage, {PH1: badge.title}),
202
+ actions: [
203
+ {
204
+ label: i18nString(UIStrings.badgeSettings),
205
+ onClick: () => {/* TODO(ergunsh): Implement */},
206
+ },
207
+ {label: i18nString(UIStrings.viewProfile), onClick: () => { /* TODO(ergunsh): Implement */ }}
208
+ ],
209
+ imageUri: badge.imageUri,
210
+ });
101
211
  }
102
212
 
103
213
  #close = (): void => {
@@ -49,6 +49,10 @@
49
49
  background: var(--sys-color-surface);
50
50
  }
51
51
 
52
+ .badge-link {
53
+ color: var(--sys-color-inverse-primary);
54
+ }
55
+
52
56
  .message {
53
57
  width: 100%;
54
58
  color: var(--sys-color-inverse-on-surface);
@@ -40,6 +40,7 @@ import * as Platform from '../../core/platform/platform.js';
40
40
  import * as Root from '../../core/root/root.js';
41
41
  import * as SDK from '../../core/sdk/sdk.js';
42
42
  import * as Protocol from '../../generated/protocol.js';
43
+ import * as Badges from '../../models/badges/badges.js';
43
44
  import type * as Elements from '../../models/elements/elements.js';
44
45
  import type * as IssuesManager from '../../models/issues_manager/issues_manager.js';
45
46
  import * as TextUtils from '../../models/text_utils/text_utils.js';
@@ -1603,6 +1604,7 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
1603
1604
 
1604
1605
  if (attributeName !== null && (attributeName.trim() || newText.trim()) && oldText !== newText) {
1605
1606
  this.nodeInternal.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this));
1607
+ Badges.UserBadges.instance().recordAction(Badges.BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED);
1606
1608
  return;
1607
1609
  }
1608
1610
 
@@ -1661,6 +1663,8 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
1661
1663
  if (!treeOutline) {
1662
1664
  return;
1663
1665
  }
1666
+
1667
+ Badges.UserBadges.instance().recordAction(Badges.BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED);
1664
1668
  const newTreeItem = treeOutline.selectNodeAfterEdit(wasExpanded, error, newNode);
1665
1669
  // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
1666
1670
  // @ts-expect-error
@@ -2581,6 +2585,9 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
2581
2585
  const adorner = this.adorn(config);
2582
2586
  const onClick = async(): Promise<void> => {
2583
2587
  const {nodeIds} = await node.domModel().agent.invoke_forceShowPopover({nodeId, enable: adorner.isActive()});
2588
+ if (adorner.isActive()) {
2589
+ Badges.UserBadges.instance().recordAction(Badges.BadgeAction.MODERN_DOM_BADGE_CLICKED);
2590
+ }
2584
2591
  for (const closedPopoverNodeId of nodeIds) {
2585
2592
  const node = this.node().domModel().nodeForId(closedPopoverNodeId);
2586
2593
  const treeElement = node && this.treeOutline?.treeElementByNode.get(node);
@@ -2617,6 +2624,9 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
2617
2624
  const onClick = ((() => {
2618
2625
  if (adorner.isActive()) {
2619
2626
  node.domModel().overlayModel().highlightGridInPersistentOverlay(nodeId);
2627
+ if (isSubgrid) {
2628
+ Badges.UserBadges.instance().recordAction(Badges.BadgeAction.MODERN_DOM_BADGE_CLICKED);
2629
+ }
2620
2630
  } else {
2621
2631
  node.domModel().overlayModel().hideGridInPersistentOverlay(nodeId);
2622
2632
  }
@@ -2658,6 +2668,7 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
2658
2668
  const onClick = ((() => {
2659
2669
  if (adorner.isActive()) {
2660
2670
  node.domModel().overlayModel().highlightGridInPersistentOverlay(nodeId);
2671
+ Badges.UserBadges.instance().recordAction(Badges.BadgeAction.MODERN_DOM_BADGE_CLICKED);
2661
2672
  } else {
2662
2673
  node.domModel().overlayModel().hideGridInPersistentOverlay(nodeId);
2663
2674
  }
@@ -2786,6 +2797,7 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
2786
2797
  const model = node.domModel().overlayModel();
2787
2798
  if (adorner.isActive()) {
2788
2799
  model.highlightContainerQueryInPersistentOverlay(nodeId);
2800
+ Badges.UserBadges.instance().recordAction(Badges.BadgeAction.MODERN_DOM_BADGE_CLICKED);
2789
2801
  } else {
2790
2802
  model.hideContainerQueryInPersistentOverlay(nodeId);
2791
2803
  }
@@ -38,6 +38,7 @@ import * as Common from '../../core/common/common.js';
38
38
  import * as i18n from '../../core/i18n/i18n.js';
39
39
  import * as Root from '../../core/root/root.js';
40
40
  import * as SDK from '../../core/sdk/sdk.js';
41
+ import * as Badges from '../../models/badges/badges.js';
41
42
  import * as Elements from '../../models/elements/elements.js';
42
43
  import * as IssuesManager from '../../models/issues_manager/issues_manager.js';
43
44
  import * as CodeHighlighter from '../../ui/components/code_highlighter/code_highlighter.js';
@@ -1332,6 +1333,8 @@ export class ElementsTreeOutline extends
1332
1333
  return;
1333
1334
  }
1334
1335
 
1336
+ Badges.UserBadges.instance().recordAction(Badges.BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED);
1337
+
1335
1338
  // Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
1336
1339
  this.runPendingUpdates();
1337
1340
 
@@ -41,6 +41,7 @@ import * as Platform from '../../core/platform/platform.js';
41
41
  import * as Root from '../../core/root/root.js';
42
42
  import * as SDK from '../../core/sdk/sdk.js';
43
43
  import * as Protocol from '../../generated/protocol.js';
44
+ import * as Badges from '../../models/badges/badges.js';
44
45
  import * as Bindings from '../../models/bindings/bindings.js';
45
46
  import * as TextUtils from '../../models/text_utils/text_utils.js';
46
47
  import * as Buttons from '../../ui/components/buttons/buttons.js';
@@ -1546,6 +1547,8 @@ export class StylePropertiesSection {
1546
1547
  if (!success) {
1547
1548
  return Promise.resolve();
1548
1549
  }
1550
+
1551
+ Badges.UserBadges.instance().recordAction(Badges.BadgeAction.CSS_RULE_MODIFIED);
1549
1552
  return this.matchedStyles.recomputeMatchingSelectors(rule).then(updateSourceRanges.bind(this, rule));
1550
1553
  }
1551
1554
 
@@ -10,6 +10,7 @@ import * as i18n from '../../core/i18n/i18n.js';
10
10
  import * as Platform from '../../core/platform/platform.js';
11
11
  import * as Root from '../../core/root/root.js';
12
12
  import * as SDK from '../../core/sdk/sdk.js';
13
+ import * as Badges from '../../models/badges/badges.js';
13
14
  import * as Bindings from '../../models/bindings/bindings.js';
14
15
  import * as TextUtils from '../../models/text_utils/text_utils.js';
15
16
  import type * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js';
@@ -3292,6 +3293,10 @@ export class StylePropertyTreeElement extends UI.TreeOutline.TreeElement {
3292
3293
 
3293
3294
  const overwriteProperty = !this.newProperty || hasBeenEditedIncrementally;
3294
3295
  let success: boolean = await this.property.setText(styleText, majorChange, overwriteProperty);
3296
+ if (success && majorChange) {
3297
+ Badges.UserBadges.instance().recordAction(Badges.BadgeAction.CSS_RULE_MODIFIED);
3298
+ }
3299
+
3295
3300
  // Revert to the original text if applying the new text failed
3296
3301
  if (hasBeenEditedIncrementally && majorChange && !success) {
3297
3302
  majorChange = false;
@@ -336,19 +336,13 @@ export class GenericSettingsTab extends UI.Widget.VBox implements SettingsTab {
336
336
  this.#updateSyncSectionTimerId = -1;
337
337
  }
338
338
 
339
- void Promise
340
- .all([
341
- new Promise<Host.InspectorFrontendHostAPI.SyncInformation>(
342
- resolve => Host.InspectorFrontendHost.InspectorFrontendHostInstance.getSyncInformation(resolve)),
343
- Root.Runtime.hostConfig.devToolsGdpProfiles?.enabled ? Host.GdpClient.GdpClient.instance().getProfile() :
344
- Promise.resolve(undefined),
345
- ])
346
- .then(([syncInfo, gdpProfile]) => {
339
+ void new Promise<Host.InspectorFrontendHostAPI.SyncInformation>(
340
+ resolve => Host.InspectorFrontendHost.InspectorFrontendHostInstance.getSyncInformation(resolve))
341
+ .then(syncInfo => {
347
342
  this.syncSection.data = {
348
343
  syncInfo,
349
344
  syncSetting: Common.Settings.moduleSetting('sync-preferences') as Common.Settings.Setting<boolean>,
350
345
  receiveBadgesSetting: Common.Settings.Settings.instance().moduleSetting('receive-gdp-badges'),
351
- gdpProfile: gdpProfile ?? undefined,
352
346
  };
353
347
  if (!syncInfo.isSyncActive || !syncInfo.arePreferencesSynced) {
354
348
  this.#updateSyncSectionTimerId = window.setTimeout(this.updateSyncSection.bind(this), 500);