MAU, DAU も計測できるようになった redis-objects-daily-counter v0.3.0
前回のブログで redis-objects-daily-counter という gem を作ったという話をしました。
今回は v0.3.0
をリリースしたのでそちらのご紹介です。
【おさらい】redis-objects-daily-counter はどういう gem なのか
redis-objects gem の拡張として作られており、日付の変更毎に新しい counter を定義してくれる daily_counter
機能を提供する gem です。
例えば以下のような Homepage
という class で daily_counter :pv
という定義をします。
class Homepage include Redis::Objects daily_counter :pv, expireat: -> { Time.now + 2_678_400 } # about a month def id 1 end end
すると以下のように #pv
というインスタンスメソッドが使えるようになります。
これは redis-objects
gem の counter
の拡張であり、 #increment
や #decrement
というメソッドで Redis 上の値を更新することが可能です。
# 2021-04-01 homepage = Homepage.new homepage.id # 1 homepage.pv.increment homepage.pv.increment homepage.pv.increment puts homepage.pv.value # 3
daily_counter
が counter
とどのように違うかというと、日付が変わるとRedis 上の Key が変化するという点です。
# 2021-04-02 (next day) puts homepage.pv.value # 0 homepage.pv.increment homepage.pv.increment puts homepage.pv.value # 2
これは、 daily_counter
が以下のようなルールに則って Redis の Key にアクセスするように振る舞うためです。
model_name:id:field_name:yyyy-mm-dd
なお、この時のタイムゾーンは実行中の Ruby プロセスの標準タイムゾーンに従いますが、 Rails 上で実行した場合は application.rb
で指定したタイムゾーンに従います。
日付が変わった後も、前日までの Key-Value は削除していないので、 #range
や #[]
や #at
というメソッドでアクセスすることが可能です。
ちなみに #range
や #[]
では値が返却されますが、 #at
では Redis::Counter
インスタンスが返却されます。
v0.2.0
までは #[]
と #at
は同じ挙動でしたが、 v0.3.0
からそういう実装に変わりました 🙏
# 2021-04-01 homepage.pv.increment(3) # 2021-04-02 (next day) homepage.pv.increment(2) # 2021-04-03 (next day) homepage.pv.increment(5) homepage.pv[Date.new(2021, 4, 1)] # => 3 homepage.pv[Date.new(2021, 4, 1), 3] # => [3, 2, 5] homepage.pv[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => [3, 2] homepage.pv.delete_at(Date.new(2021, 4, 1)) homepage.pv.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => [0, 2, 5] homepage.pv.at(Date.new(2021, 4, 2)) # => #<Redis::Counter key="homepage:1:pv:2021-04-02"> homepage.pv.at(Date.new(2021, 4, 2)).value # 2
v0.3.0 で追加された機能とは
daily_counter
に加えて daily_set
が追加されました。
こちらは redis-objects
gem の set
に daily-
機能を追加したものになります。
class Homepage include Redis::Objects daily_set :dau, expireat: -> { Time.now + 2_678_400 } # about a month def id 1 end end homepage.dau << 'user1' homepage.dau << 'user2' homepage.dau << 'user1' # dup ignored puts homepage.dau.members # ['user1', 'user2'] puts homepage.dau.length # 2 puts homepage.dau.count # alias of #length
set
は重複を受け付けない Array
と考えて良いと思います。
上記の例を見てもらうと分かりますが user1
を 2 回追加しても #members
による取得結果には 1 つしか含まれていません。
これを使って何が実現できるかというと、 DAU (Daily Active User) の計測です。
ログインしたユーザーの ID を daily_set
で定義した変数に追加していくことで、その日アクティブだったユーザー一覧とその数を知ることが出来ます。
# 2021-04-01 homepage.dau.merge('user1', 'user2') puts homepage.dau.members # ['user1', 'user2'] # 2021-04-02 (next day) puts homepage.dau.members # [] homepage.dau.merge('user2', 'user3') puts homepage.dau.members # ['user2', 'user3'] # 2021-04-03 (next day) homepage.dau.merge('user4') homepage.dau.at(Date.new(2021, 4, 2)) # => #<Redis::Set key="homepage:1:dau:2021-04-02"> homepage.dau.at(Date.new(2021, 4, 2)).members # => ['user2', 'user3'] homepage.dau.at(Date.new(2021, 4, 2)).length # => 2
#range
や #[]
を使う事で指定した期間内の値を結合することも出来ます。
これによって DAU 目的で記録した値から MAU を求めることも可能です。
homepage.dau[Date.new(2021, 4, 1)] # => ['user1', 'user2'] homepage.dau[Date.new(2021, 4, 1), 3] # => ['user1', 'user2', 'user3', 'user4'] homepage.dau[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => ['user1', 'user2', 'user3'] homepage.dau.delete_at(Date.new(2021, 4, 1)) homepage.dau.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => ['user2', 'user3', 'user4']
#at
で一日ずつ値を取得して Ruby の Array#uniq
を使っても同じ結果が得られますが、 #range
や #[]
の方が内部で Redis の SUNION
を利用するため高速だと思います。
ちなみに #range
や #[]
の結果は Ruby の Array なので、 #length
を使って長さを求める際は実行中のマシンリソースを消費します。
一方 #at
では Redis::Set
を返しており、 Redis::Set#length
は内部で Redis の SCARD
を実行するため、 #length
は #at
からの方が高速だと思います。
ただし一定期間の値の結合は #range
や #[]
を使う必要があるので、結局マシンリソースの消費は避けられないですね。。
この辺りの仕様は実装していてかなり頭を悩ませました。なので今後のアップデートで破壊的な変更が入るかも知れません。
monthly_set
なども実装してあります
前回の記事でも紹介したような monthly_counter
に相当する monthly_set
なども使えます。
MAU, WAU, DAU で使い分けると良いかと思います。
annual_set
- Key format:
model_name:id:field_name:yyyy
- Redis is a highly volatile key-value store, so I don't recommend using it.
- Key format:
monthly_set
- Key format:
model_name:id:field_name:yyyy-mm
- Key format:
weekly_set
- Key format:
model_name:id:field_name:yyyyWw
- Key format:
daily_set
- Key format:
model_name:id:field_name:yyyy-mm-dd
- Key format:
hourly_set
- Key format:
model_name:id:field_name:yyyy-mm-ddThh
- Key format:
minutely_set
- Key format:
model_name:id:field_name:yyyy-mm-ddThh:mi
- Key format: