とあるプロジェクトにて。
こんなかんじでfirestoreのエラー検知をしようとしているコードがありました。
try {
connectFirestore();
} catch {
window.alert('firebase接続失敗!');
}
try {
watchFirestoreUpdate();
} catch {
window.alert('firebase参照失敗!');
}
try {
updateFirestore();
} catch {
window.alert('firebase更新失敗!');
}
しかし、意図的にエラーを起こして動作確認したところ、、、
検知できてませんでした。
この記事は、firestoreのエラー検知をちゃんとやろうとした、格闘の記録です。 firestoreは使い慣れていないため、変なことをしていたらご指摘いただけるとありがたいです。
## 前提 * 言語はtsです * フロントエンドです * firebaseのバージョンは7です * firestoreをつかって、別システムと連携をするページです * 監視や更新がひっそりと失敗するのは、要件上クリティカルにまずいです
## アウトライン やろうとしたことの一覧と、結果を記載します。
やろうとしたこと | 結果 | 何をした? |
---|---|---|
初回接続エラーの検知 | できた | setTimeout()で接続タイムアウトを検知 |
途中切断の検知 | 一応できたけど挙動が怪しいので導入見送り | setInterval()で定期的に接続状態をチェック |
NW切断の検知 | できた | window.addEventListener('offline')を設定 |
参照エラーの検知 | できた | コールバックを使う |
更新エラーの検知 | できた | コールバックを使う |
## エラー検知、その前に 以降のコードでは、↓を使います。
import firebase from 'firebase';
// 本記事の各所で使う汎用モジュール
let singletonApp: firebase.app.App | null = null;
export const getApp = (): firebase.app.App => {
if (singletonApp === null) {
// 初回だけinitialize (2回やるとduplicateエラーになる)
singletonApp = firebase.initializeApp(/*引数省略*/);
}
return singletonApp;
};
export const getRef = (path?: string): firebase.database.Reference => {
return getApp().database().ref(path);
};
## 初回接続エラーの検知 ページ読み込みの直後に行っている、認証や、firebase.database.Reference取得の失敗を検知できるようにしました。 認証エラーは素直にPromiseで検知できたのですが、reference取得エラーの検知が厄介でした。 接続失敗したときに各種イベントが発火するわけもないため、タイムアウトで検知するようにしました。
// ロード時に実行する関数
// => 認証エラーと接続タイムアウトでalertを出す
export const onLoad = (): void => {
getApp()
.auth()
.signInWithEmailAndPassword('e-mail@gmail.com', 'password1234')
.then(() => {
console.log('firebase: authentication ok');
})
.catch((err) => {
console.log(err);
window.alert('firebase認証失敗')
});
alertOnConnectionTimeout();
};
const alertOnConnectionTimeout = (): void => {
// TIMEOUT_SEC秒経ってもfirebase接続ができなければalertを出す
const TIMEOUT_SEC = 15;
checkConnectionTimeout(TIMEOUT_SEC)
.then(() => {
console.log('firebase: connection ok');
})
.catch((err) => {
console.log(err);
window.alert('firebase接続失敗');
});
};
const checkConnectionTimeout = (timeoutSec: number): Promise<void> => {
return new Promise((resolve, reject) => {
getRef('.info/connected').on('value', (snapshot: any) => {
// 接続したらここに来る
if (snapshot.val() == false) {
return;
}
resolve();
});
window.setTimeout(() => {
// タイムアウトしたらここに来る
reject(new Error('timeout'));
}, timeoutSec * 1000);
});
};
なお、以下はダメでした。
// ダメだったケース
export const onLoad = (): void => {
checkConnectionOnce();
};
const checkConnectionOnce = (): void => {
getRef('.info/connected').once('value', (snapshot: any) => {
// 接続失敗なら、そもそもここに来ない
if (snapshot.val() == false) {
// 接続後、1回目のvalueイベントでここにくる (=正常系でアラートが出てしまう)
window.alert('Firebaseの接続に失敗しました');
return;
}
// 接続後、2回目のvalueイベントでここにくる (=onceだとここに到達しない)
// => よって、↑ではonceではなくonで見ることにした
console.log('firebase: connection ok');
});
};
## 途中切断の検知 自分 <-(a)-> firebase <-(b)-> 他人 のうち、(a)の途中切断を検知しようとしました。 一応、以下のコードで検知できるとは思うのですが、
- devtoolで確認したところメモリリークしているっぽい挙動だった
- 次項のNW切断エラーを入れれば大半のケースで検知できるし、シンプルになる
という理由から、導入はやめました。
(NW切断の検知だけだと、NW正常でfirebase側に障害があった場合は検知できません。とはいえ参照・更新のタイミングでエラー検知できるため、大過ないだろうと考えました)
// ロード時に実行する関数
export const onLoad = (): void => {
// INTERVAL_SEC秒ごとに、firebaseの接続状態をチェック
const INTERVAL_SEC = 30;
setInterval(
checkConnectionTimeout, // 中身は前項を参照
INTERVAL_SEC * 1000
);
};
なお、onDisconnectといういかにも今回のケースに使えそうな名前の関数がありましたが、これは使えませんでした。
// onDisconnect()を使うと、
// 切断時に、firebase側でfirestoreの値を更新してくれる。
// ただ、値を更新をするためのインターフェースしか用意されていないため、
// window.alert()を出す、みたいなことには使えない。
getRef('my/app/path/is_connected').onDisconnect().set(false);
/* 余談
* onDisconnect()による値の更新には罠があります。
* タブを閉じた場合は、即時値が反映されますが、
* NW切断の場合は、3分程たたないと値が反映されません。
* NW切断も含めてリアルタイムに値を反映したい場合は、何かしら別の手段が必要です。
*/
## NW切断の検知 firebaseではなく、ブラウザの機能をつかいました。超簡単。
window.addEventListener('offline', () => {
window.alert('ネットワーク切断');
}, false);
## 参照エラーの検知 [Reference](https://firebase.google.com/docs/reference/js/firebase.database.Reference)を使う場合、promiseではなく、なつかしのコールバックを使ってエラーを検知します。この仕様に気づかずそこそこハマりました。 (改めて型定義を見返すまで気づけなかったです。。。盲点でした。)
const watchMyAppPath = () => {
getRef('my/app/path').on('value', (snapshot) => {
console.dir(snapshot.val());
}, captureWatchError);
};
const captureWatchError = (err: any) => {
if (err) {
console.log(err);
window.alert('firebase参照失敗');
}
};
なお、試してませんが、以下を使えば普通にtry/catchできるかもしれません。
- promisifyを使ってpromise化する
-
ThenableReferenceを使ってみる
(名前から想像するに、thenやcatchを使えるようにしたReferenceと思われます。ドキュメントをみても使い方が分からなかったのと、変更の影響がでかそうだったため導入を見送りました)
## 更新エラーの検知 参照エラーと同じく、コールバックを使いました。
const updateMyAppPath = () => {
getRef('my/app/path/boolean').set(true, captureWatchError);
};
const captureUpdateError = (err: any) => {
if (err) {
console.log(err);
window.alert('firebase更新失敗');
}
};
## まとめ * 異常系もちゃんと動作確認をしよう * .info/connectedを見ればfirebaseの接続状態が分かる * window.addEventListener('offline') でNW切断を検知できる * onDisconnect()でfirebase切断時に値を更新できるが、NW切断だとラグがあるので注意 * firestoreの参照、更新エラーにはコールバック関数を使う
以上です。