boxcars 0.8.3 → 0.8.5

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.
@@ -20,16 +20,58 @@ module Boxcars
20
20
  #
21
21
  # @param event [String, Symbol] The name of the event to track.
22
22
  # @param properties [Hash] A hash of properties associated with the event.
23
- def track(event:, properties:)
23
+ # @param observation [Boxcars::Observation, nil] Optional observation object to extract user context from.
24
+ def track(event:, properties:, observation: nil)
24
25
  return unless backend
25
26
 
26
- backend.track(event:, properties:)
27
+ # Merge user context from observation if present
28
+ final_properties = properties.dup
29
+ final_properties = merge_user_context(final_properties, observation.user_context) if observation&.user_context?
30
+
31
+ backend.track(event:, properties: final_properties)
27
32
  rescue StandardError
28
33
  # Fail silently as requested.
29
34
  # Optionally, if Boxcars had a central logger:
30
35
  # Boxcars.logger.warn "Boxcars::Observability: Backend error during track: #{e.message} (#{e.class.name})"
31
36
  end
32
37
 
38
+ # Tracks an observation event, automatically extracting user context if present
39
+ # @param observation [Boxcars::Observation] The observation to track
40
+ # @param event [String, Symbol] The event name (defaults to 'boxcar_observation')
41
+ # @param additional_properties [Hash] Additional properties to include
42
+ def track_observation(observation, event: 'boxcar_observation', **additional_properties)
43
+ properties = {
44
+ observation_note: observation.note,
45
+ observation_status: observation.status,
46
+ timestamp: Time.now.iso8601
47
+ }.merge(additional_properties)
48
+
49
+ # Add all observation context (including user_context) to properties
50
+ properties.merge!(observation.added_context) if observation.added_context
51
+
52
+ track(event:, properties:, observation:)
53
+ end
54
+
55
+ private
56
+
57
+ # Merge user context into properties with proper namespacing
58
+ # @param properties [Hash] The existing properties
59
+ # @param user_context [Hash] The user context to merge
60
+ # @return [Hash] The merged properties
61
+ def merge_user_context(properties, user_context)
62
+ return properties unless user_context.is_a?(Hash)
63
+
64
+ # Add user context with proper prefixing for analytics systems
65
+ user_properties = {}
66
+ user_context.each do |key, value|
67
+ # Use $user_ prefix for PostHog compatibility
68
+ user_key = key.to_s.start_with?('$user_') ? key : "$user_#{key}"
69
+ user_properties[user_key] = value
70
+ end
71
+
72
+ properties.merge(user_properties)
73
+ end
74
+
33
75
  # Flushes any pending events if the backend supports it.
34
76
  # This is useful for testing or when you need to ensure events are sent before the process exits.
35
77
  def flush
@@ -52,21 +52,9 @@ module Boxcars
52
52
  # @param properties [Hash] A hash of properties for the event.
53
53
  # It's recommended to include a `:user_id` for user-specific tracking.
54
54
  def track(event:, properties:)
55
- # Ensure properties is a hash, duplicate to avoid mutation by PostHog or other backends
56
- tracking_properties = properties.is_a?(Hash) ? properties.dup : {}
57
-
58
- distinct_id = tracking_properties.delete(:user_id) || tracking_properties.delete('user_id') || "anonymous_user"
59
-
60
- # The PostHog gem's capture method handles distinct_id and properties.
61
- # It's important that distinct_id is a string.
62
- @posthog_client.capture(
63
- distinct_id: distinct_id.to_s, # Ensure distinct_id is a string
64
- event: event.to_s, # Ensure event name is a string
65
- properties: tracking_properties
66
- )
67
- # The posthog-ruby client handles flushing events asynchronously.
68
- # If immediate flushing is needed for testing or specific scenarios:
69
- # @posthog_client.flush
55
+ properties = {} unless properties.is_a?(Hash)
56
+ distinct_id = properties.delete(:user_id) || current_user_id || "anonymous_user"
57
+ @posthog_client.capture(distinct_id:, event:, properties:)
70
58
  end
71
59
 
72
60
  # Flushes any pending events to PostHog immediately.
@@ -74,5 +62,12 @@ module Boxcars
74
62
  def flush
75
63
  @posthog_client.flush if @posthog_client.respond_to?(:flush)
76
64
  end
65
+
66
+ # in Rails, this is a way to find the current user id
67
+ def current_user_id
68
+ return unless defined?(::Current) && ::Current.respond_to?(:user)
69
+
70
+ ::Current.user&.id
71
+ end
77
72
  end
78
73
  end
@@ -52,5 +52,45 @@ module Boxcars
52
52
  def self.err(note, **)
53
53
  new(note:, status: :error, **)
54
54
  end
55
+
56
+ # create a new Observation with user context
57
+ # @param note [String] The text to use for the observation
58
+ # @param user_context [Hash] User information (e.g., { id: 123, email: "user@example.com", role: "admin" })
59
+ # @param status [Symbol] :ok or :error
60
+ # @param added_context [Hash] Any additional context to add to the result
61
+ # @return [Boxcars::Observation] The observation
62
+ def self.with_user(note, user_context:, status: :ok, **)
63
+ new(note:, status:, user_context:, **)
64
+ end
65
+
66
+ # create a new Observation with user context and status :ok
67
+ # @param note [String] The text to use for the observation
68
+ # @param user_context [Hash] User information (e.g., { id: 123, email: "user@example.com", role: "admin" })
69
+ # @param added_context [Hash] Any additional context to add to the result
70
+ # @return [Boxcars::Observation] The observation
71
+ def self.ok_with_user(note, user_context:, **)
72
+ with_user(note, user_context:, status: :ok, **)
73
+ end
74
+
75
+ # create a new Observation with user context and status :error
76
+ # @param note [String] The text to use for the observation
77
+ # @param user_context [Hash] User information (e.g., { id: 123, email: "user@example.com", role: "admin" })
78
+ # @param added_context [Hash] Any additional context to add to the result
79
+ # @return [Boxcars::Observation] The observation
80
+ def self.err_with_user(note, user_context:, **)
81
+ with_user(note, user_context:, status: :error, **)
82
+ end
83
+
84
+ # Extract user context from the observation
85
+ # @return [Hash, nil] The user context if present
86
+ def user_context
87
+ added_context[:user_context]
88
+ end
89
+
90
+ # Check if this observation has user context
91
+ # @return [Boolean] true if user context is present
92
+ def user_context?
93
+ !user_context.nil?
94
+ end
55
95
  end
56
96
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Boxcars
4
4
  # The current version of the gem.
5
- VERSION = "0.8.3"
5
+ VERSION = "0.8.5"
6
6
  end
data/lib/boxcars.rb CHANGED
@@ -218,6 +218,7 @@ module Boxcars
218
218
  end
219
219
 
220
220
  require_relative "boxcars/version"
221
+ require_relative "boxcars/observation"
221
222
  require_relative "boxcars/observability_backend"
222
223
  require_relative "boxcars/observability"
223
224
  # If users want it, they can require 'boxcars/observability_backends/multi_backend'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: boxcars
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.3
4
+ version: 0.8.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Francis Sullivan
@@ -139,9 +139,9 @@ files:
139
139
  - Gemfile
140
140
  - Gemfile.lock
141
141
  - LICENSE.txt
142
- - POSTHOG_TEST_README.md
143
142
  - README.md
144
143
  - Rakefile
144
+ - USER_CONTEXT_GUIDE.md
145
145
  - bin/console
146
146
  - bin/setup
147
147
  - boxcars.gemspec
@@ -1,118 +0,0 @@
1
- # Boxcars Engines PostHog Observability Test
2
-
3
- This test program demonstrates how to use the Boxcars library with PostHog observability backend to track AI engine usage.
4
-
5
- ## Prerequisites
6
-
7
- 1. **PostHog Ruby Gem**: Install the required gem
8
- ```bash
9
- gem install posthog-ruby
10
- ```
11
-
12
- 2. **Environment Variables**: Ensure your `.env` file contains:
13
- ```
14
- POSTHOG_API_KEY=your_posthog_project_api_key
15
- POSTHOG_HOST=https://app.posthog.com # or your self-hosted instance
16
- ```
17
-
18
- 3. **AI Provider API Keys**: For testing different engines, you'll need:
19
- ```
20
- openai_access_token=your_openai_token
21
- GOOGLE_API_KEY=your_google_api_key
22
- ANTHROPIC_API_KEY=your_anthropic_key
23
- GROQ_API_KEY=your_groq_key
24
- ```
25
-
26
- ## Usage
27
-
28
- ### Method 1: IRB Interactive Session (Recommended)
29
-
30
- Start IRB with the required dependencies:
31
-
32
- ```bash
33
- irb -r dotenv/load -r boxcars -r debug -r boxcars/observability_backends/posthog_backend
34
- ```
35
-
36
- Then in the IRB session:
37
-
38
- ```ruby
39
- # Load and run the test
40
- load 'test_engines_with_posthog.rb'
41
-
42
- # Or set up PostHog backend manually:
43
- Boxcars::Observability.backend = Boxcars::PosthogBackend.new(
44
- api_key: ENV['POSTHOG_API_KEY'],
45
- host: ENV['POSTHOG_HOST']
46
- )
47
-
48
- # Run manual tests
49
- manual_test(model: 'gpt-4o', prompt: 'What is machine learning?')
50
- manual_test(model: 'flash', prompt: 'Explain Ruby in one sentence')
51
- manual_test(model: 'sonnet', prompt: 'Write a short poem about coding')
52
- ```
53
-
54
- ### Method 2: Direct Ruby Execution
55
-
56
- ```bash
57
- ruby test_engines_with_posthog.rb
58
- ```
59
-
60
- ## What the Test Does
61
-
62
- 1. **Initializes PostHog Backend**: Sets up the PostHog observability backend with your API credentials
63
- 2. **Tests Multiple Engines**: Runs tests against various AI engines:
64
- - Gemini Flash (Default)
65
- - GPT-4o (OpenAI)
66
- - Claude Sonnet (Anthropic)
67
- - Groq Llama
68
- 3. **Tracks Observability Events**: Each API call generates PostHog events with AI-specific properties
69
- 4. **Provides Manual Testing**: Includes a `manual_test` function for interactive testing
70
-
71
- ## PostHog Events
72
-
73
- The test will generate events in PostHog with properties like:
74
-
75
- - `$ai_model`: The AI model used (e.g., "gpt-4o", "gemini-2.5-flash")
76
- - `$ai_provider`: The provider (e.g., "openai", "google", "anthropic")
77
- - `$ai_input_tokens`: Number of input tokens
78
- - `$ai_output_tokens`: Number of output tokens
79
- - `$ai_latency`: Response time in seconds
80
- - `$ai_http_status`: HTTP status code
81
- - `$ai_trace_id`: Unique trace identifier
82
- - `$ai_is_error`: Boolean indicating if there was an error
83
-
84
- ## Viewing Results
85
-
86
- After running the test:
87
-
88
- 1. Go to your PostHog dashboard
89
- 2. Navigate to Events or Live Events
90
- 3. Look for events with AI-related properties
91
- 4. You can create insights and dashboards to analyze AI usage patterns
92
-
93
- ## Troubleshooting
94
-
95
- - **Missing PostHog gem**: Install with `gem install posthog-ruby`
96
- - **Missing API keys**: Check your `.env` file has the required keys
97
- - **Engine errors**: Some engines may fail if you don't have valid API keys for those providers
98
- - **No events in PostHog**: Check your PostHog API key and host configuration
99
-
100
- ## Available Models
101
-
102
- You can test with these model aliases:
103
-
104
- - `flash` - Gemini 2.5 Flash
105
- - `gpt-4o` - OpenAI GPT-4o
106
- - `sonnet` - Claude Sonnet
107
- - `groq` - Groq Llama
108
- - `online` - Perplexity Sonar
109
- - And many more (see `lib/boxcars/engines.rb`)
110
-
111
- ## Example Manual Tests
112
-
113
- ```ruby
114
- # Test different models
115
- manual_test(model: 'flash', prompt: 'Explain quantum computing')
116
- manual_test(model: 'gpt-4o', prompt: 'Write a Python function to sort a list')
117
- manual_test(model: 'sonnet', prompt: 'What are the benefits of functional programming?')
118
- manual_test(model: 'groq', prompt: 'Describe the difference between AI and ML')