session-intent
Answer "how are people using <site>", "what are visitors doing on <page>", "why is nobody downloading/copying/signing up", "watch the recordings for X", "where do people drop off" for a site that runs
Files1
View on GitHubSKILL.mdskills/session-intent/SKILL.md
GitHubsession-intent
The idea: you own the page's source, so you know what each visitor was supposed to do. Write that down as an ordered intent spec, then score every PostHog session against it. The output is one finding: the step people fail at, the counts, two replay links, and the fix. Under 200 words.
The pipeline (build it as a small script)
- List hosts/pages with sessions (HogQL over
eventsgrouped by$host,$pathname). - Pull sessions for one page over N days, with their events and, where they exist, the recording snapshots.
- Score each session step by step against the intent spec (below): reached, missed, or
unknown. - Summarise with counts computed in code, then let a model write the finding from the counts and a few exemplar sessions. The model never writes a number: counts come from the scorer and are printed by code.
Writing an intent spec (sites/<slug>/intent.json)
Derive it from the SOURCE; don't ask the site owner.
- Find the template or generator, not the built HTML. Pin
source_commitwithgit rev-parse --short HEAD. job: one sentence, in the visitor's words, naming who it was made for.- Steps in order: land -> see the thing -> (optional narrow) -> take it / act on it.
successis the step that means the job got done. Each step'ssourcenamesfile:line, and includes the layout CSS lines too (the grid and its media query). Without them a model will propose a DOM move that breaks desktop. matchalternatives per step. Find which custom events the page sends: grepsend('/posthog.captureand any shared tracker. Kinds:pageview,event(+prop,min/equals/contains),scroll_px(rrweb scrollY),click(substring oftag#id.class "text"),input.- Measure positions at the visitor's real viewport with headless WebKit (e.g. 393x659):
first row top, CTA top, document height. Put them in
measured_at. A scroll step'sscroll_pxthreshold = element top minus one viewport, plus a little.
{"slug": "pricing-page", "url": "https://example.com/pricing/", "source_commit": "abc1234",
"job": "A shop owner checks what the plan costs and starts a trial.",
"measured_at": {"viewport": "393x659", "cta_top": 1420, "doc_height": 3900},
"steps": [
{"id": "land", "match": [{"kind": "pageview"}], "source": "templates/pricing.html:1"},
{"id": "see_price", "match": [{"kind": "scroll_px", "min": 800}], "source": "templates/pricing.html:42, static/site.css:118-131"},
{"id": "start_trial", "success": true, "match": [{"kind": "event", "name": "trial_click"}, {"kind": "click", "contains": "Start trial"}], "source": "templates/pricing.html:77"}
]}
Rules (each one cost a cycle)
- Scroll % is meaningless on long pages. On a 100k-px list, 3,785px scrolled reads as
max_depth 0. Usescroll_pxor an in-list interaction with"decides_miss": false. Never use a % event alone. filterTestAccountsdoes not catch QA bots. Drop bursts (3+ distinct ids, one device profile, within 120s) and print aleft out:line before quoting a count.- Check the device count before calling it a crowd. 18 sessions can be one iPhone reopening a restored Safari tab (every replay opens at the same scrollY and hides in 0.2s). Never guess who a visitor is; describe the device and the pattern.
- Verify the model's fix before reporting it: inject the CSS or JS into the live page
with Playwright
add_style_tagat 393 AND 1440 width, and measure. Verify its tap claims against the cached session JSON. Models tend to understate counts. - Most sessions may have no recording (lazy-loaded recording). Events-only steps are scored
unknownunless an instrumented event can decide them. - Never edit the site from this skill without the owner's word. Report the fix.
PostHog API traps
- Snapshots:
GET /api/projects/<id>/session_recordings/<sid>/snapshots?blob_v2=truelists sources, then fetch each withsource=blob_v2&start_blob_key=k&end_blob_key=k(both are required, otherwise 400). JSONL of[window_id, event]. Payloads withcv: "2024-10"are gzip carried as latin-1 strings. Split on\nonly:splitlines()breaks inside them. - HogQL
filterTestAccounts: pass"filters": {"filterTestAccounts": true}AND put{filters}in the WHERE clause, or it does nothing. A shell eats$session_id, so query from Python, not curl. /session_recordings/<sid>404 means no recording, even when the session has events.- Use a personal API key with read scopes only.