Visitor identity
A visitor is identified by a visitor id — a UUID stored in visit_visitors.token. There is no fingerprinting and no IP guessing: the id travels in a cookie, a header or a request field.
Resolution order
Section titled “Resolution order”On every tracked request (TrackVisit, POST /visits/collect, Visits::track(), and the Login/Logout listeners and Visits::identify()), TokenResolver::resolve() takes the first valid id from:
- the
X-Visitor-Idheader, or else thevisitor_idinput (query string, form field or JSON body) - the visitor cookie (
visits.cookie.name,visits_visitor_idby default) inheritFrom— only inVisits::track(), see Custom events- the authenticated user’s most recently active visitor — see below
- a newly generated UUID
“Valid” means matching visits.visitor_id.format_regex (a UUID by default). An id in another format is ignored without error and the next source is tried.
The cookie is queued on the response for every tracked request, whichever source the id came from, with a lifetime of cookie.ttl_minutes (two years). It is a normal Laravel cookie: encrypted by EncryptCookies and httpOnly. Cookies are only written to responses that pass through the web group (AddQueuedCookiesToResponse) — on api routes the client has to keep the id itself.
POST /visits/collect also returns the id in its JSON body ({"visitor_id": "..."}) for clients without a cookie jar.
Linking a visitor to a user
Section titled “Linking a visitor to a user”visit_visitors.user_type/user_id hold the user currently known on that device. Three things set them:
- Laravel’s
Loginevent —MergeVisitorIdentityresolves the current visitor id and links the visitor to the user, then firesVisitorIdentified. It also writes the user onto the visitor’s open session if that session has no user yet. Visits::identify($user)— the same merge without a login, see below.- Any tracked request of an authenticated user —
RecordVisitJobcopies$request->user()onto the visitor if it has no user yet. A visitor linked to another user is left alone; onlyLoginandidentify()move it. Linking a visitor that had no user firesVisitorIdentified(since 0.13.8).
visit_sessions.user_type/user_id is a snapshot: set when the session is opened by an authenticated request, or by the merge above if still empty, and never changed afterwards — even if another account logs in on the same device later.
user_type is the user model’s morph class, so a Relation::morphMap() alias is respected. user_id is a string column — UUID and ULID keys work.
Identifying without a login
Section titled “Identifying without a login”A guest checkout matches or creates a User by email or phone — no password, no Auth::login(), no Login event. Link the visitor explicitly:
use Fomvasss\Visits\Facades\Visits;
$user = User::firstOrCreate(['email' => $request->email]);
Visits::identify($user);Don’t dispatch a fake Login event for this. Other Login listeners (sign-in notifications, fraud checks, counters) would treat a form submission as an authentication. The reverse holds too: when a real authentication happens without Auth::login() — a Sanctum token issued after checking a password or an OTP — dispatching Login yourself is accurate and links the visitor:
event(new \Illuminate\Auth\Events\Login('sanctum', $user, false));Logged-in users on Bearer-token APIs
Section titled “Logged-in users on Bearer-token APIs”A cookie only comes back when the browser sends it: always on the same origin, cross-origin only with credentials: 'include' and CORS supports_credentials. A typical token API (Authorization: Bearer ...) never gets the visitor cookie back, so every Visits::track() for a logged-in user would create a new anonymous visitor.
To prevent that, when the request has no id of its own, the resolver uses the authenticated user’s most recently active visitor:
$request->user()->visitorProfiles()->latest('last_seen_at')->value('token');This needs the HasVisits trait on the user model — without it the step is skipped silently:
use Fomvasss\Visits\Concerns\HasVisits;
class User extends Authenticatable{ use HasVisits;}Only the first anonymous visit before any visitor is linked to the user cannot be reconnected this way. $request->user() uses the default guard — call Auth::shouldUse('sanctum') (or authenticate through the matching middleware) before Visits::track() if your API uses another guard.
Logout
Section titled “Logout”By default logout changes nothing: the device stays linked to the last user, which keeps attribution continuous on a personal device. For shared or kiosk devices:
'reset_identity_on_logout' => true,On Logout the visitor’s user_type/user_id are cleared. Session snapshots are not touched. A tracked request of that user that is still waiting in the queue will link the visitor again when it runs.
Custom token resolver
Section titled “Custom token resolver”Ids come from Fomvasss\Visits\Support\TokenResolver, bound in the container through visits.token_resolver. Subclass it to change how ids are generated or validated — for example to accept identifiers from an external system:
namespace App\Support;
use Fomvasss\Visits\Support\TokenResolver;
class VisitorIdResolver extends TokenResolver{ public function generate(): string { return (string) \Illuminate\Support\Str::ulid(); }
public function isValidFormat(string $token): bool { return (bool) preg_match('/^[0-9A-HJKMNP-TV-Z]{26}$/', $token); }}'token_resolver' => \App\Support\VisitorIdResolver::class,Every class that type-hints TokenResolver receives the override. Generated ids must fit the 64-character token column, and generate() must produce ids that isValidFormat() accepts, or every request gets a new visitor. Public methods: Contracts.