| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as Common from '../../core/common/common.js'; |
| import * as Host from '../../core/host/host.js'; |
| import {assertNotNullOrUndefined} from '../../core/platform/platform.js'; |
| |
| import {processEventForDebugging, processImpressionsForDebugging} from './Debugging.js'; |
| import type {Loggable} from './Loggable.js'; |
| import {getLoggingState, type LoggingState} from './LoggingState.js'; |
| |
| export async function logImpressions(loggables: Loggable[]): Promise<void> { |
| const impressions = await Promise.all(loggables.map(async loggable => { |
| const loggingState = getLoggingState(loggable); |
| assertNotNullOrUndefined(loggingState); |
| const impression: |
| Host.InspectorFrontendHostAPI.VisualElementImpression = {id: loggingState.veid, type: loggingState.config.ve}; |
| if (typeof loggingState.config.context !== 'undefined') { |
| impression.context = await contextAsNumber(loggingState.config.context); |
| } |
| if (loggingState.parent) { |
| impression.parent = loggingState.parent.veid; |
| } |
| if (loggingState.size) { |
| impression.width = Math.round(loggingState.size.width); |
| impression.height = Math.round(loggingState.size.height); |
| } |
| return impression; |
| })); |
| if (impressions.length) { |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.recordImpression({impressions}); |
| processImpressionsForDebugging(loggables.map(l => getLoggingState(l) as LoggingState)); |
| } |
| } |
| |
| export const logResize = (loggable: Loggable, size: DOMRect): void => { |
| const loggingState = getLoggingState(loggable); |
| if (!loggingState) { |
| return; |
| } |
| loggingState.size = size; |
| const resizeEvent: Host.InspectorFrontendHostAPI |
| .ResizeEvent = {veid: loggingState.veid, width: loggingState.size.width, height: loggingState.size.height}; |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.recordResize(resizeEvent); |
| processEventForDebugging('Resize', loggingState, {width: Math.round(size.width), height: Math.round(size.height)}); |
| }; |
| |
| export const logClick = (throttler: Common.Throttler.Throttler) => ( |
| loggable: Loggable, event: Event, options?: {doubleClick?: boolean}) => { |
| const loggingState = getLoggingState(loggable); |
| if (!loggingState) { |
| return; |
| } |
| const clickEvent: |
| Host.InspectorFrontendHostAPI.ClickEvent = {veid: loggingState.veid, doubleClick: Boolean(options?.doubleClick)}; |
| if (event instanceof MouseEvent && 'sourceCapabilities' in event && event.sourceCapabilities) { |
| clickEvent.mouseButton = event.button; |
| } |
| void throttler.schedule(async () => { |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.recordClick(clickEvent); |
| processEventForDebugging( |
| 'Click', loggingState, {mouseButton: clickEvent.mouseButton, doubleClick: clickEvent.doubleClick}); |
| }, Common.Throttler.Scheduling.DELAYED); |
| }; |
| |
| export const logHover = (throttler: Common.Throttler.Throttler) => async (event: Event) => { |
| const loggingState = getLoggingState(event.currentTarget as Element); |
| assertNotNullOrUndefined(loggingState); |
| const hoverEvent: Host.InspectorFrontendHostAPI.HoverEvent = {veid: loggingState.veid}; |
| void throttler.schedule(async () => { |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.recordHover(hoverEvent); |
| processEventForDebugging('Hover', loggingState); |
| }, Common.Throttler.Scheduling.DELAYED); |
| }; |
| |
| export const logDrag = (throttler: Common.Throttler.Throttler) => async (event: Event) => { |
| const loggingState = getLoggingState(event.currentTarget as Element); |
| assertNotNullOrUndefined(loggingState); |
| const dragEvent: Host.InspectorFrontendHostAPI.DragEvent = {veid: loggingState.veid}; |
| void throttler.schedule(async () => { |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.recordDrag(dragEvent); |
| processEventForDebugging('Drag', loggingState); |
| }, Common.Throttler.Scheduling.DELAYED); |
| }; |
| |
| export async function logChange(loggable: Loggable): Promise<void> { |
| const loggingState = getLoggingState(loggable); |
| assertNotNullOrUndefined(loggingState); |
| const changeEvent: Host.InspectorFrontendHostAPI.ChangeEvent = {veid: loggingState.veid}; |
| const context = loggingState.pendingChangeContext; |
| if (context) { |
| changeEvent.context = await contextAsNumber(context); |
| } |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.recordChange(changeEvent); |
| processEventForDebugging('Change', loggingState, {context}); |
| } |
| |
| let pendingKeyDownContext: string|null = null; |
| |
| export const logKeyDown = (throttler: Common.Throttler.Throttler) => |
| async (loggable: Loggable|null, event: Event|null, context?: string) => { |
| if (!(event instanceof KeyboardEvent)) { |
| return; |
| } |
| const loggingState = loggable ? getLoggingState(loggable) : null; |
| const codes = (typeof loggingState?.config.track?.keydown === 'string') ? loggingState.config.track.keydown : ''; |
| if (codes.length && !codes.split('|').includes(event.code) && !codes.split('|').includes(event.key)) { |
| return; |
| } |
| const keyDownEvent: Host.InspectorFrontendHostAPI.KeyDownEvent = {veid: loggingState?.veid}; |
| if (!context && codes?.length) { |
| context = contextFromKeyCodes(event); |
| } |
| |
| if (pendingKeyDownContext && context && pendingKeyDownContext !== context) { |
| void throttler.process?.(); |
| } |
| |
| pendingKeyDownContext = context || null; |
| void throttler.schedule(async () => { |
| if (context) { |
| keyDownEvent.context = await contextAsNumber(context); |
| } |
| |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.recordKeyDown(keyDownEvent); |
| processEventForDebugging('KeyDown', loggingState, {context}); |
| pendingKeyDownContext = null; |
| }); |
| }; |
| |
| function contextFromKeyCodes(event: Event): string|undefined { |
| if (!(event instanceof KeyboardEvent)) { |
| return undefined; |
| } |
| const key = event.key; |
| const lowerCaseKey = key.toLowerCase(); |
| const components = []; |
| if (event.shiftKey && key !== lowerCaseKey) { |
| components.push('shift'); |
| } |
| if (event.ctrlKey) { |
| components.push('ctrl'); |
| } |
| if (event.altKey) { |
| components.push('alt'); |
| } |
| if (event.metaKey) { |
| components.push('meta'); |
| } |
| components.push(lowerCaseKey); |
| return components.join('-'); |
| } |
| |
| export async function contextAsNumber(context: string|undefined): Promise<number|undefined> { |
| if (typeof context === 'undefined') { |
| return undefined; |
| } |
| const number = parseInt(context, 10); |
| if (!isNaN(number)) { |
| return number; |
| } |
| if (!crypto.subtle) { |
| // Layout tests run in an insecure context where crypto.subtle is not available. |
| return 0xDEADBEEF; |
| } |
| const encoder = new TextEncoder(); |
| const data = encoder.encode(context); |
| const digest = await crypto.subtle.digest('SHA-1', data); |
| return new DataView(digest).getInt32(0, true); |
| } |
| |
| export async function logSettingAccess(name: string, value: number|string|boolean): Promise<void> { |
| let numericValue: number|undefined = undefined; |
| let stringValue: string|undefined = undefined; |
| if (typeof value === 'string') { |
| stringValue = value; |
| } else if (typeof value === 'number' || typeof value === 'boolean') { |
| numericValue = Number(value); |
| } |
| const nameHash = await contextAsNumber(name); |
| if (!nameHash) { |
| return; |
| } |
| const settingAccessEvent: Host.InspectorFrontendHostAPI.SettingAccessEvent = { |
| name: nameHash, |
| numeric_value: numericValue, |
| string_value: await contextAsNumber(stringValue), |
| }; |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.recordSettingAccess(settingAccessEvent); |
| processEventForDebugging('SettingAccess', null, {name, numericValue, stringValue}); |
| } |
| |
| export async function logFunctionCall(name: string, context?: string): Promise<void> { |
| const nameHash = await contextAsNumber(name); |
| if (typeof nameHash === 'undefined') { |
| return; |
| } |
| const functionCallEvent: |
| Host.InspectorFrontendHostAPI.FunctionCallEvent = {name: nameHash, context: await contextAsNumber(context)}; |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.recordFunctionCall(functionCallEvent); |
| processEventForDebugging('FunctionCall', null, {name, context}); |
| } |