boomerangjs
Version:
boomerang always comes back, except when it hits something
1,328 lines (1,250 loc) • 139 kB
JavaScript
/**
* 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