エラーを出さずに壊れていた7つの事例:本物の抽選を捨てる・読めていない・失敗を握りつぶす
トレカダッシュ 開発の記録 / 7 件のまとめ
いちばん厄介な不具合は、エラーが出ないものです。処理は「成功」と言い、画面には何かが出ているのに、本物の抽選が黙って捨てられていたり、リンク先を一度も読めていなかったりします。ここには、そうした「静かに壊れていた」7 つの実例と、どうやって気づいたか、どう直したかを、起きた順にまとめました。
それぞれの節は、もとは別々に公開していた記録です(2026 年 10 月から)。数字と出典は、その時の作業の記録のままです。テーマごとに読めるよう、1 つの記事にまとめ直しました(2026 年 11 月 7 日)。
本物の抽選を、静かに捨てていた 4 つの実例:正規表現・年のない日付・まとめて聞く・null
2026 年 10 月 17 日の記録。
iOS アプリ「トレカダッシュ」の集める処理には、「これは対象外」と判断して捨てる場所がたくさんあります。捨てる判定は、安全側に見えます。間違えても、画面にゴミが出ないだけで、誰も困らない、ように見えるからです。
ところが、この種の間違いは、気づくきっかけがありません。本物の抽選を捨てていても、画面には何も出ないからです。この記事では、そうやって本物を静かに捨てていた、または捨てかけた、4 つの実例を書きます。
数字と日付は、2026 年 8 月 28 日〜9 月 20 日の作業の記録から書いています。
1. 「x.com」が、「e-starbox.com」に一致していた(8 月 31 日)
応募のリンクが、SNS のリンクなら、お店のページではないので、採用しません。そのために、除外するサイトの名前を並べて、1 つの正規表現(文字列のパターン)にしていました。
new RegExp(["x\\.com", "note\\.com", ...].join("|"))
この書き方は、サイト名の途中にも一致します。x\.com は、e-starbox.com の後半に当たります。note\.com は mynote.com に当たります。
実害は、抽選販売のサービス「抽選ロトボックス」の応募ページ(shoplottery.e-starbox.com)で出ていました。このサービスの応募リンクは、「SNS だから店のリンクではない」と判断されて、一度も採用されていませんでした。
直し方は、ホスト名だけの項目は、「// の直後か、ドットの直後」から始まるときだけ一致させる((?:^|//|\.) を前に付ける)ことでした。足したあとは、通したい URL と、弾きたい URL の両方で、13 例を試しました。除外のリストは安全側に見えるので、間違っていても、誰も疑いませんでした。
2. 年の書いていない日付を、6 年前と読んだ(8 月 28 日)
まとめサイトや店の告知は、「8 月 30 日(日)」のように、年を書きません。この文字列を AI に読ませて、締切を取っていました。
入荷情報のサイトの取り込みで、ポケモンカードの案件が、軒並み2020 年 8 月 30 日(6 年前)として返ってきました。締切が過去だと、入口の関門が弾きます。その結果、このサイト経由のポケモンカードの案件が、まるごと取り込まれていませんでした。ワンピースやドラゴンボールは、たまたま 2026 年と読まれていたので、気づきにくい状態でした。
直し方は、AI に渡す指示に、今日の日付と、「今日より前の年を補ってはいけない」を入れることでした。読める案件は、0 件から 46 件になりました。「投稿日と矛盾しない年」とだけ書いても、投稿日が分からないときに、何年になるのかが決まらないので、足りません。
3. まとめて聞いた答えの、数がずれた(9 月 19 日)
AI への問い合わせは、1 件ずつ送ると、回数分の費用がかかります。そこで、複数の投稿の「販売の形式」を、まとめて 1 回で聞く仕組みを入れました。複数行の投稿の本文を、「1. 本文」「2. 本文」と番号をつけて、改行でつないで渡していました。
AI から見ると、投稿の中の改行と、投稿どうしの区切りが、区別できません。X の監視を再開した 1 日だけで、返ってきた答えの数が合わなかった回が 8 回ありました。数が合わないときは、その組を全部「対象外」にする作りだったので、その組の本物の抽選は、まとめて捨てられました。
もっと厄介なのは、数が合った組でも、別の投稿の答えが当たっていたことです。遊戯王カードの抽選が、対象外として捨てられていました。
直し方は、まとめて渡すときは、JSON の配列([{no, text}])か、本文に出てこない見出し(「【記事 N】」)で区切り、返事にも番号を含めさせることです。数が合わないときは、既定値の「対象外」に倒さず、1 件ずつ聞き直します。
4. 「決められない」を、「無い」と読んだ(9 月 20 日)
ある関数は、投稿の文章から、トレカのジャンルを推定して返します。2 つ以上のジャンルが書かれていると、「決められない」ので null を返します。
別の処理が、この関数を使って、「カードの語が無い(=玩具だけの告知)」を判定していました。!関数(text) という書き方です。ここで、「決められない(null)」が「無い」と読まれました。
ポケモンカード、ワンピース、遊戯王、ドラゴンボール、ガンプラ、と並べる、まとめの投稿は、複数のジャンルが書かれているので null になります。それが「カードの語ゼロ」と読まれ、さらに「ガンプラ」が見つかったので、玩具の告知と判定されました。
もし、このまま動いていたら、毎朝 5:15 に動く一括の非表示の処理が、表示中の本物の抽選 129 件(30th CELEBRATION や、世界最強の戦士など、9 つの商品)を、伏せるところでした。止められたのは、一括の処理を動かす前に、件数だけを数える下見(ドライラン)をしたからです。配備の前に、129 件という数字が出て、気づきました。
直し方は、「無い」を判定したいときは、見つかったジャンルを全部返す関数を使い、その長さが 0 かどうかで見ることでした。
分かったこと
捨てる判定の間違いは、苦情が出ない。だから自分で数える。
- 除外のリストも、間違える。安全側に見える判定ほど、疑われません。
- 通したいものでも試す。弾きたいものだけで試すと、通したいものまで弾いていても、気づきません。(1 の 13 例は、両方でした)
- AI には、前提を全部渡す。今日の日付も、区切りも、書かないと、AI は黙って別の前提で答えます。
- 数が合わないときに、「対象外」に倒さない。分からないときの既定値が「捨てる」だと、本物がまとめて消えます。
- 「無い」と「決められない」は、別の答え。戻り値の
nullを、無条件に否定しない。 - 毎日の一括の処理は、動かす前に、件数を数える。129 件が、事前に分かりました。
候補は見つかっていたのに、答えを読む所で毎回落ちていた:200 トークンで切れた URL と、ハイフンで 400 になる検索
2026 年 10 月 17 日の記録。
iOS アプリ「トレカダッシュ」の、まとめサイト由来の記事は、お店の公式の告知のページへ、リンクを張り替えます。ところが、開発者モードの報告が、続けて 7 件、来ました。「正しい告知の URL は、これ。ちゃんとやって」。
調べると、候補は見つかっていたのに、答えを読む所で、毎回落ちていました。この記事は、その真因と、同じ時期に見つけた、よく似た 1 件の記録です。
数字は、2026 年 8 月 23 日と 24 日の作業の記録から書いています。
真因:AI の返事が、200 トークンで途中で切れていた
公式の告知ページを選ぶ処理は、最後に AI に、「候補のどれが、そのお店の告知か」を答えさせます。その返事の長さの上限(max_output_tokens)を、200 にしていました。
ところが、返事には、日本語をパーセント符号化した、長い URLが入ります。1 つで 200 文字を超えます。返事は途中で切れ、JSON として読めなくなり、Unterminated string in JSON という例外になっていました。
WARN [応募リンク] 公式ページの判定に失敗 [お宝創庫・プレイズ / …]: Unterminated st…
→ 対象 10 件、すべて「見つからず」
候補は、見つかっていたのです。答えを読むところで、毎回、落ちていました。上限を 800 に上げました。さらに、それでも切れたときに、全部を捨てず、"url": "… の部分だけを拾い直す救済も入れました。
直しても、採用が 0 件だった:AI が指示に忠実だった
JSON を直しても、採用は 0 件でした。ログを見ると、候補はあるのに、「採用なし」です。原因は、AI が、指示に忠実だったことでした。お店の告知は、こういう書き方をします。
「【アプリ会員様限定】再版/ポケモンカードゲーム 4 種 抽選販売に関して」
個別の商品名を書いていません。指示には、「商品が違う場合は、選ぶな」とありました。AI は、それに従って、拒否していたのです。
2 つ直しました。
- 商品名で絞った検索に加えて、いつも、
site:店のドメイン 抽選も検索して、候補にまとめる。(「空振りしたときだけ」では、無関係なページが 1 件返るだけで、発動しませんでした。) - 指示に、「その店自身の告知のページが、複数の商品をまとめて案内していて、個別の名前を書いていないときは、選んでよい」を足す。ただし、ほかの店やまとめサイトは、まとめて案内していても、絶対に選ばない。
結果は、10 件のうち、3 件が、お店の公式の告知のページへ張り替わりました。
もう 1 つ、アプリ限定の店(アプリでしか、抽選の案内を出さない店)のために、「アプリでご確認ください」と出せる一覧を作りました。アプリが実在すると分かっている店だけです。推測で足しません。
よく似た 1 件:楽天のキーワード検索が、ハイフンで丸ごと失敗していた(8 月 24 日)
ログに、「楽天市場の個別弾の検索に失敗しました」が、遊戯王だけで 8 種以上、繰り返し出ていました。調べると、楽天の API は、検索のキーワードのハイフンの使い方によっては、リクエスト自体が 400 wrong_parameter で失敗しました。
| キーワードの形 | 結果 |
|---|---|
"A - B"(ハイフンの前後に空白) | 400 |
"A B "(末尾に空白だけ残る) | 400 |
"A B-"(末尾がハイフンで終わる) | 400 |
"AB-"(空白を挟まない単語 + 末尾ハイフン) | 200(正常) |
遊戯王の商品名は、「LIMIT OVER COLLECTION - THE RIVALS -」のように、副題をハイフンで挟む書き方が多く、この形の弾は、個別弾の検索から、一度も拾えていませんでした。
直し方は、2 段階です。(1) ハイフンの直後の空白を詰める。(2) 末尾に残るハイフンと空白を落とす。「LIMIT OVER COLLECTION - THE RIVALS -」は、「LIMIT OVER COLLECTION -THE RIVALS」になり、200 を確かめました。同じ問題は、楽天ブックスの API でも、起きることを確かめたので、同じ整形を、キーワードを渡す 4 つの関数に当てました。
分かったこと
候補が見つかっているのに、結果が 0 件のときは、答えを読む所を疑う。
- 返事の長さの上限は、返す中身から決める。パーセント符号化した URL は、長いです。
- 失敗は、全部捨てず、拾い直す。切れた JSON から、必要な部分だけ拾えます。
- AI は、指示に忠実。指示が、現実の書き方に合っていないと、正しく拒否します。
- 外の API の癖は、実測で切り分ける。ハイフンの前後で、400 と 200 が分かれました。
- 同じ癖は、同じ API を使う、ほかの場所にもある。楽天ブックスでも起きました。
「確認済み」の印が付いていたのに、リンク先を一度も読めていなかった:t.co の中継ページ
2026 年 10 月 17 日の記録。
iOS アプリ「トレカダッシュ」は、お店の X(旧 Twitter)の告知から、トレカの抽選販売の情報を集めています。告知には、応募用のフォームへのリンクが付いています。アプリは、そのリンク先も読みに行って、締切の日時や、もう受付が終わっていないかを確かめています。
ある日、「締切の日時が違う」という報告が来ました。調べると、締切の日時どころではなく、リンク先のページを、一度も読めていませんでした。しかも、読めていないのに、「確認済み」の印が付いていました。
この記事は、2026 年 10 月 16 日の作業の記録(調査の内容と、数字)から書いています。
症状:閉じたフォームが、受付中のまま出ていた
報告された抽選は、ゲームショップの「30th CELEBRATION カードセット 9 種」の X の告知でした。応募先の Google フォームを開くと、「回答の受け付けは終了しました」と出ます。ところが、アプリでは、この抽選が表示中のままで、締切の日時も空でした。
おかしいのは、この記事に「受付が終わっていないか確認した」という印(日時)が付いていたことです。確認したのに、終わっていることに気づかなかった。確認が、中身を見ていなかったことになります。
原因:t.co は、ブラウザのふりをすると、別のページを返す
X の投稿に貼られたリンクは、t.co という短縮 URL を通ります。普通は、開くと、本来のリンク先へ転送されます(HTTP の 301)。
ところが、ブラウザの User-Agent(「私は Chrome です」という名乗り)を付けて開くと、転送されず、JavaScript で飛ばす、約 500 バイトの中継ページが返ってきます。アプリの読み取り処理は、2 か所とも、ブラウザの名乗りを付けて開いて、「転送に従う」設定にしていました。その結果、次のことが起きていました。
- 転送されないので、最後の URL は、
t.coのままです。 - 読んだ本文は、応募フォームではなく、中継ページの HTML でした。
- つまり、応募フォームの中身を、一度も読んでいませんでした。
ブラウザの名乗りを足したのは、8 月 6 日のことでした。今回の不具合は、その変更の副作用でした。
実際に、どれくらい読めていなかったか
報告された 1 件だけの問題かを確かめるために、表示中の X の抽選・予約の記事のうち、t.co のリンクを持つものを全部調べました。
| 調べたこと | 結果 |
|---|---|
t.co を持つ、表示中の記事 | 220 件 |
| そのうち、中継ページが返ってきた記事 | 220 件(全部) |
| そのうち、実は閉じたフォームなのに、表示中だった記事 | 9 件 |
9 件は、複数の店にまたがっていました。1 つの店の問題ではなく、仕組みの問題です。
もう 1 つ、見逃せない手がかりがありました。毎日動いている、「締切が空の記事を直す」処理が、「対象 126 件、更新 0 件」を、毎日繰り返していたことです。対象が 126 件あるのに、直せたのが 0 件。これは、「直すものが無かった」のではなく、「読めていないから、直せなかった」の結果でした。
直し方
転送先を調べる処理を、fetchTcoTarget という 1 つの関数にまとめました。手順は、こうです。
- 名乗りを付けずに、
t.coを開く。転送には従わず(redirect: "manual")、最初の転送先だけを受け取る。 - 転送先が返ってきたら、それを使う。
- 中継ページが返ってきたら、その中の自動転送の指定(
<meta http-equiv="refresh">)を読んで、本当の転送先を取り出す。 - その転送先を、今度はブラウザの名乗りを付けて開く。
名乗りを付けるか付けないかで、返ってくるものが違う。だから、「短縮 URL を解く」ときは名乗りなし、「本来のページを読む」ときはブラウザの名乗りあり、と使い分けました。同じ失敗を繰り返さないよう、テストも足しています。
すでに付いていた、「確認済み」の印をどうするか
直しただけでは、足りません。誤って付いた「確認済み」の印が、記事に残っているからです。印がある記事は、「もう確認した」と見なされて、再確認の対象から外れます。コードを直しても、既存の記事は直りません。
そこで、条件に合う 195 件(X の抽選・予約で、投稿へのリンクを持ち、t.co を持つもの)から、印を外しました。そのうえで、受付終了の確認と、締切の再抽出を、手で実行しました。
| 処理 | 直す前 | 直した後 |
|---|---|---|
| 締切の再抽出(対象になった件数/更新できた件数) | 対象 126 件・更新 0 件(毎日) | 対象 123 件・更新 14 件 |
同じ日に、受付終了の確認の頻度も見直しました。締切の無い記事は 6 時間おき(締切のある記事は 24 時間おき)、1 回に見る件数は 40 件から 80 件、1 回の上限は 7 分です。t.co が読めるようになったので、閉じたフォームを丸 1 日出し続けないようにするためです。
分かったこと
「確認した」の印は、読めたときだけ付ける。
- 印は、成功したときだけ書く。読めなかったのに「確認した」と書くと、直した後も、その記事は二度と見直されません。
- 外部のページを読んだら、目的のページかを確かめる。最後の URL が、短縮 URL のままではないか。本文が、期待した種類のページか。
- 「更新 0 件」が続くのは、異常かもしれない。対象があるのに、毎日 0 件。「ない」のか「できない」のかを、区別します。
- 通信の設定を変えたら、それを使う全部の通信を見直す。8 月に足したブラウザの名乗りが、2 か月後に、
t.coの読み取りを壊していました。
「取れた」と読んでいたのは、ただの案内ページだった:3日間、商品が0件
2026 年 10 月 8 日の記録。
iOS アプリ「トレカダッシュ」は、通販サイトの販売ページを定期的に見に行って、抽選や先着の情報を集めています。ある日、ポケモンセンターオンラインの取得が、3 日間ずっと、商品 0 件になっていたことが分かりました。エラーは出ていません。「取れた」と判定されていたからです。何が起きていたのか、コミットの記録で書きます。
このサイトには、トレカダッシュの作品ページはまだありません。ここに書くのは、開発の記録です。
「取れた」の判定が、甘かった
ページの取得は、外部のサービス(Scrape.do)を通して行っています。取得の設定には、安いものから高いものまであって、安い設定で取れなければ、強い設定に切り替える(昇格する)作りです(取得の費用の記事と同じ仕組みです)。
この「取れたかどうか」の判定が、問題でした。ポケモンセンターのページは、安い設定で取ると、2,410 バイトの、「JavaScript を有効にしてください」という案内ページが返ってきます。商品の一覧ではなく、ただの案内です。ところが、これを「取れた」と読んで、成功と扱っていました。成功なら、強い設定への切り替えも行われません。
こうして、9 月 4 日から 3 日間、取得は「成功」し続けて、商品は 0 件のまま、でした。
同じページを、3つの設定で叩いて測った
原因を推測で決めないために、ポケモンセンターの検索ページを、取得の設定を変えて実際に取り直しました。安い設定は 1 クレジット、強い設定(super と render を併用)は 25 クレジットかかります。
| 取得の設定 | 応答 | 中身 |
|---|---|---|
| 安い設定(1 クレジット) | 200 | 2,410 バイト。「このサイトでは JavaScript を有効にする必要があります」だけ |
| super + render(25 クレジット) | 200 | 278,322 バイト。商品 39 件 |
応答コードはどちらも 200 です。通信の成功だけを見ていたら、この 2 つは区別できません。違いは中身の大きさだけで、約 115 倍ありました。
守りはあったのに、2 つとも寸前で通り抜けていた
「取れたかどうか」を見る条件は、もともと何もなかったわけではありません。ページに次の 2 つの確認がありました。
- 本文が 2,000 文字未満なら、取れていないとみなす。案内ページは 2,410 バイトで、わずかに超えていました。
- 本文に「JavaScript が無効」という文言があれば、取れていないとみなす。実際の文言は「JavaScript を有効にする必要があります」で、この文言には一致しませんでした。
どちらも、想定した形の少しだけ外側で、通り抜けています。確認の条件は、想定した 1 つの形にしか合っていませんでした。実際に来るページの形は、いくつもあります。
通ってしまった後は、次のことが起きます。
- 強い設定に切り替え直さない。商品は 0 件のまま。
- 「安い設定で取れる」と覚えて、強い設定への切り替えを解除する。
- 以後ずっと 0 件。エラーが 1 件も出ないので、ログにも何も残らない。
3 日間の数字
| 日時 | ポケモンセンターの取得件数 |
|---|---|
| 9 月 4 日 12:45 まで | 10 件か 8 件(安定していた) |
| 9 月 4 日 12:45 から 9 月 7 日 | 0 件が 323 回連続。エラーは 1 件もなし |
| 直した後に手で実行 | 10 件に戻った |
直し方
- 「取れた」の判定を、1 か所に集める。それまで、判定が複数の場所に散らばっていました。ブロックされたページかどうかを見る関数(
scrapeDoResultLooksBlocked)を 1 本にして、全部がそれを使うようにしました。 - 通った設定を、覚える。一度成功した設定を覚えておき、次からは、そこから試します。これで、1 回の取得に使うクレジットが、36 から 25 に減りました。
新しい判定は、何を「取れていない」と読むか
scrapeDoResultLooksBlocked は、次のどれかに当てはまると「取れていない」と答え、その理由の文字列も返します。理由はログに必ず残るので、あとから「なぜ強い設定に上がったか」が読めます。
- 本文が短すぎる。
- 順番待ちのページ(Queue-it)である。
- 中身が小さいのに、JavaScript を求める案内がある。
3 つ目には、落とし穴があります。「JavaScript が無効になっています」という一文は、まっとうなページにも、noscript という要素として常に入っていることがあります。実際に、別の店(ヤマダ)のページで一度、取れているページを「取れていない」と誤診しました。そこで、この判定は8,000 バイト未満のときだけ当てる形にしました。大きなページは、noscript があっても本物とみなします。
この関門は、安い設定、super、render と super の併用の、3 つの段階すべてで同じものを通します。
通った設定を、覚えるようにした
昇格のしくみ自体にも、無駄がありました。それまでは、取得の関数が自分の中で設定を上げたときだけ、通った設定を覚えていました。呼び出し側が設定を切り替えて上げた分は、覚えていなかったのです。その結果、毎回 1 + 10 + 25 の36 クレジットを払っていました。覚えるようにしたので、次回からは 25 クレジットで始まります。安い設定も、定期的に試し直します。
書き込みは、設定が変わったときだけにしています。毎回書くと、今度はデータベースの書き込み費用が増えるからです。
同じ日に見つかった、もう一つの汚れ
同じ点検で、別の問題も見つかりました。商品名の「別表記」の食い違いを判定する処理が、照合の入口と、お掃除の側で、別々に書かれていました。お掃除の側にだけ、「商品の形が食い違う」という判定が無かったのです。その結果、トライアルデッキの別表記が、ブースターパックの商品に残ったままになり、毎日、誤った照合を呼び続けていました。判定を共通の関数にして、27 商品の 46 件を除去しました。
あわせて、「更新する文書が無い」というエラー(7 日間で 64 件)も解消しました。商品は、キャッシュが古いときに読み直す。記事は、消えていたら見送って、残りの重複処理を続ける、という形にしています。
気づいた経緯と、ほかの「0 件」4 つの確認
気づいたきっかけは、作業の最初に必ず見る定型の点検です。健全性のチェックに、「取得 0 件が 48 時間続いている」という警告が並びます。それまでは、そこに並んだ名前を「掲載が無いだけ」と流していました。この回は、流さずに中身を確かめました。
同じ警告に並んでいたほかの 4 つも、1 つずつ見ました。全部、まっとうな 0 件でした。なぜ 0 件でよいのかを書き残しておかないと、次に見た人が同じ確認をやり直すことになります。
| 情報源 | 確かめた中身 |
|---|---|
| 三洋堂 | ページに「現在受付中の抽選販売はありません」と書いてある。取得も成功している(56KB)。 |
| コナミスタイル | 16 件取れるが、バッジが全部「遊戯王カードゲームポイント交換専用」。現金では買えない景品なので、設計どおり除外している。 |
| 楽天市場 | もともと 1〜2 件が普通の情報源。14 日間で 0 件が 1,860 回、1 件が 2,345 回あった。 |
| ミニストップオンライン | どの設定でも取れない(20 秒の待ちを 2 回ずつ、3 設定すべてで応答なし)。抽選受付のときだけページが出る作りなので、休止の仕組みに任せている。 |
同じ「0 件」でも、掲載が無い 0 件、設計どおり除く 0 件、取れていない 0 件は別のものです。件数だけでは区別できないので、中身を見て、理由を書き残しています。
分かったこと
「成功」と「中身が取れた」は、別のもの。
- 成功の判定は、中身で見る。通信が通ったことと、欲しいものが取れたことは、別です。「取れたものが 0 件」が続いたら、それ自体を異常として見張る必要があります。
- 判定は、1 か所に。同じ判定が 2 か所にあると、片方だけが古くなります。照合の入口とお掃除のように。
- エラーが出ないことは、うまくいっていることの証拠にならない。
この記事は、2026 年 9 月 7 日のコミットと、その日の作業の記録から書きました。数字(2,410 バイト、278,322 バイト、323 回、36 と 25 クレジット)は、その記録に残っていた実測値です。
表示中の記事のリンク先を、実際に開いて確かめた:約 60 件の間違いと、最初の点検が間違えた 3 つのこと
2026 年 10 月 17 日の記録。
iOS アプリ「トレカダッシュ」の記事には、お店や通販サイトのリンク先が付いています。記事の題名や値段が正しくても、リンク先のページが、閉じていたり、別の商品だったりすれば、利用者には役に立ちません。
2026 年 9 月 15 日の夜、「遷移先の内容まで見て、問題ないか調べてほしい」という依頼を受けて、表示中の記事のリンク先を、実際に開いて、中身を記事と突き合わせました。商品、店、受付中か、販売の形式、締切、値段です。約 60 件の間違いが見つかりました。
数字は、2026 年 9 月 15 日の作業の記録から書いています。点検は、書き込みをしない形で行い、見つかったものを別に直しました。
どう調べたか
- X の投稿(361 件):削除されたものは 0 件でした。本文は、以前の点検で突き合わせ済みです。
- X 以外(Yahoo! を除く 703 件):直接、または取得サービス(日本の IP を指定)でリンク先を読み、AI で判定しました。Amazon は、必ず取得サービス経由です。
最初の版の点検は、3 つ間違えていました。
- Amazon を、シンガポールから開いた。価格がシンガポールドルで出て、別の出品者が見えたので、それを「誤り」と判定していました。
- 楽天を、文字化けしたまま読んだ。文字コード(EUC-JP)を判定していませんでした。
- Yahoo! を、6 並列で 501 件開いた。本番と同じ IP が、HTTP 489 で拒否されました(別の記事に書いています)。Yahoo! は、途中で打ち切りました。
点検の道具が間違っていると、「間違い」の数が増えます。文字コードの判定、Amazon を日本から開くこと、「この商品そのもの」の在庫だけで判定すること、を直してから、流し直しました。
見つかった間違い(約 60 件)
| 中身 | 件数 | 直し |
|---|---|---|
| Amazon の「招待リクエスト」なのに、予約・先着になっていた | 10 | 抽選に直す |
| Amazon の「予約注文」なのに、先着になっていた | 7 | 予約に直す |
| 楽天ブックスで「予約受付中」なのに、先着 | 3 | 予約に直す |
| 休店中の出店者(Yahoo!) | 6 | 伏せる |
| 売り切れ、受付終了なのに、表示されていた | 多数 | 伏せる |
| リンク先が、別の店、または終わった抽選だった | 3 | 店の X の投稿へ戻す |
| リンク先が、別の商品だった | 2 | 伏せる |
| 値段の食い違い | 約 10 | 直す |
| 店名が空 | 1 | 補う |
特に多かったのは、販売の形式の取り違えです。Amazon は、先着でも予約でもなく、招待を受けて買える「招待リクエスト」があります。これを「先着」と扱うと、利用者は、急いでも買えません。それまでの処理は、招待の印を付けるだけで、販売の形式を直していませんでした。
「売り切れ」の判定語に当たらなかったものもありました。Yahoo! の売り切れのページは、画面には「在庫なし」と出ていますが、判定語のどれにも当たっていませんでした。そこで、ページに埋め込まれている、その商品自身の在庫データ("stock":{"isAvailable":false)で判定します。画面の文言より、データのほうが確かです。
「応募リンクを開くと、サイトのトップへ飛ぶ」
同じ日の別の点検で、見つかったものです。ある店の抽選の、告知画像の QR コードが指す URL が、実際に開くと、そのサイトのトップページに転送されていました。応募のページではありません。
応募のリンクを採用する、唯一の関門に、判定を足しました。実際に開いて、もとの URL はトップ以外のパスなのに、転送先のパスが空(または、トップらしい名前のファイル)なら、採用しません。読めないとき(403、タイムアウト)は、採用のままにします。ボット対策をしている店が多いので、読めないことは、悪いことの印にしません。同じ URL は、6 時間控えます。
直さなかったもの、直せなかったもの
見つかったものを、全部「間違い」と数えなかったのも、大事なところです。確かめて、直していないものがあります。
- 楽天市場の「売り切れ」:ページのデータ上は在庫あり(在庫数を出さない設定)。AI の読み違いでした。
- Amazon の「他の購入オプションを見る」:ほかの出品者がいるので、売り切れとは限りません。
- Amazon の英語の題名:自動翻訳された名前で、商品は合っていました。
- 読めなかった 85 件:楽天市場(広告経由のリンク)、会員サイト、LINE、Google フォームなど。
点検の結果は、直す、直さない、読めなかったの 3 つに分けて、理由とともに残しています。「直さない」にも、理由が要ります。
分かったこと
題名と値段が合っていても、リンク先まで開かないと、分からない。
- リンク先を、実際に開く。記事の中身だけを見ても、リンク先が閉じていることは分かりません。
- 点検の道具を、先に点検する。国、文字コード、並列数。道具の間違いが、「間違い」を増やします。
- 画面の文言より、埋め込まれたデータ。売り切れは、文言ではなく、在庫のデータで判定します。
- 見つけたものを、全部「間違い」にしない。直さないものにも、理由を残します。
- 読めなかったことを、悪い印にしない。ボット対策で読めないのは、普通のことです。
失敗を捨てると、画面は嘘をつく:「○件登録しました」と出て 1 件も入っていなかったカレンダー、固まる文字認識、店名を積み忘れた通知
2026 年 10 月 17 日の記録。
iOS アプリ「トレカダッシュ」で、「失敗を、握りつぶしたせいで、嘘の成功を表示していた」という不具合が、続けて見つかりました。この記事は、そのうちの 3 つの記録です。
この記事は、2026 年 8 月 25 日の作業の記録から書いています。
1. カレンダーに 1 件も入っていないのに、「○件登録しました」
アプリには、抽選の締切を、iPhone のカレンダーに書き出す機能があります。ビルドの警告(result of 'try?' is unused)から、この処理を見直しました。try? は、Swift で、失敗を捨てる書き方です。この機能は、保存の処理を try? store.save(...) と try? store.commit() で呼び、失敗を捨てていました。
その結果、こうなっていました。
- 1 件もカレンダーに入っていないのに、「○件登録しました」と表示される。
- 「書き出し済み」の記録だけが残る。
- もう一度押すと、「新しく登録できる案件はありませんでした」と出て、永久に登録できなくなる。
直し方は、失敗を呼び出し元に返して、成功した件数だけ数えることでした。既定のカレンダーが無い端末は、先に検出して知らせます。案内の文も、理由ごとに分けました。それまでは、どんな失敗でも、「アクセスが許可されていません」と出していました。
2. 文字認識が失敗すると、画面が固まる
応募の画像から文字を読み取る機能(OCR)も、try? handler.perform([request]) で、失敗を捨てていました。文字認識が失敗すると、コールバック(終わったときの呼び出し)が、一度も呼ばれません。読み取り中の「ぐるぐる」が止まらず、画面を閉じる以外に、抜けられなくなっていました。
直し方は、失敗したときも、「認識し終わった(見つからなかった)」状態に戻すことです。
ビルドの警告、result of 'try?' is unused、は、握りつぶしの目印です。ビルドを流して、警告を定期的に見ます。
3. 通知に、店名が出ない:バッファに積み忘れた
別の話ですが、同じ形です。「通知に、店名がない」という報告が来ました。記事には、店名(sellerName)が、正しく入っています。それなのに、通知には出ていませんでした。
抽選と予約の通知は、いったんバッファ(一時置き場)に積んでから、まとめて送る設計です(サイレントな時間帯の保留と、送る回数の制限のため)。ところが、店名を、バッファに引き継いでいませんでした。送るときに、店名を探しても、もう情報が無く、カテゴリ(「ライヴポケット」)しか残っていません。
この症状は、前の報告の直し方が表に出した、という面もありました。「通知が、ライヴポケットという名前で来る」を直したとき、店名が分からない場合は、null を返すようにしました。この積み忘れが、「間違った店名」から、「店名なし」に、症状を変えて表に出たのです。
2 種類のバッファ(ジャンル別と、商品別)の両方に、同じ抜けがあったので、両方を直しました。すでに積まれていた 5 件は、記事から店名を補って、送るときに店名が出ることを確かめました。
同じ抜けは、8 月 17 日にも、別の項目(商品名)で起きていました。教訓は、バッファに積むときは、送るときに使う項目を、全部持たせることです。組み立て済みの文言をそのまま保存する、ほかの 2 つのバッファでは、この問題は起きません。
分かったこと
失敗を捨てると、画面は、嘘をつく。
- 失敗は、呼び出し元に返す。捨てると、成功の表示と、実際が食い違います。
- 失敗したときも、「終わった」状態に戻す。コールバックが呼ばれないと、画面が固まります。
- 成功の記録は、成功したときだけ。失敗しても残ると、再試行ができなくなります。
- ビルドの警告を読む。
try?の警告は、握りつぶしの目印です。 - バッファには、送るときに使う項目を、全部積む。後から、別の項目で、同じ抜けが出ます。
確認できなかったときに、通していた:「fail-open」が転売品を招いた話
2026 年 10 月 8 日の記録。
iOS アプリ「トレカダッシュ」で、「Amazon に転売の価格がたくさん入ってきている」という報告が、アプリ内の不具合報告から 3 件届きました。うち 1 件は、¥79,800 の、明らかな転売品でした。原因は、わざと入れていた「確認できなかったら通す」という設計の、盲点でした。HANDOFF とコミット(2026年7月30日)の記録で書きます。
このサイトには、トレカダッシュの作品ページはまだありません。ここに書くのは、開発の記録です。
価格の関門と、「通す」設計
アプリは、通販サイト(楽天市場、Yahoo!ショッピング、Amazon など)の出品を拾って、フィードに出します。そのとき、出品価格が、商品の定価から見て妥当かを調べる「価格の関門」があります。定価を、AI(Web 検索つき)に調べさせて、極端に高い転売品を弾く仕組みです。
この関門には、「定価を確認できなかった場合は、審査せずに通す」という動きがありました。これは意図した設計です。AI が本当に定価を知らない商品を、確認できないだけで除外してしまうと、本物の出品まで消してしまうからです。いわゆる fail-open(失敗したら開く=通す)です。
盲点:検索の上限に達した場合も、同じ道を通っていた
AI の Web 検索には、1 日 300 回の上限があります(想定外の費用を防ぐための回路遮断器)。ところが、関門は、定価を確認できなかった理由を、区別していませんでした。
- AI が「この商品の定価は知らない」と答えた。
- 1 日の上限に達して、確認すること自体ができなかった。
この二つが、どちらも「定価不明 → 通す」の同じ道を通っていました。前者は意図どおりです。後者は、「調べる前に、道が閉まっていた」だけなのに、関門が、素通しになっていたのです。
穴が、実害になった理由
この穴は、前の変更(Amazon の対象を広げた回)をきっかけに表に出ました。対象を広げたことで、新しく価格を確認する必要がある商品が、一気に増えました。その結果、1 日の上限に、頻繁に達するようになった。上限に達した間は、関門が素通しになって、転売品が大量に混ざりました。
さらに、二次被害がありました。定価が分からなかった結果は、理由を問わず24 時間キャッシュされる設計でした。そのため、一度上限に当たった商品は、上限が解除されたあとも、最大 24 時間、素通しのままになります。
直し方:理由を区別する
専用の目印(QUOTA_EXCEEDED)を作って、二つを区別できるようにしました。
| 状況 | 関門の動き |
|---|---|
| AI が本当に定価を知らなかった(null) | これまでどおり通す(fail-open のまま) |
| 1 日の上限で、確認自体ができなかった(QUOTA_EXCEEDED) | 除外する(fail-close) |
後者は、24 時間のキャッシュにも入れません。次に呼ばれたときに、あらためて確認を試みます。この関門は、楽天市場、Yahoo!ショッピング、Amazon、AI による出品の発見、楽天ブックスの先着など、価格の審査を使うすべての機能に共通なので、一回の修正が、広い範囲に効きます(コミットの記録では、18 の関数を配備しています)。
報告のあった 3 件は、手作業で非表示にしました。そのあとは、5 分おきに動く再審査のバッチが、同じ見逃しを拾い直す想定です。
確認できていないこと:記録には、「上限のリセット後に、転売価格の報告が来なくなったかを、次のセッションで確認する」と書いてあります。その結果は、この記事を書いた時点で、記録にありません。
ほかの判定にも、同じ穴は無いか
その後の点検(2026年7月30日)で、販売形式の判定や商品の照合など、ほかの AI による判定も確認しました。いずれも、失敗したときは、安全側(除外、または null)に倒れる設計で、新しい fail-open の穴は見つかりませんでした。
分かったこと
「確認できなかった」には、二種類ある。知らなかったのと、調べられなかったのと。
- 通す設計は、「通してよい理由」を、はっきりさせてから使う。「分からないから通す」と「調べられないから通す」は、別のものです。
- 失敗の結果を、理由を問わず、キャッシュしない。一時的な失敗を、24 時間、固定してしまいました。
- 前の変更が、別の場所の穴を、実害にする。Amazon の対象を広げたことが、上限の穴を、表に出しました。
この記事は、HANDOFF(第182回)とコミット(2026年7月30日)の記録から書きました。
それぞれの要点
- 本物の抽選を、静かに捨てていた 4 つの実例:「x.com」が「e-starbox.com」に一致して応募リンクが一度も採用されなかった、年のない日付が 6 年前と読まれてポケカ案件が 0 件だった、まとめ判定の数がずれた、「決められない」が「無い」と読まれ 129 件を伏せかけた。捨てる判定の失敗の記録。
- 候補は見つかっていたのに、答えを読む所で毎回落ちていた:公式の告知ページを選ぶ AI の返事が 200 トークンで途中で切れて毎回失敗していた。直しても 0 件だったのは、店の告知が個別の商品名を書かないため、AI が指示に忠実に拒否していたから。楽天の検索は、ハイフンの位置で 400 になった。
- 「確認済み」の印が付いていたのに、リンク先を一度も読めていなかった:X の告知の応募リンクを、ブラウザの名乗りを付けて開くと、t.co は転送せず中継ページを返した。表示中の 220 件が全部読めておらず、9 件は閉じたフォームだった。直し方と、誤った「確認済み」の印の外し方の記録。
- 「取れた」と読んでいたのは、ただの案内ページだった:ポケモンセンターの取得が3日間ずっと商品0件だった。安い設定が返した2,410バイトの「JavaScriptを有効に」ページを、成功と読んでいた。判定を1か所に集めた記録。
- 表示中の記事のリンク先を、実際に開いて確かめた:表示中の記事のリンク先 1,000 件超を開いて、記事と突き合わせた。Amazon の招待リクエストが先着になっていた、休店中・売り切れが表示中、リンク先が別の店・別の商品、応募リンクがサイトのトップへ転送。最初の点検自体も 3 つ間違えていた。
- 失敗を捨てると、画面は嘘をつく:try? で失敗を捨てていたため、カレンダーに 0 件なのに「登録しました」と出て二度と登録できなくなった。文字認識の失敗で画面が固まった。通知バッファに店名を積み忘れて「店名なし」になった。
- 確認できなかったときに、通していた:「定価を確認できなかったら通す」という設計に、検索の1日上限という盲点があった。「知らなかった」と「調べられなかった」を区別して直した記録。
ほかの記録は、Notes の一覧から、テーマごとに読めます。トレカダッシュそのものは、作品ページに書いています。
ほかの記録
- データの不具合を直すときの7つの教訓:すでにあるデータ・一括処理の事故・重複の数え方・通知の関門 — データの不具合を直すときに踏んだ7つの記録。関門を直しても既存データは直らない、後から掃除が入口の不具合を隠す、一括処理の事故、重複の数え方、通知は取り消せない。
- App Store 審査で止まったこと:はじめての提出の「Guideline 2.1 Information Needed」と、4つのリジェクト — 個人開発の iOS アプリを App Store に出すときに止まった記録。はじめての提出で来た Guideline 2.1 Information Needed の6項目と、iPad のスクリーンショットなど4つのリジェクト。
- AI と Firestore の費用を、測り直して削った5つの記録:Gemini の計器・読み取りの91%・取得の費用 — 個人開発のアプリで、AI(Claude・Gemini)と Firestore、取得の費用を削った5つの記録。削る前に計器を疑い、円で出して、どこに使われているかを数えた。
コメント
感想や、記事の間違いの指摘は、ここへ。書くには、GitHub のアカウントが要ります。
押すと、giscus(giscus.app)と GitHub に接続します。押さなければ、接続しません。詳しくはプライバシーポリシーに書いています。