# Тестуємо в проді, але не на користувачах

У Twitter я побачив ось цей твіт:

%[https://x.com/GithubProjects/status/1948652726297985270] 

Я ось цей “гумор” про пʼятничні деплої взагалі не розумію. Ну бо це не гумор, це якась хвора стигма в професіональному оточенні. Люди, щоб нормалізувати певні явища, інколи застосовують гумор до них. І через це, сприйняття змінюється з “щось, що можна поліпшити, бо це проблема” на “ха-ха, та це ж нормально, посміємось, це не проблема”. Але, хай там як, цей пост не про когнітивні пастки та викривлення. Про них можна написати й окремий пост, де розповісти про те, як наш мозок обдурює сам себе - дуже цікаві штуки насправді.

А в цьому пості я вирішив сфокусуватись на одній ідеї, яка безпосередньо й дозволяє в більшості випадків деплоїти в пʼятницю й не боятись нікого зламати. Але перед тим як я підійду конкретно до цієї теми, я зайду трішки здалеку. Чому? Та банально, тому що в мене зараз на це є настрій. Ну і, тому що вважаю, що донести цю ідею й пояснити чому вона працює - важливо, як мінімум для мене. Тож я почну трішки здалеку і пропоную почати з дуже простого кейсу.

## CLI параметри

Ви всі у своїй роботі використовуєте CLI параметри. Чи то ви працюєте з `git`, чи то з `curl`, чи купа інших програм. Щоб скорегувати програму, аби вона виконувала ті дії, які вам потрібні, ви зазвичай вказуєте підкоманди чи параметри. Ну от візьмімо за простий приклад команду `curl`:

```bash
curl google.com
```

Викликавши цю команду ви очікувано отримаєте якийсь HTML у себе в терміналі:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753865517505/424b4386-87e9-49b2-9c6f-94751c22d451.png align="center")

Але що ви робите, коли ви хочете змінити поведінку програми, не змінюючи саму програму? Ви передаєте параметри *(якщо програма їх підтримує звісно).* Ну от, наприклад, ми хочемо подивитись на детальнішу інформацію стосовно зʼєднання і додаємо параметр:

```bash
curl --verbose google.com
```

Додали прапорець `--verbose` і викликали програму:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753865680291/22905358-6ee1-4a66-b5ec-70e6987bd43a.png align="center")

І ось ми вже бачимо зовсім іншу поведінку програми, яка не просто видає HTML, а ще й додатково виводить в термінал інформацію про зʼєднання.

Так от до чого я це все веду? От подумайте, у вас є одна і та ж сама програма, яку ми викликали вперше і вдруге. Той же самий `curl`, за тим же самим шляхом, той же самий файл. Але при цьому, ми отримали різні поведінки, залежно від вказаних параметрів від користувача.

Наведу ще один приклад.

## Змінні середовища

Я підозрюю, що багато з вас, хто читає цей пост, працює з вебом. Тож ви точно чули про environment variables. Це звичайні пари ключ\\значення, які операційна система прокидає у ваш процес. І вже ваш процес може прочитати ці пари та дістати значення. Зазвичай їх часто використовують для довгоживучих серверів, але й не тільки. Так ось.

Скажімо, у вас є сервіс, який пише деякі логи, щоб краще розуміти що відбувається. Але ви, по класиці, зробили так, що у вас є рівні логів. Ну це зазвичай щось типу `error`, `warn` та `info`. І от, наприклад, `info` логів у вас багато і ви б хотіли їх вимкнути. Багато бібліотек надають цю можливість, вказуючи через `LOG_LEVEL` рівень логів, які ви б хотіли писати.

Тож ви запустили свій сервіс, вказали йому в змінних оточення `LOG_LEVEL=warn` і таким чином вимкнули інформаційні логи.

А тепер питання. Чи змінювали ви щось у своїй програмі для цього? Чи може у бібліотеці, яку ви використовуєте? Не думаю. Ви можете розгорнути такий же самий сервіс, вказати йому інший `LOG_LEVEL` і ви отримаєте два однакових сервіси, але які вже виконують різні шматки коду, скажімо так. І все це без зміни самого коду.

## Що між ними спільного?

Думаю, цих двох прикладів достатньо, щоб передати ось цю концепцію - мати код, хід якого можна контролювати зовнішніми чинниками.

І це неважливо насправді як саме реалізовується ця концепція. Як я і показав на прикладах, в CLI це можуть бути параметри до самої програми. В сервісах це можуть бути змінні середовища.

Всіх їх обʼєднує одна риса - **не змінюючи код програми, ми можемо контролювати її хід**.

## Прості реалізації

Ну от, познайомились з ідеєю. Але ідеї ідеями, але без прикладу буде неповноцінно. Як би ми могли дуже просто реалізувати ось цю концепцію з “зроби мені А, якщо X, а якщо ні, то зроби B”. Я буду частково використовувати для цього псевдокод, але дуже сильно відрізнятись від реальної реалізації воно не буде. Ну і як ви вже могли зрозуміти, звісно тут велику роль відіграє if statement.

Ну от візьмемо той же `curl`, як би це могло виглядати там?

```typescript
// These are some other functions somewhere else in the program
declare class Connection {}
declare function doingAllTheStuffWith(url: string): Connection;
declare function outputVerboseInfoFor(connection: Connection): void;
declare function outputResponseFrom(connection: Connection): void;

import { parseArgs } from "node:util";

const args = parseArgs({
  options: {
    verbose: { type: "boolean", default: false },
    url: { type: "string", default: "" },
  },
});

const connection = doingAllTheStuffWith(args.values.url);

// !!!
// This is the important part for this blog post
if (args.values.verbose === true) {
  outputVerboseInfoFor(connection);
}

outputResponseFrom(connection);
```

Зверніть увагу, що наш код з самого початку може видавати `verbose` логи для нашого псевдо `Connection`. Але! Робитиме він це лише за умови, що йому прокинули параметр `--verbose`. В усіх інших випадках, програма банально не зайде в ту гілку, вона буде пропущена.

Аналогічно можна було б зробити й за допомогою змінних оточення. Додамо альтернативний спосіб ввімкнути наші verbose логи:

```typescript
if (args.values.verbose === true || typeof process.env["CURL_VERBOSE"] !== "undefined") {
  outputVerboseInfoFor(connection);
}
```

І ось в нас вже є програма, хід якої ми можемо змінювати лише вказуючи додаткові параметри чи змінні оточення.

Дочитали ви до цього місця і такі думаєте “Жека, ну це ж смішно, ми й так це розуміємо”. І матимете рацію. Ви ж знаєте, я полюбляю йти від простого до складного, тож зараз я почну ускладнювати вам задачку.

## Прапорець або ввімкнений, або вимкнений

Основний великий мінус цього підходу, про який я так довго пишу, в тому, що у нього бінарна логіка. Ну тобто, ви або можете ввімкнути це у програмі, або вимкнути. І з цим, особливо якщо ми говоримо про прод і про користувачів, є дуже велика проблема.

Така бінарна логіка може працювати в короткострокових програмах по типу CLI утиліт чи щось подібне. Для одного сценарію ви викликали програму з одним набором параметрів, а для іншого сценарію - з іншими.

Якщо ж ми починаємо говорити про довгострокові сервери, які постійно працюють, то ми ж не будемо для кожного запиту підіймати окремий сервіс з іншим набором змінних оточення. Правда ж?

Ось це і є проблема. Маючи описаний вище підхід, ми то, звісно, можемо підняти сервіс з одним набором конфігурацій, але цей набір буде працювати або для всіх, або ні для кого.

Що як, я б хотів підняти сервіс, але щоб той код, який я написав, працював лише для мене. А у мого колеги він би не відпрацьовував? І тим паче не відпрацьовував би для наших користувачів? Хоч він вже і на проді. Іншими словами, наш if має вміти робити щось типу такого:

```diff
- if (args.values.verbose === true || typeof process.env["CURL_VERBOSE"] !== "undefined") {
+ if (thisIsMineRequestAndNoOneElse) {
  outputVerboseInfoFor(connection);
}
```

Ось тут ми й приходимо до того, що я так люблю - ускладнення!

## Прийняття рішення в run-time

Для того, щоб ми могли реалізувати цей умовний `thisIsMineRequestAndNoOneElse`, нам вже недостатньо мати ту статичну інформацію, яку ми маємо, коли передаємо її через прапорці чи через змінні оточення. Бо цієї інформації банально недостатньо, щоб зрозуміти хто є хто у запиті.

Тому наший if має кудись сходити з якимись даними й щоб цей хтось нам сказав чи це має бути `true`, чи `false` *(дуже спрощено)*. І в принципі зі збором даних на нашій стороні все відносно легко. Ми можемо просто підготувати контекст з важливою інформацією та передати її тому, хто буде приймати рішення.

Для простого прикладу, нехай у нас буде якийсь Express сервер, в якому вже є налаштовані middleware, які роблять аутентифікацію й засовує ваші дані в `req.user`. Тоді, ми могли б викликати функцію, яка скаже нам, а чи можна для цього користувача ввімкнути цей шлях, чи ні:

```diff
- if (thisIsMineRequestAndNoOneElse) {
+ if (featureEnabledFor(req.user)) {
  outputVerboseInfoFor(connection);
}
```

Перенесення цих рішень у динамічну площину та додавання контекстів, перед тим як приймати рішення, й робить можливим реалізацію прапорців на більш гранулярному рівні. Тепер ми вже можемо прапорці вмикати не лише для конкретної програми, а і для конкретних користувачів чи інших обʼєктів, які містяться в тій програмі на момент обробки.

Проблема тепер лише в тому, щоб правильно побудувати потрібний контекст для прийняття рішення. А там ви вже що вирішите, так і буде, обмежень майже немає, майже. Захотіли зробити, щоб прапорець був ввімкнений тільки коли `context.date == Feb 30 2222`? Та робіть, запихайте в контекст дату, ну і налаштуйте того, хто прийматиме рішення, що якщо дата `Feb 30 2222` в контексті, то прапорець треба ввімкнути.

Але я оце говорю постійно про оцього “хтось”, та й так не сказав, про кого ж мова. Що ж, тепер поговоримо про тих, хто приймає рішення, й місце, де ви це все налаштовуєте.

## Feature Management Platform

Ці інструменти називають по різному в різних компаніях, бо хтось будує свої власні платформи, як от Wix, й називає по своєму. Хтось бере вже готові COTS *(Component-Of-The-Shelf)*. Тому і терміни вигадують різні. Мені ж ближче більш таке загальне використання терміну - Feature Management Platform.

Що ж це за комбайни? Це платформи, які надають вам, якщо дуже грубо:

* Базу даних для зберігання налаштованих правил. Для кого, коли, в який час, як довго й тому подібне, вся ця інформація зберігається в цих базах даних і потім використовується для прийняття рішення. *(Маю на увазі, що це не їх власні бази даних, а просто що вони використовують БД для зберігання налаштованих прапорців з усіма їх правилами)*.
    
* Web інтерфейс для зручного налаштування правил. Через нього ви, як розробники, й кажете, що має бути ввімкнено, а що вимкнено, для кого, як надовго й т.і. Зазвичай це процес по типу “Створити новий прапорець → Вмикати лише для користувачів з email, який закінчується на company.com → Вмикати прапорець лише один раз із десяти випадків” ну і тому подібне.
    
* Рушій для прийняття рішень, який безпосередньо й бере участь в тому, що потім повертається у ваш код у вигляді `true` чи `false`. Ви даєте йому свій контекст, про який ми поговорили трішки раніше, а він на основі переданих йому даних, порівнює це з налаштованими правилами із бази даних й каже вам - так, прапорець ввімкнено для цього користувача, або ні, не ввімкнено.
    

Ось це все і є тим, що потрібно, щоб ви могли змінювати хід програми залежно від того, хто нею користується - ваш користувач чи ви.

1. Налаштували прапорець через Web-інтерфейс, дали йому назву, вказали правила по типу “для цих email” та “на ці дати” й т.д.
    
2. У своєму коді зібрали контекст для прапорця, в який запхали потрібні дані, як от пошта чи унікальний ідентифактор користувача чи ще щось.
    
3. Відправили контекст на Feature Management Platform й отримали відповідь чи для цього контексту прапорець ввімкнено чи ні.
    

## Про які платформи я чув?

Щоб ви могли детальніше пошукати та почитати про всі ці платформи, накидаю сюди список, який я принаймні чув, бачив та читав щось про них.

### Unleash

Наскільки я зрозумів, то вважається одним із найбільших платформ цього типу з відкритим вихідним кодом. Є і документація й приклади, можна їх і на on-prem розгортати, наскільки памʼятаю. Наявність клієнтів під більшість мов програмування. Ось їх сторінка - [Unleash](https://www.getunleash.io).

Їх інтерфейс, як на мене, доволі гарний, що теж плюс:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753873390509/7271739f-e8d5-4b65-a477-8c62c282440e.png align="center")

Є в них і всілякі залежності, сегменти, стратегії активацій - але то все вже більш для досвідчених і можна про то окремий пост зробити.

### GitLab Feature Toggles

Наш проєкт хоститься на GitLab і ми там використовуємо й реєстри для контейнерів й всіляких пакетів для npm, Helm і так далі. Так ось у них [також є і прапорці](https://docs.gitlab.com/operations/feature_flags/). Єдине що, інтерфейс у них набагато гірший й мені важко сказати, що в тій реалізації вони нам якось корисні. Все доволі просто й, наскільки я зрозумів, під капотом в них там unleash. Тому можна починати з GitLab, а потім переїхати на Unleash, коли масштаби виростуть.

### OpenFeature

Не зовсім платформа, а скоріш спроби стандартизувати все що повʼязане з прапорцями. Можете використати як [джерело додаткової інформації](https://openfeature.dev), щоб детальніше з ними познайомитись.

## Простий прапорець в Unleash

Ну от вирішили ви таки спробувати це все і встановили собі Unleash, наприклад. Як буде виглядати ваш код з усіма цими модними прапорцями?

В першу чергу, вам потрібно буде налаштувати прапорець в інтерфейсі, де вказуєте стратегії активації, сегменти й тому інше, якщо воно вам, звісно, потрібно.

Через те, що я постійно розповідав про активацію прапорця для конкретного користувача в системі, то зробімо прапорець, якому ми скажемо, що ввімкни наш if для мене і мого колеги *(ID випадкові)*:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753875764778/e5870f66-ec7d-4b69-9496-461a42bbef44.png align="center")

Зберігши цю стратегію активації в нашому прапорці з іменем `demoApp.step2`, в нас тепер є місце, куди ми можемо піти спитати, чи можна нам в той закритий для користувачів if чи ні. *Так, я використовую Demo App від Unleash, ну бо а чого ні, в мене зараз немає ніде серверів з ним.*

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753875982817/d5192d1c-50cc-403c-a8ca-6c0e1a639e91.png align="center")

Але як це виглядатиме в коді? Налаштувати прапорець зі своїми власними User ID в системі ми то налаштували, а як передати? Ну, грубо кажучи, от як я розповідав раніше в прикладі з Express, так і передавати. Це просто обʼєкт з контекстом, в якому можуть лежати й інші значення, наприклад:

```diff
+ const context = {
+   userId: req.user.id,
+   sessionId: req.session.id,
+   remoteAddress: req.ip,
+   properties: {
+     region: 'EMEA',
+   },
+ };

- if (featureEnabledFor(req.user)) {
+ if (unleash.isEnabled('demoApp.step2', context)) {
  outputVerboseInfoFor(connection);
}
```

Unleash SDK зробить запит до свого рушія, передасть туди, в тому числі, User ID із вашої бази даних. На сервері Unleash відбудеться порівняння, він побачить User ID в списку тих, кому прапорець можна ввімкнути й поверне вам в if `true`. Якщо ж User ID не співпаде, то буде `false`.

Ось таким чином ми і зробили можливість умовно не робочий код деплоїти в прод і при цьому не ламати наших користувачів. Бо ми буквально цей код викликаємо тільки для конкретних ситуацій, конкретних користувачів, тенантів й тому інше.

За допомогою цих прапорців великі компанії роблять в тому числі й A/B тестування, підписки по типу Premium, Ultra й так далі, з різним набором функціоналу.

## Підсумовуючи

Концепція прапорців вже дуже давно як не нова. Про неї говорили ще чуть не з 2010-их, а то й раніше. Швидкий пошук показав, що і Fowler в 2017 щось там про нього писав, й на DOU вже проскакували статті про них в 2019. Звісно, що до нас воно прийшло здалеку, тому є деяка затримка, але так, якось так - концепція перевірена часом.

Основна ідея в тому, що ви, як розробник, змінюєте хід програми, не змінюючи код програми. Ви деплоїте в пʼятницю ввечері неробочий код в прод, але ховаєте його за прапорцем зі стратегією активації “тільки для мене”. Тому якщо хтось і зламається в пʼятницю ввечері - то це тільки ви.
