Next.js+TypeScriptをmonorepoで構築したアプリに共通処理を配置するモジュールを作成した

はじめに

こんにちは! RECJob開発チームの福山(@posion_404)です。 社内ではpoisonって呼ばれております。

今回は、プロダクトを作成している際に共通処理をモジュール化して管理することを実施したのでそのことについて書いていきます!

なぜモジュール化を分けようと思ったのか

今チームで開発している、RECJobというプロダクトはReact/Next.js+TypeScriptをmonorepoで構築されています。

そこで、各アプリにまたがって同じ処理を書くことが増えてきたので、いっそのこと共通ディレクトリを作成してそこで管理しようということになりました。 同じ処理を書く際にアプリをまたぐことで使用面においても管理面においても色々と制御しやすいと考えました。

(monorepoの構築に関してはこちらの記事で紹介しているので一読してみてください!)

techblog.roxx.co.jp

実際の作業内容

共通ディレクトリを作成する

私が作った構造としては下記のような形になります!

front
├── app1
├── app2
├── app3
├── modules ⇨ 今回の共通ディレクトリ
│   ├── src
│        └── utils ⇨ 処理が格納されるディレクトリ
│   ├── .eslintrc.json
│   ├── package.json
│    └── tsconfig.json
├── package.json
└── yarn.lock

同じ階層にはeslintrcやpackage.jsonも用意して、testを実施したりplugin等を楽に入れられるようにしました。 これによって、modulesの品質等も担保できるかと思います。

各アプリから呼べるようにする

各アプリの階層には、tsconfigがそれぞれ用意されています。 tsconfigの中でcompilerOptionsのpathsによって、importする際の命名規則を設定できるかと思います。 そこに、今回のmodulesフォルダの階層を設定することで、楽に処理を呼び出せるようにしました。

app1/tsconfig.json

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@recjob/modules/*": ["../modules/src/*"]
    },
  },
}

また、next.configに関しても修正します。

app1/next.config.js

const nextConfig = {
  reactStrictMode: true,
  experimental: {
 // trueに設定する
    externalDir: true,
  }
}

externalDirに関しては、Next側で用意されているもので 外部のディレクトリへアクセスできるようにしてくれる設定となります。 この設定が入ることにより、importを呼べたりすることができるということになります!

参考 Experimental feature for allowing importing Typescript files outside of the root directory by shuding · Pull Request #22867 · vercel/next.js · GitHub

導入したことによる効果

導入したことにより、チーム内で高評価を得ることができました! 実装する際も各アプリでそれぞれ書くのではなくmodulesに格納することでそれを呼び出せばいいということにより、ストレス等も激減されました!

また、今回は他チームのエンジニアさんにもヘルプをもらいながら実施しました。とても感謝しております!

まだ処理についてのtestや共通処理化できていない箇所が残っています。 今後もリファクタリングを進めて、より良い品質を目指していきたいと思います!

また、このやり方以外にも共通処理を作る際のフォルダ設定等があるかもしれないと思いますので コメント等で教えていただけると幸いです!

現在株式会社ROXXは一緒にはたらく仲間を募集中です。 herp.careers

herp.careers

herp.careers

LIFF InspectorでLIFFアプリのデバッグ環境を構築する

こんにちは、RECJob開発チームエンジニアの佐藤(@r_sato1201)です。
この度、業務でLIFFを触ることになり、開発する上でデバッグできる環境を構築したのでやり方をまとめていきたいと思います。

LIFFとは?

LINE Front-end Frameworkの略で、LINEが提供するウェブアプリのプラットフォームです。
LINEのユーザー情報を取得しユーザー情報を活用した機能を提供することができます。

developers.line.biz

LIFF Inspectorとは?

LIFF InspectorはLIFF専用のDevToolsです。
LIFF Inspectorを使えばLINE上でLIFFアプリを動かしている際にChromeのDevToolsのElements, Console, Networkタブを使いデバッグすることができます。

LIFF Inspectorは

  • LIFF Inspector Server (LIFFアプリとChromeDevtoolsの通信を中継するサーバー)
  • LIFF Inspector Plugin(LIFF Inspectorを利用可能にするLIFFプラグイン

の2つのコンポーネントから構成されています。

github.com

デバッグ環境の構築

ここからはデバッグ環境の構築方法と、実際にデバッグする方法を説明していきます。
RECJobではLaravel + Nextを採用しているので大まかな流れは以下です。

  1. LIFF Inspectorサーバーを起動する
  2. LIFF Inspector PluginをLIFFアプリにインストールする
  3. ngrokでローカルサーバーをSSL化し公開
  4. LIFFアプリのエンドポイントURLに設定
  5. Next側のenvの値を変更
  6. Chromeで開発者ツールを表示

1. LIFF Inspectorサーバーの起動

以下のコマンドを実行してLIFF Inspectorサーバーを起動します。

npx @line/liff-inspector

実行するとDebugger listening on ws://XXX.X.X.X:9222というWebSocketのURLが表示されます。 これがLIFF InspectorサーバーのURLになります。

2. LIFF Inspector PluginをLIFFアプリにインストールする

LIFF Inspectorを利用可能にするためにLIFF Inspector Pluginをインストールします。

yarn add --dev @line/liff-inspector

次に、liff.initしているファイルでLIFF Inspector Pluginをliff.useしてプラグインを使えるようにします。

+ import liff from '@line/liff';
+ import LIFFInspectorPlugin from '@line/liff-inspector'


+ liff.use(new LIFFInspectorPlugin())
await liff.init({ liffId: LIFF_ID })

LIFF Inspector Pluginは、実際のliff.initが動作する前に、LIFF Inspector Server に接続を試みるようです。
接続が成功し、liff.initが完了すると、LIFF Inspectorを使ったデバッグができるようになります。

3. ngrokでローカルサーバーをHTTPSで公開

LIFFアプリはHTTPSでホストされているため、LIFF Inspector ServerがHTTPSでホストされていないと、mixed contentとなり接続することができません。
そのため、LIFF Inspector ServerをHTTPSでホストする必要があります。
また、LIFFアプリをLINEアプリがはいっている端末上で動かすため、ローカルサーバーを外部公開する必要があります。

そこで、ngrokでフロント側、サーバー側、LIFF Inspector Serverの3つをHTTPSで公開します。

3-1. ngrokをHomebrewでインストール

ngrok公式サイトでアカウント登録してメール認証を済ませ、下記のコマンドでngrokをインストールしてください。

brew install ngrok/ngrok/ngrok

3-2. 認証トークンの設定

ngrok公式サイト管理画面の「Getting Started > setup & installation」で認証トークンを取得して以下のコマンドを実行します。

ngrok config add-authtoken <token>

3-3. ngrokのconfigファイルを書き換える

3つのポートを公開するために、ngrokの設定ファイルを書き換えます。
configファイルは以下にあります。

Linux: "~/.config/ngrok/ngrok.yml"
MacOS (Darwin): "~/Library/Application\ Support/ngrok/ngrok.yml"
Windows: "%HOMEPATH%\AppData\Local\ngrok\ngrok.yml"

ngrok.com

公開したいそれぞれのportをngrok.ymlに追加します。

ngrok.yaml

version: "2"
authtoken: <token>
tunnels:
  api:
    addr: 8080
    proto: http
  front:
    addr: 3000
    proto: http
  liff-inspector:
    addr: 9222
    proto: http

3-3. ngrokで3つのポートを公開する

以下のコマンドを実行し3つのポートをHTTPSで公開します。

ngrok start api front liff-inspector

4. LIFFアプリのエンドポイントURLに設定

LIFFアプリのエンドポイントURLを変更します。
LIFFアプリに設定しているエンドポイントにli.origin=wss://xxxx-xxx-xxx.ngrok.ioというクエリをつけてURLを設定してください。
{frontのngrok公開URL}/?li.origin=wss://{LIFF Inspector Serverのngrok公開URL}

例) http://localhost:3000をエンドポイントに設定したい場合で、ngrokで公開したURLが以下の場合

localhost ngrokで発行したURL
localhost::3000 https://front.jp.ngrok.io
localhost::9222 https://line-inspector.jp.ngrok.io
https://front.jp.ngrok.io/?wss://line-inspector.jp.ngrok.io

になります。
※ front, line-inspectorの部分はご自身で実際ngrokで公開したURLに置換してください。(xxxx-xxx-xxx-xxx-xみたいなやつです)

5. Next側のenvの値を変更

Next側のenvのAPI_ROOTをngrokで発行したURLに変更します。

- NEXT_PUBLIC_API_ROOT=http://localhost:8080/api
+ NEXT_PUBLIC_API_ROOT=https://api.jp.ngrok.io/api

apiの部分はご自身で実際ngrokで公開したURLに置換してください

6. Chromeで開発者ツールを表示

LINEアプリに端末からアクセスすると、起動したLIFF Inspectorサーバーのログに下記のようなログが表示されます。

connection from client, id: 1234567890-xxxxxxxx
DevTools URL: devtools://devtools/bundled/inspector.html?wss=xxxx-xx-xxx-xxx-xxx.jp.ngrok.io/?hi_id=1234567890-xxxxxxxx

このdevtools://devtools/bundled/inspector.html?wss=xxxx-xx-xxx-xxx-xxx.jp.ngrok.io/?hi_id=1234567890-xxxxxxxxChromeのアドレスバーに入力することで開発者ツールが表示され、デバックができるようになります。

最後に

LIFF InspectorでLIFFアプリのデバッグ環境を構築する方法を書いてみました。
ngrokのは無料プランだと公開URLが毎回変わるため、公開するたびにURLを変更しなくてはいけませんが、有料プランにすることでURLを固定できるので
毎回、URLを変更するのが面倒な方は有料プランを検討したほうが良いと思います。

現在株式会社ROXXは一緒にはたらく仲間を募集中です。 herp.careers

herp.careers

herp.careers

サクッと作れる! React でブラウザ拡張機能のテンプレートを作ってみた

Reactを用いてブラウザ拡張機能を作成する際に、サクッと作り始められる目的でテンプレートを作ったので紹介します。

基本の構成としては、React + TypeScript + esbuildを使ったものとなっており、@htlsneさんのテンプレートをベースに、開発を始める際に個人的にそろっていてほしい環境を追加で導入したものになります。 zenn.dev

※ベースの段階でbuild周りの実装など綺麗にまとまっており素晴らしいテンプレートとなっていますので、環境周りは自分でやるから最低限でいいよという方にはベースのこちらをお勧めします。

成果物

github.com

想定する利用場面

  • Reactで環境構築を考えずにブラウザ拡張機能を作り始めたい時にお使いいただけます。
  • それ以外の場合は...

モジュールバンドラー

冒頭でも説明しましたが、esbuildを利用しています。 buildの実行、ターミナルで以下コマンドを実行することで、build.tsをエントリーポイントとしてビルドが走ります。

npm run build

バンドル周りの詳細な実装については、ベースとしたテンプレートを説明している記事で詳しく解説されていたので、そちらをご覧ください。 content-script.jsファイルなど、popup.html以外の実装が必要になった場合にも、build.tsaddBuildFile()addBuildFile()にパスを追加するだけで簡単にビルドができるような設定になっているようです。

zenn.dev

スタイリング

tailwindcssのようなCSSフレームワークを使いたかったのですが、2022/07時点では、esbuild本家でPostCSSがサポートされておらず、自分で拡張する必要があったので今回は導入しませんでした。 その代わり、手軽に導入できることもあり、emotionを導入することでCSS-in-JSでスタイリングするようにしました。

emotion.sh

なお、CSS ModulesはComponentファイル内に配置しないためにCSS-in-JSと比較して可読性が劣ったり、localスコープでのスタイルの適用が難しかったりという理由から、インラインでstyleタグを利用することについては、レンダリングの度にスタイルを読み込む必要があることからDOM Elements - Reactで非推奨とされており、それぞれ候補から外しました。

CSS-in-JSを実現するライブラリには、古くから使われているstyled-componentsがあったり、zero runtime CSS-in-JSとして実行時ではなくbuild時にCSSを生成してくれるlinariaもありましたが、linariaは動的にスタイルを生成することができず、この辺はもう少し知見を溜めてから導入したいこともあり、後発で多機能なemotionを採用することとしました。

開発補助

テスト

テストについては、プロジェクト毎にユニットテストからE2Eテストなどをどこまで必要とするのか状況次第であることを想定して導入していません。 また、Jestなどであれば後から簡単に導入できるため、上述の理由も含めてあえて事前に導入する必要はないと判断しました。

linter/formatter

linter/formatterについてはベースのテンプレートを踏襲して、eslint、prettierを使っています。

eslintはtypescript-eslintreact/recommendedなど、基本的にReactで導入する際にデフォルトで推奨されている設定がメインになります。

追加している要素として、emotionのeslintルールを追加しています。

.eslint.js

module.exports = {
  plugins: ['@typescript-eslint', '@emotion'],
  ...
  rules: {
    ...
    // emotion
    '@emotion/jsx-import': 'error',
    '@emotion/pkg-renaming': 'error',
    '@emotion/no-vanilla': 'error',
    '@emotion/import-from-emotion': 'error',
    '@emotion/styled-import': 'error',
  },
  ...
};

emotionのStyledを利用する際に、構文のルールを統一してくれたりするルールなどが含まれています。

CI/CD

CI

huskylint-stagedを利用して、pre-commit時にコードフォーマットと型の静的解析を行なっています。

github.com

その他テストを導入したら、Github Actionsを使ってPRを作成した際にテストを回すように設定してもよさそうですね。

CD

デリバリーに関しては、Github ActionsでChromeFirefox用のdistファイルをそれぞれ.zipファイルに圧縮して、tagでリリースを作成することを想定していますが、こちらは現在未対応となっています。

Tips

スタイルのテーマ管理

@emotion/reactのライブラリ側で定義されたTheme型を上書くことで、独自テーマを一貫して利用することができます。

emotion.sh

popup/Popup.tsx

const Title = styled.h1((props) => {
  const { theme } = props;
  return {
    ...theme.typography.h1,
    color: theme.palette.text.primary, // props.themeオブジェクトから呼び出せる
  };
});
emotion.d.ts

// @emotion/react で定義されている Theme を上書き
declare module '@emotion/react' {
  export interface Theme {
    typography: {
      h1: TypographyType;
      h2: TypographyType;
      h3: TypographyType;
    };
    palette: PaletteType;
  }
}
popup/index.tsx

// 独自テーマの定義
const theme = {...}

ReactDOM.render(
  <ThemeProvider theme={theme}>
    <Popup />
  </ThemeProvider>,
  document.getElementById('root')
);

詳細な使い方については、以下の記事にわかりやすくまとまっていたのでこちらを参照ください。

zenn.dev

作成したChrome拡張機能を利用する方法

npm run run:chromeコマンドなどでデバックはできますが、これだけでは実際に使える状態にはなりません。 ここでは作成したブラウザ拡張機能Chromeで利用する方法を簡単にご紹介します。

  1. npm run buildもしくはnpm run build:chromeコマンドを実行すると、ルートディレクトリにdist-chromeというビルドの成果物が含まれたディレクトリが生成されます。
  2. chrome://extensions にアクセスします。
  3. デベロッパーモード」と表記のあるトグルスイッチをクリックして、デベロッパーモードを有効にします。
  4. 「パッケージ化されていない拡張機能を読み込む」ボタンを押すと選択モーダルが開くので、ビルドしたdist-chromeディレクトリを選択します。

developer.chrome.com

以上で作成したChrome拡張機能が利用できるようになります。

まとめ

以上、さくっと作りたい時に使えるブラウザ拡張機能のテンプレートの紹介でした。 デリバリー周りなどもう少し調整したい箇所はありますが、サクッと作り始めるという目的は達成できるテンプレートになったかと思います。 今後リポジトリを更新した際は、合わせてこちらのドキュメントも随時更新していこうと思っています。

はじめてブラウザ拡張機能を作る方なども、手軽に作れるので是非お試しください。

UA プロパティの計測データを Analytics Reporting API v4 で一括エクスポート救出大作戦する

この記事は個人ブログと同じ内容です

www.ritolab.com


Google Analytics(以下、GA)では、2022 年 7 月にユニバーサルアナリティクス(以下、UA)プロパティでの計測処理が停止します。

support.google.com

計測停止後、およそ半年程度は GA の画面から計測結果を閲覧できるとのことですが、それ以降はページすら見れなくなるそうです。

これを受けて我々は UA プロパティから GA4 プロパティへと移行しなくてはならないわけですが、移行と言っても正確には GA4 プロパティでの計測を新たに開始することであり、この時、これまで UA プロパティで計測してきた過去のデータを GA4 プロパティ側に持ってくることはできません。

では折角積み上げてきた UA プロパティでの計測データをこのまま手放すしかないのでしょうか。

今回は Google が提供している Analytics Reporting API を使って UA での計測データを一括してエクスポートしていきます。

Google Analytics Reporting API v4

Google アナリティクス Reporting API v4 は、GA のレポートデータを取得することのできる GA 公式の API です。

developers.google.com

Google アナリティクス Reporting API v4 は、Google アナリティクス のレポートデータにアクセスするための最も高度な プログラマティック メソッドです。 Google アナリティクス Reporting API では、次のことが可能です。

  • Google アナリティクスのデータを表示するカスタマイズされたマイレポートを作成する。

  • 複雑なレポートタスクを自動化して時間を節約する。

  • Google アナリティクスのデータを他のビジネス アプリケーションと統合する。

API を使わなくても GA の画面から各レポートのデータエクスポートが出来ますが、できるだけ未集計のデータで落としたいなどの場合にはこれでは難しい場合があります。

例えばレポートの日付範囲を選択するとその範囲で集計されたレポートがエクスポートされるため、ログデータのように時系列で並んだ個々の計測データとして落とすことはおそらく標準の計測項目だけでは不可能です。 (カスタムディメンジョン使ったらいけるかもしれないけどやってみたことはありません)

こういった、できるだけ「1 計測 1 レコード」のような形式で期間的に広範囲なデータをまとめてエクスポートしたい場合は Analytics Reporting API が使えます。

これを使って GA から UA での計測データを一括で持ってきます。

エクスポートするデータの計画

まずはどんなデータを、どんな形で落とすかを計画しておきます。

今回は、PV(PageView)のデータをエクスポートしたいとしましょう。

エクスポートするデータはできるだけ集計されておらず、計測された日時順に並んだログのような体裁で落として、それを何らかのデータベースに入れて分析に使えるようなデータとして落としたいです。

引っ張るものとしては以下がほしいですね

  • 計測日時
  • ページ URL(どのページを閲覧したのか)
  • リファラー(pvなのでセットでほしい)
  • ホスト名(環境が複数あっても識別できるように)

ひとまずこれくらいのデータが UA プロパティから持ってこれれば、ページビューのデータが最低限救出できたと言えそうです。

Analytics Reporting API の有効化

まずは GCP 側で Reporting API を使えるようにします。

以下のページの「1. API を有効にする」を行えば API が使えるようになります。

developers.google.com

やることは大きく 3 つです。

  • Analytics Reporting API の有効化
  • サービスアカウントを作成し、キー(Credential)を作成。そしてそれをダウンロード(JSONファイル)
  • GA 側に作成した API ユーザーを登録

2 で取得する Credential(JSONファイル)は API を使用する際に必要になります。

実装について

Analytics Reporting API を用いたデータエクスポートの実装は PHP で行います。

composer を実行できる環境が必要です。

パッケージインストール

Google API クライアント ライブラリを composer でインストールします。

composer require google/apiclient:^2.0

Analytics Reporting API を用いたデータエクスポートの実装

それでは Reporting API を用いて、UA データの救出プログラムを実装していきます。

サンプルコードは予め Github に上げてあります。

手続き的に実装するとしても、以下を上から順番につらつらと記述していけば実行できます。

1. クライアント初期化

まずは API と通信を行うクライアントオブジェクトを作成(インスタンス化)します。

<?php

$client = new Client();
$client->setApplicationName("Analytics Reporting");
$client->setAuthConfig('<ここに credential JSON ファイルのパス>');
$client->setScopes(['https://www.googleapis.com/auth/analytics.readonly']);
$analytics = new AnalyticsReporting($client);

setAuthConfig() の引数には、事前にダウンロードした credential(JSON ファイル)のパスを渡します。

ここで最終的に作成する AnalyticsReporting オブジェクトが、 GA のレポートデータにアクセスする役割を担います。

2. リクエストの作成

次に、GA へのリクエストを実装します。

「どのようなデータを取得したいか」の条件の部分を定義していく工程です。

リクエストオブジェクト作成

<?php

$request = new ReportRequest();
$request->setViewId('<ViewID>');

setViewId() の引数には、UA プロパティの「ビュー ID」を渡します。

「ビュー ID」は、GA の UA プロパティ管理画面「ビュー」にある ID のことです。

取得する日付範囲の指定

<?php

$dateRange = new DateRange();
$dateRange->setStartDate('2022-07-01');
$dateRange->setEndDate('2022-07-09');
$request->setDateRanges($dateRange);

いつからいつまでのデータを取得するかをセットしています。

ディメンジョン指定

<?php

$pagePath = new Dimension();
$pagePath->setName("ga:pagePath");

$hostname = new Dimension();
$hostname->setName("ga:hostname");

$dateHourMinute = new Dimension();
$dateHourMinute->setName("ga:dateHourMinute");

$sourceMedium = new Dimension();
$sourceMedium->setName("ga:sourceMedium");

$previousPagePath = new Dimension();
$previousPagePath->setName("ga:previousPagePath");

$request->setDimensions([
    $dateHourMinute, $hostname, $pagePath, $sourceMedium, $previousPagePath
]);

ディメンジョン(軸)を指定しています。

エクスポートするデータの計画に従い、今回は以下をディメンジョンとして指定しました。

  • ga:pagePath:計測ページ URL
  • ga:hostname:ホスト名
  • ga:dateHourMinute:計測日時
  • ga:sourceMedium:参照元 / メディア
  • ga:previousPagePath:前のページ URL

ちなみに、リファラのディメンジョンとして ga:fullReferrer がありますが、「外部から来たのか」「サイト内回遊なのか」がわかれば良いので今回は取得していません。

参照元 / メディア」を見れば「外部リンクから」「検索結果から」「ブックマークなど直接」くらいはわかるので、あとは「前のページ URL」がどうなっているかでサイト内回遊かどうかがわかる。というところで今回のディメンジョン指定になっています。

ディメンジョンとセットする値の対照は下記ページに記載があるので実際に指定したいものを探せます。

ga-dev-tools.web.app

ディメンジョンのフィルター

指定したディメンジョンに対してフィルターを掛けることができます。

特定のページに絞ったり、あのリファラは除外したり。みたいなことです。

<?php

$pathFilter = new DimensionFilter();
$pathFilter->setDimensionName('ga:pagePath');
$pathFilter->setOperator('REGEXP');
$pathFilter->setExpressions(['^\/entry\/[0-9]+$']);

$filters = new DimensionFilterClause();
$filters->setFilters([$pathFilter]);

$request->setDimensionFilterClauses($filters);

上の例では、ページ URL に対して、正規表現で指定したものに合致するページのみに絞り込む指定を行っています。

フィルタリングする条件が複数ある場合は、以下のようにして記述します。

<?php

// ページ URL 絞り込み
$pathFilter = new DimensionFilter();
$pathFilter->setDimensionName('ga:pagePath');
$pathFilter->setOperator('REGEXP');
$pathFilter->setExpressions(['^\/entry\/[0-9]+$']);

// 特定のリファラを除外
$referrerFilter = new DimensionFilter();
$referrerFilter->setDimensionName('ga:fullReferrer');
$referrerFilter->setOperator('EXACT');
$referrerFilter->setExpressions(['(direct)']);

$filters = new DimensionFilterClause();
$filters->setOperator('AND'); // 両方の条件を AND 条件として絞る
$filters->setFilters([$pathFilter, $referrerFilter]);

$request->setDimensionFilterClauses($filters);

ポイントとして、DimensionFilterClause クラスの setOperator() によって AND 条件か OR 条件かを指定できます。

なお、DimensionFilter クラスの setOperator() によって、「条件の形式(完全一致、部分一致など)」を指定できますが、引数としてどういった形式を指定できるのかは下記のページで確認することができます。

developers.google.com

ちなみにディメンジョンとしてセットできるのは最大 9 個までです。

メトリクス指定

次に、メトリクスを指定します。

<?php

$pageView = new Metric();
$pageView->setExpression("ga:pageviews");
$pageView->setAlias("pageviews");
$request->setMetrics([$pageView]);

今回はページビューを取りたいので、その指定を行っています。

ここで、できる限り集計されていない生データのような状態(1 計測 1 レコード)でエクスポートしたいのですが、標準のディメンジョンの場合、時間に関するものの最小値が「分」のため、同一分の PV はまとまってしまうので、そこは許容するしかありません。 (カスタムディメンジョンを作成してマイクロミリ秒まで送信したらもしかしたらいけるかもしませんが、同一分で全く同じ PV ならどうせ移植先のデータベースで同じものとして集計するだろうし、今回はこのままにしています)

データの並び順指定

<?php

$orderByDate = new OrderBy();
$orderByDate->setFieldName('ga:dateHourMinute');
$orderByDate->setSortOrder('ASCENDING');
$request->setOrderBys([$orderByDate]);

時系列で並べたいので、ga:dateHourMinute にて昇順で取得するように指定しています。

ページサイズ指定

<?php

$request->setPageSize(2000);

「一回のリクエストで何件取得したいか」を指定しています。

ここでは 2000 を指定していますが、取得する全件のデータが 10,000 件だった場合、そのうちの 2,000 件を一回のリクエストで取得する。という意味になります。

Reporting API ではページングやオフセット的にデータを取得できるので、エクスポート対象が数万、数十万件であっても、このページサイズの指定によってマシンのスペックに見合った件数に分割して取得していくことができます。

つまり、2000 件ずつの取得でループさせて計 5 回のリクエストにて 10000 件取得していくような流れで実装するイメージです。

これで一通りのリクエストを作成できました。

3. データの取得

作成したリクエストを以て、GA からデータを取得します。

<?php

$body = new GetReportsRequest();
$body->setReportRequests([$request]);
$response = $analytics->reports->batchGet($body);

setReportRequests() の引数である変数 $request は、この前の工程で作成した ReportRequest オブジェクトです。

前工程で作成した AnalyticsReporting オブジェクトより、(厳密には内包されている Reports オブジェクトの)batchGet() を実行することで、GA からデータを取得できます。

変数 $response にデータが入ってくるので、あとは好きに整形して CSV に書き込むなりすれば、UA プロパティからデータをエクスポートできます。

UA データ救出大作戦実行

動作確認を行ってみます。

これまでは Reporting API の実装をポイントで解説したため、ループや取得データの整形、保存含め、一連の実装については Github のサンプルコードを参照いただくとわかりやすいと思います。 (サンプルコードは、必要な箇所を設定・定義すれば動作するようにはなっていますが、PC に予め composer と Docker Desktop のインストールが必要です。)

今回は、2022 年 6 月(一ヶ月分)の、ある箇所のページビューのみに絞って取得してみます。 3000 件ずつ取得し、データは CSV に書き込んでいくようにしています。

ターミナルにてコマンド実行します。

% docker compose run --rm php-81-cli php index.php
2022-06-01 - 2022-06-30
remaining: 42291
remaining: 39291
remaining: 36291
remaining: 33291
remaining: 30291
remaining: 27291
remaining: 24291
remaining: 21291
remaining: 18291
remaining: 15291
remaining: 12291
remaining: 9291
remaining: 6291
remaining: 3291
remaining: 291

3,000 件ずつ取得し、全部で 42,300 件程度データをエクスポートしました。

date,datetime,hostname,pageview,page_location,source_medium,previous_page_path
2022-06-01,"2022-06-01 00:06:00",www.ritolab.com,1,/entry/56,"google / organic",(entrance)
2022-06-01,"2022-06-01 00:08:00",www.ritolab.com,1,/entry/24,"google / organic",(entrance)
2022-06-01,"2022-06-01 00:09:00",www.ritolab.com,1,/entry/54,"google / organic",(entrance)
2022-06-01,"2022-06-01 00:09:00",www.ritolab.com,1,/entry/93,"google / organic",(entrance)
.
.
.
2022-06-30,"2022-06-30 23:50:00",www.ritolab.com,1,/entry/217,"google / organic",(entrance)
2022-06-30,"2022-06-30 23:51:00",www.ritolab.com,1,/entry/93,"google / organic",(entrance)
2022-06-30,"2022-06-30 23:52:00",www.ritolab.com,1,/entry/93,"google / organic",(entrance)
2022-06-30,"2022-06-30 23:57:00",www.ritolab.com,1,/entry/67,"google / organic",(entrance)

まとめ

今回はページビューで取りましたが、ユーザーやセッションなど、ディメンジョンやメトリクスの指定で一通りデータは取れますし何より一括でまとめて落とせますので、UA プロパティのデータが必要な場合には Analytics Reporting API を活用すると効率的にデータを持ってこれます。

ちなみにリクエストに関する制限(どれくらいの数や量をリクエストできるのか)は以下に記載がありますが、そこそこ寛容なので大きすぎる規模でなければ制限ラインには抵触しないかなと思います。

developers.google.com

Analytics Reporting API 便利なので試してみてください。

サンプルコード


現在 back check 開発チームでは一緒に働く仲間を募集中です。

herp.careers

herp.careers

herp.careers

herp.careers

herp.careers

herp.careers

Clarityをプロダクト上に設定してみた

はじめに

こんにちは! 4月からRECJob開発チームに所属したエンジニアの福山(@posion_404)です。 社内ではpoisonって呼ばれております。

今回は、チームで作っているプロダクトにCralityを導入したので、備忘録も兼ねて記事を書こうと思います。

Clarityとは

Microsoft社が出しているヒートマップツールになります。無料ながら一定以上のサイト解析ができるため、サイト訪問者の行動や、LPでどこが見られているかなど、サイトやページ改善に大いに役立ちます。

なぜ導入したのか

ユーザーがログインしてからどのように操作するかを見ることができるため、その操作ログを観察し、NextActionを生み出すことでプロダクトの改善につながることから導入を決めたという背景があります。

現時点でも、観察から改善に繋がったケースがあり、とても重宝しているツールとなっております。

設定手順

1.ユーザー登録

ホームページにアクセスをし、登録を行なってください。

2.プロジェクトの作成

アカウントを登録し、サインインが完了すると下のような画面になると思います。

そこで「新しいプロジェクトを作成する」ボタンを押下してプロジェクトを作成してください。

押下すると、必要情報を入力することになるので、

  • 「名前」にプロジェクト名

  • 「Web サイト URL」にCralityで観察したいWebサイトのURL

をそれぞれ入力して、「新しいプロジェクトに追加する」を押下してください。

3.コードを取得

その後、Clarityの導入方法を選択する画面になります。

Shopify、SquareSpace、WixWordPress などを使用していればそこにインストールすることになりますが、今回はプラットフォームは使わずコードを自分で入れる方法で実施します。

「追跡コードを取得」を押下すると下のような画面になるので、クリップボードに登録してご自身のWebサイトに組み込みましょう!

4.データを見る

コードを入れると、下準備は完了となります。

ですが、すぐにはデータは取得されず画面が変わらないかと思います。 公式のよくある質問を見るとこのような記載がされているかと思います。

When will I start seeing data on the Clarity dashboard? Once you finish installing the script on your website, Clarity will start collecting the data. You can view this data in your project dashboard within 2 hours of adding the script. You can verify this by following these steps.

docs.microsoft.com

コードを入れてから2時間以上経過するとデータを取得することができるという仕様のようです。 実際に、データが取れると下のような画面で確認できるかと思います。

これで、設定に関しては完了となり、ユーザーの情報やWebサイトに訪問した際に、どのような操作をしているのかを確認することができるようになります!

GTM(Google Tag Manager)との接続

上記で紹介したやり方とは違い、GTMを使用している方であれば、GTMの設定につなげてそこから情報を取得することも可能となっております。

GTMで設定するとClarityが勝手にGTMのAPIを叩いてGTMに設定が追加されるので、既に使用しているトリガー等を用いてデータが取得できるようになってます。

これはとてもありがたい限りですね!

カスタムタグに関して

ユーザーのIDを取得してその値でフィルタで絞ってログを確認したい方もいると思います。

こちらに関しても実施可能で、上記で取得したコードの中に埋め込むことでユーザーのID等の情報も併せて取得することができます。

Clarity.js

(function(c,l,a,r,i,t,y){
      c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
      t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
      y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
  })(window, document, "clarity", "script", "xxx");
  clarity("set", "取得したいKey名", "Key名に対するValue値");

最後に

Clarityを初めて扱ったので、備忘録として残しました。 次回は、実際にCralityをどのように使用しプロダクト改善に充てているのかなどの内容で書いてみたいと思います。

現在株式会社ROXXは一緒にはたらく仲間を募集中です。 もし興味がある方はお気軽にお問い合わせください!

herp.careers

herp.careers

herp.careers

コーチングわいわい会で学んだ「やりたい」と「いきいき」

この記事は個人ブログと同じ内容です

コーチングわいわい会で学んだ「やりたい」と「いきいき」

はじめに

こんにちは、back check 事業部でスクラムマスターをやっているぐっきー(@Area029S)こと山口です。 今回は外部イベントのコーチングわいわい会のご紹介と参加して学んだことをお話ししたいと思います。

コーチングわいわい会

コーチングわいわい会とは、職業やドメインを超えてコーチングという言葉が浸透してきた昨今、「コーチングについて、プロのコーチも、学んでいる人も、興味がある人もフラットに語れる場」としてTAKAKING22さんが開催してくれたオンラインイベントです。

私はスクラムマスターとして、チームや組織の成長を考える上でコーチングがどう機能するのかについて、コーチングと関わりのあるいろいろな人の声を聞いてみたく参加してきました。

イベント内容

このイベントでは事前にアジェンダを用意せず、コーチングをテーマに参加者が議論したい内容を提案し、興味をもった人たちが自主的にセッション枠を割り当てて議論を作っていく形式で開催されました。

参加者の層も幅広く、プロのコーチからスクラムマスター、開発責任者、エンジニアなど最近勉強を始めた人、コーチングってなにそれ美味しいの?な人を含め様々な人が参加されていました。 日頃からアジャイルコミュニティのイベントに参加している方が多かったことも含め、武士以外は開発に関わる職種の人が多かったです。

会場の雰囲気

参加者が議題を持ち寄っていることもあり、どのセッション会場も盛り上がっていました。

印象的だったこととして、コーチングのイベントだけあり参加者の方々みなさんが聞き上手でスピーカーの話をどんどん深堀りしていく様子がどの会場でも見受けられました。 その結果コーチングの理解の浅い私も、コーチング初見者の方々も議論を途切れることなく理解を深めることのできる場が成立していたように思います。

わいわい会で得た学び

クライアントとコーチング双方のコミットが大切

コーチングがうまく機能しなかった体験談を聞くことができました。

クライアントとしてコーチングを体験したスピーカーがコーチと対話をする中で yes と言わざるを得ない質問に誘導されていると感じてしまう状況があったというお話しでした。 その結果、コーチはなにを聞きたくてこの対話に望んでいるのか不信感を感じてしまい成果を得られなかったという内容でした。

ここでスピーカーが得た学びとしては、コーチにもそれぞれ相性があり、クライアントがいかにナチュラルな状態でコーチとの対話に臨めるかが大切ということでした。 コーチングもクライアントとコーチ双方がお互いに力を発揮できる状態でないとうまく機能しない、その上でクライアントもコミットしようとすることが大切だという議論がありました。

また、期待値の不一致があった場合にはコーチングの中止を切り出してくれるコーチがいたという話もあり、必要なことをズバッと言ってくれることも大切だよねという意見がありました。

やりたいことへの気づき

また、成功体験についてもお話を聞くことができました。

スピーカーさんが研修でたまたまコーチングを受けたときのお話です。 コーチから「お金も時間も十分にあって、なんでもできるならなにがしたい?」という質問を受けて「静かな湖畔の家に住みたい」という欲求に気づくことができた。そして、この願いは実は仕事の引退まで我慢する必要もなく、今できることだった。その結果、現在は長野に引っ越してやりたいことを一つ叶えた状態になれた。という内容でした。

この話では、コーチングによって自分の願いに気付けた。無限には来ない未来というやつですねぇ。というコメントがありました。

私自身、自分の欲求を再認識するということは見失いがちだからこそ大切だと感じました。

おわりに

気づいていないだけで、日々の仕事でも自分が喜びを感じられるような小さな「やりたい」はたくさん散らばっていると思います。 日々の仕事の小さな「やりたい」が視えるといきいきと働けるのではないでしょうか?もしかしたら「やりたい」のためには働くことすら不要かもしれませんね。

コーチングは「やりたい」に気づかせてくれる手段のひとつです。 もしよければ、コーチングを学んでみてはいかがしょうか?

「やりたい」を大切にしていきいきと働きましょう!

最後に、現在 back check 開発チームは一緒にはたらく仲間を募集中です。
ご興味をもっていただけたましたら、 DM 、もしくは下記の応募フォームにてお気軽にカジュアル面談をご依頼ください。

herp.careers herp.careers herp.careers herp.careers herp.careers herp.careers

Laravel SanctumのXSRF-TOKENクッキーを環境毎に使えるようにする方法

はじめに

こんにちは、RECJob開発チームエンジニアの佐藤(@r_sato1201)です。
RECJobではLaravelを採用しており、認証システムにLaravel SanctumのSPA認証を使用しています。

今回は、Sanctumを使用した際にXSRF-TOKENクッキーの取り扱いでハマったので備忘録も兼ねて書きたいと思います。

Laravel SanctumのSPA認証の流れ

まず、Laravel SanctumのSPA認証の流れを書きたいと思います。
(仕組みや詳しいことは公式リファレンスのSanctumのページを参照してください。)

  1. /sanctum/csrf-cookieにリクエストしCSRF保護を初期化
  2. CSRFトークンを含むXSRF-TOKENクッキーを発行してセット(以降はX-XSRF-TOKENヘッダにトークンを入れてリクエストする)
  3. ログインする
  4. セッションを保持し、セッションクッキーを発行
  5. Sanctumのミドルウェアを設定している何らかのルーティングにリクエス
  6. セッションクッキー内で自動的に認証
  7. 認証成功の場合は継続、失敗の場合は401 or 419を返す

ざっくりとですが大体こんな感じです。

何が起きたか

今回起きた事象ですが、本番環境(hoge.jp)にログイン後、ステージング環境(xxx.hoge.jp)でログインする際に
上の流れの2で発行したXSRF-TOKENのキー名が競合し419(csrf token mismatch)が起きるようになってしまいました。

どう解決するか

本番環境とステージング環境とでXSRF-TOKENクッキーのキー名を変更し、それを使えるようにします。

まず、XSRF-TOKENクッキーを生成する際に各環境のprefixをつけるようにします。
例)
本番環境:PRODUCTION-XSRF-TOKEN
ステージング環境:STAGING-XSRF-TOKEN

具体的には、VerifyCsrfTokenミドルウェアの新しいクッキーを生成する箇所をオーバーライドし、各環境のprefixをつけたキー名にするようにします。

app/Http/Middleware/VerifyCsrfToken.php

<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
use Symfony\Component\HttpFoundation\Cookie;

class VerifyCsrfToken extends Middleware
{
    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array<int, string>
     */
    protected $except = [
        //
    ];

    /**
     * Create a new "XSRF-TOKEN" cookie that contains the CSRF token.
     *
     * @param \Illuminate\Http\Request $request
     * @param array $config
     * @return \Symfony\Component\HttpFoundation\Cookie
     */
    protected function newCookie($request, $config)
    {
        return new Cookie(
            strtoupper(config('app.env')) . '-XSRF-TOKEN',
            $request->session()->token(),
            $this->availableAt(60 * $config['lifetime']),
            $config['path'],
            $config['domain'],
            $config['secure'],
            false,
            false,
            $config['same_site'] ?? null
        );
    }
}


次に、フロント側でキー名を変えたXSRF-TOKENを受け取れるようにします。
キー名を変更していなければAxiosなどのHTTP Clientライブラリであれば、特に設定しないでもこのあたりは自動でやってくれますが
今回は変更したキー名で受け取れるようaxiosのInstance生成時に設定します。

api/axiosInstance.ts

import axios from 'axios'

const APP_ENV = process.env.NEXT_PUBLIC_APP_ENV ?? ''

export const axiosInstance = axios.create({
  withCredentials: true,
  xsrfCookieName: APP_ENV.toUpperCase() + '-XSRF-TOKEN',
})

これらを行うだけで、XSRF-TOKENのキー名の競合を解決しそれぞれの環境で扱うことができるようになります。

最後に

同じようなことにハマった人の手助けとなればと思い書きました。
今後も業務の中で発見した知見があったら書いていきたいと思います。

現在株式会社ROXXは一緒にはたらく仲間を募集中です。 herp.careers

herp.careers

herp.careers

参考