Class: Dommy::Rack::Trace
- Inherits:
-
Object
- Object
- Dommy::Rack::Trace
- Defined in:
- lib/dommy/rack/trace.rb,
lib/dommy/rack/trace/ndjson.rb,
lib/dommy/rack/trace/formatter.rb,
lib/dommy/rack/trace/dom_observer.rb,
lib/dommy/rack/trace/param_filter.rb
Overview
A structured, virtual-time-ordered record of what a Session did: user
actions, HTTP exchanges (each redirect hop included), document loads,
script boots, console output, JS errors, and — opt-in — DOM mutations.
One ordered event stream, not a log: it powers readable failure reports
and machine-readable diagnostics (to_text / to_ndjson).
session = Dommy::Rack::Session.new(app, trace: true)
session.visit "/login"
session. "Login"
puts session.trace.to_text
The Trace subscribes to the Session's request/response/document seams and,
when JS is enabled, to the SessionRuntime's console/js_error/script seams.
User-action grouping comes from the Session's navigation verbs calling
__internal_open_action (the shared field verbs are intentionally not
instrumented). seq — assigned at emit time — is the authoritative
timeline order; t is the virtual clock (nil before a document exists).
Defined Under Namespace
Modules: Formatter Classes: DomObserver, Event, Ndjson, ParamFilter
Constant Summary collapse
- LEVELS =
Recording verbosity.
:offrecords nothing;:actionsrecords the action / http / form / document / dom timeline;:verboseadds the JS-realm streams (script / console / js_error). %i[off actions verbose].freeze
- REALM_TYPES =
%i[script console js_error].freeze
Instance Attribute Summary collapse
-
#artifacts ⇒ Object
readonly
Returns the value of attribute artifacts.
-
#events ⇒ Object
readonly
Returns the value of attribute events.
-
#level ⇒ Object
readonly
Returns the value of attribute level.
Class Method Summary collapse
-
.attach(session, level: :verbose, dom: false, filter: ParamFilter::DEFAULT, snapshots: false) ⇒ Object
Build a Trace, wire it to the session's (and its runtime's) seams, and return it.
Instance Method Summary collapse
-
#__internal_open_action(verb, label = nil) ⇒ Object
Open a user-action group.
-
#__internal_record_form(method:, url:, params: nil) ⇒ Object
Record a form submission (method, path, filtered params).
-
#__internal_wire ⇒ Object
Subscribe to the session and (when present) runtime seams.
- #actions ⇒ Object
- #console ⇒ Object
-
#current_page ⇒ Object
The page currently loaded (for failure context).
- #documents ⇒ Object
- #dom_mutations ⇒ Object
-
#flush_dom ⇒ Object
Drain the current window's microtasks so queued MutationObserver records are delivered (JS mode does this via after_interaction; non-JS / tests call this explicitly).
- #forms ⇒ Object
-
#http ⇒ Object
--- Queries (read-only views over the event stream) ---.
-
#initialize(session, level: :verbose, dom: false, filter: ParamFilter::DEFAULT, snapshots: false) ⇒ Trace
constructor
A new instance of Trace.
- #js_errors ⇒ Object
- #last_action ⇒ Object
- #recent(count = 10) ⇒ Object
- #record_error(message:, exception_class: nil, source: nil) ⇒ Object
-
#save(dir, status: "ok", metadata: nil) ⇒ Object
Write a self-contained trace bundle to
dir:trace.ndjsonplus anartifacts/directory with each captured snapshot as its own file, the NDJSON referencing them by relative path. - #scripts ⇒ Object
- #to_ndjson(status: "ok", wall_time: nil, metadata: nil) ⇒ Object
- #to_s ⇒ Object
-
#to_text(limit: nil) ⇒ Object
--- Formatting ---.
- #write_ndjson(path, **opts) ⇒ Object
Constructor Details
#initialize(session, level: :verbose, dom: false, filter: ParamFilter::DEFAULT, snapshots: false) ⇒ Trace
Returns a new instance of Trace.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/dommy/rack/trace.rb', line 49 def initialize(session, level: :verbose, dom: false, filter: ParamFilter::DEFAULT, snapshots: false) @session = session @level = LEVELS.include?(level) ? level : :verbose @dom = dom @param_filter = ParamFilter.new(filter) @snapshots = snapshots @events = [] @artifacts = {} # seq => captured content (e.g. DOM HTML), kept out of the event @started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) @seq = 0 @action_seq = nil @pending_request = nil @observers = [] end |
Instance Attribute Details
#artifacts ⇒ Object (readonly)
Returns the value of attribute artifacts.
47 48 49 |
# File 'lib/dommy/rack/trace.rb', line 47 def artifacts @artifacts end |
#events ⇒ Object (readonly)
Returns the value of attribute events.
47 48 49 |
# File 'lib/dommy/rack/trace.rb', line 47 def events @events end |
#level ⇒ Object (readonly)
Returns the value of attribute level.
47 48 49 |
# File 'lib/dommy/rack/trace.rb', line 47 def level @level end |
Class Method Details
.attach(session, level: :verbose, dom: false, filter: ParamFilter::DEFAULT, snapshots: false) ⇒ Object
Build a Trace, wire it to the session's (and its runtime's) seams, and
return it. A :off trace wires nothing.
41 42 43 44 45 |
# File 'lib/dommy/rack/trace.rb', line 41 def self.attach(session, level: :verbose, dom: false, filter: ParamFilter::DEFAULT, snapshots: false) trace = new(session, level: level, dom: dom, filter: filter, snapshots: snapshots) trace.__internal_wire trace end |
Instance Method Details
#__internal_open_action(verb, label = nil) ⇒ Object
Open a user-action group. Following events carry this action's seq as
their action_seq until the next action opens.
88 89 90 91 92 93 94 95 96 |
# File 'lib/dommy/rack/trace.rb', line 88 def __internal_open_action(verb, label = nil) return if @level == :off @seq += 1 @action_seq = @seq @events << Event.new(seq: @seq, t: now_ms, wall_ms: monotonic_ms, type: :action, name: verb, action_seq: nil, data: {verb: verb, label: label, source: __internal_caller_source}) nil end |
#__internal_record_form(method:, url:, params: nil) ⇒ Object
Record a form submission (method, path, filtered params). Form params arrive as ordered [name, value] pairs; present them as a masked hash.
106 107 108 |
# File 'lib/dommy/rack/trace.rb', line 106 def __internal_record_form(method:, url:, params: nil) __internal_emit(:form, {method: method, path: path_of(url) || url, params: @param_filter.form_params(params)}) end |
#__internal_wire ⇒ Object
Subscribe to the session and (when present) runtime seams. Called by
.attach; safe to call once.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/dommy/rack/trace.rb', line 66 def __internal_wire return if @level == :off @session.on_request { |env| __internal_on_request(env) } @session.on_response { |response| __internal_on_response(response) } runtime = @session.respond_to?(:__internal_js_runtime) ? @session.__internal_js_runtime : nil if runtime runtime.on_document { |window| __internal_on_document(window) } runtime.on_script { |element, error| __internal_emit(:script, script_data(element, error), window: current_window) } runtime.on_console { |log| __internal_emit(:console, console_data(log), window: current_window) } runtime.on_js_error { |error| __internal_emit(:js_error, {message: (error)}, window: current_window) } else @session.on_document_loaded { |window| __internal_on_document(window) } end nil end |
#actions ⇒ Object
128 |
# File 'lib/dommy/rack/trace.rb', line 128 def actions = events_of(:action) |
#console ⇒ Object
125 |
# File 'lib/dommy/rack/trace.rb', line 125 def console = events_of(:console) |
#current_page ⇒ Object
The page currently loaded (for failure context).
134 |
# File 'lib/dommy/rack/trace.rb', line 134 def current_page = {url: @session.current_url, title: @session.document&.title} |
#documents ⇒ Object
123 |
# File 'lib/dommy/rack/trace.rb', line 123 def documents = events_of(:document) |
#dom_mutations ⇒ Object
129 |
# File 'lib/dommy/rack/trace.rb', line 129 def dom_mutations = events_of(:dom) |
#flush_dom ⇒ Object
Drain the current window's microtasks so queued MutationObserver records are delivered (JS mode does this via after_interaction; non-JS / tests call this explicitly).
113 114 115 116 117 118 |
# File 'lib/dommy/rack/trace.rb', line 113 def flush_dom return unless @dom current_window&.scheduler&.drain_microtasks nil end |
#forms ⇒ Object
127 |
# File 'lib/dommy/rack/trace.rb', line 127 def forms = events_of(:form) |
#http ⇒ Object
--- Queries (read-only views over the event stream) ---
122 |
# File 'lib/dommy/rack/trace.rb', line 122 def http = events_of(:http) |
#js_errors ⇒ Object
126 |
# File 'lib/dommy/rack/trace.rb', line 126 def js_errors = events_of(:js_error) |
#last_action ⇒ Object
130 |
# File 'lib/dommy/rack/trace.rb', line 130 def last_action = actions.last |
#recent(count = 10) ⇒ Object
131 |
# File 'lib/dommy/rack/trace.rb', line 131 def recent(count = 10) = @events.last(count) |
#record_error(message:, exception_class: nil, source: nil) ⇒ Object
98 99 100 101 102 |
# File 'lib/dommy/rack/trace.rb', line 98 def record_error(message:, exception_class: nil, source: nil) data = {exception_class: exception_class, message: presence()}.compact __internal_emit(:error, {label: "Expectation failed", source: source, data: data}.compact, window: current_window) end |
#save(dir, status: "ok", metadata: nil) ⇒ Object
Write a self-contained trace bundle to dir: trace.ndjson plus an
artifacts/ directory with each captured snapshot as its own file, the
NDJSON referencing them by relative path. Returns dir. This is what the
standalone viewer opens (and v/o resolve paths against).
155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/dommy/rack/trace.rb', line 155 def save(dir, status: "ok", metadata: nil) require "fileutils" ::FileUtils.mkdir_p(::File.join(dir, "artifacts")) paths = @artifacts.each_with_object({}) do |(seq, content), out| rel = ::File.join("artifacts", "art_#{seq}.html") ::File.write(::File.join(dir, rel), content) out[seq] = {path: rel} end ndjson = Ndjson.new(@events, level: @level, metadata: , artifacts: paths, end_wall_ms: monotonic_ms).document(status: status) ::File.write(::File.join(dir, "trace.ndjson"), ndjson) dir end |
#scripts ⇒ Object
124 |
# File 'lib/dommy/rack/trace.rb', line 124 def scripts = events_of(:script) |
#to_ndjson(status: "ok", wall_time: nil, metadata: nil) ⇒ Object
141 142 143 144 145 |
# File 'lib/dommy/rack/trace.rb', line 141 def to_ndjson(status: "ok", wall_time: nil, metadata: nil) inline = @artifacts.transform_values { |content| {content: content, encoding: "utf-8"} } Ndjson.new(@events, level: @level, wall_time: wall_time, metadata: , artifacts: inline, end_wall_ms: monotonic_ms).document(status: status) end |
#to_s ⇒ Object
139 |
# File 'lib/dommy/rack/trace.rb', line 139 def to_s = to_text |
#to_text(limit: nil) ⇒ Object
--- Formatting ---
138 |
# File 'lib/dommy/rack/trace.rb', line 138 def to_text(limit: nil) = Formatter::Text.new(limit ? @events.last(limit) : @events).render |
#write_ndjson(path, **opts) ⇒ Object
147 148 149 |
# File 'lib/dommy/rack/trace.rb', line 147 def write_ndjson(path, **opts) ::File.write(path, to_ndjson(**opts)) end |