🙌

TypeScriptで障害対応の効率化してみた話

に公開

はじめに

プロダクト開発において、障害報告を受け対応することは避けて通れない重要な業務です。しかし、この業務には多くの課題があります:報告内容の粒度がバラバラ、必要な情報が不足、ヒアリングに時間がかかる、複数の監視ツールを確認する必要がある...

この記事では、私が個人的に取り組んだ障害報告の効率化について紹介します。Google Formを起点に、Google Apps Script、GitHub Actions、TypeScriptを組み合わせて構築したAI駆動の重複検出システムの実装と、その過程で得た技術的な学びを共有します。

また、単純にTypeScriptで何か作りたいという想いに加え、小さい部分でもAIで効率化を行っていきたいという考えがあり、まずはコストを抑えて簡単に作ってみて、チームに体験してもらうことを優先しました。

直面していた課題:効率的な障害対応への渇望

開発チームでは、以下のような課題に直面していました:

  • 障害報告の品質バラツキ:カスタマーサポート、開発者、社内利用者など、様々な立場からの報告内容が統一されていない
  • 調査先の分散:複数のツール(Datadog、PlanetScale等)で同じような調査を繰り返す必要がある。障害発生時刻の前後でログを絞り込み、類似のクエリを各サービス・各報告で手動作成する作業が非効率
  • 重複報告の問題:同じ障害が複数回報告され、対応リソースが無駄になる。今後のプロダクト成長を見据え、大量の障害報告・要望を効率的に処理できるスケーラブルな仕組みの構築が急務
  • コンテキストスイッチ:開発作業中に割り込みで調査タスクが入る
  • 調査時間の長期化:必要な情報を集めるだけで数十分かかることも

解決への道筋

私が目指したのは、障害報告から解決までの全工程での見直しでした:

  • 報告フォーマットの統一:Google Formで必要な情報を構造化し、毎回のヒアリング時間を削減
  • 自動化の推進:監視ツールのURL自動生成、SQLクエリの自動作成により、調査開始までの時間を短縮
  • AI活用の導入:重複検出とクエリ生成にAIを活用し、人的リソースをより重要な課題解決に集中
  • GitHub連携:Issue自動作成で開発フローに統合し、障害対応の進捗を透明化

特に重複検出機能については、将来的なプロダクト成長で予想される大量の要望に対する準備として位置づけ、スケーラブルな仕組みを早期に構築することを重視しました。

システム構成:Google FormからGitHub Issueまでの自動化フロー

全体アーキテクチャ

  • 重複確認部

重複検出の実際の動作

以下は実際にシステムが重複を検出し、自動的にラベル付けとコメントを行った例です:

※この時の開発では簡単にAIが類似した既存Issueを検出し、「duplicated」ラベリングなどで、バグフィックスのトリアージ時のフィルタリングしやすい情報を増やすなどを行った。

技術選択の理由

Google Form

  • 非エンジニアでも簡単に使える
  • 小さく始められる(HubSpotなど有料サービスへの移行も将来的に検討可能)
  • 既存のGoogle Workspaceとの統合が容易

Google Apps Script (GAS)

  • 小さく始めるのに最適
  • Googleサービスとのネイティブ統合
  • サーバー管理不要

Qdrant Vector Database

  • ベクトル検索に特化した高性能
  • REST APIでTypeScriptから簡単に利用可能
  • セルフホストも可能でコスト効率が良い

Gemini AI

  • 比較的低コストで利用可能で使いやすかったため、素早く試すという内容にマッチ。

Google Apps Scriptでの前処理

まず、Google Formの回答を受け取り、それをトリガーとして必要な情報を整形するGASを実装しました。以下は主要な処理のベースラインコードです:

function onFormSubmit(e) {
    // フォーム回答の処理
    var processedResponse = processFormResponses(itemResponses);
    var formData = processedResponse.formData;
    var issueBody = processedResponse.issueBody;

    // Datadog URL自動生成
    const datadogURLs = createDatadogURLs(formData);
    
    // PlanetScale クエリ自動生成
    const planetscaleQueries = createPlanetscaleQueries(formData);
    
    // GitHub Issue作成
    var issueData = generateIssueBody(formData, issueBody, datadogURLs, planetscaleQueries);
    createGithubIssue(issueData.specialFields.title, issueData.issueBody);
    
    // Slack通知
    notifyToSlackWithContent(formData, issueData.specialFields);
}

監視ツールへのリンク・データベースへのクエリ自動生成

DatadogはログフィルタリングのためパラメータとしてURLに含みます。
createDatadogURLs関数では、フォームで入力された障害発生時刻を基に、Datadogで該当時間帯のログやRUM(Real User Monitoring)を確認できるURLを自動生成します。以下はその処理のコア部分です:

function createDatadogURLs(formData) {
    const occurrenceTime = new Date(formData['発生日時']);
    const fromTime = new Date(occurrenceTime.getTime() - 60 * 60 * 1000); // 1時間前
    const toTime = new Date(occurrenceTime.getTime() + 60 * 60 * 1000);   // 1時間後
    
    // 物件IDやユーザーIDに基づいたフィルタリングを含むURLを生成
    return {
        logs: `https://app.datadoghq.com/logs?query=...&from_ts=${fromTime.getTime()}&to_ts=${toTime.getTime()}`,
        rum: `https://app.datadoghq.com/rum/explorer?query=...&from_ts=${fromTime.getTime()}&to_ts=${toTime.getTime()}`
    };
}

createPlanetscaleQueries関数では、Gemini AIを活用してフォームの内容から適切なPlanetScale(MySQL)クエリを自動生成します。プロダクトのテーブル構造をAIに与え、障害内容に関連するデータを抽出するSQLを作成してもらいます。テーブル構成を把握するためのコンテキストとして、schema.prismaを参考にするようにしました。

技術的挑戦:TypeScriptでのAIシステム構築

実装の背景

多くの障害報告を受ける中で、優先度をつけて対応していますが、対応が間に合わないものが再発して同じ問題が重複起票されることが時折あります。チーム運用で、これが繰り返されることによりGitHub Issueが荒れるのを防ぎたいという課題がありました。また「重複するIssueを探し当てる」という非生産的な時間的コストの削減も必要だと考えました。

重複検出システムのアイデアは、election2024というオープンソースの政治マニフェスト作成プロジェクトからヒントを得ました。このプロジェクトでは、政策提案に対するIssueやPull Requestを通じて市民が協働で政策を改善していく仕組みを提供しており、その中で類似した政策提案を効率的に管理する必要がありました。この「類似コンテンツの管理」という課題に対するアプローチに強く感銘を受け、障害報告の重複管理に転用できないかと考えていました。

このアイデアを障害報告の重複検出に応用できると考え、私のチームが担当しているプロダクトがフルスタックTypeScriptで統一されていることから、保守性とチーム内での知識共有を考慮してTypeScriptで同様の機能を実現することに挑戦しました。

TypeScript実装で直面した主な課題

PythonからTypeScriptへの実装において、いくつか処理方法が異なる部分があったので、主要な違いをピックアップして紹介します。

1. エンベディング処理の違い

Python実装(オリジナル):

def create_embedding(text):
    result = model.embed_content(text)
    return result.embedding.values  # 直接的な値の取得が可能

TypeScript実装(移行後):

async _createEmbedding(text: string): Promise<number[]> {
    const model = this.gemini_client.getGenerativeModel({ model: EMBEDDING_MODEL });
    const result = await model.embedContent(text);
    const embedding = await result.embedding;
    return embedding.values; // 非同期処理の考慮が必要
}

TypeScriptでは非同期処理がPromiseベースで明示的に型付けされるため、コンパイル時にエラーハンドリングの漏れを発見しやすくなりました。

2. 非同期処理パターンの違い

Python:

async def process_issue(self, issue_content):
    similar_issues = await self.qdrant_handler.search_similar_issues(issue_content)
    if similar_issues:
        duplicate_id = await self._check_duplication(issue_content, similar_issues)

TypeScript:

async processIssue(issue_content: string): Promise<void> {
    const similar_issues = await this.qdrant_handler.searchSimilarIssues(issue_content);
    if (similar_issues.length) {
        const duplicate_id = await this._checkDuplication(issue_content, similar_issues);
    }
}

学び: TypeScriptではsimilar_issues.lengthのように配列のlengthプロパティを使った条件分岐で、型システムが配列の存在を保証してくれます。Pythonのif similar_issues:では空配列でもTruthyな値として判定されるケースがありますが、TypeScriptの明示的な型チェックにより、より安全なコードが書けました。

3. ライブラリエコシステムの違い

パッケージの対応関係:

  • openai@google/generative-ai
  • qdrant-client@qdrant/js-client-rest
  • PyGithuboctokit

Gemini API選択の理由:

  • コスト効率: OpenAI APIと比較して料金が安く設定されている
  • 無料枠の充実: 開発・テスト段階では十分な無料枠が利用可能
  • 日本語対応力: 日本語の障害報告の理解と処理に優れている

実装の詳細:TypeScript版重複検出システム

環境設定とクラス構造

class Config {
    github_token: string | undefined;
    qd_api_key: string | undefined;
    qd_url: string | undefined;
    github_repo: string | undefined;
    issue_number: number | undefined;
    gemini_api_key: string | undefined;

    constructor() {
        // 環境変数の読み込みと検証
        this.github_token = process.env.GITHUB_TOKEN;
        this.qd_api_key = process.env.QD_API_KEY;
        // ...
    }
}

メインの処理フローと依存性注入

IssueProcessorクラスは、GitHub、Qdrant、Gemini AIの各ハンドラーを依存性注入で受け取り、Issue処理の全体的なフローを管理します。類似Issueが見つからない場合は新規として登録し、見つかった場合はAIによる重複判定を行います。重複が検出された場合は、自動的に「duplicated」ラベルを付与し、該当する既存Issueへの参照コメントを追加して関係性を明確化します。

class IssueProcessor {
    constructor(
        private github_handler: GithubHandler,
        private qdrant_handler: QdrantHandler,
        private gemini_client: GoogleGenerativeAI
    ) {}
    
    async processIssue(issue_content: string): Promise<void> {
        const similar_issues = await this.qdrant_handler.searchSimilarIssues(issue_content);
        
        if (!similar_issues.length) {
            await this.qdrant_handler.addIssue(issue_content, this.github_handler.issue_number);
            return;
        }

        const duplicate_id = await this._checkDuplication(issue_content, similar_issues);
        if (duplicate_id) {
            await this._handleDuplication(duplicate_id);
        } else {
            await this.qdrant_handler.addIssue(issue_content, this.github_handler.issue_number);
        }
    }
}

ベクトル検索とAI判定の統合

QdrantHandlerは、テキストをGemini APIでベクトル化し、Qdrantデータベースで類似検索を実行します。検索結果はスコア付きで返され、後段のAI判定で重複の可能性を評価する材料となります。

class QdrantHandler {
    async searchSimilarIssues(text: string): Promise<SearchResult[]> {
        const embedding = await this._createEmbedding(text);
        const results = await this.client.search(COLLECTION_NAME, {
            vector: embedding,
            limit: MAX_RESULTS
        });
        
        return results.map((result: any) => ({
            id: result.id as number,
            score: result.score,
            payload: result.payload as { text: string }
        }));
    }

    async _createEmbedding(text: string): Promise<number[]> {
        const model = this.gemini_client.getGenerativeModel({ model: EMBEDDING_MODEL });
        const result = await model.embedContent(text);
        const embedding = await result.embedding;
        return embedding.values;
    }
}

重複判定ロジック

ベクトル検索で類似したIssueが見つかった場合、Gemini AIにプロンプトを送信して人間的な判断による重複確認を行います。AIは既存Issueのリストと新しいIssueを比較し、重複している場合は該当するIssue IDを返します。

async _checkDuplication(issue_content: string, similar_issues: SearchResult[]): Promise<number> {
    const prompt = this._createDuplicationCheckPrompt(issue_content, similar_issues);
    const model = this.gemini_client.getGenerativeModel({ model: GEMINI_MODEL });
    const result = await model.generateContent(prompt);
    const response = await result.response;
    let review = response.text() || "";
    
    // レスポンスの整形
    if (review.includes(":")) {
        review = review.split(":").pop() || "";
    }
    
    const id = parseInt(review);
    return !isNaN(id) && id !== 0 ? id : 0;
}

CI/CD統合

GitHub Actions連携

GitHub ActionsでIssueが作成されるたびに自動的にTypeScriptスクリプトが実行されます。必要な環境変数(API キーなど)はGitHub Secretsで管理し、セキュアな実行環境を確保しています。

name: Issue Review
on:
  issues:
    types: [opened]

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
      - run: pnpm install
      - name: Run issue review
        run: pnpm --filter @my-project/issue-review run review-issue
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          QD_API_KEY: ${{ secrets.QD_API_KEY }}
          QD_URL: ${{ secrets.QD_URL }}
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}

TypeScript実装での学び

1. ベクトルデータベースとの連携での学び

@qdrant/js-client-restを使用したベクトル検索の実装で学んだことは以下の通りです:

  • REST APIの利便性: gRPCではなくREST APIベースのため、TypeScriptから直感的にアクセスできました
  • 型安全性の恩恵: レスポンスの型定義により、検索結果の構造を事前に把握でき、フィールドアクセス時の誤りを防げました
  • 非同期処理の統一: async/awaitパターンで他のAPI呼び出しと統一的に書けるため、コードの可読性が向上しました
  • エラーハンドリング: try-catchブロックでベクトル検索のエラーとAI API呼び出しのエラーを分離して処理できました

2. GitHub Actionsとの統合

Node.jsベースのTypeScript実行環境は、GitHub Actionsでの環境構築が軽量で、CI/CDパイプラインへの組み込みが容易でした。pnpmを使ったパッケージ管理により、依存関係のインストールも高速に行えました。

導入後の成果と改善効果

実現できた改善

  • 調査作業の自動化: 監視ツールのURL自動生成により手作業が削減
  • チーム間の連携改善: 必要な情報が最初から揃うようになった
  • ナレッジの蓄積: 障害パターンの可視化

今後の展望

今回は「小さく始める」ことを重視して最小限の機能でリリースしましたが、改善の余地は多くあります。QAチームなどからも良いFBをいくつか受け、構想は膨らむばかりです。:

  1. プロンプトエンジニアリングの改善: 重複判定の精度向上とより詳細な類似度分析
  2. フォーム項目の最適化: 実際の運用を通じて必要な情報の見直し
  3. 通知システムの拡張: SlackだけでなくTeamsやメール通知への対応
  4. ダッシュボード機能: 障害傾向の可視化や統計情報の提供
  5. 自動クエリ生成の改善: より複雑な調査クエリの自動生成
  6. カテゴリ別重複検出: UIの問題、パフォーマンス問題、データの問題など、障害種別ごとの専門的な重複判定
  7. 優先度自動判定: 過去の障害データから緊急度を自動的に判定する機能
  8. 影響範囲予測: 類似した過去の障害から影響範囲を予測し、事前に関係者に通知
  9. 自動修正提案: よくある障害パターンに対して、修正手順やコード例を自動提案
  10. リアルタイム監視連携: 障害報告と同時にリアルタイムで監視データを収集・分析

まとめ

この記事では、障害報告の効率化という実際の課題に対して、TypeScriptでAI駆動システムを構築した取り組みについて紹介しました。

Pythonプロジェクトからのヒントを得つつも、TypeScriptエコシステムの特徴を活かした実装により、既存の開発環境に自然に統合できるシステムを構築することができました。

「小さく始める」アプローチを取ることで、早期にリリースでき、実際の運用を通じて改善点を見つけられる状態になりました。実運用に載せていく段階で、プロンプトの精度やベクトル検索の閾値調整など、実際のデータに基づいた改善点を早期にキャッチアップできたのは大きなメリットでした。

TypeScriptでのAI活用は、Pythonほど情報が多くありませんが、型安全性や開発体験の面で多くのメリットがあることを実感できました。まだ改善の余地は多くありますが、チームメンバーからも「障害対応が楽になった」という声をもらえており、手応えを感じています。今後もさらなる自動化と精度向上に向けて改善を続けていきます。


この記事で使用・記載した技術スタック

  • Frontend: Google Apps Script, Google Form
  • Backend: Node.js, TypeScript, Hono
  • AI: Google Gemini API, Qdrant Vector Database
  • CI/CD: GitHub Actions, pnpm workspace
  • Monitoring: Datadog, PlanetScale

参考リンク

注意: この記事で掲載しているコードは全て主要機能のベースラインのみを抜粋したものです。実際の実装では、エラーハンドリング、セキュリティ対策、認証処理など、プロダクション環境に必要な詳細な実装が含まれています。

Discussion