UNPKG

boomerangjs

Version:

boomerang always comes back, except when it hits something

1,328 lines (1,250 loc) 139 kB
/** * The Continuity plugin measures performance and user experience metrics beyond * just the traditional Page Load timings. * * This plugin has a corresponding {@tutorial header-snippets} that helps measure events prior to Boomerang loading. * * ## Approach * * The goal of the Continuity plugin is to capture the important aspects of your * visitor's overall _user experience_ during page load and beyond. For example, the * plugin measures when the site appeared _Visually Ready_, and when it was _Interactive_. * * In addition, the Continuity plugin captures in-page interactions (such as keys, * clicks and scrolls), and monitors how the site performed when responding to * these inputs. * * Finally, the Continuity plugin is utilizing cutting-edge browser * performance APIs like [LongTasks](https://w3c.github.io/longtasks/) to get * important insights into how the browser is performing. * * Here are some of the metrics that the Continuity plugin captures: * * * Timers: * * **Time to Visually Ready**: When did the user feel like they could interact * with the site? When did it look ready? (see below for details) * * **Time to Interactive**: After the page was Visually Ready, when was the * first time the user could have interacted with the site, and had a * good (performant) experience? (see below for details) * * **Time to First Interaction**: When was the first time the user tried to * interact (key, click or scroll) with the site? * * **First Input Delay**: For the first interaction on the page, how * responsive was it? * * Interaction metrics: * * **Interactions**: Keys, mouse movements, clicks, and scrolls (counts and * an event log) * * **Delayed Interactions**: How often was the user's interaction delayed * more than 50ms? * * **Rage Clicks**: Did the user repeatedly clicked on the same element/region? * * Page performance metrics: * * **Frame Rate data**: FPS during page load, minimum FPS, number of long frames * * **Long Task data**: Number of Long Tasks, how much time they took, attribution * to what caused them * * **Page Busy**: Measurement of the page's busyness * * This data is captured during the page load, as well as when the user later * interacts with the site (if configured via * {@link BOOMR.plugins.Continuity.init `afterOnload`}). * These metrics are reported at regular intervals, so you can see how they * change over time. * * If configured, the Continuity plugin can send additional beacons after a page * interaction happens (via {@link BOOMR.plugins.Continuity.init `monitorInteractions`}). * * ## Configuration * * The `Continuity` plugin has a variety of options to configure what it does (and * what it doesn't do): * * ### Monitoring Long Tasks * * If {@link BOOMR.plugins.Continuity.init `monitorLongTasks`} is turned on, * the Continuity plugin will monitor [Long Tasks](https://w3c.github.io/longtasks/) * (if the browser supports it). * * Long Tasks represent work being done on the browser's UI thread that monopolize * the UI thread and block other critical tasks from being executed (such as reacting * to user input). Long Tasks can be caused by anything from JavaScript * execution, to parsing, to layout. The browser fires `LongTask` events * (via the `PerformanceObserver`) when a task takes over 50 milliseconds to execute. * * Long Tasks are important to measure as a Long Task will block all other user input * (e.g. clicks, keys and scrolls). * * Long Tasks are powerful because they can give _attribution_ about what component * caused the task, i.e. the source JavaScript file. * * If {@link BOOMR.plugins.Continuity.init `monitorLongTasks`} is enabled: * * * A `PerformanceObserver` will be turned on to capture all Long Tasks that happen * on the page. * * Long Tasks will be used to calculate _Time to Interactive_ * * A log (`c.lt`), timeline (`c.t.longtask`) and other Long Task metrics (`c.lt.*`) will * be added to the beacon (see Beacon Parameters details below) * * The log `c.lt` is a JSON (or JSURL) object of compressed `LongTask` data. See * the source code for what each attribute maps to. * * Long Tasks are currently a cutting-edge browser feature and will not be available * in older browsers. * * Enabling Long Tasks should not have a performance impact on the page load experience, * as collecting of the tasks are via the lightweight `PerformanceObserver` interface. * * ### Monitoring Page Busy * * If {@link BOOMR.plugins.Continuity.init `monitorPageBusy`} is turned on, * the Continuity plugin will measure Page Busy. * * Page Busy is a way of measuring how much work was being done on the page (how "busy" * it was). Page Busy is calculated via `setInterval()` polling: a timeout is scheduled * on the page at a regular interval, and _busyness_ is detected if that timeout does * not fire at the time it was expected to. * * Page Busy is a percentage -- 100% means that the browser was entirely busy doing other * things, while 0% means the browser was idle. * * Page Busy is _just an estimate_, as it uses sampling. As an example, if you have * a high number of small tasks that execute frequently, Page Busy might run at * a frequency that it either detects 100% (busy) or 0% (idle). * * Page Busy is not the most efficient way of measuring what the browser is doing, * but since it is calculated via `setInterval()`, it is supported in all browsers. * The Continuity plugin currently measures Page Busy by polling every 32 milliseconds. * Page Busy is disabled if Long Tasks are supported in the browser. * * Page Busy can be an indicator of how likely the user will have a good experience * when they interact with it. If Page Busy is 100%, the user may see the page lag * behind their input. * * If {@link BOOMR.plugins.Continuity.init `monitorPageBusy`} is enabled: * * * The Page Busy monitor will be active (polling every 32 milliseconds) (unless * Long Tasks is supported and enabled) * * Page Busy will be used to calculate _Time to Interactive_ * * A timeline (`c.t.busy`) and the overall Page Busy % (`c.b`) will be added to the * beacon (see Beacon Parameters details below) * * Enabling Page Busy monitoring should not have a noticeable effect on the page load * experience. The 32-millisecond polling is lightweight and should barely register * on JavaScript CPU profiles. * * Page Busy is disabled in Firefox, as that browser * [de-prioritizes](https://bugzilla.mozilla.org/show_bug.cgi?id=1270059) `setInterval()` during page load. * * ### Monitoring Frame Rate * * If {@link BOOMR.plugins.Continuity.init `monitorFrameRate`} is turned on, * the Continuity plugin will measure the Frame Rate of the page via * [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame). * * `requestAnimationFrame` is a browser API that can be used to schedule animations * that run at the device's refresh rate. It can also be used to measure how many * frames were actually delivered to the screen, which can be an indicator of how * good the user's experience is. * * `requestAnimationFrame` is available in * [all modern browsers](https://caniuse.com/#feat=requestanimationframe). * * If {@link BOOMR.plugins.Continuity.init `monitorFrameRate`} is enabled: * * * `requestAnimationFrame` will be used to measure Frame Rate * * Frame Rate will be used to calculate _Time to Interactive_ * * A timeline (`c.t.fps`) and many Frame Rate metrics (`c.f.*`) will be added to the * beacon (see Beacon Parameters details below) * * Enabling Frame Rate monitoring should not have a noticeable effect on the page load * experience. The frame callback may happen up to the device's refresh rate (which * is often 60 FPS), and the work done in the callback should be barely visible * in JavaScript CPU profiles (often less than 5ms over a page load). * * ### Monitoring Interactions * * If {@link BOOMR.plugins.Continuity.init `monitorInteractions`} is turned on, * the Continuity plugin will measure user interactions during the page load and beyond. * * Interactions include: * * * Mouse Clicks: Where the user clicked on the screen * * Rage Clicks: Clicks to the same area repeatedly * * Mouse Movement: Rough mouse movement will be tracked, but these interactions will * not send a beacon on their own, nor be used for _Time to First Interaction_ * calculations. * * Keyboard Presses: Individual key codes are not captured * * Scrolls: How frequently and far the user scrolled * * Distinct Scrolls: Scrolls that happened over 2 seconds since the last scroll * * Page Visibility changes * * Orientation changes * * Pointer Down and Up, Mouse Down and Touch Start: * Timestamp of these events is used to track and calculate interaction metrics * like _First Input Delay_ * * These interactions are monitored and instrumented throughout the page load. By using * the event's `timeStamp`, we can detect how long it took for the physical event (e.g. * mouse click) to execute the JavaScript listening handler (in the Continuity plugin). * If there is a delay, this is tracked as an _Interaction Delay_. Interaction Delays * can be an indicator that the user is having a degraded experience. * * The very first interaction delay will be added to the beacon as the * _First Input Delay_ - this is tracked as the user's first experience * with your site is important. * * In addition, if {@link BOOMR.plugins.Continuity.init `afterOnLoad`} is enabled, * these interactions (except Mouse Movements) can also trigger an `interaction` * beacon after the Page Load. {@link BOOMR.plugins.Continuity.init `afterOnLoadMaxLength`} * can be used to control how many milliseconds after Page Load interactions will be * measured for. * * After a post-Load interaction occurs, the plugin will wait for * {@link BOOMR.plugins.Continuity.init `afterOnLoadMinWait`} milliseconds before * sending the `interaction` beacon. If another interaction happens within that * timeframe, the plugin will wait another {@link BOOMR.plugins.Continuity.init `afterOnLoadMinWait`} * milliseconds. This is to ensure that groups of interactions will be batched * together. The plugin will wait up to 60 seconds to batch groups of interactions * together, at which point a beacon will be sent immediately. * * If {@link BOOMR.plugins.Continuity.init `monitorInteractions`} is enabled: * * * Passive event handlers will be added to monitor clicks, keys, etc. * * A log and many interaction metrics (`c.i.*`, `c.ttfi`) will be added to the * beacon (see Beacon Parameters details below) * * For `interaction` beacons, the following will be set: * * * `rt.tstart` will be the timestamp of the first interaction * * `rt.end` will be the timestamp of the last interaction * * `rt.start = 'manual'` * * `http.initiator = 'interaction'` * * Enabling interaction monitoring will add lightweight passive event handlers * to `scroll`, `click`, `mousemove` and `keydown` events. These event handlers * should not delay the user's interaction, and are used to measure delays and * keep a log of interaction events. * * ### Monitoring Page Statistics * * If {@link BOOMR.plugins.Continuity.init `monitorStats`} is turned on, * the Continuity plugin will measure statistics about the page and browser over time. * * These statistics include: * * * Memory Usage: `usedJSHeapSize` (Chrome-only) * * [Battery Level](https://developer.mozilla.org/en-US/docs/Web/API/Battery_Status_API) * * DOM Size: Number of bytes of HTML in the root frame * * DOM Length: Number of DOM nodes in the root frame * * Mutations: How often and how much the page is changing * * If {@link BOOMR.plugins.Continuity.init `monitorStats`} is enabled: * * * Events and polls will be setup to monitor the above statistics * * A timeline (`c.t.*`) of these statistics will be added to the beacon (see * details below) * * Enabling Page Statistic monitoring adds a poll to the page every second, gathering * the above statistics. These statistics should take less than 5ms JavaScript CPU * on a desktop browser each poll, but this monitoring is probably the most * expensive of the Continuity plugin monitors. * * This option is off by default, and can be turned on via the * {@link BOOMR.plugins.Continuity.init `monitorStats`} config option. * * ### Monitoring Layout Shifts * * If {@link BOOMR.plugins.Continuity.init `monitorLayoutShifts`} is turned on, * the Continuity plugin will measure visual instability via the * [Layout Instability API](https://github.com/WICG/layout-instability), and will calculate the Cumulative * Layout Shift (CLS) score. * * The Cumulative Layout Shift (CLS) score approximates the severity of visual layout changes by monitoring * how DOM nodes shift during the user experience. A CLS of `0` indicates a stable view where no DOM nodes shifted. * Each time an unexpected layout shifts occur, the CLS increases. CLS is represented in decimal form, with a * value of `0.1` indicating a fraction of the screen's elements were affected. CLS values can be larger than * `1.0` if the layout shifts multiple times. * * See [web.dev/cls](https://web.dev/cls/) for a more detailed explanation. * * CLS is included on the beacon as `c.cls`, and resets each beacon, so represents the CLS since the last beacon. * * In addition to the CLS, this plugin captures data about each of the contributing layout shifts of the CLS, * including the layout shift value, start time, and sources, within `c.cls.d` on the beacon. * * The top layout shift value is included on the beacon as `c.cls.tops`, and the corresponding Pseudo-CSS selector * of the first source of the top layout shift is included on the beacon as `c.cls.topid`. * * Like `c.cls`, `c.cls.tops`, `c.cls.d`, and `c.cls.topid` reset each beacon. `c.cls.tops`, `c.cls.d`, and * `c.cls.topid` also undergo compression and jsUrl serialization before being added to the beacon. * * This option is on by default, and can be disabled via the * {@link BOOMR.plugins.Continuity.init `monitorLayoutShifts`} config option. * * ## New Timers * * There are 4 new timers from the Continuity plugin that center around user * interactions: * * * **Time to Visually Ready** (VR) * * **Time to Interactive** (TTI) * * **Time to First Interaction** (TTFI) * * **First Input Delay** (FID) * * _Time to Interactive_ (TTI), at it's core, is a measurement (timestamp) of when the * page was interact-able. In other words, at what point does the user both believe * the page could be interacted with, and if they happened to try to interact with * it then, would they have a good experience? * * To calculate Time to Interactive, we need to figure out two things: * * * Does the page appear to the visitor to be interactable? * * We'll use one or more Visually Ready Signals to determine this * * If so, what's the first time a user could interact with the page and have a good * experience? * * We'll use several Time to Interactive Signals to determine this * * ### Visually Ready * * For the first question, "does the page appear to be interactable?", we need to * determine when the page would _look_ to the user like they _could_ interact with it. * * It's only after this point that TTI could happen. Think of Visually Ready (VR) as * the anchor point of TTI -- it's the earliest possible timestamp in the page's * lifecycle that TTI could happen. * * We have a few signals that might be appropriate to use as Visually Ready: * * First Paint (if available) * * We should wait at least for the first paint on the page * * i.e. IE's [`msFirstPaint`](https://msdn.microsoft.com/en-us/library/ff974719) * or Chrome's `firstPaintTime` * * These might just be paints of white, so they're not the only signal we should use * * First Contentful Paint (if available) * * Via [PaintTiming](https://www.w3.org/TR/paint-timing/) * * Largest Contentful Paint (if available) * * Via [Largest Contentful Paint API](https://wicg.github.io/largest-contentful-paint/) * * [domContentLoadedEventEnd](https://msdn.microsoft.com/en-us/library/ff974719) * * "The DOMContentLoaded event is fired when the initial HTML document has been * completely loaded and parsed, without waiting for stylesheets, images, * and subframes to finish loading" * * This happens after `domInteractive` * * Available in NavigationTiming browsers via a timestamp and all other * browser if we're on the page in time to listen for readyState change events * * Hero Images (if defined) * * Instead of tracking all Above-the-Fold images, it could be useful to know * which specific images are important to the site owner * * Defined via a simple CSS selector (e.g. `.hero-images`) * * Can be measured via ResourceTiming * * Will add Hero Images Ready `c.tti.hi` to the beacon * * "My Framework is Ready" (if defined) * * A catch-all for other things that we can't automatically track * * This would be an event or callback from the page author saying their page is ready * * They could fire this for whatever is important to them, i.e. when their page's * click handlers have all registered * * Will add Framework Ready `c.tti.fr` to the beacon * * Once the last of all of the above have happened, Visually Ready has occurred. * * Visually Ready will add `c.tti.vr` to the beacon. * * Visually Ready is only included on regular Page Load and Single Page App Hard navigation beacons. It is not * suitable for Single Page App Soft navigation beacons as the page has already been visually ready at the start * of the soft navigation. * * #### Controlling Visually Ready via Framework Ready * * There are two additional options for controlling when Visually Ready happens: * via Framework Ready or Hero Images. * * If you want to wait for your framework to be ready (e.g. your SPA has loaded or * a button has a click handler registered), you can add an * option {@link BOOMR.plugins.Continuity.init `ttiWaitForFrameworkReady`}. * * Once enabled, TTI won't be calculated until the following is called: * * ``` * // my framework is ready * if (BOOMR && BOOMR.plugins && BOOMR.plugins.Continuity) { * BOOMR.plugins.Continuity.frameworkReady(); * } * ``` * * #### Controlling Visually Ready via Hero Images * * If you want to wait for your hero/main images to be loaded before Visually Ready * is measured, you can give the plugin a CSS selector via * {@link BOOMR.plugins.Continuity.init `ttiWaitForHeroImages`}. * If set, Visually Ready will be delayed until all IMGs that match that selector * have loaded, e.g.: * * ``` * BOOMR.init({ * ... * Continuity: { * enabled: true, * ttiWaitForHeroImages: ".hero-image" * } * }); * ``` * * Note this only works in ResourceTiming-supported browsers (and won't be used in * older browsers). * * If no images match the CSS selector at Page Load, this setting will be ignored * (the plugin will not wait for a match). * * ### Time to Interactive * * After the page is Visually Ready for the user, if they were to try to interact * with the page (click, scroll, type), when would they have a good experience (i.e. * the page responded in a satisfactory amount of time)? * * We can use some of the signals below, when available: * * * Frame Rate (FPS) * * Available in all modern browsers: by using `requestAnimationFrame` we can * get a sense of the overall frame rate (FPS) * * To ensure a "smooth" page load experience, ideally the page should never drop * below 20 FPS. * * 20 FPS gives about 50ms of activity to block the main thread at any one time * * Long Tasks * * Via the PerformanceObserver, a Long Tasks fires any time the main thread * was blocked by a task that took over 50ms such as JavaScript, layout, etc * * Great indicator both that the page would not have been interact-able and * in some cases, attribution as to why * * Page Busy via `setInterval` * * By measuring how long it takes for a regularly-scheduled callback to fire, * we can detect other tasks that got in the way * * Can give an estimate for Page Busy Percentage (%) * * Available in every browser * * Delayed interactions * * If the user interacted with the page and there was a delay in responding * to the input * * The {@link BOOMR.plugins.Continuity.init `waitAfterOnload`} option will delay * the beacon for up to that many milliseconds if Time to Interactive doesn't * happen by the browser's `load` event. You shouldn't set it too high, or * the likelihood that the page load beacon will be lost increases (because of * the user navigating away first, or closing their browser). If * {@link BOOMR.plugins.Continuity.init `waitAfterOnload`} is reached and TTI * hasn't happened yet, the beacon will be sent immediately (missing the TTI timer). * * If you set {@link BOOMR.plugins.Continuity.init `waitAfterOnload`} to `0` * (or it's not set), Boomerang will send the beacon at the regular page load * event. If TTI didn't yet happen, it won't be reported. * * If you want to set {@link BOOMR.plugins.Continuity.init `waitAfterOnload`}, * we'd recommend a value between `1000` and `5000` (1 and 5 seconds). * * Time to Interaction will add `c.tti` to the beacon. It will also add `c.tti.m`, * which is the higest-accuracy method available for TTI calculation: `lt` (Long Tasks), * `raf` (FPS), or `b` (Page Busy). * * Time to Interaction is only included on regular Page Load and Single Page App Hard navigation beacons. It is not * suitable for Single Page App Soft navigation beacons as the page is already interactive at the start of * the soft navigation. * * Note: TTI isn't as reliable of a metric on Firefox, as it does not yet support * Long Tasks (as of 2022), and has bugs with Page Busy monitoring (it intentionally delays setTimeouts * during Page Load), so only Frame Rate monitoring is available. * * #### Algorithm * * Putting these two timers together, here's how we measure Visually Ready and * Time to Interactive: * * 1. Determine the highest Visually Ready timestamp (VRTS): * * Largest Contentful Paint (if available) * * First Contentful Paint (if available) * * First Paint (if available) * * `domContentLoadedEventEnd` * * Hero Images are loaded (if configured) * * Framework Ready (if configured) * * 2. After VRTS, calculate Time to Interactive by finding the first period of * 500ms where all of the following are true: * * There were no Long Tasks * * The FPS was always above 20 (if available) * * Page Busy was less than 10% (if the above aren't available) * * ### Time to First Interaction * * Time to First Interaction (TTFI) is the first time a user interacted with the * page. This may happen during or after the page's `load` event. * * The events that are tracked are: * * Mouse Clicks * * Keyboard Presses * * Scrolls * * Page Visibility changes * * Orientation changes * * Time to First Interaction is not affected by Mouse Movement. * * Time to First Interaction will add `c.ttfi` to the beacon. * * If the user does not interact with the page by the beacon, there will be no * `c.ttfi` on the beacon. * * ### First Input Delay * * If the user interacted with the page by the time the beacon was sent, the * Continuity plugin will also measure how long it took for the JavaScript * event handler to fire. This measurement is referred to as the First Input Delay (FID). * * This can give you an indication of the page being otherwise busy and unresponsive * to the user if the callback is delayed. * * This time (measured in milliseconds) is added to the beacon as `c.fid`. * * If the {@link BOOMR.plugins.EventTiming `EventTiming`} plugin is included, * this measurement is deferred to the First Input Delay calculated by that plugin. * EventTiming is the most accurate way of measuring First Input Delay. * * If the {@link BOOMR.plugins.EventTiming `EventTiming`} plugin is not included, or the * browser does not support the EventTiming API, this plugin contains a polyfill to measure * FID based on the browser's interaction events: `click`, `mousedown`, `keydown`, `touchstart`, * `pointerdown` followed by `pointerup`. Measuring FID via a polyfill is less accurate than * via EventTiming, and will generally result in higher FID values than EventTiming-based FID. * * Note: FID measurements are only gathered up to the point of the first Page Load beacon. This * may differ from the Chrome User Experience (CrUX) FID measurements, which are taken up to the * point the page is unloaded. This means boomerang's FID measurements are biased towards interactions * that happened up to the Page Load event (which is likely a busy time for the browser). It's likely * that boomerang-based FID measurements will be higher than CrUX-based FID measurements as a result. * * ## Timelines * * If {@link BOOMR.plugins.Continuity.init `sendTimeline`} is enabled, many of * the above options will add bucketed "timelines" to the beacon. * * The Continuity plugin keeps track of statistics, interactions and metrics over time * by keeping track of these counts at a granularity of 100-millisecond intervals. * * As an example, if you are measuring Long Tasks, its timeline will have entries * whenever a Long Task occurs. * * Not every timeline will have data for every interval. As an example, the click * timeline will be sparse except for the periods where there was a click. Statistics * like DOM Size are captured only once every second. The Continuity plugin is * optimized to use as little memory as possible for these cases. * * ### Compressed Timeline Format * * If {@link BOOMR.plugins.Continuity.init `sendTimeline`} is enabled, the * Continuity plugin will add several timelines as `c.t.[name]` to the beacon * in a compressed format. * * An example timeline may look like this: * * ``` * c.t.fps = 03*a*657576576566766507575*8*65 * c.t.domsz = 11o3,1o4 * c.t.mousepct = 2*5*0053*4*00050718 * ``` * * The format of the compressed timeline is as follows: * * `[Compression Type - 1 character][Data - everything else]` * * * Compression Type is a single character that denotes how each timeline's bucket * numbers are compressed: * * `0` (for smaller numbers): * * Each number takes a single character, encoded in Base-64 * * If a number is >= 64, the number is converted to Base-36 and wrapped in * `.` characters * * `1` (for larger numbers) * * Each number is separated by `,`s * * Each number is encoded in Base-36 * * `2` (for percentages) * * Each number takes two characters, encoded in Base-10 * * If a number is <= 0, it is `00` * * If a number is >= 100, it is `__` * * In addition, for repeated numbers, the format is as follows: * * `*[Repeat Count]*[Number]` * * Where: * * * Repeat Count is encoded Base-36 * * Number is encoded per the rules above * * From the above example, the data would be decompressed to: * * ``` * c.t.fps = * [3, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 7, 5, 7, 6, 5, 7, 6, 5, 6, 6, 7, 6, 6, * 5, 0, 7, 5, 7, 5, 6, 6, 6, 6, 6, 6, 6, 6, 5]; * * c.t.domsz = [2163, 2164]; * * c.t.mousepct = [0, 0, 0, 0, 0, 53, 0, 0, 0, 0, 5, 7, 18]; * ``` * * The timeline can be decompressed via * {@link BOOMR.plugins.Continuity.decompressBucketLog `decompressBucketLog`} * (for debug builds). * * The Continuity Epoch (`c.e`) and Continuity Last Beacon (`c.lb`) are timestamps * (Base-36) that indicate what timestamp the first bucket represents. If both are * given, the Last Beacon timestamp should be used. * * For example: * * ``` * c.e=j5twmlbv // 1501611350395 * c.lb=j5twmlyk // 1501611351212 * c.t.domsz=11o3,1o4 // 2163, 2164 using method 1 * ``` * * In the above example, the first value of `2163` (`1o3` Base-36) happened * at `1501611351212`. The second value of `2164` (`1o4` Base-36) happened * at `1501611351212 + 100 = 1501611351312`. * * For all of the available timelines, see the Beacon Parameters list below. * * ## Logs * * If {@link BOOMR.plugins.Continuity.init `sendLog`} is enabled, the Continuity * plugin will add a log to the beacon as `c.l`. * * The following events will generate a Log entry with the listed parameters: * * * Scrolls (type `0`): * * `y`: Y pixels * * Clicks (type `1`): * * `x`: X pixels * * `y`: Y pixels * * Mouse Movement (type `2`): * * Data is captured at minimum 10 pixel granularity * * `x`: X pixels * * `y`: Y pixels * * Keyboard presses (type `3`): * * (no data is captured) * * Visibility Changes (type `4`): * * `s` * * `0`: `visible` * * `1`: `hidden` * * `2`: `prerender` * * `3`: `unloaded` * * Orientation Changes (type `5`): * * `a`: Angle * * The log is put on the beacon in a compressed format. Here is an example log: * * ``` * c.l=214y,xk9,y8p|142c,xk5,y8v|34kh * ``` * * The format of the compressed timeline is as follows: * * ``` * [Type][Timestamp],[Param1 type][Param 1 value],[... Param2 ...]|[... Event2 ...] * ``` * * * Type is a single character indicating what type of event it is, per above * * Timestamp (`navigationStart` epoch in milliseconds) is Base-36 encoded * * Each parameter follows, separated by commas: * * The first character indicates the type of parameter * * The subsequent characters are the value of the parameter, Base-36 encoded * * From the above example, the data would be decompressed to: * * ``` * [ * { * "type": "mouse", * "time": 1474, * "x": 729, * "y": 313 * }, * { * "type": "click", * "time": 5268, * "x": 725, * "y": 319 * }, * { * "type": "key", * "time": 5921, * } * ] * ``` * * The plugin will keep track of the last * {@link BOOMR.plugins.Continuity.init `logMaxEntries`} entries in the log * (default 100). * * The timeline can be decompressed via * {@link BOOMR.plugins.Continuity.decompressBucketLog `decompressLog`} (for * debug builds). * * ## Overhead * * When enabled, the Continuity plugin adds new layers of instrumentation to * each page load. It also keeps some of this instrumentation enabled * after the `load` event, if configured. By default, these instrumentation * "monitors" will be turned on: * * * Long Tasks via `PerformanceObserver` * * Frame Rate (FPS) via `requestAnimationFrame` * * Page Busy via `setInterval` polling (if Long Tasks aren't supported) * * Monitoring of interactions such as mouse clicks, movement, keys, and scrolls * * Page statistics like DOM size/length, memory usage, and mutations * * Each of these monitors is designed to be as lightweight as possible, but * enabling instrumentation will always incur non-zero CPU time. Please read * the above sections for overhead information on each monitor. * * With the Continuity plugin enabled, during page load, you may see the plugin's * total CPU usage over the entire length of that page load reach 10-35ms, depending on * the hardware and makeup of the host-site. In general, for most modern websites, * this means Boomerang should still only account for a few percentage points of * overall page CPU usage with the Continuity plugin enabled. * * The majority of this CPU usage increase is from Page Statistics reporting and * FPS monitoring. You can disable either of these monitors individually if desired * ({@link BOOMR.plugins.Continuity.init `monitorStats`} and * {@link BOOMR.plugins.Continuity.init `monitorFrameRate`}). * * During idle periods (after page load), the Continuity plugin will continue * monitoring the above items if {@link BOOMR.plugins.Continuity.init `afterOnload`} * is enabled. This may increase Boomerang JavaScript CPU usage as well. Again, * the majority of this CPU usage increase is from Page Statistic reporting and * Frame Rate monitoring, and can be disabled. * * When Long Tasks aren't supported by the browser, Page Busy monitoring via * `setInterval` should only 1-2ms CPU during and after page load. * * ## Prerendering * * The following beacon parameters are affected by Prerendering and will be offset by the * `activationStart` time (if any): * * * `c.tti` (Time to Interactive) * * `c.ttfi` (Time to First Interaction) * * ## Beacon Parameters * * The following parameters will be added to the beacon: * * * `c.b`: Page Busy percentage (Base-10) * * `c.c.r`: Rage click count (Base-10) * * `c.c`: Click count (Base-10) * * `c.cls`: Cumulative Layout Shift score (since last beacon) (Base-10 fraction) * * `c.cls.d`: Cumulative Layout Shift data (since last beacon) * * `c.cls.tops`: Top Layout Shift score within CLS (since last beacon) * * `c.cls.topid`: Pseudo-CSS selector of the first source corresponding to the Top Layout Shift score * (since last beacon) * * `c.e`: Continuity Epoch timestamp (when everything started measuring) (Base-36) * * `c.f.d`: Frame Rate duration (how long it has been measuring) (milliseconds) (Base-10) * * `c.f.l`: Number of Long Frames (>= 50ms) (Base-10) * * `c.f.m`: Minimum Frame Rate (Base-10) per `COLLECTION_INTERVAL` * * `c.f.s`: Frame Rate measurement start timestamp (Base-36) * * `c.f`: Average Frame Rate over the Frame Rate Duration (Base-10) * * `c.fid`: First Input Delay (milliseconds) (Base-10) * * `c.i.a`: Average interaction delay (milliseconds) (Base-10) * * `c.i.dc`: Delayed interaction count (Base-10) * * `c.i.dt`: Delayed interaction time (milliseconds) (Base-10) * * `c.k.e`: Keyboard ESC count (Base-10) * * `c.k`: Keyboard event count (Base-10) * * `c.l`: Log (compressed) * * `c.lb`: Last Beacon timestamp (Base-36) * * `c.lt.n`: Number of Long Tasks (Base-10) * * `c.lt.tt`: Total duration of Long Tasks (milliseconds) (Base-10) * * `c.lt`: Long Task data (compressed) * * `c.m.n`: Mouse movement pixels (Base-10) * * `c.m.p`: Mouse movement percentage (Base-10) * * `c.s.d`: Distinct scrolls (scrolls that happen 2 seconds after the last) (Base-10) * * `c.s.p`: Scroll percentage (Base-10) * * `c.s.y`: Scroll y (pixels) (Base-10) * * `c.s`: Scroll count (Base-10) * * `c.t.click`: Click timeline (compressed) * * `c.t.domln`: DOM Length timeline (compressed) * * `c.t.domsz`: DOM Size timeline (compressed) * * `c.t.fps`: Frame Rate timeline (compressed) * * `c.t.inter`: Interactions timeline (compressed) * * `c.t.interdly`: Delayed Interactions timeline (compressed) * * `c.t.key`: Keyboard press timeline (compressed) * * `c.t.longtask`: LongTask timeline (compressed) * * `c.t.mem`: Memory usage timeline (compressed) * * `c.t.mouse`: Mouse movements timeline (compressed) * * `c.t.mousepct`: Mouse movement percentage (of full screen) timeline (compressed) * * `c.t.scroll`: Scroll timeline (compressed) * * `c.t.scrollpct`:Scroll percentage (of full page) timeline (compressed) * * `c.t.mut`: DOM Mutations timeline (compressed) * * `c.ttfi`: Time to First Interaction (milliseconds) (Base-10) * * `c.tti.fr`: Framework Ready (milliseconds) (Base-10) * * `c.tti.hi`: Hero Images ready (milliseconds) (Base-10) * * `c.tti.m`: Time to Interactive Method (`lt`, `raf`, `b`) * * `c.tti.vr`: Visually Ready (milliseconds) (Base-10) * * `c.tti`: Time to Interactive (milliseconds) (Base-10) * * @class BOOMR.plugins.Continuity */ (function() { var impl; BOOMR = window.BOOMR || {}; BOOMR.plugins = BOOMR.plugins || {}; if (BOOMR.plugins.Continuity) { return; } // // Constants available to all Continuity classes // /** * Timeline collection interval */ var COLLECTION_INTERVAL = 100; /** * Maximum length (ms) that events will be recorded, if not * a SPA. */ var DEFAULT_AFTER_ONLOAD_MAX_LENGTH = 60000; /** * Time to Interactive polling period (after onload, how often we'll * check to see if TTI fired yet) */ var TIME_TO_INTERACTIVE_WAIT_POLL_PERIOD = 500; /** * Compression Modes */ /** * Most numbers are expected to be 0-63, though larger numbers are * allowed. */ var COMPRESS_MODE_SMALL_NUMBERS = 0; /** * Most numbers are expected to be larger than 63. */ var COMPRESS_MODE_LARGE_NUMBERS = 1; /** * Numbers are from 0 to 100 */ var COMPRESS_MODE_PERCENT = 2; /** * Log types */ var LOG_TYPE_SCROLL = 0; var LOG_TYPE_CLICK = 1; var LOG_TYPE_MOUSE = 2; var LOG_TYPE_KEY = 3; var LOG_TYPE_VIS = 4; var LOG_TYPE_ORIENTATION = 5; /** * Base64 number encoding */ var BASE64_NUMBER = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"; /** * Large number delimiter (.) * * For COMPRESS_MODE_SMALL_NUMBERS, numbers larger than 63 are wrapped in this * character. */ var LARGE_NUMBER_WRAP = "."; /** * Listener Options args with Passive and Capture set to true */ var listenerOpts = {passive: true, capture: true}; // Performance object var p = BOOMR.getPerformance(); // Metrics that will be exported var externalMetrics = {}; /** * Epoch - when to base all relative times from. * * If the browser supports NavigationTiming, this is navigationStart. * * If not, just use 'now'. */ var epoch = p && p.timing && p.timing.navigationStart ? p.timing.navigationStart : BOOMR.now(); /** * Debug logging * * @param {string} msg Message */ function debug(msg) { BOOMR.debug(msg, "Continuity"); } /** * Compress JSON to a string for a URL parameter in the best way possible. * * If BOOMR.utils.Compression.jsUrl, or UserTimingCompression is available (which has JSURL), * use that. The data will start with the character `~`. * * Otherwise, use JSON.stringify. The data will start with the character `{`. * * @param {object} obj Data * * @returns {string} Compressed data */ function compressJson(data) { var jsUrlFn = (BOOMR.utils.Compression && BOOMR.utils.Compression.jsUrl) || (window.UserTimingCompression && window.UserTimingCompression.jsUrl) || (BOOMR.window.UserTimingCompression && BOOMR.window.UserTimingCompression.jsUrl); if (jsUrlFn) { return jsUrlFn(data); } else if (window.JSON) { return JSON.stringify(data); } else { // JSON isn't available return ""; } } /** * Gets a compressed bucket log. * * Each bucket is represented by a single character (the value of the * bucket base 64), unless: * * 1. There are 4 or more duplicates in a row. Then the format is: * *[count of dupes]*[number base 64] * 2. The value is greater than 63, then the format is: * _[number base 36]_ * * @param {number} type Compression type * @param {boolean} backfill Backfill * @param {object} dataSet Data * @param {number} sinceBucket Lowest bucket * @param {number} endBucket Highest bucket * * @returns {string} Compressed log */ function compressBucketLog(type, backfill, dataSet, sinceBucket, endBucket) { var out = "", val = 0, i, j, dupes, valStr, nextVal, wroteSomething; if (!dataSet) { return ""; } // if we know there's no data, return an empty string if (dataSet.length === 0) { return ""; } if (backfill) { if (typeof dataSet[sinceBucket] === "undefined") { dataSet[sinceBucket] = 0; } // pre-fill buckets for (i = sinceBucket + 1; i <= endBucket; i++) { if (typeof dataSet[i] === "undefined") { dataSet[i] = dataSet[i - 1]; } } } for (i = sinceBucket; i <= endBucket; i++) { val = (typeof dataSet[i] === "number" && !isNaN(dataSet[i])) ? dataSet[i] : 0; // // Compression modes // if (type === COMPRESS_MODE_SMALL_NUMBERS) { // Small numbers can be max 63 for our single-digit encoding if (val <= 63) { valStr = BASE64_NUMBER.charAt(val); } else { // large numbers get wrapped in .s valStr = LARGE_NUMBER_WRAP + val.toString(36) + LARGE_NUMBER_WRAP; } } else if (type === COMPRESS_MODE_LARGE_NUMBERS) { // large numbers just get Base36 encoding by default valStr = val.toString(36); } else if (type === COMPRESS_MODE_PERCENT) { // // Percentage characters take two digits always, with // 100 = __ // if (val < 99) { // 0-pad valStr = val <= 9 ? ("0" + Math.max(val, 0)) : val; } else { // 100 or higher valStr = "__"; } } // compress sequences of the same number 4 or more times if ((i + 3) <= endBucket && (dataSet[i + 1] === val || (val === 0 && dataSet[i + 1] === undefined)) && (dataSet[i + 2] === val || (val === 0 && dataSet[i + 2] === undefined)) && (dataSet[i + 3] === val || (val === 0 && dataSet[i + 3] === undefined))) { dupes = 1; // loop until we're past the end bucket or we find a non-dupe while (i < endBucket) { if (dataSet[i + 1] === val || (val === 0 && dataSet[i + 1] === undefined)) { dupes++; } else { break; } i++; } nextVal = "*" + dupes.toString(36) + "*" + valStr; } else { nextVal = valStr; } // add this value if it isn't just 0s at the end if (val !== 0 || i !== endBucket) { // // Small numbers fit into a single character (or are delimited // by _s), so can just be appended to each other. // // Percentage always takes two characters. // if (type === COMPRESS_MODE_LARGE_NUMBERS) { // // Large numbers need to be separated by commas // if (wroteSomething) { out += ","; } } wroteSomething = true; out += nextVal; } } return wroteSomething ? (type.toString() + out) : ""; } /* BEGIN_DEBUG */ /** * Decompresses a compressed bucket log. * * See {@link compressBucketLog} for details * * @param {string} data Data * @param {number} [minBucket] Minimum bucket * * @returns {object} Decompressed log */ function decompressBucketLog(data, minBucket) { var out = [], i, j, idx = minBucket || 0, endChar, repeat, num, type; if (!data || data.length === 0) { return []; } // strip the type out type = parseInt(data.charAt(0), 10); data = data.substring(1); // decompress string repeat = 1; for (i = 0; i < data.length; i++) { if (data.charAt(i) === "*") { // this is a repeating number // move past the "*" i++; // up to the next * is the repeating count (base 36) endChar = data.indexOf("*", i); repeat = parseInt(data.substring(i, endChar), 36); // after is the number i = endChar; continue; } else if (data.charAt(i) === LARGE_NUMBER_WRAP) { // this is a number larger than 63 // move past the wrap character i++; // up to the next wrap character is the number (base 36) endChar = data.indexOf(LARGE_NUMBER_WRAP, i); num = parseInt(data.substring(i, endChar), 36); // move to this end char i = endChar; } else { if (type === COMPRESS_MODE_SMALL_NUMBERS) { // this digit is a number from 0 to 63 num = decompressBucketLogNumber(data.charAt(i)); } else if (type === COMPRESS_MODE_LARGE_NUMBERS) { // look for this digit to end at a comma endChar = data.indexOf(",", i); if (endChar !== -1) { // another index exists later, read up to that num = parseInt(data.substring(i, endChar), 36); // move to this end char i = endChar; } else { // this is the last number num = parseInt(data.substring(i), 36); // we're done i = data.length; } } else if (type === COMPRESS_MODE_PERCENT) { // check if this is 100 if (data.substr(i, 2) === "__") { num = 100; } else { num = parseInt(data.substr(i, 2), 10); } // take two characters i++; } } out[idx] = num; for (j = 1; j < repeat; j++) { idx++; out[idx] = num; } idx++; repeat = 1; } return out; } /** * Decompresses a bucket log Base64 number (0 - 63) * * @param {string} input Character * * @returns {number} Base64 number */ function decompressBucketLogNumber(input) { if (!input || !input.charCodeAt) { return 0; } // convert to ASCII character code var chr = input.charCodeAt(0); if (chr >= 48 && chr <= 57) { // 0 - 9 return chr - 48; } else if (chr >= 97 && chr <= 122) { // a - z return (chr - 97) + 10; } else if (chr >= 65 && chr <= 90) { // A - Z return (chr - 65) + 36; } else if (chr === 95) { // - return 62; } else if (chr === 45) { // _ return 63; } else { // unknown return 0; } } /** * Decompresses the log into events * * @param {string} data Compressed log * * @returns {object} Decompressed log */ function decompressLog(data) { var val = "", i, j, eventData, events, out = [], evt; // each event is separate by a | events = data.split("|"); for (i = 0; i < events.length; i++) { eventData = events[i].split(","); evt = { type: parseInt(eventData[0].charAt(0), 10), time: parseInt(eventData[0].substring(1), 36) }; // add all attributes for (j = 1; j < eventData.length; j++) { evt[eventData[j].charAt(0)] = eventData[j].substring(1); } out.push(evt); } return out; } /* END_DEBUG */ // // Constants // /** * Number of "idle" intervals (of COLLECTION_INTERVAL ms) before * Time to Interactive is called. * * 5 * 100 = 500ms (of no long tasks > 50ms and FPS >= 20) */ var TIME_TO_INTERACTIVE_IDLE_INTERVALS = 5; /** * For Time to Interactive, minimum FPS. * * ~20 FPS or max ~50ms blocked */ var TIME_TO_INTERACTIVE_MIN_FPS = 20; /** * For Time to Interactive, minimum FPS per COLLECTION_INTERVAL. */ var TIME_TO_INTERACTIVE_MIN_FPS_PER_INTERVAL = TIME_TO_INTERACTIVE_MIN_FPS / (1000 / COLLECTION_INTERVAL); /** * For Time to Interactive, max Page Busy (if LongTasks aren't supported) * * ~50% */ var TIME_TO_INTERACTIVE_MAX_PAGE_BUSY = 50; /** * Determines TTI based on input timestamps, buckets and data * * @param {number} startTime Start time * @param {number} visuallyReady Visually Ready time * @param {number} startBucket Start bucket * @param {number} endBucket End bucket * @param {number} idleIntervals Idle intervals to start with * @param {object} data Long Task, FPS, Busy and Interaction Data buckets */ function determineTti(startTime, visuallyReady, startBucket, endBucket, idleIntervals, data) { var tti = 0, lastBucketVisited = startBucket, haveSeenBusyData = false; for (var j = startBucket; j <= endBucket; j++) { lastBucketVisited = j; if (data.longtask && data.longtask[j]) { // had a long task during this interval idleIntervals = 0; continue; } if (data.fps && (!data.fps[j] || data.fps[j] < TIME_TO_INTERACTIVE_MIN_FPS_PER_INTERVAL)) { // No FPS or less than 20 FPS during this interval idleIntervals = 0; continue; } if (data.busy) { // Page Busy monitor is activated if (haveSeenBusyData && typeof data.busy[j] === "undefined") { // We saw previous Busy data, but no Busy data filled in for this bucket yet! // Break and try again later. // This could happen if the PageBusyMonitor timer hasn't fired for this bucket yet. lastBucketVisited--; break; } else if (!haveSeenBusyData && typeof data.busy[j] !== "undefined") { haveSeenBusyData = true; } if (data.busy[j] > TIME_TO_INTERACTIVE_MAX_PAGE_BUSY) { // Too busy idleIntervals = 0; continue; } } if (data.interdly && data.interdly[j]) { // a delayed interaction happened idleIntervals = 0; continue; } // this was an idle interval idleIntervals++; // if we've found enough idle intervals, mark TTI as the beginning // of this idle period if (idleIntervals >= TIME_TO_INTERACTIVE_IDLE_INTERVALS) { tti = startTime + ((j + 1 - TIME_TO_INTERACTIVE_IDLE_INTERVALS) * COLLECTION_INTERVAL); // ensure we don't set TTI befor