Docs
API 1.39
Status
Concepts

Obstacles and captchas

What Kaabist does about the dialog, consent banner, tour or toast in the way of your step, how it learns a site, and what it does about a captcha.

An obstacle is anything painted over the element a step wants: a cookie banner, an announcement dialog, a product tour, a toast. Kaabist detects it, closes it the way a person would, checks that it is gone, and tells you what it did, so a program does not need a step for every overlay a site might show.

#When to use it

The obstacle layer is always on in the browser tiers. You configure it when a site needs something different from the defaults: an overlay that is part of the page and must be left alone (options.obstacles.ignore), a modal that is the page and must never be painted over (options.obstacles.allow_hide: false), or a captcha policy other than parking (policies.captcha).

Read this page when a result carries an obstacle_dismissed warning, when a job parks with the reason obstacle or captcha, or when the review queue offers a dismiss proposal.

#When it runs

Before every element action (click, fill, hover and the rest), and whenever a step fails because its target is covered, Kaabist looks at what is in the way. A step that cannot click a button because something sits on top of it is the most common way an otherwise correct program fails, so this is treated as a normal situation rather than an error.

#What counts as an obstacle

Four detectors run inside the page, and anything found by any of them is a candidate:

  1. Semantics: role="dialog", role="alertdialog", aria-modal="true", <dialog open>.
  2. Geometry: a fixed, sticky or absolutely positioned element that covers at least 40 % of the viewport, or that sits over the exact point your step was aiming at, and that is either stacked above z-index: 100 or accompanied by a <body> that has stopped scrolling.
  3. Markers: the frameworks that name themselves. Consent: OneTrust, Cookiebot, Didomi, Usercentrics, Osano, Quantcast, TrustArc, Sourcepoint, cookieconsent, Klaro, Axeptio, CookieYes, Complianz, Borlabs, and the common generic ids and classes (#cookie-banner, .cookie-consent, …). Tours: driver.js, Shepherd, intro.js, react-joyride, Hopscotch, reactour. Toasts: [role=alert], [role=status], react-toastify, Sonner, MUI Snackbar, Notyf.
  4. The hit test: whatever the browser says is actually painted at the point of your target.

Each obstacle comes back with a kind, how much of the viewport it covers, whether it is over your target, and every button inside it that might make it go away:

kindwhat it is
dialoga modal dialog
consenta cookie or privacy banner, or a consent wall
touran onboarding or product tour
toasta notification stack
overlaya covering layer that is none of the above
unknownsomething covers the target and nothing more can be said about it
busya loading overlay: a spinner, a block-UI layer, an aria-busy element

A busy overlay is waited for, never dismissed: the step waits until it goes away by itself, within the step's timeout, and the wait does not count toward the guards below.

#Which button, and in what order

A button's label is read against the dismiss dictionary: nearly three hundred phrases in sixteen languages (English, Turkish, German, French, Spanish, Italian, Portuguese, Dutch, Polish, Russian, Arabic, Japanese, Chinese, Korean, Swedish, Czech) plus the symbols close icons use (×, ✕, ✖, ✗, ⨯, X). The dictionary is data, not code: it grows when somebody takes over a job and clicks their way out of an overlay Kaabist did not recognise, site by site, and a phrase that three different sites have taught becomes part of everyone's dictionary.

Labels are read from the button's text, its aria-label and its title, and each is sorted into one of six kinds. The order is what a person would do:

kindexamplewhat it costs you
closeClose, Kapat, Schließen, 閉じるnothing
rejectReject all, Tümünü reddet, Alle ablehnenrefuses tracking on your behalf
laterNot now, Daha sonra, Späternothing
skipSkip, Atla, Überspringennothing
acceptAccept all cookies, Tümünü kabul etagrees on your behalf
nextNext, Sonraki, 次へnothing, and it is never clicked

An unlabelled control is scored by where it sits: a small button in the obstacle's top-right corner is a close icon on nearly every site, and one in the middle of a dialog is a call to action.

#The resolution order

Rungs are tried in order, and each one is verified before the next is tried: the overlay has to be gone, or your target has to be reachable again, within 1.5 seconds. "The click did not throw" is not success.

  1. A recipe you approved. If somebody in your organisation approved a dismiss proposal for this site with remember for this site, that recipe runs first. This is the fast path: one click, no guessing.
  2. ARIA. A control inside the obstacle whose aria-label says it closes: the page telling Kaabist in its own words.
  3. The dictionary. The best-scoring close, reject, later or skip control. accept is used only for a consent banner that offers nothing else, and when it happens the warning carries the detail accepted_consent, so you can see that a consent decision was made for you.
  4. Escape, on the document.
  5. A backdrop click, at a point inside the dimmer and outside the dialog.
  6. Hiding, with an injected <style>; see the gating rule below.
  7. Parking. The job stops as needs_attention with the reason obstacle, a screenshot, and one proposal per button nobody tried, so approving one makes the next run automatic.

After a successful dismissal the page is looked at again, because obstacles come in sequences: a consent wall in front of a tour that only starts once consent is given. A second overlay is dealt with when it covers your target or at least 40 % of the viewport.

#What it reports

A step that dismissed something carries one warning, naming the rung that finished the job:

{ "code": "obstacle_dismissed", "step": "steps[1]", "detail": "dictionary" }

detail is the rung (recipe, aria, dictionary, escape, backdrop, hide), or accepted_consent when a consent banner had to be accepted. The step's record in GET /v1/jobs/{id}/steps has an obstacle object with the whole story: what was detected, its kind, whether it was resolved, how, the label of the control that was clicked (control_text), the locators of the overlay's root, and how many rungs were tried.

#How Kaabist learns a site

A dismissal by ARIA, the dictionary, Escape or hiding files a dismiss proposal: the overlay (payload.obstacle), what closed it (payload.action, such as { "click": [ … ] } or { "press": "Escape" }), and the hostname. Approving it with remember: "site" turns it into a recipe on your organisation's site profile, and the next job on that hostname closes the overlay at rung 1. A backdrop click is never proposed: it is a point on the screen, and the same overlay on a wider window has a different one. The same proposal is filed at most once a day per site and action, so a busy schedule does not fill your review queue.

#When Kaabist will hide something, and when it will not

Hiding does not dismiss an overlay, it paints over it, so it is last, and it is gated by kind:

  • Always allowed for overlay, toast, tour and unknown. None of those carries a decision.
  • For dialog and consent, allowed only while options.obstacles.allow_hide is on. It is on by default. Set it to false for a page whose modal is the page (an age gate, a paywall, a terms dialog), and the job will park and ask a person instead of continuing behind it.

You can also tell Kaabist to leave something alone:

{ "options": { "obstacles": { "allow_hide": false, "ignore": ["#support-bubble"] } } }

An element matching an ignore selector (at most 20) is never reported as an obstacle and never clicked.

#Guards

A site that re-renders its banner after every dismissal would otherwise spend a whole job clicking the same button and succeeding every time. Three limits stop that, and passing any of them parks the job as needs_attention: obstacle with the detail loop_guard:

  • three clicks on one obstacle,
  • three obstacles in one step,
  • ten dismissals in one job.

#Captchas

policies.captcha decides what a captcha means for your program:

policywhat happens
park (default)the job parks as needs_attention with the reason captcha, so a person can finish it
failthe job fails at once with blocked
solvea captcha provider is asked for a token, at two credits per solve

Kaabist recognises reCAPTCHA (v2, v3 and invisible), hCaptcha, Turnstile, GeeTest and FunCaptcha/Arkose by their widget attributes and script URLs, never by prose, so a page that writes about reCAPTCHA is not treated as one. Under solve, the credits are charged before the provider is asked, the token is written into the field the vendor uses, the page's own callback is invoked, and the failed step is run again once.

Everything that can go wrong with the platform's solver parks: no provider configured, not enough credits, a provider that timed out (two minutes), or a second challenge on the same step after a solved one. A captcha is the one obstacle a person can always get past, so stopping to ask is the right answer. If you bring your own solver and it refuses your key or runs out of balance, the job fails with byo_captcha_auth_failed or byo_captcha_balance instead, because that is something only you can fix, and its solves are not billed by Kaabist.

#Worked example

Three situations show the layer at work. Each run needs a browser tier.

A consent wall in front of a dialog. A product page at https://shop.example.com/p/1 shows a full-screen consent wall (#cookie-banner, buttons Accept all and Reject) and, 300 ms after load, an announcement dialog behind it. A program that clicks the price:

{
  "kaab": "1",
  "options": { "max_tier": "browser" },
  "steps": [
    { "goto": "https://shop.example.com/p/1" },
    { "sleep": 600 },
    { "click": ".price" }
  ]
}

The click finds .price covered. The layer detects the wall as consent by its marker and clicks Reject: the dictionary prefers reject to accept, so no consent was given on your behalf and there is no accepted_consent detail. It looks again, finds the dialog now in front of the price, and closes it with its Anladım ("Got it") button. The job succeeds with one warning, { "code": "obstacle_dismissed", "step": "steps[2]", "detail": "dictionary" }. The step record's obstacle names the last thing it dealt with, kind: "dialog", resolved: true, how: "dictionary", two attempts, and the review queue has two dismiss proposals, one for the wall and one for the dialog, each with source: "heuristic" and a confidence of 0.85.

Approve both with remember: "site" and the next job on that hostname closes each overlay by recipe, with detail: "recipe", and files nothing new.

A product tour. The app at https://app.example.com opens a driver.js-style three-step tour over its product list, with Next and Skip. A step that clicks #total finds it covered; the tour is detected by its marker as tour, and the layer clicks Skip, never Next. The step record shows kind: "tour" and control_text: "Skip".

When it parks. A page can also show a dialog whose only way out is an unlabelled glyph. With "options": { "obstacles": { "allow_hide": false } }, no rung can close it and hiding is forbidden, so the job parks with status: "needs_attention" and reason: "obstacle", a screenshot in attention.screenshot, and a proposal per control nobody tried. You can apply one with POST /v1/jobs/{id}/resume, or take over the job and close it yourself; what you click becomes a dismiss proposal for the next run.

#Pitfalls

  • Look for accepted_consent. If a site's banner offers only Accept, Kaabist accepts it to continue, and says so. If agreeing is not acceptable for your use, treat a result with that detail as a failure in your own code, or deal with the banner in an explicit step of your program.
  • allow_hide defaults to on. A paywall or an age gate is a dialog too; turn hiding off for programs where the modal's content matters.
  • Ignore furniture. A chat bubble or a sticky header that sits over a target will be dismissed every run unless you list it in ignore.
  • A loop guard means the site fights back. Three dismissals of the same overlay usually mean the site re-renders it on purpose. Approve a recipe, or take over once and let the recording teach the site.
  • The http tier has no overlays to close. It reads the HTML the server sent; there is nothing painted over it. Obstacles matter only once a job runs in a browser.

#Reference

Was this page helpful?