langfuse-ruby 0.1.6 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e15fd7f7f4e2e8b1a8018f27de35f42a3124973c8a455b50781a0d08924aa3d
4
- data.tar.gz: 91253f7addf89cef33fcf047376f8667fae8e2e9d75f096b4096d4c876d06b20
3
+ metadata.gz: 7b2c65ac52311951b2658204cbf3f76afff090465ef9e500718f59f194dbcea9
4
+ data.tar.gz: db397fdd6ca24db06b67c92eed232663d7845504d9dc0e7759098ebdbe88e620
5
5
  SHA512:
6
- metadata.gz: 62d8773683844ecc44a1d258765f0522f77e83b02cbb7b7441d892e3e6a9252348f8ccfd331e185186b46aa9fb30203146e7beffd29a1330b5ab14ff75862a47
7
- data.tar.gz: 23e33a0b7d7b8b25c07e9663cf320771a6db8160fb18302179acc8aeaaf99c1740add31c84448106df46efe22e89b2c36ce7bae6b66e600e525226854a2003d7
6
+ metadata.gz: a1ba2f150b0a6d16ccc99d26144455d6fcb66d95a7e9d644bb50d12de91c95fa7751b6b3145f507a74da1b367eacaf3d43a91073a249815695f7260fc2c66d56
7
+ data.tar.gz: 6d27a0bd423f14637597acfc243eaf4c74e3e533eb87ceab62e46fffc8249bd3043866ec6d5a0450b8eb7bc82b29c4e553fdfe3ae9c4c3d468d5fa241b0a3c68
@@ -7,15 +7,30 @@ on:
7
7
  branches: [ master ]
8
8
 
9
9
  jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v6
15
+
16
+ - name: Set up Ruby
17
+ uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: '4.0'
20
+ bundler-cache: true
21
+
22
+ - name: Run RuboCop
23
+ run: bundle exec rubocop
24
+
10
25
  test:
11
26
  runs-on: ubuntu-latest
12
27
 
13
28
  strategy:
14
29
  matrix:
15
- ruby-version: ['3.1', '3.2', '3.3', '3.4']
30
+ ruby-version: ['3.1', '3.2', '3.3', '3.4', '4.0']
16
31
 
17
32
  steps:
18
- - uses: actions/checkout@v4
33
+ - uses: actions/checkout@v6
19
34
 
20
35
  - name: Set up Ruby ${{ matrix.ruby-version }}
21
36
  uses: ruby/setup-ruby@v1
@@ -29,25 +44,21 @@ jobs:
29
44
  - name: Run offline tests
30
45
  run: bundle exec ruby scripts/test_offline.rb
31
46
 
32
- - name: Run RuboCop
33
- run: bundle exec rubocop
34
- continue-on-error: true
35
-
36
47
  build:
37
48
  runs-on: ubuntu-latest
38
- needs: test
49
+ needs: [lint, test]
39
50
 
40
51
  steps:
41
- - uses: actions/checkout@v4
52
+ - uses: actions/checkout@v6
42
53
 
43
54
  - name: Set up Ruby
44
55
  uses: ruby/setup-ruby@v1
45
56
  with:
46
- ruby-version: '3.4'
57
+ ruby-version: '4.0'
47
58
  bundler-cache: true
48
59
 
49
60
  - name: Build gem
50
61
  run: gem build langfuse-ruby.gemspec
51
62
 
52
63
  - name: Verify gem can be installed
53
- run: gem install langfuse-ruby-*.gem
64
+ run: gem install langfuse-ruby-*.gem
@@ -9,14 +9,16 @@ jobs:
9
9
  release:
10
10
  runs-on: ubuntu-latest
11
11
  if: startsWith(github.ref, 'refs/tags/v')
12
+ permissions:
13
+ contents: write
12
14
 
13
15
  steps:
14
- - uses: actions/checkout@v4
16
+ - uses: actions/checkout@v6
15
17
 
16
18
  - name: Set up Ruby
17
19
  uses: ruby/setup-ruby@v1
18
20
  with:
19
- ruby-version: '3.4'
21
+ ruby-version: '4.0'
20
22
  bundler-cache: true
21
23
 
22
24
  - name: Run tests
data/.mise.toml ADDED
@@ -0,0 +1,8 @@
1
+ # mise project toolchain config
2
+ # https://mise.jdx.dev/configuration.html
3
+ #
4
+ # This project defaults to the latest stable Ruby release.
5
+ # Run `mise install` after cloning to install it, then `bundle install`.
6
+
7
+ [tools]
8
+ ruby = "latest"
data/.rubocop.yml CHANGED
@@ -47,7 +47,7 @@ Metrics/CyclomaticComplexity:
47
47
  Max: 20
48
48
 
49
49
  Metrics/PerceivedComplexity:
50
- Max: 20
50
+ Max: 25
51
51
 
52
52
  Metrics/ParameterLists:
53
53
  Enabled: false
@@ -63,4 +63,4 @@ Lint/UnusedMethodArgument:
63
63
  - 'lib/langfuse/evaluation.rb'
64
64
 
65
65
  Metrics/ClassLength:
66
- Max: 600
66
+ Max: 800
data/CHANGELOG.md CHANGED
@@ -7,17 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-07-18
11
+
10
12
  ### Added
11
- - **Simplified API**: New class-level convenience methods for easier usage
12
- - `Langfuse.trace(name, **options, &block)` - Block-based tracing with automatic flush
13
- - `Langfuse.get_prompt(name, variables:, retries:)` - Get and compile prompts with retry support
14
- - `Langfuse.client` - Thread-safe singleton client access
15
- - `Langfuse.flush` / `Langfuse.shutdown` / `Langfuse.reset!` - Client lifecycle management
16
- - **Graceful Degradation**: Null object pattern for fault tolerance
17
- - `NullTrace`, `NullGeneration`, `NullSpan`, `NullEvent` - No-op objects when Langfuse is unavailable
18
- - **Retry Support**: `get_prompt` now supports configurable retries with exponential backoff (default: 2 retries)
19
- - New example file `examples/simplified_usage.rb` demonstrating the simplified API
20
- - Comprehensive test coverage for convenience methods (28 new tests)
13
+ - **Environment support**: `environment` config / `LANGFUSE_TRACING_ENVIRONMENT` env var, injected into trace, observation and score bodies
14
+ - **Sampling**: `sample_rate` config / `LANGFUSE_SAMPLE_RATE` env var, deterministic trace-based sampling (all events of a trace share the same decision)
15
+ - **Masking**: `mask` callable applied to `input`/`output`/`metadata` before sending, for PII redaction
16
+ - **flush_at threshold**: flush as soon as the queue reaches `flush_at` events (default 15, env `LANGFUSE_FLUSH_AT`), via a condition-variable wake-up on the flush thread
17
+ - **Batch chunking**: ingestion batches are split to respect the 3.5 MB API limit; oversized single events are dropped with a warning
18
+ - **207 partial-success handling**: per-event errors from the ingestion API are logged via the structured logger
19
+ - **Score full fields**: `session_id`, `dataset_run_id`, `metadata`, `config_id`, `queue_id`, `id`, `environment`, and string values for CATEGORICAL/CORRECTION scores; `create_score` alias
20
+ - **Generation usage_details / cost_details**: new v4 usage model (arbitrary keys such as cache tokens) alongside legacy `usage`
21
+ - **Generation prompt linking**: `prompt:` accepts a `Langfuse::Prompt` or `{ name:, version: }` hash, emitted as `promptName`/`promptVersion`
22
+ - **Trace public field**: `public:` flag for shareable traces
23
+ - **LANGFUSE_BASE_URL** env var alias (new SDK standard) alongside `LANGFUSE_HOST`
24
+ - **Structured Logger**: replaces `puts` with `Logger`; level controlled by `debug` / `LANGFUSE_DEBUG`
25
+ - **at_exit shutdown hook**: pending events are flushed on process exit (configurable via `shutdown_on_exit`)
26
+ - **W3C hex IDs in OTel mode**: native 32-char trace IDs and 16-char span IDs for OTel ingestion
27
+ - **OTel exporter attributes**: `langfuse.environment`, `langfuse.trace.public`, `langfuse.internal.as_root`, `langfuse.observation.usage_details`, `langfuse.observation.cost_details`, `langfuse.observation.prompt.name`, `langfuse.observation.prompt.version`
28
+ - **Simplified API**: Class-level convenience methods (`Langfuse.trace`, `get_prompt`, `client`, `flush`, `shutdown`, `reset!`) with graceful degradation via null objects
29
+ - **Retry Support**: `get_prompt` supports configurable retries with exponential backoff (default: 2 retries)
30
+ - **Ruby 4.0 support**: CI matrix covers Ruby 3.1–4.0; explicit `base64` / `tsort` dependencies for Ruby 4.0 gem packaging
31
+
32
+ ### Fixed
33
+ - **OTel mode scores lost**: scores were exported as OTLP spans with `langfuse.score.*` attributes, which the server does not map to Langfuse scores. Scores now always route through the ingestion API (`score-create` batch), with trace/observation IDs normalized to OTel hex IDs so they attach to the correct entities
34
+ - **OTel flush failure drops scores**: when OTEL export of non-score events fails, score events in the same batch are now re-queued together with OTEL events (they were previously drained by `flush` and permanently lost on `raise`)
35
+ - **OTel ID mismatch**: observation-level scores referenced full UUIDs while spans used truncated hex, breaking attachment. IDs are now normalized on both sides
36
+ - **Span/Generation score missing trace_id**: `Span#score` and `Generation#score` now pass `trace_id` so the server can attach observation-level scores correctly
37
+ - **Process-level singleton**: `Langfuse.client` was thread-local (`Thread.current`), creating one client + flush thread per thread under Puma/Sidekiq. Now a single process-wide client guarded by a `Mutex`
38
+ - **Idempotent shutdown**: `shutdown` can be called multiple times safely
39
+ - **Body serialization**: event bodies now only camelCase top-level keys; user data under `input`/`output`/`metadata`/`usageDetails`/`costDetails`/`modelParameters` is passed through verbatim so user-provided keys are not mangled
40
+
41
+ ### Changed
42
+ - `Langfuse.client` is now process-wide instead of thread-local. Use `Langfuse.new` for isolated clients in tests
43
+ - Default `flush_interval` behavior unchanged, but the flush thread now also wakes on the `flush_at` threshold
44
+ - `Configuration` gains `environment`, `sample_rate`, `mask`, `flush_at`, `logger`, `shutdown_on_exit` attributes
21
45
 
22
46
  ## [0.1.5] - 2025-12-26
23
47
 
data/CLAUDE.md CHANGED
@@ -8,8 +8,14 @@ This is the official Ruby SDK for [Langfuse](https://langfuse.com) - an open-sou
8
8
 
9
9
  ## Common Commands
10
10
 
11
+ Ruby version is managed with [mise](https://mise.jdx.dev) via `.mise.toml`
12
+ (defaults to the latest stable Ruby). Run `mise install` after cloning, then
13
+ `bundle install`. With mise's shell integration active, the commands below work
14
+ directly; in a non-mise shell prefix them with `mise exec --`.
15
+
11
16
  ```bash
12
17
  # Install dependencies
18
+ mise install # install the pinned Ruby version
13
19
  bundle install
14
20
 
15
21
  # Run all RSpec tests
@@ -39,12 +45,13 @@ bundle exec rake release_gem
39
45
  ### Core Classes
40
46
 
41
47
  - **`Langfuse`** ([lib/langfuse.rb](lib/langfuse.rb)) - Module with class-level convenience methods (`trace`, `get_prompt`, `client`, `flush`, `shutdown`, `reset!`)
42
- - **`Langfuse::Client`** ([lib/langfuse/client.rb](lib/langfuse/client.rb)) - Main entry point. Handles API authentication, HTTP connections (via Faraday), event queuing, and background flush thread for auto-batching events.
48
+ - **`Langfuse::Client`** ([lib/langfuse/client.rb](lib/langfuse/client.rb)) - Main entry point. Handles API authentication, HTTP connections (via Faraday), event queuing, sampling/masking, and background flush thread for auto-batching events.
43
49
  - **`Langfuse::Trace`** ([lib/langfuse/trace.rb](lib/langfuse/trace.rb)) - Top-level container for a request/session.
44
50
  - **`Langfuse::Span`** ([lib/langfuse/span.rb](lib/langfuse/span.rb)) - Timed operation with enhanced type support.
45
- - **`Langfuse::Generation`** ([lib/langfuse/generation.rb](lib/langfuse/generation.rb)) - LLM call tracking.
51
+ - **`Langfuse::Generation`** ([lib/langfuse/generation.rb](lib/langfuse/generation.rb)) - LLM call tracking (supports `usage_details`/`cost_details` and prompt linking).
46
52
  - **`Langfuse::Event`** ([lib/langfuse/event.rb](lib/langfuse/event.rb)) - Point-in-time events.
47
53
  - **`Langfuse::Prompt`** ([lib/langfuse/prompt.rb](lib/langfuse/prompt.rb)) - Prompt templates with caching.
54
+ - **`Langfuse::OtelExporter`** ([lib/langfuse/otel_exporter.rb](lib/langfuse/otel_exporter.rb)) - Maps Langfuse events to OTLP/HTTP JSON when `ingestion_mode: :otel`.
48
55
  - **`Langfuse::NullTrace/NullGeneration/NullSpan`** ([lib/langfuse/null_objects.rb](lib/langfuse/null_objects.rb)) - Null objects for graceful degradation.
49
56
 
50
57
  ### Simplified API (Recommended)
@@ -63,10 +70,14 @@ Langfuse.get_prompt("my-prompt", variables: { name: "Alice" }, retries: 3)
63
70
 
64
71
  ### Event Flow
65
72
 
66
- 1. Observations (traces, spans, generations, events) are created via Client methods
67
- 2. Events are queued in `@event_queue` (thread-safe `Concurrent::Array`)
68
- 3. Background flush thread sends batched events to `/api/public/ingestion` endpoint
69
- 4. Manual flush available via `client.flush`; graceful shutdown via `client.shutdown`
73
+ 1. Observations (traces, spans, generations, events, scores) are created via Client methods
74
+ 2. Bodies are prepared via `Utils.prepare_event_body` (top-level camelCase; user data under `input`/`output`/`metadata`/etc. is left verbatim), then environment injection, masking, and sampling are applied
75
+ 3. Events are queued in `@event_queue` (thread-safe `Concurrent::Array`)
76
+ 4. Background flush thread wakes on `flush_interval` **or** when the queue reaches `flush_at`
77
+ 5. Flush path depends on `ingestion_mode`:
78
+ - `:legacy` → batched POST to `/api/public/ingestion` (chunked to 3.5 MB)
79
+ - `:otel` → non-score events via OTLP `/api/public/otel/v1/traces`; **scores always go through the ingestion API**, with IDs normalized to OTel hex so they attach correctly. On OTEL transport failure, both OTEL and score events from the batch are re-queued
80
+ 6. Manual flush via `client.flush`; idempotent shutdown via `client.shutdown` (plus optional `at_exit` hook)
70
81
 
71
82
  ### Observation Types
72
83
 
@@ -81,7 +92,7 @@ Enhanced types are implemented as spans with `as_type` metadata sent to the API.
81
92
  Client accepts config via:
82
93
  1. Constructor parameters
83
94
  2. `Langfuse.configure` block
84
- 3. Environment variables: `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_HOST`, `LANGFUSE_FLUSH_INTERVAL`, `LANGFUSE_AUTO_FLUSH`
95
+ 3. Environment variables: `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_HOST` / `LANGFUSE_BASE_URL`, `LANGFUSE_FLUSH_INTERVAL`, `LANGFUSE_FLUSH_AT`, `LANGFUSE_AUTO_FLUSH`, `LANGFUSE_TRACING_ENVIRONMENT`, `LANGFUSE_SAMPLE_RATE`, `LANGFUSE_DEBUG`, `LANGFUSE_INGESTION_MODE`
85
96
 
86
97
  ### Error Handling
87
98
 
@@ -95,6 +106,7 @@ Graceful degradation: When Langfuse is unavailable, `Langfuse.trace` yields a `N
95
106
  - Uses Faraday for HTTP with Basic Auth (public_key:secret_key)
96
107
  - Prompt names with special characters are auto-URL-encoded via `Utils.url_encode`
97
108
  - `trace-update` events merge into existing `trace-create` in queue (deduplication)
98
- - All keys are converted to camelCase before API submission via `Utils.deep_camelize_keys`
99
- - Thread-safe singleton client via `Thread.current[:langfuse_client]`
109
+ - Event bodies use `Utils.prepare_event_body` (top-level camelCase only; nested user data is not mangled)
110
+ - Process-wide singleton client via `Langfuse::CLIENT_MUTEX` (not thread-local); use `Langfuse.new` for isolated clients in tests
111
+ - In OTel mode, IDs are W3C hex (`generate_trace_id` / `generate_observation_id`); scores normalize refs via `OtelExporter.to_otel_trace_id` / `to_otel_span_id`
100
112
  - `get_prompt` supports configurable retries with exponential backoff
data/Gemfile CHANGED
@@ -7,4 +7,5 @@ gemspec
7
7
 
8
8
  gem 'rake', '~> 13.0'
9
9
  gem 'rspec', '~> 3.0'
10
+ gem 'simplecov', '~> 0.22', require: false
10
11
  gem 'yard', '~> 0.9'
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- langfuse-ruby (0.1.6)
4
+ langfuse-ruby (0.2.0)
5
+ base64 (>= 0.1.0)
5
6
  concurrent-ruby (~> 1.0)
6
7
  faraday (>= 1.8, < 3.0)
7
8
  faraday-multipart (~> 1.0)
@@ -21,11 +22,12 @@ GEM
21
22
  bigdecimal
22
23
  rexml
23
24
  diff-lcs (1.6.2)
25
+ docile (1.4.1)
24
26
  faraday (2.13.2)
25
27
  faraday-net_http (>= 2.0, < 3.5)
26
28
  json
27
29
  logger
28
- faraday-multipart (1.1.1)
30
+ faraday-multipart (1.2.0)
29
31
  multipart-post (~> 2.0)
30
32
  faraday-net_http (3.4.1)
31
33
  net-http (>= 0.5.0)
@@ -76,12 +78,18 @@ GEM
76
78
  parser (>= 3.3.7.2)
77
79
  prism (~> 1.4)
78
80
  ruby-progressbar (1.13.0)
81
+ simplecov (0.22.0)
82
+ docile (~> 1.1)
83
+ simplecov-html (~> 0.11)
84
+ simplecov_json_formatter (~> 0.1)
85
+ simplecov-html (0.13.2)
86
+ simplecov_json_formatter (0.1.4)
87
+ tsort (0.2.0)
79
88
  unicode-display_width (3.1.4)
80
89
  unicode-emoji (~> 4.0, >= 4.0.4)
81
- unicode-emoji (4.0.4)
90
+ unicode-emoji (4.2.0)
82
91
  uri (1.0.3)
83
- vcr (6.3.1)
84
- base64
92
+ vcr (6.4.0)
85
93
  webmock (3.25.1)
86
94
  addressable (>= 2.8.0)
87
95
  crack (>= 0.3.2)
@@ -98,6 +106,8 @@ DEPENDENCIES
98
106
  rake (~> 13.0)
99
107
  rspec (~> 3.0)
100
108
  rubocop (~> 1.0)
109
+ simplecov (~> 0.22)
110
+ tsort (>= 0.1.0)
101
111
  vcr (~> 6.0)
102
112
  webmock (~> 3.0)
103
113
  yard (~> 0.9)
data/Makefile CHANGED
@@ -1,4 +1,4 @@
1
- .PHONY: install test spec test-offline test-all lint lint-fix build release clean console help tag
1
+ .PHONY: install test spec test-offline test-all lint lint-fix build release clean console help tag coverage
2
2
 
3
3
  help: ## Show this help
4
4
  @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
@@ -17,6 +17,10 @@ test-all: ## Run all tests (spec + offline)
17
17
 
18
18
  test: spec ## Alias for spec
19
19
 
20
+ coverage: ## Run tests and open coverage report
21
+ bundle exec rake spec
22
+ open coverage/index.html
23
+
20
24
  lint: ## Run RuboCop linter
21
25
  bundle exec rubocop
22
26
 
@@ -26,7 +30,7 @@ lint-fix: ## Run RuboCop with auto-correct
26
30
  build: ## Build the gem
27
31
  bundle exec rake build
28
32
 
29
- release: tag build ## Release: tag + build + push to RubyGems
33
+ release: tag ## Release: tag + build + push to RubyGems
30
34
  bundle exec rake release_gem
31
35
 
32
36
  clean: ## Remove built gem files
@@ -39,7 +43,7 @@ console: ## Start an IRB console with the gem loaded
39
43
  tag: ## Create and push a version tag. Usage: make tag [VERSION=x.y.z]
40
44
  @git fetch --tags; \
41
45
  if [ -z "$(VERSION)" ]; then \
42
- LATEST=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
46
+ LATEST=$$(git tag -l 'v[0-9]*' --sort=-v:refname | head -n1); \
43
47
  if [ -z "$$LATEST" ]; then \
44
48
  NEW_TAG="v0.0.1"; \
45
49
  else \
@@ -51,7 +55,7 @@ tag: ## Create and push a version tag. Usage: make tag [VERSION=x.y.z]
51
55
  fi; \
52
56
  else \
53
57
  NEW_TAG="v$(VERSION)"; \
54
- LATEST=$$(git tag -l 'v*' --sort=-v:refname | head -n1); \
58
+ LATEST=$$(git tag -l 'v[0-9]*' --sort=-v:refname | head -n1); \
55
59
  if [ "$$LATEST" = "$$NEW_TAG" ]; then \
56
60
  echo "Tag $$NEW_TAG already exists on remote, deleting and re-pushing..."; \
57
61
  git tag -d "$$NEW_TAG" 2>/dev/null || true; \
data/README.md CHANGED
@@ -16,6 +16,10 @@ Ruby SDK for [Langfuse](https://langfuse.com) - the open-source LLM engineering
16
16
 
17
17
  ## Installation
18
18
 
19
+ This gem requires Ruby >= 3.1 and is tested against Ruby 3.1–4.0. For
20
+ development, Ruby version is managed with [mise](https://mise.jdx.dev) (defaults
21
+ to the latest stable Ruby via `.mise.toml`).
22
+
19
23
  Add this line to your application's Gemfile:
20
24
 
21
25
  ```ruby
@@ -34,6 +38,17 @@ Or install it yourself as:
34
38
  $ gem install langfuse-ruby
35
39
  ```
36
40
 
41
+ ### Development setup with mise
42
+
43
+ ```bash
44
+ # Install mise (if not already installed), then trust the project config and
45
+ # install the pinned Ruby version.
46
+ brew install mise # macOS; see mise docs for other OSes
47
+ mise install # installs Ruby from .mise.toml
48
+ bundle install # install gem dependencies
49
+ bundle exec rake spec # run the test suite
50
+ ```
51
+
37
52
  ## Quick Start
38
53
 
39
54
  ### 1. Initialize the Client
@@ -58,6 +73,52 @@ end
58
73
  client = Langfuse.new
59
74
  ```
60
75
 
76
+ ### OpenTelemetry (OTEL) Ingestion Mode
77
+
78
+ Langfuse v4 introduces a faster data model powered by OpenTelemetry. To use
79
+ it, enable the OTEL ingestion mode. This sends data via the OTLP/HTTP JSON
80
+ endpoint (`/api/public/otel/v1/traces`) with the `x-langfuse-ingestion-version: 4`
81
+ header for real-time ingestion and observation-level online evaluators.
82
+
83
+ ```ruby
84
+ # Via constructor
85
+ client = Langfuse.new(
86
+ public_key: "pk-lf-...",
87
+ secret_key: "sk-lf-...",
88
+ ingestion_mode: :otel
89
+ )
90
+
91
+ # Via global configuration
92
+ Langfuse.configure do |config|
93
+ config.public_key = "pk-lf-..."
94
+ config.secret_key = "sk-lf-..."
95
+ config.ingestion_mode = :otel
96
+ end
97
+
98
+ # Via environment variable
99
+ # LANGFUSE_INGESTION_MODE=otel
100
+ ```
101
+
102
+ All existing tracing APIs work unchanged. The SDK maps Langfuse events to
103
+ OpenTelemetry spans with the appropriate `langfuse.*` and `gen_ai.*` attributes.
104
+ No additional dependencies are required.
105
+
106
+ **Scores in OTel mode:** scores are not part of the OTLP trace mapping. The SDK
107
+ always sends them through the ingestion API (`/api/public/ingestion`) as
108
+ `score-create` events, and normalizes `trace_id` / `observation_id` to W3C hex
109
+ IDs so they attach to the correct OTel-ingested entities. If an OTEL export
110
+ fails mid-batch, both the OTEL events and any scores from that batch are
111
+ re-queued for retry.
112
+
113
+ ```ruby
114
+ # Scores work the same in both modes
115
+ client = Langfuse.new(ingestion_mode: :otel, ...)
116
+ trace = client.trace(name: "chat")
117
+ generation = trace.generation(name: "llm", model: "gpt-4o")
118
+ generation.score(name: "faithfulness", value: 0.9)
119
+ client.flush # traces/spans → OTLP; scores → ingestion API
120
+ ```
121
+
61
122
  ### 2. Basic Tracing
62
123
 
63
124
  ```ruby
@@ -146,16 +207,16 @@ end
146
207
  ### Other Class Methods
147
208
 
148
209
  ```ruby
149
- # Get the thread-safe singleton client
210
+ # Get the process-wide, thread-safe singleton client
150
211
  client = Langfuse.client
151
212
 
152
213
  # Manual flush (when not using block-based tracing)
153
214
  Langfuse.flush
154
215
 
155
- # Shutdown the client
216
+ # Shutdown the client (idempotent; also runs via at_exit when shutdown_on_exit is true)
156
217
  Langfuse.shutdown
157
218
 
158
- # Reset the singleton (useful for testing)
219
+ # Reset the singleton (useful for testing; prefer Langfuse.new for isolated clients)
159
220
  Langfuse.reset!
160
221
  ```
161
222
 
@@ -376,6 +437,97 @@ generation.score(
376
437
 
377
438
  ## Advanced Usage
378
439
 
440
+ ### Tracing Environment, Sampling and Masking
441
+
442
+ ```ruby
443
+ # Tag all events with a tracing environment (also via LANGFUSE_TRACING_ENVIRONMENT)
444
+ client = Langfuse.new(
445
+ public_key: "pk-lf-...",
446
+ secret_key: "sk-lf-...",
447
+ environment: "production"
448
+ )
449
+
450
+ # Sample a fraction of traces deterministically (also via LANGFUSE_SAMPLE_RATE)
451
+ # All events of a trace share the same keep/drop decision.
452
+ sampled_client = Langfuse.new(
453
+ public_key: "pk-lf-...",
454
+ secret_key: "sk-lf-...",
455
+ sample_rate: 0.1
456
+ )
457
+
458
+ # Mask sensitive fields before sending (applied to input/output/metadata)
459
+ masked_client = Langfuse.new(
460
+ public_key: "pk-lf-...",
461
+ secret_key: "sk-lf-...",
462
+ mask: ->(value) { value.to_s.gsub(/\b\d{16}\b, "***CARD***") }
463
+ )
464
+ ```
465
+
466
+ ### Batch flushing
467
+
468
+ Events are flushed in the background every `flush_interval` seconds, or as soon
469
+ as `flush_at` events are queued (default 15, env `LANGFUSE_FLUSH_AT`). Batches
470
+ are automatically split to respect the 3.5 MB ingestion API limit, and a
471
+ process-wide `at_exit` hook flushes pending events on shutdown.
472
+
473
+ ```ruby
474
+ client = Langfuse.new(
475
+ public_key: "pk-lf-...",
476
+ secret_key: "sk-lf-...",
477
+ flush_at: 50, # flush after 50 events
478
+ flush_interval: 10, # or every 10 seconds
479
+ shutdown_on_exit: true # flush on process exit (default)
480
+ )
481
+ ```
482
+
483
+ ### Scores with full fields
484
+
485
+ Scores can target a trace, an observation, a session, or a dataset run, and
486
+ carry metadata, a config reference, and an annotation queue link:
487
+
488
+ ```ruby
489
+ # Trace-level score
490
+ trace.score(name: "accuracy", value: 0.9, comment: "good")
491
+
492
+ # Observation-level score (trace_id is set automatically on Span/Generation)
493
+ generation.score(name: "faithfulness", value: 0.8, data_type: "NUMERIC")
494
+
495
+ # Session-level score
496
+ client.score(name: "csat", value: 5, session_id: "sess-1", data_type: "NUMERIC")
497
+
498
+ # Dataset-run score with metadata and config link
499
+ client.score(
500
+ name: "hallucination",
501
+ value: 0.2,
502
+ dataset_run_id: "run-1",
503
+ trace_id: "trace-1",
504
+ metadata: { evaluator: "llm-judge" },
505
+ config_id: "cfg-abc",
506
+ data_type: "NUMERIC"
507
+ )
508
+
509
+ # Categorical string value
510
+ client.score(name: "label", value: "good", trace_id: "t1", data_type: "CATEGORICAL")
511
+ ```
512
+
513
+ ### Generation usage details, cost details and prompt linking
514
+
515
+ ```ruby
516
+ # New v4 usage model (arbitrary keys, e.g. cache tokens)
517
+ gen = trace.generation(
518
+ name: "chat",
519
+ model: "gpt-4o",
520
+ usage_details: { input: 100, output: 50, cache_read: 30 },
521
+ cost_details: { input: 0.001, output: 0.003, total: 0.004 }
522
+ )
523
+ gen.end(output: "response")
524
+
525
+ # Link a generation to a prompt version (accepts a Langfuse::Prompt or a hash)
526
+ prompt = Langfuse.get_prompt("chat-prompt")
527
+ gen = trace.generation(name: "chat", model: "gpt-4o", prompt: prompt)
528
+ # or: prompt: { name: "chat-prompt", version: 3 }
529
+ ```
530
+
379
531
  ### Error Handling
380
532
 
381
533
  ```ruby
@@ -405,11 +557,16 @@ client = Langfuse.new(
405
557
  public_key: "pk-lf-...",
406
558
  secret_key: "sk-lf-...",
407
559
  host: "https://your-instance.langfuse.com",
408
- debug: true, # Enable debug logging
409
- timeout: 30, # Request timeout in seconds
410
- retries: 3, # Number of retry attempts
411
- flush_interval: 30, # Event flush interval in seconds (default: 5)
412
- auto_flush: true # Enable automatic flushing (default: true)
560
+ debug: true, # Enable debug logging (or LANGFUSE_DEBUG=true)
561
+ timeout: 30, # Request timeout in seconds
562
+ retries: 3, # Number of retry attempts
563
+ flush_interval: 30, # Event flush interval in seconds (default: 5)
564
+ flush_at: 50, # Flush once this many events are queued (default: 15)
565
+ auto_flush: true, # Enable automatic flushing (default: true)
566
+ environment: "prod", # Tracing environment (or LANGFUSE_TRACING_ENVIRONMENT)
567
+ sample_rate: 0.5, # Keep 50% of traces deterministically (or LANGFUSE_SAMPLE_RATE)
568
+ mask: ->(v) { v }, # Callable applied to input/output/metadata
569
+ shutdown_on_exit: true # Flush pending events on process exit (default: true)
413
570
  )
414
571
  ```
415
572
 
@@ -420,9 +577,14 @@ You can also configure the client using environment variables:
420
577
  ```bash
421
578
  export LANGFUSE_PUBLIC_KEY="pk-lf-..."
422
579
  export LANGFUSE_SECRET_KEY="sk-lf-..."
423
- export LANGFUSE_HOST="https://cloud.langfuse.com"
580
+ export LANGFUSE_HOST="https://cloud.langfuse.com" # or LANGFUSE_BASE_URL
424
581
  export LANGFUSE_FLUSH_INTERVAL=5
582
+ export LANGFUSE_FLUSH_AT=15
425
583
  export LANGFUSE_AUTO_FLUSH=true
584
+ export LANGFUSE_TRACING_ENVIRONMENT="production"
585
+ export LANGFUSE_SAMPLE_RATE=0.5
586
+ export LANGFUSE_DEBUG=false
587
+ export LANGFUSE_INGESTION_MODE=legacy # or otel
426
588
  ```
427
589
 
428
590
  ### Automatic Flush Control
@@ -43,6 +43,7 @@ Gem::Specification.new do |spec|
43
43
  spec.require_paths = ['lib']
44
44
 
45
45
  # Dependencies
46
+ spec.add_dependency 'base64', '>= 0.1.0' # stdlib in Ruby < 4.0, bundled gem since Ruby 4.0
46
47
  spec.add_dependency 'concurrent-ruby', '~> 1.0'
47
48
  spec.add_dependency 'faraday', '>= 1.8', '< 3.0'
48
49
  spec.add_dependency 'faraday-multipart', '~> 1.0'
@@ -54,6 +55,7 @@ Gem::Specification.new do |spec|
54
55
  spec.add_development_dependency 'rake', '~> 13.0'
55
56
  spec.add_development_dependency 'rspec', '~> 3.0'
56
57
  spec.add_development_dependency 'rubocop', '~> 1.0'
58
+ spec.add_development_dependency 'tsort', '>= 0.1.0' # stdlib in Ruby < 4.0, default gem since Ruby 4.0
57
59
  spec.add_development_dependency 'vcr', '~> 6.0'
58
60
  spec.add_development_dependency 'webmock', '~> 3.0'
59
61
  spec.add_development_dependency 'yard', '~> 0.9'