🔍
Next.jsの静的ページにNew Relic Browser Agentを導入する
#tech
#Web
2024-11-16
このサイト(lapla.dev)は,Next.jsで構築されていて,しばしば趣味の一環でオーバーエンジニアリング的な施策を導入してきた.今回はその一環として,New Relic Browser Agentを導入したので,それについて書く.
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でも少し書くだけだったからあまり有意点ではない(結果論的になってしまうが).
https://newrelic.com/signupから作る.作った後に何か聞かれるが無視して良い.
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のダッシュボード