Быстрый старт#
Для проведения эксперимента требуется выполнить три шага:
- Подготовить функцию, которая будет запрашивать фичи
- Подготовить функцию, которая будет отправлять экспоужер
- Получить набор фичей, построить на их основе ветвление в коде и при необходимости отправить экспоужер
Интеграция с помощью 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();
Рекомендации#
- В одном сервисе рекомендуется использоваться только один тег, чтобы получать все фичи пользователя одним запросом
- Количество отправленных экспоужеров не влияет на построение отчета - учитывается только один экспожер в сутки. В связи с этим рекомендуется собирать экспоужеры в пачки без повторений
- Отправка пачки экспоужеров должна выполняться асинхронно, чтобы не влиять на время получения ответа пользователем