activerabbit-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,309 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Example standalone usage of ActiveRabbit::Client (non-Rails applications)
4
+
5
+ require 'active_rabbit/client'
6
+
7
+ # Basic configuration
8
+ ActiveRabbit::Client.configure do |config|
9
+ config.api_key = 'your-api-key-here'
10
+ config.project_id = 'your-project-id'
11
+ config.api_url = 'https://api.activerabbit.com'
12
+ config.environment = ENV.fetch('RACK_ENV', 'development')
13
+
14
+ # Standalone applications might want different settings
15
+ config.batch_size = 20
16
+ config.flush_interval = 10
17
+ config.enable_performance_monitoring = true
18
+ config.enable_pii_scrubbing = true
19
+ end
20
+
21
+ # Example: Sinatra application
22
+ require 'sinatra'
23
+
24
+ class MyApp < Sinatra::Base
25
+ configure do
26
+ # Add exception handling middleware
27
+ use Rack::CommonLogger
28
+
29
+ # Custom middleware for ActiveRabbit context
30
+ use Class.new do
31
+ def initialize(app)
32
+ @app = app
33
+ end
34
+
35
+ def call(env)
36
+ request = Rack::Request.new(env)
37
+
38
+ # Set request context
39
+ Thread.current[:active_rabbit_request_context] = {
40
+ method: request.request_method,
41
+ path: request.path_info,
42
+ query_string: request.query_string,
43
+ user_agent: request.user_agent,
44
+ ip_address: request.ip
45
+ }
46
+
47
+ begin
48
+ @app.call(env)
49
+ rescue Exception => e
50
+ # Track unhandled exceptions
51
+ ActiveRabbit::Client.track_exception(
52
+ e,
53
+ context: {
54
+ request: {
55
+ method: request.request_method,
56
+ path: request.path_info,
57
+ params: request.params
58
+ }
59
+ }
60
+ )
61
+ raise
62
+ ensure
63
+ Thread.current[:active_rabbit_request_context] = nil
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ get '/api/users/:id' do
70
+ # Track API endpoint usage
71
+ ActiveRabbit::Client.track_event(
72
+ 'api_endpoint_accessed',
73
+ {
74
+ endpoint: '/api/users/:id',
75
+ user_id: params[:id]
76
+ }
77
+ )
78
+
79
+ # Performance monitoring
80
+ user_data = ActiveRabbit::Client.performance_monitor.measure('user_lookup') do
81
+ # Simulate database lookup
82
+ sleep(0.1)
83
+ { id: params[:id], name: "User #{params[:id]}" }
84
+ end
85
+
86
+ content_type :json
87
+ user_data.to_json
88
+ end
89
+
90
+ post '/api/process' do
91
+ begin
92
+ # Process some data
93
+ result = process_data(params)
94
+
95
+ ActiveRabbit::Client.track_event(
96
+ 'data_processed',
97
+ {
98
+ records_processed: result[:count],
99
+ processing_time_ms: result[:duration]
100
+ }
101
+ )
102
+
103
+ { status: 'success', result: result }.to_json
104
+ rescue ProcessingError => e
105
+ ActiveRabbit::Client.track_exception(
106
+ e,
107
+ context: {
108
+ input_data: params,
109
+ processing_stage: e.stage
110
+ },
111
+ tags: {
112
+ component: 'data_processor',
113
+ severity: 'medium'
114
+ }
115
+ )
116
+
117
+ status 422
118
+ { error: 'Processing failed' }.to_json
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ def process_data(data)
125
+ start_time = Time.current
126
+
127
+ # Simulate processing
128
+ count = data['items']&.length || 0
129
+ sleep(count * 0.01) # Simulate work
130
+
131
+ {
132
+ count: count,
133
+ duration: ((Time.current - start_time) * 1000).round(2)
134
+ }
135
+ end
136
+ end
137
+
138
+ # Example: Background worker script
139
+ class BackgroundWorker
140
+ def initialize
141
+ # Configure ActiveRabbit for background processes
142
+ ActiveRabbit::Client.configure do |config|
143
+ config.api_key = ENV['active_rabbit_API_KEY']
144
+ config.project_id = ENV['active_rabbit_PROJECT_ID']
145
+ config.environment = ENV.fetch('ENVIRONMENT', 'development')
146
+ config.server_name = "worker-#{Socket.gethostname}"
147
+ end
148
+ end
149
+
150
+ def run
151
+ puts "Starting background worker..."
152
+
153
+ ActiveRabbit::Client.track_event('worker_started', {
154
+ hostname: Socket.gethostname,
155
+ pid: Process.pid
156
+ })
157
+
158
+ loop do
159
+ begin
160
+ job = fetch_next_job
161
+ break unless job
162
+
163
+ process_job(job)
164
+ rescue => e
165
+ ActiveRabbit::Client.track_exception(
166
+ e,
167
+ context: { component: 'background_worker' },
168
+ tags: { severity: 'high' }
169
+ )
170
+
171
+ sleep 5 # Wait before retrying
172
+ end
173
+ end
174
+
175
+ ActiveRabbit::Client.track_event('worker_stopped')
176
+ ActiveRabbit::Client.shutdown
177
+ end
178
+
179
+ private
180
+
181
+ def fetch_next_job
182
+ # Simulate job fetching
183
+ return nil if rand > 0.8 # 20% chance of getting a job
184
+
185
+ {
186
+ id: SecureRandom.uuid,
187
+ type: ['email', 'report', 'cleanup'].sample,
188
+ data: { user_id: rand(1000) }
189
+ }
190
+ end
191
+
192
+ def process_job(job)
193
+ transaction_id = ActiveRabbit::Client.performance_monitor.start_transaction(
194
+ "job_#{job[:type]}",
195
+ metadata: { job_id: job[:id] }
196
+ )
197
+
198
+ begin
199
+ # Simulate job processing
200
+ case job[:type]
201
+ when 'email'
202
+ send_email(job[:data])
203
+ when 'report'
204
+ generate_report(job[:data])
205
+ when 'cleanup'
206
+ cleanup_data(job[:data])
207
+ end
208
+
209
+ ActiveRabbit::Client.track_event(
210
+ 'job_completed',
211
+ {
212
+ job_id: job[:id],
213
+ job_type: job[:type]
214
+ }
215
+ )
216
+ rescue => e
217
+ ActiveRabbit::Client.track_exception(
218
+ e,
219
+ context: {
220
+ job: job,
221
+ component: 'job_processor'
222
+ }
223
+ )
224
+ raise
225
+ ensure
226
+ ActiveRabbit::Client.performance_monitor.finish_transaction(
227
+ transaction_id,
228
+ additional_metadata: { status: 'completed' }
229
+ )
230
+ end
231
+ end
232
+
233
+ def send_email(data)
234
+ sleep(0.2) # Simulate email sending
235
+ end
236
+
237
+ def generate_report(data)
238
+ sleep(0.5) # Simulate report generation
239
+ end
240
+
241
+ def cleanup_data(data)
242
+ sleep(0.1) # Simulate cleanup
243
+ end
244
+ end
245
+
246
+ # Example: Rake task integration
247
+ # lib/tasks/data_migration.rake
248
+ namespace :data do
249
+ desc "Migrate user data"
250
+ task migrate_users: :environment do
251
+ ActiveRabbit::Client.configure do |config|
252
+ config.api_key = ENV['active_rabbit_API_KEY']
253
+ config.project_id = ENV['active_rabbit_PROJECT_ID']
254
+ config.environment = 'migration'
255
+ end
256
+
257
+ start_time = Time.current
258
+ processed_count = 0
259
+ error_count = 0
260
+
261
+ ActiveRabbit::Client.track_event('migration_started', {
262
+ task: 'migrate_users',
263
+ started_at: start_time
264
+ })
265
+
266
+ begin
267
+ User.find_each do |user|
268
+ begin
269
+ migrate_user(user)
270
+ processed_count += 1
271
+ rescue => e
272
+ error_count += 1
273
+ ActiveRabbit::Client.track_exception(
274
+ e,
275
+ context: {
276
+ user_id: user.id,
277
+ migration_task: 'migrate_users'
278
+ }
279
+ )
280
+ end
281
+ end
282
+
283
+ duration = Time.current - start_time
284
+
285
+ ActiveRabbit::Client.track_event('migration_completed', {
286
+ task: 'migrate_users',
287
+ duration_seconds: duration.round(2),
288
+ processed_count: processed_count,
289
+ error_count: error_count
290
+ })
291
+
292
+ puts "Migration completed: #{processed_count} users processed, #{error_count} errors"
293
+ ensure
294
+ ActiveRabbit::Client.shutdown
295
+ end
296
+ end
297
+ end
298
+
299
+ # Usage examples:
300
+
301
+ # 1. Run the Sinatra app
302
+ # ruby standalone_usage.rb
303
+
304
+ # 2. Run the background worker
305
+ # worker = BackgroundWorker.new
306
+ # worker.run
307
+
308
+ # 3. Run the rake task
309
+ # rake data:migrate_users
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "logger"
5
+
6
+ module ActiveRabbit
7
+ module Client
8
+ class Configuration
9
+ attr_accessor :api_key, :api_url, :project_id, :environment
10
+ attr_accessor :timeout, :open_timeout, :retry_count, :retry_delay
11
+ attr_accessor :batch_size, :flush_interval, :queue_size
12
+ attr_accessor :enable_performance_monitoring, :enable_n_plus_one_detection
13
+ attr_accessor :enable_pii_scrubbing, :pii_fields
14
+ attr_accessor :ignored_exceptions, :ignored_user_agents
15
+ attr_accessor :release, :server_name, :logger
16
+ attr_accessor :before_send_event, :before_send_exception
17
+
18
+ def initialize
19
+ @api_url = ENV.fetch("active_rabbit_API_URL", "https://api.activerabbit.com")
20
+ @api_key = ENV["active_rabbit_API_KEY"]
21
+ @project_id = ENV["active_rabbit_PROJECT_ID"]
22
+ @environment = ENV.fetch("active_rabbit_ENVIRONMENT", detect_environment)
23
+
24
+ # HTTP settings
25
+ @timeout = 30
26
+ @open_timeout = 10
27
+ @retry_count = 3
28
+ @retry_delay = 1
29
+
30
+ # Batching settings
31
+ @batch_size = 100
32
+ @flush_interval = 30 # seconds
33
+ @queue_size = 1000
34
+
35
+ # Feature flags
36
+ @enable_performance_monitoring = true
37
+ @enable_n_plus_one_detection = true
38
+ @enable_pii_scrubbing = true
39
+
40
+ # PII scrubbing
41
+ @pii_fields = %w[
42
+ password password_confirmation token secret key
43
+ credit_card ssn social_security_number phone email
44
+ first_name last_name name address city state zip
45
+ ]
46
+
47
+ # Filtering
48
+ @ignored_exceptions = %w[
49
+ ActiveRecord::RecordNotFound
50
+ ActionController::RoutingError
51
+ ActionController::InvalidAuthenticityToken
52
+ CGI::Session::CookieStore::TamperedWithCookie
53
+ ]
54
+
55
+ @ignored_user_agents = [
56
+ /Googlebot/i,
57
+ /bingbot/i,
58
+ /facebookexternalhit/i,
59
+ /Twitterbot/i
60
+ ]
61
+
62
+ # Metadata
63
+ @release = detect_release
64
+ @server_name = detect_server_name
65
+ @logger = detect_logger
66
+
67
+ # Callbacks
68
+ @before_send_event = nil
69
+ @before_send_exception = nil
70
+ end
71
+
72
+ def valid?
73
+ return false unless api_key
74
+ return false if api_key.empty?
75
+ return false unless api_url
76
+ return false if api_url.empty?
77
+ true
78
+ end
79
+
80
+ def api_endpoint(path)
81
+ "#{api_url.chomp('/')}/#{path.to_s.sub(/^\//, '')}"
82
+ end
83
+
84
+ def should_ignore_exception?(exception)
85
+ return false unless exception
86
+
87
+ ignored_exceptions.any? do |ignored|
88
+ case ignored
89
+ when String
90
+ exception.class.name == ignored
91
+ when Class
92
+ exception.is_a?(ignored)
93
+ when Regexp
94
+ exception.class.name =~ ignored
95
+ else
96
+ false
97
+ end
98
+ end
99
+ end
100
+
101
+ def should_ignore_user_agent?(user_agent)
102
+ return false unless user_agent
103
+
104
+ ignored_user_agents.any? do |pattern|
105
+ case pattern
106
+ when String
107
+ user_agent.include?(pattern)
108
+ when Regexp
109
+ user_agent =~ pattern
110
+ else
111
+ false
112
+ end
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def detect_environment
119
+ return Rails.env if defined?(Rails) && Rails.respond_to?(:env)
120
+ return Sinatra::Base.environment.to_s if defined?(Sinatra)
121
+
122
+ ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
123
+ end
124
+
125
+ def detect_release
126
+ # Try to detect from common CI/deployment environment variables
127
+ ENV["HEROKU_SLUG_COMMIT"] ||
128
+ ENV["GITHUB_SHA"] ||
129
+ ENV["GITLAB_COMMIT_SHA"] ||
130
+ ENV["CIRCLE_SHA1"] ||
131
+ ENV["TRAVIS_COMMIT"] ||
132
+ ENV["BUILD_VCS_NUMBER"] ||
133
+ detect_git_sha
134
+ end
135
+
136
+ def detect_git_sha
137
+ return unless File.directory?(".git")
138
+
139
+ `git rev-parse HEAD 2>/dev/null`.chomp
140
+ rescue
141
+ nil
142
+ end
143
+
144
+ def detect_server_name
145
+ ENV["DYNO"] || # Heroku
146
+ ENV["HOSTNAME"] || # Docker
147
+ ENV["SERVER_NAME"] ||
148
+ Socket.gethostname
149
+ rescue
150
+ "unknown"
151
+ end
152
+
153
+ def detect_logger
154
+ return Rails.logger if defined?(Rails) && Rails.respond_to?(:logger)
155
+
156
+ Logger.new($stdout).tap do |logger|
157
+ logger.level = Logger::INFO
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module ActiveRabbit
6
+ module Client
7
+ class EventProcessor
8
+ attr_reader :configuration, :http_client
9
+
10
+ def initialize(configuration, http_client)
11
+ @configuration = configuration
12
+ @http_client = http_client
13
+ @event_queue = Concurrent::Array.new
14
+ @processor_thread = nil
15
+ @shutdown = false
16
+ end
17
+
18
+ def track_event(name:, properties: {}, user_id: nil, timestamp: nil)
19
+ return if @shutdown
20
+
21
+ event_data = build_event_data(
22
+ name: name,
23
+ properties: properties,
24
+ user_id: user_id,
25
+ timestamp: timestamp
26
+ )
27
+
28
+ # Apply before_send callback if configured
29
+ if configuration.before_send_event
30
+ event_data = configuration.before_send_event.call(event_data)
31
+ return unless event_data # Callback can filter out events by returning nil
32
+ end
33
+
34
+ @event_queue << event_data
35
+ ensure_processor_running
36
+ end
37
+
38
+ def flush
39
+ return if @event_queue.empty?
40
+
41
+ events = @event_queue.shift(@event_queue.length)
42
+ return if events.empty?
43
+
44
+ events.each_slice(configuration.batch_size) do |batch|
45
+ http_client.post_batch(batch)
46
+ end
47
+ end
48
+
49
+ def shutdown
50
+ @shutdown = true
51
+ @processor_thread&.kill
52
+ flush
53
+ end
54
+
55
+ private
56
+
57
+ def build_event_data(name:, properties:, user_id:, timestamp:)
58
+ data = {
59
+ name: name.to_s,
60
+ properties: scrub_pii(properties || {}),
61
+ timestamp: (timestamp || Time.now).iso8601(3),
62
+ environment: configuration.environment,
63
+ release: configuration.release,
64
+ server_name: configuration.server_name
65
+ }
66
+
67
+ data[:user_id] = user_id if user_id
68
+ data[:project_id] = configuration.project_id if configuration.project_id
69
+
70
+ # Add context information
71
+ data[:context] = build_context
72
+
73
+ data
74
+ end
75
+
76
+ def build_context
77
+ context = {}
78
+
79
+ # Runtime information
80
+ context[:runtime] = {
81
+ name: "ruby",
82
+ version: RUBY_VERSION,
83
+ platform: RUBY_PLATFORM
84
+ }
85
+
86
+ # Framework information
87
+ if defined?(Rails)
88
+ context[:framework] = {
89
+ name: "rails",
90
+ version: Rails.version
91
+ }
92
+ elsif defined?(Sinatra)
93
+ context[:framework] = {
94
+ name: "sinatra",
95
+ version: Sinatra::VERSION
96
+ }
97
+ end
98
+
99
+ # Request information (if available)
100
+ if defined?(Thread) && Thread.current[:active_rabbit_request_context]
101
+ context[:request] = Thread.current[:active_rabbit_request_context]
102
+ end
103
+
104
+ context
105
+ end
106
+
107
+ def scrub_pii(data)
108
+ return data unless configuration.enable_pii_scrubbing
109
+
110
+ PiiScrubber.new(configuration).scrub(data)
111
+ end
112
+
113
+ def ensure_processor_running
114
+ return if @processor_thread&.alive?
115
+
116
+ @processor_thread = Thread.new do
117
+ loop do
118
+ break if @shutdown
119
+
120
+ begin
121
+ sleep(configuration.flush_interval)
122
+ flush unless @event_queue.empty?
123
+ rescue => e
124
+ configuration.logger&.error("[ActiveRabbit] Event processor error: #{e.message}")
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end