Returning Visitor Recognition¶
Overview¶
Returning Visitor Recognition persists visitor profile data across sessions, enabling personalized experiences for returning visitors. When a visitor comes back, the system remembers their email, company, and other information.
How It Works¶
Visitor Identification¶
Visitors are identified across sessions using:
person_id- PostHog'sdistinct_id(persisted in browser)site_domain- Scoped to each site
The combination (site_domain, person_id) uniquely identifies a visitor.
Data Persistence¶
What's Stored¶
| Field | Source | Persists |
|---|---|---|
email |
Captured in conversation | ✅ |
company_name |
Enrichment or inferred | ✅ |
domain |
Enrichment | ✅ |
sector |
Enrichment or inferred | ✅ |
sub_sector |
Enrichment or inferred | ✅ |
company_description |
Enrichment | ✅ |
lifetime_interest_score |
Interest signals | ✅ |
lead_qualification_data |
Form field extraction | ✅ |
Storage Location¶
Data is stored in Supabase:
visitorstable - Profile data (email, enrichment_data)conversationstable - Session state snapshots
Loading on Session Start¶
When a new session begins (chatbot._prepare_query_state):
# Load visitor profile data
existing_visitor_data = await storage.get_visitor_profile_data(site_domain, person_id)
# Merge with current session (only missing fields)
for field in ["email", "company_name", "domain", "sector", "sub_sector"]:
current_value = getattr(current_profile, field, None)
if not current_value or current_value == "unknown":
profile_updates[field] = existing_value
Impact on Features¶
Booking Flow¶
If email is already known: - Bot skips asking for email - Immediately confirms and redirects - Faster conversion for returning visitors
# In booking_handler.py
has_email = _has_valid_email(visitor_profile)
if has_email:
# Generate confirmation, skip email collection
Interest Signals¶
Lifetime interest score persists: - High-interest visitors get appropriate treatment - Score accumulates across sessions
if existing_lifetime_score > 0:
deserialized.interest_signals_state.lifetime_interest_score = existing_lifetime_score
Lead Qualification¶
Form field values persist: - CTA URLs can be personalized immediately - No need to re-ask qualification questions
Key Files¶
Backend¶
| File | Purpose |
|---|---|
ixchat/chatbot.py |
Loads and merges visitor data on session start |
ixdata/clients/conversation_storage.py |
get_visitor_profile_data(), get_lifetime_interest_score() |
ixchat/nodes/visitor_profiler.py |
Updates profile during conversation |
Database Queries¶
Load Visitor Profile¶
response = await (
client.table("visitors")
.select("email, enrichment_data")
.eq("site_domain", site_domain)
.eq("person_id", person_id)
.execute()
)
Load Lifetime Interest Score¶
response = await (
client.table("conversations")
.select("state")
.eq("site_domain", site_domain)
.eq("person_id", person_id)
.order("created_at", desc=True)
.limit(1)
.execute()
)
Debugging¶
Look for these log patterns:
📋 Loaded visitor profile data from Supabase: fields=['email', 'company_name']
📋 Loaded lifetime_interest_score=85 from previous sessions
📋 Loaded lead qualification data: fields=['company_size', 'industry']
And in conversation storage:
Privacy Considerations¶
- Data is scoped to each site (no cross-site tracking)
person_idis a PostHog ID, not PII- Email is only stored when explicitly provided
- Data can be deleted on request
Testing¶
poetry run pytest packages/ixchat/tests/test_visitor_profiling.py -v
poetry run pytest packages/ixchat/tests/test_merge_visitor_profiles.py -v
Related¶
- Visitor Enrichment - How company data is initially captured
- In-Chat Booking Email - Email capture and booking flow
- Interest Signals - How interest scores work