diff --git a/.gitignore b/.gitignore index d081f71..1583711 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /bin /dist /coverage -/cmd/server/cache_data \ No newline at end of file +/cmd/server/cache_data +.env \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 53d39b1..2609898 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,4 +1,4 @@ -// Package docs GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// Package docs GENERATED BY SWAG; DO NOT EDIT // This file was generated by swaggo/swag package docs @@ -34,7 +34,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Campaigns" + "Activate" ], "summary": "Activate a campaign", "operationId": "activate", diff --git a/docs/swagger.json b/docs/swagger.json index 749ac7a..60deb63 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -26,7 +26,7 @@ "application/json" ], "tags": [ - "Campaigns" + "Activate" ], "summary": "Activate a campaign", "operationId": "activate", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 26edd68..7947d2d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -171,7 +171,7 @@ paths: $ref: '#/definitions/handlers.errorMessage' summary: Activate a campaign tags: - - Campaigns + - Activate /campaigns: post: consumes: diff --git a/examples/advanced_features_demo/Makefile b/examples/advanced_features_demo/Makefile new file mode 100644 index 0000000..b87dd4e --- /dev/null +++ b/examples/advanced_features_demo/Makefile @@ -0,0 +1,76 @@ +.PHONY: help run setup start-infra stop-infra clean test + +help: ## Show this help message + @echo 'Usage: make [target]' + @echo '' + @echo 'Available targets:' + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-15s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +setup: ## Copy .env.example to .env (edit with your credentials) + @if [ ! -f .env ]; then \ + cp .env.example .env; \ + echo "✓ Created .env file - please edit with your credentials"; \ + else \ + echo "✓ .env file already exists"; \ + fi + +start-infra: ## Start Redis and Decision API with docker-compose + @echo "Starting infrastructure..." + docker compose -f docker-compose.demo.yml up -d redis decision-api + @echo "Waiting for services to be healthy..." + @sleep 5 + @echo "✓ Infrastructure ready!" + @echo "" + @echo "Services:" + @echo " - Decision API: http://localhost:8080" + @echo " - Redis: localhost:6379" + @echo "" + @echo "Next: Update .env with your ENV_ID and API_KEY, then run 'make run'" + +stop-infra: ## Stop all services + docker compose -f docker-compose.demo.yml down + +run: setup ## Run the demo application + @echo "Running advanced features demo..." + @echo "" + go run main.go + +run-docker: setup ## Run demo in Docker + docker compose -f docker-compose.demo.yml --profile demo up demo-app + +test: ## Test connection to Decision API + @echo "Testing Decision API connection..." + @curl -f http://localhost:8080/health || (echo "❌ Decision API not reachable" && exit 1) + @echo "✓ Decision API is healthy" + @echo "" + @echo "Testing Redis connection..." + @docker exec $$(docker ps -qf "name=redis") redis-cli ping || (echo "❌ Redis not reachable" && exit 1) + @echo "✓ Redis is healthy" + +logs: ## Show Decision API logs + docker compose -f docker-compose.demo.yml logs -f decision-api + +logs-redis: ## Show Redis logs + docker compose -f docker-compose.demo.yml logs -f redis + +clean: ## Remove all containers and volumes + docker compose -f docker-compose.demo.yml down -v + rm -f .env + +inspect-cache: ## Inspect Redis cache contents + @echo "Redis cache keys:" + @docker exec $$(docker ps -qf "name=redis") redis-cli keys "*" + @echo "" + @echo "To view a specific key:" + @echo " docker exec -it $$(docker ps -qf 'name=redis') redis-cli get ''" + +clear-cache: ## Clear all Redis cache + @echo "Clearing Redis cache..." + @docker exec $$(docker ps -qf "name=redis") redis-cli flushall + @echo "✓ Cache cleared" + +build: ## Build the demo binary + go build -o demo main.go + @echo "✓ Built ./demo" + +.DEFAULT_GOAL := help diff --git a/examples/advanced_features_demo/README.md b/examples/advanced_features_demo/README.md new file mode 100644 index 0000000..3c61e85 --- /dev/null +++ b/examples/advanced_features_demo/README.md @@ -0,0 +1,301 @@ +# Advanced Features Demo - Decision API + +This demo showcases the advanced features of the Flagship Decision API: + +- **Experience Continuity (XPC)**: Consistent visitor assignments across sessions +- **1 Visitor 1 Test (1v1t)**: Single campaign assignment per visitor + +## Prerequisites + +1. **Running Decision API** instance with persistent cache (Redis or DynamoDB) +2. **Flagship environment** with campaigns configured +3. **Environment credentials**: ENV_ID and API_KEY + +## Setup + +### 1. Configure the Demo + +Edit `main.go` and update these constants: + +```go +const ( + decisionAPIURL = "http://localhost:8080" // Your Decision API endpoint + envID = "your_env_id" // Your Flagship environment ID + apiKey = "your_api_key" // Your Flagship API key +) +``` + +### 2. Start Decision API with Persistent Cache + +For **Experience Continuity** to work, you need persistent storage: + +#### Option A: Using Redis (Recommended) + +```bash +# Start Redis +docker run -d -p 6379:6379 redis + +# Start Decision API with Redis cache +docker run -p 8080:8080 \ + -e ENV_ID=your_env_id \ + -e API_KEY=your_api_key \ + -e CACHE_TYPE=redis \ + -e CACHE_OPTIONS_REDISHOST=host.docker.internal:6379 \ + flagshipio/decision-api +``` + +#### Option B: Using Docker Compose + +```bash +# Use the provided docker-compose.yml in the root directory +cd ../.. +docker compose up -d +``` + +#### Option C: Using Local Cache + +```bash +docker run -p 8080:8080 \ + -e ENV_ID=your_env_id \ + -e API_KEY=your_api_key \ + -e CACHE_TYPE=local \ + -e CACHE_OPTIONS_DBPATH=/data/cache \ + -v $(pwd)/cache_data:/data/cache \ + flagshipio/decision-api +``` + +### 3. Configure Flagship Account Settings + +In your Flagship dashboard, enable: + +- **Experience Continuity (XPC)**: Settings → Advanced → Cross-Platform Consistency +- **1 Visitor 1 Test (1v1t)**: Settings → Advanced → Single Assignment + +Create at least 2-3 campaigns with different targeting rules to see the features in action. + +## Running the Demo + +```bash +cd examples/advanced_features_demo +go run main.go +``` + +## What to Expect + +### Scenario 1: Experience Continuity (XPC) + +``` +Visitor: alice_1234567890 +Making first request to Decision API... +✓ First request: Received 1 campaign(s) + 1. Campaign ID: campaign_abc123 + Variation: Variation A (var_xyz789) + Type: ab + +Activating campaign: campaign_abc123 (variation: var_xyz789) +✓ Campaign activated - assignment now cached + +Making second request (simulating return visit)... +✓ Second request: Received 1 campaign(s) + 1. Campaign ID: campaign_abc123 + Variation: Variation A (var_xyz789) + Type: ab + +✓ EXPERIENCE CONTINUITY VERIFIED: Same variation assigned across requests! +``` + +**Key Point**: The visitor receives the **same variation** on subsequent requests because the assignment is cached. + +### Scenario 2: 1 Visitor 1 Test (1v1t) + +``` +Visitor: bob_1234567891 +With 1v1t enabled, visitor should be in AT MOST 1 campaign + +✓ Retrieved campaigns: 1 + 1. Campaign ID: campaign_def456 + Variation: Control (var_control) + Type: ab + +✓ 1 VISITOR 1 TEST VERIFIED: Visitor assigned to at most 1 campaign +``` + +**Key Point**: Even if multiple campaigns are active and the visitor qualifies for several, they are assigned to **at most one** to avoid interaction effects. + +### Scenario 3: Cross-Session Consistency + +``` +Simulating 3 sessions for visitor: charlie_1234567892 + +--- Session 1 --- +Campaigns received: 1 +Assigned variation: var_xyz789 +✓ Campaign activated + +--- Session 2 --- +Campaigns received: 1 +Assigned variation: var_xyz789 + +--- Session 3 --- +Campaigns received: 1 +Assigned variation: var_xyz789 + +✓ CROSS-SESSION CONSISTENCY VERIFIED: Same variation across all sessions! +``` + +**Key Point**: Assignments persist across sessions (days, weeks) as long as the cache is maintained. + +### Scenario 4: Context-Based Targeting + +``` +Visitor: premium_user_1234567893 +Context: map[country:US plan:premium vip:true] +Eligible campaigns: 1 + 1. Campaign ID: premium_feature_test + Variation: Premium Flow (var_premium) + Flags: + - showNewUI: true + - discountPercent: 20 + +Visitor: free_user_1234567894 +Context: map[country:FR plan:free vip:false] +Eligible campaigns: 1 + 1. Campaign ID: free_tier_test + Variation: Standard Flow (var_standard) + Flags: + - showNewUI: false + - discountPercent: 0 + +✓ Context-based targeting allows different campaigns for different user segments +``` + +**Key Point**: Visitor context determines campaign eligibility, enabling sophisticated audience targeting. + +## Troubleshooting + +### "Different variations - cache may not be configured" + +**Problem**: Experience Continuity not working + +**Solutions**: + +1. Verify cache is configured (not using empty/memory-only cache) +2. Check Redis/DynamoDB is running and accessible +3. Ensure `CacheEnabled: true` in Flagship settings +4. Check Decision API logs for cache errors + +### "⚠ Visitor in X campaigns - 1v1t may not be enabled" + +**Problem**: 1 Visitor 1 Test not enforced + +**Solutions**: + +1. Enable "Single Assignment" in Flagship dashboard +2. Verify environment configuration is fetched correctly +3. Check that `Enabled1V1T: true` in account settings +4. Allow time for configuration to propagate (up to 1 minute polling interval) + +### "API returned status 400/500" + +**Problem**: Request errors + +**Solutions**: + +1. Verify ENV_ID and API_KEY are correct +2. Check Decision API is running (`curl http://localhost:8080/health`) +3. Ensure campaigns exist in your Flagship environment +4. Check Decision API logs for detailed errors + +## Architecture Diagram + +``` +┌─────────────────┐ +│ Demo App │ +│ │ +│ - alice (XPC) │ +│ - bob (1v1t) │ +│ - charlie │ +└────────┬────────┘ + │ HTTP Requests + │ (POST /v2/campaigns) + │ + ▼ +┌─────────────────┐ ┌──────────────┐ +│ Decision API │◄────►│ Redis Cache │ +│ (Port 8080) │ │ (Assignments)│ +└────────┬────────┘ └──────────────┘ + │ + │ Poll Config (1min) + ▼ +┌─────────────────┐ +│ Flagship CDN │ +│ (Configuration) │ +└─────────────────┘ + │ + │ Send Tracking + ▼ +┌─────────────────┐ +│ Data Collection │ +│ (Analytics) │ +└─────────────────┘ +``` + +## Key Takeaways + +1. **XPC requires persistent storage** - Use Redis, DynamoDB, or local cache (not memory) +2. **1v1t reduces test pollution** - Visitors see consistent, single-test experiences +3. **Activation triggers caching** - Call `/activate` to persist assignments +4. **Context drives targeting** - Visitor attributes determine campaign eligibility +5. **Cache is customer-managed** - In self-hosted mode, you control cache availability + +## Advanced Usage + +### Custom Visitor Context + +Add more context fields for sophisticated targeting: + +```go +context := map[string]interface{}{ + "age": 28, + "country": "US", + "plan": "premium", + "signupDate": "2024-01-15", + "totalSpent": 1250.50, + "device": "mobile", + "appVersion": "3.2.1", + "experiments": []string{"feature_x", "feature_y"}, +} +``` + +### Anonymous to Authenticated Visitor + +```go +// First visit (anonymous) +anonymousID := "anon_xyz123" +resp1, _ := client.GetCampaigns(anonymousID, context) + +// User logs in (reconciliation) +authenticatedID := "user@example.com" +// Use anonymous_id in request to link identities +``` + +### Monitoring Cache Performance + +Check if assignments are cached: + +```bash +# Redis +redis-cli keys "*" +redis-cli get "env_id.visitor_id" + +# Decision API metrics +curl http://localhost:8080/metrics +``` + +## Next Steps + +1. **Integrate into your application** - Use this demo as a reference +2. **Set up monitoring** - Track cache hit rates and decision latency +3. **Configure backups** - Ensure cache data is backed up for continuity +4. **Test failover** - Verify behavior when cache is unavailable +5. **Review legal guidance** - See `SELF_HOSTING_LEGAL_GUIDANCE.md` for operational responsibilities diff --git a/examples/advanced_features_demo/docker-compose.demo.yml b/examples/advanced_features_demo/docker-compose.demo.yml new file mode 100644 index 0000000..7cab790 --- /dev/null +++ b/examples/advanced_features_demo/docker-compose.demo.yml @@ -0,0 +1,57 @@ +version: "3.9" + +services: + # Redis for assignment caching + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + + # Decision API with Redis cache + decision-api: + image: flagshipio/decision-api:latest + ports: + - "8080:8080" + environment: + # Flagship credentials - UPDATE THESE! + ENV_ID: ${ENV_ID:-your_env_id} + API_KEY: ${API_KEY:-your_api_key} + + # Server configuration + ADDRESS: ":8080" + LOG_LEVEL: info + LOG_FORMAT: text + + # Cache configuration (Redis for XPC) + CACHE_TYPE: redis + CACHE_OPTIONS_REDISHOST: redis:6379 + # CORS configuration + CORS_ENABLED: "true" + CORS_ALLOWED_ORIGINS: "*" + + # Polling interval for config updates + POLLING_INTERVAL: 60s + depends_on: + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"] + interval: 10s + timeout: 5s + retries: 3 + +volumes: + redis_data: + driver: local + +networks: + default: + name: flagship-demo diff --git a/examples/advanced_features_demo/go.mod b/examples/advanced_features_demo/go.mod new file mode 100644 index 0000000..8a61dae --- /dev/null +++ b/examples/advanced_features_demo/go.mod @@ -0,0 +1,6 @@ +module github.com/flagship-io/decision-api/examples/advanced_features_demo + +go 1.21 + +// This is a standalone demo application +// No dependencies required beyond Go standard library diff --git a/pkg/handlers/activate.go b/pkg/handlers/activate.go index 579df99..dfff632 100644 --- a/pkg/handlers/activate.go +++ b/pkg/handlers/activate.go @@ -18,7 +18,7 @@ import ( // Activate returns a flag activation handler // @Summary Activate a campaign -// @Tags Campaigns +// @Tags Activate // @Description Activate a campaign for a visitor ID // @ID activate // @Accept json