Visitor graph (Task 357)
Behavioural analytics that connect anonymous page visits to known :Person contacts. Replaces the anonymous click-through metric from Task 336 with a fully attributed graph.
What this gives the operator
- A morning briefing surface: "who has been on the site overnight, and what did they look at?"
- An engagement-ranked nurture queue, ordered by recency × depth × dwell.
- A graph-backed click-through-rate report for property recommendations.
- A full event timeline for any one session, for prep and diagnosis.
- A signed-cookie that recognises a named visitor on later visits without re-clicking the marketing link.
Data model
| Node | Meaning |
|---|---|
:Session | One browser tab session. Composite key (accountId, sessionId). |
:AnonVisitor | Pre-identification browser identity. Merges into :Person on first signed-token click. |
:PageView | One page load. Carries referrer, path, optional dwellMs. |
:Click | One DOM click on a tagged element (data-track="<label>"). |
:ScrollMilestone | Roll-up of scroll depth — one node per :PageView, maxDepth ∈ {25,50,75,100}. |
:Page | URL metadata. Content-only, survives erasure. |
:Recommendation | Materialised [property-recommended] log line for CTR computation. |
| Edge | Direction |
|---|---|
VISITED | Person → Session or AnonVisitor → Session |
OWNS_VISITOR | Person → AnonVisitor (cross-session merge) |
HAS_EVENT | Session → PageView / Click / ScrollMilestone |
OF_PAGE | PageView → Page |
OF_LISTING | PageView → Listing |
FOR_SESSION | Recommendation → Session |
How identity gets resolved
The signed-token cookie mxy_v carries a :Person elementId, signed HMAC-SHA256 with a brand-local 32-byte secret (file at ~/.<brand>/credentials/visitor-token-secret, minted on first read). When the recommender's /listings/<slug>/click?session=<sk>&v=<token> URL is visited:
- The click handler verifies the HMAC on
<token>. - If valid, the same token value is written into
mxy_v(Max-Age 30 days, SameSite=Lax, HttpOnly, Secure). - On subsequent visits,
POST /v/eventreads the cookie, verifies it, and attributes every event to the bound:Person.
When a previously-anonymous browser binds for the first time, any :Session already attributed to the :AnonVisitor is re-attached to the :Person, and the merge fires [anonvisitor-merge] with the count of reattributed sessions.
Tools
All under the real-agent-buyers plugin, admin-side only:
| Tool | Purpose |
|---|---|
visitor-recent-by-person | Recent sessions for a known :Person (morning round, 1:1 prep). |
visitor-recent-by-page | Recent visitors of a given listing slug or URL. |
visitor-engagement-score | Engagement-ranked :Person list (nurture queue). |
visitor-recommendation-ctr | Graph-backed CTR over a window, joined from :Recommendation and :Click nodes. |
visitor-session-detail | Full event timeline for one :Session. |
visitor-event-ingest | Admin companion to POST /v/event for test harness work. |
visitor-backfill-from-logs | One-shot importer: parses [property-recommended] and [property-card-click] log lines into the graph; used to recover late-arriving sessions and for ad-hoc forensics. |
mint-visitor-token | Mints a signed token bound to a :Person for outbound URLs in morning-round, lead-nurturing, vendor-updates. Returns { token, expiryMs }; the agent appends &v=<token> to /listings/<slug>/click?session=<sk>. Same secret file as the UI server; both processes share it via the wx-create pattern. (Task 362) |
Privacy
The full description is at /privacy on every brand domain. Highlights:
- First-party cookie only, no third-party scripts.
- Retention is erasure-on-request, not time-based; visit data persists until a
:Personis erased. - Right-to-erasure cascades through
contact-erase::Session, every:HAS_EVENTchild, and any owned:AnonVisitorare removed when the:Personis erased.:Pageand:Listingare content metadata and intentionally preserved.
Verification
Quick checks the operator can run after deployment:
- Load a published listing page; grep
[visitor-event] type=pageviewinserver.logwithin 1s. - Scroll past 50%; grep
[visitor-event] type=scroll depth=50. - Click an element marked
data-track="floorplan"; grep[visitor-event] type=click label=floorplan. - Run
visitor-backfill-from-logsover a log window where live writes were lost (process restart, etc.); the response reportsrecWrittenandclickWrittencounts. Subsequent runs over the same window are idempotent for:Recommendationand append-only for:Click.
Failure signals
| Symptom | What it means | Where to look |
|---|---|---|
[visitor-event] count drops to zero with no [v-event-error] | Pixel silently failing on the brand domain (probably CSP, CORS, or origin mismatch). | Check brand.json publishedSiteOrigins; check browser console on a published listing page. |
[token-bind] reject reason=bad-sig spikes | HMAC verify failing — either the secret rotated and old cookies are being rejected (expected during rotation) or the recommender is minting against a stale secret. | Compare ~/.<brand>/credentials/visitor-token-secret across processes. |
[anonvisitor-merge] never fires after first signed-token click | The pixel isn't reading the cookie. | Inspect the mxy_v cookie in DevTools; check CORS Access-Control-Allow-Credentials: true. |
[v-event-error] reason=rate-limit for legitimate operator traffic | Operator IP shares a NAT with high-volume crawlers. | Adjust RATE_LIMIT in visitor-event.ts or whitelist the IP at the proxy. |