Fingerprint
Gives each visitor a cookieless identifier without keeping personal data. By default it hashes the anonymized IP (IPv4 /24, IPv6 /48), the user agent reduced to browser, major version and OS, and the site the visitor is on, keyed with a secret salt and rotated daily. The raw IP never reaches the event, the same visitor gets different hashes on different sites and different days, and without the salt the hash cannot be recomputed.
Installation
npm install @walkeros/server-transformer-fingerprint- Integrated
- Bundled
import { startFlow } from '@walkeros/collector';
import { transformerFingerprint } from '@walkeros/server-transformer-fingerprint';
await startFlow({
transformers: {
fingerprint: {
code: transformerFingerprint,
config: {
settings: { salt: process.env.FINGERPRINT_SALT },
},
},
},
});Add to your flow.json:
"transformers": {
"fingerprint": {
"package": "@walkeros/server-transformer-fingerprint",
"config": {
"settings": { "salt": "$env.FINGERPRINT_SALT" }
}
}
}Configuration
This transformer uses the standard transformer config wrapper (consent, data, env, id, ...). For the shared fields see transformer configuration. Package-specific fields live under config.settings and are listed below.
Settings
| Property | Type | Description | More |
|---|---|---|---|
salt | string | Secret key for the hash (HMAC), e.g. "$env.FINGERPRINT_SALT". Without it the hash can be reversed to the IP; a warning is logged. | |
rotate | 'daily' | 'hourly' | 'none' | Rotation window in UTC: the same visitor gets a new hash each window. Default: "daily" | |
ip | any | boolean | Client IP, anonymized to /24 (IPv4) or /48 (IPv6) before hashing. Default: "ingest.ip" when fields is not set | |
userAgent | any | boolean | User agent, reduced to browser, major version and OS before hashing. Default: "ingest.userAgent" when fields is not set | |
site | any | boolean | Site the visitor is on; a URL is reduced to its hostname. Default: "event.source.url" when fields is not set | |
fields | Mapping.Value[] | Extra fields to include in hash (order matters). Each resolved via getMappingValue with source { event, ingest }. When set, named inputs are only used if set explicitly. | |
output | string | Dot-notation path where hash is stored on the event. Default: "user.hash" | |
length | integer | Truncate hash to this length. Default: full 64-char SHA-256 hash |
Mapping
This package does not define custom rule-level settings. For the standard rule fields (consent, condition, data, batch, name, policy) see mapping.
Examples
IP anonymization
The ip input anonymizes the IP before hashing (IPv4 /24, IPv6 /48), so visitors from the same 10.0.42.* subnet with the same browser, major version and OS share a hash within a day. No custom fn needed. Config: { salt: "$env.FINGERPRINT_SALT", length: 16 }
{
"name": "page view",
"data": {
"domain": "www.example.com",
"title": "Privacy Policy",
"id": "/privacy"
},
"id": "ev-1700000602",
"trigger": "load",
"entity": "page",
"action": "view",
"timestamp": 1700000602,
"source": {
"type": "express",
"platform": "server"
}
}return {
"event": {
"name": "page view",
"data": {
"domain": "www.example.com",
"title": "Privacy Policy",
"id": "/privacy"
},
"user": {
"hash": "44d9154b9a9b3792"
},
"id": "ev-1700000602",
"trigger": "load",
"entity": "page",
"action": "view",
"timestamp": 1700000602,
"source": {
"type": "express",
"platform": "server"
}
}
}Server fingerprint
Default server fingerprint: the anonymized ingest.ip, the reduced ingest.userAgent and the site, keyed with settings.salt and rotated daily. Config: { salt: "$env.FINGERPRINT_SALT", length: 16 }. Requires source config.ingest. The hash depends on the salt and the day.
{
"name": "page view",
"data": {
"domain": "www.example.com",
"title": "Getting Started",
"id": "/docs/getting-started"
},
"id": "ev-1700000600",
"trigger": "load",
"entity": "page",
"action": "view",
"timestamp": 1700000600,
"source": {
"type": "express",
"platform": "server"
}
}return {
"event": {
"name": "page view",
"data": {
"domain": "www.example.com",
"title": "Getting Started",
"id": "/docs/getting-started"
},
"user": {
"hash": "158f99cc06e33fd6"
},
"id": "ev-1700000600",
"trigger": "load",
"entity": "page",
"action": "view",
"timestamp": 1700000600,
"source": {
"type": "express",
"platform": "server"
}
}
}What gets hashed
| Input | Default | Reduced to |
|---|---|---|
ip | ingest.ip | 203.0.113.0 (IPv4 /24), 2001:db8:abcd:: (IPv6 /48) |
userAgent | ingest.userAgent | Chrome/124/Windows |
site | event.source.url | the hostname, e.g. www.example.com |
The source must extract ip and userAgent into ingest:
"config": {
"ingest": {
"map": {
"ip": { "key": "ip" },
"userAgent": { "key": "headers.user-agent" }
}
}
}Behind a load balancer, ip is the balancer's address; extract the client from headers.x-forwarded-for instead. An IPv4 client on a dual-stack server (::ffff:203.0.113.7) is treated as its IPv4 address.
Set an input to another mapping value to read it from elsewhere, or to false to leave it out. When an input resolves empty, the transformer logs one warning and hashes without it.
Salt
The salt is the HMAC key. Keep it secret and out of the config file ($env.FINGERPRINT_SALT), and give each deployment its own. Without a salt, anyone who knows the inputs can recompute the hash, and an IP hash can be reversed by trying every address. The transformer still runs without one, but logs a warning at startup.
Rotation
rotate sets how long the same visitor keeps the same hash: daily (default), hourly or none. Windows are UTC days or hours, so every instance agrees on them. Rotation limits tracking across windows while keeping sessions together within one.
Site
The default reads the page URL the walkerOS browser source puts on every event; @walkeros/transformer-ga4 sets it from the hit's page URL (dl), so upgrading to a version that does changes the hash of GA4-decoded events once. Events without it, such as server-side events, need site pointed at what the collector knows: a path parameter (ingest.property), a header (ingest.origin, mapped in the source's config.ingest), or a fixed value for a single-site deployment:
settings: {
salt: process.env.FINGERPRINT_SALT,
site: { value: 'www.example.com' },
}The site matters when one collector with one salt serves several sites: it keeps one visitor's hashes apart across them.
Extra fields
fields adds values to the hash as they resolve, from { event, ingest }. To transform a value first, use fn alone; it receives { event, ingest } and returns the value (a fn next to a key never runs):
settings: {
salt: process.env.FINGERPRINT_SALT,
ip: 'ingest.ip',
userAgent: 'ingest.userAgent',
fields: [{ fn: ({ ingest }) => ingest.region }],
}When fields is set, the named inputs are only used if set explicitly, as above. A config with only fields hashes exactly those fields, plus the salt and the rotation window.
In the complete example
flow-complete.json, the tested example flow, runs fingerprint first in the server's
collector.next and feeds the id to Piwik PRO as its visitor id:
{
"package": "@walkeros/server-transformer-fingerprint",
"config": {
"settings": {
"salt": "$env.FINGERPRINT_SALT",
"length": 16,
"site": [
"event.source.url",
"ingest.origin"
]
}
},
"examples": { … }
}qualityuser.hash is an HMAC over the anonymized IP, browser with major version, OS and site with the secret salt, rotating daily. It is a visitor id for Piwik PRO, not hashing of personal data.
{
"name": "order complete",
"entity": "order",
"action": "complete",
"data": {
"id": "ORD-100",
"total": 149.8,
"currency": "EUR",
"coupon": "WELCOME10"
},
"id": "ev-order-complete",
"trigger": "load",
"timestamp": 1700000000000,
"consent": {
"functional": true,
"marketing": true
},
"globals": {
"language": "en",
"pageGroup": "checkout"
},
"user": {
"device": "d3v1c3",
"session": "s3ss10n",
"id": "cust-42",
"email": "jane@example.com"
},
"source": {
"type": "express",
"platform": "server",
"url": "https://www.example.com/checkout/thanks"
},
"nested": [
{
"entity": "product",
"data": {
"id": "SKU-1",
"name": "Trail Runner",
"price": 129.9,
"quantity": 1
}
},
{
"entity": "product",
"data": {
"id": "SKU-2",
"name": "Running Socks",
"price": 9.95,
"quantity": 2
}
}
]
}return {
"event": {
"name": "order complete",
"entity": "order",
"action": "complete",
"data": {
"id": "ORD-100",
"total": 149.8,
"currency": "EUR",
"coupon": "WELCOME10"
},
"id": "ev-order-complete",
"trigger": "load",
"timestamp": 1700000000000,
"consent": {
"functional": true,
"marketing": true
},
"globals": {
"language": "en",
"pageGroup": "checkout"
},
"user": {
"device": "d3v1c3",
"session": "s3ss10n",
"id": "cust-42",
"email": "jane@example.com",
"hash": "7f1b2860213c9d3d"
},
"source": {
"type": "express",
"platform": "server",
"url": "https://www.example.com/checkout/thanks"
},
"nested": [
{
"entity": "product",
"data": {
"id": "SKU-1",
"name": "Trail Runner",
"price": 129.9,
"quantity": 1
}
},
{
"entity": "product",
"data": {
"id": "SKU-2",
"name": "Running Socks",
"price": 9.95,
"quantity": 2
}
}
]
}
}cookielessId of step fingerprint. Guide chapter qualityResult
The hash is stored at output (default user.hash), length characters long (default: the full 64):
// Input event (before fingerprint transformer)
{ name: 'page view', entity: 'page', action: 'view', ... }
// Output event (after fingerprint transformer)
{ name: 'page view', entity: 'page', action: 'view', user: { hash: '3f1c9a...' }, ... }The transformer never throws.