# Visit logger (Agent Skill)

> A server-side log of who opened a shared demo, proposal or quote, from where and on what: prefetches and bots filtered out, edge geolocation, sittings, a first-open announcement and admin history panels.

Page: https://timerise.ai/skills/visit-logger

Agent Skill `visit-logger`: https://github.com/timerise-ai/visit-logger. Category: Infrastructure & security. Runs on Next.js; backend: Postgres/Supabase or Firestore behind a `VisitStore` seam.

Latest release: v0.1.5 (2026-09-28).

Install: npx skills add timerise-ai/visit-logger. Timerise builds it on a brief; the price comes from the brief.

When you send a client a demo, a proposal or a quote, the one thing sales wants to know is whether it was opened, when, and from where. Web analytics cannot answer that. They count anonymous traffic, and they are blind to the one visitor who matters.

The naive answer, logging every request, is worse than nothing. Prefetches, mail scanners, staff previews and reloads all arrive as requests. Each one becomes a false "the customer opened it" in someone's Slack, and a phone call made on a fact that was never true.

This skill gives a coding agent a visit log built for that: a page-view filter that drops prefetches and Server Actions, bot detection beyond the framework's list, edge geolocation and a parsed browser fingerprint, repeat visitors matched safely, visits grouped into sittings, a first-open announcement, and admin panels that show one customer's history at a time. It also records the same fingerprint at sign-up, so a brief shows where and on what it was started.

Log who opened what, from where and on what, server-side in a Next.js App Router app: an IP, edge-geolocation and parsed-browser fingerprint for every real page view of a shared resource and for lifecycle moments (sign-in link requested, account created, signed in), stored in Postgres/Supabase or Firestore, grouped into sittings, with first-open announcements and admin history panels. Use when: (1) sales, success or an owner must know whether and when someone opened a demo, proposal, quote, report or shared link, (2) a "signed up from" location and device is wanted at sign-up, (3) the user mentions: visit log, visitor tracking, log visits to the database, geolocation headers, x-vercel-ip-city, x-forwarded-for, cf-ipcountry, user agent parsing, userAgent(), isBot, device type, "who opened the link", "customer opened the demo" Slack ping, visit history, last seen, sign-up location. Carries the page-view filter that drops prefetches and Server Actions, bot detection for what the framework's list misses, NULL-safe repeat-visitor matching, the per-visitor announcement throttle, truncation-honest summaries, and the Supabase first-sign-in trap. Next.js App Router on Vercel or Cloudflare; the store is a seam. Not product analytics, not client-side tracking, not a consent manager.

This skill was written by the engineer who has shipped this module. The earlier implementation it was audited against was a visit log on a sales site, behind the shared links a team sends customers and on its sign-in routes. The templates hold the properties such a log has to hold: every stored page view is a document navigation or an App Router client navigation, never a prefetch, an asset or a Server Action; an automated or internal client is labelled, never announced and never counted as a prior visit; the address and the geography come from the edge the request really crossed; a visitor with no IP or no user agent still matches itself; a history that could not be read says so, and a truncated one counts as a lower bound; a first sign-in is the first confirmed one. The three suites state each one, and [`references/provenance.md`](references/provenance.md) has the record.

What it builds:
- A page-view filter that drops prefetches and Server Actions
- Bot detection past the framework's list
- Edge geolocation
- NULL-safe repeat-visitor matching
- Sittings
- First-open announcements
- Admin panels

When to use:
- A shared demo, proposal, quote or report must show who opened it and when, and notify someone the first time; a sign-up should record where and on what.
- An existing visit log needs auditing: false "opened" pings, a wrong first-opened date, visitors counted twice.

Non-negotiables:
- Tracking never touches the response. The fingerprint is read synchronously from the request, and the store, the write and the announcement are deferred with `after()`. A tracking call that awaited a database write would put that write's latency and its failures in front of the customer, so the write path never throws and its result is a flag, not an exception. The write-failure and read-failure paths are in the suite.
- A bot or an internal visit is never announced and never counts as a prior visit. An automated client that follows an emailed link, or a staff member previewing what the customer will see, arrives before the customer does; if either counts, the customer's first open is announced to nobody. Both are stored and flagged, and a test holds that neither makes the customer's own first visit look like a return.
- The IP comes from the edge the request really crossed. `x-forwarded-for` is only trustworthy where the platform overwrites it, and behind another proxy it holds that proxy's address. The adapter is chosen explicitly, once, and `EdgeHeaders.ip` is the only parser in the app.
- The tables are the server's alone. Visit rows are a log about people who never asked to see it, and Supabase's default privileges grant every new table to `anon`, so the migration revokes explicitly and the access posture is checked by applying it against the client roles.
- A failed read is never rendered as an empty history. "Could not load" and "Not opened yet" are different facts to the person deciding whether to call, and only one of them is a reason to call. Both result types carry `failed`, and the panels render it as an alert.
- An IP is stored only with a purpose and a retention period. The address and the geography are personal data whatever the log is for, so the purge ships with the table and the erasure path is written before the first row is stored.

Not this:
- Anonymous traffic, funnels, dashboards: Vercel Web Analytics, PostHog, Plausible. This log is attributed to one resource and one person
- Clicks, scroll depth, time on page: A client analytics SDK. This is server-side and sees requests only
- Cookie consent or an ePrivacy banner: The host's consent manager. This sets no cookies, but an IP is still personal data
- Blocking bots or fraud: Vercel BotID, a WAF, Cloudflare Bot Management. `isBot` here labels, it never blocks
- Hiding a whole site behind one PIN: [`site-pin-gate`](https://github.com/timerise-ai/site-pin-gate). Gating one resource is the host's auth or token layer

What to tell your agent:
- Log who opens our shared demo links: time, city, country, device and browser, stored in Supabase, with a Slack message the first time a customer opens one.
- Record where and on what device each user signed up, and show it on their profile in the admin panel.
- Our visit log says the customer opened the demo when nobody did. Filter out prefetches, bots and staff previews.

Agent eval results (newest per agent and prompt):
- Gemini CLI (gemini-3.8-flash) on v0.1.5, 2026-09-28: pass (Supabase)
- Codex CLI (gpt-6-astra) on v0.1.5, 2026-09-28: pass (Supabase)
- Claude Code (claude-opus-5-5) on v0.1.5, 2026-09-28: pass (Supabase)

Recent releases:
- v0.1.5 (2026-09-28): Fix release, from scoring the prompt-1 agent eval runs against 0.1.4.
- v0.1.4 (2026-09-28): Wording release, from scoring the prompt-1 agent eval runs against 0.1.3. The templates' code is unchanged; one template comment is.
- v0.1.3 (2026-09-28): Fix release, from scoring the prompt-1 agent eval runs against 0.1.2.
