diff --git a/.circleci/config.yml b/.circleci/config.yml index 7940d981..fe020e0a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,114 +1,240 @@ -# This config is equivalent to both the '.circleci/extended/orb-free.yml' and the base '.circleci/config.yml' -version: 2.1 +version: "2.1" -# Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects. -# See: https://circleci.com/docs/2.0/orb-intro/ orbs: - node: circleci/node@4.7 - cypress: cypress-io/cypress@1 + node: circleci/node@5.1.0 + aws-ecr: circleci/aws-ecr@9.0 + aws-cli: circleci/aws-cli@3.1 + terraform-helper: anyvan/terraform-helper@1 + aws: anyvan/aws_auth_config@1 + deployments: anyvan/deployments@1 + aws-ecr-helper: anyvan/aws-ecr-helper@2 + aws-tunnel-helper: anyvan/aws-tunnel-helper@2 + anyvan_github_ssh_key: anyvan/ssh_key_github@1 + +parameters: + region: + default: eu-west-1 + type: string + modules: + default: v0.0.79 + type: string + terraform_version: + default: "1.9.5" + type: string + jira_ticket_number: + type: string + default: $(echo $CIRCLE_BRANCH | sed -ne 's/[A-Z]*-\([0-9]*\)-.*/\1/p') + image_tag: + type: string + default: $CIRCLE_SHA1 + workflow: + type: string + default: "deploy_to_pr_env" + app_name: + type: string + default: "twilio-webchat-widget" + ecr_repo_host: + type: string + default: "331151898531.dkr.ecr.eu-west-1.amazonaws.com" + executors: - with-chrome: + base: docker: - - image: 'cypress/browsers:node14.17.0-chrome91-ff89' + - image: cimg/base:current -jobs: - cleanUp: - docker: - - image: cimg/node:14.18.1 - steps: - - checkout - - run: - command: npm install twilio --no-save - name: Install Twilio client - - run: - command: | - yarn e2eCleanupExistingTasks \ - accountSid=$TWILIO_ACCOUNT_SID \ - authToken=$TWILIO_AUTH_TOKEN \ - name: Cleanup existing tasks - runUnitTests: + node-docker: docker: - - image: cimg/node:14.18.1 - steps: - - checkout - - node/install-packages: - pkg-manager: yarn-berry - with-cache: true - - run: - command: yarn test:nowatch - name: Run YARN tests - cypressTestsOnSauce: + - image: cimg/node:20.19.2 + resource_class: small + + iac-base: docker: - - image: cimg/node:lts + - image: $AWS_ECR_CIRCLECI_IMAGES_REPOSITORY_HOST/base/iac-base:noble + aws_auth: + oidc_role_arn: arn:aws:iam::$HORIZONTAL_ACCOUNT:role/circleci_oidc_role + +jobs: + build-and-push: + parameters: + image: + type: string + project-root: + type: string + env: + type: string + repository: + type: string + entry: + type: string + dockerfile: + type: string + executor: node-docker steps: - checkout + - attach_workspace: + at: ~/ - run: - name: "Install dependencies" - command: | - yarn install --frozen-lockfile - - run: - name: "Initialize accounts" - command: | - yarn bootstrap \ - accountSid=$TWILIO_ACCOUNT_SID \ - authToken=$TWILIO_AUTH_TOKEN \ - addressSid=$TWILIO_ADDRESS_SID \ - apiKey=$TWILIO_API_KEY \ - apiSecret=$TWILIO_API_SECRET \ - conversationsServiceSid=$TWILIO_CONVERSATIONS_SERVICE_SID - - run: - name: "Running Application" - background: true - command: | - yarn start & yarn server:ci - - run: - name: "Set Up Tunnel" - background: true - command: | - curl https://saucelabs.com/downloads/sc-4.7.1-linux.tar.gz -o saucelabs.tar.gz - tar -xzf saucelabs.tar.gz - cd sc-* - bin/sc -u ${SAUCE_USERNAME} -k ${SAUCE_ACCESS_KEY} --region ${SAUCE_REGION} --tunnel-name ${SAUCE_TUNNEL_IDENTIFIER} - - run: - name: "Install Sauce Labs saucectl" + name: Ensure docker-credential-helper is installed command: | - curl -fsSL -o get_saucectl.sh https://saucelabs.github.io/saucectl/install && \ - chmod 700 get_saucectl.sh && \ - sudo ./get_saucectl.sh -b /usr/local/bin + sudo apt update && sudo apt install amazon-ecr-credential-helper + - setup_remote_docker: + docker_layer_caching: false + version: default - run: - name: "Configure Sauce saucectl" + name: "Build and push image to ECR" command: | - saucectl configure -u ${SAUCE_USERNAME} -a ${SAUCE_ACCESS_KEY} - - run: - name: "Run Cypress on Sauce" - command: | - saucectl run --tunnel-name ${SAUCE_TUNNEL_IDENTIFIER} -e TEST_EMAIL=${TEST_EMAIL} + + PROJECT_REPOSITORY_URL=<< pipeline.parameters.ecr_repo_host >>/<< parameters.image >> + echo "Repository URL: $PROJECT_REPOSITORY_URL" + docker build -f << parameters.dockerfile >> -t "${PROJECT_REPOSITORY_URL}:${CIRCLE_SHA1}" . + docker push "${PROJECT_REPOSITORY_URL}:${CIRCLE_SHA1}" + tag_image: + executor: + name: aws-ecr/default + parameters: + additional_tag: + type: string + source_tag: + type: string + default: "$CIRCLE_SHA1" + steps: + - attach_workspace: + at: ~/ + - anyvan_github_ssh_key/install_ssh_key + - aws-ecr/tag_image: + repo: "<< pipeline.parameters.app_name >>" + source_tag: "<< parameters.source_tag >>" + target_tag: "<< parameters.additional_tag >>" + workflows: - build: + # Default workflow - Deploy to PR Environment + deploy_to_pr_env: + when: + and: + - not: + equal: ["main", << pipeline.git.branch >>] + - not: + equal: ["", << pipeline.parameters.jira_ticket_number >>] jobs: - - cleanUp - - runUnitTests: + - aws/init: &aws_init + name: "Log in to AWS" + context: anyvan + region: << pipeline.parameters.region >> + account: PRODUCTION HORIZONTAL TEST1 + persist: .aws +# - aws-ecr-helper/create_aws_ecr: &create_ecr +# name: "Create AWS ECR Client repository" +# context: anyvan +# module_path: "iac/terraform/image_repository" +# app_name: twilio-webchat-client +# tf_version: << pipeline.parameters.terraform_version >> +# requires: +# - "Log in to AWS" +# - aws-ecr-helper/create_aws_ecr: +# <<: *create_ecr +# name: "Create AWS ECR Server repository" +# app_name: twilio-webchat-server + - aws-ecr-helper/ecr_login: &ecr_login + name: "Log in to AWS ECR" + context: anyvan + requires: +# - "Create AWS ECR Server repository" +# - "Create AWS ECR Client repository" + - "Log in to AWS" + # Build Server Image for PR Environment + - build-and-push: + name: build_webchat_server_pr + project-root: . + entry: . + repository: server + image: twilio-webchat-server + dockerfile: Dockerfile.server + env: development + requires: + - "Log in to AWS ECR" + + # Build Client Image for PR Environment + - build-and-push: + name: build_webchat_client_pr + project-root: . + entry: . + repository: client + image: twilio-webchat-client + dockerfile: Dockerfile.client + env: development + requires: + - "Log in to AWS ECR" + - hold_pr: + type: approval + requires: + - build_webchat_client_pr + - build_webchat_server_pr + # Plan Terraform for PR Environment + - terraform-helper/plan: &plan + name: plan_webchat_widget_pr + context: + - anyvan + - twilio-flex-stage + workspace: << pipeline.parameters.app_name >>_branches<< pipeline.parameters.jira_ticket_number >> + var: >- + jira_ticket_number=<< pipeline.parameters.jira_ticket_number >>, + server_container_image_url=<< pipeline.parameters.ecr_repo_host >>/twilio-webchat-server:${CIRCLE_SHA1}, + client_container_image_url=<< pipeline.parameters.ecr_repo_host >>/twilio-webchat-client:${CIRCLE_SHA1}, + git_sha=$CIRCLE_SHA1, + terraform_version=<< pipeline.parameters.terraform_version >> + var_file: env/development.tfvars + modules_branch: << pipeline.parameters.modules >> + path: iac/terraform/ + terraform_version: << pipeline.parameters.terraform_version >> + requires: + - hold_pr + + # Apply Terraform for PR Environment + - terraform-helper/apply: &apply + name: apply_webchat_widget_pr + context: anyvan + workspace: << pipeline.parameters.app_name >>_branches<< pipeline.parameters.jira_ticket_number >> + modules_branch: << pipeline.parameters.modules >> + path: iac/terraform/ + terraform_version: << pipeline.parameters.terraform_version >> requires: - - cleanUp - - cypress/run: + - plan_webchat_widget_pr + + pull_request_closed: + when: + and: + - equal: [pull_request_closed, << pipeline.parameters.workflow >>] + - not: + equal: [main, << pipeline.git.branch >>] + - not: + equal: ["", << pipeline.parameters.jira_ticket_number >>] + jobs: + - aws/init: + *aws_init + - terraform-helper/plan: + <<: *plan + name: "Plan destroy PR environment" + env: branches + destroy: true + var_file: env/branches.tfvars + workspace: << pipeline.parameters.app_name >>_branches<< pipeline.parameters.jira_ticket_number >> requires: - - cleanUp - executor: with-chrome - post-install: - - run: "yarn bootstrap \ - accountSid=$TWILIO_ACCOUNT_SID \ - authToken=$TWILIO_AUTH_TOKEN \ - addressSid=$TWILIO_ADDRESS_SID \ - apiKey=$TWILIO_API_KEY \ - apiSecret=$TWILIO_API_SECRET \ - conversationsServiceSid=$TWILIO_CONVERSATIONS_SERVICE_SID - " - browser: chrome - yarn: true - start: 'yarn start & yarn server:ci' # start server before running tests - store_artifacts: true - - cypressTestsOnSauce: + - "Log in to AWS" + - terraform-helper/apply: + <<: *apply + name: "Apply Destroy of PR environment" + destroy: true + workspace: << pipeline.parameters.app_name >>_branches<< pipeline.parameters.jira_ticket_number >> requires: - - cleanUp + - "Plan destroy PR environment" + + + + + + + + + diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..3ad96a21 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,22 @@ +node_modules +npm-debug.log +yarn-debug.log +yarn-error.log +.git +.gitignore +README.md +.env +.nyc_output +coverage +.nyc_output +.coverage +.cache +build +dist +.DS_Store +*.log +.vscode +.idea +*.swp +*.swo +*~ diff --git a/.eslintrc b/.eslintrc index 7d0683a3..34c93f26 100644 --- a/.eslintrc +++ b/.eslintrc @@ -34,6 +34,7 @@ "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/no-shadow": ["error"], "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/member-ordering": "off", "import/no-duplicates": ["error"], "spaced-comment": "warn", "prefer-named-capture-group": "off" diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 00000000..e64a3648 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,113 @@ +# Docker Setup for Twilio Webchat Widget + +This project includes Docker configuration to run both the server and client applications in separate containers. + +## Prerequisites + +- Docker installed on your system +- Docker Compose installed +- `.env` file with your environment variables + +## Quick Start + +### Option 1: Using Docker Compose (Recommended) + +Start both services at once: +```bash +yarn docker:up +``` + +Stop both services: +```bash +yarn docker:down +``` + +View logs: +```bash +yarn docker:logs +``` + +### Option 2: Using Individual Docker Commands + +#### Server Only +```bash +# Build and run server +yarn docker:server + +# Or build and run separately +yarn docker:server:build +yarn docker:server:run +``` + +#### Client Only +```bash +# Build and run client +yarn docker:client + +# Or build and run separately +yarn docker:client:build +yarn docker:client:run +``` + +## Available Scripts + +| Script | Description | +|--------|-------------| +| `yarn docker:server` | Build and run server container | +| `yarn docker:client` | Build and run client container | +| `yarn docker:server:build` | Build server container only | +| `yarn docker:client:build` | Build client container only | +| `yarn docker:server:run` | Run existing server container | +| `yarn docker:client:run` | Run existing client container | +| `yarn docker:up` | Start both services with Docker Compose | +| `yarn docker:down` | Stop both services | +| `yarn docker:build` | Build both containers | +| `yarn docker:logs` | View logs from both services | + +## Ports + +- **Client**: http://localhost:3000 +- **Server**: http://localhost:3002 + +## Development Features + +### Hot Reload +Both containers include volume mounts for hot reloading during development: +- Server changes in `./server/` will trigger nodemon restart +- Client changes in `./src/` will trigger React development server reload + +### Environment Variables +The containers will use your `.env` file for environment variables. Make sure to create this file with your Twilio credentials and other configuration. + +## Production Build + +For production, you can modify the Dockerfiles to build the React app and serve it from the Express server: + +1. Build the React app: `yarn build` +2. Serve static files from the Express server +3. Use a single container for both client and server + +## Troubleshooting + +### Port Already in Use +If you get port conflicts, make sure no other services are running on ports 3000 or 3002: +```bash +# Stop existing containers +yarn docker:down + +# Kill any processes using the ports +lsof -ti:3000 | xargs kill -9 +lsof -ti:3002 | xargs kill -9 +``` + +### Environment Variables +Ensure your `.env` file contains all required variables: +- Twilio Account SID +- Twilio Auth Token +- Workspace SID +- Workflow SID +- Other configuration variables + +### Container Networking +The containers are connected via a Docker network called `twilio-widget-network`. The client can reach the server at `http://server:3002` within the Docker network. + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..1094be7c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +# Build stage +FROM node:18-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package.json yarn.lock ./ + +# Install dependencies +RUN yarn install --frozen-lockfile + +# Copy source code +COPY . . + +# Build the React app +RUN yarn build + +# Production stage +FROM node:18-alpine + +WORKDIR /app + +# Copy package files +COPY package.json yarn.lock ./ + +# Install only production dependencies +RUN yarn install --frozen-lockfile --production + +# Copy built React app from builder stage +COPY --from=builder /app/build ./build + +# Copy server files +COPY server ./server + +# Create a non-root user +RUN addgroup -g 1001 -S nodejs +RUN adduser -S nodejs -u 1001 + +# Change ownership of the app directory +RUN chown -R nodejs:nodejs /app +USER nodejs + +# Expose the port the server runs on +EXPOSE 3002 + +# Start the server +CMD ["yarn", "server:ci"] diff --git a/Dockerfile.client b/Dockerfile.client new file mode 100644 index 00000000..bab0bd92 --- /dev/null +++ b/Dockerfile.client @@ -0,0 +1,27 @@ +# Use Node.js 18 Alpine for smaller image size +FROM node:18-alpine + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package.json yarn.lock ./ + +# Install dependencies +RUN yarn install --frozen-lockfile --production=false + +# Copy source code +COPY public/ ./public/ +COPY src/ ./src/ +COPY tsconfig.json ./ +COPY config-overrides.js ./ + +# Expose port +EXPOSE 3000 + +# Set environment variables +ENV NODE_ENV=development +ENV PORT=3000 + +# Start the React development server +CMD ["yarn", "start", "--host", "0.0.0.0"] diff --git a/Dockerfile.server b/Dockerfile.server new file mode 100644 index 00000000..7d7f658a --- /dev/null +++ b/Dockerfile.server @@ -0,0 +1,27 @@ +# Use Node.js 18 Alpine for smaller image size +FROM node:18-alpine + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package.json yarn.lock ./ + +# Install dependencies +RUN yarn install --frozen-lockfile --production=false + +# Copy server source code +COPY server/ ./server/ +COPY scripts/ ./scripts/ + +# Create build directory for static files +RUN mkdir -p build + +# Expose port +EXPOSE 3002 + +# Set environment variables +ENV NODE_ENV=development + +# Start the server +CMD ["yarn", "server"] diff --git a/STUDIO_FLOW_SETUP.md b/STUDIO_FLOW_SETUP.md new file mode 100644 index 00000000..40d63078 --- /dev/null +++ b/STUDIO_FLOW_SETUP.md @@ -0,0 +1,103 @@ +# Studio Flow Integration Setup + +This webchat widget now triggers a Twilio Studio Flow instead of creating TaskRouter tasks directly. + +## Required Environment Variables + +Add these variables to your `.env` file: + +```env +# Studio Flow Configuration +STUDIO_FLOW_SID=FWxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# Conversations Service (already configured) +CONVERSATIONS_SERVICE_SID=ISxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# Optional: Keep TaskRouter config for fallback or Studio Flow integration +TASKROUTER_WORKSPACE_SID=WSxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +TASKROUTER_WORKFLOW_SID=WWxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +## Setup Steps + +### 1. Create a Studio Flow +1. Go to Twilio Console → Studio → Flows +2. Create a new flow or use an existing one +3. Copy the Flow SID (starts with `FW`) + +### 2. Configure Flow Parameters +Your Studio Flow will receive these parameters: +- `conversationSid`: The Twilio Conversations SID +- `customerName`: Customer's friendly name +- `customerEmail`: Customer's email address +- `customerQuery`: Customer's initial query +- `channelType`: Set to 'webchat' +- `direction`: Set to 'inbound' +- Any additional form data from the pre-engagement form + +### 3. Flow Design Options + +#### Option A: Direct to Flex +- Use a "Connect to Flex" widget in your Studio Flow +- Pass the conversation SID and other parameters to Flex +- Flex will create the TaskRouter task automatically + +#### Option B: Custom Logic Before Flex +- Add widgets for form validation, routing logic, or business rules +- Use "Connect to Flex" as the final step +- You can add delays, conditional routing, or automated responses + +#### Option C: No Flex Integration +- Handle the entire conversation in Studio Flow +- Use "Send to Flex" widget only when human agent is needed + +### 4. Example Flow Structure +``` +Trigger → Set Variables → [Optional Logic] → Connect to Flex → End +``` + +### 5. Flow Trigger Configuration +- **Trigger Type**: Webhook or REST API +- **To**: Will be set to the conversation SID automatically +- **From**: Will be set to your Conversations Service SID automatically +- **Parameters**: All form data and conversation details + +## Benefits + +✅ **Flexible Pre-Processing** +- Form validation and enrichment +- Business logic and routing rules +- Automated responses before agent assignment + +✅ **Better Control** +- Conditional routing based on form data +- Integration with external systems +- Custom business workflows + +✅ **Enhanced Analytics** +- Flow execution tracking +- Step-by-step analytics +- Custom metrics and reporting + +## Testing + +1. Start the server: `npm run server` +2. Open the webchat widget +3. Fill out the form and submit +4. Check Studio Flow console for the new execution +5. Verify the flow parameters are passed correctly + +## Troubleshooting + +- **Flow not triggering**: Check `STUDIO_FLOW_SID` is correct +- **Conversation SID missing**: Verify `CONVERSATIONS_SERVICE_SID` is set +- **Parameters missing**: Verify form data is being passed correctly +- **Flow execution fails**: Check Studio Flow logs for detailed error messages + +## Alternative: Webhook-Based Approach + +If you prefer to use webhooks instead of direct API calls, you can: + +1. Set up a webhook endpoint in your Studio Flow +2. Modify the controller to send a webhook request instead of using the Studio API +3. This approach can be more flexible for complex integrations diff --git a/TASKROUTER_SETUP.md b/TASKROUTER_SETUP.md new file mode 100644 index 00000000..b9342692 --- /dev/null +++ b/TASKROUTER_SETUP.md @@ -0,0 +1,99 @@ +# TaskRouter Integration Setup + +This webchat widget now creates TaskRouter tasks for full Flex Insights coverage and control. + +## Required Environment Variables + +Create a `.env` file in the `server` directory with these variables: + +```env +# Twilio Account Credentials +ACCOUNT_SID=your_twilio_account_sid +AUTH_TOKEN=your_twilio_auth_token + +# Twilio API Keys (for token generation) +API_KEY=your_twilio_api_key +API_SECRET=your_twilio_api_secret + +# Twilio Conversations Service +CONVERSATIONS_SERVICE_SID=your_conversations_service_sid + +# TaskRouter Configuration +TASKROUTER_WORKSPACE_SID=your_taskrouter_workspace_sid +TASKROUTER_WORKFLOW_SID=your_taskrouter_workflow_sid + +# Optional: SendGrid for email transcripts +SENDGRID_API_KEY=your_sendgrid_api_key +FROM_EMAIL=your_verified_email@domain.com +``` + +## Setup Steps + +### 1. Get Twilio Credentials +- **ACCOUNT_SID** and **AUTH_TOKEN**: From Twilio Console → Account → API Keys & Tokens +- **API_KEY** and **API_SECRET**: Create new API key in Twilio Console → Account → API Keys & Tokens + +### 2. Get Conversations Service SID +- Go to Twilio Console → Conversations → Services +- Create a new service or use existing one +- Copy the Service SID + +### 3. Get TaskRouter Workspace SID +- Go to Twilio Console → TaskRouter → Workspaces +- Use your Flex workspace or create a new one +- Copy the Workspace SID + +### 4. Create TaskRouter Workflow +- Go to Twilio Console → TaskRouter → Workspaces → [Your Workspace] → Workflows +- Create a new workflow for webchat tasks +- Copy the Workflow SID + +### 5. Configure Workflow +Your workflow should route tasks to agents. Example workflow configuration: + +```json +{ + "task_routing": { + "default_filter": { + "queue": "your_agent_queue_sid" + } + } +} +``` + +### 6. Set Up Webhooks (Optional) +To automatically complete tasks when conversations end: + +1. Go to Twilio Console → Conversations → Services → [Your Service] → Webhooks +2. Add webhook URL: `https://your-server.com/webhook/conversation` +3. Select events: `conversation.ended` + +## Benefits + +✅ **Full Flex Insights Coverage** +- Task lifecycle tracking +- Queue performance metrics +- Agent productivity data +- Custom attributes and routing data + +✅ **Advanced Control** +- Custom routing logic +- Skill-based assignment +- Queue management +- SLA tracking + +✅ **Complete Analytics** +- Task routing decisions +- Wait times +- Agent assignment times +- Custom business metrics + +## Testing + +1. Start the server: `cd server && npm start` +2. Open the webchat widget +3. Fill out the form and submit +4. Check TaskRouter console for the new task +5. Assign the task to an agent in Flex + +The task will include all customer information and can be routed based on your workflow configuration. diff --git a/cypress/plugins/helpers/twilioClient.ts b/cypress/plugins/helpers/twilioClient.ts index 2dc120d5..3fe6ef7e 100644 --- a/cypress/plugins/helpers/twilioClient.ts +++ b/cypress/plugins/helpers/twilioClient.ts @@ -7,6 +7,10 @@ export const getTwilioClient = () => { return twilioClient; } - twilioClient = new Twilio(process.env.ACCOUNT_SID!, process.env.AUTH_TOKEN!); + twilioClient = new Twilio( + process.env.API_KEY!, + process.env.API_SECRET!, + { accountSid: process.env.ACCOUNT_SID! } + ); return twilioClient; }; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..1ac09f98 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +services: + server: + build: + context: . + dockerfile: Dockerfile.server + ports: + - "3002:3002" + environment: + - NODE_ENV=development + # Explicitly set key environment variables + - ACCOUNT_SID=${ACCOUNT_SID} + - API_KEY=${API_KEY} + - API_SECRET=${API_SECRET} + - CONVERSATIONS_SERVICE_SID=${CONVERSATIONS_SERVICE_SID} + - TASKROUTER_WORKSPACE_SID=${TASKROUTER_WORKSPACE_SID} + - TASKROUTER_WORKFLOW_SID=${TASKROUTER_WORKFLOW_SID} + - STUDIO_FLOW_SID=${STUDIO_FLOW_SID} + - WORKSPACE_SID=${WORKSPACE_SID} + - RATING_WORKFLOW_SID=${RATING_WORKFLOW_SID} + - ALLOWED_ORIGINS=${ALLOWED_ORIGINS} + - SENDGRID_API_KEY=${SENDGRID_API_KEY} + - FROM_EMAIL=${FROM_EMAIL} + env_file: + - .env + volumes: + # Mount source code for development (hot reload) + - ./server:/app/server + - ./scripts:/app/scripts + networks: + - twilio-widget-network + + client: + build: + context: . + dockerfile: Dockerfile.client + ports: + - "3000:3000" + environment: + - NODE_ENV=development + - PORT=3000 + - REACT_APP_SERVER_URL=http://localhost:3002 + env_file: + - .env + volumes: + # Mount source code for development (hot reload) + - ./src:/app/src + - ./public:/app/public + - ./tsconfig.json:/app/tsconfig.json + - ./config-overrides.js:/app/config-overrides.js + depends_on: + - server + networks: + - twilio-widget-network + +networks: + twilio-widget-network: + driver: bridge diff --git a/iac/terraform/README.md b/iac/terraform/README.md new file mode 100644 index 00000000..7f2ed1f4 --- /dev/null +++ b/iac/terraform/README.md @@ -0,0 +1,109 @@ +# Twilio Webchat Widget Terraform Configuration + +This Terraform configuration deploys the Twilio Webchat Widget as two separate ECS services: + +## Architecture + +### Services +1. **Webchat Server** (`webchat-widget-server.tf`) + - Express.js backend API + - Port: 3002 + - Health check: `/initWebchat` + - Handles Twilio Conversations, TaskRouter, and rating functionality + +2. **Webchat Client** (`webchat-widget-client.tf`) + - React frontend application + - Port: 3000 + - Health check: `/` + - Serves the webchat widget interface + +### Networking +- **Server Internal FQDN**: `webchat-widget-server-internal.anyvan.com` +- **Client Internal FQDN**: `webchat-widget-client-internal.anyvan.com` +- **Client → Server Communication**: Client connects to server via internal FQDN + +## Deployment + +### Prerequisites +- AWS CLI configured with appropriate profile +- Terraform installed +- Docker images built and pushed to ECR + +### Environment Variables Required +- `server_container_image_url`: ECR URL for server container +- `client_container_image_url`: ECR URL for client container +- `server_internal_fqdn`: Internal FQDN for server service +- `client_internal_fqdn`: Internal FQDN for client service + +### Commands + +#### Production +```bash +cd terraform +terraform init +terraform plan -var-file=env/production.tfvars +terraform apply -var-file=env/production.tfvars +``` + +#### Staging +```bash +cd terraform +terraform init +terraform plan -var-file=env/staging.tfvars +terraform apply -var-file=env/staging.tfvars +``` + +#### Testing (PR Environment) +```bash +cd terraform +terraform init +terraform plan -var-file=env/testing.tfvars +terraform apply -var-file=env/testing.tfvars +``` + +## Configuration Files + +### Main Configuration +- `webchat-widget-server.tf`: Server ECS service configuration +- `webchat-widget-client.tf`: Client ECS service configuration +- `variables.tf`: Variable definitions +- `locals.tf`: Local variable calculations +- `providers.tf`: Terraform provider configuration + +### Environment-Specific +- `env/production.tfvars`: Production environment variables +- `env/staging.tfvars`: Staging environment variables +- `env/testing.tfvars`: Testing environment variables + +### Monitoring +- `datadog_monitor.tf`: Datadog monitoring configuration +- `datadog_dashboard.tf`: Datadog dashboard configuration +- `datadog_service_definition.tf`: Datadog service definition + +## Container Images + +### Server Container +- **Dockerfile**: `Dockerfile.server` +- **Port**: 3002 +- **Environment**: Node.js with Express.js +- **Features**: Twilio API integration, rating system, TaskRouter + +### Client Container +- **Dockerfile**: `Dockerfile.client` +- **Port**: 3000 +- **Environment**: Node.js with React +- **Features**: Webchat widget UI, real-time messaging + +## Secrets Management +- Twilio credentials stored in AWS Secrets Manager +- Referenced via `data.aws_secretsmanager_secret.twilio-secrets.arn` + +## Monitoring +- CPU and Memory monitoring for both services +- Error log monitoring +- Datadog integration for metrics and logs + +## Scaling +- Both services use Fargate for serverless container management +- CPU and Memory limits configurable per environment +- Auto-scaling based on demand diff --git a/iac/terraform/datadog_dashboard.tf b/iac/terraform/datadog_dashboard.tf new file mode 100644 index 00000000..1a42b459 --- /dev/null +++ b/iac/terraform/datadog_dashboard.tf @@ -0,0 +1,13 @@ +resource "datadog_dashboard_json" "dashboard_json" { + dashboard = < ${local.critical_threshold}" + critical_threshold = local.critical_threshold + enable_logs_sample = true + tags = [ + "env:${var.env}", + "team:${local.team}", + "service:${var.webchat_widget_name}" + ] + priority = 3 +} + +module "cpu_monitor" { + source = "terraform-registry.anyvan.com/anyvan/datadog_monitor/aws" + version = "~> 1.0" + + name = "Anyvan ${var.webchat_widget_name} CPU usage monitor - ${local.environment_name}" + message = "CPU usage of ${var.webchat_widget_name} ${local.notification_channels}${var.env == "production" ? " | @${local.team}-engineers" : ""}" + type = "query alert" + query = "avg(last_5m):avg:aws.ecs.service.cpuutilization{servicename:${module.webchat_server.service_name}} > 90" + critical_threshold = 90 + notify_no_data = true + no_data_timeframe = 20 // There is a 15+ mins delay in receiving ECS metrices on Datadog + tags = [ + "env:${var.env}", + "team:${local.team}", + "service:${var.webchat_widget_name}" + ] + priority = 5 +} + + +module "memory_monitor" { + source = "terraform-registry.anyvan.com/anyvan/datadog_monitor/aws" + version = "~> 1.0" + + name = "Anyvan ${var.webchat_widget_name} Memory usage monitor - ${local.environment_name}" + message = "Memory usage of ${var.webchat_widget_name} ${local.notification_channels}${var.env == "production" ? " | @${local.team}-engineers" : ""}" + type = "query alert" + query = "avg(last_5m):avg:aws.ecs.service.memory_utilization{servicename:${module.webchat_server.service_name}} > 90" + critical_threshold = 90 + notify_no_data = true + no_data_timeframe = 20 // There is a 15+ mins delay in receiving ECS metrices on Datadog + tags = [ + "env:${var.env}", + "team:${local.team}", + "service:${var.webchat_widget_name}" + ] + priority = 5 +} diff --git a/iac/terraform/datadog_service_definition.tf b/iac/terraform/datadog_service_definition.tf new file mode 100644 index 00000000..d1b6c84f --- /dev/null +++ b/iac/terraform/datadog_service_definition.tf @@ -0,0 +1,20 @@ +module "datadog_service_definition_yaml" { + source = "terraform-registry.anyvan.com/anyvan/datadog_service_definition_yaml/aws" + version = "~> 1.0" + + service_definition_yaml = <", var.jira_ticket_number) : var.internal_fqdn + external_fqdn = var.env == "development" ? replace(var.external_fqdn, "", var.jira_ticket_number) : var.external_fqdn + + # Server and Client specific FQDNs + server_internal_fqdn = var.env == "development" ? replace(var.server_internal_fqdn, "", var.jira_ticket_number) : var.server_internal_fqdn + client_internal_fqdn = var.env == "development" ? replace(var.client_internal_fqdn, "", var.jira_ticket_number) : var.client_internal_fqdn +} diff --git a/iac/terraform/providers.tf b/iac/terraform/providers.tf new file mode 100644 index 00000000..88235fa3 --- /dev/null +++ b/iac/terraform/providers.tf @@ -0,0 +1,89 @@ +terraform { + required_version = "~>1.9.5" # we pin the Terraform version to guarantee stability + required_providers { + aws = { + source = "hashicorp/aws" + version = ">=3.68.0" # we pin the AWS provider version to guarantee stability + } + datadog = { + source = "DataDog/datadog" + } + } + + backend "s3" { + bucket = "anyvan-terraform-state-file-aws-accounts" # name of the bucket + key = "twilio-webchat-widget.tfstate" # name of the state file for this particular terraform stack + region = "eu-west-1" # this is the region where the bucket is created. It can be in a different region to where you deploy your resources. + dynamodb_table = "anyvan-terraform-state-file-lock-aws-accounts" # table where the lock is kept + } +} + +provider "aws" { + region = var.region + profile = var.profile + + default_tags { + tags = local.default_tags + } +} + +provider "aws" { + region = var.region + profile = "production" + alias = "production" + + default_tags { + tags = local.default_tags + } +} + +provider "aws" { + region = var.region + profile = "horizontal" + alias = "horizontal" + + default_tags { + tags = local.default_tags + } +} + +provider "datadog" { + // This then require environmental variables DD_API_KEY, DD_APP_KEY and DD_HOST be set + // These are all set in the circleci anyvan context. + validate = false +} + +data "terraform_remote_state" "top" { + backend = "s3" + workspace = "prod-anyvan_com" + + config = { + bucket = "anyvan-terraform-state-file-aws-accounts" + key = "iac-terraform-top.tfstate" + region = "eu-west-1" + dynamodb_table = "anyvan-terraform-state-file-lock-aws-accounts" + } +} + +data "terraform_remote_state" "accounts" { + backend = "s3" + + config = { + bucket = "anyvan-terraform-state-file-aws-accounts" + key = "iac-terraform-accounts/iac-terraform-accounts.tfstate" + region = "eu-west-1" + dynamodb_table = "anyvan-terraform-state-file-lock-aws-accounts" + } +} + +data "terraform_remote_state" "network" { + backend = "s3" + workspace = "live-${var.vpc_environment}_vpc" + + config = { + bucket = "anyvan-terraform-state-file-aws-accounts" + key = "iac-terraform-network.tfstate" + region = "eu-west-1" + dynamodb_table = "anyvan-terraform-state-file-lock-aws-accounts" + } +} \ No newline at end of file diff --git a/iac/terraform/secrets_manager.tf b/iac/terraform/secrets_manager.tf new file mode 100644 index 00000000..1203f789 --- /dev/null +++ b/iac/terraform/secrets_manager.tf @@ -0,0 +1,5 @@ +data "aws_secretsmanager_secret" "twilio-secrets" { + name = local.is_production ? "prod-twilio-flex-secret" : "stage-twilio-flex-secret" +} + + diff --git a/iac/terraform/variables.tf b/iac/terraform/variables.tf new file mode 100644 index 00000000..b64f6b76 --- /dev/null +++ b/iac/terraform/variables.tf @@ -0,0 +1,99 @@ +variable "region" { + type = string + description = "Default region for AWS" +} +variable "env" { + type = string + description = "Default environment for AWS" +} +variable "profile" { + type = string + description = "Default profile for AWS" +} + +variable "vpc_environment" { + type = string + description = "Which of the shared VPCs to deploy into." + + validation { + condition = contains(["development", "staging", "production"], var.vpc_environment) + error_message = "Valid values for vpc_environment are: development, staging, production." + } +} +variable "terraform_version" { + type = string + description = "Version of terraform to use" +} +variable "commit_hash" { + type = string + description = "Github commit hash" +} +variable "webchat_widget_name" { + type = string + description = "The name of the webchat widget" + default = "twilio-webchat-widget" +} + +variable "jira_ticket_number" { + type = string + default = "" + description = "Jira ticket number that we are using to build a stack in Staging for a particular PR" +} + +variable "git_sha" { + type = string + default = "" + description = "Git SHA for the current commit" +} + +variable "server_container_image_url" { + type = string + description = "URL of the server container image in ECR" +} + +variable "client_container_image_url" { + type = string + description = "URL of the client container image in ECR" +} + +variable "aws_region" { + type = string + default = "eu-west-1" +} + +variable "internal_fqdn" { + type = string + default = "" + description = "Internal FQDN (private endpoint)" +} + +variable "server_internal_fqdn" { + type = string + default = "" + description = "Server internal FQDN (private endpoint)" +} + +variable "client_internal_fqdn" { + type = string + default = "" + description = "Client internal FQDN (private endpoint)" +} + +variable "external_fqdn" { + type = string + description = "External FQDN (public endpoint)" +} + +variable "fargate_task_cpu" { + type = number + description = "The size of CPU (in Amazon units) to give to the Fargate task. By default, set using environmental variables" +} + +variable "fargate_task_memory" { + type = number + description = "The amount of memory (in MB) to give to the Fargate task. By default, set using environmental variables" +} +variable "squad" { + type = string + default = "lions" +} diff --git a/iac/terraform/webchat-widget-client.tf b/iac/terraform/webchat-widget-client.tf new file mode 100644 index 00000000..33d7b843 --- /dev/null +++ b/iac/terraform/webchat-widget-client.tf @@ -0,0 +1,53 @@ +module "webchat_client" { + source = "terraform-registry.anyvan.com/anyvan/ecs_project/aws" + version = "~>1.6.2" + + app_name = "webchat-client" + env = var.env + jira_ticket_number = var.jira_ticket_number + application_container_image_url = var.client_container_image_url + commit_hash = var.git_sha + region = var.aws_region + profile = var.profile + internal_fqdn = local.client_internal_fqdn + + cpu = var.fargate_task_cpu + memory = var.fargate_task_memory + + expose_internal = { + container_port = 3000 + health_check_path = "/" + health_check_grace_period_seconds = 300 + listener_port = 443 + } + + expose_external = { + container_port = 3000 + health_check_path = "/" + health_check_grace_period_seconds = 240 + listener_port = 443 + } + + task_permissions = { + allow_sm = { + sm_arns = [] + key_arns = [] + } + ecs_task_statements = [] + } + + env_variables = { + NODE_ENV = "production" + PORT = "3000" + REACT_APP_SERVER_URL = "https://${local.server_internal_fqdn}" + } + + env_secrets = {} + enable_appconfig = false + enable_database = false + + providers = { + aws = aws + aws.production = aws.production + } +} diff --git a/iac/terraform/webchat-widget-server.tf b/iac/terraform/webchat-widget-server.tf new file mode 100644 index 00000000..8b36627f --- /dev/null +++ b/iac/terraform/webchat-widget-server.tf @@ -0,0 +1,53 @@ +module "webchat_server" { + source = "terraform-registry.anyvan.com/anyvan/ecs_project/aws" + version = "~>1.6.2" + + app_name = "webchat-server" + env = var.env + jira_ticket_number = var.jira_ticket_number + application_container_image_url = var.server_container_image_url + commit_hash = var.git_sha + region = var.aws_region + profile = var.profile + internal_fqdn = local.server_internal_fqdn + + cpu = var.fargate_task_cpu + memory = var.fargate_task_memory + + expose_internal = { + container_port = 3002 + health_check_path = "/initWebchat" + health_check_grace_period_seconds = 300 + listener_port = 443 + } + + expose_external = { + container_port = 3002 + health_check_path = "/initWebchat" + health_check_grace_period_seconds = 240 + listener_port = 443 + } + + task_permissions = { + allow_sm = { + sm_arns = [ + data.aws_secretsmanager_secret.twilio-secrets.arn + ] + key_arns = [] + } + ecs_task_statements = [] + } + + env_variables = { + NODE_ENV = "production" + } + + env_secrets = {} + enable_appconfig = false + enable_database = false + + providers = { + aws = aws + aws.production = aws.production + } +} diff --git a/package.json b/package.json index a2db70c4..148ac47e 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,17 @@ "bootstrap": "node scripts/bootstrap", "e2eCleanupExistingTasks": "node scripts/e2eCleanupExistingTasks", "deploy": "yarn build && node scripts/deploy", - "eject": "react-app-rewired eject" + "eject": "react-app-rewired eject", + "docker:server": "docker build -f Dockerfile.server -t twilio-widget-server . && docker run -p 3002:3002 --env-file .env twilio-widget-server", + "docker:client": "docker build -f Dockerfile.client -t twilio-widget-client . && docker run -p 3000:3000 --env-file .env twilio-widget-client", + "docker:server:build": "docker build -f Dockerfile.server -t twilio-widget-server .", + "docker:client:build": "docker build -f Dockerfile.client -t twilio-widget-client .", + "docker:server:run": "docker run -p 3002:3002 --env-file .env twilio-widget-server", + "docker:client:run": "docker run -p 3000:3000 --env-file .env twilio-widget-client", + "docker:up": "docker-compose up", + "docker:down": "docker-compose down", + "docker:build": "docker-compose build", + "docker:logs": "docker-compose logs -f" }, "browserslist": [ ">0.2%", diff --git a/public/index.html b/public/index.html index 5ddb00b0..e668d804 100644 --- a/public/index.html +++ b/public/index.html @@ -10,6 +10,7 @@ homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ --> +