Rork で作った学習記録アプリの再訪が伸び悩んでいて、私はプッシュ通知を厚くしました。それまで1日1件だったものを、朝・昼・夜の3件へ。理屈はいたって素朴です。思い出す回数が増えれば、戻ってくる回数も増えるはず。
最初の数日、通知経由の起動数は確かに増えました。ダッシュボードのその数字だけを見て、私はしばらく満足していました。ところが2週間後、インストールから7日後の継続率(D7 リテンション)を週ごとに並べたとき、線が下を向いていることに気づきます。通知を増やした週のコホートだけ、D7 が 14.2% から 9.8% へ落ちていました。
戻ってきてほしくて通知を足したのに、その通知が離脱を早めていた。このアプリの収益は AdMob のインタースティシャル広告に乗っているので、D7 が落ちることは翌週の広告表示回数がそのまま減ることを意味します。再訪の数字は、私にとって手触りのある売上の数字でもありました。この記事は、その思い込みを数字で解くまでの記録です。個人開発で回している小さなアプリの話ですが、通知頻度を「感覚」ではなく「コホート」で決めるやり方は、規模を問わず効くはずです。
通知経由の起動数は、増えたように見えるだけでした
最初につまずいたのは、見ていた指標が間違っていたことでした。「通知をタップして開いた回数」は、通知を3倍送れば当然ある程度増えます。送った数が増えたのだから、タップの絶対数が増えるのは自明で、これは施策の成功を意味しません。
本当に見たかったのは、通知の有無にかかわらず「そのユーザーが1週間後も戻ってきているか」でした。通知経由の起動と、通知なしの自発的な起動を、最初から分けて記録しておくべきだったのです。
// lib/analytics.js — 起動を「きっかけ別」に記録する
// これを分けておかないと、通知の効果を過大評価する
export function trackAppOpen({ source, userId }) {
// source: 'push' | 'organic' | 'deeplink'
analytics.capture('app_open', {
userId,
source, // ← ここが後の切り分けの生命線
ts: Date.now(),
});
}
// 通知タップ時
Notifications.addNotificationResponseReceivedListener(() => {
trackAppOpen({ source: 'push', userId: currentUser.id });
});
// コールドスタート(通知以外の自発起動)
if (!launchedFromNotification) {
trackAppOpen({ source: 'organic', userId: currentUser.id });
}source を分けて記録し始めてから、絵がはっきりしました。通知経由の起動は週あたり約 1.9 倍に増えていた一方で、自発的な起動(organic)は逆に 12% 減っていたのです。通知で無理に引き戻したユーザーが、自分から開く習慣を失っていく。合計するとプラスに見えず、むしろ D7 を削っていました。
インストール週でコホートを切り、指標を並べます
「通知を増やしたら悪くなった」を主張するには、増やす前と後を同じ物差しで比べる必要があります。私はインストールした週でユーザーを束ね、コホートごとに D1・D7・D30 とオプトアウト率を並べました。
| インストール週 | 通知頻度 | D1 | D7 | D30 | 通知オプトアウト率 |
|---|---|---|---|---|---|
| 第1週(基準) | 1日1件 | 31.4% | 14.2% | 6.1% | 4.3% |
| 第2週 | 1日3件 | 30.9% | 11.6% | — | 9.7% |
| 第3週 | 1日3件 | 31.1% | 9.8% | — | 13.1% |
D1 はほとんど動いていません。通知頻度は初日の体験には効かないからで、これは筋が通っています。効いていたのは D7 とオプトアウト率でした。通知を厚くした2週は D7 が下がり続け、通知の許可を切るユーザーの割合が 4.3% から 13.1% へ跳ね上がっています。一度オプトアウトされると、そのユーザーへは二度と通知が届きません。目先の再訪と引き換えに、将来の再訪チャネルを焼いていたわけです。
コホートを切る SQL は難しくありません。イベントログさえ source 付きで貯めておけば、あとは集計するだけです。
-- インストール週ごとの D7 リテンション
-- installs: user_id, install_date
-- opens: user_id, ts, source
SELECT
date_trunc('week', i.install_date) AS cohort_week,
count(distinct i.user_id) AS installs,
count(distinct case
when o.ts::date = i.install_date + interval '7 day'
then o.user_id end) AS d7_returned,
round(100.0 * count(distinct case
when o.ts::date = i.install_date + interval '7 day'
then o.user_id end) / count(distinct i.user_id), 1) AS d7_rate
FROM installs i
LEFT JOIN opens o ON o.user_id = i.user_id
GROUP BY 1
ORDER BY 1;