tracebook 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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +10 -0
  3. data/CHANGELOG.md +43 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +881 -0
  6. data/Rakefile +21 -0
  7. data/app/assets/images/tracebook/.keep +0 -0
  8. data/app/assets/javascripts/tracebook/application.js +88 -0
  9. data/app/assets/stylesheets/tracebook/application.css +173 -0
  10. data/app/controllers/concerns/.keep +0 -0
  11. data/app/controllers/tracebook/application_controller.rb +4 -0
  12. data/app/controllers/tracebook/exports_controller.rb +25 -0
  13. data/app/controllers/tracebook/interactions_controller.rb +71 -0
  14. data/app/helpers/tracebook/application_helper.rb +4 -0
  15. data/app/helpers/tracebook/interactions_helper.rb +35 -0
  16. data/app/jobs/tracebook/application_job.rb +5 -0
  17. data/app/jobs/tracebook/daily_rollups_job.rb +100 -0
  18. data/app/jobs/tracebook/export_job.rb +162 -0
  19. data/app/jobs/tracebook/persist_interaction_job.rb +160 -0
  20. data/app/mailers/tracebook/application_mailer.rb +6 -0
  21. data/app/models/concerns/.keep +0 -0
  22. data/app/models/tracebook/application_record.rb +5 -0
  23. data/app/models/tracebook/interaction.rb +100 -0
  24. data/app/models/tracebook/pricing_rule.rb +84 -0
  25. data/app/models/tracebook/redaction_rule.rb +81 -0
  26. data/app/models/tracebook/rollup_daily.rb +73 -0
  27. data/app/views/layouts/tracebook/application.html.erb +18 -0
  28. data/app/views/tracebook/interactions/index.html.erb +105 -0
  29. data/app/views/tracebook/interactions/show.html.erb +44 -0
  30. data/config/routes.rb +8 -0
  31. data/db/migrate/20241112000100_create_tracebook_interactions.rb +55 -0
  32. data/db/migrate/20241112000200_create_tracebook_rollups_dailies.rb +24 -0
  33. data/db/migrate/20241112000300_create_tracebook_pricing_rules.rb +21 -0
  34. data/db/migrate/20241112000400_create_tracebook_redaction_rules.rb +19 -0
  35. data/lib/tasks/tracebook_tasks.rake +4 -0
  36. data/lib/tasks/yard.rake +29 -0
  37. data/lib/tracebook/adapters/active_agent.rb +82 -0
  38. data/lib/tracebook/adapters/ruby_llm.rb +97 -0
  39. data/lib/tracebook/adapters.rb +6 -0
  40. data/lib/tracebook/config.rb +130 -0
  41. data/lib/tracebook/engine.rb +5 -0
  42. data/lib/tracebook/errors.rb +9 -0
  43. data/lib/tracebook/mappers/anthropic.rb +59 -0
  44. data/lib/tracebook/mappers/base.rb +38 -0
  45. data/lib/tracebook/mappers/ollama.rb +49 -0
  46. data/lib/tracebook/mappers/openai.rb +75 -0
  47. data/lib/tracebook/mappers.rb +283 -0
  48. data/lib/tracebook/normalized_interaction.rb +86 -0
  49. data/lib/tracebook/pricing/calculator.rb +39 -0
  50. data/lib/tracebook/pricing.rb +5 -0
  51. data/lib/tracebook/redaction_pipeline.rb +88 -0
  52. data/lib/tracebook/redactors/base.rb +29 -0
  53. data/lib/tracebook/redactors/card_pan.rb +15 -0
  54. data/lib/tracebook/redactors/email.rb +15 -0
  55. data/lib/tracebook/redactors/phone.rb +15 -0
  56. data/lib/tracebook/redactors.rb +8 -0
  57. data/lib/tracebook/result.rb +53 -0
  58. data/lib/tracebook/version.rb +3 -0
  59. data/lib/tracebook.rb +201 -0
  60. metadata +164 -0
data/README.md ADDED
@@ -0,0 +1,881 @@
1
+ # TraceBook
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/tracebook.svg)](https://rubygems.org/gems/tracebook)
4
+ [![CI](https://github.com/dpaluy/tracebook/actions/workflows/ci.yml/badge.svg)](https://github.com/dpaluy/tracebook/actions/workflows/ci.yml)
5
+
6
+ TraceBook is a Rails engine that ingests, redacts, encrypts, and reviews LLM interactions. It ships with a Hotwire UI, cost tracking, rollup analytics, and adapters for popular Ruby LLM libraries.
7
+
8
+ ## Features
9
+
10
+ - **Privacy-first**: Request/response payloads are redacted (PII removal) and encrypted at rest
11
+ - **Cost tracking**: Automatic token usage and cost calculation per provider/model
12
+ - **Review workflow**: Approve, flag, or reject interactions with audit trail
13
+ - **Hierarchical sessions**: Track parent-child relationships for agent chains
14
+ - **Analytics**: Daily rollups for reporting and cost analysis
15
+ - **Flexible adapters**: Built-in support for multiple providers; easy to extend
16
+ - **Production-ready**: Async job processing, export to CSV/NDJSON, filterable dashboards
17
+
18
+ ## Requirements
19
+
20
+ - Ruby 3.2+
21
+ - Rails 8.1+
22
+ - ActiveJob backend (`:async` for development; Sidekiq/SolidQueue for production)
23
+ - Database with JSONB support (PostgreSQL recommended)
24
+
25
+ ## Table of Contents
26
+
27
+ - [Installation](#installation--setup)
28
+ - [Configuration](#configuration)
29
+ - [Capturing Interactions](#capturing-interactions)
30
+ - [Manual API](#manual-api)
31
+ - [Built-in Adapters](#built-in-adapters)
32
+ - [Creating Custom Adapters](#creating-custom-adapters)
33
+ - [Creating Custom Mappers](#creating-custom-mappers)
34
+ - [Cost Tracking](#cost-tracking)
35
+ - [Reviewing Data](#reviewing-data)
36
+ - [Production Setup](#production-setup)
37
+ - [Development & Testing](#development--testing)
38
+
39
+ ## Installation & Setup
40
+
41
+ ### 1. Add the gem
42
+
43
+ ```ruby
44
+ # Gemfile
45
+ gem "tracebook" # or path: "../gems/tracebook" for local development
46
+ ```
47
+
48
+ ```bash
49
+ bundle install
50
+ ```
51
+
52
+ ### 2. Install migrations
53
+
54
+ Rails engines keep migrations inside the gem. Copy them into the host app and migrate:
55
+
56
+ ```bash
57
+ bin/rails railties:install:migrations FROM=tracebook
58
+ bin/rails db:migrate
59
+ ```
60
+
61
+ This creates the following tables:
62
+
63
+ - **`tracebook_interactions`** — Main table for LLM calls
64
+ - Encrypted columns: `request`, `response`, `review_comment`
65
+ - Indexes: `provider`, `model`, `review_state`, `session_id`, `parent_id`, `occurred_at`
66
+ - Fields: provider, model, request (JSONB), response (JSONB), usage stats, duration, review metadata, tags, etc.
67
+
68
+ - **`tracebook_pricing_rules`** — Cost per 1k tokens for provider/model patterns
69
+ - Fields: `provider`, `model_pattern` (glob), `input_per_1k`, `output_per_1k`, `currency`, `effective_from`
70
+ - Example: `provider: "openai", model_pattern: "gpt-4o*", input_per_1k: 2.50, output_per_1k: 10.00`
71
+
72
+ - **`tracebook_redaction_rules`** — PII detection patterns
73
+ - Fields: `name`, `pattern` (regex), `detector` (class name), `replacement`, `enabled`
74
+ - Built-in rules: email addresses, SSNs, credit cards, phone numbers
75
+
76
+ - **`tracebook_rollup_daily`** — Aggregated metrics by date/provider/model/project
77
+ - Composite PK: `(date, provider, model, project)`
78
+ - Fields: call counts, token sums, cost totals, error rates, avg duration
79
+
80
+ Re-run `railties:install:migrations` whenever the gem adds new migrations.
81
+
82
+ ### 3. Mount the engine
83
+
84
+ Update `config/routes.rb` to expose the UI:
85
+
86
+ ```ruby
87
+ # config/routes.rb
88
+ Rails.application.routes.draw do
89
+ # Wrap with authentication/authorization
90
+ authenticate :user, ->(u) { u.admin? } do
91
+ mount TraceBook::Engine => "/tracebook"
92
+ end
93
+
94
+ # Or use a constraint
95
+ constraints -> (req) { req.session[:user_id] && User.find(req.session[:user_id])&.admin? } do
96
+ mount TraceBook::Engine => "/tracebook"
97
+ end
98
+ end
99
+ ```
100
+
101
+ Only trusted reviewers should access the dashboard.
102
+
103
+ ### 4. Configure encryption
104
+
105
+ TraceBook uses ActiveRecord::Encryption to protect sensitive data. Generate keys and configure in your credentials:
106
+
107
+ ```bash
108
+ # Generate encryption keys
109
+ bin/rails db:encryption:init
110
+ ```
111
+
112
+ This outputs:
113
+
114
+ ```yaml
115
+ active_record_encryption:
116
+ primary_key: [generated_key]
117
+ deterministic_key: [generated_key]
118
+ key_derivation_salt: [generated_salt]
119
+ ```
120
+
121
+ Add these to `config/credentials.yml.enc`:
122
+
123
+ ```bash
124
+ EDITOR=vim bin/rails credentials:edit
125
+ ```
126
+
127
+ ```yaml
128
+ # config/credentials.yml.enc
129
+ active_record_encryption:
130
+ primary_key: <generated_key>
131
+ deterministic_key: <generated_key>
132
+ key_derivation_salt: <generated_salt>
133
+ ```
134
+
135
+ Without these keys, TraceBook will raise an error when persisting interactions.
136
+
137
+ ## Configuration
138
+
139
+ Create `config/initializers/tracebook.rb`:
140
+
141
+ ```ruby
142
+ TraceBook.configure do |config|
143
+ # ========================================
144
+ # REQUIRED: Authorization
145
+ # ========================================
146
+ # This proc receives (user, action, resource) and must return true/false
147
+ # Actions: :read, :review, :export
148
+ config.authorize = ->(user, action, resource) do
149
+ case action
150
+ when :read
151
+ user&.admin? || user&.reviewer?
152
+ when :review
153
+ user&.admin?
154
+ when :export
155
+ user&.admin?
156
+ else
157
+ false
158
+ end
159
+ end
160
+
161
+ # ========================================
162
+ # OPTIONAL: Project & Metadata
163
+ # ========================================
164
+ # Project identifier for this application
165
+ config.project_name = "TraceBook Dashboard"
166
+
167
+ # ========================================
168
+ # OPTIONAL: Persistence
169
+ # ========================================
170
+ # Use async jobs for persistence (recommended for production)
171
+ # When true, TraceBook.record! enqueues PersistInteractionJob
172
+ # When false, writes happen inline (useful for tests)
173
+ config.persist_async = Rails.env.production?
174
+
175
+ # Payload size threshold for ActiveStorage spillover (bytes)
176
+ # Interactions larger than this are stored in ActiveStorage
177
+ config.inline_payload_bytes = 64 * 1024 # 64KB
178
+
179
+ # ========================================
180
+ # OPTIONAL: Cost Tracking
181
+ # ========================================
182
+ # Default currency for cost calculations
183
+ config.default_currency = "USD"
184
+
185
+ # ========================================
186
+ # OPTIONAL: Export
187
+ # ========================================
188
+ # Available export formats
189
+ config.export_formats = %i[csv ndjson]
190
+
191
+ # ========================================
192
+ # OPTIONAL: Redaction
193
+ # ========================================
194
+ # Custom PII redactors (in addition to built-in email/SSN/etc)
195
+ config.redactors += [
196
+ ->(payload) {
197
+ # Example: Redact API keys
198
+ payload.gsub(/api_key["\s]*[:=]["\s]*\K[\w-]+/, "[REDACTED]")
199
+ },
200
+ ->(payload) {
201
+ # Example: Redact authorization headers
202
+ payload.gsub(/authorization["\s]*:["\s]*\K[^"]+/, "[REDACTED]")
203
+ }
204
+ ]
205
+ end
206
+ ```
207
+
208
+ **Important**: The `authorize` proc is mandatory. TraceBook will raise an error on boot if it's missing.
209
+
210
+ Configuration is frozen after the block runs. Call `TraceBook.reset_configuration!` in tests when you need a clean slate.
211
+
212
+ ## Capturing Interactions
213
+
214
+ ### Manual API
215
+
216
+ Call `TraceBook.record!` anywhere you have access to an LLM request/response:
217
+
218
+ ```ruby
219
+ TraceBook.record!(
220
+ provider: "openai",
221
+ model: "gpt-4o-mini",
222
+ project: "support",
223
+ request_payload: { messages: messages, temperature: 0.2 },
224
+ response_payload: response_body,
225
+ input_tokens: usage[:prompt_tokens],
226
+ output_tokens: usage[:completion_tokens],
227
+ latency_ms: 187,
228
+ status: :success,
229
+ tags: %w[triage priority],
230
+ metadata: { ticket_id: ticket.id },
231
+ user: current_user,
232
+ session_id: session_id,
233
+ parent_id: parent_interaction_id
234
+ )
235
+ ```
236
+
237
+ **Parameters:**
238
+
239
+ - **Required:**
240
+ - `provider` (String) — LLM provider name (e.g., "openai", "anthropic", "ollama")
241
+ - `model` (String) — Model identifier (e.g., "gpt-4o", "claude-3-5-sonnet-20241022")
242
+
243
+ - **Optional:**
244
+ - `project` (String) — Project/app name for filtering
245
+ - `request_payload` (Hash) — Full request sent to provider
246
+ - `response_payload` (Hash) — Full response from provider
247
+ - `request_text` (String) — Human-readable request summary
248
+ - `response_text` (String) — Human-readable response summary
249
+ - `input_tokens` (Integer) — Prompt token count
250
+ - `output_tokens` (Integer) — Completion token count
251
+ - `latency_ms` (Integer) — Request duration in milliseconds
252
+ - `status` (Symbol) — `:success`, `:error`, `:canceled`
253
+ - `error_class` (String) — Exception class name on failure
254
+ - `error_message` (String) — Exception message on failure
255
+ - `tags` (Array<String>) — Labels for filtering (e.g., ["prod", "high-priority"])
256
+ - `metadata` (Hash) — Custom metadata (e.g., `{ ticket_id: 123 }`)
257
+ - `user` (ActiveRecord object) — Associated user (polymorphic)
258
+ - `session_id` (String) — Session identifier for grouping related calls
259
+ - `parent_id` (Integer) — Parent `Interaction` ID for hierarchical chains
260
+
261
+ **Return value:**
262
+
263
+ ```ruby
264
+ result = TraceBook.record!(...)
265
+ result.success? # => true/false
266
+ result.error # => exception when persistence failed
267
+ result.interaction # => AR record when persisted inline (persist_async = false)
268
+ ```
269
+
270
+ When `config.persist_async = true`, the interaction is enqueued via `Tracebook::PersistInteractionJob`.
271
+
272
+ ### Background Jobs & Rollups
273
+
274
+ **PersistInteractionJob** handles redaction, encryption, cost calculation, and writes the `Interaction` record.
275
+
276
+ **DailyRollupsJob** summarizes counts, token totals, and cost into `RollupDaily` rows. Schedule it nightly per provider/model/project:
277
+
278
+ ```ruby
279
+ # Example: Schedule with Sidekiq Cron or whenever
280
+ Tracebook::DailyRollupsJob.perform_later(
281
+ date: Date.yesterday,
282
+ provider: "openai",
283
+ model: "gpt-4o",
284
+ project: nil
285
+ )
286
+ ```
287
+
288
+ Wrap this in your scheduler to cover all active provider/model/project combinations.
289
+
290
+ **ExportJob** streams large CSV/NDJSON exports respecting your filters.
291
+
292
+ ## Built-in Adapters
293
+
294
+ TraceBook ships with adapters that automatically capture LLM interactions from popular libraries. Adapters normalize provider-specific responses and call `TraceBook.record!`, so you get instrumentation without modifying application code.
295
+
296
+ ### RubyLLM Adapter
297
+
298
+ The RubyLLM adapter subscribes to `ActiveSupport::Notifications` events (default: `ruby_llm.request`).
299
+
300
+ **Setup:**
301
+
302
+ ```ruby
303
+ # config/initializers/tracebook_adapters.rb
304
+ TraceBook::Adapters::RubyLLM.enable!
305
+ ```
306
+
307
+ **Emit events from your LLM client:**
308
+
309
+ ```ruby
310
+ # Example: Wrapping an OpenAI client call
311
+ class OpenAIService
312
+ def chat_completion(messages:, model: "gpt-4o", **options)
313
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
314
+
315
+ request = {
316
+ model: model,
317
+ messages: messages,
318
+ **options
319
+ }
320
+
321
+ begin
322
+ response = openai_client.chat(parameters: request)
323
+ elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).to_i
324
+
325
+ ActiveSupport::Notifications.instrument("ruby_llm.request", {
326
+ provider: "openai",
327
+ request: request,
328
+ response: response,
329
+ meta: {
330
+ project: "support-chatbot",
331
+ tags: ["customer-support", "triage"],
332
+ user: current_user,
333
+ session_id: session.id,
334
+ latency_ms: elapsed_ms,
335
+ status: :success
336
+ }
337
+ })
338
+
339
+ response
340
+ rescue => e
341
+ elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).to_i
342
+
343
+ ActiveSupport::Notifications.instrument("ruby_llm.request", {
344
+ provider: "openai",
345
+ request: request,
346
+ response: nil,
347
+ meta: {
348
+ project: "support-chatbot",
349
+ user: current_user,
350
+ session_id: session.id,
351
+ latency_ms: elapsed_ms,
352
+ status: :error,
353
+ error_class: e.class.name,
354
+ error_message: e.message
355
+ }
356
+ })
357
+
358
+ raise
359
+ end
360
+ end
361
+
362
+ private
363
+
364
+ def openai_client
365
+ @openai_client ||= OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
366
+ end
367
+ end
368
+ ```
369
+
370
+ **Supported providers:** OpenAI, Anthropic, Ollama (built-in mappers). Other providers use the fallback mapper.
371
+
372
+ **Custom event name:**
373
+
374
+ ```ruby
375
+ # If your library uses a different event name
376
+ TraceBook::Adapters::RubyLLM.enable!(instrumentation: "my_llm.complete")
377
+ ```
378
+
379
+ **Disabling:**
380
+
381
+ ```ruby
382
+ # In test environment or when switching instrumentation
383
+ TraceBook::Adapters::RubyLLM.disable!
384
+ ```
385
+
386
+ ### ActiveAgent Adapter
387
+
388
+ For applications using ActiveAgent (agentic frameworks), enable the bus adapter:
389
+
390
+ ```ruby
391
+ # config/initializers/tracebook_adapters.rb
392
+ TraceBook::Adapters::ActiveAgent.enable!(bus: ActiveAgent::Bus)
393
+ ```
394
+
395
+ The adapter automatically captures agent interactions including parent-child relationships for hierarchical agent chains.
396
+
397
+ **Note:** If you omit `bus:`, the adapter attempts to locate `ActiveAgent::Bus` automatically when loaded.
398
+
399
+ ## Creating Custom Adapters
400
+
401
+ Adapters follow a simple pattern:
402
+
403
+ 1. Listen to whatever instrumentation your LLM client exposes (Notifications, middleware, observers, etc.)
404
+ 2. Normalize the payload using `Tracebook::Mappers.normalize` or build a `NormalizedInteraction` manually
405
+ 3. Call `TraceBook.record!(**normalized.to_h)`
406
+
407
+ **Example: Custom adapter for Langchain.rb**
408
+
409
+ ```ruby
410
+ # lib/tracebook/adapters/langchain_rb.rb
411
+ module Tracebook
412
+ module Adapters
413
+ module LangchainRb
414
+ extend self
415
+
416
+ def enable!
417
+ return if @enabled
418
+
419
+ # Hook into Langchain's middleware or callback system
420
+ ::Langchain::LLM::Base.after_completion do |llm, request, response, duration|
421
+ handle_completion(
422
+ provider: llm.class.provider_name,
423
+ request: request,
424
+ response: response,
425
+ duration_ms: (duration * 1000).to_i,
426
+ meta: {
427
+ project: "langchain-app",
428
+ user: Current.user,
429
+ session_id: Current.session_id
430
+ }
431
+ )
432
+ end
433
+
434
+ @enabled = true
435
+ end
436
+
437
+ def disable!
438
+ # Unhook callback
439
+ @enabled = false
440
+ end
441
+
442
+ private
443
+
444
+ def handle_completion(provider:, request:, response:, duration_ms:, meta:)
445
+ normalized = Tracebook::Mappers.normalize(
446
+ provider,
447
+ raw_request: request,
448
+ raw_response: response,
449
+ meta: meta.merge(latency_ms: duration_ms)
450
+ )
451
+
452
+ TraceBook.record!(**normalized.to_h)
453
+ rescue => error
454
+ Rails.logger.error("TraceBook LangchainRb adapter error: #{error.message}")
455
+ end
456
+ end
457
+ end
458
+ end
459
+
460
+ TraceBook = Tracebook unless defined?(TraceBook)
461
+ ```
462
+
463
+ **Enable your adapter:**
464
+
465
+ ```ruby
466
+ # config/initializers/tracebook_adapters.rb
467
+ require "tracebook/adapters/langchain_rb"
468
+ Tracebook::Adapters::LangchainRb.enable!
469
+ ```
470
+
471
+ ## Creating Custom Mappers
472
+
473
+ Mappers normalize provider-specific request/response formats into TraceBook's standard schema. Create a custom mapper when the built-in ones (OpenAI, Anthropic, Ollama) don't match your provider's format.
474
+
475
+ **Example: Custom mapper for Cohere**
476
+
477
+ ```ruby
478
+ # lib/tracebook/mappers/cohere.rb
479
+ module Tracebook
480
+ module Mappers
481
+ class Cohere < Base
482
+ def self.normalize(raw_request:, raw_response:, meta: {})
483
+ new.normalize(
484
+ raw_request: raw_request,
485
+ raw_response: raw_response,
486
+ meta: meta
487
+ )
488
+ end
489
+
490
+ def normalize(raw_request:, raw_response:, meta: {})
491
+ request = symbolize(raw_request || {})
492
+ response = symbolize(raw_response || {})
493
+ meta_info = indifferent_meta(meta)
494
+
495
+ build_interaction(
496
+ provider: "cohere",
497
+ model: request[:model] || response[:model],
498
+ project: meta_info[:project],
499
+ request_payload: raw_request,
500
+ response_payload: raw_response,
501
+ request_text: request[:message] || request[:prompt],
502
+ response_text: extract_response_text(response),
503
+ input_tokens: extract_token_count(response, :prompt_tokens),
504
+ output_tokens: extract_token_count(response, :completion_tokens),
505
+ latency_ms: meta_info[:latency_ms],
506
+ status: meta_info[:status]&.to_sym || :success,
507
+ error_class: nil,
508
+ error_message: nil,
509
+ tags: Array(meta_info[:tags]).compact,
510
+ metadata: extract_metadata(response),
511
+ user: meta_info[:user],
512
+ parent_id: meta_info[:parent_id],
513
+ session_id: meta_info[:session_id]
514
+ )
515
+ end
516
+
517
+ private
518
+
519
+ def extract_response_text(response)
520
+ response[:text] || response.dig(:generations, 0, :text)
521
+ end
522
+
523
+ def extract_token_count(response, key)
524
+ response.dig(:meta, :billed_units, key)&.to_i
525
+ end
526
+
527
+ def extract_metadata(response)
528
+ metadata = {}
529
+ metadata["generation_id"] = response[:generation_id] if response[:generation_id]
530
+ metadata["finish_reason"] = response[:finish_reason] if response[:finish_reason]
531
+ compact_hash(metadata)
532
+ end
533
+ end
534
+ end
535
+ end
536
+
537
+ TraceBook = Tracebook unless defined?(TraceBook)
538
+ ```
539
+
540
+ **Register your mapper:**
541
+
542
+ ```ruby
543
+ # lib/tracebook/mappers.rb
544
+ require_relative "mappers/cohere"
545
+
546
+ module Tracebook
547
+ module Mappers
548
+ def normalize(provider, raw_request:, raw_response:, meta: {})
549
+ case provider.to_s
550
+ when "openai"
551
+ normalize_openai(raw_request, raw_response, meta)
552
+ when "anthropic"
553
+ normalize_anthropic(raw_request, raw_response, meta)
554
+ when "ollama"
555
+ normalize_ollama(raw_request, raw_response, meta)
556
+ when "cohere"
557
+ Mappers::Cohere.normalize(
558
+ raw_request: raw_request,
559
+ raw_response: raw_response,
560
+ meta: meta
561
+ )
562
+ else
563
+ fallback_normalized(provider, raw_request, raw_response, meta)
564
+ end
565
+ end
566
+ end
567
+ end
568
+ ```
569
+
570
+ **Mapper requirements:**
571
+
572
+ - Inherit from `Tracebook::Mappers::Base`
573
+ - Implement `.normalize(raw_request:, raw_response:, meta:)`
574
+ - Return a `Tracebook::NormalizedInteraction` instance
575
+ - Handle missing fields gracefully (return `nil` for unavailable data)
576
+ - Extract token counts if available, otherwise leave as `nil`
577
+
578
+ ## Cost Tracking
579
+
580
+ TraceBook automatically calculates costs based on `PricingRule` records. Create pricing rules for your providers/models:
581
+
582
+ ```ruby
583
+ # db/seeds.rb or a migration
584
+
585
+ # OpenAI pricing (as of 2024)
586
+ TraceBook::PricingRule.create!(
587
+ provider: "openai",
588
+ model_pattern: "gpt-4o",
589
+ input_per_1k: 2.50,
590
+ output_per_1k: 10.00,
591
+ currency: "USD",
592
+ effective_from: Date.new(2024, 8, 6)
593
+ )
594
+
595
+ TraceBook::PricingRule.create!(
596
+ provider: "openai",
597
+ model_pattern: "gpt-4o-mini",
598
+ input_per_1k: 0.150,
599
+ output_per_1k: 0.600,
600
+ currency: "USD",
601
+ effective_from: Date.new(2024, 7, 18)
602
+ )
603
+
604
+ TraceBook::PricingRule.create!(
605
+ provider: "openai",
606
+ model_pattern: "o1",
607
+ input_per_1k: 15.00,
608
+ output_per_1k: 60.00,
609
+ currency: "USD",
610
+ effective_from: Date.new(2024, 12, 17)
611
+ )
612
+
613
+ # Anthropic pricing
614
+ TraceBook::PricingRule.create!(
615
+ provider: "anthropic",
616
+ model_pattern: "claude-3-5-sonnet-*",
617
+ input_per_1k: 3.00,
618
+ output_per_1k: 15.00,
619
+ currency: "USD",
620
+ effective_from: Date.new(2024, 10, 22)
621
+ )
622
+
623
+ TraceBook::PricingRule.create!(
624
+ provider: "anthropic",
625
+ model_pattern: "claude-3-5-haiku-*",
626
+ input_per_1k: 1.00,
627
+ output_per_1k: 5.00,
628
+ currency: "USD",
629
+ effective_from: Date.new(2024, 11, 1)
630
+ )
631
+
632
+ # Ollama (free/local)
633
+ TraceBook::PricingRule.create!(
634
+ provider: "ollama",
635
+ model_pattern: "*",
636
+ input_per_1k: 0.0,
637
+ output_per_1k: 0.0,
638
+ currency: "USD",
639
+ effective_from: Date.new(2024, 1, 1)
640
+ )
641
+ ```
642
+
643
+ **Glob patterns:**
644
+
645
+ - `gpt-4o` — Exact match
646
+ - `gpt-4o*` — Matches `gpt-4o`, `gpt-4o-mini`, `gpt-4o-2024-08-06`
647
+ - `claude-3-5-*` — Matches all Claude 3.5 models
648
+ - `*` — Matches everything (fallback rule)
649
+
650
+ TraceBook uses the most specific matching rule. If multiple rules match, it prefers the most recently effective one.
651
+
652
+ ## Reviewing Data
653
+
654
+ ### Dashboard UI
655
+
656
+ Visit the mount path (`/tracebook` by default) to access the dashboard.
657
+
658
+ **Index screen:**
659
+
660
+ - **Filters**: Provider, model, project, status, review state, tags, date range
661
+ - **KPI tiles**: Total calls, tokens used, total cost, error rate, avg latency
662
+ - **Interaction table**: Columns include:
663
+ - Timestamp
664
+ - Label (first 100 chars of request)
665
+ - User
666
+ - Provider/Model
667
+ - Tokens (input/output)
668
+ - Cost
669
+ - Duration (ms)
670
+ - Review state
671
+ - Actions (Approve/Flag/Reject, detail link)
672
+
673
+ **Detail screen:**
674
+
675
+ - **Header**: ID, label, user, timestamp, review state dropdown + comment form
676
+ - **Metrics panel**: Model, duration, token breakdown, cost, HTTP status
677
+ - **Collapsible sections**:
678
+ - Input (messages)
679
+ - Output (text + tool calls)
680
+ - Full JSON (request/response payloads)
681
+ - Error (if failed)
682
+ - **Sidebar**: Parent/child links, tags, session breadcrumb
683
+
684
+ **Keyboard shortcuts:**
685
+
686
+ - `j/k` — Navigate rows
687
+ - `a` — Approve selected
688
+ - `f` — Flag selected
689
+ - `r` — Reject selected
690
+ - `?` — Show help
691
+
692
+ **Bulk review:**
693
+
694
+ Select multiple interactions using checkboxes, then apply a review state to all at once.
695
+
696
+ ### Review Workflow
697
+
698
+ Interactions start in `unreviewed` state. Reviewers can transition to:
699
+
700
+ - **`approved`** — Interaction is acceptable; no issues found
701
+ - **`flagged`** — Interaction requires attention (e.g., sensitive data, unexpected behavior)
702
+ - **`rejected`** — Interaction is problematic and should not have occurred
703
+
704
+ Only `admin` users (as defined in your `authorize` proc) can change review states.
705
+
706
+ ## Production Setup
707
+
708
+ ### Queue Adapter
709
+
710
+ Configure ActiveJob to use a production queue backend:
711
+
712
+ ```ruby
713
+ # config/environments/production.rb
714
+ config.active_job.queue_adapter = :sidekiq # or :solid_queue, etc.
715
+ ```
716
+
717
+ ### Encryption Keys
718
+
719
+ **Important:** Store encryption keys securely. Never commit them to version control.
720
+
721
+ - Use Rails encrypted credentials (`bin/rails credentials:edit`)
722
+ - Or environment variables with a secrets manager (AWS Secrets Manager, HashiCorp Vault)
723
+
724
+ ### Scheduling Rollup Jobs
725
+
726
+ Use a scheduler to run `DailyRollupsJob` nightly:
727
+
728
+ **Sidekiq Cron:**
729
+
730
+ ```ruby
731
+ # config/initializers/sidekiq_cron.rb
732
+ Sidekiq::Cron::Job.create(
733
+ name: "TraceBook daily rollups - OpenAI",
734
+ cron: "0 2 * * *", # 2am daily
735
+ class: "Tracebook::DailyRollupsJob",
736
+ kwargs: { date: Date.yesterday, provider: "openai", model: nil, project: nil }
737
+ )
738
+ ```
739
+
740
+ **Whenever:**
741
+
742
+ ```ruby
743
+ # config/schedule.rb
744
+ every 1.day, at: "2:00 am" do
745
+ runner "Tracebook::DailyRollupsJob.perform_later(date: Date.yesterday, provider: 'openai', model: nil, project: nil)"
746
+ end
747
+ ```
748
+
749
+ ### Monitoring
750
+
751
+ Add error tracking to catch adapter failures:
752
+
753
+ ```ruby
754
+ # config/initializers/tracebook.rb
755
+ TraceBook.configure do |config|
756
+ # Existing config...
757
+
758
+ # Hook into error logging
759
+ config.on_error = ->(error, context) do
760
+ Sentry.capture_exception(error, extra: context) if defined?(Sentry)
761
+ Rails.logger.error("TraceBook error: #{error.message} - #{context.inspect}")
762
+ end
763
+ end
764
+ ```
765
+
766
+ ### Database Indexes
767
+
768
+ TraceBook migrations include indexes for common queries. If you add custom filters, consider additional indexes:
769
+
770
+ ```ruby
771
+ # db/migrate/xxx_add_custom_tracebook_indexes.rb
772
+ class AddCustomTracebookIndexes < ActiveRecord::Migration[7.1]
773
+ def change
774
+ add_index :tracebook_interactions, [:project, :occurred_at], name: "idx_tracebook_project_time"
775
+ add_index :tracebook_interactions, :tags, using: :gin, name: "idx_tracebook_tags"
776
+ end
777
+ end
778
+ ```
779
+
780
+ ### Data Retention
781
+
782
+ Consider archiving or deleting old interactions to manage database size:
783
+
784
+ ```ruby
785
+ # app/jobs/archive_old_interactions_job.rb
786
+ class ArchiveOldInteractionsJob < ApplicationJob
787
+ def perform
788
+ cutoff = 90.days.ago
789
+
790
+ # Option 1: Delete
791
+ TraceBook::Interaction.where("occurred_at < ?", cutoff).delete_all
792
+
793
+ # Option 2: Export to S3 before deleting
794
+ interactions = TraceBook::Interaction.where("occurred_at < ?", cutoff)
795
+ S3Archiver.archive(interactions)
796
+ interactions.delete_all
797
+ end
798
+ end
799
+ ```
800
+
801
+ ## Development & Testing
802
+
803
+ ### Inside the engine repository
804
+
805
+ ```bash
806
+ cd tracebook/
807
+ bundle install
808
+ bundle exec rails db:migrate # Run dummy app migrations
809
+ bundle exec rake test # Run full test suite
810
+ bundle exec rubocop --fix-unsafe # Fix style issues
811
+ ```
812
+
813
+ ### Inside a host application
814
+
815
+ After pulling new migrations from the gem:
816
+
817
+ ```bash
818
+ bin/rails railties:install:migrations FROM=tracebook
819
+ bin/rails db:migrate
820
+ ```
821
+
822
+ ### Testing with adapters disabled
823
+
824
+ ```ruby
825
+ # test/test_helper.rb
826
+ class ActiveSupport::TestCase
827
+ setup do
828
+ TraceBook::Adapters::RubyLLM.disable!
829
+ TraceBook.reset_configuration!
830
+
831
+ TraceBook.configure do |config|
832
+ config.authorize = ->(*) { true }
833
+ config.persist_async = false # Inline for tests
834
+ end
835
+ end
836
+ end
837
+ ```
838
+
839
+ ## API Documentation
840
+
841
+ TraceBook uses [YARD](https://yardoc.org/) for API documentation. The full API docs are available at [rubydoc.info/gems/tracebook](https://rubydoc.info/gems/tracebook).
842
+
843
+ ### Generating Documentation Locally
844
+
845
+ ```bash
846
+ # Install YARD
847
+ bundle install
848
+
849
+ # Generate documentation
850
+ bundle exec rake yard
851
+
852
+ # Generate and open in browser
853
+ bundle exec rake yard:open
854
+
855
+ # View documentation coverage stats
856
+ bundle exec rake yard:stats
857
+ ```
858
+
859
+ Documentation is generated in the `doc/` directory. Open `doc/index.html` in your browser to view.
860
+
861
+ ### Key Documentation Areas
862
+
863
+ - **{Tracebook}** - Main module and `record!` method
864
+ - **{Tracebook::Mappers}** - Provider normalization
865
+ - **{Tracebook::Adapters::RubyLLM}** - ActiveSupport::Notifications adapter
866
+ - **{Tracebook::Interaction}** - ActiveRecord model
867
+ - **{Tracebook::NormalizedInteraction}** - Standard data structure
868
+ - **{Tracebook::Result}** - Return value from `record!`
869
+
870
+ ## Contributing
871
+
872
+ 1. Fork the repo and create a topic branch
873
+ 2. Ensure `bundle exec rake test` passes
874
+ 3. Update documentation and add regression tests for new behavior
875
+ 4. Run `bundle exec rubocop -A` to fix style issues
876
+ 5. Add YARD documentation for new public methods
877
+ 6. Open a PR describing the motivation and changes
878
+
879
+ ## License
880
+
881
+ TraceBook is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).