Перейти к содержимому
К программе курса
k6: нагрузочное тестирование как система
10 / 1759%

Custom метрики и бизнес-KPI

15 минут

Зачем нужны custom метрики

Стандартные метрики k6 (http_req_duration, http_req_failed) показывают техническое состояние, но не отвечают на бизнес-вопросы:

  • Сколько заказов создано за тест?
  • Какой средний чек?
  • Какой процент отказов на этапе оплаты?
  • Сколько пользователей добавили товар в корзину, но не оформили заказ?

Custom метрики позволяют измерять бизнес-KPI прямо в нагрузочных тестах.

Бизнес-метрики ближе к деньгам, чем технические. Stakeholder'ам понятнее «conversion rate упал на 5%», чем «p95 вырос на 200ms».

Типы custom метрик

1. Trend — распределение значений

Используется для: latency, размеров payload, времени операций, стоимости заказов.

import { Trend } from "k6/metrics";
 
const orderValue = new Trend("order_value");
const checkoutTime = new Trend("checkout_time");
 
export const options = {
  thresholds: {
    order_value: ["p(50)>40", "p(95)>100"], // Средний чек > $40, p95 > $100
    checkout_time: ["p(95)<2000"], // Checkout < 2s
  },
};
 
export default function () {
  const startTime = Date.now();
 
  const res = http.post(
    `${BASE_URL}/api/orders`,
    JSON.stringify({
      items: [
        { sku: "PROD-001", price: 99.99, qty: 1 },
        { sku: "PROD-002", price: 29.99, qty: 2 },
      ],
    }),
    { headers: { "Content-Type": "application/json" } }
  );
 
  const duration = Date.now() - startTime;
  checkoutTime.add(duration);
 
  if (res.status === 201) {
    const order = res.json();
    orderValue.add(order.totalAmount); // Записываем сумму заказа
  }
}

Результат в summary:

order_value.............: avg=78.45 min=15.99 med=65.00 max=299.99 p(95)=150.00 p(99)=199.99
checkout_time...........: avg=1245ms min=800ms med=1200ms max=2100ms p(95)=1800ms p(99)=2000ms

2. Counter — счетчик событий

Используется для: количество заказов, количество ошибок, количество успешных операций.

import { Counter } from "k6/metrics";
 
const ordersCreated = new Counter("orders_created");
const ordersRejected = new Counter("orders_rejected");
const itemsAddedToCart = new Counter("items_added_to_cart");
 
export const options = {
  thresholds: {
    orders_created: ["count>100"], // Минимум 100 заказов за тест
    orders_rejected: ["count<10"], // Меньше 10 отказов
  },
};
 
export default function () {
  // Add to cart
  let res = http.post(
    `${BASE_URL}/api/cart`,
    JSON.stringify({ sku: "PROD-001" }),
    {
      headers: { "Content-Type": "application/json" },
    }
  );
 
  if (res.status === 200) {
    itemsAddedToCart.add(1);
  }
 
  // Checkout
  res = http.post(
    `${BASE_URL}/api/orders`,
    JSON.stringify({ cartId: "cart-123" }),
    {
      headers: { "Content-Type": "application/json" },
    }
  );
 
  if (res.status === 201) {
    ordersCreated.add(1);
  } else {
    ordersRejected.add(1);
  }
}

Результат в summary:

orders_created..........: 458
orders_rejected.........: 12
items_added_to_cart.....: 512

3. Rate — доля событий (0..1)

Используется для: error rate, success rate, conversion rate, abandonment rate.

import { Rate } from "k6/metrics";
 
const checkoutSuccessRate = new Rate("checkout_success_rate");
const paymentFailureRate = new Rate("payment_failure_rate");
const cartAbandonmentRate = new Rate("cart_abandonment_rate");
 
export const options = {
  thresholds: {
    checkout_success_rate: ["rate>0.95"], // > 95% успешных checkout
    payment_failure_rate: ["rate<0.02"], // < 2% неудач оплаты
    cart_abandonment_rate: ["rate<0.30"], // < 30% брошенных корзин
  },
};
 
export default function () {
  // Add to cart
  let res = http.post(
    `${BASE_URL}/api/cart`,
    JSON.stringify({ sku: "PROD-001" }),
    {
      headers: { "Content-Type": "application/json" },
    }
  );
 
  const addedToCart = res.status === 200;
 
  // Checkout (не все, кто добавил, оформляют заказ)
  if (Math.random() > 0.25) {
    // 75% оформляют заказ
    res = http.post(
      `${BASE_URL}/api/orders`,
      JSON.stringify({ cartId: "cart-123" }),
      {
        headers: { "Content-Type": "application/json" },
      }
    );
 
    const checkoutSuccess = res.status === 201;
    checkoutSuccessRate.add(checkoutSuccess);
 
    if (!checkoutSuccess) {
      paymentFailureRate.add(1); // Payment failed
    } else {
      paymentFailureRate.add(0); // Payment success
    }
 
    cartAbandonmentRate.add(0); // Completed checkout
  } else {
    cartAbandonmentRate.add(1); // Abandoned cart
  }
}

Результат в summary:

checkout_success_rate...: 97.5% ✓ 438 ✗ 12
payment_failure_rate....: 1.2%  ✓ 5   ✗ 445
cart_abandonment_rate...: 24.8% ✓ 125 ✗ 379

4. Gauge — текущее значение

Используется для: размер очереди, количество активных сессий, current balance.

import { Gauge } from "k6/metrics";
 
const activeCartSize = new Gauge("active_cart_size");
const currentBalance = new Gauge("current_balance");
 
export default function () {
  const res = http.get(`${BASE_URL}/api/cart`, {
    headers: { Authorization: `Bearer ${token}` },
  });
 
  if (res.status === 200) {
    const cart = res.json();
    activeCartSize.add(cart.items.length); // Текущий размер корзины
 
    const balance = res.json("balance");
    currentBalance.add(balance); // Текущий баланс
  }
}

Результат в summary:

active_cart_size........: 3 (последнее значение)
current_balance.........: 150.45 (последнее значение)

Продвинутые примеры

Conversion rate funnel

import { Rate, Counter } from "k6/metrics";
 
const viewedProduct = new Counter("funnel_viewed_product");
const addedToCart = new Counter("funnel_added_to_cart");
const startedCheckout = new Counter("funnel_started_checkout");
const completedOrder = new Counter("funnel_completed_order");
 
const addToCartConversion = new Rate("conversion_add_to_cart");
const checkoutConversion = new Rate("conversion_checkout");
const overallConversion = new Rate("conversion_overall");
 
export default function () {
  // Step 1: View product
  let res = http.get(`${BASE_URL}/products/PROD-001`);
  if (res.status === 200) {
    viewedProduct.add(1);
 
    // Step 2: Add to cart (70% conversion)
    if (Math.random() < 0.7) {
      res = http.post(
        `${BASE_URL}/api/cart`,
        JSON.stringify({ sku: "PROD-001" }),
        {
          headers: { "Content-Type": "application/json" },
        }
      );
 
      if (res.status === 200) {
        addedToCart.add(1);
        addToCartConversion.add(1);
 
        // Step 3: Start checkout (60% из add to cart)
        if (Math.random() < 0.6) {
          res = http.post(
            `${BASE_URL}/api/checkout/start`,
            {},
            {
              headers: { "Content-Type": "application/json" },
            }
          );
 
          if (res.status === 200) {
            startedCheckout.add(1);
 
            // Step 4: Complete order (90% из started checkout)
            if (Math.random() < 0.9) {
              res = http.post(
                `${BASE_URL}/api/orders`,
                {},
                {
                  headers: { "Content-Type": "application/json" },
                }
              );
 
              if (res.status === 201) {
                completedOrder.add(1);
                checkoutConversion.add(1);
                overallConversion.add(1);
                return; // Success
              }
            }
          }
        }
      }
    }
 
    // Failed to convert
    addToCartConversion.add(0);
    checkoutConversion.add(0);
    overallConversion.add(0);
  }
}

Результат:

funnel_viewed_product......: 1000
funnel_added_to_cart.......: 700  (70% от viewed)
funnel_started_checkout....: 420  (60% от added)
funnel_completed_order.....: 378  (90% от started)
 
conversion_add_to_cart.....: 70.0%
conversion_checkout........: 90.0%
conversion_overall.........: 37.8% (378/1000)

Cart abandonment с таймингом

import { Rate, Trend } from "k6/metrics";
 
const cartAbandonmentRate = new Rate("cart_abandonment_rate");
const timeToAbandon = new Trend("time_to_abandon");
const timeToCheckout = new Trend("time_to_checkout");
 
export default function () {
  const cartStartTime = Date.now();
 
  // Add to cart
  let res = http.post(
    `${BASE_URL}/api/cart`,
    JSON.stringify({ sku: "PROD-001" }),
    {
      headers: { "Content-Type": "application/json" },
    }
  );
 
  if (res.status === 200) {
    sleep(2 + Math.random() * 3); // Browse time 2-5s
 
    // Decision: checkout or abandon?
    if (Math.random() < 0.7) {
      // 70% checkout
      res = http.post(
        `${BASE_URL}/api/orders`,
        {},
        {
          headers: { "Content-Type": "application/json" },
        }
      );
 
      const duration = Date.now() - cartStartTime;
      timeToCheckout.add(duration);
      cartAbandonmentRate.add(0); // Completed
    } else {
      // 30% abandon
      const duration = Date.now() - cartStartTime;
      timeToAbandon.add(duration);
      cartAbandonmentRate.add(1); // Abandoned
    }
  }
}

Результат:

cart_abandonment_rate......: 30.5%
time_to_abandon............: avg=3.2s min=2.1s max=5.4s p(95)=4.8s
time_to_checkout...........: avg=4.5s min=2.3s max=7.8s p(95)=6.9s

Интеграция с tags

import { Counter } from "k6/metrics";
 
const ordersCreated = new Counter("orders_created");
 
export default function () {
  const res = http.post(
    `${BASE_URL}/api/orders`,
    JSON.stringify({ payment: "credit_card" }),
    {
      headers: { "Content-Type": "application/json" },
      tags: {
        payment_method: "credit_card",
        region: "us-east",
      },
    }
  );
 
  if (res.status === 201) {
    ordersCreated.add(1, {
      payment_method: "credit_card",
      region: "us-east",
    });
  }
}
 
export const options = {
  thresholds: {
    "orders_created{payment_method:credit_card}": ["count>50"],
    "orders_created{region:us-east}": ["count>80"],
  },
};

✅ Чек-лист завершения урока

После этого урока вы должны уметь:

Типы метрик:

  • Понимать, когда использовать Trend (распределение значений)
  • Понимать, когда использовать Counter (количество событий)
  • Понимать, когда использовать Rate (доля событий 0..1)
  • Понимать, когда использовать Gauge (текущее значение)

Бизнес-метрики:

  • Создавать метрики для order value, средний чек
  • Измерять conversion rate на разных этапах воронки
  • Отслеживать cart abandonment rate
  • Считать количество успешных/неудачных операций

Thresholds для бизнес-метрик:

  • Устанавливать пороги для conversion rate: rate>0.95
  • Устанавливать пороги для order value: p(50)>40
  • Устанавливать пороги для счетчиков: count>100

Интеграция:

  • Добавлять tags к custom метрикам для фильтрации
  • Использовать метрики в thresholds с tags
  • Выводить метрики в Grafana для визуализации

Практическое задание:

  • Добавьте custom метрику для ключевой бизнес-операции
  • Создайте conversion funnel для вашего user journey
  • Настройте thresholds для бизнес-KPI
  • Запустите тест и проанализируйте бизнес-метрики в summary

Если чек-лист пройден — переходите к уроку 11: научимся анализировать результаты и триангулировать проблемы через observability.

Custom метрики и бизнес-KPI — k6: нагрузочное тестирование как система — Potapov.me