How Identity Detection Works
ListenLayer automatically detects potential person identifiers (emails, names, phones) from various sources:
pp (Possible Person): Emitted when SDK infers identity from forms, network requests, dataLayer, etc.
id (Identify): Emitted when you explicitly call _ll.identifyPerson()
To debug: Run _ll.debug(true) in console, or use Preview Mode
Note: Test emails use @acmecorp.io domain. The SDK filters out @example.com, @test.com, and similar fake email patterns to avoid capturing test data in production.
New: Comprehensive Form Tests (45+ scenarios) - Test redirects, validation, iframes, Marketo-style injection, platform mocks, and edge cases.
New: Company Reveal Tests (7 scenarios) - Test company detection from identity events with real company domains (KickFire, Microsoft, Google, Salesforce). Includes explicit identifyPerson(), unknown domains, personal emails, and multi-company forms.
pp - Possible Person
Auto-detected from 9 sources
Inferred Identity
id - Identify
Explicit API call
Confirmed Identity
Sources (10 Total) + Company Tests + Blocking Tests
๐งช Comprehensive Field Tests
Purpose: Test ALL supported identity fields in a single event to verify the full extraction and LLM pipeline.
Fields tested: email, first_name, last_name, full_name, phone, company, company_domain, zip_code
URL Parameters (pp event)
Click to load page with all fields as URL parameters. Check console and network for pp event.
All Fields (snake_case)
Load with All Fields
email, first_name, last_name, phone, company, company_domain, zip_code
All Fields (camelCase)
Load with camelCase
email, firstName, lastName, phoneNumber, companyName, companyDomain, zipCode
dataLayer Push - pp Event (Flexible Detection)
Push dataLayer events with all fields. These use flexible detection (not person/user_data objects).
All Fields (Top-Level)
Push All Fields
dataLayer.push({
event: 'lead_captured',
email: 'comprehensive-dl@acmecorp.io',
first_name: 'DataLayer',
last_name: 'Test',
phone: '+1-555-333-4444',
company: 'DataLayer Corp',
company_domain: 'datalayercorp.io',
zip_code: '94102'
})
Nested User Object
Push Nested User
dataLayer.push({
event: 'user_action',
user: {
email: 'nested-user@acmecorp.io',
firstName: 'Nested',
lastName: 'User',
phone: '+1-555-444-5555',
company: 'Nested Corp'
}
})
Contact Object
Push Contact Object
dataLayer.push({
event: 'contact_form',
contact: {
email: 'contact-obj@acmecorp.io',
first_name: 'Contact',
last_name: 'Person',
phone: '+1-555-666-7777',
organization: 'Contact Org',
postal_code: '60601'
}
})
dataLayer Push - id Event (Explicit person/user_data)
Push dataLayer events with explicit person or user_data objects. These emit id events (confirmed identity).
person Object (All Fields)
Push person Object
dataLayer.push({
event: 'user_login',
person: {
email: 'person-full@acmecorp.io',
first_name: 'Person',
last_name: 'Full',
phone: '+1-555-888-9999',
company: 'Person Corp',
company_domain: 'personcorp.io',
zip_code: '33101'
}
})
user_data Object (All Fields)
Push user_data Object
dataLayer.push({
event: 'profile_complete',
user_data: {
email: 'userdata-full@acmecorp.io',
first_name: 'UserData',
last_name: 'Complete',
phone_number: '+1-555-111-0000',
company: 'UserData Corp',
address: {
city: 'San Francisco',
region: 'CA',
postal_code: '94102'
}
}
})
user_data + address (GA4 Style)
Push GA4 Style
dataLayer.push({
event: 'purchase',
user_data: {
email_address: 'ga4-style@acmecorp.io',
phone_number: '+1-555-222-3333',
address: {
first_name: 'GA4',
last_name: 'Style',
city: 'Austin',
region: 'TX',
postal_code: '78701',
country: 'US'
}
}
})
_ll.identifyPerson (id Event)
Explicit API call with all supported fields.
All Supported Fields
identifyPerson(all fields)
_ll.identifyPerson({
email: 'identify-full@acmecorp.io',
firstName: 'Identify',
lastName: 'Person',
phone: '+1-555-999-0000',
company: {
name: 'Identify Corp',
domain: 'identifycorp.io'
},
zipCode: '02101'
})
With External ID
identifyPerson(+ externalId)
_ll.identifyPerson({
email: 'external-id@acmecorp.io',
firstName: 'External',
lastName: 'IdUser',
externalId: 'CRM-12345',
company: { name: 'External Corp' }
})
1. Form Submit
pp
SDK monitors form submissions and extracts email/name/phone from form fields.
Trigger: Form submit event with identifiable fields
Source Code: person-registry.ts:registerFromFormData()
Test Form - Submit to trigger pp event
Email-Only Form
Hidden Email Field
1.1 Comprehensive Semantic Field Forms
These forms test all semantic field types that appear in the fields array of pp events:
first_name, last_name, full_name, phone, company, job_title, address, city, state, zip, country, unknown
Full Name Field (name)
fields: [{semantic: "full_name"}]
Name Abbreviations (fname/lname)
fields: [{semantic: "first_name"}, {semantic: "last_name"}]
Given/Family Name
fields: [{semantic: "first_name"}, {semantic: "last_name"}]
Full Name Variants
fields: [{semantic: "full_name"}]
1.2 Business & Organization Fields
Organization Field
fields: [{semantic: "company"}]
Business Name Field
fields: [{semantic: "company"}]
Position/Role Field
fields: [{semantic: "job_title"}]
Title Field
fields: [{semantic: "job_title"}]
1.3 Phone Number Variations
Telephone Field
fields: [{semantic: "phone"}]
Mobile Field
fields: [{semantic: "phone"}]
Cell Phone Field
fields: [{semantic: "phone"}]
Phone Number Field
fields: [{semantic: "phone"}]
1.4 Location & Address Fields
Town Field (city variant)
fields: [{semantic: "city"}]
Province Field (state variant)
fields: [{semantic: "state"}]
Postal Code Field (zip variant)
fields: [{semantic: "zip"}]
ZIP Code Field
fields: [{semantic: "zip"}]
1.5 Autocomplete Attribute Testing
HTML5 autocomplete attributes also trigger semantic type detection. These forms use autocomplete attributes instead of specific field names.
Autocomplete: given-name/family-name
fields: [{semantic: "first_name"}, {semantic: "last_name"}]
Autocomplete: organization
fields: [{semantic: "full_name"}, {semantic: "company"}]
Autocomplete: tel
fields: [{semantic: "full_name"}, {semantic: "phone"}]
Autocomplete: postal-code
fields: [{semantic: "full_name"}, {semantic: "zip"}]
1.6 Label-Based Detection
Fields with generic names but semantic labels/placeholders should still be detected correctly.
Label: "Your Name" โ full_name
fields: [{semantic: "full_name"}]
Label: "Company Name" โ company
fields: [{semantic: "full_name"}, {semantic: "company"}]
Label: "Your Role" โ job_title
fields: [{semantic: "full_name"}, {semantic: "job_title"}]
Label: "Your Phone" โ phone
fields: [{semantic: "full_name"}, {semantic: "phone"}]
2. Network Interception
pp
SDK intercepts XHR and fetch POST requests, scanning request bodies for email patterns. It checks known email fields AND recursively scans all values for email patterns.
Trigger: XHR/fetch POST with email in request body (JSON, FormData, or URLSearchParams)
Source Code: network/extract.ts:extractIdentity() + findEmailInObject()
2.1 Basic Request Types
Fetch POST with JSON body
Send fetch() POST with email
fetch('/api/test', {
method: 'POST',
body: JSON.stringify({
email: 'fetch@acmecorp.io',
name: 'Fetch User'
})
})
XHR POST with JSON body
Send XHR POST with email
xhr.send(JSON.stringify({
email: 'xhr@acmecorp.io',
user_name: 'XHR User'
}))
Fetch with FormData
Send fetch() with FormData
const formData = new FormData();
formData.append('email', 'formdata@acmecorp.io');
fetch('/api/test', { body: formData })
Fetch with URLSearchParams
Send fetch() with URLSearchParams
const params = new URLSearchParams();
params.append('email', 'urlsearch@acmecorp.io');
fetch('/api/test', { body: params })
XHR with Form-Urlencoded
Send XHR with form-urlencoded
xhr.setRequestHeader('Content-Type',
'application/x-www-form-urlencoded');
xhr.send('email=urlencoded@acmecorp.io&name=Test')
PUT request with JSON
Send PUT with email
fetch('/api/user/123', {
method: 'PUT',
body: JSON.stringify({
email: 'put-update@acmecorp.io'
})
})
2.2 Known Email Fields
Tests email detection via known field names (email_address, user_email, etc.).
email_address field
POST { email_address: ... }
{ email_address: 'emailaddress@acmecorp.io' }
user_email field
POST { user_email: ... }
{ user_email: 'useremail@acmecorp.io' }
contact_email field
POST { contact_email: ... }
{ contact_email: 'contactemail@acmecorp.io' }
work_email field
POST { work_email: ... }
{ work_email: 'workemail@acmecorp.io' }
$email field (analytics style)
POST { $email: ... }
{ $email: 'dollaremail@acmecorp.io' }
mail field (short)
POST { mail: ... }
{ mail: 'mailfield@acmecorp.io' }
2.3 Nested Objects
Tests recursive email scanning in nested object structures.
user.email (1 level deep)
POST { user: { email: ... } }
{ user: { email: 'nested1@acmecorp.io' } }
properties.email (analytics)
POST { properties: { email: ... } }
{ properties: { email: 'props@acmecorp.io' } }
traits.email (Segment style)
POST { traits: { email: ... } }
{ traits: { email: 'traits@acmecorp.io' } }
profile.contact.email (2 levels)
POST { profile: { contact: { email: ... } } }
{ profile: { contact:
{ email: 'nested2@acmecorp.io' }
} }
data.form.fields.email (3 levels)
POST deep nested email
{ data: { form: { fields:
{ email: 'nested3@acmecorp.io' }
} } }
checkout.billing.contact.email (4 levels)
POST very deep nested
{ checkout: { billing: { contact:
{ email: 'nested4@acmecorp.io' }
} } }
2.4 Arbitrary Field Names (Value Pattern Match)
Tests email detection based on VALUE pattern matching, regardless of field name. SDK scans all values for email patterns.
{ foo: email } (random name)
POST { foo: 'email@...' }
{ foo: 'arbitrary-foo@acmecorp.io', bar: 123 }
{ data: email } (generic)
POST { data: 'email@...' }
{ data: 'arbitrary-data@acmecorp.io' }
{ x: email } (single letter)
POST { x: 'email@...' }
{ x: 'arbitrary-x@acmecorp.io' }
{ value: email } (generic)
POST { value: 'email@...' }
{ value: 'arbitrary-value@acmecorp.io' }
{ identifier: email } (ID-like)
POST { identifier: 'email@...' }
{ identifier: 'arbitrary-id@acmecorp.io' }
{ primary: email } (generic)
POST { primary: 'email@...' }
{ primary: 'arbitrary-primary@acmecorp.io' }
2.5 Nested Arbitrary Fields
Tests email detection in nested objects with non-standard field names.
{ payload: { val: email } }
POST nested arbitrary
{ payload: { val: 'nestarb1@acmecorp.io' } }
{ response: { result: { item: email } } }
POST deep arbitrary
{ response: { result: {
item: 'nestarb2@acmecorp.io'
} } }
{ meta: { custom_xyz: email } }
POST meta arbitrary
{ meta: {
custom_xyz: 'nestarb3@acmecorp.io'
} }
{ config: { settings: { abc: email } } }
POST config arbitrary
{ config: { settings: {
abc: 'nestarb4@acmecorp.io'
} } }
2.6 With Additional PII (Name, Phone, Company)
Tests extraction of additional identity fields (first_name, last_name, phone, company).
Email + first_name + last_name
POST with names
{ email: 'names@acmecorp.io',
first_name: 'John', last_name: 'Smith' }
Email + full name
POST with full name
{ email: 'fullname@acmecorp.io',
name: 'Jane Doe' }
Email + phone
POST with phone
{ email: 'phone@acmecorp.io',
phone: '+1-555-123-4567' }
Email + company
POST with company
{ email: 'company@acmecorp.io',
company: 'Acme Corp' }
Full profile (all fields)
POST full profile
{ email, first_name, last_name,
phone, company }
CamelCase field names
POST camelCase fields
{ userEmail, firstName, lastName,
phoneNumber, companyName }
2.7 Edge Cases
Tests edge cases and special scenarios.
Email at 5 levels deep (max)
POST at max depth
{ a: { b: { c: { d: { e: {
email: 'maxdepth@acmecorp.io'
} } } } } }
Email at 6 levels (beyond limit)
POST beyond max depth
6 levels deep - SDK scans to depth 5
Should NOT detect this email
Multiple emails (first wins)
POST multiple emails
{ email: 'first@acmecorp.io',
secondary_email: 'second@acmecorp.io' }
Email in array (not supported)
POST email in array
{ contacts: ['array@acmecorp.io'] }
Arrays not recursed - should NOT detect
Empty email field
POST empty email
{ email: '' }
Empty - should NOT detect
Invalid email format
POST invalid email
{ email: 'not-an-email' }
Invalid format - should NOT detect
Note: Network interception scans request bodies using two strategies: 1) Check known email field names (email, user_email, etc.), 2) Recursively scan all object values for email patterns up to 5 levels deep. Arrays are not recursed.
3. dataLayer Push
id pp
SDK monitors dataLayer.push() calls with two-tier detection :
id events: Explicit person/user_data objects โ high confidence (1.0)
pp events: Flexible scanning of all other fields โ standard confidence (0.9)
Source Code: datalayer/listener.ts:processEvent()
3.1 Explicit Person Fields (id events)
When dataLayer events include explicit person fields (person object or user_data), the SDK emits an id event with high confidence.
person object (full)
Push person object
dataLayer.push({
event: 'login',
person: {
email: 'dl-person@acmecorp.io',
first_name: 'John',
last_name: 'Doe',
phone: '+1-555-123-4567'
}
})
// Emits: id (explicit person)
person object (email only)
Push person (email only)
dataLayer.push({
event: 'user_signup',
person: {
email: 'dl-person-email@acmecorp.io'
}
})
// Emits: id (person.email)
user_data object (GA4 style)
Push user_data
dataLayer.push({
event: 'user_identified',
user_data: {
email: 'dl-userdata@acmecorp.io',
phone_number: '+1-555-234-5678'
}
})
// Emits: id (GA4 user_data)
user_data with address
Push user_data with address
dataLayer.push({
event: 'profile_complete',
user_data: {
email: 'dl-userdata-addr@acmecorp.io',
address: {
first_name: 'Jane',
last_name: 'Smith',
city: 'Austin'
}
}
})
// Emits: id (user_data + address)
person with name field
Push person with name
dataLayer.push({
event: 'identify',
person: {
email: 'dl-person-name@acmecorp.io',
name: 'Full Name Person'
}
})
// Emits: id (person + name)
user_data phone only (no email)
Push user_data (phone only)
dataLayer.push({
event: 'phone_verify',
user_data: {
phone_number: '+1-555-987-6543'
}
})
// Emits: id (phone as identifier)
3.2 Flexible Detection (pp events)
When dataLayer events do NOT have person/user_data, the SDK scans all values for email patterns and emits pp events.
Top-level email field
Push email to dataLayer
dataLayer.push({
event: 'user_identified',
email: 'datalayer@acmecorp.io',
user_type: 'lead'
})
// Emits: pp (flexible scan)
user object (not user_data)
Push user object
dataLayer.push({
event: 'login',
user: {
email: 'user.object@acmecorp.io',
name: 'DataLayer User'
}
})
// Emits: pp (user != user_data)
Nested email (2 levels)
Push nested email
dataLayer.push({
event: 'form_complete',
form_data: {
fields: {
email_address: 'nested@acmecorp.io'
}
}
})
// Emits: pp (nested scan)
customer object
Push customer object
dataLayer.push({
event: 'customer_update',
customer: {
email: 'dl-customer@acmecorp.io',
tier: 'gold'
}
})
// Emits: pp (customer != person)
contact object
Push contact object
dataLayer.push({
event: 'contact_form',
contact: {
email: 'dl-contact@acmecorp.io',
subject: 'Inquiry'
}
})
// Emits: pp (contact != person)
visitor object
Push visitor object
dataLayer.push({
event: 'visitor_identified',
visitor: {
email: 'dl-visitor@acmecorp.io'
}
})
// Emits: pp (visitor != person)
3.3 Arbitrary Field Names (Value Pattern Match)
Tests email detection based on VALUE pattern, regardless of field name.
{ foo: email } (random)
Push { foo: 'email@...' }
dataLayer.push({
event: 'test',
foo: 'dl-arbitrary-foo@acmecorp.io'
})
{ data: email } (generic)
Push { data: 'email@...' }
dataLayer.push({
event: 'track',
data: 'dl-arbitrary-data@acmecorp.io'
})
{ x: email } (single letter)
Push { x: 'email@...' }
dataLayer.push({
event: 'custom',
x: 'dl-arbitrary-x@acmecorp.io'
})
{ val: email } nested
Push { payload: { val: 'email@...' } }
dataLayer.push({
event: 'data_push',
payload: { val: 'dl-arb-nested@acmecorp.io' }
})
3.4 Deep Nesting
Tests recursive scanning through deeply nested objects.
3 levels deep
Push 3-level nested
dataLayer.push({
event: 'deep3',
a: { b: { c: { email: 'dl-deep3@acmecorp.io' } } }
})
4 levels deep
Push 4-level nested
dataLayer.push({
event: 'deep4',
a: { b: { c: { d: { email: 'dl-deep4@acmecorp.io' } } } }
})
properties.traits.email
Push properties.traits
dataLayer.push({
event: 'segment_identify',
properties: {
traits: {
email: 'dl-traits@acmecorp.io'
}
}
})
context.user.profile.email
Push context.user.profile
dataLayer.push({
event: 'app_context',
context: {
user: {
profile: {
email: 'dl-context@acmecorp.io'
}
}
}
})
3.5 With Additional PII
Tests extraction of names, phone, and company along with email.
Email + names (top level)
Push with names
dataLayer.push({
event: 'signup',
email: 'dl-names@acmecorp.io',
first_name: 'Test',
last_name: 'User'
})
Email + phone (top level)
Push with phone
dataLayer.push({
event: 'verify',
email: 'dl-phone@acmecorp.io',
phone: '+1-555-999-8888'
})
Email + company
Push with company
dataLayer.push({
event: 'b2b_lead',
email: 'dl-company@acmecorp.io',
company: 'DataLayer Corp'
})
Full profile (all fields)
Push full profile
dataLayer.push({
event: 'complete_profile',
email: 'dl-full@acmecorp.io',
first_name: 'Complete',
last_name: 'Profile',
phone: '+1-555-777-6666',
company: 'Full Corp'
})
3.6 Edge Cases
GTM event (gtm.start)
Push gtm.start
dataLayer.push({
'gtm.start': Date.now(),
event: 'gtm.js'
})
// Should be IGNORED (GTM system event)
Empty person object
Push empty person
dataLayer.push({
event: 'test',
person: {}
})
// No email - should NOT emit id
Multiple emails (first wins)
Push multiple emails
dataLayer.push({
event: 'multi',
email: 'dl-first@acmecorp.io',
secondary_email: 'dl-second@acmecorp.io'
})
person + top-level email (id wins)
Push person + email
dataLayer.push({
event: 'dual',
person: { email: 'dl-person-wins@acmecorp.io' },
email: 'dl-toplevel@acmecorp.io'
})
// person takes priority โ id event
Two-Tier Priority: Explicit person/user_data are checked first โ id event. If not found, flexible scanning โ pp event. Objects like user, customer, visitor, contact are NOT considered explicit and go through flexible detection.
4. URL Parameters
pp
SDK scans URL query parameters for email addresses on page load. It checks both explicit email parameter names AND scans all parameter values for email patterns.
Trigger: Page load with email in any query parameter
Source Code: person-registry.ts:registerFromUrlParams()
Standard Email Parameters
Short/Ambiguous Parameters
These test email pattern detection in non-obvious parameter names.
Marketing/Platform Parameters
Common email parameters from marketing platforms and CRMs.
Arbitrary Parameters (Value Pattern Match)
These test detection based on email pattern in value, regardless of parameter name.
Note: URL param detection happens on page load. Click a link to test - you'll navigate to this page with the email in the URL. The SDK detects emails both by parameter name (email, user_email, etc.) AND by scanning all parameter values for email patterns.
5. Chat Platforms
pp
SDK captures visitor email via _ll.chat.emailCaptured() method.
Trigger: Chat platform callback with visitor email
Source Code: chat/methods.ts:emailCaptured() โ extractAndEmitFromPayload()
Simulate Drift email capture
Simulate Drift Callback
_ll.chat.emailCaptured({
platform: 'dft',
email: 'drift@acmecorp.io',
firstName: 'Drift',
lastName: 'Visitor'
})
Simulate Intercom email capture
Simulate Intercom Callback
_ll.chat.emailCaptured({
platform: 'ic',
email: 'intercom@acmecorp.io',
firstName: 'Intercom',
lastName: 'User'
})
Simulate LiveChat email capture
Simulate LiveChat Callback
_ll.chat.emailCaptured({
platform: 'lc',
email: 'livechat@acmecorp.io'
})
6. Scheduling Platforms
pp
SDK captures emails via _ll.scheduling.scheduled() method.
Trigger: Booking confirmation with invitee email
Source Code: scheduling/methods.ts:schedulingScheduled() โ extractAndEmitFromPayload()
Simulate Calendly booking
Simulate Calendly Event
_ll.scheduling.scheduled({
platform: 'cal',
email: 'calendly@acmecorp.io',
firstName: 'Calendly',
lastName: 'User',
scheduledTime: '2024-01-15T10:00:00Z'
})
Simulate HubSpot Meeting
Simulate HubSpot Booking
_ll.scheduling.scheduled({
platform: 'hsm',
email: 'hubspot-meeting@acmecorp.io',
firstName: 'HubSpot',
lastName: 'Lead',
scheduledTime: '2024-01-15T10:00:00Z'
})
Simulate Chili Piper booking
Simulate Chili Piper Event
_ll.scheduling.scheduled({
platform: 'cp',
email: 'chilipiper@acmecorp.io',
scheduledTime: '2024-01-15T10:00:00Z'
})
7. eCommerce Events
id pp
SDK extracts identity from ecommerce events using two strategies:
id events: Explicit person/user_data fields in high-value events โ confidence 1.0
pp events: Generic detection from nested objects (billing, shipping, customer) โ confidence 0.9
High-Value Events: purchase, begin_checkout, add_payment_info, add_shipping_info
Source Code: ecommerce/listener.ts:processIdentity()
7.1 Explicit Person Fields (id events)
When ecommerce events include explicit person fields (person object, user_data, or direct email/phone), the SDK emits an id event with high confidence.
Purchase with person object
Purchase (person field)
dataLayer.push({
event: 'purchase',
person: {
email: 'buyer@acmecorp.io',
first_name: 'John',
last_name: 'Buyer',
phone: '+1-555-123-4567'
},
ecommerce: { ... }
})
// Emits: id (confirmed identity)
Purchase with GA4 user_data + company
Purchase (user_data)
dataLayer.push({
event: 'purchase',
user_data: {
email: 'ga4user@acmecorp.io',
phone_number: '+1-555-234-5678',
company: 'Acme Corp',
company_domain: 'acmecorp.io',
address: { first_name: 'Jane' }
},
ecommerce: { ... }
})
// Emits: id (GA4 + company)
Begin checkout with person
Begin Checkout (person)
dataLayer.push({
event: 'begin_checkout',
person: {
email: 'checkout@acmecorp.io',
name: 'Sam Checkout'
},
ecommerce: { ... }
})
// Emits: id (explicit person)
Add shipping with person
Add Shipping (person)
dataLayer.push({
event: 'add_shipping_info',
person: {
email: 'shipping@acmecorp.io',
first_name: 'Alex',
address: { city: 'Austin' }
},
ecommerce: { ... }
})
// Emits: id (with address)
Checkout with user_data
Checkout (user_data)
dataLayer.push({
event: 'begin_checkout',
user_data: {
email: 'checkout-ga4@acmecorp.io',
phone_number: '+1-555-444-5555'
},
ecommerce: { ... }
})
// Emits: id (GA4 user_data)
Shipping with user_data
Add Shipping (user_data)
dataLayer.push({
event: 'add_shipping_info',
user_data: {
email: 'shipping-ga4@acmecorp.io',
address: {
first_name: 'Ship',
last_name: 'Recipient'
}
},
ecommerce: { ... }
})
// Emits: id (user_data + address)
Payment with user_data
Add Payment (user_data)
dataLayer.push({
event: 'add_payment_info',
user_data: {
email: 'payment-ga4@acmecorp.io',
phone_number: '+1-555-666-7777'
},
ecommerce: { ... }
})
// Emits: id (GA4 standard)
SDK purchase with person
_ll.ecommerce.purchase (person)
_ll.ecommerce.purchase({
person: {
email: 'sdk-buyer@acmecorp.io',
first_name: 'SDK',
last_name: 'Customer'
},
transaction_id: 'SDK-TXN-001',
value: 399.99
})
// Emits: id (SDK method)
7.2 Flexible Detection (pp events)
When ecommerce events contain person data in any location (SDK methods, nested objects, top-level fields not in person/user_data), the SDK scans for email patterns and emits a pp event.
SDK purchase with customer_email
_ll.ecommerce.purchase
_ll.ecommerce.purchase({
order_id: 'ORD-12345',
customer_email: 'buyer@acmecorp.io',
total: 99.99,
items: [...]
})
// Emits: pp (SDK method)
SDK checkout with email
_ll.ecommerce.beginCheckout
_ll.ecommerce.beginCheckout({
email: 'checkout@acmecorp.io',
value: 149.99,
items: [...]
})
// Emits: pp (SDK method)
SDK payment with billing_email
_ll.ecommerce.addPaymentInfo
_ll.ecommerce.addPaymentInfo({
billing_email: 'billing@acmecorp.io',
payment_type: 'credit_card'
})
// Emits: pp (SDK method)
SDK purchase with full person info (no person/user_data wrapper)
_ll.ecommerce.purchase (person at top level)
_ll.ecommerce.purchase({
order_id: 'ORD-67890',
total: 249.99,
email: 'sarah.chen@acmecorp.io',
first_name: 'Sarah',
last_name: 'Chen',
phone: '+1-555-867-5309',
company: 'Acme Corp',
items: [...]
})
// Emits: pp (NOT id โ no person/user_data wrapper)
Purchase with billing object
Purchase (billing nested)
dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: 'TXN-002',
billing: {
email: 'billing-nested@acmecorp.io'
}
}
})
// Emits: pp (nested object)
Checkout with shipping object
Checkout (shipping nested)
dataLayer.push({
event: 'begin_checkout',
ecommerce: {
shipping: {
email_address: 'shipping-nested@acmecorp.io'
}
}
})
// Emits: pp (nested scan)
Purchase with customer + company
Purchase (customer object)
dataLayer.push({
event: 'purchase',
customer: {
email: 'customer-obj@acmecorp.io',
first_name: 'Customer',
company: 'Customer Corp',
company_domain: 'customercorp.io'
},
ecommerce: { ... }
})
// Emits: pp (with company)
Payment with deep nesting
Payment (deep nested)
dataLayer.push({
event: 'add_payment_info',
ecommerce: {
checkout: {
billing_details: {
contact_info: {
email: 'deep-nested@acmecorp.io'
}
}
}
}
})
// Emits: pp (deep scan)
Payment with top-level email
Payment (top-level email)
dataLayer.push({
event: 'add_payment_info',
email: 'payment-toplevel@acmecorp.io',
phone: '+1-555-345-6789',
ecommerce: { ... }
})
// Emits: pp (not in person/user_data)
Priority: Explicit person fields are checked first. If found, an id event is emitted and generic detection is skipped. This ensures high-confidence data from documented fields takes precedence.
8. Video Platforms
pp
SDK captures lead information from video platform features like Wistia Turnstile forms, Vidyard viewer gates, or any gated video content. Emits a vle (video_lead) event AND a pp event when email is present.
Trigger: Video platform email gate, form submission, or manual _ll.video.lead() call
Source Code: video/methods.ts:videoLead() โ extractAndEmitFromPayload()
Wistia Turnstile lead capture
Simulate Wistia Turnstile
_ll.video.lead({
platform: 'wis',
email: 'wistia@acmecorp.io',
firstName: 'Wistia',
lastName: 'Viewer',
video_id: 'abc123'
})
// Emits: vle + pp (video lead)
Vidyard email gate
Simulate Vidyard Gate
_ll.video.lead({
platform: 'vy',
email: 'vidyard@acmecorp.io',
firstName: 'Vidyard',
lastName: 'Lead',
title: 'Product Demo'
})
// Emits: vle + pp (video lead)
Generic video gate
Simulate Generic Gate
_ll.video.lead({
email: 'videogated@acmecorp.io',
firstName: 'Video',
lastName: 'Lead',
video_id: 'webinar-2024'
})
// Emits: vle + pp (video lead)
9. Custom Events
pp
When _ll.track() is called, the SDK emits a cus (custom) event AND automatically scans the params for email fields. If an email is found, a pp event is also emitted with src: 'custom'.
Trigger: _ll.track('event', { email: '...' })
Events Emitted: cus (always) + pp (if email found)
Source Code: api/track.ts:track() โ extractAndEmitFromPayload()
Custom event with email property
Track with email
_ll.track('lead_captured', {
email: 'custom@acmecorp.io',
source: 'landing_page',
campaign: 'spring_2024'
})
// Emits: cus + pp (src: 'custom')
Custom event with user_email
Track with user_email
_ll.track('signup_complete', {
user_email: 'signup@acmecorp.io',
plan: 'pro'
})
// Emits: cus + pp (src: 'custom')
Custom event with nested email
Track with nested email
_ll.track('form_submitted', {
form_id: 'contact-123',
data: {
email_address: 'nested-custom@acmecorp.io'
}
})
// Emits: cus + pp (src: 'custom')
Custom event WITHOUT email (no pp)
Track without email
_ll.track('button_clicked', {
button_id: 'cta-signup',
page: 'pricing'
})
// Emits: cus ONLY (no pp - no email)
Dual Event Emission: Each _ll.track() call now emits a cus event (always) AND a pp event (when email is detected in params). The pp event has src: 'custom' to indicate it came from a custom event.
10. Explicit Identify (id event)
id
Explicit API call to identify a known person. Emits an id event (not pp).
Trigger: _ll.identifyPerson({ email: '...' })
Event Type: id (identity confirmed)
Identify with email only
identifyPerson (email)
_ll.identifyPerson({
email: 'identified@acmecorp.io'
})
Identify with full profile
identifyPerson (full)
_ll.identifyPerson({
email: 'full-profile@acmecorp.io',
first_name: 'Jane',
last_name: 'Smith',
phone: '+1-555-987-6543',
company: 'Acme Corp',
title: 'VP Marketing'
})
Identify with external ID
identifyPerson (external_id)
_ll.identifyPerson({
email: 'external-id@acmecorp.io',
external_id: 'CRM-USER-12345',
source: 'salesforce'
})
Identify with custom properties
identifyPerson (custom props)
_ll.identifyPerson({
email: 'custom-props@acmecorp.io',
properties: {
plan_type: 'enterprise',
signup_date: '2024-01-15',
lifetime_value: 5000
}
})
Identify with address fields
identifyPerson (address)
_ll.identifyPerson({
email: 'address@acmecorp.io',
first_name: 'Sarah',
last_name: 'Location',
address: '123 Main Street',
address2: 'Suite 400',
city: 'San Francisco',
state: 'CA',
country: 'USA',
postal_code: '94102'
})
Identify with full name
identifyPerson (fullName)
_ll.identifyPerson({
email: 'fullname@acmecorp.io',
full_name: 'Dr. Michael Richardson III',
title: 'Chief Technology Officer'
})
Identify with custom fields
identifyPerson (customFields)
_ll.identifyPerson({
email: 'custom@acmecorp.io',
first_name: 'Custom',
last_name: 'User',
customFields: {
plan_tier: 'enterprise',
account_manager: 'John Smith',
renewal_date: '2025-01-15',
seats: 50,
is_beta_user: true
}
})
Identify with external ID + phone
identifyPerson (external + phone)
_ll.identifyPerson({
email: 'extphone@acmecorp.io',
phone: '+1-800-555-1234',
external_id: 'HUB-CONTACT-98765',
first_name: 'Hub',
last_name: 'Contact'
})
COMPLETE PROFILE - All Fields + Custom Fields
identifyPerson (ALL FIELDS)
_ll.identifyPerson({
email: 'complete@acmecorp.io',
first_name: 'Complete',
last_name: 'Profile',
full_name: 'Complete Profile Jr.',
phone: '+1-555-COMPLETE',
title: 'Director of Everything',
address: '456 Enterprise Blvd',
address2: 'Floor 42, Office A',
city: 'New York',
state: 'NY',
country: 'United States',
postal_code: '10001',
external_id: 'CRM-COMPLETE-12345',
company: {
domain: 'acmecorp.io',
name: 'Acme Corporation'
},
customFields: {
lead_source: 'Trade Show',
lead_score: 95,
industry: 'Technology',
employee_count: '500-1000',
is_qualified: true,
assigned_rep: 'sales@acmecorp.io',
notes: 'VIP customer, handle with care'
}
})
11. Company/Organization Data
company
Tests company/organization data detection across all identity sources.
Supported Field Patterns: company, company_name, organization, organisation, org, business, business_name
Events: pp (auto-detected with company field) or id (explicit with structured company)
11.1 Explicit identifyPerson with Company (id event)
Tests the structured company format: { domain: string, name?: string }
Company with domain + name
identifyPerson (full company)
_ll.identifyPerson({
email: 'ceo@acmecorp.io',
company: {
domain: 'acmecorp.io',
name: 'Acme Corporation'
}
})
Company with domain only
identifyPerson (domain only)
_ll.identifyPerson({
email: 'user@bigtech.com',
company: {
domain: 'bigtech.com'
}
})
Full profile with company
identifyPerson (full + company)
_ll.identifyPerson({
email: 'vp@startup.io',
firstName: 'Sarah',
lastName: 'Johnson',
phone: '+1-555-111-2222',
company: {
domain: 'startup.io',
name: 'Startup Inc'
}
})
11.2 Network Requests with Company (pp event)
Tests company field detection in XHR/fetch request bodies using various field patterns.
company field
POST with company
{ email: 'emp@acmecorp.io',
company: 'Acme Corporation' }
company_name field
POST with company_name
{ email: 'sales@bizco.io',
company_name: 'BizCo Inc' }
organization field
POST with organization
{ email: 'dev@nonprof.org',
organization: 'Tech Nonprofit' }
organisation field (UK spelling)
POST with organisation
{ email: 'london@ukbiz.co.uk',
organisation: 'UK Business Ltd' }
org field (short)
POST with org
{ email: 'quick@short.io',
org: 'ShortOrg' }
business field
POST with business
{ email: 'owner@shop.io',
business: 'Local Shop LLC' }
business_name field
POST with business_name
{ email: 'mgr@store.io',
business_name: 'Main Street Store' }
company_domain field
POST with company_domain
{ email: 'user@techcorp.io',
company_domain: 'techcorp.io' }
company + company_domain
POST with both
{ email: 'ceo@megacorp.com',
company: 'MegaCorp Inc',
company_domain: 'megacorp.com' }
11.3 DataLayer with Company (pp event)
Tests company detection in dataLayer push events.
company in dataLayer
Push with company
dataLayer.push({
event: 'lead',
email: 'dl-lead@acmecorp.io',
company: 'DataLayer Corp'
})
organization in dataLayer
Push with organization
dataLayer.push({
event: 'signup',
email: 'dl-org@nonprof.org',
organization: 'Nonprofit Org'
})
Nested user.company
Push with user.company
dataLayer.push({
event: 'identify',
user: {
email: 'nested@biz.io',
company: 'Nested Corp'
}
})
company_domain in dataLayer
Push with company_domain
dataLayer.push({
event: 'b2b_signup',
email: 'sales@enterprise.io',
company_domain: 'enterprise.io'
})
company + company_domain
Push with both
dataLayer.push({
event: 'enterprise_lead',
email: 'vp@globaltech.com',
company: 'GlobalTech',
company_domain: 'globaltech.com'
})
11.4 Custom Events with Company (pp event)
Tests company detection in _ll.track() custom events.
Custom event with company
Track with company
_ll.track('lead_captured', {
email: 'lead@enterprise.io',
company: 'Enterprise Solutions',
lead_score: 85
})
Custom event with organization
Track with organization
_ll.track('demo_requested', {
email: 'demo@gov.org',
organization: 'Government Agency',
demo_type: 'enterprise'
})
Custom event with company_domain
Track with company_domain
_ll.track('saas_signup', {
email: 'user@cloudapp.io',
company_domain: 'cloudapp.io',
plan: 'enterprise'
})
Custom event with both
Track with company + domain
_ll.track('enterprise_inquiry', {
email: 'cto@fintech.co',
company: 'FinTech Solutions',
company_domain: 'fintech.co'
})
11.5 eCommerce with Company (id/pp events)
Tests company detection in eCommerce person/user_data objects.
Purchase with person.company
Purchase with company
dataLayer.push({
event: 'purchase',
person: {
email: 'buyer@biz.io',
company: 'Business Buyer LLC'
},
ecommerce: { ... }
})
Checkout with user_data.company
Checkout with company
dataLayer.push({
event: 'begin_checkout',
user_data: {
email: 'checkout@corp.io',
company: 'Corp Inc'
},
ecommerce: { ... }
})
user_data.organization
Checkout with organization
dataLayer.push({
event: 'begin_checkout',
user_data: {
email: 'edu@university.edu',
organization: 'State University'
},
ecommerce: { ... }
})
user_data.company_domain
Purchase with company_domain
dataLayer.push({
event: 'purchase',
user_data: {
email: 'buyer@saasco.io',
company_domain: 'saasco.io'
},
ecommerce: { ... }
})
user_data with company + domain
Purchase with both
dataLayer.push({
event: 'purchase',
user_data: {
email: 'exec@bigcorp.com',
company: 'BigCorp Industries',
company_domain: 'bigcorp.com'
},
ecommerce: { ... }
})
11.6 Form Submission with Company (pp event)
Tests company field detection in form submissions.
Note: Company data appears in the company field of pp events regardless of which pattern was used to detect it (company, organization, org, business, etc.). For id events via identifyPerson(), use the structured format: { domain: 'acme.io', name: 'Acme Corp' }.
Blocking Tests
blocked
Test that blocked events do NOT emit pp events. The SDK should skip identity detection for configured blocked events and skip keys.
See also: Form Blocking Tests for form-specific blocking (by ID, hash, or path).
Blocked dataLayer Events
These dataLayer event names are blocked from identity detection: internal_login, admin_action, debug_event, test_conversion
Blocked: internal_login
Push internal_login Event
Expected: NO pp event (blocked)
Blocked: admin_action
Push admin_action Event
Expected: NO pp event (blocked)
Allowed: user_login (control)
Push user_login Event
Expected: pp event SHOULD fire
Blocked Custom Events
These custom event names are blocked: test_event, debug_tracking, internal_action, staging_test
Blocked: test_event
Track test_event
Expected: NO pp event (blocked)
Blocked: debug_tracking
Track debug_tracking
Expected: NO pp event (blocked)
Allowed: button_click (control)
Track button_click
Expected: pp event SHOULD fire
Skip Keys (Field Name Blocking)
Fields containing these words are always ignored: internal_id, session_token, tracking_code (custom), plus built-in skip keys like password, token, secret, etc.
Skip Key: internal_id
Submit Form with internal_id
Expected: NO pp for internal_id field
Skip Key: session_token
Submit Form with session_token
Expected: NO pp for session_token field
Built-in Skip: password
Submit Form with password
Expected: NO pp for password field
Allowed: user_email (control)
Submit Form with user_email
Expected: pp event SHOULD fire
Event Log
Identity events fired on this page will appear below.
Clear Log
[--:--:--] Waiting for identity events...