S3のObjectCreated:PutイベントでLambdaが起動しなかった話

こんにちは、サーバーサイドエンジニア1年目のharukiです。

先日、AWS上でS3トリガーのLambda起動を試していたところ、なかなか上手く行かずに少しハマってしまったため、 今回はそちらの原因と対処法&プチ調査結果についてまとめたいと思います。

本題の前に

まず、今回のお話に出てくるシステムのイメージ図はざっくりと下記の様な形で、 LaravelからのS3へのファイルアップロードに対してLambdaを起動するといった仕組みになっています。

なお、この際AWSの3Sイベント通知ではObjectCreated:PutのみをLambdaのトリガーイベントとして設定していました。

f:id:h0r4k:20220404164302p:plain

問題発生の状況

今回発生した問題としては、アップロードしたファイルサイズによってLambdaが実行されたりされなかったりするというものでした。 (7.9MBのファイルでは成功し、16.9MBのファイルでは失敗するといった感じでした)

はじめは、Lambdaの処理に時間がかかって強制終了しているだけかなと思っていたのですが、 メトリクスのInvocationsをみてもピクリともしておらず、 そもそもLambda自体が呼び出されていないのではということで、 S3アップロード時に発生するイベントについて確認してみました。

実際に発生しているイベントを確認してみる

まず、前回のファイルアップロード時にLambdaの起動に成功・失敗したそれぞれのファイルについて、 LocalStackを使用して実際に発生するイベントを確認してみました。

前回成功していたファイル

前回Lambdaの起動に成功していた7.9MBのファイルでは、ファイルアップロード時にObjectCreated:Putのイベントが発生していました。

{
    "Records": [
        {
            "eventName": "ObjectCreated:Put",
            "s3": {
                "object": {
                    "size": 7888901,
                }
            }
        }
    ]
}
前回失敗していたファイル

前回Lambdaの起動に失敗していた16.9MBのファイルでは、ファイルアップロード時にObjectCreated:CompleteMultipartUploadのイベントが発生していました。

{
    "Records": [
        {
            "eventName": "ObjectCreated:CompleteMultipartUpload",
            "s3": {
                "object": {
                    "size": 16888901,
                }
            }
        }
    ]
}

上記の結果通り、ファイルサイズの違いによってeventNameObjectCreated:PutObjectCreated:CompleteMultipartUploadで異なっていることが確認できました。

解決方法

ということで、AWSの3Sイベント通知に上記の2つObjectCreated:PutObjectCreated:CompleteMultipartUploadをLambda実行のトリガーとして設定することで、無事に問題が解決することが分かりました。

もう少し詳細に

好奇心でもう少し詳細に調べてみます。

まず、Laravel8のS3ファイルアップロードにはleague/flysystem-aws-s3-v3が使用されており、さらにその中ではaws/aws-sdk-phpが呼び出されています。

そしてこのaws/aws-sdk-phpupload関数まで辿っていくと、Docコメントに「アップロードサイズが閾値(mup_threshold)を超えると、multipart uploadsを使用するよ」と記載されていました。

f:id:h0r4k:20220403045255p:plain
aws-sdk-php(3.216.3)のupload関数のDoc

こちらにも同様の記載があります。
Amazon S3 Transfer Manager with AWS SDK for PHP Version 3 - AWS SDK for PHP

mup_threshold (int)
Size in bytes in which a multipart upload should be used instead of PutObject. Defaults to 16777216 (16 MB).

はたしてホントでしょうか・・・?
確かめてみます。

確かめてみる

先程のDocに記載されていた、mup_thresholdの値を基準に、224-1 (16777215) byteのファイルと224 (16777216) byteの2種類のファイルを作成しました。

では、実際にこれらの2つのファイルをアップロードし、イベントの差異を確認してみましょう。

16777215 byteのファイルアップロード時の3Sイベント

結果こちらはObjectCreated:Putとなりました。

{
    "Records": [
        {
            "eventName": "ObjectCreated:Put",
            "s3": {
                "object": {
                    "size": 16777215,
                }
            }
        }
    ]
}
16777216 byteのファイルアップロード時の3Sイベント

結果こちらはObjectCreated:CompleteMultipartUploadとなりました。

{
    "Records": [
        {
            "eventName": "ObjectCreated:CompleteMultipartUpload",
            "s3": {
                "object": {
                    "size": 16777216,
                }
            }
        }
    ]
}

やはりDocに記載の通り、mup_thresholdの値 224 byteがPutObjectCreateMultipartUploadの実行の境目となっているようです。

ということで、今回はS3ファイルアップロード時のイベントについてのプチ調査記事でした。
最後まで見て頂きありがとうございました。

さいごに

これまで培った技術力を活かして地域を元気にしたいという想いを持っている方を絶賛募集中です。
ご興味をお持ちいただけましたら、是非以下の募集ページをご覧ください!

www.wantedly.com

SREがLoGoチャットボットを作ってみた

パブリテック事業部でSREを担当している大場です。
先日、LoGoチャットボットの理解を深めるため、自分でボットを作成してみたのでノウハウを書き留めておこうと思います。

LoGoチャットボットとは

パブリテック事業部では、自治体さま向けにLoGoチャットというビジネスチャットを提供していますが、あわせてLoGoチャット上で動作するボットもご利用いただいています。
LoGoチャットはdirectのOEM製品であるため、「daab SDK」を使ってボットを開発することができます。
daab SDKの詳細は以下を参照してください。
daab デベロッパー | daab デベロッパー

※実際にLoGoチャットボットを動かすには、LoGoチャットのアカウントが必要です。

「daab SDK」を使ってみる

機能としては、特定のメッセージを投稿するとボットが返事をしてくれるというだけのシンプルなものです。
Node.jsで実装しますが、実際のコードはこちらです。

'use strict';

module.exports = (robot) => {
  robot.hear(/心の俳句|おはようございます|おはよう$/i, (res) => {
    const haiku_list = [
        "草むしり いいことなんて たぶんない",
        "やわらかく おいしいものを 我は所望す",
        "夏の日に ゴキブリ退治 光る汗",
         == 省略 ==
        "春がきて それがすぎたら 次は夏"
    ];
    let idx = Math.floor(Math.random() * Math.floor(haiku_list.length));
    res.send({
        text: haiku_list[idx],
    });
    res.send({
      text: "友蔵 心の俳句",
    });
  });
};

robot.hearでメッセージを受け取り、res.sendでボットからメッセージを送信します。
今回は、”心の俳句”、"おはようございます"、"おはよう"のいずれかのメッセージを送信すると俳句のリストからランダムで選択したメッセージをボットが送信する処理になっています。
これでボットの実装は完了です。

動かしてみる

  • ボットのアカウントを準備する
    まずはボットのアカウントをLoGoチャットに参加させます。

  • daabのインストールとdaab initを実施する

$ npm install -g daab

$ daab init
? package name (作ったフォルダ名) 【そのままEnterでOK】
? version (0.1.0)  【そのままEnterでOK】
? description 【そのままEnterでOK】
? author 【そのままEnterでOK】
? choose files you need ~ 
> ( ) .cfignore
  ( ) .dockerignore
・・・略 
【そのままEnterでOK】

プロジェクトフォルダの中に以下のような構成(ファイル・フォルダ)が作成できていれば成功です。

$ ls -la
bin/
scripts/
external-script.json
package.json
  • ログインする
$ daab login
[2022-03-30 03:47:19]  WebSocket opened.    
Email: 【ボットのEmailアドレスを入力してEnter】
Password: 【ボットのパスワードを入力してEnter】
logged in.

$ cat .env  | grep HUBOT_DIRECT_TOKEN
HUBOT_DIRECT_TOKEN=【ランダムな文字列が追記されている】

正常にログインできると、.envファイルにHUBOT_DIRECT_TOKENが追記されます。

  • ボットを起動する
$ daab run 
npm WARN deprecated chokidar@1.7.0: Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependencies.
npm WARN deprecated fsevents@1.2.13: fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.
npm WARN deprecated urix@0.1.0: Please see https://github.com/lydell/urix#deprecated
npm WARN deprecated resolve-url@0.2.1: https://github.com/lydell/resolve-url#deprecated

== 省略==

[2022-03-30 03:58:33]]  WebSocket opened.

WebSocket opened.が表示されれば起動完了です。

動きました!

最後に

簡単にボットを作ることができ、友蔵オタクのメンバーにも喜んでいただけましたw
実際はAWS ECSに乗せて動かしたりしているので、今後この辺りも書いていけたらと思います。

トラストバンクではエンジニアを募集しております!
ポジションにとらわれず、いろいろなチャレンジができますのでご興味ありましたら是非募集ページをご覧ください! www.wantedly.com

npm scriptが動かない!?Windows環境で引っかかった落とし穴

トラストバンクのフロントエンドエンジニア、田口です。
ずいぶん暖かくなってきましたね。でも花粉が舞っているので毎日家にこもってエルデンリングをやってます。神秘マンです。
さて今回は既存のnpm scriptが動かなくなったときの話になります。かなり初歩的な内容ではありますが、環境によっては気づきにくい落とし穴かもしれません。

前提

ふるさとチョイスの一部コンテンツではejsを使用しており、そのコンテンツのhtmlをビルドするためにnpm scriptでgulp(と、そのプラグイン)を使っています。
htmlをビルドする際、jsonで作成しているコンテンツデータを取り込み、ejsに流し込んで最終的なhtmlにしています。
該当のnpm scriptは自分を含めた何人かの手が入っており、利用した人も複数いますが、これまでで異常は発生していませんでした。

唯一のWindowsユーザーで発生したエラー

弊社のエンジニアの多くはMacユーザーなのですが、Windowsユーザーにejsの変更作業をお願いしたところ、ejsのビルドに失敗しました。
そのWindowsユーザーはWSLを使用しておらず、Git bashを使っていました。

ログを出力してもらいながら調査していたところ、以下の箇所でエラーになっていました。

data.page = file.path.split('src/ejs/')[1].replace('.ejs', '.html').replace('index.html', '');

内容としてはパスを文字列として置換をかけているような処理です。
何がおかしいのか。file.path には文字列のファイルのパスが入っていますが、それをMacPOSIX)とWindowsで比較すると、

'/your/directory/something.html' // Mac
'C:\\your\\directory\\something.html' //Windows

このように、ディレクトリの区切り文字が異なっていました。

解決策

色々試してみましたが、お手軽なPath.sep*1を使用する形に変更しました。

data.page = file.path.split('src' + path.sep + 'ejs' + path.sep)[1].replace('.ejs', '.html').replace('index.html', '');

その他のパスを文字列として扱っている箇所も同じように変更して、エラーは発生しなくなりました。よかった!

やはりWSLか…

今回ハマった箇所としてはPOSIXWindowsの違いだったのですが、Node.jsだけでなく、様々な面でWSLの使用をお勧めすべきなのだと改めて感じました。
Node.jsに関して言えば、Windowsで引っかかる点は幾つもあります。*2
Windowsユーザーを考慮してコードを書くのも一つの手ですが、やはりWSLを使用する方向で開発組織として統一してしまうのがベストなのだと思います。

ということで今回はあっさりとここまでです。お疲れ様でした。

いつもの

トラストバンクではnpm scriptによってフロントエンドの開発効率をグングン向上できるような、開発環境整備にも強いエンジニアを募集しております! www.wantedly.com

冬も終わりなのでワーケーションしてみた話🏕

コロナ禍&基本リモートワークなこともあって、今年の冬はほとんど家に引きこもっていたSREのbutadoraです。

弊社では今年からフレックス勤務とワーケーションが可能となり、 3月に入って気候も暖かくなってきたので早速試してきました🙋

計画編

最初は単純にどこか旅行先を決めて宿からお仕事スタイルで考えていましたが、
昨年からキャンプデビューして冬キャンを夢見ていた私は近場のキャンプ場を探し始めました*1

Google Mapで埼玉県内をウロウロしていたところ、狭山市にある智光山公園キャンプ場を発見しました。
ちょうど今年の3月にリニューアルオープン予定で、Wi-Fi設備の設置といったワーケーション機能を強化したということでこちらに決めました。

今回は冬キャンもワーケーションも初めてということで、電源付きオートサイトを借りることにしました。
これで万が一Macのバッテリーが切れても電源は確保できますし、自分のキャンプ装備で寒さに耐えられなかったときに車に逃げ込むことができます💪

また、キャンプ場はチェックインが11:00からとうこともあって、 午前半休で午後からワーケーションすることにしました。

ざっくりとしたスケジュールはこんな感じです。

1日目

  • 9:00 カーシェア貸出、買い出し
  • 11:00 チェックイン、テント設営開始
  • 13:00 出勤
  • 17:00 退勤&焚き火開始
  • 22:00 消灯

2日目

  • 6:00 起床
  • 8:00 チェックアウト
  • 9:00 カーシェア返却
  • 10:00 出勤

当日の様子

キャンプ地

(キャンプ場のページに掲載されてフリー素材と化したのは内緒)

Wi-Fi設備

ちなみにCiscoのAPだった

焚き火

ご飯

ワーケーション感想

  • meet, zoomのノイズキャンセリングが強力
    • ヘリコプターや航空機の音や設営の音がしたが、通話相手には届いていなかった
    • でもカラスの声はしっかり届いていたらしい
  • 半日(4時間)程度の勤務であれば、ビデオ通話しながらでもバッテリーは持つ
    • ディスプレイの輝度が常にMAXでバッテリーの消耗が激しい
  • キーボードを打つ手がかじかんだ
    • 3月に入って暖かくなっていたものの、冬に屋外でコーディングは厳しさがありそう
  • 保温ボトルを持っていくべきだった
    • 暖かい飲みものも一瞬で冷めていくし、自宅で用意して持参すべきだった
  • なんだかんだでワーケーション最高!
    • ずっと家にこもりっぱなしだったのもあっていいリフレッシュになった
    • オープン翌日&平日だったこともあってか、お客さんも少なく快適に過ごせた*2

さいごに

昨年トラストバンクにJoinして、エンジニアが働きやすい環境づくりをSREとして推進する一方、 今回のワーケーションのような制度づくりが進められており、日を追うごとに働きやすさ向上を実感しています。

こんな働き方もできる弊社ではSREを絶賛募集中です。
興味がある方はぜひ一度お話しましょう!

www.wantedly.com

*1:某キャンプアニメに触発されて始めた口です

*2:見える範囲だと私を含めて4組だけ

Laravelでクリーンアーキテクチャ (ディレクトリ構造編)

トラストバンクふるさとチョイス事業本部CTO室に所属の海塩(うしお)と申します。
主に新規プロジェクトの立ち上げやアーキテクチャ設計を行っております。

前置き

PHP・Laravelにおけるクリーンアーキテクチャ実践の一例をご紹介します。
クリーンアーキテクチャ自体の説明はしておりませんのでご了承ください。
すでになんらかの形でドメイン駆動やクリーンアーキテクチャを実践している方に刺されば幸いです。
全体ボリュームがかなり大きいので、今回はディレクトリ構造のお話です。
今後は、

などを予定しています。

ディレクトリ全体像

こんな感じになります。

├── app
│   ├── Console
│   ├── Contexts
│   │   └── {コンテキスト名}
│   │       ├── Domain
│   │       │   ├── Entity
│   │       │   │   └── {エンティティ名}
│   │       │   ├── Exception
│   │       │   ├── Notification
│   │       │   └── Persistence
│   │       ├── Infrastructure
│   │       │   ├── Http
│   │       │   │   ├── Controller
│   │       │   │   └── Request
│   │       │   ├── Notification
│   │       │   │   └── templates
│   │       │   ├── Persistence
│   │       │   └── Presenter
│   │       │       └── templates
│   │       └── UseCase
│   │           └── {リソース名}
│   │               └── {アクション名}
│   ├── Exceptions
│   ├── Http
│   ├── Models
│   ├── Policies
│   ├── Providers
│   └── View
├── bootstrap
├── config
├── database
├── public
├── resources
├── routes
│   └── contexts
│       └── {コンテキスト名}
├── storage
├── tests
└── vendor

なるべくLaravel標準に従うことで、

  • プロジェクト参画者のキャッチアップがしやすくなる
  • Laravelのバージョンアップに低コストで追従できる

といったメリットが得られます。

app/Contexts

コンテキストを表現するディレクトリになります。
コンテキスト毎にレイヤー構成を持ち、コンテキストをまたいだ参照は一切無いようにします。

app/Contexts/{コンテキスト名}/Domain

クリーンアーキテクチャの「Enterprise Business Rules」と「Interface Adapters」にあたります。 ビジネスロジックがここに集まるようにします。
Laravelに関連するコード(グローバルメソッドやファサードも含む)は置いてはいけません。

app/Contexts/{コンテキスト名}/Domain/Entity

エンティティを配置します。
エンティティに紐づく値オブジェクトはサブディレクトリに配置します。

app/Contexts/{コンテキスト名}/Domain/Exception

コンテキストのビジネスロジックにより発生する例外を配置します。

app/Contexts/{コンテキスト名}/Domain/Notification

ビジネスロジックにおいて、何らかの通知を行う場合のインターフェースを配置します。
UserStoredNotificationみたいな名前で、handleメソッドのみを持つインターフェースであることが多いです。

app/Contexts/{コンテキスト名}/Domain/Persistence

エンティティの永続化、および復元に関連するインターフェースを配置します。
以下のセットをリソース単位で用意することが多いです。

  • UserRepository:単一エンティティの永続化と復元を行うインターフェース
  • UserListRepository:エンティティの一覧の復元を行うインターフェース
  • UserRepositoryRecord:エンティティとリポジトリとの間で使用するDTOインターフェース

app/Contexts/{コンテキスト名}/Infrastructure

クリーンアーキテクチャの「Frameworks and Drivers」にあたります。
ビジネスロジックの実現手段をここに配置します。

app/Contexts/{コンテキスト名}/Infrastructure/Http

Web経由でのコントローラを配置します。
LaravelのControllerやFormRequestを活用します。
本家クリーンアーキテクチャと大きく動きが異なります。
コントローラはユースケースを呼んで終わりではなく、戻りを受け取ります。
さらにユースケースからの戻りをプレゼンターへ渡してUIを生成し、Webのレスポンスとして返却します。

app/Contexts/{コンテキスト名}/Infrastructure/Notification

Domain/Notificationのインターフェースの実装を配置します。
メール通知であればLaravelのMailableやSendGrid APIを活用します。

app/Contexts/{コンテキスト名}/Infrastructure/Persistence

Domain/Persistenceのインターフェースの実装を配置します。
データベースへの永続化であればLaravelのEloquent Modelを活用します。

app/Contexts/{コンテキスト名}/Infrastructure/Presenter

Web経由でUIを伝えるための実装を配置します。
クリーンアーキテクチャの「Interface Adapters」にあたりますが、上述の通り構成はだいぶ異なります。
同じレイヤのコントローラから呼ばれて戻り値としてUIを返すことになるため、インターフェースと実装は分離しません。

app/Contexts/{コンテキスト名}/Infrastructure/Presenter/templates

コンテキスト固有のBladeテンプレートを配置します。
layoutや共通パーツなどはLaravel標準のresources/viewsに置きます。 \Illuminate\Support\Facades\View::addNamespaceによって動的にディレクトリを設定するため、configやLaravelコードはいじりません。

app/Contexts/{コンテキスト名}/UseCase

クリーンアーキテクチャの「Application Business Rules」にあたります。
Domainコードを活用し、ビジネスロジックを組み立てます。
ここにもLaravelに関連するコード(グローバルメソッドやファサードも含む)は置いてはいけません。

終わりに

いかがでしたでしょうか。
クリーンアーキテクチャの実践には正解がなく、日々改善を重ねています。
トラストバンクでは、堅牢で変更に強いプログラムを書きたいエンジニアの方を募集しています!

www.wantedly.com

クライアントでのエラー監視要員としてSentryを導入した話

どうもはじめまして、トラストバンクでフロントエンドエンジニアをしている田口です。 早速ですが少し前にそのふるさとチョイスにSentryを導入したので、今回はそれについて書こうと思います。

解決したかったこと

ふるさとチョイスはありがたいことにとても多くのユーザーがいます。そしてそれに比例するように、クライアント環境もたくさんあります。
特にふるさとチョイス、というより弊社のサービスはその性質上、他社さんのサービスと比べて恐らくですが、特殊な環境であったり、IEなどの古めな環境で使われることが多いです。
それゆえ、ユーザーから不具合に関する問い合わせを受けても、こちらで持っている環境ではほとんど再現せず、原因調査と改善に至らないこともあります。
まぁIEゆえに発生するような不具合は再現しやすいほうなのですが……。

このような環境依存の不具合に関する問い合わせは、サービス全体としては特殊な例ですが、それが発生したユーザーにとってはその不具合が全てですから、解決しないとなるとサービスから離れてしまい、ユーザーにとっても我々にとっても不幸な結果になります。
こうした不幸を少しでも減らすために、原因調査の手助けとしてSentryを導入することとしました。

Sentryへの登録と仮導入

Sentryについての説明は省略しますが、まずは登録をします。
登録は英語ですが、特に複雑なことはありません。必要項目を入力して進めていくと、すぐにStart upを開始できるようになります。
導入に使用する言語が選択可能で、JSだけでなくJSフレームワークであるReactやVue、Node.js、もしくは別の言語を選択することもできます。

f:id:gatag:20220225190915p:plain
クライアントサイドだけでなくサーバーサイドも

Qiitaにもさらっと書いたのですが、ふるさとチョイスはJSフレームワークをまだ導入していないので、普通のJSを選択しました。
言語を選択すると、あとはもうチュートリアルの手順に従ってコードを仕込むだけです。めちゃ簡単……

仮導入で気になったこと

仮導入していて2つほど気になったことがありました。

dsn って公開していいの?

Sentry SDKのコードを実装するにあたり、dsnがベタ書きなのが気になりました。dsnというのはお察しの通り(?)、Data Source Nameの略語です。
Sentryのドキュメントにもdsnの説明がありますが、公開された状態でも問題無いと書かれています*1ので、結局特に何もせずベタ書きにしています。

コード量が増えた

ふるさとチョイスはES2015↑のコードをWebpack + Babelに噛ませてビルドしています。
で、実際にSentry SDKを実装したところ、ビルドされたJSファイルがまあまあ膨れ上がりました。
これに関してはパフォーマンスを確認した上で許容としていますが、個人的にはなんとかしたいと思っています。なんとかならんのか

チューニング

チュートリアルの内容だけでもエラーを検知してSentry画面上で確認することはできたのですが、実際に使っていくうえではチューニングの必要性が見えてきました。

whiteListUrlsの設定

whiteListUlrsは監視するファイルを正規表現で範囲を絞り込むための設定なのですが、最初はこれをドメインレベルまでの設定にしていました。
これだけでもサードパーティのJS(マーケティングツール関連など)にて発生したエラーを監視してSentryに送るようなことは無くなりました。しかし、送られてくるエラーのほとんどが内容的にサードパーティのものであり、こちらで実装したものではありませんでした。

f:id:gatag:20220228105217p:plain
GTMのJSでエラーが起きているのをキャッチしてしまった
なんでかな〜と思っていたところ、辿っているうちに発生箇所がdocumentの中だということに気づき、document内に書かれてしまうとドメインレベルでのホワイトリスト設定は通過してしまうことが分かりました。

document内に書いてあるJSが一切無いというわけではないのですが、それらのほぼ全てがタグマネージャやアナリティクスに関するものだったため、ホワイトリストとしてはJSのディレクトリに絞りました。

tracesSampleRateの設定

これはSentryのドキュメントをちゃんと見ていなかっただけなのですが、普通にユーザーが来訪する本番環境に適用するには、tracesSampleRateをチュートリアルで書かれている1.0から下げるべきだったのです。
それに気付かずに本番環境に導入したことで、仮導入したページは一部だったにも関わらずかなりの勢いでトランザクションが消費されてしまいました。

f:id:gatag:20220228105713p:plain
来訪の少ない一部のページだけに入れたはずなのに42%も食われた
本格導入するにあたっては1.0のままではすぐにトランザクションを使い切ってしまうので、これを下げます。

そもそもSentryのトランザクションとはどういう時に使われるのか、調べてみたところどうやらSentryのトランザクションはエラー監視ではなくパフォーマンス監視に使用されているようです。*2
ふるさとチョイスはNew Relicも使用していますので、パフォーマンス監視はSentryではそれほど必要ではないはず……ということで思い切って0.001にまで下げました。

テスト環境やローカル環境時の動作の実装

これに関しては仮導入の段階で実装していました。
テスト環境やローカル環境で発生したエラーに関しては、本番環境のものとは分けて管理するか、そもそも監視しないという選択肢になると思います。
まずローカル環境に関しては全く監視しなくて良いので除外できるようにしたい……
テスト環境に関しては監視しつつも本番環境とは分けて管理したいため、何かしらの識別子が欲しいところ……
と思っていたところenvironmentというそのまんまのパラメータが用意されていました。
ということでドメインに基づいて簡単な制御を追加しておきました。

const getSentryEnv = () => {
  const domain = document.domain;
  if (/* ローカルだったら */) {
    return 'local';
  } else if (/* 本番だったら */) {
    return 'production';
  } else {
    return 'test';
  }
};
const sentryEnv = getSentryEnv();
if (sentryEnv !== 'local') {
  Sentry.init({
    // dsn等は省略
    environment: sentryEnv,
  });
}

これからやっていきたいこと

現段階ではまだ軽くチューニングをしただけですので、本格的に運用するためにはやるべきことがいくつかあります。

エラーのトリアージ方針決め

全てのエラーに全力対応するぜ!みたいな気合いや人員、時間があればいいのですが、現実そうはいかないので、エラーによっては対応しなかったり、逆に他のエラーを差し置いてでも対応する必要があるものもあると思います。

現状ではまだ一部ページだけでSentryの監視をスタートしているのですが、それでもすでに1日5件のペースでIssueが増えていますので、サービス全体を監視し始めるより前にトリアージ方針をある程度は決めなければなりません。
サンプルとなるデータも集まり始めているので、それを基に検討していきたいと思います。

フィルタリング

トリアージ方針とほとんど同じなのですが、Sentryの表示自体にもフィルタリングをかけると見やすくなるだろうなと思っています。

また、Unresolved、For Reviewなど、エラーごとにステータスをつけるというフィルタリングもありますので、ステータスの変更フローなどの検討もこれに含めて考えています。
SentryではReleaseという管理項目(?)もあり、SDKでパラメータを送ったりすることで、どのリリース後に起きたエラーかをタグ付けすることができます。これもまたエラーのフィルタリングで使えそうです。 ReleaseはGithub Actionsでの連携が基本になりそうなので、少しハードルは高そうですが、使用することでのメリットが大きそうなので検討していきたいところです。

Slackとの連携を直す

チャンネルを作成してSlackとSentryの連携まではできたものの、今のところ稀にメッセージが入るだけで、新しいエラーがSentryに追加されてもそれがSlackで通知されないという状態になってしまっています。 恐らく厳し目なフィルタリング設定をしてしまっていると思うので、連携設定の見直しをはかろうと思っています。

まとめ

クライアントで発生したエラーを簡単に取得できるようになった一方、まだまだ本格的に運用するには設定等の見直しが必要な状態ですので、引き続きSentryの面倒を見ていこうと思います。

トラストバンクではSentryを導入して運用してきたような、クライアントで発生するエラーにも強いエンジニアを募集しております! www.wantedly.com

それでは〜ノシ

ふるさとチョイスの推奨・動作環境を厳密化させる試み

トラストバンクふるさとチョイス事業本部CTO室リーダーの礒部です。

私が所属しているCTO室というチームは、エンジニア組織の横断的な管理、プロジェクトにおける技術的側面のサポート、新技術導入の提案・検証・実行などといった業務を行っています。

その取り組みの一つとして、ふるさとチョイスが正常に動作する環境(ブラウザ・OS)を厳密に定義しようという試みを進めているので、それについて書きたいと思います。

ふるさとチョイスのフロントエンド事情

推奨環境の前にふるさとチョイスのフロントエンド事情について簡単に触れておきたいと思います。

ふるさとチョイスのフロントエンド開発環境は、タスクランナーにgulp、モジュールバンドラーにwebpackを利用しています。

JavaScriptはES6以上の記法で記述されているため、レガシーな環境でも動作させられるようにwebpack+Babelで以下のbrowserslistの設定をもとにビルドを行っています。

"browserslist": [
    "last 1 version", // 各環境の最新バージョン
    "ie >= 11", //IE11以上
    "ios >= 10.2", // iOS10.2以上
    "android >= 4.4" // android4.4以上
]

CSSにおいてもSCSSのコンパイル時にautoprefixerでこの設定を参照しています。

browserslistには各環境の最新バージョンという広い指定から、ブラウザ・OSのバージョン指定といった細かい指定までできます。

この情報をもとに必要なベンダープリフィックスCSSに付与したり、JavaScriptを動かすのに必要なpolyfillを入れたりするので、あまり幅広い範囲を指定してしまうと、レガシーな環境で動かすための余計なコードが増えてファイル容量が圧迫したりパフォーマンスが低下するといったことも起こり得ます。

現状の推奨・動作環境

現状のふるさとチョイスのbrowserslistでサポートされる環境をリストアップしたものが以下です。

Android Browser 98 (0.00% global usage)
Android Browser 4.4.3-4.4.4 (0.25% global usage)
Android Browser 4.4 (0.00% global usage)
Chrome 98 (0.02% global usage)
Edge 98 (0.00% global usage)
Firefox 97 (0.02% global usage)
IE 11 (0.60% global usage)
IE Mobile 11 (0.02% global usage)
Safari on iOS 15.2-15.3 (3.50% global usage)
Safari on iOS 15.0-15.1 (5.89% global usage)
Safari on iOS 14.5-14.8 (3.53% global usage)
Safari on iOS 14.0-14.4 (0.87% global usage)
Safari on iOS 13.4-13.7 (0.28% global usage)
Safari on iOS 13.3 (0.08% global usage)
Safari on iOS 13.2 (0.02% global usage)
Safari on iOS 13.0-13.1 (0.04% global usage)
Safari on iOS 12.2-12.5 (0.60% global usage)
Safari on iOS 12.0-12.1 (0.04% global usage)
Safari on iOS 11.3-11.4 (0.04% global usage)
Safari on iOS 11.0-11.2 (0.07% global usage)
Safari on iOS 10.3 (0.11% global usage)
Safari on iOS 10.0-10.2 (0.03% global usage)
Opera 83 (0.02% global usage)
Safari 15.2-15.3 (0.86% global usage)

browserslistではIE, iOS, Android以外は最新のバージョンとなっているので、PCは範囲が狭く、スマホは逆に範囲が広いです。

browserslistCan I Useの情報(Can I UseはStatCounter GlobalStatsのデータをもとにしている)をもとにリストを作成するため、実際のサービスのユーザー情報は加味されません。

GAのアクセスユーザー情報をもとに推奨環境を設定する

そこで実際にサイトを訪れているユーザーの情報をもとにbrowserslistを設定するためbrowserslist-ga-exportを活用してみようと考えました。

browserslist-ga-exportのGithubに書かれている手順は以下のようになります

  1. GAのユーザー設定の編集から言語をEnglish(United States)を選択する
  2. GA上でカスタムレポートを作成する

    • 種類はフラットテーブルを選択
    • ディメンションはオペレーティングシステム、OSのバージョン、ブラウザ、ブラウザのバージョン、デバイスカテゴリの並び順で設定(並び順重要)
    • 指標はページビュー数を選択

    f:id:trustbank-developers:20220222093332p:plain

  3. カスタムレポートの画面からCSVをエクスポートする

    • カスタムレポートを開き、絞り込む期間を設定
    • ブラウザで昇順にソート

    f:id:trustbank-developers:20220222093426j:plain

    • 表示件数を5000件に設定
    • 「エクスポート」からCSV形式でエクスポート
  4. 3でエクスポートしたCSVを以下のコマンドで読み込ませてJSON化する(—outputPathの指定をしなければデフォルトでbrowserslist-stats.jsonが生成される)

     browserslist-ga-export --reportPath {CSVファイル} 
    

この手順で生成されたJSONをもとにbrowserslistを出します。

npx browserslist "> 1% in my stats”

サイト利用率1%以上を対象にした場合、以下のようになりました。

and_chr 88
android 98
chrome 96
chrome 95
chrome 94
chrome 93
chrome 92
chrome 91
chrome 90
chrome 89
edge 97
edge 96
edge 95
edge 94
edge 93
edge 92
edge 91
edge 90
edge 89
firefox 80
ie 11
ios_saf 15.0-15.1
ios_saf 14.5-14.8

閾値を何%にするかは、これから調整していきますが、この方法で以前よりも推奨・動作環境を厳密化させることができると思います。

終わりに

ブラウザやOSの更新頻度はどんどん早くなってきていてユーザーが利用している環境も目まぐるしく変わっていきます。そんな中でサイトの推奨・動作環境をどのように定義していくかというのは難しい問題ではありますが、仕組みでどうにか解決していきたいですね。

P.S. 2022年6月15日にとうとうIE11がサポート終了しますね。