agentbill-sdk 5.0.1 → 9.4.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.
data/lib/agentbill.rb CHANGED
@@ -1,16 +1,24 @@
1
1
  require 'net/http'
2
2
  require 'json'
3
3
  require 'securerandom'
4
+ require 'digest'
4
5
  require_relative 'agentbill/version'
5
6
  require_relative 'agentbill/tracer'
6
7
 
8
+ require_relative 'agentbill/customers'
9
+ require_relative 'agentbill/agents'
10
+ require_relative 'agentbill/orders'
11
+
7
12
  module AgentBill
8
13
  class Client
9
- attr_reader :config, :tracer
14
+ attr_reader :config, :tracer, :customers, :agents, :orders
10
15
 
11
16
  def initialize(config)
12
17
  @config = config
13
18
  @tracer = Tracer.new(config)
19
+ @customers = CustomersResource.new(config)
20
+ @agents = AgentsResource.new(config)
21
+ @orders = OrdersResource.new(config)
14
22
  end
15
23
 
16
24
  def self.init(config)
@@ -24,33 +32,21 @@ module AgentBill
24
32
  [1, text.to_s.length / 4].max
25
33
  end
26
34
 
27
- def estimate_cost(model, input_tokens, output_tokens)
28
- # Model pricing (per 1K tokens)
29
- pricing = {
30
- 'gpt-4' => { input: 0.03, output: 0.06 },
31
- 'gpt-4o' => { input: 0.005, output: 0.015 },
32
- 'gpt-4o-mini' => { input: 0.00015, output: 0.0006 },
33
- 'claude-sonnet-4-5' => { input: 0.003, output: 0.015 },
34
- 'claude-opus-4-1-20250805' => { input: 0.015, output: 0.075 },
35
- 'claude-3-5-sonnet-20241022' => { input: 0.003, output: 0.015 },
36
- 'mistral-large-latest' => { input: 0.004, output: 0.012 },
37
- 'gemini-pro' => { input: 0.00025, output: 0.0005 }
38
- }
39
-
40
- model_price = pricing[model] || { input: 0.001, output: 0.002 }
41
- (input_tokens / 1000.0 * model_price[:input]) + (output_tokens / 1000.0 * model_price[:output])
42
- end
35
+ # v9.1.0: estimate_cost() REMOVED. Cost is 100% server-side via model_pricing table.
43
36
 
44
37
  def validate_request(model, messages, estimated_output_tokens = 1000)
45
- return { 'allowed' => true } unless @config[:daily_budget] || @config[:monthly_budget]
38
+ # Always validate when customer_id is present - backend will check DB policies
39
+ return { 'allowed' => true } unless @config[:customer_id]
46
40
 
47
- uri = URI("#{@config[:base_url] || 'https://bgwyprqxtdreuutzpbgw.supabase.co'}/functions/v1/ai-cost-guard-router")
41
+ uri = URI("#{@config[:base_url] || 'https://api.agentbill.io'}/functions/v1/ai-cost-guard-router")
48
42
 
49
43
  payload = {
50
44
  api_key: @config[:api_key],
51
45
  customer_id: @config[:customer_id],
52
46
  model: model,
53
- messages: messages
47
+ messages: messages,
48
+ daily_budget_override: @config[:daily_budget],
49
+ monthly_budget_override: @config[:monthly_budget]
54
50
  }
55
51
 
56
52
  begin
@@ -74,37 +70,8 @@ module AgentBill
74
70
  end
75
71
  end
76
72
 
77
- def track_usage(model, provider, input_tokens, output_tokens, latency_ms, cost, event_name = 'ai_request')
78
- uri = URI("#{@config[:base_url] || 'https://bgwyprqxtdreuutzpbgw.supabase.co'}/functions/v1/track-ai-usage")
79
-
80
- payload = {
81
- api_key: @config[:api_key],
82
- customer_id: @config[:customer_id],
83
- agent_id: @config[:agent_id],
84
- event_name: event_name,
85
- model: model,
86
- provider: provider,
87
- prompt_tokens: input_tokens,
88
- completion_tokens: output_tokens,
89
- latency_ms: latency_ms,
90
- cost: cost
91
- }
92
-
93
- begin
94
- http = Net::HTTP.new(uri.host, uri.port)
95
- http.use_ssl = true
96
- http.read_timeout = 10
97
-
98
- request = Net::HTTP::Post.new(uri.path)
99
- request['Content-Type'] = 'application/json'
100
- request.body = payload.to_json
101
-
102
- http.request(request)
103
- puts "[AgentBill Cost Guard] Usage tracked: $#{format('%.4f', cost)}" if @config[:debug]
104
- rescue => e
105
- puts "[AgentBill Cost Guard] Tracking failed: #{e.message}" if @config[:debug]
106
- end
107
- end
73
+ # v7.17.3: track_usage method DELETED - all tracking now goes through OTEL spans
74
+ # Use self.tracer.start_span() + span.finish() + self.tracer.flush_sync() instead
108
75
 
109
76
  public
110
77
 
@@ -132,9 +99,59 @@ module AgentBill
132
99
  raise error
133
100
  end
134
101
 
102
+ # v7.6.10: Check for cached response from semantic cache
103
+ # CRITICAL FIX: Router sends cost_saved/tokens_saved at top level
104
+ if validation['cached'] && validation['response_data']
105
+ puts "[AgentBill] ✓ Cache hit - returning cached response" if config[:debug]
106
+
107
+ # v7.6.10 FIX: Check both nested 'cache'/'cache_info' AND top-level fields
108
+ cache_info = validation['cache'] || validation['cache_info'] || {}
109
+ cached_response = validation['response_data']
110
+ usage = cached_response['usage'] || {}
111
+
112
+ # v7.6.10 FIX: Extract from cache_info OR top-level validation
113
+ tokens_saved = cache_info['tokens_saved'] || validation['tokens_saved'] || usage['total_tokens'] || 0
114
+ cost_saved = cache_info['cost_saved'] || validation['cost_saved'] || 0
115
+ cached_input_tokens = usage['prompt_tokens'] || (tokens_saved * 0.2).to_i
116
+ cached_output_tokens = usage['completion_tokens'] || (tokens_saved - cached_input_tokens)
117
+ cached_total_tokens = tokens_saved > 0 ? tokens_saved : (cached_input_tokens + cached_output_tokens)
118
+
119
+ # v7.6.10 CRITICAL FIX: Set gen_ai.usage.* to 0 for cache hits
120
+ # This prevents OTEL ingestion from calculating cost for cached responses
121
+ span = tracer.start_span('openai.chat.completion', {
122
+ 'gen_ai.system' => 'openai',
123
+ 'gen_ai.request.model' => model,
124
+ 'gen_ai.operation.name' => 'chat',
125
+ 'model' => model,
126
+ 'provider' => 'openai',
127
+ 'agentbill.from_cache' => true,
128
+ 'agentbill.cache_hit' => true,
129
+ # OTEL cost calculation attributes - set to 0 for cache hits
130
+ 'gen_ai.usage.input_tokens' => 0,
131
+ 'gen_ai.usage.output_tokens' => 0,
132
+ 'gen_ai.usage.total_tokens' => 0,
133
+ # Informational attributes - actual cached token counts
134
+ 'agentbill.cached_input_tokens' => cached_input_tokens,
135
+ 'agentbill.cached_output_tokens' => cached_output_tokens,
136
+ 'agentbill.cached_total_tokens' => cached_total_tokens,
137
+ 'agentbill.tokens_saved' => tokens_saved,
138
+ 'agentbill.cost_saved' => cost_saved
139
+ })
140
+ span.set_status(0)
141
+ span.finish
142
+ tracer.flush
143
+
144
+ return cached_response
145
+ end
146
+
135
147
  # Phase 2: Execute AI call
136
148
  start_time = Time.now
137
149
  span = tracer.start_span('openai.chat.completion', {
150
+ # ✅ OTEL GenAI compliant attributes
151
+ 'gen_ai.system' => 'openai',
152
+ 'gen_ai.request.model' => model,
153
+ 'gen_ai.operation.name' => 'chat',
154
+ # ⚠️ Backward compatibility
138
155
  'model' => model,
139
156
  'provider' => 'openai'
140
157
  })
@@ -143,14 +160,19 @@ module AgentBill
143
160
  response = original_method.call(params)
144
161
  latency = ((Time.now - start_time) * 1000).round
145
162
 
146
- # Phase 3: Track actual usage
163
+ # NOTE: wrap() only creates OTEL spans, NOT signals
164
+ # Signals should be created explicitly via track_signal()
147
165
  input_tokens = response.dig(:usage, :prompt_tokens) || 0
148
166
  output_tokens = response.dig(:usage, :completion_tokens) || 0
149
- cost = config[:_client].send(:estimate_cost, model, input_tokens, output_tokens)
150
-
151
- config[:_client].send(:track_usage, model, 'openai', input_tokens, output_tokens, latency, cost, event_name)
167
+ # v9.1.0: Cost calculated server-side from token counts in span
152
168
 
153
169
  span.set_attributes({
170
+ # ✅ OTEL GenAI compliant attributes
171
+ 'gen_ai.usage.input_tokens' => input_tokens,
172
+ 'gen_ai.usage.output_tokens' => output_tokens,
173
+ 'gen_ai.usage.total_tokens' => response.dig(:usage, :total_tokens),
174
+ 'gen_ai.response.id' => response.dig(:id),
175
+ # ⚠️ Backward compatibility
154
176
  'response.prompt_tokens' => input_tokens,
155
177
  'response.completion_tokens' => output_tokens,
156
178
  'response.total_tokens' => response.dig(:usage, :total_tokens),
@@ -158,16 +180,25 @@ module AgentBill
158
180
  })
159
181
  span.set_status(0)
160
182
 
161
- puts "[AgentBill Cost Guard] ✓ Protected call completed: $#{format('%.4f', cost)}" if config[:debug]
183
+ puts "[AgentBill] ✓ OpenAI call: #{input_tokens}in/#{output_tokens}out tokens (cost calculated server-side)" if config[:debug]
184
+
185
+ # v7.5.0: Cache AI response for semantic caching
186
+ response_content = response.dig(:choices, 0, :message, :content) || ''
187
+ # v9.1.0: Fixed prompt_hash to match backend algorithm (SHA-256 of JSON, full hash)
188
+ prompt_text = JSON.generate(messages)
189
+ prompt_hash = Digest::SHA256.hexdigest(prompt_text)
190
+ config[:_client].send(:cache_response, model, prompt_hash, response_content, input_tokens + output_tokens, 0)
191
+
162
192
  response
163
193
  rescue => e
164
194
  span.set_status(1, e.message)
165
195
  raise
166
196
  ensure
167
197
  span.finish
198
+ # Auto-flush spans to prevent data loss
199
+ tracer.flush
168
200
  end
169
201
  end
170
-
171
202
  # Store reference to self for helper methods
172
203
  @config[:_client] = self
173
204
  client
@@ -196,6 +227,11 @@ module AgentBill
196
227
  # Phase 2: Execute AI call
197
228
  start_time = Time.now
198
229
  span = tracer.start_span('anthropic.message', {
230
+ # ✅ OTEL GenAI compliant attributes
231
+ 'gen_ai.system' => 'anthropic',
232
+ 'gen_ai.request.model' => model,
233
+ 'gen_ai.operation.name' => 'chat',
234
+ # ⚠️ Backward compatibility
199
235
  'model' => model,
200
236
  'provider' => 'anthropic'
201
237
  })
@@ -204,30 +240,34 @@ module AgentBill
204
240
  response = original_method.call(params)
205
241
  latency = ((Time.now - start_time) * 1000).round
206
242
 
207
- # Phase 3: Track actual usage
243
+ # NOTE: wrap() only creates OTEL spans, NOT signals
208
244
  input_tokens = response.dig(:usage, :input_tokens) || 0
209
245
  output_tokens = response.dig(:usage, :output_tokens) || 0
210
- cost = config[:_client].send(:estimate_cost, model, input_tokens, output_tokens)
211
-
212
- config[:_client].send(:track_usage, model, 'anthropic', input_tokens, output_tokens, latency, cost)
246
+ # v9.1.0: Cost calculated server-side from token counts in span
213
247
 
214
248
  span.set_attributes({
249
+ # ✅ OTEL GenAI compliant attributes
250
+ 'gen_ai.usage.input_tokens' => input_tokens,
251
+ 'gen_ai.usage.output_tokens' => output_tokens,
252
+ 'gen_ai.response.id' => response.dig(:id),
253
+ # ⚠️ Backward compatibility
215
254
  'response.input_tokens' => input_tokens,
216
255
  'response.output_tokens' => output_tokens,
217
256
  'latency_ms' => latency
218
257
  })
219
258
  span.set_status(0)
220
259
 
221
- puts "[AgentBill Cost Guard] ✓ Protected call completed: $#{format('%.4f', cost)}" if config[:debug]
260
+ puts "[AgentBill] ✓ OTEL span created: $#{format('%.4f', cost)} (use track_signal() for signals)" if config[:debug]
222
261
  response
223
262
  rescue => e
224
263
  span.set_status(1, e.message)
225
264
  raise
226
265
  ensure
227
266
  span.finish
267
+ # Auto-flush spans
268
+ tracer.flush
228
269
  end
229
270
  end
230
-
231
271
  # Store reference to self for helper methods
232
272
  @config[:_client] = self
233
273
  client
@@ -274,25 +314,59 @@ module AgentBill
274
314
  def track_signal(**params)
275
315
  raise ArgumentError, "event_name is required" unless params[:event_name]
276
316
 
277
- uri = URI("#{@config[:base_url] || 'https://bgwyprqxtdreuutzpbgw.supabase.co'}/functions/v1/record-signals")
317
+ # v7.17.1: Unified OTEL model - route through otel-collector
318
+ uri = URI("#{@config[:base_url] || 'https://api.agentbill.io'}/functions/v1/otel-collector")
278
319
 
279
- # Add timestamp if not provided
280
- params[:timestamp] ||= Time.now.to_f
320
+ # Generate trace context
321
+ trace_id = params[:trace_id] || SecureRandom.hex(16)
322
+ span_id = params[:span_id] || SecureRandom.hex(8)
323
+ now_ns = (Time.now.to_f * 1_000_000_000).to_i
281
324
 
282
- # Auto-fill customer_id or customer_external_id from config if not provided
283
- if !params[:customer_id] && !params[:customer_external_id] && @config[:customer_id]
284
- # Check if it's a UUID format - send as customer_id, else customer_external_id
285
- uuid_regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
286
- is_uuid = @config[:customer_id].match?(uuid_regex)
287
- if is_uuid
288
- params[:customer_id] = @config[:customer_id]
289
- else
290
- params[:customer_external_id] = @config[:customer_id]
291
- end
292
- end
325
+ # Auto-fill customer_id from config if not provided
326
+ customer_id = params[:customer_id] || params[:customer_external_id] || @config[:customer_id] || ""
327
+ agent_id = params[:agent_id] || params[:agent_external_id] || @config[:agent_id] || ""
293
328
 
294
- # Remove nil values
295
- payload = params.reject { |_, v| v.nil? }
329
+ # Build OTEL-compliant span attributes
330
+ attributes = [
331
+ { "key" => "agentbill.event_name", "value" => { "stringValue" => params[:event_name] } },
332
+ { "key" => "agentbill.is_business_event", "value" => { "boolValue" => true } },
333
+ { "key" => "agentbill.data_source", "value" => { "stringValue" => "ruby-sdk" } },
334
+ ]
335
+
336
+ attributes << { "key" => "agentbill.customer_id", "value" => { "stringValue" => customer_id.to_s } } if customer_id && customer_id != ""
337
+ attributes << { "key" => "agentbill.agent_id", "value" => { "stringValue" => agent_id.to_s } } if agent_id && agent_id != ""
338
+ attributes << { "key" => "agentbill.revenue", "value" => { "doubleValue" => params[:revenue] } } if params[:revenue]
339
+ attributes << { "key" => "agentbill.currency", "value" => { "stringValue" => params[:currency] || "USD" } } if params[:revenue]
340
+ attributes << { "key" => "agentbill.model", "value" => { "stringValue" => params[:model] } } if params[:model]
341
+ attributes << { "key" => "agentbill.session_id", "value" => { "stringValue" => params[:session_id] } } if params[:session_id]
342
+ attributes << { "key" => "agentbill.metadata", "value" => { "stringValue" => params[:metadata].to_json } } if params[:metadata]
343
+
344
+ # Build OTEL payload
345
+ otel_payload = {
346
+ "resourceSpans" => [{
347
+ "resource" => {
348
+ "attributes" => [
349
+ { "key" => "service.name", "value" => { "stringValue" => "agentbill-ruby-sdk" } },
350
+ { "key" => "agentbill.customer_id", "value" => { "stringValue" => customer_id.to_s } },
351
+ { "key" => "agentbill.agent_id", "value" => { "stringValue" => agent_id.to_s } },
352
+ ]
353
+ },
354
+ "scopeSpans" => [{
355
+ "scope" => { "name" => "agentbill.signals", "version" => "7.17.3" },
356
+ "spans" => [{
357
+ "traceId" => trace_id,
358
+ "spanId" => span_id,
359
+ "parentSpanId" => params[:parent_span_id] || "",
360
+ "name" => "agentbill.trace.signal",
361
+ "kind" => 1,
362
+ "startTimeUnixNano" => now_ns.to_s,
363
+ "endTimeUnixNano" => now_ns.to_s,
364
+ "attributes" => attributes,
365
+ "status" => { "code" => 1 }
366
+ }]
367
+ }]
368
+ }]
369
+ }
296
370
 
297
371
  begin
298
372
  http = Net::HTTP.new(uri.host, uri.port)
@@ -301,13 +375,12 @@ module AgentBill
301
375
  request = Net::HTTP::Post.new(uri.path)
302
376
  request['X-API-Key'] = @config[:api_key]
303
377
  request['Content-Type'] = 'application/json'
304
- request.body = payload.to_json
378
+ request.body = otel_payload.to_json
305
379
 
306
380
  response = http.request(request)
307
381
 
308
382
  if @config[:debug]
309
- trace_info = params[:trace_id] ? " (trace: #{params[:trace_id]})" : ""
310
- puts "[AgentBill] Signal tracked: #{params[:event_name]}#{trace_info}"
383
+ puts "[AgentBill] Signal tracked via OTEL: #{params[:event_name]} (trace: #{trace_id})"
311
384
  end
312
385
 
313
386
  response.code == '200'
@@ -319,8 +392,122 @@ module AgentBill
319
392
  end
320
393
  end
321
394
 
395
+ # Track a conversion event for prompt profitability analysis
396
+ # Links conversions to AI prompts to calculate ROI per prompt
397
+ #
398
+ # @param event_type [String] Type of conversion (e.g., 'purchase', 'signup', 'trial_start')
399
+ # @param event_value [Float] Revenue amount from the conversion
400
+ # @param signal_id [String, nil] Optional UUID linking to specific AI signal/prompt
401
+ # @param session_id [String, nil] Optional session identifier
402
+ # @param attribution_window_hours [Integer] Time window for attribution (default: 24 hours)
403
+ # @param currency [String] Currency code (default: 'USD')
404
+ # @param metadata [Hash, nil] Optional additional data
405
+ # @return [Hash] Response with success status and conversion_id
406
+ #
407
+ # Example:
408
+ # # Track a purchase conversion
409
+ # result = agentbill.track_conversion(
410
+ # event_type: 'purchase',
411
+ # event_value: 99.99,
412
+ # currency: 'USD'
413
+ # )
414
+ #
415
+ # # Link conversion to specific AI prompt
416
+ # result = agentbill.track_conversion(
417
+ # event_type: 'trial_signup',
418
+ # event_value: 29.99,
419
+ # signal_id: 'signal-uuid-from-prompt',
420
+ # session_id: 'session-123'
421
+ # )
422
+ def track_conversion(event_type:, event_value:, signal_id: nil, session_id: nil, attribution_window_hours: 24, currency: 'USD', metadata: nil)
423
+ uri = URI("#{@config[:base_url] || 'https://api.agentbill.io'}/functions/v1/track-conversion")
424
+
425
+ payload = {
426
+ api_key: @config[:api_key],
427
+ customer_id: @config[:customer_id],
428
+ event_type: event_type,
429
+ event_value: event_value,
430
+ signal_id: signal_id,
431
+ session_id: session_id,
432
+ attribution_window_hours: attribution_window_hours,
433
+ currency: currency,
434
+ metadata: metadata
435
+ }
436
+
437
+ begin
438
+ http = Net::HTTP.new(uri.host, uri.port)
439
+ http.use_ssl = true
440
+
441
+ request = Net::HTTP::Post.new(uri.path)
442
+ request['Content-Type'] = 'application/json'
443
+ request.body = payload.to_json
444
+
445
+ response = http.request(request)
446
+ data = JSON.parse(response.body)
447
+
448
+ if response.code != '200'
449
+ return {
450
+ success: false,
451
+ error: data['error'] || "HTTP #{response.code}"
452
+ }
453
+ end
454
+
455
+ if @config[:debug]
456
+ puts "[AgentBill] Conversion tracked: #{event_type} = $#{event_value} (ID: #{data['conversion_id']})"
457
+ end
458
+
459
+ {
460
+ success: true,
461
+ conversion_id: data['conversion_id']
462
+ }
463
+ rescue => e
464
+ if @config[:debug]
465
+ puts "[AgentBill] Failed to track conversion: #{e.message}"
466
+ end
467
+ {
468
+ success: false,
469
+ error: e.message
470
+ }
471
+ end
472
+ end
473
+
322
474
  def flush
323
475
  @tracer.flush
324
476
  end
477
+
478
+ private
479
+
480
+ # Cache AI response for semantic caching (v7.5.0)
481
+ def cache_response(model, prompt_hash, response_content, tokens_used = 0, cost = 0.0)
482
+ return if response_content.nil? || response_content.empty?
483
+
484
+ uri = URI("#{@config[:base_url] || 'https://api.agentbill.io'}/functions/v1/cache-ai-response")
485
+
486
+ payload = {
487
+ api_key: @config[:api_key],
488
+ prompt_hash: prompt_hash,
489
+ response_content: response_content,
490
+ model: model,
491
+ tokens_used: tokens_used,
492
+ cost: cost,
493
+ customer_id: @config[:customer_id],
494
+ agent_id: @config[:agent_id]
495
+ }
496
+
497
+ begin
498
+ http = Net::HTTP.new(uri.host, uri.port)
499
+ http.use_ssl = true
500
+ http.read_timeout = 5
501
+
502
+ request = Net::HTTP::Post.new(uri.path)
503
+ request['Content-Type'] = 'application/json'
504
+ request.body = payload.to_json
505
+
506
+ http.request(request)
507
+ puts "[AgentBill] Response cached for semantic caching" if @config[:debug]
508
+ rescue => e
509
+ puts "[AgentBill] Cache response failed: #{e.message}" if @config[:debug]
510
+ end
511
+ end
325
512
  end
326
513
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: agentbill-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.1
4
+ version: 9.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - AgentBill
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-06 00:00:00.000000000 Z
11
+ date: 2026-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -65,12 +65,26 @@ files:
65
65
  - agentbill.gemspec
66
66
  - examples/anthropic_basic.rb
67
67
  - examples/manual_otel_tracking.rb
68
+ - examples/ollama_basic.rb
68
69
  - examples/openai_basic.rb
69
70
  - examples/openai_custom_event.rb
71
+ - examples/perplexity_basic.rb
70
72
  - examples/zero_config.rb
71
73
  - lib/agentbill.rb
74
+ - lib/agentbill/agents.rb
75
+ - lib/agentbill/attributes.rb
76
+ - lib/agentbill/customers.rb
77
+ - lib/agentbill/distributed.rb
78
+ - lib/agentbill/exceptions.rb
79
+ - lib/agentbill/ollama_wrapper.rb
80
+ - lib/agentbill/orders.rb
81
+ - lib/agentbill/perplexity_wrapper.rb
82
+ - lib/agentbill/signal_types.rb
83
+ - lib/agentbill/signals.rb
72
84
  - lib/agentbill/tracer.rb
85
+ - lib/agentbill/tracing.rb
73
86
  - lib/agentbill/version.rb
87
+ - lib/agentbill/wrappers.rb
74
88
  homepage: https://github.com/Agent-Bill/Ruby
75
89
  licenses:
76
90
  - MIT