|
| 1 | +--- |
| 2 | +title: Anonymous Visitor Tracking |
| 3 | +layout: home |
| 4 | +parent: Concepts |
| 5 | +nav_order: 800 |
| 6 | +--- |
| 7 | + |
| 8 | +# {{ page.title }} |
| 9 | + |
| 10 | +LightNap apps with anonymous-input features (comments, ratings, public submissions, anonymous analytics, A/B test buckets) need persistent identity for visitors who are not logged in. IP-only identification loses identity on every NAT or VPN hop; rolling your own cookie scheme is error-prone. LightNap ships an opt-in middleware that mints and reads a first-party visitor cookie, then exposes the identifier on the current request. |
| 11 | + |
| 12 | +## What the middleware does |
| 13 | + |
| 14 | +On every request, `AnonymousVisitorIdMiddleware`: |
| 15 | + |
| 16 | +1. Looks for the configured cookie (default name `lna_visitor_id`). |
| 17 | +2. If a valid GUID is present, copies it to `HttpContext.Items["AnonymousVisitorId"]`. |
| 18 | +3. Otherwise mints a new GUID, sets the cookie on the response, and stores the new value on `HttpContext.Items`. |
| 19 | + |
| 20 | +Two consumers read the item: |
| 21 | + |
| 22 | +- `WebUserContext` resolves `IUserContext.Kind` to `UserContextKind.AnonymousVisitor` when the request is unauthenticated but a visitor ID is set. `GetActorId()` then returns the visitor ID, which downstream code can use for audit, last-modified-by, or partition-key purposes. |
| 23 | +- The rate-limit partitioner prefers the visitor ID over the remote IP fallback, so unauthenticated users behind shared NATs do not all share a single bucket. |
| 24 | + |
| 25 | +## When to enable it |
| 26 | + |
| 27 | +Turn it on when your app: |
| 28 | + |
| 29 | +- Accepts anonymous user-generated content (comments, votes, public form submissions). |
| 30 | +- Correlates anonymous analytics or experiment buckets across requests. |
| 31 | +- Wants per-visitor rate limiting that survives IP changes. |
| 32 | + |
| 33 | +If your app has no anonymous input surface, leave it off. The middleware is not registered by default — consumers that don't need it pay nothing. |
| 34 | + |
| 35 | +## Enabling |
| 36 | + |
| 37 | +In `Program.cs`, after the existing `Authentication` settings are loaded: |
| 38 | + |
| 39 | +```csharp |
| 40 | +var anonymousVisitorSettings = builder.Configuration |
| 41 | + .GetRequiredSection<AnonymousVisitorSettings>("AnonymousVisitor"); |
| 42 | +builder.Services.AddLightNapAnonymousVisitorTracking(anonymousVisitorSettings, bootstrapLogger); |
| 43 | +``` |
| 44 | + |
| 45 | +And in the pipeline, after `UseAuthentication()` and before the endpoints: |
| 46 | + |
| 47 | +```csharp |
| 48 | +app.UseLightNapAnonymousVisitorTracking(); |
| 49 | +``` |
| 50 | + |
| 51 | +In `appsettings.json`, add: |
| 52 | + |
| 53 | +```jsonc |
| 54 | +"AnonymousVisitor": { |
| 55 | + "CookieName": "lna_visitor_id", |
| 56 | + "Lifetime": "365.00:00:00", |
| 57 | + "SecureOnly": true |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +Both ends are commented out in the stock `Program.cs` to make the opt-in explicit. |
| 62 | + |
| 63 | +## Cookie attributes |
| 64 | + |
| 65 | +| Attribute | Default | Why | |
| 66 | +|--------------|--------------------------|------------------------------------------------------------------------------------------------| |
| 67 | +| `HttpOnly` | `true` | The cookie is server-side only; no script needs to read it. | |
| 68 | +| `SameSite` | `Lax` | Sent on same-site and top-level navigation; not on third-party iframes. | |
| 69 | +| `Secure` | `true` (via `SecureOnly`)| HTTPS only. Set `false` for local HTTP development. | |
| 70 | +| `Expires` | 1 year | Long enough to persist across browser restarts; short enough to limit linkability over time. | |
| 71 | +| `Path` | `/` | Used by the whole app. | |
| 72 | + |
| 73 | +## Privacy and retention |
| 74 | + |
| 75 | +The visitor cookie is anonymous — it does not by itself reveal who a person is. It does, however, link a person's actions over time. If your app **persists** the visitor identifier or `IUserContext.GetIpAddress()` on durable rows (audit log, user-generated content), document a retention policy that matches the rest of your privacy posture and prune older rows accordingly. See the [Audit Log](./audit-log) docs for a maintenance-task pattern. |
| 76 | + |
| 77 | +## How `IUserContext.GetActorId()` interacts |
| 78 | + |
| 79 | +Once the middleware is registered, the contract from the [IUserContext](./project-structure) primitive resolves cleanly without branching: |
| 80 | + |
| 81 | +| Request kind | `Kind` | `GetActorId()` | |
| 82 | +|-----------------------------------|---------------------|--------------------------| |
| 83 | +| Authenticated | `Authenticated` | User ID | |
| 84 | +| Unauthenticated, visitor cookie | `AnonymousVisitor` | Visitor GUID | |
| 85 | +| Unauthenticated, no cookie | `Anonymous` | Throws | |
| 86 | +| Background job / seeder | `System` | `"system"` | |
| 87 | + |
| 88 | +Callers writing audit rows or partitioning anonymous data just call `GetActorId()`; the framework guarantees the right answer. |
0 commit comments