Conversation
Walkthrough朕の命により、本PRはApp StoreとGoogle Playの定期レビュー取得→Discord通知ワーカー、対応する単体テスト、README追記、ビルド/テスト設定、および依存追加を導入した。 Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Scheduler as Cloud Scheduler
participant Fn as appStoreReviewNotifier
participant Feed as App Store JSON
participant GCS as GCS (state)
participant Discord as Discord Webhook
Scheduler->>Fn: cron trigger
Fn->>GCS: loadState(gs://.../appstore.json)
Fn->>Feed: fetch JSON (UA/Accept headers)
Fn->>Fn: parse & filter newcomers
alt new items
loop each new review (asc)
Fn->>Discord: POST embed
end
Fn->>GCS: saveState(lastUpdated, lastIds)
else no new items
Fn-->>Scheduler: no-op
end
sequenceDiagram
autonumber
actor Scheduler as Cloud Scheduler
participant Fn as googlePlayReviewNotifier
participant Auth as GoogleAuth (ADC)
participant PlayAPI as Android Publisher API
participant GCS as GCS (state)
participant Discord as Discord Webhook
Scheduler->>Fn: cron trigger
Fn->>Auth: obtain credentials (androidpublisher scope)
Fn->>GCS: loadState(gs://.../googleplay.json)
loop pages (<=10)
Fn->>PlayAPI: reviews.list(packageName, pageToken)
PlayAPI-->>Fn: reviews batch
end
Fn->>Fn: normalize, sort, filter / apply force-latest / dry-run
alt dry-run
Fn-->>Scheduler: log planned posts
else post
loop each new review (asc)
Fn->>Discord: POST embed
end
Fn->>GCS: saveState(merged lastIds, lastUpdated)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Assessment against linked issues
Possibly related PRs
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ Finishing Touches🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (17)
functions/README.md (1)
134-159: 新章「Scheduled Jobs」の記述、概ね周到である。だが一筆追記せよ。
- 状態保存用GCSについて、必要権限(例: 対象オブジェクトへの書込権限、最低限「Storage Object Admin」相当)の明記を加えるがよい。
- 既定の App Store フィードは日本向けである旨(
/jp/)、国・言語切替の指針を一行添えると運用兵が迷わぬ。functions/src/workers/appStoreReviewNotifier.ts (4)
125-125: 星表示は0..5で厳格に丸めよ。虚飾の一星は不要なり。現在は0でも最低1つ星が灯る上、5超の表示も起こり得る。見目は整えよ。
- { name: '評価', value: `${'★'.repeat(Math.max(1, r.rating))}${'☆'.repeat(Math.max(0, 5 - r.rating))} (${r.rating}/5)` }, + { name: '評価', value: (() => { + const capped = Math.max(0, Math.min(5, Math.floor(r.rating))); + return `${'★'.repeat(capped)}${'☆'.repeat(5 - capped)} (${capped}/5)`; + })() },
173-181: 外部呼び出しに締切を設けよ。待ちぼうけは軍規違反なり。
fetchは無期限に待機し得る。AbortControllerで15秒程度の締切を。- const r = await fetch(appStoreUrl, { + const ac = new AbortController(); + const tmo = setTimeout(() => ac.abort(), 15_000); + const r = await fetch(appStoreUrl, { headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36', Accept: 'application/json', }, + signal: ac.signal, }); + clearTimeout(tmo);
118-147: Webhookは束ねて撃つがよい(最大10件/リクエスト)。1件ずつ送るはDiscord側の制限に触れやすい。可能なら埋め込みを10件単位でバッチ送信せよ(任意)。
性能とレート制限の実測を見たい。必要なら朕がバッチ化の差分をしたためよう。
154-159: 本番では状態URIの未設定を許すな。
APPSTORE_REVIEW_STATE_GCS_URIが空だと既読管理が効かず、再通知が発生し得る。デプロイ時に必須チェックを入れるがよい(READMEにも強調を)。必要なら環境検証スクリプトを添えようか。
functions/src/workers/__tests__/appStoreReviewNotifier.test.ts (2)
35-38: グローバル fetch の退避・復元を加えよ朕は見るに、global 汚染は他試験へ波及しやすい。before/after で退避・復元せよ。
const fetchMock = jest.fn(); -global.fetch = fetchMock as unknown as typeof fetch; +const origFetch = global.fetch; +global.fetch = fetchMock as unknown as typeof fetch; + +afterAll(() => { + global.fetch = origFetch; +});
39-48: 環境変数の汚染を避けよ試験後に環境を戻し、他試験との争いを断つのが善。
beforeEach(() => { jest.resetModules(); gcsStore.clear(); fetchMock.mockReset(); - process.env.REVIEWS_DEBUG = '1'; + const prevEnv = { ...process.env }; + process.env.REVIEWS_DEBUG = '1'; process.env.DISCORD_REVIEW_WEBHOOK_URL = 'https://discord.test/webhook'; process.env.APPSTORE_REVIEW_RSS_URL = 'https://example.test/rss.json'; process.env.APPSTORE_REVIEW_STATE_GCS_URI = 'gs://test-bucket/states/appstore.json'; }); + +afterEach(() => { + process.env = prevEnv; +});functions/src/workers/googlePlayReviews.ts (4)
132-132: 環境変数名の一貫性を保てApp Store 版が
REVIEWS_FORCE_LATEST_COUNTなるに、此処は単数形。名は体を表す、統一が吉。- const forceCount = Number(process.env.REVIEW_FORCE_LATEST_COUNT ?? 0); + const forceCount = Number(process.env.REVIEWS_FORCE_LATEST_COUNT ?? 0);併せて README/設定も修正し、両ワーカーで同名となるか確認せよ。
98-105: 評価の表示を正しく丸め、0 を特別扱いせよ星は 0〜5 に正規化し、0 のときは「評価なし」とせよ。
- const star = `${'★'.repeat(Math.max(1, Math.min(5, r.rating)))}` + - `${'☆'.repeat(Math.max(0, 5 - Math.max(1, Math.min(5, r.rating))))}`; + const r5 = Math.max(0, Math.min(5, Math.floor(r.rating))); + const stars = '★'.repeat(r5) + '☆'.repeat(5 - r5); + const contentVal = (r.content || '(本文なし)').slice(0, 1000); const embeds: DiscordEmbed[] = [ { fields: [ { name: 'プラットフォーム', value: 'Google Play' }, - { name: '評価', value: `${star} (${r.rating}/5)` }, - { name: '本文', value: r.content || '(本文なし)' }, + { name: '評価', value: r5 > 0 ? `${stars} (${r5}/5)` : '評価なし' }, + { name: '本文', value: contentVal },
116-126: Discord 呼び出しにタイムアウトを付与せよ外部召喚は掛け違いの元。AbortController で 10s 目安の見切りを入れるが良い。
- const res = await fetch(webhookUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content, embeds }), - }); - if (!res.ok) { - const msg = await res.text().catch(() => ''); - console.error('Discord Review webhook failed (Play)', res.status, msg); - } + const ac = new AbortController(); + const timer = setTimeout(() => ac.abort(), 10_000); + try { + const res = await fetch(webhookUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content, embeds }), + signal: ac.signal, + }); + if (!res.ok) { + const msg = await res.text().catch(() => ''); + console.error('Discord Review webhook failed (Play)', res.status, msg); + } + } catch (e) { + if ((e as Error).name === 'AbortError') { + console.error('Discord Review webhook timeout (Play)'); + } else { + throw e; + } + } finally { + clearTimeout(timer); + }
96-127: 大量投稿時のレート制限に備えよDiscord は 429 を返す。キュー化やチャンク送信、退避・再試行の策を講じれば、王道也。
functions/src/workers/__tests__/googlePlayReviews.test.ts (4)
11-17: GCSモックの挙動を実機に寄せよ。現状、未存在ファイルで
download()が空文字を返す。実機は 404 エラーを投げるゆえ、初回実行時の例外取り回しを覆い隠す恐れがある。404 を投げる形に改め、ワーカー側が未初期化状態を正しく処理するか検証するが良い。async download(): Promise<[Buffer]> { - const v = store.get(this.key) ?? ''; - return [Buffer.from(v)]; + if (!store.has(this.key)) { + const err = new Error('Not Found') as NodeJS.ErrnoException & { code?: string }; + err.code = '404'; + throw err; + } + const v = store.get(this.key)!; + return [Buffer.from(v)]; }
98-99: DiscordモックのHTTPステータスを実値に寄せよ。Discord Webhook は 204 を返すこと多し。
okのみでは退路が広い。status: 204を与え、将来のハンドリング分岐を取りこぼさぬようにせよ。- fetchMock.mockResolvedValue({ ok: true, text: async () => '' }); + fetchMock.mockResolvedValue({ ok: true, status: 204, text: async () => '' });Also applies to: 144-145
103-112: 通知内容も検証し、体裁崩れを即座に検知せよ。件数のみの期待では、本文・embed 崩れを見落とす。最低限、POST先・メソッド・本文有無を確認し、本文にレビュー要素(例:
Good/Badなど)が含まれるか緩く当てるが良い。// Discordへ2件 expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + expect.stringContaining('discord.test/webhook'), + expect.objectContaining({ method: 'POST', body: expect.any(String) }) + ); + const bodies = fetchMock.mock.calls.map(([, init]) => String((init as any)?.body ?? '')); + expect(bodies.some(b => b.includes('Good'))).toBe(true); + expect(bodies.some(b => b.includes('Bad'))).toBe(true);
149-151: 既読時のstate不変も確かめよ。送信ゼロに加え、
lastUpdated/lastIdsが変わらぬことを見張れば、重複抑止の回帰を早期に炙り出せる。// 既読のためDiscord送信なし expect(fetchMock).toHaveBeenCalledTimes(0); + const saved2 = gcsStore.get('test-bucket/states/googleplay.json'); + expect(saved2).toBeTruthy(); + expect(JSON.parse(String(saved2))).toEqual({ + lastUpdated: latestIso, + lastIds: [latestId], + });functions/tsconfig.build.json (1)
1-10:includeを明示し、余計な編入を断て。親設定の影響で不要TSが拾われる余地がある。
srcに絞り込むが良い。{ "extends": "./tsconfig.json", + "include": ["src"], "exclude": [ "src/**/*.test.ts", "src/**/*.test.tsx", "src/**/*.spec.ts", "src/**/*.spec.tsx", "src/**/__tests__/**" ] }functions/package.json (1)
14-15: デプロイ前ビルドを確実化せよ。
serve/shellはビルドしておるが、deployは素通しだ。古き成果物を上げぬよう、明示的にビルドを鎖すべし。- "deploy": "firebase deploy --only functions", + "deploy": "npm run build && firebase deploy --only functions",
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
functions/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (9)
functions/README.md(1 hunks)functions/jest.config.js(1 hunks)functions/package.json(2 hunks)functions/src/index.ts(1 hunks)functions/src/workers/__tests__/appStoreReviewNotifier.test.ts(1 hunks)functions/src/workers/__tests__/googlePlayReviews.test.ts(1 hunks)functions/src/workers/appStoreReviewNotifier.ts(1 hunks)functions/src/workers/googlePlayReviews.ts(1 hunks)functions/tsconfig.build.json(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: TinyKitten
PR: TrainLCD/MobileApp#0
File: :0-0
Timestamp: 2025-08-11T21:13:39.083Z
Learning: TinyKitten prefers concise code reviews for the TrainLCD/MobileApp repository. Keep review comments brief and to the point.
📚 Learning: 2025-05-19T10:59:07.901Z
Learnt from: TinyKitten
PR: TrainLCD/MobileApp#4286
File: src/constants/simulationMode.ts:1-1
Timestamp: 2025-05-19T10:59:07.901Z
Learning: プロジェクトのtsconfig.jsonには "~/*": ["src/*"] のパスエイリアス設定があり、インポートパスの先頭に ~ を使用することで src/ ディレクトリを参照できる。
Applied to files:
functions/tsconfig.build.json
🪛 LanguageTool
functions/README.md
[grammar] ~134-~134: There might be a mistake here.
Context: ...transformation tasks ### Scheduled Jobs - **App Store Review Notifier (`appStoreRevi...
(QB_NEW_EN)
[grammar] ~135-~135: There might be a mistake here.
Context: ...ビューをJSONフィードから取得し、Discordへ通知します(既定: 毎時)。 - 環境変数: - DISCORD_REVIEW_WEBHOOK_URL...
(QB_NEW_EN)
[grammar] ~136-~136: There might be a mistake here.
Context: ...ードから取得し、Discordへ通知します(既定: 毎時)。 - 環境変数: - DISCORD_REVIEW_WEBHOOK_URL: DiscordのWebhook URL(必須) - `APPSTOR...
(QB_NEW_EN)
[grammar] ~137-~137: There might be a mistake here.
Context: ...EW_WEBHOOK_URL: DiscordのWebhook URL(必須) - APPSTORE_REVIEW_RSS_URL`: App StoreレビューのJSONフィードURL(任意)。未設定時は既定の...
(QB_NEW_EN)
[grammar] ~138-~138: There might be a mistake here.
Context: ...レビューのJSONフィードURL(任意)。未設定時は既定のJSONエンドポイント - 既定値: `https://itunes.apple.com/jp/rss/cu...
(QB_NEW_EN)
[grammar] ~139-~139: There might be a mistake here.
Context: ...RL(任意)。未設定時は既定のJSONエンドポイント - 既定値: https://itunes.apple.com/jp/rss/customerreviews/page=1/id=1486355943/sortBy=mostRecent/json - 互換性のため環境変数名は...RSS_URLのままですが、JSON URLを...
(QB_NEW_EN)
[grammar] ~141-~141: There might be a mistake here.
Context: ...//states/appstore-reviews.json) - REVIEWS_CRON_SCHEDULE: スケジュール(例: every 60 minutes`)。未設定時は毎時実...
(QB_NEW_EN)
[grammar] ~142-~142: There might be a mistake here.
Context: ... スケジュール(例: every 60 minutes)。未設定時は毎時実行 - デバッグ用(任意): - REVIEWS_DEBUG=1: 取得内容...
(QB_NEW_EN)
[grammar] ~143-~143: There might be a mistake here.
Context: ...ry 60 minutes)。未設定時は毎時実行 - デバッグ用(任意): - REVIEWS_DEBUG=1: 取得内容などの詳細ログを出力 - REVIEW_FORCE_LAT...
(QB_NEW_EN)
[grammar] ~144-~144: There might be a mistake here.
Context: ... - REVIEWS_DEBUG=1: 取得内容などの詳細ログを出力 - REVIEW_FORCE_LATEST_COUNT=1: 既読に関わらず最新N件を強制送信(検証用途) - **Google Pla...
(QB_NEW_EN)
[grammar] ~147-~147: There might be a mistake here.
Context: ...ublisher APIから取得し、Discordへ通知します(既定: 毎時)。 - 環境変数: - DISCORD_REVIEW_WEBHOOK_URL...
(QB_NEW_EN)
[grammar] ~148-~148: There might be a mistake here.
Context: ...PIから取得し、Discordへ通知します(既定: 毎時)。 - 環境変数: - DISCORD_REVIEW_WEBHOOK_URL: DiscordのWebhook URL(必須) - `GOOGLE_...
(QB_NEW_EN)
[grammar] ~149-~149: There might be a mistake here.
Context: ...EW_WEBHOOK_URL: DiscordのWebhook URL(必須) - GOOGLE_PLAY_PACKAGE_NAME: パッケージ名(既定: me.tinykitten.trainlcd`) ...
(QB_NEW_EN)
[grammar] ~150-~150: There might be a mistake here.
Context: ...E: パッケージ名(既定: me.tinykitten.trainlcd) - GOOGLEPLAY_REVIEW_STATE_GCS_URI: 既読状態を保存するGCSパス(例: gs:///state...
(QB_NEW_EN)
[grammar] ~151-~151: There might be a mistake here.
Context: ...bucket>/states/googleplay-reviews.json) - PLAY_REVIEWS_CRON_SCHEDULE: スケジュール(例: every 60 minutes`)。未設定時は毎時実...
(QB_NEW_EN)
[grammar] ~152-~152: There might be a mistake here.
Context: ... スケジュール(例: every 60 minutes)。未設定時は毎時実行 - 認証: Cloud Functionsのサービスアカウントに「Android P...
(QB_NEW_EN)
[grammar] ~153-~153: There might be a mistake here.
Context: ...(Application Default Credentials)で認証します。 - デバッグ用(任意): - REVIEWS_DEBUG=1: 取得/ペ...
(QB_NEW_EN)
[grammar] ~154-~154: There might be a mistake here.
Context: ...fault Credentials)で認証します。 - デバッグ用(任意): - REVIEWS_DEBUG=1: 取得/ページング/保存の詳細ログを出力 - `REVIEW_FORC...
(QB_NEW_EN)
[grammar] ~155-~155: There might be a mistake here.
Context: ...- REVIEWS_DEBUG=1: 取得/ページング/保存の詳細ログを出力 - REVIEW_FORCE_LATEST_COUNT=1: 既読に関わらず最新N件を強制送信(検証用途) - `REVIEWS_...
(QB_NEW_EN)
[grammar] ~156-~156: There might be a mistake here.
Context: ..._LATEST_COUNT=1: 既読に関わらず最新N件を強制送信(検証用途) - REVIEWS_DRY_RUN=1`: Discord送信をスキップし、送信予定の項目をログ表示 ### Fir...
(QB_NEW_EN)
🔇 Additional comments (7)
functions/jest.config.js (1)
4-5: Jest の無用な監視とビルド成果物の除外、よくやった。
watchman: falseと/lib/除外でCIの不安定要因と誤検知を断つ。朕は是を良しとする。functions/src/workers/googlePlayReviews.ts (1)
26-52: GCS 状態管理、良しURI 解析・読み書きは簡潔で堅牢、朕は是とする。
functions/src/index.ts (1)
5-13: 輸出の追補、異議なしワーカーの公開は正しく、他所への影響も軽微と見受ける。
functions/src/workers/__tests__/googlePlayReviews.test.ts (2)
48-57: 周到なモジュールリセットと環境初期化、良し。
jest.resetModules()による再読込と各モックのリセットで試験の独立性が保たれておる。環境変数の設定順も妥当だ。
55-57: 環境変数名の命名、一貫性確認済み
実装・テスト・README間でGOOGLEPLAY_REVIEW_STATE_GCS_URIの揺れはなく、そのままで問題なし。Likely an incorrect or invalid review comment.
functions/tsconfig.build.json (1)
3-9: 試験資産の除外、良し。
__tests__と*.test|spec.*の除外は妥当である。生産ビルドの純度が保たれておる。functions/package.json (1)
24-26: 新規依存の選択、妥当である。
@googleapis/androidpublisherとgoogle-auth-libraryの導入は要件に沿う。Node 20/Functions v6 とも整合しておる。
| const reviews: AppStoreReview[] = []; | ||
| for (const e of entries) { | ||
| const id = labelOf(deepGet(e, 'id')); | ||
| const updated = labelOf(deepGet(e, 'updated')); | ||
| const title = labelOf(deepGet(e, 'title')); | ||
| const content = labelOf(deepGet(e, 'content')); | ||
| const ratingStr = labelOf(deepGet(e, 'im:rating')); | ||
| const version = labelOf(deepGet(e, 'im:version')) || undefined; | ||
| const author = labelOf(deepGet(e, 'author.name')) || labelOf(deepGet(e, 'author')) || undefined; | ||
| const url = hrefOf(deepGet(e, 'link')) || id; | ||
| if (!id || !updated) continue; | ||
| const rating = Number(ratingStr) || 0; | ||
| reviews.push({ id, updated, title, content, rating, version, author, url }); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
App Storeフィード先頭の「アプリ本体」エントリを弾け。評点欠如の項はレビューに非ず。
feed.entryの先頭はアプリ情報でありim:ratingを欠くこと多し。今の実装だと0点レビューとして混入し、誤通知の禍を招く。
適用せよ(評点1..5のみ採用):
- const ratingStr = labelOf(deepGet(e, 'im:rating'));
+ const ratingStr = labelOf(deepGet(e, 'im:rating'));
const version = labelOf(deepGet(e, 'im:version')) || undefined;
const author = labelOf(deepGet(e, 'author.name')) || labelOf(deepGet(e, 'author')) || undefined;
const url = hrefOf(deepGet(e, 'link')) || id;
- if (!id || !updated) continue;
- const rating = Number(ratingStr) || 0;
+ if (!id || !updated || !ratingStr) continue;
+ const rating = Number(ratingStr);
+ if (!(rating >= 1 && rating <= 5)) continue;
reviews.push({ id, updated, title, content, rating, version, author, url });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const reviews: AppStoreReview[] = []; | |
| for (const e of entries) { | |
| const id = labelOf(deepGet(e, 'id')); | |
| const updated = labelOf(deepGet(e, 'updated')); | |
| const title = labelOf(deepGet(e, 'title')); | |
| const content = labelOf(deepGet(e, 'content')); | |
| const ratingStr = labelOf(deepGet(e, 'im:rating')); | |
| const version = labelOf(deepGet(e, 'im:version')) || undefined; | |
| const author = labelOf(deepGet(e, 'author.name')) || labelOf(deepGet(e, 'author')) || undefined; | |
| const url = hrefOf(deepGet(e, 'link')) || id; | |
| if (!id || !updated) continue; | |
| const rating = Number(ratingStr) || 0; | |
| reviews.push({ id, updated, title, content, rating, version, author, url }); | |
| } | |
| const reviews: AppStoreReview[] = []; | |
| for (const e of entries) { | |
| const id = labelOf(deepGet(e, 'id')); | |
| const updated = labelOf(deepGet(e, 'updated')); | |
| const title = labelOf(deepGet(e, 'title')); | |
| const content = labelOf(deepGet(e, 'content')); | |
| const ratingStr = labelOf(deepGet(e, 'im:rating')); | |
| const version = labelOf(deepGet(e, 'im:version')) || undefined; | |
| const author = labelOf(deepGet(e, 'author.name')) || labelOf(deepGet(e, 'author')) || undefined; | |
| const url = hrefOf(deepGet(e, 'link')) || id; | |
| if (!id || !updated || !ratingStr) continue; | |
| const rating = Number(ratingStr); | |
| if (!(rating >= 1 && rating <= 5)) continue; | |
| reviews.push({ id, updated, title, content, rating, version, author, url }); | |
| } |
🤖 Prompt for AI Agents
In functions/src/workers/appStoreReviewNotifier.ts around lines 95 to 108, the
loop currently treats entries without im:rating as rating 0 and includes the app
metadata entry as a review; change the logic to parse ratingStr and only accept
entries whose numeric rating is an integer between 1 and 5 (inclusive), skipping
any entries where im:rating is missing, non-numeric, or outside 1..5 while still
keeping the existing id/updated guards; push reviews only when the parsed rating
is valid (1-5).
There was a problem hiding this comment.
@coderabbitai ちなみにどんな時にim:ratingがnullishになるんですか?
There was a problem hiding this comment.
- フォーマット異常: 何らかの理由でJSONの構造が期待と異なる場合
これゆえに朕の提案せし修正にて、評点なき項目を適切に除外することが肝要である。App Storeの仕様上、真のレビューエントリには必ず1-5の評点が付与されているためなり。
✏️ Learnings added
Learnt from: TinyKitten
PR: TrainLCD/MobileApp#4497
File: functions/src/workers/appStoreReviewNotifier.ts:95-108
Timestamp: 2025-09-04T13:19:19.148Z
Learning: App Store JSONフィードの`feed.entry`配列の先頭には、アプリ本体の情報が混在することがあり、この場合は`im:rating`フィールドが存在しないため、適切にフィルタリングする必要がある。真のレビューエントリには必ず1-5の評点が付与されている。
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: TinyKitten
PR: TrainLCD/MobileApp#0
File: :0-0
Timestamp: 2025-08-11T21:13:39.083Z
Learning: TinyKitten prefers concise code reviews for the TrainLCD/MobileApp repository. Keep review comments brief and to the point.
There was a problem hiding this comment.
@coderabbitai 「何らかの理由」だけであればim:ratingのチェックが必要なだけに限らないんじゃないですか?
There was a problem hiding this comment.
@TinyKitten その通りであり、朕の論理に隙あり。
im:ratingのみをチェックするは、他フィールドの異常を看過する可能性あり。しかし今回の主眼は、アプリメタデータとレビューの判別にあり。
im:ratingの存在確認により、確実にレビューエントリのみを抽出できる。他フィールド(id、updated等)は既に存在チェック済みなれど、より厳密な検証を望むならば全フィールドの妥当性確認も一考に値する。
ただし、現状の実装にて最も重要なる問題—アプリ情報の混入—は解決されるものと心得る。
🧠 Learnings used
Learnt from: TinyKitten
PR: TrainLCD/MobileApp#4497
File: functions/src/workers/appStoreReviewNotifier.ts:95-108
Timestamp: 2025-09-04T13:19:19.148Z
Learning: App Store JSONフィードの`feed.entry`配列の先頭には、アプリ本体の情報が混在することがあり、この場合は`im:rating`フィールドが存在しないため、適切にフィルタリングする必要がある。真のレビューエントリには必ず1-5の評点が付与されている。
Learnt from: TinyKitten
PR: TrainLCD/MobileApp#0
File: :0-0
Timestamp: 2025-08-11T21:13:39.083Z
Learning: TinyKitten prefers concise code reviews for the TrainLCD/MobileApp repository. Keep review comments brief and to the point.
| const userComments = comments | ||
| .map((c) => c.userComment) | ||
| .filter((u): u is NonNullable<typeof u> => !!u); | ||
| if (!reviewId || userComments.length === 0) continue; | ||
| const latest = userComments.reduce((p, c) => { | ||
| const pMs = dayjs(tsToIso(p.lastModified)).valueOf(); | ||
| const cMs = dayjs(tsToIso(c.lastModified)).valueOf(); | ||
| return cMs > pMs ? c : p; | ||
| }); | ||
| const updated = tsToIso(latest.lastModified); | ||
| if (!updated) continue; | ||
| const language = latest.reviewerLanguage ?? undefined; | ||
| const versionName = latest.appVersionName ?? undefined; | ||
| const rating = Number(latest.starRating ?? 0) || 0; | ||
| const content = String(latest.text ?? '').trim(); | ||
| const id = `${reviewId}:${updated}`; | ||
| out.push({ id, reviewId, updated, content, rating, versionName, author, language }); | ||
| } |
There was a problem hiding this comment.
最新コメント選定の NaN 比較バグを討て
Timestamp 無効値が先頭に来ると reduce 比較が常に偽となり、正しい最新が拾えぬ。無効値を除外してから比較せよ。
- const userComments = comments
- .map((c) => c.userComment)
- .filter((u): u is NonNullable<typeof u> => !!u);
- if (!reviewId || userComments.length === 0) continue;
- const latest = userComments.reduce((p, c) => {
- const pMs = dayjs(tsToIso(p.lastModified)).valueOf();
- const cMs = dayjs(tsToIso(c.lastModified)).valueOf();
- return cMs > pMs ? c : p;
- });
- const updated = tsToIso(latest.lastModified);
- if (!updated) continue;
- const language = latest.reviewerLanguage ?? undefined;
- const versionName = latest.appVersionName ?? undefined;
- const rating = Number(latest.starRating ?? 0) || 0;
- const content = String(latest.text ?? '').trim();
+ const userComments = comments
+ .map((c) => c.userComment)
+ .filter((u): u is NonNullable<typeof u> => !!u);
+ const valid = userComments
+ .map((u) => ({ u, iso: tsToIso(u.lastModified) }))
+ .filter((x): x is { u: NonNullable<typeof x.u>; iso: string } => Boolean(x.iso));
+ if (!reviewId || valid.length === 0) continue;
+ const latest = valid.reduce((p, c) =>
+ dayjs(c.iso).isAfter(dayjs(p.iso)) ? c : p
+ );
+ const updated = latest.iso;
+ const language = latest.u.reviewerLanguage ?? undefined;
+ const versionName = latest.u.appVersionName ?? undefined;
+ const rating = Math.max(0, Math.min(5, Number(latest.u.starRating ?? 0) || 0));
+ const content = String(latest.u.text ?? '').trim();🤖 Prompt for AI Agents
In functions/src/workers/googlePlayReviews.ts around lines 75 to 92, the reduce
that picks the latest userComment can produce NaN comparisons when a comment has
an invalid/empty lastModified; exclude any comments without a valid lastModified
(or where tsToIso(lastModified) yields a falsy/invalid date) before calling
reduce, then perform the reduce on that filtered list (or handle NaN by treating
invalid timestamps as -Infinity) so the comparison uses only valid numeric
milliseconds and the correct latest comment is selected.
| if (all.length) { | ||
| const newest = all.reduce((p, c) => (dayjs(c.updated).isAfter(dayjs(p.updated)) ? c : p), all[0]); | ||
| const updatedIds = [ | ||
| ...Array.from(new Set([...(state.lastIds ?? []).slice(-20), ...all.slice(0, 20).map((r) => r.id)])), | ||
| ].slice(-60); | ||
| await saveState(stateUri, { lastUpdated: newest.updated, lastIds: updatedIds }); | ||
| if (debug) { | ||
| console.log('[PlayJob] state saved', { lastUpdated: newest.updated, lastIds: updatedIds.length }); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
lastIds 更新は最新20件を用いよ
現状は all.slice(0, 20) と順不同で古きを拾い、重複再通知の芽が残る。更新日時で整列し末尾20件を混ぜよ。
- if (all.length) {
- const newest = all.reduce((p, c) => (dayjs(c.updated).isAfter(dayjs(p.updated)) ? c : p), all[0]);
- const updatedIds = [
- ...Array.from(new Set([...(state.lastIds ?? []).slice(-20), ...all.slice(0, 20).map((r) => r.id)])),
- ].slice(-60);
+ if (all.length) {
+ const sorted = all
+ .slice()
+ .sort((a, b) => dayjs(a.updated).valueOf() - dayjs(b.updated).valueOf());
+ const newest = sorted[sorted.length - 1];
+ const updatedIds = [
+ ...Array.from(
+ new Set([
+ ...(state.lastIds ?? []).slice(-20),
+ ...sorted.slice(-20).map((r) => r.id),
+ ])
+ ),
+ ].slice(-60);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (all.length) { | |
| const newest = all.reduce((p, c) => (dayjs(c.updated).isAfter(dayjs(p.updated)) ? c : p), all[0]); | |
| const updatedIds = [ | |
| ...Array.from(new Set([...(state.lastIds ?? []).slice(-20), ...all.slice(0, 20).map((r) => r.id)])), | |
| ].slice(-60); | |
| await saveState(stateUri, { lastUpdated: newest.updated, lastIds: updatedIds }); | |
| if (debug) { | |
| console.log('[PlayJob] state saved', { lastUpdated: newest.updated, lastIds: updatedIds.length }); | |
| } | |
| if (all.length) { | |
| const sorted = all | |
| .slice() | |
| .sort((a, b) => dayjs(a.updated).valueOf() - dayjs(b.updated).valueOf()); | |
| const newest = sorted[sorted.length - 1]; | |
| const updatedIds = [ | |
| ...Array.from( | |
| new Set([ | |
| ...(state.lastIds ?? []).slice(-20), | |
| ...sorted.slice(-20).map((r) => r.id), | |
| ]) | |
| ), | |
| ].slice(-60); | |
| await saveState(stateUri, { lastUpdated: newest.updated, lastIds: updatedIds }); | |
| if (debug) { | |
| console.log('[PlayJob] state saved', { lastUpdated: newest.updated, lastIds: updatedIds.length }); | |
| } | |
| } |
🤖 Prompt for AI Agents
In functions/src/workers/googlePlayReviews.ts around lines 205 to 213, the logic
building lastIds uses all.slice(0, 20) which doesn't guarantee the newest 20
reviews and can leave duplicates causing re-notifications; sort the all array by
updated timestamp descending (newest first), then take the first 20 ids, merge
with the existing state.lastIds (keeping their most-recent 20 if needed),
deduplicate while preserving order, and finally keep the most-recent 60 ids
before saving as lastIds; keep the existing newest.updated selection and debug
log unchanged.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (7)
functions/README.md (7)
138-141: RSSという名にしてJSONを要求—用語の齟齬を正せ変数名は
...RSS_URLだが本文は「JSONフィードURL」を要求しておる。読者の混乱を防ぐため、/json 固定である旨を明記し、将来の名称移行方針も記すが良い。適用案(ドキュメント差分):
- - `APPSTORE_REVIEW_RSS_URL`: App StoreレビューのJSONフィードURL(任意)。未設定時は既定のJSONエンドポイント(日本向け `/jp/`)。国・言語を変更したい場合はこのURLの地域コードを差し替えてください。 + - `APPSTORE_REVIEW_RSS_URL`: App StoreレビューのJSONフィードURL(任意)。未設定時は既定のJSONエンドポイント(日本向け `/jp/`)。国・言語を変更したい場合はこのURLの地域コードを差し替えてください。必ず末尾が `/json` であるURLを指定すること(`/xml` は非対応)。 - 既定値: `https://itunes.apple.com/jp/rss/customerreviews/page=1/id=1486355943/sortBy=mostRecent/json` - - 互換性のため環境変数名は`...RSS_URL`のままですが、JSON URLを指定してください + - 互換性のため環境変数名は `...RSS_URL` のままだが、実体は JSON フィードである。将来 `APPSTORE_REVIEW_FEED_URL` への改名を検討し、従来名は後方互換で読み替えると良い。
141-141: GCS権限は最小化せよ書込権限に
roles/storage.objectAdminを示しておるが、必要最小は「作成+読取」で足りるはず。過剰権限を避ける指針を追記せよ。- - `APPSTORE_REVIEW_STATE_GCS_URI`: 既読状態を保存するGCSパス(例: `gs://<bucket>/states/appstore-reviews.json`)[本番必須]。バケットには少なくとも該当オブジェクトへの書込権限(例: `roles/storage.objectAdmin`)が必要です。 + - `APPSTORE_REVIEW_STATE_GCS_URI`: 既読状態を保存するGCSパス(例: `gs://<bucket>/states/appstore-reviews.json`)[本番必須]。バケットには少なくとも該当オブジェクトへの読取/作成権限が必要(最小例: `roles/storage.objectViewer` + `roles/storage.objectCreator`)。運用上問題なければ `roles/storage.objectAdmin` でも可。
142-143: スケジュールのタイムゾーンを明示せよ
every 60 minutesの例のみでは実行時刻の基準が不明である。既定はUTCであるゆえ、JST運用なら明記するが良い。- - `REVIEWS_CRON_SCHEDULE`: スケジュール(例: `every 60 minutes`)。未設定時は毎時実行 + - `REVIEWS_CRON_SCHEDULE`: スケジュール(例: `every 60 minutes`)。未設定時は毎時実行 + - `REVIEWS_TIMEZONE`: タイムゾーン(例: `Asia/Tokyo`)。未設定時はUTC
145-145: 変数の型と意味を明文化せよ
REVIEWS_FORCE_LATEST_COUNT=1は「整数N件」を意味すると読み取れる。型と用途を明記し、誤用を断て。- - `REVIEWS_FORCE_LATEST_COUNT=1`: 既読に関わらず最新N件を強制送信(検証用途) + - `REVIEWS_FORCE_LATEST_COUNT`: 整数N。既読に関わらず最新N件を強制送信(検証用途)
152-153: こちらも時刻の国境を越える—タイムゾーンを明示せよApp Store 側と同様に、Google Play 側にもタイムゾーンを用意し、表記を揃えるが良い。
- - `PLAY_REVIEWS_CRON_SCHEDULE`: スケジュール(例: `every 60 minutes`)。未設定時は毎時実行 + - `PLAY_REVIEWS_CRON_SCHEDULE`: スケジュール(例: `every 60 minutes`)。未設定時は毎時実行 + - `PLAY_REVIEWS_TIMEZONE`: タイムゾーン(例: `Asia/Tokyo`)。未設定時はUTC
156-156: 強制件数の表現を統一せよApp Store 側と同じく、整数であることを明示せよ。表記ゆれは朕の嫌うところである。
- - `REVIEWS_FORCE_LATEST_COUNT=1`: 既読に関わらず最新N件を強制送信(検証用途) + - `REVIEWS_FORCE_LATEST_COUNT`: 整数N。既読に関わらず最新N件を強制送信(検証用途)
144-146: 片方のみのDRY_RUNは不均衡—App Store側にも用意せよGoogle Play 側にのみ
REVIEWS_DRY_RUNがある。検証性と対称性の観点から、App Store 側にも同変数を実装・記載するが良い。- デバッグ用(任意): - `REVIEWS_DEBUG=1`: 取得内容などの詳細ログを出力 - - `REVIEWS_FORCE_LATEST_COUNT=1`: 既読に関わらず最新N件を強制送信(検証用途) + - `REVIEWS_FORCE_LATEST_COUNT`: 整数N。既読に関わらず最新N件を強制送信(検証用途) + - `REVIEWS_DRY_RUN=1`: Discord送信をスキップし、送信予定の項目をログ表示Also applies to: 157-158
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
functions/README.md(1 hunks)functions/src/workers/__tests__/appStoreReviewNotifier.test.ts(1 hunks)functions/src/workers/appStoreReviewNotifier.ts(1 hunks)functions/src/workers/googlePlayReviews.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- functions/src/workers/googlePlayReviews.ts
- functions/src/workers/tests/appStoreReviewNotifier.test.ts
- functions/src/workers/appStoreReviewNotifier.ts
🧰 Additional context used
🪛 LanguageTool
functions/README.md
[grammar] ~134-~134: There might be a mistake here.
Context: ...transformation tasks ### Scheduled Jobs - **App Store Review Notifier (`appStoreRevi...
(QB_NEW_EN)
[grammar] ~135-~135: There might be a mistake here.
Context: ...ビューをJSONフィードから取得し、Discordへ通知します(既定: 毎時)。 - 環境変数: - DISCORD_REVIEW_WEBHOOK_URL...
(QB_NEW_EN)
[grammar] ~136-~136: There might be a mistake here.
Context: ...ードから取得し、Discordへ通知します(既定: 毎時)。 - 環境変数: - DISCORD_REVIEW_WEBHOOK_URL: DiscordのWebhook URL(必須) - `APPSTOR...
(QB_NEW_EN)
[grammar] ~137-~137: There might be a mistake here.
Context: ...EW_WEBHOOK_URL: DiscordのWebhook URL(必須) - APPSTORE_REVIEW_RSS_URL`: App StoreレビューのJSONフィードURL(任意)。未設定時は既定の...
(QB_NEW_EN)
[grammar] ~138-~138: There might be a mistake here.
Context: ...p/)。国・言語を変更したい場合はこのURLの地域コードを差し替えてください。 - 既定値: https://itunes.apple.com/jp/rss/cu...
(QB_NEW_EN)
[grammar] ~139-~139: There might be a mistake here.
Context: ...い場合はこのURLの地域コードを差し替えてください。 - 既定値: https://itunes.apple.com/jp/rss/customerreviews/page=1/id=1486355943/sortBy=mostRecent/json - 互換性のため環境変数名は...RSS_URLのままですが、JSON URLを...
(QB_NEW_EN)
[grammar] ~141-~141: There might be a mistake here.
Context: ...権限(例: roles/storage.objectAdmin)が必要です。 - REVIEWS_CRON_SCHEDULE: スケジュール(例: every 60 minutes)。未設定時は毎時実...
(QB_NEW_EN)
[grammar] ~142-~142: There might be a mistake here.
Context: ... スケジュール(例: every 60 minutes)。未設定時は毎時実行 - デバッグ用(任意): - REVIEWS_DEBUG=1: 取得内容...
(QB_NEW_EN)
[grammar] ~143-~143: There might be a mistake here.
Context: ...ry 60 minutes)。未設定時は毎時実行 - デバッグ用(任意): - REVIEWS_DEBUG=1: 取得内容などの詳細ログを出力 - REVIEWS_FORCE_LA...
(QB_NEW_EN)
[grammar] ~144-~144: There might be a mistake here.
Context: ... - REVIEWS_DEBUG=1: 取得内容などの詳細ログを出力 - REVIEWS_FORCE_LATEST_COUNT=1: 既読に関わらず最新N件を強制送信(検証用途) - **Google Pla...
(QB_NEW_EN)
[grammar] ~147-~147: There might be a mistake here.
Context: ...ublisher APIから取得し、Discordへ通知します(既定: 毎時)。 - 環境変数: - DISCORD_REVIEW_WEBHOOK_URL...
(QB_NEW_EN)
[grammar] ~148-~148: There might be a mistake here.
Context: ...PIから取得し、Discordへ通知します(既定: 毎時)。 - 環境変数: - DISCORD_REVIEW_WEBHOOK_URL: DiscordのWebhook URL(必須) - `GOOGLE_...
(QB_NEW_EN)
[grammar] ~149-~149: There might be a mistake here.
Context: ...EW_WEBHOOK_URL: DiscordのWebhook URL(必須) - GOOGLE_PLAY_PACKAGE_NAME: パッケージ名(既定: me.tinykitten.trainlcd`) ...
(QB_NEW_EN)
[grammar] ~150-~150: There might be a mistake here.
Context: ...E: パッケージ名(既定: me.tinykitten.trainlcd) - GOOGLEPLAY_REVIEW_STATE_GCS_URI: 既読状態を保存するGCSパス(例: gs:///state...
(QB_NEW_EN)
[grammar] ~151-~151: There might be a mistake here.
Context: ...bucket>/states/googleplay-reviews.json) - PLAY_REVIEWS_CRON_SCHEDULE: スケジュール(例: every 60 minutes`)。未設定時は毎時実...
(QB_NEW_EN)
[grammar] ~152-~152: There might be a mistake here.
Context: ... スケジュール(例: every 60 minutes)。未設定時は毎時実行 - 認証: Cloud Functionsのサービスアカウントに「Android P...
(QB_NEW_EN)
[grammar] ~153-~153: There might be a mistake here.
Context: ...(Application Default Credentials)で認証します。 - デバッグ用(任意): - REVIEWS_DEBUG=1: 取得/ペ...
(QB_NEW_EN)
[grammar] ~154-~154: There might be a mistake here.
Context: ...fault Credentials)で認証します。 - デバッグ用(任意): - REVIEWS_DEBUG=1: 取得/ページング/保存の詳細ログを出力 - `REVIEWS_FOR...
(QB_NEW_EN)
[grammar] ~155-~155: There might be a mistake here.
Context: ...- REVIEWS_DEBUG=1: 取得/ページング/保存の詳細ログを出力 - REVIEWS_FORCE_LATEST_COUNT=1: 既読に関わらず最新N件を強制送信(検証用途) - `REVIEWS_...
(QB_NEW_EN)
[grammar] ~156-~156: There might be a mistake here.
Context: ..._LATEST_COUNT=1: 既読に関わらず最新N件を強制送信(検証用途) - REVIEWS_DRY_RUN=1`: Discord送信をスキップし、送信予定の項目をログ表示 ### Fir...
(QB_NEW_EN)
🔇 Additional comments (1)
functions/README.md (1)
134-136: 良き追加、目的が明瞭である「Scheduled Jobs」節の新設と2種の通知機構の概要は簡潔で、導線として十分である。続行せよ。
- README: /json 必須の明記、変数名移行方針、GCSの最小権限例(viewer+creator)、タイムゾーンENVを追加 - App Store: REVIEWS_DRY_RUN 対応、スケジュールのタイムゾーンをENV化(REVIEWS_TIMEZONE、既定UTC) - Google Play: スケジュールのタイムゾーンをENV化(PLAY_REVIEWS_TIMEZONE、既定UTC)
- コード: FEED_URL のみ参照(なければ既定値) - README: RSS_URL の記述と互換文を削除し、非対応を明記 - テスト: FEED_URL 想定のまま(変更なし)
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
functions/README.md (1)
155-155: Play Console 側の権限付与を明記せよ(ADCのみでは通らぬ)朕の覧るところ、この一文だけでは動かぬ者多し。Play Console へのユーザー招待と権限付与を明記すべし。加えて使用スコープも記すが良い。
- - 認証: Cloud Functionsのサービスアカウントに「Android Publisher API」へのアクセス権を付与し、ADC(Application Default Credentials)で認証します。 + - 認証: Cloud Functionsのサービスアカウントに「Android Publisher API」へのアクセス権を付与し、ADC(Application Default Credentials)で認証します。 + 併せて、当該サービスアカウントを Google Play Console のユーザーとして招待し、レビュー閲覧が可能な権限を付与すること(例: 「アプリ情報の表示(閲覧のみ)」。返信も行うなら「レビューへの返信」)。Play Console 側の付与が無ければ API は権限不足で失敗する。 + 使用スコープ: `https://www.googleapis.com/auth/androidpublisher`
🧹 Nitpick comments (4)
functions/README.md (4)
152-152: GCS権限要件をApp Store側と同等に明記せよGoogle Play用の既読状態オブジェクトにも最小権限を明文化すべし。読者の迷いを断つ。
- - `GOOGLEPLAY_REVIEW_STATE_GCS_URI`: 既読状態を保存するGCSパス(例: `gs://<bucket>/states/googleplay-reviews.json`) + - `GOOGLEPLAY_REVIEW_STATE_GCS_URI`: 既読状態を保存するGCSパス(例: `gs://<bucket>/states/googleplay-reviews.json`)。必要最小の権限は該当オブジェクトへの読取/作成(例: `roles/storage.objectViewer` + `roles/storage.objectCreator`)。運用都合であれば `roles/storage.objectAdmin` でも可。
138-140: 地域コードの具体例を一つ添え、迷いを討て
/json必須は良い。加えて他地域の具体例を一つ示すと、導入が速やかになろう。- - `APPSTORE_REVIEW_FEED_URL`: App StoreレビューのJSONフィードURL(任意)。未設定時は既定のJSONエンドポイント(日本向け `/jp/`)。国・言語を変更したい場合はこのURLの地域コードを差し替えてください。必ず末尾が `/json` のURLを指定してください(`/xml` は非対応)。 + - `APPSTORE_REVIEW_FEED_URL`: App StoreレビューのJSONフィードURL(任意)。未設定時は既定のJSONエンドポイント(日本向け `/jp/`)。国・言語を変更したい場合はこのURLの地域コードを差し替えること。必ず末尾が `/json` のURLを指定せよ(`/xml` は非対応)。例(米国英語): `https://itunes.apple.com/us/rss/customerreviews/page=1/id=<APP_ID>/sortBy=mostRecent/json`
141-142: 「every N minutes」とタイムゾーンの関係を注記せよ朕の知る限り、「every 60 minutes」のような表現では timeZone が実質無効となる実装もある。cron式指定時のみ有効かを確認し、一言注記を入れると誤解が減ろう。
Also applies to: 153-154
144-146: Discord Webhook の rate limit 注意を一言添えよ大量送信時は 429 の刑に処せられる。乾坤一擲の一文で運用事故を防げ。
- デバッグ用(任意): - `REVIEWS_DEBUG=1`: 取得内容などの詳細ログを出力 - `REVIEWS_FORCE_LATEST_COUNT`: 整数N。既読に関わらず最新N件を強制送信(検証用途) - `REVIEWS_DRY_RUN=1`: Discord送信をスキップし、送信予定の項目をログ表示 + - 注意: 大量送信時は Discord Webhook のレート制限(429)が発生し得るため、必要に応じてバッチ処理や送信間隔の調整を行うこと。(上同様、Google Play 側のデバッグ節にも同一の注意書きを加えるが良い)
Also applies to: 157-159
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
functions/README.md(1 hunks)functions/src/workers/__tests__/appStoreReviewNotifier.test.ts(1 hunks)functions/src/workers/appStoreReviewNotifier.ts(1 hunks)functions/src/workers/googlePlayReviews.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- functions/src/workers/tests/appStoreReviewNotifier.test.ts
- functions/src/workers/appStoreReviewNotifier.ts
- functions/src/workers/googlePlayReviews.ts
🧰 Additional context used
🪛 LanguageTool
functions/README.md
[grammar] ~134-~134: There might be a mistake here.
Context: ...transformation tasks ### Scheduled Jobs - **App Store Review Notifier (`appStoreRevi...
(QB_NEW_EN)
[grammar] ~135-~135: There might be a mistake here.
Context: ...ビューをJSONフィードから取得し、Discordへ通知します(既定: 毎時)。 - 環境変数: - DISCORD_REVIEW_WEBHOOK_URL...
(QB_NEW_EN)
[grammar] ~136-~136: There might be a mistake here.
Context: ...ードから取得し、Discordへ通知します(既定: 毎時)。 - 環境変数: - DISCORD_REVIEW_WEBHOOK_URL: DiscordのWebhook URL(必須) - `APPSTOR...
(QB_NEW_EN)
[grammar] ~137-~137: There might be a mistake here.
Context: ...EW_WEBHOOK_URL: DiscordのWebhook URL(必須) - APPSTORE_REVIEW_FEED_URL`: App StoreレビューのJSONフィードURL(任意)。未設定時は既定の...
(QB_NEW_EN)
[grammar] ~138-~138: There might be a mistake here.
Context: ...ず末尾が /json のURLを指定してください(/xml は非対応)。 - 既定値: `https://itunes.apple.com/jp/rss/cu...
(QB_NEW_EN)
[grammar] ~139-~139: There might be a mistake here.
Context: ...URLを指定してください(/xml は非対応)。 - 既定値: https://itunes.apple.com/jp/rss/customerreviews/page=1/id=1486355943/sortBy=mostRecent/json - APPSTORE_REVIEW_STATE_GCS_URI: 既読状態を保存するGCSパス(例: `gs:///state...
(QB_NEW_EN)
[grammar] ~140-~140: There might be a mistake here.
Context: ...の都合であれば roles/storage.objectAdmin でも可。 - REVIEWS_CRON_SCHEDULE: スケジュール(例: every 60 minutes)。未設定時は毎時実...
(QB_NEW_EN)
[grammar] ~141-~141: There might be a mistake here.
Context: ... スケジュール(例: every 60 minutes)。未設定時は毎時実行 - REVIEWS_TIMEZONE: タイムゾーン(例: Asia/Tokyo)。未設定時はUTC - デ...
(QB_NEW_EN)
[grammar] ~142-~142: There might be a mistake here.
Context: ...EZONE: タイムゾーン(例: Asia/Tokyo)。未設定時はUTC - デバッグ用(任意): - REVIEWS_DEBUG=1`: 取得内容...
(QB_NEW_EN)
[grammar] ~143-~143: There might be a mistake here.
Context: ...例: Asia/Tokyo)。未設定時はUTC - デバッグ用(任意): - REVIEWS_DEBUG=1: 取得内容などの詳細ログを出力 - `REVIEWS_FORCE_LA...
(QB_NEW_EN)
[grammar] ~144-~144: There might be a mistake here.
Context: ... - REVIEWS_DEBUG=1: 取得内容などの詳細ログを出力 - REVIEWS_FORCE_LATEST_COUNT: 整数N。既読に関わらず最新N件を強制送信(検証用途) - `REVI...
(QB_NEW_EN)
[grammar] ~145-~145: There might be a mistake here.
Context: ...ATEST_COUNT: 整数N。既読に関わらず最新N件を強制送信(検証用途) - REVIEWS_DRY_RUN=1`: Discord送信をスキップし、送信予定の項目をログ表示 - **Goog...
(QB_NEW_EN)
[grammar] ~148-~148: There might be a mistake here.
Context: ...ublisher APIから取得し、Discordへ通知します(既定: 毎時)。 - 環境変数: - DISCORD_REVIEW_WEBHOOK_URL...
(QB_NEW_EN)
[grammar] ~149-~149: There might be a mistake here.
Context: ...PIから取得し、Discordへ通知します(既定: 毎時)。 - 環境変数: - DISCORD_REVIEW_WEBHOOK_URL: DiscordのWebhook URL(必須) - `GOOGLE_...
(QB_NEW_EN)
[grammar] ~150-~150: There might be a mistake here.
Context: ...EW_WEBHOOK_URL: DiscordのWebhook URL(必須) - GOOGLE_PLAY_PACKAGE_NAME: パッケージ名(既定: me.tinykitten.trainlcd`) ...
(QB_NEW_EN)
[grammar] ~151-~151: There might be a mistake here.
Context: ...E: パッケージ名(既定: me.tinykitten.trainlcd) - GOOGLEPLAY_REVIEW_STATE_GCS_URI: 既読状態を保存するGCSパス(例: gs:///state...
(QB_NEW_EN)
[grammar] ~152-~152: There might be a mistake here.
Context: ...bucket>/states/googleplay-reviews.json) - PLAY_REVIEWS_CRON_SCHEDULE: スケジュール(例: every 60 minutes`)。未設定時は毎時実...
(QB_NEW_EN)
[grammar] ~153-~153: There might be a mistake here.
Context: ... スケジュール(例: every 60 minutes)。未設定時は毎時実行 - PLAY_REVIEWS_TIMEZONE: タイムゾーン(例: Asia/Tokyo)。未設定時はUTC - 認...
(QB_NEW_EN)
[grammar] ~154-~154: There might be a mistake here.
Context: ...EZONE: タイムゾーン(例: Asia/Tokyo`)。未設定時はUTC - 認証: Cloud Functionsのサービスアカウントに「Android P...
(QB_NEW_EN)
[grammar] ~155-~155: There might be a mistake here.
Context: ...(Application Default Credentials)で認証します。 - デバッグ用(任意): - REVIEWS_DEBUG=1: 取得/ペ...
(QB_NEW_EN)
[grammar] ~156-~156: There might be a mistake here.
Context: ...fault Credentials)で認証します。 - デバッグ用(任意): - REVIEWS_DEBUG=1: 取得/ページング/保存の詳細ログを出力 - `REVIEWS_FOR...
(QB_NEW_EN)
[grammar] ~157-~157: There might be a mistake here.
Context: ...- REVIEWS_DEBUG=1: 取得/ページング/保存の詳細ログを出力 - REVIEWS_FORCE_LATEST_COUNT: 整数N。既読に関わらず最新N件を強制送信(検証用途) - `REVI...
(QB_NEW_EN)
[grammar] ~158-~158: There might be a mistake here.
Context: ...ATEST_COUNT: 整数N。既読に関わらず最新N件を強制送信(検証用途) - REVIEWS_DRY_RUN=1`: Discord送信をスキップし、送信予定の項目をログ表示 ### Fir...
(QB_NEW_EN)
🔇 Additional comments (1)
functions/README.md (1)
134-147: App Store節、構成と既定値の明快さは可とす記述は簡明で導入容易。朕は是を嘉す。
close #1703
Summary by CodeRabbit