Campaign 3 gửi Push Notification cho thiết bị đã mở ứng dụng nhưng sau 24 giờ vẫn chưa đăng nhập hoặc kích hoạt tài khoản.
Thiết kế không chỉ giải quyết một điều kiện hard-code. Hệ thống cần tạo được Segment động từ:
Các nguồn FILE, OCS và SDK phải cùng cập nhật một kho customer chung. Segment chỉ lưu định nghĩa chọn customer và các phiên bản kết quả, không tạo một kho customer riêng cho từng nguồn hoặc từng Campaign.
Nguồn chọn đối tượng mới sử dụng tên backend:
APP_BEHAVIOR
Tên hiển thị trên FE đề xuất là Hành vi ứng dụng.
Không dùng tên EVENT hoặc APP_EVENT vì dễ nhầm với API event-trigger cũ. Các enum và API event-trigger cũ không thuộc thiết kế Campaign 3; có thể xóa ở một thay đổi riêng nếu không còn luồng Lucky Draw/Laoshop sử dụng.
| Lựa chọn trên FE | Giá trị backend | Cách xử lý |
|---|---|---|
| Upload file | FILE |
Import customer, tạo Segment và SegmentVersion từ file. |
| Chọn Segment có sẵn | AVAILABLE |
Campaign sử dụng Segment đã tồn tại. |
| Hành vi ứng dụng | APP_BEHAVIOR |
Lưu cây điều kiện và build SegmentVersion tại thời điểm CampaignRun chạy. |
Trong phạm vi Campaign 3, APP_BEHAVIOR chỉ được kết hợp với action NOTIFICATION_PUSH. Backend phải kiểm tra quy tắc này; không chỉ ẩn lựa chọn trên FE.
| Dữ liệu | Nơi lưu chính | Lý do |
|---|---|---|
| Customer hiện tại | MongoDB | Cần tạo/cập nhật, chống trùng và tìm nhanh theo identity. |
| Installation và FCM token hiện tại | MongoDB | Cần tính nhất quán khi đăng nhập và trước khi gửi Push. |
| PostHog event lịch sử | ClickHouse | Số lượng lớn, chủ yếu ghi mới và phân tích theo thời gian. |
| Bản chiếu customer/installation phục vụ query lớn | ClickHouse | Cho phép lọc nhiều triệu customer kết hợp với lịch sử event. |
| Segment, SegmentVersion, CampaignRun | MongoDB | Là metadata và state machine nghiệp vụ. |
| SegmentVersion member và TargetSnapshot member | ClickHouse | Danh sách có thể rất lớn và cần đọc theo batch. |
MongoDB là nguồn đúng cuối cùng đối với customer, trạng thái đăng nhập và FCM token. ClickHouse là nơi tính toán audience số lượng lớn. Không đưa FCM token vào ClickHouse, Kafka command hoặc SegmentVersion member.
| Khái niệm | Ý nghĩa |
|---|---|
CustomerProfile |
Hồ sơ khách hàng chung trong một company. FILE, OCS và SDK đều dùng chung customer này. |
CustomerIdentity |
Phone/email đã chuẩn hóa dùng để tìm customer hiện có. |
AppInstallation |
Một lượt cài ứng dụng trên một thiết bị, bao gồm FCM token và trạng thái đăng nhập. |
PostHogEvent |
Một hành vi đã xảy ra tại một thời điểm, ví dụ mở app hoặc xem sản phẩm. |
APP_BEHAVIOR Segment |
Segment lưu cây điều kiện trên customer, installation và event. |
SegmentVersion |
Kết quả đã chốt của một lần build Segment. |
CampaignRun |
Một lần chạy thực tế của Campaign. |
TargetSnapshot |
Danh sách người nhận đã chốt cho một CampaignRun. |
Một customer có thể xuất hiện trong nhiều file, nhiều Segment và nhiều Campaign nhưng vẫn sử dụng cùng một customerId trong company.
customer_profileĐây là hồ sơ customer hiện tại, không phải bảng event hoặc danh sách member của Segment.
{
"_id": "customerId",
"companyId": "companyId",
"profileType": "IDENTIFIED",
"canonicalCustomerId": "customerId",
"firstName": "Somchai",
"lastName": "...",
"language": "lo",
"attributes": {
"province": "Vientiane",
"subscriberType": "PREPAID"
},
"status": "ACTIVE",
"profileVersion": 7,
"firstSeenAt": "...",
"lastSeenAt": "...",
"createdAt": "...",
"updatedAt": "..."
}
Enum:
CustomerProfileType: ANONYMOUS, IDENTIFIED
CustomerProfileStatus: ACTIVE, INACTIVE, MERGED, ANONYMIZED
canonicalCustomerId có ý nghĩa:
_id.canonicalCustomerId để quy hành vi cũ về customer thật.Index tối thiểu:
db.customer_profile.createIndex(
{ companyId: 1, status: 1, updatedAt: -1 },
{ name: "customer_profile_company_status_updated_idx" }
)
db.customer_profile.createIndex(
{ companyId: 1, canonicalCustomerId: 1 },
{ name: "customer_profile_company_canonical_idx" }
)
customer_identityCollection này chống tạo trùng customer khi cùng phone/email xuất hiện từ FILE, OCS hoặc app login.
{
"_id": "identityId",
"companyId": "companyId",
"customerId": "customerId",
"type": "MSISDN",
"valueHash": "sha256(normalizedValue)",
"valueEncrypted": "encryptedValue",
"status": "ACTIVE",
"verified": false,
"createdAt": "...",
"updatedAt": "..."
}
Enum:
CustomerIdentityType: MSISDN, EMAIL
CustomerIdentityStatus: ACTIVE, REVOKED
Index bắt buộc:
db.customer_identity.createIndex(
{ companyId: 1, type: 1, valueHash: 1 },
{ unique: true, name: "customer_identity_company_type_hash_unique_idx" }
)
db.customer_identity.createIndex(
{ companyId: 1, customerId: 1, status: 1 },
{ name: "customer_identity_company_customer_status_idx" }
)
Không lưu LaoID ID hoặc OCS subscriber ID làm khóa customer trong phạm vi này. Các nguồn dùng phone/email đã chuẩn hóa để resolve customer.
app_installationMột customer có thể có nhiều installation. Một installation tại một thời điểm chỉ trỏ tới một customer hiện tại.
{
"_id": "installationRecordId",
"companyId": "companyId",
"appId": "UNITEL_SUPER_APP",
"installationId": "uuid-generated-by-app",
"currentCustomerId": "customerId",
"firstOpenedAt": "...",
"lastSeenAt": "...",
"everLoggedIn": false,
"lastLoggedInAt": null,
"fcmTokenEncrypted": "...",
"fcmTokenHash": "...",
"fcmStatus": "ACTIVE",
"platform": "ANDROID",
"appVersion": "1.0.0",
"language": "lo",
"installationVersion": 3,
"status": "ACTIVE",
"createdAt": "...",
"updatedAt": "..."
}
Enum:
InstallationStatus: ACTIVE, INACTIVE
FcmTokenStatus: ACTIVE, INVALID, REVOKED
PlatformType: ANDROID, IOS
Index:
db.app_installation.createIndex(
{ companyId: 1, appId: 1, installationId: 1 },
{ unique: true, name: "app_installation_company_app_installation_unique_idx" }
)
db.app_installation.createIndex(
{ companyId: 1, appId: 1, everLoggedIn: 1, fcmStatus: 1, status: 1, firstOpenedAt: 1 },
{ name: "app_installation_campaign3_filter_idx" }
)
db.app_installation.createIndex(
{ companyId: 1, appId: 1, currentCustomerId: 1, status: 1 },
{ name: "app_installation_company_app_customer_idx" }
)
db.app_installation.createIndex(
{ companyId: 1, appId: 1, fcmTokenHash: 1 },
{ name: "app_installation_company_app_token_idx", sparse: true }
)
app_behavior_field_configCollection này quy định FE được phép sử dụng field nào và backend phải đọc field đó ở đâu.
{
"appId": "UNITEL_SUPER_APP",
"fieldCode": "FIRST_OPENED_AT",
"domain": "APP_INSTALLATION",
"storageType": "CURRENT_STATE",
"queryBackend": "MONGO",
"dataType": "DATETIME",
"allowedOperators": ["BEFORE", "AFTER", "BEFORE_RELATIVE"],
"labels": {
"vi": "Thời điểm mở ứng dụng lần đầu",
"en": "First opened at",
"lo": "..."
},
"status": "ACTIVE",
"sortOrder": 1
}
Các enum nội bộ:
BehaviorDataDomain: CUSTOMER_PROFILE, APP_INSTALLATION, POSTHOG_EVENT
BehaviorStorageType: CURRENT_STATE, EVENT_HISTORY, EVENT_AGGREGATE
BehaviorQueryBackend: MONGO, CLICKHOUSE
BehaviorDataType: STRING, NUMBER, BOOLEAN, DATETIME, ENUM
physicalField hoặc biểu thức SQL không được nhận từ FE. Nếu lưu mapping vật lý trong Mongo thì giá trị phải được backend kiểm tra bằng allowlist trước khi tạo query.
app_event_catalogChỉ các event được bật audienceEnabled mới xuất hiện trên FE và được dùng để build audience.
{
"appId": "UNITEL_SUPER_APP",
"eventCode": "PRODUCT_VIEWED",
"sourceEventNames": ["Product Viewed", "product_viewed"],
"allowedMetrics": ["COUNT", "FIRST_OCCURRED_AT", "LAST_OCCURRED_AT", "EXISTS"],
"maxLookbackDays": 90,
"audienceEnabled": true,
"status": "ACTIVE",
"labels": {
"vi": "Xem sản phẩm",
"en": "Product viewed",
"lo": "..."
}
}
analytics.posthog_eventsGiữ bảng PostHog hiện tại nhưng bổ sung các dimension typed:
event_id UUID
company_id String
app_id LowCardinality(String)
event_code LowCardinality(String)
source_event_name String
installation_id String
customer_id_at_event Nullable(String)
event_time DateTime64(3, 'UTC')
received_at DateTime64(3, 'UTC')
properties_json String
trace_id String
ip String
user_agent String
Khóa sắp xếp đề xuất:
PARTITION BY toYYYYMM(event_time)
ORDER BY (
company_id,
app_id,
event_code,
toDate(event_time),
installation_id,
event_time,
event_id
)
Quy tắc:
company_id và app_id.properties_json tùy ý.event_id ổn định qua Kafka retry; thống kê và aggregate không được tính một event trùng thành hai hành vi.analytics.customer_profile_currentĐây là bản chiếu phục vụ query lớn, không thay thế MongoDB source of truth.
company_id
customer_id
canonical_customer_id
profile_type
status
language
country_code
province_code
subscriber_type
attributes
profile_version
updated_at
Engine đề xuất:
ReplacingMergeTree(profile_version)
ORDER BY (company_id, customer_id)
Query phải lấy version mới nhất bằng argMax(..., profile_version) hoặc một lớp view chuẩn. Không dùng FINAL mặc định cho query lớn.
analytics.app_installation_currentcompany_id
app_id
installation_id
current_customer_id
first_opened_at
last_seen_at
ever_logged_in
last_logged_in_at
has_active_fcm
platform
app_version
language
status
installation_version
updated_at
Engine đề xuất:
ReplacingMergeTree(installation_version)
ORDER BY (company_id, app_id, installation_id)
FCM token không tồn tại trong bảng này. Bảng chỉ có has_active_fcm để lọc candidate.
analytics.app_event_daily_aggregateDùng cho các điều kiện như “mở app ít nhất 3 lần trong 7 ngày”.
event_date
company_id
app_id
event_code
installation_id
customer_id_at_event Nullable(String)
event_count
first_event_at
last_event_at
Chỉ tạo aggregate cho event có audienceEnabled = true. Không tổng hợp mọi event PostHog theo installation nếu không có nhu cầu audience.
segment_version_memberMember cần hỗ trợ cả customer-level và installation-level:
company_id
segment_id
segment_version_id
subject_type -- CUSTOMER hoặc INSTALLATION
customer_id Nullable(String)
installation_id Nullable(String)
recipient_key
source_type -- FILE, OCS, APP_BEHAVIOR
created_at
Campaign 3 sử dụng subject_type = INSTALLATION. SMS/mail có thể sử dụng subject_type = CUSTOMER.
Không thực hiện kiểu ghi MongoDB xong gửi Kafka bằng hai thao tác độc lập. Nếu MongoDB thành công nhưng Kafka lỗi, bản chiếu ClickHouse sẽ thiếu dữ liệu.
Phương án đề xuất là Mongo Change Stream:
customer_profile/app_installation thay đổi
-> Change Stream Publisher đọc change event
-> gửi snapshot có version vào Kafka
-> Behavior Projection Worker ghi ClickHouse
Publisher lưu resume token/checkpoint để tiếp tục sau khi restart. Nếu oplog đã hết thời gian giữ và resume token không còn dùng được, chạy reconcile/full rebuild theo companyId và khoảng updatedAt.
Projection Worker chỉ nhận snapshot có version lớn hơn version đã biết. Kafka retry hoặc nhiều node xử lý cùng event không được làm trạng thái cũ ghi đè trạng thái mới.
Không nguồn nào được tự ghi trực tiếp theo quy tắc riêng vào customer_profile. Tất cả sử dụng chung:
CustomerIngestionService
-> CustomerDataNormalizer
-> CustomerIdentityResolver
-> CustomerProfileUpsertService
customer_identity trong đúng companyId.Không được tìm customer khác company. Cùng một phone ở hai company là hai customer độc lập.
FILE và OCS vừa cập nhật kho customer chung, vừa tạo membership cho đúng SegmentVersion. Không đổ raw row vào customer_profile mà không normalize/resolve.
Khi app mở lần đầu:
installationId dạng UUID và lưu bền vững.customer_profile nếu installation chưa có customer.app_installation trỏ tới anonymous customer.customer_id_at_event là anonymous customer.Khi đăng nhập:
app_installation.currentCustomerId sang customer thật.everLoggedIn = true; logout không đưa field này về false.MERGED và canonicalCustomerId trỏ tới customer thật.Không nối toàn bộ event cũ vào currentCustomerId hiện tại của installation, vì một thiết bị có thể đổi tài khoản. customer_id_at_event giữ đúng customer tại thời điểm event; canonicalCustomerId chỉ dùng để hợp nhất anonymous customer đã được xác định.
POST /sdk/v1/apps/{appId}/installations/register
PUT /sdk/v1/apps/{appId}/installations/{installationId}/fcm-token
POST /sdk/v1/apps/{appId}/installations/{installationId}/identify
POST /sdk/v1/apps/{appId}/installations/{installationId}/logout
Vai trò:
| API | Vai trò |
|---|---|
register |
Ghi nhận lần mở app đầu tiên và thông tin thiết bị. Gọi lại phải idempotent. |
fcm-token |
Cập nhật token khi Firebase cấp token mới. |
identify |
Liên kết installation với customer thật sau login thành công. |
logout |
Xóa customer đang đăng nhập khỏi session hiện tại nhưng không reset everLoggedIn. |
firstOpenedAt chỉ được gán lần đầu. Các request register tiếp theo chỉ cập nhật lastSeenAt, app version, language và platform.
POST /posthog/capture
POST /posthog/e
POST /posthog/batch
GET/POST /posthog/decide
capture, e, batch tiếp tục được tái sử dụng. decide chỉ phục vụ tương thích SDK, không dùng để build audience.
PostHogEventMessage mới:
{
"schemaVersion": 2,
"eventId": "uuid",
"companyId": "companyId",
"appId": "UNITEL_SUPER_APP",
"eventCode": "APP_OPENED",
"sourceEventName": "Application Opened",
"installationId": "installationId",
"customerIdAtEvent": "customerId",
"eventTime": "...",
"receivedAt": "...",
"propertiesJson": "{}",
"traceId": "..."
}
Backend resolve companyId và appId từ integration API key. Không nhận hai field này từ client làm nguồn tin cậy.
Event dùng cho audience bắt buộc có installationId. Event thiếu installation vẫn có thể lưu raw để debug nếu nghiệp vụ cho phép, nhưng phải đặt audienceEligible = false và không được dùng để tạo SegmentVersion.
FE lấy metadata từ backend, không tự hard-code danh sách field/operator:
GET /apps
GET /apps/{appId}/behavior-fields
GET /apps/{appId}/behavior-events
POST /app-behavior/validate
POST /app-behavior/estimate
estimate có timeout và rate limit riêng. Không dùng kết quả estimate làm TargetSnapshot.
GROUP
Nhóm điều kiện con bằng AND hoặc OR.
FIELD
Điều kiện trên trạng thái customer hoặc installation hiện tại.
EVENT_METRIC
Điều kiện dựa trên số lần, lần đầu hoặc lần cuối event xảy ra trong một khoảng thời gian.
Ví dụ Campaign 3:
{
"source": "APP_BEHAVIOR",
"appId": "UNITEL_SUPER_APP",
"rootCondition": {
"nodeType": "GROUP",
"logicalOperator": "AND",
"children": [
{
"nodeType": "FIELD",
"fieldCode": "FIRST_OPENED_AT",
"operator": "BEFORE_RELATIVE",
"relativeValue": 24,
"relativeUnit": "HOUR"
},
{
"nodeType": "FIELD",
"fieldCode": "EVER_LOGGED_IN",
"operator": "EQUALS",
"value": false
},
{
"nodeType": "FIELD",
"fieldCode": "FCM_STATUS",
"operator": "EQUALS",
"value": "ACTIVE"
}
]
}
}
Ví dụ tương lai: customer đã xem sản phẩm ít nhất ba lần trong bảy ngày:
{
"nodeType": "EVENT_METRIC",
"eventCode": "PRODUCT_VIEWED",
"metric": "COUNT",
"operator": "GREATER_THAN_OR_EQUALS",
"value": 3,
"timeWindow": {
"type": "LAST",
"value": 7,
"unit": "DAY"
}
}
Giới hạn đề xuất:
maxDepth = 3
maxTotalNodes = 20
maxGroupChildren = 10
maxInValues = 100
maxEventLookbackDays = 90
Segment không được lưu tên bảng Mongo, ClickHouse hoặc SQL. Segment chỉ lưu fieldCode, eventCode, operator và value.
AudienceQueryPlanner đọc metadata của các field rồi quyết định query ở đâu:
public enum AudienceQueryMode {
MONGO_ONLY,
CLICKHOUSE_ONLY,
HYBRID
}
| Điều kiện | Query mode | Cách chạy |
|---|---|---|
| Chỉ trạng thái hiện tại | MONGO_ONLY |
Query MongoDB bằng index và cursor. |
| Chỉ lịch sử/aggregate event | CLICKHOUSE_ONLY |
Query ClickHouse theo company, app, event và time range. |
| Có cả trạng thái hiện tại và event | HYBRID |
Query trong ClickHouse bằng current projection kết hợp event/aggregate. |
Các strategy:
MongoCurrentStateQueryStrategy
ClickHouseBehaviorQueryStrategy
HybridBehaviorQueryStrategy
Controller và Campaign service không chứa Mongo query hoặc ClickHouse SQL. Chúng chỉ gọi AudienceQueryService; service chọn strategy dựa trên plan.
Campaign 3 chỉ có field CURRENT_STATE:
firstOpenedAt <= runAt - 24h
everLoggedIn = false
fcmStatus = ACTIVE
status = ACTIVE
Planner tạo MONGO_ONLY. Worker đọc app_installation theo index và cursor/batch 1.000 bản ghi, sau đó ghi kết quả vào segment_version_member.
Không dùng skip/offset cho job lớn. Build job lưu checkpoint để node khác có thể tiếp tục khi worker dừng giữa chừng.
Ví dụ:
PRODUCT_VIEWED COUNT >= 3 trong 7 ngày
Planner tạo CLICKHOUSE_ONLY và query app_event_daily_aggregate hoặc posthog_events nếu event chưa có aggregate phù hợp. Event chưa đăng ký audienceEnabled không được query.
Ví dụ:
province = VIENTIANE
AND everLoggedIn = false
AND PRODUCT_VIEWED COUNT >= 3 trong 7 ngày
Planner tạo HYBRID. Query sử dụng:
customer_profile_current
JOIN app_installation_current
JOIN app_event_daily_aggregate
Toàn bộ phép giao/hợp tập customer được thực hiện trong ClickHouse. Không tải hàng triệu candidate ID về RAM Java để tự giao tập hợp.
Trước khi hỗ trợ HYBRID trên production, projection customer và installation phải hoàn thành và có cơ chế kiểm tra độ trễ. Không fallback âm thầm sang quét Mongo nếu projection chưa sẵn sàng; trả mã lỗi nguồn query chưa khả dụng.
Ví dụ ban đầu FIRST_OPENED_AT có:
queryBackend = MONGO
Khi app_installation_current đã ổn định, backend có thể chuyển mapping nội bộ sang ClickHouse. Segment cũ vẫn giữ fieldCode = FIRST_OPENED_AT, nên không phải migrate definition.
Worker hiện tại dự kiến bị loại bỏ nên tạo module/service Push Worker mới, không tiếp tục gắn logic mới vào worker cũ.
Quy trình một batch:
campaignRunId, targetSnapshotId, actionIndex và danh sách installationId.app_installation mới nhất từ MongoDB.INACTIVE, everLoggedIn=true hoặc token không ACTIVE.INVALID.Idempotency key:
campaignRunId + installationId + actionIndex
Không log raw FCM token, phone, email hoặc payload chứa thông tin nhạy cảm.
Mục tiêu
Mọi nguồn tạo/cập nhật cùng một customer trong company và trả về customerId ổn định.
Phạm vi
customer_profile và customer_identity.CustomerDataNormalizer, CustomerIdentityResolver, CustomerProfileUpsertService.profileVersion để Mongo Change Stream tạo projection ClickHouse đúng thứ tự.companyId, sourceType, jobId, customerId; không log raw identity.Thay đổi dữ liệu
customer_profile bổ sung profileType, canonicalCustomerId, profileVersion.customer_identity giữ identity phone/email đã mã hóa và hash.segment_version_member bắt buộc có customer_id với FILE/OCS.Case bắt buộc
| Case | Xử lý |
|---|---|
| Cùng phone xuất hiện lại từ OCS | Update customer cũ, không tạo mới. |
| Customer từ FILE đăng nhập app | Installation liên kết đúng customer FILE. |
| Chưa có phone/email khi mở app | Tạo anonymous customer. |
| Phone/email khác company | Không được match chéo company. |
| Worker chạy lại cùng batch | Không tạo customer trùng. |
Kết quả nghiệm thu
customerId khi đi qua FILE, OCS và app login.Mục tiêu
Quản lý chính xác lượt cài ứng dụng, trạng thái đăng nhập và endpoint gửi Push.
Phạm vi
app_installation.installationId do app tạo dạng UUID và giữ qua các lần mở app.firstOpenedAt bất biến; lastSeenAt được cập nhật khi register lại.everLoggedIn chỉ chuyển từ false sang true, không reset khi logout.installationVersion nguyên tử khi trạng thái ảnh hưởng audience thay đổi.installationVersion; Mongo Change Stream chịu trách nhiệm phát snapshot cập nhật projection.Case bắt buộc
| Case | Xử lý |
|---|---|
| Register lặp | Upsert idempotent, không đổi firstOpenedAt. |
| Firebase đổi token | Thay token, tăng version. |
| Một customer nhiều thiết bị | Cho phép nhiều installation. |
| Thiết bị đổi tài khoản | Event tương lai dùng customer mới; event cũ giữ customer tại thời điểm xảy ra. |
| Logout | Xóa session customer hiện tại nhưng giữ everLoggedIn=true. |
Kết quả nghiệm thu
installationId.Mục tiêu
Giữ API PostHog hiện tại nhưng mọi event dùng cho audience có đủ company, app, installation và customer tại thời điểm xảy ra.
Phạm vi
PostHogEventMessage với schemaVersion, companyId, appId, installationId, customerIdAtEvent, eventCode.TrackingIntegrationApiKeyService trả integration context active thay vì chỉ validate void.eventCode bằng app_event_catalog.Thay đổi dữ liệu
analytics.posthog_events hiện tại; không tạo thêm bảng raw trùng chức năng.posthog_daily_event_counts chỉ giữ cho dashboard tổng quan, không dùng để build audience.Kết quả nghiệm thu
Mục tiêu
Chuẩn bị dữ liệu để tương lai query hành vi kết hợp customer mà không quét Mongo hoặc parse JSON toàn bảng.
Phạm vi
customer_profile_current và app_installation_current.app_event_daily_aggregate cho event được bật audience.argMax, không dùng FINAL mặc định.Kết quả nghiệm thu
Mục tiêu
Tạo Segment động bằng cây điều kiện và tự chọn MongoDB hoặc ClickHouse theo loại dữ liệu.
Phạm vi
APP_BEHAVIOR vào nguồn Segment.GROUP, FIELD, EVENT_METRIC.app_behavior_field_config và app_event_catalog.AudienceQueryPlanner và ba strategy Mongo/ClickHouse/Hybrid.MongoCurrentStateQueryStrategy.IN và event lookback.INSERT SELECT, không tải toàn bộ ID vào RAM.Case bắt buộc
| Case | Xử lý |
|---|---|
| Chỉ field current state | MONGO_ONLY. |
| Chỉ event history | CLICKHOUSE_ONLY. |
| Có field và event | HYBRID; yêu cầu projection sẵn sàng. |
| Field/event không có trong catalog | Từ chối bằng error code. |
| Event không có time window | Từ chối. |
| Query source không khả dụng | Không fallback ngầm; trả lỗi rõ ràng. |
Kết quả nghiệm thu
if campaign3.Mục tiêu
Mỗi lần Campaign chạy phải chốt đúng audience tại runAt và không dùng nhầm version cũ.
Phạm vi
APP_BEHAVIOR chỉ đi với NOTIFICATION_PUSH trong scope hiện tại.CampaignRun PREPARING_TARGET.runAt UTC.SegmentVersion BUILDING và build job.READY khi toàn bộ member đã ghi thành công.latestReadyVersionId cũ nếu build mới lỗi.READY.Kết quả nghiệm thu
Mục tiêu
Gửi Push theo batch, không gửi nhầm user vừa đăng nhập và ghi đầy đủ kết quả.
Phạm vi
app_installation từ Mongo theo batch.INVALID khi Firebase xác nhận token hỏng.Kết quả nghiệm thu
Mục tiêu
Cho người dùng tạo Campaign 3 trên màn Campaign hiện tại mà không cần biết MongoDB, ClickHouse hoặc SQL.
Phạm vi
dataType và allowedOperators.APP_BEHAVIOR trong scope hiện tại.BUILDING hiển thị tiến độ; không coi chưa có member là lỗi ngay lập tức.Kết quả nghiệm thu
Có thể chia giai đoạn:
HYBRID sau khi projection lag và reconcile đạt yêu cầu vận hành.| Trường hợp | Xử lý |
|---|---|
| Customer đã có từ FILE rồi đăng nhập app | Liên kết installation với customer FILE, không tạo customer thật thứ hai. |
| Customer đã có từ OCS rồi đăng nhập app | Liên kết installation với customer OCS. |
| Mở app nhưng chưa có phone/email | Tạo anonymous customer và installation. |
| Đăng nhập trước 24 giờ | everLoggedIn=true, không thuộc Campaign 3. |
| Đăng nhập sau khi build nhưng trước lúc gửi | Push Worker ghi SUPPRESSED_LOGGED_IN. |
| Logout | Không reset everLoggedIn; không quay lại Campaign 3. |
| Cùng thiết bị đăng nhập tài khoản khác | Event mới gắn customer mới; event cũ không đổi. |
| FCM token refresh | Sử dụng token mới nhất từ Mongo. |
| FCM token invalid | Mark INVALID, không retry token đó. |
| PostHog event đến trùng | Không tính thành hai hành vi độc lập. |
| PostHog event đến muộn | Lưu theo eventTime; chỉ ảnh hưởng run chưa build hoặc run tương lai. |
| Event thiếu installation | Không dùng để build audience. |
| Projection ClickHouse chậm | Không bật HYBRID nếu vượt ngưỡng; final send vẫn check Mongo. |
| Campaign interval chạy lần tiếp theo | Tạo SegmentVersion mới theo runAt mới. |
| Build mới thất bại | CampaignRun lỗi/chờ retry; không dùng version cũ. |
| Hai node cùng nhận build job | Chỉ một node claim được lease. |
| Worker chết giữa batch | Chạy lại idempotent và tiếp tục theo checkpoint. |
APP_BEHAVIOR0001 Ứng dụng không tồn tại hoặc không hoạt động
APP_BEHAVIOR0002 Field không được phép dùng làm audience
APP_BEHAVIOR0003 Event không được phép dùng làm audience
APP_BEHAVIOR0004 Operator không phù hợp với kiểu dữ liệu
APP_BEHAVIOR0005 Cây điều kiện vượt giới hạn
APP_BEHAVIOR0006 Event condition thiếu time window
APP_BEHAVIOR0007 Khoảng thời gian vượt giới hạn cho phép
APP_BEHAVIOR0008 Không thể estimate audience
APP_BEHAVIOR0009 APP_BEHAVIOR hiện chỉ hỗ trợ Push Notification
APP_BEHAVIOR0010 Nguồn dữ liệu query chưa sẵn sàng
APP_BEHAVIOR0011 Không thể lập kế hoạch query từ condition
APP_PROFILE0001 Installation không tồn tại
APP_PROFILE0002 FCM token không hợp lệ
APP_PROFILE0003 Không thể identify installation
APP_PROFILE0004 Installation không thuộc company/app hiện tại
CUSTOMER0001 Không có identity hợp lệ để tạo customer
CUSTOMER0002 Customer identity đã thuộc customer khác
CUSTOMER0003 Không thể resolve customer
PUSH0001 Cấu hình Push không hợp lệ
PUSH0002 Firebase tạm thời không khả dụng
PUSH0003 FCM token đã hết hiệu lực
PUSH0004 Push command vượt số lần retry
Message được lấy từ StatusCodeEnum và file đa ngôn ngữ. Service throw error code tương ứng; không hard-code message nghiệp vụ khi error code đã có message.
Log bắt buộc có các ID phù hợp với từng luồng:
traceId
companyId
appId
customerId
installationId
campaignId
campaignRunId
segmentId
segmentVersionId
buildJobId
Kafka topic/partition/offset
batchSize
durationMs
Không log:
Raw FCM token
Raw phone/email
Access token
Toàn bộ propertiesJson nếu có PII
Metric tối thiểu:
Campaign 3 không tạo một kho user riêng. Nó sử dụng kho customer chung đã được FILE, OCS và SDK cùng cập nhật.
Điều kiện hiện tại chỉ cần trạng thái mới nhất nên được thực thi bằng MONGO_ONLY:
firstOpenedAt <= runAt - 24h
everLoggedIn = false
fcmStatus = ACTIVE
status = ACTIVE
Khi bổ sung điều kiện lịch sử PostHog, cùng definition APP_BEHAVIOR sẽ được AudienceQueryPlanner chuyển sang CLICKHOUSE_ONLY hoặc HYBRID. Segment không biết và không phụ thuộc database vật lý.
Mỗi CampaignRun build một SegmentVersion tại thời điểm chạy, chốt TargetSnapshot rồi Push Worker kiểm tra lại Mongo trước khi gửi. Cách này vừa giữ đúng lịch sử, vừa tránh gửi nhầm người vừa đăng nhập, đồng thời mở rộng được cho các hành vi ứng dụng mới mà không phải hard-code thêm một loại Campaign.