【アプリ評価★5を増やす機能】ヘビーユーザにだけレビュー依頼

3日間に10回スマホアプリを起動した人をヘビーユーザと認識して、ヘビーユーザにだけアプリのレビュー依頼した結果90%が星5評価をつけたという事例がAMLから発表されました。目から鱗だったので該当機能を実装してみました。

元ネタはなんでそんな可愛い絵ばっかり書けると話題のAMLの記事[アプリ評価が★２→★４に改善されると、ダウンロード率が5.4倍に。アプリのレビューが与える影響と、レビュー改善2つの成功事例](http://appmarketinglabo.net/appstore-review/)です。最初に「楽しんでる？」とユーザに尋ねるダイアログを表示するのは、簡単ですが効果的な良い方法だと思います。

■ 元記事から画像転載
![s5.png](https://qiita-image-store.s3.amazonaws.com/0/65312/6b50a54a-7712-99f9-8d0e-47a5626c2db6.png)

# 仕様
たぶんこんな仕様になるんじゃないかなー

1. アプリ起動回数はサーバで記録(レビューしたら報酬付与するため)
2. 3日間に10回以上起動した人にだけレビュー依頼ダイアログを表示
3. 1度レビュー依頼を出したら24時間レビュー依頼権利をユーザが得る(レビューしたら報酬付与するため)
4. 起動回数の記録にredis使ってみた

# ファイル構成とインストール
```shell-session:redisパッケージのインストール
pip install redis
```

■ ファイル構成
![スクリーンショット 2015-11-25 19.04.37.png](https://qiita-image-store.s3.amazonaws.com/0/65312/10086775-795c-137f-7d4a-a439afeb9641.png)

## 使い方
```py: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()

```

## 主ロジック
```py: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)

```

## テストコード
```py: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

```

```shell-session:テスト実行結果
>>>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 =============================================================================
```