0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【X】ブラウザ経由の自動投稿が時々落ちる原因をCDPで切り分けた手順(intentをやめた話)

0
Posted at

X の返信と予約投稿を、API ではなく専用ブラウザ(CDP)から出す仕組みで、投稿が2日近く時々落ち続けました。結論は「投稿の小窓(intent/post)を通るのをやめ、ホームと返信先ページの入力欄に直接入れる」です。途中で探査が1回通ったのを見て「直った」と書き、1時間後の本番で否定されています。

画面の自動操作で「急に部品が見つからなくなった」とき、セレクタを直す前に疑う場所が2つあります。この記事はその切り分け方の記録です。

この記事で分かること:

  • 投稿ボタンを押さない探査スクリプトで、ページの状態を数えて切り分ける方法
  • 「ページが起動していない」と「部品の中身が描かれていない」を分けて数える理由
  • intent の小窓を使わずに投稿・返信する構成と、押す前・押した後の確認
  • 1回通っただけで原因を決めると何が起きるか

前提

  • X の API の従量課金を避けるため、返信と予約投稿は専用ブラウザを CDP(Chrome DevTools Protocol)で操作して出している
  • ブラウザはログイン済みの状態で常駐しており、x.com のタブが複数開いていることがある
  • 投稿は2種類
    • 予約投稿(長文・画像つきもある。落ちた本番は1,301字・画像3枚)
    • 返信(返信先のポストに対して)
  • 症状が出る前は、どちらも intent/post の小窓を開いて文面を入れ、投稿ボタンを押していた

症状

11/2 19:16 から次の状態になりました。

  • 返信: 「投稿画面に文面が入らない・投稿ボタンが無い」で全部落ちる
  • 予約投稿: 3回に1回ほどしか通らない

エラーは出ません。スクリプト側から見えるのは「入力欄が見つからない」「ボタンが見つからない」だけです。この状態でセレクタを書き換え始めると、原因が分からないまま数日溶けます。

切り分けの手順

1. 投稿ボタンを押さない探査スクリプトを用意する

本番のジョブを何度も回すと、通った回に本当に投稿されてしまいます。押す処理を持たない探査用スクリプトを別に作り、裏のタブで intent/post を開いて DOM を数えるだけにしました。

数えたのは次の3つです。

  1. #react-root の中の要素数(ページが起動しきったか)
  2. role=dialog の中身(小窓の枠だけか、入力欄まで描かれているか)
  3. ホームの入力欄 tweetButtonInline が出ているか(比較用)

探査で評価した式の形は次のとおりです(簡略化しています)。

// Runtime.evaluate で評価する式の形(簡略化)
(() => {
  const root = document.querySelector('#react-root');
  const dialog = document.querySelector('[role="dialog"]');
  return {
    rootCount: root ? root.querySelectorAll('*').length : -1,
    hasDialog: !!dialog,
    dialogChildren: dialog ? dialog.querySelectorAll('*').length : -1,
    inline: !!document.querySelector('[data-testid="tweetButtonInline"]'),
  };
})()

2. 「起動しているか」と「描かれているか」を別々に見る

数えた結果、見えた現象は2つありました。

現象①: ページが起動しきらない

  • #react-root の中が18要素のまま増えない
  • 画面は黒地にロゴだけの起動画面
  • x.com の古いタブを閉じると起動した

現象②: 起動はするが、小窓の中身が描かれない

  • 投稿の小窓が role=dialog の枠(mask だけ)のまま中身を描かない
  • x.com のほかのタブが無い状態でも起きた
  • 20秒待っても起きた
  • 短い文でも起きた
  • エラーも失敗した通信も出ない

共通点: ホームの入力欄(tweetButtonInline)はどちらのときも出ていた

①と②は「入力欄が無い」という同じ症状に見えますが、①はページ全体の起動の問題、②は小窓という部品の描画の問題で、別物です。要素数を1つ見るだけでは区別できません。

3. 比較用の部品を同時に数える

tweetButtonInline を一緒に数えていたことで、「ログインが切れた」「x.com 全体が壊れた」ではなく、「小窓(intent)経由のときだけ描かれない」まで絞れました。切り分けでは、壊れている部品と同じページにある、壊れていない部品を1つ持っておくと判断が速くなります。

落とし穴: 1回通ったので「直った」と書いた

現象①を見た直後、x.com の古いタブを閉じてから探査を回すと2回続けて通りました。そこで次のように判断しています。

  • 「固まった x.com のタブと共有ワーカーが原因」と決めた
  • 申し送りと手順書にそう書いた
  • ジョブ同士の排他の鍵を入れた(タブの取り合いを防ぐため)

1時間後、本番の予約投稿(1,301字・画像3枚)が現象②で落ちました。②は x.com のほかのタブが無い状態でも起きるため、「古いタブと共有ワーカー」の見立てでは説明できません。見立ては否定されました。

②の条件はまだ分かっていません。分からないものは分からないまま書いておき、構成のほうで避けることにしました。

外した原因を振り返ると次の2点です。

  • 現象①と②を区別せずに「起動しない」とまとめていた
  • 探査の2回は本番と条件が違う(短い文・画像なし)のに、本番で確かめる前に原因を書いた

対処: 小窓を通らずに、その場の入力欄へ入れる

方針

  • intent の小窓は使わない
  • 本投稿は ホーム を、返信は 返信先のポストのページ を直接開く
  • そのページにもともとある入力欄に入れる

両方の現象に共通して「ホームの入力欄は出ていた」ので、そちらを使えば②に当たらずに済む、という判断です。

入力欄の絞り方

返信先のページなどでは入力欄らしき部品が複数ある場合があります。触る部品は次の条件で1つに絞ります。

  • 小窓(role=dialog)の外にある
  • tweetButtonInline と同じ入れ物の中にある入力欄
// 入力欄の絞り込みの形(簡略化)
(() => {
  const buttons = [...document.querySelectorAll('[data-testid="tweetButtonInline"]')]
    .filter((b) => !b.closest('[role="dialog"]'));
  // ボタンと同じ入れ物の中にある入力欄だけを候補にする
  // (入れ物のたどり方はページ構造に合わせて調整)
  return buttons.length;
})()

文面の入れ方

文面は CDP の Input.insertText で入れます。入力欄にフォーカスを当ててから送ります。

// 入力欄にフォーカスしたあと
await cdp.send('Input.insertText', { text: body });

押す前の確認

投稿ボタンは、次の2つが両方そろったときだけ押します。どちらかがずれていたら押さずに失敗として記録します。

  • 入力欄の本文が、入れたかった文面と 全部一致 している
  • 添付された画像の枚数が、予定の枚数と 一致 している
// 押す前の判定の形
const ok = (actualText === body) && (actualImageCount === expectedImageCount);
if (!ok) {
  throw new Error('入力内容が一致しないため投稿しない');
}
// ここで初めて投稿ボタンを押す

長文と画像つきの投稿で落ちていたので、「途中まで入った状態」「画像が足りない状態」で投稿されるのを防ぐための確認です。

押した後の確認(返信)

返信は、投稿後の応答に含まれる in_reply_to_status_id_str を見て、返信先と違っていたら消します。返信先のページで入力欄を取り違えると、別のポストへの返信や単独の投稿として出てしまうためです。

// 返信の後始末の判定の形
if (res.in_reply_to_status_id_str !== targetStatusId) {
  // 返信先が違うので、出たポストを消す
}

まとめ

  • 「部品が見つからない」ときは、セレクタを直す前に「ページが起動しきったか(要素数)」と「部品の中身が描かれているか(中身のある入れ物か)」を分けて数える
  • 壊れた部品と同じページにある、壊れていない部品(今回は tweetButtonInline)を比較用に一緒に数えると、原因の範囲を早く絞れる
  • 探査で1回通っても「直った」とせず、本番と同じ条件の1本で確かめてから原因を書く
  • 条件の分からない不具合は、原因を決めつけずに、その経路を通らない構成で避ける
  • 自動で押すボタンは「押す前に全部一致を確かめる」「押した後に結果を確かめて違えば消す」の2段で守る
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?