Skip to content

[ML-964] Custom/deterministic trace ID support#74

Open
kxzk wants to merge 3 commits intomainfrom
feature/ml-964-customdeterministic-trace-id-support
Open

[ML-964] Custom/deterministic trace ID support#74
kxzk wants to merge 3 commits intomainfrom
feature/ml-964-customdeterministic-trace-id-support

Conversation

@kxzk
Copy link
Copy Markdown
Collaborator

@kxzk kxzk commented Apr 10, 2026

TL;DR

Add Langfuse::TraceId for deterministic/custom trace IDs and wire trace_id: into start_observation / observe, matching the Python and JS SDKs.

Why

Users need to correlate external system IDs (DB primary keys, request IDs) with Langfuse traces — e.g. to score a trace later from a background job or link traces across services. An external PR (#69) attempted this but had bugs (regex anchors, implicit nil var, rubocop disables) and lacked the deterministic create_trace_id(seed:) helper both reference SDKs expose. This change implements the reference SDK surface cleanly and fixes a pre-existing ensure-block bug on observe / BaseObservation#start_observation discovered along the way.

What's in the change

  • New Langfuse::TraceId module — .create(seed:), .create_observation_id(seed:), .valid?, .valid_observation_id?, .to_span_context. Uses \A/\z anchors and SHA-256 matching the Python/JS SDKs so the same seed produces the same trace ID across all three.
  • Langfuse.create_trace_id(seed:) / Langfuse.create_observation_id(seed:) flat-API convenience methods.
  • trace_id: keyword on Langfuse.start_observation and Langfuse.observe; mutually exclusive with parent_span_context:. Invalid IDs raise ArgumentError.
  • Fixed ensure-block bug in Langfuse.observe and BaseObservation#start_observation — exceptions in the block now reliably end the span via ensure and re-raise.
  • Verified end-to-end against live Langfuse: created a deterministic trace, added a child observation and a score using the same trace_id, fetched the trace via the API and confirmed id, observations (2), score (1) and tags all present.

Checklist

  • Has label
  • Has linked issue
  • Tests added for new behavior
  • Docs updated (if user-facing)

Closes ML-964


Open with Devin

@kxzk kxzk added the enhancement New feature or request label Apr 10, 2026
Copilot AI review requested due to automatic review settings April 10, 2026 20:29
@kxzk kxzk added the enhancement New feature or request label Apr 10, 2026
@linear
Copy link
Copy Markdown

linear bot commented Apr 10, 2026

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds deterministic/custom trace ID support to the Ruby SDK (matching Python/JS), wires trace_id: into start_observation/observe, and hardens block-based observation APIs so spans reliably end via ensure even when exceptions are raised.

Changes:

  • Introduces Langfuse::TraceId helpers for deterministic/random trace and observation ID generation plus SpanContext conversion.
  • Adds Langfuse.create_trace_id / Langfuse.create_observation_id convenience APIs and trace_id: support on start_observation and observe (mutually exclusive with parent_span_context:).
  • Refactors block execution for observations to use ensure for reliable span end behavior on exceptions, with added specs.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
spec/langfuse/trace_id_spec.rb Adds unit coverage for deterministic ID generation/validation and SpanContext conversion.
spec/langfuse/base_observation_spec.rb Adds regression spec ensuring child spans end and exceptions re-raise.
spec/langfuse_spec.rb Adds coverage for new convenience APIs, trace_id: behavior, exclusivity checks, and ensure semantics in observe.
lib/langfuse/trace_id.rb Implements deterministic/random ID helpers, validation, and to_span_context.
lib/langfuse/observations.rb Refactors child block execution to ensure spans end on exceptions.
lib/langfuse.rb Wires trace_id: into start_observation/observe, adds convenience methods, and introduces a trace context resolver.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +55 to +57
def valid?(trace_id)
trace_id.is_a?(String) && TRACE_ID_PATTERN.match?(trace_id)
end
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

valid? currently only checks the hex format. Per the W3C trace-context spec, a trace ID of all zeros ("0" * 32) is invalid and may be treated as an invalid SpanContext by OpenTelemetry. Consider updating validation to also reject the all-zero trace ID to ensure to_span_context behaves consistently.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +63
# @param id [Object] Value to validate
# @return [Boolean] true when the value is a 16-char lowercase hex string
def valid_observation_id?(id)
id.is_a?(String) && OBSERVATION_ID_PATTERN.match?(id)
end
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

valid_observation_id? currently accepts any 16-char lowercase hex string, including the all-zero span ID ("0" * 16) which is invalid under the W3C trace-context spec. Tightening validation here will prevent callers from generating/accepting span IDs that OpenTelemetry may treat as invalid.

Copilot uses AI. Check for mistakes.
# Events are excluded because they auto-end at creation.
#
# @api private
def execute_child_block(child, as_type, &block)
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execute_child_block is tagged as @api private but is currently a public instance method on BaseObservation, expanding the public API surface. Consider making it private (e.g., private def execute_child_block ...) so only start_observation exposes the intended API.

Suggested change
def execute_child_block(child, as_type, &block)
private def execute_child_block(child, as_type, &block)

Copilot uses AI. Check for mistakes.
lib/langfuse.rb Outdated
Comment on lines +430 to +434
# guaranteeing the span ends via ensure — even if the block raises.
# Events are excluded because they auto-end in {start_observation}.
#
# @api private
def execute_observe_block(observation, as_type, &block)
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execute_observe_block is documented as @api private but is currently a public module method (it’s defined before the private section). Consider making it private (move it below private or declare private :execute_observe_block) to avoid exposing an internal helper as part of the public API.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

…vation

Post-review cleanup on ML-964:

- Deduplicate execute_observe_block / execute_child_block into a single
  Langfuse.run_in_observation_context; BaseObservation#start_observation
  now delegates to it
- Extract validate_observation_type! and apply_observation_attributes
  private helpers; drop the Metrics/AbcSize rubocop disable on
  start_observation (only ParameterLists remains)
- Rewrite TraceId.to_span_context YARD to accurately describe how the
  synthetic span_id is consumed
- Trim narrative comments per CLAUDE.md (WHY, not WHAT)
devin-ai-integration[bot]

This comment was marked as resolved.

- Reject the W3C all-zero trace_id and span_id in TraceId.valid? /
  valid_observation_id? so OpenTelemetry never sees an invalid SpanContext.
- Document the new trace_id: parameter on Langfuse.start_observation and
  Langfuse.observe with @param/@raise YARD tags.
- Move run_in_observation_context behind `private` on the Langfuse module
  so the internal helper isn't part of the public API; BaseObservation
  reaches it via __send__.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants