Observing your web app

TL;DR: a dozen+ examples of monitoring changes in a web application.


The web has lots of APIs for knowing what’s going on in your app. You can monitor mucho stuff and observe just about any type of change.

Changes range from simple things like DOM mutations and catching client-side errors to more complex notifications like knowing when the user’s battery is about to run out. The thing that remains constant are the ways to deal with them: callbacks, promises, events.

Below are some of use cases that I came up with. By no means is the list exhaustive. They’re mostly examples for observing the structure of an app, its state, and the properties of the device it’s running on.

Listen for DOM events (both native and custom):

window.addEventListener('scroll', e => { ... });   // user scrolls the page.

el.addEventListener('focus', e => { ... });        // el is focused.
img.addEventListener('load', e => { ... });        // img is done loading.
input.addEventListener('input', e => { ... });     // user types into input.

el.addEventListener('custom-event', e => { ... }); // catch custom event fired on el.

Listen for modifications to the DOM:

const observer = new MutationObserver(mutations => { ... });
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
characterData: true
});

Know when the URL changes:

window.onhashchange = e => console.log(location.hash);
window.onpopstate = e => console.log(document.location, e.state);

Know when the app is being viewed fullscreen:

document.addEventListener('fullscreenchange', e => console.log(document.fullscreenElement));

Read more

Know when someone is sending you a message:

// Cross-domain / window /worker.
window.onmessage = e => { ... };

// WebRTC.
const dc = (new RTCPeerConnection()).createDataChannel();
dc.onmessage = e => { ... };

Know about client-side errors:

// Client-size error?
window.onerror = (msg, src, lineno, colno, error) => { ... };

// Unhandled rejected Promise?
window.onunhandledrejection = e => console.log(e.reason);

Read more

Listen for changes to responsiveness:

const media = window.matchMedia('(orientation: portrait)');
media.addListener(mql => console.log(mql.matches));

// Orientation of device changes.
window.addEventListener('orientationchange', e => {
console.log(screen.orientation.angle)
});

Read more

Listen for changes to network connectivity:

// Online/offline events.
window.addEventListener('online', e => console.assert(navigator.onLine));
window.addEventListener('offline', e => console.assert(!navigator.onLine));

// Network Information API
navigator.connection.addEventListener('change', e => {
console.log(navigator.connection.type,
            navigator.connection.downlinkMax);
});

Read more

Listen for changes to the device battery:

navigator.getBattery().then(battery => {
battery.addEventListener('chargingchange', e => console.log(battery.charging));
battery.addEventListener('levelchange', e => console.log(battery.level));
battery.addEventListener('chargingtimechange', e => console.log(battery.chargingTime));
battery.addEventListener('dischargingtimechange', e => console.log(battery.dischargingTime));
});

Read more

Know when the tab/page is visible or in focus:

document.addEventListener('visibilitychange', e => console.log(document.hidden));

Read more

Know when the user’s position changes:

navigator.geolocation.watchPosition(pos => console.log(pos.coords))

Know when the permission of an API changes:

const q = navigator.permissions.query({name: 'geolocation'})
q.then(permission => {
permission.addEventListener('change', e => console.log(e.target.state));
});

Read more

Know when another tab updates localStorage/sessionStorage:

window.addEventListener('storage', e => alert(e))

Know when an element enters/leaves the viewport (e.g. “Is this element visible?”):

const observer = new IntersectionObserver(changes => { ... }, {threshold: [0.25]});
observer.observe(document.querySelector('#watchMe'));

Read more

Know when an element changes size:

const observer = new ResizeObserver(entries => {
for (let entry of entries) {
  const cr = entry.contentRect;
  console.log('Element:', entry.target);
  console.log(`Element size: ${cr.width}px x ${cr.height}px`);
  console.log(`Element padding: ${cr.top}px ; ${cr.left}px`);
}
});
observer.observe(document.querySelector('#watchMe'));

Read more

Know when the browser is idle (to perform extra work):

requestIdleCallback(deadline => { ... }, {timeout: 2000});

Read more

Know when the browser fetches a resource, or a User Timing event is recorded/measured:

const observer = new PerformanceObserver(list => console.log(list.getEntries()));
observer.observe({entryTypes: ['resource', 'mark', 'measure']});

Read more

Know when properties of an object change (including DOM properties):

// Observe changes to a DOM node's .textContent.
// From https://gist.github.com/ebidel/d923001dd7244dbd3fe0d5116050d227
const proxy = new Proxy(document.querySelector('#target'), {
set(target, propKey, value, receiver) {
  if (propKey === 'textContent') {
    console.log('textContent changed to: ' + value);
  }
  target[propKey] = value;
}
});
proxy.textContent = 'Updated content!';

Read more

If you’re building custom elements (web components), there are several methods, called reactions, that you can define to observe important things in the element’s lifecycle:

class AppDrawer extends HTMLElement {
constructor() {
  super(); // always need to call super() first in the ctor.
  // Instance of the element is instantiated.
}
connectedCallback() {
  // Called every time the element is inserted into the DOM.
}
disconnectedCallback() {
  // Called every time the element is removed from the DOM.
}
attributeChangedCallback(attrName, oldVal, newVal) {
  // An attribute was added, removed, updated, or replaced.
}
adoptedCallback() {
  // Called when the element is moved into a new document.
}
}
window.customElements.define('app-drawer', AppDrawer);

Read more

Know when a CSP policy has been violated:

document.addEventListener('securitypolicyviolation', e => {
console.error('CSP error:',
    `Violated '${e.violatedDirective}' directive with policy '${e.originalPolicy}'`);
});

Read more

Know when your sites uses a deprecated API, hits a browser intervention ,or feature policy violation:

const observer = new ReportingObserver((reports, observer) => {
reports.map(report => {
  console.log(report);
  // ... send report to backend ...
});
}, {buffered: true});

observer.observe();

Read more

Wowza! What’s crazy is that there are even more APIs in the works.

I suppose you could classify some of these examples as techniques or patterns (e.g. reacting to DOM events). However, many are completely new APIs designed for a specific purpose: measuring performance, knowing battery status, online/offline connectivity, etc.

Honestly, it’s really impressive how much we have access to these days. There’s basically an API for everything.


Mistake? Something missing? Leave a comment.

Update 2016-08-17 - added custom element reaction example

Update 2018-07-23 - added ResizeObserver

Update 2018-07-25 - added CSP securitypolicyviolation example