「自由な解析」から「確かなエビデンス」へ。ウェアラブルデータ解析におけるSAP(統計解析計画書)の重要性

はじめまして。テックドクターでデータ解析を担当している藤野です。

私はテックドクターへ参画する前は、約10年間にわたり医薬品開発における治験の統計解析に従事してきました。現在はその経験を活かし、ウェアラブルデバイスのデータを用いて、医療現場で活用されるデジタルバイオマーカー(※1)の開発に取り組んでいます。

ウェアラブルデータは、従来の臨床試験のデータに比べてデータ量が膨大で、項目の種類も多岐にわたります。そのため、解析の自由度が非常に高く、データサイエンティストとして非常に面白い領域です。しかしその反面、解析者の意図によって「都合の良い結果」を導き出してしまうバイアス(P-hacking等)が入り込みやすいという危険性もあります。

このように都合の良い解析とならないために、治験における解析の「規律」であるSAP(統計解析計画書)の考え方をウェアラブルデータ解析においても実践しています。
本記事では、ウェアラブルデータ解析においてなぜSAPが必要なのか、そして具体的にどのような点に留意して作成すべきかをご紹介します。

※1 デジタルバイオマーカー……デジタルデバイスで測定した『日常データ』をもとにした、病気の早期発見や治療につながる客観的指標(過去記事参照

SAP(統計解析計画書)とは

SAP(Statistical Analysis Plan:統計解析計画書)とは、解析を始める前に「どのようなデータを使い、どのような解析を行うか」を細かく記載した文書のことです。
医薬品開発で必須とされる国際的なガイドラインである「ICH-E9(臨床試験のための統計的原則)」(※2)に基づいて、解析の客観性と再現性を担保することを目的としています。

※2 PMDA(独立行政法人 医薬品医療機器総合機構)「ICH-E9(臨床試験のための統計的原則)」, URL: https://www.pmda.go.jp/int-activities/int-harmony/ich/0031.html

なぜ機械学習全盛の今、SAPが必要なのか

最新のアルゴリズム開発とは対極にある「古い慣習」のように思えるかもしれません。しかし、実はアルゴリズム開発(機械学習)の前段階として、統計的な検証を行うことには大きなメリットがあります。

①客観性の担保
解析前に解析方法を決定することで、有意差が出るまで条件を変えて解析し直すことを防ぎます。

②エビデンスの作成
解析プロセスを文書化しておくことで、社内外のステークホルダーに信頼されるエビデンスを作成します。

これらはSAPの一般的なメリットですが、くわえてウェアラブルデータ解析特有の課題への対策としてもSAPは有効です。次にご説明します。

ウェアラブルデータ解析における「信頼性」の課題

ウェアラブルデータ解析においては、その自由度の高さから、特有のリスクが存在します。

多重性の問題
心拍、睡眠、歩数など、ウェアラブルデバイスからは大量の変数が得られます。
これらを総当たりで検定にかければ、偶然有意差が見つかるリスクが高まります。

欠測バイアス(※3)の問題
日常生活では、未装着や充電切れなどによるデータの欠測が発生します。
欠測の扱いで結果が大きく変わってしまうことがあります。

これらの日常のデータを信頼できるエビデンスとするために、SAPによる事前の規定がとても有効です。

※3 バイアス……解析者の思い込みやデータの偏りによって、真実とは異なる結果が出てしまうことを指します。

イメージ図

SAPの実践例と重要なポイント

具体的に、どのような点をSAPで定義すべきでしょうか。事例をもとに3点紹介します。

有効なデータと解析対象の定義

ウェアラブルデータの場合、「データが存在する=解析に使える」とは限りません。解析前に、評価の基盤となるデータの質を定義しておく必要があります。

歩数データの例:
次のような基準をあらかじめ規定しておきます。

  • 心拍データが記録されている時間を装着時間とし、「1日の装着時間が70%(16.8時間)以上」の日を有効装着日と規定する
  • さらに、評価期間7日間のうち、少なくとも4日以上の有効装着日が存在する被験者のみを解析対象とする
  • 最後に、各有効装着日の1日の歩数を算出し、評価期間中の平均値を解析に用いる

データ有効性の判定チャート

生データから加工変数への変換ロジック

高頻度な原データを解析可能な形に集約する計算式を、SAPで明文化します。

心拍変動の例:
以下のように基準を設定します。

  • 15分間隔の区間集計で算出するロジック
  • 「30bpm以下の心拍数」など生理学的にあり得ない値を異常値として除外する閾値
「探索」と「検証」の切り分け

自由にデータを深掘りして分析することで新たな知見を得るための探索的な解析項目と、事前に決められた手順で仮説を検証するための解析項目を明確に分けます。

SAP導入の効果とメリット

ここまで主にSAPがデータ解析作業にもたらす利点を紹介してきましたが、それ以外にもSAPの導入によって、プロジェクト全体に様々なメリットがあります。

解析結果の頑健性(ロバストネス)の向上
解析を事前に規定することで、「データを見てからルール(データの採用基準や統計手法)を決める」という後出しジャンケンを防ぎます。これにより、誰が解析しても結論の変わらない安定した解析結果を得ることができます。

ロバストネス向上のイメージ図

チーム内外の円滑な議論
解析方針が詳細にドキュメント化されているため、社内のレビューはもちろん、外部のステークホルダー(スポンサー・アカデミア・CRO等)や規制当局(PMDA等)との議論がスムーズになります。

将来的な治験への布石
探索的な臨床試験の段階からSAPを導入することで、将来的に治験に進む際に、スムーズに移行できる下地となります。

また、現場で実務を担うステークホルダーにも具体的なメリットがあります。

ステークホルダー 具体的なメリット
データサイエンティスト 個別のデータ採否に迷う必要がなく、解析の再現性と効率が向上します。
統計解析担当者 欠測や中間事象(服薬不遵守や併用薬の使用)に対して、治験の標準的な考え方を適用でき、ウェアラブルデータ以外の解析と整合性が取れます。
薬事・マーケティング担当者 事前に合意・規定されたプロセスに基づくエビデンスとなるため、社内外への説明に客観的な裏付けができます。

実践を通じた気づき

10年の治験解析経験を経て、今ウェアラブルデータ解析の舞台で改めて感じるのは、SAPは決して「自由な解析を制限するもの」ではないということです。むしろ、解析結果に胸を張って「これは信頼できる結果です」と言うための武器になります。

まとめ

ウェアラブルデバイスという新しい領域だからこそ、先人が臨床試験で築き上げてきた「統計の作法」が非常に強力な武器になります。

テックドクターでは、これからも最新のテクノロジーと統計学的手法を組み合わせ、デジタルバイオマーカーの社会実装に向けた信頼性の高い解析を追求していきます。


似顔絵
書いた人:藤野

Agentic Workflow導入記:巨大プロンプトで起こる諸問題を「分割」と「構造化」で解決した話

こんにちは、テックドクターの佐藤です。

今回は、アプリ開発において Agentic Workflow(自律的なエージェントワークフロー)を組み込んだ際に直面した問題と、解決方法について書きます。

その問題とは「巨大プロンプトの限界」。それを解決に導いたのは、マルチエージェントアーキテクチャ化、そしてPydanticAIを用いた型安全な連携でした。

ヘルスケアアプリ「ポポラス」にAgentic Workflowを実装

昨年11月、「ポポラス」というスマートフォンアプリをリリースしました。

populus-app.com

ポポラスは、ユーザーのHealthKitやFitbitのデータに基づき、健康に関するパーソナライズされた受け答えを提供するヘルスケアエージェントアプリです。ユーザーからの「昨日の活動量はどうだった?」といった質問に対し、実際の歩数や睡眠データを参照しながら、自然で適切なアドバイスを行うことを目的としています。

ポポラスはユーザーの質問に対する回答生成に、単一のLLMではなく、複数の専門エージェントが協調して動くAgentic Workflowを採用しています。

AgenticWorkflowとは、AIやプログラムが「自分で考えて動く」ようにする仕組み、あるいはそのための考え方のことです。AIがある目的を達成するために、都度人間の指示を受けるのではなく、自分で手順を考えたり、必要な作業を順番に進めたりできるようにします。

詳しくはこちらのエントリで解説しているので読んでみてください。
techblog.technology-doctor.com

最初のアプローチ:万能な「神エージェント」

開発当初、私たちは全てのタスク(データ取得、分析、応答生成)を一つの大きな「メインAgent」に担わせる設計を採用しました。

当初の実装構成

アーキテクチャ……ユーザー入力と履歴を、一つの巨大なプロンプトを持つエージェントに直接渡す。

ツール群……HealthKitDataFetcherFitbitDataProcessorHealthAnalyzerGetNowなど、あらゆるツールを一度に渡していました。

プロンプト……ひとつのコンテキスト内に全ての機能とガードレール(AIの挙動が意図した範囲から外れないような制御)を詰め込んでいました。

構成図
構成イメージ

最初のアプローチで問題だった点

その結果、この「ひとつの巨大なAgent + 多数のツール」という構成では、期待通りの回答を得られませんでした。具体的には以下の問題が発生しました。

  • 推論の不安定化と無限ループ
    普通の質問をしただけでも、エージェントがツール利用の判断を誤り、データ取得処理のループに入ってしまう現象("An unexpected error occurred")が頻発しました。
  • トークン消費とコストの増大
    本来不要なツールまで呼び出してしまったり、プロンプト自体が巨大であるため入力トークン数が肥大化したりして、APIコストを圧迫しました。
  • コンテキスト理解の失敗(エッジケース)
    「昨日の歩数は?」と質問した後に「じゃあ一昨日は?」と追撃した場合などに、文脈を正しく引き継げず、適切な日付のデータを取得できないケースが多発しました。
  • デバッグの困難さ
    処理がブラックボックス化していたため、意図しない挙動(ハルシネーションなど)が起きた際、原因がプロンプトにあるのかツールの選択ミスにあるのかを特定するのが極めて困難でした。

結論として、「巨大なプロンプトと多数のツールを機械に丸投げして最適な答えを出させる」というやり方ではうまくいかないことが判明しました。

改善したアプローチ:役割分担と構造化

これらの問題を解決するために、「タスクの細分化(Task Decomposition)」と「Pydanticによる入出力の構造化」を行う設計へ移行しました。

協調型Agentic Workflowへの変更

全体の構成図を見てください。単一のAgentではなく、役割を持った専門のAgent群が連携するワークフローに変更しました。

構成図
改善された構成

異なる役割を持った4つのAgentを定義しました。

Agent 役割
RouterAgent ユーザーの意図を汲み取り、適切なエージェントへ振り分ける司令塔。
FetchAgent / AnalysisAgent データ取得と分析に特化した実働部隊。
EmpathyAgent ユーザーに寄り添う会話担当。
SorryAgent 「株価を教えて」や「医療診断して」など、アプリのスコープ外または危険な質問に対して、適切に断りを入れる担当。

これらが連携してユーザーへの応答を生成します。

PydanticAIにより入出力を縛る

Agent間の連携で最も重要なのが、次のAgentが作業しやすいように情報を整形して渡すことでした。
PydanticAI(あるいはPydanticそのもの)を活用し、モデルのフィールドにdescriptionを詳しく書くことで、LLMに対して「この項目には何を入れるべきか」を強く示唆できます。

一例として、司令塔でありユーザーの質問を直接受け取る立場であるRouterAgentの出力定義はこのようにしました。

RoterAgentの出力定義

from pydantic import BaseModel, Field
from typing import Literal

class RouterDecision(BaseModel):
    """
    ユーザーの意図を分析し、次のアクションを決定するためのモデル
    """
    target_agent: Literal["FetchAgent", "EmpathyAgent", "SorryAgent"] = Field(
        description="ユーザーの要求を満たすのに最適なエージェントを選択する。"
        "健康データが必要ならFetchAgent、日常会話ならEmpathyAgent、"
        "スコープ外(株価や医療診断など)ならSorryAgentを選択。"
    )
    
    context_query: str = Field(
        description="次のエージェントに渡すための、具体的かつ自己完結した指示文。"
        "例:ユーザーが『じゃあ一昨日は?』と言った場合、ここには"
        "『2025年10月13日の歩数データを取得してください』のように"
        "日付と目的を補完して正規化したテキストを出力すること。"
    )
    
    reasoning: str = Field(
        description="なぜそのエージェントを選択したかの理由。デバッグ用。"
    )


実際のLLM出力例
※文脈:昨日の歩数の話の続き

# User: 「じゃあ一昨日は?」
# Result:
# {
#   "target_agent": "FetchAgent",
#   "context_query": "2025-10-13の歩数データを取得",
#   "reasoning": "直前の会話履歴から歩数の話題と判断し、日付を一昨日に特定したため"
# }

このように、Field(description=...) に「日付を補完して正規化せよ」といった具体的な指示を埋め込むことで、プロンプトの一部として機能させます。

これにより、後続のFetchAgentには曖昧な「一昨日」という言葉ではなく「2025-10-13」といった確定情報が渡されることになります。FetchAgentはこれを使ってAPIを叩くだけで済みます。

マルチエージェント化によるメリット

マルチエージェント化したことにより、多くのメリットが得られました。

Agentに特化したプロンプトが書けるようになった

マルチエージェント構成では、「そのエージェントに関係ない禁止事項」を書く必要がなくなります。

例えば、

  • RouterAgent には「ツールを直接呼び出してはいけない」「最終回答を生成してはいけない」という制約だけを書けばよい
  • FetchAgent には「ユーザーに話しかけてはいけない」「指定されたツールだけを呼ぶ」「推論をしない(※API経由で正確なデータを取得させるため)」という制約だけを書けばよい
  • SummarizeAgent には「取得済みデータを要約することだけに集中する」「新しいデータ取得はしない」という制約だけを書けばよい

というように、「その役割の逸脱を防ぐためのガードレール」だけを、最小限かつ強い言葉で書けるようになります。

結果として、

  • プロンプト1つあたりの制約の数が激減
  • 各制約の意味が曖昧にならない
  • モデルが「何をしてはいけないか」をほぼ確実に守る

という状態を作ることができました。

出力が安定した

もう一つの非常に大きなメリットは、出力フォーマットが安定したことです。

巨大プロンプトで全タスクをやらせていた頃は、

  • たまにJSONが壊れる
  • たまに説明文が混ざる
  • たまにキーが変わる
  • たまに謎の自然文が出る

といった「機械処理するには致命的だが、人間が見ると一見それっぽい」出力が頻発していました。

マルチエージェント化すると、

  • RouterAgent……{ intent: "...", normalized_text: "..." } しか返さない
  • FetchAgent……{ tool_results: [...] } しか返さない
  • SummarizeAgent……{ summary: "...", insight: "..." } しか返さない

というように、各エージェントに「一種類のJSON構造しか返させない」設計にできます。

これにより、

  • パースエラーがほぼゼロになる
  • 後段の処理が例外処理だらけにならない
  • ログの可読性とデバッグ効率が爆上がりする

という、「正しい状態」に一気に寄せられました。

デバッグ可能性が劇的に向上した

マルチエージェント構成にして一番「これは正解だった」と感じたのは、デバッグのしやすさです。

巨大プロンプト時代は、なにかを失敗した際に「どこで?」「なぜ?」「どの指示を誤解した?」「モデルが悪い?プロンプトが悪い?設計が悪い?」が完全にブラックボックスでした。

マルチエージェント化すると,

  • RouterAgent のログを見る → 意図分類が間違っていることがわかる
  • FetchAgent のログを見る → ツール引数の組み立てが間違っていることがわかる
  • SummarizeAgent のログを見る → 要約ロジックが微妙であることがわかる

というように、壊れている責務の場所が一瞬で特定できるようになります。

最初のアプローチとの比較

最初の「巨大プロンプト方式」との違いを一覧表にまとめました。

項目 初期設計(失敗) 改善後の設計(成功)
Agent構成 1つの巨大なAgent 複数Agentが連携
(Router, Fetch, Analysis, Empathy, Sorry)
プロンプト 全機能を網羅した巨大プロンプト タスクごとに特化した
小規模プロンプト
データ連携 自然言語(曖昧) Pydanticモデル
(型定義+Descriptionによる指示)
エラー対応 プロンプト内で禁止事項を羅列 SorryAgentへ遷移させて分離
安定性 低い(ループ、ハルシネーション多発) 高い(責任分界点が明確)
デバッグ容易性 低い(原因が分からない) 高い(原因が明確化した)
ガードレール 巨大なプロンプトに内包 最後の出力プロンプトの
Agentのみに適応

まとめ

ヘルスケアアプリのような複雑な要件をAgentic Workflowで実現するためには、「何でもできる神エージェント」を作ろうとせず、「専門家のチーム」を作ることが成功の鍵でした。

特に、PydanticモデルのDescriptionを活用して文脈(Context)を構造化データに変換するテクニックは、LLMの推論精度を劇的に向上させます。また、SorryAgentのような「断り担当」を設けることで、他のエージェントが余計なノイズに惑わされず、本来の性能を発揮できるようになりました。

今後は、この構造を活かしつつ、RAG Agent なども追加して「ポポラスって名前の由来は?」などより幅広い質問に答えられるようにしていく予定です。

また、今後のエントリではLLMのデバッグを劇的に楽にしたLangFuseの導入などにも触れたいと思います。


似顔絵
書いた人:佐藤


CSRFの生の挙動を知りたい~FastAPIとZAPを用いて検証してみた

初めまして、テックドクターでエンジニアをしている金子です。

CSRF(クロスサイト・リクエスト・フォージェリ)は非常に古典的な攻撃手法ですが、現代のWeb開発においてもその発生原理を正確に理解しておくことは重要だと思っています。

今回の記事では、CSRFについて、実際に手を動かして検証してみます。

この記事でやること

CSRFの基本的な仕組みを理解するために、わざと脆弱なサーバーを作成します。加えて「罠サイト」も用意して攻撃を再現し、何が起きているのかを脆弱性診断ツールのOWASP ZAPで観察します。

また、防御策としてCSRFトークンの有効性についても検証していきます。

※CSRF攻撃において、被害者に踏ませるためのページを本記事では「罠サイト」と呼びます。罠サイトは攻撃者が用意するもので、被害者がページを開いただけで標的サイトへリクエストが飛ぶ仕掛けが仕込まれています。

こんな人におすすめ

  • CSRFの名前は知っているが、実際どうやって攻撃が成立するのかピンときていない
  • フレームワークが守ってくれているため、生の脆弱な挙動を見たことがない
  • 攻撃の原理を自分の言葉で説明できるようになりたい

検証環境

  • ターゲットサーバー:FastAPI
  • 罠サイト(攻撃者):HTML + JavaScript(JSフレームワークは使いません)
  • ツール:OWASP ZAP、FoxyProxy

※今回は簡易的な検証のため、ターゲットサーバー、罠サイト共にローカル環境に設置します。

第1章:CSRFの仕組み

CSRFとは

CSRF(Cross-Site Request Forgery) は、日本語では「リクエスト強要」とも呼ばれます。被害者のブラウザを踏み台にして、本人の意図しないリクエストをWebアプリケーションに送信させる攻撃手法です。

攻撃の本質は「認証済みセッションの悪用」にあります。Webアプリケーションはリクエストに含まれるCookieを見て「この人は誰か」を判断しますが、そのリクエストが「本人の意思で送られたものか」までは検証していないことが多いです。CSRFではこの点が悪用されます。

攻撃者は罠サイトを用意し、被害者がそのページを開いた瞬間、被害者のブラウザから標的サイトへ不正なリクエストを自動送信させます。このとき被害者がたまたま標的サイトにログイン中であれば、ブラウザにより自動的にCookieが不正リクエストにも付与されます。そのため、サーバ側から見れば正規ユーザーからの正当なリクエストと区別がつかず、不正リクエストが処理されてしまうというわけです。

CSRFのしくみ(引用:IPA(独立行政法人 情報処理推進機構)より)

攻撃が成立する条件

CSRF攻撃が成立するには、以下の3つの条件が同時に満たされる必要があります。

  • 被害者が標的サイトにログイン済みである……ブラウザに標的サイトのセッションCookieが保存されており、有効な状態であること。ログアウト済み、またはセッション切れの場合は攻撃が成立しません。
  • 標的サイトがリクエストの正当性を検証していない……サーバ側で、CSRFトークンの検証、Refererヘッダのチェックなどを行っていない状態。つまり、「誰から送られたリクエストか」は見ているものの、「本人が意図して送ったリクエストか」までは確認しない実装になっている必要があります。
  • 被害者が攻撃者の用意した罠ページにアクセスする……メール内のリンク、SNSの投稿、不正広告など経路は様々です。被害者がそのページを開いた瞬間、隠されたフォームやimgタグによって標的サイトへリクエストが飛びます。多くの場合、被害者は攻撃が行われたことに気づきません。

※CSRFの仕組みについて、より詳細に知りたい方は次のサイトを参考にしてみてください。
安全なウェブサイトの作り方 - 1.6 CSRF(クロスサイト・リクエスト・フォージェリ) | 情報セキュリティ | IPA 独立行政法人 情報処理推進機構

具体的なインシデント例

PC遠隔操作事件(2012年)……Web掲示板のCSRF脆弱性を悪用し、無関係な一般市民のPCから犯罪予告を投稿させた事件です。被害者は誤認逮捕され、そのうち一人の大学生は自主退学に追い込まれました。サービス側のアクセスログには被害者のIPアドレスしか残らないため、当初は被害者が犯人だと疑われるなど、CSRFの「踏み台」としての恐ろしさを社会問題として浮き彫りにした事例です。

第2章:脆弱な環境を作る

では実際に検証を進めていきましょう。
まずは攻撃ターゲットとして、脆弱性を持つサーバーを構築します。

FastAPIによる簡単なサーバーの実装

このサーバーは下記のような機能を持っています。

  • ログイン機能(ログイン時、ブラウザにCookieをセット)
  • ユーザー情報としてID、パスワードのほか残高情報を持つ
  • パスワード変更APIが存在(CSRFトークンなし)

実装コードはこのようにしました。

from fastapi import FastAPI, Response, Cookie, HTTPException
from pydantic import BaseModel

app = FastAPI()

# 簡易ユーザーDB
users = {"alice": {"password": "pass123", "balance": 10000}}
sessions = {}

@app.post("/login")
def login(username: str, password: str, response: Response):
    if users.get(username, {}).get("password") == password:
        session_id = f"session_{username}"
        sessions[session_id] = username
        response.set_cookie("session_id", session_id, samesite="none", secure=False)
        return {"message": "ログイン成功"}
    raise HTTPException(401, "認証失敗")

class TransferRequest(BaseModel):
    to_user: str
    amount: int

@app.post("/transfer")  # ← CSRFトークンなし!
def transfer(req: TransferRequest, session_id: str = Cookie(None)):
    username = sessions.get(session_id)
    if not username:
        raise HTTPException(401, "未ログイン")
    users[username]["balance"] -= req.amount
    return {"message": f"{req.amount}円を送金しました"}

標的サイトの作成

次に標的となるサイトのページを作成しました。以下のような機能を持っています。

  • credentials: 'include' でCookieがやり取りする

実装コードの一部がこちらです。(重要な箇所のみ。HTML、CSSの全部、JavaScriptの一部は割愛します)

// ログイン時(ポイント:credentials: 'include')
  const res = await fetch(`${API_BASE}/token`, {
      method: 'POST',
      body: formData,
      credentials: 'include'  // ← Cookieを受け取る
  });

// 送金時(ポイント:Cookieが自動送信される)
  const res = await fetch(`${API_BASE}/transfers`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
      credentials: 'include'  // ← Cookieが自動で付く
  });

実装した画面

上記により、このような画面ができました。

  • ログイン画面

画面キャプチャ

  • 新規登録画面

画面キャプチャ

  • ダッシュボード

画面キャプチャ

第3章:罠サイトを作る

次に罠サイトを作成します。これは実際のCSRF攻撃においては攻撃者が作成するものです。下記のような機能を持っています。

  • 隠しフォームを持つ
  • フォーム自動送信のJavaScriptが存在(今回はボタンクリック時)

実装コード(CSSは割愛)

<form id="csrfForm" action="https://localhost:8443/api/transfers" method="POST">
      <input type="hidden" name="receiver_user_id" value="attacker">
      <input type="hidden" name="amount" value="5000">
  </form>
  
  <script>
  // 被害者がクリックすると...
  document.getElementById('claimBtn').addEventListener('click', async () => {
      await fetch('https://localhost:8443/api/transfers', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
              receiver_user_id: 'attacker',
              amount: 5000
          }),
          credentials: 'include'  // ← 被害者のCookieが送られる!
      });
  });
  </script>

実装した画面

画面キャプチャ

ここまでで、検証に必要な環境の作成が完了しました。

第4章:OWASP ZAPで検証

いよいよ、ツールを使ってCSRFの挙動を検証していきます。使用するツールは2つです。

OWASP ZAP

OWASP(Open Web Application Security Project)が開発するオープンソースのセキュリティテストツールです。ローカルプロキシとして動作し、ブラウザとWebサーバー間を流れるHTTP通信をすべて傍受・記録できます。

主な機能は以下の通りです。

  • リクエスト/レスポンスの可視化……ブラウザが裏で送っているリクエストの中身(ヘッダ、Cookie、POSTパラメータ等)を確認できる
  • リクエストの改ざん・再送信……傍受したリクエストを編集して再送信することで、パラメータ操作の影響を検証できる
  • 自動スキャン……既知の脆弱性パターンを自動検出する機能もあります。(本章では使用しません)
FoxyProxy

プロキシ設定の切り替えを簡単にするブラウザ拡張機能です。ChromeFirefoxに対応しています。
通常、ZAPのようなローカルプロキシを使うにはブラウザのネットワーク設定を手動で変更する必要がありますが、FoxyProxyを使えばワンクリックで「ZAP経由」と「直接接続」を切り替えられます。検証作業中に頻繁にプロキシのON/OFFを行う場面で便利です。

攻撃の再現

下準備として、被害者のアカウントと攻撃者のアカウントをそれぞれ作成しました。単純に作成するだけなので、画像の貼付は行いません。作成時の条件とユーザIDのみ記載します。

  • 被害者のユーザーID:test_target1
  • 攻撃者のユーザーID:attacker1
  • 初期値として設定した残高:いずれのユーザーも10,000円

以下、攻撃成立までのフローです。

1. 被害者ユーザーとして正規サイトにログインします。
画面キャプチャ
画面キャプチャ
ここでZAPを確認すると、バックエンドからバックエンドからアクセストークンが発行され、Cookieに保存されたことが分かります。
画面キャプチャ


2. 別タブで罠サイトを開きます。
画面キャプチャ
画面キャプチャ


3. 「景品を受け取る」ボタンをクリックします。

その結果、Javascriptにより攻撃者に送金をするリクエストがバックエンドに送信されます。
このとき、ZAPでCookieヘッダーを見てみると、被害者がログインしたときと同じAccessTokenが使われてしまっていることがわかります。

画面キャプチャ

リクエストヘッダーとJSONペイロードも確認してみましょう。

receiver_user_id(送金を受けるアカウント)には攻撃者のアカウントが、amount(送金額)には5000円が設定されています。(このとき、sender_id(送金者)のような送金者を特定するパラメータがありませんが、バックエンドではCookieのアクセストークンによって送金者を特定できるので、仕組みとしてはsender_idを指定しなくても送金処理を実行できます。)

送金完了後に被害者のダッシュボードで画面を更新すると、残高が5000に減っていることが分かります。

画面キャプチャ

また、攻撃者のアカウントにログインすると残高が15,000に増えていることが分かります。

画面キャプチャ

以上で、CSRF攻撃が成立するまでの挙動が確認できました。

第5章:CSRFへの防御策を検証する

攻撃について理解したところで、次は防御策についても検証してみたいと思います。
CSRFへの防御策のひとつに、CSRFトークンの実装が挙げられます。

CSRFトークンの仕組み

  1. 発行: ユーザーがフォームのある画面(送金画面など)を開いたとき、サーバーは乱数で作った、「予測不可能なトークン」を生成し、HTMLの隠しフィールド(input type="hidden")に埋め込んでユーザーに渡します
  2. 送信:ユーザーが送信ボタンを押すと、入力データと一緒にこの「トークン」もサーバーへ送られます
  3. 検証: サーバーは、送られてきたトークンが、ステップ1で自分が発行したものと一致するかを確認します。
    • 一致すれば……「正規の画面からの操作だ」と判断して処理を実行
    • 不一致/トークンなしなら……「不正なリクエストだ」と判断して拒否

CSRFトークンの実装

実際に実装してみましょう。全てを掲載すると長くなるので、実装したコードのうち重要な部分のみを例として掲載します。

1. CSRFトークンの生成とCookie設定(ログイン時)

# auth.py - ログイン時にCSRFトークンをCookieにセット
import secrets

def generate_csrf_token() -> str:
    return secrets.token_hex(32)

# ログインエンドポイント内
csrf_token = generate_csrf_token()
response.set_cookie(
    key="csrf_token",
    value=csrf_token,
    httponly=False,  # JSから読み取り可能にする(重要)
    samesite="none",
    secure=True,
)


2. CSRFトークンの検証(サーバー側)

# auth.py - CSRFトークン検証関数
def verify_csrf_token(
    csrf_token_cookie: str | None = Cookie(alias="csrf_token"),
    csrf_token_header: str | None = Header(alias="X-CSRF-Token"),
) -> None:
    """CookieとヘッダーのCSRFトークンを比較"""
    if csrf_token_cookie is None or csrf_token_header is None:
        raise HTTPException(status_code=403, detail="CSRF token missing")

    if csrf_token_cookie != csrf_token_header:
        raise HTTPException(status_code=403, detail="CSRF token mismatch")

# transfer.py - 送金エンドポイントで検証を適用
@router.post("/transfers")
def transfer_money(
    request: TransferRequest,
    current_user: Annotated[UserAuth, Depends(get_current_user)],
    _csrf: Annotated[None, Depends(verify_csrf_token)],  # ← これを追加
):
    ...


3. CSRFトークンをヘッダーに含める(フロントエンド)

// dashboard.html - CookieからCSRFトークンを取得してヘッダーに含める
function getCsrfToken() {
    const cookies = document.cookie.split(';');
    for (const cookie of cookies) {
        const [name, value] = cookie.trim().split('=');
        if (name === 'csrf_token') return value;
    }
    return null;
}

// 送金リクエスト
const res = await fetch('/api/transfers', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': getCsrfToken()  // ← これを追加
    },
    body: JSON.stringify(data),
    credentials: 'include'
});

再検証

ここまででCSRFトークンの実装が完了しました。

では再びOWASP ZAPを用いて、防御策がちゃんと機能しているか検証してみましょう。

1. 被害者のアカウントにログインします。
画面キャプチャ
ZAPを確認すると、アクセストークンとCSRFトークンが発行されていることがわかります。
画面キャプチャ


2. 別タブで罠サイトを開き、「景品を受け取る」ボタンをクリックします。

ZAPを確認してみると……
画面キャプチャ
X-CSRF-Tokenがリクエストのヘッダーに存在しないことと、403エラーが出て不正なリクエストが処理されていないことが確認できます。

無事、CSRF攻撃を防御することができました!


3. 念のため、正常時のリクエストも確認します。

現在の残高は5,000円です。

画面キャプチャ

ダッシュボードより2,500円の送金操作を行いました。

画面キャプチャ

送金が実施され、残高が2,500円になりました。

ZAPでリクエストの内容を確認してみましょう。
X-CSRF-Tokenがリクエストのヘッダーに含まれていることが分かります。
画面キャプチャ

送金先である、攻撃者のアカウントにログインをして残高を確認します。
画面キャプチャ
残高が増えていました。

正常なリクエストは、正常に処理されることが確認できました。

他の防御方法

なお、CSRF対策としてCSRFトークンが唯一の手段ではありません。
他にも下記のような対策が考えられます。

  • SameSite属性の設定
  • Referer/Originヘッダの検証

本記事の趣旨から逸れるのでこれらについて詳しくは説明しませんが、複数の防御策を組み合わせ、耐攻撃性の高い実装をすることが重要です。

まとめ

今回の検証を通じて、現代のフレームワークは開発者が意識せずともCSRFから守ってくれているという事実を、肌で感じることができました。

最近はAIを使って実装をする機会が増えましたが、生成されたコードでは「何となく動くけれど、仕組みはよく分からない」という状態に陥ることも少なくありません。

こうした「何となく」の知識を確かなものにするためには、時間をかけて手を動かし、実践するプロセスもやはり重要だと改めて感じました。


似顔絵
書いた人:金子

自己申告に頼らない飲酒測定――ウェアラブルでとらえる飲酒とその影響

1.飲酒習慣を測ることの難しさ

こんにちは。この記事は、テックドクターでデータ解析を担当する坂本と藤本が共同で執筆しています。

みなさんが健康診断を受けると、飲酒の習慣を尋ねられると思います。私(坂本)はよく、「飲み過ぎ注意」と言われていました。飲酒は、健康のいろいろな面に影響を与えるとされています(※1)

飲酒の量や習慣は、主に質問紙(アンケート)などで測定されます。しかし、例えば、過去7日間を振り返って飲酒の量を思い出して回答した場合、実際より少ない量で回答してしまうなどのリスクがあると言われています(※2)
もちろん、質問紙を用いた飲酒の測定も十分に有効活用できますが、毎日の記録/長期的な記録が必要な場面では、回答の負担が大きく、継続も難しいです。

そんなとき、スマートウォッチで自動的に飲酒の記録ができたら便利ですよね。
こういった背景から、私たちは「スマートウォッチを代表とするウェアラブル端末を装着しているだけで、飲酒を自動検出し、飲酒の影響を評価する」という技術開発に挑戦することにしました。

そして先日、その成果について、第32回日本行動医学会学術総会にてポスター発表を行いました(※3)。この記事では、その発表をもとに、「ウェアラブル端末で飲酒の検出と影響評価を行う技術」、そして「ウェアラブル端末のデータからわかった、飲酒が睡眠や翌日の活動に与える影響」について紹介したいと思います。

※1
例えば、厚生労働省「健康に配慮した飲酒に関するガイドラインについて」, URL: https://www.mhlw.go.jp/stf/newpage_38541.html, Last Accsess: 2025-12-10)。

※2
例えば、
Gmel G, Daeppen JB. Recall bias for seven-day recall measurement of alcohol consumption among emergency department patients: implications for case-crossover designs. J Stud Alcohol Drugs. 2007 Mar;68(2):303-10. doi: 10.15288/jsad.2007.68.303. PMID: 17286350.

※3
◎坂本・◯藤本・深見(2025). ウェアラブル端末を用いた20歳以上の一般成人を対象とする飲酒の多変量予測モデルの開発とアルコール飲料摂取が日常生活に与える影響の評価. 第32回日本行動医学会学術総会, 神奈川県相模原市 相模原市立産業会館, 2025年12月6日. (◎は責任著者・発表者、◯は発表者)

イメージ画像

2.飲酒日を検出する指標の開発

ウェアラブルデバイスデータを用いた「飲酒日」の検出

ウェアラブル端末では、主に心拍数、歩数、睡眠などの情報が取得できます。
しかし、例えば「心拍数が高い」というだけでは、運動や緊張、発熱などと区別がつけられず、飲酒を特定することができません。飲酒を見つけ出すためには相応の工夫が必要です。

そこで、「お酒を飲むと心拍が早くなる」という経験に着想を得て、「歩いていない時間に持続的に心拍数が上昇する」という特徴を定量化する計算手法を開発しました。
この指標を、本記事では「飲酒指標(Alcohol Index)」と呼ぶことにしましょう。この指標が、飲酒と高い関連性を示しました(※4)

※4
2026年1月現時点: 特許出願中

グラフ画像
図. 飲酒があった日の飲酒指標の例

飲酒指標を代表的な情報として、睡眠中の心拍数などを加えた特徴量を用いて機械学習モデルによる解析(XGBoost)を行ったところ、飲酒日の検出精度は90%程度となりました。

グラフ画像
図. 飲酒日検知モデルの性能

予測モデルの妥当性評価と研究報告

飲酒の検出精度が90%と聞くと、「すぐにでも実用化できる」ように感じられるかもしれません。しかし、安全な技術を確立するためには、多面的な検証を積み重ねる必要があります。
例えば、「どのような人でも同じ性能が得られるのか」や「誤判定が生じやすい条件は何か」といったような、公平性やリスク、安全性等に関する検証や対策が必要となります。

研究報告において大切なのは、何が未検証/未対策であるかを誠実に報告し、次の検証に繋げることです。「TRIPOD+AI声明」という研究報告で求められる事項が整理された国際的な指針(※5)があるので、それに沿って多面的な評価と今後の課題をまとめました。

ウェアラブル端末を装着しているだけで飲酒の自動記録が可能になり、健康管理など様々な場面で活用できるようになるという未来に向けて、検証を積み重ねたいと考えています。

※5
Collins G S, Moons K G M, Dhiman P, Riley R D, Beam A L, Van Calster B et al. TRIPOD+AI statement: updated guidance for reporting clinical prediction models that use regression or machine learning methods BMJ 2024; 385 :e078378 doi:10.1136/bmj-2023-078378


3.ウェアラブルデバイスを用いた飲酒の翌日の影響評価

次に、実際に飲酒が睡眠や翌日の活動にどのような影響を及ぼすかについても解析しました。

社内での取り組みを通して可視化できたデータをご紹介していきます。

心拍変動指標について

今回解析に利用した、心拍変動に関する指標を紹介しておきます。

  • RMSSD……副交感神経活動を反映する指標。これが低下した場合、十分にリラックスできていないと考えられる。
  • SDNN……交感・副交感神経の両方の変動を反映する指標であり、値が高いほど活発な自律神経活動が発生していたと考えられる。

データと参加者

下記の対象者/対象データを解析しました。

  • 21名、4932日分の飲酒報告と対応する日のウェアラブル端末装着情報を利用(男性57%)
  • 前日の飲酒報告(非飲酒・少量・中量・大量)
  • アンケート回答日と翌日のウェアラブル装着がある者
  • 装着時間が70%以上の日のみ採用
  • 平均値±3SD以内の睡眠時間の日

※今回は飲酒の有無はアンケートを利用して判定しました。

解析方法

解析では、一元配置分散分析、線形混合モデルと呼ばれる手法を使用しました。

  • 一元配置分散分析(ANOVA)……飲酒量4段階の群間差を比較
  • 線形混合モデル(LMM)……個人差を調整し、飲酒有無の影響を精緻に検討

解析結果

まず、一元配置分散分析により見えてきた結果をご紹介していきます。

睡眠 大量の飲酒をした場合、少量・中量飲酒に比べてさらに睡眠中の最低心拍が高くなっていました。(図1)
翌日の心拍 大量の飲酒をすると、翌日の安静時心拍数が高くなっていました。(図2)
翌日の活動 少量・中量の飲酒をすると、飲酒しなかった場合に比べて翌日の歩数が多くなっていました。(図3)
大量の飲酒をすると、少量・中量の飲酒に比べて翌日の歩行ペースが遅くなっていました。(図4)
少量の飲酒をすると、飲酒しなかった場合に比べて翌日の高強度運動時間割合が上がっていました。(図5)

次に、線形混合モデルにより見えてきた結果をご紹介します。

睡眠 飲酒をすると睡眠中の最低心拍が高くなっていました。(図6)
飲酒をすると睡眠中のSDNNが高くなっていました。(図7)
飲酒をすると睡眠中のRMSSDが低くなっていました。(図8)
翌日の心拍 飲酒をすると翌日の安静時心拍数が高くなっていました。(図9)
グラフ画像
図1: 飲酒レベルごとの睡眠中の最低心拍数比較
グラフ画像
図2: 飲酒レベルごとの安静時心拍数比較
グラフ画像
図3: 飲酒レベルごとの翌日の合計歩数比較
グラフ画像
図4: 飲酒レベルごとの翌日の歩行ペース比較
グラフ画像
図5: 飲酒レベルごとの翌日の高強度運動時間比較
グラフ画像
図6: 飲酒日と非飲酒日の睡眠中の最低心拍数比較
グラフ画像
図7: 飲酒日と非飲酒日の睡眠中のSDNN比較
グラフ画像
図8: 飲酒日と非飲酒日の睡眠中のRMSSD比較
グラフ画像
図9: 飲酒日と非飲酒日の安静時心拍数比較

考察

飲酒をすると、しなかった日に比べて睡眠中の最低心拍およびSDNNが高くなり、RMSSDが低くなっています。このことから、飲酒した日は自律神経が交感神経優位な状態になりやすく、睡眠の質が低下している可能性があります。

また、大量の飲酒をすると中量以下の飲酒時に比べて睡眠中の最低心拍が高くなり、翌日の歩行ペースが遅くなります。ここからは、大量の飲酒は特に睡眠および翌日の活動に大きな影響を与える可能性が読み取れます。
一方で、少量の飲酒では飲酒していない場合との差が少ないことや、むしろ活動量が増える人もおり、個人差が大きい可能性が考えられます。

4.まとめ

ウェアラブルデバイスのデータを使用することで、飲酒日を検出する指標の開発と、飲酒の影響の評価を行うことができました。

さらなる研究によって、より安全で確かな技術を確立してゆくことが必要とされています。技術開発が進めば、アンケートの併用がなくともより手軽に飲酒の影響についての解析ができるようになるかもしれません。
ウェアラブルデバイスを活用することで日々の行動を振り返る負担を減らしつつ、健康状態への理解を深めることができます。
こうした技術が、一人ひとりの生活を少し便利に、そして健やかにする助けになることを期待しています。

5.今後の展望

飲酒を検知する技術については、まだ様々な方々を対象とした多面的な検証と安全性の評価を必要としています。

本テーマに関する共同開発にご関心のある方がいらっしゃいましたら、お気軽に、お問い合わせフォームよりテックドクターまでご連絡ください。

似顔絵似顔絵
書いた人:坂本、藤本

FlutterのFlavor設定完全ガイド:iOS/Android対応のマルチ環境を構築する

モバイルアプリ開発では、開発・ステージング・本番など複数の環境を切り替えて運用することがあります。
Flutterでのアプリ開発において、その運用に役立つのがFlavorです。

Flavorは、アプリを複数の環境ごとに設定を切り替えるための仕組みです。
例えば「開発環境ではテスト用API、本番環境では本番APIを使いたい」といった場合に、Flavorを使うことでビルド時に環境を選択し、自動的に異なる設定を適用することができます。

この記事では、そんな便利なFlavorの設定方法をご紹介します。iOS/Androidの両方に対応しているので、マルチプラットフォーム向けアプリの開発に役立ててください。

この記事はこんな人向けです
  • Flutterで複数環境のアプリビルドを構築したい方
  • iOS/Android両方のFlavor設定を網羅的に知りたい方
  • Firebase等の外部サービスを環境ごとに切り替えたい方

 

1. 概要:Flavorとは

Flutterでは「Flavor」という仕組みを使って、同一コードベースから異なる設定のアプリをビルドできます。

Flavorを使うことで、例えばこういった設定を環境ごとに切り替えられます:

  • アプリ名・アイコン:開発版と本番版を異なる見た目にすることで視覚的に区別できます
  • Bundle ID / Application ID:同一端末に複数環境のアプリを共存させることができます
  • APIエンドポイント:環境ごとに接続先の自動切り替えが可能です
  • Firebase設定:環境ごとに異なるFirebaseプロジェクトを使用できます
プラットフォームごとの実現方法

AndroidとiOSそれぞれにおいて、Flavorを定義する方法は下記のとおりです。

  • Android:Gradleの productFlavors 機能を使用
  • iOS:XcodeのSchemeとBuild Configurationを使用

それぞれ、詳しいやり方は記事中で説明していきます。

本記事で構築する環境(Flavor)

本記事では、Flutter公式ドキュメントを参考に、4環境(local、development、sandbox、production)のFlavor設定手順を解説します。

Flavor 用途 アプリ名 Bundle ID /
Application ID
local ローカル開発 [LOC] アプリ名 *.local
development 開発サーバー接続 [DEV] アプリ名 *.development
sandbox ステージング環境 [SAN] アプリ名 *.sandbox
production 本番リリース アプリ名 (サフィックスなし)

では、具体的な手順を紹介していきましょう。

2. Android側のFlavor設定

まずはAndroidでの構成方法です。
Gradleの productFlavors 機能を使用し、シンプルに定義できます。

build.gradleの設定

android/app/build.gradleflavorDimensionsproductFlavors を追加します。

android {
   // ... 既存の設定

   flavorDimensions += "default"

   productFlavors {
       create("local") {
           dimension = "default"
           manifestPlaceholders = [
               appName: "[LOC] アプリ名",
           ]
           applicationIdSuffix = ".local"
       }
       create("development") {
           dimension = "default"
           manifestPlaceholders = [
               appName: "[DEV] アプリ名",
           ]
           applicationIdSuffix = ".development"
       }
       create("sandbox") {
           dimension = "default"
           manifestPlaceholders = [
               appName: "[SAN] アプリ名",
           ]
           applicationIdSuffix = ".sandbox"
       }
       create("production") {
           dimension = "default"
           manifestPlaceholders = [
               appName: "アプリ名",
           ]
           applicationIdSuffix = ""
       }
   }
}


ポイント解説

  • flavorDimensions:Flavorのグループを定義します。複数の軸でFlavorを管理する場合に使用します(例:有料版/無料版 × 環境)
  • applicationIdSuffix:ベースのApplicationIDに追加されるサフィックスです。これにより同一端末に複数環境のアプリをインストール可能にできます。
  • manifestPlaceholders:次に説明するAndroidManifest.xmlで参照するための変数を定義します。
AndroidManifest.xmlでの変数参照

くわえて、android/app/src/main/AndroidManifest.xml でplaceholderを参照します。

<application
   android:label="${appName}"
   android:name="${applicationName}"
   android:icon="@mipmap/ic_launcher">
   <!-- ... -->
</application>

Android側の設定手順は以上です!

3. iOS側のFlavor設定

iOSのFlavor設定はAndroidより少し複雑です。
XcodeのSchemeとBuild Configurationを使用します。まずBuild ConfigurationとSchemeを作成し、それぞれを紐づけたあと、具体的な設定を定義していく流れです。

Build Configurationの作成

Xcodeで`Runner.xcodeproj`を開き、以下の手順でBuild Configurationを作成します。

  1. プロジェクトナビゲータで Runner プロジェクトを選択
  2. Info タブ → Configurations セクションを開く
  3. 「+」ボタンで以下のConfigurationを追加:
Configuration名 複製元
Debug-local Debug
Debug-development Debug
Debug-sandbox Debug
Debug-production Debug
Release-local Release
Release-development Release
Release-sandbox Release
Release-production Release
Profile-local Release
Profile-development Release
Profile-sandbox Release
Profile-production Release

重要:Configuration名は必ず {BuildType}-{flavor名} の形式にしてください。Flutterがこの命名規則でFlavorを識別します。

Schemeの作成

次に、各FlavorごとにSchemeを作成します。

  1. ProductSchemeNew Scheme… を選択
  2. Scheme名を入力(例:localdevelopmentsandboxproduction

重要:Scheme名は小文字で、Flavor名と完全に一致させるようにしてください。

SchemeとConfigurationの紐付け

作成したSchemeを、Configurationに割り当てていきます。

  1. ProductSchemeManage Schemes… を開く
  2. 各Schemeを選択し「Edit...」をクリック
  3. 以下のように各アクションにConfigurationを割り当て:


localスキームの例:

アクション Build Configuration
Run Debug-local
Test Debug-local
Profile Profile-local
Analyze Debug-local
Archive Release-local


重要:全てのSchemeで「Shared」にチェックを入れてください。これにより.xcscheme ファイルがリポジトリにコミットされ、チームで共有できます。


作成されたSchemeファイル( ios/Runner.xcodeproj/xcshareddata/xcschemes/ 配下)の例:

<?xml version="1.0" encoding="UTF-8"?>
<Scheme version = "1.7">
  <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "YES">
     <!-- 省略 -->
  </BuildAction>
  <TestAction buildConfiguration = "Debug-local">
     <!-- 省略 -->
  </TestAction>
  <LaunchAction buildConfiguration = "Debug-local">
     <!-- 省略 -->
  </LaunchAction>
  <ProfileAction buildConfiguration = "Profile-local">
     <!-- 省略 -->
  </ProfileAction>
  <AnalyzeAction buildConfiguration = "Debug-local">
  </AnalyzeAction>
  <ArchiveAction buildConfiguration = "Release-local" revealArchiveInOrganizer = "YES">
  </ArchiveAction>
</Scheme>

 

Build Settingsの設定

各ConfigurationでのBundle IDやアプリ表示名を設定します。

  1. Runner ターゲット → Build Settingsタブを開く
  2. 「+」→「Add User-Defined Setting」で以下のカスタム設定を追加:

DISPLAY_PRODUCT_NAME_PREFIX(アプリ名のプレフィックス):

Configuration
Debug-local [LOC]
Debug-development [DEV]
Debug-sandbox [SAN]
Debug-production (空)
Release-* 同上
Profile-* 同上


PRODUCT_BUNDLE_IDENTIFIER:

Configuration
*-local com.example.app.local
*-development com.example.app.development
*-sandbox com.example.app.sandbox
*-production com.example.app

 

Info.plistの設定

ios/Runner/Info.plist でBuild Settings変数を参照します。
これにより、アプリのビルド時にはConfigurationごとに設定した値が使用されるようになります。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
   <!-- アプリ表示名:プレフィックス + アプリ名 -->
   <key>CFBundleDisplayName</key>
   <string>$(DISPLAY_PRODUCT_NAME_PREFIX)アプリ名</string>

   <!-- Bundle IDはBuild Settingsから自動取得 -->
   <key>CFBundleIdentifier</key>
   <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>

   <!-- その他の設定... -->
</dict>
</plist>

iOS側の設定は以上で完了です!

4. Firebase設定(FlutterFire CLI)

複数の設定ファイルを生成することで、Flavorごとに異なるFirebaseプロジェクトを使用することができます。ここではその設定方法を紹介します。

事前に準備しておくこと

  • 各Flavorに対応したFirebaseプロジェクトが作成済み
  • Firebase CLIとFlutterFire CLIがインストール済み
# Firebase CLIのインストール
npm install -g firebase-tools
firebase login

# FlutterFire CLIのインストール
dart pub global activate flutterfire_cli

 

Flavor別の設定ファイルを生成

各Flavorに対して flutterfire config コマンドを実行し、設定ファイルを生成します。
こうすることで、FlutterFire CLIはDart設定ファイルの生成に加えて、iOS/Android両方の設定ファイルを適切な場所に配置し、必要なBuild Phase設定も自動で行います。

# development環境の設定
flutterfire config \
 --project=your-project-development \
 --out=lib/config/firebase/firebase_options_development.dart \
 --ios-bundle-id=com.example.app.development \
 --ios-out=ios/flavors/development/GoogleService-Info.plist \
 --android-package-name=com.example.app.development \
 --android-out=android/app/src/development/google-services.json

# sandbox環境の設定
flutterfire config \
 --project=your-project-sandbox \
 --out=lib/config/firebase/firebase_options_sandbox.dart \
 --ios-bundle-id=com.example.app.sandbox \
 --ios-out=ios/flavors/sandbox/GoogleService-Info.plist \
 --android-package-name=com.example.app.sandbox \
 --android-out=android/app/src/sandbox/google-services.json

# production環境の設定
flutterfire config \
 --project=your-project-production \
 --out=lib/config/firebase/firebase_options_production.dart \
 --ios-bundle-id=com.example.app \
 --ios-out=ios/flavors/production/GoogleService-Info.plist \
 --android-package-name=com.example.app \
 --android-out=android/app/src/production/google-services.json

 
ここで使用する主要なオプション

オプション 説明
--project FirebaseプロジェクトID
--out Dart設定ファイルの出力先
--ios-bundle-id iOSのBundle ID
--ios-out GoogleService-Info.plistの出力先
--android-package-name AndroidのパッケージID
--android-out google-services.jsonの出力先

 

生成されるファイル構成

コマンド実行後、以下のファイルが生成されます:

lib/config/firebase/
├── firebase_options_development.dart
├── firebase_options_sandbox.dart
└── firebase_options_production.dart

ios/flavors/
├── development/
│   └── GoogleService-Info.plist
├── sandbox/
│   └── GoogleService-Info.plist
└── production/
   └── GoogleService-Info.plist

android/app/src/
├── development/
│   └── google-services.json
├── sandbox/
│   └── google-services.json
└── production/
   └── google-services.json

つづいて、各FirebaseプロジェクトをDartのコード内から扱うための設定をします。

Flavor enumの定義

lib/config/env/flavor.dart でFlavorを定義します。Dart 3以降ではenumに直接メソッドやgetterを定義できます。

import 'package:firebase_core/firebase_core.dart';
import 'package:your_app/config/firebase/firebase_options_development.dart'
   as development;
import 'package:your_app/config/firebase/firebase_options_production.dart'
   as production;
import 'package:your_app/config/firebase/firebase_options_sandbox.dart'
   as sandbox;

enum Flavor {
 local,
 development,
 sandbox,
 production;

 /// Dart define から Flavor を取得
 static const _flavorStr = String.fromEnvironment('FLAVOR');
 static Flavor get fromDartDefine => Flavor.values.byName(_flavorStr);

 /// Firebase設定
 FirebaseOptions get firebaseOptions {
   switch (this) {
     case Flavor.local:
     case Flavor.development:
       return development.DefaultFirebaseOptions.currentPlatform;
     case Flavor.sandbox:
       return sandbox.DefaultFirebaseOptions.currentPlatform;
     case Flavor.production:
       return production.DefaultFirebaseOptions.currentPlatform;
   }
 }
}

設定自体はこれで完了です。

main.dartでの使用例

実際にDartのコード内でFirebaseを扱うにはこのようにします。

void main() async {
 WidgetsFlutterBinding.ensureInitialized();

 final flavor = Flavor.fromDartDefine;

 // Flavorに応じたFirebase初期化
 await Firebase.initializeApp(
   options: flavor.firebaseOptions,
 );

 runApp(MyApp(flavor: flavor));
}

以上で、Firebaseに関する設定は完了です。

5. アプリアイコンの切り替え

flutter_launcher_icons パッケージを使用して、Flavor別のアイコンを生成することもできます。
各環境用のアプリを視覚的に区別できるようになり、便利です。

アイコン画像の配置

各Flavor用のアイコン画像を作成し、assets/app_icons/ ディレクトリに配置します。

assets/
└── app_icons/
   ├── app_icon_local.png
   ├── app_icon_development.png
   ├── app_icon_sandbox.png
   └── app_icon_production.png

アイコン画像は1024x1024px以上の正方形PNGファイルを推奨します。

設定ファイルの作成

各Flavor用の設定ファイルを、プロジェクトルートに作成します。

flutter_launcher_icons-local.yaml

flutter_launcher_icons:
 image_path: "assets/app_icons/app_icon_local.png"
 android: true
 ios: true
 remove_alpha_ios: true

 
flutter_launcher_icons-development.yaml

flutter_launcher_icons:
 image_path: "assets/app_icons/app_icon_development.png"
 android: true
 ios: true
 remove_alpha_ios: true

 
flutter_launcher_icons-sandbox.yaml

flutter_launcher_icons:
 image_path: "assets/app_icons/app_icon_sandbox.png"
 android: true
 ios: true
 remove_alpha_ios: true

 
flutter_launcher_icons-production.yaml

flutter_launcher_icons:
 image_path: "assets/app_icons/app_icon_production.png"
 android: true
 ios: true
 remove_alpha_ios: true

 

アイコン生成

最後に、下記のコマンドを実行するとアイコンの生成が行われます。

# 全Flavorのアイコンを生成
dart run flutter_launcher_icons

# 特定Flavorのみ生成
dart run flutter_launcher_icons --flavor development

以上で、全ての設定が完了しました。

7. 実行方法

最後に、ここまでで設定したFlavorを使い、実際にアプリを実行/ビルドする方法を紹介します。

コマンドラインから実行
# local環境で実行
flutter run --flavor local --dart-define=FLAVOR=local

# development環境で実行
flutter run --flavor development --dart-define=FLAVOR=development

# sandbox環境でリリースビルド
flutter run --flavor sandbox --dart-define=FLAVOR=sandbox --release

# production環境でアーカイブ
flutter build ios --flavor production --dart-define=FLAVOR=production
flutter build appbundle --flavor production --dart-define=FLAVOR=production

 

Makefileでの管理

毎回コマンドを打つのは面倒なので、Makefileにまとめると便利です。

run: ## Run app. Pass F={local,development,sandbox,production}, M={profile,release} (optional)
ifeq (${M},)
 flutter run --flavor ${F} --dart-define=FLAVOR=${F}
else
 flutter run --flavor ${F} --dart-define=FLAVOR=${F} --${M}
endif

 
使用例:

make run F=development        # デバッグモード
make run F=production M=release  # リリースモード

 

IDE(VS Code / Android Studio)での実行

各IDEで実行する場合は、それぞれ設定が必要です。

VS Code(設定)
.vscode/launch.json に設定を追加:

{
 "version": "0.2.0",
 "configurations": [
   {
     "name": "Local",
     "request": "launch",
     "type": "dart",
     "args": ["--flavor", "local", "--dart-define=FLAVOR=local"]
   },
   {
     "name": "Development",
     "request": "launch",
     "type": "dart",
     "args": ["--flavor", "development", "--dart-define=FLAVOR=development"]
   },
   {
     "name": "Sandbox",
     "request": "launch",
     "type": "dart",
     "args": ["--flavor", "sandbox", "--dart-define=FLAVOR=sandbox"]
   },
   {
     "name": "Production",
     "request": "launch",
     "type": "dart",
     "args": ["--flavor", "production", "--dart-define=FLAVOR=production"]
   }
 ]
}


上記の設定により、Run and Debugから構成名を選択して実行できるようになります。

Android Studio(設定)

  1. 上部メニューから RunEdit Configurations... を選択
  2. 左上の「+」ボタンをクリックし、「Flutter」を選択
  3. 各Flavor用の設定を作成:
項目 設定値(developmentの例)
Name Development
Dart entrypoint lib/main.dart
Additional run args --dart-define=FLAVOR=development
Build flavor development

4. 同様に local 、sandbox、production 用の設定も作成

これにより、ツールバーのドロップダウンから実行したいFlavorを選択して実行できるようになります。

8. まとめ


本記事では、FlutterのFlavor設定をiOS/Android両対応で解説しました。

ポイントのおさらい
  1. Android:build.gradleproductFlavors で設定する
  2. iOS:Xcode Scheme + Build Configurationで設定する。命名規則が重要
  3. Firebase:FlutterFire CLIで各Flavorの設定ファイルを生成する
  4. Dart:--dart-define でFlavorを渡し、enumで管理する

Flavorを適切に設定することで、開発効率とリリース品質の両方を向上させることができます。

行動を分解すると、課題や解決策が見えてくる~「プロダクトデザインの第一歩」体験ワークショップ

こんにちは、プロダクトデザイナーの庄司です。
今回は、社内で「プロダクト開発を皆に身近に思ってもらう」を目的として開催したワークショップについて紹介します。

弊社では毎月末にその月の成果等を発表する締め会(*1)を行っています。

その中に毎月テーマを変えた全社ワークショップの時間があり(*2)、先日は私がファシリテーターとなりプロダクトデザインに関するワークショップを開催しました。

*1 参考:文化は育てるもの。また“締め会”で会いましょう
*2 参考:問いを囲んで、チームを耕す月イチワークショップ

デザインとは設計である

もともとは、毎月のワークショップを主催している組織開発のメンバーから打診を受けたのがきっかけです。

「新規プロダクトのリリース予定もあることだし、プロダクト開発を皆に身近に思ってもらえるように、デザインのワークショップを行いませんか?」というオファーでした。

説明スライドのキャプチャ

  1. 新規アプリの開発が進行中だが、社内のデザインへの理解がまだ浅い。
  2. プロダクト開発全体をデザインの観点で巻き込むことが必要。

このような課題意識を受け、情報設計の体験をしてもらうワークショップを考えました。
プロダクトデザインは、デザインという言葉のイメージから、どうしても「見た目を整える仕事でしょ?」と思われがち。確かにそういう側面もあるのですが、実際にはプロダクトの体験やそのプロダクトがもたらす課題解決まで含めた情報設計を行う仕事です。デザインとは設計であると理解してもらうことが第一歩。

そんなわけで、学生時代に「インフォメーション・アーキテクト」の授業で行ったワークを大いに思い出しつつ、多少の応用を加えたワークショップを考えました。

このワークショップは、2つのパートで構成しました。
①行動を洗い出す
②課題を見つけ、ソリューションを考える
順に紹介していきます。

①行動を洗い出してみる

例として「カップやきそばを作って食べる」場合。
その中にあるたくさんの行動を、ひとつひとつ書き出してみます。
「ビニール包装を取る」「蓋を開ける」「あと入れの袋を取り出す」……。

実際にやってみると、全てを書き出すのは果てしない作業に思えてくるかもしれません。
一つの行動として扱っているものでも、考えてみると実は無数の行動の連なりであることに気づくからです。 また、同じ「ビニール包装を取る」にしても爪を立てるのかハサミを使うのかなど多くの派生が生まれうるでしょう。

しかし今回はざっくりこれだけの行動の連なりで体験ができているということが分かればいいです。例えばこのようなサンプルを作成しました。

カップ焼きそばを作って食べる工程の図。11工程ある
良い洗い出しの例
カップ焼きそばを作って食べる工程の図。2工程
大雑把すぎるのは×

個人ワーク「歓迎会ランチの予約をするときの行動」

ワークショップでは、このパートは個人ワークとして行いました。時間は10分間。

テーマは「歓迎会ランチの予約をする」に統一しました。テーマを自由設定にせず統一することによって、あとで比較しやすく、人によって行動を分解する粒度の違いも見えやすいからです。
またより具体的にイメージするため、「ランチ参加人数10名」「12:00〜」「オフィス周辺」「予算2,000円以下」といった条件も付け加えています。

ワークはmiroにスペースを作って行いました(FigJamなどの他ツールや、紙の付箋でも実施できます)。
自分の名前のあるエリアに、一つの行動につき一つの付箋を貼っていきます。

ワークの結果

ワークの結果

まず最初の行動を見てみても、「何を食べるか考える」「アレルギーについて聞く」、はたまた「社内のグルメの●●さんにまかせる」のようなユニークなものまで個性が出ます。

終わらせ方についても、「予約」で終わる人もいれば、「予約完了のslackを送る」「カレンダーにお店のURLを貼る」など、どこまでを一連の行動とみなしているかが可視化されて非常に興味深かったです。

②課題を見つけ、ソリューションを考える

さて、行動を書き出していくにつれ、いろいろなことが見えてきます。
「この行動の不便を解消するためにこの機能が生まれたのでは?」という気づきを得たり、「こうすることで解決できるのでは?」と課題の解決法を思いついたり。

たとえばカップ焼きそばの例であれば、「お湯を捨てる」時にうっかり麺も一緒に流してしまう……という不便が思いつきますし、そのためのソリューションとして湯切りの穴が開発されたのだろう、ということも想像できます。

説明用スライドのキャプチャ

別の例として、情報デザインの大切さとして私がよく引き合いに出す事例があります。1996年に発売された日立の冷蔵庫、「野菜中心蔵」です。

それ以前の冷蔵庫は野菜室が一番下なのが普通でした。しかしこの製品は野菜室が真ん中にあります。使用シーンの行動を洗い出していくにつれ、「野菜を取り出すためにしゃがまなければいけない」という課題(ペイン)に行きつき、その解決案として野菜室を中心におくという手段を取ったであろうことが想像できます。

説明用スライドのキャプチャ

こういった気付きは、日常の中にも隠れています。

「帰宅」の行動を洗い出してみる
→「手を洗う」際に毎回「泡立てる」という行動が発生する
→「泡立てる」手間を解消するための、プッシュすると泡で出てくるディスペンサー

「webで新規会員登録」の行動を洗い出してみる
→住所を入力するフォームで、郵便番号にくわえ全ての住所を手入力する行動が発生する
→郵便番号を入力すれば、地名まで入力された状態になる自動入力フォーム

などなど。

ちなみに、ここに行動に付随する感情変化を付け加えていけば、いわゆるカスタマージャーニー、ペイシェントジャーニー等を作成することができます。(が、長くなるのでそれに関してはまた別の機会に...)

グループワーク「歓迎会ランチ予約のソリューションを考える」

ワークショップでは、このパートは40分間のグループワークとして行いました。
ランダムに指定した4~5人ずつのグループに分かれ、「行動に対するソリューションを考えてみる」をテーマにディスカッションしてもらいます。

まずは個人ワークで自分が書き出したものをグループ内で共有しあいます。
その後、みんなに共通していた行動や、書き出してみてストレスがあると感じた行動、この行動に対してこんな機能や仕組みがあったら問題が解決されるのではないか……などを話しあってもらいました。
実現可能かどうかは置いておいて、あくまで自由に話し合ってもらうのがポイントです。

最後にそれぞれのグループの代表に、どんな案がでたのかを軽く紹介してもらいました。
「多数決が決まったら勝手に予約されるシステム」「AIエージェントで条件の店をリスト化」のような開発者らしい意見が出たり、「歓迎会はいつも同じお店にする」といったそっちで解決するんかい!と笑ってしまいたくなるようなアイディアも出てきて、大変興味深かったです。

ワークショップを終えて

今回のワークショップは、情報設計というデザインの基本の部分を身近な行動に落とし込んで体験してもらおうというものでした。

どんな行動でも、洗い出し細分化していくと、無意識に行っていた無数の行動が存在することに気づきます。そしてそこには必ずユーザーの体験を改善するヒントが隠れています。

デザインとは何かを体験する目的のみならず、サービスやプロダクトを新しく考える時、はたまたそれらのUXを改善したい時など、行き詰まったらまずは「行動を洗い出してみる」ワークをぜひやってみてください。


似顔絵
書いた人:庄司

ホルモン治療薬が身体のリズムに与える影響:女性たちの半年間のデータから見えたこと

こんにちは、データサイエンスチームの瀬川です。

テックドクターでは、女性社員のみで構成された「Ladynamic」プロジェクトを通して、女性の視点に立った課題提起とデータ解析を目指しています。同プロジェクトでは女性の健康に関する様々なデータ分析を行っており、これまでのブログ記事でもいくつかの事例をご紹介しました。

techblog.technology-doctor.com

techblog.technology-doctor.com

そんなLadynamicプロジェクトから、今回は基礎体温についてのお話です。

基礎体温は排卵の前には低く、排卵の後には高くなるというように、2つの段階に分かれます(このような性質を「二相性」と呼びます)。また、この変化には月経周期と連動した周期性があります。この連動を利用して月経周期の把握ができるため、基礎体温を毎日記録されている方もいらっしゃるかと思います。

基礎体温のニ相性。引用元:病気が見えるvol.9 婦人科・乳腺外科(第4版)(メディックメディア)

スマートウォッチ等の一般的なウェアラブルデバイスで基礎体温を記録することは、現時点ではできません(※)。代わりにウェアラブルデバイスから得られる別のデータを使用して、この月経周期と連動した周期性と同様の傾向は確認できないでしょうか?

※皮膚温であればFitbit等で記録が可能ですが、その違いについては後ほど触れます。また一般的なスマートウォッチ等ではない、基礎体温測定用の専用のウェアラブル機器は存在します。

本稿では、この疑問を解明するため、月経周期とウェアラブルデバイスデータで得られた心拍数・脈拍数との関連を探っていきます。また、ホルモン製剤を服用している人と服用していない人とのデータから、服薬がその関連にどういった影響を与えるかも調べていきたいと思います。

イメージイラスト

ウェアラブルデータが月経周期と関連するかどうか

まずはウェアラブルデバイスのデータが月経周期と連動した周期性(二相性)を示すかどうかを検証しました。

【対象データ】

女性ホルモン製剤を服用していない女性社員(Aさん)の約半年間のデータを可視化しました。

使用ウェアラブルデバイス:Fitbit
指標:安静時心拍数、起床直前30分(基礎体温の計測タイミングに合わせて)の脈拍数、皮膚温

【結果】

グラフはそれぞれの指標の1日ごとのデータを点で表し、7日移動平均線を表示しています。月経開始日は縦の赤線で示しました。

グラフ

分析の結果、安静時心拍数と起床直前30分の脈拍数については、月経開始日に向けて上昇し、その後低下するという傾向が見られました。この二つの値については、月経周期と連動した周期性があると言えそうです。

一方、皮膚温では心拍数・脈拍数データほどの明確な周期性は確認できませんでした。皮膚温も基礎体温と同様に周期性を示すかと思われましたが、Fitbitを装着した手の位置(布団の中か外か)や室温といった外部環境の影響を受けやすく、月経周期の影響が反映されにくい結果になったと考えられます。

ホルモン剤を服用することによって周期性が変化するのかどうか

次に、ウェアラブルデバイスの心拍数データと月経周期との関係が、女性ホルモン製剤を服用している方々でどのように異なるかを検証しました。

【対象データ】

2名の女性社員から得られた約半年間のデータが対象です。
先ほどの検証で月経周期と特に明確な関連性が見られた、安静時心拍数に注目して分析を進めました。

  • Aさん: 何も服薬していない方
  • Bさん: ジエノゲストを服用している方

【ジエノゲストの作用】

可視化したデータを見る前に、ジエノゲストが体にどのような作用をもたらすのかを簡単に説明します。

  • ジエノゲスト:

子宮内膜症や子宮腺筋症に伴う痛みの治療に使用され、毎日服用します。
女性ホルモンの一種であるプロゲステロン受容体に対して似た働きをし、卵巣機能抑制および子宮内膜細胞の増殖抑制によりプロスタグランジン産生を抑制することから、月経困難症に対する有効性を示すと考えられます。また、LHサージを抑制し、排卵抑制作用を示すと考えられます(1)。排卵が抑えられ月経が来なくなります。

ジエノゲストの服用は、女性の身体の周期性にどのように影響を与えるのでしょうか。

【結果】

2人の安静時心拍数の経過を可視化しました。

グラフ

グラフからは、以下の特徴が見られました。

服用なし(Aさん):

先ほど見たとおり、安静時心拍数が月経開始日に向けて上昇し、その後低下するという周期性が見られます。女性ホルモンの変動が心拍数にも影響を与えている可能性があります。

ジエノゲスト服用者(Bさん):

Aさんよりグラフの変動幅が少なく、明確な周期性は見られませんでした。
ジエノゲストは毎日服用することから服用による女性ホルモン量の変動が少なくなります。これにより、安静時心拍数の周期性が、服用なしのAさんよりもみられないのではないかと考えています。

まとめ

今回の調査から、以下のようなことがわかりました。

  • 安静時心拍数と起床直前30分の脈拍数については、月経開始日に向けて上昇し、その後低下するという傾向が見られ、月経周期と連動した周期性があると言えそうです。
  • ジエノゲストを服用している方の心拍数に周期性が見られなかったことは、薬が女性ホルモンを通じて身体の周期性にも影響を与えているという可能性を示唆しているかもしれません。

ジエノゲストなど女性ホルモン剤の服用者に関する基礎体温や体調のデータはまだ多くありません。そんな中、ウェアラブルデバイスのデータは、毎朝手動で計測しなければいけない基礎体温と違って継続的に女性の体調を記録できるという点から非常に意義があると考えられます。今回の可視化から得られた「女性ホルモンが生体へ影響を与える可能性がある」という結果も、今後の女性の体調管理に役立つかもしれません。

今回の調査は少数のデータに基づいたものであり、今後医学的観点からの考察をより深める必要があります。今後も、より多くのデータを集め、女性の健康に関する課題を解き明かし、よりパーソナライズされた健康管理や治療法の開発に貢献していきたいと考えています。