開発のプロジェクト管理をGitHub ProjectsからJiraに移行しました

f:id:kazuhei0108:20210219113421p:plain

こんにちは。M&Aクラウドのかずへいです。

今年に入って開発のプロジェクト管理をGitHub ProjectからJiraに移行しました。

どのような意図で今回Jiraに移行したのかを紹介させてもらえばと思います。

GitHub Projectsを利用していたときの課題

GitHub Projectsを使い始めた時はホワイトボードの物理的なカンバンの代わりとしてだったと思います。その時のエンジニア数はまだ3名で、 GitHub Projectsの機能でも特に問題は感じていませんでした。

元の運用は、

  1. issueのマイルストーンバックログを作る。マイルストーン内ではissueが優先度順に並べられる。ストーリーポイントは数字のラベルをissueにつける。
  2. マイルストーンから次のスプリントでやる分だけをスプリントマイルストーンに移動、GitHub Projectsに紐づけてカンバンに表示させる。
  3. スプリント開始!プルリクエストにissueを紐付けておくとプルリクエストがmergeされたときにissueもdoneになって便利。

というシンプルな形でした。

しかし、メンバーが増え、リポジトリが増え、管理するタスクが増えるとこの運用がつらくなってきました。

つらかった点

GitHub Projectsでの運用で辛かった点には以下の様なものがありました。

  1. マイルストーンGitHub Repositoryに紐付いているissueをまとめる機能なので、複数のRepositoryをまたいで一つのマイルストーンにissueを集めることが出来ない。
  2. issueが入れ子関係を持てないのでlabelで管理することになり、labelが増殖する
  3. 消化したストーリーポイントを数えるのがめんどくさい
  4. asigneeをつけ忘れると誰がやってるか分からない
  5. バックログの整理が大変(GitHubマイルストーンは複数のissueをドラッグアンドドロップで動かせない😢)
  6. 何がいつリリース予定なのか整理するのが大変

たまたま、エンジニア以外からもタスク管理ツールを選定して導入して欲しいという話があったので、このタイミングでJiraを全社導入し、開発チームもJiraに移行することに決めました。

Jiraの導入

今回Jiraに乗り換えたことで上記の問題が全て解決されました!

  1. Jiraのバックログリポジトリに関係無いので、複数リポジトリにまたがるストーリーも管理できる
  2. epicを親のissue、storyを子のissueとすることによって、issueを入れ子で管理
  3. スプリントのベロシティを自動で計測してくれるし、epicに属するissueのポイントの合計もすぐ表示される
  4. カンバン上の移動にルールを追加することが出来るので入力漏れがない
  5. バックログのissueは複数選択出来、簡単に動かせる。
  6. いつ何がリリースされるのか一目瞭然

いくつかピックアップして説明していきます!

スプリントのベロシティを自動で計測してくれるし、epicに属するissueのポイントの合計もすぐ表示される

f:id:kazuhei0108:20210217192440p:plain
Jiraのベロシティーチャートの画面

スプリントの機能を使って、スプリントの開始、終了を実行すると毎回の消化ポイントを自動で集計してレポートに出してくれます。 以前はissueについてるlabelのストーリーポイントを数えていましたが、そんなことをする必要はありません!

また、epicに属するissueの合計の見積もり、完了、残りのストーリーもすぐ表示されるので、大きな機能がどれくらいでリリースされそうかわかります。

f:id:kazuhei0108:20210217193332j:plain

カンバン上の移動にルールを追加することが出来るので入力漏れがない

f:id:kazuhei0108:20210217194559j:plain
左下に足りない入力項目が出ています

カンバン上でTO DOからWIPにカードを移動させようとすると、移動するためのルールを満たしているかどうかのチェックが走ります。 今は、担当者とリリース予定の日付を入力しないとWIPに移動出来ないようにしています。

バックログのissueは複数選択出来、簡単に動かせる。

f:id:kazuhei0108:20210217194921j:plain
ドラッグアンドドロップで簡単にissueを動かせます

Jiraのバックログではスプリントとバックログが同じページに表示されており、簡単に移動させることが出来ます。

いつ何がリリースされるのか一目瞭然

f:id:kazuhei0108:20210218194616j:plain
リリース機能

Jiraのリリース機能によって、リリース予定日ごとに何をリリースするかをまとめるようになりました。 GitHubのPRと連動しているので、もうマージされたのか、まだレビュー中なのかというところまで細かく分かるようになっています。

移行手段

すでにGitHubに大量のissueが積まれており(500個以上!)、それらを整理してJiraに移行する作業はどう考えても苦行に思えました。GitHubAPIでissueをダウンロードしてcsvに整形してJiraにアップロードするというのが正攻法のようでしたが、そもそも全部移行する必要は無いのではと考えたため移行したいものだけ移行できるZapをZapierで作りました。

GitHub issueのコメントにjira移行と書くとJiraのストーリーを作ってくれた上で元のGitHub issueのURLを説明文に入れてくれるというすぐれものです。

f:id:kazuhei0108:20210218194141p:plain
zapierのzap作成画面

移行してみて

Jiraに移行したことにより、これから更にチームメンバーが増えたり、Repositoryの数が増えたりしても大丈夫な体制を構築できたと自負しています。 また、想定していたわけではないのですが、開発のビジネス要件が全てJiraにまとまるようになり、PMとのコミュニケーションが円滑になりました。 元々はGitHub Issue上でやっていたことですが、GitHub上で管理すると、どうしてもコードについてのアレコレを書いてしまいがちになったり、どこのRepositoryに起票したらいいんだ、となったりしていました。 これがJiraとGitHubで使い分けられたことにより、PMはJiraのみ見れば良い状態になりました。

まだ導入して1ヶ月ほどですが、開発チームの状態に合わせてこれからカスタマイズを続けていきます!

Nuxt.js化計画vol.5

f:id:kubotak:20200317182842j:plain
Nuxt.js化計画vol.5

第5弾です。

Nuxt.js化計画の概要についてはvol.1を参照ください。 また、過去のシリーズも通してリンクしているのでぜひ御覧ください。

tech.macloud.jp

徐々に弊社アプリケーションのフロントエンドもLaravelからNuxt.jsに移行しています。
今回は成約事例一覧M&Aお役立ち資料ページがめでたくNuxt.js化されました。

実は今までNuxt.jsによる動的なルーティングは使っていませんでした。
今回のリニューアルのタイミングで個別のページはNuxt.jsによるルーティングを利用しています。

成約事例一覧

https://macloud.jp/interviews

弊社のプラットフォームであるM&Aクラウド上での成約事例をインタビュー記事として紹介しているページです。
リニューアル前は絞り込み機能はありませんでしたが、今回Nuxt.js化する際に追加されました。
記事についているタグで絞り込むことができます。

M&Aお役立ち資料

https://macloud.jp/documents

M&A時に必須である資料のフォーマットの配布や、弊社調べによる有益な資料をダウンロードすることができるページです。

NotFoundの扱いについて

ここからは少し技術的な話をしたいと思います。
今回のリニューアルから新たにNuxt.jsの動的なルーティングを利用しています。
ここで注意しなくてはならないのは存在しないページの処理についてです。
ページのコンテンツはHTTP APIによってそのリソースが存在するか確認します。
厳密に言うとリソースが存在することを確認するというよりも、リソースを取得できない場合にそのリソースが存在しなかったという結果をHTTPのステータスコードで表現することが一般的だと思います。

Nuxt.jsではaxiosというHTTPクライアントライブラリを利用してHTTP APIからデータを取得しています。
axiosではレスポンスからHTTPのステータスコードステータスコードが取得できますので、404だった場合はNuxt.jsのerror関数を実行することで404画面を表示することが出来ます。

error({ statusCode: status })

※axiosは200系以外のステータスコードをすべてaxiosErrorという例外で扱われます。適宜設定を替える必要があります。 https://github.com/axios/axios/blob/master/lib/defaults.js#L79-L81

しかし、弊社の場合は取得した値を一旦store(Vuexによるグローバルにアクセスできるデータストア)に格納して、そのstoreからデータを取得するロジックになっています。
そのため、axiosで404の場合はエラーにはせずに、storeからの取得の際にデータがない場合に独自の例外を発生させて404を表現しています。

try {
  useInterview(store).getInterview()
} catch (e) {
  if (e instanceof NotFoundError) {
    error({
      statusCode: e.statusCode
    })
  }
}

SSRCSRに跨ったシャッフル処理

Nuxt.jsはSSR(サーバーサイドレンダリング)とCSR(クライアントサイドレンダリング)によるユニバーサルなアプリケーションを作ることが出来ます。
この挙動に関してハマリポイントがあったので共有します。
それは、シャッフルを利用したDOMの操作です。

対象の箇所は以下のようなカルーセルです。 f:id:kubotak:20210212151612p:plain ここでは表示する度に要素がシャッフルされる仕様です。
配列をシャッフルしてその配列をeachで表示する至ってシンプルなロジックです。

問題になったのは、SSR時にシャッフルされてレンダリングされた結果と、表示時にCSRでシャッフルしてレンダリングが異なるため一部のレンダリングがずれた事にありました。
imgタグのsrcがCSR時に変わっていなくてタイトルと画像が食い違うという事象がありました。

そのため、シャッフル要素に関しては都度表示が変わるためSEOとしても対応不要としてCSR時のみ実行されるように変更しました。
Nuxt.jsではこの様にSSRCSR時の挙動も把握する必要があると痛感しました。

最後に

M&Aクラウドではエンジニアを絶賛募集中ですので、興味がありましたら是非以下からご応募ください!
Nuxt.jsの挙動を熟知している方は大歓迎です!

www.wantedly.com

www.wantedly.com

Vue Component をサイトの各所で別々の条件によって表示する機能を Nuxt.js + TypeScript で実装する

Nuxt.js

Nuxt-2.14 TypeScript-3.7

こんにちは。エンジニアの濱田( @hamakou108 )です。

先日、あるモーダルをサイトの各所で別々の条件によって表示する機能を Nuxt.js + TypeScript で実装する機会がありました。 このときの設計が他の場面でも応用できそうな知見としてまとめられそうだったので、この記事で紹介したいと思います。

話さないこと

  • Vue.js (Composition API), Nuxt.js, TypeScript の基本的な使い方
  • 時間計算などの細かいアルゴリズムの実装

背景

M&Aクラウドでは M&A に役立つ資料の一部のダウンロードや買い手企業様とのマッチング機能を会員登録された方向けに提供しています。 サイトのコンテンツが気になった方に少しでもリーチするため、会員登録を促すモーダルを特定のタイミングで表示しています。 勿論モーダルが何度も表示されてしまうようでは UX を損ねるため、どのページでどのような行動を行ったときにモーダルを表示するか様々な条件によって制御されています。

設計

様々な種類の条件を手続き的に実装してしまうと、保守性に乏しいコードが出来上がってしまうのは目に見えています。 そこでまず幾つかの具体的な要件を取り上げ、それらを抽象化して設計を考えてみることにしました。

今回の要件としては例えば以下のようなものがありました。

  • ページ A の訪問時、最初に訪問してから24時間経過していたらモーダルを表示する
  • パーツ B のクリック時、累計3回目のクリックであればモーダルを表示する

これらの要件を分解してみると「ページ表示やクリックなどのイベント」と「経過時間や回数などの表示条件」の2つの要素の組み合わせという形に抽象化できそうです。 このように分離しておくと、後々「パーツ B のクリック時、最初に訪問してから24時間経過していたらモーダルを表示する」のように組み合わせが異なる要件が追加されても柔軟に対応できそうです。

よって今回はイベント表示条件という2つのモジュール、そしてそれらを利用して Vue Component にモーダルの表示可否の判定や表示イベントの保存を行う手段を提供する Manager というモジュールの主に3つに分けて設計しました。

実装

Event

イベントに関するモジュール event.ts は以下のようになりました。

// ~/compositions/event.ts

export type Event = VisitEvent | ClickEvent

export type VisitEvent = {
  key: 'visit'
  page: string
}

export type ClickEvent = {
  key: 'click'
  page: string
  element: string
}

型を判定するための key やページやパーツの名前といった型定義のみを持っています。 Vue Component でイベントを定義するときや Web Storage に保存している過去のイベント情報を取得するときにこれらの型情報を参照します。

Condition

表示条件に関するモジュール condition.ts は以下のようになりました。

// ~/compositions/condition.ts

import { CustomDate, CustomDuration } from '~/foo/bar/date.ts' // 日付関連の型定義を持つ適当なファイル

export type Condition = TimeCondition | CountCondition

export type TimeCondition = {
  key: 'time'
  durationList: Array<CustomDuration>
}

export type CountCondition = {
  key: 'count'
  countList: Array<number>
}

type CurrentState = CurrentTimeState | CurrentCountState

type CurrentTimeState = {
  key: 'time'
  firstEventOccurred: CustomDate | null
  lastModalShowed: CustomDate | null
}

type CurrentCountState = {
  key: 'count'
  count: number
}

export function useCondition(condition: Condition) {
  const canShowUnderTimeCondition = (
    currentState: CurrentTimeState
  ): boolean => {
    // イベント発生からの経過時間からモーダル表示可能か判定するロジック
  }

  const canShowUnderCountCondition = (
    currentState: CurrentCountState
  ): boolean => {
    // イベントの発生回数からモーダル表示可能か判定するロジック
  }

  const canShow = (
    currentState: CurrentState
  ): boolean => {
    if (currentState.key === 'time') {
      return canShowUnderTimeCondition(currentState)
    } else if (currentState.key === 'count') {
      return canShowUnderCountCondition(currentState)
    }
  }

  return {
    canShow
  }
}

モーダルの表示条件に関する幾つかの型定義に加え、モーダルの表示可否を判定するロジック useCondition を持っています。 表示条件を定めて useCondition の引数として与えておき、 canShow の引数として現在の状態を与えると、これらの情報に基づいて表示可否を判定します。

モーダル表示可否を判定する具体的なアルゴリズムの実装もこれはこれで面白いのですが、冗長になってしまうので割愛します。

Manager

イベントと表示条件を利用して Vue Component にモーダルの表示可否の判定や表示イベントの保存を行う手段を提供するモジュール manager.ts は以下のようになりました。

// ~/compositions/manager.ts

import { useStorage } from '~/foo/bar/storage.ts' // Web Storage 関連の型定義やロジックを持つ適当なファイル
import { Event } from '~/compositions/event'
import { Condition, useCondition } from '~/compositions/condition'

export function useManager(event: Event, condition: Condition) {
  const storage = useStorage(event, condition)

  const canShow = (): boolean => {
    const currentState = storage.getCurrentState()
    return useCondition(condition).canShow(currentState)
  }

  const saveEvent = () => {
    storage.saveEvent()
  }

  return {
    canShow,
    saveEvent
  }
}

特筆すべき点はないですが、補足として useStorage はイベントの発生回数やモーダルの表示時刻などの情報を Web Storage から取得または保存する composition です。 メソッドの引数として渡された condition に応じて必要な情報を取得・保存する役割を持ちます。 詳細な説明は本筋から逸れるので割愛します。

Vue Component

モーダルを表示するページの Vue Component は以下のようになりました。

// pages/some-page.vue

<template>
  <div>
    <some-modal :can-show="canShow"></some-modal>
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, onMounted, reactive, ref } from '@vue/composition-api'
import { customDuration } from '~/foo/bar/date.ts'
import { VisitEvent } from '~/compositions/event'
import { TimeCondition } from '~/compositions/condition'
import { useManager } from '~/compositions/manager'
import SomeModal from '~/components/some-modal.vue'

export default defineComponent({
  components: {
    SomeModal
  },
  setup() {
    const manager = ref<ReturnType<typeof useManager> | null>(null)

    onMounted(() => {
      // イベント: とあるページの訪問
      const event: VisitEvent = {
        page: 'some',
      }

      // 表示条件: 最初のイベント発生から24時間または72時間経過後
      const condition = reactive<TimeCondition>({
        durationList: [
          customDuration(1, 'days'),
          customDuration(3, 'days')
        ]
      })

      manager.value = useManager(event, condition)
      manager.value.saveEvent() // ページ表示イベントの発生情報を保存
    })

    const canShow = computed(() => {
      return manager.value !== null ? manager.value.canShow() : false
    })

    return {
      canShow
    }
  }
})
</script>

「とあるページの訪問時、最初に訪問してから24時間または72時間経過していたらモーダルを表示する」という条件を表現するため、 useManager にはそれぞれ対応するイベント、発生条件のオブジェクトを設定しています。 そして computed を使って manager オブジェクトが生成されたタイミングでモーダルの表示可否を判定し、子コンポーネントのモーダルに props としてその情報を伝えます。

今回のイベントはページの表示なので、ライフサイクルフック onMounted を使って useManager を呼び出しました。 クリックイベントの場合は対象のコンポーネントの emit イベントに対してコールバック関数を定義し、その中で useManager を呼び出すように実装します。

このように useManager を利用して Vue Component 各所でモーダルの表示可否を制御できるようになりました!

最後に

M&Aクラウドではエンジニアを絶賛募集中ですので、興味がありましたら是非以下からご応募ください! Nuxt.js や TypeScript に知見のある方は特に大歓迎です!!

www.wantedly.com

www.wantedly.com

「レバレッジ指向」という開発チームバリューについて

f:id:zacky2:20210126110429j:plain こんにちは、エンジニアの津崎です。

前回の記事で、「全員インフルエンサー」という開発チームのバリューについて紹介しました。 今回もその流れを汲んで開発チームのバリューの紹介をしていこうと思います。

開発チームのバリューについて

M&Aクラウドの開発チームには行動指針として以下の三つがあります。

このバリューは、今月(2021年1月)に開発チームによる合宿*1により策定されました。

今回は、「レバレッジ指向」というバリューについて紹介します。

レバレッジ指向」というバリューについて

レバレッジ指向」とは今回のバリュー策定で生まれた造語です。

レバレッジ」という言葉は、よくFXの話で、「レバレッジの倍率が〜」という使われ方すると思いますが、 このバリューでは、「小さい労力で大きな成果を出す」という意味として使っています。

スタートアップにおけるソフトウェア開発では、「価値を素早く届けること」と「スケールできること」を求められますが、このバランスはとても難しいです。 「価値を素早く届けること」を追いすぎると、技術的負債が増えて価値を素早く届けられなくなったり、「運用でカバー」的作業が増えて開発する時間を失ってしまいます。 一方で「スケールできること」を求めすぎると、機能開発するのに時間がかかり過ぎて商機を逃してしまいます。 このバランスはプロダクトの成長具合によって変化することが求められます。

弊社のプロダクトは、順調にユーザー数を伸ばしており、「スケールできること」を重視するフェーズに差し掛かってきました。 ユーザ増加だけでなく、エンジニアの増加や、プロダクトの複雑性の増大にも対応していく必要があります。 そんな背景があり、より長期的な目線を持って「レバレッジ」の効く選択をしようという方針が固まりました。

「指向」という言葉も議論がありました。 「しこう」には「指向」の他に、「思考」と「志向」がありますが、 「指向」は「方針の選択の局面でどちらの方向性でいくか」という判断の際に使いやすいワードであり、 「オブジェクト指向」といったエンジニアに馴染みのあるワードであるため選ばれました。

つまるところ、「レバレッジ指向」という言葉は、『長期的な目線を持って「レバレッジ」の効く方針を「指向」していこう。』という意味を表しています。

レバレッジ指向」の体現

レバレッジ指向」がバリューに設定されてからまだ日が浅いですが、早速レバレッジ指向を体現する開発を行っています。

コードによるコード生成

データに対応して単純なコードをたくさん作成しなくてはいけないケースにおいて、コードを生成するコードを実装することで対応しました。 コードによるコード生成を行うことで、今後、データに追加、削除があっても簡単に対応することができます。 少ない労力で大きな成果を出す、レバレッジ指向な実装ができたと思います。

この話はPHPer会議での登壇を予定していますので、そちらの方でお話しさせていただければと思います。

fortee.jp

NoCodeツールによる人力の排除

現在、開発チームではIssueの管理をGitHubからJiraへ移行している最中です。 必要なIssueだけJiraに移行するため、 GitHubのIssueに「Jira移行」とコメントすると自動でJiraに同じIssueが作成される仕組みをZapierを使って作りました。 こちらも人力作業を排除するレバレッジの効いた仕事といえます。

リリーススクリプトの改修

弊社のリリーススクリプトは最近になって特定条件でうまく動かない問題が発生していました。 運用でカバーできるものはなかなか根本修正されず、運用でカバーが常態化してしまうことはよくあるかと思います。 今回、「レバレッジ指向」がバリューに設定されたため、運用でカバーを排除する修正がすぐに行われました。

最後に

レバレッジ指向」を体現してプロダクトを爆伸びさせたいエンジニアを積極採用中です。 カジュアル面談もやっておりますので、お気軽にご連絡ください💪

www.wantedly.com

www.wantedly.com

*1:合宿とは名ばかりの泊まり込みのない長時間ミーティング

PHPerKaigi 2021 と「全員インフルエンサー」という開発チームバリュー

f:id:kazuhei0108:20210118230904p:plain

こんにちは。エンジニアの鈴木(@yamotuki)です。

昨年はPHPカンファレンス2020にチーム全員でプロポーザルを出し、4人が登壇することになりました。
今回は PHPerKaigi 2021 にまた全員でプロポーザルを出した話です。この記事を書いている時点ではまだ採択されたか分かりませんので、もし採択されたら追記します。

追記:
5人がプロポーザルを出して、3人が20分枠、2人がLTで発表することになりました。紹介タイトルの先頭に[採択]または[不採択]をつけました。
頑張って発表準備し為になる発表をしたいと思います。お楽しみに!

開発チームのバリューについて

M&Aクラウドの開発チームには行動指針として以下の三つがあります。

これらの詳細はまた別の記事で紹介されると思いますが、今回は全員インフルエンサーについて書きます。

経緯

以前から各種勉強会で活発に発表し、コミュニティの発展に貢献していた久保田さん(@kubotak_public)が弊社に2019年10月にジョインされました。

CTOの荒井はかねてからチーム作りの重要性を感じており、久保田さんジョインと同時期にこのブログを始めることになりました。

運用

エンジニアは情報発信が大事、と近年のWeb界隈ではよく言われていると思います。目的はスキルアップだったり、プレゼンス向上だったり、採用だったり、チーム技術交換だったり、色々あると思います。

しかし、実情としては10人に1人、会社によっては100人に1人くらいしかブログを書いたり登壇したりすることはないのではないでしょうか。 弊社ではより強いチームにするために「全員」が情報発信を積極的に行っていくことに決めました。

それが開発チームのバリューとして2019年11月の開発合宿(という名の長時間ミーティング)で「全員インフルエンサー」として言語化されました。

この行動指針に則って「全員毎週 qiita 記事投稿」(現在は不定期)や、PHPカンファレンス 2020への「全員プロポーザル宣言」が実現されました。 まだエンジニア5人のチームですが、これが10人100人になった時にもこの行動指針を続けられたなら唯一無二のチームになれると信じています。

PHPerKaigi 2021への全員プロポーザル

次の大きな祭りはなんだ?と問われれば、PHPer なら PHPerKaigi 2021 が思い浮かぶのではないでしょうか。 今回はCTOが明示的に「全員プロポーザル宣言」を出さなくても、当たり前のように全員が出すことになりました。ちなみに"沈黙"をしてひよっているのが私です。

f:id:yamotuki:20210115174429p:plain
全員プロポーザル宣言(自発)

さて、どのようなプロポーザルになったのか、紹介していきたいと思います。

[採択]モックの泥沼から脱却するために、あえてDBにつないでテストしている話

fortee.jp

CTOの荒井さんによる、前回PHPカンファレンスで話せなかったリベンジです。 テストを美しく、効率的に書くための工夫について紹介されています。 モックの泥沼に入るとテストのどこが何を表しているか分かりにくいですよね。 モック地獄から救ってくれた荒井さんに清き一票を。

[採択]Laravel のメール認証の内部実装を掘り下げる

fortee.jp

Vimマスターの濱田さんによるプロポーザルです。 今回はフレームワークの内部実装を掘り下げていこうという渋い内容になってます。 プロダクトが立ち上がった時には要らなくても、成長すればじきに必要になってくるメール認証。 案外幅広い開発者に関係するところだと思うので、見たい人はぜひ清き一票を。

[不採択]Casbinを用いたアクセス制御入門

fortee.jp

このプロジェクトを通して設計作業を学びたい!という積極性No.1の津崎さんによるプロポーザルです。 権限管理は自分で丸々実装すると大変かと思いますが、幅広い言語で使用可能な Casbin というライブラリを使うと比較的簡単にできるそうです。 弊社のプロダクトではサービスの管理ツールでCasbinによるアクセス制御を導入しているところです。 こちらも会社成長すると"あるある"の機能なので、ご期待ください。

追記:
このプロポーザルは残念ながら不採択でしたが、このブログが公開された後に出した以下の発表で採択されました。

fortee.jp

[採択]ある日突然、Laravel Queue Workerが壊れた

fortee.jp

「全員インフルエンサー」の始祖、久保田さんによる発表です。 年末に開発チームが襲われた Worker の障害の根本対応の想定される工数は、チーム全員でやっても1週間(辛い)。 そこで「レバレッジ指向」の久保田さんが華麗な解決策を編み出し半日で解決してしまった話です。 その発想の源が垣間見れるのか。楽しみですね。

[採択]プログラマ三大美徳を実現するデプロイフローを目指して

fortee.jp

私の発表です。 デプロイめんどくさいです。昔はもっと面倒でした。 フローなんてあってなかったようなプロダクト初期から、ミスなく楽するために徐々に改善してきた軌跡を共有します。

終わりに

いいなと思ったら、清き一票をお願いします!

弊社では一緒に「全員インフルエンサー」を実現してくれるチームメンバー募集してます。

www.wantedly.com

新年の挨拶2021

あけましておめでとうございます。M&Aクラウドのかずへいです。

去年から大変な時期が続いておりますが、M&Aクラウドは順調に成長しておりまして、 仲間の増加に合わせて、この度オフィスを八丁堀から新宿御苑に移転することとなりました。

f:id:kazuhei0108:20210112134651j:plain
新オフィスのエントランス

せっかくオフィスを移転したのですが、今はほとんどのメンバーはリモートワークをしております。

緊急事態宣言も出ておりますが、より柔軟に、仕事の進め方、コミュニケーションの仕方を変えながら、 いつかまたオフィスでみんなで集まってわいわいする時を思い描きながら頑張っていきたいと思います。

今年の抱負

私の今年の抱負は「発進・発信」です。

今年はM&Aクラウド開発チームとしても飛躍の年にしたいと思っていまして、そのためにはたくさんの新しいことにチャレンジし、 その取り組みを外部に出していくことでチーム全体の増強につなげていきたいと思っています。

また、メンバーの人数が増え、コミュニケーションが少なくなると、メンバー間のずれが起きやすくなりますが、 それを防ぐためには、常に理想を発信し、定着させていくことが必要です。

このブログを始めるときに自分が思い描いたループを意識して、ガンガン回していけたらと思います。

f:id:kazuhei0108:20191024121803p:plain
取り組みを発信して採用につなげる図

それでは本年もよろしくお願いいたいします。

【Laravel】Laravelのスケジューラー経由で実行しているコマンドのログを詳細に取る

こんにちは、M&Aクラウドの荒井です。

以前に弊社の津崎がElasticBeanstalkのワーカー環境の導入について紹介させていただきました。

tech.macloud.jp

弊社ではその後様々な定期実行のスケジュールを移行し、安定稼働に至っています!

ワーカー環境で実行されるコマンドが増えるにつれて、コマンドの実行状況をより詳細に確認したいというニーズが出てきました。

以下でどのように対応したのかを説明します。

Workerサーバーでスケジュールを実行する

弊社ではlaravel-aws-workerを利用しているので、Laravelのスケジュールの機能でElasticBeanstalk Workerからスケジュールを実行することができます。

GitHub - dusterio/laravel-aws-worker: Run Laravel (or Lumen) tasks and queue listeners inside of AWS Elastic Beanstalk workers

app/Console/Kernel.phpに以下の様に書いていくことでコマンド実行の設定ができます

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{

    /**
     * Define the application's command schedule.
     *
     * @param \Illuminate\Console\Scheduling\Schedule $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {

        $schedule->command('some-command')
            ->weeklyOn(1 /* 月曜日 */, '10:00')
            ->withoutOverlapping()
            ->onOneServer()
            ->runInBackground();
     }
}

?>

このようにして、月曜の10時にsome-commandを実行するという設定ができます。

課題

ここで問題になるのは、このコマンド実行はHTTPリクエスト経由の実行ではなくconsoleからのphpの実行だということです。Webサーバー経由での実行ではないので、Apacheアクセスログ等は出ません。当たり前といえば当たり前なのですが…。標準出力にコマンドを実行したときのoutputが出るだけです。

しかし、これでは実際にコマンドがどのように実行されているのか細かく見ることができません。よって以下のような解決策を取っていきました。

解決策

実行時の標準出力相当のものをログに書き出す

コマンドのコードには、ログへの出力をするコードは書かれていませんでしたが、コマンドを人間が実行するためのテキスト等を出力するコードはちゃんと書かれていたのでこれを応用することにしました。command実行時の出力を同時にログファイルにも出力します。

Illuminate/Console/Command.phpを継承して以下のようなLoggableCommand.phpクラスを定義します。

<?php
declare(strict_types=1);

namespace App\Console\Commands\Base;

use Illuminate\Console\Command;
use Illuminate\Log\LogManager;

abstract class LoggableCommand extends Command
{
    public function info($string, $verbosity = null)
    {
        parent::info($string, $verbosity);
        $this->getLogger()->info($string);
    }

    public function warn($string, $verbosity = null)
    {
        parent::warn($string, $verbosity);
        $this->getLogger()->warning($string);
    }

    public function error($string, $verbosity = null)
    {
        parent::error($string, $verbosity);
        $this->getLogger()->error($string);
    }

    /** コマンド実行時までLoggerが生成されないので、ログに必要なときに都度生成 */
    protected function getLogger()
    {
        $application = $this->getLaravel();
        return $application->make(LogManager::class)->channel('command_log');
    }
}

?>

このクラスを継承することにより、コマンドを実行するとログファイルにコマンドの標準出力に出ているものと同じものがLogのformatにwrapされて書き出されます。 ログは以下のようにcommand_logのchannelに出力されます。

time:2020-11-24 21:37:15 level:INFO  message:START some-command params:[]

Exceptionをキャッチする

コマンド実行時に発生するExceptionは何も設定しないとWebサーバー経由で起動されているPHPのerror_logと同じ箇所に出力されてしまいますが、先程のエラーと同様にcommand_logのchannelにコマンドログは統一したいです。

これにはapp/Exception/Handler.phpをカスタマイズすることで対応します。

<?php

namespace App\Exceptions;

class Handler extends ExceptionHandler
{

    public function renderForConsole($output, Exception $e)
    {
        // コンソールコマンドはスケジューラーから実行される前提
        $logger = $this->container->make(LogManager::class)->channel('command_log');
        $logger->error("{$e->getMessage()} | {$e->getTraceAsString()}");
        parent::renderForConsole($output, $e);
    }

}

?>

renderForConsoleを実装することによってConsoleでのエラーのみこちらの関数が呼ばれるようにできます。

まとめ

Laravelのスケジューラー経由で実行されるコマンドのログを詳細に取る方法を説明しました。

  • コマンドの標準出力に出力するとともにログに出力することで簡単にログへの書き込みが実装できました。
  • app/Exception/Handler.phpをカスタマイズすることでExceptionが発生した場合にもログ出力することができました。

これでスケジューラーから実行されるコマンドの内容も詳細にログに記録することができます。

最後に

M&Aクラウドでは、エンジニアを募集中です!!興味がありましたら、是非以下からご応募ください!

www.wantedly.com

www.wantedly.com

Nuxt.js化計画vol.4

こんにちは、こんばんは、kubotak(@kubotak_public)です。

早いものでシリーズも第4回となりました。 前回の記事は以下です

tech.macloud.jp

今回はログイン周りの仕様変更があり、それに伴いNuxt.js化を行いました。

新規登録の仕様変更

新規登録に関して仕様変更しました。
今までは会社を売りたい・資金調達したいユーザーは弊社サービスに登録する際に会社情報の入力を行う必要がありました。
この問題として、例えば「直近では売却意欲は低いがとりあえず最新情報の通知だけ受け取っておこう」といったカジュアルなニーズに答えることができません。

新たに仮登録というワンステップ置いた状態が追加されました。
まず会員登録すると仮登録という状態のアカウントが発行されます。
仮登録はメールアドレスとパスワード、またはFacebook認証で登録が可能となり、登録する際の敷居が下がりました。
続いて本登録(今までの会社情報入力フロー)を行うことで弊社サービスの機能をすべて利用可能になります。

登録・ログイン周りの変更

今まではLaravelで作られていた登録周りのフロントエンドを今回の改修でNuxt.js化しました。

以前から紹介しているようにフォームの各パーツはそれぞれコンポーネント化しているのである程度の形までデザイナーを介さずに実装することが出来ました。
また、これらのインプットパーツはVeeValidateを利用したインタラクティブなバリデーションが行なわれます。

ログインフォーム

登録・ログインのPOST

弊社のログインの仕組みはLaravelの標準のAuthを利用したCookieによるログイン判定となります。
これを提供するためにはブラウザにLaravelで発行したCookieを付与する必要があります。 LaravelとNuxt.jsで分離されていると、このCookieの付与ができません。

そのため、Nuxt.jsのフォームからは通常のPOSTフォームを設置しLaravelのPOSTエンドポイントに遷移して、そこでCookieを付与するようにしています。
つまり、新規登録やログイン時はSPAではなく通常のページ遷移となります。

f:id:kubotak:20201112110347p:plain

サーバーエラーやバリデーションエラーの挙動

これまで実装してきたパターンではフォームからの情報はHTTP APIにより通信し、その結果をNuxt.jsでハンドリングしていました。
しかし、登録・ログイン周りでは先述の通り従来のページ遷移となるため、サーバー側(Laravel)のエラーをフロント側(Nuxt.js)に通知する必要があります。
従来のLaravelのアプリケーションであればセッションフラッシュを利用してページを戻した先でバリデーションエラーを表示できるのですが、アプリケーションが分断されているのでこの方法は利用できません。
そのため、弊社ではエラーをCookieとして扱って状態を引き継ぐ実装をしています。
Laravel側ではエラーになった場合はCookieにエラー情報を入れて元のページに戻します。
Nuxt側ではエラーのCookieがブラウザにある場合はエラーのトーストを表示してそのCookieを削除します。これでセッションフラッシュで行っていたような挙動を再現しています。

f:id:kubotak:20201112110401p:plain

最後に

シリーズでお届けしているNuxt.js化についてはPHP Conference 2020の私のセッションで発表させていただきますのでぜひご視聴ください。 fortee.jp

また、一緒にフロントエンドを開発してくれる仲間も募集中です。
興味がある方は以下よりご応募ください。 www.wantedly.com

A/B テストの基盤を構築した話

こんにちは。エンジニアの濱田( @hamakou108 )です。

先日M&Aクラウドのサイト上で A/B テストを本格的に導入していこうという話になり、そのシステム基盤を構築する機会がありました。 今回は A/B テストの基盤構築に至った経緯とどのように実装したのかについて紹介したいと思います。

A/B テストの基盤構築に至った経緯

M&Aクラウドの開発チームでは事業上の目標到達に向けた施策実施とその効果測定を継続的に行っています。

tech.macloud.jp

A/B テストの導入によって一度に複数の案を検証することが可能となるのに加え、外的要因に左右されにくいデータが得られるため、より効果的に検証サイクルを回すことができると考えています。

さて A/B テストを実施する方法として、 Google Optimize などのツールを利用することも選択肢として挙げられます。 しかしクライアントサイドでページを差し替えるパターンではページフリックのような UX 低下を招く可能性があること、振り分けたユーザー集団が特異性を持たないように振り分けロジックを自分たちで開発・保守したいというニーズもありました。 このような理由により、ユーザーの振り分けやページの差し替えといったロジック基盤を自前で開発することになりました。

設計と実装

A/B テスト基盤の開発において工夫した主なポイントは次のようなものです。

  • セッションの特定、および偏りのないユーザー振り分け
  • Laravel と Nuxt.js の両方で同じように機能すること
  • A/B テストの開始と終了のイベントの集計

これらについて順番に解説します。

セッションの特定、および偏りのないユーザー振り分け

あるユーザーに対して行ったページの出し分けとアクションの結果を紐付けるために、そのセッションを一意に特定する必要があります。 これを実現するために A/B テスト用の Cookie を発行して、ユーザー(クライアントのブラウザ)ごとに一意な ID を割り当てています。

テストパターンの振り分けを行う際は、まずこのユーザー ID ClinetId とテスト名 TestName を元に 0 から 1 までのランダムな実数値を出力し、その値を元に以下のようなロジックでテストパターン Varinat のどれかに振り分けています。

<?php
function chooseVariant(
    TestName $testName,
    ClientId $clientId,
    array $variantList
): Variant {
    // TestName と ClientId を元に 0 から 1 までの実数値に変換する。
    $frequencyPercentage = FrequencyPercentage::makeFromString(
        $testName->rawValue() . $clientId->rawValue()
    );

    $max = new VariantPercentage(0);

    foreach ($variantList as $variant) {
        /** @var Variant $variant */
        $min = new VariantPercentage($max->rawValue());
        $max = $max->add($variant->getPercentage());

        // どの Variant を何%の確率で出現させるかあらかじめ定義してある。
        // 例えば VariantA と VariantB にそれぞれ50%を設定した場合、
        // 数値が 0-0.5 なら VariantA に、 0.5-1 なら VarinatB に振り分けられる。
        if (
            $frequencyPercentage->isGreaterThanOrEqualTo($min)
            && $frequencyPercentage->isLessThanOrEqualTo($max)
        ) {
            return $variant;
        }
    }

    throw new LogicException('A/B テストのパターンを一意に決定できませんでした。');
}

任意の文字列を 0 から 1 までの数値に変換するロジックの実装(上記コードの FrequencyPercentage::makeFromString の中身)に関しては Qiita に記事を投稿しているので、是非のぞいてみてください。

qiita.com

Laravel と Nuxt.js の両方で同じように機能すること

M&Aクラウドは大部分が Laravel で実装されていますが、一部のフロントエンドは Nuxt.js への移行が進んでいます。 Nuxt.js 移行の詳細に関しては次の記事をご覧いただければと思います。

tech.macloud.jp

tech.macloud.jp

tech.macloud.jp

プロジェクトは今も進行中のため、現時点ではフロントエンドに Laravel と Nuxt.js が混在する状態となっています。 したがって例えば出し分け対象のページを Laravel 、ユーザーアクション後に訪れるページを Nuxt.js が生成しているといったケースも考えられ、このようなケースでも A/B テストが機能するように配慮する必要がありました。 このためどちらのシステムが発行した Cookie であるかに依らず、それぞれのシステムで同じように Cookie を取り扱えるような工夫をしています。 こちらの実装の詳細に関する記事も Qiita に投稿しているので、是非のぞいてみてください。

qiita.com

A/B テストの開始と終了のイベントの集計

A/B テストの結果の集計は Google Tag Manager (以下 GTM と表記)と Google Analytics (以下 GA と表記)を用いて行っています。

GTM の方には特定のイベントを受け取ったらそのデータを GA のイベントデータの形式にフォーマットして送信するように設定しておきます。 クライアントサイドの JS からは以下のようにして GTM にイベントを飛ばすことができます。

<script>
  window.dataLayer.push({
    event: 'ABTesting',
    testCaseDefinition: testName,
    valiant: variant,
    testState: 'start'
  })
</script>

開始(対象ページへのアクセス)または終了(アクションの実行)のタイミングで GTM にイベントを飛ばすことで、 A/B テストの試行回数とアクションの結果を GA 上で確認することができるようになります。

まとめ

M&Aクラウドにおける A/B テストの基盤作成の方法について紹介しました。 A/B テストを自前で実装することを検討している方の参考になれば幸いです。

PHPカンファレンス2020に弊社のエンジニアが4名登壇します🎉

こんにちは、M&Aクラウドの津崎(@820zacky)です。

弊社エンジニアの4名がPHPカンファレンスというイベントに登壇することとなりました🎉

今回は、登壇が決定するまでの流れと、どんな発表をするか?というところを書きます。

PHPカンファレンス2020とは?

phpcon.php.gr.jp

PHPカンファレンスは日本最大のPHPのカンファレンスです。

今年開催されるPHPカンファレンス2020は、12月12日にオンラインにて開催されます。

PHPカンファレンスでは、登壇したい人がプロポーザルを提出し、それを運営者が選定することでスピーカーを決めています。

今年は、9月16日から10月5日にかけて募集され、10月18日に採択が発表されました。

いいチームの作り方 と 全員プロポーザル宣言

https://cdn-ak.f.st-hatena.com/images/fotolife/k/kazuhei0108/20191025/20191025190323.png

今回、弊社では全エンジニアがプロポーザルの提出を行いました。 これは、我々エンジニアチームの「いいチームの作り方」に基づいて、「発信」に力を入れているためです。 我々が考える「いいチームの作り方」については、ブログ記事に説明があるのでよろしければご覧ください。

tech.macloud.jp

そういう背景があり、CTOの命に依り「全員プロポーザル宣言*1」が発令されました。

プロポーザル紹介

弊社のエンジニアが提出したプロポーザルを紹介します。

[採択]再コンパイル不要! core dump さえ吐ければ gdb デバッグできます

弊社インフラの神、鈴木さん( @yamotuki ) による発表です。

segment fault怖いですか? 私は怖いです。 でも大丈夫。鈴木さんが悪魔の倒し方を教えてくれます。 悪魔との格闘の末、銀の弾丸を手にした鈴木さん。彼の目にsegment faultはどう映るか。

fortee.jp

[採択]Laravelで運用しているサービスをNuxt.jsにリプレイスする

Nuxt.jsへのリプレイスを先導する弊社のフロントエンドモンスター久保田さん ( @kubotak_public ) による発表です。

リプレイス大臣がNuxt.jsへのリプレイスの奇蹟を爆笑を交えてお届けします。

fortee.jp

[採択]PHPer のための Vim 実践入門

弊社のタイピングマスター濱田さん( @hamakou108 ) による発表です。

弊社でもっともタイピングが早い男が愛用するVim。 みんながちょっと憧れて、みんながちょっと怖がってる。 そんなVimを愛し、Vimに愛された濱田さんによる、VimIDE的なことを実現する方法の紹介です!

fortee.jp

[採択]めざせブレークポイントマスター

私(津崎 @820zacky)の発表です。

あなたはXdebugしてますか?

Xdebugを使いこなすために便利な、ブレークポイントの知られざる機能を発見したので共有します。

デバッグでみなさんを幸せにしたい気持ちで溢れたやさしいプロポーザルです。

fortee.jp

[不採択]LaravelとAWSでサービスとともに設計を成長させる

弊社で一番テーブルフットボールが強いエンジニア、CTOのかずへいさん( @kazuhei__ ) によるプロポーザルです。

弊社サービスについて、成長に伴ってどのように設計が変わっていったのか熱く語ってくれるはずでしたが、残念ながら不採択となってしまいました。

fortee.jp

[不採択]テストを設計する

こちらもCTOによるプロポーザルです。

弊社サービスのテスト設計については、エンジニア内で多くの時間を使って議論しました。テストについて考え尽くした男の発表に乞うご期待。と言いたいところでしたが、こちらも残念ながら不採択でした。

fortee.jp

終わりに

弊社では全エンジニアが頑張ってプロポーザルを出しました。 5人中4人も登壇できるのは中々の快挙ではないかなと思います。

弊社では、PHP以外にもLaravelやVueのイベントなんかにも積極的に参加していますので、 弊社のエンジニアを見かけた際はお気軽にお声がけください。

また、弊社ではエンジニアの採用を行なっています。 ベンチャー企業でのWeb開発に興味のある方は、ぜひカジュアルにご応募ください🤝

www.wantedly.com

www.wantedly.com

*1:全エンジニアを対象とし、プロポーザル最低限一個、絶対に出しましょうというキャンペーン