A/B testi ve holdout (Faz 12)
Kampanya ve journey gönderimlerinde içerik varyantlarını karşılaştırma, kontrol (holdout) grubu ile artımlı etkiyi ölçme ve kazananı otomatik/elle seçme. Tasarım kararları: ADR-0019. Ortak kod:…
Kampanya ve journey gönderimlerinde içerik varyantlarını karşılaştırma, kontrol (holdout) grubu ile artımlı etkiyi ölçme ve kazananı otomatik/elle seçme. Tasarım kararları: docs/adr/0019-ab-test-ve-tercihler.md. Ortak kod: packages/db/src/ab-testing.ts.
1. Kampanya varyantları
Kampanya oluştururken/güncellerken variants dizisi verilir (push, e-posta, SMS; en fazla 10):
{
"name": "Bahar",
"variants": [
{ "name": "A", "weight_percent": 45, "content": { "contents": { "tr": "Bahar indirimi" } } },
{ "name": "B", "weight_percent": 45, "content": { "contents": { "tr": "%20 indirim" } } },
{ "name": "Kontrol", "weight_percent": 10, "is_control": true }
],
"ab_test": {
"enabled": true,
"test_percentage": 30,
"winner": { "mode": "auto", "metric": "click_rate", "evaluate_after_minutes": 120 }
}
}Kurallar: en az 2 varyant, ağırlıklar toplamı 100, en fazla bir is_control. content: null kampanya içeriğini kullanır (yalnızca konu/başlık farkı için pratik). Varyantlar campaign_variants tablosunda tutulur; her çalıştırmada messages.variants alanına dondurulur (snapshot), böylece sonradan düzenleme geçmiş raporu değiştirmez.
2. Deterministik atama
Kitle çözümlemede (apps/worker/src/delivery/audience.ts) her alıcı için abBucket(subject, campaign_id) → 0–99 kovası hesaplanır (FNV-1a + murmur3 karıştırma). Özne user_id, yoksa subscription_id. Kova, ağırlıklara göre varyanta eşlenir; aynı kullanıcı aynı kampanyada hep aynı varyantı alır (çok cihazlı kullanıcı tüm cihazlarda aynı içerik). deliveries.variant_id atanan varyanttır.
Kabul ölçütü (plan 09): 50/50 iki varyantta dağılım ±%2 — apps/worker/src/__tests__/ab-testing.int.test.ts 1.000 kullanıcıda doğrular.
3. Holdout (kontrol grubu)
is_control varyantına düşen alıcıya mesaj gönderilmez: teslimat status = suppressed, suppression_reason = holdout olarak yazılır. Dönüşüm yine ölçülür: bastırma anından itibaren 24 saat (campaignVariantStats(..., windowHours)) içinde kullanıcının dönüşüm olayı (event_definitions.is_conversion) varsa kontrol grubunun converted sayacına girer. Böylece varyant dönüşüm oranı ile kontrol dönüşüm oranı farkı artımlı etkidir.
4. Test yüzdesi ve kazanan gönderimi
ab_test.test_percentage < 100ise ilk çalıştırma yalnızcaabBucket(subject, "t:"+campaign_id) < test_percentageolan alıcılara gider; kampanyaab_test.phase = "testing"olur verefreshCampaignStatskampanyayı tamamlanmış saymaz.- Elle seçim:
POST /v1/apps/{app_id}/campaigns/{id}/winner { "variant_id": "cvr_…" }(internal: aynı yol).recordWinnerkampanyayawinner_variant_id,decided_at,decided_byve o anki anlamlılığı yazar;createCampaignRun(db, c, now, { winnerVariantId })ile ikinci çalıştırma oluşturulur — ilk çalıştırmanın alıcıları (test grubu + holdout) hariç tutulur, kalan kitleye yalnızca kazanan varyant gider;phase = "winner_sent". - Otomatik seçim: worker
ab-evaluateişi 60 saniyede bir,winner.mode = autoolan velast_run_at + evaluate_after_minutesgeçmiş testleri değerlendirir (evaluateAbTests,apps/worker/src/processors/scheduler.ts).pickWinnermetriğe göre en iyi kontrol dışı varyantı seçer;decided_by = "auto".
5. Anlamlılık
twoProportionZTest(a, b) iki oranlı, iki kuyruklu z-testi; en iyi ile ikinci en iyi varyant arasında hesaplanır, p_value < 0.05 → significant: true. Tek varyant ya da yetersiz örneklem (n = 0, havuz oranı 0/1) → p_value = 1. Metrik paydası varyantlarda sent, kontrol grubunda holdout sayısıdır.
GET /v1/apps/{app_id}/campaigns/{id}/variants/stats (campaigns:read):
{
"ab_test": { "enabled": true, "phase": "testing", "winner": { "mode": "auto", "metric": "click_rate" } },
"variants": [
{ "variant_id": "cvr_…", "name": "A", "weight_percent": 45, "is_control": false, "targeted": 450, "sent": 448, "delivered": 440, "opened": 120, "clicked": 40, "converted": 12, "holdout": 0, "open_rate": 0.268, "click_rate": 0.089, "conversion_rate": 0.027 },
{ "variant_id": "cvr_…", "name": "Kontrol", "is_control": true, "holdout": 100, "converted": 1, "conversion_rate": 0.01, "…": "…" }
],
"leader": "cvr_…",
"significance": { "p_value": 0.03, "significant": true, "metric": "click_rate" }
}Dashboard Kampanyalar → Kampanya → Varyantlar aynı veriyi karşılaştırma tablosu ve "Kazanan seç" düğmesiyle gösterir.
6. Journey mesaj adımında A/B
send_push | send_email | send_sms düğüm verisine ab_test eklenir (packages/shared/src/journeys.ts):
{
"ab_test": {
"variants": [
{ "id": "a", "label": "Kısa", "percent": 50, "template_id": "tpl_1" },
{ "id": "b", "label": "Uzun", "percent": 50, "content": { "contents": { "tr": "…" } } }
],
"winner": { "metric": "click_rate", "min_sends": 200, "confidence": 0.95 }
}
}Motor varyantı run'a göre deterministik seçer (journeySplitBranch tohumu ${node.id}:ab); mesaj journey_node_id + variant_id ile yazılır. evaluateJourneyNodeWinners (segment tarama kilidi içinde, her dakika) düğüm bazında toplam gönderim min_sends'e ulaşınca z-testi uygular; p_value ≤ 1 − confidence ise journeys.node_winners[node_id] = variant_id yazar ve sonraki run'lar yalnızca kazananı alır. GET …/journeys/{id}/stats düğüm başına variants kırılımı döner.
7. In-app varyantları
In-app mesajlarda variants: [{ id, name, weight_percent, display }] tanımlanır; SDK'ya /sdk/v1/apps/{id}/config içinden varyant atanmış mesaj (variant_id) iner ve impression/click/dismiss olayları variant_id ile raporlanır. GET …/in-app-messages/{id} yanıtı variant_stats (impressions, clicks, ctr) içerir.
8. SDK'lar
| SDK | Çağrı |
|---|---|
| Node | hs.campaigns.variantStats(id), hs.campaigns.selectWinner(id, { variant_id }) |
| PHP | $hs->campaigns->variantStats($id), $hs->campaigns->selectWinner($id, $variantId) |
| Python / Go / Java / .NET / Ruby | OpenAPI'den üretilir (campaigns.variant_stats, campaigns.select_winner; adlar sdk/scripts/server-sdks/ir.mjs) |
9. Testler
apps/worker/src/__tests__/ab-testing.int.test.ts: 50/50 dağılım, holdout dönüşümü + kazanan + z-testi, test yüzdesi + kazanan çalıştırması, otomatik değerlendirme.apps/api/src/__tests__/phase12.int.test.ts: varyant CRUD/doğrulama, istatistik ve elle kazanan, journey A/B doğrulaması, kiracı izolasyonu.
Journey'ler (Faz 11)
Journey, kullanıcıyı bir tetikleyiciyle içeri alıp bekleme, koşul, dallanma ve gönderim adımlarından geçiren otomatik akıştır (OneSignal Journeys eşdeğeri). Tasarım kararları: ADR-0018. Şema ve doğrulama:…
Kategoriler, kullanıcı tercihleri ve tercih merkezi (Faz 12)
Kullanıcıların hangi mesaj kategorilerini hangi kanaldan almak istediğini yönetir; kategoriye kapalı kullanıcıya o kategorideki kampanya/mesaj gönderilmez. Tasarım kararları: ADR-0019 §4, §13. Ortak kod:…