Documentation · v1.0.0

LeadIntent Agent
Technical Reference

A consent-aware, browser-side tracking agent that captures visitor behaviour, classifies purchase intent, and delivers structured events to your backend pipeline — without ever reading PII from form fields.

Vanilla JS · No dependencies IIFE bundle · ~18 KB raw GDPR / DNT aware

01 —Introduction

The Lead Intent Agent (LIA) is a single-file JavaScript bundle (lia.js) designed to be embedded in any web property. It continuously observes visitor behaviour, enriches each event with device, locale, and acquisition context, and streams structured event payloads to a backend API.

LIA was built around three core principles:

  • Privacy-first — respects DoNotTrack headers, never reads form field values, and sanitises URL query parameters to remove PII before transmission.
  • Resilient delivery — events are batched in memory and delivered using fetch + keepalive with exponential-backoff retry. Page-exit events use sendBeacon semantics to survive tab closure.
  • Zero dependencies — ships as a single IIFE with no external runtime requirements, compatible with all modern browsers and SPA frameworks.
💡
Bundle structure

The source tree is split into six ES modules (identity.js, context.js, behaviour.js, suspicion.js, transport.js, tracker.js) orchestrated by agent.js. The build script (build.js) strips ES module syntax and wraps the result in an IIFE, producing lia.js and lia.min.js.

02 —Architecture Overview

The agent follows a pipeline architecture: raw browser signals flow through enrichment stages before being queued in an in-memory event buffer. A timer-based flush loop delivers batches to the API; a session update callback allows the server to push scoring data back into the agent state.

BROWSER SIGNALS scroll / click form events route changes visibility / unload AGENT CORE identity.js context.js behaviour.js tracker.js suspicion.js buildEvent() enrichment enqueue() TRANSPORT in-memory queue (≤100) 5s flush timer retry × 3 (exp backoff) sendBeacon on unload POST /batch BACKEND API POST /events/batch POST /identify session update callback (score / segment) LOCAL / SESSION STORAGE lia_visitor_id (long-lived) lia_session_id + TTL lia_first_touch

03 —Module Map

LIA is composed of six specialised modules. Each module has a single responsibility and communicates through the agent.js orchestrator.

🔑

identity.js

Generates and persists visitor_id (long-lived) and session_id (TTL-based). Falls back gracefully: localStorage → sessionStorage → in-memory.

🌍

context.js

Captures device fingerprint, browser/OS family, locale, timezone, network info, and UTM acquisition parameters. Sanitises query strings to remove PII.

📊

behaviour.js

Tracks scroll depth milestones (25/50/75/90/100%), active time, idle detection (30s), visibility changes, and fires engaged_session when both thresholds are met.

🕵️

suspicion.js

Computes a client-side bot suspicion score (0–100) using WebDriver flags, headless UA hints, zero-screen dimensions, plugin counts, and event timing variance.

🚀

transport.js

Manages event batching (max 10/batch, queue cap 100), flush timer (5s), retry logic (3 attempts, exponential backoff), and reliable unload delivery via keepalive fetch.

🎯

tracker.js

CTA click detection with booking-platform pattern matching. Form lifecycle tracking (opened → started → field touched → submitted) using IntersectionObserver and event delegation.

04 —Installation

Install LIA by dropping a configuration object and the script tag into your HTML <head>. The agent auto-initialises when the DOM is ready.

index.html HTML
<!-- 1. Declare config BEFORE the script tag -->
<script>
  window.LeadIntentConfig = {
    siteId:     'your-site-id',
    apiBaseUrl: 'https://api.yourbackend.com',
    apiKey:     'sk-...',

    // Optional overrides
    sessionTtlMinutes: 30,
    captureForms:     true,
    captureScroll:    true,
    captureClicks:    true,
    respectDoNotTrack: true,
    debug:            false,
  };
</script>

<!-- 2. Load the agent (async is fine) -->
<script async src="/lia.js"></script>
⚠️
Config-before-script

If window.LeadIntentConfig is not present when lia.js executes, the agent uses a MutationObserver on <head> to wait for the config object. Declare the config synchronously before the script for fastest initialisation.

SPA / Framework integration

For React, Vue, or Next.js applications, call the API directly after importing the bundle:

app.js JavaScript
// After lia.js has loaded (window.LeadIntentAgent is available)
window.LeadIntentAgent.init({
  siteId:     'my-site',
  apiBaseUrl: 'https://api.example.com',
  apiKey:     'sk-abc123',
  pageCategoryRules: [
    { match: '^/solutions', category: 'services' },
    { match: '^/demo',      category: 'booking'  },
  ],
});

05 —Configuration Reference

Key Type Default Description
siteId string required Unique identifier for the tracked site. Sent on every event.
apiBaseUrl string required Base URL of the backend API. The agent appends /v1/events/batch, /v1/identify, etc.
apiKey string required Sent as X-Api-Key header on all requests.
sessionTtlMinutes number 30 Session inactivity timeout in minutes. A new session is created when the TTL expires between page loads.
captureForms boolean true Enable form lifecycle tracking. Set to false to disable all form events.
captureScroll boolean true Enable scroll depth tracking and active-time measurement.
captureClicks boolean true Enable CTA click detection using default and custom selectors.
respectDoNotTrack boolean true When true, sets consent_state to essential_only if the browser's DNT header is 1.
captureHashRoutes boolean false Listen to hashchange events in addition to pushState.
ctaSelectors string[] [] Additional CSS selectors to treat as CTA elements, merged with the built-in list.
pageCategoryRules Rule[] [] Array of { match: RegExp, category: string } rules evaluated before built-in heuristics.
debug boolean false Enables verbose console.log output and forces fresh identity on every page load (development only).

06 —Consent & Privacy

LIA ships with first-party consent management hooks. Tracking is fully blocked when consent is 'denied'. A setConsent() method allows integration with any external CMP.

consent-integration.js JavaScript
// States: 'unknown' | 'granted' | 'denied' | 'essential_only'

// Example: integrate with a cookie consent banner
myCMP.onDecision((decision) => {
  window.LeadIntentAgent.setConsent(
    decision.analytics ? 'granted' : 'denied'
  );
});

// Check the current session state at any time
const session = window.LeadIntentAgent.getSession();
// → { sessionId, visitorId, leadId, score, segment }

Privacy guardrails built into the agent

  • Query strings are sanitised — only UTM params and a known allowlist pass through.
  • Form field values are never read — only field type classification (email, phone, company) is tracked.
  • DoNotTrack header forces essential_only mode when respectDoNotTrack: true.
  • The reset() method clears all persisted identity from all storage layers.

07 —Identity Module

LIA maintains two levels of identity persistence with automatic storage fallbacks:

Visitor ID

  • lia_visitor_id long-lived

Generated once per browser using crypto.randomUUID(). Persists until cleared. Used for cross-session visitor recognition.

Session ID

  • lia_session_id session TTL
  • lia_session_start timestamp

Refreshed on each interaction. Expires after sessionTtlMinutes of inactivity. A new session emits session_started.

First Touch

  • lia_first_touch preserved

Captures UTM params and referrer from the very first visit. Never overwritten — preserved for attribution across all subsequent sessions.

Storage fallback chain

All keys are written to and read from three storage layers in priority order: localStoragesessionStoragein-memory object. This ensures the agent functions in privacy-hardened browsers that block persistent storage.

08 —Behaviour Tracker

The behaviour module monitors engagement quality using non-invasive, throttled listeners. It never fires on every raw DOM event; all signals are debounced or milestone-gated.

User interacts (click/scroll/key) → markActive() Active time accumulates 30s idle timeout Checkpoints 30s / 60s / 120s → active_time_Xs engaged_session active ≥ 30s + scroll ≥ 50% page_exit summary stats → flush Parallel: scroll depth → 25% → 50% → 75% → 90% → 100% milestones emit scroll_depth_reached

09 —Transport Layer

The transport module manages all network communication. It batches events to reduce request overhead, retries failed deliveries, and ensures critical events (form submissions, bookings) are flushed immediately.

ParameterValueNotes
MAX_BATCH_SIZE10Events per batch flush
FLUSH_INTERVAL_MS5 000Timer-based flush interval
MAX_RETRY_ATTEMPTS3Retries with exponential backoff (500ms, 1s, 2s)
MAX_QUEUE_SIZE100FIFO drop when exceeded (oldest removed)

Critical events — immediate flush

The following events bypass the batch timer and trigger an immediate flush():

form_submitted · booking_started · identify · session_ended · booking_opened

Unload delivery

On pagehide / beforeunload, queued events are sent via fetch with keepalive: true, which allows the request to complete even after the page is destroyed. This is the modern replacement for navigator.sendBeacon and supports custom headers (required for the API key).

10 —Suspicion Engine

The suspicion module computes a client-side quality score to detect automated traffic. These are weak signals only; server-side enrichment is authoritative. A score ≥ 30 triggers a headless_suspected event.

SignalScoreDetection Method
webdriver_true +30 navigator.webdriver === true
headless_chrome_ua +20 /HeadlessChrome/.test(userAgent) AND no window.chrome
zero_screen +20 screen.width === 0 && screen.height === 0
no_plugins +5 navigator.plugins.length === 0 (non-Firefox)
no_languages +5 navigator.languages.length === 0
permissions_anomaly +5 typeof navigator.permissions === 'undefined'

Additionally, createEventTimingMonitor() tracks event cadence variance. If standard deviation < 15ms over a 200ms average interval, the cadence is classified as robotic.

11 —Public API

call LeadIntentAgent.init(config) Initialise the agent

Must be called once before any other method. Typically called automatically via window.LeadIntentConfig. Emits session_started or session_resumed.

call LeadIntentAgent.track(eventName, properties) Emit custom event
  • eventNamestringName of the custom event to emit. Sent with full page and context enrichment.
  • propertiesobject?Arbitrary key-value properties merged into the properties envelope.
async LeadIntentAgent.identify(payload) Tie session to a known lead
  • payload.emailstring?Lead email address. Sent to POST /v1/identify.
  • payload.namestring?Lead full name.
  • payload.companystring?Company name.

Returns the backend response including lead_id. Subsequent events will include this lead_id for attribution. Triggers a session refresh call to hydrate score and segment.

get LeadIntentAgent.getSession() Current session snapshot

Returns { sessionId, visitorId, leadId, score, segment } synchronously. Score and segment are populated after the first API round-trip.

call LeadIntentAgent.setConsent(state) Update consent state

Accepted states: 'granted' | 'denied' | 'essential_only' | 'unknown'. Setting 'denied' stops all event emission immediately.

call LeadIntentAgent.reset() Clear all identity data

Removes all keys from localStorage, sessionStorage, and in-memory store. Resets session, visitor, and lead IDs. Stops the flush timer.

12 —Event Catalogue

Event Name Type Key Properties Description
session_started lifecycle visitor_id, session_ttl_minutes Fired when a new session is created (TTL expired or first visit)
session_resumed lifecycle visitor_id Fired when an existing session is resumed within its TTL
page_view lifecycle page_category, is_entry_page, page_view_count Fired on every page navigation, including SPA route changes
route_change lifecycle previous_path, path Fired when the SPA router changes the URL (pushState or popstate)
page_exit lifecycle active_time_seconds, max_scroll_depth Fired on page unload; delivered via keepalive fetch
scroll_depth_reached behavioral depth_percent (25/50/75/90/100) Fired once per milestone per page. Throttled to 200ms.
active_time_30s behavioral active_seconds Cumulative active time reached 30 seconds
active_time_60s behavioral active_seconds Cumulative active time reached 60 seconds
active_time_120s behavioral active_seconds Cumulative active time reached 120 seconds
engaged_session intent active_seconds Active ≥ 30s AND scroll ≥ 50% — high-quality engagement signal
cta_click intent cta_text, cta_id, destination_type Click on a CTA element matching configured selectors
booking_opened intent source, href Click on a Calendly / Cal.com / Acuity booking link, or booking page view
mailto_click intent href Click on a mailto: link
form_opened critical form_id Form entered the viewport (30% threshold via IntersectionObserver)
form_started critical form_id First field focused in a form
field_touched critical form_id, field_type A classified field (email/phone/company/budget/message) was focused
form_submitted critical form_id, elapsed_seconds Form submit event fired. Triggers immediate transport flush.
pricing_view intent page_category Auto-emitted when page path matches pricing category
headless_suspected behavioral signals[], score Suspicion score ≥ 30 — automated traffic suspected