F01 event reconciliation — Reconcile event rows to unique user IDs in a specified ordered closed funnel, expose missing or reversed steps and duplicate submissions,…

HTML for this example

Select the code and use your copy shortcut. Changes in this preview are temporary.

<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>F01 event reconciliation</title><style>*{box-sizing:border-box}body{margin:0;font:16px/1.55 system-ui,sans-serif;color:var(--ink);background:var(--bg)}main{max-width:1120px;margin:auto;padding:30px}h1{font-size:clamp(30px,4vw,44px);line-height:1.12;letter-spacing:-.035em;margin:5px 0 12px}h2{font-size:23px;line-height:1.3;margin:0 0 15px}h3{font-size:17px;margin:0 0 8px}p{margin:0 0 12px}.eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:.13em;font-weight:750;color:var(--accent)}.small,footer{font-size:13px;color:var(--muted)}footer{margin:27px 0}.panel{background:white;border:1px solid var(--line);padding:23px;border-radius:9px;margin-top:22px}.two{display:grid;grid-template-columns:1.35fr 1fr;gap:23px}.row{display:flex;gap:9px;flex-wrap:wrap;align-items:center}button{font:700 14px system-ui;min-height:44px;padding:10px 16px;border:1px solid var(--accent);background:var(--accent);color:white;border-radius:5px;cursor:pointer}button.secondary{color:var(--accent);background:white}button[aria-pressed=true]{box-shadow:inset 0 0 0 2px var(--ink)}button:disabled{opacity:.45;cursor:default}:focus-visible{outline:3px solid #d28b25;outline-offset:3px}label{display:block;font-size:13px;font-weight:650}input,textarea,select{display:block;width:100%;padding:11px;font:16px/1.5 system-ui;color:var(--ink);background:white;border:1px solid #9baab6;border-radius:4px;margin:6px 0 15px}textarea{min-height:100px;resize:vertical}.error{color:#a02b20;font-size:14px;min-height:23px}.result{font-size:40px;line-height:1.25;letter-spacing:-.04em;font-weight:750}.notice{padding:15px;background:#fff3db;border-left:4px solid #cf9d3e;font-size:14px;margin:16px 0}details{margin-top:23px;border-top:1px solid var(--line);padding-top:15px}summary{cursor:pointer;min-height:44px;font-weight:700}.time-row{display:grid;grid-template-columns:75px 1fr 70px;gap:14px;padding:15px 0;border-top:1px solid var(--line)}.time-row p{font-size:14px;margin:5px 0}.time-row input{margin:0;text-align:center}.range{font-size:13px;font-weight:700;color:var(--accent)}[hidden]{display:none!important}pre{white-space:pre-wrap;overflow-wrap:anywhere;font:13px/1.6 ui-monospace,Consolas,monospace} @media(max-width:740px){main{padding:23px 16px}.two{grid-template-columns:1fr;gap:0}.panel{padding:20px}}@media(max-width:380px){main{padding:20px 12px}.panel{padding:17px}.time-row{grid-template-columns:1fr 65px}.range{grid-column:1/-1}.row button{flex:1}}:root{--ink:#203a53;--bg:#f0f4f8;--accent:#366ca0;--muted:#657c94;--line:#ccdbe8}.summary{display:grid;grid-template-columns:1.5fr 1fr;gap:24px;margin-top:24px}.funnel{padding:23px;background:white;border:1px solid var(--line);border-radius:10px}.stage{margin:15px 0}.bar{height:40px;background:#3976ae;color:white;padding:8px 12px;border-radius:3px;font-weight:700;min-width:80px}.stage p{font-size:13px;margin:7px 0}.rate{background:#244e76;color:white;padding:23px;border-radius:10px}.rate .result{font-size:50px;margin:8px 0}.rate p{font-size:14px}.rate hr{border:0;border-top:1px solid #6384a3;margin:20px 0}.event-row{display:grid;grid-template-columns:60px 65px 1fr;gap:12px;border-top:1px solid var(--line);padding:10px 0;font-size:14px}.trace{background:#eaf1f8;padding:18px;border-radius:7px}.ids button{min-width:65px}.json{font:13px/1.6 ui-monospace,Consolas,monospace;min-height:220px}@media(max-width:680px){.summary{grid-template-columns:1fr}.bar{font-size:14px}.event-row{gap:7px;grid-template-columns:50px 55px 1fr}}</style></head><body><main><p class="eyebrow">F01 · fictional ID-based closed funnel</p><h1>Six submit events. Two completed IDs.</h1><p>view → form_start → submit, in order; intervening actions allowed; each ID counted once per stage.</p><p class="small">October 20, 2026 · 09:00–10:00 America/New_York · local timestamps, no UTC conversion</p><section class="summary"><div class="funnel"><div class="stage"><div class="bar" style="width:100%">Entry · <span id="entries">5</span> IDs</div><p id="entry-ids">U01, U02, U03, U04, U05</p></div><div class="stage"><div class="bar" style="width:60%">Ordered start · <span id="starts">3</span></div><p id="start-ids">U01, U04, U05</p></div><div class="stage"><div class="bar" style="width:40%">Complete · <span id="completions">2</span></div><p id="complete-ids">U01, U05</p></div><p id="raw-count" class="small">6 raw submit events; repeat retained, not another completed ID.</p></div><div class="rate"><p>Entrant → completion</p><div id="entry-rate" class="result">40%</div><p>2 completed / 5 entrants</p><hr><p>Qualifying start → completion</p><div id="start-rate" class="result">66.7%</div><p>2 completed / 3 qualifying starts</p></div></section><section class="panel"><h2>Trace an ID through the rule</h2><div id="id-buttons" class="row ids"></div><div class="two"><div><h3 id="trace-title" style="margin-top:20px">U03 · reversed required start</h3><div id="trace" class="trace"></div></div><div><h3 style="margin-top:20px">Source event rows</h3><div class="row"><button id="show-selected" type="button" aria-pressed="true">Selected ID</button><button id="show-all" type="button" class="secondary">All original rows</button></div><div id="events" style="margin-top:15px"></div></div></div></section><section class="panel"><h2>Validate a separate local event fixture</h2><p class="small">JSON rows use id, time (HH:MM) and event. This separate test does not alter the fixed source dashboard. Missing/invalid identity or time holds all affected totals for data review.</p><label for="fixture">Local test rows<textarea id="fixture" class="json">[{"id":"TEST1","time":"09:00","event":"view"},{"id":"TEST1","time":"09:02","event":"form_start"},{"id":"TEST1","time":"09:05","event":"submit"}]</textarea></label><button id="calculate-fixture" type="button">Reconcile test fixture</button><p id="fixture-result" class="notice" role="status">Not calculated.</p></section><details><summary>Native export / definition handoff</summary><p>Confirm native event/identity fields and mapping, timestamp basis, closed entry, indirect-follow conditions, period and source completeness with the analyst’s maintained export/query. Preserve original row order and repeated submissions. ID counts are not verified people, session rates, leads or sales.</p><p>HTML reconciliation companion only; no live GA4 connection, configured mapping, report-parity proof, identity lookup, tracking, cause diagnosis, conversion advice or saved edits.</p></details></main><script>
'use strict';const $=id=>document.getElementById(id);

const raw=[['U01','09:00','view'],['U01','09:02','form_start'],['U01','09:05','submit'],['U02','09:00','view'],['U02','09:05','submit'],['U03','09:00','form_start'],['U03','09:02','view'],['U03','09:05','submit'],['U04','09:00','view'],['U04','09:02','form_start'],['U05','09:00','view'],['U05','09:02','form_start'],['U05','09:05','submit'],['U05','09:06','submit'],['U06','09:00','form_start'],['U06','09:05','submit']].map(([id,time,event])=>({id,time,event}));function reconcile(rows){if(!Array.isArray(rows))return{error:'CHECK DATA: supply an array of event rows.'};for(const r of rows)if(!r||typeof r.id!=='string'||!r.id.trim()||r.id.trim()!==r.id||typeof r.time!=='string'||!/^(09:[0-5][0-9]|10:00)$/.test(r.time)||typeof r.event!=='string'||!r.event.trim())return{error:'CHECK DATA: each row needs a nonblank exact ID, a valid 09:00–10:00 local time and event name.'};const ids=[...new Set(rows.map(r=>r.id))],entry=[],start=[],complete=[],journeys=Object.create(null);for(const id of ids){const events=rows.map((r,i)=>({...r,index:i})).filter(r=>r.id===id).sort((a,b)=>a.time.localeCompare(b.time)||a.index-b.index);let phase=0;for(const r of events){if(phase===0&&r.event==='view')phase=1;else if(phase===1&&r.event==='form_start')phase=2;else if(phase===2&&r.event==='submit')phase=3;}if(phase>=1)entry.push(id);if(phase>=2)start.push(id);if(phase===3)complete.push(id);journeys[id]={phase,events};}return{entry,start,complete,rawSubmits:rows.filter(r=>r.event==='submit').length,entryRate:entry.length?complete.length/entry.length*100:null,startRate:start.length?complete.length/start.length*100:null,journeys};}const summary=reconcile(raw);let selected='U03',all=false;const reasons={U01:'Completes view → form_start → submit in order.',U02:'Submit exists, but the required form_start is missing. Entry only.',U03:'09:00 form_start precedes the 09:02 view. No later start is supplied; entry only, not an ordered start or completion.',U04:'View and qualifying start are supplied; no submit follows.',U05:'Two raw submit events at 09:05 and 09:06 remain in the ledger. One ID completes the sequence once.',U06:'No view is supplied. Not a closed-funnel entrant; raw start and submit remain evidence.'};function render(){for(const id of Object.keys(reasons))$('inspect-'+id).setAttribute('aria-pressed',String(id===selected));$('trace-title').textContent=selected+' · journey audit';$('trace').textContent=reasons[selected]+' Stage membership: '+(['outside closed entry','entry only','entry + qualifying start','entry + qualifying start + completion'][summary.journeys[selected].phase])+'.';$('show-selected').setAttribute('aria-pressed',String(!all));$('show-all').setAttribute('aria-pressed',String(all));$('events').replaceChildren();for(const r of raw.filter(r=>all||r.id===selected)){const div=document.createElement('div');div.className='event-row';for(const s of[r.id,r.time,r.event]){const span=document.createElement('span');span.textContent=s;div.appendChild(span);}$('events').appendChild(div);}}for(const id of Object.keys(reasons)){const b=document.createElement('button');b.type='button';b.id='inspect-'+id;b.textContent=id;b.className='secondary';b.addEventListener('click',()=>{selected=id;render();});$('id-buttons').appendChild(b);}for(const id of['selected','all'])$('show-'+id).addEventListener('click',()=>{all=id==='all';render();});$('fixture').addEventListener('input',()=>{$('fixture-result').textContent='Not calculated. Previous test result cleared.';});$('calculate-fixture').addEventListener('click',()=>{let rows;try{rows=JSON.parse($('fixture').value);}catch{$('fixture-result').textContent='CHECK DATA: invalid JSON; results unresolved.';return;}const r=reconcile(rows);$('fixture-result').textContent=r.error||r.entry.length+' entrant'+(r.entry.length===1?'':'s')+' / '+r.start.length+' qualifying start'+(r.start.length===1?'':'s')+' / '+r.complete.length+' completed ID'+(r.complete.length===1?'':'s')+'; '+r.rawSubmits+' raw submit event'+(r.rawSubmits===1?'':'s')+'. Entry completion: '+(r.entryRate===null?'undefined (no entrants)':r.entryRate.toFixed(1)+'%')+'.';});render();
</script></body></html>