Danil Somsikov | acf184f | 2023-11-20 15:17:08 | [diff] [blame] | 1 | # Visual logging |
| 2 | |
| 3 | The goal of this project is to improve the logging of user interactions in |
Kateryna Prokopenko | 94742f6 | 2023-12-18 09:21:49 | [diff] [blame] | 4 | DevTools. The current UMA logging is unreliable and inconsistent. This can lead to |
Danil Somsikov | acf184f | 2023-11-20 15:17:08 | [diff] [blame] | 5 | incorrect conclusions about how users are interacting with the product. |
| 6 | |
| 7 | We want to be able to understand how users are interacting with DevTools so that |
| 8 | we can improve the product. This includes understanding what users are seeing, |
| 9 | what they are interacting with, and how they are using different features. |
| 10 | |
Benedikt Meurer | 71006b7 | 2023-12-18 08:05:50 | [diff] [blame] | 11 | To turn on the logging, you need to pass `--enable-features=DevToolsVeLogging` |
| 12 | as command line flag to Chrome. |
| 13 | |
Danil Somsikov | acf184f | 2023-11-20 15:17:08 | [diff] [blame] | 14 | ## General approach |
| 15 | |
| 16 | We log impressions and interactions for a subtree of the actual DevTools |
| 17 | DOM tree. The logging is based on an HTML attribute and an arbitrary context |
| 18 | value. The context value provides more information about the |
| 19 | element that is being logged. |
| 20 | |
| 21 | The trickiest questions are what elements to log and how to describe them. |
| 22 | There are no hard rules here, we log what we think is helpful to understand user |
| 23 | behavior. Here are several rules of thumb: |
| 24 | |
| 25 | 1. Most of the interactive elements should be logged. |
| 26 | 1. Elements that only appear under certain conditions should be logged. |
| 27 | 1. Container elements that help distinguish common UI elements should be logged. |
| 28 | |
| 29 | We describe loggable elements as a combination of visual element type and |
| 30 | context value. Visual element type is a value of `VisualElements` enum defined |
| 31 | in `front_end/ui/visual_logging/LoggingConfig.ts`. Context could be an arbitrary |
| 32 | string, its 32 bit hash is logged. No taxonomy is perfect and we are not |
| 33 | trying to create one here, we are trying to make it practical though. |
| 34 | |
| 35 | 1. Visual element types try to describe function, not appearance. For instance, |
| 36 | we have `Toggle` and `Action` as visual elements types, not `Checkbox` and |
| 37 | `Button`. This is important for keeping user actions trackable across longer time |
| 38 | frames, even if the appearance of UI elements change. |
| 39 | 1. We also try to have visual element types describe classes of elements, and |
| 40 | context describe a specific instance. For example, we log the Elements panel as |
| 41 | a `Panel` visual element with `elements` as a context, not as `ElementsPanel`. |
| 42 | This not only helps to keep the number of visual element types reasonable, but |
| 43 | also makes it easier to compare relative performance of similar UI elements. |
| 44 | 1. We log visual elements as a tree, so it is redundant to include location |
| 45 | information in either visual element type or context. For example, `TreeOutline` |
| 46 | inside `styles` `Pane` inside `elements` `Panel` is unambiguous by itself, |
| 47 | without any extra qualifiers. |
| 48 | |
| 49 | ## API |
| 50 | |
| 51 | In most cases, it is enough to put the `jslog` attribute on a corresponding HTML |
| 52 | element. There’s a number of fluent builder functions exported |
| 53 | `front_end/ui/visual_logging/visual_logging.ts` to build the attribute value. |
| 54 | These are all bound versions of `LoggingConfig.makeConfigStringBuilder` and are |
| 55 | used in the legacy UI as: |
| 56 | |
| 57 | ``` |
Wolfgang Beyer | 6d0bdde | 2024-01-31 10:49:09 | [diff] [blame] | 58 | this.element.setAttribute('jslog', `${VisualLogging.panel(context)}`); |
Danil Somsikov | acf184f | 2023-11-20 15:17:08 | [diff] [blame] | 59 | ``` |
| 60 | |
| 61 | or |
| 62 | |
| 63 | ``` |
Wolfgang Beyer | 6d0bdde | 2024-01-31 10:49:09 | [diff] [blame] | 64 | button.element.setAttribute('jslog', `${VisualLogging.dropDown('rendering-emulations') |
| 65 | .track({click: true})}`); |
Danil Somsikov | acf184f | 2023-11-20 15:17:08 | [diff] [blame] | 66 | ``` |
| 67 | |
| 68 | In LitHTML, the usage is: |
| 69 | |
| 70 | ``` |
Wolfgang Beyer | 6d0bdde | 2024-01-31 10:49:09 | [diff] [blame] | 71 | Lit.html`<td jslog=${VisualLogging.tableCell(/* context */ col.id) |
| 72 | .track({click: true})}> |
Danil Somsikov | acf184f | 2023-11-20 15:17:08 | [diff] [blame] | 73 | ``` |
| 74 | |
| 75 | ### `jslog` Builder API |
| 76 | |
| 77 | The `track()` method generates a `track:` clause and specifies exactly what needs to |
| 78 | be logged for the visual elements. If not invoked, only impressions are logged for |
| 79 | this element. Called with tracking options, an object with the following boolean properties: |
| 80 | * `click`: Whether to track clicks. |
| 81 | * `dblclick`: Whether to track double clicks. |
| 82 | * `hover`: Whether to track hover events. |
| 83 | * `drag`: Whether to track drag events. |
| 84 | * `change`: Whether to track change events. |
| 85 | * `keydown`: Whether to track keydown events. This property can be boolean or string. |
| 86 | If a string is provided, it will be used as the key code to track. Otherwise, all keydown |
| 87 | events will be tracked. |
| 88 | |
Wolfgang Beyer | 6d0bdde | 2024-01-31 10:49:09 | [diff] [blame] | 89 | The builder function accepts a `context` parameter, which sets the context for the visual |
| 90 | logging element. The context can be a string or a number. If a string is given, it is be |
| 91 | first considered to refer to a context provider (see below). If no context provider is |
| 92 | registered with this name, SHA-1 hash is computed and the first 32 bits |
Kateryna Prokopenko | 94742f6 | 2023-12-18 09:21:49 | [diff] [blame] | 93 | (little endian) is logged. Number will be logged as is. |
Danil Somsikov | acf184f | 2023-11-20 15:17:08 | [diff] [blame] | 94 | |
| 95 | The `parent()` method sets the custom parent provider for the visual logging element |
| 96 | (see below). If not invoked, the parent visual element is taken from a DOM tree structure. |
| 97 | |
| 98 | ### Context and parent providers |
| 99 | |
| 100 | Context providers are used to generate context value in a runtime. It is used |
| 101 | for both impressions and events. This is useful when relevant information is not |
| 102 | reflected in the DOM, for example when a pseudo-element is used to render a tree item |
| 103 | disclosure triangle or canvas is used to draw network waterfall. When logging |
| 104 | impressions, context may indicate if or how many elements are present (0 or 1 for |
| 105 | having a disclosure triangle or not; number of tracks in the waterfall). When |
| 106 | logging events, context could identify what was clicked (1 for disclosure |
| 107 | triangle, 0 for tree item itself, sequence number of a waterfall track). |
| 108 | |
| 109 | As this only logs a single number, it's not enough for more complex cases, |
| 110 | like diverse tracks in the Performance panel canvas, or hosted menu. For these |
| 111 | scenarios, see the section below. |
| 112 | |
| 113 | To register a context provider, call `VisualLogging.registerContextProvider`. |
| 114 | First argument is a provider name that is later used as an argument in the |
| 115 | `jslog` builder `context()` method. Second is a function that takes an Element or Event |
| 116 | and returns a number. For a disclosure triangle, this is as follows: |
| 117 | |
| 118 | ``` |
| 119 | function disclosureTriangleLoggingContextProvider( |
| 120 | e: VisualLogging.Loggable|Event): Promise<number|undefined> { |
| 121 | if (e instanceof Element) { |
| 122 | return Promise.resolve(e.classList.contains('parent') ? 1 : 0); |
| 123 | } |
| 124 | if (e instanceof MouseEvent && e.currentTarget instanceof Node) { |
| 125 | const treeElement = TreeElement.getTreeElementBylistItemNode(e.currentTarget); |
| 126 | if (treeElement) { |
| 127 | return Promise.resolve(treeElement.isEventWithinDisclosureTriangle(e) ? 1 : 0); |
| 128 | } |
| 129 | } |
| 130 | return Promise.resolve(undefined); |
| 131 | } |
| 132 | |
| 133 | |
| 134 | VisualLogging.registerContextProvider('disclosureTriangle', |
| 135 | disclosureTriangleLoggingContextProvider); |
| 136 | |
| 137 | listItemNode.setAttribute('jslog', `${VisualLogging.treeItem() |
| 138 | .track({click: true}).context('disclosureTriangle')}`); |
| 139 | ``` |
| 140 | |
| 141 | Similarly parent provides are used to specify parent visual elements in |
| 142 | runtime. This should be used rarely because, most of the time, DOM hierarchy is enough |
| 143 | to identify the parent. However, sometimes, markup doesn’t reflect the logical |
| 144 | structure, for example, when a legacy tree outline has children in an `<ol>` element, which is a |
| 145 | sibling of `<li>` that specifies the parent. In this case, you can do the following: |
| 146 | |
| 147 | ``` |
| 148 | function loggingParentProvider(e: Element): Element|undefined { |
| 149 | const treeElement = TreeElement.getTreeElementBylistItemNode(e); |
| 150 | return treeElement?.parent?.listItemElement; |
| 151 | } |
| 152 | |
| 153 | VisualLogging.registerParentProvider('parentTreeItem', |
| 154 | loggingParentProvider); |
| 155 | |
| 156 | this.listItemNode.setAttribute( |
| 157 | 'jslog', |
| 158 | `${VisualLogging.treeItem().track({click: true}).parent('parentTreeItem')}`); |
| 159 | ``` |
| 160 | |
| 161 | ### Logging beyond DOM |
| 162 | |
| 163 | Some DevTools UI is not represented in DOM, such as tracks in the Performance |
| 164 | panel or native menus. To log these, the visual logging library provides an |
| 165 | imperative API. Use it rarely, when no other options are |
| 166 | available because it requires manual orchestration and is subject to the same issues |
| 167 | as UMA histogram logging. |
| 168 | |
| 169 | First, identify the TypeScript type that corresponds/ to the element that |
| 170 | needs to be logged. For example, `ContextMenuDescriptor` is used to log native |
| 171 | menu items. This type needs to be added to the `Loggable` type definition in |
| 172 | `front_end/ui/visual_logging/Loggable.ts`. |
| 173 | |
| 174 | |
| 175 | Then call `registerLoggable` with the corresponding JavaScript |
| 176 | object, config string in the same format as the `jslog` attribute would have, |
| 177 | and an optional parent JavaScript object. For a native menu item, this is: |
| 178 | |
| 179 | |
| 180 | ``` |
| 181 | VisualLogging.registerLoggable(descriptor, `${VisualLogging.action() |
| 182 | .track({click: true}).context(descriptor.jslogContext)}`, |
| 183 | parent || descriptors); |
| 184 | ``` |
| 185 | |
| 186 | This only registers the element and doesn’t log anything yet. To log |
| 187 | impressions, explicitly call `VisualLogging.logImpressions`. |
| 188 | Similarly to log click, call `VisualLogging.logClick`. |
| 189 | |
Danil Somsikov | a05fa1c | 2023-11-21 11:02:34 | [diff] [blame] | 190 | ## Debugging |
| 191 | |
| 192 | You may find it useful to see which UI elements are annotated and how the tree |
| 193 | structure look like. To do that, call `setVeDebuggingEnabled(true)` in DevTools |
| 194 | on DevTools. This will add red outline to each visual element and will show the |
| 195 | details of logging config for an element and all its ancestors on hover. |
Benedikt Meurer | 71006b7 | 2023-12-18 08:05:50 | [diff] [blame] | 196 | |
| 197 | **Note:** This will only work if you invoked Chrome with the command line flag |
| 198 | `--enable-features=DevToolsVeLogging`. Otherwise you won't see any red lines. |
Jack Franklin | 8c5a639 | 2024-06-07 10:38:03 | [diff] [blame] | 199 | |
| 200 | You can also run `setVeDebugLoggingEnabled(true)` in DevTools on DevTools. This |
| 201 | will cause each VE log to be also logged to the DevTools on DevTools console. |
| 202 | They will also be stored in the global variable `veDebugEventsLog`. |