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/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"] 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