GameWith Developer Blog

GameWith のエンジニア、デザイナーが技術について日々発信していきます。

Google Analytics 4 property(GA4)でできるようになったこととTips #GameWith #TechWith

はじめに

こんにちは。GameWith のエンジニアの m です!

このブログはアドベントカレンダーの22日目のブログになります!

qiita.com

今回は Google Analytics 4 property について記事を書きます!

GA4とは

2020年10月14日に正式リリースされた次世代版の Google アナリティクスです。

support.google.com

Firebase向け Google アナリティクス(FA)をもとに、イベントベース測定モデルに変更されています。

個人的に大きく変わったと思う点と、GA4利用上のTipsを紹介します。

従来の Google アナリティクスは、ユニバーサルアナリティクス(UA)と記載して区別します。

変わった点

ウェブとアプリのデータを一つのプロパティでみられるようになった

FAのデータストリームを連携することで、相互にデータを同期することができます。

後述するBigQueryでも同じテーブルとして扱うことができるので、プラットフォームを跨いだ分析が容易になります。

イベントのデータ構造が柔軟になった

UAではカテゴリ、アクションのように予め大きな項目が決められており、カスタムディメンションも事前に定義が必要でした。

GA4では事前定義を必要とせず、各イベントごとにパラメータを自由に設定することができます。

UAのデータ構造

  • イベントカテゴリ
  • イベントアクション
  • イベントラベル
  • カスタムディメンション
    • (プロパティに事前定義が必要な)ユーザー定義のkey value

GA4のデータ構造

  • イベント名
  • イベントパラメータ
    • ユーザー定義のkey value
  • ユーザープロパティ
    • ユーザー定義のkey value

大規模サービスでも無料で利用しやすくなった

UAはヒット数(ページビューやイベントの発火)に制限があり、 月のヒット数が1,000万以上になると、有償版へのアップグレードが必要でした。

GA4にはイベント数(ヒット数)にあたる制限がありません。

今現在、有償版がないので確定ではありませんが、イベント単位のリッチさ(パラメータの定義数、文字数など)が課金のポイントになりそうです。

BigQueryとの連携が簡単になった

UAでは有償版のGA360でしか提供されていなかったBigQueryへのエクスポート機能が利用できます。(BigQueryへの転送やストレージの料金は発生します)

support.google.com

GAのデータはAPI経由でアクセスすると値が丸められる影響で実数値の分析が難しいですが、 BigQueryでは各イベントがレコード単位で分かれているので、加工や分析が容易になります。

スケジュールタスクを作成し、自動でイベント名ごとに別テーブルに分割して参照しやすくすることもできます。

Tips

user idについて

GA4のデータをBigQueryにエクスポートした際のテーブルスキーマには、トップレベルに user_id カラムが存在します。

ウェブでイベントパラメータのキー名に user_id を設定しておくと、自動でトップレベルにも値が入るようになります。

また、user idがない場合でも、 user_pseudo_id というカラムに自動でCookie管理のIDが振られるので、 サービス側に識別IDがない場合は、こちらをもとに行動分析が可能です。

イベントパラメータのBigQueryテーブルスキーマについて

イベントパラメータは event_params という RECORD型(REPEATEDモード) のカラムに格納されます。

support.google.com

クエリをもとにどんなデータ構造になっているかを紹介します。

event_name と event_paramsだけのテストテーブルを用意

イベントパラメータのvalueは内部的に STRING型 INTEGER型 FLOOT型 に分かれています。

今回は簡略化して以下のようにスキーマを定義しました。

f:id:takuya_minami373:20201222161435p:plain

INSERT

RECORD型のデータを挿入する場合は、STRUCT を使って構造を明示する必要があります。

REPEATEDモード なので ARRAY の記載で複数の値の格納が可能です。

INSERT
  `PROJECT_ID.DATASET_NAME.TABLE_NAME` (event_name, event_params)
VALUES
  ("event_name1", ARRAY<STRUCT<key STRING, value STRUCT<string_value STRING, int_value INT64>>>[
    ("key1",STRUCT<string_value STRING,int_value INT64>('value1', NULL)),
    ("key2",STRUCT<string_value STRING,int_value INT64>('value2', NULL))
  ]);

SELECT

SELECT
  *
FROM
  `PROJECT_ID.DATASET_NAME.TABLE_NAME`
WHERE
  event_name = 'event_name1';

普通に抽出すると画像のように1レコードに複数のイベントパラメータが存在することがわかります。 f:id:takuya_minami373:20201222161344p:plain

イベントパラメータのネストを解除するには以下のようにクエリを発行します。

SELECT
  event_name,
  (SELECT value.string_value FROM UNNEST(event_params) where key = "key1") as key1,
  (SELECT value.string_value FROM UNNEST(event_params) where key = "key2") as key2,
FROM
  `PROJECT_ID.DATASET_NAME.TABLE_NAME`
WHERE
  event_name = 'event_name1';

これでフラットなレコードに整形されるので扱いやすくなりました。

サブクエリがある分、参照データが多くなってしまうように見えますが、 1クエリ内であればよしなにキャッシュしてくれるので処理されるデータ容量は変わりません。

f:id:takuya_minami373:20201222161453p:plain

終わりに

GA4がデフォルトのプロパティとなり、今後は集中して機能拡張されていくみたいです。

UAの終了日はまだアナウンスされていないですが、早めに並行稼働を進めていくことが推奨されています。

スムーズに移行できるよう一緒に準備していきましょう!

Twitter

GameWithのDeveloper向けTwitterアカウントも開設しました。

ブログの更新情報などを発信するので良かったらフォロー宜しくお願いします!

twitter.com

2020 年に起きたワークスタイルの変化とこれからについて #GameWith #TechWith

この記事は GameWith Advent Calendar 21 日目の記事です。

最近、ブログ更新を怠っていた @serima です。Splatoon 2 でナワバリバトルをする日々を送っていますが、お仕事では開発マネージャーをしています。

2020 年は新型コロナウイルスの影響で世界的に働き方の変革を求められた年であり、弊社 GameWith も例外ではありませんでした。

このポストではフルリモートワークへの切り替えにおいて気をつけたことや、現在の課題と試みについて簡単に紹介できればと思います。

なお、現時点(2020 年 12 月)においての会社としての対応は全社で原則フルリモートワーク(在宅勤務)という方針を採用しています。

gamewith.co.jp

コロナ以前のリモートワーク運用について

もともと私が所属しているサービス開発部では、週に 2 回までのリモートワークについては事前申請を行えば利用可能という運用を行っており、半数以上のエンジニアやデザイナーがその制度を日常的に利用していました。

リモートワークの目的を下記のように定義しドキュメントにて周知したうえで、リモートワーク利用希望者にはトライアルとしてまずは 1 ヶ月間実施してもらい問題がなければ本運用に入ってもらうという進め方をしていました。

# 目的

- 柔軟な働き方を選択できるようにする
    - 育児・介護等の制約が生まれたとしても、調整して業務ができる
    - 優秀なエンジニアの業務参加機会の増加
        - 副業、フリーランス人材の活用
- 生産性の向上
    - 集中できる環境、通勤時間の短縮

フルリモートワークへの切り替えにあたって

上記の制度を運用していたおかげで、すでに自宅に開発環境が整備されていること、リモートワークにメンタル的に慣れている方が多くいました。 そのため、先陣を切る形でサービス開発部所属のメンバーについてはリモートワークを推奨とするというアナウンスを行うことができました。

ちなみに 2020 年 2 月ごろはまだ新型コロナウイルスの情報が錯綜していたこともあり、報道で出た数字も鵜呑みにせず 1 次ソースに当たったり、信頼のおける知人から情報を頂いたりと気を払いながら意思決定の材料にしていました。

f:id:serimaryo:20201222025616p:plain

ちなみに、これ以降は全社的にフルリモートワークへ強めに舵を切っていったこともあり、この日以来物理的に対面していないメンバーも数多くいるという状況です。

リモートワークの制度があったとはいえメンバー全員が同時に利用したことはなかったので、下記については特に注意してもらうようにしていました。

  1. オンラインのコミュニケーション特性を理解すること
  2. 健康(メンタル・フィジカルどちらも)に気を使うこと

Slack でのアナウンスだけでなく、ドキュメントを書いて展開するなどいま振り返ると割と口を酸っぱくして伝えていたように思います。

f:id:serimaryo:20201222025721p:plain
長期リモートワークに向けた心がけを伝えるドキュメントチラ見せ

オンラインのコミュニケーション特性を理解する

フルリモートワーク前提で入社してきたわけではないので当たり前なのですが、必ずしもテキストコミュニケーションが得意な人ばかりではありません。

気の使いすぎや行間の読みすぎでテキストコミュニケーションに時間がかかり過ぎてしまったり、言いたいことがうまく伝わらず、あらぬ誤解を招く可能性があると感じていました。

そのため、初期のタイミングから「躊躇なく」オンライン MTG を行い口頭でのコミュニケーションもうまく併用して欲しいと考えていました。

「躊躇なく」を手助けするために、会話したいと思ったら即座に場を作れるようにしておくことが重要だと思ったため Slack App の Google+ HangoutsZoom をインストールし、早いタイミングで周知を行っておきました。

f:id:serimaryo:20201222025821p:plain

メンバー全員がフレキシブルに対応してくれたおかげで、現在は躊躇なくオンライン MTG を行うのが当たり前になっています。

健康(メンタル・フィジカルどちらも)に気を使うことの大切さ

以前からチームでの朝会は必ず行うようにしていましたが、フルリモートワークに切り替えてからも継続するようにしています。

フルリモートワークはお互いの顔が見えないが故に、ちょっとしたことで精神的に孤立してしまいがちです。

そして、ちょっとした精神的な孤立をきっかけにメンタル不全に陥ることは誰しもに襲いかかる非常によくあることだと考えています。 朝会をおこなうことで 1 日に 1 度は必ずチームメンバーとは会話をすることになるので、最低限ではありますがそこを担保するようにしています。また、朝会での雑談も推奨しています。

マネージャーとしてメンバーのフィジカル面にアプローチできることは少なく、注意喚起をおこなうことくらいしかできないのですが、ドキュメントでは筋トレやウォーキング・ジョギングを勧めました。

リモートワークを実際に体験した方は実感しているかもしれませんが、よほど意識しない限りはほぼ筋肉を使わずに生活できてしまいます。

私も例外ではなく、油断するとずっと座りっぱなしだったため春頃にスマートウォッチを購入しました。座りっぱなし警告や歩数計機能を利用して最低限は体を動かすようにしています。

ちなみにリングフィットアドベンチャーを持っているチームメンバーはかなり活用しているようでした。

個へのフォーカス

お互いの顔が見えなくなったことで、同僚がどんな仕事をしているのか分かりづらくなった面があると感じ、部署全体会のコンテンツのひとつとして「業務アピール会」というものを実施するようになりました。

"チームが何をしたか"というのはチームごとの定例など情報共有のパスは存在するのですが、"あなたが何をしたか"を他のチームメンバーが知る術がありません。(もちろん積極的に聞きにいくのであれば話は別です!)

弊社の場合は複数チームでひとつのプロダクトを開発していることもあり、個々人の仕事は間違いなく相互に影響しあっているにも関わらず、です。

個へフォーカスする機会が減ってしまったことで、その人の人となりやスキルなどに関心を持つ機会が互いに失われてしまったのなら、それは非常にもったいないことだと思います。同僚として働いているのであれば、フルリモートワークであっても良い関係が築けるといいなと考えています。

そのため、

  • 個のプレゼンテーションスキルの向上
  • プレゼンターやプレゼンターの普段の仕事に関心を持てるようになるためのキッカケづくり

このあたりをねらいとしてさだめ、運営しています。

以下が試験的に導入した Spatial.chat にて実施したときの様子です。

Spatial.chat とは近くの人の声が大きく聞こえるという距離の概念がはいった一風変わったインタフェースのビデオチャットのアプリケーションです。

(こちらのツールについては本ポストではスコープ外とさせてください)

f:id:serimaryo:20201222025921p:plain

楽しめるコンテンツになるように有志にて毎月 KPT を重ねて少しずつ改善しています。

f:id:serimaryo:20201222025953p:plain
業務アピール会の直近のKPTの様子

これからについて

引き続き原則フルリモートワーク(在宅勤務)体制にて運用していく予定です。 今後はさらに、組織としても制度としても新しいワークスタイルに最適化させていきたいと考えています。

フルリモートワークに切り替えたことで、地理的な制限がなくなったことには可能性を感じていまして、採用の幅が大きく拡がったと考えています。

いまは特にサーバサイドエンジニアと開発ディレクターを募集しています。

技術力が大事という価値観も保ちながら、ユーザーに近く強い価値を提供していけるニュータイプのエンジニアを組織、個人問わず作れないかと挑戦しています。

このままで良いのかと悩んでるエンジニアとぜひ一緒にこの殻を破り、更に成長を一緒にしていきたい。その挑戦が出来る環境です。新しい事は失敗を恐れず一緒に挑戦していきましょう。

興味がある方は @serima までお気軽に DM 頂いても構いませんし、Wantedly から「話を聞きに行きたい」ボタンをポチっとしていただいても構いません。

www.wantedly.com

Twitter

GameWith の Developer 向け Twitter アカウントも開設しました。 ブログの更新情報などを発信するので良かったらフォロー宜しくお願いします!

twitter.com

ダッシュボード (Vue.js, Composition API) とテストコード #GameWith #TechWith

はじめに

こんにちは。GameWith のエンジニアの tiwu です!

このブログはアドベントカレンダーの20日目のブログになります!

qiita.com

今回は Vue.js, Typescript で開発しているダッシュボードのテストコードについて書いていきたいと思います!

導入についてはこちらの記事を御覧ください。

tech.gamewith.co.jp

サンプルページ

サンプルページのコードと共に解説していこうと思います。

例としてユーザーの一覧と検索機能を持ったダッシュボードの場合、下記のような構成になります。

- src
  - components
    - pages
      - user
        - list.vue
  - composition
    - user
      - use-list.ts

composition-api.vuejs.org

composition フォルダには Composition API RFC を参考にロジック部分を分けています。

src/components/pages/user/list.vue

検索機能とユーザー一覧を表示するページのサンプルコードです。

コンポーネントライブラリとして BootstrapVue を利用しています 👍

bootstrap-vue.org

表示するデータや取得処理は Composition API 経由で、検索は URL パラメータを書き換え更新することで検索を実現させています。

The サンプルコードと言った感じのシンプルな作りになっています。

<template>
  <div>
    <label for="input-userId">ユーザID</label>
    <b-form-input
      id="input-userId"
      type="number"
      :state="userIdModel.state"
      v-model.number="userIdModel.input"
    >
    </b-form-input>
    <b-button @click="submitSearch" variant="outline-primary">検索</b-button>
    <b-button @click="resetSearch" variant="outline-secondary">リセット</b-button>

    <b-pagination-nav :link-gen="linkGen" :number-of-pages="numberOfPage"></b-pagination-nav>
    <b-table :items="users">
      // 省略
    </b-table>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive, toRefs, getCurrentInstance } from '@vue/composition-api';
import { useList } from '@/composition/user/use-list';

export default defineComponent({
  setup() {
    const root = getCurrentInstance();
    const reactiveData = reactive({
      page: Number(root.$route.query.page || '1'),
      userIdModel: {
        key: 'userId',
        input: root.$route.query.userId ? Number(root.$route.query.userId) : null,
        state: null,
      },
    });

    const {
      fetchUserList,
      users,
      numberOfPage
    } = useList();

    const linkGen = (pageNum: number): string => {
      return `?page=${pageNum}` + '&' + buildQuery();
    };

    const submitSearch = (): void => {
      const query = buildQuery();
      window.location.href = '/list?' + query;
    };

    const resetSearch = (): void => {
      window.location.href = '/list?';
    };

    (async(): Promise<void> => {
      await fetchUserList(
        reactiveData.page,
        Number(userIdModel.input)
      );
    })();

    return {
      ...toRefs(reactiveData as any),
      users,
      numberOfPage,
      linkGen,
      submitSearch,
      resetSearch,
    };
  }
});
</script>

src/composition/users/use-list.ts

Composition API 側も至ってシンプルで、API 経由でデータを取得する関数と、ユーザー情報を持つ state を管理しています。

API のリクエストは aspida を利用しています。詳しくはこちら 👍

tech.gamewith.co.jp

import { reactive, toRefs } from '@vue/composition-api';
import { RepositoryFactory } from '@/repositories/repository-factory';

export interface User {
  id: number;
  name: string;
}

export interface Users {
  users: User[];
  numberOfPage: number;
}

export const useList = () => {
  const state = reactive<Users>({
    users: [],
    numberOfPage: 1
  });
  const DashboardRepository = RepositoryFactory.getDashboardRepository();

  const fetchUserList = async(
    page: number,
    userId?: number,
  ): Promise<void> => {
    const response = await DashboardRepository.api.user.$get({
      query: {
        page: page,
        user_id: userId,
      }
    });
    state.users = [];
    for (const user of response.users) {
      state.users.push({
        id: user.id,
        name: user.name,
      });
    }
    state.numberOfPage = response.number_of_page;
  };

  return {
    ...toRefs(state),
    fetchUserList
  };
};

テストについて

テストは Jest を利用しています。

Components のテスト

コンポーネントライブラリとして BootstrapVue を採用しているので、まずその設定を行います。

shallowMount() では BootstrapVue のコンポーネントをレンダリングしないので mount() を利用しています

ユーザーの情報をモックを利用して定義し、スナップショットテストを実行します。

import { createLocalVue, mount } from '@vue/test-utils';
import VueCompositionApi from '@vue/composition-api';
import { default as BootstrapVue, IconsPlugin } from 'bootstrap-vue';
import List from '@/components/pages/user/list.vue';
import * as useList from '@/composition/user/use-list';

const localVue = createLocalVue();
localVue.use(VueCompositionApi);
localVue.use(BootstrapVue);
localVue.use(IconsPlugin);

const mockUseList = jest.spyOn(useList, 'useList');

describe('list.vue', () => {
  it('ユーザー一覧', () => {
    const mockValue: useList.Users = {
      numberOfPage: 1,
      users: [
        {
          id: 1,
          name: 'user 1'
        },
        {
          id: 2,
          name: 'user 2'
        }
      ],
    };
    mockUseList.mockReturnValue({
      fetchUserList: jest.fn(),
      numberOfPage: mockValue.numberOfPage as any,
      users: mockValue.users as any,
    });

    const $route = {
      query: {
        page: 1,
      },
    };

    const wrapper = mount(List, {
      localVue,
      mocks: {
        $route,
      },
      stubs: {
        transition: false
      },
    });
    expect(wrapper.html()).toMatchSnapshot();
  });
});

Composition のテスト

aspida のブログでも触れてましたが、aspida のモックを作りテストを書いています。

import { createLocalVue } from '@vue/test-utils';
import VueCompositionApi from '@vue/composition-api';
import { useList, User } from '@/composition/user/use-list';
import { DashboardRepository } from '@/repositories/dashboard/repository';

const localVue = createLocalVue();
localVue.use(VueCompositionApi);

type UserListType = ReturnType<typeof DashboardRepository.api.user.$get> extends Promise<infer T> ? T : never;
const mockUserList = jest.spyOn(DashboardRepository.api.user, '$get');

describe('use-list.ts', () => {
  describe('fetchUserList', () => {
    it('ユーザー一覧の取得', async() => {
      const mockValue: UserListType = {
        number_of_page: 1,
        users: [
          {
            id: 1,
            name: 'user 1'
          },
          {
            id: 2,
            name: 'user 2'
          }
        ]
      };
      mockUserList.mockReturnValue(mockValue as any);

      const {
        fetchUserList,
        users,
        numberOfPage
      } = await useList();

      expect(users).toEqual([]);

      await fetchUserList(1);

      expect(numberOfPage.value).toEqual(mockValue.number_of_page);
      expect(users[0]).toEqual(mockValue.users[0]);
    });
  });
});

小ネタ

検索の箇所に出てきた window.location.href のテストも書いていきます

const submitSearch = (): void => {
  const query = buildQuery();
  window.location.href = '/list?' + query;
};

const resetSearch = (): void => {
  window.location.href = '/list?';
};

こちらを参考にテストを書いていきます 👍

stackoverflow.com

const buildQuery = (): string => {
  return 'username=hoge&userId=1';
};

global.window = Object.create(window);
Object.defineProperty(window, 'location', {
  value: {
    href: ''
  }
});

expect(window.location.href).toEqual('');
submitSearch();
expect(window.location.href).toEqual('/list/?username=hoge&userId=1');
resetSearch();
expect(window.location.href).toEqual('/list');

終わりに

テストできなさそうな window.location.href も意外とテストできたりするのは驚きました!

これからもどしどしテスト書いていきましょう!

Twitter

GameWithのDeveloper向けTwitterアカウントも開設しました。
ブログの更新情報などを発信するので良かったらフォロー宜しくお願いします!

twitter.com

ツール開発で firestore を初めて使って感じた利点と、ハマった落とし穴 #GameWith #TechWith

こんばんは✋

@peka3 です。 ヘムロックがお気に入りです。

ブログは久しぶりに書きます。

最近は攻略ツールを開発をしております。

先日、あつ森でのアイテムを交換するためのツール「あつ森交換掲示板」を作りました。

フロントエンドは GameWithDesignSystem を使っています。 こちらについては以前の記事に詳しく載っているのでぜひ読んでみてください。

tech.gamewith.co.jp

実装側としては Vue + TypeScript で Webコンポーネントを実装していくことになります。

そしてバックエンドなのですが、今回はすべて firestore で実装しました。

firestore については、RDB脳だとハマりどころが多かったので、今回はこちらを詳しく紹介できればと思います!!

まず最初に、あつ森交換掲示板の機能をざっくり紹介いたします。

よくあるスレッド型掲示板とそれほど違いはないので、読み飛ばしていただいても大丈夫です。

あつ森交換掲示板の紹介

あつ森交換掲示板 | あつまれどうぶつの森 - GameWith

一覧画面

f:id:peka3:20201204190004p:plain:h100
あつ森交換掲示板 一覧画面

現在の取引募集を一覧で見ることができます。 アイテムを選択して、検索をすることができます。

投稿画面

f:id:peka3:20201204191730p:plain:h100
投稿画面

欲しいアイテム、譲れるアイテムを選択して投稿します

詳細画面

f:id:peka3:20201204192204p:plain:h100
詳細画面

投稿主に対して、コメントすることができます。 この画面で交換のやり取りが行われます。

アイテム選択モーダル

f:id:peka3:20201204200802p:plain:h100
アイテム選択モーダル

アイテム一覧から様々な条件で絞り込み、アイテムを選択する画面です。

firestoreでのデータの持ち方

今回作ったコレクションは3つになります。

  • アイテム情報
    • アイテム名、サムネイル等
  • 取引情報
    • 欲しいアイテム、譲るアイテム、投稿ユーザーの情報等
    • サブコレクションで 返信情報 も保持しています
  • 削除ログ
    • 規約に反した投稿を削除した際のログ

firestore での DB 設計の勘所

firestoreで大事なところとして 「なるべく1つのコレクションに画面を出力するのに必要なすべてのデータを格納する」 というのがあると思いました。

firestore には join がない

まずfirestoreはjoinがありません。

RDBですと、取引情報には、欲しいアイテムのアイテムID、譲るアイテムのアイテムIDだけ格納し、 実際に画面でアイテム名が必要になったらjoinによってアイテム名を取得してくる、という流れになると思いますが、

firestoreでそれをやろうとすると、joinがないためアイテムID一つに対して1回読み取りクエリを投げることになります。 パフォーマンス的にも料金的にも、とても非効率なります。

そのため、今回は取引情報のなかにアイテム名やサムネイルURL等、画面の描画に必要なデータをすべて盛り込みました。

firestoreの便利だったところ

https://firebase.google.com/docs/firestore/query-data/queries?hl=ja#array_membership

firestore には array-contains 演算子というものがあります。

これが今回の「欲しいアイテム」「譲るアイテム」から特定アイテムを検索する、という要件にぴったりと合致しました。

「欲しいアイテム」と「譲るアイテム」は複数設定できます。 それぞれのアイテム名を配列で持つフィールドを用意し、そこに対して array-contains で where することで、一発で検索できるようになりました。

コード例:

wishItemTradesRef = itemTradesRef.where(
    "items",
    "array-contains",
    selectedItemNames
);

firestoreでの論理削除は非効率

firestoreでも論理削除をすることもできますが、削除フラグを持ってしまうと、削除フラグを含めた複合indexをたくさん作る必要がでてきます。 firestoreはindexも課金対象であるため、できれば避けたいです。

今回の要件として、削除された対象のログをあとから追跡できれば良かったので、削除したログを残すコレクションを別途用意し、取引情報のほうは物理削除することにしました。

firestore で、複数条件での検索、ソートが必要になったら要注意

whereによる範囲比較、orderByによるソートは、同一のフィールドを指定しないと動きません。

たとえば年齢10〜20歳の人で、かつ男性を上位に表示する、というようなクエリは発行できません。 年齢10〜20歳の人で、年齢の昇順に表示する、なら可能になります。

// これはエラーになる
usersRef.where("age", ">", 10).orderBy("gender");

// これはOK
usersRef.where("age", ">", 10).orderBy("age");

これは firestore 特有の制限ですね。

今回はこれを知らずに仕様を決めていたため、あとで調整が必要になりました。

要注意ポイントです。

(前述の array-contains は範囲比較に該当しないため問題なし)

以下、公式ドキュメントへのリンクです

firestore ではセキュリティルールを書く必要がある

firestoreのconsoleからセキュリティルールを書くことができます。

これはバリデーションのようなものであり、これを書かないと、どんな内容でもクエリを受け付けてしまいます。

セキュリティルールを書くことによって、フィールドに対して型を縛ったり、READ/WRITE制限をかけたり、このフィールドのみUPDATE可能にする、というようなこともできます。

セキュリティルールは詳しく書くとこれだけで結構なボリュームになるので割愛します。

まとめ

ちょっと要件が複雑な掲示板でしたが、 firestore のみでバックエンド問題なく実装ができました。

firestore 単体で実装がすむと、API開発をしなくてよい、APIサーバの運用を考えなくてもよいという圧倒的メリットがありますね!

ただ、利用には一癖二癖あるので、何度かfirestoreの実装を経験しないと、工数見積もりなどを正確に出すのは難しそうだと感じました。

しかしAPI開発を一切せずにこういったツールが作れるとなるとは…どんどん便利になってエンジニアとしては嬉しいかぎりですね。

firestore には未来を感じるので、これからも経験を積んでいきたいなぁと思いました。

それでは失礼します👋

GameWith Advent Calendar 2020 の他の記事もよろしくおねがいします!

qiita.com

チームの案件管理方法・モブ設計・モブ見積もりの紹介 #GameWith #TechWith

はじめに

こんにちは。GameWith のエンジニアの tiwu です。

このブログはアドベントカレンダーの4日目のブログになります!

qiita.com

今回はチームで行っている、案件の管理方法や、モブ設計・モブ見積もりについて紹介したいと思います!

案件の管理方法

自分が所属しているチームでは案件を Spreadsheet と GitHub Issue で管理しています。

それぞれ下記のような使い方をしています。

  • Spreadsheet
    • 主にエンジニア以外の人が利用する
    • 案件の概要や効果、優先度、工数などを記述する
      • 工数はエンジニアが入力
      • GitHub Issue へのリンクも記述しています
    • 表で並んでおり他の案件と比較がしやすく、優先度の決定や並び替えが容易にできます
  • GitHub Issue
    • 主にエンジニアが案件の技術的な内容を記述するために利用しています

開発当初は GitHub Issue だけで案件を管理していましたが、他の案件との比較や概要などが一覧で見辛かったので Spreadsheet を導入し、重複していますが2つのツールで管理しています。

モブ設計

案件の技術的な設計はエンジニアで集まり、モブ設計を行っています。

現在は Google Meet でリモートモブ設計をしています。

  • 「設計 vs 個人」ではなく「設計 vs チーム」という体制
  • 三人寄れば文殊の知恵
  • 互いの持っている知識の共有

あたりが意図としてあります。

モブ見積もり

モブ設計の後はモブ見積もりをしています。

こちらもモブ設計と同じような意図で実施しています。

モブ設計は Hatjitsu というツールを利用しています。

hatjitsu.toolforge.org

f:id:tiwu:20201204151550p:plain

このツールはシンプルでかなり使いやすく重宝しています!開発者の方々、ありがとうございます!

見積もりは時間ではなく、相対的なポイントで見積もっているため、 Hatjitsu とも相性が良いです。

終わりに

モブ設計やモブ見積もりはチームで協力し、チームが成長できる良い施策と考えています!

ですが、現状に満足せず課題があれば Spreadsheet を導入したようにチームで変化し、より良く変わっていけるようなチームを目指して行こうと思います!

ツイッター

ブログの更新情報などを呟いています!

twitter.com

2020年 GameWith の技術広報の1年間の振り返りとこれから #GameWith #TechWith

こんにちは。GameWith のエンジニアの tiwu です。

今年も始まりましたアドベントカレンダー!

1発目のブログは今年1年間の技術広報活動を振り返っていこうと思います!

qiita.com

Blog

今年(2020年12月1日時点)の投稿数は 17 本でした!

去年が 55 本だったので、約3分の1の投稿数になります。

PV は2万2千ほどになります!去年は3万3千ほどだったので、投稿数は3分の1ですが、PVは3分の2くらいになっています。

f:id:tiwu:20201126180437p:plain
2020年のPVの推移

f:id:tiwu:20191205144635p:plain
2019年のPVの推移

ちなみに今年1番PVの多かった記事はこちらの記事でした!

tech.gamewith.co.jp

リモートペアブログ・モブブログという新たな試み

弊社はフルリモートワークを実施しているので、今まで行っていた顔を突き合わせてのペアブログ・モブブログは実施できなくなりました。

ペアブログ・モブブログ自体は去年から月に1回開催をしており、フルリモートワークになっても月1の定期的な開催を行っています。

フルリモートワーク環境では Google Meet でビデオ会議を行い、VSCode Live Share を利用して執筆作業を行っています!

詳しくはこちらの記事を御覧ください。

tech.gamewith.co.jp

もくもく会

フルリモートワークになるまで、もくもくを月に1回開催していました!

参加していただいた方ありがとうございました!

カンファレンスへのスポンサー

今年は2月に開催された PHPerKaigi 2020 に協賛をさせていただきました!

ツイッター

去年からTwitterアカウントの運用を始めました!

twitter.com

ブログの告知が主なつぶやき内容になっています!

振り返り

フルリモートワークになってからは、主にペアブログ・モブブログが主な活動になりました。

数は減りましたが継続的にアウトプットができ良かったと思います!

フルリモートワークになっても定期的なアウトプットや広報活動を続けることができたのは、技術広報の活動に携わっていただいた方々やブログを見ているユーザー、サービスを利用しているユーザーのおかげです!ありがとうございました!

来年も継続的に、アウトプットしていくのでよろしくおねがいします!

GameWith の 手動テスト方法について #GameWith #TechWith

はじめに

こんにちは!GameWith QAエンジニアのIです!

今回のブログは GameWitfh で実施している QA について書いていきたいと思います。

QAとは

Quality Assurance の略で、プロダクトの品質を保証するための業務全般を行なっています。

企業やプロダクトによってチームがあったりなかったりその体制はまちまちですが、GameWith では開発部にQAエンジニアが所属しています。

自動テストと手動テストを併用していますが、今回は手動で行っているテストについて紹介していきたいと思います。

行なっている手動テスト

大きな開発や難易度が高いと思われるテストはQAエンジニアが実施します。

JSTQB(Japan Software Testing Qualifications Board)のテスト7原則でも言われている通り、 テストはやろうと思えば無限に行うことができ、また不具合が0であると証明することはできません。
参照:テスト7原則(Foundation Level シラバス 日本語版 Version 2018V3.1.J02より)
http://jstqb.jp/dl/JSTQB-SyllabusFoundation_Version2018V31.J02.pdf

当然リリースまでの期日も遵守しなければなりません。

その中でどのようなテストを行うのが効果的でよりリスクを下げられるのかテストの設計を行い、 それを元に実際のテストケースに落としていきます。

テストケースはExcelなどのスプレッドシートを用いて管理している場合も多いかと思いますが、 GameWithではGitHubのIssueにMarkdownのtasklist方式で記載を行い、チェックをつけています。

f:id:t_iwsk01:20201124163441p:plain
テストケースの例

この方法はプロダクトの仕様に理解があることが前提にはなりますが、 冗長な条件や手順の記載を避けることで、テスト観点(意図)が端的に伝わるテストケースに仕上げることができます。

結果的に開発担当エンジニアのレビューへの負担を軽減する効果も見込めます。
(別ドキュメントにしたり文章量が増えるとどうしても確認の工数が増え、結果的に目も行き届かなくなります)

テストが可能になり次第、あらかじめ策定した環境にてテストを実施するのですが、 ここでも経験をベースにテストケースの行間を読んだり、仕様の変更などに対して柔軟に対応していきます。

事前の準備は行いますが、全体的にはいわゆる経験ベースの探索的テストに近い部分もあります。

以下はJSTQBにおける探索的テストの定義です。
(JSTQB-Syllabus.Advanced_TA_Version2012.J01より)
http://jstqb.jp/dl/JSTQB-SyllabusFoundation_Version2018V31.J02.pdf

「探索的テストには、テスト担当者がプロダクトとその欠陥の学習、完了すべきテスト作業の計画、テストの設計と実行、および結果の報告を同時に行うという特徴がある。
 テスト担当者は、テスト実行時にテスト目標を動的に調整し、軽量のドキュメントのみを準備する」

こうしたフローを採用することで、1サイクルの短い開発にも素早く効率的にテストが行えるようにしています。

すべてのテストをQAエンジニアが行うわけではない

各プロジェクトの内容によっては、開発を担当したエンジニアに直接テストケースの作成や実施を行ってもらう場合もあります。

GitHubのPull Requestに、エンジニアが開発機能に対するテストケースを記載

QAエンジニアにメンションをつけてレビューを依頼(通知が飛ぶ)

QAエンジニアでレビューして足りない項目がないかを確認し、必要であればテストケースの加筆修正を行なってもらう
という流れです。

こうすることでエンジニアに「開発とテストはセットである」という意識を身につけてもらい、 品質への意識を高く保ってもらうという狙いがあります。

「テストや品質の責任はQAエンジニアのもの」という意識が根付いてしまうと不具合の作り込みも増え、 実際にプロダクトの品質が低下することに繋がってしまう恐れがあります。

使用するユーザーのことを想像したり、より良いプロダクトを提供しようという意識は大切なものです。

QAエンジニアはこういった品質に対する在り方の提案も行なっています。

おわりに

冒頭でQAは企業やプロダクトによってあったりなかったり、体制が異なると書きましたが、 所属する人や業務のフロー、使用しているツールなどにもよってもいろいろ条件が変わってくるため、「これが正解」と言った形は存在しません。

QAエンジニアは品質を担保することももちろん大切な業務ですが、 既存のやり方にとらわれず、プロダクト開発におけるQAの在り方をリードしていくことも大きな役割だと考えています。

今後もそのために尽力していきたいと思います!

GameWithのDeveloper向けTwitterアカウントも開設しています。
良かったらフォロー宜しくお願いします!

twitter.com