🔖

GCP育ちの私がVercel初挑戦で感動した、iframe内の認証エラーを救う「Trace IDログ戦略」

に公開

はじめに

これまでGCP(Google Cloud)をメインに、Cloud Loggingなどの重厚な基盤で監視システムを構築してきた私が、今回プロジェクトで初めてVercelを利用することになりました。

そこで直面したのが、 「iframe内での認証がSafari iOSなどでサイレントに失敗する」 という問題です。実は、 「UXやlegalのためにこのまま iframe 構成を維持すべきか、あるいはセキュリティ制約の少ないポップアップ等に切り替えるべきか」 については現在もチーム内で検討を続けています。しかし、どちらの道に進むにせよ、まずは「現場で何が起きているか」を可視化しなければ議論が始まりません。

GCPの感覚で「ログ基盤をガッツリ整えなきゃ……」と身構えていた私を救ったのは、Vercelの驚くほどシンプルで強力なログ集約の仕組みでした。

1. 課題:本番環境の「見えないエラー」をどう追うか

開発環境では完璧に動くのに、iPhoneのSafari(特にiframe内)でだけログインが失敗する。Cookieの制約(ITP)やストレージアクセスの問題だとアタリはついても、 「具体的にどのステップで止まっているのか」 がわからないと、デバッグはただの勘頼りになります。

そこで、認証フローを「実況中継」してサーバー側に送信する仕組みを作ることにしました。

2. ログの型定義(実況の台本を作る)

まず、認証の各工程に名前をつけます。複雑な「外部サービスとの同期(EXTERNAL_SYNC)」なども、一つのステップとして定義するのがコツです。

ファイル: src/types/authLog.ts

export type AuthStep = 
  | 'INIT'              // 認証開始
  | 'ACCOUNT_CHECK'     // アカウント存在確認
  | 'NONCE_FETCH'       // Nonce取得
  | 'WALLET_SIGN'       // ウォレット署名
  | 'JWT_GENERATE'      // JWT生成
  | 'STORAGE_SAVE'      // トークン保存(ブラウザ制約対策)
  | 'EXTERNAL_SYNC'     // 外部サービス同期(詰まりやすい重要工程)
  | 'FINAL_SUCCESS'     // 完了
  | 'ERROR'
  | 'JITTER_DETECTED';  // 状態の揺れ(リロード等の検知)

export type LogLevel = 'INFO' | 'WARN' | 'ERROR';

export interface AuthLogPayload {
  traceId: string;
  step: AuthStep;
  level: LogLevel;
  message: string;
  address?: string;
  details?: Record<string, unknown>;
}

3. Vercelの「API Route」をログの窓口にする

GCPならSDKを入れて認証を通して……となるところですが、Vercelでは 「API Routeの中でconsole.logを叩く」 、これだけで十分でした。

クライアントからこのAPIにログをPOSTすると、Vercel Dashboardの「Logs」に自動的に集約され、検索可能な状態になります。

ファイル: src/pages/api/auth/log-client.ts

import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') return res.status(405).end();

  const { level, step, traceId, message, address, ...rest } = req.body;

  // 🔴 ここで出力した内容が、そのままVercel Logsの検索対象になる!
  const logHeader = `[${level}] [${step}] [${traceId}]`;
  const logData = { 
    ...rest, 
    address, 
    ua: req.headers['user-agent'],
    timestamp: new Date().toISOString() 
  };

  if (level === 'ERROR') {
    console.error(`🔴 AUTH_LOG: ${logHeader} ${message}`, JSON.stringify(logData));
  } else if (level === 'WARN') {
    console.warn(`🟡 AUTH_LOG: ${logHeader} ${message}`, JSON.stringify(logData));
  } else {
    console.log(`🔐 AUTH_LOG: ${logHeader} ${message}`, JSON.stringify(logData));
  }

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

4. 認証フックへの統合(Trace IDで串刺しにする)

セッションごとに一意の traceId を発行することで、複数のログを一本の線として追跡可能にします。

ファイル: src/hooks/useSiweAuth.ts (実装イメージ)

import { useState, useRef } from "react";
import { AuthStep, LogLevel } from "@/types/authLog";

export const useSiweAuth = (props?: any) => {
  // セッションごとにIDを発行(ブラウザをリロードしない限り保持)
  const [traceId] = useState(() => Math.random().toString(36).slice(2, 10));
  
  const sendLog = async (step: AuthStep, message: string, level: LogLevel = 'INFO', details?: any) => {
    // ユーザー体験を損なわないよう、非同期で投げっぱなす
    fetch('/api/auth/log-client', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ 
        traceId, 
        step, 
        level, 
        message, 
        address: "0x1234...5678", // マスクしたアドレス
        details 
      })
    }).catch(() => {}); 
  };

  const signInWithAddress = async (address: string) => {
    sendLog('INIT', '認証フロー開始');
    
    try {
      // Step 1: Nonce取得
      sendLog('NONCE_FETCH', 'Nonce取得中...');
      const nonceRes = await fetch("/api/auth/nonce?address=" + address);
      // ...処理...

      // Step 2: 署名
      sendLog('WALLET_SIGN', 'ウォレット署名実行');
      // const signature = await activeAccount.signMessage(...);

      // Step 3: 重要かつ詰まりやすい外部同期
      sendLog('EXTERNAL_SYNC', '外部サービスとの同期開始');
      // await syncExternalService(address, props.token);
      sendLog('EXTERNAL_SYNC', '同期成功');

      sendLog('FINAL_SUCCESS', 'すべての認証が完了');
    } catch (e) {
      sendLog('ERROR', '認証失敗', 'ERROR', { error: String(e) });
    }
  };

  return { signInWithAddress };
};

5. Vercel Logsで「点」が「線」になる

本番環境でエラーが起きたら、Vercel Dashboardの Logs を開き、問題のユーザーの traceId でフィルタリングします。

すると、

  1. INIT (開始)
  2. NONCE_FETCH (成功)
  3. WALLET_SIGN (成功)
  4. ……あれ、EXTERNAL_SYNC のログが来ていない!

といった具合に、「どこまで進んで、どこで消えたか」 が時系列で一瞬でわかります。ブラウザ側でサイレントに死んでいたエラーが、サーバーサイドで可視化される瞬間は感動的です。

まとめ

「Vercelはフロントエンドをデプロイする場所」と思っていましたが、実は**「クライアントサイドのブラックボックスを暴くための、最も手軽で強力なログ基盤」**でもありました。

GCPで培った「ログを重視する文化」と、Vercelの「スピード感」を掛け合わせることで、デバッグ効率は劇的に向上します。特にiframe内など、開発者の目が届きにくい場所で戦う皆さんに、この「API Route経由のTrace ID戦略」を全力でおすすめします!

株式会社和平

Discussion