Hammer.js 本番環境におけるジェスチャーエラー監視と診断システムの実装

生産環境でのタッチインタラクション安定性確保

Hammer.js は現代の Web アプリケーションにおいて、マルチタッチジェスチャーを扱うための標準的なライブラリとして広く採用されています。しかし、多様なデバイス環境下では、ジェスチャーの認識失敗や予期せぬ動作がユーザー体験を損なう重大な要因となり得ます。本稿では、本番環境において Hammer.js の動作異常を効果的に検知し、開発チームへ報告するための監視アーキテクチャについて解説します。

1. 発生しうる障害ポイントの特定

Hammer.js の実行フローにおいて、エラーが発生しやすい主要な箇所は以下の通りです。

1.1 インスタンス生成時の設定検証

Hammer マネージャーの構築時に不適切なオプションが渡されると、即時に例外が発生します。特にカスタム認識子(Recognizer)の名前衝突や、数値オプションへの不正な型指定が原因となることが多いです。コンストラクタ呼び出しをラップし、例外を捕捉する仕組みが必要です。

function createSafeHammer(targetElement, config) {
  try {
    const manager = new Hammer(targetElement, config);
    return manager;
  } catch (exception) {
    sendDiagnostic({
      category: 'constructor_failure',
      reason: exception.message,
      trace: exception.stack,
      configSnapshot: JSON.stringify(config)
    });
    return null;
  }
}

1.2 認識子間の競合状態

複数のジェスチャー認識子(例:pan と swipe)を同時に有効化した場合、優先順位の設定ミスによりイベントが発火しないケースがあります。内部の tryEmit ロジックにおいて、状態遷移が期待通りに行われているかを追跡する必要があります。

1.3 デバイス固有の入力互換性

タッチイベントの実装はブラウザや OS によって差異があります。Hammer.js の入力モジュール(touch.js, mouse.js 等)が想定外のイベントフォーマットを受け取った際、処理が停止する可能性があります。

2. 例外捕捉と監視アーキテクチャ

エラーを見逃さないためには、アプリケーション全体をカバーする捕捉機制を構築します。

2.1 グローバルエラーリスナーの登録

ブラウザの標準エラーイベントを監視し、スタックトレースに Hammer.js 関連のファイルが含まれているかをフィルタリングします。

window.addEventListener('error', (event) => {
  const stackTrace = event.error?.stack || '';
  if (stackTrace.includes('hammer') || event.filename?.includes('hammer')) {
    notifyMonitoringService({
      level: 'critical',
      source: 'global_handler',
      details: {
        msg: event.message,
        file: event.filename,
        line: event.lineno,
        column: event.colno
      }
    });
  }
});

2.2 イベントハンドラ内の安全圏確保

個々のジェスチャーイベントリスナー内部で発生したエラーがアプリケーション全体を停止させないよう、try-catch ブロックで囲みます。

manager.on('tap press', (evt) => {
  try {
    processUserInteraction(evt);
  } catch (err) {
    notifyMonitoringService({
      level: 'warning',
      source: 'gesture_callback',
      gestureType: evt.type,
      errorInfo: {
        message: err.message,
        pointers: evt.pointers.length
      }
    });
  }
});

2.3 認識子状態のフック

認識子の状態遷移メソッドをオーバーライドし、不正な状態変化(例:失敗状態から認識完了状態への直接遷移)を検知します。

const originalStateChange = Hammer.Recognizer.prototype.setState;
Hammer.Recognizer.prototype.setState = function(newState) {
  if (this.state === Hammer.STATE_FAILED && newState === Hammer.STATE_RECOGNIZED) {
    notifyMonitoringService({
      level: 'info',
      source: 'state_machine',
      recognizerName: this.options.name,
      invalidTransition: true
    });
  }
  return originalStateChange.apply(this, arguments);
};

3. 診断データの構造と送信最適化

エラー報告時には、再現に必要なコンテキスト情報を付与し、ネットワーク負荷を考慮した送信制御を行います。

3.1 報告データスキーマ

エラー ID、発生時刻、タイプに加え、デバイス情報やジェスチャーの詳細パラメータを含めます。

{
  "diagId": "uuid-v4-string",
  "occurredAt": 1678888888888,
  "severity": "warning",
  "description": "Undefined property access in handler",
  "environment": {
    "ua": "Mozilla/5.0...",
    "viewport": "390x844",
    "libVersion": "2.0.8"
  },
  "gestureContext": {
    "type": "swipe",
    "direction": "LEFT",
    "velocity": 1.5
  }
}

3.2 キューイングとバッチ送信

頻繁なエラー発生時にサーバーへ負荷をかけないよう、ローカルキューに蓄積し、一定数または一定時間ごとにまとめて送信します。

const diagnosticQueue = [];
const QUEUE_LIMIT = 15;
const FLUSH_DELAY_MS = 20000;

function queueDiagnostic(data) {
  diagnosticQueue.push(data);
  if (diagnosticQueue.length >= QUEUE_LIMIT) {
    executeFlush();
  }
}

function executeFlush() {
  if (diagnosticQueue.length === 0) return;
  
  const payload = JSON.stringify(diagnosticQueue);
  diagnosticQueue.length = 0;

  navigator.sendBeacon('/api/diagnostics', payload) || 
  fetch('/api/diagnostics', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: payload,
    keepalive: true
  });
}

setTimeout(function periodicFlush() {
  executeFlush();
  setTimeout(periodicFlush, FLUSH_DELAY_MS);
}, FLUSH_DELAY_MS);

4. 運用上の考慮事項

監視システム自体がパフォーマンスに悪影響を与えないよう、環境に応じた制御を行います。

4.1 環境別ロギングレベル

開発環境ではコンソール出力を行い、本番環境でのみサーバー送信を行うように分岐します。

const isProd = window.location.hostname === 'example.com';

function notifyMonitoringService(data) {
  if (!isProd) {
    console.warn('[GestureDiag]', data);
    return;
  }
  queueDiagnostic(data);
}

4.2 ユーザー操作履歴の記録

エラー発生直前の操作序列を記録することで、再現手順の特定を容易にします。

const actionLog = [];
const MAX_LOG_SIZE = 10;

manager.on('*', (ev) => {
  actionLog.push({
    event: ev.type,
    timestamp: Date.now(),
    delta: { x: ev.deltaX, y: ev.deltaY }
  });
  if (actionLog.length > MAX_LOG_SIZE) {
    actionLog.shift();
  }
});

// エラー報告時に actionLog を添付
function notifyMonitoringService(data) {
  data.recentActions = [...actionLog];
  // ... 送信処理
}

4.3 送信手段の選定

ページアンロード時でもデータ消失を防ぐため、可能な限り Beacon API を利用します。

5. トラブルシューティング事例

実際の監視データに基づいた問題解決の事例を示します。

5.1 特定端末でのパンイベント未発火

現象: Android 端末の一部で水平スワイプが認識されない。

分析: 報告データより、移動距離がデフォルト閾値に達していないことが判明。

対策: 認識子の閾値を環境に合わせて調整します。

const panRecognizer = manager.get('pan');
panRecognizer.set({
  threshold: 8, // 閾値を緩和
  direction: Hammer.DIRECTION_HORIZONTAL
});

5.2 ピンチ操作中のスケール値異常

現象: 2 指ズーム時に拡大率が NaN になり表示が崩れる。

分析: 距離計算処理において、前フレームの距離が 0 で除算エラーが発生していた。

対策: 距離計算ロジックにゼロ除算ガードを追加します。

function calculateScale(currentDist, prevDist) {
  if (prevDist === 0 || !isFinite(prevDist)) {
    return 1;
  }
  return currentDist / prevDist;
}

6. 監視システムの拡張可能性

基本的なエラー報告に加え、以下の機能を追加することでさらに深い洞察を得ることができます。

  • 操作ヒートマップの可視化: エラーが多発する UI 領域を特定するため、ジェスチャー発生座標を蓄積し可視化します。
  • 自動クラスタリング: 類似したスタックトレースを持つエラーをグループ化し、優先度付けを行います。
  • しきい値アラート: 単位時間あたりのエラー率が規定値を超えた場合、開発チームへ即時通知を送付します。

これらの仕組みを導入することで、タッチインタラクションの品質を定量的に評価し、継続的な改善サイクルを確立することが可能になります。

タグ: Hammer.js javascript ErrorMonitoring TouchEvents WebPerformance

5月18日 01:07 投稿