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.
Fixed in the templates
- Automated clients counted as the customer (reproduced) The bot flag was
userAgent().isBotalone, a crawler list. HeadlessChrome,curl,python-requestsand a request with no user agent all came backfalse. 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_atwas 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 recordedsigned_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, includingcurland a request with no user agent;python-requestsparsed as"wearable".
Show 4 moreShow fewer
- 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-foritself, with an"unknown"fallback, beside the shared helper. They would drift the day the edge changed.
- Automated clients counted as the customer (reproduced) The bot flag was
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.
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-foris only trustworthy where the platform overwrites it, and behind another proxy it holds that proxy's address. The adapter is chosen explicitly, once, andEdgeHeaders.ipis 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.
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.
isBothere 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-loggerClaude 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 directoryREADME.mdThis front doorCHANGELOG.mdKeep a Changelog, one section per release, newest firstCLAUDE.mdWhat this repository is and the conventions for editing the skill itselfLICENSEMITreferences/adaptation.mdThe seam contract with the host app: the probe, the rename table, choosing the edge adapter andvia, strings, stylingreferences/fingerprint.mdTypes and formatters,describeVisitor, the edge adapters, bot detection, the page-view contract, the header factsreferences/data-model.mdThe two tables, the Supabase migration, the access posture, the Firestore shape and indexesreferences/rules.mdThe pure decisions: sittings, the announcement matrix, the origin event, truncation-honest summariesreferences/recording.mdThe failure contract, the write and read paths, theafter()entry points, announcingreferences/stores.mdVisitStorefor Supabase, Firestore and memoryreferences/capture.mdWhere to call it: a gated route handler, an App Router page, Supabase Auth sign-inreferences/admin-ui.mdThe history and activity panels, the origin line, the admin pagereferences/operations.mdPrivacy, retention, erasure, health checks, the limits no code removesreferences/testing.mdThe three suites, 56 tests, and how to run them under vitest or bunreferences/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 skillevals/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
- v0.1.5September 28, 2026
Fix release, from scoring the prompt-1 agent eval runs against 0.1.4.
- 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.
- 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.
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.
FirestoreOur 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.
- Built, checks pass
Gemini CLI0.61.0
gemini-3.8-flash
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
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.
- Built, checks pass
Codex CLIcodex-cli 0.158.0
gpt-6-astra
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
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 aserver-onlyalias invitest.config.ts. Tracking goes throughisPageViewandtrackPageVisitwith the factory, andlib/notify/slack.tsnarrows with!isFirstVisit && noveltyKnown, with no retry path. The adapter is a shipped one chosen byVISIT_EDGE, and.env.exampleuses the documented names. The report carries all six handover items. - Built, checks pass
Claude Code2.1.283
claude-opus-5-5
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
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.tsrather than theedgeoption 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:
Step 1
Brief
Tell us what the module must connect to. Takes minutes, in a chat.
Step 2
Prototype in 48 hours
A clickable prototype of your system and a quote, at no cost.
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-loggerBuild 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
Generated from the skill's own files at commit 5d23cb0. Every rule above links to where the repository says it. All skills