Skip to content
This repository was archived by the owner on May 31, 2023. It is now read-only.

Commit 710a531

Browse files
author
Hiroki SAKABE
committed
boat-bee-mlのユーザ行動履歴のグラフの更新処理をboat-beeに移動する
1 parent 713c909 commit 710a531

9 files changed

Lines changed: 829 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ jobs:
148148
book_recommendation,
149149
report,
150150
report_review_graph,
151+
report_user_action_graph,
151152
export_dynamodb_table_to_s3_request,
152153
export_dynamodb_table_to_s3_check_status,
153154
]
@@ -183,6 +184,7 @@ jobs:
183184
book_recommendation,
184185
report,
185186
report_review_graph,
187+
report_user_action_graph,
186188
export_dynamodb_table_to_s3_request,
187189
export_dynamodb_table_to_s3_check_status,
188190
]

cdk/lib/bee-slack-app-stack.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,41 @@ export class BeeSlackAppStack extends Stack {
474474
schedule: Schedule.cron({ weekDay: "MON", hour: "0", minute: "0" }), // 毎週月曜日09:00(JST)に実行
475475
targets: [new LambdaFunction(reportReviewGraphFunction)],
476476
});
477+
478+
const reportUserActionGraphFunction = new Function(
479+
this,
480+
"ReportUserActionGraphLambda",
481+
{
482+
description: buildResourceDescription({
483+
resourceName: "ReportUserActionGraphLambda",
484+
stage,
485+
}),
486+
runtime: Runtime.FROM_IMAGE,
487+
handler: Handler.FROM_IMAGE,
488+
code: Code.fromAssetImage(
489+
join(__dirname, "../../lambda/report_user_action_graph")
490+
),
491+
environment: {
492+
SLACK_CREDENTIALS_SECRET_ID: secret.secretName,
493+
CONVERTED_DYNAMODB_JSON_BUCKET:
494+
convertedDynamoDBJsonBucket.bucketName,
495+
},
496+
timeout: Duration.minutes(3),
497+
memorySize: 1024,
498+
}
499+
);
500+
501+
convertedDynamoDBJsonBucket.grantRead(reportUserActionGraphFunction);
502+
secret.grantRead(reportUserActionGraphFunction);
503+
504+
new Rule(this, "ReportUserActionGraphRule", {
505+
description: buildResourceDescription({
506+
resourceName: "ReportUserActionGraphRule",
507+
stage,
508+
}),
509+
schedule: Schedule.cron({ weekDay: "MON", hour: "0", minute: "0" }), // 毎週月曜日09:00(JST)に実行
510+
targets: [new LambdaFunction(reportUserActionGraphFunction)],
511+
});
477512
}
478513
}
479514

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[settings]
2+
profile=black
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM --platform=linux/amd64 public.ecr.aws/lambda/python:3.9
2+
3+
RUN pip install pipenv
4+
5+
COPY Pipfile Pipfile.lock ./
6+
7+
RUN pipenv install --system
8+
9+
COPY index.py .
10+
11+
CMD [ "index.lambda_handler" ]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.PHONY: all
2+
all: init format-check
3+
4+
.PHONY: init
5+
init:
6+
pipenv install --dev
7+
8+
.PHONY: format
9+
format:
10+
pipenv run black ./
11+
pipenv run isort ./
12+
13+
.PHONY: format-check
14+
format-check:
15+
pipenv run black --check ./
16+
pipenv run isort ./ --check
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[[source]]
2+
url = "https://pypi.org/simple"
3+
verify_ssl = true
4+
name = "pypi"
5+
6+
[packages]
7+
slack-sdk = "==3.19.1"
8+
boto3 = "==1.21.35"
9+
pandas = "==1.3.3"
10+
seaborn = "==0.11.2"
11+
12+
[dev-packages]
13+
black = "==22.3.0"
14+
isort = "==5.10.1"
15+
16+
[requires]
17+
python_version = "3.9"

lambda/report_user_action_graph/Pipfile.lock

Lines changed: 563 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# report_user_action_graph
2+
3+
ユーザの行動履歴のグラフを作成し、レポートを送信するハンドラ。メッセージを任意の Slack Incoming Webhook に送信する。
4+
5+
送信先の Slack Incoming Webhook URL の指定方法は、[cdk](../../cdk/README.md)を参照。
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import json
2+
import logging
3+
import os
4+
5+
import boto3
6+
import pandas as pd
7+
import seaborn as sns
8+
from slack_sdk import WebClient
9+
from slack_sdk.errors import SlackApiError
10+
11+
secretsmanager = boto3.client("secretsmanager")
12+
secret_id = os.environ["SLACK_CREDENTIALS_SECRET_ID"]
13+
secret_value = secretsmanager.get_secret_value(SecretId=secret_id)
14+
secret = json.loads(secret_value["SecretString"])
15+
16+
SLACK_BOT_TOKEN = secret["BEE_OPERATION_BOT_SLACK_BOT_TOKEN"]
17+
SLACK_CHANNEL = secret["BEE_OPERATION_BOT_SLACK_CHANNEL"]
18+
19+
20+
def lambda_handler(event, context):
21+
logger = logging.getLogger()
22+
23+
try:
24+
df = load_items_from_dynamodb("user_action")
25+
26+
file_path = "/tmp/user_action_graph.png"
27+
28+
generate_user_action_graph(df, file_path)
29+
30+
upload_file_to_slack(
31+
SLACK_BOT_TOKEN, SLACK_CHANNEL, file_path, "ユーザの行動履歴グラフを更新しました"
32+
)
33+
except Exception as e:
34+
post_message_to_slack(SLACK_BOT_TOKEN, SLACK_CHANNEL, "ユーザの行動履歴グラフの更新に失敗しました")
35+
36+
logger.error("Error updating user action graph: {}".format(e))
37+
38+
39+
def load_items_from_dynamodb(item_id: str) -> pd.DataFrame:
40+
"""
41+
DynamoDBからアイテムを取得する
42+
43+
Args:
44+
item_id: アイテムの種別を表すキー。review、user、bookなど。
45+
"""
46+
dynamodb_json_file = "/tmp/dynamodb_table.json"
47+
48+
s3 = boto3.client("s3")
49+
50+
s3.download_file(
51+
os.environ["CONVERTED_DYNAMODB_JSON_BUCKET"],
52+
"dynamodb_table.json",
53+
dynamodb_json_file,
54+
)
55+
56+
df = None
57+
58+
with open(dynamodb_json_file, "rt") as f:
59+
df = pd.read_json(f)
60+
61+
return df[df["GSI_PK"] == item_id].reset_index()
62+
63+
64+
def upload_file_to_slack(
65+
slack_token: str, channel: str, file_name: str, message: str
66+
) -> None:
67+
"""
68+
Slackのチャンネルにファイルをアップロードする
69+
"""
70+
logger = logging.getLogger()
71+
72+
try:
73+
client = WebClient(token=slack_token)
74+
75+
client.files_upload(
76+
channels=channel,
77+
initial_comment=message,
78+
file=file_name,
79+
)
80+
81+
except SlackApiError as e:
82+
logger.error("Error uploading file: {}".format(e))
83+
84+
85+
def post_message_to_slack(slack_token: str, channel: str, message: str) -> None:
86+
"""
87+
Slackのチャンネルにメッセージを送信する
88+
"""
89+
logger = logging.getLogger()
90+
91+
try:
92+
client = WebClient(token=slack_token)
93+
94+
client.chat_postMessage(
95+
channel=channel,
96+
text=message,
97+
)
98+
99+
except SlackApiError as e:
100+
logger.error("Error post message: {}".format(e))
101+
102+
103+
def generate_user_action_graph(
104+
df_user_action: pd.DataFrame, png_file_name: str
105+
) -> None:
106+
"""
107+
user_actionの時系列グラフを作成し、画像ファイルとして保存する
108+
"""
109+
df = _make_user_action_count_frame(df_user_action)
110+
111+
sns.set(rc={"figure.figsize": (30, 16)})
112+
113+
plot = sns.lineplot(data=df, x="index", y="created_at_date", hue="action")
114+
115+
plot.set_title("user action")
116+
plot.set_xlabel("date")
117+
plot.set_ylabel("action count")
118+
119+
plot.get_figure().savefig(png_file_name)
120+
121+
122+
def _make_user_action_count_frame(df_user_action: pd.DataFrame):
123+
"""
124+
user_actionを日時ごとにカウントしたデータフレームを、user_action別で作成する
125+
"""
126+
127+
# 全てのuser_actionを集計する
128+
df = _make_user_action_count_frame_one(df_user_action, "all")
129+
130+
action_name_list = [
131+
"post_review_action",
132+
"post_review_modal",
133+
"app_home_opened",
134+
"book_search_modal",
135+
"book_search_result_modal",
136+
"read_review_of_book_action",
137+
"read_review_of_user_action",
138+
"open_review_detail_modal_action",
139+
"user_info_action",
140+
"user_profile_modal",
141+
]
142+
143+
# 個別のuser_actionを集計する
144+
for action_name in action_name_list:
145+
146+
df_specific_user_action = df_user_action[
147+
df_user_action["action_name"] == action_name
148+
]
149+
150+
if df_specific_user_action.empty:
151+
continue
152+
153+
df_tmp = _make_user_action_count_frame_one(df_specific_user_action, action_name)
154+
155+
df = pd.concat([df, df_tmp])
156+
157+
return df.reset_index()
158+
159+
160+
def _make_user_action_count_frame_one(df_user_action: pd.DataFrame, action):
161+
"""
162+
user_actionを日時ごとにカウントしたデータフレームを作成する
163+
"""
164+
165+
df_user_action["created_at_date"] = df_user_action["created_at"].map(
166+
lambda x: x[:10]
167+
)
168+
169+
df = (
170+
df_user_action["created_at_date"]
171+
.value_counts()
172+
.sort_index()
173+
.to_frame()
174+
.reset_index()
175+
)
176+
df["action"] = action
177+
178+
return df

0 commit comments

Comments
 (0)