お疲れ様です!IT業界で働くアライグマです!
個人開発している開発関連ニュースのキュレーションサービス「DevPick」のAndroidアプリを、Google Playの審査に出しました。結果は否承認で、理由は「ニュース&雑誌に関するポリシー」への違反でした。今回は、指摘の中身と、調べてみて分かった2つの抜け、そして修正の途中で見つけたテストの時限爆弾について書きます。
ニュースアプリとして申請したら、ポリシー違反で否承認された
DevPickは、国内外の技術メディアや企業の公式ブログから記事を集め、AIで要約して並べるサービスです。Google Play Consoleでアプリの内容を申告するとき、「ニュースアプリ」に該当するものとして回答していました。記事を集めて見せるアプリなので、自然な選択だったと思います。
ところが、この申告をすると、アプリは「ニュース&雑誌に関するポリシー」の対象になります。審査に出したビルド(versionCode 3)は、このポリシー違反として否承認されました。指摘されたのは「アプリ内エクスペリエンス」の部分で、要件として次の3つが挙げられていました。
- アプリ内に、連絡先情報だと明示したセクション(「お問い合わせ」など)があり、メールアドレスか電話番号が載っていること
- アプリに3か月以上前のコンテンツを入れないこと
- すべての記事について、提供元(執筆者または配信元)を示すこと
このうち3つ目の提供元は、記事カードと詳細画面に媒体名を、詳細画面に著者を表示済みでした。ここは対応不要です。問題は残りの2つで、どちらも「一応やっているつもり」だったのに、アプリの中を見ると守れていませんでした。
IT女子 アラ美連絡先はWebにあった。アプリの中には無かった
連絡先そのものは、審査の前から用意してありました。Google Playへの申請準備として、サポート窓口のメールアドレスを開設し、Webサイトのお問い合わせページにそのアドレスを載せていました。届いたメールを運用用のDiscordへ転送する仕組みまで作り、Play Consoleのニュースアプリの申告欄にも、Webサイトの連絡先を書いていました。
ところが、アプリのマイページには、お問い合わせもプライバシーポリシーも、Webサイトへのリンクも一切ありませんでした。審査が見ているのは「アプリ内エクスペリエンス」です。指摘の文面は「アプリ内に」連絡先のセクションがあることを求めていたので、Webに窓口があるだけでは足りず、アプリの中に導線が無いことが指摘の中心だと判断しました。
さらに調べると、ストアに載せるアプリの説明文には「ご意見・ご要望や不具合のご報告は、アプリ内の設定画面または下記サポート窓口まで」と書いていました。アプリ内の設定画面に、問い合わせの手段は無いにもかかわらずです。説明文と実装が、別々の現実を指していました。
対応として、アプリにお問い合わせ画面を新しく作りました。この画面では次のことができます。
- サポート窓口のメールアドレスを、文字として常に表示する
- メール作成を開く(開けなかった場合は案内を出す)
- Webのお問い合わせページ、プライバシーポリシー、利用規約を開く
- 掲載記事の提供元について、配信元の媒体名を表示し、全文は配信元で読む形であることを明示する
メールアドレスを文字でも出しておけば、メール作成が開けない端末でも、連絡先そのものは画面に残ります。導線は、マイページの見出しの横(スクロールしなくても見える位置)と、マイページ末尾の「お問い合わせ」セクションの2か所に置きました。あわせて、アプリ内に散らばっていたサイトのURLや連絡先の定義を1か所にまとめ、課金画面にあった重複定義もそこへ寄せています。
コードの外でやることもありました。再申請のためにversionCodeを3から4へ上げ、Play Console側ではストア掲載情報の連絡先にWebサイトとメールアドレスを設定し、ニュースアプリの申告欄の連絡先URLをお問い合わせページに更新する、という手作業を修正の一部として整理しました。



「3か月以上前の記事を出さない」は、取り込み時の対策だけでは守れていなかった
もう1つの要件、「3か月以上前のコンテンツを入れない」についても、対策はしているつもりでした。DevPickには記事の保持期間があり、既定は90日です。記事を取り込むとき、公開日時がこの保持期間より古いものは取り込まない、という判定を以前に入れていました。
この判定は、もともと審査のためではありませんでした。更新の少ないRSSには何年も前の記事が残り続けていることがあり、それが「新着」として取り込まれておすすめの上位に出てしまう、という別の不具合への対策です。結果として、古い記事は入り口で弾かれるようになっていました。
抜けていたのは、すでに取り込んだ記事が古くなっていく側です。保持期間を過ぎた記事は、定期的な削除処理で消しています。ただし、この削除には例外があります。いいねした記事、あとで読むに入れた記事、直近7日以内に既読にした記事は、利用者の記録を守るために削除しません(直近の既読を削除から守る設計は、削除前の確認だけでは直近の既読を守れなかった話で書いています)。
そして、フィード側には「公開から何日経ったか」で絞り込む条件がありませんでした。つまり、削除を免れた記事は、90日を過ぎてもそのままフィードに出続けます。単純化すると、次のような状態でした。
(単純化した例)
取り込み時 : 公開日時が90日より前 → 取り込まない
定期削除 : 90日より前の記事を消す(いいね・あとで読む・直近の既読は除く)
フィード : 経過日数の条件なし → 削除を免れた古い記事がそのまま並ぶ
入り口と後片付けの両方で古い記事を扱っていたので、利用者の目に触れる一覧にも古い記事は出ないと考えがちです。しかし、後片付けにあえて例外を作っていた以上、一覧に出る記事の範囲は、一覧の側で決めなければいけませんでした。



記事一覧からは外し、いいねの記録は残す
考えられる直し方としては、古い記事を削除処理の例外から外す、つまり「いいねしていても90日で消す」という方法もあります。ただ、それでは利用者が残したいと思って付けた記録まで消えてしまいます。ポリシーが求めているのは、アプリの一覧に古い記事を出さないことで、利用者の記録を消すことではありません。
そこで、記録は残し、一覧からだけ外すことにしました。公開日時が保持期間より古い記事を除外する絞り込みを1つ作り、記事が並ぶ場所に適用しています。
- フィード(おすすめ順・新着順)。件数の合計も、外した後の数に揃える
- 記事一覧のAPI
- 記事詳細の下に出す関連記事
- 媒体ごとの記事件数。数えたままにすると、フィードに1件も出ない媒体の絞り込みボタンが残ってしまう
一方で、いいねやあとで読むの記録、いいね一覧・あとで読む一覧・閲覧履歴、そしてそこから開く記事詳細は、これまでどおり残しました。利用者が自分で選んで残した記事を、自分の一覧から開ける状態は変えていません。
判定の境界は、取り込み側の判定とそろえました。ちょうど保持期間の境目にある記事は残し、1秒でも古ければ外します。公開日時を持たない記事は、取り込んだ日時で代わりに判定します。ここを空のまま素通りさせると、公開日時の無い古い記事だけが一覧に残り続けるからです。単純化すると、次のような条件です。
# 単純化した例(名前は実際のものではありません)
cutoff = now - timedelta(days=retention_days)
query = query.filter(
or_(
Article.published >= cutoff,
and_(Article.published.is_(None), Article.fetched >= cutoff),
)
)
「公開日時が無ければ取り込み日時」という書き方なら、1つの式にまとめる方法もあります。それでもOR条件に分けたのは、列を関数で包んでしまうと、その列に張ったインデックスが比較に使えなくなるためです。
テストでは、保持期間を1日過ぎた記事がフィードに出ないこと、件数の合計が一覧と一致すること、いいねした古い記事がフィードからは消えてもいいね一覧には残ること、詳細は引き続き開けることを確認しています。
なお、Web検索向けのサイトマップは、今回の変更では既存の判定のままにしています。



日付を固定したテストは、90日後に壊れる時限爆弾だった
一覧から古い記事を外すと、影響はテストにも出ます。既存のテストには、2026-08-05 のような固定の日付で記事を作り、フィードの並び順などを確かめているものがありました。書いた時点では新しい記事でも、実行する日が進めば、いずれ保持期間の90日を過ぎます。その日からは記事がフィードに出なくなり、テストが突然落ち始めます。コードを何も変えていないのに、日付が進むだけで壊れる、時限爆弾のようなテストです。
問題は、どのテストがこの爆弾を抱えているかを、読んで探すのが難しいことです。そこで、今回の絞り込みの保持期間だけを一時的に3日に縮めて、テストを全件実行しました。こうすると、固定の日付を使っていて近いうちに壊れるテストが、今日の時点でまとめて落ちます。落ちたテストを、実行時点からの相対日付で記事を作る形に直しました。
たとえばカスタムRSSのテストでは、モックのRSSに入れる公開日時を固定の文字列から、実行時点から何時間前かで作る形に変えています。
# 変更前(固定の日付)
<pubDate>Sun, 13 Sep 2026 12:00:00 GMT</pubDate>
# 変更後(実行時点から24時間前)
def recent_rss_pub_date(hours_ago: float) -> str:
return format_datetime(
datetime.now(timezone.utc) - timedelta(hours=hours_ago), usegmt=True
)
<pubDate>{recent_rss_pub_date(24)}</pubDate>
このカスタムRSSのモックは、今回の変更とは関係なく、取り込み時の判定だけで12月中旬には壊れる状態でした。今回の洗い出しで、別の原因で埋まっていた爆弾も一緒に見つかった形です。
一方で、境界を確かめるテストでは、あえて固定の日付を使っています。こちらは「現在時刻」も引数として固定して渡しているので、実行日が進んでも結果は変わりません。固定の日付そのものが悪いのではなく、固定の日付と本物の現在時刻を比べてしまうことが、時限爆弾の正体でした。
実は、姉妹サービスの「AgentPick」でも、以前に保持期間のテストのハードコードされた日付を、実時刻を基準にする形へ直しています。保持期間のように「今からどれだけ前か」で振る舞いが変わる機能を足すと、同じ種類の爆弾が既存のテストに埋まりやすいと感じています。
最終的に、バックエンドのテストは全件(2,415件)成功しました。



まとめ
Google Playの審査で、DevPickのアプリが「ニュース&雑誌に関するポリシー」違反として否承認されました。指摘された要件のうち、連絡先と古い記事の2つは、どちらも対策しているつもりで守れていませんでした。
連絡先はWebサイトにあったものの、アプリの中には導線がありませんでした。ストアの説明文も、問い合わせの手段が無いアプリ内の設定画面を窓口として案内していました。アプリにお問い合わせ画面を作り、マイページから開けるようにしています。
古い記事は取り込み時に弾いていたものの、いいねや直近の既読で削除を免れた記事が、90日を過ぎてもフィードに出続けていました。記録は残したまま、記事が並ぶ場所でだけ公開日時による絞り込みをかけました。その過程で、固定の日付を使ったテストが保持期間を過ぎると壊れることにも気づき、保持期間を一時的に縮めて洗い出しています。
どちらの抜けにも共通していたのは、要件を「入り口」や「別の場所」で満たしたつもりになっていたことです。ポリシーの要件は、利用者の目に触れる場所で守られているかどうかで確かめる必要があると考えています。












