{"name":"Store conversion booster workflow","nodes":[{"id":"38aa0000-0001-4a11-8b22-c33344445555","name":"Every 6 Hours","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[-260,300],"parameters":{"rule":{"interval":[{"field":"hours","hoursInterval":6}]}}},{"id":"38aa0000-0002-4a11-8b22-c33344445555","name":"Fetch Yesterday CVR By Page + Device","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,300],"parameters":{"method":"POST","url":"https://analyticsdata.googleapis.com/v1beta/properties/GA4_PROPERTY_ID:runReport","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ dateRanges: [{ startDate: \"yesterday\", endDate: \"yesterday\" }], dimensions: [{ name: \"landingPagePlusQueryString\" }, { name: \"deviceCategory\" }], metrics: [{ name: \"sessions\" }, { name: \"ecommercePurchases\" }, { name: \"purchaseRevenue\" }, { name: \"addToCarts\" }, { name: \"checkouts\" }], limit: 250 }) }}","options":{}}},{"id":"38aa0000-0003-4a11-8b22-c33344445555","name":"Fetch Trailing 28d Baseline","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[0,100],"parameters":{"method":"POST","url":"https://analyticsdata.googleapis.com/v1beta/properties/GA4_PROPERTY_ID:runReport","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ dateRanges: [{ startDate: \"29daysAgo\", endDate: \"2daysAgo\" }], dimensions: [{ name: \"landingPagePlusQueryString\" }, { name: \"deviceCategory\" }], metrics: [{ name: \"sessions\" }, { name: \"ecommercePurchases\" }, { name: \"purchaseRevenue\" }, { name: \"addToCarts\" }, { name: \"checkouts\" }], limit: 250 }) }}","options":{}}},{"id":"38aa0000-0004-4a11-8b22-c33344445555","name":"Merge Current + Baseline","type":"n8n-nodes-base.merge","typeVersion":3,"position":[260,300],"parameters":{"mode":"append","options":{}}},{"id":"38aa0000-0005-4a11-8b22-c33344445555","name":"Score Conversion Drops","type":"n8n-nodes-base.code","typeVersion":2,"position":[520,300],"parameters":{"jsCode":"// ---------------------------------------------------------------------------\n// Conversion-drop detector.\n// Input item 0 = yesterday's GA4 runReport, item 1 = trailing 28d baseline.\n// Both come back as { rows: [ { dimensionValues:[...], metricValues:[...] } ] }.\n// We join on landing_page + device, run a two-proportion z-test on\n// (purchases / sessions) and score how much money the drop is costing.\n// ---------------------------------------------------------------------------\nconst items = $input.all().map(i => i.json);\nconst parse = (payload) => {\n  const out = {};\n  for (const row of (payload && payload.rows) || []) {\n    const d = (row.dimensionValues || []).map(v => v.value);\n    const m = (row.metricValues || []).map(v => Number(v.value) || 0);\n    const key = d[0] + '|' + d[1];\n    out[key] = {\n      landing_page: d[0],\n      device: d[1],\n      sessions: m[0],\n      purchases: m[1],\n      revenue: m[2],\n      add_to_carts: m[3],\n      checkouts: m[4],\n    };\n  }\n  return out;\n};\n\nconst current  = parse(items[0]);\nconst baseline = parse(items[1]);\n\n// Baseline is 28 days of traffic, so per-day expectations need scaling.\nconst BASELINE_DAYS = 28;\nconst MIN_SESSIONS_YESTERDAY = 150;   // below this a z-test is noise\nconst MIN_BASE_PURCHASES     = 20;    // need a stable baseline to compare against\nconst REL_DROP_TRIGGER       = 0.20;  // must be at least 20% worse\nconst Z_TRIGGER              = 1.96;  // 95% two-tailed\n\nconst results = [];\nfor (const key of Object.keys(current)) {\n  const cur = current[key];\n  const base = baseline[key];\n  if (!base) continue;\n\n  const curCvr  = cur.sessions  ? cur.purchases  / cur.sessions  : 0;\n  const baseCvr = base.sessions ? base.purchases / base.sessions : 0;\n  if (!baseCvr) continue;\n\n  const relDelta = (curCvr - baseCvr) / baseCvr;          // negative = drop\n\n  // Two-proportion z-test: pooled proportion, then standard error.\n  const pooled = (cur.purchases + base.purchases) / (cur.sessions + base.sessions);\n  const se = Math.sqrt(pooled * (1 - pooled) * (1 / cur.sessions + 1 / base.sessions));\n  const z = se > 0 ? (curCvr - baseCvr) / se : 0;\n\n  // Funnel stage deltas tell us WHERE it broke.\n  const rate = (num, den) => (den ? num / den : 0);\n  const atcDelta = rate(cur.add_to_carts, cur.sessions) - rate(base.add_to_carts, base.sessions);\n  const cartToCheckoutDelta =\n    rate(cur.checkouts, cur.add_to_carts) - rate(base.checkouts, base.add_to_carts);\n  const checkoutToPurchaseDelta =\n    rate(cur.purchases, cur.checkouts) - rate(base.purchases, base.checkouts);\n\n  // What the page \"should\" have made yesterday at baseline CVR + baseline AOV.\n  const baseAov = base.purchases ? base.revenue / base.purchases : 0;\n  const expectedPurchases = cur.sessions * baseCvr;\n  const lostPurchases = Math.max(0, expectedPurchases - cur.purchases);\n  const lostRevenue = Math.round(lostPurchases * baseAov * 100) / 100;\n\n  // 30-day pacing projection if nothing is fixed.\n  const projected30dLoss = Math.round(lostRevenue * 30 * 100) / 100;\n\n  const enoughVolume = cur.sessions >= MIN_SESSIONS_YESTERDAY &&\n                       base.purchases >= MIN_BASE_PURCHASES;\n  const significant = enoughVolume && z <= -Z_TRIGGER && relDelta <= -REL_DROP_TRIGGER;\n\n  // Severity 0-100: money weight (60) + statistical confidence (25) + size of drop (15).\n  const moneyScore = Math.min(60, (lostRevenue / 500) * 60);\n  const zScore = Math.min(25, (Math.abs(z) / 5) * 25);\n  const dropScore = Math.min(15, (Math.abs(relDelta) / 0.6) * 15);\n  const severity = Math.round(moneyScore + zScore + dropScore);\n\n  results.push({\n    dedupe_key: (cur.landing_page + '::' + cur.device).toLowerCase(),\n    landing_page: cur.landing_page,\n    device: cur.device,\n    sessions: cur.sessions,\n    purchases: cur.purchases,\n    revenue: cur.revenue,\n    cvr: Math.round(curCvr * 10000) / 100,\n    baseline_cvr: Math.round(baseCvr * 10000) / 100,\n    baseline_sessions_per_day: Math.round(base.sessions / BASELINE_DAYS),\n    rel_delta_pct: Math.round(relDelta * 1000) / 10,\n    z_score: Math.round(z * 100) / 100,\n    atc_rate_delta_pct: Math.round(atcDelta * 1000) / 10,\n    cart_to_checkout_delta_pct: Math.round(cartToCheckoutDelta * 1000) / 10,\n    checkout_to_purchase_delta_pct: Math.round(checkoutToPurchaseDelta * 1000) / 10,\n    baseline_aov: Math.round(baseAov * 100) / 100,\n    lost_revenue: lostRevenue,\n    projected_30d_loss: projected30dLoss,\n    severity,\n    significant,\n    enough_volume: enoughVolume,\n  });\n}\n\nresults.sort((a, b) => b.severity - a.severity);\nreturn results.map(r => ({ json: r }));"}},{"id":"38aa0000-0006-4a11-8b22-c33344445555","name":"Keep Significant Drops","type":"n8n-nodes-base.filter","typeVersion":2,"position":[780,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"f1","leftValue":"={{ $json.significant }}","rightValue":true,"operator":{"type":"boolean","operation":"true"}},{"id":"f2","leftValue":"={{ $json.lost_revenue }}","rightValue":100,"operator":{"type":"number","operation":"gte"}}]},"options":{}}},{"id":"38aa0000-0007-4a11-8b22-c33344445555","name":"Severity 60+?","type":"n8n-nodes-base.if","typeVersion":2,"position":[1040,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"i1","leftValue":"={{ $json.severity }}","rightValue":60,"operator":{"type":"number","operation":"gte"}}]},"options":{}}},{"id":"38aa0000-0008-4a11-8b22-c33344445555","name":"Log Low Priority Watchlist","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[1300,520],"parameters":{"operation":"append","documentId":{"__rl":true,"value":"SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=1","mode":"list","cachedResultName":"CVR Watchlist"},"columns":{"mappingMode":"autoMapInputData","value":{},"matchingColumns":[]},"options":{}}},{"id":"38aa0000-0009-4a11-8b22-c33344445555","name":"Fetch Page Speed Diagnostics","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1300,300],"parameters":{"url":"https://www.googleapis.com/pagespeedonline/v5/runPagespeed","sendQuery":true,"queryParameters":{"parameters":[{"name":"url","value":"=https://shop.example.com{{ $json.landing_page }}"},{"name":"strategy","value":"={{ $json.device === \"desktop\" ? \"desktop\" : \"mobile\" }}"},{"name":"category","value":"performance"}]},"options":{}}},{"id":"38aa0000-0010-4a11-8b22-c33344445555","name":"Read Open CVR Tickets","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[1300,80],"parameters":{"operation":"read","documentId":{"__rl":true,"value":"SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=0","mode":"list","cachedResultName":"CVR Tickets"},"options":{}}},{"id":"38aa0000-0011-4a11-8b22-c33344445555","name":"Merge Diagnostics + Ticket History","type":"n8n-nodes-base.merge","typeVersion":3,"position":[1560,300],"parameters":{"mode":"append","options":{}}},{"id":"38aa0000-0012-4a11-8b22-c33344445555","name":"Diagnose And Dedupe","type":"n8n-nodes-base.code","typeVersion":2,"position":[1820,300],"parameters":{"jsCode":"// ---------------------------------------------------------------------------\n// Turn a statistically significant CVR drop into a diagnosed, deduped ticket.\n// Input 0 = PageSpeed Insights response, input 1 = open-ticket rows from Sheets.\n// The drop metrics themselves are pulled back off the scoring node by index.\n// ---------------------------------------------------------------------------\nconst items = $input.all().map(i => i.json);\nconst drops = $('Score Conversion Drops').all().map(i => i.json)\n  .filter(d => d.significant && d.lost_revenue >= 100 && d.severity >= 60);\n\n// Open tickets, keyed for O(1) dedupe. Anything touched in the last 14 days\n// for the same page+device counts as already handled.\nconst openTickets = {};\nfor (const row of items.filter(r => r && r.dedupe_key)) {\n  const opened = new Date(row.opened_at || 0).getTime();\n  const ageDays = (Date.now() - opened) / 86400000;\n  if (String(row.status || '').toLowerCase() !== 'closed' || ageDays <= 14) {\n    openTickets[String(row.dedupe_key).toLowerCase()] = { ...row, age_days: Math.round(ageDays) };\n  }\n}\n\nconst out = [];\ndrops.forEach((drop, idx) => {\n  const ps = items[idx] || {};\n  const audits = (ps.lighthouseResult && ps.lighthouseResult.audits) || {};\n  const num = (id) => (audits[id] && typeof audits[id].numericValue === 'number')\n    ? audits[id].numericValue : null;\n  const perfScore = ps.lighthouseResult && ps.lighthouseResult.categories &&\n    ps.lighthouseResult.categories.performance\n      ? Math.round(ps.lighthouseResult.categories.performance.score * 100) : null;\n\n  const lcp = num('largest-contentful-paint');\n  const cls = num('cumulative-layout-shift');\n  const tbt = num('total-blocking-time');\n\n  // Root-cause ranking. First matching rule wins, but we keep all evidence.\n  const causes = [];\n  if (lcp !== null && lcp > 4000) causes.push('LCP ' + Math.round(lcp) + 'ms (>4s) — hero/image weight');\n  if (cls !== null && cls > 0.25) causes.push('CLS ' + cls.toFixed(2) + ' — layout shift pushing the ATC button');\n  if (tbt !== null && tbt > 600) causes.push('TBT ' + Math.round(tbt) + 'ms — third-party JS blocking interaction');\n  if (drop.atc_rate_delta_pct <= -2) causes.push('Add-to-cart rate down ' + Math.abs(drop.atc_rate_delta_pct) + 'pp — PDP/offer problem');\n  if (drop.cart_to_checkout_delta_pct <= -3) causes.push('Cart→checkout down ' + Math.abs(drop.cart_to_checkout_delta_pct) + 'pp — cart drawer / shipping cost shock');\n  if (drop.checkout_to_purchase_delta_pct <= -3) causes.push('Checkout→purchase down ' + Math.abs(drop.checkout_to_purchase_delta_pct) + 'pp — payment gateway or discount code failing');\n  if (!causes.length) causes.push('No single funnel stage or CWV failed — check traffic mix / new ad placements sending colder visitors');\n\n  const primary = causes[0];\n  const stage = drop.checkout_to_purchase_delta_pct <= -3 ? 'checkout'\n    : drop.cart_to_checkout_delta_pct <= -3 ? 'cart'\n    : drop.atc_rate_delta_pct <= -2 ? 'product_page'\n    : 'traffic_quality';\n\n  const priority = drop.severity >= 85 ? 'P1' : drop.severity >= 70 ? 'P2' : 'P3';\n  const existing = openTickets[drop.dedupe_key];\n\n  out.push({\n    json: {\n      ...drop,\n      is_new: !existing,\n      existing_ticket_url: existing ? existing.ticket_url || '' : '',\n      existing_ticket_age_days: existing ? existing.age_days : null,\n      perf_score: perfScore,\n      lcp_ms: lcp === null ? null : Math.round(lcp),\n      cls: cls === null ? null : Math.round(cls * 1000) / 1000,\n      tbt_ms: tbt === null ? null : Math.round(tbt),\n      broken_stage: stage,\n      primary_cause: primary,\n      all_causes: causes.join(' | '),\n      priority,\n      opened_at: new Date().toISOString(),\n      status: 'open',\n      ticket_title: '[' + priority + '] CVR ' + drop.rel_delta_pct + '% on ' +\n        drop.landing_page + ' (' + drop.device + ')',\n      ticket_body: [\n        'CVR ' + drop.cvr + '% vs baseline ' + drop.baseline_cvr + '% (' + drop.rel_delta_pct + '%, z=' + drop.z_score + ')',\n        'Sessions yesterday: ' + drop.sessions + ' | purchases: ' + drop.purchases,\n        'Lost revenue yesterday: $' + drop.lost_revenue + ' | projected 30d: $' + drop.projected_30d_loss,\n        'Broken stage: ' + stage,\n        'Diagnosis: ' + causes.join(' | '),\n        'Lighthouse perf: ' + perfScore + ' | LCP ' + lcp + 'ms | CLS ' + cls + ' | TBT ' + tbt + 'ms',\n      ].join('\\n'),\n    },\n  });\n});\n\nreturn out;"}},{"id":"38aa0000-0013-4a11-8b22-c33344445555","name":"New Issue?","type":"n8n-nodes-base.if","typeVersion":2,"position":[2080,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"n1","leftValue":"={{ $json.is_new }}","rightValue":true,"operator":{"type":"boolean","operation":"true"}}]},"options":{}}},{"id":"38aa0000-0014-4a11-8b22-c33344445555","name":"Build Fix Ticket","type":"n8n-nodes-base.set","typeVersion":3.3,"position":[2340,100],"parameters":{"assignments":{"assignments":[{"id":"t1","name":"Name","value":"={{ $json.ticket_title }}","type":"string"},{"id":"t2","name":"Details","value":"={{ $json.ticket_body }}","type":"string"},{"id":"t3","name":"Priority","value":"={{ $json.priority }}","type":"string"},{"id":"t4","name":"Stage","value":"={{ $json.broken_stage }}","type":"string"},{"id":"t5","name":"Landing Page","value":"={{ $json.landing_page }}","type":"string"},{"id":"t6","name":"Device","value":"={{ $json.device }}","type":"string"},{"id":"t7","name":"Lost Revenue","value":"={{ $json.lost_revenue }}","type":"number"},{"id":"t8","name":"dedupe_key","value":"={{ $json.dedupe_key }}","type":"string"},{"id":"t9","name":"opened_at","value":"={{ $json.opened_at }}","type":"string"},{"id":"t10","name":"status","value":"open","type":"string"}]},"options":{}}},{"id":"38aa0000-0015-4a11-8b22-c33344445555","name":"Create CRO Fix Ticket","type":"n8n-nodes-base.airtable","typeVersion":2.1,"position":[2600,100],"parameters":{"operation":"create","base":{"__rl":true,"value":"appCROBOARD","mode":"id"},"table":{"__rl":true,"value":"tblFixTickets","mode":"id"},"columns":{"mappingMode":"autoMapInputData","value":{}},"options":{}}},{"id":"38aa0000-0016-4a11-8b22-c33344445555","name":"Alert CRO Channel","type":"n8n-nodes-base.slack","typeVersion":2.2,"position":[2860,100],"parameters":{"select":"channel","channelId":{"__rl":true,"value":"#cro-alerts","mode":"name"},"text":"=🔻 *{{ $json.Priority }}* {{ $json[\"Landing Page\"] }} ({{ $json.Device }}) — CVR dropped, ${{ $json[\"Lost Revenue\"] }} lost yesterday. Stage: {{ $json.Stage }}. Ticket opened.","otherOptions":{}}},{"id":"38aa0000-0017-4a11-8b22-c33344445555","name":"Log Ticket To Sheet","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[3120,100],"parameters":{"operation":"append","documentId":{"__rl":true,"value":"SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=0","mode":"list","cachedResultName":"CVR Tickets"},"columns":{"mappingMode":"autoMapInputData","value":{},"matchingColumns":[]},"options":{}}},{"id":"38aa0000-0018-4a11-8b22-c33344445555","name":"Nudge Existing Ticket","type":"n8n-nodes-base.slack","typeVersion":2.2,"position":[2340,500],"parameters":{"select":"channel","channelId":{"__rl":true,"value":"#cro-alerts","mode":"name"},"text":"=⏳ Still bleeding: {{ $json.landing_page }} ({{ $json.device }}) is *{{ $json.existing_ticket_age_days }} days* into an open ticket and still {{ $json.rel_delta_pct }}% below baseline — ${{ $json.projected_30d_loss }} projected 30d loss. {{ $json.existing_ticket_url }}","otherOptions":{}}},{"id":"38aa0000-0019-4a11-8b22-c33344445555","name":"Email Growth Team The Ticket","type":"n8n-nodes-base.gmail","typeVersion":2.1,"position":[3380,100],"parameters":{"sendTo":"growth@shop.example.com","subject":"=CVR watch {{ $now.format('yyyy-LL-dd') }} — {{ $json.Priority }} on {{ $json[\"Landing Page\"] }}","message":"={{ $json.Details }}","options":{}}},{"id":"38aa0000-0020-4a11-8b22-c33344445555","name":"Note Collect","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[-300,-160],"parameters":{"content":"## 1. COLLECT\nEvery 6h pull GA4 landing-page × device conversion data for yesterday and for the trailing 28 days, then append both reports so the scorer can join them on page+device.","height":300,"width":520,"color":4}},{"id":"38aa0000-0021-4a11-8b22-c33344445555","name":"Note Detect","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[500,-160],"parameters":{"content":"## 2. DETECT\nTwo-proportion z-test on purchases/sessions vs the trailing baseline. Needs 150+ sessions, a 20%+ relative drop and z ≤ -1.96. Severity = money (60) + confidence (25) + drop size (15). Under 60 goes to the watchlist instead.","height":300,"width":520,"color":3}},{"id":"38aa0000-0022-4a11-8b22-c33344445555","name":"Note Diagnose","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[1800,-160],"parameters":{"content":"## 3. DIAGNOSE + TICKET\nPageSpeed CWVs plus funnel-stage deltas name the broken stage. Deduped against open tickets (14-day window): new → Airtable ticket + Slack + Sheet + digest, existing → aging nudge.","height":300,"width":520,"color":5}}],"connections":{"Every 6 Hours":{"main":[[{"node":"Fetch Yesterday CVR By Page + Device","type":"main","index":0},{"node":"Fetch Trailing 28d Baseline","type":"main","index":0}]]},"Fetch Yesterday CVR By Page + Device":{"main":[[{"node":"Merge Current + Baseline","type":"main","index":0}]]},"Fetch Trailing 28d Baseline":{"main":[[{"node":"Merge Current + Baseline","type":"main","index":1}]]},"Merge Current + Baseline":{"main":[[{"node":"Score Conversion Drops","type":"main","index":0}]]},"Score Conversion Drops":{"main":[[{"node":"Keep Significant Drops","type":"main","index":0}]]},"Keep Significant Drops":{"main":[[{"node":"Severity 60+?","type":"main","index":0}]]},"Severity 60+?":{"main":[[{"node":"Fetch Page Speed Diagnostics","type":"main","index":0},{"node":"Read Open CVR Tickets","type":"main","index":0}],[{"node":"Log Low Priority Watchlist","type":"main","index":0}]]},"Fetch Page Speed Diagnostics":{"main":[[{"node":"Merge Diagnostics + Ticket History","type":"main","index":0}]]},"Read Open CVR Tickets":{"main":[[{"node":"Merge Diagnostics + Ticket History","type":"main","index":1}]]},"Merge Diagnostics + Ticket History":{"main":[[{"node":"Diagnose And Dedupe","type":"main","index":0}]]},"Diagnose And Dedupe":{"main":[[{"node":"New Issue?","type":"main","index":0}]]},"New Issue?":{"main":[[{"node":"Build Fix Ticket","type":"main","index":0}],[{"node":"Nudge Existing Ticket","type":"main","index":0}]]},"Build Fix Ticket":{"main":[[{"node":"Create CRO Fix Ticket","type":"main","index":0}]]},"Create CRO Fix Ticket":{"main":[[{"node":"Alert CRO Channel","type":"main","index":0}]]},"Alert CRO Channel":{"main":[[{"node":"Log Ticket To Sheet","type":"main","index":0}]]},"Log Ticket To Sheet":{"main":[[{"node":"Email Growth Team The Ticket","type":"main","index":0}]]}},"settings":{"executionOrder":"v1"},"pinData":{}}