作為軟體工程師,開發功能通常是日常工作的一大重點,但一個功能不太可能在工程師一做好後,就直接丟給使用者,通常還會經過許多測試。
對大部分開發流程而言,應該至少會區分成兩種環境:生產(Production)與測試(Staging);依照不同的流程,可能還會有更多。區分生產環境與測試環境,可以有效避免我們把有問題的功能直接送到使用者手上。
然而,單靠這些環境可能不足以處理所有的測試需求。
最直接的例子就是錯誤率。如果只有團隊內部進行測試,樣本數可能小到難以讓某些問題浮現。有時候接入第三方服務的功能更是如此,即使對方有提供沙箱(Sandbox)環境,也難保切換到正式環境後不會出現其他問題。這些問題往往要等功能真的進入生產環境後,才有機會被注意到。
那麼,有沒有辦法讓我們即使把程式碼部署到生產環境,也可以控制某個功能要不要啟用呢?
很直覺地,我們可以透過這樣的程式碼:
const ENABLE_NEW_FUNCTION = process.env.ENABLE_NEW_FUNCTION
if (ENABLE_NEW_FUNCTION) {
// 啟用功能
}
這樣我們只需要調整環境變數,就可以控制功能是否啟用。對於 Next.js 等網站類型的應用,可能是非常直觀的方式。
然而下一步,我們可能會希望先讓內部成員測試。否則即使有這段程式碼,也只是讓開關功能變得容易一些,並沒有降低一般使用者遇到錯誤的風險。
於是,程式碼可能變成:
const ENABLE_NEW_FUNCTION_TO_ALL = process.env.ENABLE_NEW_FUNCTION_TO_ALL
const testUserIds = [...]
if (
ENABLE_NEW_FUNCTION_TO_ALL ||
testUserIds.includes(currentUser.id)
) {
// 啟用功能
}
當要對所有使用者正式發布時,一樣只需要變更環境變數即可,也保留了緊急關閉的彈性。
這或許足以涵蓋多數場景。然而隨著產品成長,越來越多奇怪的發布規則就會開始出現:
想要只對手機使用者發布,電腦先不要?於是多出了 User Agent 的判斷。
想要只對台灣發布,其他國家先不要?於是多出了國家的判斷。
想要先對 10% 的使用者發布?於是又多出了依照 User ID 分組的判斷。
const ENABLE_NEW_FUNCTION_TO_ALL = process.env.ENABLE_NEW_FUNCTION_TO_ALL
const testUserIds = [...]
if (
testUserIds.includes(currentUser.id) ||
(
isMobile(request) &&
getCountry(request) === 'TW' &&
currentUser.id % 10 === 0
)
) {
// 啟用功能
}
而每當我們想調整發布策略時,又可能需要修改設定、重新啟動服務,甚至重新建置與佈署一次,勢必會帶來一定程度的麻煩。
因此,我們可以利用功能旗標 Feature Flag,把這個開關收到 PostHog 當中,變成:
if (posthog.isFeatureEnabled('new-function')) {
// 啟用功能
}
這其實也把原本綁在一起的兩件事情拆開:部署程式碼(Deploy)與發布功能(Release)不一定要同時發生。
我們可以先把包含新功能的程式碼部署到 Production,再決定功能要在什麼時候、對哪些使用者啟用。而當需要調整發布策略時,也只需要在 PostHog 後台修改即可。
要使用這項功能,我們需要先建立一個旗標。

Flag key 就是上面程式碼中的 new-function,右下方的 Release conditions 則是控制發布策略的地方。

Rollout percentage 可以讓我們控制要發布給多少比例的使用者。PostHog 會利用 Flag Key 與使用者的 distinct_id 進行雜湊,再根據結果決定該使用者是否落在發布範圍內。
因此,在 Flag 設定與 distinct_id 維持一致的情況下,同一個使用者會得到穩定的結果,也減少了我們自己實作分組邏輯的麻煩。
你可能也注意到了 Payload。這個欄位允許我們在 Feature Flag 的結果中附帶額外資料,例如:
const flag = posthog.getFeatureFlagResult('new-function')
if (flag?.enabled) {
newFunction(flag.payload)
}
如此一來,Feature Flag 就不只能決定「開」或「關」,還可以把設定值一起傳給應用程式,藉此達到比單純控制開關更複雜的操作。
Multivariate 將於後面的 Experiment 結合另外一個功能,因此在此先跳過。
Remote Config 則與 Boolean Flag 有些不同,它比較接近「不用重新部署,就可以從 PostHog 提供應用程式設定」。一般的 Remote Config 可以提供 Payload,如果需要使用加密的 Remote Config,則必須從 Server-side 取得,不能將相關憑證暴露在前端。
例如:
const posthog = new PostHog(
'phc_...',
{
host: 'https://us.i.posthog.com',
personalApiKey: process.env.POSTHOG_PERSONAL_API_KEY,
}
)
實際使用時,這類 Key 必須留在 Server-side,不應該放進瀏覽器或其他公開環境。
到這裡,我們已經可以更有彈性地控制一個功能要在什麼時候、對哪些使用者啟用。
然而,Feature Flag 的價值還不只在降低發布風險。
先前我們花了不少時間討論「使用者成功」。當我們修改一個功能時,真正想知道的通常也不只是「新功能有沒有正常運作」,而是:這個改動有沒有真的幫助使用者更成功?
利用 Feature Flag,我們可以透過 Release conditions 逐步釋放功能給部分使用者,再利用前面介紹的產品分析 Product Analytics 觀察相關信號。例如透過 Breakdown 比較有啟用與沒有啟用功能的使用者之間,是否出現了不同的結果。
不過,看到兩群使用者的數據有所不同,還不能直接證明這個改動就是造成差異的原因。
尤其當 Release conditions 本身就選擇了不同類型的使用者,例如只對特定國家、方案或使用者群體發布時,兩群人原本就可能存在差異。
而當我們想進一步測試互斥的改動,例如「聊天清單」和「短影音」哪一種首頁更能幫助使用者成功時,只靠一般的 Feature Flag 就開始變得不太方便。
不過在進入實驗以前,還有一個更前面的問題:我們為什麼認為這個改動會讓產品變得更好?
下一篇先從假設驅動開發(Hypothesis-Driven Development)開始,把我們想改善的問題與預期結果說清楚;之後再回到 Experiment,看怎麼更有系統地驗證這個假設。
如果你願意花 30 秒留下回饋,我會用這些意見來調整後續文章:分享你的意見
