Rails 5 API application for acting as a bridge between Git service webhooks and external Ticket Tracking system
- Feature List
- Handover Checklist
- Good to Haves
- Local setup
- Implementation Details
- Mentionables
- Post Script
- Next Steps
- Roadmap - PR #1
- Setup RVM and Ruby - PR #2
- Setup commit flow hooks using OverCommit - PR #3
- Standard OverCommit Hooks
- Rubocop and Shopify Rubocop yaml - Static Code Analyzer
- rails_best_practices - Code quality metric tool (downside checks whole project on every commit)
- rails_schema_up_to_date
- Brakeman - Security vulnerabilities spotter
- Fasterer - Speed improvement suggestions
- Forbidden Branches
- PostCheckout Hooks
- Setup Rails 5 API app - PR #4
- Setup Testing - PR #5
- Active Admin and Devise Setup (basic) - PR #6
- Schema Modelling - PR #7
-
User
-
Event
-
Commit
-
EventCommit
-
Project
-
Ticket
-
TicketCommit
-
Release
-
Repository
- Generate the Entity Relationship Diagram using
rails-erd
-
- Incoming Webhook - PR #8
- Base API Controller
- Base Webhook Controller
- Webhook API
- Base Service - PR #9
- Demo Service - PR #9
- Event Parser Strategy Service - PR #10
- Exception Middlewares - PR #11
- Pull Request Parser Service - PR #13
- Push Request Parser Service - PR #12
- Release Request Parser Service - PR #14
- Commit Creation - PR #17
- Refactoring Services - PR #18
- HTTP Facade Layer using Faraday - PR #19
- DotEnv External Token management
- External Exception Management
- Communicator Layer using Faraday
- API Client
- Echo controller for testing
- Outgoing Webhook - PR #20
- Payload generator module/helper
- Service calling the API Client
- State Management using
EventCommitSync
model - Integrating
bangable
Outgoing webhook service in the controller layer - Exception propagation to the incoming webhook
- Echo Endpoint testing using
Puma
for multithreading
- CORS using Rack CORS - PR #21
- API throttling using Rack Attack - PR #22
- Incoming Webhooks Token Based Auth - PR #23
- Adding
ApiClient
model - Adding Token based Authentication action in the
BaseWebhookController
- Exception Management in case of invalid requests
- Adding
- Model RSpec - PR #24
- Service RSpec
- Service Edge Cases RSpec - P. S.
- Controller Test Cases
- Token based authentication controller testing - PR #30
- Stubbing external services success flow
- Stubbing external services error flow for EventParser
- Stubbing external services error flow for Ticket Tracking API
- Documentation - PR #26
- Server error management with Sentry - PR #28
- High Level Active Admin setup - PR #29
- RSpec for Event Commit model - PR #31
- RSpec for Push Event Parser and Release Event Parser Service - PR #37
- Integrate WebMock for mocking external calls - PR #38 -[x] RSpec for SyncEventCommitsWithTrackingApi Service using WebMock - PR #38
- Write Documentation
- Update
.env.example
- Attach Postman collection
- More Database Indexes (after API profiling ;) )
- More Application Model Validations
- Immutability Concern
- PaperTrail in case of changing the data
- Cleaner module/namespace specific routing and controller policy as the application grows
- Writing a generator for quickstarting services
ActiveSerializer
for better serialization- Avoid N+1 queries by using joins and include at appropriate places
- Using
JSON validator
to validate the payload before saving in the model - More test coverage
- Make sure you have a Postgres version greater than 9.6
- Use RVM to create a gemset across Ruby version 2.6.0 using the command
rvm use 2.6.0@commit-bridge --create
- Clone the repo
- Install the dependencies using the command
bundle install
- Before development, install the precommit hooks using
overcommit --install
overcommit --sign
- The database can be created using the command
rake db:create
- generate the schema using
rake db:migrate
- The Active Admin related seed data can be generated using the command
rake db:seed
- You can start the server using
rails s
or use the Rails consolerails c
bundle exec puma
for running the Rails Server in multi threading mode (for Webhook external API echo feature)- ActiveAdmin is installed for having a visual representation of the data. Log in the admin panel at
localhost:3000/admin
using thesecure
credentials
username: admin@commit-bridge.com
password: commit-bridge-123
- The defined seed values can be found over here
- If you want to test the API throttling using Redis, setup Redis and start the Redis server
- Change the
.env.example
as required to match your setup
- The application is made using Ruby 2.6 and Rails 5.2
- This is an API only application (with the exception of Active Admin)
- The Entity relationship diagram is present for the application schema is present over here
- The ERD is autogenerated after every
rails db:migrate
- The application uses an Model View Controller Service Design paradigm
- Services are used as business logic containers. More information can be found over here.
- Active Admin is used for high level view of the data
- Environment variables are supported through
dotenv-rails
- Sentry is used for exception and stack trace management
- Redis and Rack Attack for API throttling
- RSpec, Factory Bot, Shoulda Matchers, WebMock and Faker for writing Test cases
- Faraday for making external web requests
- Postgres as Datastore
- Generating models based on the Payload requirements
- The ERD is generated through
rails-erd
- ApiClient
- User
- Event
- Commit
- EventCommit
- EventCommitSync
- Project
- Ticket
- TicketCommit
- EventTicket
- Release
- Repository
For brevity, the application journey is as follows:
- A
User
generates anEvent
through anApiClient
credentials - An
Event
containsCommits
Commit
is a part of one or moreTickets
Commit
belongs to aUser
Tickets
belong to aProject
Release
is a specialEvent
which is made by aUser
by submitting multipleCommits
Event
is attached to aRepository
- An
Event
when registered belongs to one or moreTickets
and a singleTicket
can be across multipleEvents
ApplicationController
is required for makingDevise
/Active Admin
workApiController
should be the base controller from which our all API controllers to be subclassed from- All webhook controllers should be subclassed from
BaseWebhookController
- The Git Cloud service consumes the controller
GitCloudWebhookController#receive
- Exceptions are managed through
ExceptionHandler
concern - Responses are generated through
Response
concern - Internal custom Exceptions such as
CommitBridgeValidationError
are defied inCommitBridgeExecptions
- Token based authentication is used for the public facing APIs with the
api_key
present inApiClient
model - The application echoes the Ticket Tracking Application API through
GitCloudWebhookController#echo
for development purposes
- Validations can be broadly categories as two types: Business Related and Data Integrity related
- For Non CRUD operations, sometimes a series of business rules need to be followed to make persistent changes in the application
- Complex business processing logic is stored in service containers
- Service containers are not a replacement for model and data integrity logic
- This web service works on a MVSC design paradigm
- Services can be nested in other services
- Services can be standalone software components or common logic can be extracted into Helper Concerns
- The disadvantage of DRYing out in Helper Concerns is increased maintained complexity and coupling between multiple services through the Concerns
- Consistent interface for service consumers
- Cross component Pluggable eg. Using in Models, Rake tasks, Controllers, background Jobs, other Services
- Consistent API modelled similar to ActiveRecord
- Extendability allows for more powerful abstractions
- One more abstraction
- What should be a "Service" ambiguity
- Service or Helper decision
- Black boxes/holes of deeply nested logic
- The Base Service is classified as
ApplicationService
- A
DemoService
showcasing how services work in the application layer
- The
EventParserService
is the parent service used by the webhook and abstract the persistent data creation logic - It is consumed by the controller to parse the payload and find the required payload parser service
PullRequestParser
is used to parse payloads for Pull Request EventsPushRequestParser
is used to parse payloads for Push Request EventsReleaseRequestParser
is used to parse payloads for Release Request EventsCommitParser
is used to parse the commit payload in order to create or update commits, tickets and projectsSyncEventCommitsWithTrackingApi
is used to make Ticket Tracking API requests with appropriateCommunicator
- Token based Authentication is considered for this application
- Incoming Webhooks need to provide token which are stored in the
ApiClient
model - Auth needs to be provided in the request headers eg.
Content-Type:application/json
Authorization:Token token=fK2qGmQa73iH758DAuaWphtk
-
API keys can be expired by changing the
expiry
value -
The authentication happens in the
BaseWebhookController
and can be customized if required -
The following scenarios are possible:
- Status code
401
Unauthorized when the key has expired with the payload
{ "error_message": "API Key Expired!" }
- Status code
403
when an invalid key is provided
{ "error_message": "Please provide a valid token!" }
- Status code
403
when no headers are provided
{ "error_message": "Please provide Auth Headers!" }
- Status code
-
Future scope: Moving the authentication from simple Token based to JWT and exposing refresh token functionality over an API
- We are assuming the Ticket Tracking application operates on Token Based Authentication
- The
Communicator
extracts the token from the environment variables and sets it in the headers
- Incoming webhooks should post at
localhost:3000/webhooks/git/
with valid API Key as the token - The request headers look like
Content-Type:application/json
Authorization:Token token=fK2qGmQa73iH758DAuaWphtk
- Please import the Postman collection to interact with the application
- Sample response can also be found over here
- The External API credentials should be maintained using environment variables in the
.env
file - External API are being communicated through a facade which is used to abstract out the complexity of request creation and response parsing
- The Ticket Tracking API client is present over here
- The Communicator is used to create headers, send web request and process responses and exceptions
- We are assuming the Ticket Tracking API has token based auth and token is stored in the
.env
- Example usage of the client
new_client = TicketTrackingApi::Client.new()
payload = {"query": "released", "issues": [{"id": 66}]}
response = new_client.update_tickets_across_commit(payload)
puts response
- The client is consumed through a [service] which is tasked with the payload generation and navigate code flow based on external client exception (
ExternalApiException
)
- Application layer custom exceptions are registered in
CommitBridgeExceptions
- External API Exceptions are registered in
ExternalExceptions
and these are based class from Application ExceptionExternalApiException
- Status codes for parsing external requests are registered in
HttpStatusCode
- Messages for client/consumer facing interface can be registered in
Message
- API Exceptions, both internal and external are handled by the application
ExceptionHandler
- The APIController includes the
ExceptionHandler
and entire application is subclassed from this controller
- Test cases are written using RSpec
- A few helper RSpec utils are placed in
CommitBridgeSpecHelper
- FactoryBot is used to working with models
- Faker is used to generate fake data instead of hardcoding values
- Timecop is used for moving around time in tests
- WebMock is used for mocking external API calls
- Active Admin is not optimized for production usage
- The Active Admin can be accessed at
http://localhost:3000/admin/
with the credentials generated duringseeding
username: admin@commit-bridge.com
password: commit-bridge-123
- The intent of adding an Active Admin was to have a more visual representation of the data
- The Active Admin can be extended to cover custom use cases if required
- Currently the Active Admin has just readonly mode by purpose of keeping the data immutable.
- Based on more information required by the stakeholders, the Active Admin dashboard can be created
- API throttling is implementing using Rack Attack
- The throttling policy is present in the Rack attack initializer
- The limits can be controlled through the
.env
variableDAILY_IP_REQUEST_QUOTA
- The data is store in the Redis cache attached to the application if not, it will use the in memory cache as the default
- Testing via Artillery to make sure the throttling is working as expected
- More complex throttling policies and feedbacks can be set using an approach similar to this or this with exponential back-offs and detailed logging
- Helper commands for loadtesting are present in the
helper_commands
file - Load testing response from
Artillery
when the request quota is set to10/per day/ip
Elapsed time: 1 second
Scenarios launched: 10
Scenarios completed: 10
Requests completed: 200
Mean response/sec: 147.06
Response time (msec):
min: 3.3
max: 1206.4
median: 10.9
p95: 45.1
p99: 213.6
Codes:
200: 10
429: 190
All virtual users finished
Summary report @ 12:08:03(+0200) 2020-04-05
Scenarios launched: 10
Scenarios completed: 10
Requests completed: 200
Mean response/sec: 145.99
Response time (msec):
min: 3.3
max: 1206.4
median: 10.9
p95: 45.1
p99: 213.6
Scenario counts:
0: 10 (100%)
Codes:
200: 10
429: 190
- The service can be deployed on Heroku or AWS EC2/RDS instances based on the requirements and financial capacity
- A Continuous Integration interface such as CircleCI or TravisCI which are hooked on to RSpec
- A Continuous Deployment can be easily achieved through Heroku pipelines or with AWS by Github Actions and AWS CodePipeline
- Any scripts that need to be run before a deployment is made can be done using a
Rake
tasks
Some of the key points I keep in mind while writing software:
- APIs are like User Interfaces for Developers
- Think schemas as entities or actors
- SRP and Open/Close
- Favour composition over inheritance
- Code is as good as the tests
- Consistency matters (eg. Service objects, Rubcop, Best Practise)
- Sometimes
magic
is good (execute!
and middlewares) (only sometimes*) - Make it run. Then make it run faster (if required and proven by data)
- Personal Learnings: T. A. [R. O. T.] problem solving mindset - Top/Bottom, Algorithms/Steps, Refactor, Optimize, Test. R, O, T are reordered according to priority
- In order to enforce coding standards, uniformity and consistency, the application uses precommit hooks with the help of
overcommit
- The code formatted used is
Rubocop
which is used by several open source Ruby/Rails projects to maintain code guidelines and consistency eg. Shopify, Rails - Besides this, several precommit hooks confirm that actions deemed harmful to the codebase aren't committed.
- Several more hooks as listed in the feature list are used for maintaining code quality
- The API interface is accessible using the Postman collection and is ready for client side consumption
- Specs favour fixtures, fake data and factories for operating with data
- Test cases are added to make the application layer more robust
- Test Suite can be hooked up to a CI interface
- In case a test case breaks, it should not move to CD interface
- Errors and Exceptions are managed through Sentry
- RESTful JSON payloads are returned in case of errors
- Since this application exposes Webhooks to external clients, security was of a major concern
- Client communicate using API keys for auth purposes as described above
- The API Keys can be revoked quickly and have a default expiry of 2 weeks
- In order to prevent abuse of the APIs, throttling is set into place
- The throttling limits can be configured using environment variables
- The existing keys/states of the users can be flushed out from Redis by writing a custom
rake
command - In case of status code
429
, a custom middleware can be written to intercept such responses and fire a Sentry Alert or create a log for escalation and inspection purposes respectively
- A feature list/roadmap was creating in the ReadMe section and the features were developed accordingly
- Each feature is related to a group of similar things (Development environment setup) or an Individual component (Model test cases)
- Pull requests are created across the
master
branch from thesefeature
branches and the PRs are attached to the corresponding Feature List element in the ReadMe - At any point, the entire history of the project can be viewed using a tool such as
gitk
orSourceTree
orGitKraken
- If the event could be posted to
slug
based params, event types delegation will be responsibility of the client which can be directly routed to the appropriate service from theWebhookEventParser
service
- localhost:3000/webhooks/git/pull/
- localhost:3000/webhooks/git/push/
- localhost:3000/webhooks/git/release/
In increasing order of complexity
- Using Sidekiq or Reque to process the API payloads in background
- Background jobs for processing failed external API calls
- Extensive use of callbacks/pubsub (Wisper) for internal event driven architecture
- FSM based external API call using gems such as AASM
- Event driven Architecture using Message Queue
- Event Driven Architecture using Message Bus
- The project was started about 9 days ago on 28th March and handed off on 6th April. As communicated earlier, due to personal and professional reasons I could not start it earlier. Thank for you the extension of a week! :)
- All development done on the project after the handover can be found on the
epic/post-handover
branch - The project does not have complete RSpec coverage and it will be performed after codebase handover as I ran out of time
- I have tried to cover tests for one of each family (services/models) to give an idea about how I would go about building the test suite
- I would like to explore
WebMock
andVCR
for learning how to test better - For more consistent and robust code, I would like to implement the following gems
- Improve the test suite
- Learn how to use WebMock and VCR
- Learn stubbing and mocking use cases
- Explore ActionCable implementation for two way binding with the Api Clients
- Explore serialization related gems such as fastjson_api, ActiveSerializer
- Explore FSM related gems
- Integrate the database with Metabase for building dashboards using native SQL
- Explore more "Production Readiness" gems to increase my Rails Project Dev -> Production Knowledge