Infrastructure & security

Visit logger

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.

Released
September 28, 2026
npx skills add timerise-ai/visit-logger
v0.1.5
Current release
11
Reference docs
6
Non-negotiables
MIT
License

The problem

What problem does Visit logger solve?

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.

The module

What does the skill build?

An agent with this skill builds one module for a Next.js App Router app, on your stack. For Visit logger that module consists of:

  • 1. A page-view filter that drops prefetches and Server Actions
  • 2. Bot detection past the framework's list
  • 3. Edge geolocation
  • 4. NULL-safe repeat-visitor matching
  • 5. Sittings
  • 6. First-open announcements
  • 7. Admin panels

Provenance

Where do the rules come from?

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` has the record.

This is the engineering ledger for the person editing the skill, not a story for the reader of the README. It separates three things: what the audit of the earlier implementation changed and how the templates verify it, what was kept deliberately and why it is safe, and what was designed here and has never run in production.

The ledger separates what the audit changed, what was kept on purpose, and what has not run in production yet.

  1. Fixed in the templates

    • Automated clients counted as the customer (reproduced) The bot flag was userAgent().isBot alone, a crawler list. HeadlessChrome, curl, python-requests and a request with no user agent all came back false. Mail-security scanners follow emailed links in headless browsers, so each such open was stored as a human page view, counted in the console, and eligible for the "opened the demo" Slack message.
    • New accounts filed as returning sign-ins The auth callback called a user new when created_at was under five minutes old. Supabase creates the user when the link is requested, so anyone who opened the email more than five minutes later was recorded signed_in, and the "workspace created" Slack message, gated on the same test, never fired for them. Nothing on screen showed it: those customers simply had no "account created" row.
    • A "first opened" date that was the oldest row that fitted The history read the newest 200 page views and took the earliest of them as "first". Past 200, the console printed a wrong first-opened date and counts with no sign of truncation.
    • A failed read shown as "Not opened yet" Every read returned an empty list on error. The panel then said the customer had never looked, which is the one wrong answer a salesperson acts on.
    • Browser version in full, device guessed for scripts (reproduced) Chrome showed as "131.0.0.0". The parser leaves the device type undefined for desktops, and it was defaulted to "desktop" for everything, including curl and a request with no user agent; python-requests parsed as "wearable".
    Show 4 more
    • The rules lived inside the queries, untested The announcement decision was interleaved with its two queries, and the sign-up origin selection sat inside a database read. Only the grouping was a pure function, and nothing tested it.
    • The fingerprint mapped four times Two inserts and two row mappers each spelled out the twelve fingerprint columns. A field added to one would silently miss the others.
    • Comments that promised formats the code did not produce "Kraków, Małopolskie, PL" (the region header is a code) and "Chrome 131 on macOS" (the parser says "Mac OS", version "131.0.0.0").
    • Two client-IP parsers The sign-in route parsed x-forwarded-for itself, with an "unknown" fallback, beside the shared helper. They would drift the day the edge changed.
Read the full record in provenance.md

Non-negotiables

What are the 6 rules the module never breaks?

Every module built from this skill holds these, whoever builds it. The same list is in the skill's README and SKILL.md, so the agent reads it before it writes a line.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

Fit

When should you use it, and when not?

Use it for

  • 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.

Not for

  • Anonymous traffic, funnels, dashboardsInsteadVercel Web Analytics, PostHog, Plausible. This log is attributed to one resource and one person
  • Clicks, scroll depth, time on pageInsteadA client analytics SDK. This is server-side and sees requests only
  • Cookie consent or an ePrivacy bannerInsteadThe host's consent manager. This sets no cookies, but an IP is still personal data
  • Blocking bots or fraudInsteadVercel BotID, a WAF, Cloudflare Bot Management. isBot here labels, it never blocks
  • Hiding a whole site behind one PINInstead`site-pin-gate`. Gating one resource is the host's auth or token layer

Build it yourself

How do I install it?

One command. The skills.sh CLI installs the skill into every skills-compatible agent it finds.

$ npx skills add timerise-ai/visit-logger

Claude Code

Invoke with /visit-logger

Codex CLI

Invoke with $visit-logger

Gemini CLI

Invoke with /skills

Name the agents instead with -a, for example npx skills add timerise-ai/visit-logger -a claude-code -a codex. Or clone the repository into your agent's skills folder. Nothing in it is agent-specific.

What is inside the repository (17 entries)
  • SKILL.mdEntry point: architecture diagram, critical facts, hard rules, quick start, and the reference directory
  • README.mdThis front door
  • CHANGELOG.mdKeep a Changelog, one section per release, newest first
  • CLAUDE.mdWhat this repository is and the conventions for editing the skill itself
  • LICENSEMIT
  • references/adaptation.mdThe seam contract with the host app: the probe, the rename table, choosing the edge adapter and via, strings, styling
  • references/fingerprint.mdTypes and formatters, describeVisitor, the edge adapters, bot detection, the page-view contract, the header facts
  • references/data-model.mdThe two tables, the Supabase migration, the access posture, the Firestore shape and indexes
  • references/rules.mdThe pure decisions: sittings, the announcement matrix, the origin event, truncation-honest summaries
  • references/recording.mdThe failure contract, the write and read paths, the after() entry points, announcing
  • references/stores.mdVisitStore for Supabase, Firestore and memory
  • references/capture.mdWhere to call it: a gated route handler, an App Router page, Supabase Auth sign-in
  • references/admin-ui.mdThe history and activity panels, the origin line, the admin page
  • references/operations.mdPrivacy, retention, erasure, health checks, the limits no code removes
  • references/testing.mdThe three suites, 56 tests, and how to run them under vitest or bun
  • references/provenance.mdThe engineering ledger: what the audit changed and how the templates verify it, what was kept on purpose, and what is new in the skill
  • evals/The prompts an operator types after installing (prompts.md) and one file per agent eval: the skill installed into an empty Next.js app, one prompt, no help, then type-checked, built and tested

Recent releases

  1. v0.1.5September 28, 2026

    Fix release, from scoring the prompt-1 agent eval runs against 0.1.4.

  2. v0.1.4September 28, 2026

    Wording release, from scoring the prompt-1 agent eval runs against 0.1.3. The templates' code is unchanged; one template comment is.

  3. v0.1.3September 28, 2026

    Fix release, from scoring the prompt-1 agent eval runs against 0.1.2.

After installing

What do I tell my agent?

Say what you need in your own words; the skill supplies the how. These are starting points, and the ones we tested say how it went.

  1. 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.

  2. Record where and on what device each user signed up, and show it on their profile in the admin panel.

    Firestore
  3. Our visit log says the customer opened the demo when nobody did. Filter out prefetches, bots and staff previews.

Tested

How does it do in each agent?

We install the skill into an empty Next.js app, give the agent one of the prompts above and no further help, then type-check, build and run the tests it left behind. Nothing is fixed by hand before the checks, and a failing run is published like a passing one. The procedure and every result are public, and the first prompt runs again before each release.

  • Gemini CLI0.61.0

    gemini-3.8-flash

    Built, checks pass
    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.
    Typecheck: passBuild: passTests: pass
    Time
    7 min
    Changed
    28 files, +3,634 lines
    Stack
    Supabase
    Skill
    v0.1.5
    Run
    Sep 28, 2026

    Result fileAgent log

    What we saw

    Rubric 8/8, scored from the final summary. Templates and panels copied without modification, the suites unchanged (56 of 64), the subject from a server-side token with a redirect before logging, the first-open narrowing written as the skill now gives it, the documented variable names, and all six handover items.

  • Codex CLIcodex-cli 0.158.0

    gpt-6-astra

    Built, checks pass
    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.
    Typecheck: passBuild: passTests: pass
    Time
    10 min
    Changed
    42 files, +4,086 lines
    Stack
    Supabase
    Skill
    v0.1.5
    Run
    Sep 28, 2026

    Result fileAgent log

    What we saw

    Rubric 8/8, scored from the diff. It installed supabase-js and vitest (in a multi-line command the log reader missed), and nothing under lib/visits/ differs from the skill. The suites run unchanged among 77, with a server-only alias in vitest.config.ts. Tracking goes through isPageView and trackPageVisit with the factory, and lib/notify/slack.ts narrows with !isFirstVisit && noveltyKnown, with no retry path. The adapter is a shipped one chosen by VISIT_EDGE, and .env.example uses the documented names. The report carries all six handover items.

  • Claude Code2.1.283

    claude-opus-5-5

    Built, checks pass
    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.
    Typecheck: passBuild: passTests: pass
    Time
    4 min
    Changed
    29 files, +3,660 lines
    Stack
    Supabase
    Skill
    v0.1.5
    Run
    Sep 28, 2026

    Result fileAgent log

    What we saw

    Rubric 8/8, scored from the final summary. Templates copied unchanged with host code in files of its own, the three suites unchanged (56 of 56) under vitest, the subject from a server-side token, the documented variable names, and all six handover items in the report. It keeps the failed-read ping and says so. Its edge advice points at the default in track.ts rather than the edge option at the call site, which is wording, not a deviation.

Build it with Timerise

How long does it take, and what does it cost?

We quote this module per project. The price depends on what it has to connect to. The path to a number is short and free:

  1. Step 1

    Brief

    Tell us what the module must connect to. Takes minutes, in a chat.

  2. Step 2

    Prototype in 48 hours

    A clickable prototype of your system and a quote, at no cost.

  3. Step 3

    Build and handoff

    One project price. Source code, documentation and IP are yours.

Two ways to get Visit logger

Build it yourself

Install the skill. Your own agent builds the module.

  • MIT licensed, no strings
  • Runs in Claude Code, Codex CLI and Gemini CLI
  • The same rules our engineers build by
$ npx skills add timerise-ai/visit-logger

Build it with Timerise

Send a brief. We build Visit logger into a system you own.

  • Clickable prototype and a quote within 48 hours, free
  • One project price, no subscription, no commission
  • Source code, documentation and IP handed over
3 years of support included.

Generated from the skill's own files at commit 5d23cb0. Every rule above links to where the repository says it. All skills