Arganoの池田です。本記事では以前の記事で紹介した New Relic を導入した Next.js アプリケーションでAPMによるトレーシングを試してみます。バックエンドのトレースやエラー監視がどのように見えるのかを、実際に実装したアプリケーションで検証し、エラーの調査やパフォーマンスの分析に利用できることを確認します。

用語

今回はAPMのトランザクショントレースについての検証ですが、その前に用語について簡単に説明します。

トランザクション

アプリケーションへのリクエストについて開始から終了までの一連の処理をまとめた作業単位です。本記事では、トランザクションのトレースをNew Relicコンソールから確認し、パフォーマンスのボトルネックやエラーの発生箇所を特定する検証を行います。

トレース

トランザクションに含まれる一連の作業に関する追跡情報です。トランザクションの処理の流れ、エラー情報やクエリの応答速度を含みます。トレースは複数のスパンから構成されます。

スパン

トレースの構成単位です。APIリクエストのトレースにおいてはDB処理や外部APIリクエストなどがスパンに該当します。

検証環境

以下の環境で検証を行います。 プロジェクトのセットアップについては過去の記事を参照してください。

  • Node: 24.17.0
  • Next.js: 16.2.9
  • React: 19.2.4

実装と検証

検証のために、簡単なログインページとログインのエンドポイントを作成します。体裁を整えるためフォームの各要素には別途UIコンポーネントを利用していますが、そちらについてはご自身の環境に合わせて標準のHTML要素に変更していただいて構いません。

/app/login/page.tsx

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import {
  Field,
  FieldGroup,
  FieldLabel,
  FieldSet,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button";

const Page = () => {
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(false);

  const router = useRouter();
  
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setLoading(true);

    const formData = new FormData(e.currentTarget);
    
    try {
      const res = await fetch("/api/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          username: formData.get("username"),
          password: formData.get("password")
        })
      });

      if (res.ok) {
        router.push("/");
      } else {
        throw new Error();
      }
    } catch {
      setError("エラーが発生しました");
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <form className="w-full max-w-xs flex flex-col gap-8 mx-auto" onSubmit={handleSubmit}>
      <FieldSet>
        <FieldGroup>
          <Field>
            <FieldLabel htmlFor="username">ユーザー名</FieldLabel>
            <Input id="username" type="text" />
          </Field>
          <Field>
            <FieldLabel htmlFor="password">パスワード</FieldLabel>
            <Input id="password" type="password" placeholder="••••••••" />
          </Field>
        </FieldGroup>
      </FieldSet>
      {error && <p className="text-red-500 text-sm">{error}</p>}
      <Button type="submit" className="w-full" disabled={loading}>ログイン</Button>
    </form>
  )
};

export default Page;

/app/api/login/route.ts

import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
  const { username, password } = await request.json();

  // 動作検証用の遅延
  await new Promise((resolve) => setTimeout(resolve, 500));

  return NextResponse.json({ success: true }, { status: 200 });
}

この状態でアプリケーションを起動し、ログインボタンを押下すると0.5秒後にTOPページにリダイレクトします。一旦、この操作に対してトレースが収集できていることを確認しましょう。

trace1

New Relic のコンソールを開いてサイドメニューから APM & Services > Transactions を確認すると、 POST /api/login に515ミリ秒かかっていることが確認できました。

バックエンドで叩いている外部APIが遅延の原因となるケースも検証するため、 /app/api/login/route.ts の遅延処理を httpbin/delay/{delay} へのリクエストに変更して確認してみます。

httpbin は様々なレスポンスを返すことができるエンドポイントが予め用意されたモックサーバーです。 公式の https://httpbin.org は落ちていたり、レスポンスが非常に遅い場合があるので今回はローカルで起動しておきます。

docker pull kennethreitz/httpbin
docker run -p 80:80 kennethreitz/httpbin

/app/api/login/route.ts

import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
  const { username, password } = await request.json();

  // 動作検証用の遅延
  const external = await fetch("http://localhost/delay/2");
  await external.json();

  return NextResponse.json({ success: true }, { status: 200 });
}

New Relic のコンソールを開くと新しく追加した外部APIとの通信で2秒かかっている様子が確認できました。

trace2

最後に外部APIからエラーが返されるように実装を変更します。変更後、 httpbin のエンドポイントを /status/500 に変更します。この状態で検証アプリのログインボタンを押すと、ブラウザ上では「エラーが発生しました」と表示されるのみですが、 New Relic のコンソール上からはどのように見えるかを確認します。

import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
  const { username, password } = await request.json();

  // 動作検証用の500エラー
  const external = await fetch("http://localhost/status/500");
  if (!external.ok) {
    throw new Error("エラーが発生しました");
  }
  await external.json();

  return NextResponse.json({ success: true }, { status: 200 });
}

APM & Services > Error にエラーが表示されています。

error

この画面からトランザクションへのリンクがあるのでそちらを確認してみます。

trace3

トランザクションをクリックし、表示されているトレースから黄色くハイライトされているスパンを選択すると、レスポンスが500となっていることがわかります。この画面から今回のエラーがバックエンドの外部APIリクエストの500エラーが原因であることが特定できました。

Sentryとの比較

弊社では New Relic ではなく Sentry を利用しているプロジェクトもあります。 基本的にどちらのツールを使っていてもトレースの画面から得られる情報に大きな差はありませんが、双方のツールがどのような情報の収集に重きを置いているかという点で多少表示内容は異なるため、今回見てきたトレースの表示について情報量や見え方を Sentry と比較してみます。

Next.jsに導入したSentryプロジェクトでトレースを見てみます。

trace4

Sentry でも New Relic で見たようなトレースと同等の内容を確認できますが、 Sentry はフロントエンドの監視に優れたツールなので、細かな設定をせずともフロントエンドとバックエンドのトレースを繋げたトランザクションを生成してくれています。また、 Attributes の項目を見るとブラウザの情報やトランザクションの発生元となるページURLの情報をデフォルトで含めてくれるようになっています。

trace5

一方、 New Relic の画面ではトランザクション全体のなかで低速だったスパンがハイライトされます。そのスパンがトランザクション全体の実行時間のうち何%を占めているかが一目でわかるようになっている点や、スパンをクリックした際にネットワークリクエストの平均応答速度、スループット、スパンの実行時間のヒストグラムを確認できる点が特徴です。

まとめ

ここまで Next.js アプリケーションに New Relic を導入した際に、トレースはどのように見えるのか、またパフォーマンスやエラーの調査のためにどのように利用できるのかを検証しました。 エラーの調査の際にはブラウザからどのような操作が行われたのか、またユーザーにどのような画面が見えているのかを知りたいケースに備えるため、Browser Monitoring の導入と分散トレーシングの方法についても次回改めて紹介できればと思います。 最後までお付き合いいただきありがとうございました。