> Part of the walkerOS documentation. Project overview and full index: <https://www.walkeros.io/llms.txt>

# Fingerprint

<!-- -->

[Server](#)[ ](https://github.com/elbwalker/walkerOS/tree/main/packages/server/transformers/fingerprint)

<!-- -->

[Source code](https://github.com/elbwalker/walkerOS/tree/main/packages/server/transformers/fingerprint)[ ](https://www.npmjs.com/package/@walkeros/server-transformer-fingerprint)

<!-- -->

[Package](https://www.npmjs.com/package/@walkeros/server-transformer-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[​](#installation "Direct link to 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" }
    }
  }
}
```

[See bundled mode setup](/preview/pr-720/docs/getting-started/modes/bundled.md) | [CLI reference](/preview/pr-720/docs/apps/cli.md)

## Configuration[​](#configuration "Direct link to Configuration")

This <!-- -->transformer<!-- --> uses the standard <!-- -->transformer<!-- --> config wrapper (consent, data, env, id, ...). For the shared fields see [transformer<!-- --> configuration](/docs/transformers#configuration). Package-specific fields live under `config.settings` and are listed below.

## Settings[​](#settings "Direct link to 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`    | `Array<any>`                    | 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[​](#mapping "Direct link to Mapping")

This package does not define custom rule-level settings. For the standard rule fields (consent, condition, data, batch, name, policy) see [mapping](/docs/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 }

Event

```
{
  "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"
  }
}
```

Out

```
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.

Event

```
{
  "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"
  }
}
```

Out

```
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[​](#what-gets-hashed "Direct link to 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](/preview/pr-720/docs/mapping/value.md) 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[​](#salt "Direct link to 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[​](#rotation "Direct link to 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[​](#site "Direct link to Site")

The default reads the page URL the walkerOS browser source puts on every event. Events without it, such as decoded GA4 hits or 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[​](#extra-fields "Direct link to 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.

## Result[​](#result "Direct link to Result")

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.
