Thiết kế backend A/B Testing cho Campaign

1. Mục tiêu

Tài liệu này mô tả phần backend cần có để màn tạo Campaign hỗ trợ chia khách hàng vào nhiều nhóm hành động theo tỷ lệ phần trăm.

Ví dụ FE cấu hình:

CONTROL là nhóm khách hàng vẫn thuộc tệp mục tiêu và vẫn được tính trong báo cáo, nhưng hệ thống không thực hiện hành động nào với họ. Nhóm này giúp so sánh kết quả của việc chạy quảng cáo với trường hợp không chạy quảng cáo.

Luồng trong phạm vi 5 Campaign được chốt như sau:

Chế độ Cách xử lý một khách hàng
abTestingEnabled = false Campaign có một nhóm gửi thông thường. Mỗi lần dispatch gửi đúng nội dung/kênh đã cấu hình.
abTestingEnabled = true Mỗi khách hàng chỉ thuộc đúng một actionChain hoặc CONTROL. Không chạy các chain còn lại.

2. Quy tắc nghiệp vụ đã chốt

2.1. FE gửi gì cho BE

FE chỉ gửi tỷ lệ của các nhóm hành động thực sự. FE không tạo một actionChain giả cho CONTROL và không cần gửi controlPercent.

{
  "abTestingEnabled": true,
  "actionChains": [
    {
      "chainCode": "VARIANT_A",
      "allocationPercent": 10.00,
      "actions": []
    },
    {
      "chainCode": "VARIANT_B",
      "allocationPercent": 20.00,
      "actions": []
    }
  ]
}

BE tính tỷ lệ nhóm đối chứng bằng công thức:

controlPercent = 100 - tổng allocationPercent của các actionChain

Trong ví dụ trên, controlPercent = 70%.

2.2. Vì sao BE vẫn phải tự kiểm tra tỷ lệ

FE có thể tính và hiển thị phần còn lại, nhưng BE vẫn phải kiểm tra vì request có thể được gửi từ Swagger, Postman, script hoặc một phiên bản FE cũ.

BE phải bảo đảm:

2.3. CONTROL không được lưu trong cấu hình Campaign

Không bổ sung các trường sau:

controlPercent
chainType
controlActionChain

CONTROL chỉ xuất hiện khi BE chốt danh sách người nhận vào campaign_target_member. Nhờ vậy:

2.4. Tỷ lệ là tỷ lệ thống kê, không phải số lượng tuyệt đối

Với tệp 1.000.000 người, tỷ lệ thực tế thường rất gần cấu hình. Với tệp chỉ có 10 người, kết quả có thể không đúng chính xác 1 người cho 10% và 2 người cho 20%.

BE sử dụng phép chia ổn định theo assignmentSeedrecipientKey, không lấy ngẫu nhiên lại mỗi lần. Cùng một Campaign, cùng một khách hàng và cùng một seed luôn cho cùng một nhóm.

2.5. Phạm vi đúng của 5 Campaign

Campaign action trong phạm vi hiện tại chỉ có nhiệm vụ gửi thông điệp. Nội dung “tặng data”, “ưu đãi” trong tài liệu khách hàng là nội dung truyền thông; việc cấp ưu đãi không phải action của Campaign.

Các action gửi được sử dụng:

NOTIFICATION_SMS
WHATSAPP_SEND_MESSAGE
NOTIFICATION_PUSH
Campaign Cách chọn đối tượng Kênh gửi Sự kiện/trạng thái cần theo dõi
Camp 1 - Chuyển đổi chéo BU OCS: thuê bao active, có giao dịch BU và chưa kích hoạt Super App SMS, WhatsApp APP_INSTALLED, APP_ACTIVATED, click theo kênh
Camp 2 - Khách mua online mới E-shop/CRM: đơn hàng vừa hoàn tất SMS hoặc WhatsApp theo cấu hình từng bước ONLINE_ORDER_COMPLETED, APP_INSTALLED, APP_ACTIVATED
Camp 3 - Tải app chưa kích hoạt SDK/PostHog: đã cài app quá 24 giờ nhưng chưa kích hoạt Push APP_INSTALLED, APP_REGISTERED, APP_ACTIVATED, push open/click
Camp 4 - Nâng cấp gói data tháng OCS: có dùng data, chưa có gói tháng SMS, WhatsApp MONTHLY_DATA_PACKAGE_REGISTERED để dừng gửi
Camp 5 - Phát triển thuê bao data OCS: máy 4G nhưng chưa phát sinh data SMS, WhatsApp DATA_USAGE_STARTED để dừng gửi

Camp 1, 4 và 5 có yêu cầu A/B rõ ràng giữa SMS và WhatsApp. Camp 2 là Journey ở các mốc ngày 0, 3 và 5; mỗi mốc tạo một lần dispatch riêng. Camp 3 là Push theo hành vi ứng dụng. Cơ chế chia variant có thể tái sử dụng cho Camp 2/3 khi BA bật A/B, nhưng không hard-code logic riêng theo số thứ tự Campaign.

2.6. Phạm vi lưu trữ

dispatchCursor, dispatchedCount và lease trong campaign_run chỉ là một bản ghi tiến độ cho cả lần chạy. Dù run có hàng triệu người nhận, MongoDB không phát sinh hàng triệu worker log.

3. Backend hiện tại đáp ứng đến đâu

Hạng mục Hiện trạng Kết luận
Lưu nhiều actionChains CampaignModel và request đã có danh sách chain. Đã có nền tảng.
Gửi SMS/WhatsApp Đã có channel worker và Kafka command tương ứng. Có nền tảng, cần chuẩn hóa contract theo run/variant.
Gửi Push Đã có hướng tách push-worker, nhưng contract Campaign và routing chưa hoàn chỉnh. Chưa đáp ứng đầy đủ Camp 3.
CampaignRun Đã có collection campaign_run và snapshot cấu hình theo từng lần chạy. Đã có nền tảng.
SegmentVersionTargetSnapshot Đã có metadata MongoDB và member ở ClickHouse. Đã có nền tảng.
Cột nhận diện biến thể campaign_target_member, delivery, click, open và bảng metric đã có variant_code. Có schema nhưng chưa nối đủ luồng.
Tracking link Model, cache payload và event click/open đã có variantCode. Có trường nhưng worker chưa truyền đầy đủ.
Báo cáo A/B theo run Đã có endpoint POST /campaigns/{campaignId}/runs/{runId}/reports/ab-comparison. Có API nền nhưng cách lấy dữ liệu còn sai với control và channel.
Sự kiện hành vi của 5 Campaign Camp 3 đã có thiết kế PostHog; OCS/đơn hàng và conversion event chưa được nối thống nhất vào báo cáo Campaign. Chưa đáp ứng đầy đủ.
Cấu hình bật A/B Chưa có abTestingEnabled. Chưa đáp ứng.
Mã ổn định của chain Chain chỉ có danh sách actions, chưa có chainCode. Chưa đáp ứng.
Tỷ lệ của chain Chưa có allocationPercent. Chưa đáp ứng.
Seed chia nhóm Chưa có assignmentSeed. Chưa đáp ứng.
Validate tổng tỷ lệ Chưa có. Chưa đáp ứng.
Chia người nhận theo tỷ lệ Cronjob đang truyền variantCode = "" cho toàn bộ target. Chưa đáp ứng.
Dispatcher chọn đúng một chain Dispatcher đang chạy tất cả chain cho mọi người. Chưa đáp ứng A/B.
Bỏ gửi cho CONTROL Chưa có khái niệm control trong target. Chưa đáp ứng.
Truyền variantCode đến worker Command và UnifiedEventMessage chưa có trường này. Chưa đáp ứng.
Resume khi dispatcher chết giữa chừng Consumer đọc toàn bộ target trong một lần, chưa lưu cursor bền vững. Chưa bảo đảm ở production.
Chống gửi trùng khi Kafka phát lại idempotencyKey trong message nhưng key chưa ổn định theo target và worker chưa thực thi chống trùng đầy đủ. Chưa hoàn chỉnh.

Kết luận: BE đã có khoảng khung dữ liệu và tracking cần thiết, nhưng chưa có luồng A/B hoạt động từ đầu đến cuối. Chỉ thêm hai trường phần trăm vào request sẽ không đủ.

4. Thiết kế dữ liệu

4.1. MongoDB campaign

Bổ sung:

abTestingEnabled: Boolean
assignmentSeed: String

Ý nghĩa:

Trường Ý nghĩa
abTestingEnabled Quyết định Campaign chạy theo luồng cũ hay chia mỗi người vào một nhóm. Mặc định false.
assignmentSeed Chuỗi bí mật do BE sinh để chia nhóm ổn định. FE chỉ đọc, không được tự sửa.

Quy tắc assignmentSeed:

4.2. CampaignActionChains

Bổ sung:

chainCode: String
allocationPercent: BigDecimal
actions: List<CampaignAction>
Trường Ý nghĩa
chainCode Mã ổn định của nhóm, ví dụ VARIANT_A. Mã này được lưu xuyên suốt vào target, event và báo cáo.
allocationPercent Tỷ lệ người nhận thuộc nhóm khi bật A/B.
actions Trong phạm vi 5 Campaign, mỗi variant chứa một action gửi SMS, WhatsApp hoặc Push.

Không sử dụng vị trí mảng groupIndex làm mã biến thể vì khi FE thêm, xóa hoặc đổi thứ tự chain, vị trí có thể thay đổi. groupIndex vẫn có thể giữ để hiển thị vị trí action, còn chainCode là khóa nghiệp vụ.

Quy tắc tạo chainCode:

4.3. MongoDB campaign_run.campaignDefinitionSnapshot

Snapshot của mỗi lần chạy phải lưu đầy đủ:

abTestingEnabled
assignmentSeed
actionChains[].chainCode
actionChains[].allocationPercent
actionChains[].actions

Dispatcher chỉ đọc cấu hình đã chốt trong campaign_run, không đọc cấu hình Campaign đang có thể bị thay đổi. Nhờ đó báo cáo của một run luôn khớp với cấu hình thực tế lúc run được tạo.

4.4. MongoDB campaign_target_snapshot

Giữ vai trò metadata của lần chốt tệp và bổ sung:

attemptNo: Long
assignmentSummary: Map<String, Long>

Ví dụ:

{
  "status": "FROZEN",
  "recipientCount": 100000,
  "assignmentSummary": {
    "VARIANT_A": 10024,
    "VARIANT_B": 19931,
    "CONTROL": 70045
  }
}

Không chuyển snapshot sang FROZEN trước khi ClickHouse insert và kiểm tra count thành công.

Để retry an toàn:

4.5. ClickHouse campaign_target_member

Các trường quan trọng:

company_id
campaign_id
campaign_run_id
target_snapshot_id
segment_version_id
recipient_key
phone
personalization_attributes
variant_code
selection_reason
created_at

Quy ước variant_code:

Giá trị Ý nghĩa
DEFAULT Campaign không bật A/B; dispatcher chạy nhóm gửi thông thường duy nhất.
VARIANT_A, VARIANT_B, ... Khách hàng chỉ chạy chain có chainCode tương ứng.
CONTROL Khách hàng thuộc tệp nhưng không chạy action.
FALLBACK Chỉ dùng nếu sau này có chính sách fallback được phê duyệt; không dùng trong phạm vi hiện tại.

channel trong bảng target không được dùng làm nguồn xác định biến thể. chainCode mới là nhóm A/B; channel là SMS, WhatsApp hoặc Push của variant và được lưu trong delivery/engagement event. Query target không được group theo channel vì target được chốt theo variant trước khi gửi.

4.6. ClickHouse event

Không tạo worker_log hoặc bảng thực thi action theo từng người trong MongoDB. Dữ liệu cần cho 5 Campaign được chia thành hai nhóm.

Nhóm 1 - Kết quả gửi và tương tác với thông điệp

Tái sử dụng các bảng hiện có và truyền đúng variant_code:

campaign_delivery_event
campaign_click_event
campaign_open_event

Quy ước theo kênh:

Kênh Event cần ghi
SMS SENT, FAILED, DELIVERED nếu provider hỗ trợ, CLICKED qua tracking link
WhatsApp SENT, FAILED, DELIVERED, READ, CLICKED
Push SENT, FAILED, OPENED, CLICKED

campaign_open_event có thể lưu READ của WhatsApp và OPENED của Push khi bổ sung engagement_type, hoặc đổi thành bảng chung campaign_engagement_event nếu team quyết định làm schema mới. Không tạo bảng riêng cho từng Campaign.

Nhóm 2 - Sự kiện hành vi dùng để chọn target, dừng gửi và tính conversion

Sử dụng một event contract chung; dữ liệu App từ SDK/PostHog tiếp tục nằm trong analytics.posthog_events, còn event từ E-shop/CRM/OCS được chuẩn hóa về cùng các trường logic sau:

event_date
event_time
event_id
company_id
customer_id Nullable(String)
recipient_key Nullable(String)
installation_id Nullable(String)
event_code
source
source_event_id
properties
received_at

Danh mục event tối thiểu:

ONLINE_ORDER_COMPLETED
APP_INSTALLED
APP_REGISTERED
APP_ACTIVATED
MONTHLY_DATA_PACKAGE_REGISTERED
DATA_USAGE_STARTED

Không bắt buộc mọi nguồn phải ghi vào đúng một bảng vật lý. posthog_events vẫn là bảng raw cho App; OCS có thể cung cấp snapshot/trạng thái theo ngày. Lớp report/audience phải chuẩn hóa chúng thành cùng event_coderecipient_key/customer_id, không đọc từ Mongo worker log.

Khi tính conversion theo A/B, Report Service nối event hành vi với campaign_target_member bằng company_id + recipient_key/customer_id và cửa sổ thời gian của CampaignRun. variant_code lấy từ target đã chốt, không yêu cầu nguồn OCS/PostHog biết Campaign nào đã gửi.

4.7. ClickHouse campaign_metric_1m

Schema hiện có trường target_snapshot_idvariant_code, nhưng hai trường này chưa nằm trong ORDER BY của SummingMergeTree. Khi ClickHouse merge dữ liệu, các dòng khác biến thể có nguy cơ bị cộng chung.

Khóa mới phải chứa tối thiểu:

ORDER BY (
  company_id,
  campaign_id,
  campaign_run_id,
  target_snapshot_id,
  variant_code,
  window_start,
  channel
)

Vì ClickHouse không thay đổi sorting key đơn giản như database giao dịch, môi trường chưa có dữ liệu nên nên tạo lại bảng hoặc tạo bảng campaign_metric_1m_v2, backfill nếu cần, rồi đổi view/query.

5. Thuật toán chia nhóm

5.1. Chia theo 10.000 bucket

Sử dụng 10.000 bucket để hỗ trợ độ chính xác 0.01%:

bucket = sipHash64(assignmentSeed + ":" + recipientKey) % 10000

Với cấu hình A 10%, B 20%:

Khoảng bucket Kết quả
0 - 999 VARIANT_A
1000 - 2999 VARIANT_B
3000 - 9999 CONTROL

Phép chia được thực hiện ngay trong câu INSERT SELECT từ segment_version_member sang campaign_target_member. Không tải toàn bộ người nhận lên RAM của Java.

5.2. Vì sao chia lúc tạo TargetSnapshot

5.3. Người nhận thiếu phone hoặc FCM token

Không được tự chuyển người đó sang variant khác vì việc chuyển nhóm sẽ làm sai tỷ lệ và sai kết quả thử nghiệm.

Luồng xử lý:

  1. Người nhận vẫn giữ variant_code đã được chia.
  2. Trước khi chạy action, worker hoặc dispatcher kiểm tra dữ liệu bắt buộc của action.
  3. Nếu thiếu, ghi event SKIPPED hoặc FAILED với mã lý do rõ ràng.
  4. Báo cáo tách số eligible, skippedfailed; không tính họ thành gửi thành công.

FE/BA nên tạo Segment có dữ liệu phù hợp với các kênh được chọn. Khi so sánh SMS với WhatsApp hoặc Push, đây là điều kiện quan trọng để kết quả có ý nghĩa.

6. Hợp đồng API

6.1. Tạo và cập nhật Campaign

Các request tạo/cập nhật bổ sung:

{
  "abTestingEnabled": true,
  "actionChains": [
    {
      "chainCode": "VARIANT_A",
      "allocationPercent": 10.00,
      "actions": [
        {
          "type": "NOTIFICATION_SMS"
        }
      ]
    },
    {
      "chainCode": "VARIANT_B",
      "allocationPercent": 20.00,
      "actions": [
        {
          "type": "WHATSAPP_SEND_MESSAGE"
        }
      ]
    }
  ]
}

Response trả lại cấu hình các chain đã được BE chuẩn hóa, đặc biệt là chainCode do BE sinh:

{
  "abTestingEnabled": true,
  "actionChains": [
    {
      "chainCode": "VARIANT_A",
      "allocationPercent": 10.00,
      "actions": []
    },
    {
      "chainCode": "VARIANT_B",
      "allocationPercent": 20.00,
      "actions": []
    }
  ]
}

FE tự tính tỷ lệ hiển thị của nhóm đối chứng bằng 100 - tổng allocationPercent. BE cũng tính công thức này độc lập để validate và chia TargetSnapshot, nhưng không nhận, không lưu và không cần trả trường controlPercent.

6.2. Quyền sửa cấu hình

Trạng thái Quy tắc
DRAFT, REJECTED Cho sửa A/B config.
SUBMITTED Nếu sửa nội dung/tỷ lệ thì đưa Campaign về DRAFT và yêu cầu submit/approve lại; không sửa âm thầm.
IN_PROGRESS, FINISHED, STOPPED Không cho sửa cấu hình A/B. Muốn thay đổi phải nhân bản Campaign.

Không cho sửa tỷ lệ của một run đã tạo. Mỗi CampaignRun dùng snapshot cấu hình riêng.

6.3. Mã lỗi đề xuất

Tạo CampaignAbErrorCode implements ErrorCode. Khi throw chỉ truyền error code; message được lấy từ file đa ngôn ngữ, không hard-code message tiếng Anh trong service.

Error code HTTP Ý nghĩa
CAMPAIGN_AB0001 400 A/B đã bật nhưng không có nhóm hành động hợp lệ.
CAMPAIGN_AB0002 400 Tỷ lệ của một nhóm phải lớn hơn 0 và không vượt quá 100.
CAMPAIGN_AB0003 400 Tổng tỷ lệ các nhóm vượt quá 100%.
CAMPAIGN_AB0004 400 Thử nghiệm phải có ít nhất hai nhóm có ý nghĩa, tính cả CONTROL.
CAMPAIGN_AB0005 400 chainCode trùng hoặc sử dụng mã dành riêng.
CAMPAIGN_AB0006 409 Cấu hình A/B không thể thay đổi sau khi Campaign đã chạy.
CAMPAIGN_AB0007 400 Loại Campaign/action hiện chưa hỗ trợ A/B.
CAMPAIGN_AB0008 409 Không thể chốt nhóm A/B do TargetSnapshot không hợp lệ.
CAMPAIGN_AB0009 500 Tổng số target sau khi chia nhóm không khớp tệp nguồn.

Thêm message cho tối thiểu các file:

messages_vi.properties
messages_en.properties
messages_lo.properties
messages_zh.properties

7. Luồng tổng thể

[Diagram]

8. Luồng tạo hoặc sửa Campaign

[Diagram]

9. Luồng tạo Run và chia TargetSnapshot

[Diagram]

10. Luồng dispatch theo variant

[Diagram]

Lưu ý: DISPATCHED có nghĩa đã phát hết command, không đồng nghĩa tất cả worker đã xử lý thành công. Chỉ chuyển COMPLETED sau khi kết quả action cuối cùng đã được đối soát hoặc sau bước reconciliation.

11. Luồng gửi của một variant

[Diagram]

Idempotency key đề xuất:

campaignRunId:targetSnapshotId:recipientKey:chainCode

Key này không thay đổi khi Kafka phát lại message hoặc dispatcher restart. Lease PROCESSING phải ngắn và có thể được node khác claim lại sau khi hết hạn; trạng thái COMPLETED giữ theo thời gian campaign còn có khả năng retry. MongoDB không lưu worker log theo từng người nhận. ClickHouse giữ event để audit và báo cáo.

12. Luồng tracking và báo cáo

[Diagram]

13. Thiết kế báo cáo A/B

13.1. Không biến dữ liệu cũ thành CONTROL

Query hiện tại đang coi variant_code rỗng là CONTROL. Cách này sai vì dữ liệu rỗng là Campaign cũ hoặc dữ liệu chưa gắn variant, không phải người được chủ động giữ lại làm nhóm đối chứng.

Quy ước mới:

variant_code rỗng/null -> DEFAULT hoặc LEGACY
variant_code = CONTROL -> đúng nhóm đối chứng

13.2. Target không group theo channel

Target được chia theo chain, còn channel thuộc từng action. Vì vậy:

13.3. Response đề xuất

{
  "campaignRunId": "runId",
  "totalTargetCount": 100000,
  "variants": [
    {
      "variantCode": "VARIANT_A",
      "configuredPercent": 10.00,
      "actualTargetCount": 10024,
      "actualPercent": 10.024,
      "sentCount": 9980,
      "failedCount": 44,
      "deliveredCount": null,
      "deliveryMetricAvailable": false,
      "readCount": 0,
      "uniqueOpenCount": 4300,
      "uniqueClickCount": 900,
      "conversionEventCode": "APP_ACTIVATED",
      "conversionCount": 620
    },
    {
      "variantCode": "CONTROL",
      "configuredPercent": 70.00,
      "actualTargetCount": 70045,
      "actualPercent": 70.045,
      "sentCount": 0,
      "failedCount": 0,
      "readCount": 0,
      "uniqueOpenCount": 0,
      "uniqueClickCount": 0,
      "conversionEventCode": "APP_ACTIVATED",
      "conversionCount": 90
    }
  ]
}

Không đổi tên SENT thành DELIVERED. deliveredCount chỉ có giá trị khi nhà cung cấp có callback xác nhận giao thành công.

14. Phân chia task

Quan hệ phụ thuộc

[Diagram]

Bảng giao việc

Task Kết quả bàn giao chính Module chính Phụ thuộc
T01 API Campaign lưu và validate được cấu hình A/B common, laoads Không
T02 TargetSnapshot có variant_code ổn định và có CONTROL common, cronjob T01
T03 Dispatcher chạy đúng chain, có checkpoint và chống phát lại common, campaign-dispatcher T02
T04 Channel worker/tracking giữ đúng variant và chuẩn hóa event của 5 Campaign SMS, WhatsApp, Push, common T03; T06 đối với Push
T05 Flink và Report API trả số liệu đúng từng variant ads-analytics-flink-job, laoads, ClickHouse T02, T04
T06 Camp 3 gửi và theo dõi Push được push-worker, dispatcher, API T01
T07 Script, feature flag và thứ tự deploy an toàn Tất cả module liên quan T01-T05

T01 - Campaign contract, validation và MapStruct

Mục tiêu

Cho phép API tạo, sửa, xem và nhân bản Campaign lưu đúng cấu hình A/B mà không làm thay đổi Campaign cũ.

Phạm vi thay đổi

Quy tắc cost

expectedCost = audienceSize * tổng(chainCost * allocationPercent / 100)

Tiêu chí hoàn thành

T02 - Run snapshot và chia TargetSnapshot

Mục tiêu

Chia từng target vào đúng một variant ngay khi tạo snapshot, thực hiện hoàn toàn theo batch trong ClickHouse.

Phạm vi thay đổi

Log bắt buộc

companyId, campaignId, campaignRunId, targetSnapshotId
segmentVersionId, attemptNo, assignmentSeedHashPrefix
sourceCount, insertedCount, count theo variant, durationMs
failureCode

Không log toàn bộ assignmentSeed, phone, FCM token hoặc dữ liệu cá nhân.

Tiêu chí hoàn thành

T03 - Dispatcher routing, checkpoint và chống gửi trùng

Mục tiêu

Dispatcher đọc variant_code, chỉ chạy chain tương ứng và có thể tiếp tục khi một node chết giữa chừng.

Phạm vi thay đổi

Log bắt buộc

campaignRunId, targetSnapshotId, batchNo, cursor
batchSize, treatmentCount, controlCount, skippedCount
leaseOwner, durationMs, publishFailureCount

Tiêu chí hoàn thành

T04 - Channel Worker, tracking và event của 5 Campaign

Mục tiêu

Giữ variantCode từ command đến kết quả gửi, tương tác và conversion để báo cáo không bị mất dấu nhóm.

Phạm vi thay đổi

Tiêu chí hoàn thành

T05 - Flink metric và Report API

Mục tiêu

Trả báo cáo đúng theo từng variant, không trộn dữ liệu giữa run/snapshot và không coi dữ liệu legacy là control.

Phạm vi thay đổi

Tiêu chí hoàn thành

T06 - Hoàn thiện Push Notification cho Camp 3

Mục tiêu

Gửi Push cho Camp 3 và cho phép cơ chế variant tái sử dụng Push khi được cấu hình.

Phạm vi thay đổi

Task này có thể triển khai song song với T02, nhưng phải hoàn thành trước khi nghiệm thu A/B có Push.

T07 - Migration và triển khai an toàn

Mục tiêu

Đưa các trường và message mới lên nhiều service mà không làm Campaign cũ dừng chạy hoặc làm Kafka consumer cũ không đọc được message.

Phạm vi thay đổi

Thứ tự deploy

ClickHouse/Mongo scripts
-> common contract
-> worker consumers
-> campaign-dispatcher
-> cronjob/target builder
-> laoads API/report
-> Flink job
-> bật feature flag cho company thử nghiệm

Tiêu chí hoàn thành

15. Ma trận kiểm tra nghiệp vụ

Case Kết quả mong đợi
A/B tắt, request có nhiều hơn 1 chain Từ chối; Campaign thường chỉ có một nhóm gửi.
A/B bật, A=10, B=20 Target được gán A, B hoặc CONTROL; chỉ A/B sinh command.
A/B bật, A=50, B=50 Không có target CONTROL.
A/B bật, A=20 Hợp lệ: A 20%, CONTROL 80%.
A/B bật, A=100 Không hợp lệ vì chỉ có một nhóm có ý nghĩa.
A/B bật, A=60, B=50 Từ chối CAMPAIGN_AB0003.
Hai chain trùng chainCode Từ chối CAMPAIGN_AB0005.
Target của variant SMS thiếu phone Không chuyển variant; ghi SKIPPED/FAILED cho đúng variant.
Target CONTROL Có trong TargetSnapshot và báo cáo, không có send command.
Kafka phát lại command Worker nhận cùng idempotency key và không gọi provider lần hai.
Dispatcher chết sau 20 batch Node khác tiếp tục từ checkpoint đã lưu.
Campaign cũ có variant rỗng Báo cáo xếp DEFAULT/LEGACY, không xếp CONTROL.
Nhân bản Campaign Campaign mới có seed mới và chainCode hợp lệ.
Sửa Campaign đã SUBMITTED Chuyển về DRAFT, yêu cầu duyệt lại.
Sửa Campaign đã chạy Từ chối; người dùng phải nhân bản Campaign.
Camp 1 chia SMS 10%, WhatsApp 20% 70% còn lại được ghi CONTROL; conversion dùng APP_ACTIVATED.
Camp 2 đến mốc ngày 3 nhưng user đã kích hoạt App Không đưa user vào TargetSnapshot của bước ngày 3.
Camp 3 target không còn FCM token hợp lệ Không chuyển variant; ghi SKIPPED_INVALID_ENDPOINT.
Camp 4 đã có MONTHLY_DATA_PACKAGE_REGISTERED Loại khỏi các lần gửi tuần tiếp theo và ghi nhận conversion cho run phù hợp.
Camp 5 đã có DATA_USAGE_STARTED Loại khỏi các lần gửi tuần tiếp theo và ghi nhận conversion cho run phù hợp.
Người thuộc CONTROL tự kích hoạt App Vẫn tính conversion cho CONTROL để có số nền so sánh.

16. Thứ tự triển khai đề xuất

  1. T01 để chốt contract giữa FE và BE.
  2. T02 để tạo được TargetSnapshot có variant thật.
  3. T03 để chạy đúng variant và không gửi cho control.
  4. T04 để event không mất variantCode và có chống trùng.
  5. T05 để báo cáo đúng trên dữ liệu đã phát sinh.
  6. T06 triển khai song song để đáp ứng Push Notification của Camp 3.
  7. T07 chuẩn bị script, thứ tự deploy và chỉ bật feature flag sau khi các task lõi hoàn thành.

Không bật abTestingEnabled trên production trước khi T01 đến T05 hoàn thành. Có thể deploy schema/field trước với mặc định false để Campaign cũ tiếp tục chạy bình thường.

17. Ngoài phạm vi đợt này

Các phần này có thể bổ sung sau mà không phải thay đổi nguyên tắc chainCode, assignmentSeed, variant_code và TargetSnapshot đã thiết kế ở trên.