> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kameleoon.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 実験結果を Notion にエクスポートする

> Automation API を使用して実験とその結果を取得し、ベイズ成功確率から最も成果の良いバリエーションを判断して、単一の Python スクリプトでレコードを Notion データベースにアップサートします。

Automation API を使用して実験とその結果を取得し、それらを Notion のページプロパティに変換して、単一の Python スクリプトを使用してレコードを Notion データベースにアップサートします。

## ゴール

このチュートリアルでは、[kameleoon\_to\_notion.py](/assets/developer-docs/script/apis/automation-api-rest/tutorials/kameleoon_to_notion.py.zip) スクリプトの動作をステップごとに説明します。Kameleoon の**実験 ID** と Notion の**データベース ID** を指定すると、スクリプトは実験のメタデータと統計結果を取得し、最も成果の良いバリエーションを判断して、データを Notion の *Experiments* データベースにマッピングし、レコードを Notion に書き戻します。同じ実験に対してスクリプトを再実行すると、重複を作成する代わりに既存のページを更新します。

ステップ 1〜5 は、[Airtable エクスポートのチュートリアル](./exporting-experiment-results-to-airtable) と同じリクエストとポーリングのフローを再利用します。変わるのは書き込み先のみです。

<Note>
  Notion の API にはネイティブなアップサート機能がありません。スクリプトは、データベースのデータソースに対して実験名と一致するタイトルを持つページを問い合わせ、見つかった場合はそのページを更新し、見つからない場合は新しいページを作成することで、アップサートをエミュレートします。本チュートリアルは、各データベースを 1 つ以上の**データソース**を中心に整理する Notion API バージョン `2025-09-03` を対象としています。
</Note>

## 要件

* **Kameleoon API の認証情報。** Automation API にはアクセストークンが必要です。スクリプトは `client_credentials` グラントを使用して、`client_id` と `client_secret` からプログラムでアクセストークンを取得します。[アクセストークンを取得する](/ja/developer-docs/apis/automation-api-rest/get-started/get-started#1-アクセストークンを取得する) を参照してください。

* **Notion の内部インテグレーショントークン。** [notion.so/my-integrations](https://www.notion.so/my-integrations) でインテグレーションを作成し、そのトークンをコピーします。

* ***Experiments* スキーマを持つ Notion データベース**。次のプロパティが必要です: `Experiment Name`（title）、`Status`（select）、`Start date`（date）、`End date`（date）、`Notes`（rich text）、`Actual`（number）、`Probability`（select）、`Result`（select）。

* **Notion のデータベース ID。** データベースをフルページとして開きます（ID は URL 内の `?v=` ビューパラメータの前にある 32 文字の文字列です）。

* **Python 3.9 以上**、`requests` ライブラリを含む（`pip install requests`）。

<Warning>
  データベースをインテグレーションと共有してください。共有しないと、すべてのリクエストが `object_not_found` を返します。データベースを開き、**`•••` → Connections → Add connections** に進んで、インテグレーションを選択します。
</Warning>

<Warning>
  すべての認証情報は環境変数に保存してください。スクリプトにシークレットをハードコードしないでください。
</Warning>

```bash theme={null}
export KAMELEOON_CLIENT_ID="..."
export KAMELEOON_CLIENT_SECRET="..."
export NOTION_TOKEN="..."
```

このチュートリアルでは、例として実験 **Product Page Redesign**（ID `188308`）を使用します。オリジナルバリエーションに加えて、*Redesign 1*（ID `828220`）と *Redesign 2*（ID `828221`）の 2 つのバリエーションがあります。

## 1. Automation API で認証する

**エンドポイント:** トークンエンドポイントに POST リクエストを送信して、アクセストークンを取得します。

```
POST https://api.kameleoon.com/oauth/token
```

| フィールド          | 型      | 説明                            |
| -------------- | ------ | ----------------------------- |
| grant\_type    | String | `client_credentials` に設定します。  |
| client\_id     | String | Automation API のクライアント ID。    |
| client\_secret | String | Automation API のクライアントシークレット。 |

**例:**

```python theme={null}
def kameleoon_token(client_id, client_secret):
    resp = requests.post(
        "https://api.kameleoon.com/oauth/token",
        headers={"Content-Type": "application/x-www-form-urlencoded"},
        data={
            "grant_type": "client_credentials",
            "client_id": client_id,
            "client_secret": client_secret,
        },
    )
    resp.raise_for_status()
    return resp.json()["access_token"]
```

**レスポンス:**

```json theme={null}
{ "access_token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9..." }
```

返された `access_token` は、以降のすべての Automation API リクエストで `Bearer` トークンとして送信されます。アクセストークンはデフォルトで 2 時間有効です。

## 2. 実験を取得する

**エンドポイント:** [Get an experiment](/api-reference/experiment/get-an-experiment) エンドポイントに GET リクエストを送信して、実験のメタデータを取得します。

```
GET https://api.kameleoon.com/experiments/{experimentId}
```

| フィールド        | 型       | 説明                     |
| ------------ | ------- | ---------------------- |
| experimentId | Integer | 必須のパスパラメータ。取得する実験の ID。 |

**例:**

```python theme={null}
def get_experiment(token, experiment_id):
    resp = requests.get(
        f"https://api.kameleoon.com/experiments/{experiment_id}",
        headers={"Authorization": f"Bearer {token}"},
    )
    resp.raise_for_status()
    return resp.json()
```

**レスポンス（一部省略）:**

```json theme={null}
{
  "id": 188308,
  "name": "Product Page Redesign",
  "status": "STOPPED",
  "dateStarted": "2025-01-15T09:00:00Z",
  "dateEnded": "2025-02-12T18:00:00Z",
  "description": "Testing two redesigns of the product page.",
  "mainGoalId": 279599,
  "variations": [828220, 828221]
}
```

スクリプトは、Notion のページ用に `name`、`status`、`dateStarted`、`dateEnded`、`description` を読み取り、次のステップで結果リクエストの範囲を絞り込むために `mainGoalId` を読み取ります。

<Note>
  API はデフォルトで `mainGoalId` を返すため、これを読み取るために `optionalFields` パラメータは必要ありません。Automation API は `status` フィールドの固定された列挙値を公開しておらず、トークンは今後変わる可能性があります。[ステップ 7](#7-データを-notion-プロパティにマッピングする) では `status` を大文字・小文字を区別せずにマッチングするため、アカウント間の大文字・小文字の違いによってマッピングが破綻することはありません。
</Note>

## 3. 実験の結果をリクエストする

**エンドポイント:** [Request experiment's results](/api-reference/experiment/request-experiments-results) エンドポイントに POST リクエストを送信して、結果レポートの生成をトリガーします。

```
POST https://api.kameleoon.com/experiments/{experimentId}/results
```

| フィールド                | 型       | 説明                                                                           |
| -------------------- | ------- | ---------------------------------------------------------------------------- |
| experimentId         | Integer | 必須のパスパラメータ。                                                                  |
| goalsIds             | Array   | レポートを指定したゴール ID に限定します。スクリプトは実験の `mainGoalId` を渡します。                         |
| referenceVariationId | String  | 比較の基準として使用するバリエーション。`"0"` はオリジナルページを使用します。                                   |
| visitorData          | Boolean | 訪問ベースのデータには `false`、訪問者ベースのデータには `true` を指定します。                              |
| sequentialTesting    | Boolean | ベイズ成功確率の代わりに、信頼区間に逐次検定を使用する場合は `true` に設定します。いずれか一方の手法のみを有効にしてください。          |
| bayesian             | Boolean | レポートにベイズ成功確率を含める場合は `true` に設定します。スクリプトはこの値を読み取って *Probability* プロパティに設定します。 |
| conversionType       | String  | `ALL_CONVERSION` または `CONVERTED_VISITS`。                                     |

**例:**

```python theme={null}
def request_results(token, experiment_id, goal_id):
    body = {
        "visitorData": False,
        "sequentialTesting": False,
        "bayesian": True,
        "referenceVariationId": "0",
        "conversionType": "ALL_CONVERSION",
        "goalsIds": [goal_id] if goal_id else None,
    }
    resp = requests.post(
        f"https://api.kameleoon.com/experiments/{experiment_id}/results",
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
            "Accept": "*/*",
        },
        json=body,
    )
    resp.raise_for_status()
    return resp.json()["dataCode"]
```

**レスポンス:**

```json theme={null}
{ "dataCode": "14443931880924098266207585267983330260134079899081739889989435434342588016765" }
```

Kameleoon はレポートを非同期で生成します。エンドポイントは `dataCode` を返し、次のステップではこれを使用して結果をポーリングします。

<Note>
  このスクリプトには `bayesian: true` が必要で、`sequentialTesting: false` を設定します。`bayesian` と `sequentialTesting` は有意性を算出するための代替手法であり、このチュートリアルはベイズ成功確率をレポートします。ベイズ推定を有効にすると、レポートの `reliability` の値にベイズ成功確率（あるバリエーションが参照バリエーションに勝る確率）が反映され、スクリプトはこの値を *Probability* プロパティにマッピングします。アカウントが別のデフォルトの統計手法を使用している場合は、Kameleoon アプリ内の同じレポートと照らして値を確認してください。
</Note>

## 4. 結果をポーリングする

**エンドポイント:** [Poll results](/api-reference/data/poll-results) エンドポイントに GET リクエストを送信し、レポートが準備できるまで取得を繰り返します。

```
GET https://api.kameleoon.com/results?dataCode={dataCode}
```

| フィールド    | 型      | 説明                      |
| -------- | ------ | ----------------------- |
| dataCode | String | 前のステップで返された必須のクエリパラメータ。 |

レスポンスの `status` は、レポートが計算されている間は `WAITING`、データが利用可能になると `READY`、失敗時は `ERROR` または `TIMEOUT` になります。ステータスが `ERROR` または `TIMEOUT` の場合、レスポンスにはトップレベルの `errorDescription` が含まれます。スクリプトは、ステータスが `READY` になるまで一定の間隔でポーリングします。

**例:**

```python theme={null}
def poll_results(token, data_code, max_attempts=30, delay=2.0):
    for _ in range(max_attempts):
        resp = requests.get(
            "https://api.kameleoon.com/results",
            headers={"Authorization": f"Bearer {token}"},
            params={"dataCode": data_code},
        )
        resp.raise_for_status()
        payload = resp.json()
        status = payload.get("status")
        if status == "READY":
            return payload["data"]
        if status in ("ERROR", "TIMEOUT"):
            raise RuntimeError(payload.get("errorDescription") or status)
        time.sleep(delay)  # status == "WAITING"
    raise TimeoutError("Timed out waiting for results.")
```

**レスポンス（一部省略）:**

```json theme={null}
{
  "status": "READY",
  "data": {
    "variationData": {
      "_reference": { "breakdownData": { "_reference": { "generalData": {
        "goalsData": { "279599": { "conversionRate": 0.0149 } } } } } },
      "828220": { "breakdownData": { "_reference": { "generalData": {
        "goalsData": { "279599": {
          "reliability": 100.0,
          "improvementRate": 211.48,
          "conversionRate": 0.0466
        } } } } } },
      "828221": { "breakdownData": { "_reference": { "generalData": {
        "goalsData": { "279599": {
          "reliability": 100.0,
          "improvementRate": -43.33,
          "conversionRate": 0.0085
        } } } } } }
    }
  }
}
```

## 5. 最も成果の良いバリエーションを選択する

結果には、`variationData` の下にバリエーションごとに 1 つのエントリと、オリジナルページ用の `_reference` の行が含まれます。各バリエーションについて、リクエストしたゴールの指標は `breakdownData._reference.generalData.goalsData[goalId]` の下にあります。

スクリプトは `_reference` エントリをスキップし、各バリエーションの `improvementRate` と `reliability`（ベイズ成功確率）を読み取り、改善率が最も高いバリエーションを最も成果の良いものとして選択します。あるバリエーションの `goalsData` に要求したゴール ID が含まれていない場合、スクリプトは存在するゴールにフォールバックします。[ステップ 3](#3-実験の結果をリクエストする) で `goalsIds` によりリクエストを単一のゴールに絞り込んでいるため、このフォールバックには通常ほかに選択できるゴールがありません。後でマッピングされる *Result* プロパティには、そのバリエーションが十分に高い成功確率と正の上昇率を達成し、正真正銘の勝利と見なせるかどうかが記録されます。

**例:**

```python theme={null}
def pick_best_variation(result_data, goal_id):
    best, best_improvement = {}, float("-inf")
    for variation_id, vdata in result_data["variationData"].items():
        if variation_id == "_reference":
            continue
        general = vdata["breakdownData"]["_reference"]["generalData"]
        goals_data = general.get("goalsData", {})
        if not goals_data:
            continue
        key = str(goal_id) if str(goal_id) in goals_data else next(iter(goals_data))
        metrics = goals_data[key]
        improvement = metrics.get("improvementRate")
        if improvement is not None and improvement > best_improvement:
            best_improvement = improvement
            best = {
                "variation_id": variation_id,
                "improvement_rate": improvement,
                "bayesian_probability": metrics.get("reliability"),
            }
    return best
```

この例では、両方のバリエーションが 100% のベイズ成功確率に達していますが、*Redesign 1*（`828220`）は +211.48% の改善を示しているのに対し、*Redesign 2* は -43.33% です。したがって、*Redesign 1* が最も成果の良いバリエーションであり、95% を超える確率と正の上昇率を伴う正真正銘の勝者です。

## 6. Notion のデータソースを解決する

バージョン `2025-09-03` 以降、Notion データベースは 1 つ以上の**データソース**を格納するコンテナとなり、ページの書き込みとクエリはデータベース ID ではなくデータソース ID を対象とします。この 2 つの ID は相互に置き換えることができません。

**エンドポイント:** [Retrieve a database](https://developers.notion.com/reference/retrieve-a-database) エンドポイントに GET リクエストを送信して、データベースを取得し、そのデータソースを確認します。

```
GET https://api.notion.com/v1/databases/{databaseId}
```

Notion へのすべてのリクエストは、インテグレーショントークンを `Bearer` トークンとして、また `Notion-Version` ヘッダーとともに送信します。

**例:**

```python theme={null}
NOTION_VERSION = "2025-09-03"


def notion_headers(token):
    return {
        "Authorization": f"Bearer {token}",
        "Notion-Version": NOTION_VERSION,
        "Content-Type": "application/json",
    }


def get_data_source_id(token, database_id):
    resp = requests.get(
        f"https://api.notion.com/v1/databases/{database_id}",
        headers=notion_headers(token),
    )
    resp.raise_for_status()
    data_sources = resp.json().get("data_sources", [])
    if not data_sources:
        raise RuntimeError("The database has no data sources.")
    return data_sources[0]["id"]
```

**レスポンス（一部省略）:**

```json theme={null}
{
  "object": "database",
  "id": "255104cd-477e-808c-b279-d39ab803a7d2",
  "data_sources": [
    { "id": "bc1211ca-e3f1-4939-ae34-5260b16f627c", "name": "Experiments" }
  ]
}
```

スクリプトは最初のデータソースを使用します。データベースに複数のデータソースが存在する場合は、*Experiments* のプロパティと一致するスキーマを持つものを選択してください。

## 7. データを Notion プロパティにマッピングする

スクリプトは、実験のメタデータと最も成果の良いバリエーションの指標を、Notion のプロパティ値に変換します。各プロパティタイプには、それぞれ固有の JSON 形式があります。

| Notion プロパティ    | 型         | ソース                               | 変換                                                                                                                              |
| --------------- | --------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Experiment Name | Title     | `experiment.name`                 | そのまま使用。アップサートのキーとして使用します。                                                                                                       |
| Status          | Select    | `experiment.status`               | 大文字・小文字を区別せずにマッピングします: `active → Running`、`draft`/`planned → Implementing`、`stopped`/`diverted → Completed`、`paused → Defunct`。 |
| Start date      | Date      | `experiment.dateStarted`          | 日時を ISO 形式の日付（`YYYY-MM-DD`）に切り詰めたもの。                                                                                            |
| End date        | Date      | `experiment.dateEnded`            | 日時を ISO 形式の日付に切り詰めたもの。                                                                                                          |
| Notes           | Rich text | `experiment.description`          | そのまま使用。                                                                                                                         |
| Actual          | Number    | 最も成果の良いバリエーションの `improvementRate` | そのまま使用（測定された上昇率、%）。                                                                                                             |
| Probability     | Select    | 最も成果の良いバリエーションのベイズ成功確率            | 区分: `≥95` → `80% - High`、`≥80` → `50% - Medium`、それ以外は `20% - Low`。                                                              |
| Result          | Select    | ベイズ成功確率 + `improvementRate`       | 確率が `95` 以上かつ上昇率が正 → `Success`。`95` 以上かつ上昇率が負 → `Failure`。それ以外は `Inconclusive`。                                                 |

スクリプトは空の値を省略するため、更新時に既存のプロパティ値が空欄で上書きされることはありません。Notion は不足している *select* の選択肢を自動的に作成しますが、プロパティ自体は正しい型でデータソースのスキーマにあらかじめ存在している必要があります。

**例:**

```python theme={null}
STATUS_MAP = {
    "ACTIVE": "Running",
    "DRAFT": "Implementing",
    "PLANNED": "Implementing",
    "PAUSED": "Defunct",
    "STOPPED": "Completed",
    "DIVERTED": "Completed",
}


def map_status(status):
    return STATUS_MAP.get((status or "").upper())


def to_iso_date(value):
    return value[:10] if value else None


def bayesian_to_probability(probability):
    if probability is None:
        return None
    if probability >= 95:
        return "80% - High"
    if probability >= 80:
        return "50% - Medium"
    return "20% - Low"


def derive_result(probability, improvement):
    if probability is None or improvement is None:
        return "Inconclusive"
    if probability >= 95 and improvement > 0:
        return "Success"
    if probability >= 95 and improvement < 0:
        return "Failure"
    return "Inconclusive"


def build_notion_properties(experiment, best):
    probability = best.get("bayesian_probability")
    improvement = best.get("improvement_rate")
    props = {}

    name = experiment.get("name")
    if name:
        props["Experiment Name"] = {"title": [{"text": {"content": name}}]}

    status = map_status(experiment.get("status"))
    if status:
        props["Status"] = {"select": {"name": status}}

    start = to_iso_date(experiment.get("dateStarted"))
    if start:
        props["Start date"] = {"date": {"start": start}}

    end = to_iso_date(experiment.get("dateEnded"))
    if end:
        props["End date"] = {"date": {"start": end}}

    notes = experiment.get("description")
    if notes:
        props["Notes"] = {"rich_text": [{"text": {"content": notes}}]}

    if improvement is not None:
        props["Actual"] = {"number": improvement}

    bucket = bayesian_to_probability(probability)
    if bucket:
        props["Probability"] = {"select": {"name": bucket}}

    result = derive_result(probability, improvement)
    if result:
        props["Result"] = {"select": {"name": result}}

    return props
```

<Note>
  Notion では、データソースごとに 1 つの title プロパティしか許可されません。スクリプトは `Experiment Name` という名前のプロパティをキーにしてアップサートを行います。title プロパティの名前が異なる場合は、ここと [ステップ 8](#8-ページを-notion-にアップサートする) のクエリフィルタの両方でその名前に変更してください。
</Note>

## 8. ページを Notion にアップサートする

Notion にはアップサート用のエンドポイントがないため、スクリプトはデータソースに対して `Experiment Name` が一致するページを問い合わせ、見つかった場合はそのページを更新し、見つからない場合は新しいページを作成します。

**検索:** [Query a data source](https://developers.notion.com/reference/query-a-data-source) エンドポイントを使用して、タイトルフィルタでデータソースに問い合わせます。

```
POST https://api.notion.com/v1/data_sources/{dataSourceId}/query
```

**作成:** [Create a page](https://developers.notion.com/reference/post-page) エンドポイントを使用して、データソースを親とするページを追加します。

```
POST https://api.notion.com/v1/pages
```

**更新:** [Update page properties](https://developers.notion.com/reference/patch-page) エンドポイントを使用して、一致したページのプロパティを上書きします。

```
PATCH https://api.notion.com/v1/pages/{pageId}
```

**例:**

```python theme={null}
def find_page(token, data_source_id, name):
    resp = requests.post(
        f"https://api.notion.com/v1/data_sources/{data_source_id}/query",
        headers=notion_headers(token),
        json={
            "filter": {"property": "Experiment Name", "title": {"equals": name}},
            "page_size": 1,
        },
    )
    resp.raise_for_status()
    results = resp.json().get("results", [])
    return results[0]["id"] if results else None


def upsert_page(token, data_source_id, properties):
    title = properties.get("Experiment Name", {}).get("title")
    name = title[0]["text"]["content"] if title else None

    page_id = find_page(token, data_source_id, name) if name else None
    if page_id:
        resp = requests.patch(
            f"https://api.notion.com/v1/pages/{page_id}",
            headers=notion_headers(token),
            json={"properties": properties},
        )
        action = "Updated"
    else:
        resp = requests.post(
            "https://api.notion.com/v1/pages",
            headers=notion_headers(token),
            json={
                "parent": {"type": "data_source_id", "data_source_id": data_source_id},
                "properties": properties,
            },
        )
        action = "Created"
    resp.raise_for_status()
    return resp.json(), action
```

**レスポンス（一部省略）:**

```json theme={null}
{
  "object": "page",
  "id": "1a2b3c4d-5e6f-7081-9abc-def012345678",
  "properties": {
    "Experiment Name": { "title": [{ "plain_text": "Product Page Redesign" }] },
    "Status": { "select": { "name": "Completed" } },
    "Start date": { "date": { "start": "2025-01-15" } },
    "End date": { "date": { "start": "2025-02-12" } },
    "Actual": { "number": 211.48 },
    "Probability": { "select": { "name": "80% - High" } },
    "Result": { "select": { "name": "Success" } }
  }
}
```

## 9. スクリプトを実行する

実験 ID と Notion のデータベース ID を引数として渡します:

```bash theme={null}
python kameleoon_to_notion.py \
  --experiment-id 188308 \
  --database-id 255104cd477e808cb279d39ab803a7d2
```

スクリプトは、認証、取得した実験、最も成果の良いバリエーション、解決されたデータソース、マッピングされたプロパティ、そして Notion のページを作成したか更新したかという各ステップを出力します。

## カスタマイズに関する注意事項

* **ステータスマッピング** は `STATUS_MAP` 定数にあり、API が返すステータストークンをキーとし、大文字・小文字を区別せずにマッチングします。*Status* の選択肢が `Running` / `Implementing` / `Completed` / `Defunct` と異なる場合はターゲットの値を調整し、単一の `GET /experiments/{experimentId}` でアカウントが返すトークンを確認してください。
* **Probability** は測定されたベイズ成功確率からマッピングされ、結果リクエストで `bayesian: true` が必要です。*Probability* プロパティが実験前の見積もりを手動で入力するものである場合は、`build_notion_properties` から `Probability` のブロックを削除してください。
* **ゴールの選択** には実験の `mainGoalId` を使用します。別のゴールについてレポートするには、そのゴール ID を `request_results` と `pick_best_variation` に渡してください。
* **アップサートキー。** タイトルの照合は厳密に一致させるため、`Experiment Name` の大文字・小文字や空白の違いは、既存のページを更新する代わりに新しいページを作成してしまいます。検索してから書き込むフローはアトミックではないため、同じ実験に対して 2 つのエクスポートを同時に実行しないでください。
* **API バージョン。** スクリプトは `Notion-Version: 2025-09-03` に固定されています。後でデータベースに 2 つ目のデータソースを追加した場合は、`get_data_source_id` を更新して名前で正しいものを選択するようにしてください。
* **レート制限。** Automation API は 10 秒あたり最大 50 リクエスト、1 時間あたり最大 1,000 リクエストまで許可しており、Notion API の平均は 1 秒あたり約 3 リクエストです。多数の実験をまとめて処理する場合は、トークンをキャッシュし、スロットリングを追加してください。
