User-Agent が使えなくなる世界線で何を考えたら良いだろう

※ この記事は 2021/03 時点での情報になり、今後大きく変動する可能性があります。

[User-Agent が Google Chrome で凍結・非推奨](https://forest.watch.impress.co.jp/docs/serial/yajiuma/1229968.html)になる という話題が年明け以降一部の界隈で盛り上がっています。

また、Chrome に限らず、 Firefox, Safari など Chromium 以外のベンダでも User-Agent（以下 UA） の凍結・非推奨が予想されており、近い将来 UA を使わずにデバイスの判定をする世界線は避けて通れなくなりそうです。

## UA の代替となる指標
Google は UA を凍結・非推奨とする代わりに [User-Agent Client Hints (以下 UA-CH)](https://wicg.github.io/ua-client-hints/) を新たなデバイス等の判定の指標として提案しています。

※ UA-CH は W3C の Draft Community なので、今後仕様が変わる可能性があります。



例えば Chromeから任意の URI にリクエストを行う場合、Webサーバ側では以下のような値が Request Header に含まれて送信されます（`chrome://flags` の `Experimental Web Platform features` を有効することで試すことが可能です）。

```
sec-ch-ua: "Chromium";v="86", "\"Not\\A;Brand";v="99", "Google Chrome";v="86"
sec-ch-ua-mobile: ?0 

# ?1 が true (mobile である), ?0 が false (mobile でない) を指す
# デフォルトはこの２つの Key/Value 値になります
# 他にもプロパティはあるので詳しくは Document を参照下さい
```

また、Apache 等の Middleware 側でも Accept-CH などを setHeader することで、自身の開発している Webサーバ からのレスポンスに上記以外の UA-CH を含めることも可能です。

## UA-CH で何ができる
UA の代替として使えることが考えられます。
現行だと Chromium ベースに限っての話になりますが、Client, Web Server 側両方とも検知が可能です。

### Client 側で UA-CH を取得する
Web API で提供されている Navigator インターフェースより行うことができます。
現在、 `navigator.userAgentData` が UA-CH のインターフェースとして活用できます。

```js:公式より
/** @see https://github.com/WICG/ua-client-hints#for-example */

const uaData = navigator.userAgentData;
const brands = uaData.brands;     // [ {brand: "Google Chrome", version: "84"}, {brand: "Chromium", version: "84"} ]
const mobileness = uaData.mobile; // false
(async ()=>{
  // `getHighEntropyValues()` returns a Promise, so needs to be `await`ed on.
  const highEntropyValues = await uaData.getHighEntropyValues(
      ["platform", "platformVersion", "architecture", "model", "uaFullVersion"]);
  const platform = highEntropyValues.platform;               // "Mac OS X"
  const platformVersion = highEntropyValues.platformVersion; //"10_15_4"
  const architecture = highEntropyValues.architecture;       // "x86"
  const model = highEntropyValues.model;                     // ""
  const uaFullVersion = highEntropyValues.uaFullVersion;     // "84.0.4113.0"
})();
```

これにより、**ブラウザの特定** 及び **デバイス判定（Desktop か Mobile）** を行うことができます。
UA で文字列を正規表現で舐めて判定していた時代より随分に楽になりそうです。

ただ、`userAgentData` の実装は Chromium ベースのブラウザなの（このインターフェースの仕様も半年で微妙に変わっている...ex `brands` が3ヶ月前は `uaList` だったり）で、他のベンダ（Firefox, Safari）が UA を廃止した時の代替実装がまだどういうものなのか、また、userAgentData を実装するかさえもわからないため、Client 側の判定方針は今後も注視する必要がありそうです。

Ref ) [Chromium ベースのブラウザは検証している方がいるので参考までに](https://qiita.com/nightyknite/items/257251b9692dff1b62be#chrome%E4%BB%A5%E5%A4%96%E3%81%AEchromium%E7%B3%BB%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%81%AE%E5%A0%B4%E5%90%88)

### Web Server 側で UA-CH を取得する
先程のコードブロックに記載通り、Web Server 側では Request Header に UA-CH の情報が含まれているので、
Client のように Web API を介した情報の取得をせずとも対応が可能なので、Server 側のほうが現行だと対応しやすいかもしれません。

Server はリクエストを受ける側ですので、UA-CH と UA で判定処理をかき分けておけばある程度の状態は把握することができるでしょう。
例えば、「モバイルかどうか」の判定は以下のようなコードで表現できたりします。

```ts
type LegacyUA = string | undefined;
type CHUAM = string | string[] | undefined;

const judgeIsMobile = (legacyUA: LegacyUA, CHUAM: CHUAM): boolean => {
    let isLegacyMobile = false;
    let isCHMobile = false;

    /** Legacy User-Agent judgment */
    if (legacyUA) {
        const isIOS = /iP(hone|(o|a)d)/.test(legacyUA);
        const isAndroid = /Android/.test(legacyUA);
        isLegacyMobile = isIOS || isAndroid;
    }

    /**
     * Client Hints judgment for Chromium
     * CHUAM (sec-ch-ua-mobile) has `?0 (false)` or `?1 (true)` judgment value.
     */
    if (typeof CHUAM === 'string') {
        const capture = /^\?(?<mobile>\d{1})$/.exec(CHUAM);
        const hasMobile = capture?.groups?.mobile || '0';
        isCHMobile = Boolean(parseInt(hasMobile));
    }

    return isLegacyMobile || isCHMobile;
};
 
/** Usage */
const legacyUA = req.headers['user-agent'];
const CHUAM = req.headers['sec-ch-ua-mobile'];
 
const isMobile = judgeIsMobile(legacyUA, CHUAM);
 
/** Use isMobile logics */
// ...
```

※1 上記の処理を [npm パッケージ](https://www.npmjs.com/package/ccjudge) として使えるように公開してみました。需要があるかわかりませんが、今後UA-CHに動きがあれば更新していきたいと思います。
※2 サーバサイドも [CPU Bitness のインターフェースが追加されたり](https://github.com/WICG/ua-client-hints/commit/5b7f97fe3ce7c150c5f3eb83d95bc6ec5a112068) まだまだ動きがありそうです。

### UA-CH を効率良く使う
これまでは UA-CH の現行の仕様で何ができるかを考えてみました。
ただ、UA-CH はデフォルトの値だけではなく、`Viewport` などにも対応しています。

例えば、HTML に `<meta httpEquiv="Accept-CH" content="Viewport-Width, Width" />` を設置して、Web Server 側でリクエストを受け付けるとします。
![meta-viewport.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55010/55a98579-1bcd-1daa-8666-c71d54728f75.png)

そうすると、
![get-viewport.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55010/42798ad5-3a4a-92dc-cbfa-6386f949b612.png)

Request Header には `viewport-width` の値が反映された状態でリクエストが来ます。これは便利ですね。

デバイス判定はできないにしても、CSSレイアウトなど `Viewport` で振り分けていることって意外と多いので、デバイスの Width などで判定をするなどのビジネルルールでソフトウェアを開発する場合などは、こちらのほうが自分たちで制御しやすいのでいいのでは？とも考えられます。

UA-CH がカバーしているプロパティは、[chromium 内のこのソースコード](https://github.com/chromium/chromium/blob/master/third_party/blink/common/client_hints/client_hints.cc#L21)で確認できます。

## まとめ
現行の世界線とこれからの世界線がどういうことになりそうかまとめてみます。

<table>
<tr>
<th></th>
<th>Chromium (Chrome など)</th>
<th>Webkit (Safari)</th>
<th>Gecko (Firefox)</th>
</tr>
<tr>
<td>Client</td>
<td>
<ul>
<li>UA 凍結・廃止の意向</li>
<li>UA-CH / navigator.userAgentData を使って判定が可能（になるかもしれない）</li>
<li><a href="https://support.google.com/analytics/thread/56504372?hl=en">GAに関しての指摘</a> もされており、UA と代替となる技術で GA は運用されることが示唆されています(viewport-width などはそれも睨んでいるのかな？など思ってみたり)</li>
</ul>
</td>
<td>
<ul>
<li><a href="https://gigazine.net/news/20200629-apple-web-api-decline/">一部の Web API の実装を拒否する意向</a></li>
<li>navigator などが無くなるわけではないが注視は必要か</li>
<li>現行もUAを固定（iPad 周りの判定しづらい）していたり、今後も予告なく凍結などする可能性あり（予告がないため）</li>
</ul>
</td>
<td>
<ul>
<li><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=935216">UA-CH の実装予定はあり（優先度低い）</a></li>
<li><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=935216#c70">2021年になって議論が少し進んでいる模様</a></li>
<li>現行は UA 対応か (UA-CH が実装されれば userAgentData ワンちゃんあり？)</li>
<li>Safari 同様一部の Web API の実装をしない傾向にある</li>
</ul>
</td>
</tr>
<tr>
<td>Web Server</td>
<td>
<ul>
<li>Request Header 内の UA / UA-CH での判定</li>
<li>setHeader などで UA-CH の対応も可能</li>
<li>Viewport などでのデバイス判定利用をすることもできるたけ柔軟な対応が可能</li>
</ul>
</td>
<td>
<ul>
<li>現行は UA</li>
<li>Client 側の実装が突然されなくなる可能性あり、Safari は他のベンダに比べて情報を取得できなくなる可能性が高い</li>
</ul>
</td>
<td>
<ul>
<li>現行は UA</li>
<li>Client で UA-CH の実装が行われればこちらは問題なさそう</li>
</ul>
</td>
</tr>
</table>

現行だと、Chrome はなんとかなりそうですが、Safari, Firefox は注視していく必要がありそうです。ただ、[今年は感染症の影響もあり、Chrome 側も UA の本格的な廃止は来年にずれ込むようです。](https://groups.google.com/a/chromium.org/g/blink-dev/c/-2JIRNMWJ7s/m/u-YzXjZ8BAAJ)[chrome v89 でも凍結はまだそう。](https://groups.google.com/a/chromium.org/g/blink-dev/c/-2JIRNMWJ7s/m/fSR21hp0AQAJ)

Firefox は[こちらのようなポジションを取っており](https://github.com/mozilla/standards-positions/issues/202)、また、[Tor ブラウザも Gecko ベースなので](https://bugzilla.mozilla.org/show_bug.cgi?id=935216#c65) 慎重になっているのかなという考察しています。

フロントエンドは `navigator.userAgentData` の I/F が結構な頻度で変わっているのもあり、現行プロダクションレベルでの対応は中々骨が折れそうですが、Web Server 側での分岐処理などは一旦事前に検討しておくことが可能な印象です。BFF など上手く活用できるとクライアントの煩雑な処理はある程度避けられるのかなと考えたりしています。

数年で UA 周りの景色はだいぶ変わるかもしれませんね。