Skip to content

Commit 1fc6bad

Browse files
feat(segmentation-service): implement User Segmentation and Audience Service (#363)
- New NestJS microservice at microservices/segmentation-service/ that builds and manages user cohorts for targeted campaigns, A/B testing, and personalization in the Quest Service platform. - Entities: Segment, Rule, Membership, SegmentEvent, AbExperiment, AbAssignment (TypeORM, PostgreSQL, jsonb for flexible metadata). - Rule engine with 14 operators (equals, notEquals, in, notIn, contains, notContains, gt, gte, lt, lte, between, exists, notExists, regex) and AND/OR combinators across rule order. Field resolution supports attributes.* dot-paths. - SegmentationService exposes segment CRUD, manual membership, rule add/ remove, evaluation sweeps, membership check, AND/OR overlap analysis, segment size metrics, and an A/B experiment endpoint with deterministic MD5-hashed variant assignment (sticky via Redis cache). - Real-time ingestSignal endpoint upserts a UserSignal in Redis and synchronously evaluates against the active segment set, with a source hierarchy (MANUAL > REALTIME > EVALUATION) so manual additions are never auto-removed by the engine. - A cron scheduler triggers periodic evaluation for active segments whose evaluationIntervalSeconds is reached. - Docker + docker-compose for Postgres + Redis + the service. Env template + README explaining every endpoint. - Tests: 17 unit tests across 3 suites (rule engine, segmentation service with relations-aware InMemoryRepo and stateful Redis cache mock, health controller). Type-check passes; lint clean (one non-blocking warning).
1 parent e31f598 commit 1fc6bad

38 files changed

Lines changed: 14272 additions & 0 deletions
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[TEMPLATE]
2+
NODE_ENV=development
3+
PORT=3023
4+
5+
DB_HOST=localhost
6+
DB_PORT=5432
7+
DB_USER=postgres
8+
DB_PASSWORD=password
9+
DB_NAME=segmentation_db
10+
11+
REDIS_HOST=localhost
12+
REDIS_PORT=6379
13+
REDIS_PASSWORD=redis123
14+
# REDIS_URL=redis://:redis123@localhost:6379
15+
16+
SEGMENTATION_SCHEDULER_ENABLED=true
17+
SEGMENTATION_SEED_DEFAULTS=true
18+
SEGMENTATION_EVAL_INTERVAL_MS=60000
19+
SEGMENTATION_REDIS_NAMESPACE=segmentation
20+
SEGMENT_AB_TEST_VARIANT_SALT=quest-segmentation-ab-v1
21+
DEFAULT_AB_TRAFFIC_SPLIT=0.5
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module.exports = {
2+
parser: '@typescript-eslint/parser',
3+
parserOptions: {
4+
project: './tsconfig.json',
5+
tsconfigRootDir: __dirname,
6+
sourceType: 'module',
7+
},
8+
plugins: ['@typescript-eslint', 'prettier'],
9+
extends: [
10+
'eslint:recommended',
11+
'plugin:@typescript-eslint/recommended',
12+
'plugin:prettier/recommended',
13+
],
14+
env: {
15+
node: true,
16+
jest: true,
17+
},
18+
ignorePatterns: ['dist', 'node_modules'],
19+
rules: {
20+
'@typescript-eslint/no-explicit-any': 'off',
21+
'@typescript-eslint/no-floating-promises': 'warn',
22+
'@typescript-eslint/no-unsafe-argument': 'warn',
23+
'@typescript-eslint/no-unused-vars': [
24+
'warn',
25+
{ argsIgnorePattern: '^_' },
26+
],
27+
},
28+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
node_modules/
2+
dist/
3+
coverage/
4+
.env
5+
*.log
6+
.DS_Store
7+
.vscode/
8+
.tsbuildinfo
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"singleQuote": true,
3+
"trailingComma": "all"
4+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
FROM node:20-alpine AS build
2+
3+
WORKDIR /app
4+
5+
COPY package*.json ./
6+
RUN npm install
7+
8+
COPY . .
9+
RUN npm run build
10+
11+
FROM node:20-alpine
12+
13+
WORKDIR /app
14+
15+
COPY --from=build /app/dist ./dist
16+
COPY --from=build /app/node_modules ./node_modules
17+
COPY package*.json ./
18+
19+
ENV NODE_ENV=production
20+
EXPOSE 3023
21+
22+
CMD ["npm", "run", "start:prod"]
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Segmentation Service
2+
3+
Independent NestJS microservice that manages user cohorts for targeted campaigns,
4+
A/B testing, and personalization inside the Quest Service platform.
5+
6+
## Highlights
7+
8+
- **Segments** defined declaratively by ordered rule groups (AND / OR combinators).
9+
- **Rule-based**, **behavioral**, and **demographic** segmentation engines.
10+
- **Real-time membership updates** through an event feed (signals + scheduled
11+
evaluations + Redis cache invalidation).
12+
- **Overlap analysis** between two or more segments and a flexible **size metric**
13+
stream for dashboards.
14+
- **A/B Experiment assignments** with deterministic hashing, sticky variants, and
15+
configurable traffic splits.
16+
- **Docker** support (Postgres + Redis + service) with one-command bring-up.
17+
- **Independent runtime**: the service can be deployed, scaled, and tested in
18+
isolation from the rest of the platform.
19+
20+
## Project Layout
21+
22+
```
23+
microservices/segmentation-service/
24+
├── Dockerfile
25+
├── docker-compose.yml
26+
├── nest-cli.json
27+
├── package.json
28+
├── tsconfig.json
29+
├── tsconfig.build.json
30+
├── .env.example
31+
├── .eslintrc.cjs
32+
├── .prettierrc
33+
├── .gitignore
34+
└── src/
35+
├── main.ts
36+
├── app.module.ts
37+
├── app.controller.ts
38+
└── segmentation/
39+
├── segmentation.module.ts
40+
├── segmentation.controller.ts
41+
├── segmentation.service.ts
42+
├── segmentation-rule-engine.service.ts
43+
├── segmentation.scheduler.ts
44+
├── redis-cache.service.ts
45+
├── entities/
46+
│ ├── segment.entity.ts
47+
│ ├── rule.entity.ts
48+
│ ├── membership.entity.ts
49+
│ ├── segment-event.entity.ts
50+
│ └── ab-experiment.entity.ts
51+
├── dto/
52+
│ ├── create-segment.dto.ts
53+
│ ├── update-segment.dto.ts
54+
│ ├── create-rule.dto.ts
55+
│ ├── ingest-user-event.dto.ts
56+
│ ├── evaluate-segment.dto.ts
57+
│ ├── create-experiment.dto.ts
58+
│ └── assign-experiment.dto.ts
59+
└── interfaces/
60+
└── user-signal.interface.ts
61+
```
62+
63+
## Run Locally
64+
65+
```bash
66+
cp .env.example .env
67+
npm install
68+
npm run start:dev
69+
```
70+
71+
Or bring the service up together with Postgres and Redis:
72+
73+
```bash
74+
docker compose up --build
75+
```
76+
77+
## Useful Endpoints
78+
79+
| Method | Path | Purpose |
80+
| ------ | --------------------------------------- | -------------------------------------- |
81+
| GET | `/api/health` | Liveness probe |
82+
| GET | `/api/segments` | List every segment |
83+
| POST | `/api/segments` | Create a segment (with inline rules) |
84+
| GET | `/api/segments/:id` | Fetch a single segment with rules |
85+
| PATCH | `/api/segments/:id` | Update segment metadata / status |
86+
| DELETE | `/api/segments/:id` | Archive / delete a segment |
87+
| POST | `/api/segments/:id/rules` | Append a new rule to a segment |
88+
| DELETE | `/api/segments/:id/rules/:ruleId` | Remove a rule from a segment |
89+
| POST | `/api/segments/:id/evaluate` | Force a re-evaluation of members |
90+
| POST | `/api/segments/:id/membership` | Manually add / remove members |
91+
| GET | `/api/segments/:id/members` | List current members of a segment |
92+
| GET | `/api/segments/:id/size` | Read cached size + last computed time |
93+
| POST | `/api/segments/:id/check/:userId` | Check whether a user matches a segment |
94+
| POST | `/api/events` | Send a behavioral signal |
95+
| POST | `/api/segments/overlap` | Compute overlap between segment ids |
96+
| POST | `/api/experiments` | Create a new A/B experiment |
97+
| POST | `/api/experiments/:id/assign` | Assign (and cache) a variant for user |
98+
| GET | `/api/dashboard` | Service-wide summary |
99+
100+
## Rule Schema
101+
102+
```jsonc
103+
{
104+
"field": "country",
105+
"operator": "equals",
106+
"value": "US",
107+
"combinator": "AND",
108+
"order": 0
109+
}
110+
```
111+
112+
Operators: `equals`, `notEquals`, `in`, `notIn`, `contains`, `notContains`,
113+
`gt`, `gte`, `lt`, `lte`, `between`, `exists`, `notExists`, `regex`.
114+
115+
Behavioral fields: `action`, `eventCount`, `lastEventAt`, `totalSpend`,
116+
`level`, `xp`, `streak`, `consecutiveDays`. They are evaluated against the
117+
freshest user signal stored under `segmentation:user:{userId}` in Redis.
118+
119+
## Tests
120+
121+
```bash
122+
npm run lint:check
123+
npm run type-check
124+
npm run test
125+
```
126+
127+
## Docker Build
128+
129+
```bash
130+
docker build -t segmentation-service .
131+
docker run --rm -p 3023:3023 segmentation-service
132+
```
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
version: '3.8'
2+
3+
services:
4+
segmentation-service:
5+
build:
6+
context: .
7+
dockerfile: Dockerfile
8+
container_name: segmentation_service
9+
ports:
10+
- '${SEGMENTATION_PORT:-3023}:3023'
11+
environment:
12+
- NODE_ENV=development
13+
- PORT=3023
14+
- DB_HOST=postgres
15+
- DB_PORT=5432
16+
- DB_USER=${DB_USER:-postgres}
17+
- DB_PASSWORD=${DB_PASSWORD:-password}
18+
- DB_NAME=${SEGMENTATION_DB_NAME:-segmentation_db}
19+
- REDIS_HOST=redis
20+
- REDIS_PORT=6379
21+
- REDIS_PASSWORD=${REDIS_PASSWORD:-redis123}
22+
- SEGMENTATION_SCHEDULER_ENABLED=${SEGMENTATION_SCHEDULER_ENABLED:-true}
23+
- SEGMENTATION_EVAL_INTERVAL_MS=${SEGMENTATION_EVAL_INTERVAL_MS:-60000}
24+
depends_on:
25+
postgres:
26+
condition: service_healthy
27+
redis:
28+
condition: service_healthy
29+
networks:
30+
- segmentation-network
31+
32+
postgres:
33+
image: postgres:15-alpine
34+
container_name: segmentation_postgres
35+
ports:
36+
- '${SEGMENTATION_DB_PORT:-5436}:5432'
37+
environment:
38+
POSTGRES_USER: ${DB_USER:-postgres}
39+
POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
40+
POSTGRES_DB: ${SEGMENTATION_DB_NAME:-segmentation_db}
41+
volumes:
42+
- segmentation_postgres_data:/var/lib/postgresql/data
43+
healthcheck:
44+
test: ['CMD-SHELL', 'pg_isready -U ${DB_USER:-postgres}']
45+
interval: 5s
46+
timeout: 5s
47+
retries: 5
48+
networks:
49+
- segmentation-network
50+
51+
redis:
52+
image: redis:7-alpine
53+
container_name: segmentation_redis
54+
ports:
55+
- '${SEGMENTATION_REDIS_PORT:-6383}:6379'
56+
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-redis123}
57+
volumes:
58+
- segmentation_redis_data:/data
59+
healthcheck:
60+
test: ['CMD', 'redis-cli', '-a', '${REDIS_PASSWORD:-redis123}', '--raw', 'incr', 'ping']
61+
interval: 10s
62+
timeout: 3s
63+
retries: 5
64+
networks:
65+
- segmentation-network
66+
67+
volumes:
68+
segmentation_postgres_data:
69+
segmentation_redis_data:
70+
71+
networks:
72+
segmentation-network:
73+
driver: bridge
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"$schema": "https://json.schemastore.org/nest-cli",
3+
"collection": "@nestjs/schematics",
4+
"sourceRoot": "src",
5+
"compilerOptions": {
6+
"deleteOutDir": true
7+
}
8+
}

0 commit comments

Comments
 (0)