diff --git a/before/script.js b/after/script.js index c58a571..ddc3b46 100644 --- a/before/script.js +++ b/after/script.js @@ -1,10 +1,9 @@ /* - BEFORE — Broken lead-capture workflow (DEMO) - ============================================ - This script intentionally reproduces realistic automation failures so the - "after" repair can be compared against it. Every intentional bug is marked - with a "BUG:" comment explaining the problem and what it maps to in a real - Zapier / Make / n8n / webhook setup. + AFTER — Repaired lead-capture workflow (DEMO) + ============================================= + This is the "before" workflow after the Repair Package. Each fix is marked + with a "FIX:" comment showing what was repaired and what it maps to in a + real Zapier / Make / n8n / webhook setup. This is a demo. Nothing is sent to a real CRM, Slack workspace, or inbox. */ @@ -14,12 +13,31 @@ const crm = []; const form = document.getElementById("lead-form"); const statusPanel = document.getElementById("status-panel"); -const vagueError = document.getElementById("vague-error"); const crmBody = document.getElementById("crm-body"); const slackPanel = document.getElementById("slack-panel"); const emailPanel = document.getElementById("email-panel"); +const logEl = document.getElementById("error-log"); const devEvidence = document.getElementById("dev-evidence"); +let logStarted = false; + +function ts() { + return new Date().toLocaleTimeString("en-GB", { hour12: false }); +} + +function log(kind, message) { + if (!logStarted) { + logEl.innerHTML = ""; + logStarted = true; + } + const line = document.createElement("div"); + line.className = `log-line log-${kind}`; + const label = { ok: "OK ", err: "ERR ", warn: "WARN", info: "INFO" }[kind] || "INFO"; + line.innerHTML = `${ts()} ${label} ${message}`; + logEl.appendChild(line); + logEl.scrollTop = logEl.scrollHeight; +} + function setStatus(rows) { statusPanel.innerHTML = rows .map((r) => `
${r.text}
`) @@ -32,39 +50,67 @@ function renderCrm() { return; } crmBody.innerHTML = crm - .map((lead, i) => { - const dupClass = lead.duplicate ? "row-dup" : ""; - const badge = lead.duplicate - ? `duplicate` - : `unverified`; - return ` + .map( + (lead, i) => ` ${i + 1} - ${lead.name || "—"} - ${lead.email || "—"} + ${lead.name} + ${lead.email} ${lead.company || "—"} - ${badge} - `; - }) + verified + ` + ) .join(""); } -// BUG: webhook handling is unreliable and non-deterministic. -// Maps to: a Zapier/Make webhook step that returns 422 but the Zap continues anyway. -function fakeWebhookCall(lead) { - // Returns a fake HTTP-style response. Sometimes "fails" but the caller ignores it. - if (!lead.email || !lead.email.includes("@") || lead.email.endsWith("@")) { - return { ok: false, status: 422, body: "Unprocessable Entity — missing or invalid email" }; - } - // BUG: even "ok" responses are flaky — ~30% randomly fail, simulating an unreliable endpoint. - if (Math.random() < 0.3) { - return { ok: false, status: 502, body: "Bad Gateway — upstream timeout" }; +// FIX: proper email validation before anything else runs. +// Maps to: a Zapier "Filter"/"Formatter" or Make validation module that stops +// invalid leads from continuing through the workflow. +function isValidEmail(email) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} + +// FIX: reliable webhook handling with bounded retries. +// Maps to: configuring retry-on-error in the webhook/HTTP step instead of +// silently continuing past failures. +function callWebhookWithRetry(lead, maxAttempts = 3) { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + // Simulated flaky upstream: occasionally returns 502, but we now retry. + const flaky = Math.random() < 0.3 && attempt < maxAttempts; + if (flaky) { + log("warn", `Webhook attempt ${attempt} failed (502) — retrying`); + continue; + } + log("ok", `Webhook responded 200 on attempt ${attempt}`); + return { ok: true, status: 200, attempts: attempt }; } - return { ok: true, status: 200, body: "OK" }; + return { ok: false, status: 502, attempts: maxAttempts }; +} + +function renderSlack(lead) { + slackPanel.innerHTML = ` +
+
WF
+
+ Workflow Bot #leads · just now +
:tada: New lead captured
+
${lead.name} — ${lead.email}${lead.company ? " · " + lead.company : ""}
+
Saved to CRM · confirmation email queued
+
+
`; +} + +function renderEmail(lead) { + const first = lead.name.split(" ")[0] || "there"; + emailPanel.innerHTML = ` +
+
Thanks for reaching out, ${first}
+
To: ${lead.email} · From: team@example.com
+
Hi ${first}, we've received your details and a member of the team will follow up shortly. — The Team
+
`; } form.addEventListener("submit", (e) => { e.preventDefault(); - vagueError.classList.add("hidden"); const data = new FormData(form); const lead = { @@ -73,72 +119,81 @@ form.addEventListener("submit", (e) => { company: (data.get("company") || "").trim(), }; - const evidence = []; - evidence.push(`> POST /webhook/lead`); - evidence.push(`> payload: ${JSON.stringify(lead)}`); - - // BUG: NO validation. Invalid emails and empty names are accepted into the pipeline. - // Maps to: missing "Filter"/"Formatter" validation step in Zapier/Make. - - // BUG: NO duplicate check. The same lead can be inserted multiple times. - // Maps to: missing "Find Record" / "Lookup Row" step before "Create Row". - const isDuplicate = crm.some((l) => l.email && l.email === lead.email); - - // Call the unreliable webhook but mostly ignore the result. - const resp = fakeWebhookCall(lead); - evidence.push(`< ${resp.status} ${resp.body}`); + setStatus([{ state: "warn", text: "Processing…" }]); + log("info", `Lead submitted: ${lead.email || "(no email)"}`); + + // FIX: validate required fields and email format before processing. + if (!lead.name || !isValidEmail(lead.email)) { + const reason = !lead.name ? "name is required" : "email is invalid"; + log("err", `Validation failed: ${reason}`); + setStatus([ + { state: "bad", text: `Validation failed: ${reason}` }, + { state: "idle", text: "Lead not saved — fix the input and resubmit" }, + ]); + devEvidence.textContent = + `> POST /webhook/lead\n> payload: ${JSON.stringify(lead)}\n< 422 Unprocessable Entity — ${reason}\n` + + `Validation step rejected the lead before any CRM write. No partial/duplicate data created.`; + return; + } + log("ok", "Form validated"); + + // FIX: duplicate detection BEFORE writing to the CRM. + // Maps to: a "Find Record" / "Lookup Row" step before "Create Row". + const duplicate = crm.find((l) => l.email.toLowerCase() === lead.email.toLowerCase()); + if (duplicate) { + log("warn", `Duplicate detected: ${lead.email} already in CRM — skipping insert`); + setStatus([ + { state: "good", text: "Form validated" }, + { state: "warn", text: "Duplicate detected — existing record kept" }, + ]); + devEvidence.textContent = + `> lookup email=${lead.email}\n< match found (id ${crm.indexOf(duplicate) + 1})\n` + + `Duplicate check passed: insert skipped, no duplicate row created.`; + return; + } + log("ok", "Duplicate check passed"); + + // FIX: reliable webhook handling with retries. + const resp = callWebhookWithRetry(lead); + if (!resp.ok) { + log("err", "Webhook failed after retries — lead queued for manual review"); + setStatus([ + { state: "good", text: "Form validated" }, + { state: "good", text: "Duplicate check passed" }, + { state: "bad", text: "Webhook failed after retries (surfaced clearly)" }, + ]); + devEvidence.textContent = + `> POST /webhook/lead (x${resp.attempts} attempts)\n< 502 Bad Gateway\n` + + `Failure is surfaced to the user and logged — not swallowed.`; + return; + } - // BUG: the workflow saves the lead regardless of webhook outcome. - // It also flags duplicates only for display, but still inserts them. - crm.push({ ...lead, duplicate: isDuplicate }); + // FIX: confirmed CRM write only after validation + dedupe + webhook success. + crm.push(lead); renderCrm(); + log("ok", `CRM row created for ${lead.email}`); - // BUG: notification step "fails silently" — error is swallowed, user never sees it. - // Maps to: a Slack action that errors but the workflow is set to "continue on error" - // with no alerting, so the team is never notified. - try { - if (resp.status !== 200) { - throw new Error("notification skipped due to upstream error"); - } - // Even on success, this branch is broken and never renders anything. - // (Intentional: the message is built but never written to the panel.) - const _silentlyDropped = `New lead: ${lead.name}`; - void _silentlyDropped; - } catch (err) { - // Swallowed. Nothing surfaced to the UI or logs. - console.error("[before] Notification step failed silently:", err.message); - } - // Slack panel is never updated — stays empty. + // FIX: notification actually renders and is logged (no silent failure). + renderSlack(lead); + log("ok", "Internal notification queued (Slack mock)"); - // BUG: confirmation email is never triggered at all. The step simply does not exist. - // Maps to: a missing "Send Email" / Gmail action in the workflow. + // FIX: confirmation email step added and logged. + renderEmail(lead); + log("ok", "Confirmation email queued (email mock)"); - // BUG: vague, unhelpful error. No indication of which step failed or why. - // Maps to: a generic "Something went wrong" with no run log. setStatus([ - { state: "warn", text: "Submission received" }, - { state: "bad", text: "Workflow status unclear" }, + { state: "good", text: "Form validated" }, + { state: "good", text: "Duplicate check passed" }, + { state: "good", text: "CRM row created" }, + { state: "good", text: "Internal notification queued" }, + { state: "good", text: "Confirmation email queued" }, ]); - vagueError.textContent = "Something went wrong."; - vagueError.classList.remove("hidden"); - // Developer evidence: realistic raw signals, but no structured log for the user. - evidence.push(""); - if (isDuplicate) { - evidence.push(`! Duplicate lead inserted without check (email already exists: ${lead.email})`); - } - if (resp.status !== 200) { - evidence.push(`! Webhook error: ${resp.status} ${resp.body}`); - } - evidence.push(`! Notification step failed silently (no alert raised)`); - evidence.push(`! Confirmation email step missing — no email queued`); - evidence.push(`! No retry logic, no structured run log`); - evidence.push(`! Manual admin follow-up still required`); - devEvidence.textContent = evidence.join("\n"); - - // BUG: error log panel is never populated — there is no real logging. - // Console gets one realistic error line; the user-facing log stays empty. - console.error( - "[before] Webhook error: 422 Unprocessable Entity — missing email (example of unlogged failure)" - ); + devEvidence.textContent = + `> POST /webhook/lead\n> payload: ${JSON.stringify(lead)}\n` + + `< 200 OK (after ${resp.attempts} attempt${resp.attempts > 1 ? "s" : ""})\n\n` + + `No workflow-blocking errors detected in this demo.\n` + + `Steps: validated -> duplicate-checked -> CRM write -> notification -> email -> logged.`; + + form.reset(); });