エラーを出さずに壊れていた7つの事例:本物の抽選を捨てる・読めていない・失敗を握りつぶす

English version

トレカダッシュ 開発の記録 / 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 かどうかで見ることでした。

分かったこと

捨てる判定の間違いは、苦情が出ない。だから自分で数える。

候補は見つかっていたのに、答えを読む所で毎回落ちていた: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 つ直しました。

  1. 商品名で絞った検索に加えて、いつも、site:店のドメイン 抽選 も検索して、候補にまとめる。(「空振りしたときだけ」では、無関係なページが 1 件返るだけで、発動しませんでした。)
  2. 指示に、「その店自身の告知のページが、複数の商品をまとめて案内していて、個別の名前を書いていないときは、選んでよい」を足す。ただし、ほかの店やまとめサイトは、まとめて案内していても、絶対に選ばない。

結果は、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 件のときは、答えを読む所を疑う。

「確認済み」の印が付いていたのに、リンク先を一度も読めていなかった: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 か所とも、ブラウザの名乗りを付けて開いて、「転送に従う」設定にしていました。その結果、次のことが起きていました。

ブラウザの名乗りを足したのは、8 月 6 日のことでした。今回の不具合は、その変更の副作用でした。

実際に、どれくらい読めていなかったか

報告された 1 件だけの問題かを確かめるために、表示中の X の抽選・予約の記事のうち、t.co のリンクを持つものを全部調べました。

調べたこと結果
t.co を持つ、表示中の記事220 件
そのうち、中継ページが返ってきた記事220 件(全部)
そのうち、実は閉じたフォームなのに、表示中だった記事9 件

9 件は、複数の店にまたがっていました。1 つの店の問題ではなく、仕組みの問題です。

もう 1 つ、見逃せない手がかりがありました。毎日動いている、「締切が空の記事を直す」処理が、「対象 126 件、更新 0 件」を、毎日繰り返していたことです。対象が 126 件あるのに、直せたのが 0 件。これは、「直すものが無かった」のではなく、「読めていないから、直せなかった」の結果でした。

直し方

転送先を調べる処理を、fetchTcoTarget という 1 つの関数にまとめました。手順は、こうです。

  1. 名乗りを付けずに、t.co を開く。転送には従わず(redirect: "manual")、最初の転送先だけを受け取る。
  2. 転送先が返ってきたら、それを使う。
  3. 中継ページが返ってきたら、その中の自動転送の指定(<meta http-equiv="refresh">)を読んで、本当の転送先を取り出す。
  4. その転送先を、今度はブラウザの名乗りを付けて開く。

名乗りを付けるか付けないかで、返ってくるものが違う。だから、「短縮 URL を解く」ときは名乗りなし、「本来のページを読む」ときはブラウザの名乗りあり、と使い分けました。同じ失敗を繰り返さないよう、テストも足しています。

すでに付いていた、「確認済み」の印をどうするか

直しただけでは、足りません。誤って付いた「確認済み」の印が、記事に残っているからです。印がある記事は、「もう確認した」と見なされて、再確認の対象から外れます。コードを直しても、既存の記事は直りません。

そこで、条件に合う 195 件(X の抽選・予約で、投稿へのリンクを持ち、t.co を持つもの)から、印を外しました。そのうえで、受付終了の確認と、締切の再抽出を、手で実行しました。

処理直す前直した後
締切の再抽出(対象になった件数/更新できた件数)対象 126 件・更新 0 件(毎日)対象 123 件・更新 14 件

同じ日に、受付終了の確認の頻度も見直しました。締切の無い記事は 6 時間おき(締切のある記事は 24 時間おき)、1 回に見る件数は 40 件から 80 件、1 回の上限は 7 分です。t.co が読めるようになったので、閉じたフォームを丸 1 日出し続けないようにするためです。

分かったこと

「確認した」の印は、読めたときだけ付ける。

「取れた」と読んでいたのは、ただの案内ページだった:3日間、商品が0件

2026 年 10 月 8 日の記録。

iOS アプリ「トレカダッシュ」は、通販サイトの販売ページを定期的に見に行って、抽選や先着の情報を集めています。ある日、ポケモンセンターオンラインの取得が、3 日間ずっと、商品 0 件になっていたことが分かりました。エラーは出ていません。「取れた」と判定されていたからです。何が起きていたのか、コミットの記録で書きます。

このサイトには、トレカダッシュの作品ページはまだありません。ここに書くのは、開発の記録です。

「取れた」の判定が、甘かった

ページの取得は、外部のサービス(Scrape.do)を通して行っています。取得の設定には、安いものから高いものまであって、安い設定で取れなければ、強い設定に切り替える(昇格する)作りです(取得の費用の記事と同じ仕組みです)。

この「取れたかどうか」の判定が、問題でした。ポケモンセンターのページは、安い設定で取ると、2,410 バイトの、「JavaScript を有効にしてください」という案内ページが返ってきます。商品の一覧ではなく、ただの案内です。ところが、これを「取れた」と読んで、成功と扱っていました。成功なら、強い設定への切り替えも行われません。

こうして、9 月 4 日から 3 日間、取得は「成功」し続けて、商品は 0 件のまま、でした。

同じページを、3つの設定で叩いて測った

原因を推測で決めないために、ポケモンセンターの検索ページを、取得の設定を変えて実際に取り直しました。安い設定は 1 クレジット、強い設定(super と render を併用)は 25 クレジットかかります。

取得の設定応答中身
安い設定(1 クレジット)2002,410 バイト。「このサイトでは JavaScript を有効にする必要があります」だけ
super + render(25 クレジット)200278,322 バイト。商品 39 件

応答コードはどちらも 200 です。通信の成功だけを見ていたら、この 2 つは区別できません。違いは中身の大きさだけで、約 115 倍ありました。

守りはあったのに、2 つとも寸前で通り抜けていた

「取れたかどうか」を見る条件は、もともと何もなかったわけではありません。ページに次の 2 つの確認がありました。

どちらも、想定した形の少しだけ外側で、通り抜けています。確認の条件は、想定した 1 つの形にしか合っていませんでした。実際に来るページの形は、いくつもあります。

通ってしまった後は、次のことが起きます。

  1. 強い設定に切り替え直さない。商品は 0 件のまま。
  2. 「安い設定で取れる」と覚えて、強い設定への切り替えを解除する。
  3. 以後ずっと 0 件。エラーが 1 件も出ないので、ログにも何も残らない。

3 日間の数字

日時ポケモンセンターの取得件数
9 月 4 日 12:45 まで10 件か 8 件(安定していた)
9 月 4 日 12:45 から 9 月 7 日0 件が 323 回連続。エラーは 1 件もなし
直した後に手で実行10 件に戻った

直し方

新しい判定は、何を「取れていない」と読むか

scrapeDoResultLooksBlocked は、次のどれかに当てはまると「取れていない」と答え、その理由の文字列も返します。理由はログに必ず残るので、あとから「なぜ強い設定に上がったか」が読めます。

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 件は別のものです。件数だけでは区別できないので、中身を見て、理由を書き残しています。

分かったこと

「成功」と「中身が取れた」は、別のもの。

この記事は、2026 年 9 月 7 日のコミットと、その日の作業の記録から書きました。数字(2,410 バイト、278,322 バイト、323 回、36 と 25 クレジット)は、その記録に残っていた実測値です。

表示中の記事のリンク先を、実際に開いて確かめた:約 60 件の間違いと、最初の点検が間違えた 3 つのこと

2026 年 10 月 17 日の記録。

iOS アプリ「トレカダッシュ」の記事には、お店や通販サイトのリンク先が付いています。記事の題名や値段が正しくても、リンク先のページが、閉じていたり、別の商品だったりすれば、利用者には役に立ちません。

2026 年 9 月 15 日の夜、「遷移先の内容まで見て、問題ないか調べてほしい」という依頼を受けて、表示中の記事のリンク先を、実際に開いて、中身を記事と突き合わせました。商品、店、受付中か、販売の形式、締切、値段です。約 60 件の間違いが見つかりました。

数字は、2026 年 9 月 15 日の作業の記録から書いています。点検は、書き込みをしない形で行い、見つかったものを別に直しました。

どう調べたか

最初の版の点検は、3 つ間違えていました。

点検の道具が間違っていると、「間違い」の数が増えます。文字コードの判定、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 時間控えます。

直さなかったもの、直せなかったもの

見つかったものを、全部「間違い」と数えなかったのも、大事なところです。確かめて、直していないものがあります。

点検の結果は、直す、直さない、読めなかったの 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() で呼び、失敗を捨てていました。

その結果、こうなっていました。

直し方は、失敗を呼び出し元に返して、成功した件数だけ数えることでした。既定のカレンダーが無い端末は、先に検出して知らせます。案内の文も、理由ごとに分けました。それまでは、どんな失敗でも、「アクセスが許可されていません」と出していました。

2. 文字認識が失敗すると、画面が固まる

応募の画像から文字を読み取る機能(OCR)も、try? handler.perform([request]) で、失敗を捨てていました。文字認識が失敗すると、コールバック(終わったときの呼び出し)が、一度も呼ばれません。読み取り中の「ぐるぐる」が止まらず、画面を閉じる以外に、抜けられなくなっていました。

直し方は、失敗したときも、「認識し終わった(見つからなかった)」状態に戻すことです。

ビルドの警告、result of 'try?' is unused、は、握りつぶしの目印です。ビルドを流して、警告を定期的に見ます。

3. 通知に、店名が出ない:バッファに積み忘れた

別の話ですが、同じ形です。「通知に、店名がない」という報告が来ました。記事には、店名(sellerName)が、正しく入っています。それなのに、通知には出ていませんでした。

抽選と予約の通知は、いったんバッファ(一時置き場)に積んでから、まとめて送る設計です(サイレントな時間帯の保留と、送る回数の制限のため)。ところが、店名を、バッファに引き継いでいませんでした。送るときに、店名を探しても、もう情報が無く、カテゴリ(「ライヴポケット」)しか残っていません。

この症状は、前の報告の直し方が表に出した、という面もありました。「通知が、ライヴポケットという名前で来る」を直したとき、店名が分からない場合は、null を返すようにしました。この積み忘れが、「間違った店名」から、「店名なし」に、症状を変えて表に出たのです。

2 種類のバッファ(ジャンル別と、商品別)の両方に、同じ抜けがあったので、両方を直しました。すでに積まれていた 5 件は、記事から店名を補って、送るときに店名が出ることを確かめました。

同じ抜けは、8 月 17 日にも、別の項目(商品名)で起きていました。教訓は、バッファに積むときは、送るときに使う項目を、全部持たせることです。組み立て済みの文言をそのまま保存する、ほかの 2 つのバッファでは、この問題は起きません。

分かったこと

失敗を捨てると、画面は、嘘をつく。

確認できなかったときに、通していた:「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 回の上限があります(想定外の費用を防ぐための回路遮断器)。ところが、関門は、定価を確認できなかった理由を、区別していませんでした。

この二つが、どちらも「定価不明 → 通す」の同じ道を通っていました。前者は意図どおりです。後者は、「調べる前に、道が閉まっていた」だけなのに、関門が、素通しになっていたのです。

穴が、実害になった理由

この穴は、前の変更(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 の穴は見つかりませんでした。

分かったこと

「確認できなかった」には、二種類ある。知らなかったのと、調べられなかったのと。

この記事は、HANDOFF(第182回)とコミット(2026年7月30日)の記録から書きました。

それぞれの要点

ほかの記録は、Notes の一覧から、テーマごとに読めます。トレカダッシュそのものは、作品ページに書いています。

ほかの記録

コメント

感想や、記事の間違いの指摘は、ここへ。書くには、GitHub のアカウントが要ります。

押すと、giscus(giscus.app)と GitHub に接続します。押さなければ、接続しません。詳しくはプライバシーポリシーに書いています。