Перейти к содержанию

Быстрый старт#

Для проведения эксперимента требуется выполнить три шага:

  1. Подготовить функцию, которая будет запрашивать фичи
  2. Подготовить функцию, которая будет отправлять экспоужер
  3. Получить набор фичей, построить на их основе ветвление в коде и при необходимости отправить экспоужер

Интеграция с помощью API#

Далее приведены примеры реализации каждого из шагов, которые можно собрать в одну полную программу. При этом реализация строится на использовании API сплиттера.

Шаг №1. Функция получения фичей#

Пример кода, позволяющего получить фичи:

<?php
define("PLATFORM_DESKTOP", 1);
define("AB_TAG", "ab_go_lib_test");

class Platform
{
  public int $id;
  public ?string $version;
}

class Participant
{
  public ?int $userId;
  public ?string $visitorId;
}

class GetFeaturesReq
{
  public string $tag;
  public Platform $platform;
  public Participant $participant;
}

class Feature
{
  public string $label;
  public string $experimentLabel;
  public string $exposureParams;
}

class Features
{
  public array $features;
}

class GetFeaturesRes
{
  public Features $result;
}

function getFeatures(Platform $platform, Participant $participant)
{
  $req = new GetFeaturesReq();
  $req->participant = $participant;
  $req->platform = $platform;
  $req->tag = AB_TAG;

  $ch = curl_init('https://<host>/getFeaturesByTag/');

  curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type' => 'application/json',
    'X-source' => 'local'
  ));
  curl_setopt($ch, CURLOPT_POST, 1);
  curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($req, JSON_UNESCAPED_UNICODE));
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  curl_setopt($ch, CURLOPT_HEADER, false);
  $curlRes = curl_exec($ch);
  curl_close($ch);

  $res_array = json_decode($curlRes, true);
  $features = $res_array['result']['features'];

  $res = array();
  for ($i = 0; $i < count($features); ++$i) {
    $res[$features[$i]['experimentLabel']] = $features[$i]['label'];
  }

  return $res;
}
>

Пример кода, позволяющего получить фичи:

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

@Serializable
data class Participant(
    val userId: Int? = null,
    val visitorId: String? = null,
)

@Serializable
data class Platform(
    val id: Int,
    val version: String? = null,
)

@Serializable
data class Req(
    val tag: String,
    val platform: Platform,
    val participant: Participant,
)

@Serializable
data class Feature(
    val label: String,
    val experimentLabel: String,
    val exposureParams: String,
)

@Serializable
data class Features(
    val features: List<Feature>,
)

@Serializable
data class Res(
    val result: Features,
)

const val TAG = "ab_go_lib_test" // тег экспериментов, для которых будут выгруженны фичи

fun getFeatures(platform: Platform, participant: Participant): HashMap<String, String> {
    val json = Json.encodeToString(
        Req(
            tag=TAG,
            platform = platform,
            participant = participant,
        )
    )

    val client = HttpClient.newBuilder().build();
    val request = HttpRequest.newBuilder()
        .header("X-source", "local")
        .uri(URI.create("https://<host>/getFeaturesByTag/"))
        .POST(HttpRequest.BodyPublishers.ofString(json))
        .build()
    val response = client.send(request, HttpResponse.BodyHandlers.ofString())
    val result = Json.decodeFromString<Res>(response.body())

    val features = HashMap<String, String> ()
    for (feature in result.result.features){
        features[feature.experimentLabel] = feature.label
    }
    return features
}

Соответствующие зависимости для .gradle.kts:

plugins {
    kotlin("jvm") version "1.9.21"
    kotlin("plugin.serialization") version "1.9.21"
}
dependencies {
    testImplementation(kotlin("test-junit"))
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}

Пример кода, позволяющего получить фичи:

import Foundation

struct Platform: Codable{
  var id: Int
  var version: String?
}

struct Participant: Codable{
  var userId: Int?
  var visitorId: String?
}

struct Req: Codable{
  var tag: String
  var platform: Platform
  var participant: Participant
}

func getFeatures(platform: Platform, participant: Participant) async throws -> [String: String] {
  let url = URL(string: "https://<host>/getFeaturesByTag/")
  let body = Req(
    tag: "ab_go_lib_test", 
    platform: platform, 
    participant: participant
  )
  let jsonData = try? JSONEncoder().encode(body)

  var request = URLRequest(url: url!)
  request.httpBody = jsonData
  request.httpMethod = "POST"
  request.setValue("local", forHTTPHeaderField: "X-source")
  request.setValue("application/json", forHTTPHeaderField: "Accept")
  request.setValue("application/json", forHTTPHeaderField: "Content-Type")

  let (data, _) = try await URLSession.shared.data(for: request)

  let res = try JSONDecoder().decode(Res.self, from: data)
  var features = [String: String]()
  for feature in res.result.features{
    features[feature.experimentLabel] = feature.label
  }
  return features
}

Пример кода, позволяющего получить фичи:

interface Platform {
  id: number;
  version?: string;
}

interface Participant {
  userId?: number;
  visitorId?: string;
}

interface GetFeaturesByTagRequest {
  tag: string;
  platform: Platform;
  participant: Participant;
}

interface Feature {
  experimentLabel: string;
  label: string;
}

interface GetFeaturesByTagResponse {
  result: {
    features: Feature[];
  };
}

interface FeaturesMap {
  [experimentLabel: string]: string;
}


async function getFeatures(platform: Platform, participant: Participant): Promise<FeaturesMap> {
  const url = "https://<host>/getFeaturesByTag/";
  const body: GetFeaturesByTagRequest = {
    tag: "ab_go_lib_test",
    platform,
    participant,
  };
  const jsonData = JSON.stringify(body);

  const response = await fetch(url, {
    method: "POST",
    body: jsonData,
    headers: {
      "X-source": "local",
      "Accept": "application/json",
      "Content-Type": "application/json",
    },
  });

  if (!response.ok) {
    throw new Error(`HTTP error! Status: ${response.status}`);
  }

  const res: GetFeaturesByTagResponse = await response.json();

  const features: FeaturesMap = {};

  for (const feature of res.result.features) {
    features[feature.experimentLabel] = feature.label;
  }
  return features;
}

Шаг №2. Функция отправки экспоужера#

Пример кода, реализующего отправку экспоужера:

<?php
class Exposure
{
  public string $experimentLabel;
  public string $abc;
  public Platform $platform;
  public Participant $participant;
}

class ExposeReq
{
  public array $exposures;
}

function expose(Platform $platform, Participant $participant, string $experimentLabel)
{
  $exposure = new Exposure();
  $exposure->platform = $platform;
  $exposure->participant = $participant;
  $exposure->experimentLabel = $experimentLabel;
  $exposure->abc = "";

  $req = new ExposeReq();
  $req->exposures = array($exposure);

  $ch = curl_init('https://<host>/exposeManyV2/');

  curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type' => 'application/json',
    'X-source' => 'local'
  ));
  curl_setopt($ch, CURLOPT_POST, 1);
  curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($req, JSON_UNESCAPED_UNICODE));
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  curl_setopt($ch, CURLOPT_HEADER, false);
  curl_exec($ch);
  curl_close($ch);
}
>

Пример кода, реализующего отправку экспоужера:

@Serializable
data class Exposures(
    val exposures: List<Exposure>,
)

@Serializable
data class Exposure(
    val experimentLabel: String,
    val abc: String,
    val platform: Platform,
    val participant: Participant,
)

fun expose(platform: Platform, participant: Participant, experimentLabel: String) {
    val json = Json.encodeToString(
        Exposures(
            exposures = listOf(
                Exposure(
                    experimentLabel = experimentLabel,
                    abc = "",
                    participant = participant,
                    platform = platform,
                ),
            ),
        )
    )

    val client = HttpClient.newBuilder().build();
    val request = HttpRequest.newBuilder()
        .header("X-source", "local")
        .uri(URI.create("https://<host>/exposeManyV2/"))
        .POST(HttpRequest.BodyPublishers.ofString(json))
        .build()
    client.send(request, HttpResponse.BodyHandlers.ofString())
}

Пример кода, реализующего отправку экспоужера

struct Exposure: Codable{
  var experimentLabel: String
  var abs: String
  var platform: Platform
  var participant: Participant
}

struct Exposures: Codable{
  var exposures: [Exposure]
}

func expose(platform: Platform, participant: Participant, experimentLabel: String) async throws {
  let url = URL(string: "https://<host>/exposeManyV2/")
  let body = Exposures(
    exposures: [
      Exposure(
        experimentLabel: experimentLabel,
        abs: "", 
        platform: platform, 
        participant: participant
      )
    ]
  )
  let jsonData = try? JSONEncoder().encode(body)

  var request = URLRequest(url: url!)
  request.httpBody = jsonData
  request.httpMethod = "POST"
  request.setValue("local", forHTTPHeaderField: "X-source")
  request.setValue("application/json", forHTTPHeaderField: "Accept")
  request.setValue("application/json", forHTTPHeaderField: "Content-Type")

  _ = try await URLSession.shared.data(for: request)
}

Пример кода, реализующего отправку экспоужера:

interface Platform {
  id: number;
  version?: string;
}

interface Participant {
  userId?: number;
  visitorId?: string;
}

interface Exposure {
  experimentLabel: string;
  abc: string;
  platform: Platform;
  participant: Participant;
}

interface ExposeReq {
  exposures: Exposure[];
}

async function expose(platform: Platform, participant: Participant, experimentLabel: string): Promise<void> {
  const exposure: Exposure = {
    experimentLabel,
    abc: "",
    platform,
    participant,
  };

  const req: ExposeReq = {
    exposures: [exposure],
  };

  const url = "https://<host>/exposeManyV2/";
  const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-source": "local",
    },
    body: JSON.stringify(req),
  });

  if (!response.ok) {
    throw new Error(`HTTP error! Status: ${response.status}`);
  }
}

// Example usage:
const platform: Platform = { id: 1 };
const participant: Participant = { userId: 123 };
const experimentLabel = "example_experiment";

try {
  await expose(platform, participant, experimentLabel);
  console.log("Exposure successful!");
} catch (error) {
  console.error("Error exposing: ", error.message);
}

Шаг №3. Ветвление#

Пример кода, реализующего ветвление:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>

  <style>
    .button {
      background-color: #32de84; /* Green */
    }
    .button2 {
      background-color: #008CBA; /* Blue */
    }
  </style>
</head>

<?php
/* Код из предыдущих шагов */

function chooseClass(array $features)
{
  if ($features['demo_test'] == 'test') {
    expose();
    return "button button2"; // blue
  }
  if ($features['demo_test'] == 'control') {
    expose();
    return "button"; // green
  }
  return "button"; // green
}

$platform = new Platform();
$platform->id = PLATFORM_DESKTOP;

$participant = new Participant();
$participant->userId = 801834992;

$features = getFeatures($platform, $participant);
$buttonClass = chooseClass($features);
?>

<body>
  <button class="<?php echo $buttonClass ?>">ВВОД</button>
</body>

</html>

Пример кода, реализующего ветвление:

const val PLATFORM_ANDROID = 5

fun main() {
    val platform = Platform(
        id=PLATFORM_ANDROID, // платформа, на которой пользователь столкнулся с экспериментом
    )
    val participant = Participant(
        userId = 64584683, // ID пользователя
    )
    val features = getFeatures(platform, participant)
    println(chooseButtonColor(features, platform, participant))
}

const val DEFAULT_BUTTON_COLOR = "GREEN"
const val CONTROL_BUTTON_COLOR = "GREEN"
const val TEST_BUTTON_COLOR = "BLUE"
const val BUTTON_COLOR_CHANGE_EXP = "color_button_change"
fun chooseButtonColor(features: HashMap<String, String>, platform: Platform, participant: Participant): String{
    if (!features.containsKey(BUTTON_COLOR_CHANGE_EXP)){
        return DEFAULT_BUTTON_COLOR
    }

    expose(platform, participant, BUTTON_COLOR_CHANGE_EXP)

    if (features[BUTTON_COLOR_CHANGE_EXP] == "test"){
        return TEST_BUTTON_COLOR
    }
    if (features[BUTTON_COLOR_CHANGE_EXP] == "control"){
        return CONTROL_BUTTON_COLOR
    }
    return DEFAULT_BUTTON_COLOR
}

Пример кода, реализующего ветвление:

let DEFAULT_BUTTON_COLOR = "GREEN"
let CONTROL_BUTTON_COLOR = "GREEN"
let TEST_BUTTON_COLOR = "BLUE"
let BUTTON_COLOR_CHANGE_EXP = "color_button_change"
func chooseButtonColor(features: [String: String], platform: Platform, participant: Participant) async throws -> String{
  if features[BUTTON_COLOR_CHANGE_EXP] == nil {
    return DEFAULT_BUTTON_COLOR
  }

  try await expose(platform: platform, participant: participant, experimentLabel: BUTTON_COLOR_CHANGE_EXP)

  if (features[BUTTON_COLOR_CHANGE_EXP] == "test"){
    return TEST_BUTTON_COLOR
  }
  if (features[BUTTON_COLOR_CHANGE_EXP] == "control"){
    return CONTROL_BUTTON_COLOR
  }

  return DEFAULT_BUTTON_COLOR
}

let PLATFORM_IOS = 4

func main(){
  let platform = Platform(
    id: PLATFORM_IOS
  )
  let participant = Participant(
    userId: 64584683
  )

  Task{
    do{
      let features = try await getFeatures(platform:platform, participant:participant)
      let buttonColor = try await chooseButtonColor(features: features, platform: platform, participant: participant)
      print(buttonColor)
    }catch{
      print("Oops")
    }
  }
  sleep(3)
}

main()

Пример кода, реализующего ветвление:

const DEFAULT_BUTTON_ClS = "btn-green"
const CONTROL_BUTTON_CLS = "btn-blue"
const TEST_BUTTON_CLS = "btn-green"
const BUTTON_COLOR_CHANGE_EXP = "color_button_change"

async function chooseClass(features: FeaturesMap): Promise<string> {
  if (features[BUTTON_COLOR_CHANGE_EXP] === 'test') {
    await expose();
    return TEST_BUTTON_CLS; // blue
  }
  if (features[BUTTON_COLOR_CHANGE_EXP] === 'control') {
    await expose();
    return CONTROL_BUTTON_CLS; // green
  }
  return DEFAULT_BUTTON_ClS; // green
}

// Example usage:
async function main() {
    const platform: Platform = { id: 1 };
    const participant: Participant = { userId: 123 };
    
    try {
      const features = await getFeatures(platform, participant);
      const buttonClass = await chooseClass(features);
    
      const buttonElement = document.createElement("button");
      buttonElement.className = buttonClass;
      buttonElement.innerText = "ВВОД";
    
      document.body.appendChild(buttonElement);
    } catch (error) {
      console.error("Error: ", error.message);
    }
}

main();

Рекомендации#

  1. В одном сервисе рекомендуется использоваться только один тег, чтобы получать все фичи пользователя одним запросом
  2. Количество отправленных экспоужеров не влияет на построение отчета - учитывается только один экспожер в сутки. В связи с этим рекомендуется собирать экспоужеры в пачки без повторений
  3. Отправка пачки экспоужеров должна выполняться асинхронно, чтобы не влиять на время получения ответа пользователем