Without a foreign key or UTM-derived reference linking conversion records to the originating email send, you cannot compute campaign ROI, per-campaign conversion rates, or the revenue contribution of any individual sequence or A/B test. You can observe that a purchase happened, but you cannot answer which campaign drove it. GDPR Art. 5(1)(a) (lawfulness, purpose limitation) and CCPA §1798.120 (right to opt out of sale tied to identifiable data processing) both implicitly require that you understand why you hold each piece of customer data — including behavioral data collected via ePD Art. 5(3) tracking pixels. Blind conversion logs satisfy none of these requirements and make all email program measurement impossible.
Critical because unlinked conversion data makes it impossible to compute campaign ROI, evaluate A/B test outcomes on revenue metrics, or attribute any business result to an email program — the entire analytics layer loses its core function.
Parse UTM parameters from the conversion request and persist them as explicit columns alongside the conversion record. Do this server-side — client-side analytics tools lose the data on ad blockers and private browsers:
async function recordConversion(
contactId: string,
type: string,
value: number,
req: Request
) {
const url = new URL(req.url)
await db.conversions.create({
data: {
id: crypto.randomUUID(),
contact_id: contactId,
type,
value,
utm_source: url.searchParams.get('utm_source') ?? null,
utm_medium: url.searchParams.get('utm_medium') ?? null,
utm_campaign: url.searchParams.get('utm_campaign') ?? null,
utm_content: url.searchParams.get('utm_content') ?? null,
converted_at: new Date()
}
})
}
Add a campaign_id column to your conversions table and populate it by joining utm_campaign against your campaigns table at write time so queries don't require a join on every read.
ID: campaign-analytics-attribution.attribution-conversion.conversion-linked-to-send
Severity: critical
What to look for: Examine how conversion events (purchases, signups, form completions) are stored and whether they can be traced back to a specific email send. Look for a foreign key or reference from the conversion record to a send or campaign ID. Check for conversion tracking routes that parse UTM parameters from the landing page URL and store the campaign reference alongside the conversion. Without this link, you cannot compute true campaign ROI or per-campaign conversion rates.
Pass criteria: Conversion events include a reference (campaign ID, send ID, or UTM-derived campaign identifier) that links back to the originating email send. The link is stored in the database, not just logged. Count all conversion-recording code paths and verify at least 1 column stores a campaign reference.
Fail criteria: Conversion events are stored without any campaign reference. UTM parameters are not captured at conversion time. You cannot query "how many conversions came from campaign X" from the database.
Skip (N/A) when: The project does not track conversion events (no purchases, signups, or measurable goal completions).
Detail on fail: Example: "Conversion events table has no campaign_id or utm_campaign column — cannot attribute conversions to email sends" or "UTM parameters captured in analytics tool but not stored server-side — cannot join with send data"
Remediation: Capture and store UTM parameters at conversion time:
// On your conversion endpoint or page server action
async function recordConversion(
contactId: string,
conversionType: string,
value: number,
utmParams: Partial<UtmParams>
) {
await db.conversions.create({
data: {
id: crypto.randomUUID(),
contact_id: contactId,
type: conversionType,
value,
utm_source: utmParams.source ?? null,
utm_medium: utmParams.medium ?? null,
utm_campaign: utmParams.campaign ?? null,
utm_content: utmParams.content ?? null,
utm_term: utmParams.term ?? null,
converted_at: new Date()
}
})
}
// Parse UTMs from the request's Referer or cookie
function extractUtmParams(req: Request): Partial<UtmParams> {
const url = new URL(req.url)
return {
source: url.searchParams.get('utm_source') ?? undefined,
medium: url.searchParams.get('utm_medium') ?? undefined,
campaign: url.searchParams.get('utm_campaign') ?? undefined,
content: url.searchParams.get('utm_content') ?? undefined,
term: url.searchParams.get('utm_term') ?? undefined
}
}