Thank you for your interest in contributing to the Langfuse Ruby SDK! This document provides guidelines for development, testing, and submitting contributions.
- Development Setup
- Makefile Commands
- Running Tests
- Code Quality
- Submitting Issues
- Submitting Pull Requests
- Code Style Guidelines
- Ruby >= 3.2.0 (specified in
.ruby-version) - Bundler >= 2.0
-
Fork and clone the repository:
git clone https://github.com/YOUR-USERNAME/langfuse-rb.git cd langfuse-rb -
Install dependencies:
make setup # or: bundle install -
Verify the setup by running tests:
make test # or: bundle exec rspec
The project includes a Makefile with convenient commands for common development tasks:
make help- Show all available commandsmake setup- Install dependencies (bundle install)make test- Run RSpec test suite (bundle exec rspec)make lint- Run RuboCop linter (bundle exec rubocop)make fix- Auto-fix RuboCop violations (bundle exec rubocop -A)make check- Run tests + lint (CI check)make build- Build the gem (gem build langfuse.gemspec)make install- Build and install gem locallymake clean- Remove generated files (gem files, coverage, pkg/)make env- Copy.env.exampleto.env(if exists)
# Before starting work
make setup
# During development
make test # Run tests
make lint # Check code style
make fix # Auto-fix style issues
# Before committing
make check # Run both tests and lint (what CI runs)make test
# or: bundle exec rspecbundle exec rspec spec/langfuse_rb/client_spec.rbbundle exec rspec spec/langfuse_rb/client_spec.rb:42The project uses SimpleCov for test coverage reporting:
- Coverage reports are automatically generated when running tests
- View the report at
coverage/index.html - Target coverage: >95%
- Current coverage: 99.7%
Run RuboCop with auto-fix:
make fix
# or: bundle exec rubocop -ARun RuboCop without auto-fix (check only):
make lint
# or: bundle exec rubocopCheck specific file:
bundle exec rubocop lib/langfuse_rb/client.rbRun both tests and linting (what CI runs):
make checkThe project follows Ruby community conventions:
- Ruby Version: Target Ruby 3.2+
- Line Length: Max 120 characters
- String Literals: Double quotes enforced
- Frozen String Literals: All files must include
# frozen_string_literal: trueat the top - Naming:
- Classes:
PascalCase - Methods:
snake_case - Constants:
SCREAMING_SNAKE_CASE
- Classes:
- Method Length: Max 22 lines (excluding specs)
When submitting a bug report, please include:
- Clear Title: Brief description of the issue
- Ruby Version: Output of
ruby --version - Gem Version: Version of langfuse gem you're using
- Steps to Reproduce: Minimal code example that reproduces the issue
- Expected Behavior: What you expected to happen
- Actual Behavior: What actually happened
- Error Messages: Full error messages and stack traces
### Bug: get_prompt fails with 401 error despite valid credentials
**Ruby Version:** ruby 3.2.2 (2023-03-30 revision e51014f9c0)
**Gem Version:** langfuse-rb 1.0.0
**Steps to Reproduce:**
\`\`\`ruby
Langfuse.configure do |config|
config.public_key = ENV['LANGFUSE_PUBLIC_KEY']
config.secret_key = ENV['LANGFUSE_SECRET_KEY']
end
prompt = Langfuse.client.get_prompt("greeting")
\`\`\`
**Expected:** Fetch prompt successfully
**Actual:** Raises `Langfuse::UnauthorizedError: 401 Unauthorized`
**Error Message:**
\`\`\`
Langfuse::UnauthorizedError: Invalid credentials
from lib/langfuse/api_client.rb:45:in `get_prompt'
\`\`\`When requesting a feature:
- Use Case: Describe your use case and why this feature is needed
- Proposed Solution: If applicable, describe how you envision the feature working
- Alternatives: Any alternative solutions you've considered
- Additional Context: Any other relevant information
If possible, include a failing test that demonstrates the issue:
# frozen_string_literal: true
require "spec_helper"
RSpec.describe "Bug: prompt caching not working" do
let(:config) do
Langfuse::Config.new do |c|
c.public_key = "pk_test_123"
c.secret_key = "sk_test_456"
c.base_url = "https://cloud.langfuse.com"
c.cache_ttl = 60
end
end
it "caches prompts between requests" do
client = Langfuse::Client.new(config)
# First request
prompt1 = client.get_prompt("greeting")
# Second request (should use cache)
prompt2 = client.get_prompt("greeting")
# This fails - both requests hit the API
expect(api_calls_count).to eq(1) # Currently fails with 2
end
endThis makes it much easier for maintainers to understand and fix the issue!
- Run all tests: Ensure
make testpasses - Run linter: Ensure
make fixhas no remaining offenses - Run CI check: Ensure
make checkpasses (runs both tests and lint) - Add tests: Include tests for any new functionality or bug fixes
- Update documentation: Update README.md and relevant docs if adding user-facing features
- Check coverage: Maintain or improve test coverage (>95%)
- Fork the Repository: Create your own fork
- Create a Feature Branch:
git checkout -b feature/my-new-feature
- Make Your Changes: Write code, tests, and documentation
- Commit Your Changes:
git commit -m "Add feature: description of feature" - Push to Your Fork:
git push origin feature/my-new-feature
- Open a Pull Request: Submit PR against the
mainbranch
- Title: Clear, concise description of the change
- Description:
- What changed and why
- Link to any related issues
- Screenshots/examples if applicable
- Tests: All tests must pass
- Coverage: Maintain >95% coverage
- Documentation: Update docs if needed
- Commits: Keep commits focused and atomic
#### `TL;DR`
Add per-request timeout configuration to API calls.
#### `Why`
Users need fine-grained control over request timeouts for different operations.
#### `Checklist`
- [x] Has label
- [x] Has linked issue
- [x] Tests added for new behavior
- [x] Docs updated (if user-facing)
Closes #123All test files must start with # frozen_string_literal: true:
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Langfuse::Client do
# ... tests ...
endFollow the Arrange-Act-Assert pattern and use described_class:
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Langfuse::Client do
let(:config) do
Langfuse::Config.new do |c|
c.public_key = "pk_test_123"
c.secret_key = "sk_test_456"
c.base_url = "https://cloud.langfuse.com"
end
end
describe "#get_prompt" do
context "when prompt exists" do
it "returns the prompt" do
# Arrange
stub_request(:get, "https://cloud.langfuse.com/api/public/v2/prompts/greeting")
.to_return(status: 200, body: { name: "greeting" }.to_json)
client = described_class.new(config)
# Act
prompt = client.get_prompt("greeting")
# Assert
expect(prompt.name).to eq("greeting")
end
end
end
endUse instance_double for mocking dependencies:
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Langfuse::SomeClass do
let(:api_client) { instance_double(Langfuse::ApiClient) }
let(:config) { instance_double(Langfuse::Config) }
subject(:client) { described_class.new(api_client: api_client, config: config) }
it "uses the mocked dependency" do
allow(api_client).to receive(:some_method).and_return("result")
expect(client.do_something).to eq("result")
end
endProvide clear, actionable error messages:
# ✅ Good
raise ArgumentError, "fallback must be a String for text prompts, got #{fallback.class}. " \
"For chat prompts, use an Array of message hashes."
# ❌ Bad
raise ArgumentError, "Invalid fallback type"Use YARD format for public APIs:
# Get a prompt from Langfuse with caching support
#
# @param name [String] The prompt name
# @param options [Hash] Options hash
# @option options [Integer] :version Specific version number
# @option options [String] :label Label filter (e.g., "production")
# @return [TextPromptClient, ChatPromptClient] The prompt client
# @raise [NotFoundError] If prompt doesn't exist
# @raise [ApiError] If API request fails
#
# @example Fetch latest production prompt
# prompt = client.get_prompt("greeting", label: "production")
def get_prompt(name, **options)
# ...
endTests use WebMock to stub HTTP requests. WebMock is configured in spec_helper.rb to disable external HTTP requests by default:
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Langfuse::Client do
before do
stub_request(:get, "https://cloud.langfuse.com/api/public/v2/prompts/greeting")
.to_return(
status: 200,
body: { name: "greeting", prompt: "Hello!" }.to_json,
headers: { "Content-Type" => "application/json" }
)
end
it "fetches a prompt" do
# ... test code ...
end
endNote: External HTTP requests are disabled by default in tests. Always stub API requests using WebMock.
Use binding.pry (requires pry gem) or debugger:
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Langfuse::Client do
it "debugs the test" do
require "pry"
binding.pry # Debugger will stop here
expect(prompt).to eq("Hello")
end
endNote that Langfuse.reset! is called before each test (configured in spec_helper.rb), so tests start with a clean state.
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Langfuse::Client do
let(:config) do
Langfuse::Config.new do |c|
c.public_key = "pk_test_123"
c.secret_key = "sk_test_456"
c.base_url = "https://cloud.langfuse.com"
c.cache_ttl = 60
end
end
it "caches prompts" do
client = described_class.new(config)
# First call hits API
stub_request(:get, "https://cloud.langfuse.com/api/public/v2/prompts/greeting")
.to_return(status: 200, body: { name: "greeting" }.to_json)
prompt1 = client.get_prompt("greeting")
# Second call uses cache (no API call)
prompt2 = client.get_prompt("greeting")
expect(prompt1.name).to eq(prompt2.name)
# Verify only one API call was made
expect(WebMock).to have_requested(:get, %r{/prompts/greeting}).once
end
endTo test against a real Langfuse instance:
- Create a free account at cloud.langfuse.com using your GitHub account
- Navigate to Settings → API Keys → Create new API Keys
- Set values for all
LANGFUSE_*variables in.env:
LANGFUSE_HOST=https://cloud.langfuse.com
LANGFUSE_PUBLIC_KEY=pk-lf-xxxxxxxx
LANGFUSE_SECRET_KEY=sk-lf-xxxxxxxx- Bugs: Open an issue
- Security Issues: Email security@langfuse.com (do not open public issues)
By contributing to this project, you agree that your contributions will be licensed under the MIT License.
Thank you for contributing to the Langfuse Ruby SDK! 🎉