From 889fc578821e1d541e2700374c9118fe3f14d7d5 Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Thu, 1 Oct 2026 18:44:33 +0200 Subject: [PATCH 1/2] Add failing tests for logging the private session id Since Rack 1.6.12 and 2.0.8 (and in rack-session), session.id is a Rack::Session::SessionId. SessionContext logs that object: MessagePack can't encode it, so the HTTP device drops the whole batch, and its public id is the cookie value of server-side stores like Pool. The tests use Cookie and Pool sessions, so the Gemfile gets rack-session (Rack 3 moved the session middlewares there; its 1.x is an empty shim). Co-Authored-By: Claude Opus 5.5 --- Gemfile | 2 + spec/logtail-rack/session_context_spec.rb | 67 +++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 spec/logtail-rack/session_context_spec.rb diff --git a/Gemfile b/Gemfile index 7aef0bb..103e22e 100644 --- a/Gemfile +++ b/Gemfile @@ -5,3 +5,5 @@ gemspec gem "base64" if RUBY_VERSION >= "3.4.0" # No longer a default gem on Ruby 4.0 and TruffleRuby 40, and logtail 0.1.17 requires it undeclared gem "logger" +# Rack 3 moved the session middlewares the tests use into this gem; its 1.x releases for older Racks are empty +gem "rack-session" diff --git a/spec/logtail-rack/session_context_spec.rb b/spec/logtail-rack/session_context_spec.rb new file mode 100644 index 0000000..252bae0 --- /dev/null +++ b/spec/logtail-rack/session_context_spec.rb @@ -0,0 +1,67 @@ +require "spec_helper" +require "rack/session/cookie" +require "rack/session/pool" + +RSpec.describe Logtail::Integrations::Rack::SessionContext do + [Rack::Session::Cookie, Rack::Session::Pool].each do |session_store| + it "log the private id of a #{session_store.name} session as a String", :aggregate_failures do + skip "Rack::Session::SessionId is new in Rack 1.6.12 and 2.0.8" unless defined?(Rack::Session::SessionId) + # Makes the session store generate a known session id + secure_random = Object.new + def secure_random.hex(_length) + "5e55102d0f1c4a6b9e2d7c8a1b3f6e90" + end + app = Rack::Builder.new do + use session_store, secret: "x" * 64, secure_random: secure_random + use Logtail::Integrations::Rack::SessionContext + run lambda { |env| + env["rack.session"]["visits"] = 1 + Logtail::Config.instance.logger.info("inside the app") + [200, {}, ["ok"]] + } + end.to_app + request = Rack::MockRequest.new(app) + + entries = capture_log_entries do + session_cookie = Array(request.get("/").headers["Set-Cookie"]).first.split(";").first + request.get("/", "HTTP_COOKIE" => session_cookie) + end + + # The first request has no session cookie yet. The private id is "2::" and the SHA-256 digest of the + # session id, the key Rack's server-side stores use. The session id is the cookie value of those stores. + expect(entries.map { |entry| entry.context_snapshot[:session] }).to eq([nil, { id: "2::5ea0adef81b3967d06c0bfc9021a8d4657afbe5a960a41977b99a3e0739e9087" }]) + expect { entries.map(&:to_hash).to_msgpack }.not_to raise_error + end + end + + it "log a String session id as it is" do + app = lambda { |env| + Logtail::Config.instance.logger.info("inside the app") + [200, {}, ["ok"]] + } + env = Rack::MockRequest.env_for("/", "rack.session" => Struct.new(:id).new("plain-session-id")) + + entries = capture_log_entries { described_class.new(app).call(env) } + + expect(entries.map { |entry| entry.context_snapshot[:session] }).to eq([{ id: "plain-session-id" }]) + end + + # Collects the LogEntry objects the way the HTTP log device receives them, before MessagePack encodes them + def capture_log_entries(&blk) + old_logger = Logtail::Config.instance.logger + + entries = [] + device = Object.new + device.define_singleton_method(:write) { |entry| entries << entry } + device.define_singleton_method(:close) {} + logger = Logtail::Logger.new(device) + logger.formatter = Logtail::Logger::PassThroughFormatter.new + Logtail::Config.instance.logger = logger + + blk.call + + entries + ensure + Logtail::Config.instance.logger = old_logger + end +end From df2335bbacba00ea876f6b61d2d9579d046961bc Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Thu, 1 Oct 2026 18:50:57 +0200 Subject: [PATCH 2/2] Log the private session id instead of the SessionId object SessionContext logs id.private_id when the session id responds to it: "2::" and the SHA-256 digest of the session id, the key Rack's server-side stores use. It is a String, so MessagePack can encode the batch again, and unlike the public id it isn't the session cookie of Pool, Redis or Memcache stores. Plain String ids from older Racks are logged as before. Co-Authored-By: Claude Opus 5.5 --- lib/logtail-rack/session_context.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/logtail-rack/session_context.rb b/lib/logtail-rack/session_context.rb index 4618d78..e4d808b 100755 --- a/lib/logtail-rack/session_context.rb +++ b/lib/logtail-rack/session_context.rb @@ -25,7 +25,10 @@ def get_session_id(env) if session = env['rack.session'] if session.respond_to?(:id) Logtail::Config.instance.debug { "Rack env session detected, using id attribute" } - session.id + id = session.id + # Since Rack 1.6.12 and 2.0.8 a Rack::Session::SessionId, which MessagePack can't encode. Its public id + # is the session cookie of server-side stores, the private id a hash of it that can't be used as one. + id.respond_to?(:private_id) ? id.private_id : id elsif session.respond_to?(:[]) Logtail::Config.instance.debug { "Rack env session detected, using the session_id key" } session["session_id"]