require 'net/http' require 'time' require 'json' require 'makitoo/feature_flag/version' require 'makitoo/feature_flag/memory_cache' require 'makitoo/feature_flag/user' module Makitoo module FeatureFlag class Client def initialize(application_id, options = {}) @logger = if options.has_key?(:logger) options[:logger] elsif defined?(Rails) Rails.logger else logger = Logger.new STDOUT logger.progname = 'Makitoo::FeatureFlag' logger end @logger.debug 'Initializing makitoo feature flag' @application_id = application_id @server = if options.has_key?(:server) options[:server] elsif options[:ssl] 'https://features.makitoo.com/api/v1' else 'http://features.makitoo.com/api/v1' end @cache = options[:cache] || MemoryCache.new @logger @environment_name = options[:environment_name] || 'production' @concurrency = options[:concurrency] || 1 @force_sync = options[:force_sync] || @concurrency < 1 start_background_threads @logger.debug 'Makitoo initialized' end def is_active(feature_name, user_or_id, default_state = false) @logger.debug 'Start is_active' feature_configuration = get_feature_configuration(feature_name, User.of(user_or_id)) is_visible = if feature_configuration.nil? default_state else feature_configuration[:state] == 'ON' end event = if is_visible create_seen_event(feature_name, user) else create_skip_event(feature_name, user) end send_event(event) @logger.debug 'Stop is_active' is_visible end def success(feature_name, user_or_id) @logger.debug 'Start is_active' send_event(create_success_event(feature_name, User.of(user_or_id))) @logger.debug 'Stop success' end def track(event_name, user_or_id, properties = {}) @logger.debug 'Start is_active' send_event(create_track_event(event_name, User.of(user_or_id), properties)) @logger.debug 'Stop track' end def init_segment(segment_id, user_or_id) @logger.debug 'Start init_segment' post_json(@server + '/associate-foreign-id', { foreignId: segment_id, source: 'segment.com', installationId: User.of(user_or_id).id }) @logger.debug 'Stop init_segment' end private def start_background_threads if !@force_sync @queue = Queue.new @logger.debug 'Background threads starting' (1..@concurrency).each do Thread.new do @logger.debug 'Background thread started' begin loop do begin @logger.debug 'Async request waiting for task' val = @queue.pop @logger.debug 'Async request start' res = Net::HTTP.start(val[:url].host, val[:url].port, use_ssl: val[:url].scheme == 'https') {|http| http.request(val[:req]) } @logger.debug 'Async request end' rescue Exception => e @logger.debug 'Error !!!' log_error(e) end end ensure @logger.debug 'Background thread stopped' end end end end end def log_error(e) @logger.error 'Exception' @logger.error e.message e.backtrace.each { |line| @logger.error line } end def refresh_feature_configurations_cache(user) feature_configurations = send_event(create_refresh_event(user), false) @cache.put('features.' + user.id, feature_configurations) feature_configurations end def get_feature_configuration(feature_name, user) feature_configurations = @cache.get('features.' + user.id) if feature_configurations.nil? feature_configurations = refresh_feature_configurations_cache(user) end if feature_configurations.nil? nil else feature_configurations[feature_name] end end def create_seen_event(feature_name, user) create_execution_event(feature_name, 'SEEN', user) end def create_skip_event(feature_name, user) create_execution_event(feature_name, 'SKIP', user) end def create_success_event(feature_name, user) create_execution_event(feature_name, 'SUCCESS', user) end def create_execution_event(feature_name, event_type, user) event = create_event(event_type, user) event[:featureName] = feature_name event end def create_track_event(event_name, user, properties) event = create_event('TRACK', user) event[:eventName] = event_name event[:properties] = properties event end def create_refresh_event(user) create_event('REFRESH', user) end def create_event(event_type, user) { eventType: event_type, date: Time.now().iso8601, count: 1, installationId: user.id, context: create_context(user), lib: { version: '0.0.1', type: 'ruby' } } end def post_json(url_str, data, async) url = URI.parse(url_str) req = Net::HTTP::Post.new(url.to_s, 'Content-Type' => 'application/json') req.body = data.to_json if async && !@force_sync @logger.debug 'Queue async event' @queue << {url: url, req: req} nil else @logger.debug 'Sync request start' res = Net::HTTP.start(url.host, url.port, use_ssl: url.scheme == 'https') {|http| http.request(req) } @logger.debug 'Sync request end' res.body end end def send_event(event, async = true) begin body = post_json(@server + '/event?' + URI.encode_www_form({ application: @application_id, environmentName: @environment_name }), event, async) if !body.nil? result = JSON.parse(body); feature_configurations = {} result['features'].each{|feature| feature_configurations[feature['name']] = { name: feature['name'], state: feature['state'] } } feature_configurations end rescue => e log_error(e) end end def create_context(user) { user: user.as_json } end end end end