【GAS】貢献度を目に見えるカタチに。Slackで受け取った特定のスタンプ数を集計しチャンネルに通知する
ことはじめ
こんにちは!アスエネ株式会社でバックエンドエンジニアをしています、ハルピンです!
皆さんの会社や組織では、Valueの浸透度や貢献度について、どのように見える形で測っていますか?
元々、上記記事を参考とさせていただきながら、Slack上の発言に対してついたValueスタンプを1週間分集計し、Slackでお知らせするアプリを運用していました。
突然の、エラー
ただ、 ある日を境に、正常に処理ができなくなってしまいました。
コードは特に変更していないのに、GASの実行処理中にエラーが出て途中で処理が終わってしまいます。なぜに…。
なぜエラーになるのか?
エラーログを見てみると、「Exceeded maximum execution time
」と表示されていました。
GASには通称「30分の壁」というのがあり、実行時間の上限である30分以内で処理を終えないといけなく、それを超えると処理が終了してしまうそうです。
※無料プランだと6分が上限で、「6分の壁」となるそうです。
以前まではポスト数も制限時間内で処理できてましたが、組織規模が大きくなっており、
ポスト数が増えている関係で、時間内に処理できなくなってしまいました…。
また、ポストの情報を取得するAPI、具体的にはconversations.historyやconversations.replies
というAPIは、一定期間の投稿を取得する際にはリクエストの制限値が定められています。
そこでポスト取得を意図的に遅らせるよう処理をしていたのですが、こちらも実行時間がオーバーしてしまった原因でした。
リクエストを遅らせるとエラーは起きなくなるけど、実行時間の上限を超えてしまう。
この問題を解決するためには、組織の拡大とともに投稿数が増えている現状を考慮し、既存の処理方法を見直す必要があることがわかりました。
対応したこと
GASの実行する処理のフローを見直しました。
結論から伝えますとこれまで行っていた、
- 1週間をまとめて集計し、Slackで通知する
形から、
1) 1週間をまとめて処理せず、1日ずつ集計処理する
2) 1週間分の集計をし、Slackで通知する
方式に変更することにしました。
1)については、先述の記事を元にしながら
- 投稿の取得期間を、1日に限定する
- 1回のリクエストごとインターバルを設定し、1分で50回以上送らないようにする
ように、実装を修正しました。
1日分集計の関数を実行すると、202X-XX-XXvalue_getのようなファイル名で、このように出力されます。
2)については、こちらのように実装しました。
gasコード
// 週を指定するための定数(カレンダーは日曜始まりの前提)
const THIS_WEEK = 0;
const LAST_WEEK = 1;
const TWO_WEEKS_AGO = 2;
function execWeeklyAmountAndSendSlack() {
var today = Utilities.formatDate(new Date(), "JST", "yyyy-MM-dd");
var targetWeek = LAST_WEEK;
var weekDescription = getWeekDescription(targetWeek);
var amountSheetName = today + '-' + weekDescription + '-' + '集計';
Logger.log('集計シート作成_処理開始')
createWeeklyAmountSheet(amountSheetName, targetWeek)
Logger.log('集計シート作成_処理終了')
Logger.log('集計シート整理_処理開始')
calcVaueSumAndRearrangementSheet(amountSheetName)
Logger.log('集計シート整理_処理終了')
Logger.log('slack送信開始');
generateRankingAndSendSlack(amountSheetName);
Logger.log('slack送信完了');
Logger.log('処理完了!!');
}
function getSpecificSheets(targetWeek) {
var sheetNames = getSheetNamesForWeek(targetWeek);
var sheets = [];
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
for(var i in sheetNames) {
var sheet = spreadsheet.getSheetByName(sheetNames[i]);
if(sheet) {
sheets.push(sheet);
} else {
Logger.log('Sheet not found: ' + sheetNames[i]);
}
}
return sheets;
}
//集約シート作成(集計処理はまだ)
function createWeeklyAmountSheet(amountSheetName, targetWeek) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const new_sheet = ss.insertSheet();
new_sheet.setName(amountSheetName);
new_sheet.activate();
ss.moveActiveSheet(1);
var sheets = getSpecificSheets(targetWeek);
for(let i = 0; i < sheets.length; i++) {
let sheet_name = sheets[i].getName();
let values = sheets[i].getRange(2, 1, sheets[i].getLastRow() - 1, sheets[i].getLastColumn()).getValues(); //1行目は不要な情報(取得シート名)なため調整で-1
for(let j = 0; j < values.length; j++) {
values[j].unshift(sheet_name);
}
let range = new_sheet.getRange(new_sheet.getLastRow() + 1, 1, values.length, values[0].length);
range.setValues(values);
}
}
// 1週間のシートを集計しスプシに出す
function calcVaueSumAndRearrangementSheet(amountSheetName) {
var fileName = SpreadsheetApp.getActiveSpreadsheet();
var sheetName = fileName.getSheetByName(amountSheetName);
// 20XX-XX-XXvalue_getの取得したい列の範囲を引数に記載する
var data = sheetName.getRange("B:M").getValues();
var sum = {};
for(var i = 0; i < data.length; i++) {
var name = data[i][0];
if(sum[name] === undefined) {
sum[name] = data[i].slice(1);
} else {
for(var j = 1; j < data[i].length; j++) {
sum[name][j - 1] += data[i][j];
}
}
}
var sortedKeys = Object.keys(sum).sort(function(a, b) {
var aSum = sum[a].reduce(function(p, c) {
return p + c;
}, 0);
var bSum = sum[b].reduce(function(p, c) {
return p + c;
}, 0);
return bSum - aSum;
});
// 多い順にスプシを並び変える
sheetName.clearContents();
var values = ['integrity', 'owner_ship', 'go_fast', 'stay_hungry', 'moon_shot', 'think_positive', 'kaizen', 'technology', 'jiseki', 'for_the_team'];
values.unshift('slack_name', '合計');
sheetName.getRange(1, 1, 1, values.length).setValues([values]);
for(var i = 0; i < sortedKeys.length; i++) {
var key = sortedKeys[i];
var values = sum[key];
sheetName.getRange(i + 2, 1).setValue(key);
for(var j = 0; j < values.length; j++) {
sheetName.getRange(i + 2, j + 2).setValue(values[j]);
}
}
}
/**
* 月曜から金曜までのシートの配列を作る
* ex:
* [ '2023-05-22value_get',
'2023-05-23value_get',
'2023-05-24value_get',
'2023-05-25value_get',
'2023-05-26value_get' ]
**/
function getSheetNamesForWeek(weeksAgo) {
let sheetNames = [];
// 今日の日付を取得
let today = new Date();
// weeksAgoに基づいて週の月曜日を取得
let monday = new Date();
monday.setDate(today.getDate() - today.getDay() + (today.getDay() == 0 ? -6 : 1) - (7 * weeksAgo));
// 月曜から金曜までの日付を配列に追加
for(let i = 0; i < 5; i++) {
let currentDate = new Date(monday);
currentDate.setDate(monday.getDate() + i);
let formattedDate = Utilities.formatDate(currentDate, "JST", "yyyy-MM-dd");
sheetNames.push(formattedDate + "value_get");
}
return sheetNames;
}
function getWeekDescription(weekNumber) {
switch(weekNumber) {
case THIS_WEEK:
return '今週';
case LAST_WEEK:
return '先週';
case TWO_WEEKS_AGO:
return '2週前';
default:
return '未定義の週番号';
}
}
// 順位を算出。
// ランキングの結果は常に rank, name, score の順序としている
// 7位まで算出しているが、同率があることを考慮されている
function getRanking(sheetName) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
var data = sheet.getRange('A2:B' + sheet.getLastRow()).getValues();
data.sort(function(a, b) {
return b[1] - a[1]; // sort by score
});
var ranking = [];
var rank = 1,
prev_score = null;
for(var i = 0; i < data.length; i++) {
if(data[i][1] !== prev_score) {
rank = ranking.length + 1;
prev_score = data[i][1];
}
//ここだけ修正すれば、何位までをslackに送るか設定できる
if(rank > 7) {
break;
}
ranking.push([rank, data[i][0], data[i][1]]);
}
return ranking;
}
function generateRankingAndSendSlack(amountSheetName) {
var fileName = SpreadsheetApp.getActiveSpreadsheet();
var ranking = getRanking(amountSheetName);
let msg = '👑 先週のvalue獲得ランキング'
msg += '\n======================';
let medals = [':first_place_medal:', ':second_place_medal:', ':third_place_medal:'];
// 3位以下は動的に7位になるまで取得。6位が複数あれば6位までの人を集計
for(let i = 3; i < ranking.length; i++) {
medals[i] = ` ${i+1}th `;
}
for(let j = 0; j < ranking.length; j++) {
msg = msg + "\n" + medals[ranking[j][0] - 1] + ranking[j][1] + ': ' + ranking[j][2] + "回 \n";
}
msg += '======================\n';
ss_url = fileName.getUrl();
msg += '詳細はこちら: ' + ss_url;
const channelId = PropertiesService.getScriptProperties().getProperty('CHANNEL_ID’); // チャンネルID
const url = 'https://slack.com/api/chat.postMessage'
try {
const payload = {
'token': PropertiesService.getScriptProperties().getProperty('BOT_TOKEN’), // botのトークン
'channel': channelId,
'text': msg
};
const params = {
"method": "post",
"payload": payload
};
const res = UrlFetchApp.fetch(url, params);
const data = JSON.parse(res.getContentText());
if(data.error) {
throw "POST message error: " + data.error;
}
} catch(e) {
Logger.log(e);
return "err: " + e;
}
}
ざっくり解説すると、上記のexecWeeklyAmountAndSendSlack関数を実行することで、以下の処理が行われます。
a) 先週分の集計シートを作成する
b) 集計シートに、先週分のデータをコピーする
c) 1人あたりが獲得したValue10種類分の、合計が多い順に並び替えする
d) 上位7名を抽出してslackのチャンネルに投稿する
これらの処理は日毎の処理は毎日、週間の集計と通知は月曜日のお昼に実行されるよう、GASのトリガーで設定しました。自動化万歳ですね!
これにより、以前はボトルネックになっていた、時間制限とリクエスト制限の両方の問題を解決できるようになりました!!
月曜日のお昼になると・・・
はい!このように通知されました!いい感じです!(同率3位もいい感じに表示されてますね✨)
やってみて
久しぶりに実際に投稿すると、自分が思っていたより反響がありました。
- 多くバリューをもらえた方に対し、同僚を讃えあう姿があった
- 貢献度の高いメンバーに対して、部署を超えて全社的な場でスポットライトが当たるようになった
ことがあり、これらのポジティブな反響や変化を間近で感じとれることができたことは、実装者としては嬉しいですね!
今回の記事が、同様なお悩みを持つ方に届き、役に立てることができれば幸いです。
ぜひ参考にしてください!
最後になりますが、アスエネでは一緒に働くエンジニアを募集しています。
まずは、選考要素のないカジュアル面談も歓迎しています。
僕のMeetyも記載しておきますね。
気候変動問題の解決、そして未来に誇れる仕事に関心や共感のある方は、ぜひお話を聞きに来てください!
Discussion