Identifying Users
Link sessions to your users for segmentation and filtering
Last updated:
Basic Identification
Call identify() once you know something about your visitor. Pass a flat object with two optional canonical slots (id, email) and any other key/value pairs you want attached to the visitor profile.
// Full identity (id + email + custom properties)
SessionSight.identify({
id: 'user_abc123',
email: '[email protected]',
plan: 'pro',
});
// Email only (pre-internal-id, e.g. newsletter signup)
SessionSight.identify({ email: '[email protected]' });
// Opaque id only (pre-email auth)
SessionSight.identify({ id: 'user_abc123' });
// Bind data without claiming an identity
SessionSight.identify({ plan: 'pro', team: 'platform' });
// No-op
SessionSight.identify({});Both id and email are optional. An empty identify({}) is a no-op. A call with only custom properties (no id, no email) binds data to the current anonymous visitor without claiming an identity.
When to call identify()
Fire identify() on every page where you know who the user is, not only on the sign-in event. A few flows depend on this:
- Consent waffles. If a logged-in user declines cookies and later accepts again, the SDK mints a fresh
ss_vidon re-accept. Callingidentify()on the new page tells the server “this fresh visitor is the same identity as before”; the Visitors page folds the new row into the existing identity via the email or userId alias. - Cross-device. A visitor on desktop and the same email on mobile is one human. The server only knows that if both browsers call
identify(). - Hydration / SPA route changes. Most apps read auth state from a store on hydration; that’s the natural place to fire
identify()as well.
Practically: wherever your code reads the current user from a store or context (if (user) ...), call identify() right there. The SDK no-ops cheaply when nothing has changed since the last call.
id vs email
Use both when you have both. The two slots serve different purposes:
| Slot | What it is | When to use |
|---|---|---|
id | An opaque stable identifier (your app’s internal user ID, a UUID, or a hashed identifier). | When the user has a record in your system (logged in, signed up). |
email | The user’s email, normalized to lowercase and trimmed. | When you know the email (newsletter signup, login by email, magic link). Even without an id. |
Sessions that share either slot collapse into a single identity on the Visitors page. A visitor identified by email on desktop and the same email on mobile is a single human; a visitor identified by id on one device and the same id on another is also a single human. When a later identify() call carries both slots, the two streams converge into one connected component.
Validation
identify() throws synchronously on caller bugs so you see them at the call site instead of as silent ingest failures.
| Rule | Throws when violated |
|---|---|
id must not be email-shaped | Use the email slot for emails. |
id must not contain PII (SSN, credit card, credentials, phone) | Use a non-PII identifier. |
id length | More than MAX_ID_LEN characters (256). |
email shape | Single @, has a TLD, no whitespace. |
email length | More than MAX_EMAIL_LEN characters (320, per RFC 5321). |
| Custom property key length | More than MAX_CUSTOM_KEY_LEN characters (128). |
| Custom property value length | More than MAX_CUSTOM_VALUE_LEN characters (1024) for string values. |
| Custom property count | More than MAX_CUSTOM_PROPERTY_COUNT keys (20). id and email do not count. |
| Custom property value type | Must be string, number, or boolean. |
| Reserved keys | id and email are reserved and routed to dedicated slots automatically. |
Throwing-behavior note
When the payload is built from external or user-driven data, wrap the call in try/catch so one bad value does not interrupt your page.
// When the payload is built from external data, wrap in try/catch
// so a single bad value cannot stop your page from loading.
try {
SessionSight.identify({
id: account.userId,
email: account.email,
plan: account.subscription?.tier,
});
} catch (err) {
console.warn('SessionSight.identify rejected payload', err);
}PII filtering on custom properties
PII in custom property values is silently dropped (not thrown), because unintentional PII (a username field that happens to contain an email, an address field built from form data) is the common case and forcing every caller to defensively sanitize would be heavy.
SessionSight.identify({
id: 'user-1',
plan: 'pro',
ssn: '123-45-6789', // dropped (value matches SSN regex)
apiKey: 'sk-ant-xxx...', // dropped (value matches credential regex)
phone: '555-123-4567', // dropped (value matches phone regex)
});
// Server receives: { id: 'user-1', customProperties: { plan: 'pro' } } Emails in custom property keys and values pass through unchanged. Numeric and boolean values are never inspected.
Scrubbing is best-effort
The SDK detects well-known credential shapes (Stripe, OpenAI, Anthropic, AWS, GitHub, Slack, JWTs, generic sk- / pk- patterns) along with SSNs, credit cards, IBANs, and phone numbers. It cannot catch every possible secret: internal-tool tokens, proprietary API key formats, or custom identifier schemes will pass through unchanged. You remain responsible for not passing secrets into identify(). Treat the built-in scrubbing as a safety net, not a replacement for sanitizing data before you hand it to the SDK.
Merge Semantics
Every identify() call merges into the existing visitor profile. Each field provided in the call overwrites that field’s prior value; fields not provided are preserved. There is no destructive-replace mode and no way to clear a field via identify().
// Profile starts empty.
SessionSight.identify({ email: '[email protected]' });
// Profile: { email: '[email protected]', userId: null, customProperties: {} }
SessionSight.identify({ plan: 'free' });
// Profile: { email: '[email protected]', userId: null,
// customProperties: { plan: 'free' } }
SessionSight.identify({ plan: 'pro', team: 'platform' });
// Profile: { email: '[email protected]', userId: null,
// customProperties: { plan: 'pro', team: 'platform' } }
// `plan` was overwritten; `team` was added; `email` survived.
SessionSight.identify({ id: 'user_abc123' });
// Profile: { email: '[email protected]', userId: 'user_abc123',
// customProperties: { plan: 'pro', team: 'platform' } }
// id slot filled; everything else preserved.Practical consequences:
- Do not re-send fields you do not intend to change. Sending
{ plan: 'pro' }will not wipe a previously-set email. - To represent “plan removed,” pass a sentinel value (
{ plan: 'none' }); the SDK never interpretsnullorundefinedas a clear-the-field signal.
Custom Properties
Use categorical and descriptive values:
| Good Properties | Examples |
|---|---|
| Plan tier | plan: 'pro', plan: 'starter' |
| Account type | accountType: 'enterprise' |
| User role | role: 'admin' |
| Industry | industry: 'saas' |
| Signup source | source: 'organic' |
Do not set monetary values (revenue, LTV, purchase amounts) as user properties. The Insights SDK uses a public API key, so these values could be spoofed from the browser console. Use the Goals SDK with a secret API key for revenue tracking instead.
Visitor ID
The SDK generates a persistent visitorId stored in both localStorage and a first-party cookie (ss_vid) on your domain. The ID is a random UUID with no connection to personal information.
You can access it two ways:
Option A: getVisitorId()
Read the visitor ID from the SDK and send it to your backend:
// Option A: Read it from the SDK
const visitorId = SessionSight.getVisitorId();
// Send to your backend however you like
fetch('/api/your-endpoint', {
headers: { 'x-visitor-id': visitorId },
});Option B: Read the ss_vid Cookie
The SDK automatically sets a first-party cookie, so your backend can read it directly from the request. No frontend code needed:
// Option B: Read the cookie on your server (Node/Express)
const visitorId = req.cookies['ss_vid'];
// Pass to the Flags SDK for segment-based targeting
await FeatureFlags.refresh({
userId: 'user-123',
visitorId,
});This is particularly useful for the Feature Flags SDK, which can use the visitorId for segment-based targeting.
Notes
- In incognito/private browsing, a session-only ID is used (no cookie or localStorage persists)
- Calling
identify()withidoremaillinks the anonymous visitor to your user - The cookie is first-party (
SameSite=Lax), set by your domain. No third-party tracking implications