← All posts

API & helpers

Build a price and stock watcher with MutationObserver

You want that item, but only at the right price — or you want to know the moment a sold-out product is back. Refreshing the tab all day is no way to live. This article builds a price and stock watcher with a JustZix JS rule: a MutationObserver watches the relevant element, compares the value, and alerts you when your condition is met.

How the watcher works

The plan has four parts, and each maps to a section below:

  1. Pick a stable selector for the price or stock element.
  2. Attach a MutationObserver so you react to changes without polling.
  3. Parse the text into a comparable number or boolean.
  4. Notify yourself — an on-page banner or a system notification — and remember the last value.

It all runs client-side in the tab. Keep the product page open (a pinned tab is ideal) and the rule does the watching.

Picking a stable selector

The watcher is only as reliable as its selector. Open DevTools, Elements panel, and find the element holding the price. Prefer, in this order:

Avoid hashed classes (.css-1a2b3c) and long descendant chains — they break on the next deploy. Many shops also embed structured data in a <script type="application/ld+json"> block; if the visible DOM is messy, parsing that JSON is often the most stable route.

Setting up the MutationObserver

A MutationObserver fires a callback whenever the watched subtree changes — no polling, no wasted CPU. Watch the price node for text and child changes:

const SELECTOR = '[itemprop="price"]';   // your stable selector
const priceEl = document.querySelector(SELECTOR);

if (!priceEl) {
  console.warn('[watcher] price element not found - check the selector');
} else {
  const observer = new MutationObserver(() => checkPrice(priceEl));
  observer.observe(priceEl, {
    childList: true,
    subtree: true,
    characterData: true
  });

  // Also check once on load - the price may already be a deal
  checkPrice(priceEl);
}

Single-page-app shops sometimes replace the whole price node on update. If your observer goes silent, observe a stable parent container with subtree: true and re-query the price inside the callback.

Parsing the price into a number

Price text is messy: currency symbols, thousands separators, decimal commas. Strip it down to a clean number before comparing:

// Turn "$1,299.00" or "1 299,00 zl" into 1299
function parsePrice(text) {
  if (!text) return NaN;
  // Keep digits, dot and comma; drop everything else
  let s = text.replace(/[^0-9.,]/g, '');
  // If comma is the decimal separator, normalise it
  if (/,\d{2}$/.test(s)) s = s.replace(/\./g, '').replace(',', '.');
  else s = s.replace(/,/g, '');
  return parseFloat(s);
}

Comparing against a threshold

Now the decision logic. Compare the parsed price to your target and only alert when it actually crosses — and only once per drop, so you are not spammed on every minor DOM mutation:

const TARGET = 999;            // alert when price drops to/below this
const STORAGE_KEY = 'jz-watch-' + location.pathname;

function checkPrice(el) {
  const price = parsePrice(el.textContent);
  if (Number.isNaN(price)) return;

  const last = parseFloat(localStorage.getItem(STORAGE_KEY)) || Infinity;
  localStorage.setItem(STORAGE_KEY, String(price));

  console.log('[watcher] price now', price, '- last', last);

  // Fire only when we newly cross the threshold
  if (price <= TARGET && last > TARGET) {
    notify('Price drop! Now ' + price + ' (target ' + TARGET + ')');
  }
}

Watching for back-in-stock

Same pattern, different signal. Instead of a number you watch a boolean — is the buy button enabled, is the "out of stock" label gone:

function checkStock() {
  const soldOut = document.querySelector('[class*="out-of-stock" i], .sold-out');
  const buyBtn = document.querySelector('button[name="add-to-cart"], .add-to-cart');
  const inStock = !soldOut && buyBtn && !buyBtn.disabled;

  const wasInStock = localStorage.getItem('jz-stock') === 'yes';
  localStorage.setItem('jz-stock', inStock ? 'yes' : 'no');

  if (inStock && !wasInStock) {
    notify('Back in stock! Grab it now.');
  }
}

Notifying yourself

An alert is useless if you do not see it. Use two channels: an unmissable on-page banner, plus a system notification for when the tab is in the background.

function notify(message) {
  // 1. On-page banner - always visible in the tab
  let bar = document.getElementById('jz-watch-bar');
  if (!bar) {
    bar = document.createElement('div');
    bar.id = 'jz-watch-bar';
    bar.style.cssText =
      'position:fixed;top:0;left:0;right:0;z-index:2147483647;' +
      'background:#16a34a;color:#fff;font:600 15px/1.4 sans-serif;' +
      'padding:12px 16px;text-align:center;';
    document.body.appendChild(bar);
  }
  bar.textContent = 'JZ watcher: ' + message;

  // 2. System notification - works when the tab is hidden
  if (Notification.permission === 'granted') {
    new Notification('JustZix watcher', { body: message });
  } else if (Notification.permission !== 'denied') {
    Notification.requestPermission();
  }
}

The z-index of 2147483647 is the maximum 32-bit integer — it guarantees the banner sits above anything the site renders. System notifications need permission, which the browser only grants from a user gesture or a previously approved origin, so the first run may just request it; the banner covers you in the meantime.

Persisting the last seen value

You saw localStorage used above, and it does real work. It survives reloads, so the watcher knows whether a change is genuinely new. It also de-duplicates alerts — you compare against the stored value and only notify on a real crossing. Key it per page (location.pathname) so several product watchers can coexist without overwriting each other.

Wiring it up in JustZix

  1. Create a rule with the URL pattern of the exact product, e.g. https://shop.example.com/product/12345*.
  2. Paste the combined script into the JS tab — set SELECTOR and TARGET for that product.
  3. Pin the tab and leave it open; the observer reacts whenever the shop updates the DOM.
  4. Want a periodic re-check even without DOM changes? Add a setInterval that reloads the page every few minutes.

See also

Stop refreshing that tab. Install JustZix, drop the watcher script into a per-product rule, and let the browser tell you the moment the price is right.

Rate this post

No ratings yet — be the first.

Try it yourself

Install JustZix and paste any snippet from this article. Two minutes from zero to a working rule across all your devices.

Get JustZix

Features · How it works · Examples · Use cases