poltertab 1.3.0 → 1.5.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: browser-navigation-strategy
3
- description: Use when driving a real browser with PolterTab's browser_* tools — navigating, scraping, or clicking through a site whose DOM is virtualized, obfuscated, shadow-rooted, or iframe-sandboxed, and selectors keep going stale.
3
+ description: Use when driving a real browser with PolterTab's browser_* tools — extracting repeating records, paginating a listing, navigating, scraping, or clicking through a site whose DOM is virtualized, obfuscated, shadow-rooted, or iframe-sandboxed, and selectors keep going stale.
4
4
  ---
5
5
 
6
6
  # Browser Navigation Strategy
@@ -9,49 +9,140 @@ PolterTab drives the user's real Chrome profile. Sessions are already logged in,
9
9
  and every action happens in a window the user can see. That makes it powerful and
10
10
  unforgiving: a wrong click lands on a real account.
11
11
 
12
- ## Read the page before you touch it
12
+ ## Pick the right tool before you start
13
13
 
14
- `browser_snapshot` is the first call on any unfamiliar page. It returns visible,
15
- interactive elements with `@e1`-style refs, and it descends into shadow roots and
16
- child frames — so what it shows you is the page as it really is, not just the
17
- top-level light DOM.
14
+ | What you want | Use |
15
+ | --- | --- |
16
+ | Many similar records on a page (cards, rows, listings) | `browser_extract` |
17
+ | Those records across many pages | `browser_extract_all` |
18
+ | One element's text | `browser_get_text` |
19
+ | Page title, og: tags, schema.org data | `browser_scrape` with `fields` |
20
+ | To find out what a page contains | `browser_snapshot` |
21
+ | Data the DOM does not hold (virtualized/obfuscated) | intercept + `browser_get_network_state` |
18
22
 
19
- Never guess a selector from a URL or a screenshot. Snapshot, then act on what
20
- came back.
23
+ Getting this wrong is the most expensive mistake available. Scraping 100 records
24
+ one field at a time costs a round-trip per page and leaves you reassembling
25
+ records by hand; `browser_extract_all` is one call.
21
26
 
22
- ## Prefer the network over the DOM
27
+ ## Repeating records: use browser_extract
23
28
 
24
- On a virtualized list (LinkedIn, most infinite feeds), the DOM holds only the
25
- rows currently on screen, under class names that change between deploys. Scraping
26
- it yields a fraction of the data and breaks next week.
29
+ A flat scrape returns a list per selector, so you have to work out which value
30
+ belongs to which record. That works until a field is optional — then one missing
31
+ value shifts every later record and the result looks completely plausible.
32
+
33
+ `browser_extract` takes a container and resolves each field *inside* it:
34
+
35
+ ```json
36
+ {
37
+ "record": ".agent-card",
38
+ "fields": {
39
+ "name": { "sel": "a.agent-card-name", "get": "text" },
40
+ "url": { "sel": "a.agent-card-name", "get": "href" },
41
+ "phone": { "sel": "a[href^='tel:']", "get": "href", "strip": "tel:" },
42
+ "socials": { "sel": "a.social-button", "get": "href", "many": true }
43
+ },
44
+ "anchor": "url",
45
+ "output_file": "agents.csv"
46
+ }
47
+ ```
48
+
49
+ - A missing field is `null`. It never shifts.
50
+ - `sel` is relative to the record; omit it (or `"."`) for the record itself.
51
+ - `get`: `text` (default), `href`, `src`, or any attribute name. `href`/`src`
52
+ come back absolute.
53
+ - `anchor` names the field every real record has. Records without it — the
54
+ placeholder cards padding a last page — are dropped and counted.
55
+
56
+ **Read the response before trusting it.** `fill_rates` and `warnings` tell you
57
+ whether the spec is right:
58
+
59
+ ```
60
+ fill_rates: { name: 12, phone: 10, socials: 0 }
61
+ warnings: ["socials: 0/12 within record scope, but 40 matches page-wide
62
+ for a.social-button — record boundary likely too narrow"]
63
+ ```
64
+
65
+ That warning means the container you picked excludes a sibling holding the
66
+ field. Widen `record` and re-run. Without it, an empty column reads as "these
67
+ records have no socials" — confidently wrong.
68
+
69
+ A field at 0 with *no* page-wide matches means the selector is wrong, not the
70
+ boundary. The message distinguishes the two; act on which one you got.
71
+
72
+ ## Paginating: use browser_extract_all
73
+
74
+ Same spec plus a `url_template` containing `{page}`. It runs the loop itself, so
75
+ pages cost a browser round-trip instead of a model round-trip.
76
+
77
+ Dedup on `key` — a stable per-record id, the detail URL or an element `id`, never
78
+ position. It halts on the first of `limit_reached`, `empty_page`,
79
+ `duplicate_page`, `fill_rate_deviation`, or `max_pages`, and always says which.
80
+
81
+ Two of those are worth understanding:
82
+
83
+ - **`duplicate_page`** — the page returned records already seen. This is what a
84
+ site does with a param it does not recognise: it serves page 1 again. Without
85
+ the check you would collect the same 12 records eight times.
86
+ - **`fill_rate_deviation`** — a field well-populated on page 1 collapsed on a
87
+ later page, so the learned spec no longer fits. Everything collected so far is
88
+ returned; re-run from `start_page` with a spec covering both layouts. Set
89
+ `fill_tolerance: 0` if a site is legitimately heterogeneous.
90
+
91
+ ## Take the free structured data first
27
92
 
28
- The reliable path:
93
+ Before writing any selector, try:
94
+
95
+ ```json
96
+ { "fields": ["meta", "jsonld"] }
97
+ ```
98
+
99
+ on `browser_scrape`. `title`, every `og:`/`twitter:` tag, and any schema.org
100
+ `ld+json` blob come back with no selectors at all. Real estate, jobs, products,
101
+ events and profile pages very often ship a complete typed record this way —
102
+ name, telephone, email, postal address, employer — which beats any selector you
103
+ could write and does not break on redesign.
104
+
105
+ This is the cheapest call available on a detail page. Try it before building an
106
+ enrichment scrape.
107
+
108
+ ## Prefer the network over the DOM — when the DOM is the problem
109
+
110
+ On a virtualized list (LinkedIn, most infinite feeds), the DOM holds only the
111
+ rows on screen, under class names that change between deploys. There, the
112
+ network is the reliable path:
29
113
 
30
114
  1. `browser_set_intercept_patterns` if the site uses endpoints outside the
31
115
  defaults (`graphql`, `/api/`, `voyager`, `feed`).
32
116
  2. `browser_smart_scroll` to trigger lazy loading. It yields until new requests
33
117
  land, so you do not have to guess a wait.
34
- 3. `browser_get_network_state` to read the structured JSON the page itself
35
- received.
118
+ 3. `browser_get_network_state` to read the JSON the page itself received.
119
+
120
+ **But check first that there is a network path at all.** Plenty of sites are
121
+ server-rendered: the records ship in the initial HTML and no XHR ever fires.
122
+ Chasing the network there costs several calls and returns an empty buffer that
123
+ looks identical to "you set the wrong pattern". If `browser_extract` already
124
+ returns full records, or `fields: ["jsonld"]` has the data, the page is
125
+ server-rendered — use the DOM and move on.
36
126
 
37
- Pass `output_file` on `browser_get_network_state` for anything large. The payload
38
- goes to disk and you get a summary — a 4 MB GraphQL response does not belong in
39
- the context window.
127
+ The default intercept patterns include `/api/`, which matches analytics and maps
128
+ traffic on most sites. An almost-empty-looking buffer full of Google requests is
129
+ that, not your data.
40
130
 
41
131
  ## Shadow DOM, iframes, and late modals
42
132
 
43
133
  PolterTab handles these without special flags, which is worth knowing so you do
44
134
  not work around problems it already solves:
45
135
 
46
- - **Shadow roots** — including closed ones — are pierced automatically. A
47
- selector that matches inside a web component just works.
136
+ - **Shadow roots** — including closed ones — are pierced automatically, in
137
+ `browser_extract`'s record scope too.
48
138
  - **Child frames** are searched in parallel when the top frame misses, so an
49
139
  element inside a sandboxed iframe is reachable by plain selector.
50
140
  - **Late-rendering elements** get retried for ~3s. A modal that mounts a beat
51
141
  after the click that opened it does not need a manual wait.
52
142
 
53
- If an element genuinely is not there, you get `Element not found` after the
54
- retry window. Treat that as "not on this page", not "try again immediately".
143
+ `Element not found` after that window means "not on this page", not "try again".
144
+ `browser_extract` does not throw for this — it returns `records_found: 0` with
145
+ the warnings above, which tell you more than an error would.
55
146
 
56
147
  ## Selectors, in order of durability
57
148
 
@@ -63,32 +154,69 @@ retry window. Treat that as "not on this page", not "try again immediately".
63
154
  Long generated class chains (`.css-1x2y3z4`) are the worst of both worlds:
64
155
  brittle and unreadable. Reach for a `@e` ref from the last snapshot instead.
65
156
 
157
+ ## Keep payloads out of the context window
158
+
159
+ Every read tool takes `output_file`. The payload goes to
160
+ `~/.poltertab/downloads/` and you get back row count, field names, fill rates
161
+ and two sample records. Use it for anything bigger than a glance.
162
+
163
+ `.csv` and `.jsonl` are written in those formats, so the file is the deliverable
164
+ — never re-type extracted records into a file by hand, which costs more than the
165
+ extraction did.
166
+
167
+ `browser_snapshot` on a real page runs to tens of KB. Narrow it with
168
+ `interactive_only` or `max_nodes`, or send it to `output_file`.
169
+
170
+ Text fields are cut at `max_text` (default 500) and the result says when it
171
+ happened. Raise it rather than assuming you have the whole value.
172
+
173
+ ## Known limits — do not fight these
174
+
175
+ - **Records spanning sibling elements.** `browser_extract` needs one container
176
+ per record. A table where an item's fields live in two sibling `<tr>`s (Hacker
177
+ News is the canonical case) has no such container. Extract twice and join on a
178
+ shared id — never on position.
179
+ - **Pagination that is not a URL parameter.** `browser_extract_all` only drives
180
+ `{page}` in a URL. Infinite scroll, a "Load more" button, and cursor tokens
181
+ need `browser_smart_scroll` or clicking, in a loop you run yourself.
182
+ - **Virtualized lists.** `browser_extract` reads the DOM, so it sees only the
183
+ rendered window. Use the network path.
184
+ - **`output_file` writes only to `~/.poltertab/downloads/`.** A path is reduced
185
+ to its basename; the returned `file` is where it actually went.
186
+
66
187
  ## Carry knowledge between sessions
67
188
 
68
189
  `browser_get_site_memory(hostname)` at the start of work on a site, and
69
- `browser_save_site_memory` the moment you solve something non-obvious — a cookie
70
- wall that intercepts the first click, a required query parameter, the shape of a
71
- GraphQL response worth reusing.
190
+ `browser_save_site_memory` the moment you solve something non-obvious. Save the
191
+ record root, the field spec that worked, the pagination parameter, and any
192
+ obstacle with its fix. `www.` is normalized and a full URL is accepted, so
193
+ lookups do not miss.
72
194
 
73
- The point is to not rediscover the same obstacle next week. Save the obstacle and
74
- the fix together; a note saying "this page is tricky" helps nobody.
195
+ A note saying "this page is tricky" helps nobody. Save what you would need to
196
+ skip the discovery entirely next time.
75
197
 
76
198
  ## Multiple tabs
77
199
 
78
200
  `browser_session_create` gives a named tab you can return to. Every page-facing
79
- tool takes `session` (or `tabId`) to target it. Omit both and PolterTab uses the
80
- last tab it navigated, falling back to the active tab — fine for single-track
81
- work, ambiguous the moment two flows interleave. Name your sessions when running
82
- more than one.
201
+ tool takes `session` (or `tabId`).
83
202
 
84
- Close what you opened with `browser_session_close`.
203
+ Omit both and the first `browser_navigate` opens its own tab — it does not
204
+ commandeer the tab the user is looking at — and later calls reuse it. That is
205
+ fine for single-track work and ambiguous the moment two flows interleave, so
206
+ name your sessions when running more than one. Close what you opened with
207
+ `browser_session_close`.
85
208
 
86
209
  ## Pitfalls
87
210
 
211
+ - Do not scrape repeating records field-by-field. Use `browser_extract`, and
212
+ read its `warnings`.
213
+ - Do not zip flat arrays into records by position. One optional field silently
214
+ ruins the whole dataset.
215
+ - Do not loop pages yourself when `url_template` covers the site.
88
216
  - Do not use `browser_get_text` or `browser_scrape` on virtualized lists. Use
89
217
  `browser_smart_scroll` then `browser_get_network_state`.
90
- - Do not invent intercept patterns. Check the defaults, then set only what the
91
- site actually needs.
218
+ - Do not invent intercept patterns, and do not assume an empty buffer means you
219
+ asked wrong — the site may be server-rendered.
92
220
  - Do not paste large scraped payloads into the conversation. Use `output_file`.
93
221
  - Do not retry a failed selector unchanged. The retry already happened inside
94
222
  the tool — snapshot again and find out what the page really contains.