New Relic Vue.js統合ガイド - Vue.jsアプリケーションの効果的な監視

Vue.jsは、そのリアクティブデータシステムと柔軟なアーキテクチャにより、開発者に優れた体験を提供しますが、その独特なライフサイクルとデータフローは監視において特別な考慮が必要です。New Relic Browser AgentとVue.jsの統合により、リアクティブシステムの動作、コンポーネントのパフォーマンス、ユーザーインタラクションを詳細に追跡できます。

Vue.js監視の特徴

Vue.jsアプリケーションの監視では、そのアーキテクチャ固有の要素を理解することが重要です。

リアクティブシステムの監視

Vue.jsのリアクティブシステムは、データの変更を検知して自動的にDOMを更新しますが、この過程でのパフォーマンス影響を監視することが重要です。

データ変更の追跡により、プロパティの更新頻度と再レンダリングの関係を分析できます。ウォッチャーの効率性では、watchcomputedプロパティの実行時間を測定して最適化の機会を特定します。

Vue Routerとの統合

Vue Routerを使用するアプリケーションでは、SPA特有のルーティング監視が必要です。

javascript
// Vue Router 4との統合
import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/products', component: Products },
    { path: '/dashboard', component: Dashboard }
  ]
});

// ルート変更の監視
router.afterEach((to, from) => {
  if (typeof newrelic !== 'undefined') {
    newrelic.addPageAction('vue_route_change', {
      toPath: to.path,
      toName: to.name,
      fromPath: from.path,
      fromName: from.name,
      params: JSON.stringify(to.params),
      query: JSON.stringify(to.query),
      timestamp: Date.now()
    });
  }
});

export default router;

Vue.js プラグインとしての実装

New RelicをVue.jsプラグインとして実装することで、アプリケーション全体で一貫した監視を実現できます。

基本的なプラグイン実装

javascript
// new-relic-vue-plugin.js
const NewRelicVuePlugin = {
  install(app, options = {}) {
    // グローバルエラーハンドラー
    app.config.errorHandler = (err, instance, info) => {
      if (typeof newrelic !== 'undefined') {
        newrelic.noticeError(err, {
          vueComponent: instance?.$options.name || 'Anonymous',
          vueInfo: info,
          instanceData: JSON.stringify(instance?.$data || {}),
          propsData: JSON.stringify(instance?.$props || {}),
          vueVersion: app.version,
          errorInfo: err.stack
        });
      }
      
      // 開発環境でのログ出力
      if (process.env.NODE_ENV === 'development') {
        console.error('Vue Error Handler:', err);
        console.error('Component Info:', info);
      }
    };
    
    // グローバルプロパティの追加
    app.config.globalProperties.$newrelic = {
      trackEvent: (eventName, attributes = {}) => {
        if (typeof newrelic !== 'undefined') {
          newrelic.addPageAction(eventName, {
            ...attributes,
            timestamp: Date.now(),
            userAgent: navigator.userAgent
          });
        }
      },
      
      trackError: (error, context = {}) => {
        if (typeof newrelic !== 'undefined') {
          newrelic.noticeError(error, context);
        }
      }
    };
    
    // Vue Router統合
    if (options.router) {
      options.router.afterEach((to, from) => {
        if (typeof newrelic !== 'undefined') {
          newrelic.addPageAction('vue_navigation', {
            to: to.path,
            from: from.path,
            name: to.name,
            timestamp: Date.now()
          });
        }
      });
    }
    
    // Vuex統合
    if (options.store) {
      options.store.subscribe((mutation, state) => {
        if (typeof newrelic !== 'undefined') {
          newrelic.addPageAction('vuex_mutation', {
            type: mutation.type,
            payload: JSON.stringify(mutation.payload).slice(0, 200),
            timestamp: Date.now()
          });
        }
      });
    }
  }
};

export default NewRelicVuePlugin;

プラグインの使用

javascript
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import NewRelicVuePlugin from './plugins/new-relic-vue-plugin';

const app = createApp(App);

app.use(router);
app.use(store);
app.use(NewRelicVuePlugin, { router, store });

app.mount('#app');

Composition APIとの統合

Vue 3のComposition APIを活用して、コンポーネントレベルでの詳細な監視を実装します。

パフォーマンス追跡のComposable

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

export function usePerformanceTracking(componentName) {
  const renderCount = ref(0);
  const mountTime = ref(null);
  
  onMounted(() => {
    mountTime.value = performance.now();
    
    if (typeof newrelic !== 'undefined') {
      newrelic.addPageAction('vue_component_mounted', {
        component: componentName,
        mountTime: mountTime.value,
        timestamp: Date.now()
      });
    }
  });
  
  onUpdated(() => {
    renderCount.value++;
    
    nextTick(() => {
      if (typeof newrelic !== 'undefined') {
        newrelic.addPageAction('vue_component_updated', {
          component: componentName,
          renderCount: renderCount.value,
          timeSinceMount: performance.now() - (mountTime.value || 0),
          timestamp: Date.now()
        });
      }
    });
  });
  
  onUnmounted(() => {
    const lifeTime = performance.now() - (mountTime.value || 0);
    
    if (typeof newrelic !== 'undefined') {
      newrelic.addPageAction('vue_component_unmounted', {
        component: componentName,
        lifeTime: lifeTime,
        totalRenders: renderCount.value,
        timestamp: Date.now()
      });
    }
  });
  
  return {
    renderCount: renderCount.value
  };
}

// API追跡のComposable
export function useApiTracking() {
  const trackApiCall = async (apiName, apiFunction, context = {}) => {
    const startTime = performance.now();
    
    try {
      const result = await apiFunction();
      const duration = performance.now() - startTime;
      
      if (typeof newrelic !== 'undefined') {
        newrelic.addPageAction('vue_api_success', {
          apiName,
          duration,
          context: JSON.stringify(context),
          resultSize: JSON.stringify(result).length,
          timestamp: Date.now()
        });
      }
      
      return result;
    } catch (error) {
      const duration = performance.now() - startTime;
      
      if (typeof newrelic !== 'undefined') {
        newrelic.addPageAction('vue_api_error', {
          apiName,
          duration,
          error: error.message,
          context: JSON.stringify(context),
          timestamp: Date.now()
        });
        
        newrelic.noticeError(error, {
          apiCall: true,
          apiName,
          context
        });
      }
      
      throw error;
    }
  };
  
  return { trackApiCall };
}

// リアクティブデータの変更追跡
export function useReactivityTracking(watchTargets, componentName) {
  const changeCount = ref(0);
  
  watchEffect(() => {
    changeCount.value++;
    
    if (typeof newrelic !== 'undefined' && changeCount.value > 1) {
      newrelic.addPageAction('vue_reactivity_change', {
        component: componentName,
        changeCount: changeCount.value,
        watchTargetsCount: Object.keys(watchTargets).length,
        timestamp: Date.now()
      });
    }
  });
  
  return { changeCount };
}

使用例

javascript
// ProductList.vue
<template>
  <div>
    <h2>商品一覧 (更新回数: {{ renderCount }})</h2>
    <div v-if="loading">読み込み中...</div>
    <div v-else>
      <product-card
        v-for="product in products"
        :key="product.id"
        :product="product"
      />
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { usePerformanceTracking, useApiTracking, useReactivityTracking } from '@/composables/useNewRelicTracking';

const props = defineProps(['category']);

const products = ref([]);
const loading = ref(false);

// パフォーマンス追跡
const { renderCount } = usePerformanceTracking('ProductList');

// API追跡
const { trackApiCall } = useApiTracking();

// リアクティビティ追跡
const { changeCount } = useReactivityTracking(
  { products, loading, category: props.category },
  'ProductList'
);

// データ取得
onMounted(async () => {
  loading.value = true;
  
  try {
    const result = await trackApiCall(
      'fetchProducts',
      () => fetch(`/api/products?category=${props.category}`).then(r => r.json()),
      { category: props.category }
    );
    
    products.value = result;
  } catch (error) {
    console.error('商品取得エラー:', error);
  } finally {
    loading.value = false;
  }
});
</script>

Vuex/Pinia状態管理との統合

Vue.jsの状態管理ライブラリとの統合により、アプリケーションの状態変更を詳細に監視できます。

Vuex統合

javascript
// store/index.js
import { createStore } from 'vuex';

const store = createStore({
  state: {
    user: null,
    products: [],
    cart: []
  },
  
  mutations: {
    SET_USER(state, user) {
      state.user = user;
    },
    ADD_TO_CART(state, product) {
      state.cart.push(product);
    }
  },
  
  actions: {
    async fetchProducts({ commit }, category) {
      const startTime = performance.now();
      
      try {
        const response = await fetch(`/api/products?category=${category}`);
        const products = await response.json();
        
        commit('SET_PRODUCTS', products);
        
        const duration = performance.now() - startTime;
        
        if (typeof newrelic !== 'undefined') {
          newrelic.addPageAction('vuex_action_success', {
            action: 'fetchProducts',
            duration,
            category,
            productCount: products.length,
            timestamp: Date.now()
          });
        }
        
        return products;
      } catch (error) {
        const duration = performance.now() - startTime;
        
        if (typeof newrelic !== 'undefined') {
          newrelic.addPageAction('vuex_action_error', {
            action: 'fetchProducts',
            duration,
            error: error.message,
            category,
            timestamp: Date.now()
          });
        }
        
        throw error;
      }
    }
  }
});

// Vuex プラグインでの監視
const newRelicPlugin = (store) => {
  store.subscribe((mutation, state) => {
    if (typeof newrelic !== 'undefined') {
      newrelic.addPageAction('vuex_mutation', {
        type: mutation.type,
        payload: JSON.stringify(mutation.payload).slice(0, 100),
        stateSize: JSON.stringify(state).length,
        timestamp: Date.now()
      });
    }
  });
  
  store.subscribeAction({
    before: (action, state) => {
      // アクション開始の記録
      if (typeof newrelic !== 'undefined') {
        newrelic.addPageAction('vuex_action_start', {
          type: action.type,
          payload: JSON.stringify(action.payload).slice(0, 100),
          timestamp: Date.now()
        });
      }
    },
    after: (action, state) => {
      // アクション完了の記録
      if (typeof newrelic !== 'undefined') {
        newrelic.addPageAction('vuex_action_complete', {
          type: action.type,
          timestamp: Date.now()
        });
      }
    }
  });
};

store.plugins = [newRelicPlugin];

export default store;

Pinia統合

javascript
// stores/products.js
import { defineStore } from 'pinia';

export const useProductsStore = defineStore('products', {
  state: () => ({
    products: [],
    loading: false,
    error: null
  }),
  
  actions: {
    async fetchProducts(category) {
      const startTime = performance.now();
      this.loading = true;
      this.error = null;
      
      try {
        const response = await fetch(`/api/products?category=${category}`);
        const products = await response.json();
        
        this.products = products;
        
        const duration = performance.now() - startTime;
        
        if (typeof newrelic !== 'undefined') {
          newrelic.addPageAction('pinia_action_success', {
            store: 'products',
            action: 'fetchProducts',
            duration,
            category,
            productCount: products.length,
            timestamp: Date.now()
          });
        }
      } catch (error) {
        this.error = error.message;
        
        const duration = performance.now() - startTime;
        
        if (typeof newrelic !== 'undefined') {
          newrelic.addPageAction('pinia_action_error', {
            store: 'products',
            action: 'fetchProducts',
            duration,
            error: error.message,
            category,
            timestamp: Date.now()
          });
        }
        
        throw error;
      } finally {
        this.loading = false;
      }
    }
  }
});

カスタムディレクティブでの監視

Vue.jsのカスタムディレクティブを使用して、DOM要素レベルでの監視を実装できます。

javascript
// directives/track.js
export const trackDirective = {
  mounted(el, binding) {
    const { value, arg, modifiers } = binding;
    
    const trackEvent = (event) => {
      if (typeof newrelic !== 'undefined') {
        newrelic.addPageAction(`vue_directive_${arg || 'interaction'}`, {
          element: el.tagName.toLowerCase(),
          eventType: event.type,
          value: typeof value === 'string' ? value : JSON.stringify(value),
          modifiers: Object.keys(modifiers),
          timestamp: Date.now()
        });
      }
    };
    
    // イベントタイプに応じたリスナー追加
    if (modifiers.click) {
      el.addEventListener('click', trackEvent);
    }
    
    if (modifiers.scroll) {
      el.addEventListener('scroll', trackEvent);
    }
    
    if (modifiers.input) {
      el.addEventListener('input', trackEvent);
    }
    
    // デフォルトはクリックイベント
    if (!modifiers.click && !modifiers.scroll && !modifiers.input) {
      el.addEventListener('click', trackEvent);
    }
  }
};

// 使用例
// <button v-track:button-click.click="{ action: 'purchase', product: product.id }">
//   購入する
// </button>

実装のベストプラクティス

Vue.js環境でのNew Relic監視を最適化するための推奨事項をまとめます。

パフォーマンス最適化

リアクティブシステムとの調和により、Vue.jsのリアクティブシステムに過度な負荷をかけないよう、監視データの収集と送信を適切に最適化します。

非同期処理の活用では、nextTickを使用してDOM更新後のタイミングで監視データを収集し、レンダリングをブロックしないよう配慮します。

開発体験の向上

開発用と本番用の設定分離により、開発環境では詳細な監視を行い、本番環境では必要最小限のデータのみを収集します。

TypeScript統合では、Composition APIとTypeScriptを組み合わせて、型安全な監視コードを作成します。

まとめ

New RelicとVue.jsの統合により、リアクティブシステムの動作から状態管理、ユーザーインタラクションまで、Vue.jsアプリケーションの全体像を詳細に監視できるようになります。Composition APIとプラグインシステムを活用することで、保守性が高く効率的な監視システムを構築できます。

次のステップでは、Angularアプリケーションでの統合方法について詳しく解説します。Angular特有のアーキテクチャと監視手法を学んでいきましょう。


関連記事: Angular統合ガイド関連記事: React統合ガイド