
今日、プログラマーのデビッド・ギルバートソンによる記事の翻訳を共有したいと思います。彼は、Reactで書かれたWebプロジェクトのエラーを追跡して再現できる実験システムについて話しています。 このアプローチは他の環境に移すことができると信じていますが、すべてが順調です。
エラー報告のアプローチ
おそらく、Webプロジェクトのエラーに関する情報を収集するために、このようなシンプルなシステムを使用しているのでしょう(次の例では、私に石を投げないでください):
window.onerror = err => fetch(`/errors/${err}`);
エラーレポートを確認するには、フレンドリーなIT専門家に依頼して、
/errors
で始まる404ページに関するすべての記録を含むファイルを提供するだけで十分です。
ただし、このアプローチで得られる「コード」は、エラーが発生した正確な場所を見つけるのに役立ちません。 おそらく、ここで何かを改善し、ファイルと行番号に関する情報を含むエラーメッセージを生成する必要があります。
window.addEventListener('error', e => { fetch('/errors', { method: 'POST', body: `${e.message} (in ${e.filename} ${e.lineno}:${e.colno})`, }); });
このコードは良識のどこかでバランスを取っていますが、これまでのところ、それはもっと深刻な何かの骨格にすぎません。 エラーが特定のデータに関連している場合、行番号に関する情報は特に有益ではありません。
エラーが発生した時点でユーザーのアクティビティに関する完全なレポートがあれば便利です。これにより、障害が発生した状況を再現できます。 たとえば、次のようなもの:

ユーザーアクティビティレポート
最も興味深いのは、ユーザーが製品に関する詳細情報のページに移動し(ステップ4)、購入ボタン(最後の5番目のステップ)をクリックしたことです。
特定の製品のデータに何か疑わしいことが起こっているとすぐに推測できるため、同じリンクにアクセスして、「これを$で購入」という購入ボタンをクリックします。
もちろん、これを行った後、私は同じ間違いを見ます。 この特定のアイテムには価格がないため、
toLocaleString
を呼び出すと失敗します。 私たちの前に、あまり経験のない開発者の典型的な監督があります。
しかし、ユーザーのサイトとのやり取りがはるかに複雑な場合はどうでしょうか? ユーザーが作業をURLに反映していない多くのタブの1つであるか、フォームのデータをチェックしているときにエラーが発生した可能性があります。 リンクをたどってボタンをクリックしても、このようなエラーは明らかになりません。
この状況では、エラーが発生するまですべてのユーザーアクションを再現できるようにしたいと考えています。 理想的には、再生中に「次のステップ」というボタンをクリックするだけです。
想像力を失っていないなら、これを自分自身に想像してみてください。

DOMを観察してユーザーアクションを再現する
画面に表示されるエラー情報とエディターで開くファイルは、Reactアプリの作成のメリットです。
実験の形で、「モデレートされていないユーザビリティテスト」のようなものを実行できるシステムを実際に構築したことに注意してください。 ユーザーのアクションを追跡して再生するコードを作成し、知識のある1人のジョンに、彼がこのすべてについてどう思うかを尋ねました。 彼はこれは馬鹿げたアイデアだと言ったが、私のコードはエラーを再現するのに役立つだろうと付け加えた。
実際、ここでこれについてお話したいと思います。 ありがとう、ジョン。
システムコア
問題のコードはここにあります 。 あなたは私の話よりもそれを読むことに興味があるかもしれません。 以下に関数の簡易バージョンを示し、全文へのリンクを提供します。
さまざまなユーザーアクションをインターセプトするいくつかの関数を含むモジュールrecord.jsがあります。 これはすべて
journey
オブジェクトに分類され、エラーが発生したときにサーバーに転送できます。
アプリケーションの入力ポイントで、次のような
startRecording()
関数を呼び出して情報の収集を開始します。
const journey = { meta: {}, steps: [], }; export const startRecording = () => { journey.meta.startTime = Date.now(); journey.meta.startUrl = document.location.href; journey.meta.screenWidth = window.innerWidth; journey.meta.screenHeight = window.innerHeight; journey.meta.userAgent = navigator.userAgent; };
エラーが発生した場合、
journey
オブジェクトは、たとえば、分析のためにサーバーに送信できます。 これを行うには、対応するイベントハンドラーが接続されます。
window.addEventListener('error', sendErrorReport);
sendErrorReport
関数
sendErrorReport
、
journey
オブジェクトと同じモジュールで宣言されます。
export const sendErrorReport = (err) => { journey.meta.endTime = Date.now(); journey.meta.error = `${err.message} (in ${err.filename} ${err.lineno}:${err.colno})`; fetch('/error', { method: 'POST', body: JSON.stringify(journey) }) .catch(console.error); };
ところで、誰かが
JSON.stringify(err)
コマンドがエラーの本文を提供しない理由を説明できれば、非常にクールになります。
これまでのところ、これらすべてに特別な利点はありません。 しかし、今では他のすべてを構築するためのフレームワークがあります。
アプリケーションが状態ベースの場合(つまり、DOMがいくつかの主要な状態に基づいてのみ表示される場合)、より簡単に生きることができます(エラーが発生する可能性が低くなると想定してリスクがあります)。 エラーを再現しようとすると、単純に状態を再作成できます。これにより、このエラーが発生する可能性が高くなります。
アプリケーションが最新のテクノロジーに基づいていない場合、バインディングを使用し、ユーザーがページと対話する方法に直接基づいて何かを表示すると、問題はもう少し複雑になります。 エラーを再現するには、マウスクリック、要素の損失とフォーカスに関連するイベント、およびキーボードキーを押すことを再現する必要があります。 確かに、ユーザーがクリップボードからフィールドに何かを貼り付けた場合の対処方法を言うのは難しいと思います。 ここで私は実験で幸運を願うことができます。
私は認めたいです-私は怠け者で利己的な人ですので、私が話すことは私が扱う技術、すなわちReactとReduxで構築されたプロジェクトに焦点を当てます。
これがまさに私が傍受したいものです:
- すべてのスケジュールされたアクション(結果として、状態ストアへの変更の「再生」を有効にすることが可能になります)。
- URLの変更(つまり、URLを更新できるようになります)。
- ページをクリックすると(ユーザーがクリックしたボタンやリンクを自分の目で確認できるようになります)。
- スクロール(これにより、エラー発生時にユーザーがページで見たものを正確に知ることができます)。
Reduxアクションインターセプト
以下は、
journey
オブジェクトのReduxアクションをインターセプトして保存するために使用されるコードです。
export const captureActionMiddleware = () => next => action => { journey.steps.push({ time: Date.now(), type: INTERACTION_TYPES.REDUX_ACTION, data: action, }); return next(action); };
最初に、構造
= () => next => action => {
を見ることができます。これは、一見理解できないことはまったく不可能です。 それでも理解できない場合は、読んでください 。 確かに、これを掘り下げるのではなく、もっと重要なことをするほうがいいでしょう。たとえば、幸せな笑顔を描く練習をします。これは、誕生日を祝福するときに役立ちます。
このコードで理解する最も重要なことは、プロジェクトで果たす役割です。 つまり、彼はReduxの「アクション」を実行中に
journey
オブジェクトに配置するのに忙しくしています。
次に、Reduxリポジトリを作成するときに上記の関数を適用し、リンクをこの
applyMiddleware()
フレームワークの関数に渡します。
const store = createStore( reducers, applyMiddleware(captureActionMiddleware), ); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );
URLの変更を記録する
URL変更キャプチャが実行される場所は、アプリケーションのルーティング方法によって異なります。
ReactルーターはURLの変更を検出するのにあまり役に立たないので、 このアプローチ、または多分これに頼らなければなりません。 Reactルーターを使用して、
onRouteChange
ハンドラーを設定したいだけ
onRouteChange
。 これが私だけでなく必要であることは注目に値します。 たとえば、多くは、仮想ページビュー情報をGoogleアナリティクスに送信する必要に直面しています。
それはともかく、私はほとんどのサイトのために私自身のルーティングシステムを書くことを好む、それは約17分しかかからず、最終的に、起こることは非常に高速であることが判明したからだ。
URLの変更を傍受するために、URLが変更されるたびに呼び出される次の関数を準備しました。
export const captureCurrentUrl = () => { journey.steps.push({ time: Date.now(), type: INTERACTION_TYPES.URL_CHANGE, data: document.location.href, }); };
私は彼女を2か所で呼びます
history.push()
コマンドを実行してURLを更新するのと同じ場所で、またユーザーがブラウザの[
]ボタンをクリックすると呼び出される
popstate
イベントでも:
window.addEventListener('popstate', () => { // - , captureCurrentUrl(); });
ユーザーアクションの記録
おそらく、これは文字通りどこにでも埋め込む必要があるため、サイトでの作業に関する情報を傍受するための最も「侵入的な」メカニズムです。 それが私の欲望だけに依存していれば、私はこれを気にしません。 しかし、私が思ったように、ユーザーがどこをクリックしたかを正確に知らなければ再現できないエラーに遭遇しました。
いずれにせよ、タスクは面白かったので、ここでその解決策について説明します。 Reactで開発するとき、私は常に
<Link>
および
<Button>
コンポーネントを使用します;その結果、集中クリック代行受信システムの開発は非常に簡単です。
<Link>
見てください:
const Link = props => ( <a href={props.to} data-interaction-id={props.interactionId} // onClick={(e) => { e.preventDefault(); captureInteraction(e); // historyManager.push(props.to); }} > {props.children} </a> );
ここで話しているのは、
data-interaction-id={props.interactionId}
および
captureInteraction(e);
。
セッションを再現するときが来たら、ユーザーがクリックしたものを強調したいと思います。 これを行うには、何らかのセレクターが必要です。 クリックした要素には識別子(
id
)があると自信を持って述べることができますが、何らかの理由で覚えていないので、監視システム用に特別に設計されたものの方が良いと判断しましたユーザーアクティビティ用。
captureInteraction()
関数は次のとおりです。
export const captureInteraction = (e) => { journey.steps.push({ time: Date.now(), type: INTERACTION_TYPES.ELEMENT_INTERACTION, data: { interactionId: e.target.dataset.interactionId, textContent: e.target.textContent, }, }); };
ここで完全なコードを見つけることができます。このコードでは、セッションを再生した後、要素が再び見つかるようにチェックされます。
他の情報と同様に、必要なものを収集して、
journey.steps.push
コマンドを実行します。
スクロール
私が残しているのは、ユーザーが表示しているページのどの部分を正確に知るために、スクロールデータを記録するかだけを伝えることです。 たとえば、ページが一番下に巻き戻されてフォームに入力し始めた場合、スクロールせずにこれを再現してもあまりメリットはありません。
多くの小さなイベントを記録する際にシステムリソースを無駄にせず、
Lodash
を使用するために、連続するすべてのスクロールイベントを1つのイベントに収集します。
const startScrollCapturing = () => { function handleScroll() { journey.steps.push({ type: INTERACTION_TYPES.SCROLL, data: window.scrollY, }); } window.addEventListener('scroll', debounce(handleScroll, 200)); };
このコードの作業バージョンでは 、連続スクロールに関連するイベントは除外されています。
startScrollCapturing()
関数は、アプリケーションが初めて起動されたときに呼び出されます。
追加のアイデア
これは私のプロジェクトで使用されていないアイデアの短いリストです。 おそらく、それらを実装する価値があると思うでしょう。
- Esc、
Tab
、Enter
などのキーボードキーのキーストロークをインターセプトします。
- アプリケーションの作業ウィンドウのサイズ変更に関する情報の記録(スクロール位置を考慮して、発生していることを再現することが重要な場合)。
- 再生中の呼び出しは、スクロール位置をインターセプトする代わりに、エレメントが選択されたときのscrollIntoView()を呼び出します 。
-
localStorage
およびcookies
コピーを作成します(サイトの動作に影響する場合)。
- そして最後に、ユーザーは通常、誰かが入力したものすべて、特にクレジットカード番号、パスワードなどを傍受して保存した場合、それを好まない。 したがって、あなたのサイトで作業している間、彼のアクションがどこかに記録されていることを誰も知らないことが非常に重要です(もちろん、私は冗談を言っていることを理解しています)。
ここでは、記事の元のバージョンを公開した後に追加しました。 いくつかのコメントで述べられていることとは対照的に、この資料で説明されている方法は、個人データのセキュリティまたは保護に関する追加の懸念を引き起こさないことに注意できます。 この場合、すでにユーザーの機密データを使用している場合、そのようなデータの収集と保存に適用される要件は、エラーレポートを準備および送信するときにも適用される必要があります。 たとえば、ユーザーに対応する質問をせずにフォームデータを自動的に保存しない場合、ユーザーに質問せずにエラーレポートを自動的に送信しないでください。 ユーザーの個人データを送信する前に、特別なフィールドに設定されたチェックマークの形でユーザーから同意を得る義務がある場合は、エラーレポートを送信する前に同じことを行う必要があります。 システムに登録するときにアドレス
/signup
にユーザーデータを送信する場合、またはエラーが発生する場合にアドレス
/error
にユーザーデータを送信する場合、特別な違いはありません。 あちこちで最も重要なことは、データを正しく合法的に扱うことです。
おそらく、既に会話を終了していると思われますが、この時点では、ユーザーがサイトで行っていることのみを記録しました。 次に、最も興味深い部分、つまり録音の再生を見てみましょう。
ユーザーアクションの再生
サイトでの作業中にユーザーが実行したアクションの再現については、次の2つの問題について説明します。
- ユーザーアクションを再現してエラーの原因を調査するために使用するインターフェイス。
- サイトコードに埋め込まれ、外部から管理できるメカニズム。
ユーザーアクションを再現するためのインターフェイス
このページでは、ユーザーのアクションを繰り返すために、
iFrame
使用されます
iFrame
では、Webサイトが開き、その上で
journey
オブジェクトに以前に記録されたステップが再生されます。
このページは、エラーが発生したセッションに関する情報をダウンロードし、記録された各ステップをサイトに送信します。サイトは状態を変更し、同じエラーが発生します。
このページを開くと、見苦しいシンプルなインターフェイスが表示されます。その後、iPadで表示されているかのようにサイトが読み込まれます(ここでは、タブレットの通常の写真を使用しました。
これは、記事の冒頭で示したのと同じアニメーション画像です。 ここでそのコードを見つけることができます。

ユーザーセッションを再生するプロセス
[
Next step
]ボタンをクリックすると、
iFrame
に
iFrame.contentWindow.postMessage(nextStep, '*')
構造を使用してメッセージ
iFrame
送信されます。 URLの変更に関連する例外が1つあります。 つまり、同様の状況では、
iFrame
src
プロパティは単に変更されます。 アプリケーションの場合、これは実際にはページ全体の更新であるため、これが機能するかどうかは、ページ間でアプリケーションの状態を転送する方法によって異なります。
わからない場合、
postMessage —
、異なるウィンドウ間の対話を提供するために作成されたWindowオブジェクトのメソッドです (この場合、これはページのメインウィンドウと
iFrame
開かれたウィンドウです)。
実際、ユーザーアクションを再現するページについて言えることはこれだけです。
外部からサイトを管理するためのメカニズム
サイトでの作業中にユーザーアクションを再現するメカニズムは、 playback.jsファイルに実装されています。
アプリケーションの起動時に、リポジトリに分類され、後で呼び出すことができるメッセージを予期する関数を呼び出します。 これは開発モードでのみ行われます。
const store = createStore( // ); if (process.env.NODE_ENV === 'development') { startListeningForPlayback(store); }
これは 、このコードが使用される場所です。
興味のある関数は次のようになります。
export const startListeningForPlayback = (store) => { window.addEventListener('message', (message) => { switch (message.data.type) { case INTERACTION_TYPES.REDUX_ACTION: store.dispatch(message.data.data); break; case INTERACTION_TYPES.SCROLL: window.scrollTo(0, message.data.data); break; case INTERACTION_TYPES.ELEMENT_INTERACTION: highlightElement(message.data.data.interactionId); break; default: // - , return; } }); };
ここでは、そのフルバージョンを見つけることができます。
Reduxアクションを使用する場合、それらはリポジトリにディスパッチされ、それ以上はディスパッチされません。
スクロールが再生されるとき、まさにあなたが期待できることが行われます。 この状況では、ページの幅が正しいことが重要です。 プロジェクトリポジトリを見ると、ユーザーがウィンドウのサイズを変更したり、たとえばサイトが表示されているモバイルデバイスを回転させたりすると、すべてが正しく動作することが
scrollIntoView() —
、
scrollIntoView() —
を呼び出すこと
scrollIntoView() —
、いずれにしても合理的な解決策だと思います。
highlightElement()
関数は、単純に要素の周囲に境界線を追加します。 彼女のコードは次のようになります。
function highlightElement(interactionId) { const el = document.querySelector(`[data-interaction-id="${interactionId}"]`); el.style.outline = '5px solid rgba(255, 0, 0, 0.67)'; setTimeout(() => { el.style.outline = ''; }, 2000); }
いつものように、これはこの関数の完全なコードです。
まとめ
React / Reduxアプリケーションでの簡単なエラー報告システムを見ました。 実際に役立ちますか? これは、プロジェクトに表示されるエラーの数と、それらを見つけるのがどれほど難しいかによると思います。
エラーが発生した場合は、URLを書き留めてその情報を保存しておけば、問題の原因を特定するのに役立ちます。 または、ユーザーアクションを記録するシステムは成功しているように見えますが、サイトとのセッションを再生するページは成功していません。 たとえば、iOSのSafari 9でのみ発生するエラーが発生した場合、セッション再生ページは役に立たなくなります。エラーを繰り返すことはできないからです。
いろいろな研究について話しますが、その中の1つについて話しましたが、実際のプロジェクトの1つで実験の結果として作成されたものを埋め込む準備ができているかどうかを自問するとき、私にとって真実の瞬間が来ます。 この場合、この質問に対する答えはノーです。
いずれにせよ、ユーザーアクションを傍受して再現するシステムで作業することは、私が何か新しいことを学ぶことを可能にする興味深い体験です。 さらに、あるサイトに監視システムを迅速に実装する必要がある場合、いつかこれらすべてが役に立つと信じています。
親愛なる読者! バグはどのように処理しますか? .