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監視をマスターしたら、以下の学習を進めてください:
- 第10章: New Relic Logs - ログ管理とコンテキスト分析
- 第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 で、ログ管理とコンテキスト分析を習得しましょう!