對(duì)于一個(gè)人數(shù)30-50的創(chuàng)業(yè)團(tuán)隊(duì)來說,如何讓每個(gè)成員及時(shí)了解產(chǎn)品的變化是一個(gè)有意思的話題
為什么讓產(chǎn)品信息共享?
因?yàn)椋瑘F(tuán)隊(duì)不同的 team 的工作側(cè)重點(diǎn)各有不同:
- Support Team 關(guān)注用戶使用產(chǎn)品時(shí)出現(xiàn)的問題,及時(shí)掌握產(chǎn)品變化,可以讓 Support Team 的工作有預(yù)先準(zhǔn)備,給用戶提供最新指導(dǎo),工作效率更高。
- Growth Team 關(guān)注產(chǎn)品的價(jià)值,舊功能的優(yōu)化和新功能的上線都會(huì)影響他們的決策,例如 A/B Test,QA 等。
- Develop Team 關(guān)注產(chǎn)品的功能實(shí)現(xiàn),別人代碼的更新可能會(huì)影響自己的開發(fā)。
“變化”的分類:
- 大變化:可以理解為milestone (里程碑),比如大功能的上線。
- 這種大的變化適合專門的文檔去記錄,比如產(chǎn)品更新Blog:簡單明了的文字、截圖、視頻教程等。
- 小變化:
- 用戶看得見的變化:小的UI優(yōu)化,Bug修復(fù)。
- 用戶體驗(yàn)的變化:性能的優(yōu)化(更加流暢),交互的變化(更加合理)。
解決思路
第一個(gè)問題,如何搜集變化?
- 方案一:成員每日寫工作總結(jié)。
- 根據(jù)工作內(nèi)容抽離出“變化”。
- 有個(gè)成員專門去維護(hù)一個(gè)或一系列文檔。
- 優(yōu)點(diǎn):系統(tǒng)性強(qiáng)。
- 缺點(diǎn):人力成本高,適合有一定規(guī)模的團(tuán)隊(duì)。
- 方案二:可以維護(hù)一個(gè) wiki。
- 每個(gè)成員主動(dòng)將“變化”寫入文檔中去。
- 優(yōu)點(diǎn):調(diào)動(dòng)成員的積極性。
- 缺點(diǎn):
- 需要一些工具,如文檔協(xié)作,存儲(chǔ)與檢索。
- 需要一些規(guī)范和 review。
- 方案三:利用第三方工具 (github) 自動(dòng)搜集。
- 檢索 Pull Request 中詳細(xì)描述這次PR的內(nèi)容,搜集起來。
- 優(yōu)點(diǎn):節(jié)省人力,有利于PR規(guī)范化。
- 缺點(diǎn):需要相關(guān)開發(fā)工作。
我們 Strikingly 崇尚敏捷,高效和自動(dòng)化。采用了方案三。Develop Team 使用 Pull Request 模板,每天將特殊的 Pull Request 搜集起來。
第二個(gè)問題,如何通知每個(gè)成員?
- 發(fā)消息:“去看文檔的更新”
- 優(yōu)點(diǎn):可以融入我們的日常交流中去。
- 缺點(diǎn):消息流很容易丟失在其他討論中
- 發(fā)郵件:將產(chǎn)品的“變化”寫進(jìn)郵件里面。
- 優(yōu)點(diǎn):比較正式,可自定義HTML,樣式可控制,便于給歸檔。
- 缺點(diǎn):需要相關(guān)開發(fā)工作。
我們決定采用郵件的方式,每天發(fā)送一個(gè)以郵件的形式通知團(tuán)隊(duì)的每個(gè)人。
結(jié)合起來后大致分成7個(gè)步驟:
- 設(shè)置Github
- 約定PR模板
- 記錄部署時(shí)間
- 處理 github webhook
- 數(shù)據(jù)存儲(chǔ)到 Redis
- 準(zhǔn)備郵件
- 循環(huán)發(fā)郵件
開發(fā)配置相關(guān):
- Rails 后端
- Redis 臨時(shí)存儲(chǔ)
- github 的 webhook 服務(wù)
- 用到的一些 gem:
- git_api: 訪問 github API
- maruku: 將 Markdown 轉(zhuǎn)換為 HTML
- sidekiq/sidetiq: 后臺(tái)循環(huán)發(fā)送郵件
Step 1: 設(shè)置 Github
- 項(xiàng)目的 settings 中設(shè)置 webhook
- 設(shè)置 Payload URL
- 設(shè)置 events 類型:勾選 Pull Request
- 添加 Personal access token
- 使用 gem github_api, 添加配置文件
Step 2: 約定 PR 模板
示例 PR 模板:

pr_template.png
模板的重要性:
- 規(guī)范 Develop Team 的開發(fā)流程。
- 如果不使用模板,我們開發(fā)的這個(gè)工具,沒有任何卵用O__O"…
Step 3: 記錄部署時(shí)間
由于有些PR雖然被merge了,但并不一定被部署了,所以我們需要將最近一次部署的時(shí)間記錄下來。
class DeployLogger
DEPLOY_KEY = 'AWESOME_REPO_LAST_DEPLOY_KEY'
# 根據(jù)實(shí)際情況使用默認(rèn)時(shí)間
def self.deploy_at
$redis.get(DEPLOY_KEY) || '2015-08-10T02:27:38Z'
end
def self.deploy_at= time
$redis.set DEPLOY_KEY, time
$redis.expire DEPLOY_KEY, 60.days
end
end
Step 4: 處理 github webhook
- 獲取 PR 的 Number
- 因?yàn)槲覀儾⒉荒芟嘈?webhook,所以我們應(yīng)該基于 webhook 的信息主動(dòng)通過 github API 獲取數(shù)據(jù)。
- 篩選PR: merged 到指定分支(develop) 且含有 - [x] contains user facing changes
- 整合信息,提取出 PR 的Number,title,body,獲取 PR 提交username等。
Step 5: 存儲(chǔ)到 Redis
class PrDescription
LAST_SEND_EMAIL_KEY = 'LAST_SEND_EMAIL_KEY'
# 使用 redis ordered set
# merged_at 時(shí)間秒數(shù)作為 score
def self.create columns = {}
key_word = columns[:key_word]
merged_at = columns[:merged_at]
content = columns[:content]
$redis.zadd key_word, Time.parse(merged_at).to_i, content
$redis.expire key_word, 2.days
end
def self.count_of key_word
$redis.zcount key_word, '-inf', '+inf'
end
# 根據(jù)情況做適當(dāng)調(diào)整
def self.last_send_email_at
$redis.get(LAST_SEND_EMAIL_KEY) || '2015-08-10T02:27:38Z'
end
# 根據(jù)情況做適當(dāng)調(diào)整
def self.last_send_email_at= time
$redis.set LAST_SEND_EMAIL_KEY, time
$redis.expire LAST_SEND_EMAIL_KEY, 5.days
end
# 獲取當(dāng)前部署好的PR
# 范圍:最近的一次發(fā)郵件的時(shí)間到最近的一次部署時(shí)間
def self.descriptions_need_to_send_for key_word
from_time_score = Time.parse(self.last_send_email_at).to_i + 0.01
to_time_score = Time.parse(DeployLogger.deploy_at).to_i
$redis.zrangebyscore key_word, from_time_score, to_time_score
end
end
Step 6: 準(zhǔn)備郵件
# 在每個(gè) PrDescription 之間要用"\n"隔開,注意是雙引號(hào),需要轉(zhuǎn)義,不能加空格,會(huì)影響 Markdown 的轉(zhuǎn)化
markdown_string = PrDescription.descriptions_need_to_send_for(USER_FACING_CHANGES_KEY).join("\n")
if markdown_string.present?
# 使用gem maruku 將Markdown
# 有些小問題:
# 1. 需要將 " 轉(zhuǎn)換成 '
# 2. 去除 '\n'
@content = Maruku.new(markdown_string).to_html.gsub(/\"/,"'").gsub(/\n/, '').html_safe
mail subject: "User Facing Changes #{Date.today}", from: PRODUCT_EMAIL, to: TEAM_EMAIL
# 設(shè)置最后一次發(fā)郵件的時(shí)間
PrDescription.last_send_email_at = DeployLogger.deploy_at
end
Step 7: 循環(huán)發(fā)郵件
# 用到 gem: sidekiq 和 sidetiq
class SendToSupportTeamChangesEmail
include Sidekiq::Worker
include Sidetiq::Schedulable
sidekiq_options :queue => :default, :backtrace => true, :retry => 2
# everyday 8:30 AM
# 請(qǐng)注意時(shí)區(qū),可能需要轉(zhuǎn)換
recurrence { daily.hour_of_day(8).minute_of_hour(30) }
def perform
# 負(fù)責(zé)發(fā)送內(nèi)部郵件的類
InternalMailer.user_facing_change_email.deliver
end
end
最后,如果今天有PR merged 到 develop 分支并且還被部署到 production 環(huán)境,我們明天一早會(huì)受到一封郵件:
這方面的探索還在繼續(xù),思路和代碼還有可多地方可以提升,請(qǐng)各位多多指教。