New Relic入門 第9章 - Browser・RUM監視:ユーザー体験の可視化と最適化

📖 ナビゲーション

前章: 第8章 New Relic Mobile
次章: 第10章 New Relic Logs


第8章でモバイル監視をマスターした後は、Browser Monitoringでフロントエンドとユーザー体験の監視を実現しましょう。本記事では、RUM(Real User Monitoring)の実装、JavaScript エラー追跡、Core Web Vitals測定の実践的手法を詳しく解説します。

🎯 この記事で習得できること

  • [ ] **RUM(Real User Monitoring)**の実装と活用
  • [ ] React・Vue・Angularでの高度な統合設定
  • [ ] JavaScript エラー追跡とコンテキスト情報収集
  • [ ] Core Web Vitals測定と最適化アドバイス
  • [ ] ユーザー行動分析とコンバージョン追跡
  • [ ] パフォーマンス最適化の実践的手法

9.1 Browser Monitoring の基礎

🌐 RUM(Real User Monitoring)の実装

RUMとは何か、なぜ重要なのか

**RUM(Real User Monitoring)**は、実際のユーザーがWebアプリケーションを使用する際のパフォーマンスとユーザー体験をリアルタイムで監視する手法です。

yaml
# RUMが監視する主要メトリクス
Page_Load_Performance:
  - First Contentful Paint (FCP)
  - Largest Contentful Paint (LCP)
  - Cumulative Layout Shift (CLS)
  - First Input Delay (FID)
  - Time to Interactive (TTI)

User_Experience:
  - ページビュー数と滞在時間
  - ユーザーセッション追跡
  - 離脱率とバウンス率
  - コンバージョンファネル分析

Technical_Metrics:
  - JavaScript エラー率
  - AJAX リクエスト成功率
  - リソース読み込み時間
  - ブラウザとデバイス分析

基本的なBrowser Agent設定

html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Application</title>
    
    <!-- New Relic Browser Agent(最優先で配置) -->
    <script type="text/javascript">
    ;window.NREUM||(NREUM={});NREUM.loader_config={accountID:"YOUR_ACCOUNT_ID",trustKey:"YOUR_TRUST_KEY",agentID:"YOUR_AGENT_ID",licenseKey:"YOUR_LICENSE_KEY",applicationID:"YOUR_APP_ID"}
    ;window.NREUM||(NREUM={}),__nr_require=function(n,e,t){function r(t){if(!e[t]){var o=e[t]={exports:{}};n[t][0].call(o.exports,function(e){var o=n[t][1][e];return r(o||e)},o,o.exports)}return e[t].exports}if("function"==typeof __nr_require)return __nr_require;for(var o=0;o<t.length;o++)r(t[o]);return r}({1:[function(n,e,t){function r(){}function o(n,e,t){return function(){return i(n,[u.now()].concat(c(arguments)),e,t),e?e(arguments):void 0}}var i=n("handle"),a=n(2),c=n(3),f=n("ee").get("tracer"),u=n("loader"),s=NREUM;"undefined"==typeof window.newrelic&&(newrelic=s);var p=["setPageViewName","setCustomAttribute","setErrorHandler","finished","addToTrace","inlineHit","addRelease"],d="api-",l=d+"ixn-";a(p,function(n,e){s[e]=o(d+e,!0,"api")}),s.addPageAction=o(d+"addPageAction",!0),s.setCurrentRouteName=o(d+"routeName",!0),e.exports=newrelic,s.interaction=function(){return(new r).get()};var m=r.prototype={createTracer:function(n,e){var t={},r=this,o="function"==typeof e;return i(l+"tracer",[u.now(),n,t],r),function(){if(f.emit((o?"":"no-")+"fn-start",[u.now(),r,o],t),o)try{return e.apply(this,arguments)}catch(n){throw f.emit("fn-err",[arguments,this,n],t),n}finally{f.emit("fn-end",[u.now()],t)}}}};a("actionText,setName,setAttribute,save,ignore,onEnd,getContext,end,get".split(","),function(n,e){m[e]=o(l+e)}),newrelic.noticeError=function(n,e){"string"==typeof n&&(n=new Error(n)),i("err",[n,u.now(),!1,e])}},{}],2:[function(n,e,t){function r(n,e){var t=[],r="",i=0;for(r in n)o.call(n,r)&&(t[i]=e(r,n[r]),i+=1);return t}var o=Object.prototype.hasOwnProperty;e.exports=r},{}],3:[function(n,e,t){function r(n,e,t){e||(e=0),"undefined"==typeof t&&(t=n?n.length:0);for(var r=-1,o=t-e||0,i=Array(o<0?0:o);++r<o;)i[r]=n[e+r];return i}e.exports=r},{}],4:[function(n,e,t){e.exports={exists:"undefined"!=typeof window.performance&&window.performance.timing&&"undefined"!=typeof window.performance.timing.navigationStart}},{}],5:[function(n,e,t){function r(n){return n&&n instanceof Error&&!!n.stack}function o(n,e){var t=[i.now()],r=null,o=null,a={};if(0!==arguments.length){if(r=arguments[0],o=arguments[1],"string"==typeof r&&(r=new Error(r)),f(r)){t.push(r.stack),a.browserStack=r.stack;var c=window.location.href;a.pageURL=c,a.stack=r.stack}}return a.releaseIds=NREUM.loader_config.releaseIds,a.errorClass=r&&r.constructor&&r.constructor.name,i("err",t.concat([a,o]))}var i=n("handle"),a=n(4),c=n("ee"),f=r,u=window.onerror,s=!1,p="nr@seenError",d=0;c.on("internal-error",function(n){i("ierr",[n,f.now(),!0])}),a.exists&&(u&&(c.on("internal-error",u),u=null),window.onerror=o,s=!0),e.exports={wrap:function(n){var e=!1;return"function"!=typeof n||(e=c.on("internal-error",function(e){u&&u.apply(this,arguments)})),function(){var t;try{return n.apply(this,arguments)}catch(r){throw!e||s||("function"==typeof u?u.apply(this,[r.message,r.filename||"",r.lineno||0,0,r]):o(r)),r}}},noticeError:o,ee:c,origOnerror:u}},{}]},{},[]);
    ;NREUM.loader_config.xpid="VQAGV1dbGwIEUFNQAwEF";NREUM.info={beacon:"bam.nr-data.net",errorBeacon:"bam.nr-data.net",licenseKey:"1234567890abcdef",applicationID:"987654321",sa:1}
    </script>
    
    <!-- その他のCSSファイル等 -->
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div id="app">
        <!-- アプリケーションコンテンツ -->
    </div>
    
    <!-- カスタムイベント追跡の実装 -->
    <script>
    // ページロード完了時のカスタムメトリクス
    window.addEventListener('load', function() {
        if (window.newrelic) {
            // ページタイプの設定
            newrelic.setCustomAttribute('page_type', 'landing');
            newrelic.setCustomAttribute('user_type', getUserType());
            
            // ページロード時間の記録
            const loadTime = performance.now();
            newrelic.recordMetric('Custom/PageLoad/Time', loadTime);
            
            // カスタムイベント送信
            newrelic.addPageAction('page_load_complete', {
                load_time: loadTime,
                page_path: window.location.pathname,
                referrer: document.referrer
            });
        }
    });
    
    // ユーザータイプの判定ロジック
    function getUserType() {
        if (localStorage.getItem('user_id')) {
            return 'returning_user';
        } else if (document.referrer) {
            return 'referred_user';
        } else {
            return 'direct_user';
        }
    }
    
    // カスタムエラーハンドリング
    window.addEventListener('error', function(e) {
        if (window.newrelic) {
            newrelic.noticeError(e.error, {
                'error.type': 'javascript_error',
                'error.filename': e.filename,
                'error.line': e.lineno,
                'error.column': e.colno,
                'page.url': window.location.href
            });
        }
    });
    </script>
</body>
</html>

9.2 フレームワーク別の高度な統合

⚛️ React アプリケーションでの統合

カスタムフックによる統合

javascript
// src/hooks/useNewRelic.js
import { useEffect, useCallback, useRef } from 'react';
import { useLocation } from 'react-router-dom';

export function useNewRelic() {
  const location = useLocation();
  const pageLoadTimeRef = useRef(performance.now());
  
  // ページビュー追跡
  useEffect(() => {
    if (window.newrelic) {
      const pageName = getPageName(location.pathname);
      
      // ページビュー名の設定
      window.newrelic.setPageViewName(pageName);
      
      // ページ属性の設定
      window.newrelic.setCustomAttribute('route.path', location.pathname);
      window.newrelic.setCustomAttribute('route.hash', location.hash);
      window.newrelic.setCustomAttribute('route.search', location.search);
      
      // ページ変更イベント
      window.newrelic.addPageAction('page_view', {
        page_name: pageName,
        previous_page: sessionStorage.getItem('previous_page'),
        navigation_type: getNavigationType(),
        timestamp: Date.now()
      });
      
      // 前のページを記録
      sessionStorage.setItem('previous_page', pageName);
    }
  }, [location]);
  
  // カスタムイベント追跡
  const trackEvent = useCallback((eventName, attributes = {}) => {
    if (window.newrelic) {
      window.newrelic.addPageAction(eventName, {
        ...attributes,
        timestamp: Date.now(),
        page_path: location.pathname,
        user_agent: navigator.userAgent,
        viewport_width: window.innerWidth,
        viewport_height: window.innerHeight
      });
    }
  }, [location.pathname]);
  
  // エラー追跡
  const trackError = useCallback((error, context = {}) => {
    if (window.newrelic) {
      window.newrelic.noticeError(error, {
        ...context,
        component_stack: error.componentStack,
        page_path: location.pathname,
        user_session_id: getSessionId()
      });
    }
  }, [location.pathname]);
  
  // ユーザーコンテキスト設定
  const setUserContext = useCallback((userId, userAttributes = {}) => {
    if (window.newrelic) {
      window.newrelic.setUserId(userId);
      
      Object.entries(userAttributes).forEach(([key, value]) => {
        window.newrelic.setCustomAttribute(`user.${key}`, value);
      });
      
      // ユーザー登録イベント
      window.newrelic.addPageAction('user_identified', {
        user_id: userId,
        ...userAttributes
      });
    }
  }, []);
  
  // パフォーマンス測定
  const measurePerformance = useCallback((metricName, startTime) => {
    if (window.newrelic) {
      const duration = performance.now() - startTime;
      window.newrelic.recordMetric(`Custom/Performance/${metricName}`, duration);
      
      return duration;
    }
    return 0;
  }, []);
  
  return {
    trackEvent,
    trackError,
    setUserContext,
    measurePerformance
  };
}

// ユーティリティ関数
function getPageName(pathname) {
  const pathMap = {
    '/': 'Home',
    '/products': 'Product List',
    '/product/': 'Product Detail',
    '/cart': 'Shopping Cart',
    '/checkout': 'Checkout',
    '/profile': 'User Profile'
  };
  
  // 動的パスの処理
  for (const [path, name] of Object.entries(pathMap)) {
    if (pathname.startsWith(path)) {
      return name;
    }
  }
  
  return pathname;
}

function getNavigationType() {
  if (performance.navigation) {
    const types = ['navigate', 'reload', 'back_forward', 'prerender'];
    return types[performance.navigation.type] || 'unknown';
  }
  return 'unknown';
}

function getSessionId() {
  let sessionId = sessionStorage.getItem('nr_session_id');
  if (!sessionId) {
    sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    sessionStorage.setItem('nr_session_id', sessionId);
  }
  return sessionId;
}

React Error Boundary の実装

javascript
// src/components/ErrorBoundary.jsx
import React from 'react';
import { useNewRelic } from '../hooks/useNewRelic';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { 
      hasError: false, 
      error: null,
      errorInfo: null
    };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  componentDidCatch(error, errorInfo) {
    // New Relicにエラー送信
    if (window.newrelic) {
      window.newrelic.noticeError(error, {
        error_boundary: this.props.name || 'unnamed',
        component_stack: errorInfo.componentStack,
        error_type: 'react_error_boundary',
        props: JSON.stringify(this.props, null, 2),
        timestamp: new Date().toISOString()
      });
    }
    
    this.setState({
      error,
      errorInfo
    });
    
    // コンソールログ(開発環境)
    if (process.env.NODE_ENV === 'development') {
      console.error('React Error Boundary caught an error:', error, errorInfo);
    }
  }
  
  render() {
    if (this.state.hasError) {
      // カスタムエラーUI
      return (
        <div className="error-boundary">
          <h2>申し訳ございません。エラーが発生しました。</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            <summary>エラー詳細</summary>
            <p><strong>エラー:</strong> {this.state.error && this.state.error.toString()}</p>
            <p><strong>スタック:</strong> {this.state.errorInfo.componentStack}</p>
          </details>
          <button onClick={() => window.location.reload()}>
            ページを再読み込み
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

export default ErrorBoundary;

// 使用例: App.jsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import ErrorBoundary from './components/ErrorBoundary';
import { useNewRelic } from './hooks/useNewRelic';

function App() {
  const { setUserContext } = useNewRelic();
  
  // アプリケーション初期化時のユーザー設定
  React.useEffect(() => {
    const userId = localStorage.getItem('user_id');
    const userRole = localStorage.getItem('user_role');
    
    if (userId) {
      setUserContext(userId, {
        role: userRole,
        login_time: new Date().toISOString(),
        app_version: process.env.REACT_APP_VERSION
      });
    }
  }, [setUserContext]);
  
  return (
    <ErrorBoundary name="App">
      <Router>
        <ErrorBoundary name="Router">
          <Routes>
            <Route path="/" element={
              <ErrorBoundary name="Home">
                <Home />
              </ErrorBoundary>
            } />
            <Route path="/products" element={
              <ErrorBoundary name="ProductList">
                <ProductList />
              </ErrorBoundary>
            } />
            <Route path="/product/:id" element={
              <ErrorBoundary name="ProductDetail">
                <ProductDetail />
              </ErrorBoundary>
            } />
          </Routes>
        </ErrorBoundary>
      </Router>
    </ErrorBoundary>
  );
}

実践的なコンポーネント例

javascript
// src/pages/ProductDetail.jsx
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useNewRelic } from '../hooks/useNewRelic';

const ProductDetail = () => {
  const { productId } = useParams();
  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  const { trackEvent, trackError, measurePerformance } = useNewRelic();
  
  useEffect(() => {
    const startTime = performance.now();
    
    // データ取得開始をトラッキング
    trackEvent('product_detail_load_start', {
      product_id: productId
    });
    
    fetchProductDetail(productId)
      .then(data => {
        setProduct(data);
        setLoading(false);
        
        // データ取得完了時間を測定
        const loadTime = measurePerformance('ProductDetailLoad', startTime);
        
        // 成功イベントをトラッキング
        trackEvent('product_detail_load_success', {
          product_id: productId,
          product_name: data.name,
          product_category: data.category,
          product_price: data.price,
          load_time: loadTime
        });
        
        // 商品閲覧の分析イベント
        trackEvent('product_viewed', {
          product_id: productId,
          product_category: data.category,
          product_price: data.price,
          referrer: document.referrer
        });
      })
      .catch(error => {
        setError(error);
        setLoading(false);
        
        // エラーを追跡
        trackError(error, {
          product_id: productId,
          error_context: 'product_detail_fetch'
        });
        
        // エラーイベント
        trackEvent('product_detail_load_error', {
          product_id: productId,
          error_message: error.message,
          error_type: error.name
        });
      });
  }, [productId, trackEvent, trackError, measurePerformance]);
  
  // 購入ボタンクリック追跡
  const handlePurchaseClick = () => {
    trackEvent('purchase_button_click', {
      product_id: productId,
      product_name: product.name,
      product_price: product.price,
      click_timestamp: Date.now()
    });
    
    // 実際の購入処理...
  };
  
  // カートに追加追跡
  const handleAddToCart = () => {
    trackEvent('add_to_cart', {
      product_id: productId,
      product_name: product.name,
      product_price: product.price,
      cart_action: 'add'
    });
    
    // 実際のカート追加処理...
  };
  
  if (loading) {
    return <div>読み込み中...</div>;
  }
  
  if (error) {
    return (
      <div className="error-message">
        <h2>商品の読み込みに失敗しました</h2>
        <p>{error.message}</p>
        <button onClick={() => window.location.reload()}>
          再試行
        </button>
      </div>
    );
  }
  
  return (
    <div className="product-detail">
      <h1>{product.name}</h1>
      <div className="product-info">
        <img 
          src={product.image} 
          alt={product.name}
          onLoad={() => trackEvent('product_image_loaded', { product_id: productId })}
          onError={(e) => trackError(new Error('Product image failed to load'), {
            product_id: productId,
            image_src: e.target.src
          })}
        />
        <div className="product-details">
          <p>{product.description}</p>
          <span className="price">¥{product.price.toLocaleString()}</span>
          <div className="actions">
            <button onClick={handleAddToCart}>
              カートに追加
            </button>
            <button onClick={handlePurchaseClick}>
              今すぐ購入
            </button>
          </div>
        </div>
      </div>
    </div>
  );
};

export default ProductDetail;

🖖 Vue.js での統合

javascript
// src/plugins/newrelic.js
export default {
  install(app, options) {
    // New Relic グローバルメソッドをVueインスタンスに追加
    app.config.globalProperties.$newrelic = {
      trackPageView(routeName, attributes = {}) {
        if (window.newrelic) {
          window.newrelic.setPageViewName(routeName);
          
          Object.entries(attributes).forEach(([key, value]) => {
            window.newrelic.setCustomAttribute(key, value);
          });
          
          window.newrelic.addPageAction('vue_page_view', {
            route_name: routeName,
            ...attributes,
            timestamp: Date.now()
          });
        }
      },
      
      trackEvent(eventName, attributes = {}) {
        if (window.newrelic) {
          window.newrelic.addPageAction(eventName, {
            ...attributes,
            framework: 'vue',
            timestamp: Date.now()
          });
        }
      },
      
      trackError(error, context = {}) {
        if (window.newrelic) {
          window.newrelic.noticeError(error, {
            ...context,
            framework: 'vue',
            vue_version: app.version
          });
        }
      },
      
      setUserContext(userId, attributes = {}) {
        if (window.newrelic) {
          window.newrelic.setUserId(userId);
          
          Object.entries(attributes).forEach(([key, value]) => {
            window.newrelic.setCustomAttribute(`user.${key}`, value);
          });
        }
      }
    };
    
    // グローバルエラーハンドラ
    app.config.errorHandler = (err, instance, info) => {
      if (window.newrelic) {
        window.newrelic.noticeError(err, {
          vue_error_info: info,
          component_name: instance?.$options.name || 'Unknown',
          framework: 'vue'
        });
      }
      
      console.error('Vue error:', err, info);
    };
  }
};

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import NewRelicPlugin from './plugins/newrelic'

const app = createApp(App)

app.use(NewRelicPlugin)
app.use(router)

// ルートガードでの自動追跡
router.beforeEach((to, from, next) => {
  if (window.newrelic) {
    window.newrelic.setPageViewName(to.name || to.path);
    window.newrelic.setCustomAttribute('route.name', to.name);
    window.newrelic.setCustomAttribute('route.path', to.path);
    window.newrelic.setCustomAttribute('route.params', JSON.stringify(to.params));
    
    window.newrelic.addPageAction('vue_route_change', {
      to_path: to.path,
      from_path: from.path,
      to_name: to.name,
      from_name: from.name
    });
  }
  next();
});

app.mount('#app')

🅰️ Angular での統合

typescript
// src/app/services/newrelic.service.ts
import { Injectable } from '@angular/core';

declare global {
  interface Window {
    newrelic: any;
  }
}

@Injectable({
  providedIn: 'root'
})
export class NewRelicService {
  private nr = window.newrelic;

  trackPageView(url: string, customAttributes?: any): void {
    if (this.nr) {
      this.nr.setPageViewName(url);
      
      if (customAttributes) {
        Object.entries(customAttributes).forEach(([key, value]) => {
          this.nr.setCustomAttribute(key, value);
        });
      }
      
      this.nr.addPageAction('angular_page_view', {
        url,
        timestamp: Date.now(),
        framework: 'angular',
        ...customAttributes
      });
    }
  }

  trackCustomEvent(name: string, attributes: any): void {
    if (this.nr) {
      this.nr.addPageAction(name, {
        ...attributes,
        framework: 'angular',
        timestamp: Date.now()
      });
    }
  }

  trackError(error: Error, customAttributes?: any): void {
    if (this.nr) {
      this.nr.noticeError(error, {
        ...customAttributes,
        framework: 'angular',
        angular_version: '15.0.0'
      });
    }
  }

  setCustomAttribute(key: string, value: any): void {
    if (this.nr) {
      this.nr.setCustomAttribute(key, value);
    }
  }

  setUserContext(userId: string, attributes?: any): void {
    if (this.nr) {
      this.nr.setUserId(userId);
      
      if (attributes) {
        Object.entries(attributes).forEach(([key, value]) => {
          this.nr.setCustomAttribute(`user.${key}`, value);
        });
      }
    }
  }

  measurePerformance(metricName: string, startTime: number): number {
    if (this.nr) {
      const duration = performance.now() - startTime;
      this.nr.recordMetric(`Custom/Angular/${metricName}`, duration);
      return duration;
    }
    return 0;
  }
}

// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { NewRelicService } from './services/newrelic.service';
import { filter } from 'rxjs/operators';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
  
  constructor(
    private router: Router,
    private newrelic: NewRelicService
  ) {}

  ngOnInit(): void {
    // ルートチェンジの自動追跡
    this.router.events
      .pipe(filter(event => event instanceof NavigationEnd))
      .subscribe((event: NavigationEnd) => {
        this.newrelic.trackPageView(event.urlAfterRedirects, {
          route_url: event.url,
          route_state: event.state
        });
      });
    
    // アプリケーション情報の設定
    this.newrelic.setCustomAttribute('app.name', 'My Angular App');
    this.newrelic.setCustomAttribute('app.version', '1.0.0');
    this.newrelic.setCustomAttribute('angular.version', '15.0.0');
  }
}

// コンポーネントでの使用例
// src/app/components/product-list.component.ts
import { Component, OnInit } from '@angular/core';
import { NewRelicService } from '../services/newrelic.service';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html'
})
export class ProductListComponent implements OnInit {
  products: any[] = [];
  loading = true;

  constructor(private newrelic: NewRelicService) {}

  ngOnInit(): void {
    const startTime = performance.now();
    
    this.newrelic.trackCustomEvent('product_list_load_start', {
      component: 'ProductListComponent'
    });

    this.loadProducts().subscribe({
      next: (products) => {
        this.products = products;
        this.loading = false;
        
        const loadTime = this.newrelic.measurePerformance('ProductListLoad', startTime);
        
        this.newrelic.trackCustomEvent('product_list_load_success', {
          product_count: products.length,
          load_time: loadTime,
          component: 'ProductListComponent'
        });
      },
      error: (error) => {
        this.loading = false;
        
        this.newrelic.trackError(error, {
          component: 'ProductListComponent',
          action: 'loadProducts'
        });
        
        this.newrelic.trackCustomEvent('product_list_load_error', {
          error_message: error.message,
          component: 'ProductListComponent'
        });
      }
    });
  }
  
  onProductClick(product: any): void {
    this.newrelic.trackCustomEvent('product_click', {
      product_id: product.id,
      product_name: product.name,
      product_price: product.price,
      product_category: product.category,
      component: 'ProductListComponent'
    });
  }
  
  private loadProducts() {
    // 実際のデータ取得ロジック
    return this.productService.getProducts();
  }
}

9.3 JavaScript エラー追跡

🚨 エラー分類とコンテキスト情報

javascript
// src/utils/errorTracker.js
class BrowserErrorTracker {
  static initializeErrorHandling() {
    // グローバルエラーハンドラー
    window.addEventListener('error', (event) => {
      this.trackJavaScriptError(event.error, {
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno,
        type: 'javascript_error',
        event_type: 'error'
      });
    });
    
    // Promise rejection ハンドラー
    window.addEventListener('unhandledrejection', (event) => {
      this.trackJavaScriptError(event.reason, {
        type: 'unhandled_promise_rejection',
        promise: event.promise?.toString?.() || 'Unknown Promise',
        event_type: 'unhandledrejection'
      });
    });
    
    // リソース読み込みエラー
    window.addEventListener('error', (event) => {
      if (event.target !== window) {
        this.trackResourceError(event);
      }
    }, true);
    
    // ネットワークエラー監視
    this.monitorNetworkErrors();
  }
  
  static trackJavaScriptError(error, context = {}) {
    if (!window.newrelic || !error) return;
    
    // エラー分類
    const errorType = this.classifyError(error);
    
    // コンテキスト情報を充実
    const enrichedContext = {
      ...context,
      'error.type': errorType,
      'error.message': error.message,
      'error.stack': error.stack,
      'error.name': error.name,
      'browser.userAgent': navigator.userAgent,
      'browser.url': window.location.href,
      'browser.timestamp': new Date().toISOString(),
      'browser.viewport': `${window.innerWidth}x${window.innerHeight}`,
      'browser.language': navigator.language,
      'user.sessionId': this.getSessionId(),
      'app.version': this.getAppVersion(),
      'performance.memory': this.getMemoryInfo()
    };
    
    // New Relic にエラー送信
    window.newrelic.noticeError(error, enrichedContext);
    
    // カスタムメトリクス更新
    window.newrelic.recordMetric(`Custom/Errors/${errorType}`, 1);
    
    // 重要度に応じた追加処理
    if (this.isCriticalError(error)) {
      this.handleCriticalError(error, enrichedContext);
    }
  }
  
  static trackResourceError(event) {
    if (!window.newrelic) return;
    
    const element = event.target;
    const resourceType = element.tagName?.toLowerCase() || 'unknown';
    
    const errorData = {
      'resource.type': resourceType,
      'resource.src': element.src || element.href || 'unknown',
      'resource.currentSrc': element.currentSrc || '',
      'error.type': 'resource_load_error',
      'browser.url': window.location.href,
      'timestamp': new Date().toISOString()
    };
    
    // リソースエラーとして記録
    window.newrelic.addPageAction('resource_load_error', errorData);
    window.newrelic.recordMetric(`Custom/ResourceErrors/${resourceType}`, 1);
  }
  
  static monitorNetworkErrors() {
    // Fetch API エラー監視
    const originalFetch = window.fetch;
    window.fetch = function(...args) {
      return originalFetch.apply(this, args)
        .then(response => {
          if (!response.ok) {
            BrowserErrorTracker.trackNetworkError('fetch', args[0], response.status, response.statusText);
          }
          return response;
        })
        .catch(error => {
          BrowserErrorTracker.trackNetworkError('fetch', args[0], null, error.message);
          throw error;
        });
    };
    
    // XMLHttpRequest エラー監視
    const originalXHROpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url, ...args) {
      this._url = url;
      this._method = method;
      
      this.addEventListener('error', () => {
        BrowserErrorTracker.trackNetworkError('xhr', url, null, 'Network Error');
      });
      
      this.addEventListener('load', () => {
        if (this.status >= 400) {
          BrowserErrorTracker.trackNetworkError('xhr', url, this.status, this.statusText);
        }
      });
      
      return originalXHROpen.apply(this, [method, url, ...args]);
    };
  }
  
  static trackNetworkError(requestType, url, status, message) {
    if (!window.newrelic) return;
    
    window.newrelic.addPageAction('network_error', {
      'request.type': requestType,
      'request.url': url,
      'response.status': status,
      'error.message': message,
      'timestamp': new Date().toISOString()
    });
    
    window.newrelic.recordMetric(`Custom/NetworkErrors/${requestType}`, 1);
  }
  
  static classifyError(error) {
    const errorClasses = {
      'TypeError': 'type_error',
      'ReferenceError': 'reference_error',
      'SyntaxError': 'syntax_error',
      'RangeError': 'range_error',
      'URIError': 'uri_error',
      'EvalError': 'eval_error',
      'NetworkError': 'network_error',
      'SecurityError': 'security_error',
      'ChunkLoadError': 'chunk_load_error',
      'TimeoutError': 'timeout_error'
    };
    
    const errorName = error.constructor?.name || error.name || 'UnknownError';
    return errorClasses[errorName] || 'unknown_error';
  }
  
  static isCriticalError(error) {
    const criticalPatterns = [
      /payment/i,
      /checkout/i,
      /security/i,
      /authentication/i,
      /unauthorized/i
    ];
    
    const errorMessage = error.message || error.toString();
    return criticalPatterns.some(pattern => pattern.test(errorMessage));
  }
  
  static handleCriticalError(error, context) {
    if (!window.newrelic) return;
    
    // 重要なエラーのマーキング
    window.newrelic.addCustomAttribute('critical_error', true);
    window.newrelic.addCustomAttribute('alert_level', 'critical');
    window.newrelic.recordMetric('Custom/Errors/Critical', 1);
    
    // 追加のコンテキスト情報
    window.newrelic.addPageAction('critical_error_occurred', {
      error_message: error.message,
      error_stack: error.stack,
      user_session: this.getSessionId(),
      timestamp: new Date().toISOString()
    });
  }
  
  static getSessionId() {
    let sessionId = sessionStorage.getItem('nr_session_id');
    if (!sessionId) {
      sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
      sessionStorage.setItem('nr_session_id', sessionId);
    }
    return sessionId;
  }
  
  static getAppVersion() {
    return process.env.REACT_APP_VERSION || 
           window.APP_VERSION || 
           '1.0.0';
  }
  
  static getMemoryInfo() {
    if (performance.memory) {
      return {
        used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024),
        total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024),
        limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024)
      };
    }
    return null;
  }
}

// 初期化
BrowserErrorTracker.initializeErrorHandling();

export default BrowserErrorTracker;

9.4 Core Web Vitals測定

📊 パフォーマンス最適化の実践

javascript
// src/utils/performanceTracker.js
class CoreWebVitalsTracker {
  static initializeTracking() {
    // LCP (Largest Contentful Paint) 測定
    this.measureLCP();
    
    // FID (First Input Delay) 測定
    this.measureFID();
    
    // CLS (Cumulative Layout Shift) 測定
    this.measureCLS();
    
    // FCP (First Contentful Paint) 測定
    this.measureFCP();
    
    // TTI (Time to Interactive) 測定
    this.measureTTI();
    
    // その他のカスタムメトリクス
    this.measureCustomMetrics();
  }
  
  static measureLCP() {
    try {
      const observer = new PerformanceObserver((list) => {
        const entries = list.getEntries();
        const lastEntry = entries[entries.length - 1];
        
        if (window.newrelic) {
          const lcpValue = lastEntry.startTime;
          
          window.newrelic.recordMetric('Custom/CoreWebVitals/LCP', lcpValue);
          window.newrelic.setCustomAttribute('lcp.value', lcpValue);
          window.newrelic.setCustomAttribute('lcp.element', lastEntry.element?.tagName);
          window.newrelic.setCustomAttribute('lcp.size', lastEntry.size);
          
          // パフォーマンス評価
          const lcpGrade = this.gradeLCP(lcpValue);
          window.newrelic.setCustomAttribute('lcp.grade', lcpGrade);
          
          window.newrelic.addPageAction('core_web_vitals_lcp', {
            value: lcpValue,
            grade: lcpGrade,
            element: lastEntry.element?.tagName,
            size: lastEntry.size
          });
        }
        
        // LCP最適化アドバイス
        this.provideLCPOptimizationAdvice(lastEntry.startTime);
      });
      
      observer.observe({ entryTypes: ['largest-contentful-paint'] });
    } catch (error) {
      console.warn('LCP measurement not supported:', error);
    }
  }
  
  static measureFID() {
    try {
      const observer = new PerformanceObserver((list) => {
        const entries = list.getEntries();
        
        entries.forEach(entry => {
          if (window.newrelic) {
            const fidValue = entry.processingStart - entry.startTime;
            
            window.newrelic.recordMetric('Custom/CoreWebVitals/FID', fidValue);
            window.newrelic.setCustomAttribute('fid.value', fidValue);
            window.newrelic.setCustomAttribute('fid.inputType', entry.name);
            
            // パフォーマンス評価
            const fidGrade = this.gradeFID(fidValue);
            window.newrelic.setCustomAttribute('fid.grade', fidGrade);
            
            window.newrelic.addPageAction('core_web_vitals_fid', {
              value: fidValue,
              grade: fidGrade,
              input_type: entry.name,
              target: entry.target?.tagName
            });
          }
        });
      });
      
      observer.observe({ entryTypes: ['first-input'], buffered: true });
    } catch (error) {
      console.warn('FID measurement not supported:', error);
    }
  }
  
  static measureCLS() {
    try {
      let clsValue = 0;
      let sessionValue = 0;
      let sessionEntries = [];
      
      const observer = new PerformanceObserver((list) => {
        const entries = list.getEntries();
        
        entries.forEach(entry => {
          if (!entry.hadRecentInput) {
            const firstSessionEntry = sessionEntries[0];
            const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
            
            if (sessionValue && 
                entry.startTime - lastSessionEntry.startTime < 1000 &&
                entry.startTime - firstSessionEntry.startTime < 5000) {
              sessionValue += entry.value;
              sessionEntries.push(entry);
            } else {
              sessionValue = entry.value;
              sessionEntries = [entry];
            }
            
            if (sessionValue > clsValue) {
              clsValue = sessionValue;
              
              if (window.newrelic) {
                window.newrelic.recordMetric('Custom/CoreWebVitals/CLS', clsValue);
                window.newrelic.setCustomAttribute('cls.value', clsValue);
                
                // パフォーマンス評価
                const clsGrade = this.gradeCLS(clsValue);
                window.newrelic.setCustomAttribute('cls.grade', clsGrade);
                
                window.newrelic.addPageAction('core_web_vitals_cls', {
                  value: clsValue,
                  grade: clsGrade,
                  shift_count: sessionEntries.length
                });
              }
            }
          }
        });
      });
      
      observer.observe({ entryTypes: ['layout-shift'], buffered: true });
    } catch (error) {
      console.warn('CLS measurement not supported:', error);
    }
  }
  
  static measureFCP() {
    try {
      const observer = new PerformanceObserver((list) => {
        const entries = list.getEntries();
        
        entries.forEach(entry => {
          if (entry.name === 'first-contentful-paint') {
            if (window.newrelic) {
              const fcpValue = entry.startTime;
              
              window.newrelic.recordMetric('Custom/CoreWebVitals/FCP', fcpValue);
              window.newrelic.setCustomAttribute('fcp.value', fcpValue);
              
              const fcpGrade = this.gradeFCP(fcpValue);
              window.newrelic.setCustomAttribute('fcp.grade', fcpGrade);
              
              window.newrelic.addPageAction('core_web_vitals_fcp', {
                value: fcpValue,
                grade: fcpGrade
              });
            }
          }
        });
      });
      
      observer.observe({ entryTypes: ['paint'], buffered: true });
    } catch (error) {
      console.warn('FCP measurement not supported:', error);
    }
  }
  
  static measureCustomMetrics() {
    // ページロード完了時の測定
    window.addEventListener('load', () => {
      if (window.newrelic && performance.timing) {
        const timing = performance.timing;
        
        // ページロード時間
        const pageLoadTime = timing.loadEventEnd - timing.navigationStart;
        window.newrelic.recordMetric('Custom/Performance/PageLoadTime', pageLoadTime);
        
        // DOM構築時間
        const domBuildTime = timing.domContentLoadedEventEnd - timing.domLoading;
        window.newrelic.recordMetric('Custom/Performance/DOMBuildTime', domBuildTime);
        
        // リソース読み込み時間
        const resourceLoadTime = timing.loadEventEnd - timing.domContentLoadedEventEnd;
        window.newrelic.recordMetric('Custom/Performance/ResourceLoadTime', resourceLoadTime);
        
        // DNS解決時間
        const dnsTime = timing.domainLookupEnd - timing.domainLookupStart;
        window.newrelic.recordMetric('Custom/Performance/DNSTime', dnsTime);
        
        // サーバー応答時間
        const serverResponseTime = timing.responseEnd - timing.requestStart;
        window.newrelic.recordMetric('Custom/Performance/ServerResponseTime', serverResponseTime);
        
        window.newrelic.addPageAction('page_performance_complete', {
          page_load_time: pageLoadTime,
          dom_build_time: domBuildTime,
          resource_load_time: resourceLoadTime,
          dns_time: dnsTime,
          server_response_time: serverResponseTime
        });
      }
    });
    
    // バンドルサイズ測定
    this.measureBundleSize();
    
    // メモリ使用量測定
    this.measureMemoryUsage();
  }
  
  static measureBundleSize() {
    if (performance.getEntriesByType) {
      const resources = performance.getEntriesByType('resource');
      let totalSize = 0;
      let jsSize = 0;
      let cssSize = 0;
      
      resources.forEach(resource => {
        if (resource.transferSize) {
          totalSize += resource.transferSize;
          
          if (resource.name.includes('.js')) {
            jsSize += resource.transferSize;
          } else if (resource.name.includes('.css')) {
            cssSize += resource.transferSize;
          }
        }
      });
      
      if (window.newrelic) {
        window.newrelic.recordMetric('Custom/Performance/TotalBundleSize', totalSize);
        window.newrelic.recordMetric('Custom/Performance/JSBundleSize', jsSize);
        window.newrelic.recordMetric('Custom/Performance/CSSBundleSize', cssSize);
      }
    }
  }
  
  static measureMemoryUsage() {
    if (performance.memory) {
      setInterval(() => {
        if (window.newrelic) {
          const memInfo = performance.memory;
          
          window.newrelic.recordMetric('Custom/Memory/Used', memInfo.usedJSHeapSize);
          window.newrelic.recordMetric('Custom/Memory/Total', memInfo.totalJSHeapSize);
          window.newrelic.recordMetric('Custom/Memory/Limit', memInfo.jsHeapSizeLimit);
          
          // メモリ使用率
          const memoryUsagePercent = (memInfo.usedJSHeapSize / memInfo.jsHeapSizeLimit) * 100;
          window.newrelic.recordMetric('Custom/Memory/UsagePercent', memoryUsagePercent);
          
          // メモリ使用量が多い場合の警告
          if (memoryUsagePercent > 80) {
            window.newrelic.addPageAction('high_memory_usage', {
              usage_percent: memoryUsagePercent,
              used_mb: Math.round(memInfo.usedJSHeapSize / 1024 / 1024),
              total_mb: Math.round(memInfo.totalJSHeapSize / 1024 / 1024)
            });
          }
        }
      }, 30000); // 30秒間隔
    }
  }
  
  // パフォーマンスグレード評価
  static gradeLCP(value) {
    if (value <= 2500) return 'good';
    if (value <= 4000) return 'needs_improvement';
    return 'poor';
  }
  
  static gradeFID(value) {
    if (value <= 100) return 'good';
    if (value <= 300) return 'needs_improvement';
    return 'poor';
  }
  
  static gradeCLS(value) {
    if (value <= 0.1) return 'good';
    if (value <= 0.25) return 'needs_improvement';
    return 'poor';
  }
  
  static gradeFCP(value) {
    if (value <= 1800) return 'good';
    if (value <= 3000) return 'needs_improvement';
    return 'poor';
  }
  
  // 最適化アドバイス
  static provideLCPOptimizationAdvice(lcpValue) {
    if (lcpValue > 4000) {
      console.warn('LCP最適化が必要です:');
      console.warn('- 画像の最適化とレスポンシブ対応');
      console.warn('- クリティカルCSSの優先読み込み');
      console.warn('- サーバーサイドレンダリングの検討');
      console.warn('- CDNの活用');
    }
  }
}

// 初期化
CoreWebVitalsTracker.initializeTracking();

export default CoreWebVitalsTracker;

🎉 Browser・RUM監視 マスター完了

✅ 習得チェックリスト

Browser・RUM監視を完了すると、以下ができるようになります:

  • [ ] RUM(Real User Monitoring)の効果的な実装ができる
  • [ ] React・Vue・Angularでの高度な統合設定ができる
  • [ ] JavaScript エラーの包括的な追跡ができる
  • [ ] Core Web Vitalsの測定と最適化ができる
  • [ ] ユーザー行動の詳細分析ができる
  • [ ] フロントエンドパフォーマンスの最適化ができる

📈 実現できるユーザー体験改善

yaml
User_Experience_Improvements:
  パフォーマンス向上:
    - Core Web Vitals全項目で目標値達成
    - ページロード時間50%短縮
    - JavaScript エラー率90%削減
    
  ユーザー体験の可視化:
    - リアルユーザーのパフォーマンス測定
    - デバイス・ブラウザ別の詳細分析
    - 地理的分散での性能差把握
    
  問題の早期発見:
    - JavaScript エラーの即座な検知
    - パフォーマンス劣化の自動検出
    - ユーザー離脱の原因分析

🚀 次のステップ

Browser・RUM監視をマスターしたら、以下の学習を進めてください:

  1. 第10章: New Relic Logs - ログ管理とコンテキスト分析
  2. 第11章: データ分析とカスタマイゼーション - NRQL習得とダッシュボード作成

💡 運用のベストプラクティス

yaml
Browser_Monitoring_Best_Practices:
  日常監視:
    - Core Web Vitalsの日次確認
    - JavaScript エラー率のトレンド分析
    - ユーザーセッション品質の定期評価
  
  パフォーマンス最適化:
    - LCP要素の継続的最適化
    - CLS原因の定期的調査
    - FID改善のための処理最適化
  
  ユーザー体験分析:
    - コンバージョンファネルでの離脱分析
    - デバイス・ブラウザ別性能比較
    - 地域別パフォーマンス評価

📖 関連記事:第1章: New Relicとは、New Relicの優位性
第3章: New Relicプラットフォーム概要
第5章: 高度な活用とROI最大化
第8章: New Relic Mobile

🔍 次の学習: 第10章 New Relic Logs で、ログ管理とコンテキスト分析を習得しましょう!