トップ 追記

2017-02-25 [長年日記]

_ 意図通りのURLにユーザーを誘導したい

HTMLでは

<link rel="canonical">

を書いてあげればよい。

JavaScriptでは

location.host

を見て意図通りの URL でなかったら location を書き換えてあげればよい。

サーバ側アプリでは

基本的には HTTP_HOST を見る。ところが構成によってここで取得できる値は変わる。*1

例えば Heroku の場合はもともと *.herokuapp.com の名前を持っている。これに DNS の ANAME や CNAME で hostname を設定し、これをユーザーに見せる URL として設定することになるのだが、ここに CDN が加わると設定方法にバリエーションができる。

※ 以下はアプリケーションサーバが Heroku にある前提で書いていくが、別にどんなサーバでも構わない。単にデフォルトの名前が分かりやすく決まっているので参考に挙げやすいだけである。

a) CDN側にだけDNSを設定する
  1. Heroku 側では別な endpoint を定義せず
  2. CDN 側で endpoint を定義する(例えば example.com で Origin を *.herokuapp.com に)

形になっている場合、当然だが Heroku 側で拾える HTTP_HOST はデフォルトのものだけになる。

+-----+
| DNS |
+-----+
   ↓
+-----+    +-----+
| CDN | → | App |
+-----+    +-----+

App で拾えるのはデフォルトの *.herokuapp.com だけ。なぜなから CDN から *.herokuapp.com でアクセスされるから。

これの対処は後述。

b) CDNにもAppにもDNSを設定する
      +-----+
      | DNS |
      +-----+
         ↓
   +----------+
  ↓          ↓
+-----+    +-----+
| CDN | → | App |
+-----+    +-----+

App の endpoint も DNS で設定して

  • example.com ( Heroku )
  • cdn.example.com ( CDN )

のように設定できるなら App 側で Canonical(この場合は *.herokuapp.com ではなく example.com)かどうかを HTTP_HOST で取得することができる。

CDN経由かどうか

ということで今度は CDN 経由かどうかを知りたい場合。上の a) のパターン。

  • Via ヘッダを見れば経由サーバがあるかどうかは分かる
  • Heroku のようなマルチテナント PaaS や Load Balancer 経由のアクセスの場合は常に Via 値があるので、意図したサーバを経由したかどうかは「Viaヘッダがあるかどうか」だけでは判別できない
  • CDN や Load Balancer 経由かどうかは「Via ヘッダに入る文字列」での判別になる
    • 多段の場合は複数の情報が文字列で入る

ということで例えば Rack であれば

env['HTTP_VIA'].include?(特徴的な文字列)

で判別できる。

*1 Rails 4 までだと直接 HTTP_HOST を見ずに request.host を使えばいいと思う。5 以降はこのメソッドなくなったそうなんだけど、使ってないので分からない。


2017-02-19 [長年日記]

_ JavaScript復習2017 - importとuse strictとメソッド定義 -

背景

Ecma 262 + JScript でユニットテストを書いて から 10 年以上が経ち、途中 Rails 3 + CoffeeScript いいじゃないか、Underscore.js いいじゃないか期を経て現在は ES2015 が標準で当たり前ですよね、な時代になった。

ES2015 自体はなんでもかんでも式になってくれないと CoffeeScript の代わりにはならないよ、return 書くの面倒くさいよという気持ち以外はまぁそんなもんかなという程度の理解だったんだけど、Mithril を試した時 のように無理やり Browserfiy で CoffeeScript を挟んだりせずにそろそろ 素直に ES2015 をフルに使って書きつつ、それ以外に本当に必要なライブラリのセットをまとめる作業を行った方が UI 側の実装に弾みがつくかなと思って触ってみたらどうも ES2015 がフル実装されている処理系はないっぽいので、結局ややこしさが残っているのだなということが分かってきた。

ということでそろそろ少なくとも ES2015 でよく分からんなと思っていた部分を整理していこうと思う。これ以上時間が経つと情報が見つからなくなるかもしんないし。

これを書いている時点での自分の理解をまとめると Node.js と Browserify を適当に使っている範囲のもので、Ecma 262 3rd 以降に加わったものはちゃんと追えておらず、以下のような状態。

  • Webフロントエンドのツールチェインはブラウザなしに動く Node.js であれこれ処理するのが基本になった
  • 伝統的な JS にはライブラリを読み込む機能はなく、ホスト側でまかなっていた(<script>)が、XHR + eval などで動的に読み込むこともできる
  • Node.js 登場以降 module と require の概念が持ち込まれて function 以外のスコープが増えた
    • Browserify は Node.js のこのメリットをブラウザの世界に持ち込んだもの
    • ついでに Node.js 上でしか動かない機能以外は組み込めるようになった
  • Babel は先行案の JS の文法を使ったコードを変換して Ecma 5 相当にしてくれる
    • ので、先行案の文法*1 を使ったコードが Ecma 5 相当と互換性のあるブラウザで動作するようになる*2

ここから、もうちょっとつっこんだ部分の理解を深めたいと思う。

ちなみに、以降の確認は主に以下の環境で行っている。

  • Node.js v6.9.5
  • Yarn 0.19.1
  • Babel 6.23.1

import / export

import とかよく見るじゃないですか、React とか Babel 前提になったコードで。でもなんとなく見よう見まねの域を出なくて気持ち悪いと思っていたのでした。

2017-02 時点で結論から言うとまず Babel を使えと。そういう状況らしい。

ES Modules(ES2015) import -> Babel -> CommonJS require

結局 require に書き換えて読み込むことになる。

安定して動かせる import は今のところこの手の変換以外ないみたい。

単純な import
import './export' -> require('./export')
無名で(defaultで)定義して import 側で名前の割り当て

export default で名前のない function や class を export できるので、import 側で名前を割り当てて利用する。

※ 以降、<name> となっている部分は <> 含めて実際の名前に読み替えplz

import.js

import <name> from './export'

<name>()

export.js

export default function() {
  ...
}

を babel で変換すると

Object.defineProperty(exports, "__esModule", {
  value: true
});

exports.default = function () {
  ...
};

てな感じで exports.default に function が入る。class の場合はもうちょっと複雑になるけど

export default class {
  method {
    ...
  }
}

var _class = function() {
  ...

  _createClass(_class, [{
    key: <method>,
    value: function <method>() {
      ...
    }
  }]);

  return _class;
}();

exports.default = _class;

てな感じで、やはり exports.default に class が入る。

名前付きで定義したものを export / import

{ } で囲む。

※ as は割愛

import.js

import { <name> } from './export'

<name>()

export.js

function <name>() {
  ...
}

export { <name> }

class もまったく同様。

'use strict'

Strict モード - JavaScript | MDN

Ecmascript 5 で追加された機能で Babel で変換したコードには現時点では自動で追加される。

  1. 意図せぬグローバル汚染を防止
  2. 代入の失敗は例外に
  3. 削除できないプロパティの削除をエラーに
  4. オブジェクトリテラルのプロパティ重複もエラーに
  5. 関数の引き数名の重複もエラーに
  6. 8進数表記の禁止

基本的には予期せぬエラーが起きにくくなる改善なんだけど、一つ問題があって strict モードでの動作の指定をグローバルに行おうとした場合、strict モードでの動作を意図したコードと strict モードでの動作を意図していないコードを混ぜると、先に決まったモードで動くのでお互いに意図せぬ動作をする場合があること。

したがってどんなコードを混ぜ込んでいるか分からない、伝統的な JS のコピペベースのライブラリの利用を行った場合、この指定は function 単位で行うことが望ましい。でないとどこで何が起きるかよく分からなくなってしまう。

いや、面倒だよねそれ。

というところまで分かったところで Babel に戻ると、Babel は es2015 の preset で Ecmascript 5 に変換する際に自動的に 'use strict' を必ずファイルの先頭に置くので、自動的にすべて strict モードで動作するように変換される。

ということで Ecmascript 5 を前提にするなら Babel 使えという話に帰ってくるのであった。

class / メソッド定義

クラス - JavaScript | MDN

  • class定義は事前に済んでいないと呼び出せない
    • なんか PHP 3 までの制限のような?
  • constructor, extends, super なんか PHP というか Java っぽい?
  • name: function() {} が 単に name() {} で書けるようになった
    • オブジェクトリテラルでもclass式でも同様
    • やった! function 地獄からの解放?
    • でも割とブラウザの対応状況はよくない(まぁ Babel 通すならどっちでもいいけど)

メソッドとグローバルな関数の定義はベツモノで function を書かずに済むようになったわけではないうえに => (arrow function)もまたベツモノ。

いったんここまで。

Tags: JavaScript

*1 ESnext と呼ばれる

*2 例えば IE9 以降とか。


2017-01-14 [長年日記]

_ localStorageでcookieのexpireのようなことをしたい

とある事情でブラウザ上のデータの一部を expire したくなった。イメージとしては cookie の expire みたいなものなんだけど cookie でサーバに送る必要もないし、数が増える可能性があるので localStorage でやりたい。

まず、基本を調べてみると

ということになっている。

あれこれ探してみたけど、扱いやすそうなのは

かな。あと見つけたのは

で、有名っぽそうに見える

js-data

にはそういう API はなかった。

なるほど。需要がないとはあんまり思わないんだけど、JS は Model 周りがまだまだ弱いのか、自分の探し方が悪いのか。とりあえず localstorage-ttl 試してみようかな。

Tags: JavaScript

*1 昨今のブラウザでは気にする必要ないんだっけ?


2016-12-28 [長年日記]

_ 2016年に設計なんてない、そこそこの量のJavaScriptのエラーを監視して対策し始めました雑感

公開されるどこにも記録を残していないような気がするが、2016年の初めからとある事情により JavaScript のエラーをサーバに送りつけて監視サービスに送りつけてエラーの発生を知り、修正する、ということを地味にくり返していた。

そこに至る顛末と今後の分析の予定のお話。

背景

これまで扱ってきたものはそこまで JS ヘビーでないものが多く、また自分で書くものはできるだけユニットテストが動くように書いていた and そもそも監視サービスが入っていなかったので、エラーのログをサーバに送るとか監視するとか、そこまで手をかけていなかった。

しかし今回の案件は初期の設計では考えてもみなかった量のカウボーイスタイル JS がコミットされしまい、要するに非常にイキのいいフレッシュなレガシーコードがてんこ盛りで動いている状態になってしまった。

(あーはい、全部ぼくがコードレビューしてリジェクトすれば防げたんです、すいません。それをやったら別な角度からこっぴどく叱られたのでしょうけど。)

まずは静的解析

CI に乗せるまでも苦労したのだが、CI に乗せたのを機に eslint は動くようにしていた。

その段階ではほぼ致命的な問題は見つからなかったので、ちょっと安心していたのだけれど、やはり自動化されたテストのないコードが増えると厳しい。「なんかうまく動いていない感じがする」「再現性を確認できるレベルの報告は来ないが、何かは起きてるっぽい」という状態であり、またこの状態では積極的にはやりたくないが、どうしても変更はせざるを得ない。(完全無欠で変更不要なコードなんてあり得ない。)

cache busterできていない問題

そもそもの話なんだけど、件のサイトは AssetPipeline 的な仕組みが標準で入っていないので、当然のように JS がキャッシュされて、ブラウザによって動作がマチマチになってしまう問題を抱えていた。Rails 界隈では AssetPipeline 批判がすでに一周回り終わってしまっているが、世間にはまだそのような便利な仕組みがあることさえ知らずに素朴にキャッシュの効く生 JS を書くお仕事もあるわけです。はい。

が、なにせ量が多く、納得のいく形の cache buster まで持っていく時間を捻出できないので、そこは無視することにした。「cache の問題で意図した動作がユーザーの手元で再現できない場合がある」ことを記録するに留めることとした。

ガッデム。*1

とにかくwindow.onerrorでサーバに送る

以前ならテストコードのないサーバサイドのアプリもエラーログで追い詰めつつ改修していくというのも割とよくやったが、最近はそういうやんちゃなコード扱ってないし、そもそも JS だとエラーログねーし…と思っていたが、「いや、何かで見たな」と思い返し、考え方を変えて、「まずはよく分からない状態を分かるようにする」ことにした。

大手Webサービスがクライアント側で発生したJavaScriptのエラーをどう収集しているのか まとめ - Qiita

を参考に、いちばんざっくり書けそうなのは

  • window.onerror
  • Ajax(サーバサイドアプリは単独ドメインなので)

の組み合わせで、サーバサイドに送ってサーバ側で NewRelic に送ることかなと考えた*2。サーバサイドのエラーはすでに NewRelic に送ることができていたため、サーバ側に来てさえしまえばなんとかなると考えたのだ*3

NewRelic なら Error rate が基準(デフォルトでは 5.0%)以上になると通知してくれるので、何かが起きていることは可視化されやすいし、そこには

  • URI
  • ソース上のエラー発生箇所
  • エラーメッセージ

といった基本的な情報が含まれているので、「分からないものが分かるようになる」段階としてはひとまず十分と言えるだろう。

びっくり仰天サーバサイドでJavaScriptリテラルを生成するコードの存在

上の記録をやり始めてから明確にエラーが減り(JS側の動作がおかしくてサーバ側にエラーが起きていたものも含む)、喜んでいたのもつかの間、いちばんショックなコードに出くわした。

ざっくり要約するとサーバ側のテンプレートで

var obj = val1 + {{$val2}};

みたいなことをやっているので、サーバ側で $val2 が null になると JavaScript 実行時には

var obj = val1 + ;

みたいなコードになってしまい、SyntaxError になるというもの。

こ れ は ひ ど い

2015年のコードでこれはないよ。値は HTTP か data-* で渡せよ。eslint 意味ねーだろ!!

泣く泣く

{{}}

の中に条件演算子を埋め込んで回りましたとさ…。

※ サーバサイドの View で JavaScript を書くのをそもそも禁止すれば少しはマシになりそうな気がするんだけど、これを静的解析で見つけることは可能なのかなぁ? 人間がダメ出しすることはできても機械的に弾けないとやはり不安は残る。

無視すべきエラーとGoogle Analyticsへの記録

最近あった、ちょっと困ったパターン。

  • Chrome 55 で実装された PointerEvents がバグっててエラー急増
  • Babel を使って書いてる部分があって、IE 8 以下でエラーを吐く
  • iOS の Google Search App が(恐らくZoomイベントに)バグを抱えていてエラーを吐く

こいつらはブラウザ側の問題でかつサポート外か、警告は出るが動作はするというものなので、無視する(サーバに送らない)ことにした。

※ Google Search App だとググってからでないとサイトを訪れることができないので、ほぼ production 環境以外ではデバッグできないわけだけど、みんなこんなもんどうやって相手してんの?

無視するだけだと怖いのでGoogle Analyticsに記録することにした

もしかしたら Chrome 56 で修正されるかもしれないし、闇雲に無視してしまうのは本来拾えるはずの情報を捨てることであり、なかなか怖い。ということでアプリが動いているサーバには送らないが Google Analytics には送ることとした。これなら通知はされないがあとで記録を見返すことはできる。

Google AnalyticsでJavaScriptエラーをトラッキングする

ただし、記録、閲覧できる情報量に不満があるので、もしかしたらイベントの方がよいかもしれない。

Google Analyticsを利用してクライアントサイドのエラーのレポーティングを行う - Thousand Years

とは言え、だいたい普通はイベントに関しては「何らかの効果の捕捉」に使うもので、利用回数に制限のあるイベントトラッキングを、何かの拍子に爆発しかねないエラーの記録に使うのはややリスキーだよね。

Google Analytics は分析目的の詳細な情報の記録ではなく、あくまで数の参考に留めておくのがよいのかもしれない。

まだ改善は続く

自分以外の人間が書くコードに対して、自分の得意なテストコードベースの手法だけを適用していこうとするのはやはり無理がある。一朝一夕には TDD はできるようにならないし、自社に十分な人数のデキるエンジニアが揃っていない場合は理想だけを追っても意味はない。

ということもあって、エラーログを単に「現在エラーが起きている」ことを知るためだけに使うのではなく、エラー発生率の変化など、指標として可視化してふり返りや分析にも利用できるようにしたらどうだろう、そしてたぶんこれは単独のサイトで考えるのではなく、会社全体としてログ分析基盤の一環として考えるべきなのではないかと思い始めている。

NewRelic以外のログ分析の基盤を用意したい

と考えると、単なる尻拭い案件ではなくなり、急に面白みが増してきた。

ERRORログが多すぎるWebアプリに出会ったら | GMOインターネット 次世代システム研究室

なんて話もあるが、現在我々には

Amazon Athena – サーバーレスのインタラクティブなクエリサービス – AWS

があるので、ログを S3 に放り込んで Athena で分析するのはアリだなと考えている。

NewRelic はエラーの通知とグラフィカルな可視化にはよいが、そもそも NewRelic のサイトの応答が重く、Error も Similar なもので畳まれて追いにくくなるので、もっと分析に特化したものは用意した方がよいように感じている。NewRelic はすぐに対応するにはよいが、長いスパンで見たい場合にはあまり向いていないなーと思っていたが、似たようなことを感じている人がいた。

※ export も自分で API を叩くコードを書かなきゃいけないし。

なぜ私たちはSumo Logicを捨ててBigQueryを選んだのか - tech.guitarrapc.cóm

この記事では BigQuery に入れることにしたようだが、自分には基盤整備の時間が十分にあるわけでもないので、「とにかくなんでも後回し」で考えると S3 + Athena かなという気がしている。幸い、ログを S3 に保存するのは自動化できている。

※ ところでバックトレースは複数行にまたがってしまうのに行に対して検索を掛ける SQL ベースの分析基盤はエラーログ分析には実は向いていないような気がしないでもないのですが、すでに基盤を構築済みの皆さんはどうしてるんですか? トレースを改行のない状態に畳み込んでから記録してる? そうするとロガーに手を加える必要がありますよね?

ログの情報量アップ

これまでは window.onerror に標準で渡ってくる情報を頼りに、トレースを取らずにそのままサーバに送っていたが、さすがにつらくなってきたので、

JavaScriptエラーログ収集に役立つツール・ライブラリ・手法まとめ - WPJ

を参考に

StackTrace.JS - Framework-agnostic, micro-library for getting stack traces in all web browsers

csnover/TraceKit: Attempts to create stack traces for unhandled JavaScript exceptions in all major browsers.

を使ってバックトレースは欲しいと思っているところ。

全体的な目標としては

JavaScript 祭で発表してきました - 若き JavaScripter の悩み

こんな感じですよね。よくまとまっていて分かりやすい資料をありがとうございます。

バグを防ぐ、デバッグという意味では本稼動してるアプリのログはやはり後手に回っているので、本来は前行程で防ぎたい。でも今は便利なツールが揃っているし、後手に回ったなら回ったなりにできることはいろいろあるし、これに慣れてくると、ちゃんとログ残したり監視できていないサイトはデプロイするの怖いという気もしてきそう。

エンジニアは怠惰で贅沢な生きものだもの。

*1 ま、でもあれです。ぶっちゃけ地方には JS 専門の人なんてほぼ皆無で、みんなこんなもんすよ。と思ってないと平静を保てません。全部自分で書けるわけじゃないのが現実です。

*2 この1年半ほどでようやく NewRelic の恩恵に与かりまくっている。

*3 実際には試しに入れた Rails のサイトでは目的の機能はさっくり実現できたのだが、本命のサイトは PHP でできていてかつグローバルにエラーを拾ってゴニョゴニョするという悪魔のような機能が動いていて、そこで四苦八苦することになるのだがそれは今回のテーマではないので割愛。