{"id":7249,"date":"2026-06-04T06:55:17","date_gmt":"2026-06-04T06:55:17","guid":{"rendered":"https:\/\/www.coffee.ai\/articles\/website-visitor-tracking-script-example\/"},"modified":"2026-06-04T06:55:17","modified_gmt":"2026-06-04T06:55:17","slug":"website-visitor-tracking-script-example","status":"publish","type":"post","link":"https:\/\/www.coffee.ai\/articles\/website-visitor-tracking-script-example","title":{"rendered":"Website Visitor Tracking Script Example"},"content":{"rendered":"<p><em>Written by: Doug Camplejohn, CEO &amp; Co-Founder, Coffee<\/em><\/p>\n<h2 id=\"key-takeaways\">Key Takeaways<\/h2>\n<ul>\n<li>Up to 98% of B2B website traffic remains anonymous, so companies miss high-intent accounts and lose pipeline opportunities.<\/li>\n<li>Consent-compliant visitor tracking needs site admin access, an analytics baseline, and a GDPR-compliant consent mechanism.<\/li>\n<li>This seven-step process covers client-side tracking, UUID persistence, backend endpoints, consent handling, IP enrichment, ICP scoring, and CRM integration.<\/li>\n<li>Self-built solutions require ongoing maintenance for consent records, enrichment contracts, scoring updates, and CRM mappings.<\/li>\n<li>Skip the manual work with <a href=\"https:\/\/www.coffee.ai\/pricing\" target=\"_blank\">Coffee\u2019s instant visitor identification and CRM-ready leads<\/a>.<\/li>\n<\/ul>\n<h2>Check These Prerequisites Before You Add the Script<\/h2>\n<p>Confirm you have the right access and tooling before you install the script. You need site admin access to modify the <code>&lt;head&gt;<\/code> tag or a Google Tag Manager container so you can place the tracking code. An existing analytics baseline such as Google Analytics 4 or GTM helps you validate that the new script captures traffic correctly. Because the backend runs on Node.js and Express, you also need basic familiarity with both. Finally, decide on your consent banner approach before installation, because the script must respect user consent from the moment it loads.<\/p>\n<p><a href=\"https:\/\/web-tracking.eu\/blog\/eprivacy-article-5-3-explained\" target=\"_blank\" rel=\"noindex nofollow\">Under the ePrivacy Directive (as complemented by GDPR for personal data), explicit consent is required before loading non-essential tracking scripts<\/a>, so your consent mechanism must be in place before the pixel fires for EU visitors.<\/p>\n<h2>Step 1: Add a Minimal Client-Side Tracking Script<\/h2>\n<p><a href=\"https:\/\/swetrix.com\/blog\/how-to-track-visitors-to-a-website\" target=\"_blank\" rel=\"noindex nofollow\">Client-side visitor tracking starts with a small JavaScript snippet in the <code>&lt;head&gt;<\/code> section<\/a>. This snippet collects page-view and interaction data and sends it to your backend. The implementation below uses <code>sendBeacon<\/code> for reliability on page unload and <code>fetch<\/code> as a fallback.<\/p>\n<pre><code>\/\/ tracker.js \u2014 Step 1: Minimal client-side tracking (function () { var ENDPOINT = 'https:\/\/your-backend.com\/track'; function getConsent() { return localStorage.getItem('tracking_consent') === 'true'; } function buildPayload() { return JSON.stringify({ visitorId: localStorage.getItem('vid') || null, url: location.href, referrer: document.referrer, ts: Date.now(), ua: navigator.userAgent }); } function send(payload) { var blob = new Blob([payload], { type: 'application\/json' }); if (navigator.sendBeacon) { navigator.sendBeacon(ENDPOINT, blob); } else { fetch(ENDPOINT, { method: 'POST', body: payload, headers: { 'Content-Type': 'application\/json' }, keepalive: true }); } } if (getConsent()) { send(buildPayload()); } })(); <\/code><\/pre>\n<p><strong>Common pitfalls:<\/strong> <a href=\"https:\/\/usercentrics.com\/guides\/server-side-tagging\/server-side-vs-client-side-tracking\" target=\"_blank\" rel=\"noindex nofollow\">Ad blockers affect roughly 30% of web traffic and directly block client-side tracking scripts<\/a>. Safari\u2019s ITP and Firefox\u2019s Enhanced Tracking Protection further reduce attribution accuracy. A server-side relay endpoint in Step 3 reduces the impact of both issues.<\/p>\n<p>Before you build that endpoint, you need a way to recognize returning visitors across sessions. Without that capability, every visit looks like a new lead.<\/p>\n<h2>Step 2: Persist Returning Visitors with localStorage and UUID<\/h2>\n<p>Assign each browser a stable UUID on the first visit and reuse it on later visits. This approach lets you stitch sessions into a returning-visitor journey.<\/p>\n<pre><code>\/\/ Step 2: UUID generation and persistence function getOrCreateVisitorId() { var key = 'vid'; var existing = localStorage.getItem(key); if (existing) return existing; var id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(\/[xy]\/g, function (c) { var r = Math.random() * 16 | 0; return (c === 'x' ? r : (r &amp; 0x3 | 0x8)).toString(16); }); localStorage.setItem(key, id); return id; } \/\/ Call before buildPayload() and inject into visitorId field <\/code><\/pre>\n<h2>Step 3: Build the Node.js and Express Backend Endpoint<\/h2>\n<p>The backend receives, validates, and stores events. Keep the route thin and handle enrichment asynchronously in Step 5.<\/p>\n<pre><code>\/\/ server.js \u2014 Step 3: Express endpoint const express = require('express'); const app = express(); app.use(express.json()); const events = []; \/\/ Replace with your DB write (Postgres, MongoDB, etc.) app.post('\/track', (req, res) =&gt; { const { visitorId, url, referrer, ts, ua } = req.body; if (!url || !ts) return res.status(400).json({ error: 'Missing fields' }); const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.socket.remoteAddress; events.push({ visitorId, url, referrer, ts, ua, ip, receivedAt: Date.now() }); res.status(202).json({ ok: true }); }); app.listen(3000, () =&gt; console.log('Tracker listening on :3000')); <\/code><\/pre>\n<h2>Step 4: Add GDPR and CCPA Consent Handling<\/h2>\n<p><a href=\"https:\/\/evelan.de\/en\/blog\/gdpr-website-requirements\" target=\"_blank\" rel=\"noindex nofollow\">A legally compliant cookie banner must block all non-essential scripts until consent is given and provide equivalent Reject and Accept buttons without dark patterns.<\/a> <a href=\"https:\/\/raidboxes.io\/en\/blog\/security\/data-privacy-websites\" target=\"_blank\" rel=\"noindex nofollow\">Consent must be stored with a timestamp, the specific purposes consented to, and the banner version shown at the time.<\/a><\/p>\n<pre><code>\/\/ Step 4: Consent storage and opt-in toggle function setConsent(granted) { localStorage.setItem('tracking_consent', granted ? 'true' : 'false'); localStorage.setItem('consent_ts', Date.now()); localStorage.setItem('consent_version', '1.2'); \/\/ bump on policy changes if (granted) initTracker(); \/\/ fire tracker only after consent } \/\/ Wire to your banner buttons: \/\/ document.getElementById('accept-btn').addEventListener('click', () =&gt; setConsent(true)); \/\/ document.getElementById('reject-btn').addEventListener('click', () =&gt; setConsent(false)); <\/code><\/pre>\n<h2>Step 5: Enrich Visitors by Matching IP or Domain<\/h2>\n<p>Corporate IP matching can identify visitors from corporate networks, but accuracy drops for remote workers, mobile networks, and VPN traffic. Call an enrichment API asynchronously after storing the raw event. Once the API returns company data such as industry, employee count, and other firmographic fields, you can use that data in your scoring logic in the next step.<\/p>\n<h2>Step 6: Turn Identified Visitors into CRM Leads with Buyer-Persona Matching<\/h2>\n<p>Effective conversion of anonymous visits into outreach targets requires scoring identified visitors against an ICP, weighting page-level intent signals such as pricing page visits, setting minimum session thresholds, and applying recency windows. The function below produces a score and a suggested contact role.<\/p>\n<pre><code>\/\/ Step 6: ICP scoring and persona matching function scoreVisitor(enrichment, sessionData) { let score = 0; const ICP = { industries: ['SaaS', 'FinTech'], minEmployees: 10, maxEmployees: 500 }; if (ICP.industries.includes(enrichment.industry)) score += 30; if (enrichment.employees &gt;= ICP.minEmployees &amp;&amp; enrichment.employees &lt;= ICP.maxEmployees) score += 20; if (sessionData.pagesViewed.includes('\/pricing')) score += 35; if (sessionData.durationSeconds &gt; 120) score += 15; const suggestedPersona = score &gt;= 70 ? ['VP of Sales', 'Head of RevOps'] : ['Marketing Manager']; return { score, suggestedPersona, pushToCRM: score &gt;= 70 }; } <\/code><\/pre>\n<p>At a score threshold of 70 or higher, the visitor qualifies as a high-intent lead. At that point, push the enriched record and suggested contacts to your CRM via API. This push requires manual mapping of enrichment fields to CRM fields, version management when either schema changes, and error handling when the API call fails. That manual stitching consumes engineering hours at scale.<\/p>\n<p><a href=\"https:\/\/www.coffee.ai\/pricing\" target=\"_blank\">Let Coffee handle the stitching automatically<\/a>, with no engineering hours required.<\/p>\n<h2>Step 7: Install the Script via &lt;head&gt; Tag or Google Tag Manager<\/h2>\n<p><strong>Option A: Direct &lt;head&gt; tag<\/strong><\/p>\n<pre><code>&lt;!-- Paste before closing &lt;\/head&gt; --&gt; &lt;script src=\"https:\/\/your-cdn.com\/tracker.js\" defer&gt;&lt;\/script&gt; <\/code><\/pre>\n<p><strong>Option B: Google Tag Manager<\/strong> In GTM, create a new Custom HTML tag, paste the tracker script contents, set the trigger to &#8220;All Pages,&#8221; and publish. <a href=\"https:\/\/usercentrics.com\/guides\/server-side-tagging\/server-side-vs-client-side-tracking\" target=\"_blank\" rel=\"noindex nofollow\">Google Analytics client-side tracking uses gtag.js or Google Tag Manager to record page views and events before sending data to Google\u2019s servers<\/a>, so GTM already acts as a familiar container for this pattern.<\/p>\n<h2>Validate Your Visitor Tracking Pipeline<\/h2>\n<p>Use this checklist to confirm the pipeline works end to end.<\/p>\n<ul>\n<li>Open your browser\u2019s Network tab and confirm a POST to <code>\/track<\/code> fires on page load after consent is granted.<\/li>\n<li>Verify the backend logs show <code>visitorId<\/code>, <code>ip<\/code>, and <code>url<\/code> fields populated.<\/li>\n<li>Reload the page and confirm the same <code>visitorId<\/code> UUID is reused so returning visitor stitching works.<\/li>\n<li>Decline consent and confirm no POST fires.<\/li>\n<li>Check your CRM for a new lead record after a high-score session crosses the threshold.<\/li>\n<li><a href=\"https:\/\/raidboxes.io\/en\/blog\/security\/data-privacy-websites\" target=\"_blank\" rel=\"noindex nofollow\">Confirm consent timestamps and banner version are stored alongside each record<\/a> for audit purposes.<\/li>\n<\/ul>\n<h2>Scale Your Tracking with UTMs, SPAs, and Better Storage<\/h2>\n<p>For UTM attribution, append <code>utm_source<\/code>, <code>utm_medium<\/code>, and <code>utm_campaign<\/code> from <code>URLSearchParams<\/code> to the payload. This approach remains compatible with cookieless setups because <a href=\"https:\/\/swetrix.com\/blog\/how-to-track-visitors-to-a-website\" target=\"_blank\" rel=\"noindex nofollow\">privacy-first analytics tools support UTM campaign tracking and funnel analysis without fingerprinting<\/a>. If you track a single-page app instead of a traditional multi-page site, call the tracker function on each client-side route change rather than waiting for a full page load, or you will only capture the initial landing page.<\/p>\n<p>As team size grows, replace the in-memory <code>events<\/code> array with a write-optimized store such as ClickHouse, BigQuery, or Postgres with a time-series extension. Add a queue such as Redis or SQS between the Express endpoint and the enrichment worker so latency spikes do not block event ingestion.<\/p>\n<h2>Frequently Asked Questions<\/h2>\n<h3>How long does initial setup take?<\/h3>\n<p>Dropping the tracking snippet into your site\u2019s head tag or GTM container takes under 15 minutes for a developer with site access. The Node.js backend, consent handling, and enrichment integration add two to four hours of work depending on your existing infrastructure. If you use Coffee\u2019s pixel instead, installation is a single copy-paste step and Coffee verifies the connection automatically, with no backend to build or maintain.<\/p>\n<h3>How long is visitor data retained?<\/h3>\n<p>With a self-built script, retention depends on your database configuration and privacy policy. GDPR requires you to disclose retention periods in your privacy notice and delete data once the stated period expires. A common practice for B2B visitor data is to retain raw session events for a limited time and enriched company-level records for a longer period, with automated deletion jobs enforcing those windows. Coffee\u2019s data handling follows its SOC 2 Type 2 and GDPR-compliant data policies, with retention periods disclosed in its Data Processing Agreement.<\/p>\n<h3>Is the workflow SOC 2 and GDPR compliant?<\/h3>\n<p>The self-built script can be made compliant by implementing the consent gate in Step 4, storing consent records with timestamps and banner versions, executing a Data Processing Agreement with every enrichment provider, and enabling TLS on all endpoints. Coffee is SOC 2 Type 2 certified and GDPR compliant out of the box. Customer data is not used to train public models.<\/p>\n<h3>What changes when you switch from a self-built script to Coffee\u2019s agent-powered pixel?<\/h3>\n<p>With a self-built script, you maintain the backend endpoint, the enrichment API integration, the ICP scoring logic, the CRM push, and the consent audit trail yourself. Coffee\u2019s pixel replaces the entire backend stack. The agent surfaces persona-matched prospects with enrichment pre-filled, which removes the manual scoring and CRM mapping steps described above. High-fit visitors trigger real-time Slack notifications, and one click adds the prospect to Coffee or your connected Salesforce or HubSpot instance. Competitors like RB2B and Warmly surface either company-only data or undifferentiated people lists, while Coffee closes the loop from pixel hit to persona-matched outreach inside the agent.<\/p>\n<h2>Conclusion: Move from Script to an Agent-Powered Pipeline<\/h2>\n<p>The seven-step implementation above gives any B2B team a consent-compliant, IP-enriched, persona-scored visitor tracking pipeline built on standard JavaScript and Node.js. It resolves the core problem of invisible traffic and produces CRM-ready leads with buyer-persona matching. The remaining constraint is maintenance, because every enrichment provider contract, scoring threshold update, consent-version bump, and CRM field mapping becomes an ongoing engineering obligation.<\/p>\n<p>Coffee\u2019s pixel and Suggested Leads feature remove that obligation entirely. Drop one script tag, connect your CRM, and define your buyer persona. Coffee\u2019s agent handles identification, enrichment, persona matching, and outreach routing automatically, <a href=\"https:\/\/www.coffee.ai\/pricing\" target=\"_blank\">turning every qualifying visit into a named, actionable lead<\/a> without a single line of backend code.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn to build a consent-compliant visitor tracking script\u2014or let Coffee identify anonymous visitors and deliver CRM-ready leads instantly.<\/p>\n","protected":false},"author":11,"featured_media":7248,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-7249","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.coffee.ai\/articles\/wp-json\/wp\/v2\/posts\/7249","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.coffee.ai\/articles\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.coffee.ai\/articles\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.coffee.ai\/articles\/wp-json\/wp\/v2\/users\/11"}],"replies":[{"embeddable":true,"href":"https:\/\/www.coffee.ai\/articles\/wp-json\/wp\/v2\/comments?post=7249"}],"version-history":[{"count":0,"href":"https:\/\/www.coffee.ai\/articles\/wp-json\/wp\/v2\/posts\/7249\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.coffee.ai\/articles\/wp-json\/wp\/v2\/media\/7248"}],"wp:attachment":[{"href":"https:\/\/www.coffee.ai\/articles\/wp-json\/wp\/v2\/media?parent=7249"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.coffee.ai\/articles\/wp-json\/wp\/v2\/categories?post=7249"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.coffee.ai\/articles\/wp-json\/wp\/v2\/tags?post=7249"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}