> ## Documentation Index
> Fetch the complete documentation index at: https://jam.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Sanitizers

> Remove personal data from Recording Link console logs, network requests, and WebSocket messages before it leaves your page.

Sanitizers remove personal data from the console logs, network requests, and WebSocket messages that Recording Links capture. They are functions that run in your page while someone records, so Jam receives only what they return. When nobody records, nothing leaves the page and sanitizers don't run.

[Auto-blur](/docs/auto-blur) hides what the video shows. Jam's own redaction of known secret keys, such as `password` and `authorization`, still runs on network requests after your sanitizers.

Sanitizers need a [connected domain](/docs/custom-recording-domain) with Jam installed by the SDK or by script tags.

This sanitizers object uses all four options:

<CodeGroup>
  ```javascript JavaScript theme={"theme":"css-variables"}
  function hideCardNumbers(text) {
    return text && text.replace(/\b\d{16}\b/g, "[card]");
  }

  // An XHR URL can be relative, so resolve it against the page
  function pathOf(url) {
    return new URL(url, location.href).pathname;
  }

  const sanitizers = {
    network: {
      requestSanitizer: function (request) {
        // Drop every request to the payments API, with its response
        if (pathOf(request.url).startsWith("/api/payments")) return null;
        // Header names arrive lowercase
        delete request.headers["x-customer-id"];
        request.body = hideCardNumbers(request.body);
        return request;
      },
      responseSanitizer: function (response) {
        // Response header names arrive lowercase too
        delete response.headers["x-customer-email"];
        if (pathOf(response.url) === "/api/profile") {
          response.body = undefined;
        }
        return response;
      },
      webSocketSanitizer: function (message) {
        // Drop every message on the chat connection, but keep the connection
        if (pathOf(message.url) === "/chat") return null;
        // Messages are text: parse JSON to remove a field from what the page sends
        if (message.action === "send" && message.data.charAt(0) === "{") {
          const payload = JSON.parse(message.data);
          delete payload.sessionToken;
          message.data = JSON.stringify(payload);
        }
        message.data = hideCardNumbers(message.data);
        return message;
      },
    },
    console: {
      logSanitizer: function (log) {
        // Each argument is a value: a string, a number, or an object you can edit
        log.args = log.args.map(function (arg) {
          const text = JSON.stringify(arg);
          return JSON.parse(text.replace(/[\p{L}\p{N}._%+-]+@[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)+/gu, "[email]"));
        });
        return log;
      },
    },
  };
  ```

  ```typescript TypeScript theme={"theme":"css-variables"}
  import type { JamSanitizerOptions } from "@jam.dev/recording-links/sdk";

  function hideCardNumbers(text: string) {
    return text.replace(/\b\d{16}\b/g, "[card]");
  }

  // An XHR URL can be relative, so resolve it against the page
  function pathOf(url: string) {
    return new URL(url, location.href).pathname;
  }

  const sanitizers: JamSanitizerOptions = {
    network: {
      requestSanitizer: (request) => {
        // Drop every request to the payments API, with its response
        if (pathOf(request.url).startsWith("/api/payments")) return null;
        // Header names arrive lowercase
        delete request.headers["x-customer-id"];
        request.body = request.body && hideCardNumbers(request.body);
        return request;
      },
      responseSanitizer: (response) => {
        // Response header names arrive lowercase too
        delete response.headers["x-customer-email"];
        if (pathOf(response.url) === "/api/profile") {
          response.body = undefined;
        }
        return response;
      },
      webSocketSanitizer: (message) => {
        // Drop every message on the chat connection, but keep the connection
        if (pathOf(message.url) === "/chat") return null;
        // Messages are text: parse JSON to remove a field from what the page sends
        if (message.action === "send" && message.data.startsWith("{")) {
          const payload = JSON.parse(message.data);
          delete payload.sessionToken;
          message.data = JSON.stringify(payload);
        }
        message.data = hideCardNumbers(message.data);
        return message;
      },
    },
    console: {
      logSanitizer: (log) => {
        // Each argument is a value: a string, a number, or an object you can edit
        log.args = log.args.map((arg) => {
          const text = JSON.stringify(arg);
          return JSON.parse(text.replace(/[\p{L}\p{N}._%+-]+@[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)+/gu, "[email]"));
        });
        return log;
      },
    },
  };
  ```
</CodeGroup>

## Options

Set only the options you need. Each sanitizer receives a copy of one event. Return the copy, edited or not, to keep it, or return `null` to drop it. Jam reads back only the fields you can change and ignores edits to read-only fields.

<ResponseField name="network.requestSanitizer" type="(request: NetworkRequest) => NetworkRequest | null">
  Receives each request. Return `null` to drop the request and its response.

  <Expandable title="NetworkRequest" defaultOpen>
    <ResponseField name="type" type="&#x22;fetch&#x22; | &#x22;xhr&#x22; | &#x22;resource&#x22;" pre={["read-only"]}>
      How Jam saw the request. A `resource` request is one Jam sees only through timing data, such as an image, a script, a page load, or a request sent before Jam loaded. It has a `url` but no `method`, `headers`, or `body`.
    </ResponseField>

    <ResponseField name="url" type="string">
      The URL, for example `"https://example.com/api/users?id=42"`. Fetch and `resource` URLs are absolute. An XHR URL is the string the page passed to `open()`, so it can be relative, such as `"/api/users"`. To read its parts, parse it with `new URL(request.url, location.href)`.
    </ResponseField>

    <ResponseField name="method" type="string | undefined" pre={["read-only"]}>
      For example `"POST"`. `undefined` for a `resource` request.
    </ResponseField>

    <ResponseField name="headers" type="Record<string, string>">
      Header names are lowercase, whatever case the page used, for example `{ "content-type": "application/json" }`. Jam captures request headers only for fetch requests, so this is `{}` for `xhr` and `resource` requests.
    </ResponseField>

    <ResponseField name="body" type="string | undefined">
      The body as text, for example `'{"email":"ada@example.com"}'`. `undefined` when the request has no body or Jam didn't capture it, such as a file upload or a body over 1 MB.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="network.responseSanitizer" type="(response: NetworkResponse) => NetworkResponse | null">
  Receives the response of each completed fetch and XHR request. Return `null` to drop the response headers and body. Jam keeps the request, status, and timing.

  <Expandable title="NetworkResponse" defaultOpen>
    <ResponseField name="url" type="string" pre={["read-only"]}>
      The URL that `requestSanitizer` returned.
    </ResponseField>

    <ResponseField name="method" type="string | undefined" pre={["read-only"]}>
      The request method, for example `"GET"`.
    </ResponseField>

    <ResponseField name="status" type="number | undefined" pre={["read-only"]}>
      The HTTP status, for example `200`. `undefined` or `0` when no response arrived, such as after a network error or an aborted request.
    </ResponseField>

    <ResponseField name="headers" type="Record<string, string>">
      Header names are lowercase, whatever case the server used.
    </ResponseField>

    <ResponseField name="body" type="string | undefined">
      The body as text. `undefined` when Jam didn't capture it, such as a binary response, a body over 1 MB, or a request that failed.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="network.webSocketSanitizer" type="(message: WebSocketMessage) => WebSocketMessage | null">
  Receives each text WebSocket message the page sends or receives. Return `null` to drop the message. Jam keeps the connection.

  <Expandable title="WebSocketMessage" defaultOpen>
    <ResponseField name="action" type="&#x22;send&#x22; | &#x22;receive&#x22;" pre={["read-only"]}>
      Whether the page sent or received the message.
    </ResponseField>

    <ResponseField name="url" type="string" pre={["read-only"]}>
      The connection URL, for example `"wss://example.com/chat"`.
    </ResponseField>

    <ResponseField name="data" type="string">
      The message text. Jam never captures binary messages.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="console.logSanitizer" type="(log: ConsoleLog) => ConsoleLog | null">
  Receives each console message and uncaught error. Return `null` to drop the log.

  <Expandable title="ConsoleLog" defaultOpen>
    <ResponseField name="level" type="&#x22;error&#x22; | &#x22;warn&#x22; | &#x22;info&#x22; | &#x22;log&#x22; | &#x22;debug&#x22;" pre={["read-only"]}>
      The console method the page called. An uncaught error has the level `"error"` and one argument, its message, for example `"Uncaught Error: boom"`.
    </ResponseField>

    <ResponseField name="args" type="JsonValue[]">
      One entry per argument the page passed, for example `console.log("signed in", { id: 1 })` arrives as `["signed in", { id: 1 }]`. Each entry is a value `JSON.parse` can return: a string, a number, a boolean, `null`, an array, or an object. Objects are copies, so editing them doesn't change your page. An argument JSON can't hold, such as an `Error`, arrives as its text, for example `"Error: boom"`.
    </ResponseField>
  </Expandable>
</ResponseField>

In TypeScript, import these types from `@jam.dev/recording-links/sdk`, or type the whole object as `JamSanitizerOptions` as in the example above.

Request and response bodies are text, so a sanitizer can parse JSON or match a regular expression. Keep a JSON or form body in its format. Jam's own redaction of secret keys reads only JSON and form bodies, so it skips a body that a sanitizer turned into other text.

Jam writes console arguments back as JSON, so return values that JSON can hold. Jam replaces an argument such as `undefined`, a function, or a `BigInt` with `JAM_CUSTOM_SANITIZER_REDACTED`.

Jam calls `requestSanitizer` twice for each fetch and XHR request: when it starts and when it completes. Return the same result both times. To handle kinds of requests differently, check `type`.

Sanitizers don't cover WebSocket connection URLs, console stack traces, fetch error messages, or the page URL. Jam's own redaction still removes secret query parameters from WebSocket URLs.

## When a sanitizer fails

Jam replaces the data with `JAM_CUSTOM_SANITIZER_REDACTED` instead of sending it unsanitized:

* **It throws an error:** Jam replaces the data the sanitizer covers. That is the URL path, headers, and body of a request or response, the data of a WebSocket message, or the arguments of a log. The method, status, timing, and URL origin stay. If `requestSanitizer` throws, Jam replaces the response too, because `responseSanitizer` can no longer see the real URL.
* **It returns nothing or a value that isn't an object:** Jam replaces all the data the sanitizer covers.
* **It returns one field with the wrong type:** Jam replaces only that field. A wrong `url` also replaces the response.
* **It returns a `Promise`:** Jam replaces the data, because it doesn't wait for the `Promise`. Sanitizers must be synchronous.
* **An option has the wrong type or a misspelled name**, for example `requestSanitiser`: Jam replaces the data of every request and WebSocket message, or of every log, depending on the option.
* **`setSanitizers()` gets a misspelled `network` or `console` key**, for example `conosle`: Jam replaces the data of every request, WebSocket message, and log.

<Warning>
  `initialize()` also accepts other SDK options, so it ignores a misspelled `network` or `console` key and sends that data unsanitized. Check those key names, or call `setSanitizers()`.
</Warning>

Don't log or send requests from a sanitizer. Jam drops console logs a sanitizer writes. A request it sends runs the sanitizer again and can loop forever.

## Add sanitizers to your site

Pick the method that matches your install:

* **SDK:** pass the sanitizers to `initialize`.

  ```javascript theme={"theme":"css-variables"}
  import * as jam from "@jam.dev/recording-links/sdk";

  jam.initialize({ teamId: "your-team-id", ...sanitizers });
  ```

* **Script tags:** call `window.jam.capture.setSanitizers(sanitizers)` in a module script placed right after the `capture.js` tag. Jam sends nothing before a recording starts, so this call covers every event.

  ```html theme={"theme":"css-variables"}
  <script type="module" src="https://js.jam.dev/capture.js"></script>
  <script type="module">
    const sanitizers = { /* the object above */ };
    if (typeof window.jam?.capture?.setSanitizers === "function") {
      window.jam.capture.setSanitizers(sanitizers);
    } else {
      console.error("Jam capture.js didn't load first: sanitizers are not set.");
    }
  </script>
  ```

  Keep both tags `type="module"`, in this order, and without `async`. Module scripts run in document order, so `capture.js` has run before your script checks for it. If the error appears, fix the tags before you rely on the sanitizers.

  A call made while a recording runs covers only the events Jam hasn't sent yet. To cover every event, call it right after `capture.js`, or use the SDK.

Jam reads the sanitizers once, when you pass them. Editing the object afterwards has no effect. To change them, call `setSanitizers` again.

`setSanitizers` replaces every sanitizer that `initialize` set, including any that the new call leaves out. Jam doesn't merge the two. To turn every sanitizer off, call `setSanitizers({})`. If you pass `undefined` or `null`, for example a variable that isn't set yet, Jam replaces the data of every request and every log.

Jam doesn't support sanitizers on Google Tag Manager installs. To use sanitizers, install Jam with the SDK or script tags.

## Examples

Each example is one function. They are starting points: adapt them to the data your app handles.

* Put a network example under `network` and a console example under `console` in your sanitizers object.
* Keep helpers such as `PRIVATE_FIELDS` outside the object.
* To use two examples for the same option, merge their code into one function.

### Network examples

#### Remove a header

Jam lowercases header names, so match them in lowercase. Jam captures request headers only for fetch requests.

```javascript theme={"theme":"css-variables"}
requestSanitizer(request) {
  delete request.headers["x-api-key"];
  return request;
}
```

For response headers, use the same code in `responseSanitizer` with `response.headers`.

#### Mask card numbers in request bodies

This example matches 16-digit card numbers written without spaces or dashes.

```javascript theme={"theme":"css-variables"}
requestSanitizer(request) {
  if (request.body) {
    request.body = request.body.replace(/\b\d{16}\b/g, "[card]");
  }
  return request;
}
```

#### Remove fields from a JSON body

This example hides the listed fields at any depth. A body that isn't JSON stays as it is.

```javascript theme={"theme":"css-variables"}
const PRIVATE_FIELDS = ["email", "firstName", "lastName", "phone"];

function hideFields(body) {
  try {
    return JSON.stringify(JSON.parse(body), (key, value) =>
      PRIVATE_FIELDS.includes(key) ? "[hidden]" : value,
    );
  } catch {
    return body;
  }
}

requestSanitizer(request) {
  if (request.body) {
    request.body = hideFields(request.body);
  }
  return request;
}
```

For responses, use the same code in `responseSanitizer` with `response.body`.

#### Remove a token from the URL

Replace `token` with the name of your query parameter.

```javascript theme={"theme":"css-variables"}
requestSanitizer(request) {
  const url = new URL(request.url, location.href);
  url.searchParams.delete("token");
  request.url = url.toString();
  return request;
}
```

#### Drop requests to an endpoint

Return `null` to drop the request and its response. This example drops every request whose path starts with `/api/payments`. It parses the URL first, so a query string that mentions the path doesn't match.

```javascript theme={"theme":"css-variables"}
requestSanitizer(request) {
  const { pathname } = new URL(request.url, location.href);
  if (pathname.startsWith("/api/payments")) {
    return null;
  }
  return request;
}
```

#### Hide a response body

Jam keeps the request, status, and timing. To drop the response headers as well, return `null` instead.

```javascript theme={"theme":"css-variables"}
responseSanitizer(response) {
  if (new URL(response.url, location.href).pathname === "/api/profile") {
    response.body = undefined;
  }
  return response;
}
```

### Console examples

`log.args` is an array with one value per argument: a string, a number, a boolean, `null`, an array, or an object. Objects are copies, so editing one doesn't change your page.

#### Hide emails

This example converts each argument to JSON, replaces emails, and parses the result. It finds emails in strings and inside objects, including addresses with non-English letters such as `josé@example.com`.

```javascript theme={"theme":"css-variables"}
logSanitizer(log) {
  log.args = log.args.map((arg) =>
    JSON.parse(
      JSON.stringify(arg).replace(/[\p{L}\p{N}._%+-]+@[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)+/gu, "[email]"),
    ),
  );
  return log;
}
```

#### Remove fields from logged objects

This example hides the listed fields at any depth in objects and arrays.

```javascript theme={"theme":"css-variables"}
const PRIVATE_FIELDS = ["email", "firstName", "lastName", "phone"];

logSanitizer(log) {
  log.args = log.args.map((arg) =>
    JSON.parse(
      JSON.stringify(arg, (key, value) =>
        PRIVATE_FIELDS.includes(key) ? "[hidden]" : value,
      ),
    ),
  );
  return log;
}
```

#### Drop debug logs

Return `null` to drop a log.

```javascript theme={"theme":"css-variables"}
logSanitizer(log) {
  if (log.level === "debug") {
    return null;
  }
  return log;
}
```

Before you ship, read what happens when a sanitizer [returns the wrong value or throws an error](#when-a-sanitizer-fails).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.