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の監視における違い
# 監視手法の比較
従来のWebサイト:
ページ読み込み: "毎回サーバーからHTML取得"
監視ポイント: "ページごとの読み込み時間"
エラー検出: "サーバーエラー中心"
状態管理: "サーバーサイド中心"
SPA:
ページ読み込み: "初回のみ、以降はJavaScriptで動的更新"
監視ポイント: "ルート遷移、状態変更、非同期処理"
エラー検出: "JavaScript実行時エラー中心"
状態管理: "クライアントサイド中心"
SPA監視で特に重要な要素
1. ルート遷移の監視 SPAでは、URLが変更されてもページ全体はリロードされません。この「仮想的なページ遷移」を適切に監視する必要があります。
// 従来のページ遷移(ブラウザが自動的にページ読み込みイベントを発火)
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の実装
// 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
};
};
実際のコンポーネントでの使用例
// 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の実装
// 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 の使用例
// 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 の実装
// 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 の使用例
// 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;
// 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 の実装
// 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 コンポーネントでの実装例
<!-- 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 プラグインの実装
// 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 };
プラグインの使用例
// 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');
<!-- コンポーネントでの使用例 -->
<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 の実装
// 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 を使用したコンポーネントの実装
// 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リクエストを自動的に監視できます。
// 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の変更検知サイクルを監視し、パフォーマンス問題を特定できます。
// 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 エラー包括追跡システム
自動エラー分類とパターン分析
効果的なエラー管理のためには、発生したエラーを自動的に分類し、パターンを分析することが重要です。
エラー分類システムの実装
// 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
});
}
}
エラー分類システムの使用例
// 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監視システム
// 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詳細分析