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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +49 -0
- data/IMPLEMENTATION_SUMMARY.md +220 -0
- data/README.md +317 -0
- data/Rakefile +10 -0
- data/TESTING_GUIDE.md +585 -0
- data/examples/rails_app_testing.rb +437 -0
- data/examples/rails_integration.rb +243 -0
- data/examples/standalone_usage.rb +309 -0
- data/lib/active_rabbit/client/configuration.rb +162 -0
- data/lib/active_rabbit/client/event_processor.rb +131 -0
- data/lib/active_rabbit/client/exception_tracker.rb +157 -0
- data/lib/active_rabbit/client/http_client.rb +137 -0
- data/lib/active_rabbit/client/n_plus_one_detector.rb +188 -0
- data/lib/active_rabbit/client/performance_monitor.rb +150 -0
- data/lib/active_rabbit/client/pii_scrubber.rb +169 -0
- data/lib/active_rabbit/client/railtie.rb +328 -0
- data/lib/active_rabbit/client/sidekiq_middleware.rb +130 -0
- data/lib/active_rabbit/client/version.rb +7 -0
- data/lib/active_rabbit/client.rb +119 -0
- data/lib/active_rabbit.rb +3 -0
- data/script/test_production_readiness.rb +403 -0
- data/sig/active_rabbit/client.rbs +6 -0
- metadata +155 -0
|
@@ -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
|