tracebook 0.1.1 → 1.0.1
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 +4 -4
- data/CHANGELOG.md +56 -24
- data/README.md +197 -713
- data/app/assets/javascripts/tracebook/application.js +92 -35
- data/app/assets/stylesheets/tracebook/application.css +1882 -55
- data/app/controllers/tracebook/application_controller.rb +25 -0
- data/app/controllers/tracebook/chats_controller.rb +229 -0
- data/app/controllers/tracebook/comments_controller.rb +25 -0
- data/app/helpers/tracebook/chats_helper.rb +29 -0
- data/app/models/tracebook/chat_review.rb +19 -0
- data/app/models/tracebook/comment.rb +14 -0
- data/app/models/tracebook/message_cost.rb +12 -0
- data/app/models/tracebook/pricing_rule.rb +6 -8
- data/app/views/tracebook/chats/index.html.erb +77 -0
- data/app/views/tracebook/chats/show.html.erb +94 -0
- data/config/routes.rb +6 -6
- data/db/migrate/20260325000100_create_tracebook_message_costs.rb +19 -0
- data/db/migrate/20260325000200_create_tracebook_chat_reviews.rb +19 -0
- data/db/migrate/{20241112000300_create_tracebook_pricing_rules.rb → 20260325000300_create_tracebook_pricing_rules.rb} +3 -3
- data/db/migrate/20260325000500_create_tracebook_comments.rb +15 -0
- data/lib/generators/tracebook/install/install_generator.rb +6 -9
- data/lib/generators/tracebook/install/templates/initializer.rb.tt +11 -5
- data/lib/tasks/tracebook_tasks.rake +14 -4
- data/lib/tracebook/adapters/ruby_llm.rb +19 -81
- data/lib/tracebook/adapters.rb +5 -4
- data/lib/tracebook/config.rb +83 -104
- data/lib/tracebook/engine.rb +8 -0
- data/lib/tracebook/errors.rb +0 -2
- data/lib/tracebook/pricing/calculator.rb +11 -6
- data/lib/tracebook/pricing.rb +0 -2
- data/lib/tracebook/redaction/pattern.rb +124 -0
- data/lib/tracebook/redaction/pipeline.rb +32 -0
- data/lib/tracebook/seeds/pricing_rules.rb +62 -0
- data/lib/tracebook/version.rb +1 -1
- data/lib/tracebook.rb +46 -152
- metadata +23 -51
- data/app/controllers/tracebook/exports_controller.rb +0 -25
- data/app/controllers/tracebook/interactions_controller.rb +0 -71
- data/app/helpers/tracebook/interactions_helper.rb +0 -35
- data/app/jobs/tracebook/daily_rollups_job.rb +0 -100
- data/app/jobs/tracebook/export_job.rb +0 -162
- data/app/jobs/tracebook/persist_interaction_job.rb +0 -160
- data/app/mailers/tracebook/application_mailer.rb +0 -6
- data/app/models/tracebook/interaction.rb +0 -103
- data/app/models/tracebook/redaction_rule.rb +0 -81
- data/app/models/tracebook/rollup_daily.rb +0 -73
- data/app/views/tracebook/interactions/index.html.erb +0 -108
- data/app/views/tracebook/interactions/show.html.erb +0 -44
- data/db/migrate/20241112000100_create_tracebook_interactions.rb +0 -55
- data/db/migrate/20241112000200_create_tracebook_rollups_dailies.rb +0 -24
- data/db/migrate/20241112000400_create_tracebook_redaction_rules.rb +0 -19
- data/lib/tracebook/adapters/active_agent.rb +0 -82
- data/lib/tracebook/mappers/anthropic.rb +0 -59
- data/lib/tracebook/mappers/base.rb +0 -38
- data/lib/tracebook/mappers/ollama.rb +0 -49
- data/lib/tracebook/mappers/openai.rb +0 -75
- data/lib/tracebook/mappers.rb +0 -283
- data/lib/tracebook/normalized_interaction.rb +0 -86
- data/lib/tracebook/redaction_pipeline.rb +0 -88
- data/lib/tracebook/redactors/base.rb +0 -29
- data/lib/tracebook/redactors/card_pan.rb +0 -15
- data/lib/tracebook/redactors/email.rb +0 -15
- data/lib/tracebook/redactors/phone.rb +0 -15
- data/lib/tracebook/redactors.rb +0 -8
- data/lib/tracebook/result.rb +0 -53
data/README.md
CHANGED
|
@@ -1,45 +1,26 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Tracebook
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/tracebook)
|
|
4
4
|
[](https://github.com/dpaluy/tracebook/actions/workflows/ci.yml)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Cost tracking and review dashboard for [RubyLLM](https://github.com/crmne/ruby_llm) conversations.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Tracebook is a Rails engine that sits on top of RubyLLM's `acts_as_chat` and `acts_as_message` models. It adds per-message cost calculation, chat-level review workflows, and a Hotwire-powered dashboard — without duplicating any conversation data.
|
|
9
9
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **Analytics**: Daily rollups for reporting and cost analysis
|
|
17
|
-
- **Flexible adapters**: Built-in support for multiple providers; easy to extend
|
|
18
|
-
- **Production-ready**: Async job processing, export to CSV/NDJSON, filterable dashboards
|
|
12
|
+
- **Cost tracking**: Per-message cost calculation based on configurable pricing rules
|
|
13
|
+
- **Review workflow**: Approve or flag entire chat conversations with comments
|
|
14
|
+
- **Dashboard**: Browse chats, view conversation threads, see cost/token summaries
|
|
15
|
+
- **RubyLLM native**: Reads directly from your Chat and Message models — no data duplication
|
|
19
16
|
|
|
20
17
|
## Requirements
|
|
21
18
|
|
|
22
|
-
- Ruby 3.
|
|
19
|
+
- Ruby 3.4+
|
|
23
20
|
- Rails 8.1+
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
## Table of Contents
|
|
28
|
-
|
|
29
|
-
- [Installation](#installation--setup)
|
|
30
|
-
- [Configuration](#configuration)
|
|
31
|
-
- [Capturing Interactions](#capturing-interactions)
|
|
32
|
-
- [Manual API](#manual-api)
|
|
33
|
-
- [Built-in Adapters](#built-in-adapters)
|
|
34
|
-
- [Creating Custom Adapters](#creating-custom-adapters)
|
|
35
|
-
- [Creating Custom Mappers](#creating-custom-mappers)
|
|
36
|
-
- [Cost Tracking](#cost-tracking)
|
|
37
|
-
- [Reviewing Data](#reviewing-data)
|
|
38
|
-
- [Production Setup](#production-setup)
|
|
39
|
-
- [Securing the Dashboard](#securing-the-dashboard)
|
|
40
|
-
- [Development & Testing](#development--testing)
|
|
41
|
-
|
|
42
|
-
## Installation & Setup
|
|
21
|
+
- [RubyLLM](https://github.com/crmne/ruby_llm) with `acts_as_chat` / `acts_as_message` models
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
43
24
|
|
|
44
25
|
```bash
|
|
45
26
|
bundle add tracebook
|
|
@@ -47,798 +28,301 @@ bin/rails generate tracebook:install
|
|
|
47
28
|
bin/rails db:migrate
|
|
48
29
|
```
|
|
49
30
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
### Mount the engine
|
|
53
|
-
|
|
54
|
-
Add to `config/routes.rb`:
|
|
31
|
+
Mount the engine in `config/routes.rb`:
|
|
55
32
|
|
|
56
33
|
```ruby
|
|
57
|
-
mount
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
See [Securing the Dashboard](#securing-the-dashboard) for authentication options.
|
|
61
|
-
|
|
62
|
-
### Optional: Configure encryption
|
|
63
|
-
|
|
64
|
-
TraceBook supports ActiveRecord::Encryption for encrypting sensitive payload data at rest. This is **optional** but recommended for production environments handling sensitive data.
|
|
65
|
-
|
|
66
|
-
**Step 1: Generate encryption keys**
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
bin/rails db:encryption:init
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
This outputs:
|
|
73
|
-
|
|
74
|
-
```yaml
|
|
75
|
-
active_record_encryption:
|
|
76
|
-
primary_key: [generated_key]
|
|
77
|
-
deterministic_key: [generated_key]
|
|
78
|
-
key_derivation_salt: [generated_salt]
|
|
34
|
+
mount Tracebook::Engine => "/tracebook"
|
|
79
35
|
```
|
|
80
36
|
|
|
81
|
-
|
|
37
|
+
Seed pricing rules for common providers:
|
|
82
38
|
|
|
83
39
|
```bash
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
```yaml
|
|
88
|
-
# config/credentials.yml.enc
|
|
89
|
-
active_record_encryption:
|
|
90
|
-
primary_key: <generated_key>
|
|
91
|
-
deterministic_key: <generated_key>
|
|
92
|
-
key_derivation_salt: <generated_salt>
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
**Step 3: Enable encryption in your app**
|
|
96
|
-
|
|
97
|
-
Create an initializer to add encryption to the Interaction model:
|
|
98
|
-
|
|
99
|
-
```ruby
|
|
100
|
-
# config/initializers/tracebook_encryption.rb
|
|
101
|
-
Rails.application.config.after_initialize do
|
|
102
|
-
Tracebook::Interaction.class_eval do
|
|
103
|
-
encrypts :request_payload, :response_payload
|
|
104
|
-
end
|
|
105
|
-
end
|
|
40
|
+
bin/rails tracebook:seed_pricing
|
|
106
41
|
```
|
|
107
42
|
|
|
108
|
-
> **Note**: Enabling encryption on an existing database requires migrating existing unencrypted data. See the [Rails encryption guide](https://guides.rubyonrails.org/active_record_encryption.html) for migration strategies.
|
|
109
|
-
|
|
110
43
|
## Configuration
|
|
111
44
|
|
|
112
|
-
The install generator creates `config/initializers/tracebook.rb` with sensible defaults.
|
|
113
|
-
|
|
114
|
-
Available options:
|
|
115
|
-
|
|
116
45
|
```ruby
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
# Set to false for tests or simple setups
|
|
123
|
-
config.persist_async = Rails.env.production?
|
|
46
|
+
# config/initializers/tracebook.rb
|
|
47
|
+
Tracebook.configure do |config|
|
|
48
|
+
# Class names for your RubyLLM models
|
|
49
|
+
config.chat_class = "Chat" # default
|
|
50
|
+
config.message_class = "Message" # default
|
|
124
51
|
|
|
125
|
-
#
|
|
126
|
-
config.
|
|
52
|
+
# Currency for cost calculations
|
|
53
|
+
config.default_currency = "USD" # default
|
|
127
54
|
|
|
128
|
-
#
|
|
129
|
-
config.
|
|
130
|
-
config.auto_subscribe_active_agent = true
|
|
55
|
+
# How to display the user in the dashboard
|
|
56
|
+
config.actor_display = ->(actor) { actor.try(:name) }
|
|
131
57
|
|
|
132
|
-
#
|
|
133
|
-
config.
|
|
134
|
-
->(payload) { payload.gsub(/api_key=\w+/, "api_key=[REDACTED]") }
|
|
135
|
-
]
|
|
58
|
+
# Items per page
|
|
59
|
+
config.per_page = 25 # default
|
|
136
60
|
end
|
|
137
61
|
```
|
|
138
62
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
## Capturing Interactions
|
|
63
|
+
## PII Redaction
|
|
142
64
|
|
|
143
|
-
|
|
65
|
+
Tracebook includes an opt-in PII redaction pipeline for unstructured natural language in LLM conversations. Nothing is redacted unless explicitly configured.
|
|
144
66
|
|
|
145
|
-
|
|
67
|
+
### Enabling Patterns
|
|
146
68
|
|
|
147
69
|
```ruby
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
project: "support",
|
|
152
|
-
request_payload: { messages: messages, temperature: 0.2 },
|
|
153
|
-
response_payload: response_body,
|
|
154
|
-
input_tokens: usage[:prompt_tokens],
|
|
155
|
-
output_tokens: usage[:completion_tokens],
|
|
156
|
-
latency_ms: 187,
|
|
157
|
-
status: :success,
|
|
158
|
-
tags: %w[triage priority],
|
|
159
|
-
metadata: { ticket_id: ticket.id },
|
|
160
|
-
user: current_user,
|
|
161
|
-
session_id: session_id,
|
|
162
|
-
parent_id: parent_interaction_id
|
|
163
|
-
)
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
**Parameters:**
|
|
167
|
-
|
|
168
|
-
- **Required:**
|
|
169
|
-
- `provider` (String) — LLM provider name (e.g., "openai", "anthropic", "ollama")
|
|
170
|
-
- `model` (String) — Model identifier (e.g., "gpt-4o", "claude-3-5-sonnet-20241022")
|
|
171
|
-
|
|
172
|
-
- **Optional:**
|
|
173
|
-
- `project` (String) — Project/app name for filtering
|
|
174
|
-
- `request_payload` (Hash) — Full request sent to provider
|
|
175
|
-
- `response_payload` (Hash) — Full response from provider
|
|
176
|
-
- `request_text` (String) — Human-readable request summary
|
|
177
|
-
- `response_text` (String) — Human-readable response summary
|
|
178
|
-
- `input_tokens` (Integer) — Prompt token count
|
|
179
|
-
- `output_tokens` (Integer) — Completion token count
|
|
180
|
-
- `latency_ms` (Integer) — Request duration in milliseconds
|
|
181
|
-
- `status` (Symbol) — `:success`, `:error`, `:canceled`
|
|
182
|
-
- `error_class` (String) — Exception class name on failure
|
|
183
|
-
- `error_message` (String) — Exception message on failure
|
|
184
|
-
- `tags` (Array<String>) — Labels for filtering (e.g., ["prod", "high-priority"])
|
|
185
|
-
- `metadata` (Hash) — Custom metadata (e.g., `{ ticket_id: 123 }`)
|
|
186
|
-
- `user` (ActiveRecord object) — Associated user (polymorphic)
|
|
187
|
-
- `session_id` (String) — Session identifier for grouping related calls
|
|
188
|
-
- `parent_id` (Integer) — Parent `Interaction` ID for hierarchical chains
|
|
189
|
-
|
|
190
|
-
**Return value:**
|
|
191
|
-
|
|
192
|
-
```ruby
|
|
193
|
-
result = TraceBook.record!(...)
|
|
194
|
-
result.success? # => true/false
|
|
195
|
-
result.error # => exception when persistence failed
|
|
196
|
-
result.interaction # => AR record when persisted inline (persist_async = false)
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
When `config.persist_async = true`, the interaction is enqueued via `Tracebook::PersistInteractionJob`.
|
|
200
|
-
|
|
201
|
-
### Background Jobs & Rollups
|
|
202
|
-
|
|
203
|
-
**PersistInteractionJob** handles redaction, cost calculation, and writes the `Interaction` record.
|
|
204
|
-
|
|
205
|
-
**DailyRollupsJob** summarizes counts, token totals, and cost into `RollupDaily` rows. Schedule it nightly per provider/model/project:
|
|
206
|
-
|
|
207
|
-
```ruby
|
|
208
|
-
# Example: Schedule with Sidekiq Cron or whenever
|
|
209
|
-
Tracebook::DailyRollupsJob.perform_later(
|
|
210
|
-
date: Date.yesterday,
|
|
211
|
-
provider: "openai",
|
|
212
|
-
model: "gpt-4o",
|
|
213
|
-
project: nil
|
|
214
|
-
)
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
Wrap this in your scheduler to cover all active provider/model/project combinations.
|
|
218
|
-
|
|
219
|
-
**ExportJob** streams large CSV/NDJSON exports respecting your filters.
|
|
220
|
-
|
|
221
|
-
## Built-in Adapters
|
|
222
|
-
|
|
223
|
-
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.
|
|
70
|
+
Tracebook.configure do |config|
|
|
71
|
+
# Enable individual patterns
|
|
72
|
+
config.redact :email, :phone, :ssn, :credit_card
|
|
224
73
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
**Setup:**
|
|
230
|
-
|
|
231
|
-
```ruby
|
|
232
|
-
# config/initializers/tracebook_adapters.rb
|
|
233
|
-
TraceBook::Adapters::RubyLLM.enable!
|
|
74
|
+
# Or enable a whole group
|
|
75
|
+
config.redact :pii, :api_keys
|
|
76
|
+
end
|
|
234
77
|
```
|
|
235
78
|
|
|
236
|
-
|
|
79
|
+
### Available Patterns
|
|
80
|
+
|
|
81
|
+
| Pattern | Detects | Validation |
|
|
82
|
+
|---------|---------|------------|
|
|
83
|
+
| `email` | Email addresses | -- |
|
|
84
|
+
| `phone` | Phone numbers (US format) | -- |
|
|
85
|
+
| `ssn` | Social Security Numbers | SSA area-number range check |
|
|
86
|
+
| `credit_card` | Credit card numbers | Luhn algorithm |
|
|
87
|
+
| `openai_key` | OpenAI API keys (`sk-...`) | -- |
|
|
88
|
+
| `anthropic_key` | Anthropic API keys (`sk-ant-...`) | -- |
|
|
89
|
+
| `aws_key` | AWS access key IDs (`AKIA...`) | -- |
|
|
90
|
+
| `stripe_key` | Stripe API keys | -- |
|
|
91
|
+
| `github_token` | GitHub tokens (`ghp_`, `gho_`, etc.) | -- |
|
|
92
|
+
| `ipv4` | IPv4 addresses | Octet range 0-255 |
|
|
93
|
+
| `bearer_token` | Authorization bearer tokens | -- |
|
|
94
|
+
| `jwt` | JSON Web Tokens | -- |
|
|
95
|
+
| `private_key` | PEM-format private key blocks | -- |
|
|
96
|
+
|
|
97
|
+
### Pattern Groups
|
|
98
|
+
|
|
99
|
+
| Group | Patterns included |
|
|
100
|
+
|-------|-------------------|
|
|
101
|
+
| `pii` | `email`, `phone`, `ssn` |
|
|
102
|
+
| `financial` | `credit_card` |
|
|
103
|
+
| `api_keys` | `openai_key`, `anthropic_key`, `aws_key`, `stripe_key`, `github_token` |
|
|
104
|
+
| `auth` | `bearer_token`, `jwt` |
|
|
105
|
+
| `network` | `ipv4` |
|
|
106
|
+
| `crypto` | `private_key` |
|
|
107
|
+
|
|
108
|
+
### Custom Patterns
|
|
237
109
|
|
|
238
110
|
```ruby
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
model: model,
|
|
246
|
-
messages: messages,
|
|
247
|
-
**options
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
begin
|
|
251
|
-
response = openai_client.chat(parameters: request)
|
|
252
|
-
elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).to_i
|
|
253
|
-
|
|
254
|
-
ActiveSupport::Notifications.instrument("ruby_llm.request", {
|
|
255
|
-
provider: "openai",
|
|
256
|
-
request: request,
|
|
257
|
-
response: response,
|
|
258
|
-
meta: {
|
|
259
|
-
project: "support-chatbot",
|
|
260
|
-
tags: ["customer-support", "triage"],
|
|
261
|
-
user: current_user,
|
|
262
|
-
session_id: session.id,
|
|
263
|
-
latency_ms: elapsed_ms,
|
|
264
|
-
status: :success
|
|
265
|
-
}
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
response
|
|
269
|
-
rescue => e
|
|
270
|
-
elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).to_i
|
|
271
|
-
|
|
272
|
-
ActiveSupport::Notifications.instrument("ruby_llm.request", {
|
|
273
|
-
provider: "openai",
|
|
274
|
-
request: request,
|
|
275
|
-
response: nil,
|
|
276
|
-
meta: {
|
|
277
|
-
project: "support-chatbot",
|
|
278
|
-
user: current_user,
|
|
279
|
-
session_id: session.id,
|
|
280
|
-
latency_ms: elapsed_ms,
|
|
281
|
-
status: :error,
|
|
282
|
-
error_class: e.class.name,
|
|
283
|
-
error_message: e.message
|
|
284
|
-
}
|
|
285
|
-
})
|
|
286
|
-
|
|
287
|
-
raise
|
|
288
|
-
end
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
private
|
|
292
|
-
|
|
293
|
-
def openai_client
|
|
294
|
-
@openai_client ||= OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
|
|
295
|
-
end
|
|
111
|
+
Tracebook.configure do |config|
|
|
112
|
+
config.redact_pattern(
|
|
113
|
+
/policy[:\s]*\d{10}/i,
|
|
114
|
+
"[POLICY_NUMBER]",
|
|
115
|
+
name: "policy_number"
|
|
116
|
+
)
|
|
296
117
|
end
|
|
297
118
|
```
|
|
298
119
|
|
|
299
|
-
|
|
120
|
+
### Custom Redactors
|
|
300
121
|
|
|
301
|
-
|
|
122
|
+
Provide any callable (proc, lambda, or object responding to `call`):
|
|
302
123
|
|
|
303
124
|
```ruby
|
|
304
|
-
|
|
305
|
-
|
|
125
|
+
Tracebook.configure do |config|
|
|
126
|
+
config.custom_redactors << ->(text) {
|
|
127
|
+
text.gsub(/MRN-\d{8}/, "[MEDICAL_RECORD]")
|
|
128
|
+
}
|
|
129
|
+
end
|
|
306
130
|
```
|
|
307
131
|
|
|
308
|
-
|
|
132
|
+
### Using Redaction
|
|
309
133
|
|
|
310
134
|
```ruby
|
|
311
|
-
#
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
### ActiveAgent Adapter
|
|
316
|
-
|
|
317
|
-
For applications using ActiveAgent (agentic frameworks), enable the bus adapter:
|
|
135
|
+
# Redact text directly
|
|
136
|
+
Tracebook.redact("Email user@test.com or call 555-123-4567")
|
|
137
|
+
# => "Email [EMAIL] or call [PHONE]"
|
|
318
138
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
139
|
+
# Use in your application before saving messages
|
|
140
|
+
content = Tracebook.redact(user_input)
|
|
141
|
+
chat.ask(content)
|
|
322
142
|
```
|
|
323
143
|
|
|
324
|
-
|
|
144
|
+
### Planned: LLM-Based Redaction
|
|
325
145
|
|
|
326
|
-
|
|
146
|
+
For context-sensitive PII that regex can't catch (e.g. "my social is seven eight two three three three two"), a future version will support LLM-based redaction using a local model (e.g., Ollama) to detect PII in natural language before persistence.
|
|
327
147
|
|
|
328
|
-
##
|
|
148
|
+
## Tracebook Tables
|
|
329
149
|
|
|
330
|
-
|
|
150
|
+
Tracebook adds four tables — all prefixed with `tracebook_` to avoid collisions:
|
|
331
151
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
152
|
+
| Table | Purpose |
|
|
153
|
+
|-------|---------|
|
|
154
|
+
| `tracebook_message_costs` | Cost + latency per message (polymorphic join to your Message) |
|
|
155
|
+
| `tracebook_chat_reviews` | Review state per chat (polymorphic join to your Chat) |
|
|
156
|
+
| `tracebook_comments` | Comments on chat reviews |
|
|
157
|
+
| `tracebook_pricing_rules` | Cost per token by provider/model |
|
|
335
158
|
|
|
336
|
-
|
|
159
|
+
Your Chat and Message tables are untouched.
|
|
337
160
|
|
|
338
|
-
|
|
339
|
-
# lib/tracebook/adapters/langchain_rb.rb
|
|
340
|
-
module Tracebook
|
|
341
|
-
module Adapters
|
|
342
|
-
module LangchainRb
|
|
343
|
-
extend self
|
|
344
|
-
|
|
345
|
-
def enable!
|
|
346
|
-
return if @enabled
|
|
347
|
-
|
|
348
|
-
# Hook into Langchain's middleware or callback system
|
|
349
|
-
::Langchain::LLM::Base.after_completion do |llm, request, response, duration|
|
|
350
|
-
handle_completion(
|
|
351
|
-
provider: llm.class.provider_name,
|
|
352
|
-
request: request,
|
|
353
|
-
response: response,
|
|
354
|
-
duration_ms: (duration * 1000).to_i,
|
|
355
|
-
meta: {
|
|
356
|
-
project: "langchain-app",
|
|
357
|
-
user: Current.user,
|
|
358
|
-
session_id: Current.session_id
|
|
359
|
-
}
|
|
360
|
-
)
|
|
361
|
-
end
|
|
362
|
-
|
|
363
|
-
@enabled = true
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
def disable!
|
|
367
|
-
# Unhook callback
|
|
368
|
-
@enabled = false
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
private
|
|
372
|
-
|
|
373
|
-
def handle_completion(provider:, request:, response:, duration_ms:, meta:)
|
|
374
|
-
normalized = Tracebook::Mappers.normalize(
|
|
375
|
-
provider,
|
|
376
|
-
raw_request: request,
|
|
377
|
-
raw_response: response,
|
|
378
|
-
meta: meta.merge(latency_ms: duration_ms)
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
TraceBook.record!(**normalized.to_h)
|
|
382
|
-
rescue => error
|
|
383
|
-
Rails.logger.error("TraceBook LangchainRb adapter error: #{error.message}")
|
|
384
|
-
end
|
|
385
|
-
end
|
|
386
|
-
end
|
|
387
|
-
end
|
|
161
|
+
## Cost Calculation
|
|
388
162
|
|
|
389
|
-
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
**Enable your adapter:**
|
|
163
|
+
After an LLM response, call `Tracebook.calculate_cost!` to record the cost:
|
|
393
164
|
|
|
394
165
|
```ruby
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
166
|
+
Tracebook.calculate_cost!(
|
|
167
|
+
message,
|
|
168
|
+
provider: "openai",
|
|
169
|
+
model: "gpt-4o",
|
|
170
|
+
latency_ms: elapsed_ms
|
|
171
|
+
)
|
|
398
172
|
```
|
|
399
173
|
|
|
400
|
-
|
|
174
|
+
This looks up the matching pricing rule, calculates input/output costs, and creates a `tracebook_message_costs` record joined to the message.
|
|
401
175
|
|
|
402
|
-
|
|
176
|
+
### Integration Example
|
|
403
177
|
|
|
404
|
-
|
|
178
|
+
In a typical RubyLLM app, hook into the chat response flow:
|
|
405
179
|
|
|
406
180
|
```ruby
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
class Cohere < Base
|
|
411
|
-
def self.normalize(raw_request:, raw_response:, meta: {})
|
|
412
|
-
new.normalize(
|
|
413
|
-
raw_request: raw_request,
|
|
414
|
-
raw_response: raw_response,
|
|
415
|
-
meta: meta
|
|
416
|
-
)
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
def normalize(raw_request:, raw_response:, meta: {})
|
|
420
|
-
request = symbolize(raw_request || {})
|
|
421
|
-
response = symbolize(raw_response || {})
|
|
422
|
-
meta_info = indifferent_meta(meta)
|
|
423
|
-
|
|
424
|
-
build_interaction(
|
|
425
|
-
provider: "cohere",
|
|
426
|
-
model: request[:model] || response[:model],
|
|
427
|
-
project: meta_info[:project],
|
|
428
|
-
request_payload: raw_request,
|
|
429
|
-
response_payload: raw_response,
|
|
430
|
-
request_text: request[:message] || request[:prompt],
|
|
431
|
-
response_text: extract_response_text(response),
|
|
432
|
-
input_tokens: extract_token_count(response, :prompt_tokens),
|
|
433
|
-
output_tokens: extract_token_count(response, :completion_tokens),
|
|
434
|
-
latency_ms: meta_info[:latency_ms],
|
|
435
|
-
status: meta_info[:status]&.to_sym || :success,
|
|
436
|
-
error_class: nil,
|
|
437
|
-
error_message: nil,
|
|
438
|
-
tags: Array(meta_info[:tags]).compact,
|
|
439
|
-
metadata: extract_metadata(response),
|
|
440
|
-
user: meta_info[:user],
|
|
441
|
-
parent_id: meta_info[:parent_id],
|
|
442
|
-
session_id: meta_info[:session_id]
|
|
443
|
-
)
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
private
|
|
447
|
-
|
|
448
|
-
def extract_response_text(response)
|
|
449
|
-
response[:text] || response.dig(:generations, 0, :text)
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
def extract_token_count(response, key)
|
|
453
|
-
response.dig(:meta, :billed_units, key)&.to_i
|
|
454
|
-
end
|
|
455
|
-
|
|
456
|
-
def extract_metadata(response)
|
|
457
|
-
metadata = {}
|
|
458
|
-
metadata["generation_id"] = response[:generation_id] if response[:generation_id]
|
|
459
|
-
metadata["finish_reason"] = response[:finish_reason] if response[:finish_reason]
|
|
460
|
-
compact_hash(metadata)
|
|
461
|
-
end
|
|
462
|
-
end
|
|
463
|
-
end
|
|
464
|
-
end
|
|
181
|
+
class ChatResponseJob < ApplicationJob
|
|
182
|
+
def perform(chat_id, content)
|
|
183
|
+
chat = Chat.find(chat_id)
|
|
465
184
|
|
|
466
|
-
|
|
467
|
-
|
|
185
|
+
chat.ask(content) do |chunk|
|
|
186
|
+
# stream chunks...
|
|
187
|
+
end
|
|
468
188
|
|
|
469
|
-
|
|
189
|
+
# After response, calculate cost for the last assistant message
|
|
190
|
+
message = chat.messages.where(role: "assistant").last
|
|
191
|
+
model = chat.model
|
|
470
192
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
module Mappers
|
|
477
|
-
def normalize(provider, raw_request:, raw_response:, meta: {})
|
|
478
|
-
case provider.to_s
|
|
479
|
-
when "openai"
|
|
480
|
-
normalize_openai(raw_request, raw_response, meta)
|
|
481
|
-
when "anthropic"
|
|
482
|
-
normalize_anthropic(raw_request, raw_response, meta)
|
|
483
|
-
when "ollama"
|
|
484
|
-
normalize_ollama(raw_request, raw_response, meta)
|
|
485
|
-
when "cohere"
|
|
486
|
-
Mappers::Cohere.normalize(
|
|
487
|
-
raw_request: raw_request,
|
|
488
|
-
raw_response: raw_response,
|
|
489
|
-
meta: meta
|
|
490
|
-
)
|
|
491
|
-
else
|
|
492
|
-
fallback_normalized(provider, raw_request, raw_response, meta)
|
|
493
|
-
end
|
|
494
|
-
end
|
|
193
|
+
Tracebook.calculate_cost!(
|
|
194
|
+
message,
|
|
195
|
+
provider: model.provider,
|
|
196
|
+
model: model.model_id
|
|
197
|
+
)
|
|
495
198
|
end
|
|
496
199
|
end
|
|
497
200
|
```
|
|
498
201
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
- Inherit from `Tracebook::Mappers::Base`
|
|
502
|
-
- Implement `.normalize(raw_request:, raw_response:, meta:)`
|
|
503
|
-
- Return a `Tracebook::NormalizedInteraction` instance
|
|
504
|
-
- Handle missing fields gracefully (return `nil` for unavailable data)
|
|
505
|
-
- Extract token counts if available, otherwise leave as `nil`
|
|
202
|
+
## Pricing Rules
|
|
506
203
|
|
|
507
|
-
|
|
204
|
+
Tracebook calculates costs using `PricingRule` records. Seed defaults for common providers:
|
|
508
205
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
```ruby
|
|
512
|
-
# db/seeds.rb or a migration
|
|
513
|
-
|
|
514
|
-
# OpenAI pricing (as of 2024)
|
|
515
|
-
TraceBook::PricingRule.create!(
|
|
516
|
-
provider: "openai",
|
|
517
|
-
model_pattern: "gpt-4o",
|
|
518
|
-
input_per_1k: 2.50,
|
|
519
|
-
output_per_1k: 10.00,
|
|
520
|
-
currency: "USD",
|
|
521
|
-
effective_from: Date.new(2024, 8, 6)
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
TraceBook::PricingRule.create!(
|
|
525
|
-
provider: "openai",
|
|
526
|
-
model_pattern: "gpt-4o-mini",
|
|
527
|
-
input_per_1k: 0.150,
|
|
528
|
-
output_per_1k: 0.600,
|
|
529
|
-
currency: "USD",
|
|
530
|
-
effective_from: Date.new(2024, 7, 18)
|
|
531
|
-
)
|
|
532
|
-
|
|
533
|
-
TraceBook::PricingRule.create!(
|
|
534
|
-
provider: "openai",
|
|
535
|
-
model_pattern: "o1",
|
|
536
|
-
input_per_1k: 15.00,
|
|
537
|
-
output_per_1k: 60.00,
|
|
538
|
-
currency: "USD",
|
|
539
|
-
effective_from: Date.new(2024, 12, 17)
|
|
540
|
-
)
|
|
541
|
-
|
|
542
|
-
# Anthropic pricing
|
|
543
|
-
TraceBook::PricingRule.create!(
|
|
544
|
-
provider: "anthropic",
|
|
545
|
-
model_pattern: "claude-3-5-sonnet-*",
|
|
546
|
-
input_per_1k: 3.00,
|
|
547
|
-
output_per_1k: 15.00,
|
|
548
|
-
currency: "USD",
|
|
549
|
-
effective_from: Date.new(2024, 10, 22)
|
|
550
|
-
)
|
|
551
|
-
|
|
552
|
-
TraceBook::PricingRule.create!(
|
|
553
|
-
provider: "anthropic",
|
|
554
|
-
model_pattern: "claude-3-5-haiku-*",
|
|
555
|
-
input_per_1k: 1.00,
|
|
556
|
-
output_per_1k: 5.00,
|
|
557
|
-
currency: "USD",
|
|
558
|
-
effective_from: Date.new(2024, 11, 1)
|
|
559
|
-
)
|
|
560
|
-
|
|
561
|
-
# Ollama (free/local)
|
|
562
|
-
TraceBook::PricingRule.create!(
|
|
563
|
-
provider: "ollama",
|
|
564
|
-
model_pattern: "*",
|
|
565
|
-
input_per_1k: 0.0,
|
|
566
|
-
output_per_1k: 0.0,
|
|
567
|
-
currency: "USD",
|
|
568
|
-
effective_from: Date.new(2024, 1, 1)
|
|
569
|
-
)
|
|
206
|
+
```bash
|
|
207
|
+
bin/rails tracebook:seed_pricing
|
|
570
208
|
```
|
|
571
209
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
- `gpt-4o` — Exact match
|
|
575
|
-
- `gpt-4o*` — Matches `gpt-4o`, `gpt-4o-mini`, `gpt-4o-2024-08-06`
|
|
576
|
-
- `claude-3-5-*` — Matches all Claude 3.5 models
|
|
577
|
-
- `*` — Matches everything (fallback rule)
|
|
578
|
-
|
|
579
|
-
TraceBook uses the most specific matching rule. If multiple rules match, it prefers the most recently effective one.
|
|
580
|
-
|
|
581
|
-
## Reviewing Data
|
|
582
|
-
|
|
583
|
-
### Dashboard UI
|
|
584
|
-
|
|
585
|
-
Visit the mount path (`/tracebook` by default) to access the dashboard.
|
|
586
|
-
|
|
587
|
-
**Index screen:**
|
|
588
|
-
|
|
589
|
-
- **Filters**: Provider, model, project, status, review state, tags, date range
|
|
590
|
-
- **KPI tiles**: Total calls, tokens used, total cost, error rate, avg latency
|
|
591
|
-
- **Interaction table**: Columns include:
|
|
592
|
-
- Timestamp
|
|
593
|
-
- Label (first 100 chars of request)
|
|
594
|
-
- User
|
|
595
|
-
- Provider/Model
|
|
596
|
-
- Tokens (input/output)
|
|
597
|
-
- Cost
|
|
598
|
-
- Duration (ms)
|
|
599
|
-
- Review state
|
|
600
|
-
- Actions (Approve/Flag/Reject, detail link)
|
|
601
|
-
|
|
602
|
-
**Detail screen:**
|
|
603
|
-
|
|
604
|
-
- **Header**: ID, label, user, timestamp, review state dropdown + comment form
|
|
605
|
-
- **Metrics panel**: Model, duration, token breakdown, cost, HTTP status
|
|
606
|
-
- **Collapsible sections**:
|
|
607
|
-
- Input (messages)
|
|
608
|
-
- Output (text + tool calls)
|
|
609
|
-
- Full JSON (request/response payloads)
|
|
610
|
-
- Error (if failed)
|
|
611
|
-
- **Sidebar**: Parent/child links, tags, session breadcrumb
|
|
210
|
+
This creates rules for OpenAI, Anthropic, Gemini, and Ollama models.
|
|
612
211
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
- `j/k` — Navigate rows
|
|
616
|
-
- `a` — Approve selected
|
|
617
|
-
- `f` — Flag selected
|
|
618
|
-
- `r` — Reject selected
|
|
619
|
-
- `?` — Show help
|
|
620
|
-
|
|
621
|
-
**Bulk review:**
|
|
622
|
-
|
|
623
|
-
Select multiple interactions using checkboxes, then apply a review state to all at once.
|
|
624
|
-
|
|
625
|
-
### Review Workflow
|
|
626
|
-
|
|
627
|
-
Interactions start in `unreviewed` state. Reviewers can transition to:
|
|
628
|
-
|
|
629
|
-
- **`approved`** — Interaction is acceptable; no issues found
|
|
630
|
-
- **`flagged`** — Interaction requires attention (e.g., sensitive data, unexpected behavior)
|
|
631
|
-
- **`rejected`** — Interaction is problematic and should not have occurred
|
|
632
|
-
|
|
633
|
-
Only `admin` users (as defined in your `authorize` proc) can change review states.
|
|
634
|
-
|
|
635
|
-
## Production Setup
|
|
636
|
-
|
|
637
|
-
### Securing the Dashboard
|
|
638
|
-
|
|
639
|
-
The dashboard should only be accessible to trusted reviewers. Here are common approaches:
|
|
640
|
-
|
|
641
|
-
**Devise with admin check:**
|
|
212
|
+
### Adding Custom Rules
|
|
642
213
|
|
|
643
214
|
```ruby
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
215
|
+
Tracebook::PricingRule.create!(
|
|
216
|
+
provider: "xai",
|
|
217
|
+
model_glob: "grok-4-1-fast*",
|
|
218
|
+
input_cents_per_unit: 20, # per 1k tokens
|
|
219
|
+
output_cents_per_unit: 50,
|
|
220
|
+
effective_from: Date.new(2025, 7, 1),
|
|
221
|
+
currency: "USD"
|
|
222
|
+
)
|
|
648
223
|
```
|
|
649
224
|
|
|
650
|
-
|
|
225
|
+
### Glob Patterns
|
|
651
226
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
end
|
|
657
|
-
```
|
|
227
|
+
- `gpt-4o` — exact match
|
|
228
|
+
- `gpt-4o*` — matches `gpt-4o`, `gpt-4o-mini`, `gpt-4o-2024-08-06`
|
|
229
|
+
- `claude-3-5-*` — matches all Claude 3.5 models
|
|
230
|
+
- `*` — fallback for any model
|
|
658
231
|
|
|
659
|
-
|
|
232
|
+
When multiple rules match, Tracebook prefers the most specific glob (most literal characters), then the most recent `effective_from` date.
|
|
660
233
|
|
|
661
|
-
|
|
662
|
-
# config/routes.rb
|
|
663
|
-
TraceBook::Engine.middleware.use Rack::Auth::Basic do |username, password|
|
|
664
|
-
ActiveSupport::SecurityUtils.secure_compare(username, ENV["TRACEBOOK_USER"]) &
|
|
665
|
-
ActiveSupport::SecurityUtils.secure_compare(password, ENV["TRACEBOOK_PASSWORD"])
|
|
666
|
-
end
|
|
234
|
+
## Review Workflow
|
|
667
235
|
|
|
668
|
-
|
|
669
|
-
```
|
|
236
|
+
Reviews happen at the chat level, not per-message. In the dashboard:
|
|
670
237
|
|
|
671
|
-
|
|
238
|
+
1. Open a chat to see the full conversation thread
|
|
239
|
+
2. Click **Approve** or **Flag**
|
|
240
|
+
3. Add comments for context
|
|
672
241
|
|
|
673
|
-
|
|
242
|
+
Programmatic access:
|
|
674
243
|
|
|
675
244
|
```ruby
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
```
|
|
245
|
+
chat = Chat.find(id)
|
|
246
|
+
review = Tracebook::ChatReview.for_chat(chat)
|
|
679
247
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
- Use Rails encrypted credentials (`bin/rails credentials:edit`)
|
|
685
|
-
- Or environment variables with a secrets manager (AWS Secrets Manager, HashiCorp Vault)
|
|
686
|
-
|
|
687
|
-
### Scheduling Rollup Jobs
|
|
688
|
-
|
|
689
|
-
Use a scheduler to run `DailyRollupsJob` nightly:
|
|
690
|
-
|
|
691
|
-
**Sidekiq Cron:**
|
|
692
|
-
|
|
693
|
-
```ruby
|
|
694
|
-
# config/initializers/sidekiq_cron.rb
|
|
695
|
-
Sidekiq::Cron::Job.create(
|
|
696
|
-
name: "TraceBook daily rollups - OpenAI",
|
|
697
|
-
cron: "0 2 * * *", # 2am daily
|
|
698
|
-
class: "Tracebook::DailyRollupsJob",
|
|
699
|
-
kwargs: { date: Date.yesterday, provider: "openai", model: nil, project: nil }
|
|
248
|
+
review.update!(
|
|
249
|
+
review_state: :approved,
|
|
250
|
+
reviewed_by: "admin@example.com"
|
|
700
251
|
)
|
|
252
|
+
|
|
253
|
+
review.comments.create!(author: "admin", body: "Looks good")
|
|
701
254
|
```
|
|
702
255
|
|
|
703
|
-
|
|
256
|
+
### Review States
|
|
704
257
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
```
|
|
258
|
+
| State | Meaning |
|
|
259
|
+
|-------|---------|
|
|
260
|
+
| `pending` | Not yet reviewed (default) |
|
|
261
|
+
| `approved` | Reviewed and accepted |
|
|
262
|
+
| `flagged` | Needs attention |
|
|
711
263
|
|
|
712
|
-
|
|
264
|
+
## Dashboard
|
|
713
265
|
|
|
714
|
-
|
|
266
|
+
The dashboard is available at `/tracebook/chats` (or wherever you mount the engine).
|
|
715
267
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
# Existing config...
|
|
268
|
+
### Chat List (`/tracebook/chats`)
|
|
269
|
+
- All chats with actor, model, message count, token usage, cost, review state
|
|
270
|
+
- KPIs: total chats, messages, cost
|
|
720
271
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
end
|
|
727
|
-
```
|
|
272
|
+
### Chat Detail (`/tracebook/chats/:id`)
|
|
273
|
+
- Full conversation thread (user and assistant messages)
|
|
274
|
+
- Per-message token counts and costs
|
|
275
|
+
- Review controls (approve/flag/reset)
|
|
276
|
+
- Comment thread
|
|
728
277
|
|
|
729
|
-
###
|
|
278
|
+
### Actor Display
|
|
730
279
|
|
|
731
|
-
|
|
280
|
+
By default, actors are shown as `Name` or `ClassName#id`. Customize with:
|
|
732
281
|
|
|
733
282
|
```ruby
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
add_index :tracebook_interactions, :tags, using: :gin, name: "idx_tracebook_tags"
|
|
283
|
+
config.actor_display = ->(actor) {
|
|
284
|
+
case actor
|
|
285
|
+
when User then actor.email
|
|
286
|
+
else "#{actor.class}##{actor.id}"
|
|
739
287
|
end
|
|
740
|
-
|
|
288
|
+
}
|
|
741
289
|
```
|
|
742
290
|
|
|
743
|
-
|
|
291
|
+
## Securing the Dashboard
|
|
744
292
|
|
|
745
|
-
|
|
293
|
+
The engine inherits from `ActionController::Base`. Restrict access with route constraints:
|
|
746
294
|
|
|
747
295
|
```ruby
|
|
748
|
-
#
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
S3Archiver.archive(interactions)
|
|
759
|
-
interactions.delete_all
|
|
760
|
-
end
|
|
296
|
+
# HTTP Basic Auth
|
|
297
|
+
mount Tracebook::Engine => "/tracebook",
|
|
298
|
+
constraints: ->(req) {
|
|
299
|
+
Rack::Auth::Basic::Request.new(req.env).provided? &&
|
|
300
|
+
Rack::Auth::Basic::Request.new(req.env).credentials == ["admin", ENV["TRACEBOOK_PASSWORD"]]
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# Devise
|
|
304
|
+
authenticate :user, ->(u) { u.admin? } do
|
|
305
|
+
mount Tracebook::Engine => "/tracebook"
|
|
761
306
|
end
|
|
762
307
|
```
|
|
763
308
|
|
|
764
309
|
## Development & Testing
|
|
765
310
|
|
|
766
|
-
### Inside the engine repository
|
|
767
|
-
|
|
768
311
|
```bash
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
bundle exec rails db:migrate # Run dummy app migrations
|
|
772
|
-
bundle exec rake test # Run full test suite
|
|
773
|
-
bundle exec rubocop --fix-unsafe # Fix style issues
|
|
774
|
-
```
|
|
312
|
+
# Run tests
|
|
313
|
+
bin/rails test
|
|
775
314
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
After updating the gem, install any new migrations:
|
|
779
|
-
|
|
780
|
-
```bash
|
|
781
|
-
bin/rails tracebook:install:migrations
|
|
782
|
-
bin/rails db:migrate
|
|
315
|
+
# Seed pricing in development
|
|
316
|
+
bin/rails tracebook:seed_pricing
|
|
783
317
|
```
|
|
784
318
|
|
|
785
|
-
###
|
|
319
|
+
### Reset configuration in tests
|
|
786
320
|
|
|
787
321
|
```ruby
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
setup do
|
|
791
|
-
TraceBook::Adapters::RubyLLM.disable!
|
|
792
|
-
TraceBook.reset_configuration!
|
|
793
|
-
|
|
794
|
-
TraceBook.configure do |config|
|
|
795
|
-
config.authorize = ->(*) { true }
|
|
796
|
-
config.persist_async = false # Inline for tests
|
|
797
|
-
end
|
|
798
|
-
end
|
|
799
|
-
end
|
|
322
|
+
setup { Tracebook.reset_configuration! }
|
|
323
|
+
teardown { Tracebook.reset_configuration! }
|
|
800
324
|
```
|
|
801
325
|
|
|
802
|
-
## API Documentation
|
|
803
|
-
|
|
804
|
-
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).
|
|
805
|
-
|
|
806
|
-
### Generating Documentation Locally
|
|
807
|
-
|
|
808
|
-
```bash
|
|
809
|
-
# Install YARD
|
|
810
|
-
bundle install
|
|
811
|
-
|
|
812
|
-
# Generate documentation
|
|
813
|
-
bundle exec rake yard
|
|
814
|
-
|
|
815
|
-
# Generate and open in browser
|
|
816
|
-
bundle exec rake yard:open
|
|
817
|
-
|
|
818
|
-
# View documentation coverage stats
|
|
819
|
-
bundle exec rake yard:stats
|
|
820
|
-
```
|
|
821
|
-
|
|
822
|
-
Documentation is generated in the `doc/` directory. Open `doc/index.html` in your browser to view.
|
|
823
|
-
|
|
824
|
-
### Key Documentation Areas
|
|
825
|
-
|
|
826
|
-
- **{Tracebook}** - Main module and `record!` method
|
|
827
|
-
- **{Tracebook::Mappers}** - Provider normalization
|
|
828
|
-
- **{Tracebook::Adapters::RubyLLM}** - ActiveSupport::Notifications adapter
|
|
829
|
-
- **{Tracebook::Interaction}** - ActiveRecord model
|
|
830
|
-
- **{Tracebook::NormalizedInteraction}** - Standard data structure
|
|
831
|
-
- **{Tracebook::Result}** - Return value from `record!`
|
|
832
|
-
|
|
833
|
-
## Contributing
|
|
834
|
-
|
|
835
|
-
1. Fork the repo and create a topic branch
|
|
836
|
-
2. Ensure `bundle exec rake test` passes
|
|
837
|
-
3. Update documentation and add regression tests for new behavior
|
|
838
|
-
4. Run `bundle exec rubocop -A` to fix style issues
|
|
839
|
-
5. Add YARD documentation for new public methods
|
|
840
|
-
6. Open a PR describing the motivation and changes
|
|
841
|
-
|
|
842
326
|
## License
|
|
843
327
|
|
|
844
|
-
|
|
328
|
+
MIT License. See [MIT-LICENSE](MIT-LICENSE).
|