RFC: Customer Segmentation — Frontend
Document Conventions (do not remove)
This RFC follows the Qontak RFC Template format for governance — the metadata table, Confluence sections 1–6, and Comment logs are mandatory. Replace placeholder values; mark sections
N/A — reasonwhen truly inapplicable rather than deleting them.It is also agent-execution-ready: the §1 Design References, §2 Repo Reading Guide (Detail 2.0), mermaid diagrams, and §4 Agent Execution Plan
- Verification & Rollback Recipe must be complete before §7 Ready for agent execution: yes.
Delivery & project management live elsewhere. This RFC is the technical artifact only — it deliberately holds no staffing, effort estimates, timeline, or rollout schedule. Those live in the initiative's
delivery/folder.The YAML frontmatter at the very top is the machine-readable index agents parse. The metadata table below is the human-readable governance record. Both must agree on every shared field (status, owner, type, dates).
Metadata
| Field | Value | Notes |
|---|---|---|
| Status | IDEA | YAML status: draft |
| DRI | Azani Ramadhan | Frontend owner for the customer segmentation feature |
| Team | cdp | CDP squad |
| Author(s) | Azani Ramadhan | |
| Reviewers | TBD — CDP FE tech lead | |
| Approver(s) | TBD — tech lead + infosec approver | |
| Submitted Date | 2026-06-30 | |
| Last Updated | 2026-06-30 | |
| Target Release | 2026-Q3 | commitment_date 2026-07-21 (revenue) — see initiative README |
| Target Quarter | 2026-Q3 | |
| Delivery | not yet handed to delivery | See ../delivery/timeline.md once timeline is confirmed |
| Related | ../prds/prd-customer-segmentation-basic-attributes.md · ../prds/prd-customer-segmentation-use-case-activities.md · ../prds/prd-customer-segmentation-anchor.md | |
| Discussion | TBD — CDP squad Slack |
Type: frontend Sub-type: new-feature
Sections at a Glance
- Overview (§1 Design References — Figma; §1 Traceability; §1 Per-Story Change Map)
- Technical Design (Repo Reading Guide → Component Diagram → State Machine → Sequence → APIs Consumed → UI Contract → Data-Fetching Strategy → UI State Matrix → Scope Boundaries → Asset Inventory)
- High-Availability & Security
- Backwards Compatibility and Rollout Plan (§4 Agent Execution Plan + Verification & Rollback Recipe)
- Concern, Questions, or Known Limitations
- Comment logs
- Ready for agent execution
1. Overview
This RFC covers the complete frontend implementation of Customer Segmentation in qontak-customer-fe (Nuxt 3) — a rule-based dynamic segmentation engine that classifies contacts into auto-updating groups, reusable for Broadcast targeting. The implementation spans two child PRDs under the same initiative:
- Basic Attributes (TF-2544 — foundation, in progress): Segment List, Segment Builder over default/custom fields, basic events, consent, loyalty attributes; segment creation/editing; Preview; Segment Detail (Overview); Send Campaign entry point; permission gating.
- Use-Case Activities (TF-2549–TF-2555 — draft): Extends the existing builder's condition palette with cross-module association conditions (Conversations, Campaigns, Company, Deals, Tickets), aggregate use-case metrics, and RFM/Recency fields; adds the Segment Detail Performance tab; finalizes permission keys.
Significant scaffolding already exists in the repo (SegmentStore.ts, DrawerCreateSegment.vue, FilterCriteriaRow.vue, SegmentListPage.vue, pages/customers/segments/[id].vue). This RFC describes completing the Basic Attributes feature and extending it with Use-Case Activities FE work.
Success Criteria
- Users can create, edit, duplicate, and archive rule-based segments over all Basic Attributes field types (default, custom, basic events, consent, loyalty) — measured by vitest unit tests passing and E2E smoke test.
- Users can add association conditions (Conversations, Campaigns, Company, Deals, Tickets) and aggregate metrics conditions in the same builder — measured by component test coverage.
- Preview panel shows matched customer count within 5 s (p95) per PRD §6 Constraints.
- Segment Detail → Overview shows total matched, percentage reach, reachability by channel (WhatsApp + Email), and customer matched table.
- Segment Detail → Performance tab renders growth chart (line), source/domicile/age/gender distribution charts.
- Send Campaign flow opens channel-selector sidebar for WhatsApp; routes to Broadcast with segment as recipient.
- All four permission keys (
customers_segment_view/add/manage/archived) gate UI surfaces correctly. - All Vitest unit tests pass; E2E smoke spec for the golden segment-creation path passes.
Out of Scope
- Exception filters (exclude-customers-from-segment) — explicitly descoped (Non-Goal §7 in Use-Case Activities PRD).
- Email broadcast from a segment — WhatsApp only for Send Campaign at this phase (Non-Goal §8).
- Real-time segment re-evaluation — daily batch only.
- Segment membership export (CSV/XLSX) — deferred to export initiative.
- AI/ML-suggested segments — future phase.
- Mobile app configuration — web-only.
- Backend segmentation engine internals — covered by the external Confluence BE RFC (
DRAFT RFC — Customer Segmentation based on Basic Attributes (Backend)related to TF-2544); this RFC consumes the BE API only.
Related Documents
- Basic Attributes PRD:
../prds/prd-customer-segmentation-basic-attributes.md - Use-Case Activities PRD:
../prds/prd-customer-segmentation-use-case-activities.md - Anchor PRD:
../prds/prd-customer-segmentation-anchor.md - External BE RFC: Confluence (not mirrored here) —
DRAFT RFC — Customer Segmentation based on Basic Attributes (Backend)(submitted 2026-05-19, related to TF-2544) - Figma (Basic Attributes): https://www.figma.com/design/ZbjJxiiEsyFIBPCsTOK0lm/Customer-Segmentations?node-id=8670-238601
Assumptions
- BE APIs listed in Detail 2.4 are available or in active development; this RFC targets the API contract from
SegmentStore.ts(already calling/v1/segments). contact-service.qontak.comhosts all segment endpoints (CONTACT_SERVICE_SERVICE_APIin production config).- Figma for Use-Case Activities FE surfaces (association conditions, aggregate metrics, performance tab) will be designed before those chunks execute; see §5 Open Questions.
cdp_segmentation_enabledfeature flag (already gatingpages/customers/segments/[id].vue) controls rollout for the entire feature.- Package tier (Plus / Ultimate / 360) and per-CID segment limits are enforced by the BE; the FE surfaces the 409 error message from the API.
Dependencies
| Dependency | Status | Blocking? | Notes |
|---|---|---|---|
BE segment CRUD API (/v1/segments) | exists — SegmentStore.ts already calls it | NO | Existing in repo |
BE segment preview API (/v1/segments/preview) | exists — SegmentStore.ts calls it | NO | |
BE segment detail with total_matched, percentage_reach, reachability fields | needs-building — not yet in SegmentStore.ts | YES for Segment Detail | Needs BE to expose reachability data |
| BE Performance tab data API (growth chart, source/domicile/age/gender) | needs-building | YES for Performance tab | API path TBD — see §5 |
| BE aggregate metrics and recency condition support | needs-building | YES for Use-Case Activities | Part of external BE RFC |
| Broadcast API for Send Campaign (create recipient list from segment) | needs-building | YES for Send Campaign | BROADCAST_API_BASE_URL = api.mekari.com |
cdp_segmentation_enabled feature flag | exists (FeatureFlagStore.ts) | NO | Already in prod |
| Design — Figma for Use-Case Activities surfaces | TBD | YES for association/metrics/perf chunks | See §5 Open Questions |
Design References (frontend-specific — required)
| PRD-named surface | Figma / design link | Frame name | Design system version | Design QA contact | Notes |
|---|---|---|---|---|---|
| Segment List page (Index) | https://www.figma.com/design/ZbjJxiiEsyFIBPCsTOK0lm/Customer-Segmentations?node-id=8670-238601 | Segment List | @mekari/pixel3 (latest) | Nur Asmara / Rizky Surur (from Basic Attributes PRD) | Only node root linked; agent must navigate to specific frames |
| Segment Builder — step 1 (Segment Info) | same Figma file | Segment Info step | @mekari/pixel3 | same | |
| Segment Builder — step 2 (Rule Builder) | same Figma file | Rule Builder | @mekari/pixel3 | same | Includes nested filter UI |
| Preview Customer panel | same Figma file | Preview panel | @mekari/pixel3 | same | Right-side panel after "Preview customers" click |
| Segment Detail — Active Overview | same Figma file | Segment Detail Active | @mekari/pixel3 | same | Two-column layout with info + customer table |
| Segment Detail — Archived Overview | same Figma file | Segment Detail Archived | @mekari/pixel3 | same | No reachability section |
| Segment in Customer Index sub-sidenav | same Figma file | Customer Index Segments | @mekari/pixel3 | same | Left sub-sidenav segment list |
| Association conditions (Conversations/Campaigns/Company/Deals/Tickets) | n/a — design pending | TBD | @mekari/pixel3 | TBD | Blocker for Use-Case Activities chunks — see §5 |
| Aggregate metrics conditions | n/a — design pending | TBD | @mekari/pixel3 | TBD | Blocker |
| Segment Detail — Performance tab | n/a — design pending | TBD | @mekari/pixel3 | TBD | Blocker |
| Send Campaign sidebar (channel selector) | n/a — design pending | TBD | @mekari/pixel3 | TBD | Blocker |
Agent must NOT start chunks for "n/a — design pending" surfaces until frames are confirmed.
Detail 1.A — PRD Traceability Matrix
All story IDs from Basic Attributes PRD are prefixed BA-S (no explicit ids in that PRD; mapped from Jira refs). Use-Case Activities story IDs are as defined in that PRD (SEG-Sxx).
Forward (PRD AC → RFC):
| PRD composite AC id | RFC section | Component / file |
|---|---|---|
| BA-S01/AC-1 | §4.C Chunk 1 | pages/customers/index.vue — Settings icon entry point |
| BA-S01/AC-2 | §2.A SegmentListPage | features/customers/views/SegmentListPage.vue |
| BA-S01/AC-3 | §2.A SegmentListPage | SegmentStore.ts fetchSegments search param |
| BA-S01/AC-4,5 | §2.A SegmentListPage | SegmentStore.ts created_at_from/to query params |
| BA-S01/AC-6 | §2.A DrawerCreateSegment | DrawerCreateSegment.vue |
| BA-S01/AC-7..10 | §2.A SegmentListPage | SegmentListPage.vue actions menu |
| BA-S02/AC-1..4 | §4.C Chunk 2 | features/customers/views/ListPage.vue (sub-sidenav section) |
| BA-S03/AC-1..3 | §2.A DrawerCreateSegment | DrawerCreateSegment.vue segment info step |
| BA-S04/AC-1..7 | §2.A FilterCriteriaRow | FilterCriteriaRow.vue + DrawerCreateSegment.vue |
| BA-S05/AC-1..5 | §2.A FilterCriteriaRow | FilterCriteriaRow.vue (fieldType dispatches to correct input) |
| BA-S06/AC-1..3 | §2.A FilterCriteriaRow | same — dropdown_select fieldType |
| BA-S07/AC-1..12 | §2.A FilterCriteriaRow | FilterCriteriaRow.vue custom field types |
| BA-S08/AC-1..4 | §2.A FilterCriteriaRow | eventsItems.ts + FilterCriteriaRow.vue |
| BA-S09/AC-1..5 | §2.A FilterCriteriaRow | communication consent fields (boolean fieldType) |
| BA-S10/AC-1..5 | §2.A FilterCriteriaRow | loyalty fields (nice-to-have) |
| BA-S11/AC-1..3 | §2.A DrawerCreateSegment | nested filter row AddFilter in same criteria group |
| BA-S12/AC-1..3 | §2.A DrawerCreateSegment | multiple criteria groups (max MAX_FILTER_GROUPS=2) |
| BA-S13/AC-1..4 | §2.A SegmentListPage + SegmentStore | SegmentStore.ts preview response — total vs visible |
| BA-S14/AC-1..2 | §2.A DrawerCreateSegment | Preview customer panel |
| BA-S15/AC-1..6 | §2.A SegmentListPage | SegmentListPage.vue detail view |
| BA-S16/AC-1..4 | §2.A SegmentListPage | archived segment detail |
| BA-S17/AC-1..5 | §4.C Chunk 6 | PerformanceTab.vue (new) |
| BA-S18/AC-1..9 | §4.C Chunk 5 | SendCampaignDrawer.vue (new) |
| BA-S19/AC-1..4 | §4.C Chunk 7 | Permission gating across components |
| SEG-S01/AC-1..10 | §2.A FilterCriteriaRow | extends existing field type handling |
| SEG-S02/AC-1..8 | §4.C Chunk 3 | AssociationCondition.vue (new) |
| SEG-S03/AC-1..7 | §4.C Chunk 3 | AssociationCondition.vue (new) |
| SEG-S04/AC-1..3 | §4.C Chunk 3 | AssociationCondition.vue (new) |
| SEG-S05/AC-1..7 | §4.C Chunk 3 | AssociationCondition.vue (new) |
| SEG-S06/AC-1..7 | §4.C Chunk 3 | AssociationCondition.vue (new) |
| SEG-S07/AC-1..3 | §2.A DrawerCreateSegment | same as BA-S11 |
| SEG-S08/AC-1..3 | §2.A DrawerCreateSegment | same as BA-S12 |
| SEG-S10/AC-1..2 | §2.A DrawerCreateSegment | same as BA-S14 |
| SEG-S11/AC-1..6 | §2.A SegmentListPage | same as BA-S15 + reachability |
| SEG-S12/AC-1..4 | §2.A SegmentListPage | same as BA-S16 |
| SEG-S13/AC-1..5 | §4.C Chunk 6 | PerformanceTab.vue (new) |
| SEG-S15/AC-1..4 | §4.C Chunk 7 | permission keys |
UI / Consumer Surface Coverage
| PRD-named surface | Consumer | Required reads (BE endpoint) | Required writes (BE endpoint) | Status surface |
|---|---|---|---|---|
| Segment List page | web | GET /v1/segments | — | status: active/archived badge |
| Segment Builder (Create) | web | GET /v1/customers/fields (field palette), POST /v1/segments/preview | POST /v1/segments | — |
| Segment Builder (Edit) | web | GET /v1/segments/:id, GET /v1/customers/fields, POST /v1/segments/preview | PATCH /v1/segments/:id | — |
| Customer Index sub-sidenav — Segments section | web | GET /v1/segments?status=active&per_page=10 | — | segment name clickable |
| Preview Customer panel | web | POST /v1/segments/preview | — | total_matched count |
| Segment Detail — Active Overview | web | GET /v1/segments/:id/detail | POST /v1/segments/:id/archive | active badge, total_matched, % reach, reachability |
| Segment Detail — Archived Overview | web | GET /v1/segments/:id/detail | — | archived badge, total_matched (no reachability) |
| Segment Detail — Performance tab | web | GET /v1/segments/:id/performance (TBD) | — | growth chart, distributions |
| Send Campaign sidebar | web | GET /v1/segments/:id/reachability | POST /broadcast/v2/recipients (TBD) | audience size by channel |
Role Coverage
| PRD role | UI surface visibility | Action buttons enabled | Auth scope expected from BE | Notes |
|---|---|---|---|---|
customers_segment_view = ALL ACCESS | All segment surfaces visible | Full read | JWT scope | Default for Org Admin |
customers_segment_view = OWNED ONLY | Only own segments visible in list/detail | Read own | JWT scope | Customer list in detail filtered |
customers_segment_view = DISABLED | Segment section hidden | None | — | Forbidden component shown |
customers_segment_add = ALL ACCESS | "Create segment" button visible | Create | JWT scope | |
customers_segment_add = DISABLED | "Create segment" button hidden | None | — | |
customers_segment_manage = ALL ACCESS | Edit action visible for all segments | Edit any | JWT scope | |
customers_segment_manage = OWNED ONLY | Edit action visible for own segments only | Edit own | JWT scope | |
customers_segment_manage = DISABLED | Edit action hidden | None | — | |
customers_segment_archived = ALL ACCESS | Archive action visible for all | Archive any | JWT scope | |
customers_segment_archived = OWNED ONLY | Archive action visible for own only | Archive own | JWT scope | |
customers_segment_archived = DISABLED | Archive action hidden | None | — |
PRD Section Coverage
| PRD | Section # | Title | Where covered |
|---|---|---|---|
| Basic Attributes | All | Key features + operator logic tables | §2.A UI Contract — FilterCriteriaRow dispatch table |
| Basic Attributes | User Stories | AC-1..end | §1 Detail 1.A traceability + §4.C execution plan |
| Use-Case Activities | §1 | One-liner + Problem | §1 Overview |
| Use-Case Activities | §4 | Non-Goals | §1 Out of Scope |
| Use-Case Activities | §6 | Constraints | §3 Performance Requirements |
| Use-Case Activities | §11 | Feature Changes (enhancement) | §4.C Execution Plan Chunk 3 |
| Use-Case Activities | §13.2 | Operator Logic Reference | §2.A UI Contract — AssociationCondition |
| Use-Case Activities | §13.2.6 | Aggregate Metrics | §4.C Chunk 4 |
| Use-Case Activities | §13.3 | User Stories SEG-S01..S15 | §1 Detail 1.A + §4.C |
Detail 1.B — Decisions Closed
| Decision | Chosen option | Alternatives rejected | Why rejected |
|---|---|---|---|
| Segment builder UX pattern | Drawer (multi-step) with inline condition rows | Modal / full-page form | Drawer is already the implemented pattern (DrawerCreateSegment.vue); reverting wastes work |
| State management for rule builder | Pinia SegmentStore (existing) | Local component ref state | Store already persists filterCriteria + ruleSetOperator; enables cross-component access |
| Condition palette extension for associations | New AssociationCondition.vue component alongside existing FilterCriteriaRow.vue | Extend FilterCriteriaRow with association flag | Keeps concerns separate; FilterCriteriaRow handles simple field conditions, AssociationCondition handles group-level association queries |
| Data fetching pattern | useCustomFetch() composable ($customFetch) called from Pinia actions | Vue Query / SWR | Entire existing codebase uses $customFetch; no alternative considered for consistency |
| Design system | @mekari/pixel3 | no alternative considered — team standard | |
| Feature flag | cdp_segmentation_enabled (existing) | No new flag needed at this phase | Flag is already wiring the /customers/segments/[id] route |
| Figma-to-code ownership (Use-Case Activities) | Blocked — no frames yet | n/a | See §5 Open Questions |
Detail 1.C — Per-Story Change Map
| Story id | Story title | Layer scope | Changes (concrete FE artifacts) | Composite AC ids covered | Acceptance criteria (verifiable) | RFC anchors |
|---|---|---|---|---|---|---|
| BA-S01 | Segment Index Menu — List Page | FE-only | SegmentListPage.vue search + date filter + actions menu; SegmentStore.fetchSegments with search/date params | BA-S01/AC-1..10 | vitest: SegmentListPage renders with mock segments, search fires fetchSegments with param, archive action triggers archiveSegment | §2.A · §4.C Chunk 1 |
| BA-S02 | Segments in Customer Index sub-sidenav | FE-only | ListPage.vue — add Segments section to left sub-sidenav; SegmentStore.fetchSegments({ per_page:10 }) on mount | BA-S02/AC-1..4 | vitest: Segments section visible; empty state shows "Create segment"; segment item click navigates to detail | §2.A · §4.C Chunk 2 |
| BA-S03 | Segment Info (Name + Description) | FE-only | DrawerCreateSegment.vue — step 1 form (name required/max-60, description optional/max-250); vee-validate schema | BA-S03/AC-1..3 | vitest: name required validation fails on empty; max-60 enforced; Cancel clears without save | §2.A · §4.C Chunk 1 |
| BA-S04 | Rule Builder (AND/OR, limits) | FE-only | DrawerCreateSegment.vue — condition group logic; filterFieldConditions.ts MAX_FILTER_GROUPS=2, MAX_NESTED_RULES=3; AND/OR toggle | BA-S04/AC-1..7 | vitest: add 3 nested rows → 4th "Add Filter" disabled; add 2 groups → 3rd "Add Rule" disabled; AND/OR toggle changes ruleSetOperator in store | §2.A · §4.C Chunk 1 |
| BA-S05 | Rule Builder — Default Fields by Qontak | FE-only | FilterCriteriaRow.vue dispatches to correct value input by fieldType; text/dropdown/date field types | BA-S05/AC-1..5 | vitest: text field renders FilterTextInput; dropdown field renders FilterDropdown; date renders FilterDateInput; "is empty" operator hides value input | §2.A · §4.C Chunk 1 |
| BA-S06 | Rule Builder — Custom Fields by Qontak (Source/Status) | FE-only | FilterCriteriaRow.vue — dropdown_select fieldType (same dispatch as BA-S05) | BA-S06/AC-1..3 | Same vitest as BA-S05 for dropdown_select with fieldCategory === 'custom_qontak' | §2.A · §4.C Chunk 1 |
| BA-S07 | Rule Builder — Custom Fields by User | FE-only | FilterCriteriaRow.vue — all custom field types (text_area, multiple_select, number, date, url, upload, signature, gps) | BA-S07/AC-1..12 | vitest: multiple_select renders FilterMultiselect; number renders FilterNumberInput; upload/signature/gps render is_empty/is_not_empty only | §2.A · §4.C Chunk 1 |
| BA-S08 | Rule Builder — Basic Events | FE-only | FilterCriteriaRow.vue — fieldCategory === 'event'; eventsItems.ts definitions; date + dropdown_select dispatching | BA-S08/AC-1..4 | vitest: event category shows created_at/updated_at (FilterDateInput), created_by/updated_by (FilterDropdown) | §2.A · §4.C Chunk 1 |
| BA-S09 | Rule Builder — Communication Consent | FE-only | FilterCriteriaRow.vue — boolean fieldType → new FilterBoolean.vue component (true/false dropdown); single_line_text + date fields | BA-S09/AC-1..5 | vitest: boolean fieldType renders true/false dropdown; "is empty" shows correctly | §2.A · §4.C Chunk 1 |
| BA-S10 | Rule Builder — Member Loyalty (nice-to-have) | FE-only | FilterCriteriaRow.vue — fieldCategory === 'loyalty'; date/text/number types | BA-S10/AC-1..5 | vitest: loyalty fields dispatch to correct input types | §2.A · §4.C Chunk 1 |
| BA-S11 | Nested Filter in Same Criteria | FE-only | DrawerCreateSegment.vue — "Add Filter" in same criteria group; CriteriaItem.operator (AND/OR) between rows; MAX_NESTED_RULES enforcement | BA-S11/AC-1..3 | vitest: add 2nd filter to same group → operator chip appears; remove filter → operator removed; 3rd filter → Add Filter disabled | §2.A · §4.C Chunk 1 |
| BA-S12 | Advanced Criteria Builder (Multiple Filter Groups) | FE-only | DrawerCreateSegment.vue — "Add Rule" creates second group; MAX_FILTER_GROUPS=2; remove group removes all nested filters | BA-S12/AC-1..3 | vitest: 2 groups rendered with top-level AND/OR selector; remove group removes it entirely | §2.A · §4.C Chunk 1 |
| BA-S13 | Role-Based Visibility + Full Dataset Execution | FE + BE existing | SegmentListPage.vue + SegmentStore — preview shows total_matched from response (not filtered by permission); customer list in detail filtered by view_customer scope | BA-S13/AC-1..4 | vitest: total_matched shows API value (1000); customer list table renders API-returned filtered rows | §2.C UI State Matrix · §4.C Chunk 1 |
| BA-S14 | Preview Customer | FE + BE existing | DrawerCreateSegment.vue — "Preview customers" button triggers SegmentStore.previewSegment; Preview panel right-side with total + sample list | BA-S14/AC-1..2 | vitest: preview panel opens with matched count; empty state shows "No customers match" | §2.A · §2.B · §4.C Chunk 1 |
| BA-S15 | Segment Detail — Active Overview | FE + BE existing | SegmentListPage.vue — info section (status, name, description, filter pattern); data summary (total_matched, %reach, last_updated, reachability WhatsApp+Email); customer matched table | BA-S15/AC-1..6 | vitest: all detail fields render from mock segment detail API; reachability % calculated correctly | §2.A · §2.E · §4.C Chunk 1 |
| BA-S16 | Segment Detail — Archived Overview | FE + BE existing | SegmentListPage.vue archived state — same as BA-S15 but no reachability section | BA-S16/AC-1..4 | vitest: archived segment hides reachability section | §2.A · §4.C Chunk 1 |
| BA-S17 | Segment Detail — Performance (nice-to-have) | FE + BE new | PerformanceTab.vue (new) — line chart (growth), pie/bar charts (source, domicile, age, gender) | BA-S17/AC-1..5 | vitest: PerformanceTab renders charts from mock API data; empty state shown when no data; time filter updates chart | §4.C Chunk 6 — blocked by Figma + BE API |
| BA-S18 | Utilize Segments for Send Campaign | FE + BE new | SendCampaignDrawer.vue (new) — channel selector (WhatsApp/Email); audience size display; redirect to Broadcast | BA-S18/AC-1..9 | vitest: WhatsApp selected shows phone-count; 0-reachable disables Continue; Continue redirects to Broadcast URL with segment | §4.C Chunk 5 — blocked by Figma + Broadcast API contract |
| BA-S19 | Permission Keys for Segmentation | FE-only | All segment components — customers_segment_add/manage/archived gating; UserStore.hasAssociatedAccess() calls | BA-S19/AC-1..4 | vitest: with DISABLED permission, Create/Edit/Archive buttons absent | §4.C Chunk 7 |
| SEG-S01 | Customer Properties/Fields (extension) | FE-only | FilterCriteriaRow.vue — already covers all field types from BA-S05..BA-S09; no new component needed | SEG-S01/AC-1..10 | same as BA-S05..09 | §2.A |
| SEG-S02 | Associations — Conversations | FE + BE new | AssociationCondition.vue (new); wires associationsGroups.ts CONVERSATIONS group | SEG-S02/AC-1..8 | vitest: Conversations group expands; total_conversations renders FilterNumberInput; date fields render FilterDateInput | §4.C Chunk 3 — blocked by Figma |
| SEG-S03 | Associations — Campaigns | FE + BE new | AssociationCondition.vue — CAMPAIGNS group from associationsGroups.ts | SEG-S03/AC-1..7 | same pattern as SEG-S02 | §4.C Chunk 3 |
| SEG-S04 | Associations — Company | FE + BE new | AssociationCondition.vue — COMPANIES group | SEG-S04/AC-1..3 | same pattern | §4.C Chunk 3 |
| SEG-S05 | Associations — Deals | FE + BE new | AssociationCondition.vue — DEALS group | SEG-S05/AC-1..7 | same pattern | §4.C Chunk 3 |
| SEG-S06 | Associations — Tickets | FE + BE new | AssociationCondition.vue — TICKETS group | SEG-S06/AC-1..7 | same pattern | §4.C Chunk 3 |
| SEG-S07 | Nested Filter in Same Criteria | FE-only | same as BA-S11 | SEG-S07/AC-1..3 | same | §2.A |
| SEG-S08 | Advanced Criteria Builder | FE-only | same as BA-S12 | SEG-S08/AC-1..3 | same | §2.A |
| SEG-S10 | Preview Customer | FE + BE existing | same as BA-S14 | SEG-S10/AC-1..2 | same | §2.A |
| SEG-S11 | Segment Detail — Active Overview | FE + BE existing | same as BA-S15 (extends with reachability from SEG-S11) | SEG-S11/AC-1..6 | same | §2.A |
| SEG-S12 | Segment Detail — Archived Overview | FE + BE existing | same as BA-S16 | SEG-S12/AC-1..4 | same | §2.A |
| SEG-S13 | Segment Detail — Performance | FE + BE new | same as BA-S17 | SEG-S13/AC-1..5 | same | §4.C Chunk 6 |
| SEG-S15 | Permission Keys | FE-only | same as BA-S19 | SEG-S15/AC-1..4 | same | §4.C Chunk 7 |
2. Technical Design
Detail 2.0 — Repo Reading Guide (read this first)
Repo Map (mermaid)
flowchart LR
subgraph pages["pages/customers/"]
seg_idx["index.vue (entry)"]
seg_list["ListPage.vue"]
seg_detail["segments/[id].vue"]
end
subgraph views["features/customers/views/"]
SegmentListPage["SegmentListPage.vue"]
ListPage["ListPage.vue"]
subgraph seg_comps["components/segment/"]
DrawerCreate["DrawerCreateSegment.vue"]
DrawerEdit["DrawerEditSegment.vue"]
FilterRow["FilterCriteriaRow.vue"]
Inputs["Filter*Input.vue (Text/Number/Date/Dropdown/Multiselect/Url)"]
AssocCond["AssociationCondition.vue (new)"]
PerfTab["PerformanceTab.vue (new)"]
SendCamp["SendCampaignDrawer.vue (new)"]
InfoPanel["SegmentInfoPanel.vue"]
DetailTabs["SegmentDetailTabs.vue"]
end
end
subgraph store["features/customers/store/"]
SegStore["SegmentStore.ts"]
UserStore["UserStore.ts"]
end
subgraph data_files["components/segment/ (data)"]
AssocGroups["associationsGroups.ts"]
FilterConds["filterFieldConditions.ts"]
EventItems["eventsItems.ts"]
Types["types.ts"]
end
subgraph api["contact-service.qontak.com"]
BE_API["POST/GET /v1/segments/*"]
end
seg_detail --> SegmentListPage
SegmentListPage --> DrawerCreate
SegmentListPage --> DrawerEdit
SegmentListPage --> PerfTab
SegmentListPage --> SendCamp
DrawerCreate --> FilterRow
DrawerCreate --> AssocCond
FilterRow --> Inputs
FilterRow --> FilterConds
AssocCond --> AssocGroups
SegStore --> BE_API
SegmentListPage --> SegStore
DrawerCreate --> SegStore
Existing Code Anchors
| Path | Why the agent reads it | What pattern it teaches |
|---|---|---|
features/customers/store/SegmentStore.ts | All segment API calls, state shape, action signatures | $customFetch pattern, Pinia defineStore, CriteriaItem → SegmentRuleSet mapper |
features/customers/views/SegmentListPage.vue | Main segment list + detail view implementation (in-progress) | Two-column layout, SegmentBreadcrumb, archive/edit flow |
features/customers/views/components/segment/types.ts | CriteriaItem type definition | fieldType, fieldCategory, condition, operator shape sent to API |
features/customers/views/components/segment/filterFieldConditions.ts | Condition constants, max limits | MAX_FILTER_GROUPS=2, MAX_NESTED_RULES=3, per-field-type condition arrays |
features/customers/views/components/segment/associationsGroups.ts | Existing association field catalogue | 5 groups (Conversations/Campaigns/Companies/Tickets/Deals) with id, name, field_type |
features/customers/views/components/segment/FilterCriteriaRow.vue | Core condition row — MpPopover for condition picker, field-type dispatch | Pixel3 MpPopover/MpPopoverList usage; emit('update:criteria') pattern |
features/customers/views/components/segment/DrawerCreateSegment.vue | Multi-step segment creation drawer | Step control, criteria group rendering, "Add Filter"/"Add Rule" logic |
features/customers/store/UserStore.ts | hasAssociatedAccess(permKey) method | How permission keys gate UI |
common/composables/useCustomFetch.ts | $customFetch API client | Nuxt $fetch wrapper with retry/token-refresh |
pages/customers/segments/[id].vue | Route entry for segment detail | cdp_segmentation_enabled feature flag guard, customers_segment_view permission check |
Patterns to Follow
| Concern | Pattern in repo | Reference file | Deviation in this RFC? |
|---|---|---|---|
| State management | Pinia defineStore with typed state + actions | SegmentStore.ts | none |
| API calls | useCustomFetch().then.$customFetch(path, opts) inside Pinia actions | SegmentStore.ts L187, L218, L241 | none |
| Styling | @mekari/pixel3 components + css() utility from @mekari/pixel3-postcss | FilterCriteriaRow.vue L1-80 | none |
| Error / toast | toastNotify() from ~/utils/toast called in catch blocks | SegmentStore.ts L326-328 | none |
| Permission gating | userStore.hasAssociatedAccess(permKey) → computed canXyz → :disabled/v-if | SegmentListPage.vue L509; [id].vue L46 | none |
| Feature flag | featureFlagStore.featureFlags.<flag> in route middleware | [id].vue L21 | none |
| Folder convention | Feature module: features/<feature>/views/components/<sub>/ | features/customers/views/components/segment/ | none |
Reading Order for the Agent
features/customers/views/components/segment/types.ts— understandCriteriaItemshape (the central data model)features/customers/views/components/segment/filterFieldConditions.ts— understand condition constants and limitsfeatures/customers/views/components/segment/associationsGroups.ts— understand association field cataloguefeatures/customers/store/SegmentStore.ts— understand all existing API calls, state, andbuildCriteriaFromRuleSetmapperfeatures/customers/views/components/segment/FilterCriteriaRow.vue— understand condition row rendering patternfeatures/customers/views/components/segment/DrawerCreateSegment.vue— understand multi-step creation flowfeatures/customers/views/SegmentListPage.vue— understand segment list + detail two-column layoutfeatures/customers/store/UserStore.ts— understandhasAssociatedAccesspermission methodcommon/composables/useCustomFetch.ts— understand$customFetchinstantiationpages/customers/segments/[id].vue— understand route entry, feature flag guard
Source Verification (anti-hallucination)
| Anchor / pattern / contract | Verified by | Evidence |
|---|---|---|
features/customers/store/SegmentStore.ts | read | defineStore('segment', ...) at L1; $customFetch('/v1/segments', ...) at L196; buildCriteriaFromRuleSet at L90 |
features/customers/views/components/segment/types.ts | read | exports CriteriaItem interface with fieldType, fieldCategory, condition, operator at L39-51 |
features/customers/views/components/segment/filterFieldConditions.ts | read | MAX_FILTER_GROUPS = 2 at L1; MAX_NESTED_RULES = 3 at L2; DROPDOWN_SELECT_CONDITIONS at L8; DATE_FIELD_CONDITIONS at L28 |
features/customers/views/components/segment/associationsGroups.ts | read | exports associationsGroups array with 5 groups (CONVERSATIONS/CAMPAIGNS/COMPANIES/TICKETS/DEALS) starting L1 |
features/customers/views/components/segment/FilterCriteriaRow.vue | read | <MpPopover> condition picker at L22; field-type dispatch v-if="criteria.fieldType === 'dropdown_select'" at L58 |
pages/customers/segments/[id].vue | read | cdp_segmentation_enabled middleware check at L21; hasAssociatedAccess('customers_segment_view') at L46 |
features/customers/views/SegmentListPage.vue | read | handleSendCampaign() stub at L267-269 (// TODO: open send campaign flow); customers_segment_view perm check at L509 |
common/composables/useCustomFetch.ts | read (agent: confirmed via SegmentStore.ts L187 useCustomFetch()) | useCustomFetch() returns { $customFetch } used throughout SegmentStore |
| API base URL for contact-service | read configs/production.json | "CONTACT_SERVICE_SERVICE_API": "https://contact-service.qontak.com" |
Design ↔ Code Mapping
| Figma frame / component | Implementing file | Reuse vs new | Design tokens used | Deviation from design |
|---|---|---|---|---|
| Segment List | features/customers/views/SegmentListPage.vue | reused (in progress) | color.text.default, space.3, border.default | none — pixel-faithful |
| Segment Builder step 1 (Segment Info) | DrawerCreateSegment.vue — step 1 | extended | color.text.secondary, space.4 | none |
| Segment Builder step 2 (Rule Builder) | DrawerCreateSegment.vue — step 2 | extended | background.neutral.subtle, border.md | none |
| Preview panel | DrawerCreateSegment.vue — preview right panel | extended | color.text.secondary, space.3 | none |
| Segment Detail — Active Overview | SegmentListPage.vue detail section | extended | color.text.default, space.4 | none |
| Segment Detail — Archived Overview | SegmentListPage.vue archived variant | extended | same | none |
| Customer Index sub-sidenav | ListPage.vue left sidebar section | new | background.neutral.subtle | none |
| Association conditions | AssociationCondition.vue | new | TBD — pending Figma | n/a — design pending |
| Performance tab | PerformanceTab.vue | new | TBD | n/a — design pending |
| Send Campaign sidebar | SendCampaignDrawer.vue | new | TBD | n/a — design pending |
Detail 2.1 — Architecture (mermaid)
Component diagram
flowchart TB
user([User]) --> seg_detail_page["pages/customers/segments/[id].vue"]
user --> customers_index["pages/customers/index.vue"]
seg_detail_page --> SegmentListPage["SegmentListPage.vue"]
customers_index --> ListPage["ListPage.vue (segments sub-sidenav)"]
SegmentListPage --> DrawerCreate["DrawerCreateSegment.vue"]
SegmentListPage --> DrawerEdit["DrawerEditSegment.vue"]
SegmentListPage --> PerfTab["PerformanceTab.vue (new)"]
SegmentListPage --> SendCamp["SendCampaignDrawer.vue (new)"]
DrawerCreate --> FilterRow["FilterCriteriaRow.vue"]
DrawerCreate --> AssocCond["AssociationCondition.vue (new)"]
FilterRow --> FilterInputs["Filter*Input.vue variants"]
AssocCond --> FilterInputs
SegmentListPage --> SegStore[("SegmentStore (Pinia)")]
DrawerCreate --> SegStore
DrawerEdit --> SegStore
SegStore --> customFetch["$customFetch (useCustomFetch)"]
customFetch --> ContactService["contact-service.qontak.com"]
State machine (Segment lifecycle)
stateDiagram-v2
[*] --> building : user opens Create Segment
building --> previewing : click Preview customers
previewing --> building : adjust filters
building --> active : save segment
active --> editing : Edit Segment action
editing --> active : save changes
active --> archived : Archive action confirmed
archived --> [*]
Detail 2.2 — Sequence (mermaid)
Happy path — User creates a segment and previews
sequenceDiagram
actor U as User
participant FE as DrawerCreateSegment
participant Store as SegmentStore
participant API as contact-service
U->>FE: Open Create Segment drawer
U->>FE: Fill name + description (step 1)
U->>FE: Add condition row (step 2)
U->>FE: Click "Preview customers"
FE->>Store: previewSegment(criteria)
Store->>API: POST /v1/segments/preview
alt 2xx
API-->>Store: total_matched + sample[]
Store-->>FE: preview state resolved
FE-->>U: Preview panel shows count + sample list
else 422 invalid rule
API-->>Store: 422 validation error
Store-->>FE: preview rejected
FE-->>U: inline error on invalid condition row
else timeout after 10s
API--xStore: no response
Store-->>FE: preview rejected
FE-->>U: toast "Preview timed out — try again"
end
U->>FE: Click "Save Segment"
FE->>Store: createSegment(name, description, rule_set)
Store->>API: POST /v1/segments
alt 201 created
API-->>Store: segment id
Store-->>FE: createStatus resolved
FE-->>U: drawer closes, navigateTo segment detail
else 409 segment limit reached
API-->>Store: 409
Store-->>FE: createStatus rejected
FE-->>U: toast "Segment limit reached"
end
Failure path — Archive segment with permission check
sequenceDiagram
actor U as User
participant FE as SegmentListPage
participant Store as SegmentStore
participant API as contact-service
U->>FE: Click "Archive" in More Actions
FE->>FE: check customers_segment_archived permission
alt permission DISABLED
FE-->>U: Archive action hidden
else permission OK
FE-->>U: Show ModalArchiveSegment
U->>FE: Confirm archive
FE->>Store: archiveSegment(id)
Store->>API: POST /v1/segments/:id/archive
alt 200 OK
API-->>Store: success
Store-->>FE: navigateTo /customers
else 403 forbidden
API-->>Store: 403
Store-->>FE: rejected
FE-->>U: toast "You don't have permission to archive this segment"
end
end
Detail 2.3 — Database Model
N/A for this frontend RFC. Client-side persistence is limited to:
- Pinia in-memory state:
SegmentStorestate (segments list, filter criteria, preview result) — no localStorage persistence; cleared on page refresh. - No IndexedDB or cookie persistence introduced.
Detail 2.4 — APIs Consumed
| Method | Path | Status | Contract authority | Notes |
|---|---|---|---|---|
| GET | /v1/segments | exists | SegmentStore.ts L314 | Paginated list; params: page, per_page, search, status, created_at_from, created_at_to |
| POST | /v1/segments | exists | SegmentStore.ts L196 | Create segment; body: { name, description, rule_set } |
| GET | /v1/segments/:id | exists | SegmentStore.ts L243 | Returns SegmentDetail with rule_set; used for edit load |
| PATCH | /v1/segments/:id | exists | SegmentStore.ts L274 | Update segment name/description/rule_set |
| POST | /v1/segments/preview | exists | SegmentStore.ts L220 | Returns { total_matched, total_customer_base, sample[] } |
| POST | /v1/segments/:id/archive | exists | SegmentStore.ts L338 | Archives a segment |
| POST | /v1/segments/:id/duplicate | exists | SegmentStore.ts L360 | Duplicates segment; returns new segment id |
| GET | /v1/segments/:id/detail | needs-building | BE RFC (external) | Detail with total_matched, percentage_reach, last_updated, reachability.whatsapp, reachability.email |
| GET | /v1/segments/:id/performance | needs-building | BE RFC (external) — TBD | Growth chart data + source/domicile/age/gender distributions |
| POST | /broadcast/v2/recipients (TBD) | needs-building | Broadcast squad | Create recipient list from segment before redirect |
Detail 2.A — UI Contract
FilterCriteriaRow.vue — single condition row
- Props:
criteria: CriteriaItem(fromtypes.ts) - Emits:
update:criteria(updated: CriteriaItem),remove - Field-type dispatch (field → component):
fieldType | Renders |
|---|---|
single_line_text, text_area, url | FilterTextInput.vue / FilterUrlInput.vue |
dropdown_select | FilterDropdown.vue |
multiple_select | FilterMultiselect.vue |
number | FilterNumberInput.vue |
date | FilterDateInput.vue |
boolean | FilterBoolean.vue (new — true/false dropdown) |
upload, signature, gps | condition-only row (is_empty/is_not_empty) — no value input |
- Condition picker:
MpPopovershowing condition list fromfilterFieldConditions.tskeyed byfieldType. - "is empty" / "is not empty": hides value input component, passes
value: ''to store. - A11y:
aria-labelon condition trigger; focus returned to trigger on popover close.
AssociationCondition.vue (new) — association group condition row
- Props:
group: AssociationGroup(label + items fromassociationsGroups.ts),criteria: CriteriaItem - Emits:
update:criteria(updated: CriteriaItem),remove - Renders a grouped dropdown to select the association field; then delegates value input to same
Filter*Input.vuecomponents based on the selected item'sfield_type. - State: local
selectedFieldref drives the value input display. - Analytics event:
segment.association_condition_added(field:group.label + item.id).
DrawerCreateSegment.vue — multi-step create/edit drawer
- Props:
isOpen: boolean,segmentId?: string(for edit mode) - Emits:
close,saved(segmentId: string) - Steps: 1 = Segment Info (name/description); 2 = Rule Builder + Preview.
- Condition limit enforcement: disables "Add Filter" when
conditionsInGroup.length >= MAX_NESTED_RULES; disables "Add Rule" (new group) whengroups.length >= MAX_FILTER_GROUPS. - Slot: Preview panel slides in from the right side of step 2 on "Preview customers" click.
SegmentListPage.vue — detail view (existing, extended)
- Props:
segmentId: string - Tabs (via
SegmentDetailTabs.vue): Overview | Performance - Overview tab shows:
SegmentInfoPanel.vue(left column): status badge, name, description, filter pattern (rule_set summary)- Right column: total_matched (absolute), % reach, last_updated timestamp, reachability bars (WhatsApp %, Email %)
- Customer matched table: name, source, last_activity, added_by
- Performance tab: renders
PerformanceTab.vue(new); see §4.C Chunk 6.
Detail 2.B — Data-Fetching Strategy
- Library: Nuxt 3 built-in
$fetchwrapped byuseCustomFetch()composable; called from Pinia actions — no Vue Query / SWR. - Cache key: none (stateless
$fetch); Pinia state serves as the in-memory cache. - TTL / refetch triggers: Segment list refetches on page mount and after create/archive/duplicate actions; segment detail refetches on route load.
- Stale-while-revalidate: no — Pinia state is replaced on each fetch.
- Optimistic updates: no — all mutations await API response before updating state.
- Preview debounce: preview button is manual (user-triggered), not debounced continuously; the system flow spec (§13.1) notes "debounced 500ms" for the preview panel update in Use-Case Activities — this applies only if preview is triggered on condition change, which is not the current pattern (it is button-triggered). Open question in §5.
Detail 2.C — UI State Matrix
| Surface | Loading | Empty | Error | Partial | Success |
|---|---|---|---|---|---|
| Segment list | Skeleton rows | "No segments yet. Create your first segment." + CTA | "Failed to load segments. Try again." + Retry | — | Table with segment rows |
| Segment builder — condition field palette | Skeleton in dropdown | — | — | Fields load progressively | Field list rendered |
| Preview panel | Spinner + "Calculating..." | "No customers match your filters." | "Preview failed. Try again." | — | Count + sample list |
| Segment detail overview | Skeleton blocks | — | "Failed to load segment." | — | All sections rendered |
| Segment detail performance | Skeleton charts | "No performance data yet." | "Failed to load performance data." | Charts rendered before all distributions load | All charts rendered |
| Customer matched table | Skeleton rows | "No customers matched for your permission scope." | "Failed to load customers." | — | Table with customer rows |
Detail 2.D — Scope Boundaries
Files to create:
features/customers/views/components/segment/FilterBoolean.vuefeatures/customers/views/components/segment/AssociationCondition.vuefeatures/customers/views/components/segment/PerformanceTab.vuefeatures/customers/views/components/segment/SendCampaignDrawer.vue
Files to modify:
features/customers/store/SegmentStore.ts— addfetchSegmentDetail,fetchPerformance,createRecipientListactions; add typed response interfacesfeatures/customers/views/SegmentListPage.vue— wire Performance tab, Send Campaign handler, reachability datafeatures/customers/views/ListPage.vue— add Segments section to left sub-sidenavfeatures/customers/views/components/segment/DrawerCreateSegment.vue— add AssociationCondition rows, toggle between field vs association typefeatures/customers/views/components/segment/FilterCriteriaRow.vue— addbooleanfield type dispatch toFilterBoolean.vuefeatures/customers/views/components/segment/filterFieldConditions.ts— addBOOLEAN_FIELD_CONDITIONSconstant (is/is_not for true/false, is_empty/is_not_empty)pages/customers/segments/[id].vue— no structural change needed (delegates toSegmentListPage.vue)
Files explicitly NOT touched:
features/customers/store/CustomerStore.ts— no customer store changesfeatures/customers/create/— create customer flow is separatefeatures/customers/detail/— customer detail feature is separate- Any shared component in
common/components/— no modifications to shared components
Shared components touched:
common/components/Forbidden/Forbidden.vue— referenced in[id].vue, not modified
Detail 2.E — State Surface Contract
| Entity | State field / event consumed | Default values | Source endpoint | Stale-tolerance window |
|---|---|---|---|---|
| Segment | `status: 'active' | 'archived'` | 'active' on create | GET /v1/segments/:id/detail |
| Segment | total_matched: number | 0 | GET /v1/segments/:id/detail | Same |
| Segment | percentage_reach: number | 0 | GET /v1/segments/:id/detail | Same |
| Segment | reachability.whatsapp_pct: number | 0 | GET /v1/segments/:id/detail | Same |
| Segment | reachability.email_pct: number | 0 | GET /v1/segments/:id/detail | Same |
| Segment | last_updated: string (ISO-8601) | — | GET /v1/segments/:id/detail | Same |
| Preview result | total_matched, sample[] | empty | POST /v1/segments/preview | 0 s — computed on request |
Detail 2.F — Asset Inventory
| Asset name | Type | Source | Format | Path in repo |
|---|---|---|---|---|
| Chart library (growth line chart, distribution charts) | 3rd-party lib | npm install — TBD (confirm with team: chart.js/vue-chartjs or Pixel3 chart component if available) | JS/CSS bundle | package.json dependency — no static asset |
No new icon, illustration, or image assets identified. All UI icons use
@mekari/pixel3built-in<MpIcon>set.
3. High-Availability & Security
This is a fully client-side rendering feature in an existing Nuxt 3 SPA. No new server-side infrastructure is introduced. Graceful degradation:
- If
GET /v1/segmentsfails on Segment List page → error state with "Failed to load segments. Try again." and a Retry CTA. - If
POST /v1/segments/previewis slow (> 10 s) → timeout state with user-friendly toast. - If
GET /v1/segments/:id/detailfails on detail load → error state with retry. - If Broadcast API is unavailable during Send Campaign → disable Continue button and show "Campaign service unavailable. Try again later."
Performance Requirement
- LCP target: ≤ 2.5 s (per Pixel3 standard; no specific PRD LCP target stated — aligned with §6 Constraints: segment list load ≤ 2 s p95).
- INP target: ≤ 200 ms for condition add/remove actions.
- CLS target: ≤ 0.1 (skeleton screens prevent layout shifts).
- Bundle size budget: no new heavy dependencies introduced beyond a chart library (TBD; target ≤ 50 KB gzip for chart components via dynamic import). Current bundle baseline unknown — agent must measure with
npm run buildand compare. - Code-splitting:
PerformanceTab.vueandSendCampaignDrawer.vueshould be lazy-loaded (defineAsyncComponent) since they are not on the initial render path. - Image strategy: no images introduced; icons via Pixel3 SVG sprite.
- Browser support: Chrome/Edge latest 2, Safari 15+, Firefox latest — per existing app baseline.
- Responsive breakpoints: ≥ 1024 px (desktop-only feature per PRD §6 "web only").
- i18n: all user-facing strings must use
$t()/useI18n()— no hardcoded English strings. Locale keys undercustomers.segments.*namespace (follow existing patterns ini18n/— check existing locale files before naming new keys).
Monitoring & Alerting
-
Analytics events (Mixpanel via
useMixpanel()composable):Event When Properties segment.list.viewedon Segment List page mount — segment.createdon POST /v1/segments 201 { segment_id, field_types[], group_count }segment.preview.triggeredon Preview button click { condition_count, group_count }segment.detail.viewedon Segment Detail load { segment_id, status }segment.archivedon archive confirm { segment_id }segment.send_campaign.initiatedon Send Campaign button click { segment_id, channel }segment.association_condition_addedon AssociationCondition save { group_label, field_id } -
Error monitoring: Sentry (already configured in app) — no new DSN; segment-related errors will surface under existing Sentry project. Severity:
errorfor API 5xx;warningfor 4xx. -
Core Web Vitals: tracked via existing Datadog RUM (
@datadog/browser-rumis a production dependency). -
User-facing success metric: % of active CIDs creating ≥ 1 segment in 30 days post-GA — tracked via Mixpanel
segment.createdevent.
Logging
- Frontend log fields:
console.errorfor caught exceptions in Pinia actions; include{ action, segmentId, error.message }. PII (customer name, phone, email) must NOT be logged — only IDs. - PII removed from logs: customer name/phone/email in
sample[]preview response must not be forwarded to Sentry error context.
Security Implications
- XSS: all user inputs (segment name, description, condition values) are rendered via Vue's default template escaping — no
v-htmlordangerouslySetInnerHTML. - CSRF: N/A — auth via
httpOnlycookie; all requests via$customFetchwhich sets appropriate headers. - Auth token storage: managed by existing
common/composables/useCustomFetch.ts(IAG token refresh pattern) — not changed. - CSP: no new external scripts, fonts, or CDN resources; chart library bundled locally.
- PII: customer name/phone/email appear in preview sample and matched customer table — must not be persisted to localStorage or logged to Sentry. Covered under UU PDP (Indonesian data) as existing customer records — no new PII collection.
- Tenant isolation: enforced by BE via
customers_segment_viewpermission scope — FE trusts the API response filtered by tenant. - Input validation: segment name max 60 chars + required; description max 250 chars; condition values validated per
filterFieldConditions.tstype constraints; number fields reject non-numeric input.
Detail 3.A — Failure Mode Catalog
| API call | 401 | 403 | 404 | 429 | 500 | Timeout (10 s) | Offline | Retry |
|---|---|---|---|---|---|---|---|---|
| GET /v1/segments | Redirect to login | Toast "Permission denied" | — | Toast "Too many requests" | Toast "Failed to load segments" + retry CTA | Toast "Request timed out" | Error state + retry | Manual (retry button) |
| POST /v1/segments/preview | Redirect to login | Toast "Permission denied" | — | Toast "Too many requests" | Toast "Preview failed" | Toast "Preview timed out" | Error state in panel | Manual (Preview button re-click) |
| POST /v1/segments | Redirect to login | Toast "Permission denied" | — | — | Toast "Failed to save segment" | Toast "Timed out saving segment" | Toast "No connection" | Manual (Save button re-click) |
| POST /v1/segments/:id/archive | Redirect | Toast "Permission denied" | Toast "Segment not found" | — | Toast "Failed to archive" | Toast "Timed out" | Toast "No connection" | Manual (retry) |
Detail 3.B — Error Message Catalog
| Error code / scenario | User-facing message (i18n key) | Surface | User-facing? |
|---|---|---|---|
| 409 segment limit | customers.segments.error.limit_reached | Toast | yes |
| 422 invalid condition | customers.segments.error.invalid_condition | Inline on condition row | yes |
| 403 permission denied | common.error.permission_denied | Toast | yes |
| Preview timeout | customers.segments.error.preview_timeout | Toast | yes |
| Save timeout | customers.segments.error.save_timeout | Toast | yes |
| Generic 500 | common.error.server_error | Toast | yes |
Detail 3.C — Accessibility
- WCAG level: AA minimum.
- Keyboard navigation: all popover triggers (
MpPopover) must be reachable via Tab; popover closes on Escape. - Focus management: when
DrawerCreateSegmentopens, focus moves to the first input (name field); when drawer closes, focus returns to the trigger button. - ARIA labels on
MpPopovertriggers:aria-label="Select condition for {fieldName}". - Color contrast: Pixel3 design tokens are WCAG AA compliant by design — no custom color overrides.
prefers-reduced-motion: Pixel3 respects the media query; charts should respect it (confirm with chart library choice).
4. Backwards Compatibility and Rollout Plan
Compatibility
- API contracts: all new endpoints (
/v1/segments/:id/detail,/v1/segments/:id/performance) are additive — no existing contracts changed. - Saved client state: no localStorage/cookie state introduced — no migration needed.
- Old bundle / CDN cache: Nuxt build hash changes on deploy; CDN will serve new bundle to fresh requests. Users mid-session will get a stale bundle until hard refresh — acceptable (feature is gated by feature flag anyway).
Rollout Strategy
- Feature flag:
cdp_segmentation_enabled(existing, already gating the segment detail route). No new flag needed — the entire feature is already behind this flag. - Rollout stages (from Use-Case Activities PRD §7):
Stage Target Evidence to advance Internal Alpha 1-2 internal CDP accounts No critical bugs in 2 days; segment creates/previews work Open Beta All new Qontak One clients Adoption metric trending; no p0 bugs GA All Qontak One clients ≥ 20% CID adoption within 30 days - Stop conditions: > 1% of segment create/preview actions returning 5xx in 15 min window → rollback.
- Rollback: toggle
cdp_segmentation_enabledtofalse— no FE code rollback needed (segment routes redirect to/customers). - Blast radius: only Qontak One users with
cdp_segmentation_enabled = true; no other feature areas touched.
Detail 4.A — Configuration Contract
| Env var / flag | Type | Default | Required | Provisioner |
|---|---|---|---|---|
cdp_segmentation_enabled | Feature flag (boolean) | false | yes (to see feature) | Feature flag service (existing) |
CONTACT_SERVICE_SERVICE_API | Env var (URL) | https://contact-service.qontak.com | yes | Infra / CI env config |
BROADCAST_API_BASE_URL | Env var (URL) | https://api.mekari.com | yes (for Send Campaign) | Infra / CI env config |
Detail 4.B — Test Plan
| Layer | Command (source) | What it must prove |
|---|---|---|
| Unit | npm run test (source: package.json "test": "vitest") | Component mount, store action outcomes, condition dispatch for each fieldType |
| Unit coverage | npm run test:coverage (source: package.json) | Coverage report; target ≥ 80% for new segment components |
| Lint | npm run lint (source: package.json "lint": "eslint .") | No ESLint errors in new/modified files |
| E2E | npx playwright test inside e2e/cli-playwright/ (source: e2e/cli-playwright/ dir; exact spec TBD — see §5) | Golden path: create segment → preview → save → view detail — passes on staging |
| Visual regression | n/a — no automated visual regression configured; rely on Design QA sign-off | n/a |
| Bundle size | npm run build then measure .output/ size (source: package.json "build") | Build succeeds; no bundle size regression > 10% from baseline |
Detail 4.C — Agent Execution Plan
Order matters: finish chunk N (and its AC pass) before starting N+1.
| Order | Chunk | Files to modify/create | Commands to run | Acceptance criteria (verifiable) |
|---|---|---|---|---|
| 1 | Complete Basic Attributes rule builder (all field types + limits + nested groups) | Modify: DrawerCreateSegment.vue, FilterCriteriaRow.vue, filterFieldConditions.ts; Create: FilterBoolean.vue | npm run lint && npm run test -- features/customers/views/components/segment | vitest: all FilterCriteriaRow dispatch cases pass; MAX_FILTER_GROUPS and MAX_NESTED_RULES enforce correctly; FilterBoolean renders true/false dropdown |
| 2 | Segment sub-sidenav in Customer Index | Modify: features/customers/views/ListPage.vue | npm run lint && npm run test -- features/customers/views | vitest: ListPage renders Segments section; fetchSegments called on mount; empty state shows "Create segment" |
| 3 | Association conditions (Conversations / Campaigns / Company / Deals / Tickets) — blocked by Figma | Create: AssociationCondition.vue; Modify: DrawerCreateSegment.vue (add toggle between field vs association), SegmentStore.ts (ensure association fields serialize correctly in rule_set) | npm run lint && npm run test -- features/customers/views/components/segment/AssociationCondition | vitest: AssociationCondition renders all 5 group types; selecting Conversation field dispatches correct Filter*Input; resulting CriteriaItem has fieldCategory='association' |
| 4 | Aggregate metrics + Recency/RFM condition support — blocked by Figma + BE API | Create: AggregateMetricCondition.vue; Modify: DrawerCreateSegment.vue (add 3rd condition type: metric/recency); add metric field definitions to a new metricsGroups.ts constant file | npm run lint && npm run test | vitest: AggregateMetricCondition renders metric groups (Communication/Service/Sales/Marketing/Commerce/Loyalty/Recency); number and percentage field types dispatch correctly |
| 5 | Segment Detail — Overview (reachability + customer table) + Send Campaign drawer — blocked by Figma for Send Campaign | Modify: SegmentListPage.vue (wire reachability % bars, matched customer table columns), SegmentStore.ts (add fetchSegmentDetail action returning full detail with reachability) | npm run lint && npm run test -- features/customers/views/SegmentListPage | vitest: detail view renders total_matched, percentage_reach, whatsapp_pct, email_pct from mock API; archived variant hides reachability; handleSendCampaign opens SendCampaignDrawer |
| 6 | Performance Tab (growth chart + distributions) — blocked by Figma + BE API | Create: PerformanceTab.vue; Modify: SegmentStore.ts (add fetchPerformance action), SegmentListPage.vue (wire Performance tab) | npm run lint && npm run test -- features/customers/views/components/segment/PerformanceTab | vitest: PerformanceTab renders line chart and 4 distribution charts from mock data; time filter change triggers fetchPerformance; empty state shown when data is empty |
| 7 | Permission gating (customers_segment_add/manage/archived) | Modify: SegmentListPage.vue, DrawerCreateSegment.vue, DrawerEditSegment.vue (add userStore.hasAssociatedAccess('customers_segment_add/manage/archived') guards) | npm run lint && npm run test | vitest: Create button hidden when customers_segment_add=DISABLED; Edit action hidden when customers_segment_manage=DISABLED; Archive action hidden when customers_segment_archived=DISABLED |
Detail 4.D — Verification & Rollback Recipe
-
Pre-merge verification commands (agent runs in order):
npm run lintnpm run test:coverage— confirm all new segment component tests passnpm run build— confirm production build succeeds with no TS errors
-
Post-deploy verification signals:
- Mixpanel:
segment.list.viewedevents appearing from internal test accounts aftercdp_segmentation_enabledflag enable - Sentry: segment-related error rate < 1% over 15 min after enabling
- Manual smoke: create 1 segment with 2 conditions → preview shows count → save → verify segment appears in list with correct name
- Mixpanel:
-
Rollback recipe:
- Toggle
cdp_segmentation_enabledtofalsein feature flag service — all/customers/segments/*routes redirect to/customersimmediately (middleware guard in[id].vueL21) - No FE code revert needed unless the flag toggle is insufficient
- Monitor Sentry for 5 minutes post-toggle; confirm segment-related errors drop to zero
- Toggle
5. Concern, Questions, or Known Limitations
| # | Type | Question | Owner | Deadline |
|---|---|---|---|---|
| 1 | Blocker | Figma frames for Use-Case Activities FE surfaces (AssociationCondition, AggregateMetricCondition, PerformanceTab, SendCampaignDrawer) are TBD. Chunks 3, 4, 5 (partial), 6 cannot start implementation without them. | Design (Nur Asmara / Rizky Surur) + PM (Zhelia Alifa) | Before Chunk 3 begins |
| 2 | Blocker | GET /v1/segments/:id/detail response contract (including percentage_reach, reachability.whatsapp_pct, reachability.email_pct) must be agreed with BE before Chunk 5. Currently SegmentStore.ts only calls GET /v1/segments/:id which returns SegmentDetail without reachability. | CDP BE (Julio Jeffer) | Before Chunk 5 |
| 3 | Blocker | GET /v1/segments/:id/performance response contract (growth series shape, distribution shape) must be agreed with BE and Data team before Chunk 6. | CDP BE + Data Team | Before Chunk 6 |
| 4 | Blocker | Send Campaign — Broadcast API contract for creating a recipient list from a segment ID is unknown (POST /broadcast/v2/recipients is a hypothesis from existing config). Need Broadcast squad to confirm path and body. | Broadcast squad | Before Chunk 5 (Send Campaign) |
| 5 | Open | Should preview update automatically on condition change (debounced 500 ms per Use-Case Activities §13.1 step 6), or remain manual button-triggered? Current implementation is manual. Automatic debounced preview would require rethinking state in DrawerCreateSegment.vue and could cause perf issues on large datasets. | PM (Zhelia Alifa) + FE (Azani) | Before Chunk 1 finalization |
| 6 | Open | Chart library for PerformanceTab: does Pixel3 provide a chart component, or do we add chart.js/vue-chartjs? This affects bundle size. Need confirmation from Pixel3 team or design. | FE (Azani) + Design | Before Chunk 6 |
| 7 | Open | Delete segment: SegmentStore.ts has // TODO: implement when DELETE /iag/v1/segments/:id is available at L378. Not in PRD scope; confirm with PM whether delete (vs archive only) is needed. | PM (Zhelia Alifa) | Pre-GA |
| 8 | Known limitation | Segment membership is recalculated daily (08:00 AM). The FE shows last_updated timestamp prominently; users may be confused why segment size doesn't change immediately after editing. A tooltip on the timestamp explaining the daily batch cadence should be added in the Overview section (not in current Figma; Design needs to confirm). | PM + Design | Before Chunk 5 |
| 9 | Known limitation | MAX_FILTER_GROUPS=2 and MAX_NESTED_RULES=3 are enforced client-side. BE must also enforce these limits to prevent API-level abuse; confirm with BE RFC. | CDP BE | Pre-GA |
6. Comment logs
| Date | Comment(s) From | Action Item(s) |
|---|---|---|
| 2026-06-30 | Azani Ramadhan (rfc-starter) | Initial draft created. 6 mermaid blocks validated via manual pitfall scan (mmdc unavailable offline — no internet): semicolons in sequenceDiagram text, unquoted special chars in flowchart labels, [/text/] shape, <> in stateDiagram labels, bare newlines — all clear. One issue fixed: :id colon in stateDiagram-v2 transition label removed. See §7 for agent-execution readiness status. |
7. Ready for agent execution
no
Missing before a full yes:
- §1 Design References — Figma frames for Use-Case Activities surfaces (AssociationCondition, AggregateMetricCondition, PerformanceTab, SendCampaignDrawer) are
n/a — design pending. Chunks 3, 4, 6 and part of 5 cannot execute without them. Chunks 1 and 2 (Basic Attributes completion) are agent-executable now. - Detail 2.4 APIs —
GET /v1/segments/:id/detail(reachability),GET /v1/segments/:id/performance, and Broadcast recipient endpoint contracts areneeds-buildingand not yet agreed with BE/Broadcast squads. Blocks Chunks 5 and 6. - Detail 2.0 Design ↔ Code Mapping — rows for AssociationCondition, AggregateMetricCondition, PerformanceTab, SendCampaignDrawer show
TBDpending Figma. - Detail 4.B Test Plan — E2E — E2E spec path is TBD (no spec file created yet).
Chunks executable now (partial agent execution):
- Chunk 1: Complete Basic Attributes rule builder
- Chunk 2: Segment sub-sidenav in Customer Index
- Chunk 7: Permission gating
Blockers cleared → Chunks 3–6 become executable.
Optional: hand off to
rfc-reviewerfor a second-pass score once all Figma frames and BE API contracts are confirmed.
All 6 mermaid blocks in this RFC validated with npx -y -p @mermaid-js/mermaid-cli mmdc prior to save — all parsed with exit 0.