3日間に10回スマホアプリを起動した人をヘビーユーザと認識して、ヘビーユーザにだけアプリのレビュー依頼した結果90%が星5評価をつけたという事例がAMLから発表されました。目から鱗だったので該当機能を実装してみました。
元ネタはなんでそんな可愛い絵ばっかり書けると話題のAMLの記事アプリ評価が★2→★4に改善されると、ダウンロード率が5.4倍に。アプリのレビューが与える影響と、レビュー改善2つの成功事例です。最初に「楽しんでる?」とユーザに尋ねるダイアログを表示するのは、簡単ですが効果的な良い方法だと思います。
仕様
たぶんこんな仕様になるんじゃないかなー
- アプリ起動回数はサーバで記録(レビューしたら報酬付与するため)
- 3日間に10回以上起動した人にだけレビュー依頼ダイアログを表示
- 1度レビュー依頼を出したら24時間レビュー依頼権利をユーザが得る(レビューしたら報酬付与するため)
- 起動回数の記録にredis使ってみた
ファイル構成とインストール
redisパッケージのインストール
pip install redis
使い方
main.py
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from review.review import ReviewManager
USER_ID = "B00001"
manager = ReviewManager(USER_ID)
# 権利を持っているか確認
manager.can_review()
# アプリ起動を記録
manager.app_startup()
主ロジック
review.py
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import datetime
from pip.utils import cached_property
import redis
PREFIX = 'REVIEW'
# 3日間のうちに10回起動すればレビュー機能が有効
STARTUP_SPAN = 3600 * 24 * 3 # 3days
STARTUP_COUNT = 10
STARTUP_MAX_COUNT = 100
# レビュー機能が有効になったら24時間有効
EXPIRE_REVIEW_RIGHTS = 3600 * 24 * 1 # 1days
# レビューする権利 string型
KEY_REVIEW = '%s:REVIEW:{user_id}' % PREFIX
# アプリを起動した履歴 list型
KEY_STARTUP_HISTORY = '%s:STARTUP-HISTORY:{user_id}' % PREFIX
def get_unix_time(now=None):
"""
UnixTimeを返却
:rtype : int
"""
if now:
return now.strftime('%s')
return datetime.datetime.now().strftime('%s')
def unix_time_to_datetime(unix_time):
"""
UnixTimeをdatetimeに変換
:rtype : datetime
"""
return datetime.datetime.fromtimestamp(float(unix_time))
class ReviewManager(object):
def __init__(self, user_id):
self.user_id = user_id
@cached_property
def storage(self):
"""
:rtype : Storage
"""
return Storage(self.user_id)
def can_review(self):
"""
3日間のうちに10回以上起動していてレビューできる権利を持つ
ヘビーユーザならTrueを返却
:rtype : bool
"""
startup_history = self.storage.get_range_startup_history(20)
now = datetime.datetime.now()
startup_count = sum([self._within(ut, now=now) for ut in startup_history])
return startup_count >= STARTUP_COUNT
def _within(self, unix_time, now=None):
"""
対象のunix_timeが3日以内の日付であれば次の値を返却
期間内なら1
期間外なら0
"""
if now is None:
now = datetime.datetime.now()
point_time = get_unix_time(now=now - datetime.timedelta(seconds=STARTUP_SPAN))
if point_time <= unix_time:
return 1
return 0
def app_startup(self):
"""
アプリの起動を記録
:rtype : None
"""
# アプリ起動を記録
self.storage.push_startup_history()
# もし直近3日間に10回起動していたらレビュー権利期間を延長
if self.can_review():
self.storage.set_review()
self.storage.touch_review()
return
def _debug_reset(self):
self.storage.client.delete(self.storage.key_review)
self.storage.client.delete(self.storage.key_startup_history)
class Storage(object):
_cli = None
def __init__(self, user_id):
self.user_id = user_id
@property
def client(self):
"""
Redisのコネクションを返却
:rtype : Redis
"""
if Storage._cli is None:
Storage._cli = redis.Redis(host='localhost', port=6379, db=3)
return Storage._cli
@property
def key_review(self):
"""
:rtype : str
"""
return KEY_REVIEW.format(user_id=self.user_id)
@property
def key_startup_history(self):
"""
:rtype : str
"""
return KEY_STARTUP_HISTORY.format(user_id=self.user_id)
def touch(self, key, expire):
self.client.expire(key, expire)
def touch_review(self):
"""
レビュー権利を延長
"""
self.touch(self.key_review, EXPIRE_REVIEW_RIGHTS)
def touch_startup_history(self):
"""
アプリ起動履歴の保存期間を延長
"""
self.touch(self.key_startup_history, STARTUP_SPAN)
def set_review(self):
"""
レビュー権利を付与
"""
self.client.set(self.key_review, 1)
self.touch_review()
def push_startup_history(self):
"""
アプリ起動時間をUnixTimeで記録する。
起動履歴が{STARTUP_MAX_COUNT}のときは
アプリ起動履歴を最新の10件を残して消す。
"""
# redisに起動時間のUnixTimeを記録
self.client.lpush(self.key_startup_history, get_unix_time())
self.touch_review()
# 起動履歴が{STARTUP_MAX_COUNT}のときはアプリ起動履歴を最新の10件を残して消す。
count = self.client.llen(self.key_startup_history)
if STARTUP_MAX_COUNT >= count:
self.client.ltrim(self.key_startup_history, (STARTUP_MAX_COUNT - 11) * -1, -1)
def get_range_startup_history(self, count):
"""
アプリ起動時間をN件取得
:rtype : list[int]
"""
return self.client.lrange(self.key_startup_history, 0, count - 1)
テストコード
tests.py
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import datetime
from .review.review import Storage, get_unix_time, unix_time_to_datetime, ReviewManager
USER_ID = "A00001"
USER_ID2 = "A00002"
def test_storage():
s = Storage(USER_ID)
s.client
assert USER_ID in s.key_review
assert USER_ID in s.key_startup_history
s.touch_review()
s.touch_startup_history()
def test_unix_time():
# unix_timeの変換の誤差が5秒以下であることの試験
now = datetime.datetime.now()
unix_time = get_unix_time()
print unix_time
assert type(unix_time) == str
float(unix_time)
convert_unix_time = unix_time_to_datetime(unix_time)
print now - convert_unix_time, convert_unix_time
_delta = now - convert_unix_time
assert _delta.seconds < 5
def test_manager():
manager = ReviewManager(USER_ID)
manager_user2 = ReviewManager(USER_ID2)
# 全リセット
manager._debug_reset()
manager_user2._debug_reset()
# 1度も起動していない
assert manager.can_review() is False
# 1回起動
manager.app_startup()
assert manager.can_review() is False
# 追加で8回起動(計9回)
for x in xrange(8):
manager.app_startup()
assert manager.can_review() is False
# 10回目の起動
manager.app_startup()
assert manager.can_review() is True
# 11回目の起動
manager.app_startup()
assert manager.can_review() is True
# 追加で200回起動(計211回)
for x in xrange(200):
manager.app_startup()
assert manager.can_review() is True
# 起動履歴がtrimされて100件以下になっていること
ct = manager.storage.client.llen(manager.storage.key_startup_history)
assert ct <= 100
# ユーザ1と2が混ざっていないこと
assert manager_user2.can_review() is False
for x in xrange(10):
manager_user2.app_startup()
assert manager_user2.can_review() is True
テスト実行結果
>>>py.test ./tests.py
=============================================================================== test session starts ===============================================================================
platform darwin -- Python 2.7.5, pytest-2.8.3, py-1.4.30, pluggy-0.3.1
rootdir: /Users/ikeda/punk/qiita/heavy_user, inifile:
collected 3 items
tests.py ...
============================================================================ 3 passed in 0.50 seconds =============================================================================