diff options
Diffstat (limited to 'devtools/docs/contributor/frontend')
-rw-r--r-- | devtools/docs/contributor/frontend/csp.md | 56 | ||||
-rw-r--r-- | devtools/docs/contributor/frontend/react-guidelines.md | 73 | ||||
-rw-r--r-- | devtools/docs/contributor/frontend/react.md | 157 | ||||
-rw-r--r-- | devtools/docs/contributor/frontend/redux-guidelines.md | 52 | ||||
-rw-r--r-- | devtools/docs/contributor/frontend/redux.md | 160 | ||||
-rw-r--r-- | devtools/docs/contributor/frontend/svgs.md | 42 | ||||
-rw-r--r-- | devtools/docs/contributor/frontend/telemetry.md | 447 |
7 files changed, 987 insertions, 0 deletions
diff --git a/devtools/docs/contributor/frontend/csp.md b/devtools/docs/contributor/frontend/csp.md new file mode 100644 index 0000000000..e15fdf76f7 --- /dev/null +++ b/devtools/docs/contributor/frontend/csp.md @@ -0,0 +1,56 @@ + +The DevTools toolbox is loaded in an iframe pointing to about:devtools-toolbox. This iframe has a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) applied, which will mitigate potential attacks. However this may limit the resources that can be loaded in the toolbox documenth. + +# Current DevTools CSP + +The current policy for about:devtools-toolbox is: +``` +default-src chrome: resource:; img-src chrome: resource: data:; object-src 'none' +``` + +This means: +- `chrome://` and `resource://` are allowed for any resource +- `chrome://` and `resource://` and `data://` are allowed for images + +For more information about which resources and requests are in scope of the CSP, you can read the [default-src documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src). + +# Scope of the DevTools CSP + +This content security policy only applies to the toolbox document for now. If you are working within the document of a panel or if you are working on the server, those limitations should not apply. + +Note that even when working in the document of a panel, we are sometimes interacting with the toolbox document, for instance to show tooltips. So typically any resource created for a tooltip will be subject to the CSP limitations. + +# Recognizing CSP issues + +Open the Browser Toolbox, if you see errors such as + +``` +JavaScript Error: "Content-Security-Policy: The page’s settings blocked the loading of a resource [...]" +``` + +it means you are trying to load a resource with a forbidden scheme. + +# Fixing CSP issues + +If your implementation hits a CSP issue, the first suggestion is to try to use a supported scheme. If this is not an option, check if you can perform your request or load your resource outside of the toolbox document. For instance if the resource you are loading is related to the debugged target, the request can (and probably should) be made from an actor in the DevTools server and then forwarded from the server to the client. Requests made by the server will not be impacted by the CSP. + +If it seems like the only solution is to update the CSP, get in touch with security peers in order to discuss about your use case. You can [file a bug in Core/DOM: security](https://bugzilla.mozilla.org/enter_bug.cgi?product=Core&component=DOM%3A%20Security). + +# Fixing CSP issues in tests + +If the issue comes from test code, it should be possible to update the test to use a supported scheme. A typical issue might be trying to load an iframe inside of the toolbox with a data-uri. Instead, you can create an HTML support file and load it from either a chrome:// or a resource:// URL. + +In general once a support file is added you can access it via: +- `https://example.com/browser/[path_to_file]` +- or `chrome://mochitests/content/browser/[path_to_file]` + +For instance [devtools/client/aboutdebugging/test/browser/resources/service-workers/controlled-sw.html](https://searchfox.org/mozilla-central/source/devtools/client/aboutdebugging/test/browser/resources/service-workers/controlled-sw.html) is accessed in tests via `http://example.com/browser/devtools/client/aboutdebugging/test/browser/resources/service-workers/controlled-sw.html`. + +If you absolutely have to use an unsupported scheme, you can turn off CSPs for the test only. To do so, you need to temporarily update two preferences: + +``` +await pushPref("security.csp.enable", false); +await pushPref("dom.security.skip_about_page_has_csp_assert", true); +``` + +The `pushPref` helper will ensure the preferences come back to their initial value at the end of the test. diff --git a/devtools/docs/contributor/frontend/react-guidelines.md b/devtools/docs/contributor/frontend/react-guidelines.md new file mode 100644 index 0000000000..0622daf2ce --- /dev/null +++ b/devtools/docs/contributor/frontend/react-guidelines.md @@ -0,0 +1,73 @@ + +# Guidelines for Writing React + +These are soft rules for writing react devtools code. Try to stick to +these for consistency, and if you disagree, file a bug to change these +docs and we can talk about it. + +**Please also read** the [coding + standards](https://wiki.mozilla.org/DevTools/CodingStandards#React_.26_Redux) +for react and redux code. The guidelines here are more general +patterns not specific to code style. + +## Why no JSX? + +You probably already noticed we don't use JSX. The answer isn't +complicated: we don't build our JS code, and we write directly for our +JS engine, SpiderMonkey. It already supports much of ES6, but it does +not support JSX (which is not a standard). + +This may change if we ever adopt a build step. Even so, the author is +not convinced that JSX helps enough to warrant all the syntax. It is +clearer sometimes, but it can be noisy switching between JSX and JS a +lot. + +It's not as bad as you may think! If you are used to JSX it may be an +adjustment, but you won't miss it too much. + +## One component per file + +Try to only put one component in a file. This helps avoid large files +full of components, but it's also technically required for how we wrap +components with factories. See the next rule. + +It also makes it easier to write tests because you might not export +some components, so tests can't access them. + +You can include small helper components in the same file if you really +want to, but note that they won't be directly tested and you will have +to use `React.createElement` or immediately wrap them in factories to +use them. + +## Export the component directly and create factory on import + +Modules are the way components interact. Ideally every component lives +in a separate file and they require whatever they need. This allows +tests to access all components and use module boundaries to wrap +components. + +For example, we don't use JSX, so we need to create factories for +components to use them as functions. A simple way to do this is on +import: + +```js +const Thing1 = React.createFactory(require('./thing1')); +const Thing2 = React.createFactory(require('./thing2')); +``` + +It adds a little noise, but then you can do `Thing1({ ... })` instead +of `React.createElement(Thing1, { ... })`. Definitely worth it. + +Additionally, make sure to export the component class directly: + +```js +const Thing1 = React.createClass({ ... }); +module.exports = Thing1; +``` + +Do not export `{ Thing1 }` or anything like that. This is required for +the factory wrapping as well as hot reloading. + +## More to Come + +This is just a start. We will add more to this document. diff --git a/devtools/docs/contributor/frontend/react.md b/devtools/docs/contributor/frontend/react.md new file mode 100644 index 0000000000..770320f570 --- /dev/null +++ b/devtools/docs/contributor/frontend/react.md @@ -0,0 +1,157 @@ + +We use [React](http://facebook.github.io/react/) to write our user +interfaces. In here you can find an explanation of why we chose React +and a short primer on it. Additionally, we list best practices that +all devtools code should adhere to when writing React. + +# Quick Intro + +This is a very quick introduction on how to *use* React, but does not +explain in-depth the concepts behind it. If you want more in-depth +articles, I recommend the following links: + +* http://facebook.github.io/react/docs/tutorial.html - the official tutorial +* https://github.com/petehunt/react-howto - how to learn React +* http://jlongster.com/Removing-User-Interface-Complexity,-or-Why-React-is-Awesome - long read but explains the concepts in depth + +React embraces components as a way of thinking about UIs. Components +are the center of everything: they are composable like functions, +testable like JSON data, and provide lifecycle APIs for more complex +scenarios. + +A component can represent anything from a single item in a list to a +complete virtualized grid that is made up of sub-components. They can +be used to abstract out "behaviors" instead of UI elements (think of a +`Selectable` component). React's API makes it easy to break up your UI +into whatever abstractions you need. + +The core idea of a component is simple: it's something that takes +properties and returns a DOM-like structure. + +```js +function Item({ name, iconURL }) { + return div({ className: "item" }, + img({ className: "icon", href: iconURL }), + name); +} +``` + +The `div` and `span` functions refer to `React.DOM.div` and +`React.DOM.span`. React provides constructors for all DOM elements on +`React.DOM`. These conform to the standard API for creating elements: +the first argument takes properties, and the rest are children. + +You can see component composition kick in when using `Item`: + +```js +const Item = React.createFactory(require('./Item')); + +function List({ items }) { + return div({ className: "list" }, + items.map(item => Item({ name: item.name, icon: item.iconURL))); +} +``` + +You can use custom components exactly the same way you use native +ones! The only difference is we wrapped it in a factory when importing +instead of using the React.DOM functions. Factories are just a way of +turning a component into a convenient function. Without factories, you +need to do do `React.createElement(Item, { ... })`, which is exactly +the same as `Item({ ... })` if using a factory. + +## Rendering and Updating Components + +Now that we have some components, how do we render them? You use +`React.render` for that: + +```js +let items = [{ name: "Dubois", iconURL: "dubois.png" }, + { name: "Ivy", iconURL: "ivy.png" }]; + +React.render(List({ items: items }), + document.querySelector("#mount")); +``` + +This renders a `List` component, given `items`, to a DOM node with an +id of `mount`. Typically you have a top-level `App` component that is +the root of everything, and you would render it like so. + +What about updating? First, let's talk about data. The above +components take data from above and render out DOM structure. If any +user events were involved, the components would call callbacks passed +as props, so events walk back up the hierarchy. The conceptual model +is data goes down, and events come up. + +You usually want to change data in response to events, and rerender +the UI with the new data. What does that look like? There are two +places where React will rerender components: + +1\. Any additional `React.render` calls. Once a component is mounted, +you can call `React.render` again to the same place and React will see +that it's already mounted and perform an update instead of a full +render. For example, this code adds an item in response to an event +and updates the UI, and will perform optimal incremental updates: + +```js +function addItem(item) { + render([...items, item]); +} + +function render(items) { + React.render(List({ items: items, + onAddItem: addItem }), + document.querySelector("#mount")); +} + +render(items); +``` + +2\. Changing component local state. This is much more common. React +allows components to have local state, and whenever the state is +changed with the `setState` API it will rerender that specific +component. If you use component local state, you need to create a +component with `createClass`: + +```js +const App = React.createClass({ + getInitialState: function() { + return { items: [] }; + }, + + handleAddItem: function(item) { + const items = [...this.props.items, item]; + this.setState({ items: items }); + }, + + render: function() { + return List({ items: this.state.items, + onAddItem: this.handleAddItem }); + } +}); + ``` + +If you are using something like Redux to manage state this is handled +automatically for you with the library you use to bind Redux with +React. See more in [Redux](redux.md). + +## DOM Diffing + +What does it mean when React "updates" a component, and how does it +know which DOM to change? React achieves this with a technique called +DOM diffing. This alleviates the need for the programmer to worry +about how updates are actually applied to the DOM, and components can +render DOM structure declaratively in response to data. In the above +examples, when adding an item, React knows to only add a new DOM node +instead of recreating the whole list each time. + +DOM diffing is possible because our components return what's called +"virtual DOM": a lightweight JSON structure that React can use to diff +against previous versions, and generate minimal changes to the real DOM. + +This also makes it really east to test components with a real DOM: +just make sure the virtual DOM has what it should. + +## Next + +Read the [React Guidelines](react-guidelines.md) next to learn how to +write React code specifically for the devtools. diff --git a/devtools/docs/contributor/frontend/redux-guidelines.md b/devtools/docs/contributor/frontend/redux-guidelines.md new file mode 100644 index 0000000000..1782a6de47 --- /dev/null +++ b/devtools/docs/contributor/frontend/redux-guidelines.md @@ -0,0 +1,52 @@ +### Getting data from the store + +To get data from the store, use `connect()`. + +When using connect, you'll break up your component into two parts: + +1. The part that displays the data (presentational component) + + // todos.js + const Todos = React.createClass({ + propTypes: { + todos: PropTypes.array.isRequired + } + + render: function() {...} + }) + + module.exports = Todos; + +2. The part that gets the data from the store (container component) + + // todos-container.js + const Todos = require("path/to/todos"); + + function mapStateToProps(state) { + return { + todos: state.todos + }; + } + + module.exports = connect(mapStateToProps)(Todos); + + +`connect()` generates the container component. It wraps around the presentational component that was passed in (e.g. Todos). + +The `mapStateToProps` is often called a selector. That's because it selects data from the state object. When the container component is rendering, the the selector will be called. It will pick out the data that the presentational component is going to need. Redux will take this object and pass it in to the presentational component as props. + +With this setup, a presentational component is easy to share across different apps. It doesn't have any dependencies on the app, or any hardcoded expectations about how to get data. It just gets props that are passed to it and renders them. + +For more advanced use cases, you can pass additional parameters into the selector and `connect()` functions. Read about those in the [`connect()`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) docs. + +--- + +Need to answer the following questions: + +* How do I do I load asynchronous data? +* How do I do optimistic updates or respond to errors from async work? +* Do I use Immutable.js for my state? +* What file structure should I use? +* How do I test redux code? + +And more. diff --git a/devtools/docs/contributor/frontend/redux.md b/devtools/docs/contributor/frontend/redux.md new file mode 100644 index 0000000000..e090060f5d --- /dev/null +++ b/devtools/docs/contributor/frontend/redux.md @@ -0,0 +1,160 @@ + +We use [Redux](https://github.com/reactjs/redux) to manage application +state. The [docs](http://redux.js.org/) do a good job explaining the +concepts, so go read them. + +# Quick Intro + +Just like the [React introduction](react.md), this is a quick +introduction to redux, focusing on how it fits into React and why we +chose it. + +One of the core problems that React does not address is managing +state. In the React intro, we talked about data flowing down and +events flowing up. Conceptually this is nice, but you quickly run into +awkward situations in large apps. + +Let's look at an example. Say you have a page with a tabbed interface. +Here, `Tab1` is managing a list of items, so naturally it uses local +state. `Tab2` renders different stuff. + +```js +const Tab1 = React.createClass({ + getInitialState: function() { + return { items: [] }; + }, + + handleAddItem: function(item) { + this.setState({ items: [...this.state.items, item]}); + }, + + render: function() { + /* ... Renders the items and button to add new item ... */ + } +}); + +const Tab2 = React.createClass({ + render: function() { + /* ... Renders other data ... */ + } +}); + +// Assume `Tab1` and `Tab2` are wrapped with a factory when importing +const Tabs = React.createClass({ + render: function() { + return div( + { className: 'tabs' }, + // ... Render the tab buttons ... + Tab1(), + Tab2() + ); + } +}); +``` + +What happens when `Tab2` needs the list of items though? This scenario +comes up all time: components that aren't directly related need access +to the same state. A small change would be to move the `items` state +up to the `Tabs` component, and pass it down to both `Tab1` and `Tab2`. + +But now `Tabs` has to implement the `handleAddItem` method to add an +item because it's managing that state. This quickly gets ugly as the +end result is the root component ends up with a ton of state and +methods to manage it: a [god +component](https://en.wikipedia.org/wiki/God_object) is born. + +Additionally, how do we know what data each tab needs? We end up +passing *all* the state down because we don't know. This is not a +modular solution: one object managing the state and every component +receiving the entire state is like using tons of global variables. + +## Evolution of Flux + +Facebook addressed this with the +[flux](https://facebook.github.io/flux/) architecture, which takes the +state out of the components and into a "store". Redux is the latest +evolution of this idea and solves a lot of problems previous flux +libraries had (read it's documentation for more info). + +Because the state exists outside the component tree, any component can +read from it. Additionally, **state is updated with +[actions](http://redux.js.org/docs/basics/Actions.html)** that any +component can fire. We have [guidelines](redux-guidelines) for where +to read/write state, but it completely solves the problem described +above. Both `Tab1` and `Tab2` would be listening for changes in the +`item` state, and `Tab1` would fire actions to change it. + +With redux, **state is managed modularly with +[reducers](http://redux.js.org/docs/basics/Reducers.html)** but tied +together into a single object. This means a single JS object +represents most* of your state. It may sound crazy at first, but think +of it as an object with references to many pieces of state; that's all +it is. + +This makes it very easy to test, debug, and generally think about. You +can log your entire state to the console and inspect it. You can even +dump in old states and "replay" to see how the UI changed over time. + +I said "most*" because it's perfectly fine to use both component local +state and redux. Be aware that any debugging tools will not see local +state at all though. It should only be used for transient state; we'll +talk more about that in the guidelines. + +## Immutability + +Another important concept is immutability. In large apps, mutating +state makes it very hard to track what changed when. It's very easy to +run into situations where something changes out from under you, and +the UI is rendered with invalid data. + +Redux enforces the state to be updated immutably. That means you +always return new state. It doesn't mean you do a deep copy of the +state each time: when you need to change some part of the tree you +only need to create new objects to replace the ones your changing (and +walk up to the root to create a new root). Unchanged subtrees will +reference the same objects. + +This removes a whole class of errors, almost like Rust removing a +whole class of memory errors by enforcing ownership. + +## Order of Execution + +One of best things about React is that **rendering is synchronous**. That +means when you render a component, given some data, it will fully +render in the same tick. If you want the UI to change over time, you +have to change the *data* and rerender, instead of arbitrary UI +mutations. + +The reason this is desired is because if you build the UI around +promises or event emitters, updating the UI becomes very brittle +because anything can happen at any time. The state might be updated in +the middle of rendering it, maybe because you resolved a few promises +which made your rendering code run a few ticks later. + +Redux embraces the synchronous execution semantics as well. What this +means is that everything happens in a very controlled way. When +updating state through an action, all reducers are run and a new state +is synchronously generated. At that point, the new state is handed off +to React and synchronously rendered. + +Updating and rendering happen in two phases, so the UI will *always* +represent consistent state. The state can never be in the middle of +updating when rendering. + +What about asynchronous work? That's where +[middleware](http://redux.js.org/docs/advanced/Middleware.html) come +in. At this point you should probably go study our code, but +middleware allows you to dispatch special actions that indicate +asynchronous work. The middleware will catch these actions and do +something async, dispatching "raw" actions along the way (it's common +to emit a START, DONE, and ERROR action). + +**Ultimately there are 3 "phases" or level of abstraction**: the async +layer talks to the network and may dispatch actions, actions are +synchronously pumped through reducers to generate state, and state is +rendered with react. + +## Next + +Read the [Redux Guidelines](redux-guidelines.md) next to learn how to +write React code specifically for the devtools. diff --git a/devtools/docs/contributor/frontend/svgs.md b/devtools/docs/contributor/frontend/svgs.md new file mode 100644 index 0000000000..d0de19a2e1 --- /dev/null +++ b/devtools/docs/contributor/frontend/svgs.md @@ -0,0 +1,42 @@ +# Panel SVGs +These are the guidelines for creating devtools SVGs to make sure they're as small and neatly formatted as possible. The Mozilla Developer SVG guidelines can be found [here](https://developer.mozilla.org/en-US/docs/Web/SVG). + +## Explanation of Pixel Grid +Since so many of our SVGs appear so small, designing them on the pixel grid will help them not appear fuzzy when they're sized down to 16x16 pixels. There is program-specific documentation in both the [Illustrator](#illustrator) and [Sketch](#sketch) sections. + +## Panel Icon Requirements +The devtools panel icons do a couple of things in a specific way; following these guidelines will help stick your patch: + +1. **Inline fill colors.** Devtools panel icons all use ```fill="#0b0b0b"``` in the ```<svg>``` tag. +2. **Inline opacities.** Devtools panel icons also inline opacities on their relevant path. + +## Illustrator +For Illustrator you'll want the following document settings: + +- **Document settings**: ```Units: pixels```, ```Advanced``` > check ```Align New Objects to Pixel Grid``` +- **Transform Panel**: for existing artwork not on pixel grid, select shape and then within ```Transform``` > ```Advanced``` > check ```Align to Pixel Grid``` + +You can get a more detailed breakdown with images [here](http://medialoot.com/blog/3-valuable-pixel-perfect-illustrator-techniques/). + +You can download a sample Illustrator file [here](https://www.dropbox.com/home/Mozilla_MobileUX_Share/Internal%20Assets/Templates/Firefox?preview=pixel-grid-illustrator.ai). + +### Tips for Object Creation +When you're designing your icons in a graphics editor like Adobe Illustrator, there are a lot of things you can do that will bring down the size of the file and make your SVGs easier for the developers to work with. Here are some of them: + +- **Expand paths**: Instead of having multiple shapes overlapping each other, expand shapes using the pathfinder. +![Use pathfinder to expand shapes](../resources/pathfinder.gif) +- Simplify paths (```Object``` > ```Path``` > ```Simplify```) +- Expand objects so that strokes become objects. This has the added benefit of keeping the stroke size intact as the SVG is resized. +![Expand strokes to make them objects](../resources/expand-strokes.gif) + +## Sketch +Sketch vector work is a little different but the fundamentals (keeping your SVG small, expanding all paths) is the same. Here's what we've found helps to build clean icons: + +- **Build your icon at 16x16 with the Pixel Grid turned on.** You can turn the pixel grid on at ```View > Canvas > Show Pixels``` + +- **Make sure that all x/y coordinates are full pixels for lines/rectangles.** Sub-pixels = not on pixel grid. +![Position in the upper right hand corner of Sketch](../resources/sketch-position.png) + +- **Expand all your paths so strokes expand properly as the SVG gets resized.** You can do this at ```Layer > Paths > Vectorize Stroke```. + +- **Align anything that isn't boxy to the pixel grid with item selected then ```Layer > Round to Nearest Pixel Edge```.** diff --git a/devtools/docs/contributor/frontend/telemetry.md b/devtools/docs/contributor/frontend/telemetry.md new file mode 100644 index 0000000000..5636031f7a --- /dev/null +++ b/devtools/docs/contributor/frontend/telemetry.md @@ -0,0 +1,447 @@ +# Telemetry + +We use telemetry to get metrics of usage of the different features and panels in DevTools. This will help us take better, informed decisions when prioritising our work. + +## Adding metrics to a tool + +The process to add metrics to a tool roughly consists in: + +1. Adding the probe to Firefox +2. Using Histograms.json probes in DevTools code +3. Using Scalars.yaml probes in DevTools code +4. Using Events.yaml probes in DevTools code for analysis in Amplitude. +5. Getting approval from the data team + +### 1. Adding the probe to Firefox + +The first step involves creating entries for the probe in one of the files that contain declarations for all data that Firefox might report to Mozilla. + +These files are: + +- `toolkit/components/telemetry/Histograms.json` +- `toolkit/components/telemetry/Scalars.yaml` +- `toolkit/components/telemetry/Events.yaml` + +Scalars allow collection of simple values, like counts, booleans and strings and are to be used whenever possible instead of histograms. + +Histograms allow collection of multiple different values, but aggregate them into a number of buckets. Each bucket has a value range and a count of how many values we recorded. + +Events allow collection of a number of properties keyed to a category, method, object and value. Event telemetry helps us tell a story about how a user is interacting with the browser. + +Both scalars & histograms allow recording by keys. This allows for more flexible, two-level data collection. + +#### The different file formats + +The data team chose YAML for `Scalars.yaml` and `Events.yaml` because it is easy to write and provides a number of features not available in JSON including comments, extensible data types, relational anchors, strings without quotation marks, and mapping types preserving key order. + +While we previously used JSON for similar purposes in histograms.json, we have used YAML here because it allows for comments and is generally easier to write. + +The data team are considering moving the histograms over to YAML format at some point. + +If it's the first time you add one of these, it's advised to follow the style of existing entries. + +New data types have been added over the years, so it's quite feasible that some of our probes are not the most suitable nowadays. + +There's more information about types (and telemetry in general) on [this page](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/start/adding-a-new-probe.html) and [this other page](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/index.html). + +And of course, in case of doubt, ask! + +### Adding probes to `Histograms.json` + +Our entries are prefixed with `DEVTOOLS_`. For example: + +``` + "DEVTOOLS_DOM_OPENED_COUNT": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "count", + "bug_numbers": [1343501], + "description": "Number of times the DevTools DOM Inspector has been opened.", + "releaseChannelCollection": "opt-out" + }, + "DEVTOOLS_DOM_TIME_ACTIVE_SECONDS": { + "alert_emails": ["dev-developer-tools@lists.mozilla.org"], + "expires_in_version": "never", + "kind": "exponential", + "bug_numbers": [1343501], + "high": 10000000, + "n_buckets": 100, + "description": "How long has the DOM inspector been active (seconds)" + }, +``` + +There are different types of probes you can use. These are specified by the `kind` field. Normally we use `count` for counting how many times the tools are opened, and `exponential` for how many times a panel is active. + +### Adding probes to `Scalars.yaml` + +Our entries are prefixed with `devtools.`. For example: + +```yaml +devtools.toolbar.eyedropper: + opened: + bug_numbers: + - 1247985 + - 1352115 + description: Number of times the DevTools Eyedropper has been opened via the inspector toolbar. + expires: never + kind: uint + notification_emails: + - dev-developer-tools@lists.mozilla.org + release_channel_collection: opt-out + record_in_processes: + - 'main' + +devtools.copy.unique.css.selector: + opened: + bug_numbers: + - 1323700 + - 1352115 + description: Number of times the DevTools copy unique CSS selector has been used. + expires: "57" + kind: uint + notification_emails: + - dev-developer-tools@lists.mozilla.org + release_channel_collection: opt-out + record_in_processes: + - 'main' +``` + +### Adding probes to `Events.yaml` + +Our entries are prefixed with `devtools.`. For example: + +```yaml +devtools.main: + open: + objects: ["tools"] + bug_numbers: [1416024] + notification_emails: ["dev-developer-tools@lists.mozilla.org", "hkirschner@mozilla.com"] + record_in_processes: ["main"] + description: User opens devtools toolbox. + release_channel_collection: opt-out + expiry_version: never + extra_keys: + entrypoint: How was the toolbox opened? CommandLine, ContextMenu, HamburgerMenu, KeyShortcut, SessionRestore or SystemMenu + first_panel: The name of the first panel opened. + host: "Toolbox host (positioning): bottom, side, window or other." + splitconsole: Indicates whether the split console was open. + width: Toolbox width (px). +``` + +### 2. Using Histograms.json probes in DevTools code + +Once the probe has been declared in the `Histograms.json` file, you'll need to actually use it in our code. + +First, you need to give it an id in `devtools/client/shared/telemetry.js`. Similarly to the `Histograms.json` case, you'll want to follow the style of existing entries. For example: + +```js +dom: { + histogram: "DEVTOOLS_DOM_OPENED_COUNT", + timerHistogram: "DEVTOOLS_DOM_TIME_ACTIVE_SECONDS" +}, +``` + +... would correspond to the probes we declared in the previous section. + +Then, include that module on each tool that requires telemetry: + +```js +let Telemetry = require("devtools/client/shared/telemetry"); +``` + +Create a telemetry instance on the tool constructor: + +```js +this._telemetry = new Telemetry({ useSessionId: true }); +``` +`useSessionId` allows to aggregate all records behind a randomly unique "session_id" +extra attribute. For example, this helps aggregate all data recorded for one precise +toolbox instance. + +And use the instance to report e.g. tool opening... + +```js +this._telemetry.toolOpened("mytoolname", this); +``` + +... or closing: + +```js +this._telemetry.toolClosed("mytoolname", this); +``` + +Note that `mytoolname` is the id we declared in the `telemetry.js` module. + +### 3. Using Scalars.yaml probes in DevTools code + +Once the probe has been declared in the `Scalars.yaml` file, you'll need to actually use it in our code. + +First, you need to give it an id in `devtools/client/shared/telemetry.js`. You will want to follow the style of existing lowercase histogram entries. For example: + +```js +toolbareyedropper: { + scalar: "devtools.toolbar.eyedropper.opened", // Note that the scalar is lowercase +}, +copyuniquecssselector: { + scalar: "devtools.copy.unique.css.selector.opened", +}, +``` + +... would correspond to the probes we declared in the previous section. + +Then, include that module on each tool that requires telemetry: + +```js +let Telemetry = require("devtools/client/shared/telemetry"); +``` + +Create a telemetry instance on the tool constructor: + +```js +this._telemetry = new Telemetry(); +``` + +And use the instance to report e.g. tool opening... + +```js +this._telemetry.toolOpened("mytoolname", this); +``` + +Notes: + +- `mytoolname` is the id we declared in the `Scalars.yaml` module. +- Because we are not logging tool's time opened in `Scalars.yaml` we don't care + about toolClosed. Of course, if there was an accompanying `timerHistogram` + field defined in `telemetry.js` and `histograms.json` then `toolClosed` should + also be added. + +### 4. Using Events.yaml probes in DevTools code + +Once the probe has been declared in the `Events.yaml` file, you'll need to actually use it in our code. + +It is crucial to understand that event telemetry have a string identifier which is constructed from the `category`, `method`, `object` (name) and `value` on which the event occurred. This key points to an "extra" object that contains further information about the event (we will give examples later in this section). + +Because these "extra" objects can be from completely independent code paths we +can send events and leave them in a pending state until all of the expected extra properties have been received. + +First, include the telemetry module in each tool that requires telemetry: + +```js +let Telemetry = require("devtools/client/shared/telemetry"); +``` + +Create a telemetry instance on the tool constructor: + +```js +this._telemetry = new Telemetry(); +``` + +And use the instance to report e.g. tool opening... + +```js +// Event telemetry is disabled by default so enable it for your category. +this._telemetry.setEventRecordingEnabled(true); + +// If you already have all the properties for the event you can send the +// telemetry event using: +// this._telemetry.recordEvent(method, object, value, extra) e.g. +this._telemetry.recordEvent("open", "tools", null, { + "entrypoint": "ContextMenu", + "first_panel": "Inspector", + "host": "bottom", + "splitconsole": false, + "width": 1024, +}); + +// If your "extra" properties are in different code paths you will need to +// create a "pending event." These events contain a list of expected properties +// that can be populated before or after creating the pending event. + +// Use the category, method, object, value combinations above to add a +// property... we do this before creating the pending event simply to +// demonstrate that properties can be sent before the pending event is created. +this._telemetry.addEventProperty( + this, "open", "tools", null, "entrypoint", "ContextMenu"); + +// In this example `"open", "tools", null` make up the +// signature of the event and needs to be sent with all properties. + +// Create the pending event using +// this._telemetry.preparePendingEvent(this, method, object, value, +// expectedPropertyNames) e.g. +this._telemetry.preparePendingEvent(this, "open", "tools", null, + ["entrypoint", "first_panel", "host", "splitconsole", "width", "session_id"] +); + +// Use the category, method, object, value combinations above to add each +// property. +this._telemetry.addEventProperty( + this, "open", "tools", null, "first_panel", "inspector"); +this._telemetry.addEventProperty( + this, "open", "tools", null, "host", "bottom"); +this._telemetry.addEventProperty( + this, "open", "tools", null, "splitconsole", false); +this._telemetry.addEventProperty( + this, "open", "tools", null, "width", 1024); + +// You can also add properties in batches using e.g.: +this._telemetry.addEventProperties(this, "open", "tools", null, { + "first_panel": "inspector", + "host": "bottom", + "splitconsole": false, + "width": 1024 +}); + +``` + +Notes: + +- `mytoolname` is the id we declared in the `Scalars.yaml` module. +- Because we are not logging tool's time opened in `Scalars.yaml` we don't care + about toolClosed. Of course, if there was an accompanying `timerHistogram` + field defined in `telemetry.js` and `histograms.json` then `toolClosed` should + also be added. + +#### Note on top level panels + +The code for the tabs uses their ids to automatically report telemetry when you switch between panels, so you don't need to explicitly call `toolOpened` and `toolClosed` on top level panels. + +You will still need to call those functions on subpanels, or tools such as `about:debugging` which are not opened as tabs. + +#### Testing + +The telemetry module will print warnings to stdout if there are missing ids. It is strongly advisable to ensure this is working correctly, as the module will attribute usage for undeclared ids to a generic `custom` bucket. This is not good for accurate results! + +To see these warnings, you need to have the `browser.dom.window.dump.enabled` browser preference set to `true` in `about:config` (and restart the browser). + +Then, try doing things that trigger telemetry calls (e.g. opening a tool). Imagine we had a typo when reporting the tool was opened: + +```js +this._telemetry.toolOpened('mytoolnmae', this); + ^^^^ typo, should be *mytoolname* +``` + +Would report an error to stdout: + +```text +Warning: An attempt was made to write to the mytoolnmae histogram, which is not defined in Histograms.json +``` + +So watch out for errors. + +#### Testing Event Telemetry + +This is best shown via an example: + +```js +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Toolbox } = require("devtools/client/framework/toolbox"); +const { TelemetryTestUtils } = ChromeUtils.importESModule("resource://testing-common/TelemetryTestUtils.sys.mjs"); + +const URL = "data:text/html;charset=utf8,browser_toolbox_telemetry_close.js"; +const { RIGHT, BOTTOM } = Toolbox.HostType; +const DATA = [ + { + category: "devtools.main", + method: "close", + object: "tools", + value: null, + extra: { + host: "right", + width: w => w > 0, + } + }, + { + category: "devtools.main", + method: "close", + object: "tools", + value: null, + extra: { + host: "bottom", + width: w => w > 0, + } + } +]; + +add_task(async function() { + // Let's reset the counts. + Services.telemetry.clearEvents(); + + // Ensure no events have been logged + TelemetryTestUtils.assertNumberOfEvents(0); + + await openAndCloseToolbox("webconsole", SIDE); + await openAndCloseToolbox("webconsole", BOTTOM); + + checkResults(); +}); + +async function openAndCloseToolbox(toolId, host) { + const tab = await addTab(URL); + const toolbox = await gDevTools.showToolboxForTab(tab, { toolId }); + + await toolbox.switchHost(host); + await toolbox.destroy(); +} + +function checkResults() { + TelemetryTestUtils.assertEvents(DATA, {category: "devtools.main", method: "close", object: "tools"}); +} +``` + +#### Compile it + +You need to do a full Firefox build if you have edited either `Histograms.json` or `Events.yaml`, as they are processed at build time, and various checks will be run on them to guarantee they are valid. + +```bash +./mach build +``` + +If you use `mach build faster` or artifact builds, the checks will not be performed, and your try builds will fail ("bust") when the checks are run there. + +Save yourself some time and run the checks locally. + +NOTE: Changes to `Scalars.yaml` *are* processed when doing an artifact build. + +### 4. Getting approval from the data team + +This is required before the changes make their way into `mozilla-central`. + +To get approval, attach your patch to the bug in Bugzilla, and set two flags: + +- a `review?` flag for a data steward. +- a `needinfo?` flag to hkirschner (our product manager, so he vouches that we're using the data) + +Be sure to explain very clearly what is the new probe for. E.g. "We're seeking approval for tracking opens of a new panel for debugging Web API ABCD" is much better than just asking for feedback without background info. + +This review shouldn't take too long: if there's something wrong, they should tell you what to fix. If you see no signs of activity after a few days, you can ask in `#developers`. + +Note that this review is *in addition* to normal colleague reviews. + +Click [here](https://wiki.mozilla.org/Firefox/Data_Collection#Requesting_Data_Collection) for more details. + +## Accessing existing data + +### Local data + +Go to `about:telemetry` to see stats relating to your local instance. + +### Global data + +Data aggregated from large groups of Firefox users is available at [telemetry.mozilla.org](https://telemetry.mozilla.org). + +Reports are written with SQL. For example, here's one comparing [usage of some DevTools panels](https://sql.telemetry.mozilla.org/queries/1000#table). + +If you want to get better understanding of how people are using the tools, you are encouraged to explore this data set by writing your own reports. + +The easiest way to get started is to *fork* an existing report and modify it to get used to the syntax, as SQL for massive data tables is very different from SQL for a humble blog engine, and you'll find some new operators that might look unfamiliar. + +It's also recommended to take small steps and run the queries often to detect errors before they're too complicated to solve, particularly if you're not experienced with this (yet). + +Slow queries will be interrupted by the system, so don't worry about "fetching too much data" or "using too many resources". There's built-in protection to avoid your code eating up the Telemetry database. + +Funnily, if you're based in Europe, you might be in luck, as the website tends to be more responsive during European working hours than it is at Pacific working hours, as seemingly there's less people in Europe interacting with it. |