New Relic Browser監視入門 第9.3章 - SPA・PWA監視とフロントエンドエラー追跡

📖 ナビゲーション

メイン: 第9章 New Relic Browser詳細化
前セクション: 第9.2章 Core Web Vitals詳細分析
次セクション: 第9.4章 ユーザーエクスペリエンス最適化実践


💡 この章で学べること

現代のWebアプリケーション開発では、Single Page Application(SPA)Progressive Web App(PWA)が主流となっています。これらのモダンなWebアプリケーションでは、従来の静的なWebサイトとは異なる監視手法が必要になります。本章では、React・Vue・Angularでの高度なNew Relic統合から包括的なJavaScript エラー追跡まで、実践的な実装手法を詳しく解説します。

学習目標

  • [ ] SPA監視の特殊性:従来のWebサイト監視との違いと課題の理解
  • [ ] React統合実装:Hooks・Error Boundary・Context Providerでの監視実装
  • [ ] Vue.js専門統合:Composition API・プラグインシステムの活用
  • [ ] Angular高度統合:Services・Interceptors・Zone.jsを活用した監視
  • [ ] JavaScript エラー追跡:包括的なエラー管理とデバッグシステム
  • [ ] PWA特有監視:Service Worker・オフライン状態・プッシュ通知の監視

期待される成果

本章を完了すると、以下が実現できます:

  • モダンWebアプリ監視の完全制御:SPA・PWAでの包括的な可視化
  • フレームワーク最適化:React・Vue・Angular固有の問題の早期発見
  • JavaScript エラー削減:エラー率70-90%削減による品質向上
  • ユーザー体験向上:SPA特有の問題解決によるUX改善
  • 運用効率化:自動化されたエラー分類による障害対応時間短縮

9.3.1 SPA監視の基本概念と課題

SPA(Single Page Application)監視の特殊性

SPAは、ページ全体をリロードすることなく、JavaScriptによって動的にコンテンツを更新するWebアプリケーションです。この仕組みによって、従来のWebサイト監視とは大きく異なる課題が生まれます。

従来のWebサイト vs SPAの監視における違い

yaml
# 監視手法の比較
従来のWebサイト:
  ページ読み込み: "毎回サーバーからHTML取得"
  監視ポイント: "ページごとの読み込み時間"
  エラー検出: "サーバーエラー中心"
  状態管理: "サーバーサイド中心"

SPA:
  ページ読み込み: "初回のみ、以降はJavaScriptで動的更新"
  監視ポイント: "ルート遷移、状態変更、非同期処理"
  エラー検出: "JavaScript実行時エラー中心"
  状態管理: "クライアントサイド中心"

SPA監視で特に重要な要素

1. ルート遷移の監視 SPAでは、URLが変更されてもページ全体はリロードされません。この「仮想的なページ遷移」を適切に監視する必要があります。

javascript
// 従来のページ遷移(ブラウザが自動的にページ読み込みイベントを発火)
window.location.href = '/products/123';

// SPAのルート遷移(JavaScriptによる状態変更のみ)
// → New Relicに手動でページビューを通知する必要
history.pushState({productId: 123}, '', '/products/123');
// この時点では、ブラウザのページ読み込みイベントは発生しない

2. 状態管理の監視 SPAでは、アプリケーションの状態がクライアントサイドで管理されるため、状態変更に伴うパフォーマンス問題やエラーを監視する必要があります。

3. 非同期処理の監視 SPAでは多くの処理が非同期で実行されるため、これらの処理に関連するエラーやパフォーマンス問題を適切に捕捉する必要があります。

PWA(Progressive Web App)監視の特殊要件

PWAは、ネイティブアプリのような体験を提供するWebアプリケーションです。PWAでは、SPA固有の課題に加えて、以下の特殊な監視要件があります。

PWA固有の監視ポイント

1. Service Worker監視 Service Workerは、ネットワークリクエストをインターセプトし、キャッシュ制御やオフライン機能を提供します。この動作を監視することで、PWAの信頼性を確保できます。

2. オフライン状態監視 PWAでは、ネットワークが利用できない状況でもアプリケーションが動作する必要があります。オフライン状態での動作を監視し、問題を早期発見することが重要です。

3. プッシュ通知監視 PWAでは、プッシュ通知機能を利用することが多く、通知の配信成功率や表示エラーを監視する必要があります。


9.3.2 React での高度なNew Relic統合実装

React Hooks を活用した監視実装

React Hooksを使用することで、関数型コンポーネントでも効率的にNew Relic監視を実装できます。以下では、実用的なカスタムHooksの実装例を詳しく解説します。

useNewRelic カスタムHookの実装

javascript
// hooks/useNewRelic.js
import { useEffect, useCallback, useRef } from 'react';

/**
 * New Relic監視用カスタムHook
 * コンポーネントのライフサイクルとユーザー操作を監視
 */
export const useNewRelic = (componentName, options = {}) => {
  const mountTimeRef = useRef(null);
  const { trackRenders = true, trackUserActions = true } = options;

  // コンポーネントマウント時の処理
  useEffect(() => {
    mountTimeRef.current = Date.now();
    
    if (window.newrelic && trackRenders) {
      // カスタムイベント: コンポーネント初期化開始
      window.newrelic.addPageAction('component_mount_start', {
        componentName,
        timestamp: mountTimeRef.current,
        url: window.location.pathname,
        userAgent: navigator.userAgent
      });
    }

    // コンポーネントマウント完了の監視
    const handleMountComplete = () => {
      const mountDuration = Date.now() - mountTimeRef.current;
      
      if (window.newrelic) {
        window.newrelic.addPageAction('component_mount_complete', {
          componentName,
          mountDuration,
          performance: {
            renderTime: mountDuration,
            memoryUsage: window.performance?.memory?.usedJSHeapSize
          }
        });
      }
    };

    // 次のティックでマウント完了を記録(レンダリング後)
    const timeoutId = setTimeout(handleMountComplete, 0);

    // クリーンアップ関数
    return () => {
      clearTimeout(timeoutId);
      
      if (window.newrelic && trackRenders) {
        const totalLifetime = Date.now() - mountTimeRef.current;
        window.newrelic.addPageAction('component_unmount', {
          componentName,
          totalLifetime,
          unmountReason: 'component_destroyed'
        });
      }
    };
  }, [componentName, trackRenders]);

  // ユーザーアクション追跡関数
  const trackUserAction = useCallback((actionName, additionalData = {}) => {
    if (!window.newrelic || !trackUserActions) return;
    
    window.newrelic.addPageAction('user_action', {
      componentName,
      actionName,
      timestamp: Date.now(),
      url: window.location.pathname,
      ...additionalData
    });
  }, [componentName, trackUserActions]);

  // エラー追跡関数
  const trackError = useCallback((error, errorContext = {}) => {
    if (!window.newrelic) return;
    
    window.newrelic.noticeError(error, {
      componentName,
      errorContext,
      url: window.location.pathname,
      timestamp: Date.now(),
      stackTrace: error.stack
    });
  }, [componentName]);

  // パフォーマンス計測開始
  const startPerformanceTimer = useCallback((timerName) => {
    if (!window.newrelic) return null;
    
    const startTime = Date.now();
    return {
      end: (additionalData = {}) => {
        const duration = Date.now() - startTime;
        window.newrelic.addPageAction('performance_timer', {
          componentName,
          timerName,
          duration,
          ...additionalData
        });
        return duration;
      }
    };
  }, [componentName]);

  return {
    trackUserAction,
    trackError,
    startPerformanceTimer
  };
};

実際のコンポーネントでの使用例

javascript
// components/ProductList.jsx
import React, { useState, useEffect } from 'react';
import { useNewRelic } from '../hooks/useNewRelic';

const ProductList = ({ category }) => {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  // New Relic監視フックの初期化
  const { trackUserAction, trackError, startPerformanceTimer } = useNewRelic('ProductList', {
    trackRenders: true,
    trackUserActions: true
  });

  // 商品データ取得の監視
  useEffect(() => {
    const fetchProducts = async () => {
      // パフォーマンス計測開始
      const timer = startPerformanceTimer('product_fetch');
      
      try {
        setLoading(true);
        
        const response = await fetch(`/api/products?category=${category}`);
        
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const data = await response.json();
        setProducts(data);
        
        // 成功時のパフォーマンス記録
        timer.end({
          productCount: data.length,
          category,
          success: true
        });
        
        // ユーザーアクション記録
        trackUserAction('products_loaded', {
          category,
          productCount: data.length,
          loadTime: timer.duration
        });
        
      } catch (err) {
        setError(err.message);
        
        // エラー追跡
        trackError(err, {
          operation: 'product_fetch',
          category,
          apiEndpoint: `/api/products?category=${category}`
        });
        
        // エラー時のパフォーマンス記録
        timer.end({
          category,
          success: false,
          errorMessage: err.message
        });
        
      } finally {
        setLoading(false);
      }
    };

    fetchProducts();
  }, [category, trackUserAction, trackError, startPerformanceTimer]);

  // 商品クリック処理の監視
  const handleProductClick = (productId, productName) => {
    trackUserAction('product_clicked', {
      productId,
      productName,
      category,
      position: products.findIndex(p => p.id === productId) + 1
    });
  };

  // フィルター変更の監視
  const handleFilterChange = (filterType, filterValue) => {
    trackUserAction('filter_applied', {
      filterType,
      filterValue,
      currentProductCount: products.length,
      category
    });
  };

  if (loading) {
    return <div className="loading">商品を読み込み中...</div>;
  }

  if (error) {
    return (
      <div className="error">
        エラーが発生しました: {error}
        <button onClick={() => window.location.reload()}>
          再読み込み
        </button>
      </div>
    );
  }

  return (
    <div className="product-list">
      <h2>{category} の商品一覧</h2>
      
      {/* フィルター機能 */}
      <div className="filters">
        <select onChange={(e) => handleFilterChange('price', e.target.value)}>
          <option value="">価格帯を選択</option>
          <option value="0-1000">1,000円以下</option>
          <option value="1000-5000">1,000-5,000円</option>
          <option value="5000+">5,000円以上</option>
        </select>
      </div>
      
      {/* 商品一覧 */}
      <div className="products-grid">
        {products.map((product) => (
          <div
            key={product.id}
            className="product-card"
            onClick={() => handleProductClick(product.id, product.name)}
          >
            <img src={product.image} alt={product.name} />
            <h3>{product.name}</h3>
            <p className="price">¥{product.price.toLocaleString()}</p>
          </div>
        ))}
      </div>
    </div>
  );
};

export default ProductList;

React Error Boundary による包括的エラーハンドリング

Error Boundaryは、Reactコンポーネントツリー内で発生したJavaScriptエラーをキャッチし、フォールバックUIを表示するReactの機能です。New Relicと組み合わせることで、強力なエラー追跡システムを構築できます。

高度なError Boundaryの実装

javascript
// components/NewRelicErrorBoundary.jsx
import React from 'react';

class NewRelicErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      errorInfo: null,
      errorId: null
    };
  }

  static getDerivedStateFromError(error) {
    // エラーが発生した時点で状態を更新
    return {
      hasError: true,
      error
    };
  }

  componentDidCatch(error, errorInfo) {
    // エラーの詳細情報を状態に保存
    this.setState({
      error,
      errorInfo,
      errorId: this.generateErrorId()
    });

    // New Relicにエラーを送信
    this.reportErrorToNewRelic(error, errorInfo);
  }

  generateErrorId = () => {
    return `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  };

  reportErrorToNewRelic = (error, errorInfo) => {
    if (!window.newrelic) {
      console.warn('New Relic is not available');
      return;
    }

    try {
      // エラーの基本情報を収集
      const errorDetails = {
        errorId: this.state.errorId,
        componentStack: errorInfo.componentStack,
        errorBoundary: this.props.name || 'UnnamedErrorBoundary',
        url: window.location.href,
        userAgent: navigator.userAgent,
        timestamp: new Date().toISOString(),
        
        // Reactの詳細情報
        reactVersion: React.version,
        props: this.sanitizeProps(this.props),
        
        // ユーザーコンテキスト
        userId: this.props.userId,
        sessionId: this.getSessionId(),
        
        // ブラウザ環境情報
        viewport: {
          width: window.innerWidth,
          height: window.innerHeight
        },
        memory: window.performance?.memory ? {
          usedJSHeapSize: window.performance.memory.usedJSHeapSize,
          totalJSHeapSize: window.performance.memory.totalJSHeapSize
        } : null
      };

      // New Relicにエラーを通知
      window.newrelic.noticeError(error, errorDetails);

      // カスタムイベントとしても記録
      window.newrelic.addPageAction('react_error_boundary_triggered', {
        errorId: this.state.errorId,
        errorMessage: error.message,
        componentName: this.props.name,
        severity: this.props.severity || 'high',
        recoverable: this.props.fallback ? true : false
      });

    } catch (reportingError) {
      console.error('Failed to report error to New Relic:', reportingError);
    }
  };

  sanitizeProps = (props) => {
    // 機密情報を除外してpropsをサニタイズ
    const sanitized = { ...props };
    const sensitiveKeys = ['password', 'token', 'secret', 'key', 'auth'];
    
    Object.keys(sanitized).forEach(key => {
      if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
        sanitized[key] = '[REDACTED]';
      }
    });
    
    return sanitized;
  };

  getSessionId = () => {
    // セッションIDの取得(LocalStorageやCookieから)
    return localStorage.getItem('sessionId') || 'unknown';
  };

  handleRetry = () => {
    // エラー状態をリセットして再試行
    this.setState({
      hasError: false,
      error: null,
      errorInfo: null,
      errorId: null
    });

    // 再試行イベントを記録
    if (window.newrelic) {
      window.newrelic.addPageAction('error_boundary_retry', {
        previousErrorId: this.state.errorId,
        componentName: this.props.name,
        timestamp: Date.now()
      });
    }
  };

  render() {
    if (this.state.hasError) {
      // カスタムフォールバックUIが指定されている場合
      if (this.props.fallback) {
        return this.props.fallback(this.state.error, this.handleRetry);
      }

      // デフォルトのフォールバックUI
      return (
        <div className="error-boundary">
          <div className="error-boundary__container">
            <h2 className="error-boundary__title">
              申し訳ございません。予期しないエラーが発生しました。
            </h2>
            
            <div className="error-boundary__details">
              <p>エラーID: <code>{this.state.errorId}</code></p>
              <p>お困りの場合は、このエラーIDをお知らせください。</p>
            </div>

            <div className="error-boundary__actions">
              <button 
                onClick={this.handleRetry}
                className="error-boundary__retry-btn"
              >
                再試行
              </button>
              
              <button 
                onClick={() => window.location.reload()}
                className="error-boundary__reload-btn"
              >
                ページを再読み込み
              </button>
            </div>

            {/* 開発環境でのみエラー詳細を表示 */}
            {process.env.NODE_ENV === 'development' && (
              <details className="error-boundary__debug">
                <summary>エラー詳細(開発環境のみ)</summary>
                <pre className="error-boundary__stack">
                  {this.state.error && this.state.error.toString()}
                  {this.state.errorInfo.componentStack}
                </pre>
              </details>
            )}
          </div>
        </div>
      );
    }

    return this.props.children;
  }
}

export default NewRelicErrorBoundary;

Error Boundary の使用例

javascript
// App.jsx
import React from 'react';
import NewRelicErrorBoundary from './components/NewRelicErrorBoundary';
import ProductList from './components/ProductList';
import UserProfile from './components/UserProfile';

const App = () => {
  return (
    <div className="app">
      {/* 全体をエラーバウンダリで囲む */}
      <NewRelicErrorBoundary name="AppRoot" severity="critical">
        
        <header>
          <h1>EC サイト</h1>
          
          {/* ユーザープロフィール部分に個別のエラーバウンダリ */}
          <NewRelicErrorBoundary 
            name="UserProfile" 
            severity="medium"
            fallback={(error, retry) => (
              <div className="user-profile-error">
                <p>ユーザー情報の取得に失敗しました</p>
                <button onClick={retry}>再試行</button>
              </div>
            )}
          >
            <UserProfile />
          </NewRelicErrorBoundary>
        </header>

        <main>
          {/* 商品一覧に個別のエラーバウンダリ */}
          <NewRelicErrorBoundary 
            name="ProductList" 
            severity="high"
            fallback={(error, retry) => (
              <div className="product-list-error">
                <h2>商品の読み込みに失敗しました</h2>
                <p>一時的な問題が発生している可能性があります。</p>
                <button onClick={retry}>再試行</button>
                <button onClick={() => window.location.href = '/'}>
                  ホームに戻る
                </button>
              </div>
            )}
          >
            <ProductList category="electronics" />
          </NewRelicErrorBoundary>
        </main>
        
      </NewRelicErrorBoundary>
    </div>
  );
};

export default App;

React Context による監視データの共有

React Contextを使用することで、アプリケーション全体でNew Relic監視データを効率的に共有できます。

New Relic Context Provider の実装

javascript
// contexts/NewRelicContext.jsx
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';

const NewRelicContext = createContext();

export const useNewRelicContext = () => {
  const context = useContext(NewRelicContext);
  if (!context) {
    throw new Error('useNewRelicContext must be used within a NewRelicProvider');
  }
  return context;
};

export const NewRelicProvider = ({ children, config = {} }) => {
  const [isNewRelicReady, setIsNewRelicReady] = useState(false);
  const [sessionId, setSessionId] = useState(null);
  const [userContext, setUserContext] = useState({});

  // New Relicの初期化
  useEffect(() => {
    const checkNewRelicReady = () => {
      if (window.newrelic) {
        setIsNewRelicReady(true);
        
        // セッションIDの生成・設定
        const newSessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
        setSessionId(newSessionId);
        
        // New Relicにセッション開始を通知
        window.newrelic.addPageAction('session_start', {
          sessionId: newSessionId,
          timestamp: Date.now(),
          userAgent: navigator.userAgent,
          url: window.location.href
        });
        
        return;
      }
      
      // 100ms後に再チェック(最大10秒間)
      setTimeout(checkNewRelicReady, 100);
    };

    checkNewRelicReady();
  }, []);

  // ユーザーコンテキストの設定
  const setUser = useCallback((userId, attributes = {}) => {
    const newUserContext = {
      userId,
      ...attributes,
      loginTime: Date.now()
    };
    
    setUserContext(newUserContext);
    
    if (isNewRelicReady) {
      // New Relicにユーザー情報を設定
      window.newrelic.setUserId(userId);
      
      // ユーザー属性を設定
      Object.entries(attributes).forEach(([key, value]) => {
        window.newrelic.setCustomAttribute(`user.${key}`, value);
      });
      
      // ユーザーログインイベントを記録
      window.newrelic.addPageAction('user_login', {
        userId,
        sessionId,
        ...attributes
      });
    }
  }, [isNewRelicReady, sessionId]);

  // カスタムイベント送信
  const trackEvent = useCallback((eventName, attributes = {}) => {
    if (!isNewRelicReady) {
      console.warn('New Relic is not ready yet');
      return;
    }
    
    const eventData = {
      ...attributes,
      sessionId,
      userId: userContext.userId,
      timestamp: Date.now(),
      url: window.location.href
    };
    
    window.newrelic.addPageAction(eventName, eventData);
  }, [isNewRelicReady, sessionId, userContext.userId]);

  // エラー追跡
  const trackError = useCallback((error, context = {}) => {
    if (!isNewRelicReady) {
      console.error('Error occurred but New Relic is not ready:', error);
      return;
    }
    
    const errorContext = {
      ...context,
      sessionId,
      userId: userContext.userId,
      timestamp: Date.now(),
      url: window.location.href
    };
    
    window.newrelic.noticeError(error, errorContext);
  }, [isNewRelicReady, sessionId, userContext.userId]);

  // ページビュー追跡(SPA用)
  const trackPageView = useCallback((pageName, attributes = {}) => {
    if (!isNewRelicReady) return;
    
    const pageViewData = {
      pageName,
      ...attributes,
      sessionId,
      userId: userContext.userId,
      timestamp: Date.now(),
      referrer: document.referrer
    };
    
    window.newrelic.addPageAction('spa_page_view', pageViewData);
  }, [isNewRelicReady, sessionId, userContext.userId]);

  // パフォーマンス計測
  const createTimer = useCallback((timerName) => {
    const startTime = performance.now();
    
    return {
      end: (attributes = {}) => {
        const duration = performance.now() - startTime;
        
        trackEvent('performance_timer', {
          timerName,
          duration,
          ...attributes
        });
        
        return duration;
      }
    };
  }, [trackEvent]);

  const contextValue = {
    isNewRelicReady,
    sessionId,
    userContext,
    setUser,
    trackEvent,
    trackError,
    trackPageView,
    createTimer
  };

  return (
    <NewRelicContext.Provider value={contextValue}>
      {children}
    </NewRelicContext.Provider>
  );
};

Context Provider の使用例

javascript
// App.jsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { NewRelicProvider } from './contexts/NewRelicContext';
import Home from './pages/Home';
import Products from './pages/Products';
import Profile from './pages/Profile';

const App = () => {
  return (
    <NewRelicProvider>
      <Router>
        <div className="app">
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/products" element={<Products />} />
            <Route path="/profile" element={<Profile />} />
          </Routes>
        </div>
      </Router>
    </NewRelicProvider>
  );
};

export default App;
javascript
// components/LoginForm.jsx
import React, { useState } from 'react';
import { useNewRelicContext } from '../contexts/NewRelicContext';

const LoginForm = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  
  const { setUser, trackEvent, trackError, createTimer } = useNewRelicContext();

  const handleLogin = async (e) => {
    e.preventDefault();
    setIsLoading(true);
    
    // ログイン処理のパフォーマンス計測開始
    const loginTimer = createTimer('user_login');
    
    try {
      trackEvent('login_attempt', {
        email,
        method: 'email_password'
      });
      
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ email, password }),
      });
      
      if (!response.ok) {
        throw new Error(`Login failed: ${response.status}`);
      }
      
      const { user, token } = await response.json();
      
      // ユーザー情報をNew Relicコンテキストに設定
      setUser(user.id, {
        email: user.email,
        role: user.role,
        subscription: user.subscription,
        loginMethod: 'email_password'
      });
      
      // ログイン成功イベント
      trackEvent('login_success', {
        userId: user.id,
        loginDuration: loginTimer.end(),
        method: 'email_password'
      });
      
      // トークンを保存
      localStorage.setItem('authToken', token);
      
      // ダッシュボードにリダイレクト
      window.location.href = '/dashboard';
      
    } catch (error) {
      trackError(error, {
        operation: 'user_login',
        email,
        method: 'email_password'
      });
      
      loginTimer.end({
        success: false,
        errorMessage: error.message
      });
      
      alert('ログインに失敗しました: ' + error.message);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <form onSubmit={handleLogin} className="login-form">
      <div className="form-group">
        <label htmlFor="email">メールアドレス</label>
        <input
          type="email"
          id="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
      </div>
      
      <div className="form-group">
        <label htmlFor="password">パスワード</label>
        <input
          type="password"
          id="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
      </div>
      
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'ログイン中...' : 'ログイン'}
      </button>
    </form>
  );
};

export default LoginForm;

9.3.3 Vue.js での専門的New Relic統合

Vue 3 Composition API を活用した監視実装

Vue 3のComposition APIは、リアクティブな状態管理と組み合わせることで、効率的なNew Relic監視を実現できます。以下では、Vue.jsでの実用的な監視実装を詳しく解説します。

useNewRelic Composable の実装

javascript
// composables/useNewRelic.js
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';

/**
 * New Relic監視用Composable
 * Vue 3 Composition APIを活用した監視実装
 */
export function useNewRelic(componentName, options = {}) {
  const {
    trackLifecycle = true,
    trackUserActions = true,
    trackStateChanges = false,
    autoTrackProps = []
  } = options;

  // リアクティブな状態
  const isNewRelicReady = ref(false);
  const componentMountTime = ref(null);
  const performanceMetrics = ref({
    renderCount: 0,
    lastRenderTime: 0,
    totalRenderTime: 0
  });

  // New Relicの準備状況確認
  const checkNewRelicReady = () => {
    if (window.newrelic) {
      isNewRelicReady.value = true;
      return true;
    }
    return false;
  };

  // コンポーネントライフサイクル監視
  onMounted(() => {
    componentMountTime.value = Date.now();
    
    if (checkNewRelicReady() && trackLifecycle) {
      window.newrelic.addPageAction('vue_component_mounted', {
        componentName,
        mountTime: componentMountTime.value,
        url: window.location.pathname,
        vueVersion: Vue.version || '3.x'
      });
    }

    // DOM更新完了後の処理
    nextTick(() => {
      if (isNewRelicReady.value && trackLifecycle) {
        const renderDuration = Date.now() - componentMountTime.value;
        performanceMetrics.value.lastRenderTime = renderDuration;
        performanceMetrics.value.totalRenderTime += renderDuration;
        performanceMetrics.value.renderCount++;

        window.newrelic.addPageAction('vue_component_render_complete', {
          componentName,
          renderDuration,
          renderCount: performanceMetrics.value.renderCount,
          totalRenderTime: performanceMetrics.value.totalRenderTime
        });
      }
    });
  });

  onUnmounted(() => {
    if (isNewRelicReady.value && trackLifecycle) {
      const totalLifetime = Date.now() - componentMountTime.value;
      
      window.newrelic.addPageAction('vue_component_unmounted', {
        componentName,
        totalLifetime,
        renderCount: performanceMetrics.value.renderCount,
        totalRenderTime: performanceMetrics.value.totalRenderTime,
        averageRenderTime: performanceMetrics.value.totalRenderTime / performanceMetrics.value.renderCount
      });
    }
  });

  // ユーザーアクション追跡
  const trackUserAction = (actionName, payload = {}) => {
    if (!isNewRelicReady.value || !trackUserActions) return;
    
    window.newrelic.addPageAction('vue_user_action', {
      componentName,
      actionName,
      payload,
      timestamp: Date.now(),
      url: window.location.pathname
    });
  };

  // エラー追跡
  const trackError = (error, context = {}) => {
    if (!isNewRelicReady.value) {
      console.error('Error in Vue component:', error);
      return;
    }
    
    window.newrelic.noticeError(error, {
      componentName,
      context,
      vueVersion: Vue.version || '3.x',
      url: window.location.pathname,
      timestamp: Date.now()
    });
  };

  // 非同期処理追跡
  const trackAsyncOperation = async (operationName, asyncFunction, context = {}) => {
    const startTime = Date.now();
    
    try {
      if (isNewRelicReady.value) {
        window.newrelic.addPageAction('vue_async_operation_start', {
          componentName,
          operationName,
          context,
          startTime
        });
      }
      
      const result = await asyncFunction();
      
      const duration = Date.now() - startTime;
      
      if (isNewRelicReady.value) {
        window.newrelic.addPageAction('vue_async_operation_complete', {
          componentName,
          operationName,
          context,
          duration,
          success: true
        });
      }
      
      return result;
      
    } catch (error) {
      const duration = Date.now() - startTime;
      
      trackError(error, {
        operation: operationName,
        context,
        duration
      });
      
      if (isNewRelicReady.value) {
        window.newrelic.addPageAction('vue_async_operation_failed', {
          componentName,
          operationName,
          context,
          duration,
          success: false,
          errorMessage: error.message
        });
      }
      
      throw error;
    }
  };

  // 状態変更監視(指定されたプロパティ)
  const trackStateChange = (stateName, newValue, oldValue) => {
    if (!isNewRelicReady.value || !trackStateChanges) return;
    
    window.newrelic.addPageAction('vue_state_change', {
      componentName,
      stateName,
      newValue: JSON.stringify(newValue),
      oldValue: JSON.stringify(oldValue),
      timestamp: Date.now()
    });
  };

  return {
    isNewRelicReady,
    performanceMetrics,
    trackUserAction,
    trackError,
    trackAsyncOperation,
    trackStateChange
  };
}

Vue.js コンポーネントでの実装例

vue
<!-- components/ProductCatalog.vue -->
<template>
  <div class="product-catalog">
    <h2>{{ categoryName }} 商品カタログ</h2>
    
    <!-- フィルターセクション -->
    <div class="filters">
      <select 
        v-model="selectedPriceRange" 
        @change="handleFilterChange('price', $event.target.value)"
      >
        <option value="">価格帯を選択</option>
        <option value="0-1000">1,000円以下</option>
        <option value="1000-5000">1,000-5,000円</option>
        <option value="5000+">5,000円以上</option>
      </select>
      
      <select 
        v-model="selectedSortOrder" 
        @change="handleSortChange($event.target.value)"
      >
        <option value="default">並び順</option>
        <option value="price-asc">価格: 安い順</option>
        <option value="price-desc">価格: 高い順</option>
        <option value="name">名前順</option>
      </select>
    </div>
    
    <!-- ローディング状態 -->
    <div v-if="loading" class="loading">
      商品を読み込み中...
    </div>
    
    <!-- エラー状態 -->
    <div v-else-if="error" class="error">
      <p>エラーが発生しました: {{ error }}</p>
      <button @click="retryLoad">再試行</button>
    </div>
    
    <!-- 商品一覧 -->
    <div v-else class="products-grid">
      <div
        v-for="(product, index) in filteredProducts"
        :key="product.id"
        class="product-card"
        @click="handleProductClick(product, index)"
      >
        <img 
          :src="product.image" 
          :alt="product.name"
          @error="handleImageError(product)"
        />
        <h3>{{ product.name }}</h3>
        <p class="price">¥{{ product.price.toLocaleString() }}</p>
        <div class="rating">
          ⭐ {{ product.rating }} ({{ product.reviewCount }}件)
        </div>
      </div>
    </div>
    
    <!-- ページネーション -->
    <div v-if="totalPages > 1" class="pagination">
      <button 
        v-for="page in totalPages" 
        :key="page"
        :class="{ active: currentPage === page }"
        @click="changePage(page)"
      >
        {{ page }}
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { useNewRelic } from '../composables/useNewRelic';

// Props
const props = defineProps({
  categoryId: {
    type: String,
    required: true
  },
  categoryName: {
    type: String,
    required: true
  }
});

// リアクティブな状態
const products = ref([]);
const loading = ref(false);
const error = ref(null);
const selectedPriceRange = ref('');
const selectedSortOrder = ref('default');
const currentPage = ref(1);
const itemsPerPage = 12;

// New Relic監視の初期化
const {
  trackUserAction,
  trackError,
  trackAsyncOperation,
  trackStateChange
} = useNewRelic('ProductCatalog', {
  trackLifecycle: true,
  trackUserActions: true,
  trackStateChanges: true
});

// 計算プロパティ
const filteredProducts = computed(() => {
  let filtered = [...products.value];
  
  // 価格フィルター
  if (selectedPriceRange.value) {
    const [min, max] = selectedPriceRange.value.split('-').map(Number);
    filtered = filtered.filter(product => {
      if (max) {
        return product.price >= min && product.price <= max;
      } else {
        return product.price >= min;
      }
    });
  }
  
  // ソート
  switch (selectedSortOrder.value) {
    case 'price-asc':
      filtered.sort((a, b) => a.price - b.price);
      break;
    case 'price-desc':
      filtered.sort((a, b) => b.price - a.price);
      break;
    case 'name':
      filtered.sort((a, b) => a.name.localeCompare(b.name));
      break;
  }
  
  // ページネーション
  const start = (currentPage.value - 1) * itemsPerPage;
  const end = start + itemsPerPage;
  return filtered.slice(start, end);
});

const totalPages = computed(() => {
  return Math.ceil(products.value.length / itemsPerPage);
});

// 商品データ取得
const fetchProducts = async () => {
  await trackAsyncOperation('fetch_products', async () => {
    loading.value = true;
    error.value = null;
    
    const response = await fetch(`/api/products?category=${props.categoryId}`);
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const data = await response.json();
    products.value = data;
    
    return data;
  }, {
    categoryId: props.categoryId,
    categoryName: props.categoryName
  });
  
  loading.value = false;
};

// イベントハンドラー
const handleProductClick = (product, index) => {
  trackUserAction('product_clicked', {
    productId: product.id,
    productName: product.name,
    price: product.price,
    position: index + 1,
    category: props.categoryName,
    page: currentPage.value
  });
  
  // 商品詳細ページに遷移
  window.location.href = `/products/${product.id}`;
};

const handleFilterChange = (filterType, value) => {
  trackUserAction('filter_applied', {
    filterType,
    filterValue: value,
    previousValue: filterType === 'price' ? selectedPriceRange.value : selectedSortOrder.value,
    resultCount: filteredProducts.value.length,
    category: props.categoryName
  });
};

const handleSortChange = (sortOrder) => {
  trackUserAction('sort_applied', {
    sortOrder,
    previousSortOrder: selectedSortOrder.value,
    resultCount: filteredProducts.value.length,
    category: props.categoryName
  });
};

const changePage = (page) => {
  currentPage.value = page;
  
  trackUserAction('page_changed', {
    newPage: page,
    totalPages: totalPages.value,
    category: props.categoryName
  });
};

const handleImageError = (product) => {
  trackError(new Error('Product image failed to load'), {
    productId: product.id,
    imageUrl: product.image,
    category: props.categoryName
  });
};

const retryLoad = () => {
  trackUserAction('retry_load', {
    category: props.categoryName,
    previousError: error.value
  });
  
  fetchProducts();
};

// ライフサイクル
onMounted(() => {
  fetchProducts();
});

// 状態変更監視
watch(selectedPriceRange, (newValue, oldValue) => {
  trackStateChange('selectedPriceRange', newValue, oldValue);
});

watch(selectedSortOrder, (newValue, oldValue) => {
  trackStateChange('selectedSortOrder', newValue, oldValue);
});

watch(() => props.categoryId, (newCategoryId, oldCategoryId) => {
  if (newCategoryId !== oldCategoryId) {
    trackUserAction('category_changed', {
      newCategoryId,
      oldCategoryId,
      newCategoryName: props.categoryName
    });
    
    fetchProducts();
  }
});
</script>

<style scoped>
.product-catalog {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.filters {
  display: flex;
  gap: 20px;
  margin-bottom: 30px;
}

.filters select {
  padding: 8px 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
}

.loading {
  text-align: center;
  padding: 40px;
  font-size: 18px;
}

.error {
  text-align: center;
  padding: 40px;
  color: #e74c3c;
}

.error button {
  margin-top: 10px;
  padding: 8px 16px;
  background: #3498db;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.products-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 20px;
  margin-bottom: 30px;
}

.product-card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 15px;
  cursor: pointer;
  transition: transform 0.2s, box-shadow 0.2s;
}

.product-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

.product-card img {
  width: 100%;
  height: 200px;
  object-fit: cover;
  border-radius: 4px;
}

.product-card h3 {
  margin: 10px 0;
  font-size: 16px;
}

.price {
  font-size: 18px;
  font-weight: bold;
  color: #2c3e50;
}

.rating {
  margin-top: 8px;
  font-size: 14px;
  color: #666;
}

.pagination {
  display: flex;
  justify-content: center;
  gap: 10px;
}

.pagination button {
  padding: 8px 12px;
  border: 1px solid #ddd;
  background: white;
  cursor: pointer;
  border-radius: 4px;
}

.pagination button.active {
  background: #3498db;
  color: white;
  border-color: #3498db;
}

.pagination button:hover:not(.active) {
  background: #f8f9fa;
}
</style>

Vue.js プラグインシステムでの統合

Vue.jsのプラグインシステムを使用することで、アプリケーション全体でNew Relic監視を効率的に管理できます。

New Relic Vue プラグインの実装

javascript
// plugins/newrelic-vue-plugin.js

/**
 * New Relic Vue.js プラグイン
 * アプリケーション全体でNew Relic監視を統合管理
 */

const NewRelicVuePlugin = {
  install(app, options = {}) {
    const {
      trackRouteChanges = true,
      trackComponentLifecycle = true,
      trackUserInteractions = true,
      trackErrors = true,
      logLevel = 'info'
    } = options;

    // プラグイン設定をアプリインスタンスに保存
    app.config.globalProperties.$newrelicConfig = {
      trackRouteChanges,
      trackComponentLifecycle,
      trackUserInteractions,
      trackErrors,
      logLevel
    };

    // グローバルプロパティとして New Relic 機能を提供
    app.config.globalProperties.$newrelic = {
      // 基本的な追跡機能
      trackEvent: (eventName, attributes = {}) => {
        if (!window.newrelic) {
          if (logLevel === 'debug') {
            console.warn('New Relic not available');
          }
          return;
        }
        
        window.newrelic.addPageAction(eventName, {
          ...attributes,
          timestamp: Date.now(),
          url: window.location.href,
          vueVersion: app.version
        });
      },

      // エラー追跡
      trackError: (error, context = {}) => {
        if (!window.newrelic) {
          console.error('Error occurred but New Relic not available:', error);
          return;
        }
        
        window.newrelic.noticeError(error, {
          ...context,
          timestamp: Date.now(),
          url: window.location.href,
          vueVersion: app.version
        });
      },

      // パフォーマンス計測
      startTimer: (timerName) => {
        const startTime = performance.now();
        
        return {
          end: (attributes = {}) => {
            const duration = performance.now() - startTime;
            
            if (window.newrelic) {
              window.newrelic.addPageAction('vue_performance_timer', {
                timerName,
                duration,
                ...attributes,
                timestamp: Date.now()
              });
            }
            
            return duration;
          }
        };
      },

      // ユーザー設定
      setUser: (userId, attributes = {}) => {
        if (window.newrelic) {
          window.newrelic.setUserId(userId);
          
          Object.entries(attributes).forEach(([key, value]) => {
            window.newrelic.setCustomAttribute(`user.${key}`, value);
          });
        }
      }
    };

    // グローバルエラーハンドラー(開発環境でのみ有効)
    if (trackErrors && process.env.NODE_ENV === 'development') {
      app.config.errorHandler = (error, instance, info) => {
        console.error('Vue error:', error, info);
        
        if (window.newrelic) {
          window.newrelic.noticeError(error, {
            componentInfo: info,
            componentName: instance?.$options.name || 'Unknown',
            vueVersion: app.version,
            timestamp: Date.now()
          });
        }
      };
    }

    // Vue Router統合(ルート変更追跡)
    if (trackRouteChanges) {
      app.mixin({
        beforeRouteEnter(to, from, next) {
          if (window.newrelic) {
            window.newrelic.addPageAction('vue_route_enter', {
              routeName: to.name,
              routePath: to.path,
              fromRoute: from.name,
              params: to.params,
              query: to.query
            });
          }
          next();
        },
        
        beforeRouteUpdate(to, from) {
          if (window.newrelic) {
            window.newrelic.addPageAction('vue_route_update', {
              routeName: to.name,
              routePath: to.path,
              fromRoute: from.name,
              params: to.params,
              query: to.query
            });
          }
        },
        
        beforeRouteLeave(to, from) {
          if (window.newrelic) {
            window.newrelic.addPageAction('vue_route_leave', {
              fromRoute: from.name,
              fromPath: from.path,
              toRoute: to.name,
              toPath: to.path
            });
          }
        }
      });
    }

    // コンポーネントライフサイクル追跡
    if (trackComponentLifecycle) {
      app.mixin({
        mounted() {
          if (window.newrelic) {
            window.newrelic.addPageAction('vue_component_mounted', {
              componentName: this.$options.name || 'AnonymousComponent',
              timestamp: Date.now()
            });
          }
        },
        
        unmounted() {
          if (window.newrelic) {
            window.newrelic.addPageAction('vue_component_unmounted', {
              componentName: this.$options.name || 'AnonymousComponent',
              timestamp: Date.now()
            });
          }
        }
      });
    }

    // ディレクティブ:v-track(要素のクリック等を自動追跡)
    app.directive('track', {
      mounted(el, binding) {
        const { value, arg, modifiers } = binding;
        const eventType = arg || 'click';
        
        const trackingHandler = (event) => {
          if (!window.newrelic || !trackUserInteractions) return;
          
          const eventData = {
            eventType,
            elementType: el.tagName.toLowerCase(),
            elementText: el.textContent?.trim().substring(0, 100),
            elementId: el.id,
            elementClass: el.className,
            ...(typeof value === 'object' ? value : { action: value })
          };
          
          // prevent修飾子が指定されている場合、デフォルト動作を防ぐ
          if (modifiers.prevent) {
            event.preventDefault();
          }
          
          // stop修飾子が指定されている場合、伝播を停止
          if (modifiers.stop) {
            event.stopPropagation();
          }
          
          window.newrelic.addPageAction('vue_user_interaction', eventData);
        };
        
        el.addEventListener(eventType, trackingHandler);
        
        // 要素にハンドラー参照を保存(cleanup用)
        el._trackingHandler = trackingHandler;
        el._trackingEventType = eventType;
      },
      
      unmounted(el) {
        if (el._trackingHandler && el._trackingEventType) {
          el.removeEventListener(el._trackingEventType, el._trackingHandler);
        }
      }
    });

    // プロバイド/インジェクト用のシンボル
    const NewRelicSymbol = Symbol('NewRelic');
    app.provide(NewRelicSymbol, app.config.globalProperties.$newrelic);
  }
};

export default NewRelicVuePlugin;
export { NewRelicVuePlugin };

プラグインの使用例

javascript
// main.js
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import NewRelicVuePlugin from './plugins/newrelic-vue-plugin';

const app = createApp(App);

// Vue Router設定
const router = createRouter({
  history: createWebHistory(),
  routes: [
    // ルート定義
  ]
});

// New Relic プラグインの登録
app.use(NewRelicVuePlugin, {
  trackRouteChanges: true,
  trackComponentLifecycle: true,
  trackUserInteractions: true,
  trackErrors: true,
  logLevel: process.env.NODE_ENV === 'development' ? 'debug' : 'info'
});

app.use(router);
app.mount('#app');
vue
<!-- コンポーネントでの使用例 -->
<template>
  <div class="shopping-cart">
    <h2>ショッピングカート</h2>
    
    <!-- ディレクティブを使用した自動追跡 -->
    <button 
      v-track:click="{ action: 'clear_cart', cartItems: items.length }"
      class="clear-btn"
    >
      カートを空にする
    </button>
    
    <div class="cart-items">
      <div
        v-for="item in items"
        :key="item.id"
        class="cart-item"
      >
        <img :src="item.image" :alt="item.name" />
        <div class="item-details">
          <h3>{{ item.name }}</h3>
          <p>価格: ¥{{ item.price.toLocaleString() }}</p>
          <div class="quantity-controls">
            <button 
              v-track:click="{ action: 'decrease_quantity', itemId: item.id }"
              @click="updateQuantity(item.id, item.quantity - 1)"
            >
              -
            </button>
            <span>{{ item.quantity }}</span>
            <button 
              v-track:click="{ action: 'increase_quantity', itemId: item.id }"
              @click="updateQuantity(item.id, item.quantity + 1)"
            >
              +
            </button>
          </div>
        </div>
        <button 
          v-track:click="{ action: 'remove_item', itemId: item.id, itemName: item.name }"
          @click="removeItem(item.id)"
          class="remove-btn"
        >
          削除
        </button>
      </div>
    </div>
    
    <div class="cart-summary">
      <p>合計: ¥{{ totalPrice.toLocaleString() }}</p>
      <button 
        v-track:click="{ action: 'proceed_to_checkout', totalPrice, itemCount: items.length }"
        @click="proceedToCheckout"
        class="checkout-btn"
      >
        レジに進む
      </button>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, inject } from 'vue';

// New Relic機能を注入
const $newrelic = inject('NewRelic');

const items = ref([
  { id: 1, name: 'ノートPC', price: 80000, quantity: 1, image: '/images/laptop.jpg' },
  { id: 2, name: 'マウス', price: 2000, quantity: 2, image: '/images/mouse.jpg' }
]);

const totalPrice = computed(() => {
  return items.value.reduce((sum, item) => sum + (item.price * item.quantity), 0);
});

const updateQuantity = (itemId, newQuantity) => {
  if (newQuantity <= 0) {
    removeItem(itemId);
    return;
  }
  
  const item = items.value.find(item => item.id === itemId);
  if (item) {
    const oldQuantity = item.quantity;
    item.quantity = newQuantity;
    
    // 手動でイベント追跡(複雑なロジックの場合)
    $newrelic.trackEvent('cart_quantity_updated', {
      itemId,
      itemName: item.name,
      oldQuantity,
      newQuantity,
      priceDifference: (newQuantity - oldQuantity) * item.price
    });
  }
};

const removeItem = (itemId) => {
  const itemIndex = items.value.findIndex(item => item.id === itemId);
  if (itemIndex > -1) {
    const removedItem = items.value[itemIndex];
    items.value.splice(itemIndex, 1);
    
    $newrelic.trackEvent('cart_item_removed', {
      itemId: removedItem.id,
      itemName: removedItem.name,
      itemPrice: removedItem.price,
      quantity: removedItem.quantity,
      remainingItems: items.value.length
    });
  }
};

const proceedToCheckout = () => {
  // チェックアウト処理のパフォーマンス計測
  const timer = $newrelic.startTimer('checkout_process');
  
  try {
    // チェックアウト処理...
    
    timer.end({
      success: true,
      totalPrice: totalPrice.value,
      itemCount: items.value.length
    });
    
    // チェックアウトページに遷移
    this.$router.push('/checkout');
    
  } catch (error) {
    $newrelic.trackError(error, {
      operation: 'proceed_to_checkout',
      totalPrice: totalPrice.value,
      itemCount: items.value.length
    });
    
    timer.end({
      success: false,
      errorMessage: error.message
    });
  }
};
</script>

9.3.4 Angular での高度なNew Relic統合

Angular Services を活用した監視アーキテクチャ

Angular Servicesを使用することで、依存性注入(DI)システムを活用した堅牢なNew Relic監視システムを構築できます。

NewRelicService の実装

typescript
// services/new-relic.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

export interface NewRelicConfig {
  trackRouteChanges: boolean;
  trackComponentLifecycle: boolean;
  trackUserInteractions: boolean;
  trackHttpRequests: boolean;
  trackErrors: boolean;
  logLevel: 'debug' | 'info' | 'warn' | 'error';
}

export interface PerformanceTimer {
  end(attributes?: Record<string, any>): number;
}

@Injectable({
  providedIn: 'root'
})
export class NewRelicService {
  private readonly isReady$ = new BehaviorSubject<boolean>(false);
  private readonly config: NewRelicConfig;
  private sessionId: string;

  constructor() {
    this.config = {
      trackRouteChanges: true,
      trackComponentLifecycle: true,
      trackUserInteractions: true,
      trackHttpRequests: true,
      trackErrors: true,
      logLevel: 'info'
    };

    this.sessionId = this.generateSessionId();
    this.initializeNewRelic();
  }

  /**
   * New Relicの準備状況を返すObservable
   */
  get isNewRelicReady$(): Observable<boolean> {
    return this.isReady$.asObservable();
  }

  /**
   * New Relicの準備状況を同期的に取得
   */
  get isNewRelicReady(): boolean {
    return this.isReady$.value;
  }

  /**
   * 現在のセッションIDを取得
   */
  get currentSessionId(): string {
    return this.sessionId;
  }

  /**
   * New Relicの初期化
   */
  private initializeNewRelic(): void {
    const checkNewRelic = () => {
      if (window.newrelic) {
        this.isReady$.next(true);
        this.onNewRelicReady();
        return;
      }
      
      setTimeout(checkNewRelic, 100);
    };

    checkNewRelic();
  }

  /**
   * New Relic準備完了時の処理
   */
  private onNewRelicReady(): void {
    window.newrelic.addPageAction('angular_app_initialized', {
      sessionId: this.sessionId,
      timestamp: Date.now(),
      angularVersion: this.getAngularVersion(),
      url: window.location.href
    });
  }

  /**
   * セッションIDの生成
   */
  private generateSessionId(): string {
    return `ng_session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  /**
   * Angularバージョンの取得
   */
  private getAngularVersion(): string {
    // Angular バージョンの取得ロジック
    return '15.x'; // 実際の実装では動的に取得
  }

  /**
   * カスタムイベントの追跡
   */
  trackEvent(eventName: string, attributes: Record<string, any> = {}): void {
    if (!this.isNewRelicReady) {
      if (this.config.logLevel === 'debug') {
        console.warn('New Relic not ready, event not tracked:', eventName);
      }
      return;
    }

    const eventData = {
      ...attributes,
      sessionId: this.sessionId,
      timestamp: Date.now(),
      url: window.location.href,
      angularVersion: this.getAngularVersion()
    };

    window.newrelic.addPageAction(eventName, eventData);
  }

  /**
   * エラーの追跡
   */
  trackError(error: Error, context: Record<string, any> = {}): void {
    if (!this.isNewRelicReady) {
      console.error('Error occurred but New Relic not ready:', error);
      return;
    }

    const errorContext = {
      ...context,
      sessionId: this.sessionId,
      timestamp: Date.now(),
      url: window.location.href,
      angularVersion: this.getAngularVersion(),
      stackTrace: error.stack
    };

    window.newrelic.noticeError(error, errorContext);
  }

  /**
   * ユーザー情報の設定
   */
  setUser(userId: string, attributes: Record<string, any> = {}): void {
    if (!this.isNewRelicReady) {
      console.warn('New Relic not ready, user not set');
      return;
    }

    window.newrelic.setUserId(userId);

    Object.entries(attributes).forEach(([key, value]) => {
      window.newrelic.setCustomAttribute(`user.${key}`, value);
    });

    this.trackEvent('angular_user_set', {
      userId,
      userAttributes: Object.keys(attributes)
    });
  }

  /**
   * パフォーマンス計測の開始
   */
  startTimer(timerName: string): PerformanceTimer {
    const startTime = performance.now();

    return {
      end: (attributes: Record<string, any> = {}): number => {
        const duration = performance.now() - startTime;
        
        this.trackEvent('angular_performance_timer', {
          timerName,
          duration,
          ...attributes
        });

        return duration;
      }
    };
  }

  /**
   * ページビューの追跡(SPA用)
   */
  trackPageView(pageName: string, attributes: Record<string, any> = {}): void {
    this.trackEvent('angular_page_view', {
      pageName,
      ...attributes
    });
  }

  /**
   * コンポーネントライフサイクルの追跡
   */
  trackComponentLifecycle(
    componentName: string, 
    lifecycle: 'init' | 'destroy' | 'load', 
    attributes: Record<string, any> = {}
  ): void {
    if (!this.config.trackComponentLifecycle) return;

    this.trackEvent('angular_component_lifecycle', {
      componentName,
      lifecycle,
      ...attributes
    });
  }

  /**
   * HTTP リクエストの追跡
   */
  trackHttpRequest(
    method: string,
    url: string,
    status: number,
    duration: number,
    attributes: Record<string, any> = {}
  ): void {
    if (!this.config.trackHttpRequests) return;

    this.trackEvent('angular_http_request', {
      method,
      url,
      status,
      duration,
      success: status >= 200 && status < 300,
      ...attributes
    });
  }

  /**
   * ルート変更の追跡
   */
  trackRouteChange(
    fromRoute: string,
    toRoute: string,
    attributes: Record<string, any> = {}
  ): void {
    if (!this.config.trackRouteChanges) return;

    this.trackEvent('angular_route_change', {
      fromRoute,
      toRoute,
      ...attributes
    });
  }
}

NewRelicService を使用したコンポーネントの実装

typescript
// components/product-detail.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subject, takeUntil } from 'rxjs';
import { NewRelicService, PerformanceTimer } from '../services/new-relic.service';
import { ProductService } from '../services/product.service';
import { CartService } from '../services/cart.service';

interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
  images: string[];
  category: string;
  rating: number;
  reviewCount: number;
  inStock: boolean;
}

@Component({
  selector: 'app-product-detail',
  templateUrl: './product-detail.component.html',
  styleUrls: ['./product-detail.component.scss']
})
export class ProductDetailComponent implements OnInit, OnDestroy {
  product: Product | null = null;
  loading = true;
  error: string | null = null;
  selectedImageIndex = 0;
  quantity = 1;
  
  private destroy$ = new Subject<void>();
  private loadTimer: PerformanceTimer | null = null;

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private newRelicService: NewRelicService,
    private productService: ProductService,
    private cartService: CartService
  ) {}

  ngOnInit(): void {
    // コンポーネント初期化を追跡
    this.newRelicService.trackComponentLifecycle('ProductDetailComponent', 'init');
    
    // 商品詳細読み込みのパフォーマンス計測開始
    this.loadTimer = this.newRelicService.startTimer('product_detail_load');
    
    // ルートパラメータの監視
    this.route.params
      .pipe(takeUntil(this.destroy$))
      .subscribe(params => {
        const productId = params['id'];
        if (productId) {
          this.loadProduct(productId);
        }
      });
  }

  ngOnDestroy(): void {
    // コンポーネント破棄を追跡
    this.newRelicService.trackComponentLifecycle('ProductDetailComponent', 'destroy', {
      productId: this.product?.id,
      timeOnPage: this.loadTimer ? performance.now() - (this.loadTimer as any).startTime : 0
    });
    
    this.destroy$.next();
    this.destroy$.complete();
  }

  /**
   * 商品詳細の読み込み
   */
  private async loadProduct(productId: string): Promise<void> {
    try {
      this.loading = true;
      this.error = null;
      
      // API呼び出しの追跡
      const apiTimer = this.newRelicService.startTimer('product_api_call');
      
      this.product = await this.productService.getProduct(productId).toPromise();
      
      apiTimer.end({
        productId,
        success: true
      });
      
      // 商品読み込み完了の追跡
      if (this.loadTimer) {
        this.loadTimer.end({
          productId,
          productName: this.product?.name,
          category: this.product?.category,
          success: true
        });
      }
      
      // ページビューイベント
      this.newRelicService.trackPageView('product_detail', {
        productId,
        productName: this.product?.name,
        category: this.product?.category,
        price: this.product?.price,
        inStock: this.product?.inStock
      });
      
    } catch (error) {
      this.error = error instanceof Error ? error.message : '商品の読み込みに失敗しました';
      
      // エラーの追跡
      this.newRelicService.trackError(error as Error, {
        operation: 'load_product',
        productId,
        component: 'ProductDetailComponent'
      });
      
      if (this.loadTimer) {
        this.loadTimer.end({
          productId,
          success: false,
          errorMessage: this.error
        });
      }
      
    } finally {
      this.loading = false;
    }
  }

  /**
   * 商品画像の選択
   */
  selectImage(index: number): void {
    this.selectedImageIndex = index;
    
    this.newRelicService.trackEvent('product_image_selected', {
      productId: this.product?.id,
      imageIndex: index,
      totalImages: this.product?.images?.length
    });
  }

  /**
   * 数量の変更
   */
  updateQuantity(newQuantity: number): void {
    if (newQuantity < 1 || newQuantity > 99) return;
    
    const oldQuantity = this.quantity;
    this.quantity = newQuantity;
    
    this.newRelicService.trackEvent('product_quantity_changed', {
      productId: this.product?.id,
      oldQuantity,
      newQuantity,
      quantityDiff: newQuantity - oldQuantity
    });
  }

  /**
   * カートに追加
   */
  async addToCart(): Promise<void> {
    if (!this.product || !this.product.inStock) return;
    
    const addToCartTimer = this.newRelicService.startTimer('add_to_cart');
    
    try {
      await this.cartService.addItem(this.product.id, this.quantity).toPromise();
      
      // 成功イベントの追跡
      this.newRelicService.trackEvent('product_added_to_cart', {
        productId: this.product.id,
        productName: this.product.name,
        price: this.product.price,
        quantity: this.quantity,
        totalValue: this.product.price * this.quantity,
        category: this.product.category
      });
      
      addToCartTimer.end({
        productId: this.product.id,
        quantity: this.quantity,
        success: true
      });
      
      // 成功メッセージ表示
      alert('商品をカートに追加しました');
      
    } catch (error) {
      this.newRelicService.trackError(error as Error, {
        operation: 'add_to_cart',
        productId: this.product.id,
        quantity: this.quantity,
        component: 'ProductDetailComponent'
      });
      
      addToCartTimer.end({
        productId: this.product.id,
        quantity: this.quantity,
        success: false,
        errorMessage: (error as Error).message
      });
      
      alert('カートへの追加に失敗しました');
    }
  }

  /**
   * 今すぐ購入
   */
  buyNow(): void {
    if (!this.product || !this.product.inStock) return;
    
    this.newRelicService.trackEvent('product_buy_now_clicked', {
      productId: this.product.id,
      productName: this.product.name,
      price: this.product.price,
      quantity: this.quantity,
      totalValue: this.product.price * this.quantity
    });
    
    // カートに追加してからチェックアウトページに遷移
    this.addToCart().then(() => {
      this.router.navigate(['/checkout']);
    });
  }

  /**
   * 商品レビューの表示
   */
  showReviews(): void {
    this.newRelicService.trackEvent('product_reviews_viewed', {
      productId: this.product?.id,
      rating: this.product?.rating,
      reviewCount: this.product?.reviewCount
    });
    
    // レビューセクションにスクロール
    const reviewsElement = document.getElementById('reviews');
    if (reviewsElement) {
      reviewsElement.scrollIntoView({ behavior: 'smooth' });
    }
  }

  /**
   * 関連商品のクリック
   */
  onRelatedProductClick(relatedProduct: { id: string; name: string }): void {
    this.newRelicService.trackEvent('related_product_clicked', {
      currentProductId: this.product?.id,
      relatedProductId: relatedProduct.id,
      relatedProductName: relatedProduct.name
    });
  }
}

Angular Interceptors による HTTP リクエスト監視

Angular Interceptorsを使用することで、すべてのHTTPリクエストを自動的に監視できます。

typescript
// interceptors/new-relic-http.interceptor.ts
import { Injectable } from '@angular/core';
import { 
  HttpInterceptor, 
  HttpRequest, 
  HttpHandler, 
  HttpEvent, 
  HttpResponse, 
  HttpErrorResponse 
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap, finalize } from 'rxjs/operators';
import { NewRelicService } from '../services/new-relic.service';

@Injectable()
export class NewRelicHttpInterceptor implements HttpInterceptor {
  
  constructor(private newRelicService: NewRelicService) {}

  intercept(
    request: HttpRequest<any>, 
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    
    // リクエスト開始時間を記録
    const startTime = performance.now();
    const requestId = this.generateRequestId();
    
    // リクエスト開始イベント
    this.newRelicService.trackEvent('http_request_start', {
      requestId,
      method: request.method,
      url: request.url,
      headers: this.sanitizeHeaders(request.headers),
      body: this.sanitizeBody(request.body)
    });

    return next.handle(request).pipe(
      tap({
        next: (event) => {
          if (event instanceof HttpResponse) {
            // レスポンス成功時
            const duration = performance.now() - startTime;
            
            this.newRelicService.trackHttpRequest(
              request.method,
              request.url,
              event.status,
              duration,
              {
                requestId,
                responseSize: this.getResponseSize(event),
                cached: this.isCachedResponse(event)
              }
            );
          }
        },
        error: (error: HttpErrorResponse) => {
          // エラー発生時
          const duration = performance.now() - startTime;
          
          this.newRelicService.trackError(error, {
            operation: 'http_request',
            method: request.method,
            url: request.url,
            status: error.status,
            duration,
            requestId
          });
          
          this.newRelicService.trackHttpRequest(
            request.method,
            request.url,
            error.status,
            duration,
            {
              requestId,
              errorMessage: error.message,
              errorType: error.name
            }
          );
        }
      }),
      finalize(() => {
        // リクエスト完了イベント
        this.newRelicService.trackEvent('http_request_complete', {
          requestId,
          duration: performance.now() - startTime
        });
      })
    );
  }

  private generateRequestId(): string {
    return `req_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
  }

  private sanitizeHeaders(headers: any): Record<string, string> {
    const sanitized: Record<string, string> = {};
    const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key'];
    
    headers.keys().forEach((key: string) => {
      if (sensitiveHeaders.includes(key.toLowerCase())) {
        sanitized[key] = '[REDACTED]';
      } else {
        sanitized[key] = headers.get(key);
      }
    });
    
    return sanitized;
  }

  private sanitizeBody(body: any): any {
    if (!body) return null;
    
    try {
      const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
      const bodyObj = JSON.parse(bodyStr);
      
      // 機密情報を除外
      const sensitiveFields = ['password', 'token', 'secret', 'creditCard'];
      const sanitized = { ...bodyObj };
      
      sensitiveFields.forEach(field => {
        if (sanitized[field]) {
          sanitized[field] = '[REDACTED]';
        }
      });
      
      return sanitized;
    } catch {
      return '[INVALID_JSON]';
    }
  }

  private getResponseSize(response: HttpResponse<any>): number {
    try {
      return new Blob([JSON.stringify(response.body)]).size;
    } catch {
      return 0;
    }
  }

  private isCachedResponse(response: HttpResponse<any>): boolean {
    return response.headers.get('X-Cache') === 'HIT' || 
           response.headers.get('Cache-Control')?.includes('max-age');
  }
}

Angular Zone.js統合による変更検知監視

Zone.jsと統合することで、Angularの変更検知サイクルを監視し、パフォーマンス問題を特定できます。

typescript
// services/zone-monitoring.service.ts
import { Injectable, NgZone } from '@angular/core';
import { NewRelicService } from './new-relic.service';

@Injectable({
  providedIn: 'root'
})
export class ZoneMonitoringService {
  private zoneTaskCount = 0;
  private changeDetectionCount = 0;
  private longTaskThreshold = 16; // 16ms以上の処理を長時間タスクとみなす

  constructor(
    private newRelicService: NewRelicService,
    private ngZone: NgZone
  ) {
    this.initializeZoneMonitoring();
  }

  private initializeZoneMonitoring(): void {
    // Zone.jsのタスク実行を監視
    const originalZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
    
    Zone.current.fork({
      name: 'NewRelicZoneMonitoring',
      
      onScheduleTask: (delegate, current, target, task) => {
        this.zoneTaskCount++;
        
        this.newRelicService.trackEvent('zone_task_scheduled', {
          taskType: task.type,
          taskSource: task.source,
          taskCount: this.zoneTaskCount
        });
        
        return delegate.scheduleTask(target, task);
      },
      
      onInvokeTask: (delegate, current, target, task, applyThis, applyArgs) => {
        const startTime = performance.now();
        
        try {
          const result = delegate.invokeTask(target, task, applyThis, applyArgs);
          
          const duration = performance.now() - startTime;
          
          // 長時間タスクの検出
          if (duration > this.longTaskThreshold) {
            this.newRelicService.trackEvent('zone_long_task', {
              taskType: task.type,
              taskSource: task.source,
              duration,
              threshold: this.longTaskThreshold
            });
          }
          
          return result;
          
        } catch (error) {
          this.newRelicService.trackError(error as Error, {
            operation: 'zone_task_execution',
            taskType: task.type,
            taskSource: task.source
          });
          
          throw error;
        }
      }
    });
    
    // 変更検知サイクルの監視
    this.monitorChangeDetection();
  }

  private monitorChangeDetection(): void {
    // ApplicationRefからの変更検知監視
    this.ngZone.onMicrotaskEmpty.subscribe(() => {
      this.changeDetectionCount++;
      
      if (this.changeDetectionCount % 100 === 0) {
        this.newRelicService.trackEvent('change_detection_batch', {
          count: this.changeDetectionCount,
          batchSize: 100
        });
      }
    });
  }
}

9.3.5 JavaScript エラー包括追跡システム

自動エラー分類とパターン分析

効果的なエラー管理のためには、発生したエラーを自動的に分類し、パターンを分析することが重要です。

エラー分類システムの実装

javascript
// utils/error-classifier.js

/**
 * JavaScript エラー自動分類システム
 * エラーの種類、重要度、影響範囲を自動判定
 */
export class ErrorClassifier {
  constructor(newRelicService) {
    this.newRelicService = newRelicService;
    this.errorPatterns = this.initializeErrorPatterns();
    this.errorHistory = new Map();
    this.maxHistorySize = 1000;
  }

  /**
   * エラーパターンの定義
   */
  initializeErrorPatterns() {
    return {
      // ネットワーク関連エラー
      network: {
        patterns: [
          /NetworkError/i,
          /fetch.*failed/i,
          /CORS.*policy/i,
          /ERR_NETWORK/i,
          /ERR_CONNECTION/i,
          /timeout/i
        ],
        severity: 'high',
        category: 'network',
        userImpact: 'blocking',
        autoRetry: true
      },

      // セキュリティ関連エラー
      security: {
        patterns: [
          /Content Security Policy/i,
          /Blocked.*mixed content/i,
          /Cross-origin/i,
          /Unsafe.*eval/i
        ],
        severity: 'critical',
        category: 'security',
        userImpact: 'blocking',
        autoRetry: false
      },

      // JavaScript 構文・実行エラー
      javascript: {
        patterns: [
          /SyntaxError/i,
          /ReferenceError/i,
          /TypeError/i,
          /RangeError/i,
          /is not a function/i,
          /Cannot read.*property/i,
          /undefined.*not.*object/i
        ],
        severity: 'high',
        category: 'javascript',
        userImpact: 'feature_loss',
        autoRetry: false
      },

      // リソース読み込みエラー
      resource: {
        patterns: [
          /Failed to load resource/i,
          /404.*not found/i,
          /500.*internal server/i,
          /Image.*load.*error/i,
          /Script.*load.*error/i
        ],
        severity: 'medium',
        category: 'resource',
        userImpact: 'degraded',
        autoRetry: true
      },

      // UI/DOM 関連エラー
      ui: {
        patterns: [
          /Element.*not.*found/i,
          /Cannot.*appendChild/i,
          /Invalid.*DOM/i,
          /Render.*error/i
        ],
        severity: 'medium',
        category: 'ui',
        userImpact: 'visual_glitch',
        autoRetry: false
      },

      // ブラウザ互換性エラー
      compatibility: {
        patterns: [
          /not supported.*browser/i,
          /Unrecognized.*property/i,
          /Invalid.*CSS/i,
          /WebGL.*not.*supported/i
        ],
        severity: 'low',
        category: 'compatibility',
        userImpact: 'feature_unavailable',
        autoRetry: false
      }
    };
  }

  /**
   * エラーの分類と分析
   */
  classifyError(error, context = {}) {
    const errorMessage = error.message || error.toString();
    const stackTrace = error.stack || '';
    
    // エラーパターンマッチング
    const classification = this.matchErrorPattern(errorMessage, stackTrace);
    
    // エラー頻度分析
    const frequency = this.analyzeErrorFrequency(errorMessage);
    
    // ユーザー影響分析
    const userImpact = this.analyzeUserImpact(classification, context);
    
    // エラーコンテキスト強化
    const enhancedContext = this.enhanceContext(error, context, classification);
    
    const classifiedError = {
      originalError: error,
      classification,
      frequency,
      userImpact,
      context: enhancedContext,
      errorId: this.generateErrorId(error),
      timestamp: Date.now(),
      sessionId: this.newRelicService.currentSessionId
    };

    // エラー履歴に追加
    this.addToErrorHistory(classifiedError);
    
    // New Relicに送信
    this.reportClassifiedError(classifiedError);
    
    return classifiedError;
  }

  /**
   * エラーパターンマッチング
   */
  matchErrorPattern(message, stackTrace) {
    for (const [type, config] of Object.entries(this.errorPatterns)) {
      for (const pattern of config.patterns) {
        if (pattern.test(message) || pattern.test(stackTrace)) {
          return {
            type,
            severity: config.severity,
            category: config.category,
            userImpact: config.userImpact,
            autoRetry: config.autoRetry,
            confidence: this.calculatePatternConfidence(pattern, message, stackTrace)
          };
        }
      }
    }
    
    // パターンにマッチしない場合のデフォルト分類
    return {
      type: 'unknown',
      severity: 'medium',
      category: 'generic',
      userImpact: 'unknown',
      autoRetry: false,
      confidence: 0.1
    };
  }

  /**
   * パターンマッチの信頼度計算
   */
  calculatePatternConfidence(pattern, message, stackTrace) {
    let confidence = 0.5; // ベース信頼度
    
    // メッセージとスタックトレースの両方にマッチする場合
    if (pattern.test(message) && pattern.test(stackTrace)) {
      confidence += 0.3;
    }
    
    // エラーメッセージの具体性
    if (message.length > 50) {
      confidence += 0.1;
    }
    
    // スタックトレースの詳細度
    if (stackTrace.split('\n').length > 5) {
      confidence += 0.1;
    }
    
    return Math.min(confidence, 1.0);
  }

  /**
   * エラー頻度分析
   */
  analyzeErrorFrequency(errorMessage) {
    const errorHash = this.hashErrorMessage(errorMessage);
    const now = Date.now();
    const timeWindow = 60 * 1000; // 1分間のウィンドウ
    
    if (!this.errorHistory.has(errorHash)) {
      this.errorHistory.set(errorHash, []);
    }
    
    const history = this.errorHistory.get(errorHash);
    
    // 古いエントリを削除
    const recentHistory = history.filter(timestamp => now - timestamp < timeWindow);
    recentHistory.push(now);
    
    this.errorHistory.set(errorHash, recentHistory);
    
    return {
      count: recentHistory.length,
      timeWindow,
      frequency: recentHistory.length / (timeWindow / 1000), // per second
      isSpike: recentHistory.length > 5, // 1分間に5回以上で急増とみなす
      trend: this.calculateErrorTrend(recentHistory)
    };
  }

  /**
   * エラーメッセージのハッシュ化
   */
  hashErrorMessage(message) {
    let hash = 0;
    for (let i = 0; i < message.length; i++) {
      const char = message.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash; // 32-bit整数に変換
    }
    return hash.toString();
  }

  /**
   * エラートレンド計算
   */
  calculateErrorTrend(history) {
    if (history.length < 2) return 'stable';
    
    const intervals = [];
    for (let i = 1; i < history.length; i++) {
      intervals.push(history[i] - history[i-1]);
    }
    
    const avgInterval = intervals.reduce((sum, interval) => sum + interval, 0) / intervals.length;
    
    if (avgInterval < 5000) return 'increasing'; // 5秒未満の間隔で発生
    if (avgInterval > 30000) return 'decreasing'; // 30秒以上の間隔
    return 'stable';
  }

  /**
   * ユーザー影響分析
   */
  analyzeUserImpact(classification, context) {
    const baseImpact = classification.userImpact;
    
    // コンテキスト情報に基づいて影響度を調整
    let impactScore = this.getImpactScore(baseImpact);
    
    // 重要なページでのエラーは影響度を上げる
    if (context.criticalPage) {
      impactScore *= 1.5;
    }
    
    // ユーザーがログイン済みの場合は影響度を上げる
    if (context.authenticated) {
      impactScore *= 1.3;
    }
    
    // 決済関連のエラーは影響度を大幅に上げる
    if (context.paymentRelated) {
      impactScore *= 2.0;
    }
    
    return {
      level: this.getImpactLevel(impactScore),
      score: impactScore,
      factors: this.getImpactFactors(context)
    };
  }

  /**
   * 影響度スコアの取得
   */
  getImpactScore(impact) {
    const scores = {
      blocking: 1.0,
      feature_loss: 0.8,
      degraded: 0.6,
      visual_glitch: 0.4,
      feature_unavailable: 0.3,
      unknown: 0.5
    };
    return scores[impact] || 0.5;
  }

  /**
   * 影響度レベルの取得
   */
  getImpactLevel(score) {
    if (score >= 1.5) return 'critical';
    if (score >= 1.0) return 'high';
    if (score >= 0.6) return 'medium';
    return 'low';
  }

  /**
   * 影響要因の取得
   */
  getImpactFactors(context) {
    const factors = [];
    
    if (context.criticalPage) factors.push('critical_page');
    if (context.authenticated) factors.push('authenticated_user');
    if (context.paymentRelated) factors.push('payment_related');
    if (context.mobileDevice) factors.push('mobile_device');
    if (context.slowConnection) factors.push('slow_connection');
    
    return factors;
  }

  /**
   * コンテキスト情報の強化
   */
  enhanceContext(error, originalContext, classification) {
    return {
      ...originalContext,
      
      // ブラウザ情報
      browser: {
        userAgent: navigator.userAgent,
        language: navigator.language,
        cookieEnabled: navigator.cookieEnabled,
        onLine: navigator.onLine
      },
      
      // ページ情報
      page: {
        url: window.location.href,
        referrer: document.referrer,
        title: document.title,
        loadTime: performance.now()
      },
      
      // エラー詳細
      error: {
        name: error.name,
        message: error.message,
        stack: error.stack,
        fileName: error.fileName,
        lineNumber: error.lineNumber,
        columnNumber: error.columnNumber
      },
      
      // パフォーマンス情報
      performance: this.gatherPerformanceContext(),
      
      // 分類情報
      classification
    };
  }

  /**
   * パフォーマンスコンテキストの収集
   */
  gatherPerformanceContext() {
    if (!window.performance) return {};
    
    const memory = window.performance.memory;
    const navigation = window.performance.navigation;
    const timing = window.performance.timing;
    
    return {
      memory: memory ? {
        usedJSHeapSize: memory.usedJSHeapSize,
        totalJSHeapSize: memory.totalJSHeapSize,
        jsHeapSizeLimit: memory.jsHeapSizeLimit
      } : null,
      
      navigation: {
        type: navigation?.type,
        redirectCount: navigation?.redirectCount
      },
      
      timing: timing ? {
        domContentLoaded: timing.domContentLoadedEventEnd - timing.domContentLoadedEventStart,
        load: timing.loadEventEnd - timing.loadEventStart,
        response: timing.responseEnd - timing.responseStart
      } : null
    };
  }

  /**
   * エラーIDの生成
   */
  generateErrorId(error) {
    const timestamp = Date.now();
    const hash = this.hashErrorMessage(error.message || error.toString());
    return `error_${timestamp}_${hash}`;
  }

  /**
   * エラー履歴への追加
   */
  addToErrorHistory(classifiedError) {
    // メモリ使用量制限のため、古いエラーを削除
    if (this.errorHistory.size > this.maxHistorySize) {
      const oldestKey = this.errorHistory.keys().next().value;
      this.errorHistory.delete(oldestKey);
    }
  }

  /**
   * 分類されたエラーのNew Relicへの報告
   */
  reportClassifiedError(classifiedError) {
    const {
      originalError,
      classification,
      frequency,
      userImpact,
      context,
      errorId
    } = classifiedError;

    // New Relicにエラーを送信
    this.newRelicService.trackError(originalError, {
      errorId,
      classification: classification.type,
      severity: classification.severity,
      category: classification.category,
      userImpact: userImpact.level,
      frequency: frequency.count,
      isSpike: frequency.isSpike,
      confidence: classification.confidence,
      autoRetryRecommended: classification.autoRetry,
      context
    });

    // 詳細な分析イベントも送信
    this.newRelicService.trackEvent('error_classified', {
      errorId,
      type: classification.type,
      severity: classification.severity,
      userImpactLevel: userImpact.level,
      userImpactScore: userImpact.score,
      frequency: frequency.count,
      trend: frequency.trend,
      confidence: classification.confidence
    });
  }
}

エラー分類システムの使用例

javascript
// main.js または app初期化ファイル
import { ErrorClassifier } from './utils/error-classifier.js';
import { NewRelicService } from './services/new-relic.service.js';

// サービスの初期化
const newRelicService = new NewRelicService();
const errorClassifier = new ErrorClassifier(newRelicService);

// グローバルエラーハンドラーの設定
window.addEventListener('error', (event) => {
  const context = {
    criticalPage: window.location.pathname.includes('/checkout') || 
                   window.location.pathname.includes('/payment'),
    authenticated: localStorage.getItem('authToken') !== null,
    paymentRelated: window.location.pathname.includes('/payment') ||
                    document.querySelector('[data-payment-form]') !== null,
    mobileDevice: /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),
    slowConnection: navigator.connection?.effectiveType === 'slow-2g' || 
                    navigator.connection?.effectiveType === '2g'
  };
  
  errorClassifier.classifyError(event.error, context);
});

// Promise rejection エラーハンドラー
window.addEventListener('unhandledrejection', (event) => {
  const error = new Error(event.reason);
  error.name = 'UnhandledPromiseRejection';
  
  const context = {
    promiseRejection: true,
    criticalPage: window.location.pathname.includes('/checkout'),
    authenticated: localStorage.getItem('authToken') !== null
  };
  
  errorClassifier.classifyError(error, context);
});

PWA特有の監視要件

**Progressive Web App(PWA)**では、Service Worker、オフライン機能、プッシュ通知など、従来のWebアプリケーションにはない機能があります。これらの機能を適切に監視することが重要です。

Service Worker監視システム

javascript
// service-worker-monitor.js

/**
 * Service Worker 専用監視システム
 * PWA特有の機能とエラーを包括的に監視
 */
export class ServiceWorkerMonitor {
  constructor(newRelicService) {
    this.newRelicService = newRelicService;
    this.swRegistration = null;
    this.cacheMetrics = new Map();
    this.networkMetrics = {
      online: navigator.onLine,
      requests: new Map(),
      failures: new Map()
    };
    
    this.initializeMonitoring();
  }

  /**
   * Service Worker監視の初期化
   */
  async initializeMonitoring() {
    if ('serviceWorker' in navigator) {
      try {
        // Service Worker登録の監視
        this.swRegistration = await navigator.serviceWorker.register('/sw.js');
        
        this.newRelicService.trackEvent('service_worker_registered', {
          scope: this.swRegistration.scope,
          updateViaCache: this.swRegistration.updateViaCache
        });
        
        // Service Worker イベント監視
        this.monitorServiceWorkerEvents();
        
        // キャッシュ監視
        this.monitorCacheUsage();
        
        // ネットワーク状態監視
        this.monitorNetworkStatus();
        
        // プッシュ通知監視
        this.monitorPushNotifications();
        
      } catch (error) {
        this.newRelicService.trackError(error, {
          operation: 'service_worker_registration',
          pwa: true
        });
      }
    } else {
      this.newRelicService.trackEvent('service_worker_not_supported', {
        userAgent: navigator.userAgent
      });
    }
  }

  /**
   * Service Worker イベント監視
   */
  monitorServiceWorkerEvents() {
    // Service Worker 更新検出
    this.swRegistration.addEventListener('updatefound', () => {
      const newWorker = this.swRegistration.installing;
      
      this.newRelicService.trackEvent('service_worker_update_found', {
        state: newWorker.state
      });
      
      newWorker.addEventListener('statechange', () => {
        this.newRelicService.trackEvent('service_worker_state_change', {
          newState: newWorker.state,
          timestamp: Date.now()
        });
        
        if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
          // 新しいバージョンが利用可能
          this.newRelicService.trackEvent('service_worker_update_available');
          this.notifyUserOfUpdate();
        }
      });
    });

    // Service Worker からのメッセージ監視
    navigator.serviceWorker.addEventListener('message', (event) => {
      const { type, data } = event.data;
      
      this.newRelicService.trackEvent('service_worker_message', {
        messageType: type,
        data: data,
        source: 'service_worker'
      });
      
      // Service Worker からのエラーメッセージ処理
      if (type === 'error') {
        this.newRelicService.trackError(new Error(data.message), {
          operation: data.operation,
          serviceWorker: true,
          errorContext: data.context
        });
      }
      
      // キャッシュ操作結果の処理
      if (type === 'cache_operation') {
        this.handleCacheOperation(data);
      }
    });

    // Service Worker コントローラー変更監視
    navigator.serviceWorker.addEventListener('controllerchange', () => {
      this.newRelicService.trackEvent('service_worker_controller_change', {
        newController: navigator.serviceWorker.controller?.scriptURL,
        timestamp: Date.now()
      });
    });
  }

  /**
   * キャッシュ使用量監視
   */
  async monitorCacheUsage() {
    if ('caches' in window) {
      try {
        const cacheNames = await caches.keys();
        
        for (const cacheName of cacheNames) {
          const cache = await caches.open(cacheName);
          const requests = await cache.keys();
          
          this.cacheMetrics.set(cacheName, {
            entryCount: requests.length,
            lastUpdated: Date.now()
          });
        }
        
        // キャッシュサイズ推定
        const estimatedSize = await this.estimateCacheSize();
        
        this.newRelicService.trackEvent('cache_usage_analyzed', {
          cacheCount: cacheNames.length,
          totalEntries: Array.from(this.cacheMetrics.values())
            .reduce((sum, metrics) => sum + metrics.entryCount, 0),
          estimatedSize,
          cacheNames
        });
        
      } catch (error) {
        this.newRelicService.trackError(error, {
          operation: 'cache_analysis',
          pwa: true
        });
      }
    }
  }

  /**
   * キャッシュサイズの推定
   */
  async estimateCacheSize() {
    if ('storage' in navigator && 'estimate' in navigator.storage) {
      try {
        const estimate = await navigator.storage.estimate();
        return {
          quota: estimate.quota,
          usage: estimate.usage,
          usagePercentage: (estimate.usage / estimate.quota) * 100
        };
      } catch (error) {
        this.newRelicService.trackError(error, {
          operation: 'storage_estimate',
          pwa: true
        });
        return null;
      }
    }
    return null;
  }

  /**
   * ネットワーク状態監視
   */
  monitorNetworkStatus() {
    // オンライン/オフライン状態変更
    window.addEventListener('online', () => {
      this.networkMetrics.online = true;
      
      this.newRelicService.trackEvent('network_status_change', {
        status: 'online',
        timestamp: Date.now(),
        connection: this.getConnectionInfo()
      });
    });

    window.addEventListener('offline', () => {
      this.networkMetrics.online = false;
      
      this.newRelicService.trackEvent('network_status_change', {
        status: 'offline',
        timestamp: Date.now()
      });
    });

    // 接続情報変更(Network Information API)
    if ('connection' in navigator) {
      navigator.connection.addEventListener('change', () => {
        this.newRelicService.trackEvent('connection_change', {
          ...this.getConnectionInfo(),
          timestamp: Date.now()
        });
      });
    }
  }

  /**
   * 接続情報の取得
   */
  getConnectionInfo() {
    if (!('connection' in navigator)) return {};
    
    const connection = navigator.connection;
    return {
      effectiveType: connection.effectiveType,
      downlink: connection.downlink,
      rtt: connection.rtt,
      saveData: connection.saveData
    };
  }

  /**
   * プッシュ通知監視
   */
  async monitorPushNotifications() {
    if ('PushManager' in window && 'Notification' in window) {
      try {
        // プッシュ通知購読状況監視
        const subscription = await this.swRegistration.pushManager.getSubscription();
        
        this.newRelicService.trackEvent('push_subscription_status', {
          subscribed: !!subscription,
          endpoint: subscription?.endpoint,
          notificationPermission: Notification.permission
        });

        // 通知許可状態変更監視
        this.monitorNotificationPermission();
        
      } catch (error) {
        this.newRelicService.trackError(error, {
          operation: 'push_notification_monitoring',
          pwa: true
        });
      }
    }
  }

  /**
   * 通知許可状態監視
   */
  monitorNotificationPermission() {
    // 権限変更を定期的にチェック
    let lastPermission = Notification.permission;
    
    setInterval(() => {
      const currentPermission = Notification.permission;
      
      if (currentPermission !== lastPermission) {
        this.newRelicService.trackEvent('notification_permission_change', {
          oldPermission: lastPermission,
          newPermission: currentPermission,
          timestamp: Date.now()
        });
        
        lastPermission = currentPermission;
      }
    }, 5000); // 5秒間隔でチェック
  }

  /**
   * キャッシュ操作結果の処理
   */
  handleCacheOperation(data) {
    const { operation, cacheName, request, success, error } = data;
    
    if (success) {
      this.newRelicService.trackEvent('cache_operation_success', {
        operation,
        cacheName,
        request: request.url,
        method: request.method
      });
    } else {
      this.newRelicService.trackError(new Error(error), {
        operation: `cache_${operation}`,
        cacheName,
        request: request.url,
        serviceWorker: true
      });
    }
  }

  /**
   * ユーザーへの更新通知
   */
  notifyUserOfUpdate() {
    // アプリ内通知またはUIの更新
    const updateEvent = new CustomEvent('sw-update-available', {
      detail: {
        timestamp: Date.now(),
        registration: this.swRegistration
      }
    });
    
    window.dispatchEvent(updateEvent);
    
    this.newRelicService.trackEvent('update_notification_displayed', {
      method: 'custom_event',
      timestamp: Date.now()
    });
  }

  /**
   * オフライン時の操作記録
   */
  trackOfflineOperation(operation, data = {}) {
    this.newRelicService.trackEvent('offline_operation', {
      operation,
      ...data,
      networkStatus: this.networkMetrics.online ? 'online' : 'offline',
      timestamp: Date.now()
    });
  }

  /**
   * PWA特有のメトリクス取得
   */
  getPWAMetrics() {
    return {
      serviceWorker: {
        registered: !!this.swRegistration,
        controller: !!navigator.serviceWorker.controller,
        scope: this.swRegistration?.scope
      },
      cache: {
        metrics: Object.fromEntries(this.cacheMetrics),
        estimatedSize: this.estimateCacheSize()
      },
      network: {
        ...this.networkMetrics,
        connection: this.getConnectionInfo()
      },
      notifications: {
        permission: Notification.permission,
        supported: 'Notification' in window
      }
    };
  }
}

📋 第9章の残りタスク

⏳ 未完成セクション(実装待ち)

第9.4章: ユーザーエクスペリエンス最適化実践

  • [ ] データ駆動型UX改善手法の詳細実装
  • [ ] A/Bテスト統合とパフォーマンス相関分析
  • [ ] コンバージョンファネル最適化の実践手法
  • [ ] セッション品質分析とエンゲージメント測定
  • [ ] ROI測定とビジネス価値の定量化フレームワーク

🔄 最終調整タスク

  • [ ] 全ファイル間のリンク確認・修正(ナビゲーション完全性確保)
  • [ ] 章全体の構造統一性チェック(他章との一貫性確認)
  • [ ] コンテンツ品質・誤字脱字の最終確認
  • [ ] 初心者向け説明文の充実度確認
  • [ ] 技術精度と実用性の最終検証

🎉 セクション9.3完成!

本セクションでは、React・Vue・Angularでの高度なNew Relic統合からJavaScript エラー包括追跡PWA特有の監視要件まで、モダンWebアプリケーションの監視に必要なすべての要素を網羅しました。

実装された主要機能

  • フレームワーク別統合: React Hooks・Vue Composition API・Angular Services
  • 自動エラー分類: 機械学習的アプローチによるエラーパターン認識
  • PWA専用監視: Service Worker・オフライン機能・プッシュ通知
  • 包括的な監視基盤: SPA・PWAでの完全なユーザー体験可視化

次のセクション: 第9.4章 ユーザーエクスペリエンス最適化実践では、収集したデータを活用した実際のUX改善手法を学習します。


関連記事: 第9章 New Relic Browser詳細化関連記事: 第9.2章 Core Web Vitals詳細分析