> ## 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.

# Exportar los resultados de experimentos a Airtable

> Recupere un experimento y sus resultados con la Automation API, determine la variación con mejor rendimiento a partir de la probabilidad de éxito bayesiana, e inserte o actualice el registro en una tabla de Airtable con un único script de Python.

## Objetivo

Este tutorial describe cómo funciona el script [kameleoon\_to\_airtable.py](https://storage.googleapis.com/kameleoon-storage-documentation/developers/scripts/kameleoon_to_airtable.py), paso a paso. Dado un **ID de experimento** de Kameleoon, un **ID de base** de Airtable y un **ID de tabla** de Airtable, el script recupera los metadatos y los resultados estadísticos del experimento, determina la variación con mejor rendimiento, asigna los datos al esquema *Experiments* de Airtable y escribe el registro en Airtable. Volver a ejecutar el script para el mismo experimento actualiza la fila existente en lugar de crear un duplicado.

Este tutorial continúa el tutorial anterior sobre [cómo recuperar los resultados de experimentos usando la Automation API](./retrieving-experiment-results-using-the-automation-api) y reutiliza el mismo flujo de solicitud y consulta.

## Requisitos

* **Credenciales de la API de Kameleoon.** La Automation API requiere un token de acceso. El script obtiene uno de forma programática a partir de un `client_id` y un `client_secret` mediante el grant `client_credentials`. Consulte [Obtener un token de acceso](/es/developer-docs/apis/automation-api-rest/get-started/get-started#1-obtain-an-access-token).

* **Un token de acceso personal de Airtable** con el alcance `data.records:write` en la base de destino.

* **El ID de base y el ID de tabla de Airtable.** Ambos aparecen en la URL de la tabla o en la documentación de la API de la base. El ID de base comienza con `app`, y el ID de tabla, con `tbl`.

* **Una tabla de Airtable con el esquema *Experiments* ya creado.** El script escribe en los siguientes campos: `Experiment Name`, `Status`, `Start date`, `End date`, `Notes`, `Actual`, `Probability` y `Result`. También espera que existan los campos de entrada manual `Assignee`, `Category`, `Prediction`, `Mkt Est`, `Eng Est` y `Attachments`, aunque nunca los establece. Airtable rechaza la escritura en un nombre de campo que no exista ya en la tabla, y `typecast` solo convierte los tipos de valor de los campos existentes (no crea campos ni opciones de selección faltantes). Cree la tabla con estos campos, y con las opciones de selección de `Status` correspondientes, antes de ejecutar el script. Consulte el [paso 6](#6-asignar-los-datos-a-los-campos-de-airtable) para conocer el valor que recibe cada campo.

* **Python 3.9 o superior** con la biblioteca `requests` (`pip install requests`).

<Warning>
  Almacene todas las credenciales en variables de entorno. Nunca codifique secretos directamente en el script.
</Warning>

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

El tutorial utiliza el experimento de ejemplo **Product Page Redesign** (ID `188308`), con dos variaciones además de la original: *Redesign 1* (ID `828220`) y *Redesign 2* (ID `828221`).

## 1. Autenticarse con la Automation API

**Endpoint:** Obtenga un token de acceso enviando una solicitud POST al endpoint de token.

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

| Campo          | Tipo   | Descripción                                 |
| -------------- | ------ | ------------------------------------------- |
| grant\_type    | String | Establézcalo en `client_credentials`.       |
| client\_id     | String | Su ID de cliente de la Automation API.      |
| client\_secret | String | Su secreto de cliente de la Automation API. |

**Ejemplo:**

```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"]
```

**Respuesta:**

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

Envíe el `access_token` devuelto como token `Bearer` en cada solicitud posterior a la Automation API. Los tokens de acceso son válidos durante 2 horas de forma predeterminada.

## 2. Recuperar el experimento

**Endpoint:** Obtenga los metadatos del experimento enviando una solicitud GET al endpoint [Get an experiment](/api-reference/experiment/get-an-experiment).

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

| Campo        | Tipo    | Descripción                                                                 |
| ------------ | ------- | --------------------------------------------------------------------------- |
| experimentId | Integer | Parámetro de ruta obligatorio. El ID del experimento que se va a recuperar. |

**Ejemplo:**

```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()
```

**Respuesta (truncada):**

```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]
}
```

El script lee `name`, `status`, `dateStarted`, `dateEnded` y `description` para el registro de Airtable, y `mainGoalId` para acotar la solicitud de resultados en el siguiente paso.

<Note>
  La API devuelve `mainGoalId` de forma predeterminada, por lo que no necesita un parámetro `optionalFields` para leerlo. La Automation API no publica una enumeración fija para el campo `status`, pero otros campos de tipo estado en toda la API usan sistemáticamente tokens en mayúsculas (por ejemplo, `STOPPED`, `ACTIVE`, `DRAFT`). El [paso 6](#6-asignar-los-datos-a-los-campos-de-airtable) asigna el campo `status` partiendo de ese supuesto. Confirme los tokens exactos que devuelve su cuenta con una solicitud real antes de confiar en la asignación.
</Note>

## 3. Solicitar los resultados del experimento

**Endpoint:** Active la generación del informe de resultados enviando una solicitud POST al endpoint [Request experiment's results](/api-reference/experiment/request-experiments-results).

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

| Campo                | Tipo    | Descripción                                                                                                                                           |
| -------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| experimentId         | Integer | Parámetro de ruta obligatorio.                                                                                                                        |
| goalsIds             | Array   | Restringe el informe a los IDs de objetivo indicados. El script pasa el `mainGoalId` del experimento.                                                 |
| referenceVariationId | String  | La variación utilizada como referencia para la comparación. `"0"` utiliza la página original.                                                         |
| visitorData          | Boolean | `false` para datos basados en visitas, `true` para datos basados en visitantes.                                                                       |
| sequentialTesting    | Boolean | Establézcalo en `true` para usar pruebas secuenciales en los intervalos de confianza.                                                                 |
| bayesian             | Boolean | Establézcalo en `true` para incluir la probabilidad de éxito bayesiana en el informe. El script lee este valor para completar el campo *Probability*. |
| conversionType       | String  | `ALL_CONVERSION` o `CONVERTED_VISITS`.                                                                                                                |

**Ejemplo:**

```python theme={null}
def request_results(token, experiment_id, goal_id):
    body = {
        "visitorData": False,
        "sequentialTesting": True,
        "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"]
```

**Respuesta:**

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

Kameleoon genera el informe de forma asíncrona. El endpoint devuelve un `dataCode`, que el [paso 4](#4-consultar-los-resultados) utiliza para consultar el resultado.

<Note>
  Este script requiere `bayesian: true`. Con Bayesian habilitado, el valor `reliability` del informe representa la **probabilidad de éxito bayesiana** (la probabilidad de que una variación supere a la referencia), que el script asigna al campo *Probability*. Confirme el valor con el mismo informe en la aplicación Kameleoon si su cuenta utiliza un método estadístico predeterminado diferente. La especificación de la Automation API marca `dateIntervals` como obligatorio, pero el ejemplo anterior lo omite y aun así devuelve un informe válido. La especificación no documenta qué valor predeterminado toma un `dateIntervals` omitido; este tutorial asume que cubre la ejecución completa del experimento, así que confirme ese comportamiento con su propia cuenta antes de confiar en él. Pase un array `dateIntervals` para acotar el informe a un período específico.
</Note>

## 4. Consultar los resultados

**Endpoint:** Recupere el informe enviando solicitudes GET al endpoint [Poll results](/api-reference/data/poll-results) hasta que esté listo.

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

| Campo    | Tipo   | Descripción                                                                                                          |
| -------- | ------ | -------------------------------------------------------------------------------------------------------------------- |
| dataCode | String | Parámetro de consulta obligatorio. Use el hash que devolvió `POST /experiments/{experimentId}/results` en el paso 3. |

El `status` de la respuesta es `WAITING` mientras Kameleoon calcula el informe, `READY` cuando los datos están disponibles, o `ERROR` / `TIMEOUT` en caso de fallo. Cuando el estado es `ERROR` o `TIMEOUT`, la respuesta incluye un `errorDescription` de nivel superior. El script consulta a intervalos fijos hasta que el estado es `READY`.

**Ejemplo:**

```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.")
```

**Respuesta (truncada):**

```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. Seleccionar la variación con mejor rendimiento

Los resultados contienen una entrada por variación bajo `variationData`, además de la línea `_reference` para la página original. Para cada variación, las métricas del objetivo solicitado se encuentran bajo `breakdownData._reference.generalData.goalsData[goalId]`.

El script omite la entrada `_reference`, lee el `improvementRate` y el `reliability` (la probabilidad de éxito bayesiana) de cada variación, y selecciona como mejor variación la que tiene la tasa de mejora más alta. El campo *Result*, asignado en el siguiente paso, registra si esa variación realmente ganó: si alcanzó una probabilidad de éxito suficientemente alta con una mejora positiva.

**Ejemplo:**

```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
```

En el ejemplo, ambas variaciones alcanzan una probabilidad de éxito bayesiana del 100%, pero *Redesign 1* (`828220`) muestra una mejora del +211,48% frente al -43,33% de *Redesign 2*. Por lo tanto, *Redesign 1* es la variación con mejor rendimiento y, con una probabilidad superior al 95% y una mejora positiva, una ganadora genuina.

## 6. Asignar los datos a los campos de Airtable

El script transforma los metadatos del experimento y las métricas de la variación con mejor rendimiento en el esquema *Experiments* de Airtable.

| Campo de Airtable | Origen                                                | Transformación                                                                                                                                               |
| ----------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Experiment Name   | `experiment.name`                                     | Directo. Se utiliza como clave de upsert.                                                                                                                    |
| Status            | `experiment.status`                                   | Asignado sin distinguir mayúsculas y minúsculas: `ACTIVE → Running`, `DRAFT`/`PLANNED → Implementing`, `STOPPED`/`DIVERTED → Completed`, `PAUSED → Defunct`. |
| Start date        | `experiment.dateStarted`                              | Fecha y hora truncadas a una fecha ISO (`YYYY-MM-DD`).                                                                                                       |
| End date          | `experiment.dateEnded`                                | Fecha y hora truncadas a una fecha ISO.                                                                                                                      |
| Notes             | `experiment.description`                              | Directo.                                                                                                                                                     |
| Actual            | `improvementRate` de la mejor variación               | Directo (mejora medida, %).                                                                                                                                  |
| Probability       | probabilidad de éxito bayesiana de la mejor variación | Agrupado por rangos: `≥95 → 80% - High`, `≥80 → 50% - Medium`, en otro caso `20% - Low`.                                                                     |
| Result            | probabilidad de éxito bayesiana + `improvementRate`   | probabilidad `≥ 95` y mejora > 0 → `Success`; `≥ 95` y mejora \< 0 → `Failure`; en cualquier otro caso → `Inconclusive`.                                     |

El script no establece los campos que no tienen origen en Kameleoon: **Assignee**, **Category**, **Prediction**, **Mkt Est**, **Eng Est** y **Attachments**. Estos campos permanecen disponibles para introducirlos manualmente en Airtable. El script también omite los valores vacíos, por lo que nunca sobrescribe una celda existente con un valor en blanco.

**Ejemplo:**

```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_airtable_fields(experiment, best):
    probability = best.get("bayesian_probability")
    improvement = best.get("improvement_rate")
    fields = {
        "Experiment Name": experiment.get("name"),
        "Status": map_status(experiment.get("status")),
        "Start date": to_iso_date(experiment.get("dateStarted")),
        "End date": to_iso_date(experiment.get("dateEnded")),
        "Notes": experiment.get("description"),
        "Actual": improvement,
        "Probability": bayesian_to_probability(probability),
        "Result": derive_result(probability, improvement),
    }
    return {k: v for k, v in fields.items() if v is not None}
```

<Note>
  La Automation API no publica una enumeración fija para el campo `status` del experimento, y los tokens pueden evolucionar. `map_status` compara sin distinguir mayúsculas y minúsculas, y devuelve `None` para un estado no reconocido, lo que omite la celda *Status* en lugar de escribir un valor incorrecto. Confirme los tokens que devuelve su cuenta con una única solicitud `GET /experiments/{experimentId}` y amplíe `STATUS_MAP` si es necesario.
</Note>

## 7. Insertar o actualizar el registro en Airtable

<Note>
  El endpoint *Update table* de Airtable (`PATCH /v0/meta/bases/{baseId}/tables/{tableId}`) solo cambia el nombre y la descripción de una tabla; no puede escribir datos en las filas. Para completar un registro, use el endpoint **records** con la opción `performUpsert`.
</Note>

**Endpoint:** Cree o actualice el registro enviando una solicitud PATCH al endpoint de registros.

```
PATCH https://api.airtable.com/v0/{baseId}/{tableId}
```

| Campo                         | Tipo    | Descripción                                                                                                |
| ----------------------------- | ------- | ---------------------------------------------------------------------------------------------------------- |
| performUpsert.fieldsToMergeOn | Array   | Campo(s) utilizado(s) para hacer coincidir un registro existente. El script combina por `Experiment Name`. |
| records                       | Array   | Una lista de registros (máximo 10 por solicitud), cada uno con un objeto `fields`.                         |
| typecast                      | Boolean | `true` permite que Airtable convierta cadenas en opciones de selección y analice fechas.                   |

**Ejemplo:**

```python theme={null}
def upsert_record(airtable_token, base_id, table_id, fields):
    resp = requests.patch(
        f"https://api.airtable.com/v0/{base_id}/{table_id}",
        headers={
            "Authorization": f"Bearer {airtable_token}",
            "Content-Type": "application/json",
        },
        json={
            "performUpsert": {"fieldsToMergeOn": ["Experiment Name"]},
            "typecast": True,
            "records": [{"fields": fields}],
        },
    )
    resp.raise_for_status()
    return resp.json()
```

**Respuesta (truncada):**

```json theme={null}
{
  "records": [
    {
      "id": "rec0Vh8KLmNoPqRsT",
      "fields": {
        "Experiment Name": "Product Page Redesign",
        "Status": "Completed",
        "Start date": "2025-01-15",
        "End date": "2025-02-12",
        "Actual": 211.48,
        "Probability": "80% - High",
        "Result": "Success"
      }
    }
  ],
  "createdRecords": [],
  "updatedRecords": ["rec0Vh8KLmNoPqRsT"]
}
```

La respuesta informa el resultado por registro: un ID devuelto bajo `createdRecords` significa que Airtable creó una fila nueva, mientras que un ID bajo `updatedRecords` significa que Airtable actualizó una fila existente.

## 8. Ejecutar el script

Pase el ID del experimento y los ID de base y tabla de Airtable como argumentos:

```bash theme={null}
python kameleoon_to_airtable.py \
  --experiment-id 188308 \
  --base-id appXXXXXXXXXXXXXX \
  --table-id tbll9adSjdedH5t3f
```

El script imprime cada paso: la autenticación, el experimento obtenido, la variación con mejor rendimiento, los campos asignados y si creó o actualizó el registro de Airtable.

## Script completo

El script completo a continuación coincide función por función con [kameleoon\_to\_airtable.py](https://storage.googleapis.com/kameleoon-storage-documentation/developers/scripts/kameleoon_to_airtable.py). Cópielo directamente o descargue el archivo desde ese enlace.

```python theme={null}
#!/usr/bin/env python3
"""Export a Kameleoon experiment's results to an Airtable *Experiments* table.

Given a Kameleoon experiment ID, an Airtable base ID, and an Airtable table ID,
this script:

  1. Authenticates with the Automation API (client_credentials grant).
  2. Retrieves the experiment metadata.
  3. Requests the experiment's results (with the Bayesian success probability).
  4. Polls until the report is ready.
  5. Selects the best-performing variation.
  6. Maps the data onto the Airtable *Experiments* schema.
  7. Upserts the record into Airtable, keyed on "Experiment Name".

Credentials are read from environment variables (never hard-code secrets):

    export KAMELEOON_CLIENT_ID="..."
    export KAMELEOON_CLIENT_SECRET="..."
    export AIRTABLE_TOKEN="..."

Usage:

    python kameleoon_to_airtable.py \
        --experiment-id 188308 \
        --base-id appXXXXXXXXXXXXXX \
        --table-id tbll9adSjdedH5t3f
"""

import argparse
import os
import sys
import time

import requests

# Maps the uppercase status tokens returned by the Automation API to the
# options of the Airtable *Status* field. Adjust the target values if your
# *Status* options differ, and confirm the tokens your account returns with a
# single GET /experiments/{experimentId}.
STATUS_MAP = {
    "ACTIVE": "Running",
    "DRAFT": "Implementing",
    "PLANNED": "Implementing",
    "PAUSED": "Defunct",
    "STOPPED": "Completed",
    "DIVERTED": "Completed",
}


# --------------------------------------------------------------------------- #
# 1. Authenticate with the Automation API
# --------------------------------------------------------------------------- #
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"]


# --------------------------------------------------------------------------- #
# 2. Retrieve the experiment
# --------------------------------------------------------------------------- #
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()


# --------------------------------------------------------------------------- #
# 3. Request the experiment's results
# --------------------------------------------------------------------------- #
def request_results(token, experiment_id, goal_id):
    body = {
        "visitorData": False,
        "sequentialTesting": True,
        "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"]


# --------------------------------------------------------------------------- #
# 4. Poll for the results
# --------------------------------------------------------------------------- #
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.")


# --------------------------------------------------------------------------- #
# 5. Select the best-performing variation
# --------------------------------------------------------------------------- #
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


# --------------------------------------------------------------------------- #
# 6. Map the data to Airtable fields
# --------------------------------------------------------------------------- #
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_airtable_fields(experiment, best):
    probability = best.get("bayesian_probability")
    improvement = best.get("improvement_rate")
    fields = {
        "Experiment Name": experiment.get("name"),
        "Status": map_status(experiment.get("status")),
        "Start date": to_iso_date(experiment.get("dateStarted")),
        "End date": to_iso_date(experiment.get("dateEnded")),
        "Notes": experiment.get("description"),
        "Actual": improvement,
        "Probability": bayesian_to_probability(probability),
        "Result": derive_result(probability, improvement),
    }
    return {k: v for k, v in fields.items() if v is not None}


# --------------------------------------------------------------------------- #
# 7. Upsert the record into Airtable
# --------------------------------------------------------------------------- #
def upsert_record(airtable_token, base_id, table_id, fields):
    resp = requests.patch(
        f"https://api.airtable.com/v0/{base_id}/{table_id}",
        headers={
            "Authorization": f"Bearer {airtable_token}",
            "Content-Type": "application/json",
        },
        json={
            "performUpsert": {"fieldsToMergeOn": ["Experiment Name"]},
            "typecast": True,
            "records": [{"fields": fields}],
        },
    )
    resp.raise_for_status()
    return resp.json()


# --------------------------------------------------------------------------- #
# 8. Orchestration
# --------------------------------------------------------------------------- #
def run(experiment_id, base_id, table_id):
    client_id = os.environ.get("KAMELEOON_CLIENT_ID")
    client_secret = os.environ.get("KAMELEOON_CLIENT_SECRET")
    airtable_token = os.environ.get("AIRTABLE_TOKEN")

    missing = [
        name
        for name, value in (
            ("KAMELEOON_CLIENT_ID", client_id),
            ("KAMELEOON_CLIENT_SECRET", client_secret),
            ("AIRTABLE_TOKEN", airtable_token),
        )
        if not value
    ]
    if missing:
        raise SystemExit(
            "Missing required environment variable(s): " + ", ".join(missing)
        )

    # 1. Authenticate.
    token = kameleoon_token(client_id, client_secret)
    print("Authenticated with the Automation API.")

    # 2. Retrieve the experiment.
    experiment = get_experiment(token, experiment_id)
    goal_id = experiment.get("mainGoalId")
    print(
        f"Fetched experiment {experiment.get('id')}: "
        f"{experiment.get('name')!r} (status: {experiment.get('status')})."
    )

    # 3-4. Request and poll for the results.
    data_code = request_results(token, experiment_id, goal_id)
    result_data = poll_results(token, data_code)

    # 5. Select the best-performing variation.
    best = pick_best_variation(result_data, goal_id)
    if best:
        print(
            f"Best-performing variation: {best['variation_id']} "
            f"(uplift {best['improvement_rate']}%, "
            f"Bayesian probability {best.get('bayesian_probability')}%)."
        )
    else:
        print("No variation data available for the requested goal.")

    # 6. Map the data to Airtable fields.
    fields = build_airtable_fields(experiment, best)
    print("Mapped Airtable fields:")
    for key, value in fields.items():
        print(f"  {key}: {value}")

    # 7. Upsert the record.
    response = upsert_record(airtable_token, base_id, table_id, fields)
    created = response.get("createdRecords") or []
    record_id = response["records"][0]["id"] if response.get("records") else None
    action = "Created" if record_id in created else "Updated"
    print(f"{action} Airtable record {record_id}.")

    return response


def main():
    parser = argparse.ArgumentParser(
        description="Export a Kameleoon experiment's results to Airtable."
    )
    parser.add_argument(
        "--experiment-id",
        required=True,
        help="ID of the Kameleoon experiment to export.",
    )
    parser.add_argument(
        "--base-id",
        required=True,
        help="Airtable base ID (starts with 'app').",
    )
    parser.add_argument(
        "--table-id",
        required=True,
        help="Airtable table ID (starts with 'tbl') or table name.",
    )
    args = parser.parse_args()

    try:
        run(args.experiment_id, args.base_id, args.table_id)
    except requests.HTTPError as exc:
        detail = ""
        if exc.response is not None:
            detail = f" — {exc.response.status_code}: {exc.response.text}"
        print(f"HTTP error: {exc}{detail}", file=sys.stderr)
        sys.exit(1)
    except (RuntimeError, TimeoutError) as exc:
        print(f"Error: {exc}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()
```

## Notas de personalización

* **La asignación de estado** vive en la constante `STATUS_MAP`, indexada por los tokens de estado en mayúsculas que devuelve la API. Ajuste los valores de destino si sus opciones de *Status* difieren de `Running` / `Implementing` / `Completed` / `Defunct`, y confirme los tokens que devuelve su cuenta con una solicitud real `GET /experiments/{experimentId}` antes de confiar en la asignación.
* **Probability** procede de la probabilidad de éxito bayesiana medida, que requiere `bayesian: true` en la solicitud de resultados. Si su campo *Probability* es en cambio una estimación previa al experimento que introduce manualmente, elimine la línea `Probability` de `build_airtable_fields`.
* **La selección de objetivo** usa el `mainGoalId` del experimento. Para generar el informe sobre un objetivo diferente, pase su ID a `request_results` y `pick_best_variation`.
* **Clave de upsert.** Airtable hace coincidir el campo de combinación de forma exacta, por lo que las diferencias de mayúsculas/minúsculas o de espacios en `Experiment Name` crean una fila nueva en lugar de actualizar la existente. Mantenga estables los nombres de los experimentos, o combine por un campo identificador estable dedicado.
* **Formato del campo Actual.** El script escribe el valor bruto de `improvementRate` (por ejemplo, `211.48`) en *Actual*. Si *Actual* es un campo Percent de Airtable, configúrelo para que espere un número simple en lugar de una fracción, o divida el valor entre 100 en `build_airtable_fields` para que coincida con un campo Percent basado en fracciones.
* **Límites de frecuencia.** La Automation API permite hasta 50 solicitudes cada 10 segundos y 1000 por hora, pero Kameleoon recomienda mantenerse por debajo de 12 llamadas por minuto y cuenta, y desaconseja usar la Automation API para el seguimiento de alta frecuencia. Si procesa muchos experimentos por lotes, almacene en caché los tokens, limite la frecuencia de las solicitudes y considere la [Data API](/es/developer-docs/apis/data-api-rest/overview) para necesidades de gran volumen.
