Amazon SNSからSlackに通知する仕組みを導入した話(実装編)

宮下(mizzy)です。

Amazon SNSからSlackに通知する仕組みを導入した話(概要編)でご紹介した仕組みの実装面について解説します。


TerraformによるAWS上のリソース設定

SNS TopicとLambda Functionの関連づけ

Slack通知用SNS Topicの作成と、実際に通知を行うLambda Functionの関連づけは、Terraformコードでこのように記述しています。

ただし、Lambda FunctionはTerraformではなくecspressoで管理しているため、Lambda Functionの情報はData Sourceで読み込んでいます。

data "aws_lambda_function" "sns_to_slack" {
  function_name = "sns-to-slack"
}

resource "aws_sns_topic" "slack_notification" {
  name = "slack-notification"
}

resource "aws_sns_topic_subscription" "slack_notification" {
  endpoint  = data.aws_lambda_function.sns_to_slack.arn
  protocol  = "lambda"
  topic_arn = aws_sns_topic.slack_notification.arn
}

resource "aws_lambda_permission" "slack_notification" {
  action        = "lambda:InvokeFunction"
  function_name = data.aws_lambda_function.sns_to_slack.function_name
  principal     = "sns.amazonaws.com"
  source_arn    = aws_sns_topic.slack_notification.arn
}

Bot User OAuth Tokenの格納場所作成とアクセス権の設定

Lambda FunctionからSlackへの接続時に必要な、Bot User OAuth Tokenを格納するためのSSMパラメータを作成し、Lambda Functionから値を読み取れるよう権限設定しています。

Bot User OAuth Tokenの値はTerraformでは設定しないため、ダミーの値を設定し、ignore_changesで変更を無視するようにしています。

resource "aws_ssm_parameter" "talentio_ruboty_bot_user_oauth_token" {
  name  = "/slack/bot-user-oauth-token/TalentioRuboty"
  type  = "SecureString"
  value = "dummy"

  lifecycle {
    ignore_changes = [value]
  }
}

resource "aws_iam_role" "sns_to_slack" {
  name               = "sns-to-slack"
  assume_role_policy = data.aws_iam_policy_document.sns_to_slack_assume_role.json
  inline_policy {
    name   = "inline"
    policy = data.aws_iam_policy_document.sns_to_slack.json
  }
}

data "aws_iam_policy_document" "sns_to_slack_assume_role" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      identifiers = ["lambda.amazonaws.com"]
      type        = "Service"
    }
  }
}

data "aws_iam_policy_document" "sns_to_slack" {
  statement {
    effect  = "Allow"
    actions = ["ssm:GetParameter"]
    resources = [
      aws_ssm_parameter.talentio_ruboty_bot_user_oauth_token.arn,
    ]
  }
}

SNSにPublishする際、以下のようにSlackAppでSlack App名を指定するようにしていますが、ここで指定された値によって、Tokenを読み取るSSMパラメータを変更することで、複数のSlack Appに対応できるようにしています。

aws sns publish \
  --topic-arn arn:aws:sns:ap-northeast-1:xxxxxxxxxxx:slack-notification \
  --subject 'Message from CLI' \
  --message '{
    "SlackApp": "TalentioRuboty",
    "SlackChannel": "mizzy-test"
  }'

通知用Lambda Function

Slackへ通知を行うLambda Function本体は、以下のような125行のRubyコードです。

require 'json'
require 'aws-sdk-ssm'
require 'uri'
require 'net/http'
require 'slack-ruby-client'

def handler(event:, context:)
  event = event['Records'][0]

  subject = event['Sns']['Subject']
  message = JSON.load(event['Sns']['Message'])

  if message['Status'] == 'Success'
    title = ":white_check_mark: #{subject}"
  elsif message['Status'] == 'Fail'
    title = ":x: #{subject}"
  elsif message['Status'] == 'Warning'
    title = ":warning: #{subject}"
  else
    title = ":information_source: #{subject}"
  end

  blocks = [{
              type: 'header',
              text: {
                type: 'plain_text',
                text: title,
              },
            }]

  if !message['User'].nil?
    blocks.append(
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: "<@#{message['User']}>",
        },
      }
    )
  end

  fields = []

  if !message['Parameters'].nil?
    message['Parameters'].each do |key, value|
      fields.append({
        type: 'mrkdwn',
        text: "*#{key}:*\n #{value}",
      })
    end
  end

  if !message['StartTime'].nil?
    start_time = Time.parse(message['StartTime'])
    end_time = Time.now
    fields.append({
      type: 'mrkdwn',
      text: "*Elapsed time:*\n #{((end_time - start_time) / 60).round(1)} minutes",
    })
  end

  # fieldsが空の場合Slackがinvalid_blocksエラーを返すので空白文字で埋める
  if fields.size == 0
    fields.append({
      type: 'mrkdwn',
      text: ' ',
    })
  end

  blocks.append({
    type: 'section',
    fields: fields,
  })

  if !message['Arn'].nil?
    link = arn_to_url(message['Arn'])
    blocks.append(
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: "*Link:*\n<#{link}|#{link}>",
        },
      }
    )
  end

  if !message['Url'].nil?
    blocks.append(
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: "*Link:*\n<#{message['Url']}|#{message['Url']}>",
        },
      }
    )
  end

  ssm = Aws::SSM::Client.new

  req = {
    name: "/slack/bot-user-oauth-token/#{message['SlackApp']}",
    with_decryption: true,
  }

  res = ssm.get_parameter(req)

  Slack.configure do |config|
    config.token = res.parameter.value
  end

  client = Slack::Web::Client.new
  client.auth_test
  client.chat_postMessage(channel: message['SlackChannel'], text: title, blocks: JSON.dump(blocks), as_user: true)

  return
end

def arn_to_url(arn)
  elements = arn.split(':')
  region = elements[3]
  "https://console.aws.amazon.com/go/view?arn=#{arn}"
end

おわりに

Amazon SNSからSlackに通知する仕組みは、各社さんそれぞれ実装していると思いますし、検索すると色々と記事が出てくるのですが、実運用レベルでどのように実装しているか、といった記事はあまりなさそうでしたので、実例として紹介してみました。

通知にはLambda FunctionではなくAWS Chatbotの利用も考えましたが、通知内容のカスタマイズを柔軟に行うことができなさそうだったので、Lambda Functionを利用する形になりました。

Amazon SNSからSlackに通知する仕組みを導入した話(概要編)でも書きましたが、元々、このような仕組みを導入したのは、AWS Step Functionsから簡単に通知できるようにしたかったからです。

Step Functionsを実際の運用でどのように利用して、どのように通知しているか、という具体例については、また後日ご紹介できればと思います。

Amazon SNSからSlackに通知する仕組みを導入した話(概要編)

宮下(mizzy)です。

タレンティオ社内で、Amazon SNSにPublishするとSlackに通知できる仕組みを導入しました。このような仕組みの事例は検索するとたくさんでてきますが、タレンティオではこうやってますよ、という例としてご紹介したいと思います。

このエントリでは、利用者視点で具体的にどのようにSNSにPublishし、どのような通知が得られるのか、について解説します。実装の詳細についてはAmazon SNSからSlackに通知する仕組みを導入した話(実装編)で解説します。


通知の投げ方

AWS CLIを利用する場合は、次のようにSNSにPublishします。

aws sns publish \
  --topic-arn arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:slack-notification \
  --subject 'Message from CLI' \
  --message '{
    "SlackApp": "TalentioRuboty",
    "SlackChannel": "mizzy-test"
  }'

タレンティオではRubyをメインで利用しているのですが、Rubyの場合は次のようにPublishします。

require 'aws-sdk-sns'

sns = Aws::SNS::Client.new

sns.publish(
  {
    topic_arn: 'arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:slack-notification',
    subject: 'Messagge from Ruby',
    message: JSON.dump(
      {
        SlackApp: 'TalentioRuboty',
        SlackChannel: 'mizzy-test',
      }
    ),
  }
)

このようにPublishすると、次のように通知されます。


通知内容のカスタマイズ

各種パラメーターを指定することで、通知内容カスタマイズすることが可能です。

Status

Statusを指定すると、タイトルの先頭の絵文字を変えることができます。

aws sns publish \
  --topic-arn arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:slack-notification \
  --subject 'Message from CLI' \
  --message '{
    "SlackApp": "TalentioRuboty",
    "SlackChannel": "mizzy-test",
    "Status": "Warning"
  }'

このようにStatusWarningを指定すると、次のように通知されます。

Statusに指定するパラメータによって、アイコンは以下のように変わります。

  • 何も指定しない場合はℹ️
  • Successの場合は✅
  • Failの場合は❌
  • Warningの場合は⚠️

Parameters

Parametersでは、任意で表示したい内容を、キーと値の形で指定します。キーや値は文字列であれば何でも大丈夫です。

aws sns publish \
  --topic-arn arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:slack-notification \
  --subject 'Message from CLI' \
  --message '{
    "SlackApp": "TalentioRuboty",
    "SlackChannel": "mizzy-test",
    "Status": "Success",
    "Parameters": {
       "Date": "2023/06/28",
       "Area": "Kanagawa",
       "Weather": ":sun_behind_rain_cloud:"
    }
  }'

このようにPublishすると、次のように通知されます。

User

Userで指定したユーザに対してメンションします。

aws sns publish \
  --topic-arn arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:slack-notification \
  --subject 'Message from CLI' \
  --message '{
    "SlackApp": "TalentioRuboty",
    "SlackChannel": "mizzy-test",
    "Status": "Fail",
    "Parameters": {
       "Date": "2022/09/15",
       "Area": "Kanagawa",
       "Weather": ":sun_behind_rain_cloud:"
    },
    "User": "mizzy"
  }'

このようにPublishすると、次のように通知されます。

Url

指定したURLを表示します。Parametersの中に入れても良いのですが、見にくくなりそうなので独立したブロックに表示するようにしています。

aws sns publish \
  --topic-arn arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:slack-notification \
  --subject 'Message from CLI' \
  --message '{
    "SlackApp": "TalentioRuboty",
    "SlackChannel": "mizzy-test",
    "Status": "Success",
    "Parameters": {
       "Date": "2022/09/15",
       "Area": "Kanagawa",
       "Weather": ":sun_behind_rain_cloud:"
    },
    "User": "mizzy",
    "Url": "https://www.talentio.co.jp/"
  }'

このようにPublishすると、次のように通知されます。


おわりに

元々、このような仕組みを導入したのは、AWS Step Functionsから簡単に通知できるようにしたかったからです。

Step Functionsを実際の運用でどのように利用して、どのように通知しているか、という具体例については、また後日ご紹介できればと思います。

Zoom App Marketplace へのアプリ公開でハマったポイント

こんにちは。タレンティオの大木です。

採用管理システムのTalentioでは、オンライン面接をサポートするために、当社のサービス上でZoom Meetingを生成する機能を提供しています。

この機能を開発する過程で、OAuth AppをZoom App Marketplace に公開する必要があります。
今回は、OAuth Appの作成・公開までのプロセスと、その過程で詰まってしまったポイントを共有したいと思います。

Zoom App Marketplace とは

Zoom App Marketplaceは、Zoomが提供する開発者向けのプラットフォームです。サードパーティが開発したZoomの機能を拡張するアプリケーションやプラグインを探したり、自分が開発したアプリケーションを公開・管理したりできる場所です。

アプリケーションを公開する場合、用途に応じたタイプを選択する必要があります。
選択できるアプリの種類には、Zoom App・OAuth・JWTなどがあります。

それぞれのタイプについて簡単な説明を記載します。

  • Zoom App:Zoomミーティングやチャット上で利用可能な追加機能やサービスの提供を行うことができます
  • JWT:生成されたJWTを利用して、Zoom APIを使用することができます ※ 2023年9月1日からは使用不可となります
  • OAuth:ユーザーの許可を得てZoomアカウントにアクセスし、アプリケーションからAPIのリクエストを送ったり、webhookのイベントを購読できます

Talentioでは、各ユーザーのZoomアカウントにアクセスして、Zoom Meetingを作成することを目的としているため、OAuthタイプを選択しました。

1. アプリケーションの作成方法

アプリケーションの作成は、Zoom App Marketplaceの管理画面から行うことができます。

アプリケーションを作成すると、認証情報が発行され、その情報をもとに開発を進めることができます。

開発を進めているとさまざまな疑問が出てくると思いますが、その際はZoom Developer Forum で質問ができます。
質問を投稿するとおおよそ1〜2営業日以内に返信が届くため、作業中に詰まってしまった際は、すぐに質問を投稿することで作業をスムーズに進められるかもしれません。

2. Zoom Market Placeへの公開申請

作成したアプリケーションを外部のZoomアカウントにインストールさせるためには、Zoom App Marketplaceへ公開する必要があります。

公開までの道のりは Zoom App Marketplace App Review Process にまとめられているステップになります。

申請時に詰まってしまったポイント

申請時に必要な対応などは、Submission Checklist にまとめられています。
これを参考に対応したものの、申請時にいくつかつまづくポイントや、審査で不合格となった箇所がありました。

1. 審査用に本番環境、あるいはそれと同等の環境の準備

Zoomのレビュアーが操作確認を行うため、今回の開発箇所を外部から操作可能な状態である必要があります。
しかし、申請時にはZoom APIを利用した機能はまだリリース前でした。

そのため、デモ環境に審査者がアクセスできるような準備をしました。
また、本番環境では非公開にしたかったため、フィーチャートグルを設けるなど、いくつかの対応を行いました。

2. 関連するページのエンドポイントは同一ドメインでなければならない

申請時には、ヘルプページや利用規約などのURLの提供が求められます。
これらのURLはリダイレクトURLに設定したドメインと一致している必要があります。

Talentioでは、サービスのドメインtalentio.com ですが、ヘルプページや利用規約ドメインtalentio.co.jp です。

これらのページのドメインを変更することは避けたかったため、
talentio.com/ヘルプページ へのアクセスがヘルプページにリダイレクトされるように設定しました。

3. 利用イメージが伝わる画像の作成

アプリのアイコンや機能の使用イメージを示す画像が求められます。 明確な規定がなかったため、公開されているZoom Appを参考に画像を作成しましたが、「画像が不十分である」という理由で一度は審査に通りませんでした。

対策として、操作手順がより理解しやすいように、以下の4枚の画像に変更しました。

  1. Zoomとの連携設定
  2. Zoomとの認証
  3. Zoom Meetingの発行
  4. 作成されたZoom URLの確認

具体的な画像については、App Marketplace を参照ください。 これらの4つの画像に変更したところ、無事に審査が通りました。

4. アンインストール時の認証情報の管理

審査段階でアンインストール時の処理が適切に考慮されていないと指摘を受けました。
そのため、認証解除の通知をWebhookで受け取ると、データベースに保存していた認証情報を削除するような仕組みを設けることにしました。

該当するチェックリストの項目は、Submission Checklist に記載されています。

おわりに

今回は、OAuth Appの作成・公開までのプロセスと、その過程で詰まってしまったポイントについてご紹介しました。

この記事が、Zoom MarketplaceへのOAuth App公開に向けた参考となれば幸いです。

TalentioがoEmbedに対応した話

こんにちはTalentioの佐藤です。

採用管理システムのTalentioでは採用ページを作ることができます。

先日この機能をアップデートし、ブログ等にURLを貼り付けるだけで採用ページを埋め込めるようにしました。これにより採用ページの共有が簡単にできるようになります。

例えば、このブログ ( はてなブログ ) にTalnetioで作成したページのURLを貼るとこのようになります。

これはoEmbedという規格に沿って実装することで実現しています。

oEmbedとは

埋め込みコンテンツの規格です。メタデータのフォーマットや取得フローを定義しています。ドキュメントはこちらです。

コンテンツの提供者と利用者がこの規格に沿って実装をすることで、通常ページのURLから埋め込み用のコンテンツを取得できるようになります。今回のアップデートでTalentioはコンテンツの提供者として必要な実装をしました。具体的には次の実装をしています。

  1. 埋め込みコンテンツを表示するURLを追加
  2. 埋め込みコンテンツのメタデータを返すAPIを実装
  3. メタデータ取得APIと採用ページの紐付け

それぞれについて詳しい内容を見ていきます。

1.埋め込みコンテンツを表示するURLを追加

埋め込み時に表示する画面のURLです。今回のケースではこのURLを追加しています。

2.埋め込みコンテンツのメタデータを返すAPIを実装

埋め込みコンテンツの表示に必要なデータを返すAPIを作ります。Talentioでは以下のようなデータを返すAPIを用意しています。

{
  "type": "rich",
  "version": "1.0",
  "provider_name": "Talentio",
  "provider_url": "https://www.talentio.co.jp",
  "html": "<iframe src=\"https://recruit.talentio.co.jp/r/1/c/talentio/embed/pages/21466\" width="100%" height=300 frameborder=0 title=\"株式会社タレンティオ | カスタマーサクセス\"></iframe>",
  "url": "https://recruit.talentio.co.jp/r/1/c/talentio/embed/pages/21466",
  "width": "100%",
  "height": 300
}

htmlには表示したいコンテンツを指定します。Talentioでは1で作成した画面を表示するiframeを設定しています。widthやheightは実際に描画する際のサイズを指定します。

APIのレスポンスに含めるデータについて、詳細はこちらを参照してください。

3.メタデータ取得APIと採用ページの紐付け

オリジナルのページにAPIのURLを指定したタグを設定し、2つを関連づけます。

実際にTalentioで作成した採用ページには以下のようなタグが設定されています。hrefには2で作成したAPIが指定されており、これにより採用ページとAPIが関連づいています。

<link href="https://open.talentio.com/oembed?url=https://recruit.talentio.co.jp/r/1/c/talentio/pages/21466&format=json" rel="alternate" title="oEmbed Profile of カスタマーサクセス" type="application/json+oembed">

紐付け方法について、詳しい仕様はこちらを参照してください。

埋め込まれるまでの流れ

以上でコンテンツ提供者の実装は完了です。後はURLをoEmbedに対応したサービスに貼り付ければOKです。利用者側のサービスが規格に沿ってデータを取得し、コンテンツを埋め込んでくれます。

具体的には以下の流れでデータを取得します。

  1. 貼り付けたURLにアクセス。仕様に沿ってタグから対応するAPIを探す
  2. 対応するAPIからメタデータを取得
  3. メタデータに沿って埋め込みコンテンツを描画

補足: oEmbedに対応しているサービス

最後にコンテンツの利用者側で対応しているサービスを調べてみました。

見つけられた範囲では、以下のサービスがoEmbed表示に対応しているようです。

毎月1回を1年間続けてきたタレンティオのTechDay運用

こんにちは、管野です。

タレンティオでは月に1回TechDayを開催しています。この日は普段の業務をお休みし、各自が好きなことを学びます。
そして1日の終わりにクロージング(発表)を行い、それぞれの成果を共有します。

2022年3月に始めて1年が過ぎました。そこで今回はTechDayの運用について振り返ってみます。

【目次】

  1. どうしてTechDayをするのか
  2. どうやってTechDayを運用しているのか
  3. TechDayを1年間やってみてどうだったか

1. どうしてTechDayをするのか

アウトプットに関する学習促進の機会を設けるため

発案当初の意図は、会社として学習促進の機会を整えることでした。Techメンバーはそれぞれ色々なインプットを行っていますが、その学びを全て今の業務に活用できるとは限りません。
活用できるかどうかは、本人が着手しているタスクや現在のアーキテクチャに影響されます。

そこでTechDayとして「各自が自由に作業できる日」を設けることになりました。
お試しで始まりましたが、今では後述する他の理由もあって継続しています。

インプットに関する学習促進の機会を設けるため

TechDayはインプットの機会としても有効です。
例えば新機能に関する技術調査や、既存のアーキテクチャで理解が弱い部分を調べることが出来ます。

後述するようにTechDayの終わりにはクロージングとして、ドキュメントにまとめて発表します。
人に伝えることを意識しながら学ぶため、情報を整理したり取捨選択することになり、インプットの質が上がります。

技術的なコミュニケーションの機会を設けるため

タレンティオでは現在フルリモートワークです。
オフィスに出勤していた頃とは違い、気軽に話せるタイミングは減っています。

みんなで集まって話し合うオンラインMTGはいくつかありますが、技術的な関心をテーマに話す場はほぼありません。
そんな中、TechDayのクロージングは各メンバーの関心事や得意分野を知れる機会になっています。

2. どうやってTechDayを運用しているのか

開催頻度は月1にしている

毎月の最終水曜日をTechDayとしています。メイン業務の開発速度を落とさないことが大前提です。
頻度を高くすると普段の業務に支障が出ますし、逆に低くするとTechDayに求める効果が薄れます。今のところ月1が丁度良いと感じています。

曜日はいつでも構わないのですが、今のチームメンバーにとって一番都合が良い水曜日にしています。

何をテーマにするかは各自の自由

GitHub DiscussionsにTechDayのトピックを一つ作り、月ごとにスレッドを立てています。 最初はGitHub Issuesを使っていましたが、業務でもDiscussionsを使うようになってからはそちらに移行しました。

メンバーは前日に「今回何をやるか」をコメントします。記載したテーマを必ずしも守る必要はなく、ここでのコメントはただの周知です。私はテーマがかぶらないようにしたいので、メンバーのコメントを見てテーマを変えたりしたこともあります。

TechDayで何をするか予定を書く

またテーマに厳密なルールはありませんが、なるべく業務に活かせるものを選ぶ傾向にあります。1日という時間制限のため調査や検証系が多くなりますが、「作ってみる」というテーマに取り組むこともあります。

当日は学習に専念して最後にチームでクロージングを行う

当日は始業から18:00までTechDayに専念しています。
もちろんサポート対応や障害など急ぎの時はそちらを優先しますが、基本的に普段の業務は進めません。そのため例えばコードレビューを依頼されることもありません。この日のSlackはとても静かです。

18:00になるとZoom上でクロージング(発表会)が始まります。
この時までにそれぞれ学んだことをKibelaに投稿します。このKibelaをもとに1人10分ほどの発表を行います。

3. TechDayを1年間やってみてどうだったか

TechDayの成果物

ここ半年のTechDayで取り組んだタイトルの一覧を、この記事の最後に載せておきます。そちらを見ると調査系が多いものの、作ってみたりチーム改善に貢献しているのもあるのが分かります。

例えば以下のような成果が生まれました。

CIや単体テストの改善

  • CI上の単体テスト(RSpec)を高速化するため並列実行を導入、12分 -> 3分に短縮した
  • (上記とは別の開催日で)遅い単体テストのコードを見直して、1~2分短縮した

開発環境の改善

  • Pull Requestに出してるブランチをステージング環境に構築できるが、その手順を簡略化した
  • タレンティオの状況に併せたER図ビューワーを作った

チームメンバーの感想

良い点

  • 普段まとまった勉強時間を取るのが難しいこともあるので、業務時間内に学習機会があるのは助かる
  • CIの高速化とか、普段の開発体験につながる成果が出てるのは良いこと
  • クロージングでの共有を前提に調べるため、いつもより情報を整理しようとして理解度が増す
  • クロージングで他の人の話を聞くだけでも、普段行っているWebでの軽い情報収集より理解が深まる
  • 取り組むテーマが限定されていないこと。幅広いテーマの話を聞けるのは興味深いし、楽しい

改善点

  • 現状でも不満ないが、強いてあげるなら1日以上あると嬉しい
    • 1日以内に終わるネタに限るので、何か作ってみるとか大きなことはしにくい
    • 毎回は難しくても半年とか年に1回だけでも、1日以上の開催があると嬉しい
  • クロージングで発表できるテーマに絞ってしまう
    • まとめ方が見えないテーマに関して「とりあえず触ってみる」ということが出来ない

おわりに

TechDayはとても良い取り組みです。個人のスキルアップだけでなく、チームの生産性や、お客様への価値提供に繋がる可能性があります。運用方法はチームの状況によって調整しつつ、引き続き継続していきます。

参考: 直近半年のTechDay記事タイトル一覧

- Amazon Comprehend の調査
- Zigについて調べてみた
- Rails7 主要な新機能の検証
- Bundle Sizeの調べ方
- タレンティオ用ER図Viewerを作ってみた
- 開発環境へのビルド・デプロイプロセスの改善
- 書籍『Webブラウザセキュリティ』を読んだ
- Rails7 のフロントエンド
- WASIについて調べてみた
- Notion APIを調べた
- 開閉アニメーションについて
- Techブログを書いてみた
- Sentry Session Replay機能の検証
- ハッシュ関数と暗号化/復号について
- SendGrid APIの活用
- Source Map
- ワンタイムパスワードを調べた
- 遅い単体テストを改善して少しだけ早くした
- 機械学習にトライ
- zustand を調べてみた
- Sentry Session Replayに関するブログを書きました
- intercomのお問い合わせをスプシに集計する
- Zodの調査
- 「React でのドラッグ&ドロップライブラリを比較してみた」というブログ記事を書いた(ている)
- ChatGPT and Whisper APIsの活用
- 仕事ではじめる機械学習を読む
- Remix を調べた

Reactのドラッグ&ドロップソートライブラリを比較

はじめに

タレンティオの宇野です。 先日、Talentio Hire で React のドラッグ&ドロップライブラリを実装する機会があったのですが、React でのドラッグ&ドロップソートライブラリは数多くあり、どれが要件にマッチするかがわかりづらかったので、比較検討した際の情報をご紹介できればと思います。

対象読者

この記事は以下の読者におすすめです。

  • React でドラッグ&ドロップソートを実装したい方
  • ライブラリを比較する際の基準の例を知りたい方

今回実装する機能

以下のようなドラッグ&ドロップで並び替えを行うオーソドックスなソート機能です。

ソート中

TLDR

今回の実装では、 dnd-kit を選択しました。以下は、その選定理由になります。
サンプルコードを見たい方はこちらをクリックしてください。

注意

この記事は 2023/03 時点の情報です。

候補ライブラリ

ざっと調べた限り、下記のライブラリがドラッグ&ドロップでのソートに対応していそうでした。
もちろん下記以外にもソート用のライブラリは存在するかと思いますが、今回はこれらを比較検討してみたいと思います。

人気度

ライブラリの利用数(ダウンロード数)や GitHub での人気度を見てみます。

ダウンロード数では react-draggable が突出して人気です。
ただし、最近はダウントレンド傾向なので、他のライブラリへの置き換えが進んでいそうです。
次点では、 react-dnd, react-beautiful-dnd が多く利用されています。

npm trends

スター数

Github でのスター数では、 react-beautiful-dnd が人気です。
Attlasian が開発元で Jila 等にも使われているようなので、そちらの信頼度も影響していそうです。

Stars
react-beautiful-dnd 29,353
react-dnd 18,885
react-sortable-hoc 10,378
react-draggable 8,153
@dnd-kit/core 7,005

メンテナンスされているか

  • dnd-kit が最もアクティブにメンテナンスされています。
    • react-sortable-hoc の後継ライブラリでもあり、初回リリースが 2021/01 と後発のライブラリではあるものの活発に開発されています。
  • react-sortable-hoc はすでにアップデートはされていませんでした。
  • react-beautiful-dnd も部分的なアップデートはされていますが、現在は機能開発、改善等のアップデートはされていません。
Version Updated Created
@dnd-kit/core 6.0.8 2023-02-19T14:48:34.147Z 2021-01-02T02:07:10.605Z
react-beautiful-dnd 13.1.1 2022-08-30T04:15:39.512Z 2017-08-10T07:15:47.946Z
react-draggable 4.4.5 2022-04-26T18:04:09.131Z 2014-07-25T21:58:31.684Z
react-dnd 16.0.1 2022-04-19T18:05:21.375Z 2014-10-19T13:55:23.335Z
react-sortable-hoc 2.0.0 2021-03-19T02:56:51.618Z 2016-06-08T03:30:55.021Z

ライブラリの状況を踏まえた評価

上記を踏まえ、この時点で、下記のライブラリは選定対象から外しました。

  • react-draggable
    • 利用者数が多いものの、現在は利用者数が減ってきている。
  • react-beautiful-dnd
    • Atlassian (Jira などの開発)で開発されているので品質には期待できそうだが、 React 18 に未対応、機能面でのアップデートがされていないなど、今後、長く利用し続けるのは難しそう。
  • react-sortable-hoc
    • 機能の要件的には問題ないが、こちらも react-beautiful-dnd と同じくアップデートされていない。

実装してみる

ライブラリの状況は把握できたので、実際にサンプルを実装して検証してみました。
ここでは、前段の選定対象で残った、react-dnddnd-kit の動作を確認します。

react-dnd

codesandbox でのサンプル
(ソートがうまく動作しない場合は、 Open preview in new window からお試しください)

dnd-kit

  • ソート用の preset が用意されているので、比較的簡単に実装が可能
  • ソート時のアニメーションも標準で付与される
  • ドラッグ時にオーバーレイする要素の位置を自由にカスタマイズ可能
  • スマートフォンにも対応している

codesandbox でのサンプル

まとめ

  • アニメーションや細かい要素のカスタマイズ等の要件にマッチ。
  • react-sortable-hoc からの改善点も多く盛り込まれている。
  • 後発のライブラリで現状利用者数は多くないが、頻繁にアップデートされており、これからも改善が期待できそう。

ということで、今回の実装では dnd-kit を採用しました。

npm trends や Google 検索では、過去に人気(ダウンロード)があったものはわかるものの、
新規採用が増えてきていたり、今後も利用しつづけられそうかどうかは判断できないので、これらの情報を踏まえつつ、実際に実装して感触を確かめるのが重要と感じました。

今回は、縦のリスト要素を並び替える目的で検証しました。
カンバンのような横も含めたドラッグ&ドロップでは、また要件が変わり、結果も変わってくるかと思うので、こちらも機会があれば試してみたいと思います。

この記事が、 React でドラッグ&ドロップソート機能を実装する際の参考になれば幸いです。

Sentry Session Replay機能の紹介

こんにちは、タレンティオの森脇です。

先日、SentryのSession Replay機能が、GA版として公開されましたので紹介します。

sentry.io

Sentryとは

Sentryとは、開発者向けのエラートラッキングとパフォーマンス監視のプラットフォームです。 例えば、エラートラッキング機能では、アプリケーションでエラーが発生した場合に、Sentry上でスタックトレース・該当のソースコードなどが確認できます。

Session Replayの紹介

Session Replayは、ユーザーの行動を動画のように再現し、エラーの発生時・発生前後の状況を確認することができます。

今までのSentryが提供する情報だけでは、再現できない問題の解決に時間がかかる場合がありました。 Session Replayでは、ユーザーの行動を視覚的に確認でき、ネットワークリクエスト、DOMイベント、コンソールメッセージ等のより正確な情報を元にトラブルシューティングが行えるようになります。

それでは、実際にどのように見えるかを確認してみます。

SDKのインストール

Session Replayを利用するには、Sentry SDKの Version 7.27.0以上が必要です。

yarn add @sentry/browser
セットアップ

Sentryの初期化時に、以下のように定義します。

examplePublicKeyには、自身の環境のKeyを設定してください。

import * as Sentry from "@sentry/browser";

Sentry.init({
  dsn: "https://[examplePublicKey]@o0.ingest.sentry.io/0",

  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,

  integrations: [
    new Sentry.Replay({
      maskAllText: true,
      blockAllMedia: true,
    }),
  ],
});

以上で完了です。

動作確認

確認のために、テキストボックスに特定の文字列を入れると、例外を発生させます。

<input
 type="text"
 onChange={e => {
    console.log(e.target.value)
    if (e.target.value == 'exception') {
        throw new Error('exception occurs')
    }
  }}
/>

画面を操作しエラーを発生させると、Sentry上でエラーが確認できます。以下のようなブラウザのDeveloper Consoleに近い情報を確認できます。

  • タイムライン
  • ユーザの行動リプレイ
  • パンくず
  • console
  • network
  • DOM Events

session replay

再生ボタンを押すと、ユーザの行動を動画のように再現できます。

(実際にTalentioで動かしたときのイメージ)

Session replayの動画

consoleへの出力内容は、タイムラインと共に確認できます。

console

エラー発生時のユーザの画面操作や、DOM・コンソールの情報が明確に分かります。

Privacyの対策

Session Replayでは、ユーザ画面の表示・入力データは、デフォルトでは*でマスクされます。 また、テキストデータだけではなく、img,object,iframe等のメディア要素もブロックされるため、個人を特定するデータはSentryへ送信されません。 ただし、一部のplaceholderがマスクされていないケースが発生したため、正しくマスクされない箇所は以下で紹介する方法で個別対応が必要です。

特定のDOM要素に属性 or CSSクラスを追加することで、その内容を記録しないようにすることも可能です。

例えば、サイドメニューののDOMにdata-sentry-blockを付与すると、サイドメニューは空白として表示されます。

<div data-sentry-block>
// sidemenu content
</div>

空白のサイドメニュー

パフォーマンスへの影響

Session Replayでは、DOMの変更を検知し、定期的にSentryへデータを送信しています。 そのため、多少のオーバヘッドは発生していますが、最適化のための対策が行われているため、 確認した環境ではユーザが体感できるほどの影響はありませんでした。

パフォーマンス最適化のための対策

  • DOMの変更の検知には、ブラウザのAPIであるMutationObserverを利用している
  • DOMの変更発生時に、全てのDOMを送信するのではなく、初回のスナップショットからの差分だけ送信している
  • サーバへ送信するDOMは、gzip圧縮されており、圧縮はWeb Workerによりバックグランドで行われている
  • 画像などの静的アセットはクライアントから転送せず、Sentryダッシュボードでの動画の再生時にアセット元のホストサーバーから直接取得されます。

実際に確認しても、一度に送信されるデータは数十バイトほどでした。

価格

エラー監視等と同じく、従量課金となっており一定数を超えると追加料金が発生します。

docs.sentry.io

まとめ

Session Replay機能は、ユーザーの行動が正確に確認できるようになるため、 再現が難しいエラーのトラブルシューティング工数削減が期待できます。

導入も容易で、既にSentryの有料プランを利用している場合は、一定数までは追加費用無しで利用できるのでおすすめです。