Ruby translation of the analytics-service module with a Rails-compatible structure and a Rails-native rules-engine flow.
This component mirrors the Rust service responsibilities:
- interaction event ingestion
- Redis hot-state management
- PostgreSQL persistence boundaries
- deterministic rule evaluation
- notification candidate persistence
- Mixpanel dispatch persistence and processing
The preferred Rails-style execution path is:
- thin controller entry point
TrackEventServicefor event creationEvaluateRulesJobfor async rule evaluationRulesEngine::Processorfor cooldown-aware matchingPushNotificationJobfor notification dispatch
- Homebrew Ruby on
PATH - PostgreSQL running locally
- Redis running locally
The verified local toolchain on this machine is:
- Ruby
3.4.4 - Bundler
2.6.9 - PostgreSQL
17 - Redis
8
All /api/v1/debug/* endpoints now require a bearer token.
The runtime config key is:
ANALYTICS_SERVICE_DEBUG_AUTH_TOKEN
Local default:
engagement-decision-local-debug-token
cd /Users/ab000111/Downloads/app-dazzle/api-service/engagement-decision-service
PATH=/opt/homebrew/opt/ruby/bin:$PATH bundle installbrew services start postgresql@17
brew services start redisQuick checks:
PATH=/opt/homebrew/opt/postgresql@17/bin:$PATH psql -d postgres -c 'select current_user, current_database();'
PATH=/opt/homebrew/opt/redis/bin:$PATH redis-cli pingExpected:
- PostgreSQL query returns one row
- Redis returns
PONG
cd /Users/ab000111/Downloads/app-dazzle/api-service/engagement-decision-service
PATH=/opt/homebrew/opt/ruby/bin:$PATH bundle exec rails db:create
PATH=/opt/homebrew/opt/ruby/bin:$PATH bundle exec rails db:migratecd /Users/ab000111/Downloads/app-dazzle/api-service/engagement-decision-service
PATH=/opt/homebrew/opt/ruby/bin:$PATH bundle exec rails runner 'user = User.find_or_create_by!(email: "local-user@example.com") { |u| u.name = "Local User" }; puts user.id'The examples below assume the printed user id is 1.
The standalone app now persists the external event_id from the request payload and treats it as the idempotency key for ingestion.
- first request with a new
event_idreturns201 Createdandstatus: "ok" - repeated request with the same
event_idreturns200 OKandstatus: "duplicate" - the duplicate response returns the original event record instead of creating a second row
Each response now also includes an ingestion object with:
status:accepted,duplicate, orfailedattempt_id: the persistedingestion_attempts.idcorrelation_id: the request correlation id used in structured logs
The service now audits every HTTP ingest attempt in ingestion_attempts, including accepted requests, duplicate replays, and failure paths such as unknown-user requests.
Terminal 1, Puma:
cd /Users/ab000111/Downloads/app-dazzle/api-service/engagement-decision-service
PATH=/opt/homebrew/opt/ruby/bin:$PATH bundle exec rails server -b 127.0.0.1 -p 3001Terminal 2, Sidekiq:
cd /Users/ab000111/Downloads/app-dazzle/api-service/engagement-decision-service
PATH=/opt/homebrew/opt/ruby/bin:$PATH bundle exec sidekiq -C config/sidekiq.ymlcurl -sS http://127.0.0.1:3001/healthExpected:
{"status":"ok","service":"engagement-decision-service"}Post two salon_detail_viewed events for the same session and salon:
curl -sS -X POST http://127.0.0.1:3001/api/v1/events \
-H 'Content-Type: application/json' \
-d '{"user_id":1,"event_type":"salon_detail_viewed","properties":{"event_id":"evt-local-a1","session_id":"session-repeat-1","salon_id":"salon-repeat-1","platform":"ios","screen_name":"salon_detail"}}'
curl -sS -X POST http://127.0.0.1:3001/api/v1/events \
-H 'Content-Type: application/json' \
-d '{"user_id":1,"event_type":"salon_detail_viewed","properties":{"event_id":"evt-local-a2","session_id":"session-repeat-1","salon_id":"salon-repeat-1","platform":"ios","screen_name":"salon_detail"}}'Inspect the decision:
curl -sS http://127.0.0.1:3001/api/v1/debug/session-decisions/session-repeat-1 \
-H 'Authorization: Bearer engagement-decision-local-debug-token'Expected outcome:
rule_id = repeat_salon_view_without_service_add- one
notification_candidate
Post one service_added event:
curl -sS -X POST http://127.0.0.1:3001/api/v1/events \
-H 'Content-Type: application/json' \
-d '{"user_id":1,"event_type":"service_added","properties":{"event_id":"evt-local-b1","session_id":"session-service-1","salon_id":"salon-service-1","service_id":"service-1","platform":"ios","screen_name":"salon_detail","cart_value":299}}'Inspect the decision:
curl -sS http://127.0.0.1:3001/api/v1/debug/session-decisions/session-service-1 \
-H 'Authorization: Bearer engagement-decision-local-debug-token'Expected outcome:
rule_id = service_added_no_nextpriority = high
curl -sS -X POST http://127.0.0.1:3001/api/v1/events \
-H 'Content-Type: application/json' \
-d '{"user_id":1,"event_type":"salon_detail_viewed","properties":{"event_id":"evt-local-dup-1","session_id":"session-dup-1","salon_id":"salon-dup-1","platform":"ios","screen_name":"salon_detail"}}'
curl -sS -X POST http://127.0.0.1:3001/api/v1/events \
-H 'Content-Type: application/json' \
-d '{"user_id":1,"event_type":"salon_detail_viewed","properties":{"event_id":"evt-local-dup-1","session_id":"session-dup-1","salon_id":"salon-dup-1","platform":"ios","screen_name":"salon_detail"}}'Expected outcome:
- first response has
status = "ok" - second response has
status = "duplicate" - both responses point to the same event record id
- the two responses have different
ingestion.attempt_idvalues because each HTTP attempt is persisted separately
curl -sS http://127.0.0.1:3001/api/v1/debug/session-hot-state/1/session-repeat-1 \
-H 'Authorization: Bearer engagement-decision-local-debug-token'This returns:
last_ten_eventsrecent_window_events
Inspect persisted HTTP ingest attempts without querying Postgres directly:
curl -sS "http://127.0.0.1:3001/api/v1/debug/ingestion-attempts?event_id=evt-local-dup-1" \
-H 'Authorization: Bearer engagement-decision-local-debug-token'Supported filters:
event_idcorrelation_idsession_idstatuslimit
Example for failed ingests:
curl -sS "http://127.0.0.1:3001/api/v1/debug/ingestion-attempts?status=failed&limit=10" \
-H 'Authorization: Bearer engagement-decision-local-debug-token'This returns the persisted ingestion_attempts rows, including:
statusfailure_typecorrelation_id- request metadata
metadata.attempted_user_idfor unknown-user failures
Requests without the bearer token return 401 unauthorized.
Puma logs request handling, controller selection, and ActiveRecord queries directly in the server terminal.
Useful checks:
- request reached the right controller
201 CreatedonPOST /api/v1/events- no
RoutingErrororUnprocessable Entity
Sidekiq logs:
- job enqueue and execution
EvaluateRulesJobPushNotificationJob- failures in rule evaluation or dispatch persistence
For a successful repeat-view flow you should see:
EvaluateRulesJobstart and finishPushNotificationJobenqueued forrepeat_salon_view_without_service_add
Inspect hot-state keys:
PATH=/opt/homebrew/opt/redis/bin:$PATH redis-cli keys 'analytics/*'Run the standalone module tests:
cd /Users/ab000111/Downloads/app-dazzle/api-service/engagement-decision-service
PATH=/opt/homebrew/opt/ruby/bin:$PATH ruby -I lib test/run_all.rbCurrent verified result:
29 runs, 81 assertions, 0 failures, 0 errors, 0 skips