🔍

Next.jsの静的ページにNew Relic Browser Agentを導入する

#tech#Web

2024-11-16

はじめに

このサイト(lapla.dev)は,Next.jsで構築されていて,しばしば趣味の一環でオーバーエンジニアリング的な施策を導入してきた.今回はその一環として,New Relic Browser Agentを導入したので,それについて書く.

New Relic

New Relicは汎用的なオブザーバビリティプラットフォームで,APM(Application Performance Monitoring),ブラウザモニタリング,インフラモニタリング,ログ管理などを提供している.

以前CIでLighthouceのスコアを計測し,適宜通知する仕組みを整えたのでそのスコアだったり,ドメインがCloudflareに乗っているので,そのアナリティクス情報などは参考にしていたが,より細かい粒度でのトレースなどを閲覧できる仕組みが無かったので欲しかったというのが導入検討の大きな動機になっている.

他に検討したもの

ここでは,New Relic以外に検討したプラットフォームについて簡単に触れる.前提として,個人サイトの規模なので課金をすることには消極的なため,基本的に無料プランの内容から選定した.

Datadog

課金しようとすると高いが機能は豊富.しかし個人サイトの規模であればNew Relicで十分カバー可能.無料プランだとretentionが1日なのも渋かった(New Relicは8日).

Sentry

どちらかというとエラー検知で,パフォーマンスモニタリング用途ではNew Relicの方が適している.

Mackerel

導入が簡単なのは良いが,結局New Relicでも少し書くだけだったからあまり有意点ではない(結果論的になってしまうが).

導入手順

New Relicアカウントの作成

https://newrelic.com/signupから作る.作った後に何か聞かれるが無視して良い.

APM Agentの導入

New Relicのブラウザモニタリングは,New Relic側が提供するJavaScriptをコピペする方式と,APM Agentを導入する方式がある(参考).昨今においてはSSGであっても簡単にAPM Agentを導入できるようになったのでそちらで進める.

まずはnewrelicパッケージをプロジェクトに追加する:

$ pnpm add newrelic

次にpages/_document.tsxないしapp/layout.tsxに対して次のようなコードを書く.

Pages Routerの場合:

import newrelic from 'newrelic';
import Document, {
    DocumentContext,
    DocumentInitialProps,
    Head,
    Html,
    Main,
    NextScript,
} from 'next/document';

type ExtendedDocumentProps = DocumentInitialProps & {
    browserTimingHeader: string;
};

class document extends Document<ExtendedDocumentProps> {
    static getInitialProps = async (ctx: DocumentContext): Promise<ExtendedDocumentProps> => {
        const initialProps = await Document.getInitialProps(ctx);

        // @ts-expect-error: TS2339
        if (!newrelic.agent.collector.isConnected()) {
            await new Promise((resolve) => {
                // @ts-expect-error: TS2339
                newrelic.agent.on('connected', resolve);
            });
        }

        const browserTimingHeader = newrelic.getBrowserTimingHeader({
            hasToRemoveScriptWrapper: true,
            // @ts-expect-error: TS2353
            allowTransactionlessInjection: true,
        });

        return {
            ...initialProps,
            browserTimingHeader,
        };
    };

    render() {
        const { browserTimingHeader } = this.props;

        return (
            <Html lang="ja">
                <Head>
                    <script
                        type="text/javascript"
                        dangerouslySetInnerHTML={{ __html: browserTimingHeader }}
                    />
                </Head>
                <body>
                    <Main />
                    <NextScript />
                </body>
            </Html>
        );
    }
}

export default document;

Pages Routerの場合,一部@types/newrelicの実装が追い付いていなさそうなので,@ts-expect-errorを使っている.

App Routerの場合:

import newrelic from 'newrelic';
import { Metadata } from 'next';
import Script from 'next/script';

export const metadata: Metadata = {
    title: 'tekitou',
    description: 'tekitou',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
    const browserTimingHeader = newrelic.getBrowserTimingHeader({
        hasToRemoveScriptWrapper: true,
    });

    return (
        <html lang="ja">
            <body className={`antialiased`}>
                {children}
                <Script
                    id="nr-browser-agent"
                    strategy="beforeInteractive"
                    dangerouslySetInnerHTML={{ __html: browserTimingHeader }}
                />
            </body>
        </html>
    );
}

ここで https://js-agent.newrelic.com に対してpreconnectとかをしておくと多少パフォーマンスが良くなる.

また,環境変数に次の2つを設定する:

NEW_RELIC_APP_NAME=APP_NAME
NEW_RELIC_LICENSE_KEY=LICENSE_KEY

あとはこの状態でビルドすれば良い.デプロイすると次のような感じでダッシュボードにデータが表示される:

New Relicのダッシュボード
New Relicのダッシュボード