<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:blog="https://jonesrussell.github.io/blog/ns"><channel><title>Analytics on Web Developer Blog</title><link>https://jonesrussell.github.io/blog/tags/analytics/</link><description>Recent content in Analytics on Web Developer Blog</description><image><title>Web Developer Blog</title><url>https://jonesrussell.github.io/blog/images/og-default.png</url><link>https://jonesrussell.github.io/blog/images/og-default.png</link></image><generator>Hugo -- 0.169.0</generator><language>en-us</language><lastBuildDate>Wed, 28 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jonesrussell.github.io/blog/tags/analytics/feed.xml" rel="self" type="application/rss+xml"/><item><title>Track campaign conversions without storing contact details</title><link>https://jonesrussell.github.io/blog/track-campaign-conversions-without-contact-data/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0000</pubDate><guid>https://jonesrussell.github.io/blog/track-campaign-conversions-without-contact-data/</guid><category>php</category><blog:tag>php</blog:tag><blog:tag>analytics</blog:tag><blog:tag>privacy</blog:tag><blog:tag>testing</blog:tag><description>Measure which registered campaign links lead to saved contact messages without copying personal details into analytics.</description><content:encoded><![CDATA[<p>Ahnii!</p>
<p>Campaign attribution usually starts with a simple question: which shared link led to a useful action? The implementation gets risky when analytics and contact data start bleeding into each other.</p>
<p>This post covers a narrower design: count landings and saved contact messages for registered campaigns while keeping names, email addresses, messages, raw query strings, and visitor identifiers out of the conversion table.</p>
<h2 id="register-campaigns-instead-of-accepting-arbitrary-tags">Register campaigns instead of accepting arbitrary tags</h2>
<p>Do not treat every UTM value as trusted analytics data. Use a finite registry that binds one internal campaign key to the exact public route and exact tag set you expect.</p>
<p>That boundary gives you several useful properties:</p>
<ul>
<li>Unknown tags never become campaign labels.</li>
<li>Duplicate or partial parameters do not create attribution.</li>
<li>A valid tag set on the wrong route is rejected.</li>
<li>Test campaigns can be marked and excluded from production totals.</li>
<li>Click IDs and unrelated query parameters never enter storage.</li>
</ul>
<p>The browser only carries the internal key after the complete registered link matches. This keeps reporting labels under editorial control instead of letting arbitrary request data shape your analytics.</p>
<h2 id="keep-attribution-short-lived-and-same-tab">Keep attribution short-lived and same-tab</h2>
<p>For a contact form reached after a landing page, you need a small amount of continuity. You do not need a durable visitor profile.</p>
<p>The implementation stores a registered campaign key and a 30-minute expiry in same-tab session storage. A second valid campaign replaces the first. Invalid explicit campaign tags clear the old value. When session storage is unavailable, attribution can remain in memory for the current page and then disappear.</p>
<p>This is deliberately limited:</p>
<ul>
<li>There is no analytics cookie.</li>
<li>There is no cross-tab identity.</li>
<li>There is no name, email, message, or raw URL in the attribution state.</li>
<li>Do Not Track or Global Privacy Control clears the state and disables collection.</li>
</ul>
<p>Those limits mean the report is not a complete readership count. That is acceptable when the goal is directional campaign measurement, not surveillance.</p>
<h2 id="count-conversions-only-after-the-contact-is-saved">Count conversions only after the contact is saved</h2>
<p>A public analytics collector should never be allowed to claim that a conversion happened. The server already knows when the contact form passes validation, rate limiting, bot checks, and persistence.</p>
<p>Record the conversion at that trusted boundary. In this implementation, the contact row and conversion event are written inside the same database transaction. If the conversion write fails, the tracked contact save rolls back too. The response cannot report success for a message while silently losing its associated measurement.</p>
<p>The conversion event contains only:</p>
<ul>
<li>The event type</li>
<li>A known campaign key or <code>null</code></li>
<li>A random event key</li>
<li>A timestamp</li>
</ul>
<p>The contact record keeps the personal information needed for a reply. The analytics table does not receive the contact row ID or any contact content.</p>
<h2 id="deduplicate-landings-without-creating-a-visitor-profile">Deduplicate landings without creating a visitor profile</h2>
<p>Repeated delivery of the same pageview can happen through retries or browser behaviour. A random pageview ID can be hashed into an event key so the same landing is counted once.</p>
<p>That key is scoped to deduplication. It is not reused to join the landing to a person, a contact record, or activity on another day. The report aggregates campaign landings and saved messages without exposing an individual journey.</p>
<h2 id="test-the-refusal-paths">Test the refusal paths</h2>
<p>The useful tests are not only the happy path. Prove that the boundaries hold when inputs are incomplete or hostile.</p>
<p>Cover at least these cases:</p>
<ul>
<li>Unknown, duplicate, partial, expired, and wrong-route campaign tags are rejected.</li>
<li>Do Not Track and Global Privacy Control prevent analytics while the contact form still works.</li>
<li>Bots, invalid forms, honeypots, and rate-limited requests do not count as conversions.</li>
<li>The public collector cannot submit a conversion event.</li>
<li>Repeated delivery of one pageview counts one landing.</li>
<li>Analytics failure rolls back a tracked contact save.</li>
<li>Stored analytics rows contain no email address or message text.</li>
<li>Test campaigns stay out of production totals.</li>
</ul>
<p>This design does not prove that someone saw the original social post. Reshares keep the same registered tag, scripts can be blocked, and privacy choices reduce the count. Report visits and saved messages for a link, not unique people or causal certainty.</p>
<p>Baamaapii</p>
]]></content:encoded></item></channel></rss>