type holyshared = Engineer<mixed>

技術的なことなど色々

自分で運用しているアプリケーションのログイン後のダッシュボードを改善した

自分で履いた靴の記録するアプリケーションを運用していて、ずっとダッシュボードに写真の投稿ボタンしか置いていませんでした。

理由は特に表示させたい情報がなかったためです。
ログインせずとも公開している情報があるので今までは特に困ってなかったのです。

f:id:holyshared:20210208184008p:plain:w500
ダッシュボード

しかし、特に偏りがでない様に均等に靴を履くというライフスタイルをとっていて、どれがあまり履けていない 靴なのかを把握するのが大変という問題がありました。

回数は公開しているので見ていけばわかるんですが、靴が30足以上あるのでページを跨がないとわからなかったりして結構不便だったのです。

なのでダッシュボードに表示する様にして、レコメンドのような形が取れないか試してみました。
表示する内容は一月単位で履いている回数が低いものを4件だけ表示する様にしました。

優先順位は今月全く履いていない靴、現状履いている回数が少ない靴の順番で表示しています。

1月から1ヶ月位運用していて全く履かない靴はなくなり、最低一月に一回は履くようになりました。
また、ただ表示しているだけなので強制力もなく6割位表示されている4件以外の靴を履いても達成できました。

「今日はこれを履きたい」と「明日は表示されている4件のうちどれか履いてもいいかな」という気持ちのバランスが丁度いいのかもしれません。

Victoryのアップグレード

f:id:holyshared:20210123195641p:plain
Victory

個人で運用しているアプリケーションでグラフライブラリのVictoryを使っていてバージョンが古くなっていたので32.3.x から35.4.xにあげてみました。
大きく仕様に変化はなかったようで、型エラーを直していくだけで対応が完了しました。

VictoryLine

線のスタイルやラベルのコールバックの仕様が変わっている様です。

  • strokeDasharray が配列ではなくなっていて、型としては StringOrNumberOrCallback に変更されている。
  • labels のコールバックの値がデータではなく、lineのオブジェクトになってる。
import { VictoryLine } from "victory";

const dottedLine = {
  data: {
    strokeDasharray: 1, // StringOrNumberOrCallbackになったので[1, 1] みたいなしてはできない
  }
};
const barData = [{ x: 1, y: 1 }];

// 前の様にしたければdatumに置き換える
const lineLabel = (value: { datum: { x: number; y: number; } }): number | null => {
  return (value.datum.y > 0 ? value.datum.y : null);
};

<VictoryLine style={dottedLine} data={barData} labels={lineLabel} />

VictoryChart

domainPaddingの型指定が変わった様です。

export type PaddingType = number | [number, number];
export type DomainPaddingPropType =
  | PaddingType
  | {
      x?: PaddingType;
      y?: PaddingType;
    };

値をリテラルで指定していたので、[0, 15] みたいな指定が number[] で解釈されていました。

import { VictoryChart, VictoryBar } from "victory";

const domainPadding = {
  x: [15, 15] as [number, number], // 型指定はDomainPaddingPropTypeなので
  y: [0, 15] as [number, number],
};
const barData = [{ x: 1, y; 1 }];

<VictoryChart domainPadding={domainPadding}>
  <VictoryBar data={barData} />
</VictoryChart>

VictoryAxis

grid.strokeのコールバック関数の引数が変わった様です。
strokeのコールバックはtickの値が引数になっていたので、args: CallbackArgsに置き換えました。

import { CallbackArgs } from "victory-core";
import { VictoryAxis } from "victory";

const valueTicks = [0, 5, 10, 15, 20];

const valueAxisStyle = {
  grid: {
    stroke: (args: CallbackArgs) => (args.tick === 0 ? colorTransparent : axisAndTickColor),  // 前はtick: numberだった
  },
};

<VictoryAxis dependentAxis tickValues={valueTicks} style={valueAxisStyle} />

GitHub ActionsでMongoDBのレプリカセット設定を行う

GitHub ActionsでMongoDBのレプリカセットを使用してテストを実行する方法を調べました。
docker-composeを使ってもよかったのですが、mongodb-github-actionがあったので試しに使用してみました。

パラメータとしては下記の2つのものが指定できるようです。

  • MongoDBのバージョン
  • レプリカセットの名前

バージョンとレプリカセットの名前しか指定できないので、接続URLにユーザー名などが指定されている場合接続できないので注意が必要です。

試したURLを載せておきます。

MONGO_URL=mongodb://localhost:27017/test?replicaSet=replset

docker-composeなどでローカルでコンテナを起動している場合はMONGO_INITDB_ROOT_USERNAMEMONGO_INITDB_ROOT_PASSWORDMONGO_INITDB_DATABASEなどを設定ファイルで指定することが多いので、これはこれでサクッと試せるのでこれはいいかなと思いました。

ただし、レプリカセットのメンバーは1つだけなので、複数ある場合のテストがある場合は今のところ使えなさそうではあります。

最後に.github/workflows/node.js.ymlの設定を載せておきます。

name: Node.js CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  build:

    runs-on: ubuntu-20.04

    strategy:
      matrix:
        node-version: [14.8]
        mongodb-version: [4.2]

    steps:
    - name: Git checkout
      uses: actions/checkout@v2

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}

    - name: Start MongoDB
      uses: supercharge/mongodb-github-action@1.3.0
      with:
        mongodb-version: ${{ matrix.mongodb-version }}
        mongodb-replica-set: replset

    - run: yarn --frozen-lockfile
    - run: yarn build
    - run: yarn test

2020年の靴の着用回数を集計した

2019年から引き続き履いているもの、買い足したもの、手放したものを集計対象にしています。

2020年は900件を投稿していたようです。
一つの靴について、だいたい年間で20回ぐらいの着用、サンダルのみ使用頻度が高いので、倍ぐらいの着用回数になっています。

靴は1年間で16足増えて、9足手放し、合計で36足になりました。

2020年に関しては、スニーカーが増えています。
BROTHER BRIDGEのBERLINを買ってから、スニーカーもいいなと思ったので、日本のメーカーのものをいくつか買ってみました。

また、2020年はコロナ禍の影響もあり自宅で仕事することが増え他ことにより運動することが減ったので散歩をすることが増えました。
大体毎日6〜7km位歩いているようです。

そのおかげが全体的な靴の着用回数が増えたようです。
2021年は30足位に抑えようと思います。

No. ブランド モデル 2020年までの着用回数 2020年の着用回数
1 Tokyo Sandals DOBULE MONK SANDAL 182 92
2 Tokyo Sandals Enginier Slip on 124 88
3 BROTHER BRIDGE JAMES 57 28
4 Rolling dub trio ROOTS 60 27
5 BROTHER BRIDGE BERLIN 27 27
6 RAMSEY 442 MILITARY CAP TOE OXFORD 57 26
7 Redwing Irish Setter 56 26
8 Crockett&Jones CONISTON 29 24
9 BROTHER BRIDGE McCLOUD 53 24
10 BROTHER BRIDGE Morgan 28 23
11 Redwing 1930s Sport Boot 30 23
12 Sanders Military Apron Derby Boots 33 22
13 SLOW WEAR LION OB-8593GT 栃木レザープレーンミッドブーツ 50 22
14 SLOW WEAR LION OB-8593G オイルドレザープレーンMIDブーツ 48 22
15 Redwing Irish Setter 51 22
16 Nicks 6inch HotShot 50 21
17 JOHN LOFGREN M-43 SERVICE SHOES 21 21
18 Wesco Custom Jobmaster 38LTT 28 20
19 Pistorelo Norwogian Split 43 20
20 Chippewa 6inch Service Boots 49 20
21 Dry Bones CYPRESS 20 20
22 NPS shoes CAMERON ESSENTIAL 20 20
23 Scotch Grain Spider 28 19
24 BROTHER BRIDGE Harry 19 19
25 Berwick Chukka boots 20 18
26 Berwick Wing tip 34 18
27 SLOW WEAR LION OB-8208G オイルドレザーサイドゴアブーツ 47 18
28 MoonStar HI BASKET W 17 17
29 Chippewa Suburban 35 16
30 Chippewa Bridgemen 40 15
31 ASAHI M022 14 14
32 Chippewa 6inch Boots Reverse Black Odessa 14 14
33 KATSUYA TOKUNAGA BOYS 14 14
34 Timberland GARRISON TRAIL GORE-TEX HIKING SHOES 14 14
35 Ballband Jackie 12 12
36 Onitsuka Tiger MEXICO 66 12 12
37 New Balance Postal 706V2 11 11
38 NPS shoes LAW 7 7
39 ASAHI M020 6 6
40 SOLOVAIR Black Grain 6 Eye Derby Boot 5 5
41 Chippewa 6inch Service Boots 33 5
42 Rolling dub trio HunterⅡ 4 4
43 Berwick Chukka boots 30 3
44 Chippewa Bridgemen 27 1
45 Hunter Chelsea Boots 30 0

Oura Ring買って見た

Oura Ring

ずっと気になっていたので買って見ました。
サイズキット付きで購入し、サイズを確認してオンラインでサイズ指定するだけでリングを届けてくれます。

届いたあとは充電して、つけてるだけでメトリクスが取れます。
気にしているのは睡眠のスコアと消費カロリーです。

自宅で仕事をすることが多くなったので、運動不足解消の為にウォーキングをしているのですが、毎日目標の距離と消費カロリーを提示してくれるので、どのくらい歩けばいいのか目安になっていいですね。

また睡眠スコアがよくて、熟睡している時間とか細かく見れます。
スコアの内訳をみて、熟睡時間が長かったから目覚めがいいのかなとか、寝返りが多かったからスコア低いんだなと色々わかります。

体調のコンディションがわかることで、寝る時間をコントロールしやすくなったので満足度はすごい高いです。
まだ使ったことない機能もあるので、色々試して行きたいです。

Microsoft Graphでユーザーのアバター画像を取得する

Microsoft Graphにはv1.0とbetaのエンドポイントがあります。
どちらにもユーザーのアバターを取得するAPIがあります。  

しかし、v1.0とbetaで取得できる場合とそうでない場合があるようです。

https://docs.microsoft.com/ja-jp/graph/api/profilephoto-get?view=graph-rest-1.0 https://docs.microsoft.com/ja-jp/graph/api/profilephoto-get?view=graph-rest-beta

v1.0とbetaで同じユーザーのアバターを取得してみる

アバターはサイズを指定して取得をできるので、必要なサイズのものを取得します。
データはバイナリで受け取るので、適当にBufferなどに変えてファイルに書き出すかData URI schemeに置き換えて、DBに格納したりすることができます。

ここではData URI schemeに変えるコードを載せておきます。

import fetch from 'node-fetch';

const v1Endpoint = 'https://graph.microsoft.com/v1.0';
const betaEndpoint = 'https://graph.microsoft.com/beta';

type Endpoint = "v1.0" | "beta";
type AvatarSize = "48x48" | "64x64" | "96x96";

type Avatar = {
  mimeType: string;
  url: string;
};

const userAvatar = (endpointType: Endpoint, size: AvatarSize) => async (userId: string): Promise<Avatar | undefined> => {
  const options = {
    method: "get",
    headers: {
      Authorization: `Bearer ${process.env.ACCESS_TOKEN}`
    }
  };

  const endpoint = endpointType === 'v1.0' ? v1Endpoint : betaEndpoint;
  const response = await fetch(`${endpoint}/users/${userId}/photos/${size}/$value`, options);

  if (!response.ok) {
    const failedBody = await response.json();
    console.log(failedBody);
    return;
  }

  const mimeType = response.headers.get('content-type') as string;

  const buffer = await response.buffer();
  const data = buffer.toString('base64');

  const url = `data:${mimeType};base64,${data}`;

  return {
    mimeType,
    url
  };
};

const v1Avatar = userAvatar("v1.0", "48x48");
const betaAvatar = userAvatar("beta", "48x48");

const main = async (userId: string) => {
  const v1 = await v1Avatar(userId);
  const beta = await betaAvatar(userId);
  console.log(v1);
  console.log(beta);
};

const userId = process.argv[2];

main(userId).then(() => {
  console.log('done');
}).catch(err => {
  console.log(err.stack);
});

実行するにはアクセストークンとユーザーIDを指定します。

ACCESS_TOKEN=[アクセストークン] \
node avatar.js [ユーザーID]

v1.0では一部のユーザーのアバターが取得できない場合がある

一部のユーザーではアバターを設定しているのにもかかわらず、アバターが取得できないユーザーがいました。
そのユーザーの場合はMailboxNotEnabledForRESTAPIがエラー本文に含まれていました。  

{
  error: {
    code: 'MailboxNotEnabledForRESTAPI',
    message: 'REST API is not yet supported for this mailbox.',
    innerError: {
      date: '2020-08-25T10:53:21',
      'request-id': xxxxxxxxxxxx
    }
  }
}

betaで問題が発生しないのは一部のAPIの制限がなくなったものと思われます。
OfficeなどのAPIがMicrosoft Graphに統合されているはずなのですが、まだ完全には終わっていないのかもしれないです。

Azure Bot ServiceのBot Framework SDKのkoa対応

最近はBot Framework v4を利用して、Botアプリケーションを作成しています。
Botアプリケーションを作成するには、SDKを利用するのですが、expressrestifyくらいしかWebアプリケーションのフレームワークが対応してないような気がします。

koaの場合

例えば、BotFrameworkAdapter.processActivity の要求にレスポンスオブジェクトを渡す必要があるのですが、 koacontext.response を渡すと status、send、end あたりのメソッドがない為に、ランタイムエラーが発生します。

これは、expressrestifyのレスポンスオブジェクトのインターフェースをBotFrameworkAdapterが期待しているからです。

また、koaの実装の場合、ミドルウェアを一通り処理した後に最終的な応答を返すので、サンプルでよくある実装をすると応答を正常に返せません。

// よくみるサンプルコード
// express/restifyの場合
app.post('/api/messages', (req, res) => {
  adapter.processActivity(req, res, async (context) => {
    await bot.run(context);
  });
});

Bot Emulatorを使用してデバッグすると、404を返した後に200を返したりします。

f:id:holyshared:20200802185533p:plain
Emulatorのログ

// 素直にkoaでサンプルのコードを実装した例
// routerはkoa-routerのインスタンス
router.post('/api/messages', async (ctx: RouterContext<{}, {}>) => {
  const { request: req, response: res } = ctx;
  adaptor.processActivity(req, res, async (turnContext: TurnContext): Promise<void> => {
    await bot.run(turnContext);
  });
});

この現象の理由は、processActivityをawaitしてない為、コールバック関数を抜けた後に、404を直ぐに返し、その後にBotのメッセージ応答が正常に終了し、200などの応答を返す為だと思います。

express/restifyの場合はawaitしなくてもレスポンスオブジェクトのendを実行したタイミングで応答が完了するので問題ないのですが、koaの場合はそうではなく、最終的な応答を返すのがフレームワーク側 なのでうまく行きません。

サンプルのコードとかだと、ただのコールバック関数で処理しているだけに見えたので若干困りました。

最終的な対応

原因がわかったので、適当なexpress/restifyのレスポンスオブジェクトを渡しつつ、processActivityをawaitして対応することにしました。

スマートな解決方法ではないですが、テキストメッセージをユーザーに送るくらいだったらこれで良さそうです。
また、WebResponseのインターフェース仕様を読む限り、end、send、statusだけ実装されてれば良いぽいのでこれであってそうではあります。

import Koa from 'koa';
import Router, { RouterContext } from 'koa-router';
import { BotFrameworkAdapter, TurnContext, WebResponse } from 'botbuilder';
import { Bot } from './bot';

const app = new Koa();
const router = new Router();

const adaptor = new BotFrameworkAdapter({
  appId: process.env.BOT_APP_CLIENT_ID,
  appPassword: process.env.BOT_APP_CLIENT_PASSWORD
});

const bot = new Bot();

const createWebResponse = (ctx: RouterContext<{}, {}>): WebResponse => ({
  status(code: number): void {
    ctx.status = code;
  },
  send(body: string): void {
    ctx.body = body;
  },
  end(): void {
  }
});

router.post('/api/messages', async (ctx: RouterContext<{}, {}>) => {
  const res = createWebResponse(ctx);
  await adaptor.processActivity(ctx.req, res, async (turnContext: TurnContext): Promise<void> => {
    await bot.run(turnContext);
  });
});

app.use(router.routes())
  .use(router.allowedMethods())
  .listen(3000);