在 Next.js 里集成 PageSpy:打造远程调试利器

421 阅读3分钟

WX20250514-182010@2x.png

前言

在生产环境调试 Next.js 应用时,开发者常常面临三大挑战:

  1. 混合渲染模式下客户端异常难以定位
  2. 用户环境信息(设备、网络、存储等)收集困难
  3. 偶现问题因缺乏现场数据难以复现

本文将通过集成 PageSpy ,实现以下调试能力增强:

  • 🎯 全生命周期错误追踪(CSR/SSR)
  • 📊 网络请求瀑布流分析
  • ⏪ 用户操作轨迹回放(基于 rrweb)
  • 📥 离线日志缓存与上传

我们开始吧!

一、前置条件

方案选型对比

集成方式适用场景包体积增量可维护性
Npm 包插件扩展、可读性高较大
Script 注入快速接入、对于SSR收集更及时(更早)、轻量级调试较小

二、集成方案

方案一:Npm 包集成

步骤 1:安装核心依赖

# 核心库
npm install @huolala-tech/page-spy-browser

# 离线日志插件(可选)
npm install @huolala-tech/page-spy-plugin-data-harbor

# 用户行为录制插件(可选)
npm install @huolala-tech/page-spy-plugin-rrweb

步骤 2:初始化配置

import { AppProps } from 'next/app';
import './app.css';
import DebugPackage from './DebugPackage';

const DEBUGS = ['local', 'dev', 'development']

function MyApp(appProps: AppProps) {
  const { Component, pageProps } = appProps
  const isDebug = DEBUGS.includes(process.env.NEXT_NODE_ENV) || Boolean(pageProps?.data?.debug)
  return <>
    <DebugPackage isDebug={isDebug} />
    <Component {...pageProps} />
  </>
}

export default MyApp

'use client';
import { useEffect, useRef } from 'react';

/**
 * 异步按需加载页面监控脚本
 * @description 该脚本用于加载页面监控的相关插件
 * 1. DataHarborPlugin: 缓存离线日志数据,提供下载 / 上传日志功能
 * 2. RRWebPlugin: 将用户操作轨迹记录到离线日志中
 * 3. PageSpy: 页面监控插件
 * 4. PageSpy.registerPlugin: 注册插件
 */
export default function DebugPackage({ isDebug }: { isDebug: boolean }) {
  const pageSpyRef = useRef(null);
  const vConsoleRef = useRef(null);
  useEffect(() => {
    if (isDebug) {
      Promise.all([
        import('@huolala-tech/page-spy-browser'),
        import('@huolala-tech/page-spy-plugin-data-harbor'),
        import('@huolala-tech/page-spy-plugin-rrweb')
      ]).then(([PageSpy, { default: DataHarborPlugin }, { default: RRWebPlugin }]) => {
        // 初始化插件(必须早于"初始化PageSpy"之前初始化&注册插件,否则插件会失效)
        const harbor = new DataHarborPlugin({
          maximum: 1 * 1024 * 1024
        });
        const rrweb = new RRWebPlugin();

        // 注册插件
        [harbor, rrweb].forEach(p => {
          PageSpy.default.registerPlugin(p);
        });

        // 初始化PageSpy
        // eslint-disable-next-line new-cap
        pageSpyRef.current = new PageSpy.default({
          api: '你的服务地址',
          clientOrigin: `${window.location.protocol}//你的服务地址`,
          project: '你的项目名称',
          autoRender: isDebug,
          title: '你的标题'
        });
      });
    }
    return () => {
      pageSpyRef.current?.abort();
    };
  }, [isDebug]);

  return null;
}

方案二:Script注入(推荐)

import { AppProps } from 'next/app';
import Head from 'next/head';
import './app.css';

const DEBUGS = ['local', 'dev', 'development']

function MyApp(appProps: AppProps) {
  const { Component, pageProps } = appProps
  const isDebug = DEBUGS.includes(process.env.NEXT_NODE_ENV) || Boolean(pageProps?.data?.debug)
  return <>
    <Head>
      {/* 尽可能早的加载PageSpy进行页面捕捉 */}
      {isDebug && (<>
        <script crossOrigin="anonymous" src="//你的服务地址/page-spy/index.min.js" />
        <script crossOrigin="anonymous" src="//你的服务地址/plugin/data-harbor/index.min.js" />
        <script crossOrigin="anonymous" src="//你的服务地址/plugin/rrweb/index.min.js" />
        <script dangerouslySetInnerHTML={{
          __html: `
          (function (w, __d, __s) {
            if(!w.$harbor) { w.$harbor = new w.DataHarborPlugin({ maximum: 1 * 1024 * 1024 }); }
            if(!w.$rrweb) { w.$rrweb = new w.RRWebPlugin(); }
            [w.$harbor, w.$rrweb].forEach(p => { w.PageSpy.registerPlugin(p); })
            w.$pageSpy = new w.PageSpy({
              api: '你的服务地址',
              title: w.location.href,
              project: '你的项目名称',
              autoRender: true
            });
          })(window,document,"script")
        `
        }} />
      </>)}
      <meta charSet="utf-8" />
      <title>在 Next.js 里集成 PageSpy:打造远程调试利器</title>
      ...其它    
    </Head>
    <Component {...pageProps} />
  </>
}

export default MyApp

三、其它方案

'use client';
import Script from 'next/script';

/**
 * 异步按需加载页面监控脚本,直接使用了Script的注入方式
 * @description 一、该脚本用于加载页面监控的相关插件
 * 1. DataHarborPlugin: 缓存离线日志数据,提供下载 / 上传日志功能
 * 2. RRWebPlugin: 将用户操作轨迹记录到离线日志中
 * 3. PageSpy: 页面监控插件
 * 4. PageSpy.registerPlugin: 注册插件
 * @description 二、PageSpy纯离线方案(更多细节可关注官网)
 * @description 三、该脚本用于加载vconsole调试工具
 */
export default function DebugScript({ isDebug }: { isDebug: boolean }) {
  return (<>
    <Script
      src="//你的服务地址/plugin/data-harbor/index.min.js"
      // strategy="lazyOnload"
      onLoad={() => {
        window.$harbor = new window.DataHarborPlugin({
          maximum: 1 * 1024 * 1024 // 内存中的数据记录达到 1M 时写入临时文件
        });
      }}
    />
    <Script
      src="//你的服务地址/plugin/rrweb/index.min.js"
      // strategy="lazyOnload"
      onLoad={() => {
        window.$rrweb = new window.RRWebPlugin();
      }}
    />
    <Script
      src="//你的服务地址/page-spy/index.min.js"
      // strategy="lazyOnload"
      onLoad={() => {
        [window.$harbor, window.$rrweb].forEach(p => {
          window.PageSpy.registerPlugin(p);
        })
        window.$pageSpy = new window.PageSpy({
          api: '你的服务地址',
          clientOrigin: `${window.location.protocol}//你的服务地址`,
          project: '你的项目名称',
          autoRender: isDebug,
          title: window.location.href
        });
      }}
    />
    {/* 纯离线方案 */}
    {/* <Script
      src="https://unpkg.com/@huolala-tech/page-spy-plugin-ospy"
      strategy="lazyOnload"
      onLoad={() => {
        window.$oSpy = new window.OSpy({
          // api: '你的服务地址',
          // project: '你的项目名称',
          title: '离线日志',
          autoRender: isDebug
        });
      }}
    /> */}
    {
      isDebug && (
        <Script
          src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"
          // strategy="lazyOnload"
          onLoad={() => {
            window.vConsole = new window.VConsole();
            setTimeout(() => {
              const vcId = document.getElementById('__vconsole')
              // eslint-disable-next-line max-nested-callbacks
              vcId?.addEventListener('click', (e) => e.stopPropagation())
              if (vcId) {
                vcId.style.position = 'relative'
                vcId.style.zIndex = 1000000
              }
            }, 1000)
          }}
        />
      )
    }
  </>);
}