Visitor ID¶
Visitor ID generates a stable anonymous identifier for browsers in scenarios where users are not logged in or cannot be reliably identified. The TrueWatch Browser RUM SDK can inject this value into the headers of XHR and Fetch requests that match the rules, allowing API gateways to rate limit by visitor. It also writes the current value into context.visitor_id in RUM events for correlation analysis.
Visitor ID must only be used as a rate-limiting bucket key and an observability correlation key. It must not be used for authentication, tenant isolation, billing, or auditing.
Version requirement: RUM SDK 3.3.15 or later.
Enable Visitor ID¶
Configure visitorId in datafluxRum.init():
datafluxRum.init({
applicationId: "<APPLICATION_ID>",
site: "<PUBLIC_OPENWAY_URL>",
clientToken: "<CLIENT_TOKEN>",
visitorId: {
enabled: true,
header: "x-rum-visitor-id",
match: [
"https://api.example.com/",
/^https:\/\/gateway\.example\.com\//,
function (url) {
return url.includes("/security/")
}
],
ttlHours: 24
}
})
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
enabled |
Boolean | No | false |
Whether Visitor ID is enabled. When not explicitly set to true, no identifier is generated and no request header is injected. |
header |
String | Required when enabled | None | The name of the custom request header to inject. It must be a valid lowercase writable request header. Authentication, CSRF, signature, distributed tracing request headers, and the SDK resource header x-rum-resource-id cannot be used. |
match |
Array | Required when enabled | None | A non-empty URL matching list that supports String, RegExp, and Function. The request header is injected if any entry matches. |
ttlHours |
Number | No | 24 |
The validity period of the Visitor ID, in hours. It must be greater than 0. After expiration, it rotates on the next use without sliding renewal. |
Request Matching Rules¶
Rules of different types in match are matched as follows:
- String: Matches the prefix of the full URL and must belong to the same Origin (protocol, domain, and port). For example,
https://api.example.comdoes not matchhttps://api.example.com.evil.test/; - RegExp: Performs regular expression matching against the full URL;
- Function: Receives the full URL and returns a Boolean.
Exceptions thrown by match functions are isolated by the SDK and the rule is treated as unmatched, without interrupting business requests. The SDK's own data reporting requests are not injected with the Visitor ID Header.
The Browser SDK only handles XHR and Fetch requests initiated after initialization. Page navigation, images, form submissions, sendBeacon, WebSocket, and requests inside Workers are outside the scope of this capability.
Identifier Lifecycle¶
Within the Visitor ID validity period, the same browser reuses the same identifier and does not generate a new value for each request. The SDK stores the identifier in localStorage first; when unavailable, it degrades to sessionStorage and then to the current page's memory:
| Storage Level | Reuse Scope |
|---|---|
localStorage |
Subsequent page visits and tabs under the same origin |
sessionStorage |
Current tab session |
| Page memory | Lifetime of the current page |
A new value is generated in the following cases:
- The current Visitor ID exceeds
ttlHours; - The application explicitly calls
resetVisitorId(); - Browser storage is cleared or inaccessible, and the previous in-memory state has ended.
When multiple valid storage records exist, the SDK selects the most recent one. After rotation, refreshing does not restore older degraded records.
A persistent identifier is generated when local storage succeeds; a session identifier is generated when degraded to session storage or memory. Applications should not parse or rely on the identifier format.
Request Headers and RUM Data¶
The request header is only injected into XHR and Fetch requests that match match. After the feature is enabled, the current Visitor ID is written to RUM events as context.visitor_id. Visitor ID is independent of RUM Session, user identity, and Trace:
sessionSampleRateortracingSampleRatedoes not determine whether the Visitor ID Header is injected;- Visitor ID does not create or force sampling of a RUM Session;
- Unsampled sessions do not report RUM data, but matched business requests can still carry the Visitor ID Header;
resetVisitorId()does not modify RUM Session, user identity, or Trace.
If the application has already set a request header with the same name as visitorId.header, the SDK overwrites that value with the current Visitor ID and outputs a warning to the console. This request header should only be managed by the SDK. Requests that do not match match retain their original headers, including the same-named header returned by injectTraceHeader.
A Request created by a same-origin iframe and sent via the current page's fetch() retains its original request method, request body, and application request headers. When the same XHR's send() fails synchronously due to invalid arguments and is sent again, the Visitor ID is not appended repeatedly. An identifier already injected into the request does not change due to rotation during retries.
The visitor_id in global, View, and event custom contexts does not override the value managed by the SDK. You can still use beforeSend to remove or adjust context before sending.
Custom events buffered before initialization or while waiting for remote configuration are populated with the Visitor ID after the SDK starts. Events that already have an SDK identifier snapshot continue to use that snapshot and do not overwrite other context at the time the event occurred.
If Visitor ID rotation occurs after a request is sent but before the Resource completes, the Resource's context.visitor_id retains the old value actually used in the request header. New requests after rotation and other subsequent RUM events use the new value.
Manual Rotation¶
After a user confirms logout or switches accounts, you can manually rotate the Visitor ID:
After calling it, subsequent matched requests and subsequent RUM events use the new value. Do not call it in 401, unauthenticated request, or other failure branches that may be triggered repeatedly; otherwise, the bucket keeps changing and creates a rate-limiting bypass channel.
CORS and Security Boundaries¶
Custom request headers cause cross-origin requests to trigger a CORS preflight. The API service must allow visitorId.header in Access-Control-Allow-Headers, and match should only cover trusted APIs.
Visitor ID is generated client-side, so users can forge, replace, or delete it. Gateways must not grant permissions based solely on this value. Rate-limiting policies should also provide IP-, session-, or other business-dimension fallback rules for requests with a missing header, and keep mitigation times short to reduce the impact of attackers impersonating other visitor IDs and causing erroneous blocking.
Cloudflare Rate Limiting Example¶
The following Cloudflare Advanced Rate Limiting template contains two rules: count by visitor identifier when the Visitor ID Header is present, and fall back to the source IP when the header is missing. It is an optional gateway deployment example, not an SDK runtime dependency; applications that do not use Cloudflare can ignore it.
Before deployment, you must modify the request path, request header name, threshold, and mitigation time. The rules are disabled by default. Create them first, then read back and check the expressions, counting dimensions, and thresholds before enabling.
Cloudflare Advanced Rate Limiting Template
{
"name": "rum_visitor_id_rate_limit",
"description": "Rate limit matched API requests by the RUM Visitor ID header, with an IP fallback when the header is missing.",
"kind": "zone",
"phase": "http_ratelimit",
"rules": [
{
"description": "RUM Visitor ID API rate limit",
"expression": "(starts_with(http.request.uri.path, \"/api/\") and len(http.request.headers[\"x-rum-visitor-id\"]) > 0)",
"action": "block",
"ratelimit": {
"characteristics": [
"cf.colo.id",
"http.request.headers[\"x-rum-visitor-id\"]"
],
"period": 60,
"requests_per_period": 100,
"mitigation_timeout": 600,
"counting_expression": "(starts_with(http.request.uri.path, \"/api/\") and len(http.request.headers[\"x-rum-visitor-id\"]) > 0)",
"requests_to_origin": true
},
"enabled": false
},
{
"description": "Missing RUM Visitor ID IP fallback",
"expression": "(starts_with(http.request.uri.path, \"/api/\") and len(http.request.headers[\"x-rum-visitor-id\"]) eq 0)",
"action": "block",
"ratelimit": {
"characteristics": ["cf.colo.id", "ip.src"],
"period": 60,
"requests_per_period": 30,
"mitigation_timeout": 600,
"counting_expression": "(starts_with(http.request.uri.path, \"/api/\") and len(http.request.headers[\"x-rum-visitor-id\"]) eq 0)",
"requests_to_origin": true
},
"enabled": false
}
]
}
For the Cloudflare rule format, see Create rate limiting rules via API and Rate limiting parameters.