Skip to content

Commit 4ab8cec

Browse files
danilsomsikovDevtools-frontend LUCI CQ
authored andcommitted
Record setting access events in the frontend
We aim at getting a distribution of the settings values across sessions and users, so we log only the first "get" but all "sets". Bug: 400345550 Change-Id: Icbd9479a15c5bed779f41c9b831551aeaa56ca48 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6317766 Commit-Queue: Danil Somsikov <[email protected]> Auto-Submit: Danil Somsikov <[email protected]> Reviewed-by: Simon Zünd <[email protected]>
1 parent afd77e5 commit 4ab8cec

File tree

9 files changed

+129
-11
lines changed

9 files changed

+129
-11
lines changed

front_end/core/common/Settings.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,3 +619,45 @@ describe('updateVersionFrom37To38', () => {
619619
assert.isFalse(onboardingFinished.get());
620620
});
621621
});
622+
623+
describe('access logging', () => {
624+
let settings: Common.Settings.Settings;
625+
let logSettingAccess!: sinon.SinonSpy;
626+
627+
beforeEach(() => {
628+
const mockStore = new MockStore();
629+
const syncedStorage = new Common.Settings.SettingsStorage({}, mockStore);
630+
const globalStorage = new Common.Settings.SettingsStorage({}, mockStore);
631+
const localStorage = new Common.Settings.SettingsStorage({}, mockStore);
632+
logSettingAccess = sinon.spy();
633+
settings = Common.Settings.Settings.instance({
634+
forceNew: true,
635+
syncedStorage,
636+
globalStorage,
637+
localStorage,
638+
logSettingAccess,
639+
});
640+
});
641+
642+
it('logs access on the first read', async () => {
643+
const setting = settings.createSetting('test-setting', false);
644+
assert.isFalse(logSettingAccess.called);
645+
646+
setting.get();
647+
assert.isTrue(logSettingAccess.calledOnceWith('test-setting', false));
648+
649+
setting.get();
650+
assert.isTrue(logSettingAccess.calledOnce);
651+
});
652+
653+
it('logs access on the every write', async () => {
654+
const setting = settings.createSetting('test-setting', false);
655+
656+
setting.set(true);
657+
assert.isTrue(logSettingAccess.calledOnceWith('test-setting', true));
658+
659+
setting.set(false);
660+
assert.isTrue(logSettingAccess.calledTwice);
661+
assert.deepEqual(logSettingAccess.secondCall.args, ['test-setting', false]);
662+
});
663+
});

front_end/core/common/Settings.ts

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,13 @@ export class Settings {
5858
#eventSupport: ObjectWrapper<GenericEvents>;
5959
#registry: Map<string, Setting<unknown>>;
6060
readonly moduleSettings: Map<string, Setting<unknown>>;
61+
#logSettingAccess?: (name: string, value: number|string|boolean) => Promise<void>;
6162

6263
private constructor(
6364
readonly syncedStorage: SettingsStorage,
6465
readonly globalStorage: SettingsStorage,
6566
readonly localStorage: SettingsStorage,
67+
logSettingAccess?: (name: string, value: number|string|boolean) => Promise<void>,
6668
) {
6769
this.#sessionStorage = new SettingsStorage({});
6870

@@ -73,6 +75,7 @@ export class Settings {
7375
this.#eventSupport = new ObjectWrapper<GenericEvents>();
7476
this.#registry = new Map();
7577
this.moduleSettings = new Map();
78+
this.#logSettingAccess = logSettingAccess;
7679

7780
for (const registration of this.getRegisteredSettings()) {
7881
const {settingName, defaultValue, storageType} = registration;
@@ -107,14 +110,15 @@ export class Settings {
107110
syncedStorage: SettingsStorage|null,
108111
globalStorage: SettingsStorage|null,
109112
localStorage: SettingsStorage|null,
113+
logSettingAccess?: (name: string, value: number|string|boolean) => Promise<void>,
110114
} = {forceNew: null, syncedStorage: null, globalStorage: null, localStorage: null}): Settings {
111-
const {forceNew, syncedStorage, globalStorage, localStorage} = opts;
115+
const {forceNew, syncedStorage, globalStorage, localStorage, logSettingAccess} = opts;
112116
if (!settingsInstance || forceNew) {
113117
if (!syncedStorage || !globalStorage || !localStorage) {
114118
throw new Error(`Unable to create settings: global and local storage must be provided: ${new Error().stack}`);
115119
}
116120

117-
settingsInstance = new Settings(syncedStorage, globalStorage, localStorage);
121+
settingsInstance = new Settings(syncedStorage, globalStorage, localStorage, logSettingAccess);
118122
}
119123

120124
return settingsInstance;
@@ -192,7 +196,7 @@ export class Settings {
192196
const storage = this.storageFromType(storageType);
193197
let setting = (this.#registry.get(key) as Setting<T>);
194198
if (!setting) {
195-
setting = new Setting(key, defaultValue, this.#eventSupport, storage);
199+
setting = new Setting(key, defaultValue, this.#eventSupport, storage, this.#logSettingAccess);
196200
this.#registry.set(key, setting);
197201
}
198202
return setting;
@@ -206,7 +210,10 @@ export class Settings {
206210
RegExpSetting {
207211
if (!this.#registry.get(key)) {
208212
this.#registry.set(
209-
key, new RegExpSetting(key, defaultValue, this.#eventSupport, this.storageFromType(storageType), regexFlags));
213+
key,
214+
new RegExpSetting(
215+
key, defaultValue, this.#eventSupport, this.storageFromType(storageType), regexFlags,
216+
this.#logSettingAccess));
210217
}
211218
return this.#registry.get(key) as RegExpSetting;
212219
}
@@ -368,11 +375,15 @@ export class Setting<V> {
368375
#hadUserAction?: boolean;
369376
#disabled?: boolean;
370377
#deprecation: Deprecation|null = null;
378+
#loggedInitialAccess = false;
379+
#logSettingAccess?: (name: string, value: number|string|boolean) => Promise<void>;
371380

372381
constructor(
373382
readonly name: string, readonly defaultValue: V, private readonly eventSupport: ObjectWrapper<GenericEvents>,
374-
readonly storage: SettingsStorage) {
383+
readonly storage: SettingsStorage,
384+
logSettingAccess?: (name: string, value: number|string|boolean) => Promise<void>) {
375385
storage.register(this.name);
386+
this.#logSettingAccess = logSettingAccess;
376387
}
377388

378389
setSerializer(serializer: Serializer<unknown, V>): void {
@@ -438,12 +449,30 @@ export class Setting<V> {
438449
this.eventSupport.dispatchEventToListeners(this.name);
439450
}
440451

452+
#maybeLogAccess(value: V): void {
453+
const valueToLog = typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ?
454+
value :
455+
this.#serializer?.stringify(value);
456+
if (valueToLog !== undefined && this.#logSettingAccess) {
457+
void this.#logSettingAccess(this.name, valueToLog);
458+
}
459+
}
460+
461+
#maybeLogInitialAccess(value: V): void {
462+
if (!this.#loggedInitialAccess) {
463+
this.#maybeLogAccess(value);
464+
this.#loggedInitialAccess = true;
465+
}
466+
}
467+
441468
get(): V {
442469
if (this.#requiresUserAction && !this.#hadUserAction) {
470+
this.#maybeLogInitialAccess(this.defaultValue);
443471
return this.defaultValue;
444472
}
445473

446474
if (typeof this.#value !== 'undefined') {
475+
this.#maybeLogInitialAccess(this.#value);
447476
return this.#value;
448477
}
449478

@@ -455,6 +484,7 @@ export class Setting<V> {
455484
this.storage.remove(this.name);
456485
}
457486
}
487+
this.#maybeLogInitialAccess(this.#value);
458488
return this.#value;
459489
}
460490

@@ -485,10 +515,12 @@ export class Setting<V> {
485515
this.eventSupport.dispatchEventToListeners(this.name, this.#value);
486516
}
487517

518+
this.#maybeLogInitialAccess(this.#value);
488519
return this.#value;
489520
}
490521

491522
set(value: V): void {
523+
this.#maybeLogAccess(value);
492524
this.#hadUserAction = true;
493525
this.#value = value;
494526
try {
@@ -600,8 +632,8 @@ export class RegExpSetting extends Setting<any> {
600632

601633
constructor(
602634
name: string, defaultValue: string, eventSupport: ObjectWrapper<GenericEvents>, storage: SettingsStorage,
603-
regexFlags?: string) {
604-
super(name, defaultValue ? [{pattern: defaultValue}] : [], eventSupport, storage);
635+
regexFlags?: string, logSettingAccess?: (name: string, value: number|string|boolean) => Promise<void>) {
636+
super(name, defaultValue ? [{pattern: defaultValue}] : [], eventSupport, storage, logSettingAccess);
605637
this.#regexFlags = regexFlags;
606638
}
607639

front_end/core/host/InspectorFrontendHost.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
type KeyDownEvent,
5656
type LoadNetworkResourceResult,
5757
type ResizeEvent,
58+
type SettingAccessEvent,
5859
type ShowSurveyResult,
5960
type SyncInformation,
6061
} from './InspectorFrontendHostAPI.js';
@@ -549,6 +550,8 @@ export class InspectorFrontendHostStub implements InspectorFrontendHostAPI {
549550
}
550551
recordKeyDown(event: KeyDownEvent): void {
551552
}
553+
recordSettingAccess(event: SettingAccessEvent): void {
554+
}
552555
}
553556

554557
// @ts-expect-error Global injected by devtools_compatibility.js

front_end/core/host/InspectorFrontendHostAPI.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,12 @@ export interface KeyDownEvent {
199199
context?: number;
200200
}
201201

202+
export interface SettingAccessEvent {
203+
name: string;
204+
numericValue?: number;
205+
stringValue?: string;
206+
}
207+
202208
// While `EventDescriptors` are used to dynamically dispatch host binding events,
203209
// the `EventTypes` "type map" is used for type-checking said events by TypeScript.
204210
// `EventTypes` is not used at runtime.
@@ -384,6 +390,7 @@ export interface InspectorFrontendHostAPI {
384390
recordDrag(event: DragEvent): void;
385391
recordChange(event: ChangeEvent): void;
386392
recordKeyDown(event: KeyDownEvent): void;
393+
recordSettingAccess(event: SettingAccessEvent): void;
387394
}
388395

389396
export interface AcceleratorDescriptor {

front_end/entrypoints/main/MainImpl.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,8 @@ export class MainImpl {
273273
// setting can't change storage buckets during a single DevTools session.
274274
const syncedStorage = new Common.Settings.SettingsStorage(prefs, hostSyncedStorage, storagePrefix);
275275
const globalStorage = new Common.Settings.SettingsStorage(prefs, hostUnsyncedStorage, storagePrefix);
276-
Common.Settings.Settings.instance({forceNew: true, syncedStorage, globalStorage, localStorage});
276+
Common.Settings.Settings.instance(
277+
{forceNew: true, syncedStorage, globalStorage, localStorage, logSettingAccess: VisualLogging.logSettingAccess});
277278

278279
if (!Host.InspectorFrontendHost.isUnderTest()) {
279280
new Common.Settings.VersionController().updateVersion();

front_end/ui/visual_logging/Debugging.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ function processElementForDebugging(element: HTMLElement, loggingState: LoggingS
126126
}
127127
}
128128

129-
type EventType = 'Click'|'Drag'|'Hover'|'Change'|'KeyDown'|'Resize';
129+
type EventType = 'Click'|'Drag'|'Hover'|'Change'|'KeyDown'|'Resize'|'SettingAccess';
130130
export function processEventForDebugging(
131131
event: EventType, state: LoggingState|null, extraInfo?: EventAttributes): void {
132132
const format = localStorage.getItem('veDebugLoggingEnabled');
@@ -163,7 +163,9 @@ export function processEventForIntuitiveDebugging(
163163

164164
export function processEventForTestDebugging(
165165
event: EventType, state: LoggingState|null, _extraInfo?: EventAttributes): void {
166-
lastImpressionLogEntry = null;
166+
if (event !== 'SettingAccess') {
167+
lastImpressionLogEntry = null;
168+
}
167169
maybeLogDebugEvent(
168170
{interaction: `${event}: ${veTestKeys.get(state?.veid || 0) || (state?.veid ? '<UNKNOWN>' : '')}`});
169171
checkPendingEventExpectation();
@@ -194,6 +196,9 @@ export interface EventAttributes {
194196
height?: number;
195197
mouseButton?: number;
196198
doubleClick?: boolean;
199+
name?: string;
200+
numericValue?: number;
201+
stringValue?: string;
197202
}
198203

199204
interface VisualElementAttributes {

front_end/ui/visual_logging/LoggingEvents.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,4 +242,19 @@ describe('LoggingEvents', () => {
242242
VisualLogging.LoggingEvents.logResize(element, new DOMRect(0, 0, 100, 50));
243243
assert.deepEqual(recordResize.firstCall.firstArg, {veid, width: 100, height: 50});
244244
});
245+
246+
it('calls UI binding to log a setting access event', async () => {
247+
const recordSettingAccess = sinon.stub(
248+
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
249+
'recordSettingAccess',
250+
);
251+
await VisualLogging.LoggingEvents.logSettingAccess('test-setting', 'test-value');
252+
assert.deepEqual(
253+
recordSettingAccess.lastCall.firstArg,
254+
{name: 'test-setting', numericValue: undefined, stringValue: 'test-value'});
255+
256+
await VisualLogging.LoggingEvents.logSettingAccess('test-setting', 123);
257+
assert.deepEqual(
258+
recordSettingAccess.lastCall.firstArg, {name: 'test-setting', numericValue: 123, stringValue: undefined});
259+
});
245260
});

front_end/ui/visual_logging/LoggingEvents.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,16 @@ export async function contextAsNumber(context: string|undefined): Promise<number
169169
const digest = await crypto.subtle.digest('SHA-1', data);
170170
return new DataView(digest).getInt32(0, true);
171171
}
172+
173+
export async function logSettingAccess(name: string, value: number|string|boolean): Promise<void> {
174+
let numericValue: number|undefined = undefined;
175+
let stringValue: string|undefined = undefined;
176+
if (typeof value === 'string') {
177+
stringValue = value;
178+
} else if (typeof value === 'number' || typeof value === 'boolean') {
179+
numericValue = Number(value);
180+
}
181+
const settingAccessEvent: Host.InspectorFrontendHostAPI.SettingAccessEvent = {name, numericValue, stringValue};
182+
Host.InspectorFrontendHost.InspectorFrontendHostInstance.recordSettingAccess(settingAccessEvent);
183+
processEventForDebugging('SettingAccess', null, {name, numericValue, stringValue});
184+
}

front_end/ui/visual_logging/visual_logging.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as NonDomState from './NonDomState.js';
1111
export type Loggable = LoggableModule.Loggable;
1212
export {DebugLoggingFormat, setVeDebuggingEnabled, setVeDebugLoggingEnabled} from './Debugging.js';
1313
export {addDocument, startLogging, stopLogging} from './LoggingDriver.js';
14-
export {logImpressions} from './LoggingEvents.js';
14+
export {logImpressions, logSettingAccess} from './LoggingEvents.js';
1515
export const logClick = (loggable: Loggable, event: Event, options: {doubleClick?: boolean} = {}): void =>
1616
LoggingEvents.logClick(LoggingDriver.clickLogThrottler)(loggable, event, options);
1717

0 commit comments

Comments
 (0)