<?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>Google-Sheets on Tarragon</title><link>https://tarrragon.github.io/blog/tags/google-sheets/</link><description>Recent content in Google-Sheets 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/tags/google-sheets/index.xml" rel="self" type="application/rss+xml"/><item><title>寫入與並發：appendRow 與 LockService</title><link>https://tarrragon.github.io/blog/automation/03-sheet-as-database/append-and-concurrency/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/automation/03-sheet-as-database/append-and-concurrency/</guid><description>&lt;p>Sheet 的寫入層要處理的核心問題是：當多個 beacon 幾乎同時打進來、各自觸發一個 &lt;code>doPost&lt;/code> 執行實例、都要往同一張表寫一列時，這些並發寫入會不會互相干擾。單筆寫入很單純，並發才是這一層真正要判斷的地方。先講單筆怎麼寫，再講並發何時需要保護。&lt;/p>
&lt;h2 id="appendrow單筆寫入">appendRow：單筆寫入&lt;/h2>
&lt;p>&lt;code>appendRow&lt;/code> 在表格最後一列的後面新增一列，是 log 型資料最自然的寫法——每個事件就是一列、append 到尾巴。它一次接一個陣列、對應各欄：&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="nx">sheet&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendRow&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="k">new&lt;/span> &lt;span class="nb">Date&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">path&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">dev&lt;/span>&lt;span class="p">]);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>appendRow&lt;/code> 的好處是它自己找「最後一列的下一列」，不必你算行號。對「一次寫一筆瀏覽」這種場景，它是最直接的選擇。要注意它是「整列 append」——如果需要一次寫很多列（例如批次彙總），逐筆呼叫 &lt;code>appendRow&lt;/code> 會慢，那時改用 &lt;code>getRange(...).setValues(二維陣列)&lt;/code> 一次寫一整塊，效能差很多。單筆用 &lt;code>appendRow&lt;/code>、批次用 &lt;code>setValues&lt;/code>，是 Sheets 寫入的基本分工。&lt;/p>
&lt;h2 id="並發兩個-dopost-同時-append-會怎樣">並發：兩個 doPost 同時 append 會怎樣&lt;/h2>
&lt;p>Apps Script 允許多個執行實例同時跑（個人帳號上限 30 個併發）。當兩個 beacon 幾乎同時到達，平台會起兩個 &lt;code>doPost&lt;/code> 實例並行執行，兩者都呼叫 &lt;code>appendRow&lt;/code>。多數情況下 Google 會把兩次 append 排到不同列、相安無事；但在高並發下，「兩個實例同時判斷『最後一列是第 100 列』、都想寫第 101 列」的競態是可能發生的，結果是一筆覆蓋另一筆、少記一筆。&lt;/p>
&lt;p>這個風險要不要處理，取決於&lt;strong>你的瞬間併發有多高&lt;/strong>。個人 blog 的流量分散在整天，任何一刻同時到達的 beacon 通常是個位數甚至零，競態機率極低、就算偶爾少記一筆對「哪篇有人看」的判斷也無影響——這種情境不必加保護，保持 &lt;code>appendRow&lt;/code> 的簡單。真正需要處理的是「短時間尖峰」：某篇文章被大量分享、一分鐘湧入幾百次瀏覽，這時並發拉高、競態才變得值得防。&lt;/p>
&lt;h2 id="lockservice需要時才序列化">LockService：需要時才序列化&lt;/h2>
&lt;p>要防競態，用 &lt;code>LockService&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">doPost&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">lock&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">LockService&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getScriptLock&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="nx">lock&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">waitLock&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">5000&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"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">try&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="kd">var&lt;/span> &lt;span class="nx">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">JSON&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">parse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">postData&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">contents&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="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 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"> 7&lt;/span>&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">appendRow&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="k">new&lt;/span> &lt;span class="nb">Date&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">path&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">dev&lt;/span> &lt;span class="o">||&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"> 8&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">finally&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="nx">lock&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">releaseLock&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">10&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">11&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">ContentService&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">createTextOutput&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">ok&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&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="p">.&lt;/span>&lt;span class="nx">setMimeType&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ContentService&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">MimeType&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">JSON&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;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>getScriptLock&lt;/code> 取的是整個腳本共用的鎖，保證所有 &lt;code>doPost&lt;/code> 實例排隊寫入。&lt;code>waitLock(5000)&lt;/code> 是「最多等 5 秒」——等不到就丟例外（併發高到 5 秒還排不進來，這筆放棄，總比無限等好）。&lt;code>releaseLock&lt;/code> 必須放在 &lt;code>finally&lt;/code>，確保就算中間出錯也會釋放，否則鎖沒放、後面的請求全部卡死。&lt;/p>
&lt;p>加鎖的代價是寫入從並行變序列，尖峰時每筆要多等前面的做完，吞吐下降。所以它是「有並發競態風險時才加」的保護，不是預設就要有的東西。判斷訊號回到前面那句：&lt;strong>瞬間併發會不會高到讓競態實際發生&lt;/strong>——不會就別加，會就加上。這也是為什麼流量統計的基本版 &lt;code>doPost&lt;/code>（模組二）刻意不含 lock：先讓簡單版跑起來，等真的遇到尖峰漏記，再加這一層。&lt;/p>
&lt;h2 id="下一步">下一步&lt;/h2>
&lt;p>寫入處理好了，資料表本身怎麼設計、以及 Sheets 累積到多大會撐不住，見&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></description><content:encoded><![CDATA[<p>Sheet 的寫入層要處理的核心問題是：當多個 beacon 幾乎同時打進來、各自觸發一個 <code>doPost</code> 執行實例、都要往同一張表寫一列時，這些並發寫入會不會互相干擾。單筆寫入很單純，並發才是這一層真正要判斷的地方。先講單筆怎麼寫，再講並發何時需要保護。</p>
<h2 id="appendrow單筆寫入">appendRow：單筆寫入</h2>
<p><code>appendRow</code> 在表格最後一列的後面新增一列，是 log 型資料最自然的寫法——每個事件就是一列、append 到尾巴。它一次接一個陣列、對應各欄：</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="nx">sheet</span><span class="p">.</span><span class="nx">appendRow</span><span class="p">([</span><span class="k">new</span> <span class="nb">Date</span><span class="p">(),</span> <span class="nx">data</span><span class="p">.</span><span class="nx">path</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="nx">dev</span><span class="p">]);</span></span></span></code></pre></div><p><code>appendRow</code> 的好處是它自己找「最後一列的下一列」，不必你算行號。對「一次寫一筆瀏覽」這種場景，它是最直接的選擇。要注意它是「整列 append」——如果需要一次寫很多列（例如批次彙總），逐筆呼叫 <code>appendRow</code> 會慢，那時改用 <code>getRange(...).setValues(二維陣列)</code> 一次寫一整塊，效能差很多。單筆用 <code>appendRow</code>、批次用 <code>setValues</code>，是 Sheets 寫入的基本分工。</p>
<h2 id="並發兩個-dopost-同時-append-會怎樣">並發：兩個 doPost 同時 append 會怎樣</h2>
<p>Apps Script 允許多個執行實例同時跑（個人帳號上限 30 個併發）。當兩個 beacon 幾乎同時到達，平台會起兩個 <code>doPost</code> 實例並行執行，兩者都呼叫 <code>appendRow</code>。多數情況下 Google 會把兩次 append 排到不同列、相安無事；但在高並發下，「兩個實例同時判斷『最後一列是第 100 列』、都想寫第 101 列」的競態是可能發生的，結果是一筆覆蓋另一筆、少記一筆。</p>
<p>這個風險要不要處理，取決於<strong>你的瞬間併發有多高</strong>。個人 blog 的流量分散在整天，任何一刻同時到達的 beacon 通常是個位數甚至零，競態機率極低、就算偶爾少記一筆對「哪篇有人看」的判斷也無影響——這種情境不必加保護，保持 <code>appendRow</code> 的簡單。真正需要處理的是「短時間尖峰」：某篇文章被大量分享、一分鐘湧入幾百次瀏覽，這時並發拉高、競態才變得值得防。</p>
<h2 id="lockservice需要時才序列化">LockService：需要時才序列化</h2>
<p>要防競態，用 <code>LockService</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">doPost</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">lock</span> <span class="o">=</span> <span class="nx">LockService</span><span class="p">.</span><span class="nx">getScriptLock</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">lock</span><span class="p">.</span><span class="nx">waitLock</span><span class="p">(</span><span class="mi">5000</span><span class="p">);</span> <span class="c1">// 最多等 5 秒拿鎖
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span>  <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="kd">var</span> <span class="nx">data</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">postData</span><span class="p">.</span><span class="nx">contents</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">SpreadsheetApp</span><span class="p">.</span><span class="nx">getActiveSpreadsheet</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"> 7</span><span class="cl">      <span class="p">.</span><span class="nx">appendRow</span><span class="p">([</span><span class="k">new</span> <span class="nb">Date</span><span class="p">(),</span> <span class="nx">data</span><span class="p">.</span><span class="nx">path</span> <span class="o">||</span> <span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="nx">dev</span> <span class="o">||</span> <span class="s2">&#34;&#34;</span><span class="p">]);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">lock</span><span class="p">.</span><span class="nx">releaseLock</span><span class="p">();</span> <span class="c1">// 一定要放，否則後面全部卡住
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="k">return</span> <span class="nx">ContentService</span><span class="p">.</span><span class="nx">createTextOutput</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">ok</span><span class="o">:</span> <span class="kc">true</span> <span class="p">}))</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">.</span><span class="nx">setMimeType</span><span class="p">(</span><span class="nx">ContentService</span><span class="p">.</span><span class="nx">MimeType</span><span class="p">.</span><span class="nx">JSON</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>getScriptLock</code> 取的是整個腳本共用的鎖，保證所有 <code>doPost</code> 實例排隊寫入。<code>waitLock(5000)</code> 是「最多等 5 秒」——等不到就丟例外（併發高到 5 秒還排不進來，這筆放棄，總比無限等好）。<code>releaseLock</code> 必須放在 <code>finally</code>，確保就算中間出錯也會釋放，否則鎖沒放、後面的請求全部卡死。</p>
<p>加鎖的代價是寫入從並行變序列，尖峰時每筆要多等前面的做完，吞吐下降。所以它是「有並發競態風險時才加」的保護，不是預設就要有的東西。判斷訊號回到前面那句：<strong>瞬間併發會不會高到讓競態實際發生</strong>——不會就別加，會就加上。這也是為什麼流量統計的基本版 <code>doPost</code>（模組二）刻意不含 lock：先讓簡單版跑起來，等真的遇到尖峰漏記，再加這一層。</p>
<h2 id="下一步">下一步</h2>
<p>寫入處理好了，資料表本身怎麼設計、以及 Sheets 累積到多大會撐不住，見<a href="/blog/automation/03-sheet-as-database/data-model-and-capacity/" data-link-title="資料模型與容量邊界" data-link-desc="raw log 表的欄位怎麼設計才好彙總、以及 Sheets 累積到多少列會開始撐不住、撐不住的訊號長什麼樣">資料模型與容量邊界</a>。</p>
]]></content:encoded></item><item><title>資料模型與容量邊界</title><link>https://tarrragon.github.io/blog/automation/03-sheet-as-database/data-model-and-capacity/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/automation/03-sheet-as-database/data-model-and-capacity/</guid><description>&lt;p>Sheets 當資料庫的資料模型設計，核心原則是「一列一事件、欄位固定、只 append 不改」。這種形狀叫 raw log（原始紀錄）：每筆瀏覽就是一列、寫進去就不再動它，彙總與分析交給後續的排程另外算。先講為什麼這樣設計，再講這張表能長到多大。&lt;/p>
&lt;h2 id="raw-log-的欄位設計">raw log 的欄位設計&lt;/h2>
&lt;p>raw log 表每一欄對應一個固定的維度，每一列是一筆完整的事件。流量統計的 raw log 大致是：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>時間&lt;/th>
 &lt;th>路徑&lt;/th>
 &lt;th>來源&lt;/th>
 &lt;th>語言&lt;/th>
 &lt;th>裝置&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>2026-07-06 20:11:03&lt;/td>
 &lt;td>/posts/foo/&lt;/td>
 &lt;td>google.com&lt;/td>
 &lt;td>zh-TW&lt;/td>
 &lt;td>mobile&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2026-07-06 20:11:47&lt;/td>
 &lt;td>/automation/&lt;/td>
 &lt;td>&lt;/td>
 &lt;td>en-US&lt;/td>
 &lt;td>desktop&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三個設計決定讓這張表好用又好彙總。&lt;strong>時間放第一欄、用接收端的伺服器時間&lt;/strong>，讓資料天然按寫入順序排列，也讓「某天的資料」用時間範圍就能篩。&lt;strong>每一欄都是原子維度、不把多個資訊塞進一欄&lt;/strong>（不要把「路徑+裝置」合併成一欄），因為彙總時要能單獨 group by 路徑、或單獨 group by 裝置，欄位分開才做得到。&lt;strong>只 append、不回頭改已寫的列&lt;/strong>，讓這張表成為不可變的事實紀錄——要改的是彙總結果、不是原始 log，原始資料保持乾淨才能隨時重算。&lt;/p>
&lt;p>raw log 直接看沒有意義（幾千列逐筆瀏覽），它的價值是當彙總的原料。把 raw log 依日期與路徑 group 成「每天每篇看幾次」的日報，是&lt;a href="https://tarrragon.github.io/blog/automation/04-triggers-automation/" data-link-title="模組四：觸發器與排程" data-link-desc="用 Apps Script 的時間觸發器把累積的原始瀏覽 log 定時彙總成看得懂的日報，以及觸發器的每日執行配額">模組四：觸發器與排程&lt;/a>的工作。&lt;/p>
&lt;h2 id="sheets-的容量邊界">Sheets 的容量邊界&lt;/h2>
&lt;p>Sheets 是為「人看的試算表」設計的，不是高吞吐資料庫，所以它有兩類上限要放在心上。&lt;/p>
&lt;p>&lt;strong>總 cell 數上限&lt;/strong>：單一試算表有一個總儲存格數量的硬上限（目前以千萬 cell 計，Google 會不定期調整，實際數字以官方為準）。raw log 每列的 cell 數等於欄數，5 欄的話，這個上限換算成列數是相當大的量——對個人 blog 是好幾年的資料，不會很快碰到。但它是有限的，不是無限成長。&lt;/p>
&lt;p>&lt;strong>讀寫效能隨列數下降&lt;/strong>：實務上先浮現的邊界是效能，比硬上限更早遇到。當 raw log 累積到數萬、數十萬列，任何「整表讀進來算」的操作（例如彙總 trigger 每次全表掃描）會越來越慢，可能逼近觸發器的&lt;a href="https://tarrragon.github.io/blog/automation/knowledge-cards/execution-quota/" data-link-title="Execution Quota（執行配額）" data-link-desc="Apps Script 個人帳號的執行時間、同時併發與觸發器每日總時間上限，決定免費膠水層能承受多大的量">單次 6 分鐘執行上限&lt;/a>。&lt;/p>
&lt;h2 id="撐不住的訊號與應對">撐不住的訊號與應對&lt;/h2>
&lt;p>判斷 Sheets 開始撐不住，看這幾個訊號：彙總 trigger 的執行時間逐月變長、逼近 6 分鐘；開啟試算表本身變慢、捲動卡頓；或 cell 數逼近上限的警告。出現這些訊號時，有兩條應對路徑，依情況選：&lt;/p>
&lt;p>&lt;strong>分表&lt;/strong>：把 raw log 按月（或按季）拆成多張工作表，例如 &lt;code>log-2026-07&lt;/code>、&lt;code>log-2026-08&lt;/code>。彙總 trigger 只讀當月表、不必掃全部歷史，執行時間就回到穩定。這是最小改動、繼續留在 Sheets 的做法，適合量成長但還在 Sheets 能力範圍內。&lt;/p>
&lt;p>&lt;strong>遷移&lt;/strong>：當量大到連分表都吃力、或需要更即時的查詢，訊號就指向「該換更重的儲存」——把接收端從 Apps Script 換成 Cloudflare Workers + D1（免費 SQLite），資料庫的查詢能力與吞吐遠高於 Sheets。遷移的完整訊號與路徑在&lt;a href="https://tarrragon.github.io/blog/automation/05-deploy-quota-security/" data-link-title="模組五：部署、配額與安全" data-link-desc="把匿名可存取的 beacon 接收端上線後，怎麼守住免費配額、擋掉濫用、保持資料乾淨、以及判斷何時該換更重的工具">模組五&lt;/a>，選型的取捨在&lt;a href="https://tarrragon.github.io/blog/automation/00-mental-model/free-tier-and-tool-choice/" data-link-title="免費額度的思考方式與工具選型" data-link-desc="判斷免費膠水層撐不撐得住自己流量時該看哪個限制、以及 Apps Script 與 Cloudflare Workers 各自適合什麼場景">模組零&lt;/a>。&lt;/p>
&lt;p>判準用一句話收斂：&lt;strong>Sheets 撐不住的第一訊號通常是彙總變慢、不是 cell 爆掉&lt;/strong>——先分表爭取空間，分表也吃力才談遷移。對絕大多數個人 blog，這些邊界很久才會碰到，先讓簡單版跑著、留意訊號即可。&lt;/p>
&lt;h2 id="事件模型會改變這裡的估算">事件模型會改變這裡的估算&lt;/h2>
&lt;p>上面的估算假設一次瀏覽產生一列。&lt;a href="https://tarrragon.github.io/blog/automation/06-reading-the-data/event-model/" data-link-title="事件模型與停留時間" data-link-desc="每次瀏覽只記一列時看不出讀者停留多久、有沒有真的在讀；補上離開事件之後，那個秒數的語意與既有報表公式的連帶影響">模組六的事件模型&lt;/a>把它改成一進一離兩列，並在 payload 上多加六個欄位——列數接近翻倍、單列的 cell 數從五增為十一，兩者相乘之後容量上限會比這裡算出來的早很多抵達。真要估的話，用那一章的欄位清單重算一次，而不是沿用這一節的數字。&lt;/p>
&lt;h2 id="下一步">下一步&lt;/h2>
&lt;p>資料進來、也知道表能長多大之後，把 raw log 變成看得懂的日報，見&lt;a href="https://tarrragon.github.io/blog/automation/04-triggers-automation/" data-link-title="模組四：觸發器與排程" data-link-desc="用 Apps Script 的時間觸發器把累積的原始瀏覽 log 定時彙總成看得懂的日報，以及觸發器的每日執行配額">模組四：觸發器與排程&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Sheets 當資料庫的資料模型設計，核心原則是「一列一事件、欄位固定、只 append 不改」。這種形狀叫 raw log（原始紀錄）：每筆瀏覽就是一列、寫進去就不再動它，彙總與分析交給後續的排程另外算。先講為什麼這樣設計，再講這張表能長到多大。</p>
<h2 id="raw-log-的欄位設計">raw log 的欄位設計</h2>
<p>raw log 表每一欄對應一個固定的維度，每一列是一筆完整的事件。流量統計的 raw log 大致是：</p>
<table>
  <thead>
      <tr>
          <th>時間</th>
          <th>路徑</th>
          <th>來源</th>
          <th>語言</th>
          <th>裝置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2026-07-06 20:11:03</td>
          <td>/posts/foo/</td>
          <td>google.com</td>
          <td>zh-TW</td>
          <td>mobile</td>
      </tr>
      <tr>
          <td>2026-07-06 20:11:47</td>
          <td>/automation/</td>
          <td></td>
          <td>en-US</td>
          <td>desktop</td>
      </tr>
  </tbody>
</table>
<p>三個設計決定讓這張表好用又好彙總。<strong>時間放第一欄、用接收端的伺服器時間</strong>，讓資料天然按寫入順序排列，也讓「某天的資料」用時間範圍就能篩。<strong>每一欄都是原子維度、不把多個資訊塞進一欄</strong>（不要把「路徑+裝置」合併成一欄），因為彙總時要能單獨 group by 路徑、或單獨 group by 裝置，欄位分開才做得到。<strong>只 append、不回頭改已寫的列</strong>，讓這張表成為不可變的事實紀錄——要改的是彙總結果、不是原始 log，原始資料保持乾淨才能隨時重算。</p>
<p>raw log 直接看沒有意義（幾千列逐筆瀏覽），它的價值是當彙總的原料。把 raw log 依日期與路徑 group 成「每天每篇看幾次」的日報，是<a href="/blog/automation/04-triggers-automation/" data-link-title="模組四：觸發器與排程" data-link-desc="用 Apps Script 的時間觸發器把累積的原始瀏覽 log 定時彙總成看得懂的日報，以及觸發器的每日執行配額">模組四：觸發器與排程</a>的工作。</p>
<h2 id="sheets-的容量邊界">Sheets 的容量邊界</h2>
<p>Sheets 是為「人看的試算表」設計的，不是高吞吐資料庫，所以它有兩類上限要放在心上。</p>
<p><strong>總 cell 數上限</strong>：單一試算表有一個總儲存格數量的硬上限（目前以千萬 cell 計，Google 會不定期調整，實際數字以官方為準）。raw log 每列的 cell 數等於欄數，5 欄的話，這個上限換算成列數是相當大的量——對個人 blog 是好幾年的資料，不會很快碰到。但它是有限的，不是無限成長。</p>
<p><strong>讀寫效能隨列數下降</strong>：實務上先浮現的邊界是效能，比硬上限更早遇到。當 raw log 累積到數萬、數十萬列，任何「整表讀進來算」的操作（例如彙總 trigger 每次全表掃描）會越來越慢，可能逼近觸發器的<a href="/blog/automation/knowledge-cards/execution-quota/" data-link-title="Execution Quota（執行配額）" data-link-desc="Apps Script 個人帳號的執行時間、同時併發與觸發器每日總時間上限，決定免費膠水層能承受多大的量">單次 6 分鐘執行上限</a>。</p>
<h2 id="撐不住的訊號與應對">撐不住的訊號與應對</h2>
<p>判斷 Sheets 開始撐不住，看這幾個訊號：彙總 trigger 的執行時間逐月變長、逼近 6 分鐘；開啟試算表本身變慢、捲動卡頓；或 cell 數逼近上限的警告。出現這些訊號時，有兩條應對路徑，依情況選：</p>
<p><strong>分表</strong>：把 raw log 按月（或按季）拆成多張工作表，例如 <code>log-2026-07</code>、<code>log-2026-08</code>。彙總 trigger 只讀當月表、不必掃全部歷史，執行時間就回到穩定。這是最小改動、繼續留在 Sheets 的做法，適合量成長但還在 Sheets 能力範圍內。</p>
<p><strong>遷移</strong>：當量大到連分表都吃力、或需要更即時的查詢，訊號就指向「該換更重的儲存」——把接收端從 Apps Script 換成 Cloudflare Workers + D1（免費 SQLite），資料庫的查詢能力與吞吐遠高於 Sheets。遷移的完整訊號與路徑在<a href="/blog/automation/05-deploy-quota-security/" data-link-title="模組五：部署、配額與安全" data-link-desc="把匿名可存取的 beacon 接收端上線後，怎麼守住免費配額、擋掉濫用、保持資料乾淨、以及判斷何時該換更重的工具">模組五</a>，選型的取捨在<a href="/blog/automation/00-mental-model/free-tier-and-tool-choice/" data-link-title="免費額度的思考方式與工具選型" data-link-desc="判斷免費膠水層撐不撐得住自己流量時該看哪個限制、以及 Apps Script 與 Cloudflare Workers 各自適合什麼場景">模組零</a>。</p>
<p>判準用一句話收斂：<strong>Sheets 撐不住的第一訊號通常是彙總變慢、不是 cell 爆掉</strong>——先分表爭取空間，分表也吃力才談遷移。對絕大多數個人 blog，這些邊界很久才會碰到，先讓簡單版跑著、留意訊號即可。</p>
<h2 id="事件模型會改變這裡的估算">事件模型會改變這裡的估算</h2>
<p>上面的估算假設一次瀏覽產生一列。<a href="/blog/automation/06-reading-the-data/event-model/" data-link-title="事件模型與停留時間" data-link-desc="每次瀏覽只記一列時看不出讀者停留多久、有沒有真的在讀；補上離開事件之後，那個秒數的語意與既有報表公式的連帶影響">模組六的事件模型</a>把它改成一進一離兩列，並在 payload 上多加六個欄位——列數接近翻倍、單列的 cell 數從五增為十一，兩者相乘之後容量上限會比這裡算出來的早很多抵達。真要估的話，用那一章的欄位清單重算一次，而不是沿用這一節的數字。</p>
<h2 id="下一步">下一步</h2>
<p>資料進來、也知道表能長多大之後，把 raw log 變成看得懂的日報，見<a href="/blog/automation/04-triggers-automation/" data-link-title="模組四：觸發器與排程" data-link-desc="用 Apps Script 的時間觸發器把累積的原始瀏覽 log 定時彙總成看得懂的日報，以及觸發器的每日執行配額">模組四：觸發器與排程</a>。</p>
]]></content:encoded></item><item><title>模組三：Sheets 當資料庫</title><link>https://tarrragon.github.io/blog/automation/03-sheet-as-database/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/automation/03-sheet-as-database/</guid><description>&lt;p>回答「Sheet 當資料庫可以撐到什麼程度、什麼時候會出問題」。Sheets 當儲存體的最大好處是它同時是儀表板——資料進去就能直接看、排序、畫圖。但它畢竟不是為高併發資料庫設計的，這一章處理三件會實際遇到的事：多個 beacon 同時 &lt;code>appendRow&lt;/code> 會不會互相覆蓋、資料欄位怎麼設計才好彙總、以及列數累積到多少會開始變慢。&lt;/p>
&lt;p>Sheets 適合的場景是資料量小到中等、需要人直接讀寫的流量統計。它撐不住的訊號很明確：列數到數萬列後讀寫變慢、瞬間併發逼近 30 時出現寫入衝突。碰到這些訊號時的判斷與遷移路徑，見&lt;a href="https://tarrragon.github.io/blog/automation/05-deploy-quota-security/" data-link-title="模組五：部署、配額與安全" data-link-desc="把匿名可存取的 beacon 接收端上線後，怎麼守住免費配額、擋掉濫用、保持資料乾淨、以及判斷何時該換更重的工具">模組五&lt;/a>。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&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 序列化寫入的時機判斷">寫入與並發：appendRow 與 LockService&lt;/a>&lt;/td>
 &lt;td>單筆 &lt;code>appendRow&lt;/code>、並發競態何時發生、&lt;code>LockService&lt;/code> 序列化的時機判斷&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&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;/td>
 &lt;td>raw log 的欄位設計、Sheets 的 cell 與效能上限、撐不住的訊號與分表 / 遷移&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/automation/02-analytics-beacon/receiver-handler/" data-link-title="接收端 handler：寫進第一筆" data-link-desc="Apps Script 這端怎麼解析 text/plain 的 beacon、用伺服器時間補上時間戳、append 進 Sheet，並在部署後確認收到第一筆真實瀏覽">模組二：接收端 handler&lt;/a>：資料怎麼進到 Sheet 的&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/automation/04-triggers-automation/" data-link-title="模組四：觸發器與排程" data-link-desc="用 Apps Script 的時間觸發器把累積的原始瀏覽 log 定時彙總成看得懂的日報，以及觸發器的每日執行配額">模組四：觸發器與排程&lt;/a>：把 raw log 彙總成報表&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/automation/05-deploy-quota-security/" data-link-title="模組五：部署、配額與安全" data-link-desc="把匿名可存取的 beacon 接收端上線後，怎麼守住免費配額、擋掉濫用、保持資料乾淨、以及判斷何時該換更重的工具">模組五：部署、配額與安全&lt;/a>：Sheets 撐不住時怎麼判斷與遷移&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>回答「Sheet 當資料庫可以撐到什麼程度、什麼時候會出問題」。Sheets 當儲存體的最大好處是它同時是儀表板——資料進去就能直接看、排序、畫圖。但它畢竟不是為高併發資料庫設計的，這一章處理三件會實際遇到的事：多個 beacon 同時 <code>appendRow</code> 會不會互相覆蓋、資料欄位怎麼設計才好彙總、以及列數累積到多少會開始變慢。</p>
<p>Sheets 適合的場景是資料量小到中等、需要人直接讀寫的流量統計。它撐不住的訊號很明確：列數到數萬列後讀寫變慢、瞬間併發逼近 30 時出現寫入衝突。碰到這些訊號時的判斷與遷移路徑，見<a href="/blog/automation/05-deploy-quota-security/" data-link-title="模組五：部署、配額與安全" data-link-desc="把匿名可存取的 beacon 接收端上線後，怎麼守住免費配額、擋掉濫用、保持資料乾淨、以及判斷何時該換更重的工具">模組五</a>。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/automation/03-sheet-as-database/append-and-concurrency/" data-link-title="寫入與並發：appendRow 與 LockService" data-link-desc="多個 beacon 同時寫進同一張 Sheet 時，appendRow 的競態風險與 LockService 序列化寫入的時機判斷">寫入與並發：appendRow 與 LockService</a></td>
          <td>單筆 <code>appendRow</code>、並發競態何時發生、<code>LockService</code> 序列化的時機判斷</td>
      </tr>
      <tr>
          <td><a href="/blog/automation/03-sheet-as-database/data-model-and-capacity/" data-link-title="資料模型與容量邊界" data-link-desc="raw log 表的欄位怎麼設計才好彙總、以及 Sheets 累積到多少列會開始撐不住、撐不住的訊號長什麼樣">資料模型與容量邊界</a></td>
          <td>raw log 的欄位設計、Sheets 的 cell 與效能上限、撐不住的訊號與分表 / 遷移</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/automation/02-analytics-beacon/receiver-handler/" data-link-title="接收端 handler：寫進第一筆" data-link-desc="Apps Script 這端怎麼解析 text/plain 的 beacon、用伺服器時間補上時間戳、append 進 Sheet，並在部署後確認收到第一筆真實瀏覽">模組二：接收端 handler</a>：資料怎麼進到 Sheet 的</li>
<li>→ <a href="/blog/automation/04-triggers-automation/" data-link-title="模組四：觸發器與排程" data-link-desc="用 Apps Script 的時間觸發器把累積的原始瀏覽 log 定時彙總成看得懂的日報，以及觸發器的每日執行配額">模組四：觸發器與排程</a>：把 raw log 彙總成報表</li>
<li>→ <a href="/blog/automation/05-deploy-quota-security/" data-link-title="模組五：部署、配額與安全" data-link-desc="把匿名可存取的 beacon 接收端上線後，怎麼守住免費配額、擋掉濫用、保持資料乾淨、以及判斷何時該換更重的工具">模組五：部署、配額與安全</a>：Sheets 撐不住時怎麼判斷與遷移</li>
</ul>
]]></content:encoded></item><item><title>免伺服器自動化實務指南：用免費雲端服務給靜態站補上動態能力</title><link>https://tarrragon.github.io/blog/automation/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/automation/</guid><description>&lt;p>本指南處理一類具體的工程問題：手上有一個沒有後端的靜態站或小專案，需要一點點動態能力（收一筆表單、記一次瀏覽、每天彙總一次資料），但這個需求還不到、也不值得租一台伺服器的程度。核心概念是「膠水層」——用別人已經在跑、且對個人用量免費的雲端服務，補上靜態站缺的那一小塊伺服器端邏輯，讓你不必自己維護一台開機的主機。&lt;/p>
&lt;p>靜態站的能力邊界很明確：瀏覽器把 HTML/JS 抓下來後，所有邏輯都在使用者的瀏覽器裡跑，沒有任何一段程式碼在你控制的伺服器上執行。這代表任何「要記在你這邊」的資料——誰來看過、表單填了什麼、累積計數——都需要一個瀏覽器以外、由你掌握的接收端。這個接收端不必是傳統伺服器；它可以是一段掛在免費平台上、只在被呼叫時才執行的函式。本指南教怎麼用這種函式把靜態站的能力補齊。&lt;/p>
&lt;p>貫穿全指南的案例是「幫這個架在 GitHub Pages 上的 blog 做流量統計」。GitHub Pages 不給 access log、也不能跑伺服器端程式碼，所以流量資料只能靠瀏覽器主動回報。這個案例會從模組零的架構推導、一路實作到模組六的資料判讀，讀者跟著做完會得到一個真的能用、資料存在自己試算表裡的流量統計系統——「自己手上」在這裡指的是資料的控制權與查詢自由，基礎設施仍然是 Google 託管的。&lt;/p>
&lt;p>第一種膠水工具選 Google Apps Script + Google Sheets：它對個人 Google 帳號免費、不需要信用卡、Sheets 直接當資料庫兼儀表板，起步門檻是所有選項裡最低的。指南後續會加入其他膠水工具（例如 Cloudflare Workers）作為對照，說明各自的適用邊界——Apps Script 適合資料量小、需要人可直接讀寫試算表的場景；當量體變大或需要低延遲時，模組五會給出換工具的判準與遷移路徑。&lt;/p>
&lt;h2 id="教材邊界">教材邊界&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>類型&lt;/th>
 &lt;th>放在本指南&lt;/th>
 &lt;th>不放在本指南&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>心智模型&lt;/td>
 &lt;td>靜態站能力邊界、膠水層架構、client beacon、免費額度的思考方式&lt;/td>
 &lt;td>大型後端架構、微服務拆分&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>工具地基&lt;/td>
 &lt;td>Apps Script web app 部署模型（&lt;code>doGet&lt;/code>/&lt;code>doPost&lt;/code>）、授權模型、V8 runtime&lt;/td>
 &lt;td>Apps Script 在 Workspace 企業版的進階整合&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>實作&lt;/td>
 &lt;td>beacon 前端、接收端 handler、Sheets 讀寫、觸發器排程、配額與安全&lt;/td>
 &lt;td>商業級 analytics 平台的自建（見 &lt;a href="https://tarrragon.github.io/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">Monitoring&lt;/a>）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料品質&lt;/td>
 &lt;td>訪客識別、事件模型、自動化流量辨識、靜默失效診斷&lt;/td>
 &lt;td>跨裝置身分解析、廣告歸因、第三方資料串接&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>選型&lt;/td>
 &lt;td>自建 vs 現成分析服務、Apps Script vs Workers 的適用邊界、何時該換工具&lt;/td>
 &lt;td>雲端主機比價、Kubernetes&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>流量分析的&lt;strong>概念層&lt;/strong>（事件分類、漏斗、cohort、歸因）不在本指南，在 &lt;a href="https://tarrragon.github.io/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">Monitoring 監控體系&lt;/a>。本指南是&lt;strong>動手做&lt;/strong>的那一半：怎麼用免費工具把資料真的收進來、存起來、彙總出報表。兩者互補——先看 Monitoring 想清楚要收什麼，再回本指南把管線搭起來。&lt;/p>
&lt;h2 id="backlog">Backlog&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>項目&lt;/th>
 &lt;th>類型&lt;/th>
 &lt;th>前置條件&lt;/th>
 &lt;th>規模&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>雙事件配對的彙總實作（平均停留、閱讀深度）&lt;/td>
 &lt;td>主章&lt;/td>
 &lt;td>模組四彙總改寫成事件感知版本&lt;/td>
 &lt;td>1 篇&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cloudflare Workers 對照與遷移路徑&lt;/td>
 &lt;td>主章&lt;/td>
 &lt;td>實機驗證免費額度與部署流程&lt;/td>
 &lt;td>2 篇&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Referrer-Policy 知識卡&lt;/td>
 &lt;td>知識卡&lt;/td>
 &lt;td>無（模組六已完整展開、判定它是否仍需獨立卡）&lt;/td>
 &lt;td>1 張&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>現成分析服務的實測對照&lt;/td>
 &lt;td>vendor&lt;/td>
 &lt;td>實測 Plausible / Umami / GoatCounter 的免費額度與能力&lt;/td>
 &lt;td>1 篇&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Monitoring 對本指南的反向連結&lt;/td>
 &lt;td>跨模組&lt;/td>
 &lt;td>盤點 monitoring 各模組的實作落點&lt;/td>
 &lt;td>小&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跨模組那一列是目前唯一的結構性不對稱：本指南對 monitoring 有五條引用，反向零條，而 monitoring 的漏斗與 cohort 分析都預設資料乾淨、可識別、無機器混入——那些前提正是模組六在處理的。&lt;/p>
&lt;p>模組六補上了資料判讀，但彙總層仍停在模組四的單事件版本——那裡目前只標明了前提與修法方向，尚未有一篇把「配對進入與離開事件、算出平均停留與閱讀深度」實作出來。順序上它依賴模組六的事件模型定案，因此排在其後。&lt;/p>
&lt;h2 id="章節">章節&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/automation/00-mental-model/" data-link-title="模組零：心智模型" data-link-desc="判斷一個動態需求要不要伺服器、資料從哪個接收端進來、免費額度撐得住多大量時的思考框架">模組零：心智模型&lt;/a>&lt;/td>
 &lt;td>靜態站能力邊界、膠水層、client beacon 架構、免費額度、自建 vs 現成服務、GAS vs Workers 選型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/automation/01-apps-script-basics/" data-link-title="模組一：Apps Script 地基" data-link-desc="搞懂 Apps Script 的 web app 部署模型與授權模型，才不會在做 beacon 時卡在網址打不通或權限被擋">模組一：Apps Script 地基&lt;/a>&lt;/td>
 &lt;td>Apps Script 是什麼、V8 runtime、web app 部署模型、授權模型、跟一般伺服器的差異&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/automation/02-analytics-beacon/" data-link-title="模組二：流量 beacon 實作" data-link-desc="把「頁面被看了就送一則事件、接收端寫進 Sheet」從零做到收到第一筆真實瀏覽紀錄時的完整實作">模組二：流量 beacon 實作&lt;/a>&lt;/td>
 &lt;td>前端 beacon、接收端 handler、寫進 Sheet、CORS 的雷與解法，從零到第一筆&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/automation/03-sheet-as-database/" data-link-title="模組三：Sheets 當資料庫" data-link-desc="用 Google Sheet 存流量資料時，怎麼處理多個 beacon 同時寫入的並發、設計資料模型、以及判斷資料量到哪會撐不住">模組三：Sheets 當資料庫&lt;/a>&lt;/td>
 &lt;td>&lt;code>appendRow&lt;/code>、資料模型、並發與 &lt;code>LockService&lt;/code>、Sheets 的容量邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/automation/04-triggers-automation/" data-link-title="模組四：觸發器與排程" data-link-desc="用 Apps Script 的時間觸發器把累積的原始瀏覽 log 定時彙總成看得懂的日報，以及觸發器的每日執行配額">模組四：觸發器與排程&lt;/a>&lt;/td>
 &lt;td>time-driven trigger 每日彙總、&lt;code>onFormSubmit&lt;/code>、把原始 log 變成日報&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/automation/05-deploy-quota-security/" data-link-title="模組五：部署、配額與安全" data-link-desc="把匿名可存取的 beacon 接收端上線後，怎麼守住免費配額、擋掉濫用、保持資料乾淨、以及判斷何時該換更重的工具">模組五：部署、配額與安全&lt;/a>&lt;/td>
 &lt;td>部署權限、免費配額上限、CORS 設定、防濫用、隱私邊界與同意機制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/automation/06-reading-the-data/" data-link-title="模組六：收到資料之後" data-link-desc="流量統計上線、資料開始累積，但欄位讀不出「這是誰、是不是同一個人、是不是機器」時的判讀與補強">模組六：收到資料之後&lt;/a>&lt;/td>
 &lt;td>訪客識別與 opt-out、事件模型與停留時間、辨識自動化流量、假故障與靜默失效診斷&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>模組零到五走的是「怎麼把管線接起來」，那條路徑上的每個障礙都能從文件讀出來。模組六的位置不同——它處理管線接好、資料真的開始累積之後才浮現的問題：來源網址大量空白、任兩列之間看不出是不是同一個人、真人與機器抓取混在一起。這些無法在設計階段推導，只有真實流量打進來才會現形。&lt;/p></description><content:encoded><![CDATA[<p>本指南處理一類具體的工程問題：手上有一個沒有後端的靜態站或小專案，需要一點點動態能力（收一筆表單、記一次瀏覽、每天彙總一次資料），但這個需求還不到、也不值得租一台伺服器的程度。核心概念是「膠水層」——用別人已經在跑、且對個人用量免費的雲端服務，補上靜態站缺的那一小塊伺服器端邏輯，讓你不必自己維護一台開機的主機。</p>
<p>靜態站的能力邊界很明確：瀏覽器把 HTML/JS 抓下來後，所有邏輯都在使用者的瀏覽器裡跑，沒有任何一段程式碼在你控制的伺服器上執行。這代表任何「要記在你這邊」的資料——誰來看過、表單填了什麼、累積計數——都需要一個瀏覽器以外、由你掌握的接收端。這個接收端不必是傳統伺服器；它可以是一段掛在免費平台上、只在被呼叫時才執行的函式。本指南教怎麼用這種函式把靜態站的能力補齊。</p>
<p>貫穿全指南的案例是「幫這個架在 GitHub Pages 上的 blog 做流量統計」。GitHub Pages 不給 access log、也不能跑伺服器端程式碼，所以流量資料只能靠瀏覽器主動回報。這個案例會從模組零的架構推導、一路實作到模組六的資料判讀，讀者跟著做完會得到一個真的能用、資料存在自己試算表裡的流量統計系統——「自己手上」在這裡指的是資料的控制權與查詢自由，基礎設施仍然是 Google 託管的。</p>
<p>第一種膠水工具選 Google Apps Script + Google Sheets：它對個人 Google 帳號免費、不需要信用卡、Sheets 直接當資料庫兼儀表板，起步門檻是所有選項裡最低的。指南後續會加入其他膠水工具（例如 Cloudflare Workers）作為對照，說明各自的適用邊界——Apps Script 適合資料量小、需要人可直接讀寫試算表的場景；當量體變大或需要低延遲時，模組五會給出換工具的判準與遷移路徑。</p>
<h2 id="教材邊界">教材邊界</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>放在本指南</th>
          <th>不放在本指南</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>心智模型</td>
          <td>靜態站能力邊界、膠水層架構、client beacon、免費額度的思考方式</td>
          <td>大型後端架構、微服務拆分</td>
      </tr>
      <tr>
          <td>工具地基</td>
          <td>Apps Script web app 部署模型（<code>doGet</code>/<code>doPost</code>）、授權模型、V8 runtime</td>
          <td>Apps Script 在 Workspace 企業版的進階整合</td>
      </tr>
      <tr>
          <td>實作</td>
          <td>beacon 前端、接收端 handler、Sheets 讀寫、觸發器排程、配額與安全</td>
          <td>商業級 analytics 平台的自建（見 <a href="/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">Monitoring</a>）</td>
      </tr>
      <tr>
          <td>資料品質</td>
          <td>訪客識別、事件模型、自動化流量辨識、靜默失效診斷</td>
          <td>跨裝置身分解析、廣告歸因、第三方資料串接</td>
      </tr>
      <tr>
          <td>選型</td>
          <td>自建 vs 現成分析服務、Apps Script vs Workers 的適用邊界、何時該換工具</td>
          <td>雲端主機比價、Kubernetes</td>
      </tr>
  </tbody>
</table>
<p>流量分析的<strong>概念層</strong>（事件分類、漏斗、cohort、歸因）不在本指南，在 <a href="/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">Monitoring 監控體系</a>。本指南是<strong>動手做</strong>的那一半：怎麼用免費工具把資料真的收進來、存起來、彙總出報表。兩者互補——先看 Monitoring 想清楚要收什麼，再回本指南把管線搭起來。</p>
<h2 id="backlog">Backlog</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>類型</th>
          <th>前置條件</th>
          <th>規模</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>雙事件配對的彙總實作（平均停留、閱讀深度）</td>
          <td>主章</td>
          <td>模組四彙總改寫成事件感知版本</td>
          <td>1 篇</td>
      </tr>
      <tr>
          <td>Cloudflare Workers 對照與遷移路徑</td>
          <td>主章</td>
          <td>實機驗證免費額度與部署流程</td>
          <td>2 篇</td>
      </tr>
      <tr>
          <td>Referrer-Policy 知識卡</td>
          <td>知識卡</td>
          <td>無（模組六已完整展開、判定它是否仍需獨立卡）</td>
          <td>1 張</td>
      </tr>
      <tr>
          <td>現成分析服務的實測對照</td>
          <td>vendor</td>
          <td>實測 Plausible / Umami / GoatCounter 的免費額度與能力</td>
          <td>1 篇</td>
      </tr>
      <tr>
          <td>Monitoring 對本指南的反向連結</td>
          <td>跨模組</td>
          <td>盤點 monitoring 各模組的實作落點</td>
          <td>小</td>
      </tr>
  </tbody>
</table>
<p>跨模組那一列是目前唯一的結構性不對稱：本指南對 monitoring 有五條引用，反向零條，而 monitoring 的漏斗與 cohort 分析都預設資料乾淨、可識別、無機器混入——那些前提正是模組六在處理的。</p>
<p>模組六補上了資料判讀，但彙總層仍停在模組四的單事件版本——那裡目前只標明了前提與修法方向，尚未有一篇把「配對進入與離開事件、算出平均停留與閱讀深度」實作出來。順序上它依賴模組六的事件模型定案，因此排在其後。</p>
<h2 id="章節">章節</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/automation/00-mental-model/" data-link-title="模組零：心智模型" data-link-desc="判斷一個動態需求要不要伺服器、資料從哪個接收端進來、免費額度撐得住多大量時的思考框架">模組零：心智模型</a></td>
          <td>靜態站能力邊界、膠水層、client beacon 架構、免費額度、自建 vs 現成服務、GAS vs Workers 選型</td>
      </tr>
      <tr>
          <td><a href="/blog/automation/01-apps-script-basics/" data-link-title="模組一：Apps Script 地基" data-link-desc="搞懂 Apps Script 的 web app 部署模型與授權模型，才不會在做 beacon 時卡在網址打不通或權限被擋">模組一：Apps Script 地基</a></td>
          <td>Apps Script 是什麼、V8 runtime、web app 部署模型、授權模型、跟一般伺服器的差異</td>
      </tr>
      <tr>
          <td><a href="/blog/automation/02-analytics-beacon/" data-link-title="模組二：流量 beacon 實作" data-link-desc="把「頁面被看了就送一則事件、接收端寫進 Sheet」從零做到收到第一筆真實瀏覽紀錄時的完整實作">模組二：流量 beacon 實作</a></td>
          <td>前端 beacon、接收端 handler、寫進 Sheet、CORS 的雷與解法，從零到第一筆</td>
      </tr>
      <tr>
          <td><a href="/blog/automation/03-sheet-as-database/" data-link-title="模組三：Sheets 當資料庫" data-link-desc="用 Google Sheet 存流量資料時，怎麼處理多個 beacon 同時寫入的並發、設計資料模型、以及判斷資料量到哪會撐不住">模組三：Sheets 當資料庫</a></td>
          <td><code>appendRow</code>、資料模型、並發與 <code>LockService</code>、Sheets 的容量邊界</td>
      </tr>
      <tr>
          <td><a href="/blog/automation/04-triggers-automation/" data-link-title="模組四：觸發器與排程" data-link-desc="用 Apps Script 的時間觸發器把累積的原始瀏覽 log 定時彙總成看得懂的日報，以及觸發器的每日執行配額">模組四：觸發器與排程</a></td>
          <td>time-driven trigger 每日彙總、<code>onFormSubmit</code>、把原始 log 變成日報</td>
      </tr>
      <tr>
          <td><a href="/blog/automation/05-deploy-quota-security/" data-link-title="模組五：部署、配額與安全" data-link-desc="把匿名可存取的 beacon 接收端上線後，怎麼守住免費配額、擋掉濫用、保持資料乾淨、以及判斷何時該換更重的工具">模組五：部署、配額與安全</a></td>
          <td>部署權限、免費配額上限、CORS 設定、防濫用、隱私邊界與同意機制</td>
      </tr>
      <tr>
          <td><a href="/blog/automation/06-reading-the-data/" data-link-title="模組六：收到資料之後" data-link-desc="流量統計上線、資料開始累積，但欄位讀不出「這是誰、是不是同一個人、是不是機器」時的判讀與補強">模組六：收到資料之後</a></td>
          <td>訪客識別與 opt-out、事件模型與停留時間、辨識自動化流量、假故障與靜默失效診斷</td>
      </tr>
  </tbody>
</table>
<p>模組零到五走的是「怎麼把管線接起來」，那條路徑上的每個障礙都能從文件讀出來。模組六的位置不同——它處理管線接好、資料真的開始累積之後才浮現的問題：來源網址大量空白、任兩列之間看不出是不是同一個人、真人與機器抓取混在一起。這些無法在設計階段推導，只有真實流量打進來才會現形。</p>
<h2 id="讀者旅程">讀者旅程</h2>
<p>想直接把流量統計做出來：模組零建立架構直覺後，跳模組二照著實作，缺概念再回模組一補。想完整理解這套膠水模式、之後套用到其他專案：模組零到六順讀。已經收到資料、正要開始看報表：直接進<a href="/blog/automation/06-reading-the-data/" data-link-title="模組六：收到資料之後" data-link-desc="流量統計上線、資料開始累積，但欄位讀不出「這是誰、是不是同一個人、是不是機器」時的判讀與補強">模組六</a>。只想查某個 Apps Script 術語：看 <a href="/blog/automation/knowledge-cards/" data-link-title="Automation 知識卡" data-link-desc="免伺服器自動化的術語索引：beacon、web app 部署、doGet/doPost、執行配額、時間觸發器、瀏覽器指紋、執行項目">knowledge-cards</a>。</p>
<hr>
]]></content:encoded></item></channel></rss>