← All guides
Alerting

Percent and Absolute Thresholds (and Why Yours May Never Fire)

Numeric predicates parse your extracted string with parseFloat. "$1,299" parses to NaN, and NaN never triggers. Here is how to extract a number that works.

Verid Guides·7 min read

The predicate that looks right and never fires

field_increases_by_percent, field_increases_by_absolute, field_decreases_by_percent, and field_decreases_by_absolute all do the same thing before they do anything else: they take the field's before and after values - both are strings if your extraction method returns strings, which CSS, XPath, and regex all do - and run them through a number parser. If that parse fails on either side, the predicate returns false, silently, with no error anywhere in the run. The monitor looks correctly configured. The field extracts fine. The value visibly changes every time you check it by hand. It just never alerts.

This is the single most common reason a numeric monitor goes quiet, and it's almost always caused by one thing: the extracted string still has a currency symbol or a thousands separator in it.

How the parser actually works

The conversion is parseFloat under a thin wrapper. If the value is already a number, it's used as is. If it's a string, it goes straight into parseFloat with no pre-processing - no stripping of $, £, €, ,, or anything else:

toNumber("1299.00")   → 1299
toNumber("$1,299.00") → NaN   // parseFloat stops at "$"
toNumber("1,299.00")  → 1     // parseFloat stops at the first comma, keeps only "1"
toNumber("in stock")  → NaN
toNumber(null)        → NaN

That second line is worth sitting with: "1,299.00" doesn't fail to parse, it partially parses - parseFloat reads digits until it hits a character it doesn't understand and returns what it got so far. A comma isn't part of a JavaScript number literal, so parsing stops after the leading 1. You don't get an error. You get 1, silently wrong, and every downstream percent or absolute comparison runs against that 1 instead of 1299.

There is no currency or thousands-separator normalization anywhere in this path today. Whatever string your extraction config hands the predicate is what parseFloat sees.

The zero-baseline rule on percent predicates

field_increases_by_percent and field_decreases_by_percent compute the move as a percentage of the previous value:

increase: ((after - before) / Math.abs(before)) * 100
decrease: ((before - after) / Math.abs(before)) * 100

If before parses to exactly 0, both predicates return false immediately, before doing the division. This isn't a bug being worked around - dividing by zero produces Infinity or NaN either way, and neither of those is a meaningful "percent increase," so a previous value of 0 is treated as "no percent predicate can fire from here" rather than as an error. A field that goes from 0 to any nonzero number needs field_increases_by_absolute (or field_changes) to catch that move - a percent predicate on it will never trigger, at any threshold.

The Math.abs(before) in both formulas matters if your baseline can be negative - a temperature or a balance, say. It keeps the percentage's sign meaningful (a move from -10 to -5 is a real increase) instead of flipping sign depending on which side of zero the previous value happened to sit.

The absolute predicates have no such rule - field_increases_by_absolute and field_decreases_by_absolute just compare the raw difference to threshold, so they work the same way whether before is 0, negative, or anything else. Their only failure mode is the same NaN problem: if either side doesn't parse, the comparison never happens.

Fixing it at the extraction layer

The parser isn't going to change out from under you, so the fix is upstream: make the extraction config return a bare number, not a formatted string. Where that fix lives depends on the method:

Regex - a capture group returns exactly the characters it spans, nothing stripped out of the middle of it. That's enough to drop a currency symbol, because you can just keep it outside the parentheses:

{ "price": "/\\$([\\d]+\\.\\d{2})/" }

matched against $149.00 returns the clean string "149.00", which parses fine. But it can't do the same trick for a thousands separator that sits inside the number, like $1,299.00 - the comma is between two digits, not at an edge you can exclude. Capturing [\d,]+\.\d{2} to make the pattern match at all just puts the comma back into the returned string, still unparseable. Regex extraction has no post-match transform step, so once the comma is inside the span you want, it comes out in the value. For a page whose numbers cross into four figures with a separator, the fix isn't a cleverer pattern - it's targeting a field that never had the separator to begin with, the same way you'd handle XPath or JSONPath below. See regex extraction for the capture-group mechanics.

XPath - the same display string usually has a clean number sitting one step away in an attribute. A price component that renders $1,299 often carries the raw value in data-price, content, or a similar attribute meant for structured data:

{ "price": "//span[@class='price']/@data-price" }

reading the attribute instead of /text() skips the formatting entirely, because the attribute was never formatted for display in the first place.

JSONPath - if the source is an API response or an embedded JSON blob, the numeric field is usually already a bare number, not a formatted string, since JSON has no native currency type. Point the path at that field directly ($.price, $.amount_cents) rather than a display_price or formatted_price sibling if one exists.

AI extraction - tell the prompt explicitly to return a bare number with no symbol or separator: "Return the price as a plain number with no currency symbol or comma, e.g. 1299.00, not $1,299.00." The model will follow an explicit numeric-format instruction; it won't infer one from "extract the price" on its own.

Whichever method you use, the fastest way to confirm the fix worked is to preview the monitor before saving it - see testing a selector before you save - and check that the previewed value has no $, no ,, and parses as a plain number by eye.

Increase, decrease, or "any move at all"

The four numeric predicates are directional on purpose - an increase predicate never fires on a decrease and vice versa, regardless of size. If you want to alert on a price moving in either direction, there's no single "any numeric change" predicate; combine the two with a composite OR:

{
  "type": "composite",
  "operator": "OR",
  "conditions": [
    { "type": "field_increases_by_percent", "field": "price", "threshold": 5 },
    { "type": "field_decreases_by_percent", "field": "price", "threshold": 5 }
  ]
}

If you don't actually care about magnitude - any price edit at all, no matter how small - skip the numeric predicates entirely and use field_changes on the same field instead. It has no parsing step at all, so a value going from "$1,299.00" to "$1,199.00" fires it correctly even though neither number would parse - it only compares the raw strings for equality, not their numeric value. Reach for a percent or absolute threshold specifically when small moves are noise and only a move past a line is worth an alert; reach for field_changes when the number itself, not its size, is what you're tracking.

What to read next

Ready to try it? Point Verid at your URL and get a signed alert on every change. 200 free runs a month, no credit card.

Try Verid for free

200 free runs a month, no credit card required.