3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

コラボフローAdvent Calendar 2023

Day 1

コラボフローモニタリングの二本立て!+イタズラのアルファ

Last updated at Posted at 2023-11-30

いつも短パン姿で会社に彷徨いているイツキでござる〜
Mediumとかでメモみたいな記事を書くことはちょこちょこありますが、こんな真面目なところで共有するのが初めてなので、
お手柔らかく〜

その前に

コラボフローにGoogle Analyticsが入ってないことに文句言いたいと思ったことのある人、挙手〜 :raised_hand:

なので、以下の2本立てでいきたいと思います。

  • 文書のモニタリング:ユーザー別、経路・文書別の利用状況の可視化!
  • アクション(判定)のモニタリング:自分の判定段階に届くから判定するまでどれくらい経ったのが?ユーザー別、経路・文書別で見れる!

(モニタリングなんか優しい言葉ではなく、もはや監視:flushed:

+アルファ

  • スマイル申請・判定:流石に上の二つだけだと真面目すぎて、面白くないなあと思って、「お菓子をくれないとイタズラするぞ」をちなんで、「笑顔くれないと判定も申請もさせないぞ」!です

概要

二本立てと言っても、流れは同じです。

  1. Javscriptカスタマイズで必要なデータを取得する
  2. Lambdaを通して、DynamoDBに書き込む
  3. 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は下のイメージを参考に。
DynamoDB Table Attributes

Lambda

Lambdaに一つNode.Js16か14をRuntimeとしたFunctionを作ります。AWSのJavascriptSDKは18に対応してないので、お間違いなく。
コラボフローのJavascript APIから実行できるように、FunctionURLをチェックして、AuthTypeNONEとします。
Function URL setup
作り終わりましたら、ConfigurationからExcution roleを特定して、DynamoDBへのアクセス権限を与えます。
Permission Set up 1
Screenshot 2023-11-03 at 10.32.08.png

使っているLambdaはこちら:

index.js
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に書き換えてくださいませ〜

documentMonitoringDemo.js
(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をモニタリングしたいフォームに追加したら、セットアップ完了!
Screenshot 2023-11-04 at 12.32.34.png
文書内容(パーツ)、経路依存ではないので、現有のものにそのまま追加したら使えます!

いざ、試しへ
apply.gif
そして、DynamoDBで確認したら、以下のItemが無事追加されました!
Dynamo Item added

QuickSight

DynamoDBのItem登録を無事確認したところで、QuickSightを使って、登録したデータを見てみましょう〜
DynamoDBはNoSQLデータベースのため、Athena経由でデータを取得して、QuickSightで可視化するという流れになります。具体的なセットアップ手順はこちらの記事にてご確認いただければと思います。

ユーザー別の申請数や、経路別の申請数等が簡単に見れます!
Screenshot 2023-11-15 at 7.54.18.png
Screenshot 2023-11-15 at 7.54.27.png

こちらは上に登録した文書申請データのみですが、例えば、下のテーブルのようなユーザーの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に新しいTableaction_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に書き換えてくださいませ〜

actionMonitoringDemo.js
(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
         }
      
  • 差し戻しや取戻しの場合、一つ前の判定履歴取得できないため、差し戻された時点をPreviousActionTimeとして設定することができなく、あくまで、前段階の判定日時をPreviousActionTimeとして使っています。
    • 今のREST APIだとこれは限界ですが、もしもっともっと使いたいという方々からの要望が多くなれば、一つ前の判定履歴を取得できるAPIを追加するかも?

+アルファ:スマイル申請・判定

言葉だけだと何をやりたいかイメージしにくいと思いますので、まず完成したものをお見せします。
smile.gif
(2回目の申請をする前、写真Retake忘れてしまいました。。。お気になさらず。。。)

正直、この部分DeepLearningとかAIに絡んでくるので、ここでは説明仕切れないような気がしますので、とりあえず、ロジックをスキップして、再現できるよう重要なところだけカバーします。

  • 以下の権限のあるAWSのIAM Roleを作成
    • AmazonSageMakerFullAccess
    • AmazonS3FullAccess
    • AmazonElasticContainerRegistryPublicFullAccess
  • こちらのJupyter Notebookをダウンロードして、実行することで、SageMaker Endpointを作成する。具体的な説明が欲しい方こちらで〜
    • 注意点:role_arnを上のものに、 endpoint_namesmileDemoTesorflowに変更する
  • こちらのRepositoryをダウンロードし、cdk deployすることで、LambdaのFunctionを作成する
  • LambdaのConsoleに行って、
    • Execution roleにAmazonSageMakerFullAccessを与えるScreenshot 2023-11-08 at 10.49.56.png
    • Function URLを取得する
  • 下記のJavascriptをイタズラしたいフォームに追加(同じくフォームや経路依存ではないです〜)。urlは上のLambdaのFunctionURLに書き換えてくださいませ〜
smileDemo.js
(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はまたまたここからなので、
どうぞよろしくお願いいたします〜

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?