Shield · Context

Identify users and add context

Out of the box, Shield shows each visit as a session with an IP, a device fingerprint, a risk score, and a recommendation. It does not know which of your users that session belongs to. This page shows how to attach your own user identifier and business context so a high-risk session can be traced back to an account in your system.

The one rule: send an identifier, never an identity

Shield never stores raw emails, usernames, phone numbers, or names.

The identifier you send must be opaque: something you can resolve in your own database, but that means nothing on its own. The field for it is user_id_hash, and it is accepted when the value looks like a hash:

  • hexadecimal, 32 to 128 characters (an MD5, SHA-1, SHA-256, or a UUID with the dashes removed), or
  • base64, 32 characters or more.

Anything else, including a plain username or a short numeric ID, is silently dropped by both the SDK and the API.

The recommended value is a SHA-256 of your internal user ID, computed on the server. Hash the ID, not the email, and add a secret pepper so the hash cannot be reversed by guessing:

// Node.js
const crypto = require('crypto');
const userIdHash = crypto
  .createHash('sha256')
  .update(`${process.env.SHIELD_PEPPER}:${user.id}`)
  .digest('hex');
// PHP
$userIdHash = hash('sha256', getenv('SHIELD_PEPPER') . ':' . $user->id);

To find the account behind a hash you see in the dashboard, compute the same hash for the user in question, or store the hash next to the user record when it is first generated.

1. Google Tag Manager template fields

No code. For sites installed with the official GTM tag template.

Open the FindIP Shield tag and expand Identify the visitor. Select the variables your site already exposes for logged-in users, typically the Data Layer Variables you use for GA4's user_id:

FieldWhat to selectWhat Shield receives
User IDe.g. {{DLV - userId}}user_id_hash — SHA-256 of the ID
Email addresse.g. {{DLV - userEmail}}email_hash — SHA-256 of the lowercased address, plus email_domain
Plane.g. {{DLV - plan}} or a constantplan
Hash saltan optional secret stringmixed into both hashes as SHA-256(salt + ':' + value)

The tag hashes the values in the browser before the SDK loads, so the raw ID and email never leave the page, and every automatic event carries the result. To resolve a hash from the dashboard, compute the same SHA-256 (with the same salt) in your own system. If your site does not expose a user ID variable yet, ask your developer to push one to the dataLayer for logged-in users; it is the same variable GA4 uses. Installed with a GTM Custom HTML tag instead? The GTM page has a version of that snippet with the same fields as placeholders, and the dashboard's Install page generates it with your site key filled in.

2. JavaScript SDK or script tag

Push to the dataLayer before init. Works with or without GTM.

The SDK reads identification keys from the dataLayer for every automatic event. Push them before the SDK initializes: either hash server-side and render the values into the page, or hash in the browser. The dashboard's Install page generates both the npm and the script-tag snippet with an Identify logged-in visitors toggle that adds the browser-hashing version with your site key filled in.

Server-rendered hashes. Compute the hashes on the server (see above) and push them synchronously, above the Shield script tag or the GTM container snippet:

<script>
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    user_id_hash: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08',
    plan: 'pro',
    lead_source: 'google_ads'
  });
</script>

Browser-hashed, npm. Fill the identify object from your app's current user, hash with WebCrypto, push, then initialize:

import { init } from '@findip/shield';

// Identify the logged-in visitor (optional).
// Fill these from your app's current user. Values are hashed in the browser;
// Shield never receives the raw user ID or email address.
const identify = {
  userId: '',   // e.g. currentUser.id
  email: '',    // e.g. currentUser.email
  plan: '',     // e.g. currentUser.plan
  salt: ''      // optional secret; use the same salt when resolving hashes
};

async function sha256(text: string): Promise<string> {
  const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
  return Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, '0')).join('');
}

async function identifyVisitor(): Promise<void> {
  const prefix = identify.salt ? identify.salt + ':' : '';
  const email = identify.email.trim().toLowerCase();
  const context: Record<string, string> = {};

  if (identify.plan) context.plan = identify.plan;
  if (email.includes('@')) {
    context.email_domain = email.split('@').pop() ?? '';
    context.email_hash = await sha256(prefix + email);
  }
  if (identify.userId) context.user_id_hash = await sha256(prefix + identify.userId.trim());

  if (Object.keys(context).length) {
    // The SDK reads these keys from the dataLayer for every automatic event.
    const w = window as unknown as { dataLayer?: object[] };
    (w.dataLayer ??= []).push(context);
  }
}

await identifyVisitor();

init({
  siteKey: 'pub_xxxxxxxxx',
  privacyMode: 'balanced',
  autoTrack: true,
  autoDetectForms: true,
});

For a plain script tag, the SDK must be loaded after the hashes are ready, because hashing is asynchronous. The Install page's script-tag snippet with the toggle enabled does exactly that: it hashes, pushes to the dataLayer, then injects v1.js (or the pinned SRI build) and calls FindIP.init().

Every automatic event (session_start, page_view, and the automatic form events) then carries those values. Two details matter:

  • The SDK takes the first value it finds for each key across all dataLayer entries. Push once, early. A later push with the same key does not replace the earlier one.
  • Only these keys are read from the dataLayer: user_id_hash, email_hash, email_domain, plan, transaction_amount, currency, form_name, lead_source. The custom object and account_age_days are accepted only through FindIP.track().

3. FindIP.track()

JavaScript SDK, or a GTM Custom HTML tag.

Manual events accept the full field list, including custom:

FindIP.track('checkout_started', {
  user_id_hash: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08',
  account_age_days: 412,
  plan: 'pro',
  transaction_amount: 129.0,
  currency: 'USD',
  custom: { cart_items: 3, coupon_applied: true, region: 'eu-west' }
});

Context passed to track() applies to that event only. Manual events do not read the dataLayer, so pass the identifier on each call.

4. WordPress, WooCommerce, and Shopify

The official plugins send coarse page-type context only (product view, checkout view, order received) and never user identifiers. To identify users on those platforms, use the GTM template fields from option 1 if you also run GTM, add the dataLayer push from option 2 to your theme template for logged-in users, or call FindIP.track() from your own code.

Field reference

Any field not in this table is dropped.

FieldTypeAccepted valuesAutomatic events (dataLayer)FindIP.track()
user_id_hashstringhash-shaped, see aboveyesyes
email_hashstringhash-shaped, see aboveyesyes
email_domainstringdomain only, e.g. gmail.com, no @yesyes
planstringup to 256 charactersyesyes
lead_sourcestringup to 256 charactersyesyes
form_namestringup to 256 charactersyesyes
transaction_amountnumberany finite numberyesyes
currencystringISO 4217 code, uppercase, e.g. USDyesyes
account_age_daysnumberany finite numbernoyes
customobjectup to 20 keys, key names up to 64 characters, values are strings up to 256 characters, numbers, or booleansnoyes

Inside custom, keys whose names contain password, card, cvv, ssn, secret, and similar are dropped, and any string value that looks like an email address, phone number, or card number is dropped, wherever it appears.

Where the context appears

Open the site in the Shield dashboard, go to Events, and click an event. The detail drawer shows the stored context under Raw payload (sanitized) as customer_context. Fields that were dropped are shown as null, which is the quickest way to check that a value passed validation.

Troubleshooting

user_id_hash is null in the payloadThe value was not hash-shaped. Send the hex or base64 digest, not the raw ID.
Context missing from automatic events under GTMThe dataLayer push ran after the Shield tag fired. Move it above the GTM container snippet or onto a tag with the Initialization trigger.
A custom value is missingIt was longer than 256 characters, matched a sensitive pattern, or the object already had 20 keys.
Debugging liveAdd debug: true to the init options (or data-debug="true" on the script tag) to log each outgoing event to the browser console.