From cae8c2bc57c6777dcddefcb017cb302b67ec223c Mon Sep 17 00:00:00 2001 From: nusquama Date: Sun, 15 Mar 2026 12:02:13 +0800 Subject: [PATCH] creation --- ...y_ga4_wow_email_reports_with_gemini_ai_executive_summary.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 workflows/Send weekly GA4 WoW email reports with Gemini AI executive summary-13993/send_weekly_ga4_wow_email_reports_with_gemini_ai_executive_summary.json diff --git a/workflows/Send weekly GA4 WoW email reports with Gemini AI executive summary-13993/send_weekly_ga4_wow_email_reports_with_gemini_ai_executive_summary.json b/workflows/Send weekly GA4 WoW email reports with Gemini AI executive summary-13993/send_weekly_ga4_wow_email_reports_with_gemini_ai_executive_summary.json new file mode 100644 index 000000000..c9fd06aa0 --- /dev/null +++ b/workflows/Send weekly GA4 WoW email reports with Gemini AI executive summary-13993/send_weekly_ga4_wow_email_reports_with_gemini_ai_executive_summary.json @@ -0,0 +1 @@ +{"id":"7sF6TGsrR5dlGVAx","meta":{"instanceId":"8329856bd6925d4f2750e54c41dcb3cdefa9cd6cf7a589adead15ca668174a9f","templateCredsSetupCompleted":true},"name":"Weekly GA4 Analytics Report For Website/App","tags":[],"nodes":[{"id":"242dcef9-b89b-4d05-b9dd-616c8f51942c","name":"GA4 - Overview Current Week","type":"n8n-nodes-base.googleAnalytics","position":[-544,-464],"parameters":{"endDate":"={{$now.minus({days: 1}).startOf('day').toFormat('yyyy-MM-dd')}}","dateRange":"custom","startDate":"={{$now.minus({days: 7}).startOf('day').toFormat('yyyy-MM-dd')}}","metricsGA4":{"metricValues":[{},{"listName":"sessions"},{"name":"bounce_rate","listName":"custom","expression":"bounceRate"},{"name":"average_session_dur","listName":"custom","expression":"averageSessionDuration"},{"name":"new_users","listName":"custom","expression":"newUsers"}]},"propertyId":{"__rl":true,"mode":"id","value":"481410811"},"dimensionsGA4":{"dimensionValues":[{}]},"additionalFields":{}},"credentials":{"googleAnalyticsOAuth2":{"id":"credential-id","name":"Google Analytics account"}},"executeOnce":true,"typeVersion":2},{"id":"eb6868d0-0dab-49ab-99ba-b34108d16a17","name":"GA4 - Overview Previous Week","type":"n8n-nodes-base.googleAnalytics","position":[-320,-464],"parameters":{"endDate":"={{$now.minus({days: 8}).startOf('day').toFormat('yyyy-MM-dd')}}","dateRange":"custom","startDate":"={{$now.minus({days: 14}).startOf('day').toFormat('yyyy-MM-dd')}}","metricsGA4":{"metricValues":[{},{"listName":"sessions"},{"name":"bounce_rate","listName":"custom","expression":"bounceRate"},{"name":"average_session_dur","listName":"custom","expression":"averageSessionDuration"},{"name":"new_users","listName":"custom","expression":"newUsers"}]},"propertyId":{"__rl":true,"mode":"id","value":"481410811"},"dimensionsGA4":{"dimensionValues":[{}]},"additionalFields":{}},"credentials":{"googleAnalyticsOAuth2":{"id":"credential-id","name":"Google Analytics account"}},"executeOnce":true,"typeVersion":2},{"id":"dca8f70f-f4ad-439a-b8b8-e9395d5b540e","name":"GA4 - Top 5 Pages","type":"n8n-nodes-base.googleAnalytics","position":[-544,-272],"parameters":{"limit":5,"endDate":"={{$now.minus({days: 1}).startOf('day').toFormat('yyyy-MM-dd')}}","dateRange":"custom","startDate":"={{$now.minus({days: 7}).startOf('day').toFormat('yyyy-MM-dd')}}","metricsGA4":{"metricValues":[{"name":"screen_page_view","listName":"custom","expression":"screenPageViews"},{"name":"average_session_duration","listName":"custom","expression":"averageSessionDuration"}]},"propertyId":{"__rl":true,"mode":"id","value":"481410811"},"dimensionsGA4":{"dimensionValues":[{"name":"unifiedScreenName","listName":"other"}]},"additionalFields":{}},"credentials":{"googleAnalyticsOAuth2":{"id":"credential-id","name":"Google Analytics account"}},"executeOnce":true,"typeVersion":2},{"id":"85950514-c48e-4cab-b3c5-159ecc0d7d4a","name":"GA4 - Top 5 Referrals","type":"n8n-nodes-base.googleAnalytics","position":[-544,-80],"parameters":{"limit":5,"endDate":"={{$now.minus({days: 1}).startOf('day').toFormat('yyyy-MM-dd')}}","dateRange":"custom","startDate":"={{$now.minus({days: 7}).startOf('day').toFormat('yyyy-MM-dd')}}","metricsGA4":{"metricValues":[{"listName":"sessions"}]},"propertyId":{"__rl":true,"mode":"id","value":"481410811"},"dimensionsGA4":{"dimensionValues":[{"name":"sessionSourceMedium","listName":"other"}]},"additionalFields":{"orderByUI":{"metricOrderBy":[{"desc":true,"metricName":"sessions"}]}}},"credentials":{"googleAnalyticsOAuth2":{"id":"credential-id","name":"Google Analytics account"}},"executeOnce":true,"typeVersion":2},{"id":"0d766056-e67f-4827-838c-e6fe339231d9","name":"GA4 - Top 5 Events","type":"n8n-nodes-base.googleAnalytics","position":[-544,112],"parameters":{"limit":5,"endDate":"={{$now.minus({days: 1}).startOf('day').toFormat('yyyy-MM-dd')}}","dateRange":"custom","startDate":"={{$now.minus({days: 7}).startOf('day').toFormat('yyyy-MM-dd')}}","metricsGA4":{"metricValues":[{"name":"event_count","listName":"custom","expression":"eventCount"}]},"propertyId":{"__rl":true,"mode":"id","value":"481410811"},"dimensionsGA4":{"dimensionValues":[{"name":"eventName","listName":"other"}]},"additionalFields":{"orderByUI":{"metricOrderBy":[{"desc":true,"metricName":"=event_count"}]}}},"credentials":{"googleAnalyticsOAuth2":{"id":"credential-id","name":"Google Analytics account"}},"executeOnce":true,"typeVersion":2},{"id":"9f0dabdd-36c0-41d3-9052-b9d2cdf8ea85","name":"GA4 - Top 5 Countries","type":"n8n-nodes-base.googleAnalytics","position":[-544,304],"parameters":{"limit":5,"endDate":"={{$now.minus({days: 1}).startOf('day').toFormat('yyyy-MM-dd')}}","dateRange":"custom","startDate":"={{$now.minus({days: 7}).startOf('day').toFormat('yyyy-MM-dd')}}","metricsGA4":{"metricValues":[{"listName":"sessions"}]},"propertyId":{"__rl":true,"mode":"id","value":"481410811"},"dimensionsGA4":{"dimensionValues":[{"listName":"country"}]},"additionalFields":{"orderByUI":{"metricOrderBy":[{"desc":true,"metricName":"sessions"}]}}},"credentials":{"googleAnalyticsOAuth2":{"id":"credential-id","name":"Google Analytics account"}},"executeOnce":true,"typeVersion":2},{"id":"1c982d79-9f8a-47c4-bb35-1b210abb1524","name":"GA4 - Device Breakdown","type":"n8n-nodes-base.googleAnalytics","position":[-544,496],"parameters":{"endDate":"={{$now.minus({days: 1}).startOf('day').toFormat('yyyy-MM-dd')}}","dateRange":"custom","startDate":"={{$now.minus({days: 7}).startOf('day').toFormat('yyyy-MM-dd')}}","metricsGA4":{"metricValues":[{"listName":"sessions"}]},"propertyId":{"__rl":true,"mode":"id","value":"481410811"},"dimensionsGA4":{"dimensionValues":[{"listName":"deviceCategory"}]},"additionalFields":{"orderByUI":{"metricOrderBy":[{"desc":true,"metricName":"sessions"}]}}},"credentials":{"googleAnalyticsOAuth2":{"id":"credential-id","name":"Google Analytics account"}},"executeOnce":true,"typeVersion":2},{"id":"7de9e216-c86a-44b3-9ae5-1872e44e264a","name":"New vs Returning Users Breakdown","type":"n8n-nodes-base.googleAnalytics","position":[-544,688],"parameters":{"endDate":"={{$now.minus({days: 1}).startOf('day').toFormat('yyyy-MM-dd')}}","dateRange":"custom","startDate":"={{$now.minus({days: 7}).startOf('day').toFormat('yyyy-MM-dd')}}","metricsGA4":{"metricValues":[{"listName":"sessions"},{}]},"propertyId":{"__rl":true,"mode":"id","value":"481410811"},"dimensionsGA4":{"dimensionValues":[{"name":"newVsReturning","listName":"other"}]},"additionalFields":{}},"credentials":{"googleAnalyticsOAuth2":{"id":"credential-id","name":"Google Analytics account"}},"executeOnce":true,"typeVersion":2},{"id":"0ea9bb61-586a-4d25-8a15-c7b875464470","name":"Weekly Monday Trigger","type":"n8n-nodes-base.scheduleTrigger","position":[-976,112],"parameters":{"rule":{"interval":[{"field":"weeks","triggerAtDay":[1],"triggerAtHour":8}]}},"typeVersion":1.3},{"id":"43f5b7c2-562e-4003-9517-7022ab148529","name":"Wait for All GA4 Data","type":"n8n-nodes-base.merge","position":[64,32],"parameters":{"mode":"combine","options":{"includeUnpaired":true},"combineBy":"combineByPosition","numberInputs":7},"typeVersion":3.2},{"id":"404f04c1-57ff-456e-917e-62ee967655be","name":"Generate AI Summary","type":"@n8n/n8n-nodes-langchain.googleGemini","position":[368,112],"parameters":{"modelId":{"__rl":true,"mode":"list","value":"models/gemini-3.1-flash-lite-preview","cachedResultName":"models/gemini-3.1-flash-lite-preview"},"options":{},"messages":{"values":[{"content":"=You are a professional digital analytics consultant. Analyze the full week-over-week data below and write a concise executive summary.\n\n=== OVERVIEW ===\nTHIS WEEK — Users: {{$('GA4 - Overview Current Week').first().json.totalUsers}} | Sessions: {{$('GA4 - Overview Current Week').first().json.sessions}} | New Users: {{$('GA4 - Overview Current Week').first().json.new_users}} | Bounce Rate: {{$('GA4 - Overview Current Week').first().json.bounce_rate}} | Avg Duration (sec): {{$('GA4 - Overview Current Week').first().json.average_session_dur}}\nPREV WEEK — Users: {{$('GA4 - Overview Previous Week').first().json.totalUsers}} | Sessions: {{$('GA4 - Overview Previous Week').first().json.sessions}} | New Users: {{$('GA4 - Overview Previous Week').first().json.new_users}} | Bounce Rate: {{$('GA4 - Overview Previous Week').first().json.bounce_rate}} | Avg Duration (sec): {{$('GA4 - Overview Previous Week').first().json.average_session_dur}}\n\n=== TOP SCREENS ===\nTHIS WEEK: {{$('GA4 - Top 5 Pages').all().map((i,idx) => `${idx+1}. ${i.json.unifiedScreenName} (${i.json.screen_page_view} views)`).join(' | ')}}\nPREV WEEK: {{$('GA4 - Top 5 Pages Previous Week').all().map((i,idx) => `${idx+1}. ${i.json.unifiedScreenName} (${i.json.screen_page_view} views)`).join(' | ')}}\n\n=== TRAFFIC SOURCES ===\nTHIS WEEK: {{$('GA4 - Top 5 Referrals').all().map((i,idx) => `${idx+1}. ${i.json.sessionSourceMedium} (${i.json.sessions} sessions)`).join(' | ')}}\nPREV WEEK: {{$('GA4 - Top 5 Referrals Previous Week').all().map((i,idx) => `${idx+1}. ${i.json.sessionSourceMedium} (${i.json.sessions} sessions)`).join(' | ')}}\n\n=== TOP EVENTS ===\nTHIS WEEK: {{$('GA4 - Top 5 Events').all().map((i,idx) => `${idx+1}. ${i.json.eventName} (${i.json.event_count})`).join(' | ')}}\nPREV WEEK: {{$('GA4 - Top 5 Events Previous Week').all().map((i,idx) => `${idx+1}. ${i.json.eventName} (${i.json.event_count})`).join(' | ')}}\n\n=== TOP COUNTRIES ===\nTHIS WEEK: {{$('GA4 - Top 5 Countries').all().map((i,idx) => `${idx+1}. ${i.json.country} (${i.json.sessions} sessions)`).join(' | ')}}\nPREV WEEK: {{$('GA4 - Top 5 Countries Previous Week').all().map((i,idx) => `${idx+1}. ${i.json.country} (${i.json.sessions} sessions)`).join(' | ')}}\n\n=== DEVICES ===\nTHIS WEEK: {{$('GA4 - Device Breakdown').all().map(i => `${i.json.deviceCategory}: ${i.json.sessions}`).join(' | ')}}\nPREV WEEK: {{$('GA4 - Device Breakdown Previous Week').all().map(i => `${i.json.deviceCategory}: ${i.json.sessions}`).join(' | ')}}\n\n=== NEW VS RETURNING ===\nTHIS WEEK: {{$('New vs Returning Users Breakdown').all().map(i => `${i.json.newVsReturning}: ${i.json.totalUsers} users`).join(' | ')}}\nPREV WEEK: {{$('GA4 - New vs Returning Previous Week').all().map(i => `${i.json.newVsReturning}: ${i.json.totalUsers} users`).join(' | ')}}\n\n=== END OF DATA ===\n\nWrite exactly 3 to 5 bullet points summarizing the most important trends and changes. If a country, screen, or source appears in one week but not the other, treat it as entered or dropped from the top 5.\n\nRules:\n- Start each bullet with a dash (-)\n- Single sentence per bullet, maximum 25 words\n- Reference specific numbers and week-over-week changes\n- No markdown, no bold, no headers\n- Do not start any bullet with \"Based on the data\""}]},"builtInTools":{}},"credentials":{"googlePalmApi":{"id":"credential-id","name":"Google Gemini(PaLM) Api account"}},"executeOnce":true,"typeVersion":1.1},{"id":"b173f712-0bfe-4ec1-8ad4-ef011a7bb3a1","name":"Build Report & Email HTML","type":"n8n-nodes-base.code","position":[864,112],"parameters":{"jsCode":"// ============================================================\n// GA4 WEEKLY REPORT v9 — ALL ALIGNMENT FIXED\n// AppStoneLab Technologies\n// ============================================================\n\n// ── 1. PULL DATA ─────────────────────────────────────────────\n\nconst currentWeek = $('GA4 - Overview Current Week').first().json;\nconst previousWeek = $('GA4 - Overview Previous Week').first().json;\n\nconst pages = $('GA4 - Top 5 Pages').all()\n .map(i => i.json)\n .map(p => ({ ...p, unifiedScreenName: (!p.unifiedScreenName || p.unifiedScreenName === '') ? '(empty)' : p.unifiedScreenName }))\n .slice(0, 5);\n\nconst pagesPrev = $('GA4 - Top 5 Pages Previous Week').all()\n .map(i => i.json)\n .map(p => ({ ...p, unifiedScreenName: (!p.unifiedScreenName || p.unifiedScreenName === '') ? '(empty)' : p.unifiedScreenName }));\n\nconst referrals = $('GA4 - Top 5 Referrals').all()\n .map(i => i.json)\n .map(r => ({ ...r, sessionSourceMedium: r.sessionSourceMedium === '(direct) / (none)' ? 'Direct / App Open' : (r.sessionSourceMedium || 'Direct / App Open') }));\n\nconst referralsPrev = $('GA4 - Top 5 Referrals Previous Week').all()\n .map(i => i.json)\n .map(r => ({ ...r, sessionSourceMedium: r.sessionSourceMedium === '(direct) / (none)' ? 'Direct / App Open' : (r.sessionSourceMedium || 'Direct / App Open') }));\n\nconst countries = $('GA4 - Top 5 Countries').all().map(i => i.json);\nconst countriesPrev = $('GA4 - Top 5 Countries Previous Week').all().map(i => i.json);\nconst devices = $('GA4 - Device Breakdown').all().map(i => i.json);\nconst devicesPrev = $('GA4 - Device Breakdown Previous Week').all().map(i => i.json);\nconst newVsRet = $('New vs Returning Users Breakdown').all().map(i => i.json);\nconst newVsRetPrev = $('GA4 - New vs Returning Previous Week').all().map(i => i.json);\n\nconst EXCLUDE_EVENTS = ['first_visit','first_open','page_view','app_remove','os_update','app_update','app_store_refund'];\nconst events = $('GA4 - Top 5 Events').all().map(i => i.json).filter(e => !EXCLUDE_EVENTS.includes(e.eventName)).slice(0, 5);\nconst eventsPrev = $('GA4 - Top 5 Events Previous Week').all().map(i => i.json).filter(e => !EXCLUDE_EVENTS.includes(e.eventName));\n\n// Gemini\nlet aiSummary = '';\ntry {\n const g = $('Generate AI Summary').first().json;\n aiSummary = g.content?.parts?.[0]?.text || g.text || g.output || g.response || g.message?.content || g.candidates?.[0]?.content?.parts?.[0]?.text || '';\n} catch(e) { aiSummary = ''; }\naiSummary = typeof aiSummary === 'string' ? aiSummary : '';\n\nlet formattedAiSummary = '';\nif (aiSummary.trim().length > 0) {\n const lines = aiSummary.split('\\n').map(l => l.trim()).filter(l => l.length > 0);\n const items = lines.map(l => `
  • ${l.replace(/^[\\*\\-•]\\s*/, '')}
  • `).join('');\n formattedAiSummary = ``;\n}\n\n// ── 2. HELPERS ───────────────────────────────────────────────\n\nfunction wowChange(current, previous) {\n const c = parseFloat(current), p = parseFloat(previous);\n if (!p || p === 0) return { text: 'N/A', color: '#9CA3AF', arrow: '—', pill: '#F3F4F6', pillText: '#6B7280' };\n const pct = ((c - p) / p) * 100;\n const up = pct >= 0;\n return { text: (up ? '+' : '') + pct.toFixed(1) + '%', color: up ? '#2D6A4F' : '#B91C1C', arrow: up ? '↑' : '↓', pill: up ? '#ECFDF5' : '#FEF2F2', pillText: up ? '#2D6A4F' : '#B91C1C' };\n}\n\nfunction wowChangeBounce(current, previous) {\n const c = parseFloat(current), p = parseFloat(previous);\n if (!p || p === 0) return { text: 'N/A', color: '#9CA3AF', arrow: '—', pill: '#F3F4F6', pillText: '#6B7280' };\n const pct = ((c - p) / p) * 100;\n const isGood = pct <= 0;\n return { text: (pct >= 0 ? '+' : '') + pct.toFixed(1) + '%', color: isGood ? '#2D6A4F' : '#B91C1C', arrow: isGood ? '↓' : '↑', pill: isGood ? '#ECFDF5' : '#FEF2F2', pillText: isGood ? '#2D6A4F' : '#B91C1C' };\n}\n\nfunction listWoWWithPrev(currItem, prevItems, nameField, valueField) {\n const prev = prevItems.find(p => p[nameField] === currItem[nameField]);\n const currVal = parseInt(currItem[valueField] || 0);\n if (!prev) return { wow: { text: 'New', arrow: '★', pill: '#FBF5EB', pillText: '#C9A96E' }, prevValue: null };\n const prevVal = parseInt(prev[valueField] || 0);\n if (!prevVal || prevVal === 0) return { wow: { text: 'New', arrow: '★', pill: '#FBF5EB', pillText: '#C9A96E' }, prevValue: null };\n const pct = ((currVal - prevVal) / prevVal) * 100;\n const up = pct >= 0;\n return {\n wow: { text: (up ? '+' : '') + pct.toFixed(1) + '%', arrow: up ? '↑' : '↓', pill: up ? '#ECFDF5' : '#FEF2F2', pillText: up ? '#2D6A4F' : '#B91C1C' },\n prevValue: prevVal,\n };\n}\n\nfunction formatDuration(s) {\n const sec = parseFloat(s);\n if (isNaN(sec)) return '0s';\n const m = Math.floor(sec / 60), r = Math.round(sec % 60);\n return m > 0 ? `${m}m ${r}s` : `${r}s`;\n}\nfunction fmt(n) { const num = parseInt(n); return isNaN(num) ? '0' : num.toLocaleString('en-IN'); }\nfunction fmtPct(n) { return (parseFloat(n) * 100).toFixed(1) + '%'; }\n\n// ── 3. WoW ───────────────────────────────────────────────────\n\nconst wow = {\n totalUsers: wowChange(currentWeek.totalUsers, previousWeek.totalUsers),\n sessions: wowChange(currentWeek.sessions, previousWeek.sessions),\n newUsers: wowChange(currentWeek.new_users, previousWeek.new_users),\n bounceRate: wowChangeBounce(currentWeek.bounce_rate, previousWeek.bounce_rate),\n avgDuration: wowChange(currentWeek.average_session_dur, previousWeek.average_session_dur),\n};\n\nconst newUsersRow = newVsRet.find(r => r.newVsReturning === 'new') || { totalUsers: 0 };\nconst returningRow = newVsRet.find(r => r.newVsReturning === 'returning') || { totalUsers: 0 };\nconst newUsersPrevRow = newVsRetPrev.find(r => r.newVsReturning === 'new') || { totalUsers: 0 };\nconst retUsersPrevRow = newVsRetPrev.find(r => r.newVsReturning === 'returning') || { totalUsers: 0 };\nconst totalNvR = (parseInt(newUsersRow.totalUsers) + parseInt(returningRow.totalUsers)) || 1;\nconst wowNewUsers = wowChange(newUsersRow.totalUsers, newUsersPrevRow.totalUsers);\nconst wowRetUsers = wowChange(returningRow.totalUsers, retUsersPrevRow.totalUsers);\nconst totalDeviceSessions = devices.reduce((s, d) => s + parseInt(d.sessions || 0), 0) || 1;\n\nconst startLabel = $now.minus({days: 7}).toFormat('MMM dd, yyyy');\nconst endLabel = $now.minus({days: 1}).toFormat('MMM dd, yyyy');\nconst prevStartLabel = $now.minus({days: 14}).toFormat('MMM dd');\nconst prevEndLabel = $now.minus({days: 8}).toFormat('MMM dd');\n\n// ── 4. DESIGN TOKENS ─────────────────────────────────────────\n\nconst C = {\n bg: '#F7F5F2', card: '#FFFFFF', headerBg: '#1C1C1C', headerSub: '#A89880',\n gold: '#C9A96E', sectionText: '#1C1C1C', label: '#6B6560', value: '#1C1C1C',\n prevValue: '#B0A89E', rowAlt: '#FDFCFB', rowBase: '#FFFFFF', divider: '#EDE8E2',\n footerBg: '#1C1C1C', footerText: '#6B6560',\n};\n\n// ── 5. HTML COMPONENTS ───────────────────────────────────────\n\nconst sectionGap = ` `;\n\nconst sectionHeader = (title, subtitle) => `\n\n \n \n
    \n ${title}\n ${subtitle ? `${subtitle}` : ''}\n
    \n`;\n\n// colHeaders accepts array of {label, align} — aligns header text to match data cells\nconst colHeaders = (cols) => `\n\n ${cols.map((c, i) => {\n const align = c.align || 'left';\n const padLeft = (align === 'left' && i === 0) ? '24px' : '12px';\n const padRight = align === 'right' ? '20px' : '12px';\n return `${c.label}`;\n }).join('')}\n`;\n\n// Change cell: \"was X\" + pill both right-aligned, stacked, in a right-aligned block\n// Both elements are display:block and text-align:right so they stack flush right\nfunction changeCell(wowObj, prevValue) {\n const wasLine = prevValue !== null\n ? `was ${fmt(prevValue)}`\n : '';\n const pillLine = `\n \n ${wowObj.arrow} ${wowObj.text}\n \n `;\n return `${wasLine}${pillLine}`;\n}\n\n// Inline pill (no \"was\" — used in overview where Last Week column already exists)\nfunction pillOnly(wowObj) {\n return `\n ${wowObj.arrow} ${wowObj.text}\n `;\n}\n\n// Overview row: label(left) / this week(right) / last week(right) / pill(right)\nconst overviewRow = (label, curr, prev, wowObj, isAlt) => `\n\n ${label}\n ${curr}\n ${prev}\n ${pillOnly(wowObj)}\n`;\n\n// List row: rank(center) / name(left) / value(right) / change cell(right-stacked)\nconst listRowWoW = (rank, name, value, wowObj, prevValue, isAlt) => `\n\n ${rank}\n ${name}\n ${value}\n ${changeCell(wowObj, prevValue)}\n`;\n\n// Bar row: label(left) / bar / sessions n+%(right) / change cell(right-stacked)\nconst barRowWoW = (label, value, total, barColor, wowObj, prevValue, isAlt) => {\n const pct = ((parseInt(value || 0) / total) * 100).toFixed(1);\n const barWidth = Math.min(99, Math.max(1, Math.round(parseFloat(pct))));\n return `\n\n ${label}\n \n \n \n \n
    \n \n \n ${fmt(value)} ${pct}%\n \n ${changeCell(wowObj, prevValue)}\n`;\n};\n\n// ── 6. BUILD ROWS ─────────────────────────────────────────────\n\nconst deviceColors = { mobile: '#C9A96E', desktop: '#5C6B73', tablet: '#8C7B6E' };\nconst deviceLabels = { mobile: 'Mobile', desktop: 'Desktop', tablet: 'Tablet' };\n\nconst deviceRows = devices.map((d, i) => {\n const cat = (d.deviceCategory || 'other').toLowerCase();\n const { wow: wowDev, prevValue } = listWoWWithPrev(d, devicesPrev, 'deviceCategory', 'sessions');\n return barRowWoW(deviceLabels[cat] || cat, d.sessions, totalDeviceSessions, deviceColors[cat] || C.gold, wowDev, prevValue, i % 2 !== 0);\n}).join('');\n\nconst nvrRows = [\n barRowWoW('New Users', newUsersRow.totalUsers, totalNvR, '#2D6A4F', wowNewUsers, parseInt(newUsersPrevRow.totalUsers) || null, false),\n barRowWoW('Returning Users', returningRow.totalUsers, totalNvR, C.gold, wowRetUsers, parseInt(retUsersPrevRow.totalUsers) || null, true),\n].join('');\n\nconst pageRows = pages.map((p, i) => {\n const { wow: w, prevValue } = listWoWWithPrev(p, pagesPrev, 'unifiedScreenName', 'screen_page_view');\n return listRowWoW(i + 1, p.unifiedScreenName, fmt(p.screen_page_view), w, prevValue, i % 2 !== 0);\n}).join('');\n\nconst referralRows = referrals.map((r, i) => {\n const { wow: w, prevValue } = listWoWWithPrev(r, referralsPrev, 'sessionSourceMedium', 'sessions');\n return listRowWoW(i + 1, r.sessionSourceMedium, fmt(r.sessions), w, prevValue, i % 2 !== 0);\n}).join('');\n\nconst eventRows = events.map((e, i) => {\n const { wow: w, prevValue } = listWoWWithPrev(e, eventsPrev, 'eventName', 'event_count');\n return listRowWoW(i + 1, e.eventName, fmt(e.event_count), w, prevValue, i % 2 !== 0);\n}).join('');\n\nconst countryRows = countries.map((c, i) => {\n const { wow: w, prevValue } = listWoWWithPrev(c, countriesPrev, 'country', 'sessions');\n return listRowWoW(i + 1, c.country, fmt(c.sessions), w, prevValue, i % 2 !== 0);\n}).join('');\n\n// ── 7. COLUMN HEADER DEFINITIONS ─────────────────────────────\n// align MUST match the text-align of the data cell below it\n\nconst hdrsOverview = [{label:'Metric',align:'left'},{label:'This Week',align:'right'},{label:'Last Week',align:'right'},{label:'Change',align:'right'}];\nconst hdrsAudience = [{label:'Segment',align:'left'},{label:'Share',align:'left'},{label:'Users',align:'right'},{label:'Change',align:'right'}];\nconst hdrsScreens = [{label:'#',align:'center'},{label:'Screen / Page',align:'left'},{label:'Views',align:'right'},{label:'Change',align:'right'}];\nconst hdrsTraffic = [{label:'#',align:'center'},{label:'Source / Medium',align:'left'},{label:'Sessions',align:'right'},{label:'Change',align:'right'}];\nconst hdrsEvents = [{label:'#',align:'center'},{label:'Event Name',align:'left'},{label:'Count',align:'right'},{label:'Change',align:'right'}];\nconst hdrsGeo = [{label:'#',align:'center'},{label:'Country',align:'left'},{label:'Sessions',align:'right'},{label:'Change',align:'right'}];\nconst hdrsDevices = [{label:'Device',align:'left'},{label:'Share',align:'left'},{label:'Sessions',align:'right'},{label:'Change',align:'right'}];\n\n// ── 8. BUILD EMAIL ────────────────────────────────────────────\n\nconst html = `\n\n\n \n \n Weekly Performance Report\n\n\n\n\n
    \n\n \n\n \n \n \n \n\n \n \n\n \n \n \n \n\n
    \n
    \n
    Analytics Report
    \n
    Weekly Performance
    Summary
    \n
    ${startLabel}  —  ${endLabel}
    \n
    \n \n \n \n \n \n
    \n
    ${fmt(currentWeek.totalUsers)}
    \n
    Users
    \n
    ${wow.totalUsers.arrow} ${wow.totalUsers.text}
    \n
    \n
    ${fmt(currentWeek.sessions)}
    \n
    Sessions
    \n
    ${wow.sessions.arrow} ${wow.sessions.text}
    \n
    \n
    ${fmtPct(currentWeek.bounce_rate)}
    \n
    Bounce
    \n
    ${wow.bounceRate.arrow} ${wow.bounceRate.text}
    \n
    \n
    ${formatDuration(currentWeek.average_session_dur)}
    \n
    Duration
    \n
    ${wow.avgDuration.arrow} ${wow.avgDuration.text}
    \n
    \n
    \n
    \n \n\n ${sectionGap}\n\n \n ${formattedAiSummary ? `\n \n ${sectionGap}` : ''}\n\n \n \n\n ${sectionGap}\n\n \n \n\n ${sectionGap}\n\n \n \n\n ${sectionGap}\n\n \n \n\n ${sectionGap}\n\n \n ${events.length > 0 ? `\n \n ${sectionGap}` : ''}\n\n \n \n\n ${sectionGap}\n\n \n \n\n ${sectionGap}\n\n
    \n \n \n \n
    \n AI Executive Summary\n Generated by Gemini\n
    ${formattedAiSummary}
    \n
    \n \n ${sectionHeader('Overview', `vs ${prevStartLabel} – ${prevEndLabel}`)}\n ${colHeaders(hdrsOverview)}\n ${overviewRow('Total Users', fmt(currentWeek.totalUsers), fmt(previousWeek.totalUsers), wow.totalUsers, false)}\n ${overviewRow('Sessions', fmt(currentWeek.sessions), fmt(previousWeek.sessions), wow.sessions, true)}\n ${overviewRow('New Users', fmt(currentWeek.new_users), fmt(previousWeek.new_users), wow.newUsers, false)}\n ${overviewRow('Bounce Rate', fmtPct(currentWeek.bounce_rate), fmtPct(previousWeek.bounce_rate), wow.bounceRate, true)}\n ${overviewRow('Avg. Session Duration', formatDuration(currentWeek.average_session_dur), formatDuration(previousWeek.average_session_dur), wow.avgDuration, false)}\n
    \n
    \n \n ${sectionHeader('Audience', 'New vs Returning · WoW')}\n ${colHeaders(hdrsAudience)}\n ${nvrRows}\n
    \n
    \n \n ${sectionHeader('Top Screens', 'By views · WoW')}\n ${colHeaders(hdrsScreens)}\n ${pageRows}\n
    \n
    \n \n ${sectionHeader('Traffic Sources', 'Top channels · WoW')}\n ${colHeaders(hdrsTraffic)}\n ${referralRows}\n
    \n
    \n \n ${sectionHeader('Top Events', 'User interactions · WoW')}\n ${colHeaders(hdrsEvents)}\n ${eventRows}\n
    \n
    \n \n ${sectionHeader('Geography', 'Top 5 countries · WoW')}\n ${colHeaders(hdrsGeo)}\n ${countryRows}\n
    \n
    \n \n ${sectionHeader('Devices', 'By device category · WoW')}\n ${colHeaders(hdrsDevices)}\n ${deviceRows}\n
    \n
    \n
    \n \n \n \n
    \n
    \n \n \n \n
    \n
    AppStoneLab Technologies
    \n appstonelab.com\n
    \n
    \n ${startLabel} — ${endLabel}
    Google Analytics 4
    \n Unsubscribe\n
    \n
    \n
    \n
    \n This report is automatically generated every Monday via n8n workflow automation. Data reflects the 7-day period ending ${endLabel}.\n
    \n
    \n
    \n
    \n\n`;\n\n// ── 9. OUTPUT ─────────────────────────────────────────────────\n\nreturn [{\n json: {\n subject: `Weekly Performance Report — ${startLabel} to ${endLabel}`,\n html,\n recipients: 'user@example.com',\n _debug: {\n currentWeekUsers: currentWeek.totalUsers,\n previousWeekUsers: previousWeek.totalUsers,\n pagesReturned: pages.length,\n eventsReturned: events.length,\n devicesReturned: devices.length,\n aiSummaryLength: aiSummary.length,\n }\n }\n}];"},"typeVersion":2},{"id":"a50596dc-72d1-4efa-8d73-d9f9ed96d5c2","name":"Send Weekly Report","type":"n8n-nodes-base.emailSend","position":[1248,112],"webhookId":"f77564d3-ff52-473e-b420-140288505d49","parameters":{"html":"={{ $json.html }}","options":{},"subject":"={{ $json.subject }}","toEmail":"={{ $json.recipients }}, jignesh.sanghani@dev.appstonelab.com","fromEmail":"={{ $json.recipients }}"},"credentials":{"smtp":{"id":"credential-id","name":"SMTP account"}},"typeVersion":2.1},{"id":"402d7cd9-f814-49f5-9ae9-985d2ae86492","name":"GA4 - Top 5 Pages Previous Week","type":"n8n-nodes-base.googleAnalytics","position":[-320,-272],"parameters":{"limit":5,"endDate":"={{$now.minus({days: 8}).startOf('day').toFormat('yyyy-MM-dd')}}","dateRange":"custom","startDate":"={{$now.minus({days: 14}).startOf('day').toFormat('yyyy-MM-dd')}}","metricsGA4":{"metricValues":[{"name":"screen_page_view","listName":"custom","expression":"screenPageViews"},{"name":"average_session_duration","listName":"custom","expression":"averageSessionDuration"}]},"propertyId":{"__rl":true,"mode":"id","value":"481410811"},"dimensionsGA4":{"dimensionValues":[{"name":"unifiedScreenName","listName":"other"}]},"additionalFields":{}},"credentials":{"googleAnalyticsOAuth2":{"id":"credential-id","name":"Google Analytics account"}},"executeOnce":true,"typeVersion":2},{"id":"98a79977-3d6d-4200-ba26-4b2f59868ada","name":"GA4 - Top 5 Referrals Previous Week","type":"n8n-nodes-base.googleAnalytics","position":[-320,-80],"parameters":{"limit":5,"endDate":"={{$now.minus({days: 8}).startOf('day').toFormat('yyyy-MM-dd')}}","dateRange":"custom","startDate":"={{$now.minus({days: 14}).startOf('day').toFormat('yyyy-MM-dd')}}","metricsGA4":{"metricValues":[{"listName":"sessions"}]},"propertyId":{"__rl":true,"mode":"id","value":"481410811"},"dimensionsGA4":{"dimensionValues":[{"name":"sessionSourceMedium","listName":"other"}]},"additionalFields":{"orderByUI":{"metricOrderBy":[{"desc":true,"metricName":"sessions"}]}}},"credentials":{"googleAnalyticsOAuth2":{"id":"credential-id","name":"Google Analytics account"}},"executeOnce":true,"typeVersion":2},{"id":"cec80406-03d0-4592-9646-e489216e32a7","name":"GA4 - Top 5 Events Previous Week","type":"n8n-nodes-base.googleAnalytics","position":[-320,112],"parameters":{"limit":5,"endDate":"={{$now.minus({days: 8}).startOf('day').toFormat('yyyy-MM-dd')}}","dateRange":"custom","startDate":"={{$now.minus({days: 14}).startOf('day').toFormat('yyyy-MM-dd')}}","metricsGA4":{"metricValues":[{"name":"event_count","listName":"custom","expression":"eventCount"}]},"propertyId":{"__rl":true,"mode":"id","value":"481410811"},"dimensionsGA4":{"dimensionValues":[{"name":"eventName","listName":"other"}]},"additionalFields":{"orderByUI":{"metricOrderBy":[{"desc":true,"metricName":"=event_count"}]}}},"credentials":{"googleAnalyticsOAuth2":{"id":"credential-id","name":"Google Analytics account"}},"executeOnce":true,"typeVersion":2},{"id":"31a96f65-3ff9-47da-b6ea-62e059d636f3","name":"GA4 - Top 5 Countries Previous Week","type":"n8n-nodes-base.googleAnalytics","position":[-320,304],"parameters":{"limit":5,"endDate":"={{$now.minus({days: 8}).startOf('day').toFormat('yyyy-MM-dd')}}","dateRange":"custom","startDate":"={{$now.minus({days: 14}).startOf('day').toFormat('yyyy-MM-dd')}}","metricsGA4":{"metricValues":[{"listName":"sessions"}]},"propertyId":{"__rl":true,"mode":"id","value":"481410811"},"dimensionsGA4":{"dimensionValues":[{"listName":"country"}]},"additionalFields":{"orderByUI":{"metricOrderBy":[{"desc":true,"metricName":"sessions"}]}}},"credentials":{"googleAnalyticsOAuth2":{"id":"credential-id","name":"Google Analytics account"}},"executeOnce":true,"typeVersion":2},{"id":"7ac51236-fc7e-4e90-9ebc-44c4a897697e","name":"GA4 - Device Breakdown Previous Week","type":"n8n-nodes-base.googleAnalytics","position":[-320,496],"parameters":{"endDate":"={{$now.minus({days: 8}).startOf('day').toFormat('yyyy-MM-dd')}}","dateRange":"custom","startDate":"={{$now.minus({days: 14}).startOf('day').toFormat('yyyy-MM-dd')}}","metricsGA4":{"metricValues":[{"listName":"sessions"}]},"propertyId":{"__rl":true,"mode":"id","value":"481410811"},"dimensionsGA4":{"dimensionValues":[{"listName":"deviceCategory"}]},"additionalFields":{"orderByUI":{"metricOrderBy":[{"desc":true,"metricName":"sessions"}]}}},"credentials":{"googleAnalyticsOAuth2":{"id":"credential-id","name":"Google Analytics account"}},"executeOnce":true,"typeVersion":2},{"id":"814f5d15-8fef-4db4-a676-deece444adb0","name":"GA4 - New vs Returning Previous Week","type":"n8n-nodes-base.googleAnalytics","position":[-320,688],"parameters":{"endDate":"={{$now.minus({days: 8}).startOf('day').toFormat('yyyy-MM-dd')}}","dateRange":"custom","startDate":"={{$now.minus({days: 14}).startOf('day').toFormat('yyyy-MM-dd')}}","metricsGA4":{"metricValues":[{"listName":"sessions"},{}]},"propertyId":{"__rl":true,"mode":"id","value":"481410811"},"dimensionsGA4":{"dimensionValues":[{"name":"newVsReturning","listName":"other"}]},"additionalFields":{}},"credentials":{"googleAnalyticsOAuth2":{"id":"credential-id","name":"Google Analytics account"}},"executeOnce":true,"typeVersion":2},{"id":"7b51be87-b503-40e8-8516-3965f3ae013d","name":"Sticky Note","type":"n8n-nodes-base.stickyNote","position":[-1888,-48],"parameters":{"color":6,"width":544,"height":480,"content":"## 📊 Weekly GA4 Business Intelligence Report\n\nAutomatically runs **every Monday at 8:00 AM** and delivers a fully formatted HTML performance report to stakeholders via email.\n\n**What this workflow does:**\n- Fetches 14 separate reports from Google Analytics 4 (Current & Previous week data for 7 key metrics)\n- Generates an AI executive summary analyzing Week-over-Week (WoW) trends using Gemini\n- Calculates WoW % changes for ALL metrics (Overview, Pages, Referrals, Events, Countries, Devices, New vs Returning)\n- Builds a premium HTML email and delivers it via Gmail/SMTP\n\n**Before using this template:**\n1. Set your GA4 Property ID in all 14 GA4 nodes\n2. Connect your Google Analytics OAuth2 credential\n3. Connect your Gemini API credential\n4. Connect your Gmail / SMTP credential\n5. Set your recipient email(s) in the Email node or Code node\n6. Set your timezone in workflow settings"},"typeVersion":1},{"id":"3b842436-77b7-4605-8969-a9b187cbcff2","name":"Sticky Note2","type":"n8n-nodes-base.stickyNote","position":[-1248,-464],"parameters":{"color":3,"width":688,"height":448,"content":"## 📡 Google Analytics 4 — Data Nodes\n\nFetches data for 7 key categories, running both **Current Week** and **Previous Week** nodes to generate deep Week-over-Week (WoW) comparisons.\n\n**⚠️ Required: Set your Property ID**\nOpen each of the 14 nodes → Property ID field → replace {YOUR_PROPERTY_ID} with your GA4 numeric property ID.\nFound at: GA4 Admin → Property Settings → Property ID\n\n**Node breakdown (Each has a Current & Previous week version):**\n- **Overview** — totals for sessions, users, bounce rate, etc.\n- **Top 5 Pages** — screens/pages by views\n- **Top 5 Referrals** — traffic sources by sessions\n- **Top 5 Events** — user interactions by count\n- **Top 5 Countries** — geographic breakdown by sessions\n- **Device Breakdown** — mobile / desktop / tablet split\n- **New vs Returning** — audience loyalty breakdown\n\n**All nodes have Execute Once = ON** to prevent duplicate rows."},"typeVersion":1},{"id":"7e2f82c1-abb2-45ff-bfdb-d876182d3495","name":"Sticky Note1","type":"n8n-nodes-base.stickyNote","position":[-1328,48],"parameters":{"color":7,"width":512,"height":288,"content":"## ⏰ Schedule Trigger\n\nFires every **Monday at 8:00 AM IST**.\n\n\n\n\n\n\n\n\n**To change timezone or time:**\nOpen workflow settings → change the Timezone to your preferred one.\n\nDefault timezone: Your Local Timezone"},"typeVersion":1},{"id":"d05075f0-2f6d-49ac-978f-fa03e5c90715","name":"Sticky Note3","type":"n8n-nodes-base.stickyNote","position":[-48,-272],"parameters":{"color":4,"width":304,"height":624,"content":"## 🔀 Merge\n\nWaits for **all GA4 data chains (14 nodes total)** to complete before passing data forward.\n\nWithout this node, Gemini and Code nodes could trigger before all GA4 data is ready - causing empty or undefined values in the report.\n\nMode: Combine by Position"},"typeVersion":1},{"id":"b4fa1e17-e992-49f6-9264-65a8e2569f47","name":"Sticky Note4","type":"n8n-nodes-base.stickyNote","position":[272,-272],"parameters":{"color":5,"width":416,"height":624,"content":"## 🤖 Gemini — AI Executive Summary\n\nGenerates a **3-paragraph executive summary** covering overall performance, audience behaviour, and actionable recommendations. It analyzes the full Week-over-Week dataset from all 14 GA4 nodes to spot trends.\n\n**Credential required:** Google Gemini API\nGet your key at: aistudio.google.com\n\n**Model:** gemini-3.1-flash-lite-preview\n\nIf Gemini fails or returns empty, the report still sends normally — the AI section is hidden from the email automatically."},"typeVersion":1},{"id":"130b47cc-f010-46a4-b926-340d8237ec7f","name":"Sticky Note5","type":"n8n-nodes-base.stickyNote","position":[704,-272],"parameters":{"color":4,"width":432,"height":624,"content":"## 🧮 Code Node — Data Processing + Email Builder\n\nDoes 4 things:\n1. Pulls data from all 14 GA4 nodes and Gemini by node name.\n2. Calculates week-over-week % changes for **every single section** (Overview, Pages, Sources, Events, Countries, Devices, New vs Returning).\n3. Builds the full HTML email with inline CSS and alignment fixes.\n4. Outputs subject, html, and recipients for the Email node.\n\n**To change recipients:**\nFind `recipients:` near the bottom of the code → update the email address.\nMultiple: `'email1@gmail.com,email2@gmail.com'`\n\n\n\n\n\n\n\n\n\n\n\n\n**To change client name in footer:**\nFind `AppStoneLab Technologies` in the HTML and replace with your client's brand name."},"typeVersion":1},{"id":"8c2b8818-4b6f-460b-9896-5d13240bd3fb","name":"Sticky Note6","type":"n8n-nodes-base.stickyNote","position":[1152,-272],"parameters":{"color":2,"width":288,"height":624,"content":"## 📧 Send Email\n\nSends the HTML report to all recipients defined in the Code node.\n\n**Fields mapped from Code node output:**\n- To → {{ $json.recipients }}\n- Subject → {{ $json.subject }}\n- HTML Body → {{ $json.html }}\n\n**Credential required:** Gmail OAuth2 or SMTP\nFor SMTP: update host, port, and login credentials in the credential settings."},"typeVersion":1}],"active":false,"pinData":{},"settings":{"binaryMode":"separate","availableInMCP":false,"executionOrder":"v1"},"versionId":"6ee0e082-565e-46a8-b079-623ec18623df","connections":{"GA4 - Top 5 Pages":{"main":[[{"node":"GA4 - Top 5 Pages Previous Week","type":"main","index":0}]]},"GA4 - Top 5 Events":{"main":[[{"node":"GA4 - Top 5 Events Previous Week","type":"main","index":0}]]},"Generate AI Summary":{"main":[[{"node":"Build Report & Email HTML","type":"main","index":0}]]},"GA4 - Top 5 Countries":{"main":[[{"node":"GA4 - Top 5 Countries Previous Week","type":"main","index":0}]]},"GA4 - Top 5 Referrals":{"main":[[{"node":"GA4 - Top 5 Referrals Previous Week","type":"main","index":0}]]},"Wait for All GA4 Data":{"main":[[{"node":"Generate AI Summary","type":"main","index":0}]]},"Weekly Monday Trigger":{"main":[[{"node":"GA4 - Overview Current Week","type":"main","index":0},{"node":"GA4 - Top 5 Pages","type":"main","index":0},{"node":"GA4 - Top 5 Referrals","type":"main","index":0},{"node":"GA4 - Top 5 Events","type":"main","index":0},{"node":"GA4 - Top 5 Countries","type":"main","index":0},{"node":"GA4 - Device Breakdown","type":"main","index":0},{"node":"New vs Returning Users Breakdown","type":"main","index":0}]]},"GA4 - Device Breakdown":{"main":[[{"node":"GA4 - Device Breakdown Previous Week","type":"main","index":0}]]},"Build Report & Email HTML":{"main":[[{"node":"Send Weekly Report","type":"main","index":0}]]},"GA4 - Overview Current Week":{"main":[[{"node":"GA4 - Overview Previous Week","type":"main","index":0}]]},"GA4 - Overview Previous Week":{"main":[[{"node":"Wait for All GA4 Data","type":"main","index":0}]]},"GA4 - Top 5 Pages Previous Week":{"main":[[{"node":"Wait for All GA4 Data","type":"main","index":1}]]},"GA4 - Top 5 Events Previous Week":{"main":[[{"node":"Wait for All GA4 Data","type":"main","index":3}]]},"New vs Returning Users Breakdown":{"main":[[{"node":"GA4 - New vs Returning Previous Week","type":"main","index":0}]]},"GA4 - Top 5 Countries Previous Week":{"main":[[{"node":"Wait for All GA4 Data","type":"main","index":4}]]},"GA4 - Top 5 Referrals Previous Week":{"main":[[{"node":"Wait for All GA4 Data","type":"main","index":2}]]},"GA4 - Device Breakdown Previous Week":{"main":[[{"node":"Wait for All GA4 Data","type":"main","index":5}]]},"GA4 - New vs Returning Previous Week":{"main":[[{"node":"Wait for All GA4 Data","type":"main","index":6}]]}}} \ No newline at end of file