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));
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);
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)
});
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);
});
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));
});
Know when the tab/page is visible or in focus:
document.addEventListener('visibilitychange', e => console.log(document.hidden));
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));
});
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'));
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'));
Know when the browser is idle (to perform extra work):
requestIdleCallback(deadline => { ... }, {timeout: 2000});
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']});
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!';
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);
Know when a CSP policy has been violated:
document.addEventListener('securitypolicyviolation', e => {
console.error('CSP error:',
`Violated '${e.violatedDirective}' directive with policy '${e.originalPolicy}'`);
});
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();
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