From ac02f088d6e0ca71a3223ebf962ed53bcfea8a69 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Mon, 13 Jul 2026 23:28:39 +0800 Subject: [PATCH 1/3] Add span origin provenance --- lib/braintrust.rb | 4 +- lib/braintrust/config.rb | 112 ++++++++++++++++++++++++- lib/braintrust/state.rb | 6 +- lib/braintrust/trace/span_processor.rb | 43 ++++++++++ 4 files changed, 158 insertions(+), 7 deletions(-) diff --git a/lib/braintrust.rb b/lib/braintrust.rb index 496fc226..b0ddeae5 100644 --- a/lib/braintrust.rb +++ b/lib/braintrust.rb @@ -43,13 +43,14 @@ class Error < StandardError; end # @param filter_ai_spans [Boolean, nil] Enable AI span filtering (overrides BRAINTRUST_OTEL_FILTER_AI_SPANS env var) # @param span_filter_funcs [Array, nil] Custom span filter functions # @param exporter [Exporter, nil] Optional exporter override (for testing) + # @param environment [Hash, nil] Span-origin environment override, e.g. { type: "ci", name: "github_actions" } # @param auto_instrument [Boolean, Hash, nil] Auto-instrumentation config: # - nil (default): use BRAINTRUST_AUTO_INSTRUMENT env var, default true if not set # - true: explicitly enable # - false: explicitly disable # - Hash with :only or :except keys for filtering # @return [State] the created state - def self.init(api_key: nil, org_name: nil, default_project: nil, app_url: nil, api_url: nil, set_global: true, blocking_login: false, enable_tracing: true, tracer_provider: nil, filter_ai_spans: nil, span_filter_funcs: nil, exporter: nil, auto_instrument: nil) + def self.init(api_key: nil, org_name: nil, default_project: nil, app_url: nil, api_url: nil, set_global: true, blocking_login: false, enable_tracing: true, tracer_provider: nil, filter_ai_spans: nil, span_filter_funcs: nil, exporter: nil, auto_instrument: nil, environment: nil) state = State.from_env( api_key: api_key, org_name: org_name, @@ -61,6 +62,7 @@ def self.init(api_key: nil, org_name: nil, default_project: nil, app_url: nil, a tracer_provider: tracer_provider, filter_ai_spans: filter_ai_spans, span_filter_funcs: span_filter_funcs, + environment: environment, exporter: exporter ) diff --git a/lib/braintrust/config.rb b/lib/braintrust/config.rb index 48a7f803..75ea1232 100644 --- a/lib/braintrust/config.rb +++ b/lib/braintrust/config.rb @@ -7,10 +7,10 @@ module Braintrust # and allows overriding with explicit options class Config attr_reader :api_key, :org_name, :default_project, :app_url, :api_url, - :filter_ai_spans, :span_filter_funcs + :filter_ai_spans, :span_filter_funcs, :environment def initialize(api_key: nil, org_name: nil, default_project: nil, app_url: nil, api_url: nil, - filter_ai_spans: nil, span_filter_funcs: nil) + filter_ai_spans: nil, span_filter_funcs: nil, environment: nil) @api_key = api_key @org_name = org_name @default_project = default_project @@ -18,6 +18,7 @@ def initialize(api_key: nil, org_name: nil, default_project: nil, app_url: nil, @api_url = api_url @filter_ai_spans = filter_ai_spans @span_filter_funcs = span_filter_funcs || [] + @environment = environment end # Create a Config from environment variables, with option overrides @@ -29,9 +30,10 @@ def initialize(api_key: nil, org_name: nil, default_project: nil, app_url: nil, # @param api_url [String, nil] API URL (overrides BRAINTRUST_API_URL env var) # @param filter_ai_spans [Boolean, nil] Enable AI span filtering (overrides BRAINTRUST_OTEL_FILTER_AI_SPANS env var) # @param span_filter_funcs [Array, nil] Custom span filter functions + # @param environment [Hash, nil] Span-origin environment override, e.g. { type: "ci", name: "github_actions" } # @return [Config] the created config def self.from_env(api_key: nil, org_name: nil, default_project: nil, app_url: nil, api_url: nil, - filter_ai_spans: nil, span_filter_funcs: nil) + filter_ai_spans: nil, span_filter_funcs: nil, environment: nil) # Parse filter_ai_spans from ENV if not explicitly provided env_filter_ai_spans = ENV["BRAINTRUST_OTEL_FILTER_AI_SPANS"] filter_ai_spans_value = if filter_ai_spans.nil? @@ -47,8 +49,110 @@ def self.from_env(api_key: nil, org_name: nil, default_project: nil, app_url: ni app_url: app_url || ENV["BRAINTRUST_APP_URL"] || "https://www.braintrust.dev", api_url: api_url || ENV["BRAINTRUST_API_URL"] || "https://api.braintrust.dev", filter_ai_spans: filter_ai_spans_value, - span_filter_funcs: span_filter_funcs + span_filter_funcs: span_filter_funcs, + environment: detect_environment(environment) ) end + + def self.detect_environment(explicit = nil) + return normalize_environment(explicit) if explicit + + env_type = env_value("BRAINTRUST_ENVIRONMENT_TYPE") + if env_type && !env_type.empty? + env_name = env_value("BRAINTRUST_ENVIRONMENT_NAME") + return { type: env_type, name: env_name }.compact + end + + { + "GITHUB_ACTIONS" => "github_actions", + "GITLAB_CI" => "gitlab_ci", + "CIRCLECI" => "circleci", + "BUILDKITE" => "buildkite", + "JENKINS_URL" => "jenkins", + "JENKINS_HOME" => "jenkins", + "TF_BUILD" => "azure_pipelines", + "TEAMCITY_VERSION" => "teamcity", + "TRAVIS" => "travis", + "BITBUCKET_BUILD_NUMBER" => "bitbucket" + }.each do |key, name| + return { type: "ci", name: name } if process_env_value(key) + end + return { type: "ci", name: "ci" } if process_env_value("CI") + + { + "VERCEL" => "vercel", + "NETLIFY" => "netlify", + "AWS_LAMBDA_FUNCTION_NAME" => "aws_lambda", + "AWS_EXECUTION_ENV" => "aws_lambda", + "K_SERVICE" => "cloud_run", + "FUNCTION_TARGET" => "gcp_functions", + "KUBERNETES_SERVICE_HOST" => "kubernetes", + "ECS_CONTAINER_METADATA_URI" => "ecs", + "ECS_CONTAINER_METADATA_URI_V4" => "ecs", + "DYNO" => "heroku", + "FLY_APP_NAME" => "fly", + "RAILWAY_ENVIRONMENT" => "railway", + "RENDER_SERVICE_NAME" => "render" + }.each do |key, name| + return { type: "server", name: name } if process_env_value(key) + end + + deployment_mode_environment(process_env_value("RAILS_ENV")) || + deployment_mode_environment(process_env_value("RACK_ENV")) + end + + def self.normalize_environment(environment) + type = environment[:type] || environment["type"] + name = environment[:name] || environment["name"] + { type: type, name: name }.compact + end + + def self.deployment_mode_environment(value) + return nil if value.nil? || value.empty? + + normalized = value.downcase + return { type: "server", name: normalized } if ["production", "staging"].include?(normalized) + return { type: "local", name: normalized } if ["development", "local"].include?(normalized) + + nil + end + + def self.env_value(key) + value = ENV[key] + value = read_braintrust_env_file_value(key) if value.nil? || value.strip.empty? + value&.strip + end + + def self.process_env_value(key) + value = ENV[key] + value&.strip unless value.nil? || value.strip.empty? + end + + def self.read_braintrust_env_file_value(key) + dir = Dir.pwd + 65.times do + path = File.join(dir, ".env.braintrust") + if File.file?(path) + File.foreach(path) do |line| + stripped = line.strip + next if stripped.empty? || stripped.start_with?("#") + + name, value = stripped.split("=", 2) + next unless name&.strip == key + + return value&.strip&.delete_prefix('"')&.delete_suffix('"')&.delete_prefix("'")&.delete_suffix("'") + end + return nil + end + + parent = File.dirname(dir) + return nil if parent == dir + + dir = parent + end + nil + rescue + nil + end end end diff --git a/lib/braintrust/state.rb b/lib/braintrust/state.rb index f95e5fd9..defaa7e1 100644 --- a/lib/braintrust/state.rb +++ b/lib/braintrust/state.rb @@ -25,8 +25,9 @@ class MissingAPIKeyError < ArgumentError; end # @param filter_ai_spans [Boolean, nil] Enable AI span filtering # @param span_filter_funcs [Array, nil] Custom span filter functions # @param exporter [Exporter, nil] Optional exporter override (for testing) + # @param environment [Hash, nil] Span-origin environment override # @return [State] the created state - def self.from_env(api_key: nil, org_name: nil, default_project: nil, app_url: nil, api_url: nil, blocking_login: false, enable_tracing: true, tracer_provider: nil, filter_ai_spans: nil, span_filter_funcs: nil, exporter: nil) + def self.from_env(api_key: nil, org_name: nil, default_project: nil, app_url: nil, api_url: nil, blocking_login: false, enable_tracing: true, tracer_provider: nil, filter_ai_spans: nil, span_filter_funcs: nil, exporter: nil, environment: nil) require_relative "config" config = Config.from_env( api_key: api_key, @@ -35,7 +36,8 @@ def self.from_env(api_key: nil, org_name: nil, default_project: nil, app_url: ni app_url: app_url, api_url: api_url, filter_ai_spans: filter_ai_spans, - span_filter_funcs: span_filter_funcs + span_filter_funcs: span_filter_funcs, + environment: environment ) new( api_key: config.api_key, diff --git a/lib/braintrust/trace/span_processor.rb b/lib/braintrust/trace/span_processor.rb index cf2ba8a9..f15fe001 100644 --- a/lib/braintrust/trace/span_processor.rb +++ b/lib/braintrust/trace/span_processor.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true +require "json" require "opentelemetry/sdk" +require_relative "../version" module Braintrust module Trace @@ -10,6 +12,9 @@ class SpanProcessor PARENT_ATTR_KEY = "braintrust.parent" ORG_ATTR_KEY = "braintrust.org" APP_URL_ATTR_KEY = "braintrust.app_url" + CONTEXT_JSON_ATTR_KEY = "braintrust.context_json" + ENVIRONMENT_TYPE_ATTR_KEY = "braintrust.environment.type" + ENVIRONMENT_NAME_ATTR_KEY = "braintrust.environment.name" def initialize(wrapped_processor, state, filters = []) @wrapped = wrapped_processor @@ -18,6 +23,8 @@ def initialize(wrapped_processor, state, filters = []) end def on_start(span, parent_context) + add_span_origin(span) + # Add default parent if span doesn't already have one has_parent = span.respond_to?(:attributes) && span.attributes&.key?(PARENT_ATTR_KEY) @@ -53,6 +60,42 @@ def force_flush(timeout: nil) private + def add_span_origin(span) + context = parse_context_json(span.respond_to?(:attributes) ? span.attributes&.[](CONTEXT_JSON_ATTR_KEY) : nil) + span_origin = context["span_origin"].is_a?(Hash) ? context["span_origin"] : {} + span_origin["name"] ||= "braintrust.sdk.ruby" + span_origin["version"] ||= Braintrust::VERSION + span_origin["instrumentation"] ||= {"name" => instrumentation_name(span)} + if @state.config&.environment && !span_origin.key?("environment") + environment = {"type" => @state.config.environment[:type]} + environment["name"] = @state.config.environment[:name] if @state.config.environment[:name] + span_origin["environment"] = environment + end + context["span_origin"] = span_origin + span.set_attribute(CONTEXT_JSON_ATTR_KEY, JSON.generate(context)) + + return unless @state.config&.environment + + span.set_attribute(ENVIRONMENT_TYPE_ATTR_KEY, @state.config.environment[:type]) + span.set_attribute(ENVIRONMENT_NAME_ATTR_KEY, @state.config.environment[:name]) if @state.config.environment[:name] + end + + def parse_context_json(raw) + return {} unless raw.is_a?(String) && !raw.strip.empty? + + parsed = JSON.parse(raw) + parsed.is_a?(Hash) ? parsed : {} + rescue JSON::ParserError + {} + end + + def instrumentation_name(span) + return span.instrumentation_scope.name if span.respond_to?(:instrumentation_scope) && span.instrumentation_scope&.respond_to?(:name) + return span.instrumentation_library.name if span.respond_to?(:instrumentation_library) && span.instrumentation_library&.respond_to?(:name) + + "braintrust-ruby" + end + def default_parent # If default_project is set, format it as "project_name:value" # The default_project should be a plain project name (e.g., "my-project") From 21ae116bab14b762f0d5dbd63e181c91b822d07f Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Wed, 15 Jul 2026 15:22:03 +0800 Subject: [PATCH 2/3] Fix Ruby span filter provenance handling --- gemfiles/anthropic.gemfile | 1 + gemfiles/anthropic_1_11.gemfile | 1 + gemfiles/anthropic_1_12.gemfile | 1 + gemfiles/anthropic_uninstalled.gemfile | 1 + gemfiles/contrib.gemfile | 1 + gemfiles/openai.gemfile | 1 + gemfiles/openai_0_33.gemfile | 1 + gemfiles/openai_0_34.gemfile | 1 + gemfiles/openai_ruby_openai.gemfile | 1 + gemfiles/openai_uninstalled.gemfile | 1 + gemfiles/opentelemetry_latest.gemfile | 1 + gemfiles/opentelemetry_min.gemfile | 1 + gemfiles/rails.gemfile | 1 + gemfiles/rails_server.gemfile | 1 + gemfiles/ruby_llm.gemfile | 1 + gemfiles/ruby_llm_1_8.gemfile | 1 + gemfiles/ruby_llm_1_9.gemfile | 1 + gemfiles/ruby_llm_uninstalled.gemfile | 1 + gemfiles/ruby_openai.gemfile | 1 + gemfiles/ruby_openai_7_0.gemfile | 1 + gemfiles/ruby_openai_8_0.gemfile | 1 + gemfiles/ruby_openai_uninstalled.gemfile | 1 + gemfiles/server.gemfile | 1 + lib/braintrust/config.rb | 14 +++++++------- lib/braintrust/trace/span_filter.rb | 5 ++++- 25 files changed, 34 insertions(+), 8 deletions(-) diff --git a/gemfiles/anthropic.gemfile b/gemfiles/anthropic.gemfile index 44722fda..495eed11 100644 --- a/gemfiles/anthropic.gemfile +++ b/gemfiles/anthropic.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/anthropic_1_11.gemfile b/gemfiles/anthropic_1_11.gemfile index 69c7ced7..64e7dbc3 100644 --- a/gemfiles/anthropic_1_11.gemfile +++ b/gemfiles/anthropic_1_11.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/anthropic_1_12.gemfile b/gemfiles/anthropic_1_12.gemfile index ba739117..7b80f41e 100644 --- a/gemfiles/anthropic_1_12.gemfile +++ b/gemfiles/anthropic_1_12.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/anthropic_uninstalled.gemfile b/gemfiles/anthropic_uninstalled.gemfile index 6c953f98..605f0583 100644 --- a/gemfiles/anthropic_uninstalled.gemfile +++ b/gemfiles/anthropic_uninstalled.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/contrib.gemfile b/gemfiles/contrib.gemfile index d7d9a50f..82983e3c 100644 --- a/gemfiles/contrib.gemfile +++ b/gemfiles/contrib.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/openai.gemfile b/gemfiles/openai.gemfile index a49d0436..7593c671 100644 --- a/gemfiles/openai.gemfile +++ b/gemfiles/openai.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/openai_0_33.gemfile b/gemfiles/openai_0_33.gemfile index 23380307..d2a4d7ba 100644 --- a/gemfiles/openai_0_33.gemfile +++ b/gemfiles/openai_0_33.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/openai_0_34.gemfile b/gemfiles/openai_0_34.gemfile index 764fa916..20d35e76 100644 --- a/gemfiles/openai_0_34.gemfile +++ b/gemfiles/openai_0_34.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/openai_ruby_openai.gemfile b/gemfiles/openai_ruby_openai.gemfile index 958a8aeb..5513bb33 100644 --- a/gemfiles/openai_ruby_openai.gemfile +++ b/gemfiles/openai_ruby_openai.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/openai_uninstalled.gemfile b/gemfiles/openai_uninstalled.gemfile index 6c953f98..605f0583 100644 --- a/gemfiles/openai_uninstalled.gemfile +++ b/gemfiles/openai_uninstalled.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/opentelemetry_latest.gemfile b/gemfiles/opentelemetry_latest.gemfile index bc8446f1..852ad2a5 100644 --- a/gemfiles/opentelemetry_latest.gemfile +++ b/gemfiles/opentelemetry_latest.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/opentelemetry_min.gemfile b/gemfiles/opentelemetry_min.gemfile index d11eca8b..ab502c8d 100644 --- a/gemfiles/opentelemetry_min.gemfile +++ b/gemfiles/opentelemetry_min.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/rails.gemfile b/gemfiles/rails.gemfile index ddc234c9..9f343fdd 100644 --- a/gemfiles/rails.gemfile +++ b/gemfiles/rails.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/rails_server.gemfile b/gemfiles/rails_server.gemfile index 4bfa2df0..814badaf 100644 --- a/gemfiles/rails_server.gemfile +++ b/gemfiles/rails_server.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/ruby_llm.gemfile b/gemfiles/ruby_llm.gemfile index f4431206..de569541 100644 --- a/gemfiles/ruby_llm.gemfile +++ b/gemfiles/ruby_llm.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/ruby_llm_1_8.gemfile b/gemfiles/ruby_llm_1_8.gemfile index 6288ac89..db181329 100644 --- a/gemfiles/ruby_llm_1_8.gemfile +++ b/gemfiles/ruby_llm_1_8.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/ruby_llm_1_9.gemfile b/gemfiles/ruby_llm_1_9.gemfile index 69400abc..1380ea1e 100644 --- a/gemfiles/ruby_llm_1_9.gemfile +++ b/gemfiles/ruby_llm_1_9.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/ruby_llm_uninstalled.gemfile b/gemfiles/ruby_llm_uninstalled.gemfile index 6c953f98..605f0583 100644 --- a/gemfiles/ruby_llm_uninstalled.gemfile +++ b/gemfiles/ruby_llm_uninstalled.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/ruby_openai.gemfile b/gemfiles/ruby_openai.gemfile index 18523602..54b725e1 100644 --- a/gemfiles/ruby_openai.gemfile +++ b/gemfiles/ruby_openai.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/ruby_openai_7_0.gemfile b/gemfiles/ruby_openai_7_0.gemfile index 582d736d..76676e7f 100644 --- a/gemfiles/ruby_openai_7_0.gemfile +++ b/gemfiles/ruby_openai_7_0.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/ruby_openai_8_0.gemfile b/gemfiles/ruby_openai_8_0.gemfile index ce51344f..6c2ed570 100644 --- a/gemfiles/ruby_openai_8_0.gemfile +++ b/gemfiles/ruby_openai_8_0.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/ruby_openai_uninstalled.gemfile b/gemfiles/ruby_openai_uninstalled.gemfile index 6c953f98..605f0583 100644 --- a/gemfiles/ruby_openai_uninstalled.gemfile +++ b/gemfiles/ruby_openai_uninstalled.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/gemfiles/server.gemfile b/gemfiles/server.gemfile index c0390122..4dbdab0c 100644 --- a/gemfiles/server.gemfile +++ b/gemfiles/server.gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" +gem "openssl", "4.0.0" gem "appraisal", "~> 2.5" gem "climate_control", "~> 1.2" gem "kramdown", "~> 2.0" diff --git a/lib/braintrust/config.rb b/lib/braintrust/config.rb index 75ea1232..d431e01f 100644 --- a/lib/braintrust/config.rb +++ b/lib/braintrust/config.rb @@ -60,7 +60,7 @@ def self.detect_environment(explicit = nil) env_type = env_value("BRAINTRUST_ENVIRONMENT_TYPE") if env_type && !env_type.empty? env_name = env_value("BRAINTRUST_ENVIRONMENT_NAME") - return { type: env_type, name: env_name }.compact + return {type: env_type, name: env_name}.compact end { @@ -75,9 +75,9 @@ def self.detect_environment(explicit = nil) "TRAVIS" => "travis", "BITBUCKET_BUILD_NUMBER" => "bitbucket" }.each do |key, name| - return { type: "ci", name: name } if process_env_value(key) + return {type: "ci", name: name} if process_env_value(key) end - return { type: "ci", name: "ci" } if process_env_value("CI") + return {type: "ci", name: "ci"} if process_env_value("CI") { "VERCEL" => "vercel", @@ -94,7 +94,7 @@ def self.detect_environment(explicit = nil) "RAILWAY_ENVIRONMENT" => "railway", "RENDER_SERVICE_NAME" => "render" }.each do |key, name| - return { type: "server", name: name } if process_env_value(key) + return {type: "server", name: name} if process_env_value(key) end deployment_mode_environment(process_env_value("RAILS_ENV")) || @@ -104,15 +104,15 @@ def self.detect_environment(explicit = nil) def self.normalize_environment(environment) type = environment[:type] || environment["type"] name = environment[:name] || environment["name"] - { type: type, name: name }.compact + {type: type, name: name}.compact end def self.deployment_mode_environment(value) return nil if value.nil? || value.empty? normalized = value.downcase - return { type: "server", name: normalized } if ["production", "staging"].include?(normalized) - return { type: "local", name: normalized } if ["development", "local"].include?(normalized) + return {type: "server", name: normalized} if ["production", "staging"].include?(normalized) + return {type: "local", name: normalized} if ["development", "local"].include?(normalized) nil end diff --git a/lib/braintrust/trace/span_filter.rb b/lib/braintrust/trace/span_filter.rb index 93887787..705838fa 100644 --- a/lib/braintrust/trace/span_filter.rb +++ b/lib/braintrust/trace/span_filter.rb @@ -16,7 +16,10 @@ module SpanFilter SYSTEM_ATTRIBUTES = [ "braintrust.parent", "braintrust.org", - "braintrust.app_url" + "braintrust.app_url", + "braintrust.context_json", + "braintrust.environment.type", + "braintrust.environment.name" ].freeze # Prefixes that indicate an AI-related span From 19633085966a4e168a7a88bd0db9107bd28a8c5e Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Wed, 15 Jul 2026 16:36:48 +0800 Subject: [PATCH 3/3] Make Ruby config environment helpers private --- lib/braintrust/config.rb | 3 +++ test/braintrust/config_test.rb | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/braintrust/config.rb b/lib/braintrust/config.rb index d431e01f..140ab082 100644 --- a/lib/braintrust/config.rb +++ b/lib/braintrust/config.rb @@ -154,5 +154,8 @@ def self.read_braintrust_env_file_value(key) rescue nil end + + private_class_method :detect_environment, :normalize_environment, :deployment_mode_environment, + :env_value, :process_env_value, :read_braintrust_env_file_value end end diff --git a/test/braintrust/config_test.rb b/test/braintrust/config_test.rb index c222e822..9d4d3423 100644 --- a/test/braintrust/config_test.rb +++ b/test/braintrust/config_test.rb @@ -49,6 +49,22 @@ def test_provides_default_values assert_equal "https://api.braintrust.dev", config.api_url end + def test_environment_helpers_are_private_class_methods + assert_includes Braintrust::Config.methods, :from_env + + [ + :detect_environment, + :normalize_environment, + :deployment_mode_environment, + :env_value, + :process_env_value, + :read_braintrust_env_file_value + ].each do |helper| + refute_includes Braintrust::Config.methods, helper + assert_includes Braintrust::Config.private_methods, helper + end + end + def test_passed_options_override_env_vars ENV["BRAINTRUST_API_KEY"] = "env-key" ENV["BRAINTRUST_ORG_NAME"] = "env-org"