<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>模組四：觸發器與排程 on Tarragon</title><link>https://tarrragon.github.io/blog/automation/04-triggers-automation/</link><description>Recent content in 模組四：觸發器與排程 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 06 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/automation/04-triggers-automation/index.xml" rel="self" type="application/rss+xml"/><item><title>時間觸發器：把 raw log 彙總成日報</title><link>https://tarrragon.github.io/blog/automation/04-triggers-automation/time-driven-aggregation/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/automation/04-triggers-automation/time-driven-aggregation/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/automation/knowledge-cards/time-driven-trigger/" data-link-title="Time-Driven Trigger（時間觸發器）" data-link-desc="讓 Apps Script 在固定時間自動執行的排程機制，把被動等呼叫的膠水層變成主動定時跑的任務">時間觸發器&lt;/a>（time-driven trigger）讓 Apps Script 從「被動等 beacon 打進來」變成「主動定時執行」。它的用途是把 raw log 這種逐筆、直接看沒意義的原始資料，每天固定時間彙總成「昨天每篇看幾次」的日報，讓人打開試算表看到的是整理過的數字。先講怎麼設定定時、再講彙總邏輯、最後講怎麼在配額內寫得有效率。&lt;/p>
&lt;h2 id="設定每日定時執行">設定每日定時執行&lt;/h2>
&lt;p>觸發器可以用程式建立，也可以在編輯器左側的「觸發條件」介面點選建立。用程式建立的好處是設定跟著專案走、可版本控制：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">createDailyTrigger&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">ScriptApp&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">newTrigger&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;aggregateYesterday&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">timeBased&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">atHour&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 每天凌晨 1 點
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nx">everyDays&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">create&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段執行一次（手動按執行），就註冊了一個「每天凌晨 1 點呼叫 &lt;code>aggregateYesterday&lt;/code>」的觸發器。凌晨執行是刻意的：那時流量低、raw log 當天的資料已經齊了，彙總前一天不會漏。要注意 &lt;code>createDailyTrigger&lt;/code> 只該跑一次——每跑一次就多註冊一個觸發器，重複跑會變成一天彙總很多次。管理現有觸發器用 &lt;code>ScriptApp.getProjectTriggers()&lt;/code> 查、&lt;code>deleteTrigger&lt;/code> 刪。&lt;/p>
&lt;h2 id="彙總邏輯">彙總邏輯&lt;/h2>
&lt;p>彙總做的事是：讀 raw log、把同一天同一路徑的瀏覽數出來、寫進另一張「日報」工作表。核心是一個 group by：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">aggregateYesterday&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">ss&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">SpreadsheetApp&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getActiveSpreadsheet&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">raw&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">ss&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getSheetByName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;工作表1&amp;#34;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">getDataRange&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">getValues&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">report&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">ss&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getSheetByName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;日報&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nx">ss&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">insertSheet&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;日報&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">counts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{};&lt;/span> &lt;span class="c1">// key = &amp;#34;日期|路徑&amp;#34; → 次數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">raw&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 從第 2 列起，跳過標題
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="nx">d&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">raw&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">];&lt;/span> &lt;span class="c1">// 時間欄
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="nx">day&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">Utilities&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">formatDate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">d&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Asia/Taipei&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;yyyy-MM-dd&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">path&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">raw&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">day&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s2">&amp;#34;|&amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">path&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nx">counts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">counts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">rows&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">Object&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">keys&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">counts&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">map&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">k&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">parts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">k&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;|&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nx">parts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nx">parts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nx">counts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">k&lt;/span>&lt;span class="p">]];&lt;/span> &lt;span class="c1">// 日期、路徑、次數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">rows&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="nx">report&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getRange&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">report&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getLastRow&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">rows&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">setValues&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">rows&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段 group by 有一個前提要明寫出來：&lt;strong>它假設 raw log 裡一列等於一次瀏覽&lt;/strong>。這在模組二的資料模型下成立，因為 beacon 只在頁面載入時送一次。日後若在 payload 加入其他事件型別（例如離開事件，見&lt;a href="https://tarrragon.github.io/blog/automation/06-reading-the-data/event-model/" data-link-title="事件模型與停留時間" data-link-desc="每次瀏覽只記一列時看不出讀者停留多久、有沒有真的在讀；補上離開事件之後，那個秒數的語意與既有報表公式的連帶影響">模組六&lt;/a>），每次瀏覽會產生多列，而這個迴圈仍然合法執行、只是把多數瀏覽數了不只一次——數字膨脹而沒有任何錯誤訊息。膨脹的倍率不固定（離開事件會丟失一部分），所以它比整數倍的錯誤更難從數字本身看出來。屆時的修法是在迴圈裡先篩事件型別：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">raw&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">!==&lt;/span> &lt;span class="s2">&amp;#34;view&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">continue&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 只數進入事件（第 6 欄）
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把這個前提寫在程式碼旁邊，是為了讓資料模型變更的人搜尋得到所有依賴它的地方。這類「條件式合法執行但已換語意」的失效方式，見&lt;a href="https://tarrragon.github.io/blog/automation/06-reading-the-data/diagnosing-silent-failures/" data-link-title="假故障與靜默失效的診斷" data-link-desc="自建的 Apps Script 流量統計看起來壞了、或看起來正常但數字不對時，分辨症狀出現的位置與問題所在的位置">假故障與靜默失效的診斷&lt;/a>。&lt;/p>
&lt;p>兩個實作決定值得說明。&lt;strong>日期用 &lt;code>Utilities.formatDate&lt;/code> 明確指定時區&lt;/strong>（這裡 &lt;code>Asia/Taipei&lt;/code>），否則跨午夜的資料可能因為時區偏移被算到錯的日子。&lt;strong>寫日報用 &lt;code>setValues&lt;/code> 一次寫一整塊、不用 &lt;code>appendRow&lt;/code> 逐列寫&lt;/strong>——彙總結果可能有幾十上百列，逐列 append 會慢且容易逼近執行時間，一次 &lt;code>setValues&lt;/code> 快得多（呼應&lt;a href="https://tarrragon.github.io/blog/automation/03-sheet-as-database/append-and-concurrency/" data-link-title="寫入與並發：appendRow 與 LockService" data-link-desc="多個 beacon 同時寫進同一張 Sheet 時，appendRow 的競態風險與 LockService 序列化寫入的時機判斷">寫入與並發&lt;/a>講的批次寫入）。&lt;/p>
&lt;h2 id="在-90-分鐘配額內寫得有效率">在 90 分鐘配額內寫得有效率&lt;/h2>
&lt;p>觸發器受一條配額約束：個人帳號所有觸發器每天總執行時間上限 90 分鐘，且單次一樣不能超過 6 分鐘（見&lt;a href="https://tarrragon.github.io/blog/automation/knowledge-cards/execution-quota/" data-link-title="Execution Quota（執行配額）" data-link-desc="Apps Script 個人帳號的執行時間、同時併發與觸發器每日總時間上限，決定免費膠水層能承受多大的量">執行配額&lt;/a>）。上面那段每次 &lt;code>getDataRange().getValues()&lt;/code> 把&lt;strong>整張 raw log&lt;/strong> 讀進來——log 還小時沒問題，但累積到數十萬列後，光讀取就可能逼近 6 分鐘。&lt;/p>
&lt;p>有效率的寫法是&lt;strong>只讀增量&lt;/strong>：記住「上次彙總處理到第幾列」，這次只讀新增的部分。用 &lt;code>PropertiesService&lt;/code> 存這個游標：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">props&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">PropertiesService&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getScriptProperties&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">lastRow&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">Number&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">props&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;lastAggregatedRow&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">sheet&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">ss&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getSheetByName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;工作表1&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">newCount&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">sheet&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getLastRow&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">lastRow&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">newCount&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">fresh&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">sheet&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getRange&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">lastRow&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">newCount&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">getValues&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 5 = 目前的欄數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// ... 只彙總 fresh ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">props&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;lastAggregatedRow&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">String&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">sheet&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getLastRow&lt;/span>&lt;span class="p">()));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>那個 &lt;code>5&lt;/code> 是目前的欄數，它與前面那個「一列等於一次瀏覽」是同一種前提：欄位增加時要跟著改，而讀少了不會報錯、只會讓後面的欄位變成 &lt;code>undefined&lt;/code>。&lt;/p>
&lt;p>&lt;strong>兩個常數必須一起改，否則會互相抵銷。&lt;/strong> 前面那個篩選寫的是 &lt;code>raw[i][5] !== &amp;quot;view&amp;quot;&lt;/code>，查的是第六欄；這裡只讀五欄時 &lt;code>raw[i][5]&lt;/code> 恆為 &lt;code>undefined&lt;/code>，條件恆真、每一列都被跳過、日報變成空白。兩處相隔一節、都不產生錯誤，而症狀（日報是空的）指向的位置與成因所在的位置不同。&lt;/p>
&lt;p>只讀增量讓每次彙總的成本跟「昨天新增多少」成正比、而不是跟「歷史總量」成正比，執行時間就穩定、不隨資料累積膨脹。這跟&lt;a href="https://tarrragon.github.io/blog/automation/03-sheet-as-database/data-model-and-capacity/" data-link-title="資料模型與容量邊界" data-link-desc="raw log 表的欄位怎麼設計才好彙總、以及 Sheets 累積到多少列會開始撐不住、撐不住的訊號長什麼樣">資料模型與容量邊界&lt;/a>講的分表是互補的兩招——分表縮小單張表、只讀增量縮小單次讀取，都是為了讓彙總不被歷史總量拖垮。&lt;/p>
&lt;h2 id="下一步">下一步&lt;/h2>
&lt;p>時間觸發器是「到點就跑」的排程。另一類觸發器是「某個事件發生就跑」，例如表單被提交——見&lt;a href="https://tarrragon.github.io/blog/automation/04-triggers-automation/form-and-event-triggers/" data-link-title="表單與事件觸發器" data-link-desc="由 Google 檔案事件（表單提交、試算表編輯）觸發的 Apps Script，以及 simple 與 installable 觸發器在權限上的差別">表單與事件觸發器&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/automation/knowledge-cards/time-driven-trigger/" data-link-title="Time-Driven Trigger（時間觸發器）" data-link-desc="讓 Apps Script 在固定時間自動執行的排程機制，把被動等呼叫的膠水層變成主動定時跑的任務">時間觸發器</a>（time-driven trigger）讓 Apps Script 從「被動等 beacon 打進來」變成「主動定時執行」。它的用途是把 raw log 這種逐筆、直接看沒意義的原始資料，每天固定時間彙總成「昨天每篇看幾次」的日報，讓人打開試算表看到的是整理過的數字。先講怎麼設定定時、再講彙總邏輯、最後講怎麼在配額內寫得有效率。</p>
<h2 id="設定每日定時執行">設定每日定時執行</h2>
<p>觸發器可以用程式建立，也可以在編輯器左側的「觸發條件」介面點選建立。用程式建立的好處是設定跟著專案走、可版本控制：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">createDailyTrigger</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">ScriptApp</span><span class="p">.</span><span class="nx">newTrigger</span><span class="p">(</span><span class="s2">&#34;aggregateYesterday&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="p">.</span><span class="nx">timeBased</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="p">.</span><span class="nx">atHour</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>          <span class="c1">// 每天凌晨 1 點
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>    <span class="p">.</span><span class="nx">everyDays</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">.</span><span class="nx">create</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段執行一次（手動按執行），就註冊了一個「每天凌晨 1 點呼叫 <code>aggregateYesterday</code>」的觸發器。凌晨執行是刻意的：那時流量低、raw log 當天的資料已經齊了，彙總前一天不會漏。要注意 <code>createDailyTrigger</code> 只該跑一次——每跑一次就多註冊一個觸發器，重複跑會變成一天彙總很多次。管理現有觸發器用 <code>ScriptApp.getProjectTriggers()</code> 查、<code>deleteTrigger</code> 刪。</p>
<h2 id="彙總邏輯">彙總邏輯</h2>
<p>彙總做的事是：讀 raw log、把同一天同一路徑的瀏覽數出來、寫進另一張「日報」工作表。核心是一個 group by：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">aggregateYesterday</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">var</span> <span class="nx">ss</span> <span class="o">=</span> <span class="nx">SpreadsheetApp</span><span class="p">.</span><span class="nx">getActiveSpreadsheet</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">raw</span> <span class="o">=</span> <span class="nx">ss</span><span class="p">.</span><span class="nx">getSheetByName</span><span class="p">(</span><span class="s2">&#34;工作表1&#34;</span><span class="p">).</span><span class="nx">getDataRange</span><span class="p">().</span><span class="nx">getValues</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kd">var</span> <span class="nx">report</span> <span class="o">=</span> <span class="nx">ss</span><span class="p">.</span><span class="nx">getSheetByName</span><span class="p">(</span><span class="s2">&#34;日報&#34;</span><span class="p">)</span> <span class="o">||</span> <span class="nx">ss</span><span class="p">.</span><span class="nx">insertSheet</span><span class="p">(</span><span class="s2">&#34;日報&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="kd">var</span> <span class="nx">counts</span> <span class="o">=</span> <span class="p">{};</span> <span class="c1">// key = &#34;日期|路徑&#34; → 次數
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>  <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">raw</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>      <span class="c1">// 從第 2 列起，跳過標題
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>    <span class="kd">var</span> <span class="nx">d</span> <span class="o">=</span> <span class="nx">raw</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">0</span><span class="p">];</span>                        <span class="c1">// 時間欄
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>    <span class="kd">var</span> <span class="nx">day</span> <span class="o">=</span> <span class="nx">Utilities</span><span class="p">.</span><span class="nx">formatDate</span><span class="p">(</span><span class="nx">d</span><span class="p">,</span> <span class="s2">&#34;Asia/Taipei&#34;</span><span class="p">,</span> <span class="s2">&#34;yyyy-MM-dd&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="kd">var</span> <span class="nx">path</span> <span class="o">=</span> <span class="nx">raw</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">1</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="kd">var</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">day</span> <span class="o">+</span> <span class="s2">&#34;|&#34;</span> <span class="o">+</span> <span class="nx">path</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nx">counts</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="nx">counts</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">||</span> <span class="mi">0</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="kd">var</span> <span class="nx">rows</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">counts</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">k</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="kd">var</span> <span class="nx">parts</span> <span class="o">=</span> <span class="nx">k</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="s2">&#34;|&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">return</span> <span class="p">[</span><span class="nx">parts</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="nx">parts</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="nx">counts</span><span class="p">[</span><span class="nx">k</span><span class="p">]];</span>   <span class="c1">// 日期、路徑、次數
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span>  <span class="p">});</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">rows</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="nx">report</span><span class="p">.</span><span class="nx">getRange</span><span class="p">(</span><span class="nx">report</span><span class="p">.</span><span class="nx">getLastRow</span><span class="p">()</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">rows</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span> <span class="mi">3</span><span class="p">).</span><span class="nx">setValues</span><span class="p">(</span><span class="nx">rows</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段 group by 有一個前提要明寫出來：<strong>它假設 raw log 裡一列等於一次瀏覽</strong>。這在模組二的資料模型下成立，因為 beacon 只在頁面載入時送一次。日後若在 payload 加入其他事件型別（例如離開事件，見<a href="/blog/automation/06-reading-the-data/event-model/" data-link-title="事件模型與停留時間" data-link-desc="每次瀏覽只記一列時看不出讀者停留多久、有沒有真的在讀；補上離開事件之後，那個秒數的語意與既有報表公式的連帶影響">模組六</a>），每次瀏覽會產生多列，而這個迴圈仍然合法執行、只是把多數瀏覽數了不只一次——數字膨脹而沒有任何錯誤訊息。膨脹的倍率不固定（離開事件會丟失一部分），所以它比整數倍的錯誤更難從數字本身看出來。屆時的修法是在迴圈裡先篩事件型別：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">raw</span><span class="p">[</span><span class="nx">i</span><span class="p">][</span><span class="mi">5</span><span class="p">]</span> <span class="o">!==</span> <span class="s2">&#34;view&#34;</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span>   <span class="c1">// 只數進入事件（第 6 欄）
</span></span></span></code></pre></div><p>把這個前提寫在程式碼旁邊，是為了讓資料模型變更的人搜尋得到所有依賴它的地方。這類「條件式合法執行但已換語意」的失效方式，見<a href="/blog/automation/06-reading-the-data/diagnosing-silent-failures/" data-link-title="假故障與靜默失效的診斷" data-link-desc="自建的 Apps Script 流量統計看起來壞了、或看起來正常但數字不對時，分辨症狀出現的位置與問題所在的位置">假故障與靜默失效的診斷</a>。</p>
<p>兩個實作決定值得說明。<strong>日期用 <code>Utilities.formatDate</code> 明確指定時區</strong>（這裡 <code>Asia/Taipei</code>），否則跨午夜的資料可能因為時區偏移被算到錯的日子。<strong>寫日報用 <code>setValues</code> 一次寫一整塊、不用 <code>appendRow</code> 逐列寫</strong>——彙總結果可能有幾十上百列，逐列 append 會慢且容易逼近執行時間，一次 <code>setValues</code> 快得多（呼應<a href="/blog/automation/03-sheet-as-database/append-and-concurrency/" data-link-title="寫入與並發：appendRow 與 LockService" data-link-desc="多個 beacon 同時寫進同一張 Sheet 時，appendRow 的競態風險與 LockService 序列化寫入的時機判斷">寫入與並發</a>講的批次寫入）。</p>
<h2 id="在-90-分鐘配額內寫得有效率">在 90 分鐘配額內寫得有效率</h2>
<p>觸發器受一條配額約束：個人帳號所有觸發器每天總執行時間上限 90 分鐘，且單次一樣不能超過 6 分鐘（見<a href="/blog/automation/knowledge-cards/execution-quota/" data-link-title="Execution Quota（執行配額）" data-link-desc="Apps Script 個人帳號的執行時間、同時併發與觸發器每日總時間上限，決定免費膠水層能承受多大的量">執行配額</a>）。上面那段每次 <code>getDataRange().getValues()</code> 把<strong>整張 raw log</strong> 讀進來——log 還小時沒問題，但累積到數十萬列後，光讀取就可能逼近 6 分鐘。</p>
<p>有效率的寫法是<strong>只讀增量</strong>：記住「上次彙總處理到第幾列」，這次只讀新增的部分。用 <code>PropertiesService</code> 存這個游標：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">props</span> <span class="o">=</span> <span class="nx">PropertiesService</span><span class="p">.</span><span class="nx">getScriptProperties</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">var</span> <span class="nx">lastRow</span> <span class="o">=</span> <span class="nb">Number</span><span class="p">(</span><span class="nx">props</span><span class="p">.</span><span class="nx">getProperty</span><span class="p">(</span><span class="s2">&#34;lastAggregatedRow&#34;</span><span class="p">)</span> <span class="o">||</span> <span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kd">var</span> <span class="nx">sheet</span> <span class="o">=</span> <span class="nx">ss</span><span class="p">.</span><span class="nx">getSheetByName</span><span class="p">(</span><span class="s2">&#34;工作表1&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="kd">var</span> <span class="nx">newCount</span> <span class="o">=</span> <span class="nx">sheet</span><span class="p">.</span><span class="nx">getLastRow</span><span class="p">()</span> <span class="o">-</span> <span class="nx">lastRow</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">newCount</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="kd">var</span> <span class="nx">fresh</span> <span class="o">=</span> <span class="nx">sheet</span><span class="p">.</span><span class="nx">getRange</span><span class="p">(</span><span class="nx">lastRow</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">newCount</span><span class="p">,</span> <span class="mi">5</span><span class="p">).</span><span class="nx">getValues</span><span class="p">();</span>   <span class="c1">// 5 = 目前的欄數
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span>  <span class="c1">// ... 只彙總 fresh ...
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span>  <span class="nx">props</span><span class="p">.</span><span class="nx">setProperty</span><span class="p">(</span><span class="s2">&#34;lastAggregatedRow&#34;</span><span class="p">,</span> <span class="nb">String</span><span class="p">(</span><span class="nx">sheet</span><span class="p">.</span><span class="nx">getLastRow</span><span class="p">()));</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>那個 <code>5</code> 是目前的欄數，它與前面那個「一列等於一次瀏覽」是同一種前提：欄位增加時要跟著改，而讀少了不會報錯、只會讓後面的欄位變成 <code>undefined</code>。</p>
<p><strong>兩個常數必須一起改，否則會互相抵銷。</strong> 前面那個篩選寫的是 <code>raw[i][5] !== &quot;view&quot;</code>，查的是第六欄；這裡只讀五欄時 <code>raw[i][5]</code> 恆為 <code>undefined</code>，條件恆真、每一列都被跳過、日報變成空白。兩處相隔一節、都不產生錯誤，而症狀（日報是空的）指向的位置與成因所在的位置不同。</p>
<p>只讀增量讓每次彙總的成本跟「昨天新增多少」成正比、而不是跟「歷史總量」成正比，執行時間就穩定、不隨資料累積膨脹。這跟<a href="/blog/automation/03-sheet-as-database/data-model-and-capacity/" data-link-title="資料模型與容量邊界" data-link-desc="raw log 表的欄位怎麼設計才好彙總、以及 Sheets 累積到多少列會開始撐不住、撐不住的訊號長什麼樣">資料模型與容量邊界</a>講的分表是互補的兩招——分表縮小單張表、只讀增量縮小單次讀取，都是為了讓彙總不被歷史總量拖垮。</p>
<h2 id="下一步">下一步</h2>
<p>時間觸發器是「到點就跑」的排程。另一類觸發器是「某個事件發生就跑」，例如表單被提交——見<a href="/blog/automation/04-triggers-automation/form-and-event-triggers/" data-link-title="表單與事件觸發器" data-link-desc="由 Google 檔案事件（表單提交、試算表編輯）觸發的 Apps Script，以及 simple 與 installable 觸發器在權限上的差別">表單與事件觸發器</a>。</p>
]]></content:encoded></item><item><title>表單與事件觸發器</title><link>https://tarrragon.github.io/blog/automation/04-triggers-automation/form-and-event-triggers/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/automation/04-triggers-automation/form-and-event-triggers/</guid><description>&lt;p>事件觸發器讓 Apps Script 在「某個 Google 檔案發生某件事」的當下自動執行，跟時間觸發器的「到點就跑」互補。最常用的是 &lt;code>onFormSubmit&lt;/code>（有人提交 Google 表單）跟 &lt;code>onEdit&lt;/code>（有人編輯試算表）。這一篇講事件觸發器能做什麼、以及一個容易踩的權限分界。&lt;/p>
&lt;h2 id="onformsubmit表單提交即時處理">onFormSubmit：表單提交即時處理&lt;/h2>
&lt;p>Google 表單本身會把回應存進一張試算表，但「存進去之後要做什麼」需要 Apps Script。&lt;code>onFormSubmit&lt;/code> 觸發器在每次有人提交表單時執行，拿得到這次提交的內容：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">onFormSubmit&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">answers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">namedValues&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// { &amp;#34;Email&amp;#34;: [&amp;#34;a@b.com&amp;#34;], &amp;#34;問題&amp;#34;: [&amp;#34;內容&amp;#34;] }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// 例如：寄一封通知信給自己
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">MailApp&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">sendEmail&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;you@gmail.com&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;新表單回應&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">JSON&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">stringify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">answers&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這讓表單從「被動收集」變成「即時反應」——提交當下就寄通知、寫進別的表、或呼叫外部 API。對 blog 的延伸應用，例如做一個「訂閱通知」或「回饋表單」，&lt;code>onFormSubmit&lt;/code> 是接手處理的入口。要注意 &lt;code>MailApp.sendEmail&lt;/code> 受每日寄信配額約束（個人帳號每天 100 封收件人，見&lt;a href="https://tarrragon.github.io/blog/automation/knowledge-cards/execution-quota/" data-link-title="Execution Quota（執行配額）" data-link-desc="Apps Script 個人帳號的執行時間、同時併發與觸發器每日總時間上限，決定免費膠水層能承受多大的量">執行配額&lt;/a>），高頻通知要留意。&lt;/p>
&lt;h2 id="onedit試算表被編輯">onEdit：試算表被編輯&lt;/h2>
&lt;p>&lt;code>onEdit&lt;/code> 在試算表任何一格被改動時觸發，拿得到改了哪一格、新值是什麼。它適合「維護試算表內部的衍生狀態」，例如某欄被填值時自動在旁邊算出對應結果。對流量統計這條主線用得不多，但它是 Sheets 自動化的常見工具，值得知道它存在。&lt;/p>
&lt;h2 id="simple-與-installable一個權限分界">simple 與 installable：一個權限分界&lt;/h2>
&lt;p>事件觸發器有兩種形式，差別在&lt;strong>能不能做需要授權的事&lt;/strong>，這是容易踩的分界。&lt;/p>
&lt;p>&lt;strong>Simple 觸發器&lt;/strong>是靠函式命名約定自動生效的：把函式命名為 &lt;code>onOpen&lt;/code>、&lt;code>onEdit&lt;/code> 這些保留名字，不必註冊就會在對應事件觸發。代價是它跑在受限環境裡——&lt;strong>不能做需要授權的操作&lt;/strong>，例如寄信、呼叫外部 URL、存取其他檔案。它適合純粹在當前檔案內、不碰外部的輕量反應。&lt;/p>
&lt;p>&lt;strong>Installable 觸發器&lt;/strong>是明確註冊的（用 &lt;code>ScriptApp.newTrigger(...).forSpreadsheet(...).onEdit().create()&lt;/code>，或在觸發條件介面建立），它以你的授權身分執行，&lt;strong>能做需要權限的操作&lt;/strong>（寄信、UrlFetch、跨檔案）。&lt;code>onFormSubmit&lt;/code> 要做寄信這類事，必須是 installable 的。&lt;/p>
&lt;p>判準很直接：&lt;strong>這個事件處理要不要碰當前檔案以外的東西&lt;/strong>——不要（只在表內算個值），simple 夠用；要（寄信、打 API、寫別的表），得用 installable。踩雷的典型情境是「我寫了一個 &lt;code>onFormSubmit&lt;/code> 想寄通知信，怎麼都沒寄出」——因為它被當成 simple 觸發器、而 simple 不能寄信；改成明確註冊的 installable 就好。&lt;/p>
&lt;h2 id="下一步">下一步&lt;/h2>
&lt;p>觸發器（時間的與事件的）把統計從「手動查」變成「自動跑」。整套統計上線後怎麼守住配額、擋濫用、保持資料乾淨，見&lt;a href="https://tarrragon.github.io/blog/automation/05-deploy-quota-security/" data-link-title="模組五：部署、配額與安全" data-link-desc="把匿名可存取的 beacon 接收端上線後，怎麼守住免費配額、擋掉濫用、保持資料乾淨、以及判斷何時該換更重的工具">模組五：部署、配額與安全&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>事件觸發器讓 Apps Script 在「某個 Google 檔案發生某件事」的當下自動執行，跟時間觸發器的「到點就跑」互補。最常用的是 <code>onFormSubmit</code>（有人提交 Google 表單）跟 <code>onEdit</code>（有人編輯試算表）。這一篇講事件觸發器能做什麼、以及一個容易踩的權限分界。</p>
<h2 id="onformsubmit表單提交即時處理">onFormSubmit：表單提交即時處理</h2>
<p>Google 表單本身會把回應存進一張試算表，但「存進去之後要做什麼」需要 Apps Script。<code>onFormSubmit</code> 觸發器在每次有人提交表單時執行，拿得到這次提交的內容：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">onFormSubmit</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">answers</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">namedValues</span><span class="p">;</span>        <span class="c1">// { &#34;Email&#34;: [&#34;a@b.com&#34;], &#34;問題&#34;: [&#34;內容&#34;] }
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="c1">// 例如：寄一封通知信給自己
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nx">MailApp</span><span class="p">.</span><span class="nx">sendEmail</span><span class="p">(</span><span class="s2">&#34;you@gmail.com&#34;</span><span class="p">,</span> <span class="s2">&#34;新表單回應&#34;</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">answers</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這讓表單從「被動收集」變成「即時反應」——提交當下就寄通知、寫進別的表、或呼叫外部 API。對 blog 的延伸應用，例如做一個「訂閱通知」或「回饋表單」，<code>onFormSubmit</code> 是接手處理的入口。要注意 <code>MailApp.sendEmail</code> 受每日寄信配額約束（個人帳號每天 100 封收件人，見<a href="/blog/automation/knowledge-cards/execution-quota/" data-link-title="Execution Quota（執行配額）" data-link-desc="Apps Script 個人帳號的執行時間、同時併發與觸發器每日總時間上限，決定免費膠水層能承受多大的量">執行配額</a>），高頻通知要留意。</p>
<h2 id="onedit試算表被編輯">onEdit：試算表被編輯</h2>
<p><code>onEdit</code> 在試算表任何一格被改動時觸發，拿得到改了哪一格、新值是什麼。它適合「維護試算表內部的衍生狀態」，例如某欄被填值時自動在旁邊算出對應結果。對流量統計這條主線用得不多，但它是 Sheets 自動化的常見工具，值得知道它存在。</p>
<h2 id="simple-與-installable一個權限分界">simple 與 installable：一個權限分界</h2>
<p>事件觸發器有兩種形式，差別在<strong>能不能做需要授權的事</strong>，這是容易踩的分界。</p>
<p><strong>Simple 觸發器</strong>是靠函式命名約定自動生效的：把函式命名為 <code>onOpen</code>、<code>onEdit</code> 這些保留名字，不必註冊就會在對應事件觸發。代價是它跑在受限環境裡——<strong>不能做需要授權的操作</strong>，例如寄信、呼叫外部 URL、存取其他檔案。它適合純粹在當前檔案內、不碰外部的輕量反應。</p>
<p><strong>Installable 觸發器</strong>是明確註冊的（用 <code>ScriptApp.newTrigger(...).forSpreadsheet(...).onEdit().create()</code>，或在觸發條件介面建立），它以你的授權身分執行，<strong>能做需要權限的操作</strong>（寄信、UrlFetch、跨檔案）。<code>onFormSubmit</code> 要做寄信這類事，必須是 installable 的。</p>
<p>判準很直接：<strong>這個事件處理要不要碰當前檔案以外的東西</strong>——不要（只在表內算個值），simple 夠用；要（寄信、打 API、寫別的表），得用 installable。踩雷的典型情境是「我寫了一個 <code>onFormSubmit</code> 想寄通知信，怎麼都沒寄出」——因為它被當成 simple 觸發器、而 simple 不能寄信；改成明確註冊的 installable 就好。</p>
<h2 id="下一步">下一步</h2>
<p>觸發器（時間的與事件的）把統計從「手動查」變成「自動跑」。整套統計上線後怎麼守住配額、擋濫用、保持資料乾淨，見<a href="/blog/automation/05-deploy-quota-security/" data-link-title="模組五：部署、配額與安全" data-link-desc="把匿名可存取的 beacon 接收端上線後，怎麼守住免費配額、擋掉濫用、保持資料乾淨、以及判斷何時該換更重的工具">模組五：部署、配額與安全</a>。</p>
]]></content:encoded></item></channel></rss>