いつも短パン姿で会社に彷徨いているイツキでござる〜
Mediumとかでメモみたいな記事を書くことはちょこちょこありますが、こんな真面目なところで共有するのが初めてなので、
お手柔らかく〜
その前に
コラボフローにGoogle Analyticsが入ってないことに文句言いたいと思ったことのある人、挙手〜
なので、以下の2本立てでいきたいと思います。
- 文書のモニタリング:ユーザー別、経路・文書別の利用状況の可視化!
- アクション(判定)のモニタリング:自分の判定段階に届くから判定するまでどれくらい経ったのが?ユーザー別、経路・文書別で見れる!
(モニタリングなんか優しい言葉ではなく、もはや監視)
+アルファ
- スマイル申請・判定:流石に上の二つだけだと真面目すぎて、面白くないなあと思って、「お菓子をくれないとイタズラするぞ」をちなんで、「笑顔くれないと判定も申請もさせないぞ」!です
概要
二本立てと言っても、流れは同じです。
- Javscriptカスタマイズで必要なデータを取得する
- Lambdaを通して、DynamoDBに書き込む
- QuickSightを使って、データをビジュアライズする
文書のモニタリング
最終的みたいものとしてはユーザー別、経路・文書別の利用状況を知りたいので、まずJavascriptを使って文書申請確定時(request.confirm.apply)に以下の情報を取得します。
- イベント
request.confirm.apply
に入っている情報:-
app_cd
:アプリケーションコード -
document_id
:文書ID(申請ごと異なる) -
process_id
:経路ID(経路別なので、同一経路、同一番号) -
title
:文書タイトル
-
-
collaboflow.getLoginUser()
で取得する情報:-
username
:申請するユーザー名 -
userid
:申請するユーザーID
-
-
new Date()
で申請日時をゲット:-
request_date
:申請日時
-
Javascriptのコードをお見せする前、まずDynamoDBとLambdaをセットアップしましょう。
DynamoDB
DynamoDBに新しいTabledocument_monitoring_Demo
作って、各Attributeは下のイメージを参考に。
Lambda
Lambdaに一つNode.Js16か14をRuntimeとしたFunctionを作ります。AWSのJavascriptSDKは18に対応してないので、お間違いなく。
コラボフローのJavascript APIから実行できるように、FunctionURL
をチェックして、AuthType
をNONE
とします。
作り終わりましたら、Configuration
からExcution role
を特定して、DynamoDBへのアクセス権限を与えます。
使っているLambdaはこちら:
const AWS = require('aws-sdk');
const docClient = new AWS.DynamoDB.DocumentClient();
const tableName = "document_monitoring_Demo"
exports.handler = async (event) => {
let body = event.body;
let bodyObject = JSON.parse(body);
let appCd = bodyObject["app_cd"];
let documentId = bodyObject["document_id"];
let documentTitle = bodyObject["title"];
let processId = bodyObject["processes_id"];
let requestDate = bodyObject["requestDate"];
let userId = bodyObject["userid"];
let userName = bodyObject["name"];
let params = {
TableName: tableName,
Item: {
id: `${appCd}_${documentId}_${userId}_${requestDate}`,
appCd,
documentId,
documentTitle,
processId,
requestDate,
userId,
userName
}
};
try {
await docClient.put(params).promise();
console.log("success");
return { body: 'Successfully created item!' };
} catch (err) {
console.log("error: ", err);
return { error: err };
}
};
カスタマイズJavscript
使っているJavascriptはこちら:
url
は上のLambdaのFunctionURLに書き換えてくださいませ〜
(function() {
'use strict';
collaboflow.events.on('request.confirm.apply', function (e) {
const now = new Date();
const now_string = now.toISOString();
// ログインしているユーザーの情報を取得する
const loginUserInfo = collaboflow.getLoginUser();
// POSTリクエストを送信
let url = "your_lambda_function_url";
let headers = {};
let data = {
...loginUserInfo,
...e,
"requestDate": now_string
};
let parseType = "json";
collaboflow.proxy.post(url, headers, data, parseType).then(function (response) {
console.log(response.status);
console.log(response.body);
});
});
})();
上のJavascriptをモニタリングしたいフォームに追加したら、セットアップ完了!
文書内容(パーツ)、経路依存ではないので、現有のものにそのまま追加したら使えます!
いざ、試しへ
そして、DynamoDBで確認したら、以下のItemが無事追加されました!
QuickSight
DynamoDBのItem登録を無事確認したところで、QuickSightを使って、登録したデータを見てみましょう〜
DynamoDBはNoSQLデータベースのため、Athena経由でデータを取得して、QuickSightで可視化するという流れになります。具体的なセットアップ手順はこちらの記事にてご確認いただければと思います。
こちらは上に登録した文書申請データのみですが、例えば、下のテーブルのようなユーザーのMetaDataをDynamoDBに登録すれば、役職や部署で絞ることもできます。
氏名 | ユーザーID | 部署 | 役職 |
---|---|---|---|
山田太郎 | taro | 営業部 | 部長 |
もちろん、毎回DynamoDBからcsvダウンロードして、Excelでみるのもありですが、QuickSightを使うことで、その手間がなくなる上、(ほぼ)RealTimeのデータを確認することができます。
判定のモニタリング
ユーザーの判定が届いてから判定するまでかかった時間を知りたいので、まずJavascriptを使って判定確定する時(request.judgement.*)に上の文書モニタリング時に取得しているものの他、以下の情報も併せて取得します。
- 判定時のイベントから
-
actionType
: 確認、承認、却下、差し戻し
-
- コラボフローREST API:
/v1/documents/{document_id}/determs
-
previousActionDate
: 前の段階の判定日時 -
phaseNumber
: 判定段階番号 -
timeDifference
: 今の日時(currentActionDate
)とpreviousActionDate
の時間差(ms)
-
DynamoとLambdaのセットアップは上と同じなので、細かいところはスキップさせていただきます。
- DynamoDBに新しいTable
action_monitoring_demo
を作成 - 新しいLambdaを以下を作成。コードは以下のものを使ってください
index.js
const AWS = require('aws-sdk'); const docClient = new AWS.DynamoDB.DocumentClient(); const tableName = "action_monitoring_demo" exports.handler = async (event) => { let body = event.body; let bodyObject = JSON.parse(body); let appCd = bodyObject["app_cd"]; let documentId = bodyObject["document_id"]; let documentTitle = bodyObject["title"]; let processId = bodyObject["processes_id"]; let currentActionDate = bodyObject["currentActionDate"]; let previousActionDate = bodyObject["previousActionDate"]; let userId = bodyObject["userid"]; let userName = bodyObject["name"]; let phaseNumber = bodyObject["phaseNumber"]; let timeDifference = bodyObject["timeDifference"]; let actionType = bodyObject["actionType"]; let params = { TableName: tableName, Item: { id: `${appCd}_${documentId}_${phaseNumber}_${userId}`, appCd, documentId, documentTitle, processId, currentActionDate, previousActionDate, userId, userName, phaseNumber, timeDifference, actionType } }; try { await docClient.put(params).promise(); console.log("success"); return { body: 'Successfully created item!' }; } catch (err) { console.log("error: ", err); return { error: err }; } };
そしてそして、下記のJavascriptをモニタリングしたいフォームに追加。
url
は上のLambdaのFunctionURLに書き換えてくださいませ〜
(function() {
'use strict';
function getRequestDate(appCd, documentId) {
return collaboflow.api.get(`/v1/documents/${documentId}`, {
"app_cd": appCd,
}).then(function(response) {
// HTTPステータスと応答ボディ
console.log(response.status, response.body);
// API応答がエラーとなった場合の処理
if(response.body.error) {
console.log('失敗', response.body.messages);
return;
}
const body = response.body;
const requestDateString = body.request_date;
console.log(requestDateString);
// const requestDateStamp = new Date(requestDateString).getTime();
// console.log(requestDateStamp)
return requestDateString;
})
.catch(function(err) {
console.log(err);
});
}
function handleJudgement(e, actionType) {
const now = new Date();
const now_string = now.toISOString();
const documentId = e.document_id;
const appCd = e.app_cd;
const loginUserInfo = collaboflow.getLoginUser();
const username = loginUserInfo.name;
var currentPhaseNumber = 0;
// コラボフローAPIで申請書情報を取得
collaboflow.api.get(`/v1/documents/${documentId}/determs`, {"app_cd": appCd,})
.then(function(response) {
// API応答がエラーとなった場合の処理
if(response.body.error) {
console.log('失敗', response.body.messages);
return;
}
const body = response.body;
const records = body.records;
var currentRecord = records.filter(function (r) {
return (r.current == true && r.determ_username == username) || (r.determ_status == "unconfirm" && r.determ_username == username)
});
if (currentRecord.length === 0) {
return
}
var shouldGetRequestDate = false;
var previousActionDate = undefined;
currentPhaseNumber = currentRecord[0].phase_number;
if (currentPhaseNumber == 1) {
shouldGetRequestDate = true
} else {
for (let i = currentPhaseNumber - 1; i >= 0; i--) {
var previousRecords = records.filter(function (r) {
return (r.phase_number == i)
});
previousRecords.sort((a, b) => {
let determDateA = new Date(a.determ_date).getTime();
let determDateB = new Date(b.determ_date).getTime();
return determDateB - determDateA;
});
if (previousRecords.length === 0 || previousRecords[0].determ_date === undefined || previousRecords[0].determ_date == "") {
continue
} else {
previousActionDate = previousRecords[0].determ_date;
break;
}
}
if (previousActionDate === undefined) {
shouldGetRequestDate = true
}
}
if (shouldGetRequestDate === true) {
return getRequestDate(appCd, documentId);
} else {
return previousActionDate
}
})
.then(function (previousActionDate) {
let timeDifference = now.getTime() - new Date(previousActionDate).getTime()
let url = "lambda_function_url";
let headers = {};
let data = {
...loginUserInfo,
...e,
"previousActionDate": previousActionDate,
"currentActionDate": now_string,
"phaseNumber": currentPhaseNumber,
"timeDifference": timeDifference,
"actionType": actionType
};
let parseType = "json";
collaboflow.proxy.post(url, headers, data, parseType).then(function (response) {
console.log(response.status);
console.log(response.body);
});
})
.catch(function(err) {
console.log(err);
});
}
// 申請内容確認ボタンクリックイベント
collaboflow.events.on('request.judgement.accept', function (e) {
handleJudgement(e, "accept");
});
collaboflow.events.on('request.judgement.reject', function (e) {
handleJudgement(e, "reject");
});
collaboflow.events.on('request.judgement.remand', function (e) {
handleJudgement(e, "remand");
});
collaboflow.events.on('request.judgement.confirm', function (e) {
handleJudgement(e, "confirm");
});
})();
同じくフォームや経路依存ではないので、現有のものにそのまま追加しっちゃても大丈夫です。
QuickSightを使ってDynamoDBに登録したデータを可視化する手順は上と同じになりますので、スキップさせていただきます〜
注意点
このアルゴリズムを使った判定モニタリングは幾つカバーできていない点がありますので、過信は禁物〜
あくまで参考情報として見ていただければ幸いです。
- 前の段階の判定日時を取得するとき、仮に前の段階が必須でない回覧かつ確認済みの場合、その日時が
PreviousActionTime
として使われてしまいます- 解決するには各フォームに使われているJsに対応する経路の情報をMAPやDict
{phaseNumber}: {経路判定情報}
として渡して、日時を取得する時を参照する - (デメリット:Jsが使いまわせなくなる、一つ一つ設定する必要があります)
-
Dict例
{ "1": "AND", "2": "CONFRIM_NOT_REQUIRED", "3": "CONFRIM_REQUIRED }
- 解決するには各フォームに使われているJsに対応する経路の情報をMAPやDict
- 差し戻しや取戻しの場合、一つ前の判定履歴取得できないため、差し戻された時点を
PreviousActionTime
として設定することができなく、あくまで、前段階の判定日時をPreviousActionTime
として使っています。- 今のREST APIだとこれは限界ですが、もしもっともっと使いたいという方々からの要望が多くなれば、一つ前の判定履歴を取得できるAPIを追加するかも?
+アルファ:スマイル申請・判定
言葉だけだと何をやりたいかイメージしにくいと思いますので、まず完成したものをお見せします。
(2回目の申請をする前、写真Retake忘れてしまいました。。。お気になさらず。。。)
正直、この部分DeepLearningとかAIに絡んでくるので、ここでは説明仕切れないような気がしますので、とりあえず、ロジックをスキップして、再現できるよう重要なところだけカバーします。
- 以下の権限のあるAWSのIAM Roleを作成
- AmazonSageMakerFullAccess
- AmazonS3FullAccess
- AmazonElasticContainerRegistryPublicFullAccess
-
こちらのJupyter Notebookをダウンロードして、実行することで、SageMaker Endpointを作成する。具体的な説明が欲しい方こちらで〜
- 注意点:
role_arn
を上のものに、endpoint_name
をsmileDemoTesorflow
に変更する
- 注意点:
- こちらのRepositoryをダウンロードし、
cdk deploy
することで、LambdaのFunctionを作成する - LambdaのConsoleに行って、
- 下記のJavascriptをイタズラしたいフォームに追加(同じくフォームや経路依存ではないです〜)。
url
は上のLambdaのFunctionURLに書き換えてくださいませ〜
(function() {
'use strict';
function openCam(){
let All_mediaDevices=navigator.mediaDevices
if (!All_mediaDevices || !All_mediaDevices.getUserMedia) {
console.log("getUserMedia() not supported.");
return;
}
All_mediaDevices.getUserMedia({
audio: false,
video: true
})
.then(function(vidStream) {
var video = document.getElementById('video');
if ("srcObject" in video) {
video.srcObject = vidStream;
} else {
video.src = window.URL.createObjectURL(vidStream);
}
video.onloadedmetadata = function(e) {
video.play();
};
})
.catch(function(e) {
console.log(e.name + ": " + e.message);
});
}
function stopCam() {
var video = document.getElementById('video');
var stream = video.srcObject;
stream.getTracks().forEach(function(track) {
track.stop();
});
}
function takePhoto() {
console.log("taking photo");
var canvas = document.getElementById('canvas');
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
let image_data_url = canvas.toDataURL('image/jpeg');
// data url of the image
// console.log(image_data_url);
}
function createHtml() {
var el = document.createElement("div");
el.id = "popup";
el.setAttribute("style","position:fixed;top:2%;right:2%;width:200px;height:400px;background-color:gray");
var p = document.createElement("p");
p.id = "smile"
p.innerHTML = "smile"
p.setAttribute("style","background-color:white;padding:5%");
el.appendChild(p);
var video = document.createElement("video");
video.id = "video"
video.setAttribute("style","position:relative;top:0%;width:100%;height:45%;background-color:yellow;autoplay");
el.appendChild(video);
var takePhotoButton = document.createElement("button");
takePhotoButton.id = "takePhotoButton";
takePhotoButton.setAttribute("style","position:relative;height:8%");
takePhotoButton.innerText = "take photo";
takePhotoButton.addEventListener("click", takePhoto);
el.appendChild(takePhotoButton)
var canvas = document.createElement("canvas");
canvas.id = "canvas"
canvas.setAttribute("style","position:relative;width:100%;height:45%;background-color:yellow");
el.appendChild(canvas);
document.body.appendChild(el)
}
function postPhoto(image_base64) {
let url = "lambda_url";
let headers = {
'Accept': 'application/json',
'Content-Type': 'multipart/form-data',
};
let parseType = "json";
return collaboflow.proxy.post(url, headers, image_base64, parseType).then(function (response) {
if (response.status == 200) {
let result = response.body
if (result == true) {
alert("素敵な笑顔ありがとう!");
stopCam();
return true
} else {
alert("笑って〜");
return false
}
}
else {
console.log(response.status);
console.log(response.body);
stopCam();
return true
}
});
}
// 申請
collaboflow.events.on('request.confirm.show', function (e) {
console.log("request.confirm.show");
createHtml();
openCam();
});
collaboflow.events.on('request.confirm.apply', function (e) {
let image_base64 = document.getElementById('canvas').toDataURL('image/jpeg').replace(/^data:image\/jpeg;base64,/, "");
return postPhoto(image_base64);
});
// 判定
collaboflow.events.on('request.judgement.show', function (data) {
createHtml();
openCam();
});
// 申請内容確認ボタンクリックイベント
collaboflow.events.on('request.judgement.accept', function (e) {
let image_base64 = document.getElementById('canvas').toDataURL('image/jpeg').replace(/^data:image\/jpeg;base64,/, "");
return postPhoto(image_base64);
});
collaboflow.events.on('request.judgement.reject', function (e) {
let image_base64 = document.getElementById('canvas').toDataURL('image/jpeg').replace(/^data:image\/jpeg;base64,/, "");
return postPhoto(image_base64);
});
collaboflow.events.on('request.judgement.remand', function (e) {
let image_base64 = document.getElementById('canvas').toDataURL('image/jpeg').replace(/^data:image\/jpeg;base64,/, "");
return postPhoto(image_base64);
});
collaboflow.events.on('request.judgement.confirm', function (e) {
let image_base64 = document.getElementById('canvas').toDataURL('image/jpeg').replace(/^data:image\/jpeg;base64,/, "");
return postPhoto(image_base64);
});
})();
ぜひ遊んでみてください!
楽しんでいただけますでしょうか?
Adventはまたまたここからなので、
どうぞよろしくお願いいたします〜