2024/3/12 追記
強引に発生確率を下げる方法について、2024年の記事で解説しています。
両記事ともある程度有効な手段かと思いますので、あわせてご参照ください。
フォームのデータを取得できませんでした。に対処したもう1つのシンプルな方法
0.はじめに
2022年12月頃から、Googleフォームの送信時トリガーで
以下のようなエラーが発生し不安定な状態となっているようです。
私の環境でも発生していたため、
一助になれればと思い対応した方法を記載しておきます。
※エラーそのものを発生させなくする方法ではありません。
1.何が起こったのか
フォーム送信時トリガーを設定した時、
送信時作成のイベントから中身を取り出そうとする段階でエラーを吐きます。
エラーメッセージ:Exception: フォームのデータを取得できませんでした。しばらくしてからもう一度お試しください。
ただ毎回起こるわけではなく、不定期に発生するのが厄介です。
2.根本解決の方法はあるのか
調査したところ、根本的に当エラーが発生しなくする方法は見つけられませんでした。
なので一旦「エラーが起きても問題ない形」をご提案します。
3.即時リトライを試みる方法
3-1.コード
//フォーム送信時トリガーを設定
function formRes(e) {
try{ //通常処理
const itemresponses = e.response.getItemResponses(); //ここで対象のエラーが発生する
const address = e.response.getRespondentEmail();
main(itemresponses, address);
}catch(e){
//エラー時
console.log(e);
//対象のフォームを取得
const form = FormApp.getActiveForm();
//対象フォームの回答を過去分から全て取得する
const allItemresponses = form.getResponses();
//最新分は配列末尾に入っているため最新分を取得
/** 精度重視であれば、allItemresponsesの中身をgetTimestamp()して日次比較、最新分を取得する方がベターでしょう。*/
const recentResponse = allItemresponses[allItemresponses.length-1];
//recentResponseは、tryの中のe.responseと似た扱いができる
const itemresponses = recentResponse.getItemResponses();
const address = recentResponse.getRespondentEmail();
main(itemresponses, address);
}
}
function main(itemresponses, address){
//フォーム回答を使って行いたい処理
//メール送信するなりチャットツールに通知するなりデータ加工して二次利用するなり
console.log("main");
}
3-2.解説
エラーが起きるところをtryに入れ、該当のエラー発生時にcatchします。
catchしたら無理に送信時トリガーでのフォーム情報を使わず、
formオブジェクト.getResponses()で対象フォームの回答全件を取得します。
そこから最新の回答を絞り込み、本来処理に持ち込む形です。
3-3.デメリット
3-3-1.ほぼ同時に複数の回答があった場合の精度が保証できない
リトライ処理中に他の回答があった場合、
最新側を取得して処理に入ってしまう可能性があります。
3-3-2.即時でない
当然、本来のフォーム送信時トリガーよりは反応が遅くなってしまいます。
3-3-3.リトライ処理自体も失敗することがある
このコードをテストしている時、リトライ処理自体も1回エラーを吐きました。
失敗した直後のcatchでリトライ処理を試みると、
結構な確率でFormにアクセスする段階でエラーを吐く印象です。
「リトライ処理自体は失敗しても5回まで繰り返す」等の対策があった方が良さそうです。
3-4.類似の方法
似ている方法として、フォーム回答を蓄積するスプレッドシートにGASを仕込み、
シート更新に応じて最新データを取得、処理する方法があります。
デメリットとしては、フォームの設問内容を追加/削除した際、
過去の質問項目もシートに残るため、正確に情報を抜き出すことが難しそうです。
「回答があったよ!」等の情報だけが必要であれば、これで良いかもしれませんね。
4.日次で処理漏れを検知、再実行する方法
4.1コード
//フォーム送信時トリガーを設定
function formRes(e) {
try{
//通常処理
const itemresponses = e.response.getItemResponses();
const address = e.response.getRespondentEmail();
main(itemresponses, address);
//ログシートを取得できれば方法は何でもいい
const logSheet = SpreadsheetApp.openById("SSのID").getSheetByName("フォーム後処理ログ");
const lastRow = logSheet.getLastRow();
const responseId = e.response.getId();
const timeStamp = e.response.getTimestamp();
//ログ書き込み
logSheet.getRange(lastRow+1,1,1,2).setValues([timeStamp, responseId]);
}catch(e){
//失敗時 一応タイムリーにエラーキャッチしたいならメール飛ばす等を実装
}
}
function main(itemresponses, address){
//フォーム回答を使って行いたい処理 メール送信するなりチャットツールに通知するなりデータ加工して二次利用するなり
console.log("main");
}
//昨日分の履行をチェックする 日次トリガーにする
function dailyCheck(){
//昨日のdayjsオブジェクト
const yesterday = dayjs.dayjs().subtract(1,"day");
//昨日の回答を全件取得する
//ライブラリdayjsを使用しているが、timestampが昨日であるかを確認できれば方法は何でもいい
const form = FormApp.openById("フォームID");
const yesterdayResponses = form.getResponses()
.filter(x => dayjs.dayjs(x.getTimestamp()).isSame(yesterday,"day"));
//昨日のフォーム回答が0件だったらここで処理終了
if(yesterdayResponses.length==0) return;
//ログシートからログを全件取得し、空白を除外、A列に入力されているtimestampが昨日のものを抽出
const logSheet = SpreadsheetApp.openById("シートID").getSheetByName("フォーム後処理ログ");
const logs = logSheet.getRange("A1:A").getValues().filter(x => x)
.filter(x => dayjs.dayjs(x[0]).isSame(yesterday,"day"));
//ログシートの情報と、フォームから直接取得した昨日の回答全件を照らし合わせ、
//ログシートに記載のないものを抽出する=送信時にtryが失敗していたものを抽出
const targets = yesterdayResponses.filter(x => !logs.find(y => y[1]==x.response.getId()));
if(targets.length == 0) return;
//メイン処理を実行する
for(let target of targets){
let itemresponses = target.getItemResponses();
let address = target.getRespondentEmail();
main(itemresponses, address);
}
}
4-2.解説
仕組み自体が少々まどろっこしいかもしれません。ゆっくり読んでください。
4-2-1.実行ログを取る
まず、本来の送信時トリガーでの処理時に必ず実行ログを取るようにします。
発動時に取得するイベントオブジェクト(e)から、
「回答ID」を取得してログシートに入力する仕組みです。
※シートはどこでもいいですが、回答記録されるSS上にシートを作っておくと管理上楽でしょう。
4-2-2.ログシートの内容と、フォーム回答全件を比較する
さて、そうすると、当該エラーを吐いた時には「回答ID」はログには残りません。
この状況を利用します。
毎日0時~1時に、対象フォームの昨日分回答を全件取得して、
その内容とログシートの内容が一致していれば、
エラーは発生していなかったという事になります。
逆に一致していなければ、ログ記録未実行=失敗しているものがあります。
コードと一緒に見ていきましょう。
4-2-3.実行が漏れていた分の処理を実行する
エラーで処理が実行されていなかった分は、本来処理を改めて実行しましょう。
実行するための回答情報は、前項で取得した未実行分の「回答ID」を使って取得します。
コードですと、この部分になります。
フォーム送信時トリガーと扱いが同じオブジェクトを取得できちゃいます。
4-3.メリット・デメリット
4-3-1.【メリット】即時リトライよりは精度が上がる
ほぼ同時に複数の回答があった場合の、回答の取り違えが発生しません。
これは項3で紹介した方法より良い部分ですね。
4-3-2.【デメリット】回答からのタイムラグが大きい
今回のケースですと、分かりやすく日次トリガーを設定して
前日分の動作に問題が無かったかを検知しますので、遅いです。
午前9時にエラーを吐いた場合、その回答への対応は翌日になります。
フォームの用途上、タイムラグが許されない場合は
数時間毎に検知できるように工夫してみたり、
エラー時にメール等で通知し日中帯は即時フォローできるようにするなど、
工夫をすればある程度カバーできる問題かと思います。
4-3-3.【デメリット】日次処理自体が失敗する可能性がある
項3の方法で触れましたが、こちらでも似たような処理を実行しているため
何度か実行を試みるような処理を追加した方が無難だと思います。
4-3-4.【デメリット】処理が煩雑になる
最後はやっぱりこれに尽きますね。地味に面倒くさい。
「修正してくれ~」と思ってしまいますね。
ですが、GASやSaaS系ツールはサーバー側の問題で不安定になりますから、
それも見越して厚めのエラー処理を当たり前に仕込むべきなのかもしれませんね。
5.さいごに
いかがでしたでしょうか。
私はというと、2つ目の方法を採用して日次で管理しています。
ちなみに私の環境では送信時トリガーのフォームを20個ほど管理していますが、
昨年12月中旬からポツポツとエラーが発生し、2023/1/16以来発生していません。
皆さんはいかがでしょうか?よければコメントください。
⇒2023年2月現在、やはりたまに発生するようです。
もしかすると解消されたのかもしれませんが、
Google側が不安定になると同様の事が起こるかもしれません。
時間がある際に対処しておく事をおすすめします。
また、強引にエラーハンドリングを行う方法として他でも使えるかもしれません。
参考にしていただけると幸いです。